@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.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 (209) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/bin/agentic-security.js +3813 -83
  3. package/dist/1122.index.js +702 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +13 -4
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/2271.index.js +165 -0
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +307 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +14 -6
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +14 -6
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +67 -3
  35. package/dist/5051.index.js +770 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +29 -7
  42. package/dist/{449.index.js → 5830.index.js} +78 -14
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +26 -8
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +580 -187
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/package.json +23 -10
  72. package/src/compare.js +6 -1
  73. package/src/dataflow/CLAUDE.md +2 -2
  74. package/src/dataflow/catalog.js +42 -0
  75. package/src/dataflow/orm-write-catalog.js +175 -0
  76. package/src/engine.js +580 -30
  77. package/src/fix/apply-fix-service.js +1 -0
  78. package/src/history-scan.js +22 -5
  79. package/src/ir/CLAUDE.md +2 -1
  80. package/src/ir/chrome-probe.mjs +150 -0
  81. package/src/ir/parser-js.js +94 -7
  82. package/src/lineage/CLAUDE.md +1203 -0
  83. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  84. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  85. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  86. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  87. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  88. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  89. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  90. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  91. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  92. package/src/lineage/classification.js +56 -0
  93. package/src/lineage/coverage.js +658 -0
  94. package/src/lineage/cross-repo-link.js +107 -0
  95. package/src/lineage/dataflow-graph.schema.json +184 -0
  96. package/src/lineage/decision-story.js +206 -0
  97. package/src/lineage/drift-policy.js +279 -0
  98. package/src/lineage/driver.js +135 -0
  99. package/src/lineage/engine.js +992 -0
  100. package/src/lineage/export-briefing.js +628 -0
  101. package/src/lineage/export-csv.js +62 -0
  102. package/src/lineage/export-json.js +238 -0
  103. package/src/lineage/export-privacy.js +258 -0
  104. package/src/lineage/federation-loader.js +111 -0
  105. package/src/lineage/field-identity.js +78 -0
  106. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  107. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  108. package/src/lineage/flow-grade.js +221 -0
  109. package/src/lineage/governance-edit.js +169 -0
  110. package/src/lineage/graph-builder.js +1114 -0
  111. package/src/lineage/graph-diff.js +431 -0
  112. package/src/lineage/graph-snapshot.js +180 -0
  113. package/src/lineage/handling-analyzer.js +168 -0
  114. package/src/lineage/ids.js +349 -0
  115. package/src/lineage/impact-assessment.js +76 -0
  116. package/src/lineage/impact-engine.js +268 -0
  117. package/src/lineage/index.js +281 -0
  118. package/src/lineage/language-coverage-tiers.js +58 -0
  119. package/src/lineage/obligation-mapping.js +126 -0
  120. package/src/lineage/obligation-predicates.js +235 -0
  121. package/src/lineage/observation-adapters.js +282 -0
  122. package/src/lineage/observation-correlation.js +622 -0
  123. package/src/lineage/observation-store.js +497 -0
  124. package/src/lineage/path-query.js +410 -0
  125. package/src/lineage/path-store.js +400 -0
  126. package/src/lineage/protection.js +53 -0
  127. package/src/lineage/recipient-profile.js +192 -0
  128. package/src/lineage/recipient-registry.js +394 -0
  129. package/src/lineage/redact-graph.js +224 -0
  130. package/src/lineage/remediation.js +417 -0
  131. package/src/lineage/resolve-destination.js +91 -0
  132. package/src/lineage/runtime-observation.js +464 -0
  133. package/src/lineage/scenario-diff.js +84 -0
  134. package/src/lineage/scenario-engine.js +251 -0
  135. package/src/lineage/scenario.js +101 -0
  136. package/src/lineage/schema.js +167 -0
  137. package/src/lineage/sink-registry.js +427 -0
  138. package/src/lineage/source-registry.js +357 -0
  139. package/src/lineage/source-seeding.js +212 -0
  140. package/src/lineage/summaries.js +590 -0
  141. package/src/lineage/transform-catalog.js +397 -0
  142. package/src/lineage/transit-protection.js +150 -0
  143. package/src/lineage/validate.js +285 -0
  144. package/src/lsp/server.js +49 -2
  145. package/src/mcp/CLAUDE.md +7 -1
  146. package/src/mcp/dataflow-tools.js +160 -0
  147. package/src/mcp/server.js +1 -1
  148. package/src/mcp/tools.js +22 -1
  149. package/src/pipeline/assurance-mode.js +64 -1
  150. package/src/pipeline/finding-schema.js +8 -1
  151. package/src/pipeline/scan-health.js +19 -1
  152. package/src/posture/CLAUDE.md +140 -0
  153. package/src/posture/accuracy-scorecard.js +60 -0
  154. package/src/posture/artifact-registry.js +76 -0
  155. package/src/posture/auditor-walkthrough.js +192 -13
  156. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  157. package/src/posture/compliance-policy.js +12 -2
  158. package/src/posture/cross-repo-memory.js +7 -2
  159. package/src/posture/fix-history.js +25 -2
  160. package/src/posture/fix-verify.js +9 -1
  161. package/src/posture/fleet.js +0 -0
  162. package/src/posture/git-history.js +13 -5
  163. package/src/posture/material-change.js +21 -2
  164. package/src/posture/mttr.js +75 -12
  165. package/src/posture/obligation-evidence-pack.js +202 -0
  166. package/src/posture/pre-incident-archaeology.js +39 -7
  167. package/src/posture/privacy-framework.js +14 -0
  168. package/src/posture/provenance/ai-authorship.js +68 -0
  169. package/src/posture/provenance/branch-entry.js +80 -0
  170. package/src/posture/provenance/cache.js +143 -0
  171. package/src/posture/provenance/confidence.js +36 -0
  172. package/src/posture/provenance/coordinator.js +786 -0
  173. package/src/posture/provenance/dag-walk.js +249 -0
  174. package/src/posture/provenance/evidence-attribution.js +59 -0
  175. package/src/posture/provenance/git-evidence.js +310 -0
  176. package/src/posture/provenance/lifecycle.js +208 -0
  177. package/src/posture/provenance/missing-control-resolver.js +137 -0
  178. package/src/posture/provenance/origin-resolver.js +342 -0
  179. package/src/posture/provenance/predicate-replay.js +133 -0
  180. package/src/posture/provenance/providers/config.js +39 -0
  181. package/src/posture/provenance/providers/github.js +62 -0
  182. package/src/posture/provenance/providers/gitlab.js +58 -0
  183. package/src/posture/provenance/repo-lineage.js +74 -0
  184. package/src/posture/provenance/sca-origin.js +139 -0
  185. package/src/posture/provenance/schema.js +255 -0
  186. package/src/posture/provenance/transitive-sca.js +147 -0
  187. package/src/posture/provenance/validate.js +30 -0
  188. package/src/posture/provenance-evidence-bundle.js +144 -0
  189. package/src/posture/remediation-ledger.js +337 -0
  190. package/src/posture/sbom-diff.js +15 -2
  191. package/src/posture/secret-history.js +10 -2
  192. package/src/posture/state-dir.js +38 -14
  193. package/src/posture/vuln-archaeology.js +8 -2
  194. package/src/pr-delta.js +25 -4
  195. package/src/report/index.js +197 -3
  196. package/src/runScan.js +34 -5
  197. package/src/sast/rate-limit.js +33 -3
  198. package/src/server/CLAUDE.md +47 -0
  199. package/src/server/graph-loader.js +141 -0
  200. package/src/server/http-server.js +325 -0
  201. package/src/server/routes.js +129 -0
  202. package/src/server/security.js +111 -0
  203. package/src/server/static-assets.js +139 -0
  204. package/src/util/git-hardening.js +128 -0
  205. package/dist/11.index.js +0 -353
  206. package/dist/259.index.js +0 -975
  207. package/dist/317.index.js +0 -300
  208. package/dist/609.index.js +0 -741
  209. package/dist/838.index.js +0 -152
