@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
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
// mode to behave differently from advisory, that is a deliberate, separate
|
|
43
43
|
// decision -- not something to guess at here.
|
|
44
44
|
|
|
45
|
+
import { isProvenanceHealthy } from '../posture/provenance/schema.js';
|
|
46
|
+
|
|
45
47
|
export const ASSURANCE_MODES = Object.freeze(['advisory', 'standard', 'strict']);
|
|
46
48
|
export const DEFAULT_ASSURANCE_MODE = 'standard';
|
|
47
49
|
|
|
@@ -56,7 +58,7 @@ function _isValidMode(mode) {
|
|
|
56
58
|
* ok:false only ever happens in strict mode; advisory/standard always ok:true
|
|
57
59
|
* (they report, they do not gate).
|
|
58
60
|
*/
|
|
59
|
-
export function evaluateAssuranceMode(mode, scanHealth) {
|
|
61
|
+
export function evaluateAssuranceMode(mode, scanHealth, findings = []) {
|
|
60
62
|
const effectiveMode = _isValidMode(mode) ? mode : DEFAULT_ASSURANCE_MODE;
|
|
61
63
|
const conditions = Array.isArray(scanHealth?.conditions) ? scanHealth.conditions : [];
|
|
62
64
|
|
|
@@ -85,6 +87,67 @@ export function evaluateAssuranceMode(mode, scanHealth) {
|
|
|
85
87
|
conditions,
|
|
86
88
|
};
|
|
87
89
|
}
|
|
90
|
+
|
|
91
|
+
// M2 §2.5: strict cares about overall scan completeness, which now
|
|
92
|
+
// explicitly includes PROVENANCE completeness, not just detector/analyzer
|
|
93
|
+
// completeness. A finding whose findingProvenance status is outside
|
|
94
|
+
// ['complete','uncommitted'] — including a finding with NO
|
|
95
|
+
// findingProvenance at all, e.g. --no-provenance was used — means strict
|
|
96
|
+
// cannot vouch for this scan's provenance the same way it already refuses
|
|
97
|
+
// to vouch for a scan with a failed analyzer.
|
|
98
|
+
//
|
|
99
|
+
// KNOWN INTERACTION: scan.secrets/scan.logicVulns are unconditionally
|
|
100
|
+
// stamped not_available today (M0+M1 deliberately deferred real origin
|
|
101
|
+
// resolution for those two channels — see the M2/M3/M4 design spec's
|
|
102
|
+
// §2.6). Any real secret or logic finding therefore fails strict mode
|
|
103
|
+
// until that resolution work lands. This is the literal, intended
|
|
104
|
+
// consequence of "never false certainty" applied to strict's own
|
|
105
|
+
// definition, not an oversight — a strict-mode operator with secrets
|
|
106
|
+
// findings should expect this until M3+ closes that gap.
|
|
107
|
+
//
|
|
108
|
+
// This list is INCOMPLETE without scan.supplyChain, and the omission
|
|
109
|
+
// matters more than the secrets/logic one above because it hits nearly
|
|
110
|
+
// every real project. engine.js stamps every supplyChain entry
|
|
111
|
+
// not_available too (see the loop over `supplyChain` right after the
|
|
112
|
+
// `annotateGitProvenance` calls), and that bucket covers three distinct
|
|
113
|
+
// populations, not one:
|
|
114
|
+
//
|
|
115
|
+
// - transitive `vulnerable_dep` findings: a genuine, if currently
|
|
116
|
+
// unresolved, DEFERRAL — same shape as secrets/logicVulns above. The
|
|
117
|
+
// vulnerable version was never declared in this repo's own manifests,
|
|
118
|
+
// so there is no local commit to walk yet, but one could exist to
|
|
119
|
+
// resolve in a later phase.
|
|
120
|
+
// - `unpinned_dep` / `no_lockfile` findings: a CATEGORY ERROR, not a
|
|
121
|
+
// deferral. These describe an ABSENT state (a version range with no
|
|
122
|
+
// pin, a manifest with no lockfile) — there is no "commit that
|
|
123
|
+
// introduced a missing lockfile" for any future resolver to find,
|
|
124
|
+
// because the finding is about the absence of an event, not an event
|
|
125
|
+
// itself. No amount of future engineering work makes these resolvable.
|
|
126
|
+
//
|
|
127
|
+
// Direct `vulnerable_dep` findings DO go through real origin resolution
|
|
128
|
+
// (`resolveDirectSCAOrigin`, gated on `isDirect`) and are not part of this
|
|
129
|
+
// limitation.
|
|
130
|
+
//
|
|
131
|
+
// Net effect: because `unpinned_dep`/`no_lockfile` findings are a category
|
|
132
|
+
// error rather than a deferral, `--assurance strict` will fail on nearly
|
|
133
|
+
// any real project that has a `package.json` (or equivalent manifest)
|
|
134
|
+
// today — an unpinned or unlocked dependency is common, and this check has
|
|
135
|
+
// no way to ever resolve one. This is a known, disclosed limitation of the
|
|
136
|
+
// current implementation, not a bug, and it is not something this check
|
|
137
|
+
// should route around: exempting these finding types from the strict-mode
|
|
138
|
+
// gate was considered and deliberately deferred to a future milestone
|
|
139
|
+
// rather than done here, so strict mode keeps refusing to vouch for
|
|
140
|
+
// provenance it cannot actually speak to.
|
|
141
|
+
const badProvenance = (Array.isArray(findings) ? findings : []).filter((f) => !isProvenanceHealthy(f?.findingProvenance));
|
|
142
|
+
if (badProvenance.length > 0) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
mode: 'strict',
|
|
146
|
+
reason: `strict mode requires complete finding provenance; ${badProvenance.length} finding(s) have status outside [complete, uncommitted]`,
|
|
147
|
+
conditions,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
88
151
|
return { ok: true, mode: 'strict', reason: null, conditions };
|
|
89
152
|
}
|
|
90
153
|
|
|
@@ -46,7 +46,14 @@ export const FINDING_SCHEMA_VERSION = 1;
|
|
|
46
46
|
// sets to a real (non-null) value today; `optional` fields are legitimately
|
|
47
47
|
// null on many findings (e.g. a finding no annotator has enriched yet).
|
|
48
48
|
export const FINDING_FIELD_GROUPS = {
|
|
49
|
-
|
|
49
|
+
// `findingProvenance` is REQUIRED, not optional, and that is deliberate:
|
|
50
|
+
// posture/provenance/coordinator.js guarantees every finding it sees leaves
|
|
51
|
+
// with a TERMINAL provenance object, expressing every failure mode as a
|
|
52
|
+
// status ('not_available', 'uncommitted', 'budget_exhausted', 'error')
|
|
53
|
+
// rather than as an absent field. So a missing findingProvenance never means
|
|
54
|
+
// "provenance didn't apply here" — it means the finding escaped annotation
|
|
55
|
+
// entirely, which is exactly the condition this group exists to surface.
|
|
56
|
+
identity: { required: ['id', 'kind', 'vuln', 'findingProvenance'], optional: ['stableId'] },
|
|
50
57
|
location: { required: ['file', 'line'], optional: ['snippet'] },
|
|
51
58
|
classification: { required: ['severity'], optional: ['cwe', 'owaspLlm', 'family', 'parser', 'tags', 'description'] },
|
|
52
59
|
confidence: { required: [], optional: ['confidence', 'confidenceTier', 'calibrated_confidence', 'calibration_reason'] },
|
|
@@ -33,6 +33,7 @@ export const SCAN_HEALTH_SCHEMA_VERSION = 1;
|
|
|
33
33
|
* @param {Array<{phase:string, err:string}>} input.annotatorErrors
|
|
34
34
|
* @param {object} [input.engineErrors] - e.g. { cppDataflowParseErrors }
|
|
35
35
|
* @param {object} [input.deepStatus] - { requested, enabled, inCi, ciOverrideAllowed, reason, failure }
|
|
36
|
+
* @param {object} [input.lineageStatus] - { requested, enabled, reason, failure } — Sub-project E, increment 5's own status, kept separate from deepStatus (IR-taint's own).
|
|
36
37
|
* @param {object} [input.analyzerCoverage] - coverage-ledger.js's
|
|
37
38
|
* summarizeCoverageForScanHealth() output: {expected, completed, failed,
|
|
38
39
|
* timedOut, skippedByPolicy}. Omitted (not just empty) is treated the
|
|
@@ -41,7 +42,7 @@ export const SCAN_HEALTH_SCHEMA_VERSION = 1;
|
|
|
41
42
|
* never a fabricated all-zero summary.
|
|
42
43
|
* @returns {object} scanHealth per PRD §10.3, additive fields only.
|
|
43
44
|
*/
|
|
44
|
-
export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engineErrors = null, deepStatus = null, analyzerCoverage = null } = {}) {
|
|
45
|
+
export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engineErrors = null, deepStatus = null, analyzerCoverage = null, lineageStatus = null } = {}) {
|
|
45
46
|
const conditions = [];
|
|
46
47
|
const safeAnnotatorErrors = Array.isArray(annotatorErrors) ? annotatorErrors : [];
|
|
47
48
|
const filesTimedOut = Number(scanMeta?.filesTimedOut) || 0;
|
|
@@ -63,6 +64,15 @@ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engin
|
|
|
63
64
|
if (deepStatus?.requested && !deepStatus.enabled) {
|
|
64
65
|
conditions.push(`deep analysis was requested but did not run: ${deepStatus.reason || 'unknown reason'}`);
|
|
65
66
|
}
|
|
67
|
+
// Sub-project E, increment 5: lineage graph build status — a SEPARATE
|
|
68
|
+
// condition from deepStatus above, never folded into it. deepStatus is
|
|
69
|
+
// specifically IR-taint's own status; conflating the two would make
|
|
70
|
+
// `scanHealth.deepAnalysis.failure` ambiguous about which subsystem
|
|
71
|
+
// actually failed (the same "distinguish A from B" discipline this
|
|
72
|
+
// module already applies to analyzerCoverage vs. annotatorErrors).
|
|
73
|
+
if (lineageStatus?.failure) {
|
|
74
|
+
conditions.push(`lineage graph build threw and was skipped: ${lineageStatus.failure}`);
|
|
75
|
+
}
|
|
66
76
|
// FR-203: a detector that threw on at least one file (captured via
|
|
67
77
|
// FR-201's runDetector isolation) is a real analysis gap distinct from
|
|
68
78
|
// an ANNOTATOR error above — annotators run post-detection over the
|
|
@@ -96,6 +106,14 @@ export function computeScanHealth({ scanMeta = null, annotatorErrors = [], engin
|
|
|
96
106
|
failure: deepStatus.failure ?? null,
|
|
97
107
|
}
|
|
98
108
|
: null,
|
|
109
|
+
lineageAnalysis: lineageStatus
|
|
110
|
+
? {
|
|
111
|
+
requested: !!lineageStatus.requested,
|
|
112
|
+
enabled: !!lineageStatus.enabled,
|
|
113
|
+
reason: lineageStatus.reason ?? null,
|
|
114
|
+
failure: lineageStatus.failure ?? null,
|
|
115
|
+
}
|
|
116
|
+
: null,
|
|
99
117
|
annotatorErrorCount: safeAnnotatorErrors.length,
|
|
100
118
|
freshness: null,
|
|
101
119
|
conditions,
|
package/src/posture/CLAUDE.md
CHANGED
|
@@ -42,6 +42,23 @@ actually took — and are flagged `reliable:false` below n=10 rather than hidden
|
|
|
42
42
|
or quoted as settled. Recording goes through `isSafeStateDir`, so it declines
|
|
43
43
|
rather than creating a stray state dir outside a project.
|
|
44
44
|
|
|
45
|
+
**Remediation ledger (M5 deliverable #6)** — `remediation-ledger.js`. The
|
|
46
|
+
I/O half of Blast-Radius: Remediation Command Center (FR-507 + AC-31) —
|
|
47
|
+
locking, JSONL append, tolerant read, and a hash chain over
|
|
48
|
+
`.agentic-security/remediation/items.jsonl`/`items.lock`, matching
|
|
49
|
+
`fix-metrics.js`'s own append-and-tolerant-read shape above (one
|
|
50
|
+
newline-terminated record per `appendFileSync`, a torn tail dropped on
|
|
51
|
+
read, never a whole-file rewrite). The pure state machine it writes
|
|
52
|
+
through — `foldRemediationItem`/`foldRemediationLedger`/
|
|
53
|
+
`validateTransition` — lives at `../lineage/remediation.js`, making this
|
|
54
|
+
the SECOND `posture/` → `lineage/` import in the codebase
|
|
55
|
+
(`auditor-walkthrough.js`'s `graph:` branch was the first — see "First
|
|
56
|
+
`posture/` → `lineage/` import" below). `appendLedgerEvent` is the single
|
|
57
|
+
place `validateTransition` is enforced; no CLI command computes validity
|
|
58
|
+
for itself. Its `withLock` is a faithful local PORT of
|
|
59
|
+
`provenance/lifecycle.js`'s own `withLock` — not an import, since that
|
|
60
|
+
function is not exported.
|
|
61
|
+
|
|
45
62
|
**Agentic verification** — `verifier.js`, `verifier-target.js`, `verifier-ephemeral.js`, `harness-discovery.js`, `adversary-agent.js`, `defender-agent.js`, `auditor-agent.js`, `three-agent-pipeline.js`.
|
|
46
63
|
|
|
47
64
|
**Methodology additions (Agentic Methodology PRD, removed post-implementation)** — default-on annotators/artifacts that layer the agentic-hunter methodology on the deterministic engine:
|
|
@@ -83,6 +100,8 @@ Wired in `bin/agentic-security.js` after every filter and after `makeDeterminist
|
|
|
83
100
|
|
|
84
101
|
**Rule lifecycle** — `custom-rules.js` (YAML pattern DSL), `rule-overrides.js` (`disable:` gated on signature), `rule-packs.js`, `rule-synthesis.js` (proposes suppressions from triage feedback), `ruleset-version.js`.
|
|
85
102
|
|
|
103
|
+
**First `posture/` → `lineage/` import.** `auditor-walkthrough.js`'s `graph:` mapping branch (Data Flow Explorer M4 sub-project 6b) imports `evaluateGraphFlowPredicate`/`buildObligationMappingFromGraphPredicate` from `../lineage/obligation-predicates.js` — the first time any module in this directory has reached into `lineage/` (previously a one-way boundary: `lineage/` never imports `posture/`, and nothing here imported it back). It mints a real `ObligationMapping` record (see `scanner/src/lineage/CLAUDE.md`) from `scan.lineageGraph` when present, purely additively — never touching the pre-existing `anySignal`/`allCleared`/`anyCleared`/`hasUnverifiableMapping` status machinery.
|
|
104
|
+
|
|
86
105
|
**NIST Privacy Framework 1.1 (`privacy-framework.js`)** — assessment + remediation
|
|
87
106
|
over the bundled `compliance-frameworks/nist-privacy-1-1.json` (all 104 controls).
|
|
88
107
|
Sits on top of `auditor-walkthrough.js`'s evaluator and adds the half a narrative
|
|
@@ -351,6 +370,127 @@ Driver: `scripts/comparison.mjs`, over the CVE-replay corpus.
|
|
|
351
370
|
|
|
352
371
|
**State artifact registry (assurance-hardening PRD FR-701/FR-703)** — `artifact-registry.js`. The registry `cmdReset` (bin/agentic-security.js) now iterates instead of two hardcoded WIPE/WIPE_DIRS Sets. Every known `.agentic-security/` artifact is classified `generated` (scanner-written, safe to delete on reset) or `operator-config` (hand- or agent-authored input, never deleted) — built from an audit of every `statePath()`/`stateDir()` call site, not guessed from filenames; several looked generated by name but turned out to be inputs (`.agentic-security/logic-claims.json`, `.agentic-security/exploit-history.jsonl`, `.agentic-security/cve-alerts.json`, `.agentic-security/network-policy.json`, `.agentic-security/current-intent.md` — see the module's own header for the evidence behind each). Guarded by a completeness test (`test/artifact-registry-completeness.test.js`) that scans for every `statePath()`/`stateDir()` literal and fails if one isn't registered — a `no-dead-modules.test.js`-style drift guard, not a snapshot.
|
|
353
372
|
|
|
373
|
+
## Finding provenance — `provenance/` (20 modules)
|
|
374
|
+
|
|
375
|
+
The only SUBDIRECTORY under `posture/`, because it is a pipeline rather than an
|
|
376
|
+
annotator: twenty small modules that together answer "which commit introduced
|
|
377
|
+
this finding, and how sure are we?" Everything outside the subdirectory sees one
|
|
378
|
+
function, `annotateGitProvenance(findings, ctx)` from `coordinator.js`, wired in
|
|
379
|
+
`engine.js` after every finding has been appended.
|
|
380
|
+
|
|
381
|
+
**Read the naming rule before you touch anything here.** The exported function is
|
|
382
|
+
`annotateGitProvenance` — NOT `annotateProvenance` (taken by
|
|
383
|
+
`sca/sigstore-verify.js`, build attestations) and NOT `annotateFindingProvenance`
|
|
384
|
+
(taken by `posture/provenance.js`, parser-corroboration signals). `engine.js`
|
|
385
|
+
imports all three; either alternative name is a duplicate binding, and the second
|
|
386
|
+
takes a findings array as its first argument exactly like this one, so a wrong
|
|
387
|
+
import would RUN rather than fail. The field is `finding.findingProvenance`,
|
|
388
|
+
never bare `.provenance` — `finding.provenance` and `supplyChainEntry.provenance`
|
|
389
|
+
are both pre-existing unrelated fields.
|
|
390
|
+
|
|
391
|
+
**The pipeline**, in call order — all LIVE-WIRED into `engine.js`'s scan unless noted:
|
|
392
|
+
|
|
393
|
+
| Module | Answers |
|
|
394
|
+
|---|---|
|
|
395
|
+
| `coordinator.js` | the integration point — budget, cache, per-finding dispatch, the terminal-status guarantee |
|
|
396
|
+
| `git-evidence.js` | the only Git wrapper (`getRepoState`, `blameLine`, `candidateCommitsForLine`, `getBlobAtCommit`, `commitMeta`) |
|
|
397
|
+
| `origin-resolver.js` | which commit introduced a SAST finding |
|
|
398
|
+
| `dag-walk.js` | (M3 §3.1) non-first-parent DAG walk + revert/cherry-pick detection for `--provenance deep` |
|
|
399
|
+
| `predicate-replay.js` | was this finding's condition true at commit X (calls `runFullScan` on that commit's blobs) |
|
|
400
|
+
| `sca-origin.js` | which commit moved a directly-declared dependency version into an advisory's vulnerable range |
|
|
401
|
+
| `transitive-sca.js` | (M3 §3.2) the same question for a TRANSITIVE dependency, re-deriving lockfile ancestry per historical commit |
|
|
402
|
+
| `branch-entry.js` | which branch/PR merge brought the origin commit into the current branch |
|
|
403
|
+
| `evidence-attribution.js` | the path:line:commit triples for source / sink / manifest |
|
|
404
|
+
| `confidence.js` | HIGH / MEDIUM / LOW plus the reasons behind it |
|
|
405
|
+
| `lifecycle.js` | the introduce / remediate / reintroduce ledger |
|
|
406
|
+
| `cache.js` | per-(HEAD, stableId, ruleset, boundary, mode) memo under its own top-level `.agentic-security/provenance-cache/` (split out from `provenance/` so it can carry a `'cache'` retentionClass the permanent lifecycle ledger must not get — see artifact-registry.js) |
|
|
407
|
+
| `schema.js` | the status/method/role/confidence enums, `emptyProvenance`, `redactFindingProvenance`, `isProvenanceHealthy` |
|
|
408
|
+
| `validate.js` | shape assertion for tests |
|
|
409
|
+
| `missing-control-resolver.js` | (M3 §3.3, FR-PROV-017) when a previously-observed safeguard disappeared — **wired into `coordinator.js`**: `resolveMissingControlOrigin` calls `resolveMissingControl` for any finding with `missingControlCandidate:true` (today, `sast/rate-limit.js`'s findings) |
|
|
410
|
+
| `providers/config.js`, `providers/github.js`, `providers/gitlab.js` | (M3 §3.4, FR-PROV-022) GitHub/GitLab PR-metadata + CODEOWNERS fetch, config resolved from `.agentic-security/provenance-providers.yml` / token env vars — **wired into `coordinator.js`**: `resolveProviderConfig` is resolved once per scan in `annotateGitProvenance`, and `fetchPRMetadata`/`fetchCodeowners` are called per `complete`-status finding (capped, see `MAX_PROVIDER_ENRICHMENTS_PER_SCAN`), landing on `findingProvenance.providerEnrichment` |
|
|
411
|
+
| `repo-lineage.js` | (M4 §4.2) loads + fully verifies an operator-declared `.agentic-security/repo-lineage.json` cross-repo link (local clones only, no remote fetch) — used by `origin-resolver.js`'s root-commit case, not a standalone-unwired module |
|
|
412
|
+
| `ai-authorship.js` | (M4 §4.3) extensible AI-authorship verifier registry (`registerAIAuthorshipVerifier`/`resolveAIAuthorship`), defaults to `{status:'unknown', verifier:null}` with nothing registered (today's real state) — wired into `origin-resolver.js`'s `originFrom`, so every SAST `findingOrigin` carries `aiAuthorship`; scoped to SAST only, not direct/transitive SCA origins |
|
|
413
|
+
|
|
414
|
+
**Four invariants, each with a test that fails if you relax it:**
|
|
415
|
+
|
|
416
|
+
- **Terminal status, always.** After `annotateGitProvenance` returns, every
|
|
417
|
+
finding carries a `findingProvenance` with one of `complete` / `partial` /
|
|
418
|
+
`uncommitted` / `not_available` / `budget_exhausted` / `error`. There is no
|
|
419
|
+
path — missing git binary, malformed finding, downstream throw — that leaves
|
|
420
|
+
the field absent. `engine.js` additionally backstops every channel OUTSIDE
|
|
421
|
+
the `_runAnnotator` wrapper, because that wrapper swallows throws —
|
|
422
|
+
`findings` and `supplyChain` with a full not_available/error catch-all as
|
|
423
|
+
before; since Task 11, `secrets` and blameable `logicVulns` go through REAL
|
|
424
|
+
resolution (real stableIds backfilled, real `annotateGitProvenance` calls
|
|
425
|
+
made), so their outside-the-wrapper coverage narrowed to a defensive
|
|
426
|
+
catch-all for whatever the real call somehow didn't reach, plus the 3
|
|
427
|
+
synthetic-line `logicVulns` producers (`license-policy:`/`deploy-platform:`/
|
|
428
|
+
`stack-playbook:`), which stay on a permanent, principled not_available —
|
|
429
|
+
never routed through `resolveOrigin` at all, not merely deferred.
|
|
430
|
+
- **Never false certainty.** A shallow clone cannot reach `complete`; an
|
|
431
|
+
unverifiable parent boundary degrades to `partial` with its reason carried
|
|
432
|
+
through. `origin-resolver.js` decides this on the `shallow` flag of the
|
|
433
|
+
repoState object, and it must come from the REAL `getRepoState()` — pass it a
|
|
434
|
+
stub and the guarantee is gone.
|
|
435
|
+
- **The lifecycle ledger only closes findings on a COMPLETE scan.** `applyScan`'s
|
|
436
|
+
remediation pass turns absence into the claim "this was fixed," which is sound
|
|
437
|
+
only if the scan looked everywhere. `runScan.js` computes `completeScan` (false
|
|
438
|
+
for `--changed-since`/`--pr` and for caller-supplied `fileContents`) and threads
|
|
439
|
+
it through `runFullScan` to `updateLifecycle`. `updateLifecycle` is also gated on
|
|
440
|
+
the `scanRoot` being **a directory that exists** — not merely truthy.
|
|
441
|
+
`resolveProjectRoot` honours a caller-supplied scanRoot only when it resolves to
|
|
442
|
+
a real directory; for `null`, for a typo'd path, or for a file, it falls back to
|
|
443
|
+
walking up from the PROCESS CWD. Both doors led to the same corruption: a scan
|
|
444
|
+
that never looked at your project writing your project's ledger, and then —
|
|
445
|
+
finding nothing while still claiming `completeScan` — remediating every open
|
|
446
|
+
finding in it. `agentic-security scan ./typo` is the reachable form. This repo's
|
|
447
|
+
own checkout accumulated a 1.1 MB ledger of spurious events that way.
|
|
448
|
+
- **One budget for the whole scan.** `engine.js` computes ONE `deadlineAt` and
|
|
449
|
+
passes it to all five of its `annotateGitProvenance` calls (SAST findings,
|
|
450
|
+
direct SCA deps, transitive SCA deps per Task 7, then secrets and blameable
|
|
451
|
+
logicVulns per Task 11); a caller-supplied `deadlineAt`/`perFindingBudgetMs`
|
|
452
|
+
wins over the coordinator's own computation. Inside, each finding gets
|
|
453
|
+
`max(2s, remaining/count)` so one deep-history finding cannot starve the rest.
|
|
454
|
+
`budget_exhausted` is the one result that is **never cached** — it is a property
|
|
455
|
+
of the run, not the repository, and caching it would pin a timeout in place
|
|
456
|
+
until HEAD moved.
|
|
457
|
+
|
|
458
|
+
**Re-entrancy brake.** `predicate-replay.js` calls `runFullScan` back on historical
|
|
459
|
+
blobs, so every internal re-scan must pass `provenance:false` or the pass recurses
|
|
460
|
+
without bound. Present callers: `history-scan.js` (×3), `pr-delta.js`,
|
|
461
|
+
`fix-verify.js`, `compare.js`; `lsp/server.js` uses the wider
|
|
462
|
+
`withStateWritesDisabled`.
|
|
463
|
+
|
|
464
|
+
**Privacy.** Author emails are collected but redacted by `redactFindingProvenance`
|
|
465
|
+
at every output boundary (`report/index.js`, `mcp/tools.js`) unless
|
|
466
|
+
`AGENTIC_SECURITY_INCLUDE_AUTHOR_EMAIL=1` / `--include-author-email`. Separately,
|
|
467
|
+
`AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1` / `--pseudonymize-authors` (PRD Section 8)
|
|
468
|
+
replaces `authorName` with a stable `Contributor-XXXXXXXX` pseudonym instead of
|
|
469
|
+
withholding it — `redactFindingProvenance` applies the same treatment to
|
|
470
|
+
`providerEnrichment.reviewers`/`codeowners` (FR-PROV-022's PR-reviewer logins and
|
|
471
|
+
raw CODEOWNERS lines), not just `findingOrigin`. Both `report/index.js` and
|
|
472
|
+
`mcp/tools.js` read the env var per call to build the redaction options
|
|
473
|
+
(`mcp/tools.js` deliberately never reads `AGENTIC_SECURITY_INCLUDE_AUTHOR_EMAIL`
|
|
474
|
+
itself — an agent caller gets no raw email regardless of that flag); the
|
|
475
|
+
`auditor-walkthrough.js` narrative reads it too, for the one `earliestOrigin`
|
|
476
|
+
field that bypasses `redactFindingProvenance` entirely (see that module's own
|
|
477
|
+
comment on why).
|
|
478
|
+
|
|
479
|
+
**At rest, `provenance/cache.js` stores the UNREDACTED record, on purpose.**
|
|
480
|
+
Redaction is a read-time/output-time concern — the same cached record gets
|
|
481
|
+
replayed back out through `redactFindingProvenance` differently per output
|
|
482
|
+
call (default vs. `--include-author-email` vs. `--pseudonymize-authors`), which
|
|
483
|
+
only works if the cache holds one raw, policy-independent copy. Pre-redacting
|
|
484
|
+
at write time would freeze whichever policy was active when the entry was
|
|
485
|
+
cached, breaking that per-call flexibility for every later reader (second
|
|
486
|
+
independent Finding Provenance PRD audit). The accepted mitigation is a
|
|
487
|
+
permissions floor, not encryption: every `cacheSet` chmods the entry file to
|
|
488
|
+
`0600` and the `provenance-cache/` directory to `0700` (same posture as
|
|
489
|
+
`integrity.js`'s per-install HMAC key). This defeats other local users/processes
|
|
490
|
+
reading the cache; it does not defeat root or the same OS user. See
|
|
491
|
+
`cache.js`'s own header for the full tradeoff writeup, including why
|
|
492
|
+
encryption-at-rest was considered and deferred.
|
|
493
|
+
|
|
354
494
|
## Gotchas
|
|
355
495
|
|
|
356
496
|
- The seed `calibration-seed.json` is small (n < 30 for several families). Don't treat it as a held-out set — that's `holdout-eval.js`'s job, against an externally-supplied JSONL.
|
|
@@ -123,6 +123,18 @@ export function aggregateCorpus(detail) {
|
|
|
123
123
|
* — measured THIS run
|
|
124
124
|
* committed { corpusBaseline, proofCorpus } — read from committed files,
|
|
125
125
|
* labelled as such in the output, never used to derive a rate
|
|
126
|
+
* scan optional — a scan-shaped object (`{findings, secrets,
|
|
127
|
+
* supplyChain}`, trimmed to just those arrays) from a run over
|
|
128
|
+
* a full (non-shallow) Git clone, used ONLY to compute
|
|
129
|
+
* provenanceCoverage below. `scripts/scorecard.mjs` passes
|
|
130
|
+
* `selfScan.provenanceScan` — the self-scan harness
|
|
131
|
+
* (bench/self-scan/measure.mjs) already runs a real
|
|
132
|
+
* `runScan()` over this project's own full git clone with
|
|
133
|
+
* provenance resolution on by default, so this reuses that
|
|
134
|
+
* run's already-computed `findingProvenance` rather than
|
|
135
|
+
* performing a second scan. Still optional: a caller with no
|
|
136
|
+
* such scan renders "not measured this run" rather than a
|
|
137
|
+
* fabricated rate. See PRD Success Metrics.
|
|
126
138
|
*/
|
|
127
139
|
export function buildScorecard(inputs) {
|
|
128
140
|
const corpus = aggregateCorpus(inputs.corpusDetail);
|
|
@@ -163,6 +175,14 @@ export function buildScorecard(inputs) {
|
|
|
163
175
|
byTier: corpus.byTier,
|
|
164
176
|
},
|
|
165
177
|
selfScan: { measuredThisRun: true, targets, polyglot: selfScan.polyglot || { total: 0, byLanguage: {} } },
|
|
178
|
+
// PRD Success Metrics: "Provenance coverage >=95% complete or uncommitted
|
|
179
|
+
// for P0-supported findings in full Git clones." `inputs.scan` is
|
|
180
|
+
// optional (see the JSDoc above) — absent when no caller yet supplies a
|
|
181
|
+
// real scan, in which case this reports "not measured" rather than a
|
|
182
|
+
// fabricated 0/0.
|
|
183
|
+
provenanceCoverage: inputs.scan
|
|
184
|
+
? { measuredThisRun: true, ...computeProvenanceCoverage(inputs.scan) }
|
|
185
|
+
: { measuredThisRun: false },
|
|
166
186
|
taintRecall: (() => {
|
|
167
187
|
const lr = inputs.layerRecall;
|
|
168
188
|
if (!lr) {
|
|
@@ -249,6 +269,28 @@ export function buildScorecard(inputs) {
|
|
|
249
269
|
};
|
|
250
270
|
}
|
|
251
271
|
|
|
272
|
+
// PRD Success Metrics: "Provenance coverage >=95% complete or uncommitted
|
|
273
|
+
// for P0-supported findings in full Git clones." P0-supported scope per
|
|
274
|
+
// the PRD's own Release Scope table: code (SAST), secrets, IaC/config,
|
|
275
|
+
// direct dependency findings. Secrets now get real origin resolution
|
|
276
|
+
// (Task 11 -- `engine.js` calls `annotateGitProvenance` on `scan.secrets`
|
|
277
|
+
// with a real per-pattern-backfilled stableId, the same as SAST findings),
|
|
278
|
+
// so this metric no longer has a structural reason to read lower for the
|
|
279
|
+
// secrets share of the denominator than for any other P0-scoped channel.
|
|
280
|
+
export function computeProvenanceCoverage(scan) {
|
|
281
|
+
const p0Findings = [
|
|
282
|
+
...(scan.findings || []),
|
|
283
|
+
...(scan.secrets || []),
|
|
284
|
+
...(scan.supplyChain || []).filter((s) => s.type === 'vulnerable_dep' && s.isDirect),
|
|
285
|
+
];
|
|
286
|
+
const d = p0Findings.length;
|
|
287
|
+
const n = p0Findings.filter((f) => {
|
|
288
|
+
const status = f.findingProvenance?.status;
|
|
289
|
+
return status === 'complete' || status === 'uncommitted';
|
|
290
|
+
}).length;
|
|
291
|
+
return { n, d };
|
|
292
|
+
}
|
|
293
|
+
|
|
252
294
|
function rateRow(r) {
|
|
253
295
|
return `| ${r.key} | ${r.entries} | ${formatRate(r.detection.n, r.detection.d)} | ${formatRate(r.silence.n, r.silence.d)} |`;
|
|
254
296
|
}
|
|
@@ -470,6 +512,24 @@ export function renderScorecardMarkdown(m) {
|
|
|
470
512
|
}
|
|
471
513
|
L.push('Per-file counts are in `docs/scorecard.json`.');
|
|
472
514
|
L.push('');
|
|
515
|
+
if (m.provenanceCoverage && m.provenanceCoverage.measuredThisRun) {
|
|
516
|
+
L.push('## Provenance coverage');
|
|
517
|
+
L.push('');
|
|
518
|
+
L.push('PRD Success Metric: **>=95% of P0-scoped findings (SAST + secrets + direct**');
|
|
519
|
+
L.push('**dependency findings) resolve to `complete` or `uncommitted` git provenance**');
|
|
520
|
+
L.push('in a full (non-shallow) clone. Transitive dependency findings are excluded —');
|
|
521
|
+
L.push('the PRD\'s Release Scope table names direct dependency findings only.');
|
|
522
|
+
L.push('');
|
|
523
|
+
L.push('| P0-scoped findings — complete/uncommitted provenance |');
|
|
524
|
+
L.push('| --- |');
|
|
525
|
+
L.push(`| ${formatRate(m.provenanceCoverage.n, m.provenanceCoverage.d)} |`);
|
|
526
|
+
L.push('');
|
|
527
|
+
L.push('Secrets, SAST, and direct-dependency findings all resolve through the same');
|
|
528
|
+
L.push('git-origin resolution pipeline, so a gap in this rate reflects the clone');
|
|
529
|
+
L.push('itself (shallow history, uncommitted lines the pipeline could not blame) —');
|
|
530
|
+
L.push('not a channel this measurement structurally cannot yet cover.');
|
|
531
|
+
L.push('');
|
|
532
|
+
}
|
|
473
533
|
// PRD F12.6 — the honest scorecard publishes the LIMITS too, not only the
|
|
474
534
|
// rates. Three claims this project makes are only meaningful with their
|
|
475
535
|
// caveat attached, and each caveat was invisible before this section:
|
|
@@ -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" },
|
|
@@ -125,6 +150,28 @@ export const ARTIFACT_REGISTRY = [
|
|
|
125
150
|
{ name: 'sca-upgrade-history', kind: 'dir', classification: 'generated', retentionClass: 'scan' },
|
|
126
151
|
{ name: 'scan-baselines', kind: 'dir', classification: 'generated', retentionClass: 'scan', source: 'posture/pr-augment.js' },
|
|
127
152
|
{ name: 'agent-scratchpad', kind: 'dir', classification: 'generated', retentionClass: 'cache', source: 'mcp/tools.js (append_scratchpad)' },
|
|
153
|
+
// Finding Provenance (M0/M1, split per PRD Section 8 retention task). Used
|
|
154
|
+
// to be one directory with two writers sharing it, which meant they could
|
|
155
|
+
// not get different retention treatment — `cmdReset`/`findExpiredArtifacts`
|
|
156
|
+
// only ever operate on exact TOP-LEVEL `.agentic-security/` directory
|
|
157
|
+
// names, never on a `/`-qualified sub-path. Now physically split:
|
|
158
|
+
// - posture/provenance/cache.js writes provenance-cache/<hash>.json — a
|
|
159
|
+
// pure HEAD-keyed memo of resolved origins, safely regenerable, no
|
|
160
|
+
// correctness dependency on being preserved. Gets retentionClass:
|
|
161
|
+
// 'cache' (7-day default / 30-day max TTL, RETENTION_DEFAULTS.cache).
|
|
162
|
+
// - posture/provenance/lifecycle.js writes provenance/lifecycle.json +
|
|
163
|
+
// .lock — the introduce/remediate/reintroduce ledger. Deliberately NO
|
|
164
|
+
// retentionClass: this is permanent history, not a cache; auto-expiring
|
|
165
|
+
// it would silently lose lifecycle events a report may already have
|
|
166
|
+
// cited. `reset` (without `--expired`) still clears it, which is
|
|
167
|
+
// explicit operator action, unlike TTL-driven auto-expiry.
|
|
168
|
+
// Old-location cache files (`provenance/cache/*.json`, written before this
|
|
169
|
+
// split) are DELIBERATELY NOT migrated — see cache.js's own comment and the
|
|
170
|
+
// commit that introduced this split. They are simply orphaned: invisible to
|
|
171
|
+
// this registry, un-swept by reset/retention, silently ignored by the new
|
|
172
|
+
// code, and harmless to leave until a human deletes them by hand.
|
|
173
|
+
{ name: 'provenance-cache', kind: 'dir', classification: 'generated', retentionClass: 'cache', source: 'posture/provenance/cache.js -- pure HEAD-keyed memo, safely regenerable, no correctness dependency on being preserved' },
|
|
174
|
+
{ name: 'provenance', kind: 'dir', classification: 'generated', source: 'posture/provenance/lifecycle.js -- the introduce/remediate/reintroduce ledger. Deliberately NO retentionClass: this is permanent history, not a cache; auto-expiring it would silently lose lifecycle events a report may already have cited.' },
|
|
128
175
|
{ name: 'AGENTS.md', kind: 'file', classification: 'generated', source: 'posture/agents-memory.js' },
|
|
129
176
|
{ name: 'AGENTS.md.archive', kind: 'file', classification: 'generated', source: 'posture/agents-memory.js' },
|
|
130
177
|
{ name: 'baseline.json', kind: 'file', classification: 'generated', source: 'bin/agentic-security.js (--set-baseline)', note: 'operator-set intent, functionally closer to operator-config than scan output — no auto-expiry' },
|
|
@@ -146,6 +193,19 @@ export const ARTIFACT_REGISTRY = [
|
|
|
146
193
|
{ name: 'deletion-report.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', source: 'posture/state-lifecycle-report.js (via bin/agentic-security.js cmdReset)' },
|
|
147
194
|
{ name: 'export-report.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', source: 'posture/state-lifecycle-report.js (via bin/agentic-security.js cmdExport)' },
|
|
148
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
|
+
|
|
149
209
|
// ── Operator-config: hand-authored (or agent-authored) input, never wiped ──
|
|
150
210
|
{ name: 'rules.yml', kind: 'file', classification: 'operator-config' },
|
|
151
211
|
{ name: 'rules', kind: 'dir', classification: 'operator-config' },
|
|
@@ -169,12 +229,28 @@ export const ARTIFACT_REGISTRY = [
|
|
|
169
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' },
|
|
170
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' },
|
|
171
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.' },
|
|
172
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' },
|
|
173
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' },
|
|
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' },
|
|
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' },
|
|
174
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)' },
|
|
175
240
|
{ name: 'current-intent.md', kind: 'file', classification: 'operator-config', note: 'developer-authored; no writer exists anywhere in src/ or bin/' },
|
|
176
241
|
{ name: 'exploit-history.jsonl', kind: 'file', classification: 'operator-config', note: 'own header comment: "operator-curated record of past confirmed exploits"' },
|
|
177
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' },
|
|
178
254
|
];
|
|
179
255
|
|
|
180
256
|
export function listGeneratedArtifacts() {
|