@clear-capabilities/agentic-security-scanner 0.175.0 → 0.177.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/agentic-security.js +206 -28
  3. package/dist/1919.index.js +1 -1
  4. package/dist/2066.index.js +851 -0
  5. package/dist/2199.index.js +1 -1
  6. package/dist/2584.index.js +3350 -0
  7. package/dist/2923.index.js +13 -0
  8. package/dist/2946.index.js +81 -0
  9. package/dist/3196.index.js +27 -0
  10. package/dist/3727.index.js +292 -0
  11. package/dist/37d64df006576248d0d8.json +286 -0
  12. package/dist/4168.index.js +509 -0
  13. package/dist/4281.index.js +993 -0
  14. package/dist/5099.index.js +96 -0
  15. package/dist/5456.index.js +291 -0
  16. package/dist/5616.index.js +1471 -0
  17. package/dist/5620.index.js +2494 -0
  18. package/dist/5817.index.js +714 -0
  19. package/dist/5937.index.js +672 -0
  20. package/dist/6333.index.js +98 -0
  21. package/dist/6655.index.js +3221 -0
  22. package/dist/7838.index.js +491 -974
  23. package/dist/818.index.js +4 -3
  24. package/dist/83.index.js +159 -0
  25. package/dist/8551.index.js +82 -0
  26. package/dist/8633.index.js +93 -0
  27. package/dist/8714.index.js +76 -76
  28. package/dist/8825.index.js +84 -0
  29. package/dist/932.index.js +186 -0
  30. package/dist/9560.index.js +358 -19
  31. package/dist/9800.index.js +6 -1
  32. package/dist/agentic-security.mjs +10 -10
  33. package/dist/agentic-security.mjs.sha256 +1 -1
  34. package/dist/b4289e728659450b5e19.js +32 -0
  35. package/package.json +8 -4
  36. package/src/artifact/CLAUDE.md +4 -0
  37. package/src/artifact/artifact-evidence.js +63 -0
  38. package/src/artifact/cosign.js +61 -0
  39. package/src/artifact/engine-source.js +72 -0
  40. package/src/artifact/oci-layout.js +47 -0
  41. package/src/artifact/registry.js +213 -0
  42. package/src/capabilities/tool-registry.js +2 -0
  43. package/src/lineage/CLAUDE.md +4 -0
  44. package/src/lineage/identity-token.js +11 -0
  45. package/src/lineage/observation-adapters.js +13 -0
  46. package/src/lineage/runtime-edge-states.js +139 -0
  47. package/src/lineage/runtime-explorer-view.js +82 -0
  48. package/src/lineage/runtime-observation.js +11 -0
  49. package/src/mcp/CLAUDE.md +3 -1
  50. package/src/mcp/proof-tools.js +132 -0
  51. package/src/mcp/server.js +1 -1
  52. package/src/mcp/tools.js +3 -1
  53. package/src/posture/CLAUDE.md +23 -0
  54. package/src/posture/artifact-registry.js +4 -0
  55. package/src/posture/custom-rules.js +54 -8
  56. package/src/posture/dnr/attack-facts.js +31 -0
  57. package/src/posture/dnr/cli.js +138 -0
  58. package/src/posture/dnr/detection-ir.js +123 -0
  59. package/src/posture/dnr/exploit-correlation.js +25 -0
  60. package/src/posture/dnr/exposure-window.js +134 -0
  61. package/src/posture/dnr/hunt.js +84 -0
  62. package/src/posture/dnr/log-reader.js +189 -0
  63. package/src/posture/dnr/matchers.js +90 -0
  64. package/src/posture/dnr/outputs.js +262 -0
  65. package/src/posture/dnr/payload-classes.js +120 -0
  66. package/src/posture/dnr/redact.js +25 -0
  67. package/src/posture/dnr/store.js +57 -0
  68. package/src/posture/dnr/validate.js +64 -0
  69. package/src/posture/dnr/view.js +51 -0
  70. package/src/posture/evaluation/lab/arm-ollama.js +88 -0
  71. package/src/posture/evaluation/lab/arms.js +142 -0
  72. package/src/posture/evaluation/lab/cli.js +135 -0
  73. package/src/posture/evaluation/lab/dataset.js +139 -0
  74. package/src/posture/evaluation/lab/materialize.js +44 -0
  75. package/src/posture/evaluation/lab/neutralize.js +144 -0
  76. package/src/posture/evaluation/lab/report.js +194 -0
  77. package/src/posture/evaluation/lab/run.js +225 -0
  78. package/src/posture/evaluation/lab/score.js +135 -0
  79. package/src/posture/evaluation/lab/scoreboard.js +48 -0
  80. package/src/posture/evaluation/lab/stats.js +41 -0
  81. package/src/posture/evaluation/lab/store.js +53 -0
  82. package/src/posture/fleet/cli.js +260 -63
  83. package/src/posture/fleet/discover-edges.js +135 -0
  84. package/src/posture/fleet/enterprise-graph.js +189 -0
  85. package/src/posture/fleet/fleet-graph.js +9 -4
  86. package/src/posture/fleet/fleet-manifest.js +50 -5
  87. package/src/posture/fleet/job-runner.js +153 -0
  88. package/src/posture/fleet/job-store.js +138 -0
  89. package/src/posture/fleet/lineage-summary.js +55 -0
  90. package/src/posture/fleet/org-health.js +56 -0
  91. package/src/posture/fleet/scan-cache.js +112 -0
  92. package/src/posture/fleet/scan-child.js +32 -0
  93. package/src/posture/proof-fabric/cli.js +34 -6
  94. package/src/posture/proof-fabric/container.js +57 -0
  95. package/src/posture/proof-fabric/fix-proof.js +2 -1
  96. package/src/posture/proof-fabric/generate-python.js +129 -0
  97. package/src/posture/proof-fabric/generate.js +214 -0
  98. package/src/posture/proof-fabric/reproducer.js +3 -0
  99. package/src/posture/proof-fabric/runner.js +44 -13
  100. package/src/posture/proof-fabric/siblings.js +77 -0
  101. package/src/posture/routing/policy-router.js +103 -0
  102. package/src/posture/rule-compiler/cli.js +101 -9
  103. package/src/posture/rule-compiler/corpus.js +13 -9
  104. package/src/posture/rule-compiler/ir.js +52 -9
  105. package/src/posture/rule-compiler/mutation.js +72 -1
  106. package/src/posture/rule-compiler/promotion.js +21 -5
  107. package/src/posture/rule-compiler/proposal.js +137 -0
  108. package/src/posture/rule-compiler/regression.js +95 -0
  109. package/src/posture/rule-compiler/store.js +45 -1
  110. package/src/posture/rule-pack-signing.js +4 -2
