commit 24e4e3368fab293ad7592434a121bc808d25766f Author: Hongming Wang Date: Wed May 6 13:53:30 2026 -0700 import from local vendored copy (2026-05-06) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c8fb9d3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,5 @@ +name: CI +on: [push, pull_request] +jobs: + validate: + uses: Molecule-AI/molecule-ci/.github/workflows/validate-plugin.yml@main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2af45b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Credentials — never commit. Use .env.example as the template. +.env +.env.local +.env.*.local +.env.* +!.env.example +!.env.sample + +# Private keys + certs +*.pem +*.key +*.crt +*.p12 +*.pfx + +# Secret directories +.secrets/ + +# Workspace auth tokens +.auth-token +.auth_token diff --git a/.molecule-ci/scripts/requirements.txt b/.molecule-ci/scripts/requirements.txt new file mode 100644 index 0000000..3aecde9 --- /dev/null +++ b/.molecule-ci/scripts/requirements.txt @@ -0,0 +1 @@ +pyyaml>=6.0 diff --git a/.molecule-ci/scripts/validate-plugin.py b/.molecule-ci/scripts/validate-plugin.py new file mode 100644 index 0000000..c42e916 --- /dev/null +++ b/.molecule-ci/scripts/validate-plugin.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Validate a Molecule AI plugin repo.""" +import os, sys, yaml + +errors = [] + +# 1. plugin.yaml exists +if not os.path.isfile("plugin.yaml"): + print("::error::plugin.yaml not found at repo root") + sys.exit(1) + +with open("plugin.yaml") as f: + plugin = yaml.safe_load(f) + +# 2. Required fields +for field in ["name", "version", "description"]: + if not plugin.get(field): + errors.append(f"Missing required field: {field}") + +# 3. Version format +v = str(plugin.get("version", "")) +if v and not all(c in "0123456789." for c in v): + errors.append(f"Invalid version format: {v}") + +# 4. Runtimes type +runtimes = plugin.get("runtimes") +if runtimes is not None and not isinstance(runtimes, list): + errors.append(f"runtimes must be a list, got {type(runtimes).__name__}") + +# 5. Has content +content_paths = ["SKILL.md", "hooks", "skills", "rules"] +found = [p for p in content_paths if os.path.exists(p)] +if not found: + errors.append("Plugin must contain at least one of: SKILL.md, hooks/, skills/, rules/") + +# 6. SKILL.md formatting check +if os.path.isfile("SKILL.md"): + with open("SKILL.md") as f: + first_line = f.readline().strip() + if first_line and not first_line.startswith("#"): + print("::warning::SKILL.md should start with a markdown heading (e.g., # Plugin Name)") + +if errors: + for e in errors: + print(f"::error::{e}") + sys.exit(1) + +print(f"✓ plugin.yaml valid: {plugin['name']} v{plugin['version']}") +if found: + print(f" Content: {', '.join(found)}") +if runtimes: + print(f" Runtimes: {', '.join(runtimes)}") diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..61fab0e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,193 @@ +# molecule-security-scan — CVE Gate for Skill Dependencies + +`molecule-security-scan` is a **supply-chain CVE gate** plugin. It wraps +`builtin_tools/security_scan.py` and runs Snyk or pip-audit against a +skill's `requirements.txt` before the skill loads, blocking or warning on +critical/high CVEs. + +**Version:** 1.0.0 +**Runtime:** `langgraph`, `claude_code`, `deepagents` +**Related:** `molecule-audit` (event retention), `molecule-compliance` (runtime +OWASP policy) + +--- + +## Repository Layout + +``` +molecule-security-scan/ +├── plugin.yaml — Plugin manifest +├── skills/ +│ └── skill-cve-gate/ +│ └── SKILL.md — Full skill documentation +└── builtin_tools/ — (harness-provided, not in this repo) + └── security_scan.py — CVE gate implementation +``` + +--- + +## How It Works + +When a skill is about to load, the gate runs a CVE scanner against its +`requirements.txt`. Selection is automatic: + +| Scanner | Requires | When selected | +|---|---|---| +| **Snyk CLI** | `snyk` binary in PATH + `SNYK_TOKEN` env | Available — preferred | +| **pip-audit** | `pip-audit` binary in PATH | Fallback when Snyk absent | +| **skip** | — | Neither available → skip silently | + +--- + +## Modes + +Configure in workspace `config.yaml`: + +```yaml +security_scan: + mode: warn # off | warn | block +``` + +| Mode | Behaviour | +|---|---| +| `off` | Skip entirely. Useful in air-gapped CI. | +| `warn` | Log WARNING + audit event on critical/high. Load skill anyway. | +| `block` | Raise `SkillSecurityError`. Skill does not load. | + +**Rollout order:** `warn` first → measure → then `block` once clean. + +--- + +## When to Install + +✅ Install on workspaces that: +- Install skills from third-party sources (marketplace, agentskills.io, uploads) +- Run in a production tenant where agent compromise is meaningful +- Must satisfy a supply-chain audit (SOC 2, ISO 27001 control A.8.28) + +❌ Skip on workspaces that only use first-party `molecule-*` plugins — +those are vetted at PR-review time in monorepo CI. + +--- + +## Audit Trail + +Every scan writes to the audit log via `audit.log_event`: + +```json +{ + "event_type": "supply_chain", + "action": "cve_scan", + "resource": "skill-name:version", + "outcome": "pass", + "detail": { + "scanner": "snyk", + "critical": 0, + "high": 0, + "medium": 2, + "low": 5 + } +} +``` + +Failures (mode=block) log `outcome: denied` + the blocking CVE ID. +**Pair with `molecule-audit`** to get the full JSONL trail. + +--- + +## SNYK_TOKEN Setup + +Set via workspace secret (never in `config.yaml`): + +```bash +curl -X POST http://localhost:8080/workspaces/$WS_ID/secrets \ + -H "Content-Type: application/json" \ + -d '{"key":"SNYK_TOKEN","value":"..."}' +``` + +The token is injected at container start as an env var. + +--- + +## Full Configuration Reference + +```yaml +security_scan: + mode: warn + # scanner: pip-audit # force override (default: auto) + severity_threshold: high # critical | high | medium | low + fail_open_if_no_scanner: true +``` + +- `severity_threshold` — only findings ≥ this level trigger warn/block. + Medium and low are always INFO-logged only. +- `fail_open_if_no_scanner` — `true` = skip silently if neither tool present; + `false` = treat as block event. + +--- + +## Anti-Patterns + +- **Do not** set `mode: block` during initial rollout — strand risk. +- **Do not** install without `molecule-audit` — scan results disappear. +- **Do not** scan first-party `molecule-*` plugins — vetted at commit time. +- **This is not your only supply-chain defence.** It catches known CVEs only. + Complement with deterministic lockfiles and registry allowlists. + +--- + +## Development + +### Prerequisites + +- Node.js >= 18 (for markdownlint, if editing `.md` files) +- Python 3.11+ (for YAML validation) +- `gh` CLI authenticated +- Write access to `Molecule-AI/molecule-ai-plugin-molecule-security-scan` + +### Setup + +```bash +git clone https://github.com/Molecule-AI/molecule-ai-plugin-molecule-security-scan.git +cd molecule-ai-plugin-molecule-security-scan + +# Validate plugin.yaml +python3 -c "import yaml; yaml.safe_load(open('plugin.yaml'))" +echo "plugin.yaml OK" +``` + +### Pre-Commit Checklist + +```bash +# YAML structure +python3 -c "import yaml; yaml.safe_load(open('plugin.yaml'))" + +# No credentials in plugin.yaml +python3 -c " +import re, sys +with open('plugin.yaml') as f: + content = f.read() +patterns = [r'sk.ant', r'ghp.', r'AKIA[A-Z0-9]'] +if any(re.search(p, content) for p in patterns): + print('FAIL: possible credentials found') + sys.exit(1) +print('No credentials: OK') +" +``` + +--- + +## Release Process + +1. Review changes: `git log origin/main..HEAD --oneline` +2. Bump `version` in `plugin.yaml` (semver) +3. Update `**Version:**` in this CLAUDE.md if conventions changed +4. Commit: `chore: bump version to X.Y.Z` +5. Tag and push: `git tag vX.Y.Z && git push origin main --tags` +6. Create GitHub Release with changelog + +--- + +## Known Issues + +See `known-issues.md` at the repo root. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1e7aa9 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# molecule-security-scan + +Molecule AI plugin. Install via the Molecule AI platform plugin system. + +## Usage + +### In org template (org.yaml) +```yaml +plugins: + - molecule-security-scan +``` + +### From URL (community install) +``` +github://Molecule-AI/molecule-ai-plugin-molecule-security-scan +``` + +## License +Business Source License 1.1 — © Molecule AI. diff --git a/known-issues.md b/known-issues.md new file mode 100644 index 0000000..050cf11 --- /dev/null +++ b/known-issues.md @@ -0,0 +1,54 @@ +# Known Issues — molecule-security-scan + +--- + +## Active Issues + +*(None currently open. This section is updated when issues are filed.)* + +--- + +## Recently Resolved + +*(No recently resolved issues.)* + +--- + +## How to Update This File + +When a new issue is identified: +1. Add it under **Active Issues** using the template below +2. Include: symptom, cause (if known), workaround +3. When fixed, move to **Recently Resolved** and note the fix version + +### Issue Template + +```markdown +## [TICKET-NUMBER] + +**Severity:** P0 / P1 / P2 / P3 +**Status:** Workaround / Fix in progress / Fix available +**Affected versions:** All / vX.Y.Z+ + +**Symptoms:** +**Cause:** +**Workaround:** +**Fix (if available):** +``` + +--- + +## Severity Definitions + +| Level | Description | +|---|---| +| P0 | CVE gate bypasses block — critical CVE loads anyway | +| P1 | False negative on known critical CVE | +| P2 | Mode=warn emits no audit event | +| P3 | Documentation or cosmetic issue | + +--- + +## Reporting + +Use the Molecule-AI/internal issue tracker. Tag with `plugin-molecule-security-scan`. diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..6521cf2 --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,16 @@ +name: molecule-security-scan +version: 1.0.0 +description: > + Supply-chain CVE gate for skill dependencies. Wraps builtin_tools/security_scan.py — + runs Snyk or pip-audit against a skill's requirements.txt before the skill + loads, blocking or warning on critical/high CVEs. Opt-in per workspace. +author: Molecule AI +tags: [security, cve, supply-chain, snyk, pip-audit] + +runtimes: + - langgraph + - claude_code + - deepagents + +skills: + - skill-cve-gate diff --git a/runbooks/local-dev-setup.md b/runbooks/local-dev-setup.md new file mode 100644 index 0000000..1c55916 --- /dev/null +++ b/runbooks/local-dev-setup.md @@ -0,0 +1,162 @@ +# Local Development Setup + +This runbook covers setting up a local development environment for +`molecule-security-scan`. + +--- + +## Prerequisites + +- Python 3.11+ +- `gh` CLI authenticated +- Write access to `Molecule-AI/molecule-ai-plugin-molecule-security-scan` + +--- + +## Clone & Bootstrap + +```bash +git clone https://github.com/Molecule-AI/molecule-ai-plugin-molecule-security-scan.git +cd molecule-ai-plugin-molecule-security-scan +``` + +--- + +## Validating Plugin Structure + +```bash +# YAML structure validation +python3 -c "import yaml; yaml.safe_load(open('plugin.yaml'))" +echo "plugin.yaml OK" + +# Check all referenced skill paths exist +python3 -c " +import yaml, os +with open('plugin.yaml') as f: + data = yaml.safe_load(f) +for skill in data.get('skills', []): + path = f'skills/{skill}/SKILL.md' + exists = os.path.exists(path) + print(f'[{\"OK\" if exists else \"MISSING\"}] {path}') +" +``` + +--- + +## Testing the CVE Gate Locally + +The `builtin_tools/security_scan.py` harness wrapper is not in this repo — it +is provided by the Molecule AI platform at runtime. To test locally: + +1. **Install a scanner** (Snyk or pip-audit): + ```bash + # Option A: pip-audit (simpler) + pip install pip-audit + pip-audit --help + + # Option B: Snyk (richer DB) + npm install -g snyk + snyk auth + ``` + +2. **Create a test skill with a vulnerable requirements.txt**: + ```bash + mkdir -p /tmp/test-skill + echo "flask==0.0.1" > /tmp/test-skill/requirements.txt + ``` + +3. **Run the scanner directly** to verify it works: + ```bash + # pip-audit + pip-audit -r /tmp/test-skill/requirements.txt + + # snyk + SNYK_TOKEN=your_token snyk test --file=/tmp/test-skill/requirements.txt + ``` + +4. **Install the plugin in a test workspace**: + ```bash + mol workspace plugin install molecule-security-scan --workspace + ``` + +5. **Trigger a skill load** and check the audit log for `supply_chain` events. + +--- + +## Verifying Scanner Auto-Selection + +The gate selects the scanner automatically. To test the priority: + +```bash +# Snyk only in PATH → should be selected +which snyk && echo "snyk: AVAILABLE" || echo "snyk: NOT FOUND" +which pip-audit && echo "pip-audit: AVAILABLE" || echo "pip-audit: NOT FOUND" + +# The gate logs which scanner was selected — check the audit trail +# for: "detail": {"scanner": "snyk", ...} +``` + +--- + +## Simulating Block Mode + +To test `mode: block`: + +```bash +# Set a known-vulnerable package in a test skill +echo "requests=2.18.0" > /tmp/vuln-skill/requirements.txt + +# Install in test workspace with mode: block in config.yaml: +# security_scan: +# mode: block + +# Try to load the vulnerable skill +# Expected: SkillSecurityError in workspace logs +# Expected audit event: outcome=denied, detail.cve_id=... +``` + +--- + +## Troubleshooting + +### Plugin loads but no scan happens + +- Check `builtin_tools/security_scan.py` is available in the harness +- Verify `config.yaml` has `security_scan.mode` set (not absent) +- Check the workspace audit log for `supply_chain` events with `outcome: skip` + +### Snyk returns no vulnerabilities + +- Confirm `SNYK_TOKEN` is set as a workspace secret +- Run `snyk auth` interactively to verify token validity +- Snyk unauthenticated mode has reduced CVE coverage — pip-audit fallback + may find issues Snyk misses + +### pip-audit fails to parse requirements.txt + +- pip-audit requires `pip >= 21.0` +- Check the requirements.txt has valid pip-installable package specs +- Run `pip-compile` to generate a locked requirements.txt if needed + +### Workspace blocked unexpectedly + +- The gate found a critical/high CVE in a transitive dependency +- Check the audit event: `"outcome": "denied"`, `"detail": {"blocked_cve": "CVE-..."}` +- Fix: update the package to a patched version, then re-load the skill + +### False positive on known-safe package + +- This is expected for first-party `molecule-*` packages +- These should be excluded from scanning (the plugin skips them automatically) +- If a false positive occurs on a third-party skill, open an issue + +--- + +## Related + +- `builtin_tools/security_scan.py` — the platform-provided CVE gate implementation +- `skills/skill-cve-gate/SKILL.md` — full skill documentation +- `molecule-audit` — event retention for scan results +- `molecule-compliance` — runtime OWASP policy companion +- Snyk CLI docs: https://docs.snyk.io/snyk-cli +- pip-audit docs: https://pypi.org/project/pip-audit/ diff --git a/skills/skill-cve-gate/SKILL.md b/skills/skill-cve-gate/SKILL.md new file mode 100644 index 0000000..7cdb595 --- /dev/null +++ b/skills/skill-cve-gate/SKILL.md @@ -0,0 +1,128 @@ +--- +name: skill-cve-gate +description: "Block or warn on CVE-vulnerable dependencies before a skill loads into the workspace. Use when a workspace installs skills from third-party sources (user-uploaded, marketplace, agentskills.io). Prevents known-bad transitive deps from running in the agent's process." +--- + +# Skill CVE Gate + +Supply-chain risk management for skill dependencies. Wraps +`builtin_tools/security_scan.py`. When a skill is about to load, the +gate runs a CVE scanner against its `requirements.txt` and either +blocks, warns, or skips depending on mode. + +## Scanners (auto-selected) + +| Scanner | Requires | When selected | +|---|---|---| +| **Snyk CLI** | `snyk` binary in PATH + `SNYK_TOKEN` env | Available — preferred (richer DB + license coverage) | +| **pip-audit** | `pip-audit` binary in PATH | Fallback when Snyk isn't installed | +| **(none)** | — | Neither available → skip with log line | + +Selection happens at scan time, per skill. No config flag needed. + +## Modes + +Configure in `config.yaml`: + +```yaml +security_scan: + mode: warn # off | warn | block +``` + +- **`off`** — skip scanning entirely. Useful in air-gapped CI that has + no network access to CVE databases, or dev loops where you know the + deps are vetted. +- **`warn`** (default) — run the scanner, log a WARNING + audit event + on any critical/high finding, but load the skill anyway. Good for + rollout phase: you see the risk surface without breaking users. +- **`block`** — raise `SkillSecurityError` when critical/high CVEs are + found. Skill does not load; agent falls back to built-in tools only. + Use once warn-phase is clean. + +## When to install + +Install on any workspace that: +- Installs skills from third-party sources (marketplace, agentskills.io, + user uploads) +- Runs in a production tenant context where agent compromise is + meaningful +- Must satisfy a supply-chain audit (SOC 2, ISO 27001 control A.8.28) + +Skip on workspaces that only use first-party plugins from +`plugins/molecule-*` — those are vetted at commit time in monorepo CI. + +## Audit trail + +Every scan writes to the audit log via `audit.log_event`: + +```json +{ + "event_type": "supply_chain", + "action": "cve_scan", + "resource": "skill-name:version", + "outcome": "pass", + "detail": { + "scanner": "snyk", + "critical": 0, + "high": 0, + "medium": 2, + "low": 5 + } +} +``` + +Failures (mode=block) log `outcome: "denied"` + the blocking CVE id. +Pair with `molecule-audit` to get the full JSONL trail. + +## SNYK_TOKEN + +Set via workspace secret (not config.yaml): + +```bash +curl -X POST http://localhost:8080/workspaces/$WS_ID/secrets \ + -H "Content-Type: application/json" \ + -d '{"key":"SNYK_TOKEN","value":"..."}' +``` + +Snyk authenticates via env var; the token is injected at container +start. Without it Snyk runs in unauthenticated mode (fewer CVE sources +available) and the fallback to pip-audit is more attractive. + +## Configuration — full + +```yaml +security_scan: + mode: warn + # Override the auto-selected scanner: + # scanner: pip-audit # force pip-audit even when snyk is available + severity_threshold: high # critical | high | medium | low + fail_open_if_no_scanner: true # skip silently when neither tool present +``` + +`severity_threshold` — only findings at or above this severity trigger +the mode behavior (warn or block). Medium and low are always logged at +INFO but never block. + +## Anti-patterns + +- **Don't** set `mode: block` during initial rollout — you'll strand + legitimate skills that have medium-severity transitive deps. Start + in `warn`, measure, then block. +- **Don't** install without also installing `molecule-audit` — the + compliance value of scanning disappears if the events aren't in a + durable log. +- **Don't** scan the monorepo's first-party plugins. They're vetted at + PR-review time. Repeat scanning wastes time + may trip on false + positives. +- **Don't** rely on this as your only supply-chain defense. It catches + known CVEs; it does NOT catch typosquatting, malicious package + updates, or signed-but-compromised releases. Complement with + deterministic lockfiles + registry allowlists. + +## Related + +- `builtin_tools/security_scan.py` — the implementation +- `molecule-compliance` — runtime OWASP policy; this is its supply- + chain counterpart +- `molecule-audit` — event retention for scan results +- Issue #256 — the proposal that led to this plugin split