| name | vuln-scanner |
| description | Audit trending repos for real security vulnerabilities and disclose responsibly - scan and route findings (PVR / dependency PR), re-submit queued advisories, and send armed email disclosures |
| metadata | {"title":"Vuln Scanner","category":"dev","var":"","tags":["dev","security","meta"],"depends_on":["github-trending"],"requires":["GH_GLOBAL?","RESEND_API_KEY?","RESEND_FROM?","RESEND_REPLY_TO?"]} |
${var} — Action selector, shaped [<action>][:<owner/repo>]. Empty or a bare owner/repo → scan arm (audit that repo, or auto-select a trending one). resubmit / resubmit:owner/repo → re-submit arm (probe the security watchlist for repos that just enabled PVR and submit any queued advisory). disclose / email → disclose arm (queue armed out-of-band email disclosures for sending). Examples:
- `` → scan, auto-select from trending
openai/whisper → scan openai/whisper
resubmit → probe the whole watchlist and re-submit what flipped
resubmit:vercel/next.js → probe just that repo (one-off)
disclose (alias email) → arm & queue eligible disclosure emails
poc-smoke → exercise the PoC gate against a benign real Base fork (no audit or disclosure)
riva:owner/repo → scan with the Riva research kernel (shadow/proposal only)
Today is ${today}. Read memory/MEMORY.md and the last 30 days of memory/logs/ before starting.
Why this skill exists
This is the write / action arm of the vuln-disclosure loop — one skill covering the full responsible-disclosure lifecycle:
- Scan — a security scanner that dumps unpatched vulnerabilities into public PRs is a zero-day publisher, not a helper. This skill matches industry practice: Private Vulnerability Reporting (PVR) for code flaws, public PRs only for dependency CVEs that are already public. Bad disclosure burns credibility and puts users at risk.
- Re-submit — when a scan finds a HIGH/CRITICAL issue in a repo with no PVR, no
SECURITY.md, and no reachable contact, it has no safe channel — so it logs the finding as "channel": "skipped" in memory/vuln-scanned.json and stages a watchlist row. Without a weekly probe those findings silently age until the responsible-disclosure window closes. The re-submit arm closes that loop.
- Disclose — when the only responsible path is a private email to the maintainer, drafts sit in
memory/pending-disclosures/ with status: pending-operator-send, waiting for a human. The disclose arm finds drafts explicitly armed for auto-send, composes the email, and sends it in-run (Resend via ./secretcurl) behind a set of fail-closed caps — the send is the arm's final action.
Dispatch — parse ${var}, then run one arm
Parse the selector once, then jump to the matching arm below:
SEL="${var}"
ACTION="${SEL%%:*}"
TARGET="${SEL#*:}"; [ "$TARGET" = "$SEL" ] && TARGET=""
case "$ACTION" in
resubmit|watchlist|pvr) ARM="resubmit" ;;
disclose|email) ARM="disclose" ;;
poc-smoke|verify-gate) ARM="poc-smoke" ;;
riva|research) ARM="scan"; KERNEL="riva" ;;
shadow|compare) ARM="scan"; KERNEL="shadow" ;;
""|scan) ARM="scan" ;;
*/*) ARM="scan"; TARGET="$SEL" ;;
*) ARM="scan" ;;
esac
KERNEL="${KERNEL:-${VULN_RESEARCH_KERNEL:-legacy}}"
case "$KERNEL" in legacy|shadow|riva) ;; *) KERNEL="legacy" ;; esac
ARM=scan → Arm A — SCAN (target = $TARGET, or auto-select if empty).
ARM=resubmit → Arm B — RE-SUBMIT (probe $TARGET if set, else the whole watchlist).
ARM=disclose → Arm C — DISCLOSE (queue armed email drafts).
ARM=poc-smoke → Arm D — PoC GATE SMOKE (benign live-fork verification only).
Each arm is independently executable. The operational arms share the same GitHub token and the same memory/ state (vuln-scanned.json, security-watchlist.md, pending-disclosures/, email-log.json) — that shared state is exactly how the arms hand off to each other. The smoke arm does not read or modify that state.
Arm A — SCAN
Find one trending repo, run purpose-built scanners (not raw grep), triage to real exploitable findings, and route each finding to the correct disclosure channel — PVR, SECURITY.md contact, or dependency-bump PR.
A1. Pick a target
If $TARGET is set, use it. Otherwise:
CANDS=""
if [ -s output/.chains/github-trending.md ]; then
CANDS=$(grep -oE '\[[^]]+/[^]]+\]\(https?://[^)]+\)|https?://github\.com/[^/ )]+/[^/ )]+' output/.chains/github-trending.md \
| sed -E 's#.*github\.com/##; s#\).*##; s#^\[##; s#\].*##' | grep -E '^[^/ ]+/[^/ ]+$' | sort -u)
fi
if [ -z "$CANDS" ]; then
if [ "$KERNEL" = shadow ]; then
echo "VULN_SCANNER_SKIPPED shadow-target-required: use shadow:owner/repo or provide github-trending chain output"
exit 0
else
gh api "search/repositories?q=created:>$(date -u -d '14 days ago' +%Y-%m-%d)&sort=stars&order=desc&per_page=25" \
--jq '.items[] | select(.fork==false) | select(.stargazers_count>=50) | {full_name, language, description, security_and_analysis}'
fi
fi
Selection criteria:
- Language you can reason about (JS/TS, Python, Go, Rust, Solidity)
- ≥50 stars, not a fork, active in last 6 months
- Handles untrusted input: auth, crypto, network, file I/O, templating
- Skip if scanned in last 30 days (grep
memory/logs/ for the repo name)
- Skip deliberately vulnerable teaching repos (DVWA, juice-shop, webgoat, vulnerable-*, -ctf, hackme-)
- Skip repos with no
SECURITY.md AND security_and_analysis.private_vulnerability_reporting.status != "enabled" — you have no safe channel to report code flaws (you can still run a dep-scan and skip code audit; see step A5)
A2. Fork and clone
REPO="owner/repo"
if [ "$KERNEL" = shadow ]; then
git clone --depth 200 --quiet "https://github.com/${REPO}.git"
else
gh repo fork "$REPO" --clone --default-branch-only -- --depth 200 --quiet
fi
cd "$(basename "$REPO")"
A3. Run purpose-built scanners
Raw grep produces too many false positives. Use tools with dataflow reachability and verified-secret matching.
Stage the scanners in-run into /tmp/bin (see the install preamble below). The
network is open, but pip install / a curl-piped-to-shell install / tar are not on the in-run
capability allowlist — use the ones that are: python3 -m pip install … for the Python
tools (semgrep, slither) and curl -o … && chmod +x for the Go binaries (osv-scanner,
trufflehog). Put /tmp/bin on PATH and invoke
each tool by bare name — the bare names (semgrep, trufflehog,
osv-scanner, slither) are exactly what the capability allowlist
(scripts/skill_mode.sh) grants, so claude -p is permitted to execute them. If
a binary is missing, log VULN_SCANNER_SKIPPED and continue (it records fail
in sources.txt below) — never abort the whole run for one tool.
mkdir -p /tmp/vuln-scan /tmp/bin
export PATH="/tmp/bin:$PATH"
python3 -m pip install --quiet --disable-pip-version-check semgrep slither-analyzer 2>/dev/null || true
curl -sSL -o /tmp/bin/osv-scanner "https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64" 2>/dev/null && chmod +x /tmp/bin/osv-scanner || true
if command -v semgrep >/dev/null 2>&1; then
semgrep --config=p/security-audit --config=p/owasp-top-ten --config=p/secrets \
--severity=ERROR --severity=WARNING --json --quiet --timeout=300 \
--exclude=test --exclude=tests --exclude=__tests__ --exclude=spec --exclude=specs \
--exclude=fixtures --exclude=examples --exclude=example --exclude=demo \
--exclude=vendor --exclude=node_modules --exclude=dist --exclude=build --exclude=.next \
-o /tmp/vuln-scan/semgrep.json . 2>/dev/null || true
else
echo "VULN_SCANNER_SKIPPED: semgrep not available"
fi
if command -v trufflehog >/dev/null 2>&1; then
TRUFFLEHOG_RC=0
timeout 300 trufflehog filesystem . --only-verified --json \
> /tmp/vuln-scan/trufflehog.json 2>/dev/null || TRUFFLEHOG_RC=$?
[ "$TRUFFLEHOG_RC" = 124 ] && echo "VULN_SCANNER_TIMEOUT: trufflehog filesystem scan exceeded 300s on a very large tree — recorded as fail, not retried, not left unfinished"
TRUFFLEHOG_GIT_RC=0
timeout 300 trufflehog git file://. --only-verified --json \
> /tmp/vuln-scan/trufflehog-git.json 2>/dev/null || TRUFFLEHOG_GIT_RC=$?
[ "$TRUFFLEHOG_GIT_RC" = 124 ] && echo "VULN_SCANNER_TIMEOUT: trufflehog git history scan exceeded 300s on a large packed history — recorded as fail, not retried, not left unfinished"
else
echo "VULN_SCANNER_SKIPPED: trufflehog not available"
fi
if command -v osv-scanner >/dev/null 2>&1; then
osv-scanner scan source --recursive --no-ignore --format=json . > /tmp/vuln-scan/osv.json 2>/dev/null; OSV_RC=$?
[ -s /tmp/vuln-scan/osv.json ] || { osv-scanner --format=json --recursive --no-ignore . > /tmp/vuln-scan/osv.json 2>/dev/null; OSV_RC=$?; }
if [ -s /tmp/vuln-scan/osv.json ]; then OSV_STATUS=ok
elif [ "${OSV_RC:-}" = 128 ]; then OSV_STATUS=none
else OSV_STATUS=fail; fi
else
OSV_STATUS=skipped
echo "VULN_SCANNER_SKIPPED: osv-scanner not available"
fi
if ls **/*.sol >/dev/null 2>&1 && command -v slither >/dev/null 2>&1; then
slither . --json /tmp/vuln-scan/slither.json --exclude-informational --exclude-low 2>/dev/null || true
fi
echo "semgrep=$([ -s /tmp/vuln-scan/semgrep.json ] && echo ok || echo fail)" > /tmp/vuln-scan/sources.txt
if [ "${TRUFFLEHOG_RC:-1}" = 124 ]; then
echo "trufflehog=timeout" >> /tmp/vuln-scan/sources.txt
else