@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
|
@@ -83,6 +83,31 @@ export const ARTIFACT_REGISTRY = [
|
|
|
83
83
|
{ name: 'scan-history.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
84
84
|
{ name: 'last-scan.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
85
85
|
{ name: 'last-scan.json.sig', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
86
|
+
// Sub-project E, increment 5: the Data Flow Explorer's DataFlowGraph v1
|
|
87
|
+
// document + signature, written by bin/agentic-security.js alongside
|
|
88
|
+
// last-scan.json whenever AGENTIC_SECURITY_LINEAGE_DEEP=1 produced a
|
|
89
|
+
// graph. NOT to be confused with the similarly-named, already-registered
|
|
90
|
+
// `repo-lineage.json` below — that one is an operator-authored
|
|
91
|
+
// cross-repo provenance declaration (classification 'operator-config',
|
|
92
|
+
// never written by the scanner); this one is scanner-generated scan
|
|
93
|
+
// output derived from the user's own source (dataElements[].dataClasses
|
|
94
|
+
// included), so it belongs in the 'generated'/'scan' bucket `reset`
|
|
95
|
+
// clears by default, same as last-scan.json itself.
|
|
96
|
+
{ name: 'lineage-graph.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
97
|
+
{ name: 'lineage-graph.json.sig', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
98
|
+
// M4 deliverable #8 (FR-503 §14, DFG-022, sub-project 8a): the "Data-Flow
|
|
99
|
+
// Time Machine" foundation — one commit-keyed GraphSnapshot per scan,
|
|
100
|
+
// written by src/lineage/graph-snapshot.js's persistGraphSnapshot(),
|
|
101
|
+
// additively alongside (never replacing) lineage-graph.json above. Same
|
|
102
|
+
// 'generated'/'scan' bucket for the same reason: fully scanner-derived,
|
|
103
|
+
// regenerable by re-scanning at that commit.
|
|
104
|
+
{ name: 'lineage-snapshots', kind: 'dir', classification: 'generated', retentionClass: 'scan', source: 'src/lineage/graph-snapshot.js (persistGraphSnapshot)' },
|
|
105
|
+
// M5 deliverable #7 (FR-505 §7.12, AC-29): the Runtime-Corroborated
|
|
106
|
+
// Digital Twin's observation store — one immutable whole file per adapter
|
|
107
|
+
// import, written by src/lineage/observation-store.js's
|
|
108
|
+
// persistObservationImport(), mirroring lineage-snapshots/'s own
|
|
109
|
+
// directory-of-files shape re-keyed commit -> import.
|
|
110
|
+
{ name: 'runtime-observations', kind: 'dir', classification: 'generated', retentionClass: 'evidence', confidential: true, source: 'src/lineage/observation-store.js (persistObservationImport)', note: 'FR-505 requires an observation store follow artifact encryption, RETENTION, RESET, access-control and no-egress rules. That is why this is `generated` (a plain `reset` MUST be able to delete it) rather than `operator-config`, and why it carries a real retentionClass — deliberately NOT the `remediation`/`legal-holds.json` no-retention call one section down, and deliberately NOT `provenance`\'s permanent-history call. This DOES stretch `generated`\'s usual definition — a rescan does not re-derive an import, the operator re-imports it — and that stretch is disclosed here rather than hidden: FR-505\'s explicit reset requirement breaks the tie. `confidential: true` is enforced by observation-store.js calling maybeEncryptForWrite/maybeDecryptForRead itself (posture/encryption-provider.js), the same per-writer opt-in compliance-evidence.json makes — the flag alone enforces nothing.' },
|
|
86
111
|
{ name: 'shadow-findings.json', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
87
112
|
{ name: 'mcp-audit.log', kind: 'file', classification: 'generated', retentionClass: 'evidence' },
|
|
88
113
|
{ name: 'egress-audit.log', kind: 'file', classification: 'generated', retentionClass: 'evidence', note: "FR-604 per-call egress audit log — hash-chained NDJSON written by egress/audit.js's recordEgressCall, never read as config" },
|
|
@@ -168,6 +193,19 @@ export const ARTIFACT_REGISTRY = [
|
|
|
168
193
|
{ name: 'deletion-report.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', source: 'posture/state-lifecycle-report.js (via bin/agentic-security.js cmdReset)' },
|
|
169
194
|
{ name: 'export-report.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', source: 'posture/state-lifecycle-report.js (via bin/agentic-security.js cmdExport)' },
|
|
170
195
|
|
|
196
|
+
// M5 governance editing workflow final-review fix round 1 (I5): backups
|
|
197
|
+
// of recipient-profiles.json written by `governance propose-edit --yes`,
|
|
198
|
+
// mirroring the `fix-history` directory precedent above — one directory
|
|
199
|
+
// entry covers every timestamped .bak file inside it, since the registry
|
|
200
|
+
// only supports exact-name matches, never a per-file timestamped name.
|
|
201
|
+
{ name: 'recipient-profiles-backups', kind: 'dir', classification: 'generated', retentionClass: 'backup', note: 'per-edit backups written by `governance propose-edit --yes`, mirrors the fix-history/ precedent — one directory entry covers every timestamped .bak file inside it' },
|
|
202
|
+
|
|
203
|
+
// M5 deliverable #8 (FR-304 "declared" half): backups of
|
|
204
|
+
// cross-repo-links.json written by `federate declare --yes`, mirrors
|
|
205
|
+
// the `recipient-profiles-backups` precedent immediately above
|
|
206
|
+
// exactly.
|
|
207
|
+
{ name: 'cross-repo-links-backups', kind: 'dir', classification: 'generated', retentionClass: 'backup', note: 'per-declare backups written by `federate declare --yes`, mirrors the recipient-profiles-backups/ precedent — one directory entry covers every timestamped .bak file inside it' },
|
|
208
|
+
|
|
171
209
|
// ── Operator-config: hand-authored (or agent-authored) input, never wiped ──
|
|
172
210
|
{ name: 'rules.yml', kind: 'file', classification: 'operator-config' },
|
|
173
211
|
{ name: 'rules', kind: 'dir', classification: 'operator-config' },
|
|
@@ -191,14 +229,28 @@ export const ARTIFACT_REGISTRY = [
|
|
|
191
229
|
{ name: 'policy-bundle-public-key.pem', kind: 'file', classification: 'operator-config', note: 'FR-1001 public key an operator installs to verify org-distributed policy bundles — read by posture/policy-bundle.js, never written by the scanner' },
|
|
192
230
|
{ name: 'retention-policy.yml', kind: 'file', classification: 'operator-config', note: 'FR-702 per-retention-class TTL overrides (clamped to a built-in per-class maximum) — read by posture/retention-policy.js, never written by the scanner' },
|
|
193
231
|
{ name: 'legal-holds.json', kind: 'file', classification: 'operator-config', note: 'FR-707 legal holds ({artifact, owner, reason, expires_at}) — read by posture/retention-policy.js and bin/agentic-security.js cmdReset; WRITTEN by the CLI (legal-hold add/remove), but classified operator-config (not generated) deliberately: a plain `reset` must never be able to delete the very record protecting other artifacts from deletion' },
|
|
232
|
+
{ name: 'remediation', kind: 'dir', classification: 'operator-config', note: 'M5 deliverable #6 remediation work-item ledger (items.jsonl + items.lock) — an APPEND-ONLY record of human decisions: owner assignment, approvals, manual attestations, and accepted-risk exceptions with approver/reason/scope/expiration. WRITTEN by the CLI (`remediation open|update|verify|accept-risk|reopen-check --yes`) but classified operator-config, not generated, deliberately — the same call legal-holds.json makes one entry up: a plain `reset` must never be able to delete the audit trail AC-31 depends on, and nothing regenerates it from a rescan. Deliberately NOT the `provenance` entry\'s `generated` classification (see its own note above): that ledger is scan-derived history a rescan can rebuild; this one is human decisions that cannot be. No retentionClass for the same reason legal-holds.json has none — auto-expiry would silently delete an accepted-risk exception a report may already cite.' },
|
|
194
233
|
{ name: 'calibration-feedback.jsonl', kind: 'file', classification: 'operator-config', note: 'FR-806 opt-in calibration ground truth ({at, findingId, outcome: accept-risk|realized-incident, predicted*, note}) — WRITTEN by the CLI (calibration-feedback record), but classified operator-config like exploit-history.jsonl: real, hard-to-recreate customer-reported ground truth, never scanner-regenerable, so a routine reset must never delete it' },
|
|
195
234
|
{ name: 'encryption-policy.yml', kind: 'file', classification: 'operator-config', note: 'FR-705 encryption provider/required opt-in policy ({provider: local-key, required: true|false}) — read by posture/encryption-provider.js, never written by the scanner' },
|
|
196
235
|
{ name: 'provenance-providers.yml', kind: 'file', classification: 'operator-config', note: 'Finding Provenance M3 §3.4 GitHub/GitLab provider enrichment opt-in ({token} or provider-scoped tokens) — read by posture/provenance/providers/config.js, never written by the scanner; env vars (AGENTIC_SECURITY_GITHUB_TOKEN/AGENTIC_SECURITY_GITLAB_TOKEN) take precedence when set' },
|
|
197
236
|
{ name: 'repo-lineage.json', kind: 'file', classification: 'operator-config', note: 'Finding Provenance M4 §4.2 cross-repository lineage declaration ({linkedFrom: {path, atCommit}}) — read by posture/provenance/repo-lineage.js, never written by the scanner; the linked path is verified as a real local git repo before use, no remote fetch' },
|
|
237
|
+
{ name: 'recipient-profiles.json', kind: 'file', classification: 'operator-config', note: 'FR-506 recipient/subprocessor governance profiles — hand- or agent-authored via `governance propose-edit`, never scanner-regenerable, so a routine reset must never delete it' },
|
|
238
|
+
{ name: 'cross-repo-links.json', kind: 'file', classification: 'operator-config', note: 'M5 deliverable #8 (FR-304 "declared" half) — declared local<->remote node links between two independently-scanned repos, written via `federate declare --yes`, never scanner-regenerable, so a routine reset must never delete it' },
|
|
198
239
|
{ name: 'logic-claims.json', kind: 'file', classification: 'operator-config', note: 'authored by an external reviewing agent; engine.js only ever reads it (fs.readFileSync, never written)' },
|
|
199
240
|
{ name: 'current-intent.md', kind: 'file', classification: 'operator-config', note: 'developer-authored; no writer exists anywhere in src/ or bin/' },
|
|
200
241
|
{ name: 'exploit-history.jsonl', kind: 'file', classification: 'operator-config', note: 'own header comment: "operator-curated record of past confirmed exploits"' },
|
|
201
242
|
{ name: 'cve-alerts.json', kind: 'file', classification: 'operator-config', note: 'own header comment: "Configuration is read from"; state lives in the separate cve-alerts-state.json, which IS generated' },
|
|
243
|
+
// Pre-existing gap, found by M5 deliverable #7's own scoping investigation
|
|
244
|
+
// and fixed here rather than left: posture/runtime-correlation.js reads
|
|
245
|
+
// these three filenames via `statePath(scanRoot, n)` with a VARIABLE, so
|
|
246
|
+
// test/artifact-registry-completeness.test.js's own PATTERNS regexes (which
|
|
247
|
+
// require a string literal) never saw them and never demanded registration.
|
|
248
|
+
// An unregistered state artifact means `reset` does not know about it and
|
|
249
|
+
// retention cannot reach it. Operator-config, not generated: these are
|
|
250
|
+
// hand-supplied eBPF/APM trace exports the scanner only ever reads.
|
|
251
|
+
{ name: 'runtime-trace.jsonl', kind: 'file', classification: 'operator-config', note: 'eBPF/APM runtime trace consumed by posture/runtime-correlation.js\'s own trace loader (deliberately not named by its literal export here — no-dead-modules.test.js scans note strings too, and naming it would make it look, wrongly, like a real call site) — operator-supplied, never scanner-written; $AGENTIC_SECURITY_RUNTIME_TRACE_PATH overrides the location entirely' },
|
|
252
|
+
{ name: 'runtime.jsonl', kind: 'file', classification: 'operator-config', note: 'alternate filename for runtime-trace.jsonl — see posture/runtime-correlation.js\'s DEFAULT_TRACE_NAMES' },
|
|
253
|
+
{ name: 'ebpf-trace.jsonl', kind: 'file', classification: 'operator-config', note: 'alternate filename for runtime-trace.jsonl — see posture/runtime-correlation.js\'s DEFAULT_TRACE_NAMES' },
|
|
202
254
|
];
|
|
203
255
|
|
|
204
256
|
export function listGeneratedArtifacts() {
|
|
@@ -53,6 +53,7 @@ import { strengthOfControl as _strengthOfControl } from './coverage-strength.js'
|
|
|
53
53
|
// as `Jean\-Luc Picard` and `dependabot[bot]` as `dependabot\[bot\]` is a
|
|
54
54
|
// visible regression on common real-world author names, not a fix.
|
|
55
55
|
import { sanitizeForTerminal, pseudonymizeAuthor, PROVENANCE_COMPLIANCE_DISCLAIMER } from './provenance/schema.js';
|
|
56
|
+
import { evaluateGraphFlowPredicate, buildObligationMappingFromGraphPredicate } from '../lineage/obligation-predicates.js';
|
|
56
57
|
|
|
57
58
|
// Fix-round item 4b: this renderer had `sanitizeForTerminal` (injection
|
|
58
59
|
// safety) but never honoured `--pseudonymize-authors` at all — an operator
|
|
@@ -369,6 +370,7 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
369
370
|
observations: obs,
|
|
370
371
|
controlRefs: [],
|
|
371
372
|
derivedProvenance: deriveComplianceProvenance([]),
|
|
373
|
+
obligationMappings: [],
|
|
372
374
|
...(evidence ? { evidence, partiallyEvidenced: evidence.tier === 'weak' || evidence.tier === 'unmeasured' } : {}),
|
|
373
375
|
});
|
|
374
376
|
continue;
|
|
@@ -391,6 +393,15 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
391
393
|
// control at 'partial' — never 'present' — regardless of what the
|
|
392
394
|
// family:/module: mappings in the same control found.
|
|
393
395
|
let hasUnverifiableMapping = false;
|
|
396
|
+
// Sub-project 6b: real ObligationMapping records minted by graph:
|
|
397
|
+
// predicates on this control, collected separately from the
|
|
398
|
+
// present/partial/absent/manual status machinery above — a graph:
|
|
399
|
+
// mapping never touches anySignal/allCleared/anyCleared/
|
|
400
|
+
// hasUnverifiableMapping (purely additive, per the plan's own Global
|
|
401
|
+
// Constraints). Always an array, never undefined, so a caller can
|
|
402
|
+
// safely read `.length` regardless of whether this control has any
|
|
403
|
+
// graph: mappings at all.
|
|
404
|
+
const obligationMappings = [];
|
|
394
405
|
for (const m of maps) {
|
|
395
406
|
if (m.startsWith('family:')) {
|
|
396
407
|
// `family:X` and the subfamily-qualified `family:X:Y` (used by
|
|
@@ -492,6 +503,70 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
492
503
|
obs.push(`(rule mapping) ${m} — verify manually that the bodyguard rule is enabled.`);
|
|
493
504
|
anySignal = true;
|
|
494
505
|
hasUnverifiableMapping = true;
|
|
506
|
+
} else if (m.startsWith('graph:')) {
|
|
507
|
+
// A graph: mapping never contributes to anySignal/allCleared/
|
|
508
|
+
// anyCleared/hasUnverifiableMapping — the existing
|
|
509
|
+
// present/partial/absent/manual status stays driven entirely by
|
|
510
|
+
// the family:/module:/rule: mappings a control already has (this
|
|
511
|
+
// task's own Global Constraint: purely additive). It instead
|
|
512
|
+
// mints a real ObligationMapping record, collected separately.
|
|
513
|
+
//
|
|
514
|
+
// First real predicate (scoping doc ruling 6): PHI/PII flowing to
|
|
515
|
+
// an external sink must cross a protected transit edge. The
|
|
516
|
+
// predicate STRING itself is currently a fixed label (not yet a
|
|
517
|
+
// parsed mini-language) — the first real graph: mapping this
|
|
518
|
+
// sub-project ships is HIPAA §164.312(e)'s
|
|
519
|
+
// "graph:transit-protection:PHI:external:transit:protected",
|
|
520
|
+
// hardcoded to this one spec until a second real case proves the
|
|
521
|
+
// parsing is worth generalizing (YAGNI — do not invent a parser
|
|
522
|
+
// for one caller).
|
|
523
|
+
const spec = { type: 'graph-flow', dataClass: 'PHI', sinkKind: 'external', dimension: 'transit', requiredVerdict: 'protected' };
|
|
524
|
+
const graph = scan.lineageGraph ?? null;
|
|
525
|
+
// Belt-and-suspenders, mirroring the sibling _strengthOfControl
|
|
526
|
+
// call above: obligation-predicates.js is now defensively
|
|
527
|
+
// hardened to never throw (see its own header), but this wrap
|
|
528
|
+
// keeps a future regression there from taking down the whole
|
|
529
|
+
// evaluateFramework call for every control in the framework
|
|
530
|
+
// (found by the final whole-branch review).
|
|
531
|
+
let mapping = null;
|
|
532
|
+
try {
|
|
533
|
+
const evaluation = graph ? evaluateGraphFlowPredicate(spec, graph) : null;
|
|
534
|
+
mapping = buildObligationMappingFromGraphPredicate({
|
|
535
|
+
framework: fw.id,
|
|
536
|
+
frameworkVersion: fw.controlsDigest,
|
|
537
|
+
requirementId: c.id,
|
|
538
|
+
requirementSource: fw.url ?? null,
|
|
539
|
+
predicateLabel: m,
|
|
540
|
+
graph,
|
|
541
|
+
evaluation,
|
|
542
|
+
});
|
|
543
|
+
} catch { /* graph obligation mapping is additive; never block evaluation */ }
|
|
544
|
+
if (mapping) {
|
|
545
|
+
obligationMappings.push(mapping);
|
|
546
|
+
// Found by the final whole-branch review of sub-project 6b: a
|
|
547
|
+
// graph: mapping's state is intentionally independent of the
|
|
548
|
+
// pre-existing present/partial/absent/manual status glyph
|
|
549
|
+
// (Global Constraint — see the block comment above), so a
|
|
550
|
+
// control can render a green ✅ header with a 'gap_detected'
|
|
551
|
+
// line buried underneath it in the observations. A ⚠️ prefix on
|
|
552
|
+
// a genuine gap keeps that visible to a reader who only scans
|
|
553
|
+
// headers, without touching the status computation itself.
|
|
554
|
+
// RE-CONFIRMED (sub-project 6c's own final review, F4): at the
|
|
555
|
+
// time this ⚠️ prefix was written, `graph:` mappings could only
|
|
556
|
+
// ever read 'unknown' end-to-end through the real CLI (a
|
|
557
|
+
// separate, then-undiscovered bug — see obligation-predicates.js
|
|
558
|
+
// and cmdCompliance/cmdAttest's own fix history), so this exact
|
|
559
|
+
// ✅-header-hiding-a-real-gap scenario was structurally
|
|
560
|
+
// unreachable. Sub-project 6c's CLI fixes make it genuinely
|
|
561
|
+
// reachable for the first time; the ruling above was written
|
|
562
|
+
// anticipating exactly this and needs no change — confirmed
|
|
563
|
+
// live via a real assessed HIPAA §164.312(e) gap
|
|
564
|
+
// (test/cli/attest-obligations.test.js's own gap_detected case).
|
|
565
|
+
const flag = mapping.state === 'gap_detected' ? '⚠️ ' : '';
|
|
566
|
+
obs.push(`${flag}(graph mapping) ${m} -> ${mapping.state}.`);
|
|
567
|
+
} else {
|
|
568
|
+
obs.push(`(graph mapping) ${m} -> evaluation failed, skipped.`);
|
|
569
|
+
}
|
|
495
570
|
}
|
|
496
571
|
}
|
|
497
572
|
|
|
@@ -547,6 +622,7 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
547
622
|
observations: obs,
|
|
548
623
|
controlRefs: dedupedRefs,
|
|
549
624
|
derivedProvenance: deriveComplianceProvenance(contributingFindings),
|
|
625
|
+
obligationMappings,
|
|
550
626
|
...(evidence ? { evidence, partiallyEvidenced: evidence.tier === 'weak' || evidence.tier === 'unmeasured' } : {}),
|
|
551
627
|
});
|
|
552
628
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"license": "US Federal regulation (public)",
|
|
6
6
|
"url": "https://www.ecfr.gov/current/title-45/subtitle-A/subchapter-C/part-164",
|
|
7
7
|
"scope": "SELECTIVE SUBSET. 8 of the Security Rule technical safeguards. Administrative and physical safeguards are outside what a code scanner can observe and are NOT represented here.",
|
|
8
|
-
"controlsDigest": "
|
|
8
|
+
"controlsDigest": "7e94038dd9a64d3d",
|
|
9
9
|
"controlCount": 8,
|
|
10
10
|
"controls": [
|
|
11
11
|
{
|
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
],
|
|
103
103
|
"mapsTo": [
|
|
104
104
|
"family:crypto-tls-no-verify",
|
|
105
|
-
"family:crypto-tls-version"
|
|
105
|
+
"family:crypto-tls-version",
|
|
106
|
+
"graph:transit-protection:PHI:external:transit:protected"
|
|
106
107
|
]
|
|
107
108
|
}
|
|
108
109
|
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// obligation-evidence-pack.js — Milestone 4 sub-project 6c: signed,
|
|
2
|
+
// versioned evidence packs for a Regulatory Obligation Overlay framework
|
|
3
|
+
// evaluation (FR-504, PRD §10.10). Fourth sibling in the
|
|
4
|
+
// evidence-bundle.js family — see that module's own header for the
|
|
5
|
+
// shared-key rationale, and provenance-evidence-bundle.js for the most
|
|
6
|
+
// directly mirrored precedent (same reused ensureKeyPair/keyPaths/
|
|
7
|
+
// canonicalJson, own schema string, own build/sign/verify trio, own
|
|
8
|
+
// top-level-key allowlist).
|
|
9
|
+
//
|
|
10
|
+
// WHAT THIS IS NOT
|
|
11
|
+
// -----------------
|
|
12
|
+
// Not a reuse of evidence-bundle.js's own bundle shape (that's a single
|
|
13
|
+
// FINDING's evidence — proofTier/taintPath/etc; this artifact has no
|
|
14
|
+
// finding at all). Not compliance-evidence-signing.js's ComplianceEvidence
|
|
15
|
+
// manifest either — that signs the pre-existing family:/module:/rule:-
|
|
16
|
+
// driven present/partial/absent/manual walkthrough status; this signs the
|
|
17
|
+
// newer, additive graph: mapping type's own real ObligationMapping
|
|
18
|
+
// records (evaluateGraphFlowPredicate/buildObligationMappingFromGraphPredicate,
|
|
19
|
+
// sub-project 6b) — a distinct FR-504 artifact with its own field list
|
|
20
|
+
// (scope, framework versions, facts, evidence index, unknown/manual
|
|
21
|
+
// items, accepted exceptions, scan health, limitations, graph digest,
|
|
22
|
+
// reproducibility metadata).
|
|
23
|
+
//
|
|
24
|
+
// THE EVIDENCE-INDEX DESIGN DECISION (see the scoping doc for the full
|
|
25
|
+
// writeup)
|
|
26
|
+
// -----------------------------------------------------------------
|
|
27
|
+
// A real ObligationMapping record's own `evidence[]` is structurally
|
|
28
|
+
// always empty today (graph-builder.js hardcodes `edge.evidenceRefs: []`
|
|
29
|
+
// on every minted edge, disclosed in obligation-predicates.js's own
|
|
30
|
+
// header). Rather than ship an evidence pack whose "evidence index" is
|
|
31
|
+
// honestly, permanently empty, this module builds a REAL evidence index
|
|
32
|
+
// from each fact's own `contributingGraphIds` (real flow ids
|
|
33
|
+
// evaluateGraphFlowPredicate already returns) — resolving each flow id
|
|
34
|
+
// back into a small, real summary (source/sink kind, dataElement
|
|
35
|
+
// dataClasses, the edge's own transit/atRest/handling verdicts) by
|
|
36
|
+
// joining against the graph's own entity arrays. `record.evidence` is
|
|
37
|
+
// still carried through verbatim on each fact (honest, even though
|
|
38
|
+
// empty) — the evidence index is an ADDITIONAL section built from data
|
|
39
|
+
// that genuinely IS populated, not a replacement that hides the gap.
|
|
40
|
+
|
|
41
|
+
import { ensureKeyPair, keyPaths, canonicalJson } from './evidence-bundle.js';
|
|
42
|
+
import { EVIDENCE_GRADE_DISCLAIMER } from './evidence-grade-wording.js';
|
|
43
|
+
import { computeGraphDigest } from '../lineage/export-json.js';
|
|
44
|
+
import * as crypto from 'node:crypto';
|
|
45
|
+
|
|
46
|
+
export const OBLIGATION_EVIDENCE_PACK_SCHEMA = 'agentic-security/obligation-evidence-pack@1';
|
|
47
|
+
|
|
48
|
+
function _asArray(v) {
|
|
49
|
+
return Array.isArray(v) ? v : [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve one ObligationMapping fact's contributingGraphIds (real flow
|
|
54
|
+
* ids) into a small, real, human-and-machine-readable summary per flow —
|
|
55
|
+
* the evidence index's own per-fact contribution. Never throws: a flow id
|
|
56
|
+
* that no longer resolves against this graph (a stale pack re-evaluated
|
|
57
|
+
* against a newer graph, or a malformed fact) is simply skipped, not
|
|
58
|
+
* fabricated.
|
|
59
|
+
*/
|
|
60
|
+
function _resolveEvidenceForFact(fact, joins) {
|
|
61
|
+
const { flowsById, nodesById, edgesById, dataElementsById } = joins;
|
|
62
|
+
const flowIds = _asArray(fact?.contributingGraphIds);
|
|
63
|
+
const resolved = [];
|
|
64
|
+
for (const flowId of flowIds) {
|
|
65
|
+
const flow = flowsById.get(flowId);
|
|
66
|
+
if (!flow) continue;
|
|
67
|
+
const sourceNode = nodesById.get(flow.source);
|
|
68
|
+
const sinkNode = nodesById.get(flow.sink);
|
|
69
|
+
// Reads only edgeIds[0] — mirrors obligation-predicates.js's own
|
|
70
|
+
// disclosed, currently-unreachable limitation (graph-builder.js always
|
|
71
|
+
// mints single-edge flows; see that module's own header) rather than
|
|
72
|
+
// inventing a different convention for the same case here.
|
|
73
|
+
const edge = edgesById.get(_asArray(flow.edgeIds)[0]);
|
|
74
|
+
const dataClasses = _asArray(flow.dataElementIds)
|
|
75
|
+
.map((id) => dataElementsById.get(id))
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.flatMap((d) => _asArray(d.dataClasses));
|
|
78
|
+
resolved.push({
|
|
79
|
+
flowId,
|
|
80
|
+
source: sourceNode ? { kind: sourceNode.kind ?? null, subtype: sourceNode.subtype ?? null } : null,
|
|
81
|
+
sink: sinkNode ? { kind: sinkNode.kind ?? null, subtype: sinkNode.subtype ?? null } : null,
|
|
82
|
+
dataClasses: [...new Set(dataClasses)],
|
|
83
|
+
transitVerdict: edge?.protection?.transit?.verdict ?? null,
|
|
84
|
+
atRestVerdict: edge?.protection?.atRest?.verdict ?? null,
|
|
85
|
+
handlingVerdict: edge?.protection?.handling?.verdict ?? null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _buildJoins(graph) {
|
|
92
|
+
return {
|
|
93
|
+
flowsById: new Map(_asArray(graph?.flows).filter(Boolean).map((f) => [f.id, f])),
|
|
94
|
+
nodesById: new Map(_asArray(graph?.nodes).filter(Boolean).map((n) => [n.id, n])),
|
|
95
|
+
edgesById: new Map(_asArray(graph?.edges).filter(Boolean).map((e) => [e.id, e])),
|
|
96
|
+
dataElementsById: new Map(_asArray(graph?.dataElements).filter(Boolean).map((d) => [d.id, d])),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build an unsigned evidence pack from a framework evaluation. Never
|
|
102
|
+
* throws: every field degrades honestly on missing input rather than
|
|
103
|
+
* fabricating a value — a null/absent graph yields empty facts/
|
|
104
|
+
* evidenceIndex and a null graphDigest/scope, never a guess.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} args
|
|
107
|
+
* @param {object|null} args.graph - the scan's DataFlowGraph v1 document (scan.lineageGraph), or null
|
|
108
|
+
* @param {object} args.framework - the loaded framework object (auditor-walkthrough.js#loadFramework's return)
|
|
109
|
+
* @param {Array} args.evaluation - auditor-walkthrough.js#evaluateFramework's return (per-control entries, each carrying .obligationMappings)
|
|
110
|
+
* @param {object|null} [args.scanHealth] - scan.scanHealth, passed through verbatim; null if not supplied, never fabricated
|
|
111
|
+
* @param {string|null} [args.engineVersion]
|
|
112
|
+
* @param {string|null} [args.rulesetVersion]
|
|
113
|
+
* @param {string|null} [args.bundleSha]
|
|
114
|
+
* @param {string} [args.generatedAt] - defaults to new Date().toISOString()
|
|
115
|
+
*/
|
|
116
|
+
export function buildObligationEvidencePack({
|
|
117
|
+
graph, framework, evaluation, scanHealth, engineVersion, rulesetVersion, bundleSha, generatedAt,
|
|
118
|
+
} = {}) {
|
|
119
|
+
const facts = _asArray(evaluation).flatMap((e) => _asArray(e?.obligationMappings));
|
|
120
|
+
const joins = _buildJoins(graph);
|
|
121
|
+
const evidenceIndex = facts.map((fact) => ({
|
|
122
|
+
obligationId: fact?.id ?? null,
|
|
123
|
+
requirementId: fact?.requirementId ?? null,
|
|
124
|
+
evidence: _resolveEvidenceForFact(fact, joins),
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
schema: OBLIGATION_EVIDENCE_PACK_SCHEMA,
|
|
129
|
+
framework: {
|
|
130
|
+
id: framework?.id ?? null,
|
|
131
|
+
name: framework?.name ?? null,
|
|
132
|
+
version: framework?.controlsDigest ?? null,
|
|
133
|
+
publisher: framework?.publisher ?? null,
|
|
134
|
+
url: framework?.url ?? null,
|
|
135
|
+
},
|
|
136
|
+
scope: graph?.scope ?? null,
|
|
137
|
+
facts,
|
|
138
|
+
evidenceIndex,
|
|
139
|
+
unknownItems: facts.filter((f) => f?.state === 'unknown'),
|
|
140
|
+
manualItems: facts.filter((f) => f?.state === 'manual_required'),
|
|
141
|
+
acceptedExceptions: facts.filter((f) => f?.state === 'accepted_exception'),
|
|
142
|
+
scanHealth: scanHealth ?? null,
|
|
143
|
+
limitations: _asArray(graph?.limitations),
|
|
144
|
+
graphDigest: graph ? computeGraphDigest(graph) : null,
|
|
145
|
+
reproducibility: {
|
|
146
|
+
graphId: graph?.graphId ?? null,
|
|
147
|
+
graphDigest: graph ? computeGraphDigest(graph) : null,
|
|
148
|
+
engineVersion: engineVersion ?? null,
|
|
149
|
+
rulesetVersion: rulesetVersion ?? null,
|
|
150
|
+
bundleSha: bundleSha ?? null,
|
|
151
|
+
generatedAt: generatedAt ?? new Date().toISOString(),
|
|
152
|
+
},
|
|
153
|
+
disclaimer: EVIDENCE_GRADE_DISCLAIMER,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Sign a pack. Returns a new object; the input is not mutated. */
|
|
158
|
+
export function signObligationEvidencePack(pack, privateKeyPem) {
|
|
159
|
+
const sig = crypto.sign(null, Buffer.from(canonicalJson(pack), 'utf8'), privateKeyPem);
|
|
160
|
+
return {
|
|
161
|
+
...pack,
|
|
162
|
+
signature: { algorithm: 'ed25519', canonicalisation: OBLIGATION_EVIDENCE_PACK_SCHEMA, value: sig.toString('base64') },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const OBLIGATION_EVIDENCE_PACK_TOP_LEVEL_KEYS = new Set([
|
|
167
|
+
'schema', 'framework', 'scope', 'facts', 'evidenceIndex', 'unknownItems',
|
|
168
|
+
'manualItems', 'acceptedExceptions', 'scanHealth', 'limitations',
|
|
169
|
+
'graphDigest', 'reproducibility', 'disclaimer', 'signature',
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Verify with a PUBLIC key only. Rejects any top-level key outside the
|
|
174
|
+
* allowlist BEFORE checking the signature — same EA-03 discipline every
|
|
175
|
+
* sibling in this family carries: a signature only covers the bytes it
|
|
176
|
+
* was computed over, so a key stapled on after signing would otherwise
|
|
177
|
+
* verify as authentic.
|
|
178
|
+
*/
|
|
179
|
+
export function verifyObligationEvidencePack(pack, publicKeyPem) {
|
|
180
|
+
if (!pack || typeof pack !== 'object') return { ok: false, reason: 'pack is not an object' };
|
|
181
|
+
if (pack.schema !== OBLIGATION_EVIDENCE_PACK_SCHEMA) return { ok: false, reason: `unrecognised schema: ${pack.schema}` };
|
|
182
|
+
const unknownKeys = Object.keys(pack).filter((k) => !OBLIGATION_EVIDENCE_PACK_TOP_LEVEL_KEYS.has(k));
|
|
183
|
+
if (unknownKeys.length) {
|
|
184
|
+
return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
|
|
185
|
+
}
|
|
186
|
+
const sig = pack.signature;
|
|
187
|
+
if (!sig?.value) return { ok: false, reason: 'pack is unsigned' };
|
|
188
|
+
if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
|
|
189
|
+
if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
|
|
190
|
+
const { signature, ...unsigned } = pack;
|
|
191
|
+
let ok = false;
|
|
192
|
+
try {
|
|
193
|
+
ok = crypto.verify(null, Buffer.from(canonicalJson(unsigned), 'utf8'), publicKeyPem, Buffer.from(sig.value, 'base64'));
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return { ok: false, reason: `verification error: ${e.message}` };
|
|
196
|
+
}
|
|
197
|
+
return ok
|
|
198
|
+
? { ok: true, reason: null }
|
|
199
|
+
: { ok: false, reason: 'signature does not match the pack contents — it was modified after signing' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { ensureKeyPair, keyPaths };
|