@@ -0,0 +1,520 @@
1
+ export const id = 7310;
2
+ export const ids = [7310];
3
+ export const modules = {
4
+
5
+ /***/ 7310:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+
9
+ // EXPORTS
10
+ __webpack_require__.d(__webpack_exports__, {
11
+ computeImpactAssessment: () => (/* binding */ computeImpactAssessment)
12
+ });
13
+
14
+ ;// CONCATENATED MODULE: ../frontend/src/lib/focus-controls.js
15
+ // Milestone 3, sub-project M3-UX-Query, Task 3. Nine focus-control graph
16
+ // traversals over the SAME DataFlowGraph v1 structure the query language
17
+ // (lib/query-language.js) reads. Each function returns the SAME
18
+ // `{nodeIds: Set<string>, edgeIds: Set<string>}` shape
19
+ // `views/architecture-view.js`'s own `resolveSelection` already produces
20
+ // for `selection` — so the render layer needs zero new consumption code to
21
+ // accept output from any of these. `resetToOverview` is deliberately NOT
22
+ // implemented here: Task 4 wires the existing `resolveSelection(graph,
23
+ // null)` directly for that control (see this file's own header note in the
24
+ // task brief) — duplicating it here would just be a second, divergent copy
25
+ // of the same empty-selection shape.
26
+
27
+ function buildAdjacency(graph) {
28
+ const forward = new Map(); // nodeId -> [{edgeId, toId}]
29
+ const backward = new Map(); // nodeId -> [{edgeId, fromId}]
30
+ for (const n of graph.nodes) { forward.set(n.id, []); backward.set(n.id, []); }
31
+ for (const e of graph.edges) {
32
+ forward.get(e.from)?.push({ edgeId: e.id, toId: e.to });
33
+ backward.get(e.to)?.push({ edgeId: e.id, fromId: e.from });
34
+ }
35
+ return { forward, backward };
36
+ }
37
+
38
+ function bfsDirection(graph, startId, adjacencyKey, adjacency) {
39
+ const nodeIds = new Set([startId]);
40
+ const edgeIds = new Set();
41
+ const queue = [startId];
42
+ while (queue.length > 0) {
43
+ const current = queue.shift();
44
+ for (const { edgeId, toId, fromId } of adjacency.get(current) ?? []) {
45
+ const nextId = adjacencyKey === 'forward' ? toId : fromId;
46
+ edgeIds.add(edgeId);
47
+ if (!nodeIds.has(nextId)) { nodeIds.add(nextId); queue.push(nextId); }
48
+ }
49
+ }
50
+ return { nodeIds, edgeIds };
51
+ }
52
+
53
+ function showDownstream(graph, nodeId) {
54
+ const { forward } = buildAdjacency(graph);
55
+ return bfsDirection(graph, nodeId, 'forward', forward);
56
+ }
57
+
58
+ function showUpstream(graph, nodeId) {
59
+ const { backward } = buildAdjacency(graph);
60
+ return bfsDirection(graph, nodeId, 'backward', backward);
61
+ }
62
+
63
+ function showAllPaths(graph, nodeId) {
64
+ const down = showDownstream(graph, nodeId);
65
+ const up = showUpstream(graph, nodeId);
66
+ return {
67
+ nodeIds: new Set([...down.nodeIds, ...up.nodeIds]),
68
+ edgeIds: new Set([...down.edgeIds, ...up.edgeIds]),
69
+ };
70
+ }
71
+
72
+ function showShortestPath(graph, fromId, toId) {
73
+ const { forward } = buildAdjacency(graph);
74
+ const cameFrom = new Map(); // nodeId -> {viaEdgeId, fromId}
75
+ const visited = new Set([fromId]);
76
+ const queue = [fromId];
77
+ let found = false;
78
+ while (queue.length > 0 && !found) {
79
+ const current = queue.shift();
80
+ for (const { edgeId, toId: nextId } of forward.get(current) ?? []) {
81
+ if (visited.has(nextId)) continue;
82
+ visited.add(nextId);
83
+ cameFrom.set(nextId, { viaEdgeId: edgeId, fromId: current });
84
+ if (nextId === toId) { found = true; break; }
85
+ queue.push(nextId);
86
+ }
87
+ }
88
+ if (!found) return { nodeIds: new Set(), edgeIds: new Set() };
89
+ const nodeIds = new Set([toId]);
90
+ const edgeIds = new Set();
91
+ let cursor = toId;
92
+ while (cursor !== fromId) {
93
+ const step = cameFrom.get(cursor);
94
+ edgeIds.add(step.viaEdgeId);
95
+ nodeIds.add(step.fromId);
96
+ cursor = step.fromId;
97
+ }
98
+ return { nodeIds, edgeIds };
99
+ }
100
+
101
+ function showExternalPathsOnly(graph) {
102
+ const externalNodeIds = new Set(graph.nodes.filter((n) => n.externality?.value === 'external').map((n) => n.id));
103
+ const nodeIds = new Set();
104
+ const edgeIds = new Set();
105
+ for (const flow of graph.flows) {
106
+ const pathNodeIds = new Set([flow.source, flow.sink]);
107
+ for (const edgeId of flow.edgeIds) {
108
+ const edge = graph.edges.find((e) => e.id === edgeId);
109
+ if (edge) { pathNodeIds.add(edge.from); pathNodeIds.add(edge.to); }
110
+ }
111
+ if ([...pathNodeIds].some((id) => externalNodeIds.has(id))) {
112
+ for (const id of pathNodeIds) nodeIds.add(id);
113
+ for (const edgeId of flow.edgeIds) edgeIds.add(edgeId);
114
+ }
115
+ }
116
+ return { nodeIds, edgeIds };
117
+ }
118
+
119
+ const UNPROTECTED_VERDICTS = new Set(['unprotected', 'mixed', 'unknown']);
120
+ function showUnprotectedPathsOnly(graph) {
121
+ const nodeIds = new Set();
122
+ const edgeIds = new Set();
123
+ for (const edge of graph.edges) {
124
+ const verdicts = [edge.protection.transit.verdict, edge.protection.atRest.verdict, edge.protection.handling.verdict];
125
+ if (verdicts.some((v) => UNPROTECTED_VERDICTS.has(v))) {
126
+ edgeIds.add(edge.id);
127
+ nodeIds.add(edge.from);
128
+ nodeIds.add(edge.to);
129
+ }
130
+ }
131
+ return { nodeIds, edgeIds };
132
+ }
133
+
134
+ function showAliases(graph, nodeId) {
135
+ const node = graph.nodes.find((n) => n.id === nodeId);
136
+ const nodeIds = new Set([nodeId]);
137
+ // Real, honest implementation. This plan's own Global Constraints section
138
+ // disclosed node.aliases as "confirmed always empty in real scan output" —
139
+ // that claim does NOT hold against the real committed flagship fixture
140
+ // (Web App/Payment API/Analytics API all carry non-empty aliases arrays
141
+ // there; see test/focus-controls.test.js for the correction). What IS
142
+ // true, confirmed against that same fixture: every alias entry is an
143
+ // alternate DISPLAY NAME for the node itself (e.g. "Checkout Form" is
144
+ // another name for the Web App node), never a pointer to a distinct
145
+ // sibling node record — no alias string in the fixture matches any other
146
+ // real node's label or id. This loop is still real, correct logic (not a
147
+ // no-op stub): it looks up each alias against the graph's own nodes and
148
+ // only adds a match it actually finds, so it will do the right thing the
149
+ // moment (if ever) alias data that DOES reference a distinct node record
150
+ // shows up in a real scan — nothing here is invented alias data.
151
+ for (const alias of node?.aliases ?? []) {
152
+ const aliasNode = graph.nodes.find((n) => n.label === alias || n.id === alias);
153
+ if (aliasNode) nodeIds.add(aliasNode.id);
154
+ }
155
+ return { nodeIds, edgeIds: new Set() };
156
+ }
157
+
158
+ function showDisconnected(graph) {
159
+ const connectedIds = new Set();
160
+ for (const e of graph.edges) { connectedIds.add(e.from); connectedIds.add(e.to); }
161
+ const nodeIds = new Set(graph.nodes.filter((n) => !connectedIds.has(n.id)).map((n) => n.id));
162
+ return { nodeIds, edgeIds: new Set() };
163
+ }
164
+
165
+ // EXTERNAL MODULE: ./src/lineage/export-json.js
166
+ var export_json = __webpack_require__(859);
167
+ // EXTERNAL MODULE: ./src/lineage/ids.js
168
+ var ids = __webpack_require__(5034);
169
+ ;// CONCATENATED MODULE: ./src/lineage/impact-assessment.js
170
+ // impact-assessment.js — M5 deliverable #4 (FR-507 §10.10): the
171
+ // ImpactAssessment extension contract — the result of asking "what is
172
+ // reachable from this compromised node/edge/flow/data element, per the
173
+ // graph's own already-scanned evidence." NOT a DataFlowGraph v1 entity,
174
+ // mirrors recipient-profile.js's/scenario.js's own contract shape
175
+ // exactly (structural-only {valid, errors} validator, zero graph
176
+ // access at construction time).
177
+ //
178
+ // See docs/superpowers/plans/2026-09-02-data-flow-explorer-m5-blast-radius-impact-scoping.md
179
+ // for the full design reasoning, including why there is no
180
+ // affectedObligationIds field (ObligationMapping records are built on
181
+ // demand per compliance framework, not stored on the graph) and why
182
+ // `scope` is always 'possible' today (no runtime-corroboration layer
183
+ // exists yet).
184
+
185
+ const IMPACT_VERSION = '1.0.0';
186
+
187
+ const IMPACT_TARGET_KINDS = Object.freeze(['node', 'edge', 'flow', 'dataElement']);
188
+
189
+ // 'possible' is the only value any producer emits today — 'observed'
190
+ // is reserved for a future Digital Twin (M5 #7) increment with a real
191
+ // runtime-corroboration signal. Both are valid schema values now so
192
+ // that increment needs no breaking change to this contract later.
193
+ const IMPACT_SCOPE_VALUES = Object.freeze(['possible', 'observed']);
194
+
195
+ // Discloses WHICH of the two genuinely different traversal semantics
196
+ // produced this record (final-review I2 fix) — `scope` alone cannot
197
+ // distinguish them, since both families emit `scope: 'possible'`.
198
+ // 'topology_reachable': a `node` target's topology-wide showAllPaths
199
+ // BFS — "everything this compromised node could push to." 'flow_
200
+ // restricted': an `edge`/`flow`/`dataElement` target's direct trace
201
+ // over the flows that actually carry it — never the topology-wide BFS.
202
+ // Without this field a JSON consumer cannot tell "exact carrier trace"
203
+ // from "topological presumption" apart, the exact silent-conflation
204
+ // this codebase's own disclosure discipline exists to prevent.
205
+ const IMPACT_TRACE_KINDS = Object.freeze(['topology_reachable', 'flow_restricted']);
206
+
207
+ function _isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
208
+ function _isStringArray(v) { return Array.isArray(v) && v.every((x) => typeof x === 'string'); }
209
+
210
+ /**
211
+ * Structural validation only — mirrors validateRecipientProfile's/
212
+ * validateScenario's own {valid, errors} shape and "never throws"
213
+ * contract.
214
+ */
215
+ function validateImpactAssessment(record) {
216
+ const errors = [];
217
+ const err = (p, message) => errors.push({ path: p, message });
218
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
219
+ err('$', 'ImpactAssessment record must be an object');
220
+ return { valid: false, errors };
221
+ }
222
+ if (!_isNonEmptyString(record.id) || !record.id.startsWith('impact:')) {
223
+ err('$.id', 'id is required and must start with "impact:"');
224
+ }
225
+ if (!_isNonEmptyString(record.version)) err('$.version', 'version is required');
226
+ if (!_isNonEmptyString(record.graphId)) err('$.graphId', 'graphId is required');
227
+ if (!_isNonEmptyString(record.graphDigest)) err('$.graphDigest', 'graphDigest is required');
228
+ if (!_isNonEmptyString(record.targetId)) err('$.targetId', 'targetId is required');
229
+ if (!IMPACT_TARGET_KINDS.includes(record.targetKind)) {
230
+ err('$.targetKind', `targetKind must be one of ${IMPACT_TARGET_KINDS.join('|')}`);
231
+ }
232
+ if (!IMPACT_SCOPE_VALUES.includes(record.scope)) {
233
+ err('$.scope', `scope must be one of ${IMPACT_SCOPE_VALUES.join('|')}`);
234
+ }
235
+ if (!IMPACT_TRACE_KINDS.includes(record.traceKind)) {
236
+ err('$.traceKind', `traceKind must be one of ${IMPACT_TRACE_KINDS.join('|')}`);
237
+ }
238
+ if (!_isStringArray(record.affectedNodeIds ?? [])) err('$.affectedNodeIds', 'affectedNodeIds must be an array of strings');
239
+ if (!_isStringArray(record.affectedEdgeIds ?? [])) err('$.affectedEdgeIds', 'affectedEdgeIds must be an array of strings');
240
+ if (!_isStringArray(record.affectedDataClasses ?? [])) err('$.affectedDataClasses', 'affectedDataClasses must be an array of strings');
241
+ if (!_isStringArray(record.affectedRecipientProfileIds ?? [])) err('$.affectedRecipientProfileIds', 'affectedRecipientProfileIds must be an array of strings');
242
+ if (!_isStringArray(record.coverageLimitations ?? [])) err('$.coverageLimitations', 'coverageLimitations must be an array of strings');
243
+ if (!_isNonEmptyString(record.generatedAt)) err('$.generatedAt', 'generatedAt is required');
244
+ return { valid: errors.length === 0, errors };
245
+ }
246
+
247
+ ;// CONCATENATED MODULE: ./src/lineage/impact-engine.js
248
+ // impact-engine.js — M5 deliverable #4 (FR-507): the pure read/
249
+ // aggregate computation behind "assess impact" from a compromised
250
+ // node/edge/flow/data element. Reuses the already-shipped, already-
251
+ // tested BFS traversal in frontend/src/lib/focus-controls.js — the
252
+ // established scanner/src/ -> frontend/src/ cross-import precedent
253
+ // (export-privacy.js's own computePrivacyViewModel import). No
254
+ // mutation, no hypothesis, no re-run of the taint/path pipeline — a
255
+ // pure filter/aggregate over the graph's own already-computed fields.
256
+
257
+
258
+
259
+
260
+
261
+
262
+ // dataElement's real stable-id prefix is 'data:' (see ids.js's own
263
+ // dataElementId(), which returns `data:${hash}` — every real
264
+ // DataFlowGraph v1 document's dataElements[] carries this prefix, not
265
+ // 'de:'). node/edge/flow below match ids.js's nodeId/edgeId/flowId
266
+ // prefixes exactly for the identical reason.
267
+ const _KIND_PREFIXES = Object.freeze({ node: 'node:', edge: 'edge:', flow: 'flow:', dataElement: 'data:' });
268
+
269
+ function _resolveTargetKind(targetId) {
270
+ if (typeof targetId !== 'string') return null;
271
+ for (const [kind, prefix] of Object.entries(_KIND_PREFIXES)) {
272
+ if (targetId.startsWith(prefix)) return kind;
273
+ }
274
+ return null;
275
+ }
276
+
277
+ // The seed set for showAllPaths, node targets only — a node target
278
+ // seeds itself; edge/flow/dataElement targets never reach this
279
+ // function any more (final-review I1 fix). See
280
+ // _flowRestrictedAffectedSet's header comment for why: compromising a
281
+ // NODE genuinely puts everything reachable from it in the blast
282
+ // radius, but compromising one EDGE/FLOW/data-element does not — the
283
+ // affected record still NAMES that edge/flow's own two endpoint nodes
284
+ // (you need them to know what the compromised channel actually
285
+ // touches), it just never treats either endpoint as ITSELF fully
286
+ // compromised for the purpose of finding everything ELSE reachable
287
+ // from it via unrelated edges — that further step is exactly the
288
+ // over-inclusion class the original dataElement fix removed.
289
+ function _seedNodeIds(graph, targetId, targetKind) {
290
+ if (targetKind === 'node') return [targetId];
291
+ return [];
292
+ }
293
+
294
+ // Generalizes the direct flow-based trace to all three "named
295
+ // relationship" target kinds (dataElement/edge/flow) — each is
296
+ // restricted to the REAL flows that carry it, never the topology-wide
297
+ // showAllPaths BFS below (that stays node-only). A compromised
298
+ // flow/edge/data-element never sweeps in an unrelated sibling flow
299
+ // that merely shares a node — the exact carrier path is already on
300
+ // each flow's own record (source/sink/edgeIds/dataElementIds), so
301
+ // approximating with topology reachability here would discard real
302
+ // data, not fill a gap (reproduced live by this task's own review on
303
+ // a branching-topology graph: one source, three sinks, one flow each
304
+ // carrying a distinct data element — a `flow:`/`edge:` target for the
305
+ // sink-A flow incorrectly swept in sinks B and C, and their unrelated
306
+ // data classes/recipients with them). `matchesFlow(flow) -> boolean`
307
+ // selects which flows carry the target. Also computes dataClasses
308
+ // directly from the SAME matched-flow set (closes a related leak: the
309
+ // edge-membership-based _affectedDataClasses below could pull in an
310
+ // unrelated flow that merely shares one edge with the target flow but
311
+ // carries a different data element — unreachable on any real
312
+ // graph-builder output today since ids.edgeId's own discriminator
313
+ // includes the data element id, but this closes the gap for a future
314
+ // builder or an externally-supplied graph). Never adds a dangling
315
+ // edge id that doesn't resolve to a real edge in graph.edges. Node
316
+ // targets keep the showAllPaths BFS above — for those, "everything
317
+ // topologically reachable from the compromised node's own endpoints"
318
+ // IS the intended pessimistic scope:'possible' semantics.
319
+ function _flowRestrictedAffectedSet(graph, matchesFlow) {
320
+ const nodeIds = new Set();
321
+ const edgeIds = new Set();
322
+ const dataClasses = new Set();
323
+ for (const f of graph.flows ?? []) {
324
+ if (!matchesFlow(f)) continue;
325
+ nodeIds.add(f.source);
326
+ nodeIds.add(f.sink);
327
+ for (const deId of f.dataElementIds ?? []) {
328
+ const de = (graph.dataElements ?? []).find((d) => d.id === deId);
329
+ for (const c of de?.dataClasses ?? []) dataClasses.add(c);
330
+ }
331
+ for (const eId of f.edgeIds ?? []) {
332
+ const edge = (graph.edges ?? []).find((e) => e.id === eId);
333
+ if (!edge) continue; // never surface a dangling/unresolved edge id
334
+ edgeIds.add(eId);
335
+ nodeIds.add(edge.from);
336
+ nodeIds.add(edge.to);
337
+ }
338
+ }
339
+ return { nodeIds, edgeIds, dataClasses };
340
+ }
341
+
342
+ // Edge-membership-based aggregation — kept ONLY as the `node` target's
343
+ // fallback (a topology-reachability target has no single "the matched
344
+ // flows" set to read dataClasses off directly, unlike the three named-
345
+ // relationship kinds above, which use _flowRestrictedAffectedSet's own
346
+ // dataClasses instead).
347
+ function _affectedDataClasses(graph, affectedEdgeIds) {
348
+ const classes = new Set();
349
+ for (const f of graph.flows ?? []) {
350
+ if (!(f.edgeIds ?? []).some((id) => affectedEdgeIds.has(id))) continue;
351
+ for (const deId of f.dataElementIds ?? []) {
352
+ const de = (graph.dataElements ?? []).find((d) => d.id === deId);
353
+ for (const c of de?.dataClasses ?? []) classes.add(c);
354
+ }
355
+ }
356
+ return [...classes].sort();
357
+ }
358
+
359
+ function _affectedRecipientProfileIds(graph, affectedNodeIds) {
360
+ return (graph.recipientProfiles ?? [])
361
+ .filter((rp) => (rp.contributingGraphIds ?? []).some((id) => affectedNodeIds.has(id)))
362
+ .map((rp) => rp.id)
363
+ .sort();
364
+ }
365
+
366
+ // Whole-graph, not scoped to the affected subgraph — no node carries a
367
+ // language field to filter by, so a per-language coverage gap is
368
+ // reported as a real, honest limitation on any assessment computed
369
+ // over this graph, not narrowed to the exact affected nodes. See this
370
+ // sub-project's own implementation plan for the full disclosed
371
+ // reasoning. Complemented, not replaced, by _affectedCoverageLimitations
372
+ // below — that function reports the real per-entity coverageStatus gaps
373
+ // among the entities THIS assessment actually affects, which this
374
+ // whole-graph language disclosure cannot express.
375
+ function _coverageLimitations(graph) {
376
+ return (graph.coverage?.languages ?? [])
377
+ .filter((l) => l.tier && l.tier !== 'full')
378
+ .map((l) => `${l.language}: coverage tier '${l.tier}'${typeof l.irTaintRecallPct === 'number' ? ` (${l.irTaintRecallPct}% measured recall)` : ''}`);
379
+ }
380
+
381
+ // Per-entity coverage gap among the entities THIS assessment actually
382
+ // affects, using the real, required `coverageStatus` field every
383
+ // node/edge/flow already carries (`modeled|partial|candidate|
384
+ // unsupported|manual` — populated by graph-builder.js, already
385
+ // consumed by export-csv.js/graph-diff.js). Complements (never
386
+ // replaces) _coverageLimitations' whole-graph language disclosure
387
+ // above — both are real, complementary limitations of the same
388
+ // assessment.
389
+ function _affectedCoverageLimitations(graph, affectedNodeIds, affectedEdgeIds) {
390
+ const limitations = [];
391
+ const nonModeledNodes = (graph.nodes ?? []).filter((n) => affectedNodeIds.has(n.id) && n.coverageStatus && n.coverageStatus !== 'modeled');
392
+ const nonModeledEdges = (graph.edges ?? []).filter((e) => affectedEdgeIds.has(e.id) && e.coverageStatus && e.coverageStatus !== 'modeled');
393
+ if (nonModeledNodes.length) {
394
+ limitations.push(`${nonModeledNodes.length} of ${affectedNodeIds.size} affected node(s) have less-than-modeled coverage (e.g. ${nonModeledNodes[0].id}: coverage tier '${nonModeledNodes[0].coverageStatus}')`);
395
+ }
396
+ if (nonModeledEdges.length) {
397
+ limitations.push(`${nonModeledEdges.length} of ${affectedEdgeIds.size} affected edge(s) have less-than-modeled coverage (e.g. ${nonModeledEdges[0].id}: coverage tier '${nonModeledEdges[0].coverageStatus}')`);
398
+ }
399
+ return limitations;
400
+ }
401
+
402
+ /**
403
+ * Compute an ImpactAssessment for `targetId` over `graph`. Throws when
404
+ * `graph` is structurally malformed (missing `nodes`/`edges` arrays —
405
+ * see `loadSignedGraph`'s own "signature-only, no schema validation"
406
+ * contract for why a signed-but-malformed graph can reach here) with a
407
+ * message prefixed `computeImpactAssessment: malformed graph — `, so a
408
+ * caller can distinguish that from the other thrown case: `targetId`
409
+ * has no recognized canonical-id prefix (a genuine caller error, not a
410
+ * missing-entity case). A well-formed targetId that does not exist in
411
+ * the graph degrades honestly to empty affected-* arrays, never an
412
+ * error — mirrors applyScenario's own skip-not-throw contract for a
413
+ * stale/missing target.
414
+ *
415
+ * `node` targets report a deliberately pessimistic "everything
416
+ * topologically reachable" blast radius (`traceKind:
417
+ * 'topology_reachable'`) — compromising a node genuinely puts
418
+ * everything it can reach in the blast radius. `edge`/`flow`/
419
+ * `dataElement` targets report only the flows/nodes/edges that
420
+ * actually carry that specific edge/flow/data element (`traceKind:
421
+ * 'flow_restricted'`) — the record still names that edge/flow's own
422
+ * two endpoint nodes (you need them to know what the channel actually
423
+ * touches), it just never treats either endpoint as itself fully
424
+ * compromised for finding everything ELSE reachable from it via
425
+ * unrelated edges. The two target-kind families answer genuinely
426
+ * different questions, both honestly disclosed via `traceKind` rather
427
+ * than silently conflated under one `scope` value.
428
+ */
429
+ function computeImpactAssessment(graph, targetId, opts = {}) {
430
+ if (!Array.isArray(graph?.nodes) || !Array.isArray(graph?.edges)) {
431
+ throw new Error('computeImpactAssessment: malformed graph — graph.nodes and graph.edges must both be arrays');
432
+ }
433
+
434
+ const targetKind = _resolveTargetKind(targetId);
435
+ if (!targetKind) {
436
+ throw new Error(`computeImpactAssessment: targetId "${targetId}" has no recognized prefix (expected one of node:/edge:/flow:/data:)`);
437
+ }
438
+
439
+ // showAllPaths (frontend/src/lib/focus-controls.js) unconditionally
440
+ // seeds its own result with the start id itself, even when that id
441
+ // does not exist in the graph at all (bfsDirection's `nodeIds = new
442
+ // Set([startId])`) — so a seed must be filtered against the graph's
443
+ // real node ids first, or a well-formed-but-nonexistent targetId
444
+ // would surface as a phantom single-node "affected" set instead of
445
+ // degrading honestly to empty arrays. The same filter is applied to
446
+ // _flowRestrictedAffectedSet's own node ids below, for the identical
447
+ // reason.
448
+ const realNodeIds = new Set((graph.nodes ?? []).map((n) => n.id));
449
+ const affectedNodeIds = new Set();
450
+ const affectedEdgeIds = new Set();
451
+ let affectedDataClasses;
452
+
453
+ if (targetKind === 'node') {
454
+ const seedNodeIds = _seedNodeIds(graph, targetId, targetKind).filter((id) => realNodeIds.has(id));
455
+ for (const seedId of seedNodeIds) {
456
+ const { nodeIds, edgeIds } = showAllPaths(graph, seedId);
457
+ for (const id of nodeIds) affectedNodeIds.add(id);
458
+ for (const id of edgeIds) affectedEdgeIds.add(id);
459
+ }
460
+ affectedDataClasses = _affectedDataClasses(graph, affectedEdgeIds);
461
+ } else {
462
+ // dataElement/edge/flow — direct flow-based trace via
463
+ // _flowRestrictedAffectedSet, never showAllPaths. See that
464
+ // function's own header comment for why.
465
+ const matchesFlow = targetKind === 'dataElement'
466
+ ? (f) => f.dataElementIds?.includes(targetId)
467
+ : targetKind === 'edge'
468
+ ? (f) => f.edgeIds?.includes(targetId)
469
+ : (f) => f.id === targetId; // flow
470
+ const { nodeIds, edgeIds, dataClasses } = _flowRestrictedAffectedSet(graph, matchesFlow);
471
+ for (const id of nodeIds) if (realNodeIds.has(id)) affectedNodeIds.add(id);
472
+ for (const id of edgeIds) affectedEdgeIds.add(id);
473
+ // N3 (final-review re-review): an edge: target that exists in
474
+ // graph.edges but appears in NO flow's own edgeIds (real for the
475
+ // real graph — an edge with zero flows using it is a legitimate
476
+ // graph shape, e.g. a dead-end connection no data currently
477
+ // traverses) must never come back fully empty — "this edge is
478
+ // compromised" means, at minimum, that edge and its own two
479
+ // endpoints are affected, flow or no flow. A nonexistent edge id
480
+ // still correctly degrades to fully empty (realNodeIds/the edge
481
+ // lookup below both filter it out).
482
+ if (targetKind === 'edge') {
483
+ const edge = (graph.edges ?? []).find((e) => e.id === targetId);
484
+ if (edge) {
485
+ affectedEdgeIds.add(edge.id);
486
+ if (realNodeIds.has(edge.from)) affectedNodeIds.add(edge.from);
487
+ if (realNodeIds.has(edge.to)) affectedNodeIds.add(edge.to);
488
+ }
489
+ }
490
+ affectedDataClasses = [...dataClasses].sort();
491
+ }
492
+
493
+ const graphDigest = (0,export_json.computeGraphDigest)(graph);
494
+ const generatedAt = opts.generatedAt ?? new Date().toISOString();
495
+
496
+ return {
497
+ id: (0,ids/* impactAssessmentId */.Oy)({ graphId: graph.graphId, graphDigest, targetId }, [generatedAt]),
498
+ version: IMPACT_VERSION,
499
+ graphId: graph.graphId,
500
+ graphDigest,
501
+ targetId,
502
+ targetKind,
503
+ scope: 'possible',
504
+ traceKind: targetKind === 'node' ? 'topology_reachable' : 'flow_restricted',
505
+ affectedNodeIds: [...affectedNodeIds].sort(),
506
+ affectedEdgeIds: [...affectedEdgeIds].sort(),
507
+ affectedDataClasses,
508
+ affectedRecipientProfileIds: _affectedRecipientProfileIds(graph, affectedNodeIds),
509
+ coverageLimitations: [
510
+ ..._coverageLimitations(graph),
511
+ ..._affectedCoverageLimitations(graph, affectedNodeIds, affectedEdgeIds),
512
+ ],
513
+ generatedAt,
514
+ };
515
+ }
516
+
517
+
518
+ /***/ })
519
+
520
+ };
@@ -1,5 +1,5 @@
1
- export const id = 552;
2
- export const ids = [552];
1
+ export const id = 7552;
2
+ export const ids = [7552];
3
3
  export const modules = {
4
4
 
5
5
  /***/ 7552:
@@ -64,7 +64,7 @@ function buildDeletionReport({ mode, dryRun, root, items, preserved }) {
64
64
 
65
65
  function writeDeletionReport(scanRoot, report) {
66
66
  try {
67
- const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .statePath */ .BQ)(scanRoot, DELETION_REPORT_FILE);
67
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__.statePath)(scanRoot, DELETION_REPORT_FILE);
68
68
  return (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .safeWriteState */ .Ep)(fp, JSON.stringify(report, null, 2) + '\n') ? fp : null;
69
69
  } catch { return null; }
70
70
  }
