import from local vendored copy (2026-05-06)
Some checks failed
CI / validate (push) Failing after 0s

This commit is contained in:
Hongming Wang 2026-05-06 13:53:30 -07:00
commit 24e4e3368f
10 changed files with 651 additions and 0 deletions

5
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,5 @@
name: CI
on: [push, pull_request]
jobs:
validate:
uses: Molecule-AI/molecule-ci/.github/workflows/validate-plugin.yml@main

21
.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1 @@
pyyaml>=6.0

View File

@ -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)}")

193
CLAUDE.md Normal file
View File

@ -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.

19
README.md Normal file
View File

@ -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.

54
known-issues.md Normal file
View File

@ -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] <Short Title>
**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`.

16
plugin.yaml Normal file
View File

@ -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

162
runbooks/local-dev-setup.md Normal file
View File

@ -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 <test-wsid>
```
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/

View File

@ -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