package/dist/818.index.js CHANGED
@@ -11,7 +11,7 @@ export const modules = {
11
11
  /* harmony export */ });
12
12
  /* harmony import */ var _states_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12090);
13
13
  /* harmony import */ var _orchestrator_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3740);
14
- /* harmony import */ var _runner_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2998);
14
+ /* harmony import */ var _runner_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(23081);
15
15
  /* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(37055);
16
16
  // Verified remediation, extended with exploit replay. This EXTENDS the fix-verify vocabulary (`verifiedFull`,
17
17
  // `degradedLegs`); it does not define a competing meaning of "verified".
@@ -27,7 +27,7 @@ export const modules = {
27
27
 
28
28
 
29
29
 
30
- async function verifyFixWithProof({ finding, reproducer, files, scanRoot, revision = null, policy = {}, staticVerification = null, siblings = null, signal, deps = {} } = {}) {
30
+ async function verifyFixWithProof({ finding, reproducer, files, scanRoot, revision = null, policy = {}, staticVerification = null, siblings = null, siblingSearchTruncated = [], signal, deps = {} } = {}) {
31
31
  const t0 = Date.now();
32
32
  const attemptFn = deps.attemptFn ?? _runner_js__WEBPACK_IMPORTED_MODULE_2__/* .runAttempt */ .Tm;
33
33
  const patchHash = (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_3__/* .sha256 */ .sc)((0,_reproducer_js__WEBPACK_IMPORTED_MODULE_3__/* .canonicalize */ .dw)(files || {}));
@@ -67,6 +67,7 @@ async function verifyFixWithProof({ finding, reproducer, files, scanRoot, revisi
67
67
  const siblingCount = Array.isArray(siblings) ? siblings.length : null;
68
68
  if (siblingCount === null) degraded.push('sibling-search-not-run');
69
69
  else if (siblingCount > 0) degraded.push(`sibling-exposures:${siblingCount}`);
70
+ if (siblingCount !== null && siblingSearchTruncated?.length) degraded.push('sibling-search-incomplete');
70
71
 
71
72
  const full = degraded.length === 0;
72
73
  // Same split as fix-verify: `ok` (nothing that ran failed) earns FIX_VERIFIED; `completeness` says whether anything was skipped.
@@ -99,7 +100,7 @@ function formatFixProof(r) {
99
100
  /* unused harmony exports independentVerify, applySeverityPolicy, _newRunId */
100
101
  /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
101
102
  /* harmony import */ var _states_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12090);
102
- /* harmony import */ var _runner_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2998);
103
+ /* harmony import */ var _runner_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(23081);
103
104
  /* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(37055);
104
105
  /* harmony import */ var _proof_tier_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40216);
105
106
  // Proof orchestration: finding + revision + reproducer + policy -> structured evidence.
