Hex Dependency Audit skill

Audit Hex deps for supply-chain security risk — bidi chars, compile-time exec, maintainer changes, typosquats, CVEs.

by oliver-kriska·MIT license·★ 560 Stars on the repo·GitHub ↗

Use now

Files of Hex Dependency Audit

oliver-kriska/main1 file shown
SKILL.md
Show the full text185 lines

Hex Dependency Audit

Non-mutating supply-chain audit for Hex packages. Runs an 8-rule MVP catalogue against changed packages, enriches with Hex API metadata, wraps existing tools (mix hex.audit, mix_audit, OSV-Scanner), and emits a triage table.

When to Use

  • After mix deps.update or mix deps.get brought in new versions
  • On PRs that touch mix.lock (pre-merge gate)
  • Before manually updating a single package (--preview <pkg>)
  • When investigating a dependency you don't recognize

Iron Laws

  1. NEVER claim a diff is clean without inspecting it. Run all 8 rules on the unpacked NEW tarball. "Looks fine" without a tool run is a false pass. Always write .claude/deps-audit/last-run.json — its absence is evidence the audit didn't actually run.
  2. NEVER install mix_audit / osv-scanner — even if asked. Detect, warn with install instructions, skip cleanly if missing. If the user says "install it," respond with the install command (e.g., mix deps.add mix_audit --only dev) and do not execute it. The audit skill is non-mutating; mix.exs / mix.lock are off-limits regardless of consent.
  3. NEVER promote a finding to BLOCK without rule citation. Every finding shows rule_id, severity, file:line, snippet, message. No handwaving.
  4. NEVER fetch from Hex API without rate-limiting. Cap at 5 req/sec. Cache metadata 7 days, top-500 list 1 day.
  5. NEVER run the audit on already-committed lock changes silently — tell the user which mode (A/B/C) is active and which (old, new) pairs resolved.
  6. LLM triage only above threshold. Native rules + Semgrep + YARA are deterministic. The hex-deps-triager agent runs only when score

    10 (1 BLOCK or 3+ WARNs), and its verdicts are advisory — never auto-suppress a finding without human review.

Operating Modes

Mode Trigger Old source New source
B (default) /phx:deps-audit git show HEAD:mix.lock working mix.lock
C (PR) /phx:deps-audit --base main git show <ref>:mix.lock working mix.lock
A (preview) /phx:deps-audit --preview httpoison locked version Hex API latest

See ${CLAUDE_SKILL_DIR}/references/operating-modes.md for full resolver logic.

Execution Flow

Default = full 8-rule scan with streaming progress. --quick opts out to CVE + retirement only. See ${CLAUDE_SKILL_DIR}/references/execution-flow.md.

Step 1: Resolve the diff

Parse the mix.lock Erlang term format for both old and new sources. Emit a list of {pkg, old_version, new_version} tuples. Surface new-only and removed-only packages separately (a removed package is not audited; a brand-new package gets old_version = nil and skips diff-only rules).

See ${CLAUDE_SKILL_DIR}/references/diff-resolver.md for shell + mix run -e snippets per mode and the JSON output contract.

Step 2: Fetch tarballs (per-run tmpdir)

For each (pkg, old, new):

mix hex.package fetch <pkg> <old> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<old>/
mix hex.package fetch <pkg> <new> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<new>/

