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 inreasoning.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
- findings, and (with
--fail-on-finding) exits 3 so a promotion gate can block a suspicious package. With--warehouseit PERSISTS the vector + derived verdict into thebehavior_featurestable (durable + queryable), read back viawarehouse behavior-log. Deep:feeds-design.mdf4.
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.
-
About —
about::identity/about::state_json: builds the SHARED [nornir_appshell::Identity] (name · Móðguðr · version · role/subtitle· both engines · the framework it is built on). Always compiled (serde-only), so the leanscanbuild — and holger / static-holger, which link modgunn's lib — answermodgunn aboutwith no heavy dep. (RECONCILED L5: the bespokestruct Identityis gone — the model lives ONCE in thenornir-appshelledda leaf, and the sharedfacett-aboutegui card renders exactly this shape, so korp / holger / modgunn feed ONE About widget.) -
Tools (feature
framework) —tools::entriesis the 🧰 menu; its one entryDemoAdvisoryFeedis a jera-backed generate → import → scan action:tools::demo_feed_specbuilds ajera::container::ContainerSpec(pinnedalpine, a/outbind) thattools::run_demo_feed(featureframework-oci) runs as a tracked job viajera::container::run_container— routed THROUGH draupnir's ONE OCI engine, never a bespoke docker call orthread::spawn. The container writes a demo OSV doc thatwarehouse import --osvthen folds. -
Service (feature
framework) —service::modgunn_service_specdeclares modgunn's world-delta gatekeeper as askidbladnir::service::ServiceSpecdriven through the sharedLifecycle(the ONE systemd renderer + a dedicatedmodgunnsystem user viaskidbladnir::systemd::ensure_system_user, L5).modgunn service --unitrenders the unit; the watcher daemon itself (ExecStart = modgunn world-delta watch) is deferred. -
Build atoms —
.nornir/build-variants.tomldeclares modgunn's outputs as data (nornir-build-thing /cargo xtask build --suite ORIGINAL). The constellation's build matrix (workspace_nordisk/.nornir/build-matrix-2026-08-02.md) gives every member the SAME five shapes —<x>-server⭐ ·<x>·<x>-cli·<x>-ui·<x>-solo— and modgunn declares exactly ONE of them:modgunn(client · fat · binmodgunn), the Engine A+B CLI with skade compiled in viawarehouse. The other four are FICTION for this member and are declared GAPS with the measurements behind them: no daemon, no gRPC wire, no UI, and therefore no solo. The suite asymmetry is encoded too: modgunn may embed only downstreamskade, never the upstream nornir / holger it exists to decouple. On 2026-08-03 the three off-matrix declarations (modgunn-lib,modgunn-imageand themodgunn-world-deltacontainer) were REMOVED — declarations only; the crate, its[[bin]]and every consumer edge are untouched. Seesuite-atoms-design.md.
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.