Móðguðr — master plan & analysis catalog (the "modgunn prompt")
This is the living driver for modgunn. Keep adding here. Referenced from
workspace_nordisk/PLAN.md(§ Móðguðr). Detail on the 25 advanced world-delta analyses lives inengine-b-analyses.md; the repo rationale/decisions innornir/.nornir/modgunn-security-scanner.md.Móðguðr guards Gjallarbrú and demands to know who crosses. She is not a blind woman — she has an Iceberg warehouse (full history, time-travel, CDC) and a graph (SCIP call-graph + resolved dep-graph). Every analysis below is something she can actually compute from those two organs.
0. Dependency direction (locked)
Móðguðr (security + SCIP world-delta engine, leaf)
/ \
holger nornir ← both depend on Móðguðr; no cycle
\
skade (embedded Apache Iceberg catalog + reader/writer)
nornir → modgunn✅ (nornir is the consumer: viz Security pane + tripwire feed)holger → modgunn✅ (policy gate / promotion gate consumescore::Verdict)modgunn → skade✅ (warehouse access — open the shared catalog directly)modgunn → nornir❌ never (that is the cycle modgunn exists to break)
modgunn talks to the warehouse through iceberg tables, not nornir's API — it opens the same physical catalog.redb + warehouse/ in the shared nornir namespace. To avoid dependency hell, the only build-time dep this adds is skade (feature warehouse, off by default; holger's lean gate stays pure-leaf).
1. Outbreak status (what's implemented vs left)
| Piece | State |
Engine A verdict core (scan.rs) — multi-ecosystem CVE/license/provenance → Pass/Warn/Block, cached by (blob_sha256, policy_version, advisory_db_rev) |
✅ done, 13 tests |
Exploit-intelligence (scan.rs) — Advisory.epss/kev; Policy.kev_blocks/epss_block_threshold/vex_not_affected; KEV→Critical, EPSS≥τ→High, VEX not_affected suppression |
✅ implemented, 3 tests (adopt-list #1) |
Supply-chain collectors (registry.rs/feeds.rs) — registry yanks (crates.io/npm/PyPI/Go retractions) → MODGUNN-YANK-*/MODGUNN-DEPRECATED-*; public-exploit lift (PoC-in-GitHub, Metasploit, Nuclei, Exploit-DB) → ≥High+[public-exploit]; ENISA EUVD exploitedSince→KEV-grade; Exploit-DB header-driven codes column; Sonatype OSS-Index purl-keyed component reports (CVE-keyed so they de-dup into the OSV row); GreyNoise (malicious/suspicious only — never the benign/RIOT half); ClearlyDefined declared-vs-discovered license compliance; Sigstore/Rekor transparency-log entries (pure-Rust base64, no unearned SLSA claim). All via warehouse import / import-iocs / license-compliance flags, anti-silent-empty enforced |
✅ implemented (heimdall loop) |
Threat-intel IOC corpus (ioc.rs + ioc_indicators table) — abuse.ch URLhaus (malware URLs) / Feodo (C2 IPv4) / SSLBL (bad-cert SHA-1) → shape-validated Ioc rows, snapshot-keyed; the garm-side signal modgunn owns for BOTH dwarves + garm. warehouse import-iocs --urlhaus/--feodo/--sslbl |
✅ implemented (heimdall loop) |
Dep-graph importer (manifests::*_lock_edges + dep_edges) — Cargo.lock/poetry.lock/package-lock.json → resolved dependency edges (npm node_modules nested-vs-hoisted), the multi-ecosystem graph the baddie-detection joins walk. warehouse import-deps |
✅ implemented (heimdall loop) |
Offline CVSS scoring (osv.rs) — compute the base score from a v2 or v3 vector when an advisory carries only a vector (was defaulting to Medium/High); validated vs FIRST reference scores; shared severity_from_base bucket |
✅ implemented (heimdall loop) |
OSV import (osv.rs) — parse OSV/GHSA/RUSTSEC JSON (single+batch) → Advisory (ecosystem normalize + severity map) |
✅ implemented, 4 tests (adopt-list #2) |
Warehouse seam over skade (warehouse.rs, feature warehouse) — open shared catalog; OWN cve_advisories/world_snapshots/behavior_deltas/tripwire_events/scorecard_signals/behavior_features; READ nornir's sbom_components; load AdvisoryDb from the CVE table; feed Engine A |
✅ implemented (this pass) |
CDC seam (commit_seq, table_delta, current_snapshot_id) over skade's read_delta/DeltaPlan |
✅ implemented (this pass) |
Engine B core (scip.rs) — modgunn-owned graph vocab (Symbol/CallEdge/DepNode/DepEdge/VulnSink/Capability), real WorldSnapshot, diff()→BehaviorDelta, Tripwire trait + battery; verdict_from() folds → core::Verdict |
✅ implemented (scip.rs, 8 tests) |
Engine B analyses — 25/25 catalog landed (was 5, then 19). Graph/anomaly/supply-chain core (scip.rs, 22): reachability-gated vuln (#4), capability-surface+drift (#2), silent fingerprint-drift (#3), sink-newly-reachable (#5), call-graph delta (#1), blast-radius PageRank (#11), bus-factor (#12), diamond/multi-version (#15), chokepoint/min-cut (#14), typosquat+dep-confusion (#8), metric-outlier z-score (#16), obfuscation/entropy (#17), release-cadence anomaly (#7), owner-cluster label-propagation community detection (#13), change-velocity release-rate bursts (#25), license-transition tracking (#19), embedding-novelty (#18), publisher/maintainer ownership-transfer anomaly (#6), SBOM drift (#20), policy-tripwire DSL (#21), threshold-tripwire DSL (#22), honeytoken/canary (#23) — plus taint-path source→sink (taint_path_findings, SAST/dataflow-rival "A12") beyond the numbered catalog. Provenance surface (provenance.rs, 2): attestation-delta (#9), source↔artifact sealed-verify (#10). Temporal (warehouse.rs, 1): world time-travel two-point diff (#24, world_diff). See engine-b-analyses.md build-status table for the full DONE map. |
✅ implemented (scip.rs 100 tests, provenance.rs, warehouse.rs) |
WorldSnapshot capture — world_snapshots+dep_edges+scip_symbols tables; capture_world_snapshot() + record_behavior_delta_findings() |
✅ implemented (warehouse.rs, gated scip+warehouse, round-trip tests) |
nornir cutover — nornir depends on modgunn; security domain logic moves out of nornir/src/security.rs |
✅ done (see §6): warehouse-routed — importer + warehouse-sourced verdict (real severity, all-High bug gone) + SBOM-emit in modgunn + live Mimir.SecurityScan RPC/viz/CLI render the warehouse verdict via lock-tolerant open_read_only. Tail: collapse osv_query/advisory_db_query to importer-only (re-source the display vulns[]) |
CDC — is it complicated? Does iceberg-rust already support it?
Not complicated for modgunn — skade already did the hard part. Answer in three points:
- iceberg-rust 0.9.1 has NO native incremental/changelog scan — only time-travel by
snapshot_id. (The vendorediceberg-arrow58fork defines the changelog record vocabulary —INSERT/DELETE/UPDATE_BEFORE/UPDATE_AFTERinmetadata_columns.rs— but exposes no scan that produces it.) - skade reconstructs CDC itself from the manifest layer.
skade::Table::read_delta(from, to)walks the snapshot lineage (parent_snapshot_id) and returns aDeltaPlan: the data files added by the window (inserts) + the equality-delete files (identity deletes), withneeds_full_reloadset for overwrite/position- delete commits an additive delta can't express. So inserts + identity-deletes are already true CDC. - modgunn just consumes it.
warehouse::Warehouse::commit_seq()(poll to notice change cheaply) →table_delta(table, from)(the changed rows + new watermark). When upstream iceberg-rust ships a native changelog scan, skade swaps its internals and modgunn's API does not change. The DataFusionsqlsurface (skadesqlfeature) is the optional richer-query path.
Skade design drawing TODO: add the
read_delta/DeltaPlanCDC path to skade's SVG design diagrams (the snapshot-lineage walk → added data-files + equality-deletes → DeltaPlan, with theneeds_full_reloadfallback edge). File under skade.nornir/design assets next to the time-travel drawing.
2. What Móðguðr can see (her two organs + the feeds)
| Organ / feed | Gives her |
| Graph — SCIP | symbol defs/refs, the call graph, monikers (cross-repo symbol identity), per-symbol reference closure, unsafe/FFI markers |
| Graph — dep-graph | resolved transitive tree per ecosystem (cargo/maven/npm/pip/go/nuget/gem/conda…), version pins, feature edges |
| Iceberg — time | every world-snapshot, time-travel between any two, CDC delta (skade read_delta), full historized series per package |
| Registry metadata | publisher, signing keys, publish timestamps, download counts, yank state, ownership |
| Provenance | CMS signatures (znippy sign), SLSA / in-toto attestations, source↔artifact linkage |
| Embeddings | nornir embedder (jina/Qwen) → code/package vectors (handed in; modgunn does not own the model) |
| Bytes | the artifact itself — files, entropy, AST, secrets |
Every analysis emits Finding{severity, kind, subject, evidence} → folded into a core::Verdict (Pass/Warn/Block), plus a Tripwire event stream for the world-delta site.
3. THE CATALOG — every security analysis, by scale
The same world can be interrogated at very different corpus sizes. modgunn is designed to answer at every scale; what changes with scale is which methods are affordable and which signals only emerge in aggregate.
Scale ladder (axis A)
| Tier | Corpus | What becomes possible | Dominant cost |
| S0 — 1 artifact | a single crate@version + its manifest | per-artifact verdict: CVE match, license, provenance, secrets, entropy, declared-vs-actual API | O(bytes) |
| S1 — 1 closure | one repo's resolved transitive tree (10²–10³ deps) | reachability-gated vuln, capability-surface, diamond/multi-version, blast-radius within the closure, SBOM | O(V+E) graph |
| S2 — N products | your org's repos (10³–10⁴ deps, overlapping) | shared-dep correlation, internal-namespace dep-confusion, org-wide blast-radius, fleet license posture | O(E) over union graph |
| S3 — ecosystem slice | a registry mirror / curated 10⁴ crates | typosquat neighborhoods, owner-clusters, cadence baselines, centrality ranking, ecosystem embedding-norm | O(n log n)–O(E·iters) |
| S4 — whole open-source world | the full registry × history (10⁶–10⁷) | population statistics, global anomaly baselines, campaign/correlated-takeover detection, supply-chain epidemiology | distributed / sampled; warehouse-resident |
Scaling math (cost model). Per-artifact checks are O(bytes), embarrassingly parallel → the gatling all-core pool. Closure checks are O(V+E). Global graph algorithms are the expensive tier: betweenness O(V·E), PageRank O(E·iters), community detection (Louvain) ~O(E), min-cut O(V·E²) (use max-flow on the small entrypoint→sink subgraph, not the whole graph). At S3/S4 these are not recomputed per query — they are materialized into Iceberg tables and refreshed incrementally off the CDC delta (only recompute the neighborhood a
read_deltatouched), which is the whole reason skade's history matters.
Method families (axis B) — with the actual math
Extends the 7 families in
engine-b-analyses.md. Each row: the analysis, the math/algorithm, and the smallest scale it needs.
F0 — Per-artifact static (S0).
| # | Analysis | Math / method |
| A1 | Known-vuln (SCA) match | exact (ecosystem,pkg,version) set-membership vs cve_advisories; range→exact expansion front-end |
| A2 | License policy | SPDX expr parse → allow/deny set membership; copyleft class lattice |
| A3 | Provenance / signature | CMS/SLSA verify; presence + signer-identity check |
| A4 | Secret / credential scan | regex + Shannon entropy threshold + known-token shapes (high-entropy ≥ ~4.5 bits/char) |
| A5 | Obfuscation / packed-code | per-file entropy spike, minification ratio, base64/hex blob density, eval/dynamic-exec AST markers |
| A6 | Declared-vs-actual API | manifest exports vs SCIP-observed public symbols (drift = hidden surface) |
| A7 | Yank / EOL / staleness | registry yank flag; age since last release |
F1 — Closure / graph-local (S1).
| # | Analysis | Math / method |
| A8 | Reachability-gated vuln | multi-source BFS/DFS from entrypoints over the call graph → CVE sink; alert only if reachable (kills ~90% version-only FP) |
| A9 | Capability-surface | reachability closure to a labeled sink set (net/fs/proc/env/FFI/unsafe/rng/clock); report the set |
| A10 | Sink-newly-reachable | set-diff of A8/A9 closures across two snapshots |
| A11 | Diamond / multi-version | group dep-graph nodes by name; |
| A12 | Taint paths | source→sink path search over the call graph (labeled sources = network/argv/env) |
F2 — Structural graph algorithms (S2–S3).
| # | Analysis | Math / method |
| A13 | Blast-radius centrality | betweenness / PageRank over dep-graph → monitoring priority |
| A14 | Bus-factor / SPOF | centrality × maintainer-count correlation |
| A15 | Owner-cluster | community detection (Louvain / label-propagation) over (dep→publisher) bipartite graph → correlated risk |
| A16 | Chokepoint / min-cut | max-flow min-cut on entrypoint→sink subgraph → minimal pin/patch set |
| A17 | Dependency-confusion | internal-namespace set membership ∩ public-registry resolvability |
| A18 | Typosquat / homoglyph | normalized edit distance (Damerau-Levenshtein) + homoglyph-fold + popularity weighting vs top-K names |
F3 — Statistical & ML anomaly (S3–S4).
| # | Analysis | Math / method |
| A19 | Metric-outlier | z-score / isolation forest over warehouse history (size, dep-count, file-count, downloads) |
| A20 | Release-cadence anomaly | per-package inter-arrival baseline → z-score; off-hours/dormant-then-publish flags |
| A21 | Embedding-novelty | cosine distance of a release's vector vs its own history and vs ecosystem centroid; > τ ⇒ semantically-novel injected code |
| A22 | Behavior-fingerprint drift | hash/embed a symbol's transitive reference closure; fingerprint changed but version/API did NOT ⇒ the silent-5th-level-dep signal |
| A23 | Campaign / correlated-takeover | co-occurrence clustering of A6/A15/A20 signals across packages in a time window |
F4 — Provenance & integrity deltas (S0–S2).
| # | Analysis | Math / method |
| A24 | Publisher/maintainer anomaly | new signing key / ownership transfer / sudden maintainer change vs provenance history |
| A25 | Attestation delta | SLSA/in-toto appears/disappears, builder-identity change, source-repo URL mismatch |
| A26 | Source↔artifact divergence | reproducible-build check: does the artifact rebuild from the tagged source SHA? |
| A27 | Pull-through poisoning | upstream-cached bytes checksum vs index-claimed checksum (content-addressing integrity) |
F5 — License / legal drift (S1–S3).
| # | Analysis | Math / method |
| A28 | License-transition | per-dep license time series; flag relicense (MIT→AGPL) |
| A29 | Copyleft-contamination path | license-compatibility lattice + transitive cut over the dep-graph |
| A30 | SBOM drift | CycloneDX/SPDX set-diff between two world snapshots (auditable "what entered/left, when") |
F6 — Temporal / CDC (S1–S4) — where Iceberg + skade earn their keep.
| # | Analysis | Math / method |
| A31 | World time-travel diff | load_table_at(s1) vs load_table_at(s2); "what did the world look like on date X" |
| A32 | Incremental change-sense | skade read_delta(from,to) → DeltaPlan (added rows + identity-deletes); poll commit_seq() |
| A33 | Change-velocity baseline | per-dep historical change-rate model; flag burst or freeze-then-spike |
| A34 | Provenance timeline reconstruction | replay the historized series for any dep — full audit trail |
F7 — Tripwires (declarative, active; any scale).
| # | Analysis | Math / method |
| A35 | Policy tripwires | declarative rules on the delta ("no new native FFI in transitive deps", "no dep added without provenance", "capability set may not grow without review") |
| A36 | Threshold tripwires | "centrality of an unverified dep crossed T", "entropy spike > σ", "capability set grew ≥ k" |
| A37 | Honeytoken / canary | planted markers; fire if a watched internal name or secret-shaped token resolves/exfiltrates externally |
4. Compute tiers (where each runs)
- Cheap (holger gate / per-serve, online, Engine A latency budget): A1–A5, A8, A17, A24, A27, A28, A35 — set lookups + small reachability.
- Medium (per-promote / nightly): A6, A9–A12, A18–A20, A25, A30, A33, A36.
- Heavy (world-delta site, 128 GB – 1 TB RAM): A13–A16, A21–A23, A26, A29, A31, A34 — whole-graph algorithms + embeddings + full SCIP closure over the transitive world. Materialized into Iceberg, refreshed off the CDC delta (§3 scaling math).
5. First slices (when Engine B starts, post-demo)
- ✅
WorldSnapshotcapture →world_snapshots+dep_edges+scip_symbolsIceberg tables (warehouse::capture_world_snapshot, gatedscip+warehouse). - ✅
diff()→BehaviorDelta: F1 #A8 (scip::reachability_gated_vulns) makes Engine A's CVE verdicts precise (reachable vs merely-present); sink-newly-reachable (#5) over the snapshot pair is the highest-signal delta tripwire. - ✅ A1 (
CallGraphDeltaTripwire) + A9 (CapabilityDriftTripwire) world-delta tripwires; A13 (blast_radius, PageRank) + A22 (FingerprintDriftTripwire). - ✅ The full catalog is landed — the Engine-B core now carries the graph/ anomaly/supply-chain analyses plus 2 provenance analyses and the temporal two-point diff verb (25/25 catalog, see
engine-b-analyses.md): diamond/multi-version (#15), chokepoint/min-cut (#14), bus-factor (#12), typosquat+dep-confusion (#8), metric-outlier z-score (#16), obfuscation/entropy (#17), release-cadence anomaly (#7), owner-cluster label-propagation community detection (#13), change-velocity release-rate bursts (#25), license-transition tracking (#19), embedding-novelty (#18), publisher/maintainer ownership-transfer anomaly (#6,MaintainerAnomalyTripwire), SBOM drift (#20,SbomDriftTripwire), policy-tripwire DSL (#21,PolicyRule), threshold-tripwire DSL (#22,ThresholdRule), honeytoken/canary (#23,honeytoken_findings), world time-travel two-point diff (#24,warehouse::world_diff), and the sealed build-provenance VERIFY +build_provenanceledger (#9/#10,provenance.rs/warehouse.rs) — plus a taint-path source→sink analysis (taint_path_findings) beyond the numbered catalog. All 25 numbered analyses now have real logic + unit tests. - Incremental refresh: wire the heavy materializations (#11/#15) to recompute only the neighborhood
read_deltatouched — TODO (CDC seamtable_deltaready).
6. nornir → modgunn cutover plan (the extraction)
Status (warehouse-routed cutover — core landed, green across modgunn + nornir + holger):
- ✅ modgunn M1 —
osv::parse_rustsec_md+rustsec_affected_ranges: rustsec.mdimport with thepatched/unaffected→RangeEventinversion, behind the off-by-defaultrustsecfeature (lean gate stays toml-free); oracle-tested against cargo-audit's rule (incl. the aws-lc multi-branch case).- ✅ modgunn M2 —
sbom::to_cyclonedx(multi-ecosystem purl); cargo output byte-compatible with nornir's prior literal.- ✅ nornir N1–N4 —
modgunndep onfeatures=["warehouse","rustsec"](skade already a nornir path-dep, so no new tree);security::import_advisory_db_into_warehouse(rustsec mirror →cve_advisories, wired intosecurity update-db);security::verdictre-sourced from the warehouse (verdicts_with/verdicts_from_warehouse) — the all-High mapper is deleted, real severity/EPSS/KEV drives the decision, matching happens once in modgunn;to_cyclonedxdelegates tomodgunn::sbom.- ✅ N5 —
warehouse::Warehouse::open_read_only(lock-tolerant copied-aside snapshot, mirroring nornir'sopen_read_only; no skade change — skade stamps absolutefile://data paths, so a copied catalog still resolves live data). The liveMimir.SecurityScanRPC now folds a warehouse-sourced verdict (verdict::verdicts_from_warehouse, real severity/EPSS/KEV) into its JSON additively — it reads a snapshot while the server holds the catalog write lock, so no contention; the viz Security tab + CLI server-mode render the Pass/Warn/Block badge.- ✅ N5 tail — the RPC's display
vulns[]is now ALSO warehouse-sourced: whencve_advisoriesis populated it is derived from the same verdict scan (one in-memory match, no OSV/rustsec double-match);query_vulns+ thevuln_findingscache remain only as the fallback for an un-imported warehouse (no regression). An authoritative Pass/Warn/Blockdecisionis emitted only when populated, else omitted (no misleading PASS). Adversarially reviewed (10 agents, 0 confirmed defects).osv_query/advisory_db_queryare now fallback/importer-only.
Public surface nornir consumers use today (from the security map; preserve or re-home each):
- Security DOMAIN → move into modgunn (leaf-clean):
scan,query_vulns,osv_query,advisory_db_query,to_cyclonedx,scan_outcome,update_advisory_db, typesSecurityReport/Component/Vuln. modgunn'sOsvScanner+warehousealready cover the verdict + advisory-feed half. - Build-orchestration → STAYS in nornir (it is nornir's build concern, not a security primitive):
components/components_warehouse_first,warm*,persist_sbom,prepare_path_deps,unresolved_path_dep_siblings,materialize_path_dep_siblings,dep_clone_base— these resolve a repo's dep closure (cargo metadata, sibling-repo cloning) and feed components into modgunn. - Seam: nornir computes
Components → hands them tomodgunn::scan→ getsVuln/Verdictback; persistence ofvuln_findings/sbom_componentsis shared (either side may write via skade, single-writer coordinated).
Consumers to update (file:line): viz/security_tab.rs:32,135; bin/nornir.rs:5104–5158; bin/nornir-server.rs:1922–2317; monitor.rs:543–557; introspect/depgraph.rs:45,55; gitio.rs:757. Add to nornir/Cargo.toml: modgunn = { path = "../modgunn", features = ["warehouse"] }.
7. World survey — prior art, modgunn's edge, and what to adopt
Web survey (2026-06) of the world's software/dependency security scanning & analysis landscape, mapped onto §3's catalog. Sources at the end of this §.
7.1 The landscape, by layer
| Layer | Real tools | What they do | Maps to |
| SCA (presence) | Trivy, Grype, OSV-Scanner (OpenSSF/Google), OWASP Dependency-Check | match deps vs a vuln DB (OSV/NVD/GHSA); flag every matching CVE | A1 |
| SCA (reachability) | Snyk, Endor Labs (commercial) | call whether the vulnerable function is reached → 70–90 % FP cut; Snyk = Java/JS/Python only | A8 |
| SBOM | Syft, CycloneDX, SPDX | generate/emit the component list | A30 |
| Prioritization | EPSS, CISA KEV, CVSS, SSVC, VEX; Grype has EPSS+KEV scoring | severity × exploit-probability × confirmed-exploited; EPSS+CVSS+KEV cuts urgent work ~95 % | gap → 7.3 |
| SAST / dataflow | Semgrep (pattern + intra-proc taint; cross-file = Pro), CodeQL (whole-program dataflow/taint via QL) | source→sink taint over the call graph | A12, A8 |
| Provenance / integrity | Sigstore (cosign/rekor/fulcio), SLSA (L1–L3), in-toto, slsa-verifier, reproducible-builds | sign + transparency-log; build-provenance levels; rebuild-from-source | A3, A24–A27 |
| Graph aggregation | GUAC (graph DB over SBOM+SLSA+vuln+Scorecard; queries paths-to-vuln, recursive transitive), deps.dev (50 M+ pkg-versions metadata), OSSF Scorecard | correlate isolated metadata into a queryable graph; betweenness → chokepoints | A13–A16 |
| Malicious-package | Socket (1700+ malicious pkgs H1-2025, most had no CVE → behavioral), OpenSSF Malicious Packages feed, academic ML | behavior/metadata anomaly, not CVE matching | A4/A5/A19/A21/A23 |
| Typosquat / dep-confusion | Socket, registry guards, academic | edit-distance/homoglyph; internal-name-on-public-registry | A17, A18 |
7.2 Móðguðr's edge (why she is not just another scanner)
The field is fragmented: SCA tools own the dep-graph, SAST tools own the call-graph, GUAC owns metadata-graph aggregation, Socket owns behavioral malware, and almost no one owns history. modgunn's two organs fuse these:
- Iceberg time-travel + CDC = history almost nobody has. Point-in-time is the industry norm; modgunn historizes the whole world and diffs any two snapshots (A22 behavior-fingerprint drift, A31–A34, A33 velocity). The "5th-level jar unchanged for years that silently changed behavior" is a signal the standard stack structurally cannot emit — it keeps no per-symbol behavior history. skade's
read_deltamakes it cheap. - One engine fuses call-graph (CodeQL-grade) + dep-graph (GUAC-grade). Reachability-gated vuln (A8) — the commercial differentiator (Snyk/Endor) — is first-class, multi-ecosystem, and offline, not a 3-language add-on.
- Airgap/offline by construction. OSV-Scanner etc. need network; modgunn's advisory DB is warehouse-resident (sealed-znippy import →
cve_advisories,db_rev= the cache key). The government "knows exactly what happens to the smallest bit" posture. - One verdict shape A→B. Gate (Engine A) and world-delta (Engine B) both emit
core::Verdict— the field needs 5+ tools stitched together for this surface.
7.3 Adopt-list (converged standards modgunn should ingest — concrete next adds)
- ✅ EPSS + KEV + VEX prioritization — DONE (
scan.rs+feeds.rs):Advisory.epss/kev,Policy.kev_blocks/epss_block_threshold/vex_not_affected; KEV→Critical, EPSS≥τ→High, VEXnot_affectedsuppression. Live feed import landed (feeds.rs):parse_epss_csv(FIRST CSV),parse_kev_json(CISA),enrich()by CVE id so the flags are populated, not just honored. - ✅ OSV as the import format — DONE (
osv.rs+versions.rs+feeds.rs): single + batch OSV/GHSA/RUSTSEC JSON →Advisory; per-ecosystem range→exact expander (versions.rs: semver/PEP440-lite/maven-lite/loose + OSV introduced/fixed/last_affected windows;osv::parse_osv_ranges+expand_advisories); OpenSSF Malicious Packages feed (feeds::parse_malicious→ CriticalMAL-*). - ✅ Sigstore / SLSA verification for A3/A25/A26 — DONE (
provenance.rs):Attestation+ProvenanceRequirement::evaluate(min-SLSA / trusted-signer / source-repo match →scan::Provenance+ findings);attestation_delta(A25 signer/source change, appears/disappears). cosign/rekor wire-up stays dynamic-plane. - ✅ OSSF Scorecard signals as A14/A24 features — DONE (
provenance.rs):ScorecardSignals(overall, maintained?, branch-protection, signed-releases, dangerous-workflow) → advisoryFindings (dangerous-workflow → High, low score / unmaintained → Medium, no branch-protection → Low).parse_scorecardingests a rawscorecard --format jsonreport into those signals offline (thefeeds::parse_*shape: pure serde_json, pass atscore>=5, Dangerous-Workflow inverted 0=found/10=clean). Warehouse-persisted viaappend_scorecard/load_scorecard(subject-keyed, latest-wins), and driveable from the CLI —modgunn scorecard --path <report.json> [--subject <name>] [--warehouse <root>]folds a report, prints the signals + findings, and (with--warehouse) persists them forwarehouse provenance-logto read back. - ✅ Behavior/metadata features for A23 (the malicious-package model) — feature extraction DONE (
behavior.rs): [parse_npm_package] folds an npmpackage.json's auto-run install hooks (preinstall/install/postinstall/prepare) into [PackageBehavior] — install-script hooks, network-in-install (dropper stage-2 fetch),# URLs, max install-script Shannon entropy (obfuscation, reusingcore::shannon_entropy— the SAME metric the SCIP A17 tripwire keys on, no twin, L5), and the sensitive-API set (child_process/eval(/atob(//dev/tcp/…). [PackageBehavior::findings] ranks the feature vector intocore::Findings (network-in-install → High, sensitive-API / obfuscation → Medium, install-hook → Low). Offline, no code executed — thefeeds::parse_*shape. Second front-end landed: [parse_pypi_setup] reads a PyPIsetup.py(Python = #2 malware ecosystem; the whole file runs at sdist install, so it is scanned in full; the install-hook signal is acmdclassinstall/develop override) through the SAMEextract_from_install_textfolder into the SAME [PackageBehavior] — no twin. Sensitive-API table extended with Python primitives (os.system/subprocess/marshal.loads/pickle.loads/__import__(). Third front-end landed: [parse_pyproject_toml] reads a PEP 621/517pyproject.toml— but declarative TOML can't be whole-file-scanned (a[project] dependencies = ["urllib3"]would trip theurllibneedle, a[project.urls] Homepage = "https://…"thehttps://needle → a spuriousnetwork_in_installon nearly every honest project), so it is TOML- section-aware: it folds only the build/install-active sections ([build-system], a PEP 517 in-treebackend-path, acmdclasstable, a[tool.*.build.hooks…]build-hook) and skips the declarative[project]deps/URLs and the dependency-list keys (requires/dependencies/optional-dependencies) even inside an active section — through the SAMEextract_from_install_textfolder into the SAME [PackageBehavior], no twin. Implemented as a tiny line-scanner with notomldependency, so the pyproject front-end stays inside the leanscangate (thetomlcrate never enters the default binary — holger/static-holger purity, matching therustsec/manifestsgating). Fourth front-end landed: [parse_setup_cfg] reads a setuptoolssetup.cfg— declarative INI with the same false-positive hazard, so likewise section-aware: a hand-rolled INI line-scanner (tracks[section]+ indent- continuation, noini/configparsercrate, same lean-gate constraint —cargo tree --no-default-features --features scanpulls 0 ini/toml) that folds only the build/install-active keys ([options]cmdclass/dependency_links, a build-active[options.entry_points]group) and skips[metadata](incl.url/project_urls) and the declarative dep keys (install_requires/extras_require/python_requires/…) even inside[options]— through the SAMEextract_from_install_textfolder into the SAME [PackageBehavior], no twin. A benign declarativesetup.cfg(install_requiresincl.urllib3, a[metadata] url = https://…) is clean; adependency_linkspayload fetch +cmdclassoverride is flagged High + Low install-hook. The isolation-forest / behaviour-sequence scoring model over the feature vector is the remaining dynamic-plane consumer (TODO — needs a training corpus/model store, off-laptop). Driveable from the CLI (landed):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--fail-on-findingexits 3 so a promotion gate can block a suspicious package (tests/cli_behavior.rs). Warehouse-persisted (landed): with--warehousethe ecosystem-tagged vector + its derived verdict (worst severity + finding ids) is appended to thebehavior_featurestable via [Warehouse::append_package_behavior] — the SAME warehouse-persist plumbingscorecard --warehouseuses (no second seam, L5) — and read back by [load_package_behaviors] / thewarehouse behavior-logverb, a field-identical round-trip (mutation-verified). All behind thewarehousefeature; the leanscan/default gate is untouched (cargo tree --no-default-features --features scanpulls 0 skade/arrow).
7.4 Sources
SCA comparisons (Aikido, OX, ARMO, AppSecSanta); OpenSSF (Scorecard/SLSA/Sigstore/GUAC), deps.dev API blog, GUAC docs; Semgrep & CodeQL (taint/dataflow); EPSS/KEV/SSVC/VEX (Endor Labs, Picus, FIRST EPSS); Socket H1-2025 malicious-package report; arXiv: malicious-PyPI/npm ML detection (2412.05259, 2309.02637, 2606.19063), SBOM-graph attack-chain prediction (2604.04977). (Full URLs in the survey turn; re-fetch on demand.)
8. Open decisions
- Advisory-DB sourcing in airgap: offline OSV/CVE snapshots carried in a sealed znippy → imported into
cve_advisories(modgunn owns the table;db_revis the import revision and the verdict cache-key component). → full plan:cve-warehouse-ingest.md— all major vuln DBs (OSV.dev/NVD/GHSA/KEV/EPSS/distro) → Iceberg, range-native matching, sealed-znippy import pipeline, scan-all-deps join; phases P0–P6. - DONE — single-writer coordination.
Warehouse::open_read_only(src/warehouse.rs:182) is the read-only copied-snapshot path, lock-tolerant against a live writer; covered byopen_read_only_reads_while_writer_holds_the_lock(src/warehouse.rs:1681). - DONE — tripwire rule DSL for F7.
PolicyRule/PolicyTripwire(src/scip.rs:1858) is the declarative rule DSL (NoCapabilityGrowth,NoNewNativeCode,NoUnverifiedDependencyAdded,ForbidEcosystem), with adefault_policy()ruleset and test coverage (policy_tripwire_fires_on_declarative_rule_breaks,src/scip.rs:2796).
9. Consolidation seam evaluation (panels / skidbladnir / jera)
Per
workspace_nordisk/.nornir/consolidation-panels-maps-design.md. Recorded so a future agent does not re-litigate: modgunn's answer to the constellation seams.
- Panels / WASM-UI
Paneltrait — N/A. modgunn is a library + a thin offline CLI (src/main.rs): no GUI, no egui surface, no viz of its own. Its verdicts are rendered by nornir's Security pane (nornir owns the panel; modgunn owns thecore::Verdictshape it draws). Nothing to port to aPanelbackend here. - Skidbladnir (service/systemd/federation) — N/A. modgunn runs no daemon and holds no socket. It is consumed in-process by holger's gate and nornir's server; those hosts own the service/systemd layer. modgunn contributes no long-lived process to unify.
- jera (job handler: process | VM | container) — candidate, NOT adopted (by design). modgunn's fleet scan fans out in-process on znippy-zoomies'
gatling_for_each(scan_components,AdvisoryEngine::fleet) — a CPU-bound all-cores map over one shared read-only engine, milliseconds to low seconds even at the 1 M-artifact / 200 k-advisory bench point. That is not a process/VM/container job and must not become one: wrapping each verdict in a jera-tracked job would add scheduling + IPC overhead to the hottest loop modgunn has and regress the very hot path this repo guards (see.nornir/engine-benchmarks.md; the newmodgunn.fleet_scan_indexedLoki arm now watches it). jera becomes relevant ONLY if a world-delta materialization run (S3/S4: whole-graph PageRank / community detection / embedding refresh off aread_delta, master-plan §3–4 "Heavy" tier) should become a durable, tracked, restartable batch job — those are long (minutes+), coarse-grained, and worth checkpointing, unlike the per-artifact scan. Decision: defer. Keep the in-process gatling scan as-is; revisit jera only when the heavy incremental-materialization pipeline (§5 item 5, still TODO) is built, and then only at the run granularity, never per-verdict.
10. Common-framework parity (2026-07-14) — About · 🧰 Tools · lifecycle
Reconciles §9 with the suite-wide mandate ("every app shares — and does not duplicate — lifecycle, thin/fat, an About + a Tools menu whose demos start containers via jera"). modgunn now carries its slice of the shared shell, CONSUMING the shared crates (L5). See
framework-design.md/framework-guide.md.
- About —
about.rs(always compiled, serde_json only):modgunn about [--json]prints the identity (name · Móðguðr · version · both engines). Lean-gate safe (holger/static-holger keep it). - 🧰 Tools + jera demo —
tools.rs(featureframework):modgunn tools/modgunn demo advisory-feed --out <dir>. TheDemoAdvisoryFeedentry is ajera::container::ContainerSpec(pinned alpine,/outbind) run as a trackedjerajob (run_container, featureframework-oci) → writes a demo OSV doc →warehouse import --osv→warehouse scan. This is the coarse-grained container job §9 allowed (NOT the per-verdict hot path — that stays in-process gatling). No bespoke docker, nothread::spawn. - Lifecycle —
service.rs(featureframework):modgunn service --unitrenders modgunn's world-delta gatekeeper as askidbladnir::service::ServiceSpecthrough the sharedLifecycle(the ONE systemd renderer + dedicatedmodgunnsystem user,AgentMode::Fat). This updates §9's "skidbladnir N/A": modgunn now declares its (future) world-delta-watcher lifecycle through the ship rather than owning none — the watcher daemon itself (ExecStart = modgunn world-delta watch) is the one deferred piece. - Lean-gate proof.
jera+skidbladnirare optional (framework = ["dep:jera", "dep:skidbladnir", "dep:anyhow"];framework-oci = ["framework", "jera/container-oci"]).cargo tree --no-default-features --features scan→ 0 jera/skidbladnir/draupnir/bollard. - Move-to-edda candidates (copied-not-shared, noted not fixed this pass): (1) a shared About component — none exists; korp draws a bespoke egui About, so the
Identitymodel is afacett-about/eddacandidate. (2) the ToolMenu model + jobssink()adapter — korp'stools.rs/jobs.rsand modgunn's are near-identical; target ajera::jobs::workspace_sinkhelper + a genericToolMenuin an edda leaf.