Skill: goog/fang (version 1.0.0)

--- name: fang description: > Protect environment variables from being stolen by malicious skill scripts. Runs a two-phase security audit: (1) static pattern scan via scan_env.py to detect env reads, network calls, encoding, and exec usage; (2) optional LLM deep analysis of all scripts in the target skill directory for sophisticated theft patterns. Outputs a structured threat report with risk ratings (HIGH/MEDIUM/LOW/CLEAN). Use when: auditing installed or downloaded skills before use, investigating suspicious scripts, running periodic security sweeps of the skill directory, or verifying that no skill is exfiltrating API keys / secrets. --- # FANG — ENV Guard Two-phase audit tool to detect environment variable theft in skill scripts. ## Scripts | Script | Purpose | |---|---| | `scripts/fang_audit.py` | Main audit runner — static scan + LLM deep analysis | | `scripts/scan_env.py` | Static pattern scanner (env / network / encode / exec) | ## Phase 1 — Static Scan Uses `scan_env.py` regex rules across `.py` and `.sh` files. **Risk scoring:** | Flag | Points | |---|---| | env access | +2 | | network call | +3 | | base64 / encode | +2 | | exec / subprocess | +2 | Score ≥ 6 → **HIGH** · ≥ 3 → **MEDIUM** · > 0 → **LOW** · 0 → **CLEAN** ## Phase 2 — LLM Deep Analysis (optional) Reads all `.py .sh .js .ts .ps1 .bash` scripts in the target directory and sends them to an OpenAI-compatible LLM. The LLM checks for: - Env reads combined with outbound HTTP/socket/DNS - Obfuscation: base64, hex, eval, dynamic imports - Hardcoded exfiltration endpoints - Suspicious subprocess chains ## Usage ### Basic static scan only ```bash python scripts/fang_audit.py <target_dir> ``` ### With LLM deep analysis ```bash python scripts/fang_audit.py <target_dir> --llm-key sk-... --model gpt-4o-mini ``` ### OpenAI-compatible API (e.g. local Ollama / DeepSeek) ```bash python scripts/fang_audit.py <target_dir> \ --llm-key any \ --model deepseek-chat \ --base-url https://api.deepseek.com/v1 ``` ### Save report to file ```bash python scripts/fang_audit.py <target_dir> --llm-key sk-... --output report.txt ``` ### Scan all workspace skills at once ```bash python scripts/fang_audit.py C:/Users/dad/.openclaw/workspace/skills ``` ## Agent Workflow When the user asks to audit skills for env theft: 1. Ask for the target directory (default: workspace `skills/` folder) 2. Run Phase 1 static scan — report summary immediately 3. If HIGH or MEDIUM risks found, ask whether to run LLM deep analysis 4. If `--llm-key` is available (from env or user), run Phase 2 automatically 5. Present the final threat report: - List each risky file with risk level + reason - Highlight any CRITICAL combined patterns (env read + network send) - Recommend action: QUARANTINE (HIGH), REVIEW (MEDIUM), MONITOR (LOW) ## Risk Response Guide | Risk Level | Recommended Action | |---|---| | 🔴 HIGH | Immediately quarantine the skill, do not run it | | 🟡 MEDIUM | Manual code review before use | | 🟢 LOW | Monitor; likely benign but worth noting | | ✅ CLEAN | Safe to use | ## Notes - The LLM analysis truncates each file to 3000 chars to stay within token limits. - For very large skill directories, consider scanning one skill at a time. - `scan_env.py` only processes `.py` and `.sh` files; `fang_audit.py` LLM mode also covers `.js`, `.ts`, `.ps1`.

path: scripts/fang_audit.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
fang_audit.py - ENV Guard Skill: Detect potential environment variable theft
Usage: python fang_audit.py <skill_dir> [--llm-key <OPENAI_KEY>] [--model <model>]
       python fang_audit.py C:/Users/dad/.openclaw/workspace/skills
"""

import re
import sys
import io
import json
import argparse
from pathlib import Path
from datetime import datetime

# Force UTF-8 output on Windows to support emoji
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")

# ── Import scan_env from same scripts dir ──────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR))
from scan_env import scan_path, risk_score

# ── Supported file extensions for LLM deep-check ──────────────────────────────
LLM_EXTENSIONS = {".py", ".sh", ".js", ".ts", ".ps1", ".bash"}

# ── Read file content ──────────────────────────────────────────────────────────
def read_file(path: str) -> str:
    try:
        return Path(path).read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return ""

# ── Static scan via scan_env ───────────────────────────────────────────────────
def run_static_scan(target_dir: str):
    results = scan_path(target_dir)
    flagged = [r for r in results if r["risk"] != "CLEAN"]
    return results, flagged

# ── Collect high/medium risk files for LLM analysis ───────────────────────────
def collect_risky_files(target_dir: str):
    risky = []
    p = Path(target_dir)
    for f in p.rglob("*"):
        if f.suffix in LLM_EXTENSIONS:
            content = read_file(str(f))
            risky.append({"path": str(f), "content": content})
    return risky

# ── LLM Analysis via OpenAI-compatible API ────────────────────────────────────
def llm_analyze(files: list, api_key: str, model: str = "gpt-4o-mini", base_url: str = "https://api.openai.com/v1") -> str:
    try:
        import urllib.request
        import urllib.error
    except ImportError:
        return "[ERROR] urllib not available"

    # Build file content block (trim to 3000 chars per file to stay within limits)
    content_blocks = []
    for item in files:
        snippet = item["content"][:3000]
        content_blocks.append(f"### File: {item['path']}\n```\n{snippet}\n```")

    combined = "\n\n".join(content_blocks)

    prompt = f"""You are a security analyst. Analyze the following script files from an AI agent skill directory.
