@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,591 @@
|
|
|
1
|
+
export const id = 1379;
|
|
2
|
+
export const ids = [1379];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 1379:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// EXPORTS
|
|
10
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
11
|
+
emitGraphDpiaArtifact: () => (/* binding */ emitGraphDpiaArtifact),
|
|
12
|
+
emitGraphRopaArtifact: () => (/* binding */ emitGraphRopaArtifact)
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
;// CONCATENATED MODULE: ../frontend/src/lib/flow-path.js
|
|
16
|
+
// Shared per-flow path/topology helpers, used by every view that needs to
|
|
17
|
+
// know "which nodes does this flow actually touch" — extracted here after
|
|
18
|
+
// Architecture View independently reimplemented the same node-collection
|
|
19
|
+
// logic inline in computeFlowSummary, to avoid a third divergent copy when
|
|
20
|
+
// Privacy View needed it too.
|
|
21
|
+
|
|
22
|
+
function flowPathNodeIds(graph, flow) {
|
|
23
|
+
const ids = new Set([flow.source, flow.sink]);
|
|
24
|
+
for (const edgeId of flow.edgeIds) {
|
|
25
|
+
const edge = graph.edges.find((e) => e.id === edgeId);
|
|
26
|
+
if (edge) {
|
|
27
|
+
ids.add(edge.from);
|
|
28
|
+
ids.add(edge.to);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return ids;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Backend AI node-subtype vocabulary (scanner/src/lineage/schema.js's
|
|
35
|
+
// SOURCE_CATEGORIES/SINK_CATEGORIES, AI-flavored entries only). This list
|
|
36
|
+
// must be hand-kept in sync with that schema file — the frontend never
|
|
37
|
+
// imports scanner/src/lineage/ at runtime (see frontend/CLAUDE.md).
|
|
38
|
+
// Corrected this increment: the previous set ('ai-assistant',
|
|
39
|
+
// 'vector-store') matched neither real enum and silently never matched
|
|
40
|
+
// any real node.
|
|
41
|
+
const AI_SUBTYPES = new Set([
|
|
42
|
+
'ai-model-provider', 'ai-local-model', 'ai-agent', 'ai-tool',
|
|
43
|
+
'ai-vector-store', 'ai-memory', 'ai-training', 'ai-evaluation', 'ai-telemetry',
|
|
44
|
+
'ai-model-output', 'ai-tool-result', 'ai-retrieved-document',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// AI relevance is computed from flow/node TOPOLOGY (does the path touch an
|
|
48
|
+
// AI-kind node), never from dataElement.aiContexts — that field is never
|
|
49
|
+
// populated by name-only classification (see scanner/src/lineage's
|
|
50
|
+
// classification.js), so an aiContexts-based filter would show zero AI
|
|
51
|
+
// relevance despite real AI-processing flows existing in the graph.
|
|
52
|
+
function isAiRelevantFlow(graph, flow) {
|
|
53
|
+
const pathNodeIds = flowPathNodeIds(graph, flow);
|
|
54
|
+
return graph.nodes.some((n) => pathNodeIds.has(n.id) && AI_SUBTYPES.has(n.subtype));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
;// CONCATENATED MODULE: ../frontend/src/lib/protection-visual.js
|
|
58
|
+
// The single source of truth for how a protection verdict renders — every
|
|
59
|
+
// view/component must call protectionVisual() rather than hardcoding a
|
|
60
|
+
// color or glyph, so AC-20 (verdicts distinguishable without color) holds
|
|
61
|
+
// everywhere by construction, not by convention.
|
|
62
|
+
//
|
|
63
|
+
// These verdict strings are NOT imported from scanner/src/lineage/protection.js
|
|
64
|
+
// — the browser bundle never imports anything under scanner/src/lineage/ at
|
|
65
|
+
// runtime (see frontend/CLAUDE.md). Keep this list in sync by hand if the
|
|
66
|
+
// backend's PROTECTION_VERDICTS/FLOW_SUMMARY_VALUES enums ever change.
|
|
67
|
+
|
|
68
|
+
// PRD §8.4, exact quoted precedence: "unprotected/prohibited → mixed →
|
|
69
|
+
// unknown/manual_required → protected/permitted → not_assessed". Read as
|
|
70
|
+
// display priority when aggregating multiple verdicts into one, not a raw
|
|
71
|
+
// severity order: not_assessed is LOWEST priority because it means "no
|
|
72
|
+
// information," and any real signal (even a protected one) should be shown
|
|
73
|
+
// instead of "not assessed." not_applicable isn't named in the PRD list;
|
|
74
|
+
// treated at the same lowest priority as not_assessed (both mean "nothing
|
|
75
|
+
// to prioritize").
|
|
76
|
+
const VERDICT_PRECEDENCE = Object.freeze([
|
|
77
|
+
'unprotected',
|
|
78
|
+
'mixed',
|
|
79
|
+
'unknown',
|
|
80
|
+
'protected',
|
|
81
|
+
'not_assessed',
|
|
82
|
+
'not_applicable',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const VISUALS = Object.freeze({
|
|
86
|
+
protected: { verdict: 'protected', label: 'Protected', glyph: '✓', lineStyle: 'solid', colorVar: '--status-protected' },
|
|
87
|
+
unprotected: { verdict: 'unprotected', label: 'Unprotected', glyph: '✗', lineStyle: 'solid', colorVar: '--status-unprotected' },
|
|
88
|
+
mixed: { verdict: 'mixed', label: 'Mixed', glyph: '±', lineStyle: 'solid', colorVar: '--status-unprotected' },
|
|
89
|
+
unknown: { verdict: 'unknown', label: 'Unknown', glyph: '?', lineStyle: 'dashed', colorVar: '--status-unknown' },
|
|
90
|
+
not_applicable: { verdict: 'not_applicable', label: 'Not applicable', glyph: '·', lineStyle: 'dotted', colorVar: '--text-secondary' },
|
|
91
|
+
not_assessed: { verdict: 'not_assessed', label: 'Not assessed', glyph: '–', lineStyle: 'dotted', colorVar: '--status-unknown' },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function protection_visual_protectionVisual(verdict) {
|
|
95
|
+
return VISUALS[verdict] ?? VISUALS.not_assessed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function worstVerdict(verdicts) {
|
|
99
|
+
for (const tier of VERDICT_PRECEDENCE) {
|
|
100
|
+
if (verdicts.includes(tier)) return tier;
|
|
101
|
+
}
|
|
102
|
+
return 'not_assessed';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
;// CONCATENATED MODULE: ../frontend/src/lib/row-filters.js
|
|
106
|
+
// Shared, deduplicated row-vs-active-filters matcher — was previously two
|
|
107
|
+
// near-identical private copies (privacy-view.js's and inventory-view.js's
|
|
108
|
+
// own rowMatchesFilters). Every check here reads a PRE-ATTACHED row
|
|
109
|
+
// property (never the graph directly) — the caller's own row-computation
|
|
110
|
+
// step is responsible for attaching whichever of these properties make
|
|
111
|
+
// sense for that row's own shape (see lib/filter-rail.js's own facet list
|
|
112
|
+
// and each view's own row-building code). A facet whose property the row
|
|
113
|
+
// does not carry AT ALL is skipped (never a hide) — this is what makes it
|
|
114
|
+
// safe for a single shared function to serve row shapes as different as a
|
|
115
|
+
// Privacy flow-row and an Inventory dataElement-row.
|
|
116
|
+
const LIST_FACETS = [
|
|
117
|
+
['dataClass', 'dataClasses', true], // true = row property is itself an array (dataClasses), match if ANY overlaps
|
|
118
|
+
['protection', 'protectionSummary', false], // false = row property is a single value, match if included in the filter's list
|
|
119
|
+
['transitVerdict', 'transitVerdict', false],
|
|
120
|
+
['atRestVerdict', 'atRestVerdict', false],
|
|
121
|
+
['handlingVerdict', 'handlingVerdict', false],
|
|
122
|
+
['sourceCategory', 'sourceCategory', false],
|
|
123
|
+
['sinkCategory', 'sinkCategory', false],
|
|
124
|
+
['destinationExternality', 'destinationExternality', false],
|
|
125
|
+
['policyVerdict', 'policyVerdict', false],
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
function matchesFilters(row, filters) {
|
|
129
|
+
for (const [filterKey, rowProp, rowIsArray] of LIST_FACETS) {
|
|
130
|
+
const activeValues = filters[filterKey];
|
|
131
|
+
if (!activeValues?.length) continue; // this facet isn't active at all
|
|
132
|
+
if (!(rowProp in row)) continue; // row doesn't carry this property — unaffected, not hidden
|
|
133
|
+
if (rowIsArray) {
|
|
134
|
+
if (!(row[rowProp] ?? []).some((v) => activeValues.includes(v))) return false;
|
|
135
|
+
} else {
|
|
136
|
+
if (row[rowProp] !== undefined && !activeValues.includes(row[rowProp])) return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Unlike the 9 facets above (list-of-selected-values), `ai` is a single
|
|
140
|
+
// boolean toggle, matching its existing shape in both views — kept as
|
|
141
|
+
// its own explicit check, not folded into LIST_FACETS's generic loop,
|
|
142
|
+
// since it's structurally different (a boolean flag, not a multi-select).
|
|
143
|
+
// Checked for ANY row carrying `isAiRelevant` — this is the real fix for
|
|
144
|
+
// Inventory's own previously-missing AI check (its private
|
|
145
|
+
// rowMatchesFilters never checked `ai` at all; Privacy's did).
|
|
146
|
+
if (filters.ai && 'isAiRelevant' in row && !row.isAiRelevant) return false;
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
;// CONCATENATED MODULE: ../frontend/src/views/privacy-view.js
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
const LIFECYCLE_STAGES = Object.freeze(['collection', 'processing', 'storage', 'sharing', 'retention', 'deletion']);
|
|
157
|
+
|
|
158
|
+
function stageForNode(node) {
|
|
159
|
+
return LIFECYCLE_STAGES.includes(node.lifecycleStages?.[0]) ? node.lifecycleStages[0] : 'processing';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function computePrivacyRow(graph, flow) {
|
|
163
|
+
const dataElement = graph.dataElements.find((d) => flow.dataElementIds.includes(d.id));
|
|
164
|
+
const pathNodeIds = flowPathNodeIds(graph, flow);
|
|
165
|
+
const pathNodes = graph.nodes.filter((n) => pathNodeIds.has(n.id));
|
|
166
|
+
// This flow's own resolved edges — the same per-edgeId lookup
|
|
167
|
+
// flowPathNodeIds() already performs internally, applied here to keep the
|
|
168
|
+
// edge objects (rather than just the node ids) so the three protection
|
|
169
|
+
// dimensions below can be aggregated per-flow via worstVerdict().
|
|
170
|
+
const pathEdges = flow.edgeIds.map((edgeId) => graph.edges.find((e) => e.id === edgeId)).filter(Boolean);
|
|
171
|
+
|
|
172
|
+
const stageCells = LIFECYCLE_STAGES.map((stage) => ({
|
|
173
|
+
stage,
|
|
174
|
+
nodeLabels: pathNodes.filter((n) => stageForNode(n) === stage).map((n) => n.label),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
const sourceNode = graph.nodes.find((n) => n.id === flow.source);
|
|
178
|
+
const sinkNode = graph.nodes.find((n) => n.id === flow.sink);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
flowId: flow.id,
|
|
182
|
+
dataElementName: dataElement?.name ?? 'unknown field',
|
|
183
|
+
dataClasses: dataElement?.dataClasses ?? [],
|
|
184
|
+
stageCells,
|
|
185
|
+
governanceRefs: flow.governanceRefs ?? {},
|
|
186
|
+
protectionSummary: flow.protectionSummary,
|
|
187
|
+
policyVerdict: flow.policyVerdict,
|
|
188
|
+
isAiRelevant: isAiRelevantFlow(graph, flow),
|
|
189
|
+
// worstVerdict() always returns a real verdict string (falling back to
|
|
190
|
+
// 'not_assessed', never null/undefined) even for an empty edge list, so
|
|
191
|
+
// these three are always set unconditionally — unlike sourceCategory/
|
|
192
|
+
// sinkCategory/destinationExternality below, there is no genuinely-absent
|
|
193
|
+
// case to guard against.
|
|
194
|
+
transitVerdict: worstVerdict(pathEdges.map((e) => e.protection.transit.verdict)),
|
|
195
|
+
atRestVerdict: worstVerdict(pathEdges.map((e) => e.protection.atRest.verdict)),
|
|
196
|
+
handlingVerdict: worstVerdict(pathEdges.map((e) => e.protection.handling.verdict)),
|
|
197
|
+
// Only set when a real, non-null value exists — keeps matchesFilters's
|
|
198
|
+
// own "property absent = unaffected, never a hide" semantics clean
|
|
199
|
+
// rather than introducing a third (present-but-null) state.
|
|
200
|
+
...(sourceNode?.subtype ? { sourceCategory: sourceNode.subtype } : {}),
|
|
201
|
+
...(sinkNode?.subtype ? { sinkCategory: sinkNode.subtype } : {}),
|
|
202
|
+
...(sinkNode?.externality?.value ? { destinationExternality: sinkNode.externality.value } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {object} graph
|
|
208
|
+
* @param {object} state
|
|
209
|
+
* @param {((flow: object) => boolean) | null} [queryPredicate] - Milestone 3,
|
|
210
|
+
* sub-project M3-UX-Query, Task 4: the query language's compiled predicate
|
|
211
|
+
* (lib/query-language.js's `compileQuery`), applied here as an ADDITIONAL
|
|
212
|
+
* condition alongside the existing dataClass/protection/ai filters — a row
|
|
213
|
+
* must pass BOTH to stay visible. Omitted/null (every pre-existing
|
|
214
|
+
* caller/test) means "no query active," matching every row, so behavior
|
|
215
|
+
* is unchanged for anyone not passing it.
|
|
216
|
+
*/
|
|
217
|
+
function computePrivacyViewModel(graph, state, queryPredicate = null) {
|
|
218
|
+
const matchesQuery = queryPredicate ?? (() => true);
|
|
219
|
+
const rows = graph.flows.map((flow) => {
|
|
220
|
+
const row = computePrivacyRow(graph, flow);
|
|
221
|
+
return {
|
|
222
|
+
...row,
|
|
223
|
+
selected: row.flowId === state.selectedId,
|
|
224
|
+
visible: matchesFilters(row, state.filters ?? {}) && matchesQuery(flow),
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
return { stages: LIFECYCLE_STAGES, rows };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const CLASS_BADGE_COLOR_VAR = { PII: '--class-pii', PHI: '--class-phi', PCI: '--class-pci' };
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {ReturnType<typeof computePrivacyViewModel>} viewModel
|
|
234
|
+
* @param {HTMLElement} canvasEl
|
|
235
|
+
* @param {(flowId: string) => void} onSelectFlow
|
|
236
|
+
*/
|
|
237
|
+
function renderPrivacyView(viewModel, canvasEl, onSelectFlow) {
|
|
238
|
+
clear(canvasEl);
|
|
239
|
+
|
|
240
|
+
const headerRow = el('tr', {}, [
|
|
241
|
+
el('th', {}, 'Field'),
|
|
242
|
+
el('th', {}, 'Protection'),
|
|
243
|
+
...viewModel.stages.map((stage) => el('th', {}, stage.charAt(0).toUpperCase() + stage.slice(1))),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
const bodyRows = viewModel.rows.map((row) => renderPrivacyRow(row, onSelectFlow));
|
|
247
|
+
|
|
248
|
+
const table = el('table', { class: 'privacy-table' }, [el('thead', {}, headerRow), el('tbody', {}, bodyRows)]);
|
|
249
|
+
|
|
250
|
+
canvasEl.appendChild(el('div', { class: 'privacy-view' }, table));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderPrivacyRow(row, onSelectFlow) {
|
|
254
|
+
const classBadges = row.dataClasses.map((cls) =>
|
|
255
|
+
el('span', { class: 'privacy-class-badge', style: `color: var(${CLASS_BADGE_COLOR_VAR[cls] ?? '--text-secondary'}); border-color: var(${CLASS_BADGE_COLOR_VAR[cls] ?? '--border-default'})` }, cls),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const fieldCell = el('td', { class: 'privacy-field-cell' }, [
|
|
259
|
+
el('div', {}, row.dataElementName),
|
|
260
|
+
el('div', {}, classBadges),
|
|
261
|
+
row.isAiRelevant ? el('span', { class: 'privacy-governance-badge', style: 'border-color: var(--context-ai); color: var(--context-ai)' }, 'AI processing') : null,
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
const visual = protectionVisual(row.protectionSummary);
|
|
265
|
+
const protectionCell = el(
|
|
266
|
+
'td',
|
|
267
|
+
{ class: 'privacy-protection-cell' },
|
|
268
|
+
el('span', { style: `border-color: var(${visual.colorVar}); color: var(${visual.colorVar})` }, `${visual.glyph} ${visual.label}`),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const stageCells = row.stageCells.map((cell) => renderStageCell(cell, row));
|
|
272
|
+
|
|
273
|
+
return el(
|
|
274
|
+
'tr',
|
|
275
|
+
{
|
|
276
|
+
class: 'privacy-row',
|
|
277
|
+
'data-selected': String(row.selected),
|
|
278
|
+
'data-visible': String(row.visible),
|
|
279
|
+
tabindex: '0',
|
|
280
|
+
role: 'button',
|
|
281
|
+
'aria-label': `${row.dataElementName} lifecycle, ${row.protectionSummary}${row.selected ? ', selected' : ''}`,
|
|
282
|
+
onClick: () => onSelectFlow(row.flowId),
|
|
283
|
+
onKeydown: (evt) => {
|
|
284
|
+
if (evt.key === 'Enter' || evt.key === ' ') {
|
|
285
|
+
evt.preventDefault();
|
|
286
|
+
onSelectFlow(row.flowId);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
[fieldCell, protectionCell, ...stageCells],
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function renderStageCell(cell, row) {
|
|
295
|
+
const children = [];
|
|
296
|
+
if (cell.nodeLabels.length > 0) {
|
|
297
|
+
children.push(el('div', {}, cell.nodeLabels.join(', ')));
|
|
298
|
+
} else {
|
|
299
|
+
children.push(el('div', { class: 'privacy-stage-cell-empty-label' }, '—'));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Governance facts are shown once, on whichever stage cell is the most
|
|
303
|
+
// relevant home for them — sharing (recipient/purpose/lawfulBasis/transfer)
|
|
304
|
+
// or retention/deletion — rather than repeating them on every cell. This
|
|
305
|
+
// loop runs unconditionally (independent of whether nodeLabels is empty)
|
|
306
|
+
// so a fact like deletion:not_found is never structurally unreachable just
|
|
307
|
+
// because a flow's path happens not to touch a deletion-stage node.
|
|
308
|
+
const governanceKeysForStage = {
|
|
309
|
+
sharing: ['recipient', 'purpose', 'lawfulBasis', 'transfer'],
|
|
310
|
+
retention: ['retention'],
|
|
311
|
+
deletion: ['deletion'],
|
|
312
|
+
};
|
|
313
|
+
const relevantKeys = governanceKeysForStage[cell.stage] ?? [];
|
|
314
|
+
for (const key of relevantKeys) {
|
|
315
|
+
if (key in row.governanceRefs) {
|
|
316
|
+
const value = row.governanceRefs[key];
|
|
317
|
+
children.push(el('div', { class: 'privacy-governance-badge' }, `${key}: ${value}`));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return el('td', { class: cell.nodeLabels.length === 0 ? 'privacy-stage-cell privacy-stage-cell-empty' : 'privacy-stage-cell' }, children);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// EXTERNAL MODULE: ./src/dataflow/privacy-governance.js
|
|
325
|
+
var privacy_governance = __webpack_require__(3308);
|
|
326
|
+
// EXTERNAL MODULE: ./src/lineage/export-json.js
|
|
327
|
+
var export_json = __webpack_require__(859);
|
|
328
|
+
;// CONCATENATED MODULE: ./src/lineage/export-privacy.js
|
|
329
|
+
// export-privacy.js — Milestone 4 deliverable #10 (DFG-020): graph-derived
|
|
330
|
+
// DPIA/RoPA export, migrating off the Layer-2 taint engine's
|
|
331
|
+
// dataflow/privacy-taint.js#emitDpiaArtifact / privacy-governance.js#emitRopaArtifact
|
|
332
|
+
// (which derive from piiFields, name-in-argument classification with no
|
|
333
|
+
// path/alias/field-mapping precision) onto the real DataFlowGraph v1's
|
|
334
|
+
// field-identity-tracked flows.
|
|
335
|
+
//
|
|
336
|
+
// Reuses frontend/src/views/privacy-view.js#computePrivacyViewModel
|
|
337
|
+
// DIRECTLY (the first live scanner/ -> frontend/ module import in this
|
|
338
|
+
// codebase, confirmed safe: that module only touches document/window
|
|
339
|
+
// inside renderPrivacyView's own function body, never at module top
|
|
340
|
+
// level, so importing it from plain Node never executes any DOM code)
|
|
341
|
+
// rather than re-deriving lifecycle-stage/dataClass/protection-verdict
|
|
342
|
+
// row computation a second time. This mirrors this package's own
|
|
343
|
+
// established discipline of reusing an already-correct, already-tested
|
|
344
|
+
// computation rather than a parallel copy that can drift.
|
|
345
|
+
//
|
|
346
|
+
// Both emit functions mirror their taint-engine-era predecessors'
|
|
347
|
+
// structure/wording (GDPR Art. 35 framing for the DPIA, the
|
|
348
|
+
// register-not-narrative RoPA table) but are populated from real graph
|
|
349
|
+
// rows.
|
|
350
|
+
//
|
|
351
|
+
// Filter wiring: opts.filter is the SAME {nodeIds, edgeIds} shape every
|
|
352
|
+
// other `dataflow export` format uses (export-json.js's own convention).
|
|
353
|
+
// It filters the GRAPH itself, via export-json.js's _filterGraph, BEFORE
|
|
354
|
+
// computePrivacyViewModel ever runs — never threaded into
|
|
355
|
+
// computePrivacyViewModel's own state.filters parameter, which expects a
|
|
356
|
+
// completely different shape (per-facet selected-value arrays, e.g.
|
|
357
|
+
// {dataClass: [...]}) and would silently no-op if handed {nodeIds, edgeIds}.
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
function _emptyState() {
|
|
364
|
+
return { selectedId: null };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Collapse embedded newlines to spaces. Every value interpolated into a
|
|
369
|
+
* Markdown line below can originate from operator-supplied prose (a
|
|
370
|
+
* governance field from .agentic-security/privacy-governance.json) or
|
|
371
|
+
* from source-derived identifiers — neither is trusted not to contain a
|
|
372
|
+
* literal newline, which would otherwise let the value break out of its
|
|
373
|
+
* line and inject arbitrary Markdown (e.g. a fake heading) mid-document.
|
|
374
|
+
*/
|
|
375
|
+
function _mdInline(value) {
|
|
376
|
+
return String(value).replace(/\r\n|\r|\n/g, ' ');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** _mdInline, plus pipe-escaping for a Markdown table cell — an
|
|
380
|
+
* unescaped `|` in a cell value shifts every later column in that row.
|
|
381
|
+
* Backslashes are escaped FIRST: a value already containing a literal
|
|
382
|
+
* `\|` (e.g. a Windows path fragment, a regex snippet) would otherwise
|
|
383
|
+
* become `\\|` — in Markdown that reads as an escaped backslash followed
|
|
384
|
+
* by a still-live, still-unescaped `|` column delimiter. */
|
|
385
|
+
function _mdCell(value) {
|
|
386
|
+
return _mdInline(value).replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Wrap a value in a Markdown inline-code span, safe even when the value
|
|
390
|
+
* itself contains backticks. CommonMark's own general rule: the fence
|
|
391
|
+
* must be one backtick longer than the longest run of consecutive
|
|
392
|
+
* backticks anywhere in the content, padded with a space on each side —
|
|
393
|
+
* a fixed double-backtick fence (this function's own earlier version)
|
|
394
|
+
* is not safe against a value containing 2+ CONSECUTIVE backticks, since
|
|
395
|
+
* the value's own run would then close the span early. */
|
|
396
|
+
function _mdCode(value) {
|
|
397
|
+
const s = _mdInline(value);
|
|
398
|
+
const runs = s.match(/`+/g);
|
|
399
|
+
const maxRun = runs ? Math.max(...runs.map((r) => r.length)) : 0;
|
|
400
|
+
if (maxRun === 0) return `\`${s}\``;
|
|
401
|
+
const fence = '`'.repeat(maxRun + 1);
|
|
402
|
+
return `${fence} ${s} ${fence}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function _scopedViewModel(graph, filter) {
|
|
406
|
+
const scopedGraph = filter ? (0,export_json/* _filterGraph */.e)(graph, filter) : graph;
|
|
407
|
+
return computePrivacyViewModel(scopedGraph, _emptyState(), null);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Group computePrivacyViewModel's rows by data class, mirroring
|
|
412
|
+
* emitDpiaArtifact's own grouping.
|
|
413
|
+
*
|
|
414
|
+
* Task-2 review finding (non-blocking, fixed): the old taint-engine
|
|
415
|
+
* emitDpiaArtifact's identical grouping code was safe only because its
|
|
416
|
+
* input (piiFields) was pre-filtered to classified fields by
|
|
417
|
+
* construction — it never saw an unclassified field at all. This
|
|
418
|
+
* function draws from EVERY graph flow via computePrivacyViewModel, so a
|
|
419
|
+
* bare `for (const cls of row.dataClasses)` silently drops any flow
|
|
420
|
+
* whose dataClasses is [] from the whole document — not merely
|
|
421
|
+
* ungrouped, genuinely INVISIBLE, with no count, no mention, nothing —
|
|
422
|
+
* for a document whose entire purpose is a complete inventory. RoPA's
|
|
423
|
+
* own sibling loop already falls back to a '(unclassified)' bucket
|
|
424
|
+
* (mirroring the same sentinel resolveGovernanceRefs's default hook
|
|
425
|
+
* uses in coverage.js, Task 1's own fix round); mirrored here so the two
|
|
426
|
+
* artifacts never disagree about how many real flows exist in scope.
|
|
427
|
+
*/
|
|
428
|
+
function _groupRowsByClass(rows) {
|
|
429
|
+
const grouped = new Map();
|
|
430
|
+
for (const row of rows) {
|
|
431
|
+
const classes = row.dataClasses.length ? row.dataClasses : ['(unclassified)'];
|
|
432
|
+
for (const cls of classes) {
|
|
433
|
+
let g = grouped.get(cls);
|
|
434
|
+
if (!g) { g = []; grouped.set(cls, g); }
|
|
435
|
+
g.push(row);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return grouped;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Graph-derived DPIA (Data Protection Impact Assessment) — narrative
|
|
443
|
+
* Markdown, mirroring dataflow/privacy-taint.js#emitDpiaArtifact's own
|
|
444
|
+
* structure. opts.filter scopes to a subset of the graph (the "filtered
|
|
445
|
+
* ... from the selected graph scope" requirement, PRD 7.4).
|
|
446
|
+
*/
|
|
447
|
+
function emitGraphDpiaArtifact(graph, opts = {}) {
|
|
448
|
+
const viewModel = _scopedViewModel(graph, opts.filter);
|
|
449
|
+
const visibleRows = viewModel.rows.filter((r) => r.visible !== false);
|
|
450
|
+
const grouped = _groupRowsByClass(visibleRows);
|
|
451
|
+
|
|
452
|
+
const lines = [];
|
|
453
|
+
lines.push('# Data Protection Impact Assessment (DPIA)');
|
|
454
|
+
lines.push('');
|
|
455
|
+
lines.push(`Generated by agentic-security scanner (graph-derived) on ${(opts.generatedAt ?? new Date().toISOString()).slice(0, 10)}.`);
|
|
456
|
+
lines.push('');
|
|
457
|
+
lines.push('This is an automated DPIA scaffold derived from real data-flow analysis');
|
|
458
|
+
lines.push('(field identity, path provenance, and protection verdicts). It must be');
|
|
459
|
+
lines.push('reviewed and completed by a privacy officer before use.');
|
|
460
|
+
lines.push('');
|
|
461
|
+
lines.push('Governance fields reflect .agentic-security/privacy-governance.json as');
|
|
462
|
+
lines.push('of the scan that produced this graph, not as of the moment this document');
|
|
463
|
+
lines.push('was exported — re-scan (with lineage deep mode enabled) after editing');
|
|
464
|
+
lines.push('that config, then re-export, to pick up the change.');
|
|
465
|
+
lines.push('');
|
|
466
|
+
if (grouped.size === 0) {
|
|
467
|
+
lines.push('No regulated data classes were identified in this graph scope.');
|
|
468
|
+
return lines.join('\n');
|
|
469
|
+
}
|
|
470
|
+
lines.push('## Data classes identified');
|
|
471
|
+
lines.push('');
|
|
472
|
+
for (const [cls, rows] of grouped) {
|
|
473
|
+
lines.push(`### ${_mdInline(cls)} (${rows.length} flow(s))`);
|
|
474
|
+
lines.push('');
|
|
475
|
+
for (const row of rows.slice(0, 20)) {
|
|
476
|
+
const crossClassNote = row.dataClasses.length > 1 ? ` (also: ${row.dataClasses.filter((c) => c !== cls).map(_mdInline).join(', ')})` : '';
|
|
477
|
+
lines.push(`- ${_mdCode(row.dataElementName)} — ${_mdInline(row.sourceCategory ?? 'unknown source')} -> ${_mdInline(row.sinkCategory ?? 'unknown sink')} (protection: ${_mdInline(row.protectionSummary)})${crossClassNote}`);
|
|
478
|
+
}
|
|
479
|
+
if (rows.length > 20) lines.push(`- ... and ${rows.length - 20} more`);
|
|
480
|
+
lines.push('');
|
|
481
|
+
lines.push(`**Governance fields for ${_mdInline(cls)}:**`);
|
|
482
|
+
lines.push('');
|
|
483
|
+
// Worst-case-wins across this class's own rows, the same tie-break
|
|
484
|
+
// Task 1's resolveGovernanceRefs default uses — never silently pick
|
|
485
|
+
// whichever row happened to be iterated last.
|
|
486
|
+
const merged = {};
|
|
487
|
+
for (const row of rows) {
|
|
488
|
+
for (const field of privacy_governance/* GOVERNANCE_FIELDS */.n4) {
|
|
489
|
+
const r = row.governanceRefs?.[field];
|
|
490
|
+
if (!r) continue;
|
|
491
|
+
if (!merged[field] || merged[field].source === 'manual_required') merged[field] = r;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// A flow with >1 dataClasses already carries a single governanceRefs
|
|
495
|
+
// record merged (worst-case-wins) across ALL of its own classes back
|
|
496
|
+
// at mint time (coverage.js#resolveGovernanceRefs) — this section can
|
|
497
|
+
// only re-merge that already-merged record across rows, never recover
|
|
498
|
+
// per-class distinctness that was lost earlier. Disclose it rather
|
|
499
|
+
// than silently presenting a cross-class value as if it were specific
|
|
500
|
+
// to this one class.
|
|
501
|
+
if (rows.some((row) => row.dataClasses.length > 1)) {
|
|
502
|
+
lines.push(`_Note: some flows in this section also belong to other data classes; the governance values below are each flow's own record merged across ALL of its classes, not verified as specific to ${_mdInline(cls)} alone._`);
|
|
503
|
+
lines.push('');
|
|
504
|
+
}
|
|
505
|
+
for (const field of privacy_governance/* GOVERNANCE_FIELDS */.n4) {
|
|
506
|
+
const r = merged[field] ?? { value: privacy_governance/* MANUAL_REQUIRED */.Dh, source: 'manual_required' };
|
|
507
|
+
lines.push(`- ${field}: ${_mdCode(r.value)}${r.source === 'operator_provided' ? ' (operator-provided)' : ''}`);
|
|
508
|
+
}
|
|
509
|
+
lines.push('');
|
|
510
|
+
}
|
|
511
|
+
lines.push('## Regulatory framework mapping');
|
|
512
|
+
lines.push('');
|
|
513
|
+
lines.push('- **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.');
|
|
514
|
+
lines.push('- **CCPA 1798.130** — Notice + access rights for collected personal information.');
|
|
515
|
+
if (grouped.has('PHI')) lines.push('- **HIPAA 164.308** — Administrative safeguards for ePHI access.');
|
|
516
|
+
if (grouped.has('PCI')) lines.push('- **PCI DSS Req. 3** — Protect stored cardholder data.');
|
|
517
|
+
lines.push('');
|
|
518
|
+
lines.push('This document organizes automated technical assessment evidence. It does');
|
|
519
|
+
lines.push("not certify compliance and is not a legal determination — see the graph's");
|
|
520
|
+
lines.push('own `disclaimer`/`limitations` fields for the full caveat.');
|
|
521
|
+
return lines.join('\n');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Graph-derived RoPA (Record of Processing Activities, GDPR Art. 30) —
|
|
526
|
+
* tabular Markdown register, mirroring
|
|
527
|
+
* dataflow/privacy-governance.js#emitRopaArtifact's own format, but ONE
|
|
528
|
+
* ROW PER (dataClass x flow) rather than per dataClass alone — a real,
|
|
529
|
+
* disclosed precision improvement the graph's own per-flow sink/
|
|
530
|
+
* destination resolution makes possible (the taint-engine version has no
|
|
531
|
+
* per-flow destination-resolution concept at all).
|
|
532
|
+
*/
|
|
533
|
+
function emitGraphRopaArtifact(graph, opts = {}) {
|
|
534
|
+
const viewModel = _scopedViewModel(graph, opts.filter);
|
|
535
|
+
const visibleRows = viewModel.rows.filter((r) => r.visible !== false);
|
|
536
|
+
|
|
537
|
+
const lines = [];
|
|
538
|
+
lines.push('# Record of Processing Activities (RoPA) — GDPR Art. 30, graph-derived');
|
|
539
|
+
lines.push('');
|
|
540
|
+
lines.push(`Generated by agentic-security scanner (graph-derived) on ${(opts.generatedAt ?? new Date().toISOString()).slice(0, 10)}. Every governance`);
|
|
541
|
+
lines.push('field below is either supplied by an operator');
|
|
542
|
+
lines.push('(.agentic-security/privacy-governance.json) or marked');
|
|
543
|
+
lines.push(`\`${privacy_governance/* MANUAL_REQUIRED */.Dh}\` — none are inferable from source code, and none are`);
|
|
544
|
+
lines.push(`guessed. A privacy officer must fill in every \`${privacy_governance/* MANUAL_REQUIRED */.Dh}\` cell`);
|
|
545
|
+
lines.push('before this document is usable as a real RoPA.');
|
|
546
|
+
lines.push('');
|
|
547
|
+
lines.push('Governance fields reflect .agentic-security/privacy-governance.json as');
|
|
548
|
+
lines.push('of the scan that produced this graph, not as of the moment this document');
|
|
549
|
+
lines.push('was exported — re-scan (with lineage deep mode enabled) after editing');
|
|
550
|
+
lines.push('that config, then re-export, to pick up the change.');
|
|
551
|
+
lines.push('');
|
|
552
|
+
if (visibleRows.length === 0) {
|
|
553
|
+
lines.push('No regulated data flows were identified in this graph scope.');
|
|
554
|
+
return lines.join('\n');
|
|
555
|
+
}
|
|
556
|
+
const header = ['Data class', 'Field', 'Source', 'Destination', 'Protection', ...privacy_governance/* GOVERNANCE_FIELDS */.n4];
|
|
557
|
+
lines.push(`| ${header.join(' | ')} |`);
|
|
558
|
+
lines.push(`|${header.map(() => '---').join('|')}|`);
|
|
559
|
+
let gapCount = 0;
|
|
560
|
+
let rowCount = 0;
|
|
561
|
+
let crossClassRows = false;
|
|
562
|
+
for (const row of visibleRows) {
|
|
563
|
+
for (const cls of row.dataClasses.length ? row.dataClasses : ['(unclassified)']) {
|
|
564
|
+
rowCount++;
|
|
565
|
+
const fieldValues = privacy_governance/* GOVERNANCE_FIELDS */.n4.map((f) => {
|
|
566
|
+
const r = row.governanceRefs?.[f] ?? { value: privacy_governance/* MANUAL_REQUIRED */.Dh, source: 'manual_required' };
|
|
567
|
+
if (r.source === 'manual_required') gapCount++;
|
|
568
|
+
return r.value;
|
|
569
|
+
});
|
|
570
|
+
const isCrossClass = row.dataClasses.length > 1;
|
|
571
|
+
if (isCrossClass) crossClassRows = true;
|
|
572
|
+
const clsCell = isCrossClass ? `${cls}*` : cls;
|
|
573
|
+
const cells = [clsCell, row.dataElementName, row.sourceCategory ?? 'unknown source', row.sinkCategory ?? 'unknown destination', row.protectionSummary, ...fieldValues];
|
|
574
|
+
lines.push(`| ${cells.map(_mdCell).join(' | ')} |`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
lines.push('');
|
|
578
|
+
if (crossClassRows) {
|
|
579
|
+
lines.push('\\* This flow also belongs to other data classes; governance fields are');
|
|
580
|
+
lines.push("this flow's own record merged across ALL of its classes, not verified as");
|
|
581
|
+
lines.push('specific to the data class shown in this row alone.');
|
|
582
|
+
lines.push('');
|
|
583
|
+
}
|
|
584
|
+
lines.push(`${gapCount} field(s) across ${rowCount} row(s) (${visibleRows.length} flow(s)) require manual input.`);
|
|
585
|
+
return lines.join('\n');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
/***/ })
|
|
590
|
+
|
|
591
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const id =
|
|
2
|
-
export const ids = [
|
|
1
|
+
export const id = 1444;
|
|
2
|
+
export const ids = [1444];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
5
|
/***/ 1444:
|
|
@@ -10,6 +10,7 @@ export const modules = {
|
|
|
10
10
|
/* harmony export */ });
|
|
11
11
|
/* unused harmony exports extractAddedLines, scanHistoryDiff */
|
|
12
12
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
13
|
+
/* harmony import */ var _util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8844);
|
|
13
14
|
// R15 (PRD §5) — git-history secret sweep.
|
|
14
15
|
//
|
|
15
16
|
// A secret removed from HEAD but present in any past commit is still
|
|
@@ -25,6 +26,7 @@ export const modules = {
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
|
|
28
30
|
// Pull the post-image (added) lines out of a unified diff: lines starting with
|
|
29
31
|
// a single '+' (not the '+++' file header). Returns reconstructed text.
|
|
30
32
|
function extractAddedLines(diffText) {
|
|
@@ -74,8 +76,15 @@ function sweepGitHistory(scanRoot, detectFn, { maxCommits = 50, timeoutMs = 2000
|
|
|
74
76
|
if (!scanRoot || typeof detectFn !== 'function') return [];
|
|
75
77
|
let out;
|
|
76
78
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// Second independent Finding Provenance PRD audit (FR-PROV-024): this
|
|
80
|
+
// scanRoot is a scanned repository, not this project's own trusted
|
|
81
|
+
// checkout. `--no-textconv` alone (the pre-existing hardening here) closes
|
|
82
|
+
// the .gitattributes textconv surface but NOT `core.fsmonitor` /
|
|
83
|
+
// `core.hooksPath` — this `log -p` call renders every historical commit's
|
|
84
|
+
// diff content, the same shape verified exploitable in
|
|
85
|
+
// provenance/git-evidence.js, so it gets the full hardening too.
|
|
86
|
+
out = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.execFileSync)('git', (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitArgs */ .Ax)(['-C', scanRoot, 'log', '-p', '-n', String(maxCommits), '--no-color', '--no-merges', '--no-textconv']),
|
|
87
|
+
{ encoding: 'utf8', maxBuffer: 96 * 1024 * 1024, timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], env: (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitEnv */ .Si)() });
|
|
79
88
|
} catch { return []; }
|
|
80
89
|
const parts = out.split(/^commit ([0-9a-f]{7,40})/m); // [pre, sha, body, sha, body, ...]
|
|
81
90
|
const findings = [];
|