@clear-capabilities/agentic-security-scanner 0.127.0 → 0.128.1
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 +60 -0
- package/dist/11.index.js +353 -0
- package/dist/113.index.js +525 -0
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +18 -7
- package/dist/637.index.js +1 -1
- package/dist/826.index.js +4 -1
- package/dist/agentic-security.mjs +1 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/engine.js +27 -1
- package/src/integrations/tickets.js +9 -3
- package/src/mcp/tools.js +17 -6
- package/src/posture/CLAUDE.md +7 -0
- package/src/posture/entrypoint-inventory.js +248 -0
- package/src/posture/falsification.js +121 -0
- package/src/posture/fix-honesty-gate.js +175 -0
- package/src/posture/fix-verify.js +18 -3
- package/src/posture/model-routing.js +126 -0
- package/src/posture/root-cause-sweep.js +262 -0
- package/src/pr-comment.js +3 -1
- package/src/util/untrusted.js +148 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
export const id = 113;
|
|
2
|
+
export const ids = [113,11];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 4113:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ verifyFixWithTests: () => (/* binding */ verifyFixWithTests)
|
|
10
|
+
/* harmony export */ });
|
|
11
|
+
/* unused harmony export runProjectTests */
|
|
12
|
+
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
|
|
13
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
14
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
15
|
+
/* harmony import */ var _fix_verify_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11);
|
|
16
|
+
// Closed-loop fix verification (v0.68).
|
|
17
|
+
//
|
|
18
|
+
// Existing `fix-verify.js` does scan + lint. This module adds the third
|
|
19
|
+
// leg: run the project's test suite against the patched file set. A fix
|
|
20
|
+
// is `verified-clean` only when:
|
|
21
|
+
//
|
|
22
|
+
// 1. Re-scan no longer fires the original finding's stableId
|
|
23
|
+
// 2. No new ≥medium findings introduced
|
|
24
|
+
// 3. Project linter (when present) passes on the patched files
|
|
25
|
+
// 4. Project test runner (when present) exits 0 within budget
|
|
26
|
+
//
|
|
27
|
+
// If the project has no detected test runner, we emit `untested-but-passes`
|
|
28
|
+
// rather than fail-closed — many small repos have no test suite and we
|
|
29
|
+
// don't want to refuse all fixes there. The verdict is honest.
|
|
30
|
+
//
|
|
31
|
+
// Design note: we run the tests against the WRITTEN patch, not an in-
|
|
32
|
+
// memory overlay — most real test runners can't be given an alternate
|
|
33
|
+
// filesystem cheaply. Callers are expected to apply the patch first
|
|
34
|
+
// (typically via fix-history.applyFix which creates a recovery backup),
|
|
35
|
+
// then call this. If verification fails, undoLast() rolls back.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
43
|
+
|
|
44
|
+
// Test-runner discovery. Each entry: a sentinel-file check + a command +
|
|
45
|
+
// args. Order matters — JS first (most common), then Python, Go, Rust,
|
|
46
|
+
// Java/Maven, Java/Gradle, Ruby.
|
|
47
|
+
function _detectRunner(scanRoot) {
|
|
48
|
+
const has = (p) => { try { return node_fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, p)); } catch { return false; } };
|
|
49
|
+
const pkg = (() => {
|
|
50
|
+
try {
|
|
51
|
+
const raw = node_fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, 'package.json'), 'utf8');
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
} catch { return null; }
|
|
54
|
+
})();
|
|
55
|
+
if (pkg && pkg.scripts && pkg.scripts.test && !/no test specified/.test(String(pkg.scripts.test))) {
|
|
56
|
+
return { runner: 'npm', cmd: 'npm', args: ['test', '--silent', '--', '--passWithNoTests'] };
|
|
57
|
+
}
|
|
58
|
+
if (has('pytest.ini') || has('pyproject.toml') || has('setup.cfg')) {
|
|
59
|
+
return { runner: 'pytest', cmd: 'pytest', args: ['-q', '--no-header', '-x'] };
|
|
60
|
+
}
|
|
61
|
+
if (has('go.mod')) {
|
|
62
|
+
return { runner: 'go-test', cmd: 'go', args: ['test', './...'] };
|
|
63
|
+
}
|
|
64
|
+
if (has('Cargo.toml')) {
|
|
65
|
+
return { runner: 'cargo-test', cmd: 'cargo', args: ['test', '--quiet'] };
|
|
66
|
+
}
|
|
67
|
+
if (has('Gemfile')) {
|
|
68
|
+
return { runner: 'rspec', cmd: 'bundle', args: ['exec', 'rspec', '--fail-fast'] };
|
|
69
|
+
}
|
|
70
|
+
if (has('pom.xml')) {
|
|
71
|
+
return { runner: 'maven', cmd: 'mvn', args: ['-q', 'test', '-DfailIfNoTests=false'] };
|
|
72
|
+
}
|
|
73
|
+
if (has('build.gradle') || has('build.gradle.kts')) {
|
|
74
|
+
return { runner: 'gradle', cmd: './gradlew', args: ['test', '--quiet', '--no-daemon'] };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Run the detected test runner. Honors a walltime budget. Caller may pass
|
|
80
|
+
// `runnerOverride` to force a specific command (rare; mostly for tests).
|
|
81
|
+
function runProjectTests(scanRoot, opts = {}) {
|
|
82
|
+
if (!scanRoot) return { ok: true, runner: 'none', skipped: true };
|
|
83
|
+
const choice = opts.runnerOverride
|
|
84
|
+
? { runner: opts.runnerOverride.cmd, cmd: opts.runnerOverride.cmd, args: opts.runnerOverride.args || [] }
|
|
85
|
+
: _detectRunner(scanRoot);
|
|
86
|
+
if (!choice) return { ok: true, runner: 'none', skipped: true, reason: 'no-test-runner-detected' };
|
|
87
|
+
const timeout = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
88
|
+
let r;
|
|
89
|
+
try {
|
|
90
|
+
r = (0,node_child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync)(choice.cmd, choice.args, {
|
|
91
|
+
cwd: scanRoot,
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
timeout,
|
|
94
|
+
env: { ...process.env, CI: '1' },
|
|
95
|
+
});
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { ok: false, runner: choice.runner, reason: 'spawn-failed', error: e.message };
|
|
98
|
+
}
|
|
99
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
100
|
+
// Runner not installed — different from "tests failed". Don't fail-closed.
|
|
101
|
+
return { ok: true, runner: choice.runner, skipped: true, reason: 'binary-missing' };
|
|
102
|
+
}
|
|
103
|
+
if (r.status === null) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false, runner: choice.runner, reason: 'timed-out',
|
|
106
|
+
output: ((r.stderr || '') + (r.stdout || '')).slice(-2000),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
ok: r.status === 0,
|
|
111
|
+
runner: choice.runner,
|
|
112
|
+
exitCode: r.status,
|
|
113
|
+
output: ((r.stderr || '') + (r.stdout || '')).slice(-2000),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Closed-loop verification: scan + lint + tests. Returns a single verdict
|
|
118
|
+
// with per-leg detail so the caller can render a precise summary.
|
|
119
|
+
//
|
|
120
|
+
// Returns:
|
|
121
|
+
// {
|
|
122
|
+
// ok: bool,
|
|
123
|
+
// verdict: 'verified-clean' | 'verification-failed' | 'untested-but-passes',
|
|
124
|
+
// legs: { scan: …, lint: …, tests: … },
|
|
125
|
+
// summary: '<human-readable line>',
|
|
126
|
+
// }
|
|
127
|
+
//
|
|
128
|
+
// The `untested-but-passes` verdict is real and intentional: scan+lint
|
|
129
|
+
// passed, but no test runner was found. This is honest signal — callers
|
|
130
|
+
// (the security-fixer agent, downstream MCP tools) can decide whether to
|
|
131
|
+
// require a stronger verdict.
|
|
132
|
+
async function verifyFixWithTests({
|
|
133
|
+
scanRoot,
|
|
134
|
+
originalFindingStableId,
|
|
135
|
+
files,
|
|
136
|
+
depFileContents,
|
|
137
|
+
runTests = true,
|
|
138
|
+
testRunnerOverride,
|
|
139
|
+
testTimeoutMs,
|
|
140
|
+
} = {}) {
|
|
141
|
+
const scanLint = await (0,_fix_verify_js__WEBPACK_IMPORTED_MODULE_3__.verifyFix)({ scanRoot, originalFindingStableId, files, depFileContents });
|
|
142
|
+
const legs = {
|
|
143
|
+
scan: { ok: scanLint.rescan?.ok ?? scanLint.ok, detail: scanLint.rescan ?? scanLint },
|
|
144
|
+
lint: { ok: scanLint.lint?.ok ?? true, detail: scanLint.lint ?? null },
|
|
145
|
+
tests: { ok: true, detail: null, skipped: true, reason: 'not-run' },
|
|
146
|
+
};
|
|
147
|
+
if (!legs.scan.ok || !legs.lint.ok) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
verdict: 'verification-failed',
|
|
151
|
+
legs,
|
|
152
|
+
summary: _summarize(legs, 'verification-failed'),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (runTests) {
|
|
156
|
+
const tests = runProjectTests(scanRoot, { runnerOverride: testRunnerOverride, timeoutMs: testTimeoutMs });
|
|
157
|
+
legs.tests = { ok: tests.ok, detail: tests, skipped: !!tests.skipped, reason: tests.reason };
|
|
158
|
+
}
|
|
159
|
+
const allOk = legs.scan.ok && legs.lint.ok && legs.tests.ok;
|
|
160
|
+
const verdict = !allOk
|
|
161
|
+
? 'verification-failed'
|
|
162
|
+
: (legs.tests.skipped ? 'untested-but-passes' : 'verified-clean');
|
|
163
|
+
return { ok: allOk, verdict, legs, summary: _summarize(legs, verdict) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _summarize(legs, verdict) {
|
|
167
|
+
const bits = [];
|
|
168
|
+
bits.push(`scan: ${legs.scan.ok ? 'pass' : 'fail'}`);
|
|
169
|
+
bits.push(`lint: ${legs.lint.skipped ? 'skip' : legs.lint.ok ? 'pass' : 'fail'}`);
|
|
170
|
+
bits.push(`tests: ${legs.tests.skipped ? 'skip' : legs.tests.ok ? 'pass' : 'fail'}`);
|
|
171
|
+
return `${verdict} (${bits.join(' · ')})`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
/***/ }),
|
|
176
|
+
|
|
177
|
+
/***/ 11:
|
|
178
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
179
|
+
|
|
180
|
+
// ESM COMPAT FLAG
|
|
181
|
+
__webpack_require__.r(__webpack_exports__);
|
|
182
|
+
|
|
183
|
+
// EXPORTS
|
|
184
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
185
|
+
runProjectLinter: () => (/* binding */ runProjectLinter),
|
|
186
|
+
verifyFix: () => (/* binding */ verifyFix),
|
|
187
|
+
verifyPatch: () => (/* binding */ verifyPatch)
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// EXTERNAL MODULE: external "node:child_process"
|
|
191
|
+
var external_node_child_process_ = __webpack_require__(1421);
|
|
192
|
+
// EXTERNAL MODULE: external "node:fs"
|
|
193
|
+
var external_node_fs_ = __webpack_require__(3024);
|
|
194
|
+
// EXTERNAL MODULE: external "node:path"
|
|
195
|
+
var external_node_path_ = __webpack_require__(6760);
|
|
196
|
+
// EXTERNAL MODULE: ./src/engine.js + 524 modules
|
|
197
|
+
var engine = __webpack_require__(8215);
|
|
198
|
+
;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
|
|
199
|
+
// Deterministic honesty gates on fix / finding output (#7).
|
|
200
|
+
//
|
|
201
|
+
// The project's verification discipline (scanner/CLAUDE.md) exists because
|
|
202
|
+
// several releases shipped broken or false because work was reported as done
|
|
203
|
+
// without confirming the artifact changed. Two of those failure modes are
|
|
204
|
+
// *textual* — they live in the prose an agent emits alongside a fix — and can
|
|
205
|
+
// be caught deterministically, with no LLM and no network:
|
|
206
|
+
//
|
|
207
|
+
// 1. Hand-wave residual-risk prose. "The input is adequately handled",
|
|
208
|
+
// "future work", "tbd", "later" — vague assurances that claim safety
|
|
209
|
+
// without naming a concrete remaining vector. A residual you can't name
|
|
210
|
+
// is a residual you're guessing about; reject the guess.
|
|
211
|
+
//
|
|
212
|
+
// 2. An unbacked "this is a false positive / provably safe" verdict. Marking
|
|
213
|
+
// a finding safe is a coverage *reduction* — it must cite a `file:line`
|
|
214
|
+
// that shows why, exactly like the rules-override gate refuses to silently
|
|
215
|
+
// shrink coverage.
|
|
216
|
+
//
|
|
217
|
+
// Plus a conservative fix-tier classifier so a partial remediation can never be
|
|
218
|
+
// labelled FULL: any workaround-only signal (rate-limit, docs, log-without-
|
|
219
|
+
// reject) is WORKAROUND; anything short of (sink signature changed + all callers
|
|
220
|
+
// routed + a discriminating test) is at most MITIGATION; only the full set with
|
|
221
|
+
// no partial-sanitization caveat earns FULL.
|
|
222
|
+
//
|
|
223
|
+
// Pure functions, no side effects, no throwing — safe to call from a command,
|
|
224
|
+
// a hook, or the MCP verify_fix path.
|
|
225
|
+
|
|
226
|
+
// Vague-assurance phrases that a real residual must never hide behind. Matched
|
|
227
|
+
// case-insensitively with word boundaries so "later" doesn't trip on
|
|
228
|
+
// "collateral" and "tbd" doesn't trip on a longer token.
|
|
229
|
+
const BANNED_RESIDUAL_PHRASES = Object.freeze([
|
|
230
|
+
'adequately handled',
|
|
231
|
+
'adequately handles',
|
|
232
|
+
'properly validated',
|
|
233
|
+
'properly handled',
|
|
234
|
+
'handled properly',
|
|
235
|
+
'handled safely',
|
|
236
|
+
'future work',
|
|
237
|
+
'more work needed',
|
|
238
|
+
'to be done',
|
|
239
|
+
'tbd',
|
|
240
|
+
'later',
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
// A citation shaped like `file:line` — one or more non-space, non-colon chars,
|
|
244
|
+
// a colon, then digits. Unanchored: it need only appear somewhere in the item.
|
|
245
|
+
const CITATION_RE = /[^\s:]+:\d+/;
|
|
246
|
+
|
|
247
|
+
// Verdicts that assert the finding is not real and therefore demand a citation.
|
|
248
|
+
// Compared after normalizing separators (`_`/space → `-`) and lowercasing, so
|
|
249
|
+
// FALSE_POSITIVE, false-positive, and "provably safe" all land here.
|
|
250
|
+
const FP_VERDICTS = Object.freeze(new Set(['false-positive', 'provably-safe', 'safe']));
|
|
251
|
+
|
|
252
|
+
function _escapeRe(s) {
|
|
253
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Reject vague-assurance / hand-wave residual-risk prose.
|
|
258
|
+
*
|
|
259
|
+
* An empty or whitespace-only residual is ok — there is no residual to lie
|
|
260
|
+
* about. A non-empty residual is rejected when it contains any banned phrase;
|
|
261
|
+
* each match yields one violation naming the offending phrase.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} residualText
|
|
264
|
+
* @returns {{ ok: boolean, violations: string[] }}
|
|
265
|
+
*/
|
|
266
|
+
function checkResidualHonesty(residualText) {
|
|
267
|
+
const text = typeof residualText === 'string' ? residualText : '';
|
|
268
|
+
if (text.trim() === '') return { ok: true, violations: [] };
|
|
269
|
+
|
|
270
|
+
const violations = [];
|
|
271
|
+
for (const phrase of BANNED_RESIDUAL_PHRASES) {
|
|
272
|
+
const re = new RegExp(`\\b${_escapeRe(phrase)}\\b`, 'i');
|
|
273
|
+
if (re.test(text)) {
|
|
274
|
+
violations.push(`vague-assurance phrase: "${phrase}"`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { ok: violations.length === 0, violations };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function _isCitation(item) {
|
|
281
|
+
if (typeof item === 'string') return CITATION_RE.test(item);
|
|
282
|
+
if (item && typeof item === 'object' && typeof item.location === 'string') {
|
|
283
|
+
return CITATION_RE.test(item.location);
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function _normalizeVerdict(verdict) {
|
|
289
|
+
return String(verdict).trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Require a file:line citation behind a "this is not real" verdict.
|
|
294
|
+
*
|
|
295
|
+
* For a false-positive / provably-safe / safe verdict (case-insensitive; also
|
|
296
|
+
* accepts FALSE_POSITIVE), at least one evidence item must be a `file:line`
|
|
297
|
+
* citation — either a string matching /[^\s:]+:\d+/ or an object
|
|
298
|
+
* `{ location: "file:line" }`. Any other verdict passes unconditionally.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} verdict
|
|
301
|
+
* @param {Array|string|object} evidence
|
|
302
|
+
* @returns {{ ok: boolean, violations: string[] }}
|
|
303
|
+
*/
|
|
304
|
+
function requireCitedEvidence(verdict, evidence) {
|
|
305
|
+
if (typeof verdict !== 'string' || !FP_VERDICTS.has(_normalizeVerdict(verdict))) {
|
|
306
|
+
return { ok: true, violations: [] };
|
|
307
|
+
}
|
|
308
|
+
const items = Array.isArray(evidence)
|
|
309
|
+
? evidence
|
|
310
|
+
: evidence == null
|
|
311
|
+
? []
|
|
312
|
+
: [evidence];
|
|
313
|
+
if (items.some(_isCitation)) return { ok: true, violations: [] };
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
violations: ['false-positive/safe verdict requires a file:line citation'],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Classify a fix into FULL | MITIGATION | WORKAROUND, conservative-first.
|
|
322
|
+
*
|
|
323
|
+
* @param {object} signals
|
|
324
|
+
* @param {boolean} signals.sinkSignatureChanged
|
|
325
|
+
* @param {boolean} signals.allCallersRouted
|
|
326
|
+
* @param {boolean} signals.testDiscriminates - a test that fails pre-fix, passes post-fix
|
|
327
|
+
* @param {boolean} [signals.rateLimitOnly]
|
|
328
|
+
* @param {boolean} [signals.docsOnly]
|
|
329
|
+
* @param {boolean} [signals.logOnlyNoReject]
|
|
330
|
+
* @param {boolean} [signals.partialSanitization]
|
|
331
|
+
* @returns {'FULL'|'MITIGATION'|'WORKAROUND'}
|
|
332
|
+
*/
|
|
333
|
+
function computeFixTier(signals) {
|
|
334
|
+
const s = signals && typeof signals === 'object' ? signals : {};
|
|
335
|
+
if (s.rateLimitOnly || s.docsOnly || s.logOnlyNoReject) return 'WORKAROUND';
|
|
336
|
+
const complete = s.sinkSignatureChanged && s.allCallersRouted && s.testDiscriminates;
|
|
337
|
+
if (s.partialSanitization || !complete) return 'MITIGATION';
|
|
338
|
+
return 'FULL';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Compose the three gates for a single fix's output.
|
|
343
|
+
*
|
|
344
|
+
* ok = residual-honesty ok AND evidence-citation ok, further constrained by the
|
|
345
|
+
* tier/residual consistency invariant:
|
|
346
|
+
* - a FULL tier must NOT carry a residual (a full fix has nothing left);
|
|
347
|
+
* - a non-FULL tier MUST document a residual (say what's still open).
|
|
348
|
+
*
|
|
349
|
+
* @param {{ residual?: string, verdict?: string, evidence?: any, signals?: object }} input
|
|
350
|
+
* @returns {{ ok: boolean, tier: string, violations: string[] }}
|
|
351
|
+
*/
|
|
352
|
+
function gateFixOutput({ residual, verdict, evidence, signals } = {}) {
|
|
353
|
+
const tier = computeFixTier(signals);
|
|
354
|
+
const residualCheck = checkResidualHonesty(residual);
|
|
355
|
+
const evidenceCheck = requireCitedEvidence(verdict, evidence);
|
|
356
|
+
|
|
357
|
+
const violations = [...residualCheck.violations, ...evidenceCheck.violations];
|
|
358
|
+
let ok = residualCheck.ok && evidenceCheck.ok;
|
|
359
|
+
|
|
360
|
+
const residualEmpty = typeof residual !== 'string' || residual.trim() === '';
|
|
361
|
+
if (tier === 'FULL' && !residualEmpty) {
|
|
362
|
+
violations.push('FULL tier cannot carry a residual');
|
|
363
|
+
ok = false;
|
|
364
|
+
}
|
|
365
|
+
if (tier !== 'FULL' && residualEmpty) {
|
|
366
|
+
violations.push('non-FULL tier must document a residual');
|
|
367
|
+
ok = false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return { ok, tier, violations };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const _internals = Object.freeze({ BANNED_RESIDUAL_PHRASES, CITATION_RE, FP_VERDICTS });
|
|
374
|
+
|
|
375
|
+
;// CONCATENATED MODULE: ./src/posture/fix-verify.js
|
|
376
|
+
// Closed-loop /fix verification (Sentinel-parity FR-L4-4, FR-L4-5).
|
|
377
|
+
//
|
|
378
|
+
// Given a candidate patch (the new file content + the finding stableId being
|
|
379
|
+
// fixed), verify it:
|
|
380
|
+
//
|
|
381
|
+
// 1. The original finding's stableId no longer fires on the patched file.
|
|
382
|
+
// 2. No new findings at severity ≥ medium were introduced by the patch.
|
|
383
|
+
// 3. The project's existing linter (when present) passes on the patched file.
|
|
384
|
+
//
|
|
385
|
+
// If any of those fail, the caller is expected to NOT apply the patch and
|
|
386
|
+
// instead surface a "fix plan" — a numbered list of steps the engineer can
|
|
387
|
+
// follow — rather than dump a broken patch on the user.
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
396
|
+
|
|
397
|
+
// Run a focused re-scan over just the patched file(s) using the in-memory
|
|
398
|
+
// engine. No filesystem write needed — we hand the new content in via the
|
|
399
|
+
// fileContents map.
|
|
400
|
+
async function verifyPatch({
|
|
401
|
+
scanRoot,
|
|
402
|
+
originalFindingStableId,
|
|
403
|
+
files, // { [relPath]: newContent }
|
|
404
|
+
depFileContents = {},
|
|
405
|
+
} = {}) {
|
|
406
|
+
if (!files || typeof files !== 'object') return { ok: false, reason: 'no-files-provided' };
|
|
407
|
+
const fileContents = { ...files };
|
|
408
|
+
let scan;
|
|
409
|
+
try {
|
|
410
|
+
scan = await (0,engine/* runFullScan */.wW)({ fileContents, depFileContents, scanRoot }, () => {});
|
|
411
|
+
} catch (e) {
|
|
412
|
+
return { ok: false, reason: 'rescan-failed', error: e.message };
|
|
413
|
+
}
|
|
414
|
+
const findings = (scan && scan.findings) || [];
|
|
415
|
+
const stillHasOriginal = !!originalFindingStableId &&
|
|
416
|
+
findings.some(f => f.stableId === originalFindingStableId);
|
|
417
|
+
if (stillHasOriginal) {
|
|
418
|
+
return { ok: false, reason: 'original-finding-still-present', stableId: originalFindingStableId };
|
|
419
|
+
}
|
|
420
|
+
const introducedHighOrAbove = findings.filter(f =>
|
|
421
|
+
(SEVERITY_RANK[f.severity] ?? 9) <= SEVERITY_RANK.medium);
|
|
422
|
+
// Don't count findings on lines outside the patched files — but our
|
|
423
|
+
// fileContents map IS the patched files, so every finding is in-scope.
|
|
424
|
+
return {
|
|
425
|
+
ok: introducedHighOrAbove.length === 0,
|
|
426
|
+
reason: introducedHighOrAbove.length === 0 ? 'verified' : 'introduced-new-findings',
|
|
427
|
+
introduced: introducedHighOrAbove.map(f => ({
|
|
428
|
+
vuln: f.vuln, file: f.file, line: f.line, severity: f.severity,
|
|
429
|
+
stableId: f.stableId,
|
|
430
|
+
})),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Detect which linter the project uses and run it on the patched files.
|
|
435
|
+
// Returns { ok, runner, output } or { ok: true, runner: 'none' } when no
|
|
436
|
+
// linter is configured (silent pass).
|
|
437
|
+
function runProjectLinter(scanRoot, filePaths) {
|
|
438
|
+
if (!scanRoot || !Array.isArray(filePaths) || filePaths.length === 0) {
|
|
439
|
+
return { ok: true, runner: 'none' };
|
|
440
|
+
}
|
|
441
|
+
const has = (p) => { try { return external_node_fs_.existsSync(external_node_path_.join(scanRoot, p)); } catch { return false; } };
|
|
442
|
+
// Pick the linter by config file present in the repo root.
|
|
443
|
+
const jsFiles = filePaths.filter(f => /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(f));
|
|
444
|
+
const pyFiles = filePaths.filter(f => /\.py$/i.test(f));
|
|
445
|
+
const goFiles = filePaths.filter(f => /\.go$/i.test(f));
|
|
446
|
+
const javaFiles = filePaths.filter(f => /\.java$/i.test(f));
|
|
447
|
+
|
|
448
|
+
if (jsFiles.length && (has('.eslintrc') || has('.eslintrc.json') || has('.eslintrc.js') || has('eslint.config.js') || has('eslint.config.mjs'))) {
|
|
449
|
+
return runLinter(scanRoot, 'eslint', ['--no-error-on-unmatched-pattern', ...jsFiles]);
|
|
450
|
+
}
|
|
451
|
+
if (pyFiles.length && (has('pyproject.toml') || has('ruff.toml') || has('.ruff.toml'))) {
|
|
452
|
+
return runLinter(scanRoot, 'ruff', ['check', ...pyFiles]);
|
|
453
|
+
}
|
|
454
|
+
if (pyFiles.length && has('.flake8')) {
|
|
455
|
+
return runLinter(scanRoot, 'flake8', pyFiles);
|
|
456
|
+
}
|
|
457
|
+
if (goFiles.length && (has('.golangci.yml') || has('.golangci.yaml'))) {
|
|
458
|
+
return runLinter(scanRoot, 'golangci-lint', ['run', ...goFiles]);
|
|
459
|
+
}
|
|
460
|
+
if (javaFiles.length && has('checkstyle.xml')) {
|
|
461
|
+
return runLinter(scanRoot, 'checkstyle', ['-c', 'checkstyle.xml', ...javaFiles]);
|
|
462
|
+
}
|
|
463
|
+
return { ok: true, runner: 'none' };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function runLinter(cwd, cmd, args) {
|
|
467
|
+
let r;
|
|
468
|
+
try {
|
|
469
|
+
r = (0,external_node_child_process_.spawnSync)(cmd, args, { cwd, encoding: 'utf8', timeout: 60_000 });
|
|
470
|
+
} catch (e) {
|
|
471
|
+
return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing', error: e.message };
|
|
472
|
+
}
|
|
473
|
+
if (r.error && r.error.code === 'ENOENT') {
|
|
474
|
+
return { ok: true, runner: cmd, skipped: true, reason: 'binary-missing' };
|
|
475
|
+
}
|
|
476
|
+
if (r.status === null) {
|
|
477
|
+
return { ok: false, runner: cmd, reason: 'timed-out', output: (r.stderr || r.stdout || '').slice(-2000) };
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
ok: r.status === 0,
|
|
481
|
+
runner: cmd,
|
|
482
|
+
exitCode: r.status,
|
|
483
|
+
output: ((r.stderr || '') + (r.stdout || '')).slice(-2000),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Top-level verify: re-scan + lint. Returns the combined verdict + a
|
|
488
|
+
// human-readable summary string suitable for surfacing to the user.
|
|
489
|
+
// Addition #7 — deterministic honesty gates on fix output. When the caller
|
|
490
|
+
// supplies `fixMeta` ({ residual, verdict, evidence, signals }) — e.g. the
|
|
491
|
+
// security-fixer agent's residual-risk text + completeness signals — the fix's
|
|
492
|
+
// claims are checked mechanically (no hand-wave residual prose, a cited
|
|
493
|
+
// file:line for any FP/safe verdict, and a FULL/MITIGATION/WORKAROUND tier). A
|
|
494
|
+
// dishonest or over-claiming fix fails the gate. When `fixMeta` is absent
|
|
495
|
+
// (the deterministic MCP write path, which has no claims to check) the honesty
|
|
496
|
+
// gate is skipped and behavior is unchanged.
|
|
497
|
+
async function verifyFix({
|
|
498
|
+
scanRoot,
|
|
499
|
+
originalFindingStableId,
|
|
500
|
+
files,
|
|
501
|
+
depFileContents,
|
|
502
|
+
fixMeta,
|
|
503
|
+
} = {}) {
|
|
504
|
+
const rescan = await verifyPatch({ scanRoot, originalFindingStableId, files, depFileContents });
|
|
505
|
+
const lint = runProjectLinter(scanRoot, Object.keys(files || {}));
|
|
506
|
+
let honesty = null;
|
|
507
|
+
if (fixMeta && typeof fixMeta === 'object') {
|
|
508
|
+
try { honesty = gateFixOutput(fixMeta); } catch { honesty = null; }
|
|
509
|
+
}
|
|
510
|
+
const ok = rescan.ok && (lint.ok || lint.skipped) && (honesty ? honesty.ok : true);
|
|
511
|
+
const summary = [
|
|
512
|
+
`re-scan: ${rescan.ok ? 'PASS' : 'FAIL — ' + rescan.reason}`,
|
|
513
|
+
`linter: ${lint.runner === 'none' ? 'skipped (no linter config)'
|
|
514
|
+
: lint.skipped ? `${lint.runner} not installed`
|
|
515
|
+
: lint.ok ? `${lint.runner} PASS`
|
|
516
|
+
: `${lint.runner} FAIL (exit ${lint.exitCode})`}`,
|
|
517
|
+
honesty ? `honesty: ${honesty.ok ? `PASS (${honesty.tier})` : 'FAIL — ' + honesty.violations.join('; ')}` : null,
|
|
518
|
+
].filter(Boolean).join('\n');
|
|
519
|
+
return { ok, rescan, lint, honesty, summary };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
/***/ })
|
|
524
|
+
|
|
525
|
+
};
|
package/dist/178.index.js
CHANGED
|
@@ -13,7 +13,7 @@ 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 _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(
|
|
16
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8215);
|
|
17
17
|
// Time-travel + counterfactual scanning (v0.68).
|
|
18
18
|
//
|
|
19
19
|
// Two new modes that exploit the pure-input shape of runFullScan:
|
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__(8215);
|
|
12
12
|
// Secrets submodule view of the engine — credential + entropy + TODO scanning.
|
|
13
13
|
|
|
14
14
|
|
package/dist/435.index.js
CHANGED
|
@@ -477,7 +477,7 @@ async function getRunScan() {
|
|
|
477
477
|
}
|
|
478
478
|
let _verifyFixCore;
|
|
479
479
|
async function getVerifyFixCore() {
|
|
480
|
-
if (!_verifyFixCore) _verifyFixCore = (await __webpack_require__.e(/* import() */
|
|
480
|
+
if (!_verifyFixCore) _verifyFixCore = (await __webpack_require__.e(/* import() */ 11).then(__webpack_require__.bind(__webpack_require__, 11))).verifyFix;
|
|
481
481
|
return _verifyFixCore;
|
|
482
482
|
}
|
|
483
483
|
|
|
@@ -993,12 +993,23 @@ const apply_fix = {
|
|
|
993
993
|
// Inline re-verify — the load-bearing gate. Must pass to write.
|
|
994
994
|
let verdict;
|
|
995
995
|
try {
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
996
|
+
const _files = Object.fromEntries(Object.entries(confinedAbs).map(([rel, v]) => [rel, v.content]));
|
|
997
|
+
if (process.env.AGENTIC_SECURITY_FIX_RUN_TESTS === '1') {
|
|
998
|
+
// Addition #7 — connect the closed-loop verifier: add the project test
|
|
999
|
+
// suite as a fourth verification leg (scan + lint + tests). Opt-in
|
|
1000
|
+
// because many repos have no runner and we must not fail-closed by
|
|
1001
|
+
// default. Normalized to the scan+lint verdict shape used below.
|
|
1002
|
+
const { verifyFixWithTests } = await __webpack_require__.e(/* import() */ 113).then(__webpack_require__.bind(__webpack_require__, 4113));
|
|
1003
|
+
const t = await verifyFixWithTests({ scanRoot: ctx.sessionRoot, originalFindingStableId: f.stableId, files: _files });
|
|
1004
|
+
verdict = { ok: t.ok, summary: t.summary, rescan: t.legs?.scan?.detail, lint: t.legs?.lint?.detail, tests: t.legs?.tests, testVerdict: t.verdict };
|
|
1005
|
+
} else {
|
|
1006
|
+
const verifyFixCore = await getVerifyFixCore();
|
|
1007
|
+
verdict = await verifyFixCore({
|
|
1008
|
+
scanRoot: ctx.sessionRoot,
|
|
1009
|
+
originalFindingStableId: f.stableId,
|
|
1010
|
+
files: _files,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1002
1013
|
} catch (e) {
|
|
1003
1014
|
return { _meta: META, applied: false, reason: `patch verification failed: ${e.message}` };
|
|
1004
1015
|
}
|
package/dist/637.index.js
CHANGED
|
@@ -11,7 +11,7 @@ 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 _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
|
|
14
|
+
/* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8215);
|
|
15
15
|
// Shadowscan / security-DELTA on PR (v0.72).
|
|
16
16
|
//
|
|
17
17
|
// Most SAST PR-comment integrations show absolute counts — "12 findings
|
package/dist/826.index.js
CHANGED
|
@@ -9,6 +9,7 @@ export const modules = {
|
|
|
9
9
|
/* harmony export */ renderPrComment: () => (/* binding */ renderPrComment)
|
|
10
10
|
/* harmony export */ });
|
|
11
11
|
/* unused harmony export _internal */
|
|
12
|
+
/* harmony import */ var _util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7097);
|
|
12
13
|
// Advisor-tone PR comment renderer (v0.72).
|
|
13
14
|
//
|
|
14
15
|
// Replaces the typical "12 findings detected, see SARIF" wall of text
|
|
@@ -36,6 +37,8 @@ export const modules = {
|
|
|
36
37
|
// route through an LLM for richer prose when AGENTIC_SECURITY_LLM_ENDPOINT
|
|
37
38
|
// is configured.
|
|
38
39
|
|
|
40
|
+
|
|
41
|
+
|
|
39
42
|
const SEVERITY_GLYPH = {
|
|
40
43
|
critical: '🟥',
|
|
41
44
|
high: '🟧',
|
|
@@ -147,7 +150,7 @@ function renderPrComment(delta, { repoName, prNumber, prTitle } = {}) {
|
|
|
147
150
|
const sev = SEVERITY_GLYPH[f.severity] || '⬜';
|
|
148
151
|
const route = _route(f);
|
|
149
152
|
const where = route ? `\`${route}\` (\`${f.file}:${f.line}\`)` : `\`${f.file}:${f.line}\``;
|
|
150
|
-
lines.push(`${sev} **${meta?.name || f.vuln}** — ${where}`);
|
|
153
|
+
lines.push(`${sev} **${meta?.name || (0,_util_untrusted_js__WEBPACK_IMPORTED_MODULE_0__/* .escapeMarkdown */ .FV)(f.vuln)}** — ${where}`);
|
|
151
154
|
if (meta) lines.push(` > ${meta.why}`);
|
|
152
155
|
if (f.remediation) {
|
|
153
156
|
const onelineFix = String(f.remediation).split('\n')[0].slice(0, 240);
|