@clear-capabilities/agentic-security-scanner 0.143.0 → 0.144.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 +304 -0
- package/bin/agentic-security.js +477 -47
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +108 -17
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +1 -1
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +165 -52
- package/dist/526.index.js +108 -17
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +1 -1
- package/dist/730.index.js +311 -0
- package/dist/736.index.js +301 -0
- package/dist/824.index.js +7 -0
- package/dist/905.index.js +88 -22
- package/dist/920.index.js +491 -0
- package/dist/970.index.js +109 -0
- package/dist/agentic-security.mjs +13 -13
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/calibration-seed.json +2 -0
- package/package.json +19 -11
- package/src/dataflow/index.js +18 -0
- package/src/dataflow/privacy-catalog.js +290 -0
- package/src/dataflow/privacy-deep-walker.js +515 -0
- package/src/dataflow/privacy-governance.js +126 -0
- package/src/dataflow/privacy-inventory.js +154 -0
- package/src/dataflow/privacy-sink-policy.js +125 -0
- package/src/dataflow/privacy-taint.js +115 -54
- package/src/dataflow/privacy-taxonomy.js +233 -0
- package/src/discovery/disprove.js +7 -3
- package/src/discovery/hunter.js +9 -5
- package/src/discovery/index.js +2 -2
- package/src/discovery/llm-invoke.js +69 -13
- package/src/egress/audit.js +147 -0
- package/src/egress/policy.js +313 -0
- package/src/egress/redact.js +180 -0
- package/src/engine.js +575 -288
- package/src/fix/apply-fix-service.js +403 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/mcp/tools.js +157 -50
- package/src/pipeline/analyzer-supervisor.js +93 -0
- package/src/pipeline/analyzer-worker.js +26 -0
- package/src/pipeline/annotator-runner.js +33 -0
- package/src/pipeline/assurance-mode.js +91 -0
- package/src/pipeline/cascade-worker-pool.js +172 -0
- package/src/pipeline/cascade-worker.js +43 -0
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/detector-runner.js +51 -0
- package/src/pipeline/enrichment-completion.js +58 -0
- package/src/pipeline/evidence-provenance.js +91 -0
- package/src/pipeline/finding-schema.js +101 -0
- package/src/pipeline/legacy-compat.js +101 -0
- package/src/pipeline/producer-collector.js +48 -0
- package/src/pipeline/producer-registry.js +112 -0
- package/src/pipeline/scan-health.js +144 -0
- package/src/posture/CLAUDE.md +2 -0
- package/src/posture/accuracy-scorecard.js +96 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +217 -0
- package/src/posture/auditor-walkthrough.js +70 -8
- package/src/posture/calibration-feedback.js +201 -0
- package/src/posture/calibration-seed.json +2 -0
- package/src/posture/calibration.js +25 -0
- package/src/posture/compliance-evidence-signing.js +131 -0
- package/src/posture/compliance-policy.js +314 -17
- package/src/posture/custom-rules.js +36 -0
- package/src/posture/deterministic.js +8 -1
- package/src/posture/encryption-provider.js +205 -0
- package/src/posture/evidence-grade-wording.js +71 -0
- package/src/posture/fix-history.js +113 -19
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +56 -7
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +90 -0
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/privacy-framework.js +33 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/state-dir.js +36 -1
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +51 -2
- package/src/report/oscal.js +7 -2
- package/src/sast/cpp.js +3 -14
- package/src/sca/llm-function-extract.js +6 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export const id = 265;
|
|
2
|
+
export const ids = [265];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 4265:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ createCascadePool: () => (/* binding */ createCascadePool)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony export DEFAULT_GRACE_MS */
|
|
12
|
+
/* harmony import */ var node_worker_threads__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5919);
|
|
13
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
|
|
14
|
+
/* harmony import */ var node_url__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3136);
|
|
15
|
+
// FR-202 phase 3b (D-0050): a pool of long-lived workers, each running engine.js's
|
|
16
|
+
// _runFileCascade for many files over its lifetime, instead of the one-shot-per-call
|
|
17
|
+
// model runWithDeadline/analyzer-supervisor.js uses (which is correct for FR-202's
|
|
18
|
+
// primitive-level proof but would force whole-project setup — _buildProjectIndex,
|
|
19
|
+
// _buildGlobalJavaTaintedMethodIndex, _loadCustomRules — to redo per FILE, an O(n^2)
|
|
20
|
+
// cost across a scan). Each worker performs that setup ONCE at creation via
|
|
21
|
+
// _initCascadeWorkerState, then answers task messages over its lifetime.
|
|
22
|
+
//
|
|
23
|
+
// A task that exceeds its deadline+grace gets its OWN worker terminated (genuinely
|
|
24
|
+
// preemptive, mirroring analyzer-supervisor.js's worker.terminate() precedent) and a
|
|
25
|
+
// fresh replacement worker is spawned to keep the pool at full size for later files —
|
|
26
|
+
// one hung file costs one worker restart, not the whole scan.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
// `new Worker()` needs a REAL file on disk -- once ncc bundles this module's code
|
|
32
|
+
// into a dist/ chunk, `import.meta.url` resolves to that chunk's location (dist/),
|
|
33
|
+
// not to src/pipeline/, so a naive path.join(HERE, 'cascade-worker.js') would look
|
|
34
|
+
// for a file that was never copied there. src/ always ships as a sibling of dist/
|
|
35
|
+
// (see package.json's "files"), in both a dev checkout and the published npm
|
|
36
|
+
// package, so fall back to the real, unbundled worker script in that case.
|
|
37
|
+
function _resolveDefaultWorkerScript() {
|
|
38
|
+
const hereDir = node_path__WEBPACK_IMPORTED_MODULE_1__.dirname((0,node_url__WEBPACK_IMPORTED_MODULE_2__.fileURLToPath)(import.meta.url));
|
|
39
|
+
if (node_path__WEBPACK_IMPORTED_MODULE_1__.basename(hereDir) === 'pipeline') return node_path__WEBPACK_IMPORTED_MODULE_1__.join(hereDir, 'cascade-worker.js');
|
|
40
|
+
return node_path__WEBPACK_IMPORTED_MODULE_1__.join(hereDir, '..', 'src', 'pipeline', 'cascade-worker.js');
|
|
41
|
+
}
|
|
42
|
+
const DEFAULT_WORKER_SCRIPT = _resolveDefaultWorkerScript();
|
|
43
|
+
const DEFAULT_GRACE_MS = 500;
|
|
44
|
+
|
|
45
|
+
function spawnReadyWorker({ workerScript, modulePath, fileContents, scanRoot }) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
let settled = false;
|
|
48
|
+
const worker = new node_worker_threads__WEBPACK_IMPORTED_MODULE_0__.Worker(workerScript, { workerData: { modulePath, fileContents, scanRoot } });
|
|
49
|
+
const finish = (fn, value) => {
|
|
50
|
+
if (settled) return;
|
|
51
|
+
settled = true;
|
|
52
|
+
worker.off('message', onMessage);
|
|
53
|
+
worker.off('error', onError);
|
|
54
|
+
fn(value);
|
|
55
|
+
};
|
|
56
|
+
const onMessage = (msg) => {
|
|
57
|
+
if (!msg) return;
|
|
58
|
+
if (msg.type === 'ready') finish(resolve, worker);
|
|
59
|
+
else if (msg.type === 'init-error') { worker.terminate().catch(() => {}); finish(reject, new Error(msg.error)); }
|
|
60
|
+
};
|
|
61
|
+
const onError = (err) => { worker.terminate().catch(() => {}); finish(reject, err); };
|
|
62
|
+
worker.on('message', onMessage);
|
|
63
|
+
worker.on('error', onError);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {object} opts
|
|
69
|
+
* @param {Record<string,string>} opts.fileContents - broadcast once to every worker at creation.
|
|
70
|
+
* @param {string|null} opts.scanRoot
|
|
71
|
+
* @param {number} [opts.poolSize=4]
|
|
72
|
+
* @param {string} opts.modulePath - absolute path to the engine.js each worker imports.
|
|
73
|
+
* @param {string} [opts.workerScript] - defaults to cascade-worker.js next to this file.
|
|
74
|
+
* @param {number} [opts.graceMs]
|
|
75
|
+
*/
|
|
76
|
+
function createCascadePool(opts = {}) {
|
|
77
|
+
const {
|
|
78
|
+
fileContents = {},
|
|
79
|
+
scanRoot = null,
|
|
80
|
+
poolSize = 4,
|
|
81
|
+
workerScript = DEFAULT_WORKER_SCRIPT,
|
|
82
|
+
modulePath,
|
|
83
|
+
graceMs = DEFAULT_GRACE_MS,
|
|
84
|
+
} = opts;
|
|
85
|
+
if (!modulePath) throw new Error('createCascadePool requires opts.modulePath');
|
|
86
|
+
if (!(Number.isInteger(poolSize) && poolSize > 0)) throw new Error('createCascadePool requires opts.poolSize to be a positive integer');
|
|
87
|
+
|
|
88
|
+
const idle = [];
|
|
89
|
+
const pendingIdle = [];
|
|
90
|
+
const allWorkers = new Set();
|
|
91
|
+
let nextTaskId = 1;
|
|
92
|
+
let closed = false;
|
|
93
|
+
let spawnFailureCount = 0;
|
|
94
|
+
let lastSpawnError = null;
|
|
95
|
+
|
|
96
|
+
function spawnAndRegister() {
|
|
97
|
+
return spawnReadyWorker({ workerScript, modulePath, fileContents, scanRoot }).then((w) => {
|
|
98
|
+
allWorkers.add(w);
|
|
99
|
+
return w;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Background respawn after a killed worker (see runFile's timeout branch)
|
|
104
|
+
// is deliberately fire-and-forget -- the timed-out task has already
|
|
105
|
+
// resolved and nothing is waiting on this promise -- but a swallowed
|
|
106
|
+
// failure here would otherwise shrink the pool invisibly (e.g. thread
|
|
107
|
+
// exhaustion making every future respawn fail silently forever). Track
|
|
108
|
+
// it instead of a bare `.catch(() => {})`.
|
|
109
|
+
function respawnInBackground() {
|
|
110
|
+
spawnAndRegister().then(releaseWorker).catch((err) => {
|
|
111
|
+
spawnFailureCount += 1;
|
|
112
|
+
lastSpawnError = err;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function releaseWorker(w) {
|
|
117
|
+
if (closed) { allWorkers.delete(w); w.terminate().catch(() => {}); return; }
|
|
118
|
+
if (pendingIdle.length) pendingIdle.shift()(w);
|
|
119
|
+
else idle.push(w);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function acquireWorker() {
|
|
123
|
+
if (idle.length) return Promise.resolve(idle.pop());
|
|
124
|
+
return new Promise((resolve) => pendingIdle.push(resolve));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ready = Promise.all(
|
|
128
|
+
Array.from({ length: poolSize }, () => spawnAndRegister().then(releaseWorker))
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Run one file's cascade on a pooled worker. Resolves with
|
|
133
|
+
* {ok:true, result} | {ok:false, error} | {ok:false, timedOut:true, timeoutMs, graceMs}.
|
|
134
|
+
* Never rejects.
|
|
135
|
+
*/
|
|
136
|
+
async function runFile(p, c, taskScanRoot, detectorErrors, taskOpts = {}) {
|
|
137
|
+
await ready;
|
|
138
|
+
if (closed) return { ok: false, error: 'pool is closed' };
|
|
139
|
+
const timeoutMs = taskOpts.timeoutMs;
|
|
140
|
+
const worker = await acquireWorker();
|
|
141
|
+
const id = nextTaskId++;
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
let settled = false;
|
|
144
|
+
let killTimer = null;
|
|
145
|
+
const onMessage = (msg) => {
|
|
146
|
+
if (settled || !msg || msg.id !== id) return;
|
|
147
|
+
settled = true;
|
|
148
|
+
if (killTimer) clearTimeout(killTimer);
|
|
149
|
+
worker.off('message', onMessage);
|
|
150
|
+
releaseWorker(worker);
|
|
151
|
+
resolve(msg.ok ? { ok: true, result: msg.result } : { ok: false, error: msg.error });
|
|
152
|
+
};
|
|
153
|
+
worker.on('message', onMessage);
|
|
154
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
155
|
+
killTimer = setTimeout(() => {
|
|
156
|
+
if (settled) return;
|
|
157
|
+
settled = true;
|
|
158
|
+
worker.off('message', onMessage);
|
|
159
|
+
allWorkers.delete(worker);
|
|
160
|
+
worker.terminate().catch(() => {});
|
|
161
|
+
if (!closed) respawnInBackground();
|
|
162
|
+
resolve({ ok: false, timedOut: true, timeoutMs, graceMs });
|
|
163
|
+
}, timeoutMs + graceMs);
|
|
164
|
+
killTimer.unref?.();
|
|
165
|
+
}
|
|
166
|
+
worker.postMessage({ type: 'task', id, p, c, scanRoot: taskScanRoot, detectorErrors });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function shutdown() {
|
|
171
|
+
closed = true;
|
|
172
|
+
await ready.catch(() => {});
|
|
173
|
+
for (const w of allWorkers) await w.terminate().catch(() => {});
|
|
174
|
+
allWorkers.clear();
|
|
175
|
+
idle.length = 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
runFile,
|
|
180
|
+
shutdown,
|
|
181
|
+
ready,
|
|
182
|
+
DEFAULT_GRACE_MS: graceMs,
|
|
183
|
+
getSpawnFailureCount: () => spawnFailureCount,
|
|
184
|
+
getLastSpawnError: () => lastSpawnError,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
/***/ })
|
|
190
|
+
|
|
191
|
+
};
|
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__(7087);
|
|
12
12
|
// Secrets submodule view of the engine — credential + entropy + TODO scanning.
|
|
13
13
|
|
|
14
14
|
|
package/dist/435.index.js
CHANGED
|
@@ -23,6 +23,12 @@ var external_node_url_ = __webpack_require__(3136);
|
|
|
23
23
|
var promises_ = __webpack_require__(1455);
|
|
24
24
|
// EXTERNAL MODULE: ./src/posture/fix-history.js
|
|
25
25
|
var fix_history = __webpack_require__(4407);
|
|
26
|
+
// EXTERNAL MODULE: ./src/fix/apply-fix-service.js
|
|
27
|
+
var apply_fix_service = __webpack_require__(7730);
|
|
28
|
+
// EXTERNAL MODULE: ./src/posture/material-change.js
|
|
29
|
+
var material_change = __webpack_require__(4629);
|
|
30
|
+
// EXTERNAL MODULE: ./src/fix/approver-registry.js
|
|
31
|
+
var approver_registry = __webpack_require__(437);
|
|
26
32
|
;// CONCATENATED MODULE: ./src/posture/deterministic-fix.js
|
|
27
33
|
// Deterministic fix synthesis (#1) — for the narrow set of vulnerability classes
|
|
28
34
|
// where a context-INDEPENDENT literal swap is a safe, correct fix, produce a
|
|
@@ -92,6 +98,8 @@ function synthesizeDeterministicPatch(finding, fileContent) {
|
|
|
92
98
|
|
|
93
99
|
// EXTERNAL MODULE: ./src/posture/integrity.js
|
|
94
100
|
var integrity = __webpack_require__(1130);
|
|
101
|
+
// EXTERNAL MODULE: ./src/posture/state-dir.js
|
|
102
|
+
var state_dir = __webpack_require__(1174);
|
|
95
103
|
// EXTERNAL MODULE: ./src/posture/cache-economics.js
|
|
96
104
|
var cache_economics = __webpack_require__(8752);
|
|
97
105
|
;// CONCATENATED MODULE: ./src/mcp/redact.js
|
|
@@ -197,10 +205,8 @@ function redactArgsBlob(s) {
|
|
|
197
205
|
return redactString(s);
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
// EXTERNAL MODULE: ./src/report/index.js +
|
|
201
|
-
var report = __webpack_require__(
|
|
202
|
-
// EXTERNAL MODULE: ./src/posture/state-dir.js
|
|
203
|
-
var state_dir = __webpack_require__(1174);
|
|
208
|
+
// EXTERNAL MODULE: ./src/report/index.js + 3 modules
|
|
209
|
+
var report = __webpack_require__(457);
|
|
204
210
|
;// CONCATENATED MODULE: ./src/posture/agents-memory.js
|
|
205
211
|
// AGENTS.md — writable continual-learning memory (harness-anatomy #2).
|
|
206
212
|
//
|
|
@@ -499,6 +505,10 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
|
|
|
499
505
|
|
|
500
506
|
|
|
501
507
|
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
|
|
502
512
|
// Lazy-loaded: these transitively pull in npm packages (@babel/core and
|
|
503
513
|
// friends) that aren't available in the plugin-cache install path
|
|
504
514
|
// (no node_modules). Deferring keeps the MCP server bootable everywhere;
|
|
@@ -863,26 +873,44 @@ const scan_diff = {
|
|
|
863
873
|
// / MAX_TOTAL_SCAN_BYTES, so this does not turn scan_diff into a
|
|
864
874
|
// full-project deep scan).
|
|
865
875
|
const runScan = await getRunScan();
|
|
866
|
-
|
|
876
|
+
// FR-704 (assurance-hardening PRD): this tool's own description promises
|
|
877
|
+
// "runs scan in memory" — without this, runFullScan's own state writers
|
|
878
|
+
// (dpia.md, ropa.md, privacy-framework.json, threat-model.json, and
|
|
879
|
+
// others) fire unconditionally on every call, silently mutating the
|
|
880
|
+
// user's real project on every pre-write self-correction scan. Confirmed
|
|
881
|
+
// by direct execution before this fix (11 state artifacts written by a
|
|
882
|
+
// single scan_diff-shaped call).
|
|
883
|
+
const result = await (0,state_dir/* withStateWritesDisabled */.Ao)(() =>
|
|
884
|
+
runScan(sessionRoot, { network: false, fileContents, deep: true, deepInCi: true }));
|
|
867
885
|
const wantSet = new Set(Object.keys(fileContents));
|
|
868
886
|
const sevRank = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
869
887
|
const min = sevRank[severity] ?? 0;
|
|
870
|
-
// Stage 6 correctness audit: this only
|
|
871
|
-
// (the SAST channel) — scan.secrets and
|
|
872
|
-
// arrays on the raw runScan() result
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
// the
|
|
880
|
-
|
|
888
|
+
// Stage 6 correctness audit (historical): this used to only read
|
|
889
|
+
// result.scan.findings (the SAST channel) — scan.secrets and
|
|
890
|
+
// scan.logicVulns are separate arrays on the raw runScan() result, and a
|
|
891
|
+
// hand-rolled 3-channel concat here was a second, divergent copy of the
|
|
892
|
+
// merge report/index.js's normalizeFindings() already does (four
|
|
893
|
+
// channels, plus per-channel defaulting and remediation-string
|
|
894
|
+
// resolution the old concat re-implemented separately and could drift
|
|
895
|
+
// from). Assurance-hardening PRD FR-105 ("JSON, SARIF, HTML, CSV, JUnit,
|
|
896
|
+
// and MCP outputs derive from the same validated object"): route through
|
|
897
|
+
// the same canonical merge every other output format uses.
|
|
898
|
+
//
|
|
899
|
+
// This closes the field-mapping/dedup divergence, but does NOT make
|
|
900
|
+
// scan_diff surface SCA/supply-chain findings end to end: this handler
|
|
901
|
+
// never builds a `depFileContents` map (everything a caller passes in
|
|
902
|
+
// `files`, manifests included, lands in `fileContents`), and manifest-
|
|
903
|
+
// based supply-chain detection in engine.js reads only `depFileContents`
|
|
904
|
+
// — so `result.scan.supplyChain` is always empty for this tool today
|
|
905
|
+
// regardless of this fix. That is a separate, real limitation (scan_diff
|
|
906
|
+
// was designed for pre-write code self-correction, not manifest
|
|
907
|
+
// scanning), left as-is rather than silently claimed fixed here.
|
|
908
|
+
const findings = (0,report.normalizeFindings)(result.scan)
|
|
881
909
|
.filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
|
|
882
910
|
.map(f => redactFinding({
|
|
883
911
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
884
|
-
title: f.
|
|
885
|
-
description: f.description, remediation:
|
|
912
|
+
title: f.vuln, cwe: f.cwe,
|
|
913
|
+
description: f.description, remediation: f.remediation,
|
|
886
914
|
}));
|
|
887
915
|
// Harness-anatomy #1: offload when the result exceeds OFFLOAD_THRESHOLD.
|
|
888
916
|
// The agent gets a head+tail preview plus a path it can page through;
|
|
@@ -1032,7 +1060,7 @@ const explain_finding = {
|
|
|
1032
1060
|
// ─── apply_fix ───────────────────────────────────────────────────────────────
|
|
1033
1061
|
const apply_fix = {
|
|
1034
1062
|
name: 'apply_fix',
|
|
1035
|
-
description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing.',
|
|
1063
|
+
description: 'Apply a fix for a finding. Two modes: (1) the stored fix.replacement, or (2) a caller-supplied `patch` (a files map) which is RE-VERIFIED inline (rescan-clean + no new ≥medium + lint) before any write — this unblocks findings that ship only a template or description. Refuses if last-scan.json fails its HMAC check, if the finding is shadow-marked, or if a path escapes the session root via lexical traversal OR a symlink. Requires confirm:true. Supports dry_run:true to preview without writing. On success, `verified:true` means verification passed but `verifiedFull:true` is the honest signal that every required leg (lint when configured, tests when a runner exists) genuinely ran — a false `verifiedFull` with `verified:true` means the pass is real but degraded (see `verify.degradedLegs`), not a full verification.',
|
|
1036
1064
|
inputSchema: {
|
|
1037
1065
|
type: 'object',
|
|
1038
1066
|
additionalProperties: false,
|
|
@@ -1073,6 +1101,27 @@ const apply_fix = {
|
|
|
1073
1101
|
partialSanitization: { type: 'boolean' },
|
|
1074
1102
|
},
|
|
1075
1103
|
},
|
|
1104
|
+
// FR-307/FR-1002: this schema had `additionalProperties: false`
|
|
1105
|
+
// and never declared `approval` — the property apply-fix-
|
|
1106
|
+
// service.js's high-impact-change gate has required since FR-307
|
|
1107
|
+
// was built. A real MCP caller supplying fixMeta.approval was
|
|
1108
|
+
// rejected by validate.js at the schema layer before the handler
|
|
1109
|
+
// ever ran, silently making the approval gate (and FR-1002's
|
|
1110
|
+
// identity check layered on it) unreachable from this tool's
|
|
1111
|
+
// only real production entry point. See D-0024.
|
|
1112
|
+
approval: {
|
|
1113
|
+
type: 'object',
|
|
1114
|
+
additionalProperties: false,
|
|
1115
|
+
properties: {
|
|
1116
|
+
approvedBy: { type: 'string', minLength: 1, maxLength: 200 },
|
|
1117
|
+
reason: { type: 'string', minLength: 1, maxLength: 1000 },
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
// FR-1003: separation-of-duties. Self-reported the same way
|
|
1121
|
+
// approvedBy is — this tool has no way to determine who actually
|
|
1122
|
+
// wrote a patch, so `author` is a claim, checked against a
|
|
1123
|
+
// configurable policy the same way `approval` is.
|
|
1124
|
+
author: { type: 'string', minLength: 1, maxLength: 200 },
|
|
1076
1125
|
},
|
|
1077
1126
|
},
|
|
1078
1127
|
},
|
|
@@ -1147,20 +1196,78 @@ const apply_fix = {
|
|
|
1147
1196
|
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
|
|
1148
1197
|
};
|
|
1149
1198
|
}
|
|
1199
|
+
// FR-307/FR-1002/D-0024: this caller-supplied-patch branch writes via
|
|
1200
|
+
// applyFixHistory() directly and never called applyVerifiedFix() — so
|
|
1201
|
+
// the high-impact-change approval gate (auth/authZ/crypto/PII/schema/
|
|
1202
|
+
// infra-privilege/public-API) built for the OTHER apply_fix branch
|
|
1203
|
+
// (stored fix.replacement) never ran here at all, for any input. Since
|
|
1204
|
+
// this is the branch the tool's own description calls the one that
|
|
1205
|
+
// covers "~100% of findings that ship only a template," that gap was
|
|
1206
|
+
// the larger of the two found this cycle. Same before/after content
|
|
1207
|
+
// shape `apply-fix-service.js` already uses — read-first-in-try/catch
|
|
1208
|
+
// (D-0012), never existsSync-then-readFileSync.
|
|
1209
|
+
const filesForMaterialClassification = {};
|
|
1210
|
+
for (const [rel, v] of Object.entries(confinedAbs)) {
|
|
1211
|
+
let before = '';
|
|
1212
|
+
try { before = await promises_.readFile(v.abs, 'utf8'); } catch { /* new file — before stays '' */ }
|
|
1213
|
+
filesForMaterialClassification[rel] = { before, after: v.content };
|
|
1214
|
+
}
|
|
1215
|
+
const materialClassification = (0,material_change/* classifyFixMaterialRisk */.kz)(filesForMaterialClassification);
|
|
1150
1216
|
if (dry_run) {
|
|
1151
|
-
return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary };
|
|
1217
|
+
return { _meta: META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
|
|
1218
|
+
}
|
|
1219
|
+
if (materialClassification.highImpactCategories.length) {
|
|
1220
|
+
const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
|
|
1221
|
+
const hasApprovalEvidence = !!(approval && typeof approval === 'object' &&
|
|
1222
|
+
typeof approval.approvedBy === 'string' && approval.approvedBy.trim().length > 0 &&
|
|
1223
|
+
typeof approval.reason === 'string' && approval.reason.trim().length > 0);
|
|
1224
|
+
if (!hasApprovalEvidence) {
|
|
1225
|
+
return {
|
|
1226
|
+
_meta: META, applied: false,
|
|
1227
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
|
|
1228
|
+
materialClassification,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
const approverRegistry = (0,approver_registry/* loadApproverRegistry */.P4)(ctx.sessionRoot);
|
|
1232
|
+
const requiredRoles = (0,approver_registry/* requiredRolesFor */.Kq)(approverRegistry, materialClassification.highImpactCategories);
|
|
1233
|
+
const identityCheck = (0,approver_registry/* verifyApprover */.$N)(approverRegistry, approval.approvedBy, requiredRoles);
|
|
1234
|
+
if (!identityCheck.verified) {
|
|
1235
|
+
return {
|
|
1236
|
+
_meta: META, applied: false,
|
|
1237
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
|
|
1238
|
+
materialClassification,
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
// FR-1003: separation-of-duties, same no-op-unless-configured gate
|
|
1242
|
+
// as apply-fix-service.js's own copy — see approver-registry.js.
|
|
1243
|
+
const sodCheck = (0,approver_registry/* checkSeparationOfDuties */.I0)(approverRegistry, fixMeta?.author, approval.approvedBy);
|
|
1244
|
+
if (!sodCheck.ok) {
|
|
1245
|
+
return {
|
|
1246
|
+
_meta: META, applied: false,
|
|
1247
|
+
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
|
|
1248
|
+
materialClassification,
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1152
1251
|
}
|
|
1153
1252
|
const written = [];
|
|
1154
1253
|
try {
|
|
1155
1254
|
for (const [rel, v] of Object.entries(confinedAbs)) {
|
|
1156
|
-
const
|
|
1255
|
+
const fileExisted = external_node_fs_.existsSync(v.abs);
|
|
1256
|
+
const originalContent = fileExisted ? await promises_.readFile(v.abs, 'utf8') : '';
|
|
1157
1257
|
const entry = await (0,fix_history/* applyFix */.oM)({
|
|
1158
|
-
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content,
|
|
1258
|
+
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content, fileExisted,
|
|
1159
1259
|
findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
|
|
1160
1260
|
});
|
|
1161
1261
|
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
1162
1262
|
}
|
|
1163
1263
|
} catch (e) {
|
|
1264
|
+
// FR-306: roll back every file THIS batch already wrote before the
|
|
1265
|
+
// failure — applyFixHistory already restored the one file that just
|
|
1266
|
+
// failed; this covers the rest, so a multi-file patch never leaves
|
|
1267
|
+
// some files patched and others not.
|
|
1268
|
+
for (const w of written) {
|
|
1269
|
+
try { await (0,fix_history/* revertEntryById */.rJ)(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
|
|
1270
|
+
}
|
|
1164
1271
|
if (e && e.name === 'FixAttemptBudgetExceededError') {
|
|
1165
1272
|
return { _meta: META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
|
|
1166
1273
|
}
|
|
@@ -1168,7 +1275,7 @@ const apply_fix = {
|
|
|
1168
1275
|
}
|
|
1169
1276
|
let acceptance = null;
|
|
1170
1277
|
try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* best-effort */ }
|
|
1171
|
-
return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance };
|
|
1278
|
+
return { _meta: META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
|
|
1172
1279
|
}
|
|
1173
1280
|
|
|
1174
1281
|
if (typeof f.fix?.replacement !== 'string') {
|
|
@@ -1205,41 +1312,47 @@ const apply_fix = {
|
|
|
1205
1312
|
};
|
|
1206
1313
|
}
|
|
1207
1314
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
maxAttempts: e.max,
|
|
1233
|
-
key: e.key,
|
|
1234
|
-
};
|
|
1315
|
+
// FR-301/A-08 (assurance-hardening PRD): this branch used to write
|
|
1316
|
+
// f.fix.replacement straight to disk with NO fresh verification — no
|
|
1317
|
+
// rescan, no lint, nothing confirming the stored replacement actually
|
|
1318
|
+
// closes the finding it claims to fix. The caller-patch branch above
|
|
1319
|
+
// already required this; there is no reason a STORED fix should be
|
|
1320
|
+
// trusted more than a caller-supplied one just because it shipped with
|
|
1321
|
+
// the finding. Routed through the same applyVerifiedFix() service the
|
|
1322
|
+
// CLI's `fix --apply` now also uses (src/fix/apply-fix-service.js) —
|
|
1323
|
+
// confinement/reserved-path are re-checked there too (harmless
|
|
1324
|
+
// redundancy with the dry_run preview above, kept for that preview's
|
|
1325
|
+
// size-diff shape) but the load-bearing addition is the verification
|
|
1326
|
+
// gate before the write.
|
|
1327
|
+
if (!f.stableId) {
|
|
1328
|
+
return { _meta: META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
|
|
1329
|
+
}
|
|
1330
|
+
const result = await (0,apply_fix_service/* applyVerifiedFix */.On)({
|
|
1331
|
+
scanRoot: ctx.sessionRoot,
|
|
1332
|
+
finding: f,
|
|
1333
|
+
files: { [f.file]: f.fix.replacement },
|
|
1334
|
+
fixMeta,
|
|
1335
|
+
});
|
|
1336
|
+
if (!result.ok) {
|
|
1337
|
+
if (result.budgetExceeded) {
|
|
1338
|
+
return { _meta: META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
|
|
1235
1339
|
}
|
|
1236
|
-
|
|
1340
|
+
return { _meta: META, applied: false, reason: result.reason, verify: result.verify || null };
|
|
1237
1341
|
}
|
|
1238
1342
|
// R25 (PRD §5): surface the running auto-fix acceptance rate after each
|
|
1239
1343
|
// applied fix, so the closed loop reports its own success metric.
|
|
1240
1344
|
let acceptance = null;
|
|
1241
1345
|
try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* metric is best-effort */ }
|
|
1242
|
-
|
|
1346
|
+
const entry = result.written[0];
|
|
1347
|
+
return {
|
|
1348
|
+
// FR-305: verifiedFull distinguishes "every required leg (lint, tests)
|
|
1349
|
+
// genuinely ran and passed" from "passed, but a required leg was
|
|
1350
|
+
// skipped or unavailable" — verified:true alone conflates them.
|
|
1351
|
+
_meta: META, applied: true, verified: true, verifiedFull: result.verifiedFull,
|
|
1352
|
+
historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
|
|
1353
|
+
integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
|
|
1354
|
+
verify: result.verify,
|
|
1355
|
+
};
|
|
1243
1356
|
},
|
|
1244
1357
|
};
|
|
1245
1358
|
|