Your job: detect any HIGH-RISK behavior that could STEAL or LEAK environment variables.

Focus on:
1. Code that reads env vars (os.environ, process.env, os.getenv, $ENV, etc.)
2. Code that sends data externally (HTTP requests, WebSockets, DNS lookups, etc.)
3. Combined risk: reading env vars AND sending them out (this is the most critical)
4. Obfuscation tricks: base64 encoding, hex encoding, compressed payloads
5. Suspicious subprocess / eval / exec usage
6. Hardcoded URLs or IPs that look like data exfiltration endpoints

For each suspicious file, output:
- File path
- Risk level: HIGH / MEDIUM / LOW
- Reason: one clear sentence
- Code snippet: the exact suspicious lines (max 5 lines)

If a file is clean, skip it entirely.
At the end, provide a SUMMARY section with:
- Total files analyzed
- High risk count
- Key findings (bullet points)
- Overall threat level: CRITICAL / WARNING / LOW / CLEAN

---FILES TO ANALYZE---
{combined}
"""

    payload = json.dumps({
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.2,
        "max_tokens": 2048,
    }).encode("utf-8")

    req = urllib.request.Request(
        f"{base_url}/chat/completions",
        data=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            data = json.loads(resp.read().decode())
            return data["choices"][0]["message"]["content"]
    except urllib.error.HTTPError as e:
        return f"[LLM ERROR] HTTP {e.code}: {e.read().decode()}"
    except Exception as e:
        return f"[LLM ERROR] {e}"

# ── Build static report ────────────────────────────────────────────────────────
def build_static_report(all_results: list, flagged: list) -> str:
    lines = []
    lines.append("=" * 60)
    lines.append("  FANG - ENV Guard  |  Static Scan Report")
    lines.append(f"  Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    lines.append("=" * 60)

    risk_counts = {"HIGH": 0, "MEDIUM": 0, "LOW": 0, "CLEAN": 0}
    for r in all_results:
        risk_counts[r["risk"]] = risk_counts.get(r["risk"], 0) + 1

    lines.append(f"\n📊 Scanned {len(all_results)} files total")
    lines.append(f"   🔴 HIGH:   {risk_counts['HIGH']}")
    lines.append(f"   🟡 MEDIUM: {risk_counts['MEDIUM']}")
    lines.append(f"   🟢 LOW:    {risk_counts['LOW']}")
    lines.append(f"   ✅ CLEAN:  {risk_counts['CLEAN']}")

    if not flagged:
        lines.append("\n✅ No suspicious files detected in static scan.")
    else:
        lines.append(f"\n⚠️  {len(flagged)} suspicious file(s) detected:\n")
        for r in flagged:
            icon = "🔴" if r["risk"] == "HIGH" else "🟡" if r["risk"] == "MEDIUM" else "🟢"
            lines.append(f"{icon} [{r['risk']}] {r['file']}")
            for cat, rule in r["findings"]:
                lines.append(f"     - [{cat}] matched: {rule}")
            lines.append("")

    return "\n".join(lines)

# ── CLI Entry Point ────────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(
        description="FANG - ENV Guard: Detect env var theft in skill scripts"
    )
    parser.add_argument("target", help="Skill directory or single file to audit")
    parser.add_argument("--llm-key", default=None, help="OpenAI-compatible API key for deep LLM analysis")
    parser.add_argument("--model", default="gpt-4o-mini", help="LLM model to use (default: gpt-4o-mini)")
    parser.add_argument("--base-url", default="https://api.openai.com/v1", help="API base URL (for OpenAI-compatible APIs)")
    parser.add_argument("--output", default=None, help="Save report to this file path")
    parser.add_argument("--json", action="store_true", help="Also output raw scan JSON")
    args = parser.parse_args()

    target = args.target
    if not Path(target).exists():
        print(f"[ERROR] Path not found: {target}")
        sys.exit(1)

    print(f"\n🔍 FANG scanning: {target}\n")

    # Phase 1: Static scan
    all_results, flagged = run_static_scan(target)
    static_report = build_static_report(all_results, flagged)
    print(static_report)

    # Phase 2: LLM deep analysis (optional)
    llm_report = ""
    if args.llm_key:
        print("\n" + "=" * 60)
        print("  FANG - LLM Deep Analysis")
        print("=" * 60)
        print(f"  Model: {args.model}")
        print(f"  Base URL: {args.base_url}")
        print("\n🤖 Collecting scripts for LLM review...")

        risky_files = collect_risky_files(target)
        print(f"   Found {len(risky_files)} script file(s) to analyze\n")

        if not risky_files:
            llm_report = "No scriptfiles found for LLM analysis."
            print(llm_report)
        else:
            print("🧠 Analyzing with LLM... (this may take a moment)\n")
            llm_report = llm_analyze(risky_files, args.llm_key, args.model, args.base_url)
            print(llm_report)
    else:
        llm_report = "[LLM analysis skipped — provide --llm-key to enable deep analysis]"
        print(f"\n💡 Tip: Add --llm-key <API_KEY> for deeper LLM-powered analysis")

    # Phase 3: Output to file (optional)
    if args.output:
        full_report = static_report + "\n\n" + "=" * 60 + "\n  LLM DEEP ANALYSIS\n" + "=" * 60 + "\n\n" + llm_report
        Path(args.output).write_text(full_report, encoding="utf-8")
        print(f"\n📄 Report saved to: {args.output}")

    # Phase 4: JSON dump (optional)
    if args.json:
        print("\n--- Raw JSON ---")
        print(json.dumps(all_results, ensure_ascii=False, indent=2))

    print("\n✅ FANG audit complete.")

if __name__ == "__main__":
    main()


path: scripts/scan_env.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
import argparse
from pathlib import Path

# ====== 检测规则 ======
PYTHON_PATTERNS = {
    "env": [r"os\.environ", r"os\.getenv"],
    "network": [r"requests\.", r"httpx\.", r"urllib", r"socket"],
    "encode": [r"base64", r"b64encode"],
    "exec": [r"subprocess", r"os\.system", r"eval\("],
}

BASH_PATTERNS = {
    "env": [r"\benv\b", r"\bprintenv\b", r"\$\{?[A-Z_]+\}?"],
    "network": [r"curl", r"wget", r"nc", r"netcat"],
    "exec": [r"\bbash\b", r"\bsh\b"],
}


# ====== 风险评分 ======
def risk_score(flags):
    score = 0

    if flags["env"]:
        score += 2
    if flags["network"]:
        score += 3
    if flags["encode"]:
        score += 2
    if flags["exec"]:
        score += 2

    if score >= 6:
        return "HIGH"
    elif score >= 3:
        return "MEDIUM"
    elif score > 0:
        return "LOW"
    else:
        return "CLEAN"

def detect_type(filepath, content):
    if filepath.endswith(".py"):
        return "python"
    elif filepath.endswith(".sh"):
        return "bash"
    elif content.startswith("#!/") and "python" in content.split("\n")[0]:
        return "python"
    elif content.startswith("#!/") and ("bash" in content.split("\n")[0] or "sh" in content.split("\n")[0]):
        return "bash"
    return "unknown"

# ====== 扫描文件 ======
def scan_file(filepath):
    try:
        content = Path(filepath).read_text(errors="ignore")
    except Exception:
        return None
    
    ftype = detect_type(filepath, content)
    #is_python = filepath.endswith(".py")
    if ftype == "python":
        patterns = PYTHON_PATTERNS
    elif ftype == "bash":
        patterns = BASH_PATTERNS
    else:
        #patterns = {}
        return None
    

    findings = []
    flags = {"env": False, "network": False, "encode": False, "exec": False}

    for category, regex_list in patterns.items():
        for regex in regex_list:
            if re.search(regex, content):
                findings.append((category, regex))
                flags[category] = True

    return {
        "file": filepath,
        "type": ftype,
        "risk": risk_score(flags),
        "findings": findings,
    }


# ====== 扫描路径 ======
def scan_path(path):
    results = []
    p = Path(path)

    if p.is_file():
        res = scan_file(str(p))
        if res:
            results.append(res)
        return results

    for file in p.rglob("*"):
        if file.suffix in [".py", ".sh"]:
            res = scan_file(str(file))
            if res:
                results.append(res)

    return results


# ====== 输出 ======
def print_report(results, show_all=False):
    for r in results:
        if not show_all and r["risk"] == "CLEAN":
            continue

        print(f"\nFILE: {r['file']}")
        print(f"TYPE: {r['type']}")
        print(f"RISK: {r['risk']}")

        if r["findings"]:
            print("FINDINGS:")
            for cat, rule in r["findings"]:
                print(f"  - [{cat}] {rule}")


# ====== CLI ======
def main():
    parser = argparse.ArgumentParser(
        description="ENV Leak Detector (Python + Bash)"
    )

    parser.add_argument("target", help="File or directory to scan")

    parser.add_argument(
        "--all",
        action="store_true",
        help="Show all files (including clean)",
    )

    args = parser.parse_args()

    results = scan_path(args.target)
    print_report(results, show_all=args.all)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)