vuln-scanner

star 517

Audit trending repos for real security vulnerabilities and disclose responsibly via PVR or dependency PRs

aaronjmars By aaronjmars schedule Updated 6/15/2026

name: Vuln Scanner category: core description: Audit trending repos for real security vulnerabilities and disclose responsibly via PVR or dependency PRs var: "" tags: [dev, security] depends_on: [github-trending]

${var} — Target repo in owner/repo. If empty, auto-select from .outputs/github-trending.md or GitHub's trending API.

Today is ${today}. Read memory/MEMORY.md and the last 30 days of memory/logs/ before starting.

Why this skill exists

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.

Goal

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.

Steps

1. Pick a target

If ${var} is set, use it. Otherwise:

# Prefer chained output from github-trending skill
if [ -s .outputs/github-trending.md ]; then
  # parse owner/repo lines; pick first that matches criteria below
  :
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

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 5)

2. Fork and clone

REPO="owner/repo"
gh repo fork "$REPO" --clone --default-branch-only -- --depth 200 --quiet
cd "$(basename "$REPO")"

3. Run purpose-built scanners

Raw grep produces too many false positives. Use tools with dataflow reachability and verified-secret matching.

mkdir -p /tmp/vuln-scan

# --- SAST: Semgrep OSS ---
pip install --quiet semgrep 2>/dev/null || true
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

# --- Secrets: TruffleHog (only-verified = actually authenticates) ---
curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh \
  | sh -s -- -b /tmp/bin 2>/dev/null || true
/tmp/bin/trufflehog filesystem . --only-verified --json \
  > /tmp/vuln-scan/trufflehog.json 2>/dev/null || true
# Also scan full git history for secrets
/tmp/bin/trufflehog git file://. --only-verified --json \
  > /tmp/vuln-scan/trufflehog-git.json 2>/dev/null || true

# --- Dependencies: osv-scanner (unified CVE DB across ecosystems) ---
curl -sSfL -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
/tmp/bin/osv-scanner --format=json --recursive . \
  > /tmp/vuln-scan/osv.json 2>/dev/null || true

