Gunnar

mikronova/modgunn

.nornir/modgunn-master-plan.md

<!-- nornir:class: reasoning -->

# 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 in [`engine-b-analyses.md`](engine-b-analyses.md); the repo
> rationale/decisions in
> [`nornir/.nornir/modgunn-security-scanner.md`](../../nornir/.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 consumes `core::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`](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:

1. **iceberg-rust 0.9.1 has NO native incremental/changelog scan** — only
   time-travel by `snapshot_id`. (The vendored `iceberg-arrow58` fork *defines*
   the changelog record vocabulary — `INSERT/DELETE/UPDATE_BEFORE/UPDATE_AFTER`
   in `metadata_columns.rs` — but exposes no scan that produces it.)
2. **skade reconstructs CDC itself from the manifest layer.** `skade::Table::read_delta(from, to)`
   walks the snapshot lineage (`parent_snapshot_id`) and returns a `DeltaPlan`:
   the data files **added** by the window (inserts) + the equality-delete files
   (identity **deletes**), with `needs_full_reload` set for overwrite/position-
   delete commits an additive delta can't express. So inserts + identity-deletes
   are already true CDC.
3. **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 DataFusion
   `sql` surface (skade `sql` feature) is the optional richer-query path.

> **Skade design drawing TODO:** add the `read_delta`/`DeltaPlan` CDC path to
> skade's SVG design diagrams (the snapshot-lineage walk → added data-files +
> equality-deletes → DeltaPlan, with the `needs_full_reload` fallback 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_delta` touched), 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`](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; |distinct versions| > 1 ∧ any vulnerable/unverified |
| 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)

1. ✅ **`WorldSnapshot` capture** → `world_snapshots` + `dep_edges` + `scip_symbols`
   Iceberg tables (`warehouse::capture_world_snapshot`, gated `scip`+`warehouse`).
