Skill: rhombusmaximus/security-sweep (version 1.1.2)

--- name: security-sweep description: Security scanner for OpenClaw skills and plugins. Scans for hardcoded secrets, dangerous exec patterns, dependency vulnerabilities, and network egress. Use when auditing installed skills/plugins, before publishing to ClawHub, or when a user requests a security review of skills or plugins. version: 1.1.1 --- # Security Sweep — Skill & Plugin Auditor Scans OpenClaw skills and plugins for: 1. **Hardcoded secrets** — API keys, tokens, passwords in code 2. **Dangerous exec patterns** — shell injection, eval, unsanitized child_process calls 3. **Dependency vulnerabilities** — npm audit failures 4. **Network egress** — unexpected outbound connections 5. **Input injection** — unsanitized user input reaching exec/file/eval ## Scan Scope **Built-in skills** (read-only, bundled with OpenClaw CLI): ``` $(brew --prefix)/Cellar/openclaw-cli/<version>/libexec/lib/node_modules/openclaw/skills/ ``` **Workspace skills** (user-installed): ``` ~/.openclaw/workspace/skills/ ``` ## Workflow ### Full Sweep Run the comprehensive scan script: ```bash SKILLS_DIR="$(brew --prefix)/Cellar/openclaw-cli/2026.3.24/libexec/lib/node_modules/openclaw/skills" WS_DIR="$HOME/.openclaw/workspace/skills" REPORT_DATE=$(date +%Y%m%d_%H%M%S) REPORT_FILE="$HOME/.openclaw/security-sweep-${REPORT_DATE}.txt" bash ~/.openclaw/workspace/skills/security-sweep/scripts/full-scan.sh \ --builtin "$SKILLS_DIR" \ --workspace "$WS_DIR" \ --output "$REPORT_FILE" ``` ### Quick Scan (fast patterns only) ```bash bash ~/.openclaw/workspace/skills/security-sweep/scripts/quick-scan.sh \ --dir "$HOME/.openclaw/workspace/skills" ``` ### Single Skill Scan ```bash bash ~/.openclaw/workspace/skills/security-sweep/scripts/skill-scan.sh \ --skill /path/to/skill ``` ### NPM Audit (workspace skills with package.json) ```bash bash ~/.openclaw/workspace/skills/security-sweep/scripts/npm-audit.sh \ --workspace "$HOME/.openclaw/workspace/skills" ``` ## Risk Categories | Level | Finding | Action | |-------|---------|--------| | 🔴 CRITICAL | Hardcoded secret (api_key, token, password) | Remove immediately, rotate credential | | 🔴 CRITICAL | `eval()` on untrusted input | Replace with safe alternative | | 🟠 HIGH | `exec()`, `spawn()` with string concatenation | Use execFile with array args | | 🟠 HIGH | Shell injection surface (bash -c, ${var} in shell) | Sanitize or use execFile | | 🟡 MEDIUM | npm audit findings (any severity) | Review and update dependencies | | 🟡 MEDIUM | Unexpected network egress | Verify necessity, document purpose | | 🟢 LOW | File permission too broad (0o777) | Restrict to 0o644/0o755 | | 🟢 INFO | process.env leak in logs | Ensure logs redact env vars | ## Reporting Reports are saved to `~/.openclaw/security-sweep-<date>.txt`. Include report path in memory after each scan. ## Periodic Scanning Offer to schedule weekly security sweeps via cron: ```bash openclaw cron add \ --name "security-sweep" \ --every 604800 \ --sessionTarget isolated \ --payload '{"kind":"agentTurn","message":"Run security sweep on all skills. Report findings. Save report to ~/.openclaw/security-sweep-<date>.txt and note in memory/YYYY-MM-DD.md if any critical issues found."}' ``` ## Sharing / ClawHub Publishing Before publishing a skill to ClawHub: 1. Run full sweep 2. Fix all CRITICAL/HIGH findings 3. Verify no secrets in SKILL.md or any scripts 4. Confirm npm audit passes with 0 vulnerabilities 5. Document all required env vars in SKILL.md ## Notes - Bundled skills (read-only, no write during scan) - Workspace skills are editable — fix findings directly - Some `execFile` usage is legitimate (openclaw CLI calls) — review context - `process.env` access is fine; concern is env vars *leaking* to untrusted processes

path: README.md
content: # Security Sweep for OpenClaw