@@ -0,0 +1,159 @@
1
+ export const id = 83;
2
+ export const ids = [83];
3
+ export const modules = {
4
+
5
+ /***/ 20083:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ __webpack_require__.r(__webpack_exports__);
9
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
+ /* harmony export */ EDGE_STATES: () => (/* binding */ EDGE_STATES),
11
+ /* harmony export */ deriveEdgeStates: () => (/* binding */ deriveEdgeStates),
12
+ /* harmony export */ identitiesUsingRiskyPaths: () => (/* binding */ identitiesUsingRiskyPaths),
13
+ /* harmony export */ runtimeFlowsAbsentFromStatic: () => (/* binding */ runtimeFlowsAbsentFromStatic),
14
+ /* harmony export */ validateExploitEvidence: () => (/* binding */ validateExploitEvidence)
15
+ /* harmony export */ });
16
+ // Edge states and the identity-aware / absent-from-static queries of the Runtime-Corroborated Security Twin (PRD 2: "Edge States",
17
+ // section 6). Pure: no fs, no I/O, never throws, never mutates the graph. It READS what `observation-correlation.js` and the
18
+ // observation store already hold; it adds no second matching engine and writes nothing into the static graph (the overlay stays an
19
+ // overlay).
20
+ //
21
+ // The six states, and when each is ALLOWED to appear:
22
+ // STATIC_ONLY no telemetry was consulted at all (not "nothing seen": nothing was looked at)
23
+ // RUNTIME_NOT_OBSERVED telemetry was consulted for this environment/window and no trusted observation maps to the edge.
24
+ // Never "unreachable": the window may be short, the exporter partial, the mapping weak.
25
+ // RUNTIME_OBSERVED at least one high- or medium-confidence observation maps to the edge
26
+ // RUNTIME_BLOCKED observations map to the edge and every decision recorded for them was `denied` (the path was
27
+ // attempted and refused); an edge with both allowed and denied observations carries OBSERVED and BLOCKED
28
+ // RUNTIME_POLICY_VIOLATION the edge was observed (not only denied) AND a flow through it is `prohibited` by the privacy policy
29
+ // RUNTIME_EXPLOIT_CORRELATED the edge was observed AND the operator supplied explicit evidence naming this edge/flow/node as the
30
+ // target of a proven exploit. Never inferred from observation alone: seeing traffic is not seeing an attack.
31
+ // Low-confidence and ambiguous observations (PRD: "low-confidence observations remain unmatched") never create a state; they are
32
+ // counted in `uncertainObservations` so the weakness is visible instead of silently dropped.
33
+ const EDGE_STATES = Object.freeze(['STATIC_ONLY', 'RUNTIME_NOT_OBSERVED', 'RUNTIME_OBSERVED', 'RUNTIME_BLOCKED', 'RUNTIME_POLICY_VIOLATION', 'RUNTIME_EXPLOIT_CORRELATED']);
34
+ const SEVERITY_ORDER = ['RUNTIME_EXPLOIT_CORRELATED', 'RUNTIME_POLICY_VIOLATION', 'RUNTIME_BLOCKED', 'RUNTIME_OBSERVED', 'RUNTIME_NOT_OBSERVED', 'STATIC_ONLY'];
35
+ const TRUSTED = new Set(['high', 'medium']);
36
+
37
+ const arr = (v) => (Array.isArray(v) ? v : []);
38
+ const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
39
+
40
+ /** Operator-supplied exploit evidence: [{ kind, id, edgeId?|flowId?|nodeId? }]. Anything malformed is reported, not trusted. */
41
+ function validateExploitEvidence(list) {
42
+ const errors = []; const ok = [];
43
+ if (list === null || list === undefined) return { evidence: [], errors };
44
+ if (!Array.isArray(list)) return { evidence: [], errors: ['exploit evidence must be an array'] };
45
+ list.forEach((e, i) => {
46
+ if (!isObj(e) || typeof e.id !== 'string' || !e.id || !['proof', 'dnr-hunt', 'incident'].includes(e.kind)) { errors.push(`entry ${i}: needs kind (proof|dnr-hunt|incident) and id`); return; }
47
+ const targets = ['edgeId', 'flowId', 'nodeId'].filter((k) => typeof e[k] === 'string' && e[k]);
48
+ if (targets.length !== 1) { errors.push(`entry ${i}: name exactly one of edgeId, flowId, nodeId`); return; }
49
+ ok.push({ kind: e.kind, id: e.id.slice(0, 120), [targets[0]]: e[targets[0]] });
50
+ });
51
+ return { evidence: ok, errors };
52
+ }
53
+
54
+ /**
55
+ * @returns {{ evaluated: boolean, byNode: Record<string, {states: string[], observations: number, allowed: number, denied: number}>, byEdge: Record<string, {states: string[], primary: string, observations: number, allowed: number, denied: number, uncertainObservations: number, identityTokens: string[], exploitEvidence: object[], note: string}>, counts: Record<string, number>, limitations: string[] }}
56
+ */
57
+ function deriveEdgeStates(graph, correlation, observations, { exploitEvidence = [] } = {}) {
58
+ const edges = arr(graph?.edges).filter((e) => isObj(e) && typeof e.id === 'string');
59
+ const flows = arr(graph?.flows).filter((f) => isObj(f) && typeof f.id === 'string');
60
+ const evaluated = !!correlation?.evaluated;
61
+ const considered = new Set(arr(correlation?.consideredObservationIds));
62
+ const byEdge = {}; const counts = Object.fromEntries(EDGE_STATES.map((s) => [s, 0]));
63
+ const limitations = [
64
+ 'RUNTIME_NOT_OBSERVED means "no trusted observation in this window", never "unreachable" or "safe".',
65
+ 'Only high- and medium-confidence mappings count; low-confidence and ambiguous observations stay unmatched and are counted as uncertain.',
66
+ 'RUNTIME_EXPLOIT_CORRELATED requires explicit operator-supplied evidence naming the edge, flow or node; observation alone never sets it.',
67
+ ];
68
+ const flowsByEdge = new Map();
69
+ for (const f of flows) for (const eid of arr(f.edgeIds)) { const a = flowsByEdge.get(eid) ?? []; a.push(f); flowsByEdge.set(eid, a); }
70
+ const obsByEdge = new Map();
71
+ for (const o of arr(observations)) {
72
+ if (!isObj(o) || (evaluated && considered.size && !considered.has(o.id))) continue;
73
+ for (const eid of arr(o.matchedEdgeIds)) { const a = obsByEdge.get(eid) ?? []; a.push(o); obsByEdge.set(eid, a); }
74
+ }
75
+ for (const e of edges) {
76
+ if (!evaluated) { byEdge[e.id] = entry(['STATIC_ONLY'], { note: 'no telemetry was consulted' }); counts.STATIC_ONLY++; continue; }
77
+ const all = obsByEdge.get(e.id) ?? [];
78
+ const trusted = all.filter((o) => TRUSTED.has(o.matchConfidence));
79
+ const uncertain = all.length - trusted.length;
80
+ if (!trusted.length) { byEdge[e.id] = entry(['RUNTIME_NOT_OBSERVED'], { uncertain, note: uncertain ? `${uncertain} low-confidence observation(s) not counted` : 'no observation in this window' }); counts.RUNTIME_NOT_OBSERVED++; continue; }
81
+ const dec = (o) => o.attributes?.['network.securityDecision'];
82
+ const allowed = trusted.filter((o) => dec(o) === 'allowed').length; const denied = trusted.filter((o) => dec(o) === 'denied').length;
83
+ const states = [];
84
+ const onlyDenied = denied > 0 && allowed === 0 && trusted.every((o) => dec(o) === 'denied');
85
+ if (!onlyDenied) states.push('RUNTIME_OBSERVED');
86
+ if (denied > 0) states.push('RUNTIME_BLOCKED');
87
+ const prohibited = flowsByEdge.get(e.id)?.some((f) => f.policyVerdict === 'prohibited');
88
+ if (!onlyDenied && prohibited) states.push('RUNTIME_POLICY_VIOLATION');
89
+ const flowIds = new Set((flowsByEdge.get(e.id) ?? []).map((f) => f.id));
90
+ const nodeIds = new Set([e.source, e.target, e.from, e.to].filter(Boolean));
91
+ const ev = arr(exploitEvidence).filter((x) => x.edgeId === e.id || (x.flowId && flowIds.has(x.flowId)) || (x.nodeId && nodeIds.has(x.nodeId)));
92
+ if (!onlyDenied && ev.length) states.push('RUNTIME_EXPLOIT_CORRELATED');
93
+ const tokens = [...new Set(trusted.map((o) => o.attributes?.['identity.token']).filter(Boolean))].sort();
94
+ byEdge[e.id] = entry(states, { observations: trusted.length, allowed, denied, uncertain, tokens, ev, note: '' });
95
+ for (const s of states) counts[s]++;
96
+ }
97
+ // Observations the matcher placed on a NODE but on no edge or flow (a queue publish, a store write with no modelled path into it).
98
+ // They still carry the allow/deny fact, so the refused queue publish is visible instead of vanishing between edge and node.
99
+ const byNode = {};
100
+ if (evaluated) {
101
+ const perNode = new Map();
102
+ for (const o of arr(observations)) {
103
+ if (!isObj(o) || (considered.size && !considered.has(o.id)) || !TRUSTED.has(o.matchConfidence) || arr(o.matchedEdgeIds).length) continue;
104
+ for (const nid of arr(o.matchedNodeIds)) { const a = perNode.get(nid) ?? []; a.push(o); perNode.set(nid, a); }
105
+ }
106
+ for (const [nid, list] of perNode) {
107
+ const dec = (o) => o.attributes?.['network.securityDecision'];
108
+ const denied = list.filter((o) => dec(o) === 'denied').length; const allowed = list.filter((o) => dec(o) === 'allowed').length;
109
+ const onlyDenied = denied > 0 && denied === list.length;
110
+ byNode[nid] = { states: [...(onlyDenied ? [] : ['RUNTIME_OBSERVED']), ...(denied ? ['RUNTIME_BLOCKED'] : [])], observations: list.length, allowed, denied };
111
+ }
112
+ }
113
+ return { evaluated, byEdge, byNode, counts, limitations };
114
+
115
+ function entry(states, { observations = 0, allowed = 0, denied = 0, uncertain = 0, tokens = [], ev = [], note = '' } = {}) {
116
+ const primary = SEVERITY_ORDER.find((s) => states.includes(s));
117
+ return { states, primary, observations, allowed, denied, uncertainObservations: uncertain, identityTokens: tokens, exploitEvidence: ev, note };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * "Show runtime flows absent from the static graph": the observations the matcher could NOT place on any static node, reduced to the
123
+ * destination identifiers the closed-world schema already allows. These are CANDIDATES. The matcher failing to place an observation
124
+ * can mean a path the static analysis missed, or a mapping the matcher is too weak to make; this lists them, it does not decide.
125
+ */
126
+ function runtimeFlowsAbsentFromStatic(observations, correlation) {
127
+ const unmatched = new Set(arr(correlation?.unmatchedObservationIds));
128
+ const rows = [];
129
+ for (const o of arr(observations)) {
130
+ if (!isObj(o) || !unmatched.has(o.id)) continue;
131
+ // an observation the matcher placed on a static NODE is not absent from the static graph; it only lacks a modelled flow
132
+ if (arr(o.matchedNodeIds).length || arr(o.matchedEdgeIds).length) continue;
133
+ const a = isObj(o.attributes) ? o.attributes : {};
134
+ const dest = ['destination.host', 'destination.service', 'destination.port', 'destination.scheme'].filter((k) => a[k] !== undefined).map((k) => `${k.split('.')[1]}=${a[k]}`).join(' ');
135
+ if (!dest) continue;
136
+ rows.push({ observationId: o.id, destination: dest, service: a['service.name'] ?? null, environment: o.environment ?? null, firstObservedAt: o.firstObservedAt ?? null, lastObservedAt: o.lastObservedAt ?? null, eventCountBand: o.eventCountBand ?? null });
137
+ }
138
+ rows.sort((x, y) => (x.destination < y.destination ? -1 : x.destination > y.destination ? 1 : 0));
139
+ return { candidates: rows, note: 'Unmatched observations are candidates, not proof that static analysis missed a path: the matcher may be too weak to place them.' };
140
+ }
141
+
142
+ /** "Show identities using risky paths": identity TOKENS (never identities) seen on edges in the risky states, with a band, never an exact count. */
143
+ function identitiesUsingRiskyPaths(edgeStates) {
144
+ const risky = new Set(['RUNTIME_POLICY_VIOLATION', 'RUNTIME_EXPLOIT_CORRELATED']);
145
+ const byToken = new Map();
146
+ for (const [edgeId, e] of Object.entries(edgeStates?.byEdge ?? {})) {
147
+ if (!e.states.some((s) => risky.has(s))) continue;
148
+ for (const t of e.identityTokens) { const x = byToken.get(t) ?? { token: t, edges: [], states: new Set() }; x.edges.push(edgeId); e.states.filter((s) => risky.has(s)).forEach((s) => x.states.add(s)); byToken.set(t, x); }
149
+ }
150
+ return {
151
+ identities: [...byToken.values()].map((x) => ({ token: x.token, edgeIds: x.edges.sort(), states: [...x.states].sort() })).sort((a, b) => (a.token < b.token ? -1 : 1)),
152
+ note: 'Tokens are keyed hashes under this install\'s key; they identify "the same identity" but never reveal who. Edges with no identity telemetry contribute nothing, which is not evidence that no identity used them.',
153
+ };
154
+ }
155
+
156
+
157
+ /***/ })
158
+
159
+ };
@@ -0,0 +1,82 @@
1
+ export const id = 8551;
2
+ export const ids = [8551];
3
+ export const modules = {
4
+
5
+ /***/ 58551:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ __webpack_require__.r(__webpack_exports__);
9
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
+ /* harmony export */ hasTamperedRecords: () => (/* binding */ hasTamperedRecords),
11
+ /* harmony export */ latestRecord: () => (/* binding */ latestRecord),
12
+ /* harmony export */ listRecords: () => (/* binding */ listRecords),
13
+ /* harmony export */ saveRecord: () => (/* binding */ saveRecord)
14
+ /* harmony export */ });
15
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
16
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(76760);
17
+ /* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31174);
18
+ /* harmony import */ var _encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(39984);
19
+ /* harmony import */ var _proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(37055);
20
+ /* harmony import */ var _proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(62199);
21
+ // Persistence for the detection-and-response loop: `.agentic-security/dnr/<findingKey>/<kind>-<digest>.json`.
22
+ // Same machinery and the same guarantees as the proof store, reused rather than copied where it is exported:
23
+ // safe state-directory writes (no symlinks, no unsafe dirs), the encryption policy, retention as evidence, legal holds, and
24
+ // a recursive secret scrub before anything touches disk. A record that no longer matches its own digest is returned FLAGGED,
25
+ // never trusted and never silently dropped.
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ const DIR = 'dnr';
34
+ const MAX_RECORD_BYTES = 1024 * 1024;
35
+ const RECORD_KINDS = Object.freeze(['detection', 'hunt', 'window', 'fix']);
36
+
37
+ const digestOf = (record) => { const { recordDigest: _omit, ...rest } = record; return (0,_proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__/* .sha256 */ .sc)((0,_proof_fabric_reproducer_js__WEBPACK_IMPORTED_MODULE_4__/* .canonicalize */ .dw)(rest)); };
38
+
39
+ function saveRecord(root, findingId, kind, record) {
40
+ if (!RECORD_KINDS.includes(kind)) return { ok: false, reason: `unknown record kind: ${kind}` };
41
+ const safe = (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.scrubDeep)(record);
42
+ safe.schema = `agentic-security/dnr-${kind}-record`;
43
+ safe.kind = kind;
44
+ safe.findingId = String(findingId);
45
+ safe.savedAt = record.savedAt ?? new Date().toISOString();
46
+ safe.recordDigest = digestOf(safe);
47
+ const text = JSON.stringify(safe, null, 2);
48
+ if (Buffer.byteLength(text) > MAX_RECORD_BYTES) return { ok: false, reason: 'record exceeds the size limit' };
49
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.ensureStateDir)(root)) return { ok: false, reason: 'no safe state directory for this project' };
50
+ const enc = (0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .maybeEncryptForWrite */ .v2)(root, DIR, text);
51
+ if (!enc.ok) return { ok: false, reason: enc.reason };
52
+ const file = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(root, DIR, (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.findingKey)(findingId), `${kind}-${safe.recordDigest.slice(0, 16)}.json`);
53
+ if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.safeWriteState)(file, enc.content)) return { ok: false, reason: 'state write refused' };
54
+ return { ok: true, file, digest: safe.recordDigest, encrypted: enc.encrypted };
55
+ }
56
+
57
+ /** Newest-first records of one kind for a finding. Tampered records are flagged. */
58
+ function listRecords(root, findingId, kind) {
59
+ const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(root, DIR, (0,_proof_fabric_store_js__WEBPACK_IMPORTED_MODULE_5__.findingKey)(findingId));
60
+ let names = [];
61
+ try { names = node_fs__WEBPACK_IMPORTED_MODULE_0__.readdirSync(dir).filter((n) => n.startsWith(`${kind}-`) && n.endsWith('.json')); } catch { return []; }
62
+ const out = [];
63
+ for (const n of names) {
64
+ const file = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, n);
65
+ try {
66
+ const rec = JSON.parse((0,_encryption_provider_js__WEBPACK_IMPORTED_MODULE_3__.maybeDecryptForRead)(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file, 'utf8')));
67
+ out.push({ file, record: rec, tampered: rec.recordDigest !== digestOf(rec), mtime: node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(file).mtimeMs });
68
+ } catch { out.push({ file, record: null, tampered: true, mtime: 0 }); }
69
+ }
70
+ return out.sort((a, b) => b.mtime - a.mtime);
71
+ }
72
+
73
+ /** Newest intact record of a kind, or null. A tampered newest record does NOT fall through silently: the caller can see it via listRecords. */
74
+ function latestRecord(root, findingId, kind) {
75
+ return listRecords(root, findingId, kind).find((r) => r.record && !r.tampered) ?? null;
76
+ }
77
+ const hasTamperedRecords = (root, findingId, kind) => listRecords(root, findingId, kind).some((r) => r.tampered);
78
+
79
+
80
+ /***/ })
81
+
82
+ };
@@ -0,0 +1,93 @@
1
+ export const id = 8633;
2
+ export const ids = [8633];
3
+ export const modules = {
4
+
5
+ /***/ 98633:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ EngineError: () => (/* binding */ EngineError),
10
+ /* harmony export */ saveFromEngine: () => (/* binding */ saveFromEngine)
11
+ /* harmony export */ });
12
+ /* unused harmony export ENGINE_LIMITS */
13
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
14
+ /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
15
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
16
+ /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(31421);
17
+ // Read an image from a LOCAL container engine (`docker save` / `podman save`), for `image scan --engine <name>` (PRD 5, requirement 1:
18
+ // "local image"). The engine produces a docker-archive tar, which then goes through the same hardened parser as any other archive.
19
+ //
20
+ // Nothing here runs the image: `save` only exports it. The engine is a process like any other in this codebase, so it has what the
21
+ // shared contract asks of one: a timeout, cancellation, process-group cleanup, and a byte cap on what it may produce. The cap
22
+ // matters because the output is streamed to a temporary file and counted as it arrives: an engine (or a mislabelled "engine") that
23
+ // writes without end is stopped at the cap instead of filling the disk. The temporary file is removed on every exit path.
24
+
25
+
26
+
27
+
28
+
29
+ class EngineError extends Error {
30
+ constructor(message, code) { super(message); this.name = 'EngineError'; this.code = code; }
31
+ }
32
+
33
+ const ENGINE_LIMITS = Object.freeze({ timeoutMs: 5 * 60_000, maxBytes: 2 * 1024 * 1024 * 1024 });
34
+ const RUNTIMES = Object.freeze(['docker', 'podman']);
35
+ // a reference, never an option: it must start with a letter or digit, so it can never be read as a flag
36
+ const REF_RE = /^[A-Za-z0-9][A-Za-z0-9._\/:@+-]{0,254}$/;
37
+
38
+ function findTool(name, pathEnv = process.env.PATH || '') {
39
+ for (const dir of pathEnv.split(node_path__WEBPACK_IMPORTED_MODULE_2__.delimiter)) {
40
+ if (!dir) continue;
41
+ const p = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, name);
42
+ try { node_fs__WEBPACK_IMPORTED_MODULE_0__.accessSync(p, node_fs__WEBPACK_IMPORTED_MODULE_0__.constants.X_OK); if (node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(p).isFile()) return node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(p); } catch { /* next */ }
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function saveFromEngine(ref, { runtime = 'docker', limits = ENGINE_LIMITS, signal = null, pathEnv = process.env.PATH } = {}) {
48
+ const L = { ...ENGINE_LIMITS, ...limits };
49
+ if (!RUNTIMES.includes(runtime)) return Promise.reject(new EngineError(`the container engine must be one of ${RUNTIMES.join(', ')}`, 'bad_runtime'));
50
+ if (!REF_RE.test(String(ref ?? '')) || String(ref).includes('..')) return Promise.reject(new EngineError('not a valid image reference', 'bad_reference'));
51
+ const bin = findTool(runtime, pathEnv);
52
+ if (!bin) return Promise.reject(new EngineError(`${runtime} is not installed or not on PATH; nothing was read`, 'engine_unavailable'));
53
+ const dir = node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.tmpdir(), 'engine-save-'));
54
+ const file = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, 'image.tar');
55
+ const cleanup = () => { try { node_fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ } };
56
+ return new Promise((resolve, reject) => {
57
+ const out = node_fs__WEBPACK_IMPORTED_MODULE_0__.createWriteStream(file, { mode: 0o600 });
58
+ out.on('error', (e) => fail(new EngineError(`could not write the export: ${e.code ?? e.message}`, 'engine_error')));
59
+ const child = (0,node_child_process__WEBPACK_IMPORTED_MODULE_3__.spawn)(bin, ['save', ref], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
60
+ let bytes = 0; let stderr = ''; let settled = false; let reason = null;
61
+ const kill = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { /* gone */ } } };
62
+ const fail = (e) => { if (settled) return; settled = true; clearTimeout(timer); signal?.removeEventListener('abort', onAbort); kill(); out.destroy(); cleanup(); reject(e); };
63
+ const timer = setTimeout(() => { reason = 'timeout'; fail(new EngineError(`${runtime} save did not finish within ${Math.round(L.timeoutMs / 1000)}s`, 'timeout')); }, L.timeoutMs);
64
+ const onAbort = () => fail(new EngineError('cancelled', 'cancelled'));
65
+ if (signal) { if (signal.aborted) { onAbort(); return; } signal.addEventListener('abort', onAbort, { once: true }); }
66
+ child.stdout.on('data', (d) => {
67
+ if (settled) return;
68
+ bytes += d.length;
69
+ if (bytes > L.maxBytes) { fail(new EngineError(`the image export is larger than ${L.maxBytes} bytes`, 'too_large')); return; }
70
+ out.write(d);
71
+ });
72
+ child.stderr.on('data', (d) => { if (stderr.length < 4096) stderr += d; });
73
+ child.on('error', (e) => fail(new EngineError(`could not start ${runtime}: ${e.code ?? e.message}`, 'engine_unavailable')));
74
+ child.on('close', (code) => {
75
+ if (settled) return;
76
+ out.end(() => {
77
+ if (settled) return;
78
+ if (code !== 0) return fail(new EngineError(`${runtime} save failed: ${stderr.trim().split('\n').pop()?.slice(0, 200) || `exit ${code}`}`, /no such image|not found|unable to find/i.test(stderr) ? 'not_found' : 'engine_error'));
79
+ try {
80
+ const buf = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(file);
81
+ settled = true; clearTimeout(timer); signal?.removeEventListener('abort', onAbort); cleanup();
82
+ resolve({ tar: buf, source: { kind: 'engine', runtime, reference: ref, bytes: buf.length } });
83
+ } catch (e) { fail(new EngineError(`could not read the exported image: ${e.code ?? e.message}`, 'engine_error')); }
84
+ });
85
+ });
86
+ void reason;
87
+ });
88
+ }
89
+
90
+
91
+ /***/ })
92
+
93
+ };
@@ -2,94 +2,94 @@ export const id = 8714;
2
2
  export const ids = [8714];
