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

mikronova/modgunn

.nornir/design.md

Móðguðr — design (f0, the whole)

Class: design · Function axis: f0 (the system) then one ## per function — verdict · feeds · behavior · warehouse · worlddelta · provenance · sbom · manifests · rollup. Strict: this file states only the shape we run. Every why, every rejected alternative, lives in reasoning.md; each function has its own deep -design.md.

f0 — the system

Móðguðr is a leaf Rust crate that hands any artifact a security verdict and, on the heavy plane, diffs the behaviour of a whole dependency world between two snapshots. One type unifies both halves: every analysis emits a core::Finding folded into a core::Verdict — Pass < Warn < Block (core.rs:15), content-keyed by (blob_sha256, policy_version, advisory_db_rev) (Verdict::cache_key, core.rs:70).

The crate is organised as nine functions wired through that shared verdict shape:

function module(s) role feature
verdict (Engine A) scan.rs, core.rs artifact CVE/license/provenance → Verdict scan (default)
feeds osv.rs, nvd.rs, feeds.rs, vex.rs, versions.rs offline advisory feeds → Advisory (range-native) scan
behavior behavior.rs npm package.json / PyPI setup.py / pyproject.toml / setup.cfg (both section-aware) install-code → A23 feature vector → Finding scan
warehouse warehouse.rs skade/Iceberg seam, CDC, fleet scan warehouse
worlddelta (Engine B) scip.rs WorldSnapshot diff → BehaviorDelta + tripwires scip
provenance provenance.rs attestation / SLSA / Scorecard checks scan
sbom sbom.rs CycloneDX 1.5 emit (multi-ecosystem purls) scan
manifests manifests.rs manifest/lockfile ingest → SbomComponent (7 ecosystems) manifests
rollup rollup.rs pure aggregate/fleet folds over findings always
framework about.rs, tools.rs, service.rs the shared suite shell: About identity · 🧰 Tools menu (jera demo) · service lifecycle (skidbladnir) about always; tools/service need framework

The reuse graph (uses). verdict.uses = [feeds, provenance] — the scanner matches against feeds' Advisorys and folds provenance's findings. warehouse.uses = [feeds, worlddelta] — it round-trips Advisorys and captures WorldSnapshots. worlddelta.uses = [core] only. rollup.uses = [warehouse] (maps its FleetFindings in) but degrades to pure input. feeds, behavior, provenance, sbom, manifests are leaf-of-leaf: pure parse/emit, no warehouse, no network (manifests.uses = [sbom] — it produces the SAME SbomComponent sbom emits; behavior.uses = [core] — it reuses core::shannon_entropy, the SAME obfuscation metric the SCIP A17 tripwire keys on, no twin). Every function speaks core::{Finding, Verdict, Severity, Decision} and nothing speaks nornir.

Two engines, one shape (the chosen split). Engine A (verdict) is the lean, always-on gate — holger's promotion path and static-holger quarantine link it with default = ["scan"] and pull no arrow/iceberg tree. Engine B (worlddelta) is the heavy world-delta site (128 GB–1 TB RAM), compiled only under --features scip. Both fold into the same core::Verdict, so a consumer reads one type regardless of which engine produced it — scip::verdict_from builds a Verdict from a BehaviorDelta exactly as scan::OsvScanner::scan builds one from an Artifact.

Leaf by construction. holger → modgunn and nornir → modgunn; modgunn → skade (the warehouse feature) and never modgunn → nornir. That one forbidden edge is the cycle the crate exists to break; the why is in reasoning.md.

The per-function ## sections below are the map; the territory is each function's own {fn}-design.md.

verdict (Engine A) — scan.rs

OsvScanner::scan folds three checks into one Verdict: known-vuln match (AdvisoryDb::query, scan.rs:213), license_finding, and provenance_finding, reduced by decide (scan.rs:366, strictest Decision wins). Exploit-intel gating lives in vuln_finding: KEV → escalate to Critical, EPSS ≥ epss_block_threshold → High, VEX not_affected → suppress. Policy is Policy (scan.rs:320). Deep: verdict-design.md.

feeds — osv.rs · nvd.rs · feeds.rs · vex.rs · versions.rs

Offline feed parsers produce scan::Advisory carrying ranges, not pre-expanded versions: osv::parse_doc, nvd::parse_nvd/enrich_with_nvd, feeds::{parse_epss_csv, parse_kev_json, parse_malicious, enrich}, vex::collect_suppressions. scan::merge_by_alias (scan.rs:224) dedups CVE↔GHSA↔RUSTSEC into one row; match is versions::affected_by (versions.rs:55) at scan time. Deep: feeds-design.md.

behavior — behavior.rs