A comprehensive security scanner for OpenClaw skills and plugins. Scans for hardcoded secrets, dangerous exec patterns, dependency vulnerabilities, network egress, and shell injection surfaces.

When secrets are found, they can be **auto-encrypted to Notion** — never stored in plain text.

---

## Features

- 🔍 **Secret Detection** — Scans for API keys, tokens, passwords, credentials hardcoded in skill files
- 🐚 **Exec Pattern Analysis** — Finds dangerous `exec()`, `spawn()`, `eval()`, and shell injection surfaces
- 📦 **NPM Audit** — Checks for vulnerable dependencies in skills with `package.json`
- 🌐 **Network Egress** — Reports unexpected outbound connections
- 🔒 **Auto-Encrypt to Notion** — Encrypts found secrets to your Notion secrets store (AES-256-GCM)
- 🚀 **CI-Ready** — Exit codes and structured output for use in GitHub Actions

---

## Quick Start

```bash
# Scan all skills
bash scripts/full-scan.sh \
  --builtin "$(brew --prefix)/Cellar/openclaw-cli/2026.3.13/libexec/lib/node_modules/openclaw/skills" \
  --workspace "$HOME/.openclaw/workspace/skills" \
  --output ~/security-sweep-report.txt

# Quick scan (fast patterns only)
bash scripts/quick-scan.sh --dir ~/.openclaw/workspace/skills
```

---

## Reports

Reports are saved to the output file you specify. Each finding includes:
- File path
- Risk level (CRITICAL / HIGH / MEDIUM / LOW / INFO)
- Recommended action

Sample output:
```
━━━ WORKSPACE SKILLS Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  🔴 CRITICAL: 0
  🟠 HIGH:     0
  🟡 MEDIUM:   0
  🟢 LOW:      0
  ℹ️  INFO:     3

━━━ Findings ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ℹ️  NETWORK: skill/example/public/js/api.js
  ℹ️  NETWORK: skill/example/src/index.js
```

---

## Risk Levels

| Level | Description | Example |
|-------|-------------|---------|
| 🔴 CRITICAL | Hardcoded secret or unsafe eval | `const key = "sk-123..."` |
| 🟠 HIGH | Dangerous exec without sanitization | `exec(userInput)` |
| 🟡 MEDIUM | Shell injection surface or npm audit failure | `bash -c "${var}"` |
| 🟢 LOW | Overly broad file permissions | `chmod 777` |
| ℹ️ INFO | Expected network egress documented | External API call |

---

## ⚠️ Warning: `--encrypt-found` Requires a Secure Private Skillbase

The `--encrypt-found` flag is powerful but **assumes your private skill directory is already secure**. It encrypts found secrets to Notion before removal — but:

- **If your private skills already contain real secrets, those secrets are already exposed** — the scan is read-only and doesn't cause leaks
- **If your Notion is compromised, encrypted blobs are useless without the master password** — but the attacker gets the ciphertext
- **The encryption protects secrets at rest in Notion** — it does NOT protect secrets in transit or in your shell history

**You should NOT use `--encrypt-found` if:**
- Your private skillbase may contain uncommitted work-in-progress with real credentials
- You haven't set up the Notion secrets store yet
- You're scanning a third-party skill you don't control

**Always review the findings before any auto-encryption** — the flag is off by default for this reason.

> **⚠️ CRITICAL:** The master password is the only way to decrypt your secrets. **Forget it and the secrets are permanently unrecoverable** — AES-256-GCM with PBKDF2 (100k iterations) is intentionally slow and cannot be brute-forced. Store it in a password manager.

## Auto-Encrypt to Notion

When `--encrypt-found` is used and `NOTION_MASTER_PASSWORD` is set, found secrets are encrypted to your Notion secrets store before being removed.

```bash
export NOTION_MASTER_PASSWORD="your-master-password"
bash scripts/full-scan.sh --encrypt-found [options]
```

Requires `notion-secrets.js` in `~/.openclaw/scripts/`. See `references/notion-encryption.md` for setup.

---

## Before Publishing

Run a full scan and confirm:
- [ ] 0 CRITICAL findings
- [ ] 0 HIGH findings
- [ ] All npm audits pass
- [ ] No secrets anywhere in the codebase
- [ ] All network egress is expected and documented

---

## GitHub Actions Integration

```yaml
- name: Security Sweep
  run: |
    bash scripts/full-scan.sh \
      --builtin "./skills" \
      --output "./security-report.txt"
    # Fail on CRITICAL or HIGH
    grep -qE "🔴|🟠" ./security-report.txt && exit 1
```