3
3
  export const modules = {
4
4
 
5
- /***/ 78714:
5
+ /***/ 58714:
6
6
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
7
 
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
- /* harmony export */ persistLanguageArtifacts: () => (/* binding */ persistLanguageArtifacts)
9
+ /* harmony export */ renderRuntimeExplorer: () => (/* binding */ renderRuntimeExplorer),
10
+ /* harmony export */ vulnerableFunctionsInProduction: () => (/* binding */ vulnerableFunctionsInProduction)
10
11
  /* harmony export */ });
11
- /* unused harmony exports LANGUAGE_ANALYSIS_FILE, LANGUAGE_BOM_FILE, LANGUAGE_ARTIFACT_SCHEMA, hasLanguageContent, buildLanguageArtifacts */
12
- /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
13
- /* harmony import */ var _posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(31174);
14
- /* harmony import */ var _posture_integrity_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(71130);
15
- /* harmony import */ var _context_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(52603);
16
- // Haskell and Nix analysis metadata as registered state artifacts (X-016).
12
+ // The runtime half of the Data Flow Explorer as ONE static, script-free HTML page (PRD 2 section 5). Pure: it renders what
13
+ // `runtime-edge-states.js` and the correlation already computed, writes nothing, and runs nothing in the browser. It exists as a page
14
+ // rather than a route on the local explore server because that server's contract (`server/CLAUDE.md`) deliberately serves only the
15
+ // signed static graph; widening it is a security decision of its own. A page needs no server, no token and no script.
17
16
  //
