Enrichers overview
An enricher runs over the ChangeSet produced by the diff core and
adds risk-signal metadata to the rendered output without modifying the
ChangeSet itself. Each is independent, has its own opt-out flag, and
follows a best-effort contract: any failure (network, rate-limit,
upstream API change) is logged once to stderr and the diff renders
without that enricher’s findings.
Shipping enrichers
| Enricher | Source | Network? | Default | Opt-out flag | Calibration |
|---|---|---|---|---|---|
| OSV.dev CVE lookup | OSV.dev /v1/querybatch + /v1/vulns/{id} | yes | on | --no-osv | --cache-ttl-hours (v0.9.6) |
| EPSS | FIRST.org /api/v1/epss | yes | on | --no-epss | --cache-ttl-hours; --fail-on-epss <0.0–1.0> |
| CISA KEV | CISA known-exploited catalog | yes | on | --no-kev | --cache-ttl-hours; --fail-on kev |
| Typosquat | Embedded top-N lists, optional XDG cache | no | on | (none — pure compute) | --typosquat-similarity-threshold (v0.9.6) |
| Multi-major version jump | The diff itself | no | on | (none — pure compute) | (hard-coded MIN_MAJOR_DELTA = 2 — see chapter for rationale) |
| Maintainer age | GitHub REST /repos/.../contributors + /commits | yes | on | --no-maintainer-age | --young-maintainer-days (v0.9.6) |
| Registry metadata | npm / PyPI / crates.io public APIs | yes | on (v0.9+) | --no-registry | --recently-published-days; --cache-ttl-hours |
| License policy | SBOM licenses field + SPDX expression eval | no | on | (configured by allow/deny lists) | --allow-licenses, --deny-licenses, --allow-exception, --deny-exception |
| Plugins | External-process plugins (v0.9.6+) | varies | off (opt-in) | (don’t pass --plugin) | (per-plugin manifest) |
Best-effort contract
Every enricher that touches the network honors the same contract:
- Per-request timeout (15s for OSV, 15s for GitHub) so a misbehaving upstream can’t hang a CI job.
- Errors warn, never block. A failed enricher logs one line to stderr (the warning is the same key every time, so it dedupes reasonably) and the diff renders without that enricher’s contributions.
- Rate-limit awareness. OSV’s
/v1/querybatchis unauthenticated; the GitHub REST API honorsGITHUB_TOKENfor the 5000/hr cap. On a403 + X-RateLimit-Remaining: 0, the maintainer-age enricher returns whatever was already collected and warns once. - Per-component caching within a single run. Repeated
cs.addedentries from the same project (e.g. monorepo subpackages sharing a GitHub repo) don’t multiply HTTP requests.
Determinism
Each enricher’s output is structured into the Enrichment graph
(vulns: HashMap<...>, typosquats: Vec<...>, version_jumps: Vec<...>,
maintainer_age: Vec<...>). Renderers iterate these in deterministic
order — Vecs in their natural BTreeMap-derived order from the
ChangeSet, the vulns HashMap with its keys sorted before emission.
This is the contract that lets peter-evans/create-or-update-comment
upsert PR comments in place: identical inputs render to byte-identical
output, so the comment body is patched only when the diff genuinely
changes.
Why these signals?
The enricher set was chosen because each maps to a real, recent, high-impact incident class:
- OSV.dev CVE lookup: published advisories, the broadest signal.
- EPSS: probability of exploitation in next 30 days; dampens false-urgency on Critical-CVSS-but-low-exploitation advisories.
- CISA KEV: known-exploited; the highest-confidence “act now” filter.
- Typosquat: malicious packages mimicking popular ones (the
plain-crypto-jsaxios dropper, the PyPI campaigns 2024–2026). - Multi-major version jump: takeover swaps, namespace reuse.
- Maintainer age: long-game social-engineering campaigns (xz / Jia Tan).
- Registry metadata: recently-published, deprecated, maintainer-set-changed — the npm Shai-Hulud-style worm precursors.
- License policy: not a malicious-code signal but a policy gate that the same diff-time reviewer is best positioned to enforce.
For organizations with environment-specific rules outside this list, the v0.9.6 Plugins protocol lets you layer custom enrichers on top without forking bomdrift.