---

## License

MIT — free to use, modify, and distribute.


path: references/notion-encryption.md
content: # Notion Encryption System

How secrets found during a security sweep are encrypted and stored in Notion.

## Overview

When the security sweep finds hardcoded secrets, you can encrypt them to your Notion workspace using `notion-secrets.js` — a local encryption tool that wraps secrets in AES-256-GCM before uploading to Notion.

**What Notion stores:** encrypted blobs — completely unreadable without the master password.
**What an attacker needs:** the master password + the Notion page ID.

## Architecture

```
Your machine                          Notion (cloud)
┌──────────────────────┐             ┌──────────────────────┐
│  Master Password      │             │                      │
│       ↓               │             │  Encrypted blob       │
│  PBKDF2 (100k iter)  │             │  [salt][iv][tag][ct] │
│       ↓               │             │                      │
│  AES-256-GCM encrypt │  ──push──>  │  Page: scan-results   │
│                      │             │                      │
│  AES-256-GCM decrypt │  ←─pull──  │  (Notion API)        │
│       ↓               │             │                      │
│  Plaintext secret    │             └──────────────────────┘
└──────────────────────┘
```

## Setup

### 1. Get a Notion API Integration

1. Go to [notion.so/my-integrations](https://www.notion.so/my-integrations)
2. Create a new integration ("Security Sweep")
3. Copy the internal API token
4. Store it: `node ~/.openclaw/scripts/notion-secrets.js put notion_api_key "<your-token>"`
5. Share the "RhomBot Secrets" database with your integration

### 2. Store the integration token (encrypted)

```bash
# The token is stored encrypted — never in plain text
node ~/.openclaw/scripts/notion-secrets.js put notion_api_key "<your-token>"
```

### 3. Set the master password

The master password is derived into an encryption key — **never stored**.

```bash
# Recommended: use environment variable (never in shell history)
export NOTION_MASTER_PASSWORD="your-strong-master-password"

# For one-off commands:
NOTION_MASTER_PASSWORD="your-password" node ~/.openclaw/scripts/notion-secrets.js <command>
```

> ⚠️ **CRITICAL:** The master password is the only way to decrypt your secrets. **Forget it and the secrets are permanently unrecoverable** — AES-256-GCM with PBKDF2 (100k iterations) is intentionally slow and cannot be brute-forced. Store it in a password manager.

## Usage

```bash
# Store a secret (use env var — avoids interactive prompt stdin issues)
NOTION_MASTER_PASSWORD="your-password" node ~/.openclaw/scripts/notion-secrets.js put my_api_key "sk-123..."

# List stored secrets (shows names only)
node ~/.openclaw/scripts/notion-secrets.js list

# Get a secret's encrypted blob
node ~/.openclaw/scripts/notion-secrets.js get my_api_key

# Decrypt a blob
node ~/.openclaw/scripts/notion-secrets.js decrypt "<blob>"

# Quick local encrypt (for testing)
node ~/.openclaw/scripts/notion-secrets.js encrypt "plaintext"
```

## How Encryption Works

**Algorithm:** AES-256-GCM
**Key derivation:** PBKDF2 with SHA-512, 100,000 iterations
**IV:** Random 16 bytes per encryption
**Tag:** 16 bytes (authentication tag)

```
plaintext → PBKDF2(pw, salt) → AES-256-GCM → [salt (32)][iv (16)][tag (16)][ciphertext] → base64
```

## Security Properties

| Property | Protection |
|----------|------------|
| Encrypted at rest | Notion only sees ciphertext |
| Key derivation | Brute-force resistant (100k PBKDF2 iterations) |
| Authenticated encryption | Tampering is detectable |
| No password storage | Master password never touches disk |
| Per-secret salt | Same secret encrypts differently each time |

## Important Notes

- **Use `NOTION_MASTER_PASSWORD` env var for scripts** — interactive prompts can have stdin mixing issues in non-TTY environments
- **The `put` command requires the env var** for reliable operation
- **`list` and `get` do not need a password** — they only read encrypted blobs (values stay encrypted client-side)
- **Decrypt requires the password** — the blob is useless without it

## Limitations

- The master password must be set via env var in CI/non-interactive environments
- Notion API rate limits apply (~3 requests/second)
- If Notion is down, secrets are unavailable

## For CI/CD

```bash
# In CI, inject the master password as a secret environment variable:
export NOTION_MASTER_PASSWORD="$NOTION_MASTER_PASSWORD"
bash scripts/full-scan.sh --encrypt-found [options]
```

The password never appears in logs — only the "Stored" confirmation is visible.


path: scripts/full-scan.sh
content: #!/usr/bin/env bash
#
# full-scan.sh — Comprehensive security sweep for OpenClaw skills
# Part of security-sweep skill
#
# Usage:
#   bash full-scan.sh --builtin /path/to/builtin/skills --workspace /path/to/workspace/skills --output /path/to/report.txt
#

set -euo pipefail

BUILTIN_DIR=""
WORKSPACE_DIR=""
OUTPUT_FILE=""
QUIET=false
ENCRYPT_FOUND=false
NOTION_SECRETS_SCRIPT="${HOME}/.openclaw/scripts/notion-secrets.js"

# Patterns for secret detection — tightened to reduce false positives
# Only match actual key=value patterns, not documentation text
SECRET_PATTERNS=(
    "(api[_-]?key|token|password|secret|credential)\s*[=:]\s*[\"'][^'\"]{8,}[\"']"
    "Bearer\s+[a-zA-Z0-9_-]{20,}"
)

# Dangerous exec patterns
EXEC_PATTERNS=(
    "exec\s*\("
    "spawn\s*\("
    "eval\s*\("
    "child_process"
    "bash\s+-c"
    "system\s*\("
    "shell\s*:\s*true"
)

# Shell injection surfaces
INJECTION_PATTERNS=(
    '"\$.*?"'
    '`\$[^`]+`'
    '\$\{[^}]+\}'
)

# Network egress
NETWORK_PATTERNS=(
    "https?://"
    "curl\s+"
    "wget\s+"
    "fetch\s*\("
    "axios\s*\("
)

# Build combined regex for grep
build_secret_regex() {
    local IFS="|"
    echo "${SECRET_PATTERNS[*]}"
}

build_exec_regex() {
    local IFS="|"
    echo "${EXEC_PATTERNS[*]}"
}

build_injection_regex() {
    local IFS="|"
    echo "${INJECTION_PATTERNS[*]}"
}

build_network_regex() {
    local IFS="|"
    echo "${NETWORK_PATTERNS[*]}"
}

# ── Parse args ──────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
    case "$1" in
        --builtin) BUILTIN_DIR="$2"; shift 2 ;;
        --workspace) WORKSPACE_DIR="$2"; shift 2 ;;
        --output) OUTPUT_FILE="$2"; shift 2 ;;
        --quiet) QUIET=true; shift ;;
        --encrypt-found) ENCRYPT_FOUND=true; shift ;;
        -h|--help)
            echo "Usage: full-scan.sh [options]"
            echo "  --builtin DIR     Bundled OpenClaw skills directory"
            echo "  --workspace DIR   Workspace skills directory"
            echo "  --output FILE     Output report file"
            echo "  --quiet           Suppress per-skill output"
            echo "  --encrypt-found   Encrypt found secrets to Notion"
            exit 0
            ;;
        *) echo "Unknown: $1"; exit 1 ;;
    esac
