@clear-capabilities/agentic-security-scanner 0.144.0 → 0.145.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 +251 -0
- package/bin/agentic-security.js +294 -3
- package/dist/113.index.js +11 -3
- package/dist/178.index.js +24 -6
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +22 -0
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +11 -3
- package/dist/637.index.js +27 -5
- package/dist/970.index.js +65 -1
- package/dist/agentic-security.mjs +9 -9
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +14 -8
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/engine.js +488 -29
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +20 -0
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/posture/CLAUDE.md +121 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +24 -0
- package/src/posture/auditor-walkthrough.js +116 -13
- 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/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/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/util/git-hardening.js +128 -0
package/dist/178.index.js
CHANGED
|
@@ -13,7 +13,8 @@ export const modules = {
|
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
14
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
15
15
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
16
|
-
/* harmony import */ var
|
|
16
|
+
/* harmony import */ var _util_git_hardening_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8844);
|
|
17
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7691);
|
|
17
18
|
// Time-travel + counterfactual scanning (v0.68).
|
|
18
19
|
//
|
|
19
20
|
// Two new modes that exploit the pure-input shape of runFullScan:
|
|
@@ -36,10 +37,14 @@ export const modules = {
|
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
|
|
39
41
|
const MAX_FILES_PER_SCAN = 5000;
|
|
40
42
|
|
|
43
|
+
// `root` is the scan target's repository, not this project's own trusted
|
|
44
|
+
// checkout — hardened per FR-PROV-024 / the second Finding Provenance PRD
|
|
45
|
+
// audit (same exposure class as provenance/git-evidence.js's `_run`).
|
|
41
46
|
function _git(root, args) {
|
|
42
|
-
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
47
|
+
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)('git', (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_3__/* .hardenGitArgs */ .Ax)(args), { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_3__/* .hardenGitEnv */ .Si)() });
|
|
43
48
|
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -99,7 +104,10 @@ function _listFilesAtRef(root, ref) {
|
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
function _readFileAtRef(root, ref, file) {
|
|
102
|
-
|
|
107
|
+
// `--no-textconv`: this blob-cat form of `show` was verified NOT
|
|
108
|
+
// reachable via a hostile textconv driver in current git — kept for
|
|
109
|
+
// defense-in-depth/uniformity, same as pr-delta.js's equivalent.
|
|
110
|
+
const r = _git(root, ['show', '--no-textconv', `${ref}:${file}`]);
|
|
103
111
|
if (!r.ok) return null;
|
|
104
112
|
return r.stdout;
|
|
105
113
|
}
|
|
@@ -111,7 +119,13 @@ async function _scanAtRef(root, ref) {
|
|
|
111
119
|
const c = _readFileAtRef(root, ref, f);
|
|
112
120
|
if (c != null) fileContents[f] = c;
|
|
113
121
|
}
|
|
114
|
-
|
|
122
|
+
// `provenance:false` — this is a HISTORICAL ref, not "the state of this repo
|
|
123
|
+
// right now". Two things must not happen here: resolving git provenance for
|
|
124
|
+
// findings that already are a point in history (pure waste), and letting
|
|
125
|
+
// updateLifecycle see this partial, historical finding set as the current
|
|
126
|
+
// one — it marks every open stableId NOT in the set as `remediated`, so one
|
|
127
|
+
// per-ref scan would mass-remediate the whole project's real open findings.
|
|
128
|
+
const scan = await (0,_engine_js__WEBPACK_IMPORTED_MODULE_4__/* .runFullScan */ .wW)({ fileContents, scanRoot: root, provenance: false }, () => {});
|
|
115
129
|
return {
|
|
116
130
|
ref,
|
|
117
131
|
fileCount: Object.keys(fileContents).length,
|
|
@@ -193,8 +207,12 @@ async function runWhatIf(root, { overlays = [], remove = [] } = {}) {
|
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
// Baseline (without overlays) for delta computation.
|
|
196
|
-
|
|
197
|
-
|
|
210
|
+
// Both legs are hypothetical snapshots being differenced against each other,
|
|
211
|
+
// never the repo's current state — `provenance:false` for the same reason as
|
|
212
|
+
// _scanAtRef above (no provenance to resolve, and updateLifecycle must not
|
|
213
|
+
// treat either snapshot as "what is open right now").
|
|
214
|
+
const baseScan = await (0,_engine_js__WEBPACK_IMPORTED_MODULE_4__/* .runFullScan */ .wW)({ fileContents: _baselineFor(fileContents, overlays, remove, root), scanRoot: root, provenance: false }, () => {});
|
|
215
|
+
const whatIfScan = await (0,_engine_js__WEBPACK_IMPORTED_MODULE_4__/* .runFullScan */ .wW)({ fileContents, scanRoot: root, provenance: false }, () => {});
|
|
198
216
|
const baseIds = new Set((baseScan.findings || []).map(f => f.stableId || f.id));
|
|
199
217
|
const wIds = new Set((whatIfScan.findings || []).map(f => f.stableId || f.id));
|
|
200
218
|
const introduced = (whatIfScan.findings || []).filter(f => !baseIds.has(f.stableId || f.id)).map(_compact);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export const id = 271;
|
|
2
|
+
export const ids = [271];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 2271:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ PROVENANCE_BUNDLE_SCHEMA: () => (/* binding */ PROVENANCE_BUNDLE_SCHEMA),
|
|
10
|
+
/* harmony export */ buildProvenanceEvidenceBundle: () => (/* binding */ buildProvenanceEvidenceBundle),
|
|
11
|
+
/* harmony export */ ensureKeyPair: () => (/* reexport safe */ _evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__.ensureKeyPair),
|
|
12
|
+
/* harmony export */ signProvenanceEvidenceBundle: () => (/* binding */ signProvenanceEvidenceBundle),
|
|
13
|
+
/* harmony export */ verifyProvenanceEvidenceBundle: () => (/* binding */ verifyProvenanceEvidenceBundle)
|
|
14
|
+
/* harmony export */ });
|
|
15
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
|
|
16
|
+
/* harmony import */ var _evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8317);
|
|
17
|
+
// Signed provenance evidence bundles (Finding Provenance PRD, M4 §4.1).
|
|
18
|
+
//
|
|
19
|
+
// Sibling to posture/evidence-bundle.js, not a generalization of it — that
|
|
20
|
+
// module's bundle shape (proofTier, taintPath, exploitability...) answers
|
|
21
|
+
// "is this finding real"; this one answers "who/when introduced it, how
|
|
22
|
+
// sure are we." Forcing one shape to cover both would leave half of every
|
|
23
|
+
// bundle null. Same four-function pattern (build/sign/verify + a schema
|
|
24
|
+
// string + a top-level-key allowlist), same Ed25519 key material — reused,
|
|
25
|
+
// not reimplemented.
|
|
26
|
+
//
|
|
27
|
+
// The allowlisted fields mirror provenance/coordinator.js's own
|
|
28
|
+
// computeDigest() material EXACTLY (stableId, findingOrigin.commit,
|
|
29
|
+
// branchIntroduction.commit, evidenceAttribution role:path:line:commit
|
|
30
|
+
// strings, method, confidence.reasons, limitations) plus repo/HEAD identity
|
|
31
|
+
// (not in computeDigest's material, since that digest never leaves the repo
|
|
32
|
+
// it was computed in, but a bundle does). Everything here is copied from
|
|
33
|
+
// what findingProvenance already computed. Nothing is inferred.
|
|
34
|
+
//
|
|
35
|
+
// canonicalJson vs canonicalBytes — resolved before writing this file
|
|
36
|
+
// -------------------------------------------------------------------
|
|
37
|
+
// evidence-bundle.js exports TWO canonicalisation helpers, not one, and they
|
|
38
|
+
// are not interchangeable. `canonicalJson(value)` is a pure, shape-agnostic
|
|
39
|
+
// deterministic serialiser (sorted keys at every level, order-preserving
|
|
40
|
+
// arrays) — safe to reuse for any bundle shape. `canonicalBytes(bundle)`
|
|
41
|
+
// is NOT generic: it hardcodes evidence-bundle.js's own six top-level keys
|
|
42
|
+
// (`schema, finding, evidence, engine, proves, doesNotProve`) when building
|
|
43
|
+
// the object it signs. Reusing `canonicalBytes` here would silently sign a
|
|
44
|
+
// filtered object missing this module's `repo` and `provenance` fields
|
|
45
|
+
// (and would read `bundle.evidence`, which a provenance bundle never has) —
|
|
46
|
+
// exactly the EA-03 failure mode the allowlist check below exists to catch,
|
|
47
|
+
// just introduced from the signing side instead of the verifying side. So
|
|
48
|
+
// this module imports the generic `canonicalJson` (as the brief's Step 1
|
|
49
|
+
// sketch already did) and signs `canonicalJson(bundle)` directly, since
|
|
50
|
+
// `buildProvenanceEvidenceBundle` never puts anything on the unsigned
|
|
51
|
+
// object beyond this module's own allowlisted fields.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
const PROVENANCE_BUNDLE_SCHEMA = 'agentic-security/provenance-evidence@1';
|
|
57
|
+
|
|
58
|
+
const PROVES = 'This bundle\'s contents are exactly what was signed at attestation time.';
|
|
59
|
+
const DOES_NOT_PROVE = 'This bundle does NOT prove the origin commit is correctly identified — read confidence.level and limitations for that. It proves the RECORD is unmodified, not that the record is right.';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build an unsigned provenance bundle from one finding's findingProvenance.
|
|
63
|
+
* Returns null for a finding with no findingProvenance at all (nothing to
|
|
64
|
+
* attest) — this is a caller error (attest a scan before its provenance
|
|
65
|
+
* pass ran), not a case to paper over with an empty bundle.
|
|
66
|
+
*/
|
|
67
|
+
function buildProvenanceEvidenceBundle(finding, { engineVersion, repoIdentity, head } = {}) {
|
|
68
|
+
if (!finding || typeof finding !== 'object') return null;
|
|
69
|
+
const fp = finding.findingProvenance;
|
|
70
|
+
if (!fp || typeof fp !== 'object') return null;
|
|
71
|
+
return {
|
|
72
|
+
schema: PROVENANCE_BUNDLE_SCHEMA,
|
|
73
|
+
finding: {
|
|
74
|
+
id: finding.id ?? null,
|
|
75
|
+
stableId: finding.stableId ?? null,
|
|
76
|
+
},
|
|
77
|
+
repo: {
|
|
78
|
+
identity: repoIdentity ?? null,
|
|
79
|
+
head: head ?? fp.analysisBasis?.head ?? null,
|
|
80
|
+
},
|
|
81
|
+
provenance: {
|
|
82
|
+
status: fp.status ?? null,
|
|
83
|
+
findingOrigin: fp.findingOrigin
|
|
84
|
+
? {
|
|
85
|
+
commit: fp.findingOrigin.commit ?? null,
|
|
86
|
+
authorName: fp.findingOrigin.authorName ?? null,
|
|
87
|
+
authorDate: fp.findingOrigin.authorDate ?? null,
|
|
88
|
+
summary: fp.findingOrigin.summary ?? null,
|
|
89
|
+
}
|
|
90
|
+
: null,
|
|
91
|
+
branchIntroduction: fp.branchIntroduction
|
|
92
|
+
? { commit: fp.branchIntroduction.commit ?? null, branch: fp.branchIntroduction.branch ?? null }
|
|
93
|
+
: null,
|
|
94
|
+
evidenceAttribution: (fp.evidenceAttribution || []).map((n) => ({
|
|
95
|
+
role: n.role ?? null, path: n.path ?? null, line: n.line ?? null, commit: n.commit ?? null,
|
|
96
|
+
})),
|
|
97
|
+
method: fp.method ?? null,
|
|
98
|
+
confidence: fp.confidence
|
|
99
|
+
? { level: fp.confidence.level ?? null, score: fp.confidence.score ?? null, reasons: fp.confidence.reasons || [] }
|
|
100
|
+
: null,
|
|
101
|
+
limitations: fp.limitations || [],
|
|
102
|
+
// M4 §4.2 final-review fix: this module was written (Task 1) before
|
|
103
|
+
// the cross-repo lineage feature existed (Task 5), so a
|
|
104
|
+
// cross-repo-resolved origin's boundary-crossing marker was dropped
|
|
105
|
+
// from the signed bundle — a foreign repository's commit SHA and a
|
|
106
|
+
// real author name, with no MACHINE-READABLE signal that the origin
|
|
107
|
+
// crossed a repository boundary (only the prose in `limitations`
|
|
108
|
+
// said so, and a programmatic verifier does not read prose). Nested
|
|
109
|
+
// under `provenance` alongside the fields above, so it is part of
|
|
110
|
+
// what gets signed — never a new top-level key.
|
|
111
|
+
historyCoverage: { crossRepoLineage: fp.historyCoverage?.crossRepoLineage ?? false },
|
|
112
|
+
},
|
|
113
|
+
engine: { engineVersion: engineVersion ?? null },
|
|
114
|
+
proves: PROVES,
|
|
115
|
+
doesNotProve: DOES_NOT_PROVE,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function signProvenanceEvidenceBundle(bundle, privateKeyPem) {
|
|
120
|
+
const sig = node_crypto__WEBPACK_IMPORTED_MODULE_0__.sign(null, Buffer.from((0,_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__/* .canonicalJson */ .dj)(bundle), 'utf8'), privateKeyPem);
|
|
121
|
+
return {
|
|
122
|
+
...bundle,
|
|
123
|
+
signature: { algorithm: 'ed25519', canonicalisation: PROVENANCE_BUNDLE_SCHEMA, value: sig.toString('base64') },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const PROVENANCE_BUNDLE_TOP_LEVEL_KEYS = new Set([
|
|
128
|
+
'schema', 'finding', 'repo', 'provenance', 'engine', 'proves', 'doesNotProve', 'signature',
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Verify with a PUBLIC key only. Rejects any top-level key outside the
|
|
133
|
+
* allowlist BEFORE checking the signature — same EA-03 fix evidence-bundle.js
|
|
134
|
+
* carries: a signature only covers the bytes it was computed over, so an
|
|
135
|
+
* unknown key stapled on after signing would otherwise verify as authentic.
|
|
136
|
+
*/
|
|
137
|
+
function verifyProvenanceEvidenceBundle(bundle, publicKeyPem) {
|
|
138
|
+
if (!bundle || typeof bundle !== 'object') return { ok: false, reason: 'bundle is not an object' };
|
|
139
|
+
if (bundle.schema !== PROVENANCE_BUNDLE_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
|
|
140
|
+
const unknownKeys = Object.keys(bundle).filter((k) => !PROVENANCE_BUNDLE_TOP_LEVEL_KEYS.has(k));
|
|
141
|
+
if (unknownKeys.length) {
|
|
142
|
+
return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
|
|
143
|
+
}
|
|
144
|
+
const sig = bundle.signature;
|
|
145
|
+
if (!sig?.value) return { ok: false, reason: 'bundle is unsigned' };
|
|
146
|
+
if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
|
|
147
|
+
if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
|
|
148
|
+
const { signature, ...unsigned } = bundle;
|
|
149
|
+
let ok = false;
|
|
150
|
+
try {
|
|
151
|
+
ok = node_crypto__WEBPACK_IMPORTED_MODULE_0__.verify(null, Buffer.from((0,_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_1__/* .canonicalJson */ .dj)(unsigned), 'utf8'), publicKeyPem, Buffer.from(sig.value, 'base64'));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return { ok: false, reason: `verification error: ${e.message}` };
|
|
154
|
+
}
|
|
155
|
+
return ok
|
|
156
|
+
? { ok: true, reason: null }
|
|
157
|
+
: { ok: false, reason: 'signature does not match the bundle contents — it was modified after signing' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/***/ })
|
|
164
|
+
|
|
165
|
+
};
|
package/dist/384.index.js
CHANGED
|
@@ -8,7 +8,7 @@ export const modules = {
|
|
|
8
8
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
9
|
/* harmony export */ scanCredentials: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Sv)
|
|
10
10
|
/* harmony export */ });
|
|
11
|
-
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
|
|
11
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7691);
|
|
12
12
|
// Secrets submodule view of the engine — credential + entropy + TODO scanning.
|
|
13
13
|
|
|
14
14
|
|
package/dist/435.index.js
CHANGED
|
@@ -207,6 +207,8 @@ function redactArgsBlob(s) {
|
|
|
207
207
|
|
|
208
208
|
// EXTERNAL MODULE: ./src/report/index.js + 3 modules
|
|
209
209
|
var report = __webpack_require__(457);
|
|
210
|
+
// EXTERNAL MODULE: ./src/posture/provenance/schema.js
|
|
211
|
+
var schema = __webpack_require__(4594);
|
|
210
212
|
;// CONCATENATED MODULE: ./src/posture/agents-memory.js
|
|
211
213
|
// AGENTS.md — writable continual-learning memory (harness-anatomy #2).
|
|
212
214
|
//
|
|
@@ -508,6 +510,10 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
|
|
|
508
510
|
|
|
509
511
|
|
|
510
512
|
|
|
513
|
+
// Git-origin provenance (Finding Provenance M0/M1). Distinct from
|
|
514
|
+
// `finding.provenance` (AI-authorship) and from an SCA entry's `provenance`
|
|
515
|
+
// (Sigstore/SLSA attestation) — see report/index.js's import comment.
|
|
516
|
+
|
|
511
517
|
|
|
512
518
|
// Lazy-loaded: these transitively pull in npm packages (@babel/core and
|
|
513
519
|
// friends) that aren't available in the plugin-cache install path
|
|
@@ -1053,6 +1059,21 @@ const explain_finding = {
|
|
|
1053
1059
|
epssScore: typeof f.epssScore === 'number' ? f.epssScore : null,
|
|
1054
1060
|
epssPercentile: typeof f.epssPercentile === 'number' ? f.epssPercentile : null,
|
|
1055
1061
|
exploitedNow: !!f.exploitedNow,
|
|
1062
|
+
// Which commit introduced this finding. `includeEmail` stays at its
|
|
1063
|
+
// DEFAULT (false) unconditionally — unlike the JSON report there is no
|
|
1064
|
+
// operator-set env escape for it here, because the consumer is an
|
|
1065
|
+
// agent that has no business receiving a committer's email address.
|
|
1066
|
+
// `pseudonymize`, by contrast, IS read back from the same env var
|
|
1067
|
+
// report/index.js's `_normalizedProvenance` reads
|
|
1068
|
+
// (AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1 / --pseudonymize-authors) —
|
|
1069
|
+
// fix-round item 4: an operator who set that policy was still getting
|
|
1070
|
+
// raw committer names (and, via providerEnrichment, raw reviewer
|
|
1071
|
+
// logins/CODEOWNERS lines) through this MCP surface because this call
|
|
1072
|
+
// passed no options object at all, silently defeating their policy at
|
|
1073
|
+
// this one output boundary while report/index.js honoured it.
|
|
1074
|
+
findingProvenance: f.findingProvenance ? (0,schema/* redactFindingProvenance */.As)(f.findingProvenance, {
|
|
1075
|
+
pseudonymize: process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS === '1',
|
|
1076
|
+
}) : null,
|
|
1056
1077
|
};
|
|
1057
1078
|
},
|
|
1058
1079
|
};
|
|
@@ -1257,6 +1278,7 @@ const apply_fix = {
|
|
|
1257
1278
|
const entry = await (0,fix_history/* applyFix */.oM)({
|
|
1258
1279
|
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content, fileExisted,
|
|
1259
1280
|
findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
|
|
1281
|
+
findingProvenance: f.findingProvenance || null,
|
|
1260
1282
|
});
|
|
1261
1283
|
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
1262
1284
|
}
|
package/dist/444.index.js
CHANGED
|
@@ -10,6 +10,7 @@ export const modules = {
|
|
|
10
10
|
/* harmony export */ });
|
|
11
11
|
/* unused harmony exports extractAddedLines, scanHistoryDiff */
|
|
12
12
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
13
|
+
/* harmony import */ var _util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8844);
|
|
13
14
|
// R15 (PRD §5) — git-history secret sweep.
|
|
14
15
|
//
|
|
15
16
|
// A secret removed from HEAD but present in any past commit is still
|
|
@@ -25,6 +26,7 @@ export const modules = {
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
|
|
28
30
|
// Pull the post-image (added) lines out of a unified diff: lines starting with
|
|
29
31
|
// a single '+' (not the '+++' file header). Returns reconstructed text.
|
|
30
32
|
function extractAddedLines(diffText) {
|
|
@@ -74,8 +76,15 @@ function sweepGitHistory(scanRoot, detectFn, { maxCommits = 50, timeoutMs = 2000
|
|
|
74
76
|
if (!scanRoot || typeof detectFn !== 'function') return [];
|
|
75
77
|
let out;
|
|
76
78
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
// Second independent Finding Provenance PRD audit (FR-PROV-024): this
|
|
80
|
+
// scanRoot is a scanned repository, not this project's own trusted
|
|
81
|
+
// checkout. `--no-textconv` alone (the pre-existing hardening here) closes
|
|
82
|
+
// the .gitattributes textconv surface but NOT `core.fsmonitor` /
|
|
83
|
+
// `core.hooksPath` — this `log -p` call renders every historical commit's
|
|
84
|
+
// diff content, the same shape verified exploitable in
|
|
85
|
+
// provenance/git-evidence.js, so it gets the full hardening too.
|
|
86
|
+
out = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.execFileSync)('git', (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitArgs */ .Ax)(['-C', scanRoot, 'log', '-p', '-n', String(maxCommits), '--no-color', '--no-merges', '--no-textconv']),
|
|
87
|
+
{ encoding: 'utf8', maxBuffer: 96 * 1024 * 1024, timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], env: (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitEnv */ .Si)() });
|
|
79
88
|
} catch { return []; }
|
|
80
89
|
const parts = out.split(/^commit ([0-9a-f]{7,40})/m); // [pre, sha, body, sha, body, ...]
|
|
81
90
|
const findings = [];
|
package/dist/449.index.js
CHANGED
|
@@ -14,6 +14,7 @@ export const modules = {
|
|
|
14
14
|
/* harmony export */ });
|
|
15
15
|
/* unused harmony export findingsExceedingSLA */
|
|
16
16
|
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
|
|
17
|
+
/* harmony import */ var _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4594);
|
|
17
18
|
// 0.8.0 Feat-11: MTTR / finding-age tracking — per-finding firstSeenAt/lastSeenAt with SLA breach detection.
|
|
18
19
|
//
|
|
19
20
|
// Stamps every finding with `firstSeenAt` (preserved from the baseline if the
|
|
@@ -25,6 +26,29 @@ export const modules = {
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
|
|
30
|
+
// FR-PROV-019: "reports never show an age without its basis and confidence."
|
|
31
|
+
// FINDING_ORIGIN is the only basis backed by a resolved git commit for the
|
|
32
|
+
// finding's actual introduction; EARLIEST_OBSERVABLE is also git-derived but
|
|
33
|
+
// partial (weaker claim, no exact introduction commit). UNCOMMITTED and
|
|
34
|
+
// FIRST_OBSERVED are both wall-clock fallbacks — the age is a first-seen
|
|
35
|
+
// timestamp, never resolved against git history — and must say so honestly
|
|
36
|
+
// rather than read like a proven date.
|
|
37
|
+
function _ageBasisLabel(ageBasis, confidence) {
|
|
38
|
+
const level = confidence?.level && confidence.level !== 'unknown' ? confidence.level.toUpperCase() : null;
|
|
39
|
+
switch (ageBasis) {
|
|
40
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FINDING_ORIGIN:
|
|
41
|
+
return `proven origin${level ? `, ${level} confidence` : ''}`;
|
|
42
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.EARLIEST_OBSERVABLE:
|
|
43
|
+
return `earliest observable commit, partial history${level ? `, ${level} confidence` : ''}`;
|
|
44
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.UNCOMMITTED:
|
|
45
|
+
return 'uncommitted — first-seen fallback, origin not proven';
|
|
46
|
+
case _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED:
|
|
47
|
+
default:
|
|
48
|
+
return 'first-seen fallback — origin not proven';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
// Stable fingerprint for cross-scan finding identity. Mirrors the dedupe key.
|
|
29
53
|
// Exported so a caller can compute the "removed since baseline" (i.e. fixed)
|
|
30
54
|
// set that computeMTTR needs, using the exact same identity function
|
|
@@ -53,6 +77,25 @@ function stampFindingTimestamps(findings, baselineMap = new Map(), now = Date.no
|
|
|
53
77
|
f.lastSeenAt = nowIso;
|
|
54
78
|
const firstMs = Date.parse(f.firstSeenAt);
|
|
55
79
|
f.ageDays = Math.max(0, Math.floor((now - firstMs) / 86400000));
|
|
80
|
+
// FR-PROV-019: age/SLA basis. ageDays above stays pure wall-clock —
|
|
81
|
+
// every existing SLA/computeMTTR consumer keeps its current meaning.
|
|
82
|
+
// ageBasis + provenAgeDays are ADDITIVE: a report can show both and
|
|
83
|
+
// explain the discrepancy, never silently swap which number "age" means.
|
|
84
|
+
const status = f.findingProvenance?.status;
|
|
85
|
+
const origin = f.findingProvenance?.findingOrigin;
|
|
86
|
+
if (status === 'complete' && origin?.authorDate) {
|
|
87
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FINDING_ORIGIN;
|
|
88
|
+
f.provenAgeDays = Math.max(0, Math.floor((now - Date.parse(origin.authorDate)) / 86400000));
|
|
89
|
+
} else if (status === 'partial' && origin?.authorDate) {
|
|
90
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.EARLIEST_OBSERVABLE;
|
|
91
|
+
f.provenAgeDays = Math.max(0, Math.floor((now - Date.parse(origin.authorDate)) / 86400000));
|
|
92
|
+
} else if (status === 'uncommitted') {
|
|
93
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.UNCOMMITTED;
|
|
94
|
+
f.provenAgeDays = f.ageDays;
|
|
95
|
+
} else {
|
|
96
|
+
f.ageBasis = _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED;
|
|
97
|
+
f.provenAgeDays = f.ageDays;
|
|
98
|
+
}
|
|
56
99
|
}
|
|
57
100
|
return findings;
|
|
58
101
|
}
|
|
@@ -84,28 +127,49 @@ function findingsExceedingSLA(findings, slaDays = null) {
|
|
|
84
127
|
});
|
|
85
128
|
}
|
|
86
129
|
|
|
87
|
-
// Median age (days) of the currently-open findings
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
// Median age (days) of the currently-open findings, PLUS the ageBasis/
|
|
131
|
+
// confidence of whichever finding landed on that median — a single-scan proxy
|
|
132
|
+
// for "how long has this debt been sitting". True MTTR (computeMTTR) needs the
|
|
133
|
+
// set of findings that were FIXED; this reports the open backlog's median age
|
|
134
|
+
// so a scan can show whether debt is getting older. Returns null on empty
|
|
135
|
+
// input. Local — surfaced only through renderSlaSummary (its sole consumer).
|
|
136
|
+
//
|
|
137
|
+
// FR-PROV-019: the days figure prefers `provenAgeDays` (git-derived when
|
|
138
|
+
// available) over the pure-wall-clock `ageDays`, and the ageBasis/confidence
|
|
139
|
+
// travel WITH the day count they describe — never a bare number with the
|
|
140
|
+
// basis looked up separately, which is how the original miss happened
|
|
141
|
+
// (ageBasis was stamped onto the finding but never reached the string that
|
|
142
|
+
// printed its age). Findings stamped by an older/test caller that never ran
|
|
143
|
+
// through stampFindingTimestamps (no ageBasis at all) degrade to the same
|
|
144
|
+
// honest "not proven" label FIRST_OBSERVED gets, never a false claim.
|
|
145
|
+
function medianOpenAge(findings) {
|
|
146
|
+
const entries = (findings || [])
|
|
147
|
+
.map(f => ({
|
|
148
|
+
days: f.provenAgeDays != null ? f.provenAgeDays : (f.ageDays || 0),
|
|
149
|
+
ageBasis: f.ageBasis || _provenance_schema_js__WEBPACK_IMPORTED_MODULE_1__/* .AGE_BASIS */ .pI.FIRST_OBSERVED,
|
|
150
|
+
confidence: f.findingProvenance?.confidence || null,
|
|
151
|
+
}))
|
|
152
|
+
.sort((a, b) => a.days - b.days);
|
|
153
|
+
if (!entries.length) return null;
|
|
154
|
+
return entries[Math.floor(entries.length / 2)];
|
|
96
155
|
}
|
|
97
156
|
|
|
98
157
|
// One-line SLA-breach summary for surfacing after a scan (#10). Returns null
|
|
99
|
-
// when nothing is past its per-severity SLA. Pairs with
|
|
158
|
+
// when nothing is past its per-severity SLA. Pairs with medianOpenAge for a
|
|
100
159
|
// "is my security debt aging" readout that the vibecoder can act on.
|
|
160
|
+
//
|
|
161
|
+
// FR-PROV-019: never prints the median age number without its basis and
|
|
162
|
+
// confidence alongside it — see medianOpenAge/_ageBasisLabel above.
|
|
101
163
|
function renderSlaSummary(findings, slaDays = null) {
|
|
102
164
|
const breached = findingsExceedingSLA(findings || [], slaDays);
|
|
103
165
|
if (!breached.length) return null;
|
|
104
166
|
const bySev = {};
|
|
105
167
|
for (const f of breached) bySev[f.severity] = (bySev[f.severity] || 0) + 1;
|
|
106
168
|
const parts = ['critical', 'high', 'medium', 'low', 'info'].filter(s => bySev[s]).map(s => `${bySev[s]} ${s}`);
|
|
107
|
-
const median =
|
|
108
|
-
const ageNote = median != null
|
|
169
|
+
const median = medianOpenAge(findings);
|
|
170
|
+
const ageNote = median != null
|
|
171
|
+
? ` (median open age ${median.days}d, ${_ageBasisLabel(median.ageBasis, median.confidence)})`
|
|
172
|
+
: '';
|
|
109
173
|
return `${breached.length} finding(s) past remediation SLA: ${parts.join(', ')}${ageNote}`;
|
|
110
174
|
}
|
|
111
175
|
|
package/dist/526.index.js
CHANGED
|
@@ -311,8 +311,8 @@ var external_node_child_process_ = __webpack_require__(1421);
|
|
|
311
311
|
var external_node_fs_ = __webpack_require__(3024);
|
|
312
312
|
// EXTERNAL MODULE: external "node:path"
|
|
313
313
|
var external_node_path_ = __webpack_require__(6760);
|
|
314
|
-
// EXTERNAL MODULE: ./src/engine.js +
|
|
315
|
-
var engine = __webpack_require__(
|
|
314
|
+
// EXTERNAL MODULE: ./src/engine.js + 229 modules
|
|
315
|
+
var engine = __webpack_require__(7691);
|
|
316
316
|
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
317
317
|
// Deterministic honesty gates on fix / finding output (#7).
|
|
318
318
|
//
|
|
@@ -723,7 +723,15 @@ async function verifyPatch({
|
|
|
723
723
|
const fileContents = { ...files };
|
|
724
724
|
let scan;
|
|
725
725
|
try {
|
|
726
|
-
|
|
726
|
+
// `provenance:false` is REQUIRED here, not an optimisation. This scan is
|
|
727
|
+
// deliberately scoped to just the patched file(s), so its finding set is a
|
|
728
|
+
// tiny subset of the project's. updateLifecycle marks every open stableId
|
|
729
|
+
// NOT in the set it is handed as `remediated` — so a single fix
|
|
730
|
+
// verification (every /fix, apply_fix, and autopilot iteration runs one)
|
|
731
|
+
// would mass-mark the rest of the project as remediated, then
|
|
732
|
+
// `reintroduced` on the next real scan. The patched content is also not
|
|
733
|
+
// committed, so there is no history to resolve provenance against anyway.
|
|
734
|
+
scan = await (0,engine/* runFullScan */.wW)({ fileContents, depFileContents, scanRoot, provenance: false }, () => {});
|
|
727
735
|
} catch (e) {
|
|
728
736
|
return { ok: false, reason: 'rescan-failed', error: e.message };
|
|
729
737
|
}
|
package/dist/637.index.js
CHANGED
|
@@ -11,7 +11,8 @@ export const modules = {
|
|
|
11
11
|
/* harmony export */ renderPrDeltaText: () => (/* binding */ renderPrDeltaText)
|
|
12
12
|
/* harmony export */ });
|
|
13
13
|
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
14
|
-
/* harmony import */ var
|
|
14
|
+
/* harmony import */ var _util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8844);
|
|
15
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7691);
|
|
15
16
|
// Shadowscan / security-DELTA on PR (v0.72).
|
|
16
17
|
//
|
|
17
18
|
// Most SAST PR-comment integrations show absolute counts — "12 findings
|
|
@@ -40,16 +41,25 @@ export const modules = {
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
|
|
43
45
|
const FILE_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|java|cs|kt|go|rb|php|sol|swift|rs|tf|yml|yaml|json|toml|md)$/i;
|
|
44
46
|
const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
45
47
|
|
|
48
|
+
// `root` is the PR's repository — a scan target, not this project's own
|
|
49
|
+
// trusted checkout — hardened per FR-PROV-024 / the second Finding
|
|
50
|
+
// Provenance PRD audit (same exposure class as
|
|
51
|
+
// provenance/git-evidence.js's `_run`).
|
|
46
52
|
function _git(root, args) {
|
|
47
|
-
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
53
|
+
const r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)('git', (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitArgs */ .Ax)(args), { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: (0,_util_git_hardening_js__WEBPACK_IMPORTED_MODULE_1__/* .hardenGitEnv */ .Si)() });
|
|
48
54
|
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
function _readFileAtRef(root, ref, file) {
|
|
52
|
-
|
|
58
|
+
// `--no-textconv`: this blob-cat form of `show` (`<ref>:<file>`, not a
|
|
59
|
+
// diff) was verified NOT reachable via a hostile textconv driver in
|
|
60
|
+
// current git, same as git-evidence.js's getBlobAtCommit — kept for
|
|
61
|
+
// defense-in-depth/uniformity.
|
|
62
|
+
const r = _git(root, ['show', '--no-textconv', `${ref}:${file}`]);
|
|
53
63
|
return r.ok ? r.stdout : null;
|
|
54
64
|
}
|
|
55
65
|
|
|
@@ -70,7 +80,12 @@ async function _scanAtRef(root, ref) {
|
|
|
70
80
|
const c = _readFileAtRef(root, ref, f);
|
|
71
81
|
if (c != null) fileContents[f] = c;
|
|
72
82
|
}
|
|
73
|
-
|
|
83
|
+
// `provenance:false` — a base-ref snapshot, not the current working state.
|
|
84
|
+
// Beyond the wasted git walks, updateLifecycle marks every open stableId
|
|
85
|
+
// absent from the finding set it is handed as `remediated`; running the PR
|
|
86
|
+
// delta gate would silently rewrite the project's lifecycle store from the
|
|
87
|
+
// base ref's findings.
|
|
88
|
+
return (0,_engine_js__WEBPACK_IMPORTED_MODULE_2__/* .runFullScan */ .wW)({ fileContents, scanRoot: root, provenance: false }, () => {});
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
function _summary(findings) {
|
|
@@ -85,7 +100,14 @@ function _summary(findings) {
|
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
function _changedFiles(root, baseRef, headRef) {
|
|
88
|
-
|
|
103
|
+
// `--no-ext-diff`: found during the second-audit-remediation sweep — this
|
|
104
|
+
// is a `git diff` call site (`--name-only`, no content rendered, so not
|
|
105
|
+
// itself a verified exploit path today, same as runScan.js's
|
|
106
|
+
// `changedSince`), but every `diff` invocation in this codebase gets it
|
|
107
|
+
// uniformly per the live exploit VERIFIED in material-change.js's
|
|
108
|
+
// classifyGitDiff (external diff drivers fire on `git diff` even with
|
|
109
|
+
// `--no-textconv`, which is a surface `--no-textconv` alone does not close).
|
|
110
|
+
const r = _git(root, ['diff', '--name-only', '--no-ext-diff', `${baseRef}...${headRef}`]);
|
|
89
111
|
if (!r.ok) return new Set();
|
|
90
112
|
return new Set(r.stdout.trim().split('\n').filter(Boolean));
|
|
91
113
|
}
|