@@ -86,7 +86,7 @@ function buildExportReport({ root, outDir, items }) {
86
86
 
87
87
  function writeExportReport(scanRoot, report) {
88
88
  try {
89
- const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .statePath */ .BQ)(scanRoot, EXPORT_REPORT_FILE);
89
+ const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__.statePath)(scanRoot, EXPORT_REPORT_FILE);
90
90
  return (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_0__/* .safeWriteState */ .Ep)(fp, JSON.stringify(report, null, 2) + '\n') ? fp : null;
91
91
  } catch { return null; }
92
92
  }
@@ -0,0 +1,78 @@
1
+ export const id = 7709;
2
+ export const ids = [7709];
3
+ export const modules = {
4
+
5
+ /***/ 7709:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ exportFlowsCSV: () => (/* binding */ exportFlowsCSV)
10
+ /* harmony export */ });
11
+ /* harmony import */ var _protection_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(965);
12
+ // export-csv.js — Milestone 4, sub-project JSON/CSV export.
13
+ //
14
+ // One row per FLOW (not node, not edge — the closest analogue in this
15
+ // domain to report/index.js's own toCSV's "one row per finding", per this
16
+ // sub-project's own scoping doc decision 3). Node/edge CSV exports are
17
+ // deferred, named explicitly in that doc, not attempted here.
18
+
19
+
20
+
21
+ // Same escaping convention as report/index.js's own toCSV — quote only
22
+ // when a comma/quote/newline is present, double embedded quotes.
23
+ function esc(v) {
24
+ if (v == null) return '';
25
+ const s = String(v);
26
+ return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
27
+ }
28
+
29
+ // Join character for a flow's multiple `dataClasses` values in one CSV
30
+ // cell: `;`, not `,`. Chosen (this module's own decision, not prescribed
31
+ // by the plan) because `,` is CSV's own delimiter — joining multiple
32
+ // classes with it would put a raw delimiter inside a single logical cell
33
+ // value, forcing outer quoting for the ordinary multi-class case (e.g. a
34
+ // field classified both PCI and PII). `;` never collides with the CSV
35
+ // grammar, so the common case ("PCI;PII") never needs quoting at all.
36
+ // `esc()` still quotes correctly if a comma ends up in cell content for
37
+ // any other reason (see export-csv.test.js's own escaping test) — this
38
+ // choice only avoids manufacturing that case in the common path.
39
+ const DATA_CLASSES_JOIN = ';';
40
+
41
+ function _dataClassesForFlow(flow, dataElementsById) {
42
+ const classes = new Set();
43
+ for (const deId of flow.dataElementIds ?? []) {
44
+ const de = dataElementsById.get(deId);
45
+ for (const c of de?.dataClasses ?? []) classes.add(c);
46
+ }
47
+ return [...classes].sort();
48
+ }
49
+
50
+ function _dimensionVerdict(flow, edgesById, dimension) {
51
+ const verdicts = (flow.edgeIds ?? [])
52
+ .map((id) => edgesById.get(id)?.protection?.[dimension]?.verdict)
53
+ .filter(Boolean);
54
+ return (0,_protection_js__WEBPACK_IMPORTED_MODULE_0__/* .aggregateVerdicts */ .SX)(verdicts);
55
+ }
56
+
57
+ function exportFlowsCSV(graph) {
58
+ const dataElementsById = new Map((graph.dataElements ?? []).map((d) => [d.id, d]));
59
+ const edgesById = new Map((graph.edges ?? []).map((e) => [e.id, e]));
60
+ const header = ['id', 'source', 'sink', 'dataClasses', 'transitVerdict', 'atRestVerdict', 'handlingVerdict', 'policyVerdict', 'coverageStatus'];
61
+ const rows = [header.join(',')];
62
+ for (const flow of graph.flows ?? []) {
63
+ rows.push([
64
+ esc(flow.id), esc(flow.source), esc(flow.sink),
65
+ esc(_dataClassesForFlow(flow, dataElementsById).join(DATA_CLASSES_JOIN)),
66
+ esc(_dimensionVerdict(flow, edgesById, 'transit')),
67
+ esc(_dimensionVerdict(flow, edgesById, 'atRest')),
68
+ esc(_dimensionVerdict(flow, edgesById, 'handling')),
69
+ esc(flow.policyVerdict), esc(flow.coverageStatus),
70
+ ].join(','));
71
+ }
72
+ return rows.join('\n');
73
+ }
74
+
75
+
76
+ /***/ })
77
+
78
+ };