2. ✅ **`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.
3. ✅ A1 (`CallGraphDeltaTripwire`) + A9 (`CapabilityDriftTripwire`) world-delta
   tripwires; A13 (`blast_radius`, PageRank) + A22 (`FingerprintDriftTripwire`).
4. ✅ **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`](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_provenance` ledger (#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.*
5. **Incremental refresh:** wire the heavy materializations (#11/#15) to recompute
   only the neighborhood `read_delta` touched — TODO (CDC seam `table_delta` ready).

---

## 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
>   `.md` import with the `patched`/`unaffected` → `RangeEvent` **inversion**, behind
>   the off-by-default `rustsec` feature (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** — `modgunn` dep on `features=["warehouse","rustsec"]` (skade
>   already a nornir path-dep, so no new tree); `security::import_advisory_db_into_warehouse`
>   (rustsec mirror → `cve_advisories`, wired into `security update-db`);
>   `security::verdict` re-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_cyclonedx` delegates to `modgunn::sbom`.
> - ✅ **N5** — `warehouse::Warehouse::open_read_only` (lock-tolerant copied-aside
>   snapshot, mirroring nornir's `open_read_only`; no skade change — skade stamps
>   absolute `file://` data paths, so a copied catalog still resolves live data).
>   The live `Mimir.SecurityScan` RPC 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: when
>   `cve_advisories` is populated it is derived from the same verdict scan (one
>   in-memory match, no OSV/rustsec double-match); `query_vulns` + the `vuln_findings`
>   cache remain only as the fallback for an un-imported warehouse (no regression).
>   An authoritative Pass/Warn/Block `decision` is emitted only when populated, else
>   omitted (no misleading PASS). Adversarially reviewed (10 agents, 0 confirmed
>   defects). `osv_query`/`advisory_db_query` are 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`, types `SecurityReport`/`Component`/`Vuln`. modgunn's
  `OsvScanner` + `warehouse` already 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 `Component`s → hands them to `modgunn::scan` → gets
  `Vuln`/`Verdict` back; persistence of `vuln_findings`/`sbom_components` is
  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:

1. **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_delta` makes it cheap.
2. **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.
3. **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.
4. **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, VEX `not_affected` suppression. 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` → Critical `MAL-*`).
- ✅ **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) → advisory `Finding`s (dangerous-workflow → **High**, low score /
  unmaintained → Medium, no branch-protection → Low). **`parse_scorecard`** ingests a raw
  `scorecard --format json` report into those signals offline (the `feeds::parse_*` shape:
  pure serde_json, pass at `score>=5`, Dangerous-Workflow inverted 0=found/10=clean).
  **Warehouse-persisted** via `append_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 for `warehouse provenance-log` to read back.
- ✅ **Behavior/metadata features** for A23 (the malicious-package model) —
  **feature extraction DONE** (`behavior.rs`): [`parse_npm_package`] folds an npm
  `package.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,
  reusing `core::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 into `core::Finding`s
  (network-in-install → **High**, sensitive-API / obfuscation → Medium, install-hook →
  Low). Offline, **no code executed** — the `feeds::parse_*` shape. **Second front-end
  landed**: [`parse_pypi_setup`] reads a PyPI `setup.py` (Python = #2 malware ecosystem;
  the whole file runs at sdist install, so it is scanned in full; the install-hook
  signal is a `cmdclass` install/develop override) through the SAME
  `extract_from_install_text` folder 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/517 `pyproject.toml` — but *declarative* TOML
  can't be whole-file-scanned (a `[project] dependencies = ["urllib3"]` would trip the
  `urllib` needle, a `[project.urls] Homepage = "https://…"` the `https://` needle → a
  spurious `network_in_install` on nearly every honest project), so it is TOML-
  **section-aware**: it folds 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 skips the declarative `[project]` deps/URLs and the dependency-list
  keys (`requires`/`dependencies`/`optional-dependencies`) even inside an active section
  — through the SAME `extract_from_install_text` folder into the SAME [`PackageBehavior`],
  no twin. Implemented as a tiny line-scanner with **no `toml` dependency**, so the
  pyproject front-end stays inside the lean `scan` gate (the `toml` crate never enters
  the default binary — holger/static-holger purity, matching the `rustsec`/`manifests`
  gating). **Fourth front-end landed**: [`parse_setup_cfg`] reads a setuptools
  `setup.cfg` — *declarative* INI with the same false-positive hazard, so likewise
  **section-aware**: a hand-rolled INI line-scanner (tracks `[section]` + indent-
  continuation, **no `ini`/`configparser` crate**, same lean-gate constraint — `cargo
  tree --no-default-features --features scan` pulls 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 SAME `extract_from_install_text` folder into the SAME
  [`PackageBehavior`], no twin. A benign declarative `setup.cfg` (`install_requires`
  incl. `urllib3`, a `[metadata] url = https://…`) is clean; a `dependency_links` payload
  fetch + `cmdclass` override 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-finding` exits 3 so a promotion gate can block a suspicious package
  (`tests/cli_behavior.rs`). **Warehouse-persisted** (landed): with `--warehouse` the
  ecosystem-tagged vector + its derived verdict (worst severity + finding ids) is appended to
  the `behavior_features` table via [`Warehouse::append_package_behavior`] — the SAME
  warehouse-persist plumbing `scorecard --warehouse` uses (no second seam, L5) — and read back
  by [`load_package_behaviors`] / the `warehouse behavior-log` verb, a field-identical round-trip
  (mutation-verified). All behind the `warehouse` feature; the lean `scan`/default gate is
  untouched (`cargo tree --no-default-features --features scan` pulls 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_rev` is the
  import revision and the verdict cache-key component). **→ full plan:**
  [`cve-warehouse-ingest.md`](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 by
  `open_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 a
  `default_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 `Panel` trait — 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 the
  `core::Verdict` shape it draws). Nothing to port to a `Panel` backend 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 new
  `modgunn.fleet_scan_indexed` Loki arm now watches it). jera becomes relevant ONLY
  if a *world-delta materialization run* (S3/S4: whole-graph PageRank / community
  detection / embedding refresh off a `read_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-design.md) / [`framework-guide.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` (feature `framework`): `modgunn tools` / `modgunn demo
  advisory-feed --out <dir>`. The `DemoAdvisoryFeed` entry is a `jera::container::ContainerSpec`
  (pinned alpine, `/out` bind) run **as a tracked `jera` job** (`run_container`, feature
  `framework-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, no `thread::spawn`.
- **Lifecycle** — `service.rs` (feature `framework`): `modgunn service --unit` renders modgunn's
  world-delta gatekeeper as a `skidbladnir::service::ServiceSpec` through the shared `Lifecycle`
  (the ONE systemd renderer + **dedicated `modgunn` system 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` + `skidbladnir` are 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 `Identity` model is a
  `facett-about`/`edda` candidate. (2) the **ToolMenu model + jobs `sink()` adapter** — korp's
  `tools.rs`/`jobs.rs` and modgunn's are near-identical; target a `jera::jobs::workspace_sink`
  helper + a generic `ToolMenu` in an edda leaf.