done

REPORT_DATE=$(date "+%Y-%m-%d %H:%M:%S %Z")
REPORT_FILE_TMP=$(mktemp)
FOUND_SECRETS_TMP=$(mktemp)

{
    echo "═══════════════════════════════════════════════════════════════"
    echo "  OpenClaw Security Sweep Report"
    echo "  ${REPORT_DATE}"
    echo "═══════════════════════════════════════════════════════════════"
} > "$REPORT_FILE_TMP"

# ── Encrypt a found secret to Notion ────────────────────────────────────────
encrypt_to_notion() {
    local label="$1"
    local secret="$2"
    local notion_script="$NOTION_SECRETS_SCRIPT"

    if [[ ! -f "$notion_script" ]]; then
        echo "    ⚠️  notion-secrets.js not found — skipping Notion store"
        return
    fi

    local master_pw="${NOTION_MASTER_PASSWORD:-}"
    if [[ -z "$master_pw" ]]; then
        echo "    ⚠️  NOTION_MASTER_PASSWORD env not set — skipping Notion store"
        return
    fi

    if echo "$master_pw" | node "$notion_script" put "scan-${label}" "$secret" 2>/dev/null | grep -q "Stored"; then
        echo "    🔒 Encrypted to Notion as 'scan-${label}'"
    else
        echo "    ⚠️  Failed to encrypt to Notion"
    fi
}