# --- Smart-contract scan (if Solidity present) ---
if ls **/*.sol >/dev/null 2>&1; then
  pip install --quiet slither-analyzer 2>/dev/null || true
  slither . --json /tmp/vuln-scan/slither.json --exclude-informational --exclude-low 2>/dev/null || true
fi

# Record what succeeded (empty output ≠ clean, could be tool failure)
echo "semgrep=$([ -s /tmp/vuln-scan/semgrep.json ] && echo ok || echo fail)" >  /tmp/vuln-scan/sources.txt
echo "trufflehog=$([ -s /tmp/vuln-scan/trufflehog.json ] && echo ok || echo fail)" >> /tmp/vuln-scan/sources.txt
echo "osv=$([ -s /tmp/vuln-scan/osv.json ] && echo ok || echo fail)"              >> /tmp/vuln-scan/sources.txt

4. Triage — read every finding before trusting it

A scanner hit is a candidate, not a vulnerability. For each candidate:

  1. Open the file at the reported line and read the surrounding 30–50 lines.
  2. Write one sentence describing what an attacker controls and what they achieve. If you can't, discard it.
  3. Check the call path — is the vulnerable function reachable from external input in production code (not tests, docs, examples)?
  4. Severity: critical (RCE, auth bypass, secret exposure), high (SQLi, stored XSS, SSRF, path traversal), medium (reflected XSS, weak crypto, missing rate limit).
  5. Assign disclosure channel per step 5.

Drop the finding if:

  • It's in test/, mock/, fixture/, example/, demo/, bench/, docs/
  • It's behind a feature flag not enabled by default
  • It requires attacker privileges equal to or greater than the attack yields
  • You'd be embarrassed to defend it to the maintainer

If 0 findings survive triage → log "clean audit — N candidates reviewed, 0 confirmed" and exit cleanly.

5. Route each finding to the correct disclosure channel

This is the core of the skill. Pick the channel by finding type:

Finding type Channel Why
Dependency CVE (osv-scanner hit) Public PR bumping the dep CVE is already public; a patch PR is net-positive
Code vulnerability (Semgrep ERROR/WARNING, verified exploitable) PVR (GitHub private advisory) Unpatched code flaw — public disclosure creates a zero-day
Verified leaked secret (TruffleHog verified) PVR + tell maintainer to rotate Publishing the file/line in a public PR tells attackers where to look
Smart-contract issue (Slither high/medium) PVR On-chain exploitation is often immediate and irreversible
No PVR enabled AND no SECURITY.md Private issue to maintainer if possible, else skip and log No safe channel = do no harm

5a. Public PR (dependency CVEs only)

git checkout -b security/bump-<pkg>-<cve>
# Update lockfile/manifest
git add -A
git commit -m "fix(deps): bump <pkg> to patch <CVE-YYYY-NNNN>

Advisory: <link to GHSA or NVD>
Severity: <high/critical>
Fixed in: <version>"
git push -u origin HEAD
gh pr create --repo "$REPO" \
  --title "fix(deps): bump <pkg> to patch <CVE-YYYY-NNNN>" \
  --body "$(cat <<EOF
Automated dependency bump to address a disclosed CVE.

- **CVE:** <id>
- **Advisory:** <url>
- **Severity:** <severity>
- **Package:** \`<name>\` → \`<fixed-version>\`

Detected by [osv-scanner](https://google.github.io/osv-scanner/). No code changes outside the lockfile/manifest.

---
Filed by [Aeon](https://github.com/aeonframework/aeon).
EOF
)"

5b. Private Vulnerability Report (code flaws, verified secrets, contract bugs)

# Private third-party reporting uses the /reports endpoint. Do NOT use the bare
# /security-advisories endpoint — that *creates* an advisory and requires
# admin/security-manager rights on the target repo, so it returns 403 on any repo
# you don't own. Classic `repo` scope is sufficient for /reports;
# `repository_advisories:write` is NOT required for third-party reporting.
gh api -X POST "/repos/$REPO/security-advisories/reports" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  -f summary="<short title>" \
  -f description="$(cat <<'EOF'
## Summary
<one-paragraph description>

## Impact
<what an attacker can do, concretely>

## Location
`path/to/file.ext:LINE`

## Proof of exploitation
<minimal PoC or request/payload — no working exploit chains>

## Suggested fix
<specific code change or pattern>

## Detected by
Aeon + <semgrep|trufflehog|slither>
EOF
)" \
  -f severity="<critical|high|medium|low>" \
  -F cwe_ids='["CWE-89"]'  # adjust per finding

Pass large report bodies via --input <file>, not a long inline heredoc — a multi-line -f description="$(cat …)" can trip the sandbox ("Unhandled node type: string"). Write the JSON payload ({summary, description, severity, cwe_ids}) to a temp file and gh api -X POST … --input payload.json.

Read the HTTP response code and branch accordingly. Never fall back to a public issue or a code-fix PR for an unpatched flaw (that publishes a zero-day):

  • 201 → reported. Record the report/advisory id and link it in the local report.
  • 403 "Repository does not have private vulnerability reporting enabled" → PVR is OFF on the repo. This is not a token-scope problem (classic repo scope is enough). Critically: the GitHub advisory web form (/security/advisories/new) is the SAME PVR backend — it returns 404 to external reporters when PVR is off. Do NOT stage that URL as the channel even if SECURITY.md recommends it (a SECURITY.md that only says "use the advisory form" is not a usable channel when PVR is disabled — confirmed on agent-reach and world-of-claudecraft, 2026-06-19). Resolve an out-of-band private contact instead, in this order: (1) SECURITY.md email / portal / vendor PSIRT; (2) README contact (email / Discord / X); (3) package metadata — pyproject.toml / setup.py author, package.json author + bugs; (4) the maintainer/owner's git commit email or GitHub profile. Stage a maintainer-ready report at .pending-disclosure/<repo>-<timestamp>.md with the resolved contact (status: pending-operator-send) — do not auto-send. Only if no out-of-band contact exists anywhere, log "no safe channel — skipped".
  • 5xx (GitHub API error on /reports, PVR enabled) → the API path is broken for this repo/token; do not retry-spam it. The web form https://github.com/<repo>/security/advisories/new is a different frontend to the same PVR backend and usually works when the API 5xxs on a PVR-enabled repo — stage the report in .pending-disclosure/ and have the operator file it there. (Contrast the 403 case above: when PVR is disabled the form 404s too, so it is not a fallback there — route to an out-of-band contact instead.)
  • Any other failure → stage in .pending-disclosure/ and surface to the operator; never publish.

Dependency-bump PRs (step 5a) are the only public channel. Hardening-class code findings (e.g. DNS-rebinding / Host-Origin allowlists) may be offered as a neutral public PR at operator discretion, but high-severity exploitable flaws (RCE, auth bypass, secret exposure, sandbox/guardrail escape) must stay on a private channel.

5c. Proposed code patch (optional, paired with 5b)

If you have a minimal fix, push it to your fork only (not a PR to upstream) and link it in the PVR description so the maintainer can cherry-pick:

git checkout -b private/fix-<slug>
# apply fix
git commit -m "draft: proposed patch for reported advisory"
git push -u origin HEAD
# DO NOT open a PR. Link the branch in the advisory body.

6. Update dedup state

Append to memory/vuln-scanned.json (create if missing) so future runs skip this repo for 30 days:

{"repo": "owner/repo", "scanned_at": "2026-04-20T16:00:00Z", "findings": <N>, "channel": "pvr|public-pr|skipped"}

7. Write local report

Save to articles/vuln-scan-${today}.md with sections for: repo metadata, scanner sources (ok/fail per tool), candidate count, confirmed findings with severity and channel, dedup note. Do not include exploit details for findings disclosed via PVR — redact file/line and link to the advisory ID instead.

8. Notify

Use ./notify. One paragraph. Lead with the verdict.

*Vuln Scanner — <repo>*
<N> confirmed findings (<severity-summary>).
Disclosed via: <PVR: advisory #123 | public PR #45 | skipped (no channel)>
Scanners: semgrep=<ok|fail>, trufflehog=<ok|fail>, osv=<ok|fail>.

If the audit was clean:

*Vuln Scanner — <repo>*
Clean audit. <M> candidates reviewed, 0 confirmed. Scanners: semgrep=ok, trufflehog=ok, osv=ok.

9. Log

Append to memory/logs/${today}.md:

### vuln-scanner
- Target: owner/repo (stars, language)
- Candidates: N | Confirmed: M
- Channels used: PVR (x), public PR (y), skipped (z)
- Scanner status: semgrep=ok trufflehog=ok osv=ok
- Advisory/PR links: [...]

Sandbox note

Scanner binaries (semgrep, trufflehog, osv-scanner, slither) are not pre-installed in the GitHub Actions sandbox, and outbound pip install / curl | sh downloads may be blocked. Operators should pre-cache them via scripts/prefetch-vuln-scanner.sh (runs before Claude starts, with full network access — see CLAUDE.md prefetch pattern). If any scanner binary is still missing at runtime, log VULN_SCANNER_SKIPPED: <tool> not available, record tool=fail in sources.txt, and continue with the remaining scanners rather than aborting the whole run.

General sandbox rules: use WebFetch as a fallback for any plain URL fetch. For anything requiring a token, use gh api (handles auth internally) or the pre-fetch/post-process pattern (see CLAUDE.md). An all-scanners-fail run must report error, not clean.

Environment variables

  • GH_TOKEN / GITHUB_TOKEN — required. Classic repo scope is sufficient, including private vulnerability reporting via the /reports endpoint (step 5b). repository_advisories:write is only needed to manage advisories on repos you own — it is not required to report to third-party repos, and its absence is not the reason a report fails (see step 5b for the real failure modes: PVR-disabled 403, or GitHub API 5xx).

Guidelines

  • Do no harm. If you can't route a finding through a safe channel, don't publish it.
  • One report per repo per run. Bundle related findings.
  • Read the code. A scanner hit alone is not a vulnerability.
  • Skip intentionally vulnerable repos (teaching tools, CTFs).
  • Don't scan the same repo twice in 30 days (memory/vuln-scanned.json).
  • Never post exploit chains publicly. PoCs go in the private advisory, not in a GitHub comment.
  • Be deferential in disclosure language — you're offering help, not grading homework.
  • Public PRs are only for dependency bumps addressing already-disclosed CVEs. Everything else is private.
  • All-scanners-failed ≠ clean. Report it as an error and do not publish anything.
Install via CLI
npx skills add https://github.com/aaronjmars/aeon --skill vuln-scanner
Repository Details
star Stars 517
call_split Forks 176
navigation Branch main
article Path SKILL.md
More from Creator