Skill: mallen-lbx/securityclaw (version 1.0.0)

--- name: securityclaw-skill description: Security-first skill auditing and quarantine for OpenClaw skills. Use when installing new skills, reviewing skills from unknown sources, scanning skills for prompt injection/exfiltration/supply-chain risks, or when a bot suspects a skill is malicious. Guides static + optional sandbox checks, quarantines suspicious skills, and produces an owner-action checklist (Delete / Report / Allow / Scan all). --- # SecurityClaw (Skill Scanner) ## Use the scanner script Run the scanner (read-only by default): ```bash python3 scripts/securityclaw_scan.py --skills-dir ~/.openclaw/skills --out report.json ``` Quarantine anything suspicious (moves folders, no deletion): ```bash python3 scripts/securityclaw_scan.py --skills-dir ~/.openclaw/skills --quarantine-dir ~/.openclaw/skills-quarantine --quarantine --out report.json ``` ## What to do when findings exist If the report shows `severity >= high` for any skill: 1) **Do not execute** the skill. 2) **Quarantine** the skill folder. 3) **Notify the owner** with: - skill name - top reasons + file/line locations - recommended action 4) Await owner instruction: - **Delete**: remove quarantined skill - **Report**: prepare public report / IOCs (no secrets) - **Allow**: add allowlist entry and restore - **Scan all**: deep scan everything ## Optional: sandbox/dynamic checks (advanced) Dynamic checks are optional and should run only after owner approval. - Prefer running unknown code with: - no network egress - read-only filesystem except a temp workspace - no access to OpenClaw config/secrets See `references/sandboxing.md`. ## Files - `scripts/securityclaw_scan.py` — main scanner + quarantine - `references/rules.md` — rule catalog (what we flag and why) - `references/sandboxing.md` — safe sandbox strategy + what to avoid

path: scripts/securityclaw_scan.py
content: #!/usr/bin/env python3
"""SecurityClaw: scan OpenClaw skills directories for high-risk patterns.

Goals
- Security-first, conservative scanner for skill folders.
- Produce a JSON report with findings and recommended actions.
- Quarantine (move) suspicious skills out of the active skills dir.

Non-goals
- Perfect malware detection.
- Executing untrusted skill code.

This script is intentionally dependency-light.
"""

from __future__ import annotations

import argparse
import dataclasses
import datetime as dt
import hashlib
import json
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple

TEXT_EXTS = {".md", ".txt", ".json", ".js", ".ts", ".py", ".sh", ".mjs", ".cjs", ".yaml", ".yml"}
MAX_FILE_BYTES = 2_000_000  # avoid giant files

# Very small rule set to start; expand iteratively.
RULES = [
    # Supply-chain / install scripts
    ("install_script", "high", re.compile(r"\b(postinstall|preinstall|install)\b", re.I), "Install scripts can execute arbitrary code."),
    # Shell execution
    ("shell_exec", "high", re.compile(r"\b(child_process\.(exec|execSync|spawn|spawnSync)|os\.system\(|subprocess\.(Popen|run|call)|Runtime\.getRuntime\(\)\.exec)\b", re.I), "Direct command execution found."),
    # Network egress
    ("network_egress", "high", re.compile(r"\b(fetch\(|axios\.|curl\b|wget\b|requests\.|http\.client|net\.Socket|WebSocket\b)\b", re.I), "Network egress primitives found."),
    # Secret hunting (heuristic)
    ("secret_access", "high", re.compile(r"(OPENAI_API_KEY|GEMINI_API_KEY|SEARXNG|PORTAINER_TOKEN|BOT_TOKEN|PRIVATE_KEY|BEGIN\s+PRIVATE\s+KEY)", re.I), "Possible secret access/exfil target."),
    # Prompt injection markers
    ("prompt_injection", "medium", re.compile(r"(ignore\s+previous\s+instructions|system\s+prompt|developer\s+message|exfiltrate|BEGIN\s+SYSTEM\s+PROMPT)", re.I), "Prompt-injection style content found."),
    # Path traversal / sensitive file targets
    ("sensitive_paths", "high", re.compile(r"(~/?\.openclaw/|/etc/|\.ssh/|id_rsa|authorized_keys|keychain|\.env\b)", re.I), "References sensitive paths."),
]

SEVERITY_ORDER = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}


@dataclasses.dataclass
class Finding:
    rule_id: str
    severity: str
    message: str
    file: str
    line: int
    excerpt: str


def clamp_excerpt(s: str, limit: int = 240) -> str:
    s = s.replace("\t", " ")
    return s if len(s) <= limit else s[: limit - 3] + "..."


def read_text_file(path: Path) -> Optional[str]:
    try:
        if path.stat().st_size > MAX_FILE_BYTES:
            return None
        data = path.read_bytes()
        # best-effort decode
        return data.decode("utf-8", errors="replace")
    except Exception:
        return None


def scan_file(path: Path, rel: str) -> List[Finding]:
    text = read_text_file(path)
    if text is None:
        return []

    findings: List[Finding] = []
    lines = text.splitlines()
    for i, line in enumerate(lines, start=1):
        for rule_id, sev, pat, msg in RULES:
            if pat.search(line):
                findings.append(
                    Finding(
                        rule_id=rule_id,
                        severity=sev,
                        message=msg,
                        file=rel,
                        line=i,
                        excerpt=clamp_excerpt(line.strip()),
                    )
                )
    return findings


