# Vuln Scan

> Systematic vulnerability scan across injection, auth, data exposure, and dependencies — traced by reading code, not grepping for keywords. Use when scanning for vulnerabilities, reviewing security, or validating threat models.

- Skill: `hashgraph-online-awesome-codex-plugins/vuln-scan` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hashgraph-online-awesome-codex-plugins/vuln-scan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hashgraph-online-awesome-codex-plugins/vuln-scan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: hashgraph-online (https://skillmd.com/u/hashgraph-online-awesome-codex-plugins)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/hashgraph-online-awesome-codex-plugins/vuln-scan

---


# Vuln Scan — Systematic Vulnerability Scanner

## Iron Law

Code you haven't traced from input to sink has vulnerabilities you haven't found. A grep hit is a lead, not a finding.

## Process

### Step 0: Load Engagement Context

Check for `.harness/engagement.md`. If present, load scope constraints — only scan in-scope paths and respect exclusions.

Check for `THREAT_MODEL.md` from a previous `/threat-model` run. If present, use its threat scenarios as scan targets. If absent, run full-surface scan.

### Step 1: Map Entry Points to Trust Boundaries

Enumerate every place untrusted data enters the system, by reading the code:

- **External → app**: HTTP handlers/routers, GraphQL resolvers, webhooks, file uploads, form fields, query params, headers
- **Process boundary**: CLI args, environment variables, IPC, deserialization of stored data (cache, DB rows written earlier, config files)
- **Client-supplied state**: cookies, JWT claims, API keys, referer/origin

For each entry point, note: what validation exists (yes/no/partial), and where the data flows next. This list is the scan's backbone — a scan that cannot name its entry points has not started.

### Step 2: Trace Data Flow to Sinks

From each entry point, follow the data by reading code until it reaches a dangerous operation, or dies (validated, parameterized, dropped). Dangerous sink classes:

| Sink class | Examples (non-exhaustive) |
|-----------|--------------------------|
| Command/code execution | shell invocation, `eval`-family, template engines with code modes, deserialization to live objects |
| Query construction | string-built SQL, raw-query escapes in any ORM (Knex `raw`, Prisma `$queryRaw`, SQLAlchemy `text()`, Sequelize `literal`, Django `extra`/`raw`, Rails `where("...")` with interpolation) |
| Filesystem | path joins with user input, upload destinations, archive extraction (zip-slip), file reads driven by request params |
| Web output | template rendering with non-auto-escaping engines, `innerHTML`-family, redirect targets, header values (CRLF) |
| Auth decisions | IDOR — object lookups keyed only on user-supplied ids without ownership checks; role checks done client-side or per-endpoint instead of per-object |

Read the surrounding code for every candidate sink. A sink that only receives validated/parameterized data is not a finding; a sink reachable from an entry point with no validation is.

### Step 3: Reason About Encoding and Context at Each Sink

For each reachable sink, ask what context the data lands in and whether the encoding matches:

- Interpolated into a SQL string? → parameterize, don't escape
- Interpolated into a shell string? → arg-array execution, don't quote
- Rendered into HTML/JS/CSS/URL? → each needs context-specific output encoding; auto-escaping templates cover HTML only when actually enabled for that template
- Joined into a path? → canonicalize, then verify containment inside the intended root
- Deserialized? → prefer format + type that cannot instantiate attacker-chosen types

### Step 4: Auth & Data-Exposure Pass

- **Default-deny**: routes/actions missing an auth middleware or check entirely — enumerate handlers and look for the gaps, not the checks
- **Object-level authorization**: for each handler that reads/writes a user-owned object, is ownership verified on THIS request?
- **Secrets**: hardcoded credentials vs. config/env references; secrets in logs, error messages, client bundles, API responses
- **Error leakage**: verbose errors/stack traces/config in production paths; error messages that distinguish "no such user" from "wrong password"

### Step 5: Dependency Pass

```bash
cargo audit 2>/dev/null || echo "cargo-audit not installed"   # Rust
npm audit 2>/dev/null || echo "npm audit not available"       # Node.js
```

For each advisory: is a fix available, and is the vulnerable code path actually reachable from this codebase's usage? Unreachable advisories are LOW/INFO, not silent drops. Also flag actively-used dependencies that are unmaintained (no release in ~2+ years, archived repo) even without a CVE.

### Grep as Accelerator, Not Oracle

Grep is fine for *candidate* sink locations (`raw(`, `exec`, `innerHTML`, `eval`, secret-ish identifiers) — use it to shortlist where to read. It misses framework-specific sinks, aliases, and data built across files. Never report a finding from a grep hit alone; never conclude "clean" because grep found nothing.

### Step 6: Produce Output

Write `VULN-FINDINGS.json`:

```json
{
  "scan_date": "ISO-8601",
  "scope": "full | incremental",
  "threat_model_ref": "THREAT_MODEL.md | null",
  "entry_points_traced": ["list every entry point examined — required for clean scans"],
  "findings": [
    {
      "id": "V1",
      "dimension": "injection | auth | exposure | dependency",
      "severity": "CRITICAL | HIGH | MEDIUM | LOW | INFO",
      "file": "path/to/file",
      "line": 42,
      "source": "untrusted-input origin (entry point)",
      "sink_chain": ["entry function", "...", "sink function"],
      "description": "what was found",
      "validated": true,
      "false_positive": false,
      "reachable": true,
      "mitigated": false,
      "threat_scenario": "T1 | null",
      "remediation": "one-line fix hint"
    }
  ],
  "summary": {
    "total": 10,
    "critical": 1,
    "high": 3,
    "medium": 4,
    "low": 2,
    "false_positives": 0
  }
}
```

### Step 7: Feed into Triage

After producing findings, suggest:
**"Run `/triage` to validate findings with adversarial review."**

## Anti-Rationalization

| Excuse | Rebuttal | What to do instead |
|--------|----------|-------------------|
| "It's an internal tool / not user-facing" | Internal boundaries are attack surfaces — lateral movement starts inside. | Trace internal entry points with the same rigor. |
| "The framework sanitizes automatically" | Auto-escaping and parameterization have opt-outs, raw escapes, and edges; business logic is framework-agnostic. | Find the raw/dangerous escapes and read them. |
| "We'll add validation later" | Later never arrives for paths that already work. | Finding = now; remediation line goes in the output. |
| "Grep found nothing, so we're clean" | Grep misses aliased and framework-specific sinks by construction. | A clean claim requires traced entry points, not an empty grep. |
| "Dependencies are vetted" | Transitive dependencies aren't. | Run the dependency pass every time the lockfile changes. |

## Evidence Required

- [ ] Every finding cites file:line + the untrusted-input origin + the sink call chain
- [ ] Each finding validated: reachable, not mitigated, severity confirmed
- [ ] A clean (or low-finding) scan lists `entry_points_traced` — absence of evidence is not evidence of absence
- [ ] All 4 dimensions completed (injection, auth, exposure, dependency)
- [ ] VULN-FINDINGS.json written with summary
- [ ] If THREAT_MODEL.md exists: each threat scenario mapped to findings
- [ ] No CRITICAL/HIGH finding dismissed without explicit justification

## Red Flags

- Reporting grep hits without reading the surrounding code
- Skipping framework-specific sink types (ORM raw escapes, non-auto-escaping templates)
- Claiming clean without naming which entry points were traced
- Scanning only changed files when full-surface scan was requested
- Marking findings as false positives without a traced reason
- VULN-FINDINGS.json with zero findings on a non-trivial codebase — the scan was likely incomplete, not the codebase clean