18
- // An ordinary scan already leaves last-scan.json. Two things a reviewer, auditor or retention policy needs about the
19
- // Haskell/Nix side were not artifacts of their own: WHAT analysed the sources (parser/resolver outcomes, capability
20
- // status, limitations, optional modes) and WHICH components the language BOM recorded. They are written beside
21
- // last-scan.json as `language-analysis.json` and `language-bom.json`, each with a detached signature, registered in
22
- // posture/artifact-registry.js (so export, retention, legal hold and `reset` all see them), and described by a record
23
- // that carries sha256 digests of every input and of the artifact bodies. No source text is ever written: paths and
24
- // digests only, so the file is metadata, not a copy of customer code.
25
-
26
-
27
-
28
-
29
-
17
+ // Five views, in the order the PRD names them: Static, Runtime, Static vs Runtime, Observed High-Risk Paths, Unexpected Runtime Paths.
18
+ // Clicking (expanding) an edge shows BOTH its static evidence and its runtime evidence. Every flow is also shown as a path
19
+ // ("source -> ... -> sink") with the state of each hop, which is the PRD's "credit-card field -> API -> payments service -> DB" view.
20
+ //
21
+ // Everything interpolated into the page is escaped. Attribute values (hosts, services) came from telemetry the operator supplied and
22
+ // are treated as hostile text. The page states, at the top, what absence of observation does and does not mean.
23
+ const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
24
+ const arr = (v) => (Array.isArray(v) ? v : []);
25
+ const SENSITIVE = new Set(['PII', 'PHI', 'PCI', 'FIN', 'CREDENTIALS']);
26
+ const CSS = 'body{font:14px/1.45 system-ui,sans-serif;margin:24px auto;max-width:1100px;padding:0 16px;color:#1b1f24;background:#fff}'
27
+ + '@media(prefers-color-scheme:dark){body{background:#14171a;color:#e6e8ea}}'
28
+ + 'table{border-collapse:collapse;width:100%;margin:8px 0}td,th{border:1px solid #8886;padding:4px 8px;text-align:left;vertical-align:top}'
29
+ + 'summary{cursor:pointer}.s{display:inline-block;padding:0 6px;border-radius:4px;border:1px solid #8888;font-size:12px;margin-right:4px}'
30
+ + '.RUNTIME_BLOCKED,.RUNTIME_POLICY_VIOLATION,.RUNTIME_EXPLOIT_CORRELATED{border-color:#c0392b;color:#c0392b}.RUNTIME_OBSERVED{border-color:#1e8449;color:#1e8449}'
31
+ + '.note{border-left:4px solid #8888;padding:4px 12px;margin:12px 0}nav a{margin-right:12px}code{font-size:12px}';
30
32
 