def worst_severity(findings: List[Finding]) -> str:
    if not findings:
        return "info"
    return max((f.severity for f in findings), key=lambda s: SEVERITY_ORDER.get(s, 0))


def skill_dirs(skills_dir: Path) -> List[Path]:
    if not skills_dir.exists():
        return []
    return [p for p in skills_dir.iterdir() if p.is_dir() and not p.name.startswith(".")]


def scan_skill(skill_path: Path, base_dir: Path) -> Dict:
    findings: List[Finding] = []

    for p in skill_path.rglob("*"):
        if p.is_symlink():
            continue
        if p.is_file():
            if p.suffix.lower() in TEXT_EXTS or p.name in {"SKILL.md", "package.json", "manifest.json"}:
                rel = str(p.relative_to(base_dir))
                findings.extend(scan_file(p, rel))

    sev = worst_severity(findings)
    return {
        "skill": skill_path.name,
        "path": str(skill_path),
        "severity": sev,
        "findingCount": len(findings),
        "findings": [dataclasses.asdict(f) for f in findings],
        "recommendation": (
            "quarantine" if SEVERITY_ORDER.get(sev, 0) >= SEVERITY_ORDER["high"] else "review"
        ),
    }


def quarantine_skill(skill_path: Path, quarantine_dir: Path) -> Path:
    quarantine_dir.mkdir(parents=True, exist_ok=True)
    ts = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
    dest = quarantine_dir / f"{ts}-{skill_path.name}"
    shutil.move(str(skill_path), str(dest))
    return dest


def main() -> int:
    ap = argparse.ArgumentParser(description="Scan OpenClaw skills for risky patterns")
    ap.add_argument("--skills-dir", required=True, help="Path to skills directory (e.g., ~/.openclaw/skills)")
    ap.add_argument("--out", required=True, help="Write JSON report to this file")
    ap.add_argument("--quarantine", action="store_true", help="Move high-severity skills to quarantine dir")
    ap.add_argument("--quarantine-dir", default=str(Path.home() / ".openclaw" / "skills-quarantine"))

    args = ap.parse_args()

    skills_dir = Path(os.path.expanduser(args.skills_dir)).resolve()
    quarantine_dir = Path(os.path.expanduser(args.quarantine_dir)).resolve()
    out_path = Path(os.path.expanduser(args.out)).resolve()

    report = {
        "ts": dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
        "skillsDir": str(skills_dir),
        "quarantineDir": str(quarantine_dir),
        "results": [],
        "summary": {"total": 0, "bySeverity": {"info": 0, "low": 0, "medium": 0, "high": 0, "critical": 0}},
        "actions": [],
    }

    sdirs = skill_dirs(skills_dir)
    report["summary"]["total"] = len(sdirs)

    for sp in sdirs:
        result = scan_skill(sp, skills_dir)
        report["results"].append(result)
        sev = result["severity"]
        report["summary"]["bySeverity"][sev] = report["summary"]["bySeverity"].get(sev, 0) + 1

        if args.quarantine and SEVERITY_ORDER.get(sev, 0) >= SEVERITY_ORDER["high"]:
            moved_to = quarantine_skill(sp, quarantine_dir)
            report["actions"].append({"action": "quarantine", "skill": result["skill"], "movedTo": str(moved_to)})

    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")

    # Exit code indicates if anything high+ was found
    high_found = any(SEVERITY_ORDER.get(r["severity"], 0) >= SEVERITY_ORDER["high"] for r in report["results"])
    return 2 if high_found else 0


if __name__ == "__main__":
    raise SystemExit(main())


path: references/sandboxing.md
content: # Sandboxing strategy (Phase 2/3)

## Goal
Run dynamic analysis of an untrusted skill without letting it:
- access secrets
- write to real config/memory
- access network
- call privileged tools

## Recommended approach
- Prefer an **OS-level sandbox** (VM/microVM/container) over app-level restrictions.

### macOS
- Use a separate macOS user account + launchd sandbox profile (advanced), or a VM.

### Linux (headless)
- bubblewrap (bwrap) / firejail
- network namespace with no egress
- read-only bind-mounts

## Dummy bot strategy
- Spawn an isolated OpenClaw instance with:
  - workspace pointing to a temp directory
  - tools allowlist = read-only + no network
  - channels disabled
- Feed it a fixed prompt that triggers the skill and record:
  - tool calls attempted
  - file paths referenced
  - any attempts to modify instructions/config

## Output
- A behavioral report merged into the static scan report.


path: references/rules.md
content: # SecurityClaw rule catalog (v0)

## What we flag (and why)

### High severity
- **Command execution** (Node child_process exec/spawn, Python subprocess, shell scripts)
- **Network egress** (fetch/axios/requests/curl/wget/WebSocket)
- **Sensitive paths** (/.openclaw/, ~/.ssh, /etc, .env)
- **Install hooks** (postinstall/preinstall) — supply-chain risk

### Medium severity
- **Prompt injection markers** inside docs ("ignore previous instructions", "system prompt")
- **Obfuscation** (base64 decode/exec patterns)

## False positives
Many legitimate skills will legitimately:
- fetch docs (network)
- run CLIs

So SecurityClaw should treat these as *risk indicators*, not guilt.
Use allowlists + contextual checks to reduce noise.


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