# ── Scan one skill directory ─────────────────────────────────────────────────
scan_skill() {
    local skill_path="$1"
    local skill_name
    skill_name=$(basename "$skill_path")

    local skill_critical=0 skill_high=0 skill_medium=0 skill_low=0 skill_info=0

    # Build the exclude args — skip node_modules, .git, and this script itself
    # NOTE: "secret" pattern is for detection only, not real secrets in this file
    local EXCLUDE_ARGS="--exclude=node_modules --exclude=.git --exclude=full-scan.sh --exclude=quick-scan.sh --exclude=*.min.js"

    # ── 1. Secret patterns ────────────────────────────────────────────
    local secret_regex
    secret_regex=$(build_secret_regex)
    # NOTE: exclude .md (docs trigger false positives) and /tests/ (test fixtures)
    while IFS= read -r file; do
        skill_critical=$((skill_critical + 1))
        echo "  🔴 SECRET: $file" >> "$FOUND_SECRETS_TMP"

        if [[ "$ENCRYPT_FOUND" == "true" ]]; then
            local secret_line
            secret_line=$(grep -rEn "$secret_regex" "$file" 2>/dev/null | head -1 || true)
            [[ -n "$secret_line" ]] && encrypt_to_notion "${skill_name}-$(basename "$file")" "$secret_line"
        fi
    done < <(grep -rEl "$secret_regex" "$skill_path" \
        --include="*.js" --include="*.ts" --include="*.sh" --include="*.py" --include="*.json" \
        $EXCLUDE_ARGS 2>/dev/null \
        | grep -v "\.md$" \
        | grep -v "/tests/" \
        | grep -v node_modules | grep -v "\.git" || true)

    # ── 2. Dangerous exec patterns ─────────────────────────────────────
    local exec_regex
    exec_regex=$(build_exec_regex)
    while IFS= read -r file; do
        skill_high=$((skill_high + 1))
        echo "  🟠 EXEC: $file" >> "$FOUND_SECRETS_TMP"
    done < <(grep -rEl "$exec_regex" "$skill_path" \
        --include="*.js" --include="*.ts" --include="*.sh" \
        $EXCLUDE_ARGS 2>/dev/null \
        | grep -v "/tests/" \
        | grep -v node_modules | grep -v "\.git" || true)

    # ── 3. Shell injection surfaces ────────────────────────────────────
    local injection_regex
    injection_regex=$(build_injection_regex)
    while IFS= read -r file; do
        skill_medium=$((skill_medium + 1))
        echo "  🟡 INJECTION: $file" >> "$FOUND_SECRETS_TMP"
    done < <(grep -rEl "$injection_regex" "$skill_path" \
        --include="*.js" --include="*.ts" --include="*.sh" \
        $EXCLUDE_ARGS 2>/dev/null \
        | grep -v "/tests/" \
        | grep -v node_modules | grep -v "\.git" || true)

    # ── 4. Network egress ─────────────────────────────────────────────
    local network_regex
    network_regex=$(build_network_regex)
    while IFS= read -r file; do
        skill_info=$((skill_info + 1))
        echo "  ℹ️  NETWORK: $file" >> "$FOUND_SECRETS_TMP"
    done < <(grep -rEl "$network_regex" "$skill_path" \
        --include="*.js" --include="*.ts" --include="*.sh" \
        $EXCLUDE_ARGS 2>/dev/null | grep -v node_modules | grep -v "\.git" || true)

    # ── 5. npm audit ─────────────────────────────────────────────────
    if [[ -f "$skill_path/package.json" && -d "$skill_path/node_modules" ]]; then
        local npm_out
        npm_out=$(cd "$skill_path" && npm audit --omit=dev --quiet 2>&1 || true)
        if echo "$npm_out" | grep -q "vulnerab"; then
            skill_medium=$((skill_medium + 1))
            echo "  🟡 NPM_AUDIT: $skill_path/package.json" >> "$FOUND_SECRETS_TMP"
        fi
    fi

    # ── Per-skill summary ────────────────────────────────────────────
    local skill_total=$((skill_critical + skill_high + skill_medium + skill_low))
    if [[ $skill_total -gt 0 ]] && [[ "$QUIET" != "true" ]]; then
        printf "  %-30s 🔴 %d 🟠 %d 🟡 %d\n" \
            "$skill_name" "$skill_critical" "$skill_high" "$skill_medium"
    fi

    echo "$skill_critical $skill_high $skill_medium $skill_low $skill_info"
}

