feeds — design
Class:
design· Function axis:f0the ingest fold ·f1per-source parsers ·f2version grammars ·f3enrichment & suppression ·f4package behaviour features (A23). Strict: only the shape we run. The why is infeeds-reasoning.md; the system view indesign.md.
f0 — the ingest fold
feeds turns a set of offline feed documents into a deduplicated Vec< scan::Advisory> carrying ranges, ready for the warehouse or a direct scan. The pipeline (as wired in main.rs:cmd_import): parse each source → merge_by_alias (scan.rs:224, dedup CVE↔GHSA↔RUSTSEC into one row via alias union / range union / max severity / OR-of-kev) → optional NVD enrichment → feeds::enrich (EPSS/KEV) → append.
An Advisory is range-native: ranges: Vec<RangeEvent> is the source of truth, affected_versions an optional pre-expanded cache, and Advisory::matches (scan.rs:183) is withdrawn ⇒ false else versions::affected_by(ecosystem, &ranges, version) OR the exact-list fallback.
Reuse: feeds is leaf-of-leaf — pure parse, no warehouse/network. It produces the Advisorys that verdict matches and warehouse round-trips.
f1 — per-source parsers
| source | entry | file:symbol | produces |
| OSV / GHSA (single) | parse_osv |
osv.rs:191 |
Vec<Advisory> (ranges + aliases + withdrawn) |
| OSV batch | parse_osv_batch |
osv.rs:197 |
Vec<Advisory> |
| OSV (auto single/batch) | parse_doc |
osv.rs:206 |
Vec<Advisory> |
| OSV dir → merged | parse_and_merge |
osv.rs:221 |
deduped Vec<Advisory> |
| OSV range specs | parse_osv_ranges |
osv.rs:244 |
Vec<AffectedRange> |
RUSTSEC .md (feature rustsec) |
parse_rustsec_md |
osv.rs:642 |
Option<Advisory> |
| RUSTSEC range inversion | rustsec_affected_ranges |
osv.rs:391 |
Vec<RangeEvent> (patched/unaffected → affected) |
| NVD CVE JSON 2.0 | parse_nvd |
nvd.rs:183 |
Vec<NvdEntry> (CVSS vector + CWE + severity) |
| NVD CPE backfill | parse_nvd_advisories |
nvd.rs:209 |
standalone Advisorys (ecosystem = "cpe") |
| CISA KEV | parse_kev_json |
feeds.rs:62 |
HashSet<String> (exploited CVE ids) |
| ENISA EUVD (exploited) | parse_euvd_exploited |
feeds.rs |
HashSet<String> (EU-authority exploitedSince CVE ids → folded into kev) |
| VulnCheck KEV | parse_vulncheck_kev |
feeds.rs |
HashSet<String> — a superset of CISA KEV (adds exploited CVEs CISA has not yet catalogued, from VulnCheck's own vulncheck_reported_exploitation); a KEV-grade "exploited in the wild" signal from a third authority (US-gov / EU / commercial-intel), folded into the SAME kev escalation set. Document is {data:[{cve:[…]}]} — an entry's cve is a LIST; lower-case cve-… canonicalized to CVE-…. --vulncheck-kev <file>; provided-but-0 ⇒ hard error (empty-is-RED). LIVE download needs a FREE VulnCheck token — this OFFLINE parser is token-free |
| FIRST EPSS | parse_epss_csv |
feeds.rs:29 |
HashMap<String, f64> |
| OpenSSF Malicious | parse_malicious |
feeds.rs:84 |
Critical MAL-* Advisorys |
| GitLab gemnasium | parse_gemnasium_yaml |
gitlab.rs |
Advisory (flat-YAML scan + affected_range grammar → ranges; source=gitlab) |
| PoC-in-GitHub | parse_poc_in_github |
feeds.rs |
HashSet<String> (CVE ids with a public PoC) |
| Metasploit modules | parse_metasploit_cves |
feeds.rs |
HashSet<String> (CVE ids with a weaponized module) |
| Nuclei templates | parse_nuclei_cves |
feeds.rs |
HashSet<String> (CVE ids with a detection/exploit template; NDJSON) |
Exploit-DB files_exploits.csv |
parse_exploitdb_csv |
feeds.rs |
HashSet<String> (CVE ids with a published, downloadable exploit — the strongest public-exploit signal). codes column located from the header row, never a fixed offset (Exploit-DB has added columns over the years, so a hardcoded index silently reads the wrong field on a newer mirror); fields split with the SHARED RFC-4180 splitter ioc::split_csv (the description column is quoted and comma-heavy) rather than a second CSV parser (L5). No codes column ⇒ Err (wrong file / HTML error page), never a fake-empty set |
| crates.io yanks | parse_crates_yanks |
registry.rs |
Low MODGUNN-YANK-cargo-* Advisorys (exact-version) |
| npm deprecations | parse_npm_deprecations |
registry.rs |
Low MODGUNN-DEPRECATED-npm-* Advisorys (exact-version) |
| PyPI yanks (PEP 592) | parse_pypi_yanks |
registry.rs |
Low MODGUNN-YANK-pypi-* Advisorys (release fully-yanked → exact-version) |
| Go retractions | parse_go_retractions |
registry.rs |
Low MODGUNN-YANK-go-* Advisorys (go.mod retract; single → exact, [lo,hi] → inclusive range) |
| Sonatype OSS-Index | parse_ossindex_report |
registry.rs |
Vec<Advisory> — the purl-keyed cross-ecosystem component report, one row per (component, vulnerability) at the component's exact version. Coords come from the ONE purl grammar (sbom::parse_purl, the inverse of sbom::purl), so a scoped npm (pkg:npm/%40scope/name) and a maven group/artifact land on exactly the coords the join keys on. A CVE-carrying finding is keyed BY its CVE (the Sonatype UUID rides as an alias) so merge_by_alias folds it into the row OSV/NVD already produced instead of double-reporting one vulnerability; only a Sonatype-only finding gets MODGUNN-OSSINDEX-<uuid>. Severity from the numeric cvssScore through the SHARED osv::severity_from_base; --ossindex |
| ecosyste.ms status | parse_ecosystems_package |
registry.rs |
Low MODGUNN-ECOSYSTEMS-{REMOVED,DEPRECATED}-* Advisorys — cross-ecosystem PACKAGE-level pulled/deprecated status (all-versions); --ecosystems, active/null status → None (real zero) |
| libraries.io status | parse_librariesio_project |
registry.rs |
Low MODGUNN-LIBRARIESIO-{REMOVED,DEPRECATED,UNMAINTAINED,HIDDEN}-* Advisorys — the elder cross-ecosystem index (ecosyste.ms is its open successor); same all-versions "stop depending on this" row from status OR a non-empty deprecation_reason. Platform label case-folded through the shared ecosystems_eco (L5). --librariesio, active/null → None (real zero). LIVE API needs a FREE registered api_key; this OFFLINE parser is key-free |
| socket.dev alerts ⚠SCAFFOLD | parse_socket_alerts |
registry.rs |
Vec<Advisory> — per-package-version supply-chain BEHAVIOUR findings (malware / install-scripts / obfuscation / telemetry), keyed on the purl type→ecosystem; worst-alert severity, any malware alert ⇒ Critical. Accepts a single object, a batch […], or {packages:[…]}. --socket. PENDING: the live Socket API is PAID — parser follows the public alert schema but the response envelope is unverified against a live feed (see §D) |
| GUAC CertifyVuln ⚠SCAFFOLD | parse_guac_certify_vuln |
registry.rs |
Vec<Advisory> — package↔vuln certifications from GUAC's graph (GraphQL CertifyVuln), walked out of the package trie (type→namespaces→names→versions); keyed BY the vuln id (CVE/GHSA canonicalized upper) so merge_by_alias folds it into the OSV/NVD row; novuln (certified NOT vulnerable) skipped. --guac. PENDING: needs a running GUAC GraphQL endpoint with data loaded — schema unvalidated against a live instance here (see §D) |
| CSAF VEX | parse_csaf_vex / collect_suppressions |
vex.rs |
suppressed CVE id set (an id marked known_affected anywhere is never suppressed — product-scoped safety) |
| Go vuln DB symbols | parse_osv_vuln_symbols |
osv.rs |
VulnSymbol{id,eco,package,symbol} (OSV ecosystem_specific.imports → the A8/b1 reachability sinks; vuln_symbols table) |
| ClearlyDefined | parse_clearlydefined / parse_clearlydefined_batch |
spdx.rs |
ClearlyDefined{ecosystem,name,version,declared,discovered,unknown_files,score} — the declared-vs-DISCOVERED license comparison, the compliance leg no vulnerability feed carries. Discovered ids are read from every facet (core/tests/data/…, because a GPL file in a non-core facet still ships) and split into leaf ids through the SHARED SPDX expression parser, so MIT OR Apache-2.0 genuinely covers a discovered Apache-2.0. .findings() → High undeclared-license / Medium no-declaration / Low unlicensed-files / Info low-score. Verb: warehouse license-compliance |
| Sigstore Rekor | parse_rekor_entries |
provenance.rs |
RekorEntry{uuid,log_index,integrated_time,kind,artifact_sha256,signer_identity,has_inclusion_proof,has_signed_timestamp} — the transparency-log leg. hashedrekord + intoto/dsse bodies, base64-decoded by a pure-Rust decoder (no base64 dep in the lean gate). attestation() is deliberately slsa_level = 0 (log presence is not a build claim); an entry with no inclusion proof is graded High, never trusted. Verb: warehouse rekor |
| SPDX license list | parse_spdx_license_list |
spdx.rs |
SpdxLicenseList{known,deprecated} — the license/compliance leg: reference data (no table), drives license-lint to flag deprecated/unknown SPDX ids via the shared expression parser |
| CAPEC → ATT&CK | parse_capec_xml |
capec.rs |
CweAttackMap (CAPEC XML Related_Weaknesses↔Taxonomy_Mappings → CWE→ATT&CK technique) — the identity/mapping leg: reference data (no table), drives attack-map; techniques_for_cwes tags a finding's CWEs with adversary techniques |
AffectedRange is the intermediate { id, ecosystem, package, severity, summary, events: Vec<RangeEvent> } — one per OSV range object, not per affected entry; expand_advisories (osv.rs:274) expands ranges to exact Advisorys against a known version list when a cache is wanted.
f2 — version grammars (versions.rs)
RangeEvent (versions.rs:26) is the OSV range vocabulary (Introduced/Fixed/LastAffected) plus one non-OSV event, IntroducedExclusive — a strictly greater than lower bound. OSV cannot express it, but NVD's versionStartExcluding means exactly that, and folding it onto the inclusive Introduced would flag the boundary version itself as vulnerable (a false positive on the version the vendor called clean). These functions drive matching:
compare(ecosystem, a, b)— per-ecosystem version ordering: semver (cargo/npm/go/nuget/composer) / PEP440-lite / maven-lite / gem (RubyGems segments,1.0.0.rc1<1.0.0) / loose, plus distro grammars dpkg (deb_cmp: epoch/upstream/revision,~ordering), rpm (rpm_cmp: rpmvercmp) and apk (apk_cmp: Alpine suffix ranks). The loose fallback (onlycpe+ unknown ecosystems now) is NOT prerelease-aware — it ranks1.5.0-rc.1above1.5.0.affected_by(ecosystem, events, version)— the scan-time predicate for one range: isversioninside its[introduced, fixed)/[introduced, last_affected]window under the ecosystem's comparator — or(introduced, …when the lower bound is anIntroducedExclusive.affected_by_any(ecosystem, ranges, version)— the OR over an advisory's range objects.Advisory::rangesisVec<Vec<RangeEvent>>(one inner list per OSV range object) precisely so ranges union instead of cancelling; GIT ranges are dropped at import (commit hashes are not versions).decode_range_sets— warehouserangescolumn decode; a pre-v3 flat event array reads back as a single range object.expand_range(versions.rs:87) — the lazy exact-list expander.
normalize_ecosystem maps OSV distro labels (Debian/Ubuntu→deb, Alpine→apk, RedHat/Rocky/Alma/SUSE→rpm) so OSV.dev's distro exports match with the right comparator.
f3 — enrichment & suppression
enrich(advisories, epss, kev)(feeds.rs:70) folds EPSS scores and the KEV flag onto advisories matched by id OR any alias (same asenrich_with_nvd): the EPSS/KEV feeds are keyed by CVE, butmerge_by_aliaskeeps the first-seen record as primary, so an actively-exploited CVE whose merged advisory is GHSA/RUSTSEC-primary carries the CVE only in its aliases — matching on aliases keeps the KEV→Critical / EPSS→High escalation firing.enrich_exploited(advisories, exploited)lifts an advisory whose id OR alias has a public PoC (PoC-in-GitHub) to at leastHighand prepends the stable[public-exploit]marker to its summary (idempotent; never lowersCritical). Schema-free — the lift + marker ride the existingseverity/summarywarehouse columns, so the signal round-trips and the baddie-detection join ranks exploit-backed findings above version-only ones. A public PoC floors atHigh(weaker evidence than KEV's confirmed-in-the-wild → Critical).enrich_with_nvd(advisories, entries)(nvd.rs:255) sets CVSS vector + CWE and lifts severity, matched by id OR alias.collect_suppressions(docs)(vex.rs:51) unions CSAFknown_not_affectedids — the set a scan drops (warehouse-scan--vex, orverdict'svex_not_affected).- Offline CVSS v3 scoring (
osv::cvss_base_score): an OSV/RUSTSEC advisory whose only severity signal is a CVSS v3.0/3.1 vector (no numeric score, nodatabase_specificlabel — very common) is scored by the spec's deterministic base-score formula and bucketed via the sharedseverity_from_base, instead of silently under-rating to Medium (OSV) / High (rustsec). Both the CVSS v3.0/3.1 vector (cvss_v3_base) and the older CVSS v2 vector (cvss_v2_base, detected by itsAu:metric) are computed; validated against FIRST's published reference scores. Only a v4 vector (a far more involved formula) still falls back to the label/Medium default.
f4 — package behaviour features (A23)
Where f1–f3 import advisory feeds keyed by a known vulnerability, behavior scores a package that has no CVE yet — the malicious-package detection axis. It is the read-side feature extractor, the same parse_* shape: offline, no code executed — it reads a package's published metadata text and folds the install-time code into the A23 feature vector academic detectors use (Socket H1-2025; arXiv 2412.05259 / 2309.02637).
Four ecosystem front-ends, one folder. parse_npm_package reads an npm package.json's auto-run scripts{}; parse_pypi_setup reads a PyPI setup.py; parse_pyproject_toml reads a PEP 621/517 pyproject.toml; parse_setup_cfg reads a setuptools setup.cfg (Python is the #2 malicious-package ecosystem). All funnel their install-time text through the SAME pure extract_from_install_text folder and land in the SAME PackageBehavior — never a per-ecosystem twin (L5); a new ecosystem is one more thin front-end. They differ only in what counts as the install text:
- npm — only the auto-run hooks
INSTALL_HOOKS(preinstall/install/postinstall/prepare) are scored: those are the scriptsnpm installexecutes without the user asking, so atest/buildscript is not the vector and is ignored. - PyPI
setup.py— asetup.pyruns in full atpip installfrom an sdist, so the whole file is the install text. Theinstall_hookssignal is acmdclassinstall/develop override (the documented persistence trick,has_cmdclass_install_override); a declarativesetup(name=…, packages=…, install_requires=…)with no dangerous call is clean, while asetup.pythaturllib.urlopens +os.systems a payload trips the network / sensitive-API findings from the whole-file scan even with nocmdclass. - PyPI
pyproject.toml— declarative TOML that does not itself run code, so a whole-file scan would be WRONG: a[project] dependencies = ["urllib3"]trips theurllibneedle and a[project.urls] Homepage = "https://…"tripshttps://→ a spuriousnetwork_in_installon nearly every honest project. Soparse_pyproject_tomlis TOML-section- aware: it scans ONLY the sections/keys that run at build/install time — the[build-system]backend spec, a PEP 517 in-tree backendbackend-path(an install-hook signal in itself), acmdclasstable, a[tool.*.build.hooks…]build-hook table — and SKIPS the declarative[project]block, the[tool.poetry]/ linter config, and the dependency-list keysrequires/dependencies/optional-dependencieseven inside an active section (a buildrequires = ["urllib3"]is a declared dep, not a network call). A benign declarative pyproject (deps incl.urllib3, ahttps://homepage) is clean; a build-hook thatcurl … | bashes a stage-2 payload IS flagged High. A tiny line-scanner — notomldependency, so the pyproject front-end stays inside the leanscangate (thetomlcrate never enters the default binary — holger / static-holger purity, matching therustsec/manifestsgating). - setuptools
setup.cfg— declarative INI, the same false-positive hazard as pyproject and the same cure:parse_setup_cfgis a section-aware INI line-scanner (tracks[section]+ indent-continuation, noini/configparsercrate, so it too stays inside the leanscangate). It scans ONLY the build/install-active keys — the[options]cmdclassmapping (a custom install/build command), the[options]dependency_linkslist (URLs setuptools fetches at build → a genuine build-time network source), and a build-active[options.entry_points]group (BUILD_ENTRY_POINT_GROUPS— adistutils.commands/ setup-keyword hook that runs at build) — and SKIPS the entire[metadata]block (incl.url/project_urls), every declarative dependency-list key (SETUP_CFG_DECLARATIVE_KEYS—install_requires/extras_require/python_requires/ …) even inside[options], and a runtimeconsole_scripts/gui_scriptsentry-point group. So a benign declarativesetup.cfg(install_requiresincl.urllib3, a[metadata] url = https://…) is clean, while adependency_linkspayload fetch + acmdclassoverride IS flagged High + Low install-hook.
Either way PackageBehavior carries: install_hooks, network_in_install (a curl/wget/fetch(/urllib/raw-URL fetch — the dropper stage-2 download), url_count, max_token_entropy (max Shannon entropy over long install-script tokens — the base64/packed-payload signal, reusing the SAME core::shannon_entropy the SCIP A17 obfuscation tripwire keys on; one metric, no twin, L5) and the sensitive_apis set (npm: child_process / eval( / atob(; Python: os.system / subprocess / marshal.loads / __import__(; shared: /dev/tcp / base64 / …).
PackageBehavior::findings ranks by academic signal strength:
| feature | severity · id |
network_in_install (dropper) |
High · MODGUNN-BEHAVIOR-NETWORK-IN-INSTALL |
sensitive_apis non-empty |
Medium · MODGUNN-BEHAVIOR-SENSITIVE-API |
max_token_entropy ≥ 4.5 |
Medium · MODGUNN-BEHAVIOR-OBFUSCATION |
install_hooks non-empty |
Low · MODGUNN-BEHAVIOR-INSTALL-HOOK |
Same core::Finding model the verdict surface consumes — the scoring model (the isolation-forest / behaviour-sequence classifier over the vector) is the dynamic-plane consumer; the feature extraction lives here so every consumer extracts it the same way.
Durable + queryable (the VERDICT ledger). A parsed PackageBehavior is not only printed: behavior --warehouse <root> PERSISTS it into the behavior_features warehouse table via Warehouse::append_package_behavior (ecosystem-tagged coords + the full feature vector + the derived verdict = worst finding severity + finding ids), through the SAME warehouse-persist plumbing scorecard --warehouse uses — no second warehouse seam (L5). The read back is load_package_behaviors (latest per (ecosystem, subject)), whose inner PackageBehavior round-trips field-identical (mutation-verified); the CLI read verb is warehouse behavior-log. All behind the warehouse feature — the lean scan/default gate stays free of the skade/arrow tree, so a plain behavior still vets a package with no warehouse build.
Surface
The modgunn warehouse import CLI (main.rs:cmd_import) is the operator face: --osv, --malicious, --nvd, --nvd-backfill, --epss, --kev, --crates-yanks, --npm-deprecations, --pypi-yanks, --ossindex, and the four public-exploit sources --exploit-poc / --metasploit / --nuclei / --exploitdb (which union into one exploit-CVE set before the single enrich_exploited lift) each feed one source into this pipeline; a malformed doc is named on stderr and counted, never silently dropped.
Anti-silent-empty (two edges): a per-doc parse failure (a truncated OSV/registry doc missing its versions/releases array) is counted in n_fail and named on stderr; a well-formed source doc that legitimately yields 0 rows (nothing yanked) is a real zero. But an enrichment feed (--epss / --kev) that was provided yet parsed to 0 rows is a truncated catalog that would silently disable the KEV→Critical / EPSS→High escalation — a fake pass — so cmd_import bails rather than import it (a provided-but-empty enrichment feed is an error, not a shrug). The same law covers the exploit union: if ANY of --exploit-poc / --metasploit / --nuclei / --exploitdb was provided yet the union is 0 CVEs, the import bails rather than silently skip the [public-exploit] lift.
f5 — superplan §D security tail (2026-07-29)
The nordisk super-plan (workspace_nordisk/.nornir/nordisk-super-plan-2026-07-29.md) §D closed its collector gap list — "Remaining: VulnCheck, socket.dev, OSSF Scorecard ingest, Libraries.io, GUAC" — as follows. All parsers are OFFLINE, zero-shell, file-in (obtaining the file — token/key/ running-instance — is the dynamic plane's job, out of modgunn's scope); every one holds empty-is-RED (a well-formed-but-empty doc is a real zero; a malformed doc is an Err, never a fake pass) and ships a real red-when-reverted test.
| §D source | status | where | why |
| OSSF Scorecard | ✅ ALREADY LANDED | provenance::parse_scorecard, ScorecardSignals, warehouse::append_scorecard, scorecard verb |
Was already on origin/master before this pass (the §D list was stale for it) — no re-work; verified present + tested |
| VulnCheck KEV | ✅ LANDED | feeds::parse_vulncheck_kev + --vulncheck-kev |
Real free product, documented {data:[{cve:[…]}]} JSON; folds into the kev escalation set (third KEV authority). Live download needs a FREE token; the parser is token-free |
| Libraries.io | ✅ LANDED | registry::parse_librariesio_project + --librariesio |
Documented project JSON; mirrors the ecosyste.ms package-status row (its open successor). Live API needs a FREE registered api_key; the parser is key-free |
| socket.dev | 🟡 SCAFFOLD / PENDING | registry::parse_socket_alerts + --socket |
The live Socket API is PAID — I cannot reach it to validate the response envelope. The parser targets Socket's PUBLIC alert schema (docs.socket.dev) with a real test, but its field-for-field fidelity to a live paid response is UNVERIFIED. Wired + labelled (SCAFFOLD) in --help; do not treat as production until validated against a real Socket response |
| GUAC | 🟡 SCAFFOLD / PENDING | registry::parse_guac_certify_vuln + --guac |
Ingesting from GUAC needs a RUNNING GUAC GraphQL endpoint with data loaded (POST /query) — no offline corpus to point at here. The parser targets GUAC's documented CertifyVuln GraphQL shape (package trie → vuln ids) with a real test, but the schema is UNVERIFIED against a live instance. Wired + labelled (SCAFFOLD); note GUAC is primarily a downstream aggregator that would consume modgunn output — this ingest path is the honest reverse direction |
Why socket.dev / GUAC are PENDING, not fake-green: each has a real parser + real unit test, so --socket / --guac are honestly usable on a correctly-shaped file. The PENDING mark is about source fidelity — I could not obtain a live sample (paid key / running instance) to confirm the real envelope matches the documented one. That is a truthful "scaffolded, unverified against the live source", never an empty-that-passes-as-green.