The A23 malicious-package feature extractor — scores a package with no CVE yet. Four ecosystem front-ends, one folder: behavior::parse_npm_package reads an npm package.json's auto-run scripts (preinstall/install/postinstall/prepare), behavior::parse_pypi_setup reads a PyPI setup.py (the whole file runs at sdist install; the install-hook signal is a cmdclass override), behavior::parse_pyproject_toml reads a PEP 621/517 pyproject.toml section-aware — folding only the build/install-active sections ([build-system] / a PEP 517 in-tree backend-path / a cmdclass table / a [tool.*.build.hooks…] build-hook) and skipping the declarative [project] deps & URLs so an honest urllib3 dep / https:// homepage does not false-positive; a tiny line-scanner with no toml dep, so it stays in the lean scan gate — and behavior::parse_setup_cfg reads a setuptools setup.cfg section-aware INI (no ini dep, same gate) — folding only the [options] cmdclass / dependency_links keys and a build-active [options.entry_points] group, skipping [metadata] (incl. url) and the declarative install_requires / extras_require / … keys. All four funnel through the SAME pure extract_from_install_text and land in the SAME PackageBehavior, no per-ecosystem twin (L5). Features: install-hook set, network_in_install (dropper stage-2 fetch), url_count, max_token_entropy (reusing core::shannon_entropy — the SAME metric the SCIP A17 tripwire keys on), and the sensitive-API set (npm + Python primitives). PackageBehavior::findings ranks the vector into core::Findings (network-in-install → High, sensitive-API/obfuscation → Medium, install-hook → Low). Reads text only, no code executed. Driveable from the CLI — modgunn behavior --path <package.json|setup.py|pyproject.toml|setup.cfg> [--ecosystem npm|pypi|pyproject|setupcfg] [--subject <name>] [--fail-on-finding] [--warehouse <root>] folds one package, prints the vector

warehouse — warehouse.rs

Warehouse opens the shared skade catalog (open / open_read_only), ensure_modgunn_tables, and OWNs cve_advisories / world_snapshots / dep_edges / scip_symbols / behavior_deltas / tripwire_events / scorecard_signals / behavior_features (the A23 behaviour-verdict ledger — append_package_behavior / load_package_behaviors, a field-identical round-trip) while READing nornir's sbom_components. CDC is commit_seq + table_delta + scan_sbom_delta over skade's read_delta; scan_fleet joins the whole SBOM × the latest advisories. Deep: warehouse-design.md.

worlddelta (Engine B) — scip.rs

A WorldSnapshot (scip.rs:295) holds symbols, call edges, dep nodes/edges and vuln sinks. diff runs the default_tripwires battery — CallGraphDeltaTripwire, CapabilityDriftTripwire, SinkNewlyReachableTripwire, FingerprintDriftTripwire, ObfuscationTripwire, MaintainerAnomalyTripwire, SbomDriftTripwire, plus the rule-configured PolicyTripwire / ThresholdTripwire at the heavy site — into a BehaviorDelta. Standing analyses: reachability_gated_vulns, capability_surface, blast_radius (PageRank), and the full 25-of-25 catalog (honeytoken, world time-travel diff, …). Deep: worlddelta-design.md, catalog: engine-b-analyses.md.

provenance — provenance.rs

ProvenanceRequirement::evaluate (provenance.rs:60) checks an Attestation against min-SLSA / trusted-signer / source-repo, returning a scan::Provenance + findings; attestation_delta reports signer/source change; ScorecardSignals::findings turns OSSF Scorecard signals into findings. Deep: provenance-design.md.

sbom — sbom.rs

to_cyclonedx (sbom.rs:47) serializes a SbomInput (components + vulns) into a CycloneDX 1.5 document; purl builds the multi-ecosystem package URL. The emit direction, complementing the warehouse SBOM read. Deep: sbom-design.md.

manifests — manifests.rs

The ingest direction, complementing sbom's emit: parse_manifest dispatches a manifest/lockfile by basename to one EcosystemParser per ecosystem (cargo/pip/npm/maven/go/nuget/gem) and produces the SAME sbom::SbomComponent model — one seam, one parser each, model never forked (Cargo.lock is the template). Pure-Rust, zero-shell (toml/roxmltree/serde_json/ hand-parse; never runs pip/npm/…). Feature manifests, off by default so the lean gate pulls no TOML/XML parser; the existing cargo warehouse-read + sbom emit path is untouched. Deep: manifests-design.md, manifests-guide.md.

rollup — rollup.rs

Pure higher-level folds for altitude: SeverityHistogram::of, repo_health, fleet_rollup, and rollup_fleet_findings (rollup.rs:191) which maps warehouse::FleetFindings in. No warehouse/network of its own. Deep: rollup-design.md.

framework — about.rs · tools.rs · service.rs

modgunn's slice of the constellation-wide COMMON FRAMEWORK — the JetBrains-style shared shell every suite app (korp / nornir / holger / dwarves / modgunn) carries: an About, a 🧰 Tools menu of demo actions, and a shared lifecycle. modgunn is a CLI, so these are CLI surfaces (modgunn about | tools | demo | service). The rule is CONSUME, never twin (L5): the demo runs a jera ContainerSpec, the service is a skidbladnir ServiceSpec.

The reuse graph. framework.uses = [jera, skidbladnir] (both optional, off the lean gate). about.uses = [] (pure data). Deep: framework-design.md · guide: framework-guide.md.

Which recipe reader this repo uses, and why (lane AJ, 2026-08-30)

Reader: nornir-build-thing 0.1.11 in [dependencies] (registry floor) and 0.1.12 in xtask; both resolve to edda's tree locally through [patch.crates-io].

Why: modgunn is BOTH — in-constellation locally and a PUBLISHED transitive of holger, nornir, znippy and knut. Its floor is therefore the newest version that exists on crates.io, because those four resolve modgunn from the registry where the [patch] does not reach; a 0.1.12 floor there would fail every one of them. Its old floating "0.1" is what let znippy sit on a registry 0.1.3. modgunn's recipe is schema 1.

The rule itself — [nornir-build-thing] schema = N, a refusal that names the version to upgrade to, and a loader that refuses to DROP a key it cannot read — lives once, in edda .nornir/build-design.md, "THE COMPATIBILITY RULE". The cross-version guard that would have caught the 2026-08-30 skew is edda/nornir-build-thing-skew.