@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5
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 +266 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +779 -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} +2 -2
- 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/{271.index.js → 2271.index.js} +2 -2
- 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 +379 -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} +5 -5
- 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} +5 -5
- 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} +2 -2
- package/dist/5051.index.js +841 -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} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- 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} +3 -3
- 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} +560 -189
- 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/dist/frontend/index.html +21 -0
- package/dist/frontend/src/app.js +176 -0
- package/dist/frontend/src/components/evidence-inspector.js +141 -0
- package/dist/frontend/src/components/filter-rail.js +119 -0
- package/dist/frontend/src/components/query-bar.js +126 -0
- package/dist/frontend/src/data/flagship-graph.js +1460 -0
- package/dist/frontend/src/export-entry.js +36 -0
- package/dist/frontend/src/lib/api-client.js +92 -0
- package/dist/frontend/src/lib/contrast.js +34 -0
- package/dist/frontend/src/lib/dom.js +24 -0
- package/dist/frontend/src/lib/escape-html.js +16 -0
- package/dist/frontend/src/lib/flow-path.js +40 -0
- package/dist/frontend/src/lib/focus-controls.js +149 -0
- package/dist/frontend/src/lib/protection-visual.js +46 -0
- package/dist/frontend/src/lib/query-language.js +240 -0
- package/dist/frontend/src/lib/row-filters.js +43 -0
- package/dist/frontend/src/lib/state.js +84 -0
- package/dist/frontend/src/main.js +83 -0
- package/dist/frontend/src/shell.js +184 -0
- package/dist/frontend/src/views/architecture-view.js +798 -0
- package/dist/frontend/src/views/inventory-view.js +292 -0
- package/dist/frontend/src/views/privacy-view.js +172 -0
- package/dist/frontend/src/views/trace-view.js +206 -0
- package/dist/frontend/styles/architecture-view.css +93 -0
- package/dist/frontend/styles/filter-rail.css +34 -0
- package/dist/frontend/styles/inspector.css +69 -0
- package/dist/frontend/styles/inventory-view.css +74 -0
- package/dist/frontend/styles/privacy-view.css +86 -0
- package/dist/frontend/styles/query-bar.css +107 -0
- package/dist/frontend/styles/shell.css +155 -0
- package/dist/frontend/styles/tokens.css +128 -0
- package/dist/frontend/styles/trace-view.css +95 -0
- package/package.json +13 -6
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- 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/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 +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- 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 +144 -0
- package/src/shared/frontend-root.js +52 -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,297 @@
|
|
|
1
|
+
export const id = 6662;
|
|
2
|
+
export const ids = [6662];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 6662:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
__webpack_require__.r(__webpack_exports__);
|
|
9
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
10
|
+
/* harmony export */ evaluateDriftPolicies: () => (/* binding */ evaluateDriftPolicies),
|
|
11
|
+
/* harmony export */ loadDriftPolicies: () => (/* binding */ loadDriftPolicies)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
|
|
14
|
+
// drift-policy.js — M4 deliverable #8, sub-project 8b, Task 2: a
|
|
15
|
+
// before/after drift-policy DSL evaluated against a GraphDiff (Task 1's
|
|
16
|
+
// own output, see graph-diff.js).
|
|
17
|
+
//
|
|
18
|
+
// Mirrors dataflow/privacy-sink-policy.js's fail-closed axis-matching
|
|
19
|
+
// STYLE exactly — the `_matchesEnvironment`/`_matchesDestination`
|
|
20
|
+
// precedent (an unset rule field is unconstrained; a set field the
|
|
21
|
+
// current context has no comparable value for does NOT match) — never
|
|
22
|
+
// its literal single-graph-state shape. That module evaluates a static
|
|
23
|
+
// state ("is this (class, sink) pair permitted right now"); this module
|
|
24
|
+
// evaluates a TRANSITION ("did this (class, sink) pair newly start
|
|
25
|
+
// flowing, or did this flow's own verdict regress"), which is why its
|
|
26
|
+
// rule shape is `{trigger, ...}`-keyed rather than `{allow: [...]}`.
|
|
27
|
+
//
|
|
28
|
+
// ── Design judgment calls made in this file (disclosed, not hidden) ──
|
|
29
|
+
//
|
|
30
|
+
// 1. `loadDriftPolicies(policyFilePath)` takes a literal file path, not
|
|
31
|
+
// a scanRoot — a deliberate, task-brief-directed divergence from
|
|
32
|
+
// `loadPrivacySinkPolicy(scanRoot)`'s own scanRoot-relative
|
|
33
|
+
// convention (`statePath(scanRoot, 'privacy-policy.json')`). Drift
|
|
34
|
+
// policies are a new artifact with no established scanRoot-relative
|
|
35
|
+
// home yet (Task 3's CLI wiring is the only real caller, and it can
|
|
36
|
+
// resolve whatever path convention it wants — e.g. a `--policy`
|
|
37
|
+
// flag, or a default under `.agentic-security/` — without this
|
|
38
|
+
// module forcing that choice). Keeping the signature a plain path
|
|
39
|
+
// keeps this module honest about being a pure evaluator with no
|
|
40
|
+
// opinion on where its config file lives.
|
|
41
|
+
//
|
|
42
|
+
// 2. The rule vocabulary generalizes `fromPolicyVerdict`/
|
|
43
|
+
// `toPolicyVerdict` to a second, structurally identical pair,
|
|
44
|
+
// `fromProtectionSummary`/`toProtectionSummary`, rather than adding a
|
|
45
|
+
// generic `field`/`from`/`to` triple. The task brief's own worked
|
|
46
|
+
// examples name `protectionSummary`'s "protected -> unknown/
|
|
47
|
+
// unprotected" transition as "an equivalent from/to pair for
|
|
48
|
+
// protectionSummary, generalizing" the policyVerdict pair — read
|
|
49
|
+
// literally, that is an instruction to add the SAME shape for a
|
|
50
|
+
// second watched field, not to invent a fully generic DSL over every
|
|
51
|
+
// WATCHED_FLOW_FIELDS entry. A generic `field`/`from`/`to` triple was
|
|
52
|
+
// considered and rejected: nothing in the brief's worked examples
|
|
53
|
+
// needs a THIRD field's transition, and a generic form would need its
|
|
54
|
+
// own validation against `WATCHED_FLOW_FIELDS` this task's tests
|
|
55
|
+
// don't exercise. Extending to a fully generic form remains easy
|
|
56
|
+
// later (both matchers already share one small helper,
|
|
57
|
+
// `_matchesVerdictTransition`) without a rule-shape migration for
|
|
58
|
+
// what's shipped today.
|
|
59
|
+
//
|
|
60
|
+
// 3. `dataClass`/`sinkCategory` are read as ADDITIONAL, optional
|
|
61
|
+
// constraints on a `changed_flow` rule too, not just `new_flow`
|
|
62
|
+
// rules — the brief's own rule shape lists them as top-level fields
|
|
63
|
+
// (not scoped textually to `new_flow` alone), and a changed flow's
|
|
64
|
+
// id is guaranteed present in `graphAfter` by construction (a
|
|
65
|
+
// `changed.flows` entry only exists when the SAME flow id survived
|
|
66
|
+
// into the after-graph — see graph-diff.js's own `computeGraphDiff`
|
|
67
|
+
// header), so the same `_resolveFlowContext` resolution that
|
|
68
|
+
// `new_flow` rules use applies unchanged. This lets an operator write
|
|
69
|
+
// "a PHI flow that regresses from protected" as one rule instead of
|
|
70
|
+
// two, and costs nothing when a rule doesn't set either field (both
|
|
71
|
+
// stay unconstrained, per the fail-closed style above).
|
|
72
|
+
//
|
|
73
|
+
// 4. A `changed_flow` rule that sets none of
|
|
74
|
+
// dataClass/sinkCategory/fromPolicyVerdict/toPolicyVerdict/
|
|
75
|
+
// fromProtectionSummary/toProtectionSummary matches EVERY
|
|
76
|
+
// `changed.flows` entry — every axis is independently unconstrained,
|
|
77
|
+
// so a rule with no axes set is the fully unconstrained case, exactly
|
|
78
|
+
// as an `allow` rule with neither `class` nor `sink` would be in
|
|
79
|
+
// `privacy-sink-policy.js` (it doesn't have that case in practice
|
|
80
|
+
// because `sink` is required there; this module has no required axis
|
|
81
|
+
// besides `trigger`, so the maximally-open rule is reachable and
|
|
82
|
+
// intentional — "alert on ANY change to a flow matching these
|
|
83
|
+
// constraints" is a real, useful policy shape).
|
|
84
|
+
//
|
|
85
|
+
// 5. The task brief's PRD-quoted worked examples also name "PII -> AI"
|
|
86
|
+
// ,"PCI -> log" (both plain `new_flow` shapes, already covered by
|
|
87
|
+
// dataClass/sinkCategory matching — no separate mechanism needed) and
|
|
88
|
+
// "new unresolved recipient" (a `governanceRefs.recipient`-shaped
|
|
89
|
+
// transition). The brief's own "Tests to write" section does not
|
|
90
|
+
// require a test for the recipient example, and it does not reduce
|
|
91
|
+
// cleanly to either matcher this task implements (a "new unresolved
|
|
92
|
+
// recipient" is a value becoming a specific sentinel, not a flow
|
|
93
|
+
// newly existing or an already-modeled verdict field transitioning)
|
|
94
|
+
// — left out rather than guessed at. Judgment call #2's rejected
|
|
95
|
+
// generic `field`/`from`/`to` form is the natural place to add it
|
|
96
|
+
// later, against a real governanceRefs.recipient fixture.
|
|
97
|
+
//
|
|
98
|
+
// 6. Fix round 1, Important 1 (see graph-diff.js's own judgment call #4):
|
|
99
|
+
// a `new_flow` rule must skip an added flow entry whose
|
|
100
|
+
// `causeClassification === 'reidentified'` — that flow isn't actually
|
|
101
|
+
// new, it's the SAME real-world (source, sink, dataElementIds) flow as
|
|
102
|
+
// a flow that existed before, just minted under a new flowId because
|
|
103
|
+
// the engine's own confidence/shape (evidenceGrade/transformationIds)
|
|
104
|
+
// changed. Firing `new_flow` on it would false-fire `--fail-on-drift`
|
|
105
|
+
// on a non-event, exactly what this fix round exists to close.
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
const POLICY_TRIGGERS = Object.freeze(['new_flow', 'changed_flow']);
|
|
110
|
+
|
|
111
|
+
function _isValidRuleShape(p) {
|
|
112
|
+
return !!p && typeof p === 'object' && !Array.isArray(p) && POLICY_TRIGGERS.includes(p.trigger);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load an operator's drift-policy rules from a literal JSON file path
|
|
117
|
+
* (see judgment call #1 above — never a scanRoot). Never throws — a
|
|
118
|
+
* missing file (ENOENT) is "no policies configured," matching
|
|
119
|
+
* `loadPrivacySinkPolicy`'s own precedent; a malformed file logs a
|
|
120
|
+
* warning and degrades to no policies. Each entry is validated loosely:
|
|
121
|
+
* an entry that isn't a plain object with a recognized `trigger` is
|
|
122
|
+
* skipped (with a warning naming how many were dropped), never crashing
|
|
123
|
+
* the whole load over one bad rule.
|
|
124
|
+
*/
|
|
125
|
+
function loadDriftPolicies(policyFilePath) {
|
|
126
|
+
const EMPTY = { policies: [] };
|
|
127
|
+
if (!policyFilePath) return EMPTY;
|
|
128
|
+
|
|
129
|
+
let raw = null;
|
|
130
|
+
try {
|
|
131
|
+
raw = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(policyFilePath, 'utf8'));
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (e.code !== 'ENOENT') {
|
|
134
|
+
console.error(`agentic-security: bad JSON in drift policy file (${policyFilePath}) — falling back to no policies (${e.message})`);
|
|
135
|
+
}
|
|
136
|
+
return EMPTY;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!Array.isArray(raw?.policies)) {
|
|
140
|
+
console.error(`agentic-security: drift policy file ${policyFilePath} has no "policies" array — falling back to no policies (expected {"policies":[...]})`);
|
|
141
|
+
}
|
|
142
|
+
const rawPolicies = Array.isArray(raw?.policies) ? raw.policies : [];
|
|
143
|
+
const policies = rawPolicies.filter(_isValidRuleShape);
|
|
144
|
+
const skipped = rawPolicies.length - policies.length;
|
|
145
|
+
if (skipped > 0) {
|
|
146
|
+
console.error(`agentic-security: skipped ${skipped} malformed drift-policy rule(s) in ${policyFilePath} (each must be an object with trigger: 'new_flow' | 'changed_flow')`);
|
|
147
|
+
}
|
|
148
|
+
return { policies };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fail-closed, matching privacy-sink-policy.js's own
|
|
152
|
+
// _matchesEnvironment/_matchesDestination precedent exactly: an unset
|
|
153
|
+
// rule value is unconstrained (matches anything); a set rule value with
|
|
154
|
+
// no comparable current value does NOT match.
|
|
155
|
+
function _matchesDataClass(ruleDataClass, dataClasses) {
|
|
156
|
+
if (ruleDataClass == null) return true;
|
|
157
|
+
if (!Array.isArray(dataClasses) || !dataClasses.length) return false;
|
|
158
|
+
return dataClasses.includes(ruleDataClass);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _matchesSinkCategory(ruleSinkCategory, sinkCategory) {
|
|
162
|
+
if (ruleSinkCategory == null) return true;
|
|
163
|
+
if (!sinkCategory) return false;
|
|
164
|
+
return ruleSinkCategory === sinkCategory;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Matches a rule's {fromX, toX} pair against the {before, after} of the
|
|
168
|
+
// changes[] entry for field X (or undefined, when that field didn't
|
|
169
|
+
// change on this flow). Unset fromX/toX is unconstrained. A set fromX/
|
|
170
|
+
// toX with no changes[] entry for that field at all (the flow changed,
|
|
171
|
+
// but not on this axis) does NOT match — fails closed, same as an
|
|
172
|
+
// environment-scoped rule against a caller who supplied no environment.
|
|
173
|
+
function _matchesVerdictTransition(ruleFrom, ruleTo, changeEntry) {
|
|
174
|
+
if (ruleFrom == null && ruleTo == null) return true;
|
|
175
|
+
if (!changeEntry) return false;
|
|
176
|
+
if (ruleFrom != null && changeEntry.before !== ruleFrom) return false;
|
|
177
|
+
if (ruleTo != null && changeEntry.after !== ruleTo) return false;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Resolves the real dataClasses/sinkCategory/sinkNode/flow for a flow id
|
|
182
|
+
// against graphAfter — shared by both new_flow (added.flows entries) and
|
|
183
|
+
// changed_flow (changed.flows entries) matching, since a changed.flows
|
|
184
|
+
// entry's id is, by construction, present in graphAfter too (see
|
|
185
|
+
// graph-diff.js's own computeGraphDiff: a changed entry only exists for
|
|
186
|
+
// a flow id surviving into the after-graph). Returns null when the id
|
|
187
|
+
// can't be resolved (should not happen for a well-formed GraphDiff
|
|
188
|
+
// produced by computeGraphDiff against its own graphAfter, but a rule
|
|
189
|
+
// must never crash the whole evaluation over a caller-supplied
|
|
190
|
+
// diff/graph pairing that doesn't actually match).
|
|
191
|
+
function _resolveFlowContext(flowId, graphAfter) {
|
|
192
|
+
const flow = (graphAfter?.flows ?? []).find((f) => f.id === flowId);
|
|
193
|
+
if (!flow) return null;
|
|
194
|
+
|
|
195
|
+
const dataClasses = [];
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
for (const deId of flow.dataElementIds ?? []) {
|
|
198
|
+
const de = (graphAfter.dataElements ?? []).find((d) => d.id === deId);
|
|
199
|
+
if (!de) continue;
|
|
200
|
+
for (const cls of de.dataClasses ?? []) {
|
|
201
|
+
if (!seen.has(cls)) { seen.add(cls); dataClasses.push(cls); }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const dataElementNames = (flow.dataElementIds ?? [])
|
|
205
|
+
.map((id) => (graphAfter.dataElements ?? []).find((d) => d.id === id)?.name)
|
|
206
|
+
.filter((n) => typeof n === 'string' && n.length > 0);
|
|
207
|
+
|
|
208
|
+
const sinkNode = (graphAfter.nodes ?? []).find((n) => n.id === flow.sink) ?? null;
|
|
209
|
+
const sinkCategory = sinkNode?.subtype ?? sinkNode?.kind ?? null;
|
|
210
|
+
|
|
211
|
+
return { flow, dataClasses, dataElementNames, sinkNode, sinkCategory };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _describeTransition(changeEntry) {
|
|
215
|
+
if (!changeEntry) return null;
|
|
216
|
+
return `${changeEntry.field} ${changeEntry.before} -> ${changeEntry.after}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function _buildViolation(rule, trigger, ctx, transitionDescriptions) {
|
|
220
|
+
const base = trigger === 'new_flow'
|
|
221
|
+
? `New flow ${ctx.flow.id} (${ctx.dataClasses.join('/') || 'unclassified data'}) newly reaches sink ${ctx.sinkCategory ?? ctx.sinkNode?.id ?? 'unknown'}`
|
|
222
|
+
: `Flow ${ctx.flow.id} changed: ${transitionDescriptions.length ? transitionDescriptions.join(', ') : 'matched changed_flow rule (no policyVerdict/protectionSummary transition named by this rule)'}`;
|
|
223
|
+
const reason = rule.reason ? `${base} (${rule.reason})` : base;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
flowId: ctx.flow.id,
|
|
227
|
+
dataElementNames: ctx.dataElementNames,
|
|
228
|
+
sinkCategory: ctx.sinkCategory,
|
|
229
|
+
sinkNodeId: ctx.sinkNode?.id ?? ctx.flow.sink,
|
|
230
|
+
trigger,
|
|
231
|
+
rule,
|
|
232
|
+
reason,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Evaluates operator-configured drift policies against a GraphDiff.
|
|
238
|
+
* `policies` is `{policies: [...]}` (loadDriftPolicies' own return
|
|
239
|
+
* shape). `graphAfter` is the AFTER graph the diff was computed against
|
|
240
|
+
* — needed because an `added.flows`/`changed.flows` entry carries only
|
|
241
|
+
* `{id, causeClassification, ...}` (see graph-diff.js's own header),
|
|
242
|
+
* never dataElement/sink info directly.
|
|
243
|
+
*
|
|
244
|
+
* Returns `{violations: [...]}` — never a bare boolean per flow; every
|
|
245
|
+
* violation names the real triggering flow (id, data element names,
|
|
246
|
+
* sink), the rule that fired, and a human-readable `reason` string.
|
|
247
|
+
*/
|
|
248
|
+
function evaluateDriftPolicies(diff, policies, graphAfter) {
|
|
249
|
+
const violations = [];
|
|
250
|
+
const rules = Array.isArray(policies?.policies) ? policies.policies : [];
|
|
251
|
+
if (!rules.length) return { violations };
|
|
252
|
+
|
|
253
|
+
for (const rule of rules) {
|
|
254
|
+
if (rule.trigger === 'new_flow') {
|
|
255
|
+
for (const addedEntry of diff?.added?.flows ?? []) {
|
|
256
|
+
// See judgment call #6 above / graph-diff.js's own judgment call
|
|
257
|
+
// #4: a reidentified added flow is not actually new.
|
|
258
|
+
if (addedEntry.causeClassification === 'reidentified') continue;
|
|
259
|
+
const ctx = _resolveFlowContext(addedEntry.id, graphAfter);
|
|
260
|
+
if (!ctx) continue;
|
|
261
|
+
if (!_matchesDataClass(rule.dataClass, ctx.dataClasses)) continue;
|
|
262
|
+
if (!_matchesSinkCategory(rule.sinkCategory, ctx.sinkCategory)) continue;
|
|
263
|
+
violations.push(_buildViolation(rule, 'new_flow', ctx, []));
|
|
264
|
+
}
|
|
265
|
+
} else if (rule.trigger === 'changed_flow') {
|
|
266
|
+
for (const changedEntry of diff?.changed?.flows ?? []) {
|
|
267
|
+
const ctx = _resolveFlowContext(changedEntry.id, graphAfter);
|
|
268
|
+
if (!ctx) continue;
|
|
269
|
+
if (!_matchesDataClass(rule.dataClass, ctx.dataClasses)) continue;
|
|
270
|
+
if (!_matchesSinkCategory(rule.sinkCategory, ctx.sinkCategory)) continue;
|
|
271
|
+
|
|
272
|
+
const changes = Array.isArray(changedEntry.changes) ? changedEntry.changes : [];
|
|
273
|
+
const policyChange = changes.find((c) => c.field === 'policyVerdict');
|
|
274
|
+
if (!_matchesVerdictTransition(rule.fromPolicyVerdict, rule.toPolicyVerdict, policyChange)) continue;
|
|
275
|
+
|
|
276
|
+
const protectionChange = changes.find((c) => c.field === 'protectionSummary');
|
|
277
|
+
if (!_matchesVerdictTransition(rule.fromProtectionSummary, rule.toProtectionSummary, protectionChange)) continue;
|
|
278
|
+
|
|
279
|
+
const transitionDescriptions = [policyChange, protectionChange].map(_describeTransition).filter(Boolean);
|
|
280
|
+
violations.push(_buildViolation(rule, 'changed_flow', ctx, transitionDescriptions));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// An unrecognized trigger was already filtered out by
|
|
284
|
+
// loadDriftPolicies' own _isValidRuleShape when loaded from disk; a
|
|
285
|
+
// directly hand-built `policies` object (as tests do) with an
|
|
286
|
+
// unrecognized trigger simply matches nothing, silently — the same
|
|
287
|
+
// "match nothing rather than throw" discipline as every other
|
|
288
|
+
// unresolvable axis in this file.
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { violations };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
/***/ })
|
|
296
|
+
|
|
297
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const id =
|
|
2
|
-
export const ids = [
|
|
1
|
+
export const id = 6675;
|
|
2
|
+
export const ids = [6675];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
5
|
/***/ 6675:
|
|
@@ -281,7 +281,7 @@ async function _queryOsvForDep({ name, ecosystem }, { fetchImpl = globalThis.fet
|
|
|
281
281
|
// ─── State persistence ───────────────────────────────────────────────────
|
|
282
282
|
|
|
283
283
|
function loadState(scanRoot) {
|
|
284
|
-
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__
|
|
284
|
+
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, 'cve-alerts-state.json');
|
|
285
285
|
if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(fp)) return { known: new Set(), lastRun: null };
|
|
286
286
|
try {
|
|
287
287
|
const j = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'));
|
|
@@ -290,7 +290,7 @@ function loadState(scanRoot) {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
function persistState(scanRoot, state) {
|
|
293
|
-
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__
|
|
293
|
+
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, 'cve-alerts-state.json');
|
|
294
294
|
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.stateWritesEnabled)()) return;
|
|
295
295
|
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_1__.dirname(fp), { recursive: true });
|
|
296
296
|
node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(fp, JSON.stringify({
|
|
@@ -300,7 +300,7 @@ function persistState(scanRoot, state) {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
function loadConfig(scanRoot) {
|
|
303
|
-
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__
|
|
303
|
+
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, 'cve-alerts.json');
|
|
304
304
|
if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(fp)) return null;
|
|
305
305
|
try { return JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
306
306
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const id =
|
|
2
|
-
export const ids = [
|
|
1
|
+
export const id = 6730;
|
|
2
|
+
export const ids = [6730,5144];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
5
|
/***/ 5144:
|
|
@@ -65,7 +65,7 @@ const LEGAL_HOLD_FILE = 'legal-holds.json';
|
|
|
65
65
|
|
|
66
66
|
function _loadRaw(scanRoot) {
|
|
67
67
|
let fp;
|
|
68
|
-
try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__
|
|
68
|
+
try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE); } catch { return []; }
|
|
69
69
|
let raw;
|
|
70
70
|
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return []; }
|
|
71
71
|
try {
|
|
@@ -135,7 +135,7 @@ function addLegalHold(scanRoot, { artifact, owner, reason, expires_at } = {}) {
|
|
|
135
135
|
const hold = { artifact, owner, reason, expires_at: expires_at || null, created_at: new Date().toISOString() };
|
|
136
136
|
const holds = _loadRaw(scanRoot);
|
|
137
137
|
holds.push(hold);
|
|
138
|
-
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__
|
|
138
|
+
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE);
|
|
139
139
|
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .safeWriteState */ .Ep)(fp, JSON.stringify(holds, null, 2) + '\n')) {
|
|
140
140
|
return { ok: false, reason: 'state writes are disabled (--no-state) or this is not a safe state directory' };
|
|
141
141
|
}
|
|
@@ -151,7 +151,7 @@ function removeLegalHold(scanRoot, artifact) {
|
|
|
151
151
|
const remaining = holds.filter(h => !(h && h.artifact === artifact));
|
|
152
152
|
const removedCount = holds.length - remaining.length;
|
|
153
153
|
if (removedCount > 0) {
|
|
154
|
-
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__
|
|
154
|
+
const fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__.statePath)(scanRoot, LEGAL_HOLD_FILE);
|
|
155
155
|
(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .safeWriteState */ .Ep)(fp, JSON.stringify(remaining, null, 2) + '\n');
|
|
156
156
|
}
|
|
157
157
|
return removedCount;
|
|
@@ -242,7 +242,7 @@ const RETENTION_DEFAULTS = {
|
|
|
242
242
|
function loadRetentionPolicy(scanRoot) {
|
|
243
243
|
if (!scanRoot) return null;
|
|
244
244
|
let fp;
|
|
245
|
-
try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__
|
|
245
|
+
try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_2__.statePath)(scanRoot, POLICY_FILE); } catch { return null; }
|
|
246
246
|
let raw;
|
|
247
247
|
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return null; }
|
|
248
248
|
try {
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export const id = 6829;
|
|
2
|
+
export const ids = [6829];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 6829:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ OBLIGATION_EVIDENCE_PACK_SCHEMA: () => (/* binding */ OBLIGATION_EVIDENCE_PACK_SCHEMA),
|
|
10
|
+
/* harmony export */ buildObligationEvidencePack: () => (/* binding */ buildObligationEvidencePack),
|
|
11
|
+
/* harmony export */ ensureKeyPair: () => (/* reexport safe */ _evidence_bundle_js__WEBPACK_IMPORTED_MODULE_0__.ensureKeyPair),
|
|
12
|
+
/* harmony export */ signObligationEvidencePack: () => (/* binding */ signObligationEvidencePack),
|
|
13
|
+
/* harmony export */ verifyObligationEvidencePack: () => (/* binding */ verifyObligationEvidencePack)
|
|
14
|
+
/* harmony export */ });
|
|
15
|
+
/* harmony import */ var _evidence_bundle_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8317);
|
|
16
|
+
/* harmony import */ var _evidence_grade_wording_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3540);
|
|
17
|
+
/* harmony import */ var _lineage_export_json_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(859);
|
|
18
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(7598);
|
|
19
|
+
// obligation-evidence-pack.js — Milestone 4 sub-project 6c: signed,
|
|
20
|
+
// versioned evidence packs for a Regulatory Obligation Overlay framework
|
|
21
|
+
// evaluation (FR-504, PRD §10.10). Fourth sibling in the
|
|
22
|
+
// evidence-bundle.js family — see that module's own header for the
|
|
23
|
+
// shared-key rationale, and provenance-evidence-bundle.js for the most
|
|
24
|
+
// directly mirrored precedent (same reused ensureKeyPair/keyPaths/
|
|
25
|
+
// canonicalJson, own schema string, own build/sign/verify trio, own
|
|
26
|
+
// top-level-key allowlist).
|
|
27
|
+
//
|
|
28
|
+
// WHAT THIS IS NOT
|
|
29
|
+
// -----------------
|
|
30
|
+
// Not a reuse of evidence-bundle.js's own bundle shape (that's a single
|
|
31
|
+
// FINDING's evidence — proofTier/taintPath/etc; this artifact has no
|
|
32
|
+
// finding at all). Not compliance-evidence-signing.js's ComplianceEvidence
|
|
33
|
+
// manifest either — that signs the pre-existing family:/module:/rule:-
|
|
34
|
+
// driven present/partial/absent/manual walkthrough status; this signs the
|
|
35
|
+
// newer, additive graph: mapping type's own real ObligationMapping
|
|
36
|
+
// records (evaluateGraphFlowPredicate/buildObligationMappingFromGraphPredicate,
|
|
37
|
+
// sub-project 6b) — a distinct FR-504 artifact with its own field list
|
|
38
|
+
// (scope, framework versions, facts, evidence index, unknown/manual
|
|
39
|
+
// items, accepted exceptions, scan health, limitations, graph digest,
|
|
40
|
+
// reproducibility metadata).
|
|
41
|
+
//
|
|
42
|
+
// THE EVIDENCE-INDEX DESIGN DECISION (see the scoping doc for the full
|
|
43
|
+
// writeup)
|
|
44
|
+
// -----------------------------------------------------------------
|
|
45
|
+
// A real ObligationMapping record's own `evidence[]` is structurally
|
|
46
|
+
// always empty today (graph-builder.js hardcodes `edge.evidenceRefs: []`
|
|
47
|
+
// on every minted edge, disclosed in obligation-predicates.js's own
|
|
48
|
+
// header). Rather than ship an evidence pack whose "evidence index" is
|
|
49
|
+
// honestly, permanently empty, this module builds a REAL evidence index
|
|
50
|
+
// from each fact's own `contributingGraphIds` (real flow ids
|
|
51
|
+
// evaluateGraphFlowPredicate already returns) — resolving each flow id
|
|
52
|
+
// back into a small, real summary (source/sink kind, dataElement
|
|
53
|
+
// dataClasses, the edge's own transit/atRest/handling verdicts) by
|
|
54
|
+
// joining against the graph's own entity arrays. `record.evidence` is
|
|
55
|
+
// still carried through verbatim on each fact (honest, even though
|
|
56
|
+
// empty) — the evidence index is an ADDITIONAL section built from data
|
|
57
|
+
// that genuinely IS populated, not a replacement that hides the gap.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const OBLIGATION_EVIDENCE_PACK_SCHEMA = 'agentic-security/obligation-evidence-pack@1';
|
|
65
|
+
|
|
66
|
+
function _asArray(v) {
|
|
67
|
+
return Array.isArray(v) ? v : [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve one ObligationMapping fact's contributingGraphIds (real flow
|
|
72
|
+
* ids) into a small, real, human-and-machine-readable summary per flow —
|
|
73
|
+
* the evidence index's own per-fact contribution. Never throws: a flow id
|
|
74
|
+
* that no longer resolves against this graph (a stale pack re-evaluated
|
|
75
|
+
* against a newer graph, or a malformed fact) is simply skipped, not
|
|
76
|
+
* fabricated.
|
|
77
|
+
*/
|
|
78
|
+
function _resolveEvidenceForFact(fact, joins) {
|
|
79
|
+
const { flowsById, nodesById, edgesById, dataElementsById } = joins;
|
|
80
|
+
const flowIds = _asArray(fact?.contributingGraphIds);
|
|
81
|
+
const resolved = [];
|
|
82
|
+
for (const flowId of flowIds) {
|
|
83
|
+
const flow = flowsById.get(flowId);
|
|
84
|
+
if (!flow) continue;
|
|
85
|
+
const sourceNode = nodesById.get(flow.source);
|
|
86
|
+
const sinkNode = nodesById.get(flow.sink);
|
|
87
|
+
// Reads only edgeIds[0] — mirrors obligation-predicates.js's own
|
|
88
|
+
// disclosed, currently-unreachable limitation (graph-builder.js always
|
|
89
|
+
// mints single-edge flows; see that module's own header) rather than
|
|
90
|
+
// inventing a different convention for the same case here.
|
|
91
|
+
const edge = edgesById.get(_asArray(flow.edgeIds)[0]);
|
|
92
|
+
const dataClasses = _asArray(flow.dataElementIds)
|
|
93
|
+
.map((id) => dataElementsById.get(id))
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.flatMap((d) => _asArray(d.dataClasses));
|
|
96
|
+
resolved.push({
|
|
97
|
+
flowId,
|
|
98
|
+
source: sourceNode ? { kind: sourceNode.kind ?? null, subtype: sourceNode.subtype ?? null } : null,
|
|
99
|
+
sink: sinkNode ? { kind: sinkNode.kind ?? null, subtype: sinkNode.subtype ?? null } : null,
|
|
100
|
+
dataClasses: [...new Set(dataClasses)],
|
|
101
|
+
transitVerdict: edge?.protection?.transit?.verdict ?? null,
|
|
102
|
+
atRestVerdict: edge?.protection?.atRest?.verdict ?? null,
|
|
103
|
+
handlingVerdict: edge?.protection?.handling?.verdict ?? null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _buildJoins(graph) {
|
|
110
|
+
return {
|
|
111
|
+
flowsById: new Map(_asArray(graph?.flows).filter(Boolean).map((f) => [f.id, f])),
|
|
112
|
+
nodesById: new Map(_asArray(graph?.nodes).filter(Boolean).map((n) => [n.id, n])),
|
|
113
|
+
edgesById: new Map(_asArray(graph?.edges).filter(Boolean).map((e) => [e.id, e])),
|
|
114
|
+
dataElementsById: new Map(_asArray(graph?.dataElements).filter(Boolean).map((d) => [d.id, d])),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build an unsigned evidence pack from a framework evaluation. Never
|
|
120
|
+
* throws: every field degrades honestly on missing input rather than
|
|
121
|
+
* fabricating a value — a null/absent graph yields empty facts/
|
|
122
|
+
* evidenceIndex and a null graphDigest/scope, never a guess.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} args
|
|
125
|
+
* @param {object|null} args.graph - the scan's DataFlowGraph v1 document (scan.lineageGraph), or null
|
|
126
|
+
* @param {object} args.framework - the loaded framework object (auditor-walkthrough.js#loadFramework's return)
|
|
127
|
+
* @param {Array} args.evaluation - auditor-walkthrough.js#evaluateFramework's return (per-control entries, each carrying .obligationMappings)
|
|
128
|
+
* @param {object|null} [args.scanHealth] - scan.scanHealth, passed through verbatim; null if not supplied, never fabricated
|
|
129
|
+
* @param {string|null} [args.engineVersion]
|
|
130
|
+
* @param {string|null} [args.rulesetVersion]
|
|
131
|
+
* @param {string|null} [args.bundleSha]
|
|
132
|
+
* @param {string} [args.generatedAt] - defaults to new Date().toISOString()
|
|
133
|
+
*/
|
|
134
|
+
function buildObligationEvidencePack({
|
|
135
|
+
graph, framework, evaluation, scanHealth, engineVersion, rulesetVersion, bundleSha, generatedAt,
|
|
136
|
+
} = {}) {
|
|
137
|
+
const facts = _asArray(evaluation).flatMap((e) => _asArray(e?.obligationMappings));
|
|
138
|
+
const joins = _buildJoins(graph);
|
|
139
|
+
const evidenceIndex = facts.map((fact) => ({
|
|
140
|
+
obligationId: fact?.id ?? null,
|
|
141
|
+
requirementId: fact?.requirementId ?? null,
|
|
142
|
+
evidence: _resolveEvidenceForFact(fact, joins),
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
schema: OBLIGATION_EVIDENCE_PACK_SCHEMA,
|
|
147
|
+
framework: {
|
|
148
|
+
id: framework?.id ?? null,
|
|
149
|
+
name: framework?.name ?? null,
|
|
150
|
+
version: framework?.controlsDigest ?? null,
|
|
151
|
+
publisher: framework?.publisher ?? null,
|
|
152
|
+
url: framework?.url ?? null,
|
|
153
|
+
},
|
|
154
|
+
scope: graph?.scope ?? null,
|
|
155
|
+
facts,
|
|
156
|
+
evidenceIndex,
|
|
157
|
+
unknownItems: facts.filter((f) => f?.state === 'unknown'),
|
|
158
|
+
manualItems: facts.filter((f) => f?.state === 'manual_required'),
|
|
159
|
+
acceptedExceptions: facts.filter((f) => f?.state === 'accepted_exception'),
|
|
160
|
+
scanHealth: scanHealth ?? null,
|
|
161
|
+
limitations: _asArray(graph?.limitations),
|
|
162
|
+
graphDigest: graph ? (0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_2__.computeGraphDigest)(graph) : null,
|
|
163
|
+
reproducibility: {
|
|
164
|
+
graphId: graph?.graphId ?? null,
|
|
165
|
+
graphDigest: graph ? (0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_2__.computeGraphDigest)(graph) : null,
|
|
166
|
+
engineVersion: engineVersion ?? null,
|
|
167
|
+
rulesetVersion: rulesetVersion ?? null,
|
|
168
|
+
bundleSha: bundleSha ?? null,
|
|
169
|
+
generatedAt: generatedAt ?? new Date().toISOString(),
|
|
170
|
+
},
|
|
171
|
+
disclaimer: _evidence_grade_wording_js__WEBPACK_IMPORTED_MODULE_1__/* .EVIDENCE_GRADE_DISCLAIMER */ .ux,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Sign a pack. Returns a new object; the input is not mutated. */
|
|
176
|
+
function signObligationEvidencePack(pack, privateKeyPem) {
|
|
177
|
+
const sig = node_crypto__WEBPACK_IMPORTED_MODULE_3__.sign(null, Buffer.from((0,_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_0__/* .canonicalJson */ .dj)(pack), 'utf8'), privateKeyPem);
|
|
178
|
+
return {
|
|
179
|
+
...pack,
|
|
180
|
+
signature: { algorithm: 'ed25519', canonicalisation: OBLIGATION_EVIDENCE_PACK_SCHEMA, value: sig.toString('base64') },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const OBLIGATION_EVIDENCE_PACK_TOP_LEVEL_KEYS = new Set([
|
|
185
|
+
'schema', 'framework', 'scope', 'facts', 'evidenceIndex', 'unknownItems',
|
|
186
|
+
'manualItems', 'acceptedExceptions', 'scanHealth', 'limitations',
|
|
187
|
+
'graphDigest', 'reproducibility', 'disclaimer', 'signature',
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Verify with a PUBLIC key only. Rejects any top-level key outside the
|
|
192
|
+
* allowlist BEFORE checking the signature — same EA-03 discipline every
|
|
193
|
+
* sibling in this family carries: a signature only covers the bytes it
|
|
194
|
+
* was computed over, so a key stapled on after signing would otherwise
|
|
195
|
+
* verify as authentic.
|
|
196
|
+
*/
|
|
197
|
+
function verifyObligationEvidencePack(pack, publicKeyPem) {
|
|
198
|
+
if (!pack || typeof pack !== 'object') return { ok: false, reason: 'pack is not an object' };
|
|
199
|
+
if (pack.schema !== OBLIGATION_EVIDENCE_PACK_SCHEMA) return { ok: false, reason: `unrecognised schema: ${pack.schema}` };
|
|
200
|
+
const unknownKeys = Object.keys(pack).filter((k) => !OBLIGATION_EVIDENCE_PACK_TOP_LEVEL_KEYS.has(k));
|
|
201
|
+
if (unknownKeys.length) {
|
|
202
|
+
return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
|
|
203
|
+
}
|
|
204
|
+
const sig = pack.signature;
|
|
205
|
+
if (!sig?.value) return { ok: false, reason: 'pack is unsigned' };
|
|
206
|
+
if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
|
|
207
|
+
if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
|
|
208
|
+
const { signature, ...unsigned } = pack;
|
|
209
|
+
let ok = false;
|
|
210
|
+
try {
|
|
211
|
+
ok = node_crypto__WEBPACK_IMPORTED_MODULE_3__.verify(null, Buffer.from((0,_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_0__/* .canonicalJson */ .dj)(unsigned), 'utf8'), publicKeyPem, Buffer.from(sig.value, 'base64'));
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return { ok: false, reason: `verification error: ${e.message}` };
|
|
214
|
+
}
|
|
215
|
+
return ok
|
|
216
|
+
? { ok: true, reason: null }
|
|
217
|
+
: { ok: false, reason: 'signature does not match the pack contents — it was modified after signing' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
/***/ })
|
|
224
|
+
|
|
225
|
+
};
|