31
- const LANGUAGE_ANALYSIS_FILE = 'language-analysis.json';
32
- const LANGUAGE_BOM_FILE = 'language-bom.json';
33
- const LANGUAGE_ARTIFACT_SCHEMA = 'agentic-security/language-analysis@1';
33
+ function renderRuntimeExplorer({ graph, edgeStates, correlation, observations = [], absent = { candidates: [] }, title = 'Runtime twin' }) {
34
+ const nodes = new Map(arr(graph?.nodes).map((n) => [n.id, n]));
35
+ const edges = arr(graph?.edges); const flows = arr(graph?.flows);
36
+ const dataEls = new Map(arr(graph?.dataElements).map((d) => [d.id, d]));
37
+ const label = (id) => esc(nodes.get(id)?.label ?? id);
38
+ const stateOf = (eid) => edgeStates?.byEdge?.[eid];
39
+ const obsByEdge = new Map();
40
+ for (const o of arr(observations)) for (const eid of arr(o.matchedEdgeIds)) { const a = obsByEdge.get(eid) ?? []; a.push(o); obsByEdge.set(eid, a); }
41
+ const chips = (states) => arr(states).map((s) => `<span class="s ${esc(s)}">${esc(s)}</span>`).join('');
42
+ const flowClasses = (f) => arr(f.dataElementIds).flatMap((id) => arr(dataEls.get(id)?.dataClasses));
43
+ const observedEdge = (eid) => stateOf(eid)?.states?.includes('RUNTIME_OBSERVED');
34
44
 
35
- const sha256 = (s) => node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(s).digest('hex');
36
- function canonical(v) {
37
- if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
38
- if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;
39
- return JSON.stringify(v === undefined ? null : v);
40
- }
45
+ const staticRows = edges.map((e) => `<tr><td>${label(e.from)} &rarr; ${label(e.to)}</td><td><code>${esc(e.id)}</code></td><td>${esc(e.protocol?.name ?? e.protocol?.kind ?? '')}</td><td>${esc(arr(e.boundaryCrossings).join(', '))}</td></tr>`).join('');
46
+ const runtimeRows = edges.map((e) => { const s = stateOf(e.id); return `<tr><td>${label(e.from)} &rarr; ${label(e.to)}</td><td>${chips(s?.states)}</td><td>${s?.observations ?? 0}</td><td>${esc(s?.identityTokens?.length ?? 0)}</td></tr>`; }).join('');
47
+ const bothRows = edges.map((e) => { const s = stateOf(e.id); return `<tr><td>${label(e.from)} &rarr; ${label(e.to)}</td><td>${esc(arr(e.boundaryCrossings).length ? 'crosses a boundary' : 'internal')}, ${esc(e.provenance ?? '')}</td><td>${chips(s?.states)}${s?.uncertainObservations ? ` <small>(${s.uncertainObservations} uncertain, not counted)</small>` : ''}</td></tr>`; }).join('');
41
48
 
42
- /** True when the scan has anything Haskell/Nix to record. */
43
- function hasLanguageContent(scan) {
44
- const lc = scan && scan.scanHealth && scan.scanHealth.languageCoverage;
45
- const files = lc && lc.totals && Number(lc.totals.files || lc.totals.analyzed || 0);
46
- return !!((lc && (files > 0 || (lc.capabilities && Object.keys(lc.capabilities).length))) || (scan && scan.languageBom && (scan.languageBom.components || []).length));
47
- }
49
+ const detail = (e) => {
50
+ const s = stateOf(e.id); const obs = obsByEdge.get(e.id) ?? [];
51
+ const o = obs.map((x) => `<li>${esc(x.adapter)} / ${esc(x.environment ?? 'no environment')}: ${esc(x.firstObservedAt)} to ${esc(x.lastObservedAt)}, count band ${esc(x.eventCountBand)}, match ${esc(x.matchMethod)} (${esc(x.matchConfidence)})${x.attributes?.['network.securityDecision'] ? `, decision ${esc(x.attributes['network.securityDecision'])}` : ''}${x.attributes?.['identity.token'] ? `, identity <code>${esc(x.attributes['identity.token'])}</code>` : ''}</li>`).join('');
52
+ return `<details><summary>${label(e.from)} &rarr; ${label(e.to)} ${chips(s?.states)}</summary>`
53
+ + `<h4>Static evidence</h4><p><code>${esc(e.id)}</code>, provenance ${esc(e.provenance)}, coverage ${esc(e.coverageStatus)}; boundaries: ${esc(arr(e.boundaryCrossings).join(', ') || 'none')}; evidence: ${esc(arr(e.evidenceRefs).join(', ') || 'none')}</p>`
54
+ + `<h4>Runtime evidence</h4>${obs.length ? `<ul>${o}</ul>` : `<p>${esc(s?.note || 'none')}</p>`}</details>`;
55
+ };
48
56
 
49
- /**
50
- * The analysis record for `scan`, plus the BOM body. `inputs` is [{path, sha256}] over the Haskell/Nix sources and
51
- * manifests (digests only). Pure: no I/O.
52
- */
53
- function buildLanguageArtifacts(scan, inputs = []) {
54
- const lc = (scan && scan.scanHealth && scan.scanHealth.languageCoverage) || null;
55
- const bom = (scan && scan.languageBom) || null;
56
- const bridges = (scan && scan.languageBridges) || null;
57
- const bomBody = bom ? JSON.stringify(bom, null, 2) : null;
58
- const record = {
59
- schema: LANGUAGE_ARTIFACT_SCHEMA,
60
- classification: { class: 'scan-metadata', containsSourceText: false, containsSecrets: false },
61
- inputs: inputs.map((i) => ({ path: String(i.path), sha256: String(i.sha256) })).sort((a, b) => (a.path < b.path ? -1 : 1)),
62
- analysis: lc ? { totals: lc.totals, byKind: lc.byKind, conditions: lc.conditions, limitations: lc.limitations, capabilities: lc.capabilities, optionalModes: lc.optionalModes } : null,
63
- bom: bom ? { file: LANGUAGE_BOM_FILE, sha256: sha256(bomBody), components: (bom.components || []).length } : null,
64
- bridges: bridges ? { sha256: sha256(canonical(bridges)) } : null,
57
+ const pathRow = (f) => {
58
+ const hops = arr(f.edgeIds).map((eid) => { const e = edges.find((x) => x.id === eid); return e ? `${label(e.from)} &rarr; ${label(e.to)} ${chips(stateOf(eid)?.states)}` : esc(eid); });
59
+ return `<tr><td>${esc(arr(f.dataElementIds).map((id) => dataEls.get(id)?.label ?? id).join(', '))}<br><small>${esc(flowClasses(f).join(', '))}</small></td><td>${hops.join('<br>') || '(no modelled hops)'}</td><td>${esc(f.policyVerdict)}</td></tr>`;
65
60
  };
66
- return { record, bomBody };
61
+ const highRisk = flows.filter((f) => arr(f.edgeIds).some(observedEdge) && (flowClasses(f).some((c) => SENSITIVE.has(c)) || f.policyVerdict === 'prohibited' || arr(f.edgeIds).some((eid) => arr(edges.find((e) => e.id === eid)?.boundaryCrossings).length)));
62
+ const blockedNodes = Object.entries(edgeStates?.byNode ?? {});
63
+ const unexpected = [
64
+ ...arr(absent.candidates).map((c) => `<tr><td>${esc(c.destination)}</td><td>observed, not placed on any static node</td><td>${esc(c.firstObservedAt)} to ${esc(c.lastObservedAt)}</td></tr>`),
65
+ ...blockedNodes.filter(([, n]) => n.states.includes('RUNTIME_BLOCKED')).map(([id, n]) => `<tr><td>${label(id)}</td><td>attempted and refused (${n.denied} denied)</td><td></td></tr>`),
66
+ ];
67
+ const section = (id, h, body) => `<section id="${id}"><h2>${h}</h2>${body}</section>`;
68
+ const table = (head, rows, empty) => (rows.length ? `<table><tr>${head.map((h) => `<th>${h}</th>`).join('')}</tr>${rows.join('')}</table>` : `<p>${esc(empty)}</p>`);
69
+ const body = [
70
+ `<h1>${esc(title)}</h1>`,
71
+ `<div class="note">${edgeStates?.evaluated ? 'Telemetry was consulted.' : '<strong>No telemetry was consulted:</strong> every edge is STATIC_ONLY. That is not evidence that nothing happens at runtime.'} <strong>Not observed does not mean unreachable or safe.</strong> Only high- and medium-confidence mappings are counted. Telemetry values shown are metadata only; identities appear only as keyed-hash tokens.</div>`,
72
+ '<nav><a href="#static">Static</a><a href="#runtime">Runtime</a><a href="#both">Static vs Runtime</a><a href="#risk">Observed high-risk paths</a><a href="#unexpected">Unexpected runtime paths</a><a href="#paths">Flow paths</a><a href="#edges">Edge detail</a></nav>',
73
+ section('static', 'Static', table(['Edge', 'Id', 'Protocol', 'Boundaries'], staticRows ? [staticRows] : [], 'No static edges.')),
74
+ section('runtime', 'Runtime', table(['Edge', 'State', 'Trusted observations', 'Identity tokens'], runtimeRows ? [runtimeRows] : [], 'No edges.')),
75
+ section('both', 'Static vs Runtime', table(['Edge', 'Static', 'Runtime'], bothRows ? [bothRows] : [], 'No edges.')),
76
+ section('risk', 'Observed high-risk paths', table(['Data', 'Path and state per hop', 'Policy'], highRisk.map(pathRow), 'No observed flow is sensitive, policy-prohibited or boundary-crossing. This is a statement about the telemetry window, not about the system.')),
77
+ section('unexpected', 'Unexpected runtime paths', table(['Destination or node', 'What was seen', 'When'], unexpected, 'Nothing unplaced or refused. Unplaced observations are candidates, not proof static analysis missed a path.')),
78
+ section('paths', 'Flow paths', table(['Data', 'Path and state per hop', 'Policy'], flows.map(pathRow), 'No flows.')),
79
+ section('edges', 'Edge detail (expand an edge for its static and runtime evidence)', edges.map(detail).join('') || '<p>No edges.</p>'),
80
+ ].join('\n');
81
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><title>${esc(title)}</title><style>${CSS}</style></head><body>${body}</body></html>\n`;
67
82
  }
68
83
 
69
- /**
70
- * Writes the artifacts under the project's state directory, each with a `.sig`. Returns {written:[{name, sha256}], skipped}.
71
- * Never throws and never blocks a scan; a project that disallows state writes gets `skipped: 'state-writes-disabled'`.
72
- */
73
- function persistLanguageArtifacts(root, scan) {
74
- if (!hasLanguageContent(scan)) return { written: [], skipped: 'no-language-content' };
75
- let inputs = [];
76
- try {
77
- const proj = (0,_context_js__WEBPACK_IMPORTED_MODULE_3__.loadLanguageProject)(root);
78
- inputs = [...Object.entries(proj.files), ...Object.entries(proj.manifests)].map(([path, text]) => ({ path, sha256: sha256(text) }));
79
- } catch { /* the record then says it has no input digests */ }
80
- const { record, bomBody } = buildLanguageArtifacts(scan, inputs);
81
- const out = [];
82
- const put = (name, body) => {
83
- const fp = (0,_posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(root, name);
84
- if (!(0,_posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(fp, body)) return false;
85
- try { (0,_posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.safeWriteState)(`${fp}.sig`, (0,_posture_integrity_js__WEBPACK_IMPORTED_MODULE_2__/* .signLastScan */ .lU)(body)); } catch { /* the signature is best effort, as for last-scan.json */ }
86
- out.push({ name, sha256: sha256(body) });
87
- return true;
88
- };
89
- const body = JSON.stringify(record, null, 2);
90
- if (!put(LANGUAGE_ANALYSIS_FILE, body)) return { written: [], skipped: 'state-writes-disabled' };
91
- if (bomBody) put(LANGUAGE_BOM_FILE, bomBody);
92
- return { written: out, skipped: null };
84
+ /** "Show vulnerable functions actually exercised in production": scan findings whose own path appears in the runtime trace. Expects findings already
85
+ * annotated by `posture/runtime-correlation.js`; it adds no matching of its own. */
86
+ function vulnerableFunctionsInProduction(findings) {
87
+ const list = arr(findings).filter((f) => f && typeof f === 'object');
88
+ const withEvidence = list.filter((f) => f.runtimeEvidence);
89
+ if (!withEvidence.some((f) => f.runtimeEvidence.state !== 'unknown')) return { evaluated: false, reason: 'no runtime trace could be evaluated (none present, or it says nothing about any finding): put one at .agentic-security/runtime-trace.jsonl (or set AGENTIC_SECURITY_RUNTIME_TRACE_PATH). This is not "no vulnerable function ran".' };
90
+ const observed = withEvidence.filter((f) => f.runtimeEvidence.state === 'observed').map((f) => ({ id: f.id, severity: f.severity, vuln: f.vuln, file: f.file, line: f.line, cwe: f.cwe, basis: f.runtimeEvidence.basis, sampling: f.runtimeEvidence.sampling?.state ?? null }));
91
+ const counts = { observed: observed.length, notObservedInWindow: withEvidence.filter((f) => f.runtimeEvidence.state === 'not_observed_in_window').length, unknown: withEvidence.filter((f) => f.runtimeEvidence.state === 'unknown').length };
92
+ return { evaluated: true, findings: observed, counts, note: 'A finding listed here has a function, line or route on its path in the trace. A finding not listed may still run: counts show how many were not observed or unknown. Absence of observation is a statement about the window and the instrumentation, not about reachability.' };
93
93
  }
94
94
 
95
95