# ── Scan a directory ────────────────────────────────────────────────────────
scan_dir() {
    local label="$1"
    local dir="$2"

    if [[ ! -d "$dir" ]]; then
        echo "  ⚠️  Directory not found: $dir"
        return
    fi

    local skill_count
    skill_count=$(find "$dir" -maxdepth 1 -type d 2>/dev/null | wc -l)
    skill_count=$((skill_count - 1))

    echo "" >> "$REPORT_FILE_TMP"
    echo "━━━ ${label} (${skill_count} skills) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >> "$REPORT_FILE_TMP"
    echo "" >> "$REPORT_FILE_TMP"
    echo "━━━ ${label} (${skill_count} skills) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "" >> "$REPORT_FILE_TMP"

    local total_critical=0 total_high=0 total_medium=0 total_low=0 total_info=0

    for skill_path in "$dir"/*/; do
        [[ ! -d "$skill_path" ]] && continue
        [[ -z "$(ls -A "$skill_path" 2>/dev/null)" ]] && continue
        [[ "$(basename "$skill_path")" == "node_modules" ]] && continue

        local skill_name
        skill_name=$(basename "$skill_path")

        printf "  Scanning %-28s ..." "$skill_name"

        local result
        result=$(scan_skill "$skill_path" 2>/dev/null | tail -1 || echo "0 0 0 0 0")
        read -r skill_critical skill_high skill_medium skill_low skill_info <<< "${result:-0 0 0 0 0}"

        total_critical=$((total_critical + skill_critical))
        total_high=$((total_high + skill_high))
        total_medium=$((total_medium + skill_medium))
        total_low=$((total_low + skill_low))
        total_info=$((total_info + skill_info))
    done

    echo "" >> "$REPORT_FILE_TMP"
    echo "━━━ ${label} Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >> "$REPORT_FILE_TMP"
    printf "  🔴 CRITICAL: %d\n" "$total_critical" >> "$REPORT_FILE_TMP"
    printf "  🟠 HIGH:     %d\n" "$total_high" >> "$REPORT_FILE_TMP"
    printf "  🟡 MEDIUM:   %d\n" "$total_medium" >> "$REPORT_FILE_TMP"
    printf "  🟢 LOW:      %d\n" "$total_low" >> "$REPORT_FILE_TMP"
    printf "  ℹ️  INFO:     %d\n" "$total_info" >> "$REPORT_FILE_TMP"
    echo "" >> "$REPORT_FILE_TMP"

    printf "  🔴 CRITICAL: %d\n" "$total_critical"
    printf "  🟠 HIGH:     %d\n" "$total_high"
    printf "  🟡 MEDIUM:   %d\n" "$total_medium"
}

# ── Run scans ────────────────────────────────────────────────────────────────
[[ -n "$BUILTIN_DIR" ]] && scan_dir "BUNDLED SKILLS" "$BUILTIN_DIR"
[[ -n "$WORKSPACE_DIR" ]] && scan_dir "WORKSPACE SKILLS" "$WORKSPACE_DIR"

# ── Final report ───────────────────────────────────────────────────────────
{
    echo "" >> "$REPORT_FILE_TMP"
    echo "═══════════════════════════════════════════════════════════════" >> "$REPORT_FILE_TMP"
    echo "  Scan Complete — ${REPORT_DATE}" >> "$REPORT_FILE_TMP"
    echo "═══════════════════════════════════════════════════════════════" >> "$REPORT_FILE_TMP"
    echo "" >> "$REPORT_FILE_TMP"

    if [[ -s "$FOUND_SECRETS_TMP" ]]; then
        echo "━━━ Findings ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >> "$REPORT_FILE_TMP"
        cat "$FOUND_SECRETS_TMP" >> "$REPORT_FILE_TMP"
        echo "" >> "$REPORT_FILE_TMP"
        echo "  ⚠️  Review findings before publishing." >> "$REPORT_FILE_TMP"
    else
        echo "  ✅ Zero findings — clean scan." >> "$REPORT_FILE_TMP"
    fi

    echo "" >> "$REPORT_FILE_TMP"
    echo "  Report: ${OUTPUT_FILE:-<stdout>}" >> "$REPORT_FILE_TMP"
} >> "$REPORT_FILE_TMP"

# ── Output ──────────────────────────────────────────────────────────────────
if [[ -n "$OUTPUT_FILE" ]]; then
    mv "$REPORT_FILE_TMP" "$OUTPUT_FILE"
    echo ""
    echo "Report saved to: $OUTPUT_FILE"
else
    cat "$REPORT_FILE_TMP"
    rm "$REPORT_FILE_TMP"
fi
rm -f "$FOUND_SECRETS_TMP"


path: scripts/npm-audit.sh
content: #!/usr/bin/env bash
#
# npm-audit.sh — Run npm audit on all workspace skills with package.json
# Part of security-sweep skill
#
# Usage:
#   bash npm-audit.sh --workspace ~/.openclaw/workspace/skills
#

set -euo pipefail

WORKSPACE_DIR=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --workspace) WORKSPACE_DIR="$2"; shift 2 ;;
        -h|--help) echo "Usage: npm-audit.sh --workspace DIR"; exit 0 ;;
        *) echo "Unknown: $1"; exit 1 ;;
    esac
done

[[ -z "$WORKSPACE_DIR" ]] && { echo "Error: --workspace required"; exit 1; }
[[ ! -d "$WORKSPACE_DIR" ]] && { echo "Error: $WORKSPACE_DIR not found"; exit 1; }

echo "Scanning workspace skills for npm vulnerabilities..."
echo ""

total_issues=0

for skill_path in "$WORKSPACE_DIR"/*/; do
    [[ ! -d "$skill_path" ]] && continue
    skill_name=$(basename "$skill_path")

    if [[ ! -f "$skill_path/package.json" ]]; then
        continue
    fi

    echo "Checking $skill_name..."

    if [[ ! -d "$skill_path/node_modules" ]]; then
        echo "  ⚠️  node_modules missing (run: npm install in $skill_path)"
        continue
    fi

    audit_output=$(cd "$skill_path" && npm audit --omit=dev --quiet 2>&1 || true)

    if echo "$audit_output" | grep -q "vulnerab"; then
        total_issues=$((total_issues + 1))
        echo "  🔴 Vulnerabilities found:"
        echo "$audit_output" | head -10 | sed 's/^/    /'
    else
        echo "  🟢 No vulnerabilities"
    fi
    echo ""
done

echo "── Summary ──────────────────────────────────────────────────────────────"
if [[ $total_issues -eq 0 ]]; then
    echo "  🟢 All workspace skills passed npm audit"
else
    echo "  🔴 $total_issues skill(s) have npm vulnerabilities — run npm audit manually to review"
fi


path: scripts/quick-scan.sh
content: #!/usr/bin/env bash
#
# quick-scan.sh — Fast security patterns scan for OpenClaw skills
# Part of security-sweep skill
#
# Usage:
#   bash quick-scan.sh --dir ~/.openclaw/workspace/skills
#

set -euo pipefail

SKILLS_DIR=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --dir) SKILLS_DIR="$2"; shift 2 ;;
        -h|--help) echo "Usage: quick-scan.sh --dir DIR"; exit 0 ;;
        *) echo "Unknown: $1"; exit 1 ;;
    esac
