@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.
- package/CHANGELOG.md +390 -0
- package/bin/agentic-security.js +3813 -83
- package/dist/1122.index.js +702 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +13 -4
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/2271.index.js +165 -0
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +307 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +14 -6
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +14 -6
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +67 -3
- package/dist/5051.index.js +770 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +29 -7
- package/dist/{449.index.js → 5830.index.js} +78 -14
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +26 -8
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +580 -187
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/package.json +23 -10
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +2 -2
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +580 -30
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +22 -1
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +140 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +76 -0
- package/src/posture/auditor-walkthrough.js +192 -13
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/compliance-policy.js +12 -2
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/fix-history.js +25 -2
- package/src/posture/fix-verify.js +9 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/git-history.js +13 -5
- package/src/posture/material-change.js +21 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +14 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +38 -14
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/report/index.js +197 -3
- package/src/runScan.js +34 -5
- package/src/sast/rate-limit.js +33 -3
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +139 -0
- package/src/util/git-hardening.js +128 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- 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 =
|
|
2
|
-
export const ids = [
|
|
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__
|
|
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__
|
|
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
|
+
};
|