Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/modgunn

.nornir/feeds-design.md

feeds — design

Class: design · Function axis: f0 the ingest fold · f1 per-source parsers · f2 version grammars · f3 enrichment & suppression · f4 package behaviour features (A23). Strict: only the shape we run. The why is in feeds-reasoning.md; the system view in design.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:

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

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:

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.