done

[[ -z "$SKILLS_DIR" ]] && SKILLS_DIR="$HOME/.openclaw/workspace/skills"
[[ ! -d "$SKILLS_DIR" ]] && { echo "Error: $SKILLS_DIR not found"; exit 1; }

echo "Quick Scan — $SKILLS_DIR"
echo ""

total_critical=0
total_high=0

for skill_path in "$SKILLS_DIR"/*/; do
    [[ ! -d "$skill_path" ]] && continue
    skill_name=$(basename "$skill_path")

    critical=0
    high=0

    # ── Secret patterns (only key=value, skip tests and docs) ────────
    [REDACTED_SECRET] -rEl \
        "(api[_-]?key|token|password|secret|credential)\s*[=:]\s*[\"'][^'\"]{8,}[\"']" \
        "$skill_path" \
        --include="*.js" --include="*.ts" --include="*.sh" --include="*.py" --include="*.json" \
        2>/dev/null | grep -v "/tests/" | grep -v "/node_modules/" || true)

    [[ -n "$secret" ]] && { critical=$((critical+1)); total_critical=$((total_critical+1)); }

    # ── Dangerous exec ───────────────────────────────────────────────
    exec_found=$(grep -rEl \
        "exec\s*\(|spawn\s*\(|eval\s*\(|child_process|bash\s+-c" \
        "$skill_path" \
        --include="*.js" --include="*.ts" --include="*.sh" \
        2>/dev/null | grep -v "/tests/" | grep -v "/node_modules/" || true)

    [[ -n "$exec_found" ]] && { high=$((high+1)); total_high=$((total_high+1)); }

    if [[ $critical -gt 0 || $high -gt 0 ]]; then
        echo "  $skill_name: 🔴 $critical 🟠 $high"
    else
        echo "  $skill_name: 🟢"
    fi
done

echo ""
echo "── Summary ──────────────────────────────────────────────"
echo "  🔴 CRITICAL: $total_critical"
echo "  🟠 HIGH:     $total_high"
echo ""
[[ $total_critical -eq 0 ]] && [[ $total_high -eq 0 ]] && echo "  🟢 All clear" || echo "  ⚠️  Review findings above"


path: scripts/skill-scan.sh
content: #!/usr/bin/env bash
#
# skill-scan.sh — Scan a single skill directory
# Part of security-sweep skill
#
# Usage:
#   bash skill-scan.sh --skill /path/to/skill
#

set -euo pipefail

SKILL_PATH=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        --skill) SKILL_PATH="$2"; shift 2 ;;
        -h|--help) echo "Usage: skill-scan.sh --skill /path/to/skill"; exit 0 ;;
        *) echo "Unknown: $1"; exit 1 ;;
    esac
done

[[ -z "$SKILL_PATH" ]] && { echo "Error: --skill required"; exit 1; }
[[ ! -d "$SKILL_PATH" ]] && { echo "Error: $SKILL_PATH not found"; exit 1; }

skill_name=$(basename "$SKILL_PATH")
echo "Scanning skill: $skill_name"

# ── Secret patterns ─────────────────────────────────────────────────────────
echo ""
echo "── Secrets ─────────────────────────────────────────────────────────────"
secretFindings=$(grep -rEn \
    "api[_-]?key|token|password|secret|credential|Bearer" \
    "$SKILL_PATH" \
    --include="*.js" --include="*.ts" --include="*.sh" --include="*.py" \
    --include="*.json" \
    -l 2>/dev/null | grep -v node_modules | grep -v "\.git" || true)

if [[ -n "$secretFindings" ]]; then
    echo "  🔴 Found:"
    for f in $secretFindings; do echo "    $f"; done
else
    echo "  🟢 None found"
fi

# ── Dangerous exec ────────────────────────────────────────────────────────────
echo ""
echo "── Dangerous Exec ──────────────────────────────────────────────────────"
execFindings=$(grep -rEn \
    "exec\s*\(|spawn\s*\(|eval\s*\(|child_process|bash\s+-c|system\s*\(|shell\s*:\s*true" \
    "$SKILL_PATH" \
    --include="*.js" --include="*.ts" --include="*.sh" \
    -l 2>/dev/null | grep -v node_modules | grep -v "\.git" || true)

if [[ -n "$execFindings" ]]; then
    echo "  🟠 Found:"
    for f in $execFindings; do echo "    $f"; done
else
    echo "  🟢 None found"
fi

# ── Shell injection ──────────────────────────────────────────────────────────
echo ""
echo "── Shell Injection Surfaces ────────────────────────────────────────────"
injectionFindings=$(grep -rEn \
    '"\$.*"|`\$[^`]+`|\$\{[^}]+\}' \
    "$SKILL_PATH" \
    --include="*.js" --include="*.ts" --include="*.sh" \
    -l 2>/dev/null | grep -v node_modules | grep -v "\.git" || true)

if [[ -n "$injectionFindings" ]]; then
    echo "  🟡 Found (unverified):"
    for f in $injectionFindings; do echo "    $f"; done
else
    echo "  🟢 None found"
fi

# ── Network egress ────────────────────────────────────────────────────────────
echo ""
echo "── Network Egress ──────────────────────────────────────────────────────"
netFindings=$(grep -rEn \
    "https?://|curl |wget |fetch\(|axios\(" \
    "$SKILL_PATH" \
    --include="*.js" --include="*.ts" --include="*.sh" \
    -l 2>/dev/null | grep -v node_modules | grep -v "\.git" || true)

if [[ -n "$netFindings" ]]; then
    echo "  ℹ️  Found:"
    for f in $netFindings; do echo "    $f"; done
else
    echo "  🟢 None found"
fi

echo ""
echo "Done."


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