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

mikronova/modgunn

.nornir/modgunn-master-plan.md

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; the repo rationale/decisions in 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)

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:

  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. 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)


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): 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):

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)

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

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.

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.