All ephemeral artifacts live under ${AUDIT_TMPDIR} (driver-owned, removed on exit). See ${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md and ${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md.

Step 3: Run the 8 MVP rules on each NEW tarball
# Rule Sev Method
1 Bidi Unicode control chars in .ex/.exs/.erl BLOCK grep
2 Code.eval_* / :erlang.apply with non-literal MFA at module scope BLOCK AST (Sourceror or regex+scope)
3 System.cmd / :os.cmd / Port.open at compile time BLOCK AST
4 :erlang.binary_to_term/1 on literal without :safe BLOCK AST
5 New :git/:path dep in mix.exs (vs old) BLOCK AST diff
6 Maintainer change between versions BLOCK Hex API
7 Base64 blobs >256 chars outside priv/static/, test/fixtures/, assets/ WARN regex
8 Levenshtein ≤2 from top-500 + download delta >1000× BLOCK Hex API + fuzzy

Full catalogue (35 rules, MVP marked) in ${CLAUDE_SKILL_DIR}/references/heuristics.md. Bash + mix run -e implementations for all 8 MVP rules in ${CLAUDE_SKILL_DIR}/references/rules-impl.md (single-pass NEW + diff rules + Hex API rules, with run_all_rules master loop).

Step 4: External tool wrappers (parallel)
  • mix hex.audit — retired-package check, always available
  • mix_audit — CVE check via GHSA, if installed (else warn + skip; do NOT install)
  • osv-scanner — CVE check via OSV.dev, if installed (else warn + skip; do NOT install)

See ${CLAUDE_SKILL_DIR}/references/external-tools.md for detection, output parsing, and severity mapping per tool.

Step 5: Hex API enrichment (per package)
  • GET /api/packages/:name — owners, downloads, inserted_at
  • GET /api/packages/:name/releases/:version — per-release publisher
  • Compute: days_since_publish, owner_age_days, download_velocity

Cap at 5 req/sec. Per-run cache under ${AUDIT_TMPDIR}/hex-api/. See ${CLAUDE_SKILL_DIR}/references/hex-api.md for endpoint contracts, caching strategy, Rule 6/8 detection, and Levenshtein implementation.

Step 5.5: Apply hex_vet.exs ledger (if present)

If hex_vet.exs exists at project root, vetted-version findings are downgraded to INFO. Unvetted versions retain their severity. Lock-vs-ledger disagreement: lock wins. See the deps-vet skill's hex-vet schema doc for the "Lock-vs-ledger disagreement" section.

Use /phx:deps-vet <pkg> <version> (separate skill) to add entries.

Step 5.7: Differential subtract

When run with DIFFERENTIAL=1 (default), findings that existed in the OLD tarball are downgraded to INFO. Net-new signals reach the renderer at full severity. See ${CLAUDE_SKILL_DIR}/references/differential.md.

Step 5.8: LLM triage (when score > threshold)

For packages where the aggregate score exceeds 10, the hex-deps-triager sonnet agent reads finding + diff windows and produces structured verdicts (confidence, verdict, rationale, fp_reasons[]). A context-supervisor consolidates verdicts across packages into triage/consolidated.md. Main skill reads only the consolidated file.

See ${CLAUDE_SKILL_DIR}/references/llm-triage.md.

Step 6: Score & render

Per-package weighted sum: BLOCK = 10, WARN = 3, INFO = 1. Risk band: 0 clean · 1–5 low · 6–15 medium · 16+ high.

Output:

  1. Stdout: markdown table — pkg | old → new | risk | findings | diff.hex.pm | maintainer-change plus a per-package detail section for any non-clean row.
  2. Sidecar (MANDATORY): Write .claude/deps-audit/last-run.json. The Phase 3 gate reads this; an audit that doesn't write it is a no-op for the gate. Always emit, even on clean runs.

--json flag emits JSON to stdout instead of markdown. See ${CLAUDE_SKILL_DIR}/references/output-renderer.md for table format, sidecar schema, exit-code rubric, and --quiet mode.

Out of scope / Phase 3 surface

  • NEVER modify mix.lock, mix.exs, or any project file (non-mutating)
  • NEVER auto-install missing tools (warn + skip)
  • Gate mix deps.{get,update,compile} via deps-audit-gate.sh. See ${CLAUDE_SKILL_DIR}/references/hook.md.
  • Prompt for /phx:compound after BLOCK findings — corpus self-feeds.
  • Emit SARIF 2.1.0 via --sarif <path> and gate CI via --ci.

References

  • ${CLAUDE_SKILL_DIR}/references/heuristics.md — full 35-rule catalogue
  • ${CLAUDE_SKILL_DIR}/references/rules-impl.md — bash + mix run -e for the 8 MVP rules
  • ${CLAUDE_SKILL_DIR}/references/operating-modes.md — Mode A/B/C resolver
  • ${CLAUDE_SKILL_DIR}/references/diff-resolver.md — shell snippets, lock parser
  • ${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md — fetch wrapper, parallel cap, cache prune
  • ${CLAUDE_SKILL_DIR}/references/external-tools.md — mix_audit, osv-scanner wrappers
  • ${CLAUDE_SKILL_DIR}/references/hex-api.md — endpoint contracts, rate limit, Rule 6/8
  • ${CLAUDE_SKILL_DIR}/references/output-renderer.md — markdown, JSON v1, exit codes, SARIF
  • ${CLAUDE_SKILL_DIR}/references/testing.md — smoke runner, fixture matrix
  • ${CLAUDE_SKILL_DIR}/references/differential.md / llm-triage.md — Phase 2 NDJSON subtract + triager
  • ${CLAUDE_SKILL_DIR}/references/semgrep.md / yara.md — Phase 2 precision layers (soft deps)
  • ${CLAUDE_SKILL_DIR}/references/cassettes.md / sarif.md / hook.md / ci-integration.md — Phase 3 surface
  • ${CLAUDE_SKILL_DIR}/references/trusted-publishers.md / skill-checklist.md — upstream + eval
  • ${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md — Phase 5 per-run ephemeral storage contract
  • ${CLAUDE_SKILL_DIR}/references/execution-flow.md / differential-cve.md — Phase 5 default scan + CVE diff
1---
2name: deps-audit
3description: Audit Hex deps for supply-chain security risk — bidi chars, compile-time exec, maintainer changes, typosquats, CVEs. Use after mix deps.update, when checking if a package upgrade is safe, or reviewing mix.lock PR diffs.
4effort: medium
5argument-hint: "[--base <ref> | --preview [pkg...]] [--quick] [--json] [--sarif <path>] [--ci] [--strict] [--no-differential] [--no-llm | --llm] [--trace]"
6---
7 
8# Hex Dependency Audit
9 
10Non-mutating supply-chain audit for Hex packages. Runs an 8-rule MVP catalogue
11against changed packages, enriches with Hex API metadata, wraps existing tools
12(`mix hex.audit`, `mix_audit`, OSV-Scanner), and emits a triage table.
13 
14## When to Use
15 
16- After `mix deps.update` or `mix deps.get` brought in new versions
17- On PRs that touch `mix.lock` (pre-merge gate)
18- Before manually updating a single package (`--preview <pkg>`)
19- When investigating a dependency you don't recognize
20 
21## Iron Laws
22 
231. **NEVER claim a diff is clean without inspecting it.** Run all 8 rules
24 on the unpacked NEW tarball. "Looks fine" without a tool run is a false
25 pass. **Always write `.claude/deps-audit/last-run.json`** — its absence
26 is evidence the audit didn't actually run.
272. **NEVER install `mix_audit` / `osv-scanner` — even if asked.** Detect,
28 warn with install instructions, skip cleanly if missing. If the user
29 says "install it," respond with the install command (e.g.,
30 `mix deps.add mix_audit --only dev`) and **do not execute it**. The
31 audit skill is non-mutating; `mix.exs` / `mix.lock` are off-limits
32 regardless of consent.
333. **NEVER promote a finding to BLOCK without rule citation.** Every finding
34 shows `rule_id`, `severity`, `file:line`, `snippet`, `message`. No
35 handwaving.
364. **NEVER fetch from Hex API without rate-limiting.** Cap at 5 req/sec.
37 Cache metadata 7 days, top-500 list 1 day.
385. **NEVER run the audit on already-committed lock changes silently** —
39 tell the user which mode (A/B/C) is active and which `(old, new)` pairs
40 resolved.
416. **LLM triage only above threshold.** Native rules + Semgrep + YARA
42 are deterministic. The `hex-deps-triager` agent runs only when score
43 > 10 (1 BLOCK or 3+ WARNs), and its verdicts are advisory — never
44 auto-suppress a finding without human review.
45 
46## Operating Modes
47 
48| Mode | Trigger | Old source | New source |
49|------|---------|-----------|-----------|
50| **B** (default) | `/phx:deps-audit` | `git show HEAD:mix.lock` | working `mix.lock` |
51| **C** (PR) | `/phx:deps-audit --base main` | `git show <ref>:mix.lock` | working `mix.lock` |
52| **A** (preview) | `/phx:deps-audit --preview httpoison` | locked version | Hex API latest |
53 
54See `${CLAUDE_SKILL_DIR}/references/operating-modes.md` for full resolver logic.
55 
56## Execution Flow
57 
58Default = full 8-rule scan with streaming progress. `--quick` opts out
59to CVE + retirement only. See `${CLAUDE_SKILL_DIR}/references/execution-flow.md`.
60 
61### Step 1: Resolve the diff
62 
63Parse the `mix.lock` Erlang term format for both old and new sources. Emit a
64list of `{pkg, old_version, new_version}` tuples. Surface
65new-only and removed-only packages separately (a removed package is not
66audited; a brand-new package gets `old_version = nil` and skips diff-only
67rules).
68 
69See `${CLAUDE_SKILL_DIR}/references/diff-resolver.md` for shell + `mix run -e` snippets per mode and the JSON output contract.
70 
71### Step 2: Fetch tarballs (per-run tmpdir)
72 
73For each `(pkg, old, new)`:
74 
75```
76mix hex.package fetch <pkg> <old> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<old>/
77mix hex.package fetch <pkg> <new> --unpack -o ${AUDIT_TMPDIR}/tarballs/<pkg>/<new>/
78```
79 
80All ephemeral artifacts live under `${AUDIT_TMPDIR}` (driver-owned, removed
81on exit). See `${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md` and
82`${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md`.
83 
84### Step 3: Run the 8 MVP rules on each NEW tarball
85 
86| # | Rule | Sev | Method |
87|---|------|-----|--------|
88| 1 | Bidi Unicode control chars in `.ex`/`.exs`/`.erl` | BLOCK | grep |
89| 2 | `Code.eval_*` / `:erlang.apply` with non-literal MFA at module scope | BLOCK | AST (Sourceror or regex+scope) |
90| 3 | `System.cmd` / `:os.cmd` / `Port.open` at compile time | BLOCK | AST |
91| 4 | `:erlang.binary_to_term/1` on literal without `:safe` | BLOCK | AST |
92| 5 | New `:git`/`:path` dep in `mix.exs` (vs old) | BLOCK | AST diff |
93| 6 | Maintainer change between versions | BLOCK | Hex API |
94| 7 | Base64 blobs >256 chars outside `priv/static/`, `test/fixtures/`, `assets/` | WARN | regex |
95| 8 | Levenshtein ≤2 from top-500 + download delta >1000× | BLOCK | Hex API + fuzzy |
96 
97Full catalogue (35 rules, MVP marked) in `${CLAUDE_SKILL_DIR}/references/heuristics.md`.
98Bash + `mix run -e` implementations for all 8 MVP rules in
99`${CLAUDE_SKILL_DIR}/references/rules-impl.md` (single-pass NEW + diff rules +
100Hex API rules, with `run_all_rules` master loop).
101 
102### Step 4: External tool wrappers (parallel)
103 
104- `mix hex.audit` — retired-package check, always available
105- `mix_audit` — CVE check via GHSA, if installed (else warn + skip; do NOT install)
106- `osv-scanner` — CVE check via OSV.dev, if installed (else warn + skip; do NOT install)
107 
108See `${CLAUDE_SKILL_DIR}/references/external-tools.md` for detection, output parsing, and severity mapping per tool.
109 
110### Step 5: Hex API enrichment (per package)
111 
112- `GET /api/packages/:name` — owners, downloads, inserted_at
113- `GET /api/packages/:name/releases/:version` — per-release publisher
114- Compute: `days_since_publish`, `owner_age_days`, `download_velocity`
115 
116Cap at 5 req/sec. Per-run cache under `${AUDIT_TMPDIR}/hex-api/`.
117See `${CLAUDE_SKILL_DIR}/references/hex-api.md` for endpoint contracts,
118caching strategy, Rule 6/8 detection, and Levenshtein implementation.
119 
120### Step 5.5: Apply `hex_vet.exs` ledger (if present)
121 
122If `hex_vet.exs` exists at project root, vetted-version findings are
123**downgraded to INFO**. Unvetted versions retain their severity.
124Lock-vs-ledger disagreement: lock wins. See the deps-vet skill's
125hex-vet schema doc for the "Lock-vs-ledger disagreement" section.
126 
127Use `/phx:deps-vet <pkg> <version>` (separate skill) to add entries.
128 
129### Step 5.7: Differential subtract
130 
131When run with `DIFFERENTIAL=1` (default), findings that existed in the
132OLD tarball are downgraded to INFO. Net-new signals reach the renderer
133at full severity. See `${CLAUDE_SKILL_DIR}/references/differential.md`.
134 
135### Step 5.8: LLM triage (when score > threshold)
136 
137For packages where the aggregate score exceeds 10, the
138`hex-deps-triager` sonnet agent reads finding + diff windows and
139produces structured verdicts (`confidence`, `verdict`, `rationale`,
140`fp_reasons[]`). A `context-supervisor` consolidates verdicts
141across packages into `triage/consolidated.md`. Main skill reads only
142the consolidated file.
143 
144See `${CLAUDE_SKILL_DIR}/references/llm-triage.md`.
145 
146### Step 6: Score & render
147 
148Per-package weighted sum: BLOCK = 10, WARN = 3, INFO = 1.
149Risk band: 0 clean · 1–5 low · 6–15 medium · 16+ high.
150 
151Output:
152 
1531. **Stdout:** markdown table — `pkg | old → new | risk | findings | diff.hex.pm | maintainer-change` plus a per-package detail section for any non-clean row.
1542. **Sidecar (MANDATORY):** Write `.claude/deps-audit/last-run.json`. The Phase 3 gate reads this; an audit that doesn't write it is a no-op for the gate. Always emit, even on clean runs.
155 
156`--json` flag emits JSON to stdout instead of markdown. See
157`${CLAUDE_SKILL_DIR}/references/output-renderer.md` for table format,
158sidecar schema, exit-code rubric, and `--quiet` mode.
159 
160## Out of scope / Phase 3 surface
161 
162- **NEVER modify** `mix.lock`, `mix.exs`, or any project file (non-mutating)
163- **NEVER auto-install** missing tools (warn + skip)
164- **Gate** `mix deps.{get,update,compile}` via `deps-audit-gate.sh`. See `${CLAUDE_SKILL_DIR}/references/hook.md`.
165- **Prompt** for `/phx:compound` after BLOCK findings — corpus self-feeds.
166- **Emit** SARIF 2.1.0 via `--sarif <path>` and gate CI via `--ci`.
167 
168## References
169 
170- `${CLAUDE_SKILL_DIR}/references/heuristics.md` — full 35-rule catalogue
171- `${CLAUDE_SKILL_DIR}/references/rules-impl.md` — bash + `mix run -e` for the 8 MVP rules
172- `${CLAUDE_SKILL_DIR}/references/operating-modes.md` — Mode A/B/C resolver
173- `${CLAUDE_SKILL_DIR}/references/diff-resolver.md` — shell snippets, lock parser
174- `${CLAUDE_SKILL_DIR}/references/tarball-fetcher.md` — fetch wrapper, parallel cap, cache prune
175- `${CLAUDE_SKILL_DIR}/references/external-tools.md` — `mix_audit`, `osv-scanner` wrappers
176- `${CLAUDE_SKILL_DIR}/references/hex-api.md` — endpoint contracts, rate limit, Rule 6/8
177- `${CLAUDE_SKILL_DIR}/references/output-renderer.md` — markdown, JSON v1, exit codes, SARIF
178- `${CLAUDE_SKILL_DIR}/references/testing.md` — smoke runner, fixture matrix
179- `${CLAUDE_SKILL_DIR}/references/differential.md` / `llm-triage.md` — Phase 2 NDJSON subtract + triager
180- `${CLAUDE_SKILL_DIR}/references/semgrep.md` / `yara.md` — Phase 2 precision layers (soft deps)
181- `${CLAUDE_SKILL_DIR}/references/cassettes.md` / `sarif.md` / `hook.md` / `ci-integration.md` — Phase 3 surface
182- `${CLAUDE_SKILL_DIR}/references/trusted-publishers.md` / `skill-checklist.md` — upstream + eval
183- `${CLAUDE_SKILL_DIR}/references/audit-tmpdir.md` — Phase 5 per-run ephemeral storage contract
184- `${CLAUDE_SKILL_DIR}/references/execution-flow.md` / `differential-cve.md` — Phase 5 default scan + CVE diff
185 

Discussion