@clear-capabilities/agentic-security-scanner 0.195.26 → 0.195.28
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 +27 -0
- package/bin/agentic-security.js +39 -7
- package/dist/396.index.js +936 -0
- package/dist/{60f58f89664e53fc2f96.json → 3eabd90d6d9c8c8dbbd9.json} +4 -3
- package/dist/5856.index.js +634 -0
- package/dist/7033.index.js +74 -14
- package/dist/7838.index.js +22 -0
- package/dist/9220.index.js +24 -1
- package/dist/agentic-security.mjs +9 -9
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -3
- package/src/dataflow/catalog.js +42 -1
- package/src/engine.js +90 -10
- package/src/language/CLAUDE.md +6 -0
- package/src/language/assurance.js +53 -2
- package/src/language/haskell-budget-state.js +85 -0
- package/src/language/haskell-coverage.js +230 -0
- package/src/language/haskell-project-models.js +107 -0
- package/src/language/nix-eval-isolation.js +74 -14
- package/src/llm-validator/bounded-context.js +87 -0
- package/src/llm-validator/claim-groups.js +105 -0
- package/src/llm-validator/disposition.js +264 -0
- package/src/llm-validator/index.js +52 -26
- package/src/pipeline/evidence-record.js +112 -0
- package/src/pipeline/scan-health.js +12 -1
- package/src/posture/CLAUDE.md +10 -2
- package/src/posture/artifact-registry.js +2 -0
- package/src/posture/custom-rules.js +7 -2
- package/src/posture/drift.js +19 -1
- package/src/posture/provenance/coordinator.js +6 -2
- package/src/posture/provenance/dag-walk.js +7 -3
- package/src/posture/provenance/origin-resolver.js +16 -2
- package/src/posture/provenance/providers/github.js +6 -3
- package/src/posture/provenance/providers/gitlab.js +5 -3
- package/src/posture/reachability-filter.js +3 -3
- package/src/posture/rule-compiler/cli.js +76 -21
- package/src/posture/rule-compiler/negative-corpus.js +39 -0
- package/src/posture/rule-compiler/promotion.js +21 -0
- package/src/posture/rule-compiler/regression.js +39 -9
- package/src/posture/rule-compiler/store.js +72 -6
- package/src/posture/suppressions.js +21 -0
- package/src/posture/watch-mode.js +22 -1
- package/src/report/disposition-view.js +90 -0
- package/src/report/index.js +47 -0
- package/src/runScan.js +60 -6
- package/src/sast/oidc-trust.js +74 -0
- package/src/sast/pipeline.js +14 -1
- package/src/sast/workflow-flow.js +272 -0
- package/src/sca/basis.js +88 -0
- package/src/scope/flags.js +55 -0
- package/src/scope/inventory.js +241 -0
package/dist/7033.index.js
CHANGED
|
@@ -1152,7 +1152,7 @@ const real = (p) => { try { return (0,external_node_fs_.realpathSync)(p); } catc
|
|
|
1152
1152
|
* The sandboxed command for a backend. Reads are an ALLOWLIST (the listed roots, system library paths and the
|
|
1153
1153
|
* evaluator); writes are an allowlist of one scratch directory; network and process creation are denied.
|
|
1154
1154
|
* @param {{backend:string, path?:string}} sb
|
|
1155
|
-
* @param {{execFile:string, args:string[], readOnly:string[], readFiles?:string[], writable:string[], cwd:string}} spec
|
|
1155
|
+
* @param {{execFile:string, args:string[], readOnly:string[], readFiles?:string[], writable:string[], cwd:string, env?:Record<string,string>}} spec
|
|
1156
1156
|
*/
|
|
1157
1157
|
function sandboxCommand(sb, spec) {
|
|
1158
1158
|
const ro = [...new Set(spec.readOnly.map(real))];
|
|
@@ -1175,34 +1175,94 @@ function sandboxCommand(sb, spec) {
|
|
|
1175
1175
|
for (const p of ro) a.push('--ro-bind', p, p);
|
|
1176
1176
|
for (const p of files) a.push('--ro-bind', p, p);
|
|
1177
1177
|
for (const p of rw) a.push('--bind', p, p);
|
|
1178
|
+
// --clearenv empties the environment bwrap itself was given, so the clean one is set explicitly inside.
|
|
1179
|
+
for (const [k, v] of Object.entries(spec.env || {})) a.push('--setenv', k, String(v));
|
|
1180
|
+
// Namespaces do not stop process creation; the seccomp filter does. Before it, a sandboxed process could spawn anything the
|
|
1181
|
+
// read-only binds expose (found the first time a CI runner had bwrap installed). On an architecture with no filter there is
|
|
1182
|
+
// none, and the probe then reports the spawn and refuses to evaluate.
|
|
1183
|
+
const seccomp = seccompNoProcesses(sb.arch || process.arch);
|
|
1184
|
+
if (seccomp) a.push('--seccomp', '3');
|
|
1178
1185
|
a.push('--chdir', spec.cwd, exec, ...spec.args);
|
|
1179
|
-
return { file: sb.path, args: a, profile: null };
|
|
1186
|
+
return { file: sb.path, args: a, profile: null, fd3: seccomp };
|
|
1180
1187
|
}
|
|
1181
1188
|
return null;
|
|
1182
1189
|
}
|
|
1183
1190
|
|
|
1191
|
+
// ── seccomp: threads yes, processes no ──────────────────────────────────────
|
|
1192
|
+
// A classic-BPF seccomp program, as `bwrap --seccomp FD` reads it (struct sock_filter, native little-endian). It denies fork and vfork,
|
|
1193
|
+
// denies clone unless CLONE_THREAD is set (so pthread_create works and posix_spawn, fork and vfork through clone do not), answers clone3
|
|
1194
|
+
// with ENOSYS (its flags are behind a pointer a filter cannot read; glibc then falls back to clone), denies x32 syscalls, and kills a
|
|
1195
|
+
// process that switches to another syscall ABI.
|
|
1196
|
+
const SECCOMP_ARCH = {
|
|
1197
|
+
x64: { audit: 0xc000003e, clone: 56, clone3: 435, deny: [57, 58], x32: true },
|
|
1198
|
+
arm64: { audit: 0xc00000b7, clone: 220, clone3: 435, deny: [], x32: false },
|
|
1199
|
+
};
|
|
1200
|
+
const CLONE_THREAD = 0x10000;
|
|
1201
|
+
function seccompNoProcesses(arch) {
|
|
1202
|
+
const A = SECCOMP_ARCH[arch];
|
|
1203
|
+
if (!A) return null;
|
|
1204
|
+
const LD = 0x20, JEQ = 0x15, JGE = 0x35, JSET = 0x45, RET = 0x06;
|
|
1205
|
+
const ALLOW = 0x7fff0000, EPERM = 0x00050000 | 1, ENOSYS = 0x00050000 | 38, KILL = 0x80000000;
|
|
1206
|
+
// Instructions with symbolic jump targets, resolved to relative offsets below.
|
|
1207
|
+
const prog = [
|
|
1208
|
+
[LD, 0, 0, 4], // arch
|
|
1209
|
+
[JEQ, null, 'kill', A.audit],
|
|
1210
|
+
[LD, 0, 0, 0], // syscall number
|
|
1211
|
+
...(A.x32 ? [[JGE, 'eperm', null, 0x40000000]] : []),
|
|
1212
|
+
[JEQ, 'enosys', null, A.clone3],
|
|
1213
|
+
...A.deny.map((nr) => [JEQ, 'eperm', null, nr]),
|
|
1214
|
+
[JEQ, null, 'allow', A.clone],
|
|
1215
|
+
[LD, 0, 0, 16], // clone flags, args[0] low word
|
|
1216
|
+
[JSET, 'allow', 'eperm', CLONE_THREAD],
|
|
1217
|
+
['allow', RET, ALLOW], ['eperm', RET, EPERM], ['enosys', RET, ENOSYS], ['kill', RET, KILL],
|
|
1218
|
+
];
|
|
1219
|
+
const at = {};
|
|
1220
|
+
prog.forEach((ins, i) => { if (typeof ins[0] === 'string') at[ins[0]] = i; });
|
|
1221
|
+
const buf = Buffer.alloc(prog.length * 8);
|
|
1222
|
+
prog.forEach((ins, i) => {
|
|
1223
|
+
const [code, jt, jf, k] = typeof ins[0] === 'string' ? [ins[1], 0, 0, ins[2]] : ins;
|
|
1224
|
+
const rel = (t) => (t === null || t === 0 ? 0 : at[t] - i - 1);
|
|
1225
|
+
buf.writeUInt16LE(code, i * 8); buf.writeUInt8(rel(jt), i * 8 + 2); buf.writeUInt8(rel(jf), i * 8 + 3); buf.writeUInt32LE(k >>> 0, i * 8 + 4);
|
|
1226
|
+
});
|
|
1227
|
+
return buf;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1184
1230
|
// ── process supervisor ───────────────────────────────────────────────────────
|
|
1185
|
-
|
|
1186
|
-
|
|
1231
|
+
// The memory of the whole process tree under `root`, in MB. Not the process group: bwrap's --new-session puts the sandboxed process in
|
|
1232
|
+
// a session and group of its own, so a group sum never saw the evaluator on Linux.
|
|
1233
|
+
function rssOfTree(root) {
|
|
1234
|
+
const r = (0,external_node_child_process_.spawnSync)('/bin/ps', ['-A', '-o', 'pid=,ppid=,rss='], { encoding: 'utf8', timeout: 2000 });
|
|
1187
1235
|
if (r.status !== 0) return 0;
|
|
1188
|
-
|
|
1236
|
+
const kids = new Map(); const rss = new Map();
|
|
1237
|
+
for (const line of r.stdout.split('\n')) {
|
|
1238
|
+
const [pid, ppid, kb] = line.trim().split(/\s+/).map((x) => parseInt(x, 10));
|
|
1239
|
+
if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue;
|
|
1240
|
+
rss.set(pid, Number.isFinite(kb) ? kb : 0);
|
|
1241
|
+
if (!kids.has(ppid)) kids.set(ppid, []);
|
|
1242
|
+
kids.get(ppid).push(pid);
|
|
1243
|
+
}
|
|
1244
|
+
let total = 0; const seen = new Set(); const stack = [root];
|
|
1245
|
+
while (stack.length) { const p = stack.pop(); if (seen.has(p)) continue; seen.add(p); total += rss.get(p) || 0; stack.push(...(kids.get(p) || [])); }
|
|
1246
|
+
return total / 1024;
|
|
1189
1247
|
}
|
|
1190
1248
|
function killGroup(pid, sig) { try { process.kill(-pid, sig); } catch { try { process.kill(pid, sig); } catch { /* gone */ } } }
|
|
1191
1249
|
|
|
1192
1250
|
/** Run a command to completion under a deadline, an output cap and a memory ceiling. Never throws. */
|
|
1193
|
-
function supervise(file, args, { env, cwd, deadlineMs, maxOutputBytes, maxRssMb, killGraceMs = 1500 }) {
|
|
1251
|
+
function supervise(file, args, { env, cwd, deadlineMs, maxOutputBytes, maxRssMb, killGraceMs = 1500, fd3 = null }) {
|
|
1194
1252
|
return new Promise((resolveP) => {
|
|
1195
1253
|
const t0 = Date.now();
|
|
1196
1254
|
let child;
|
|
1197
|
-
try { child = (0,external_node_child_process_.spawn)(file, args, { env, cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'] }); }
|
|
1255
|
+
try { child = (0,external_node_child_process_.spawn)(file, args, { env, cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe', ...(fd3 ? ['pipe'] : [])] }); }
|
|
1198
1256
|
catch (e) { return resolveP({ status: 'failed', reason: `could not start: ${e.message}`, stdout: '', stderr: '', exitCode: null, signal: null, ms: 0 }); }
|
|
1257
|
+
// fd 3 carries the seccomp program to bwrap; a child that exits before reading it closes the pipe, which is not an error here.
|
|
1258
|
+
if (fd3) { child.stdio[3].on('error', () => {}); child.stdio[3].end(fd3); }
|
|
1199
1259
|
let out = ''; let err = ''; let bytes = 0; let state = null; let done = false;
|
|
1200
1260
|
const finish = (extra = {}) => { if (done) return; done = true; clearInterval(poll); clearTimeout(dl); resolveP({ stdout: out, stderr: err.slice(0, 4000), ms: Date.now() - t0, ...extra }); };
|
|
1201
1261
|
const stop = (why) => { if (state) return; state = why; killGroup(child.pid, 'SIGTERM'); setTimeout(() => killGroup(child.pid, 'SIGKILL'), killGraceMs).unref(); };
|
|
1202
1262
|
child.stdout.on('data', (d) => { bytes += d.length; if (bytes > maxOutputBytes) { stop('output-limit'); return; } out += d.toString('utf8'); });
|
|
1203
1263
|
child.stderr.on('data', (d) => { if (err.length < 8000) err += d.toString('utf8'); });
|
|
1204
1264
|
const dl = setTimeout(() => stop('timed-out'), deadlineMs);
|
|
1205
|
-
const poll = setInterval(() => { if (!state && maxRssMb &&
|
|
1265
|
+
const poll = setInterval(() => { if (!state && maxRssMb && rssOfTree(child.pid) > maxRssMb) stop('resource-limit'); }, 250);
|
|
1206
1266
|
child.on('error', (e) => finish({ status: 'failed', reason: e.message, exitCode: null, signal: null }));
|
|
1207
1267
|
child.on('close', (code, signal) => {
|
|
1208
1268
|
killGroup(child.pid, 'SIGKILL'); // reap any descendants
|
|
@@ -1280,8 +1340,8 @@ async function probeSandbox(sb, opts = {}) {
|
|
|
1280
1340
|
const port = tcp.address().port;
|
|
1281
1341
|
try {
|
|
1282
1342
|
const exe = opts.nodePath || process.execPath;
|
|
1283
|
-
const cmd = sandboxCommand(sb, { execFile: exe, args: [script, (0,external_node_path_.join)(allowed, 'a.txt'), (0,external_node_path_.join)(outside, 'secret.txt'), (0,external_node_path_.join)(outside, 'w.txt'), (0,external_node_path_.join)(scratch, 'w.txt'), String(port), sockPath], readOnly: [allowed, (0,external_node_path_.dirname)((0,external_node_path_.dirname)(real(exe)))], readFiles: [script], writable: [scratch], cwd: scratch });
|
|
1284
|
-
const r = await supervise(cmd.file, cmd.args, { env: cleanEnv(scratch), cwd: scratch, deadlineMs: limits.probeDeadlineMs, maxOutputBytes: 64 * 1024, maxRssMb: limits.maxRssMb, killGraceMs: limits.killGraceMs });
|
|
1343
|
+
const cmd = sandboxCommand(sb, { env: cleanEnv(scratch), execFile: exe, args: [script, (0,external_node_path_.join)(allowed, 'a.txt'), (0,external_node_path_.join)(outside, 'secret.txt'), (0,external_node_path_.join)(outside, 'w.txt'), (0,external_node_path_.join)(scratch, 'w.txt'), String(port), sockPath], readOnly: [allowed, (0,external_node_path_.dirname)((0,external_node_path_.dirname)(real(exe)))], readFiles: [script], writable: [scratch], cwd: scratch });
|
|
1344
|
+
const r = await supervise(cmd.file, cmd.args, { fd3: cmd.fd3, env: cleanEnv(scratch), cwd: scratch, deadlineMs: limits.probeDeadlineMs, maxOutputBytes: 64 * 1024, maxRssMb: limits.maxRssMb, killGraceMs: limits.killGraceMs });
|
|
1285
1345
|
let attempts = null;
|
|
1286
1346
|
try { attempts = JSON.parse(r.stdout.trim()); } catch { /* unparseable: not verified */ }
|
|
1287
1347
|
if (!attempts) return { verified: false, backend: sb.backend, reason: `the probe did not complete inside the sandbox (${r.reason || r.stderr.slice(0, 120) || 'no output'})`, attempts: {} };
|
|
@@ -1304,8 +1364,8 @@ async function probeSandbox(sb, opts = {}) {
|
|
|
1304
1364
|
/** @param {{file:string, prefixArgs?:string[]}} nix the evaluator command (a stand-in in tests) */
|
|
1305
1365
|
async function detectEvaluator(nix, sb, ctx) {
|
|
1306
1366
|
const run = async (args) => {
|
|
1307
|
-
const cmd = sandboxCommand(sb, { execFile: nix.file, args: [...(nix.prefixArgs || []), ...args], readOnly: ctx.readOnly, readFiles: [...(ctx.readFiles || []), ...(nix.prefixArgs || []).filter((a) => !a.startsWith('-'))], writable: [ctx.work], cwd: ctx.work });
|
|
1308
|
-
return supervise(cmd.file, cmd.args, { env: cleanEnv(ctx.work), cwd: ctx.work, deadlineMs: 10_000, maxOutputBytes: 64 * 1024, maxRssMb: ctx.limits.maxRssMb, killGraceMs: ctx.limits.killGraceMs });
|
|
1367
|
+
const cmd = sandboxCommand(sb, { env: cleanEnv(ctx.work), execFile: nix.file, args: [...(nix.prefixArgs || []), ...args], readOnly: ctx.readOnly, readFiles: [...(ctx.readFiles || []), ...(nix.prefixArgs || []).filter((a) => !a.startsWith('-'))], writable: [ctx.work], cwd: ctx.work });
|
|
1368
|
+
return supervise(cmd.file, cmd.args, { fd3: cmd.fd3, env: cleanEnv(ctx.work), cwd: ctx.work, deadlineMs: 10_000, maxOutputBytes: 64 * 1024, maxRssMb: ctx.limits.maxRssMb, killGraceMs: ctx.limits.killGraceMs });
|
|
1309
1369
|
};
|
|
1310
1370
|
const v = await run(['--version']);
|
|
1311
1371
|
const ver = (/(\d+\.\d+(?:\.\d+)?)/.exec(v.stdout) || [])[1] || null;
|
|
@@ -1359,8 +1419,8 @@ async function runIsolatedEval(o) {
|
|
|
1359
1419
|
const feat = await detectEvaluator(o.nix, sb, ctx);
|
|
1360
1420
|
if (!feat.ok) return stopBefore('unsupported', `${feat.reason}; the project's Nix code was NOT evaluated`, { sandbox: sb, probe: { verified: true }, evaluator: feat });
|
|
1361
1421
|
const args = [...(o.nix.prefixArgs || []), ...buildEvalArgs(target)];
|
|
1362
|
-
const cmd = sandboxCommand(sb, { execFile: o.nix.file, args, readOnly: ctx.readOnly, readFiles: nixFiles, writable: [ctx.work], cwd: ctx.work });
|
|
1363
|
-
const r = await supervise(cmd.file, cmd.args, { env: cleanEnv(ctx.work, o.env || {}), cwd: ctx.work, deadlineMs: limits.deadlineMs, maxOutputBytes: limits.maxOutputBytes, maxRssMb: limits.maxRssMb, killGraceMs: limits.killGraceMs });
|
|
1422
|
+
const cmd = sandboxCommand(sb, { env: cleanEnv(ctx.work, o.env || {}), execFile: o.nix.file, args, readOnly: ctx.readOnly, readFiles: nixFiles, writable: [ctx.work], cwd: ctx.work });
|
|
1423
|
+
const r = await supervise(cmd.file, cmd.args, { fd3: cmd.fd3, env: cleanEnv(ctx.work, o.env || {}), cwd: ctx.work, deadlineMs: limits.deadlineMs, maxOutputBytes: limits.maxOutputBytes, maxRssMb: limits.maxRssMb, killGraceMs: limits.killGraceMs });
|
|
1364
1424
|
const base = { ...keep, projectCodeEvaluated: true, sandbox: { backend: sb.backend }, evaluator: { version: feat.version }, ms: r.ms, target: { attribute: target.attribute, system: target.system, installable: target.installable } };
|
|
1365
1425
|
if (r.status !== 'ok') return { ...base, status: r.status === 'failed' ? 'failed' : r.status, reason: r.reason || r.stderr.slice(0, 200) };
|
|
1366
1426
|
let data;
|
package/dist/7838.index.js
CHANGED
|
@@ -396,6 +396,7 @@ function mutatorCoverage(code) {
|
|
|
396
396
|
/* harmony export */ r_: () => (/* binding */ checkLeakage),
|
|
397
397
|
/* harmony export */ z: () => (/* binding */ gatePromotion)
|
|
398
398
|
/* harmony export */ });
|
|
399
|
+
/* unused harmony export dependentExamples */
|
|
399
400
|
/* harmony import */ var _corpus_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(43556);
|
|
400
401
|
// Promotion gate (PRD 3 "Promotion Gate"): precision/recall thresholds, zero protected-negative failures, zero
|
|
401
402
|
// benchmark leakage. A candidate rule does NOT automatically become a production rule — `evaluateRule` only
|
|
@@ -468,9 +469,30 @@ function gatePromotion({ ir, corpus, leakageCheck, thresholds } = {}) {
|
|
|
468
469
|
else if (ind.detected < ind.positives) reasons.push(`misses ${ind.positives - ind.detected} of ${ind.positives} independent vulnerable example(s)`);
|
|
469
470
|
if (ind.negatives < t.minIndependentClean) reasons.push(`${ind.negatives} independent safe example(s) (min ${t.minIndependentClean}): supply independentCleanCode`);
|
|
470
471
|
else if (ind.falseAlarms > 0) reasons.push(`fires on ${ind.falseAlarms} of ${ind.negatives} independent safe example(s)`);
|
|
472
|
+
for (const dup of dependentExamples(corpus)) reasons.push(`${dup.label} is not independent: it is the same code as ${dup.sameAs} (the source finding, its patch or a generated mutation), so it grades nothing the source did not`);
|
|
471
473
|
return { pass: reasons.length === 0, reasons, evaluation: evalResult, independent: ind };
|
|
472
474
|
}
|
|
473
475
|
|
|
476
|
+
// GA-07 §2 (0.195.27): an "independent" example that is the original snippet, its patch, or one of the compiler's own mutations
|
|
477
|
+
// (whitespace aside) is circular grading, which the independent-example rule exists to prevent. Compared after collapsing
|
|
478
|
+
// whitespace so a re-indented copy is caught; a genuinely different example with the same shape is not (that is what a rule is for).
|
|
479
|
+
const squash = (s) => String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
480
|
+
function dependentExamples(corpus) {
|
|
481
|
+
const known = new Map();
|
|
482
|
+
for (const item of [...(corpus?.positives ?? []), ...(corpus?.negatives ?? [])]) {
|
|
483
|
+
if (item.label.startsWith('independent-')) continue;
|
|
484
|
+
const k = squash(item.code);
|
|
485
|
+
if (k && !known.has(k)) known.set(k, item.label);
|
|
486
|
+
}
|
|
487
|
+
const out = [];
|
|
488
|
+
for (const item of [...(corpus?.positives ?? []), ...(corpus?.negatives ?? [])]) {
|
|
489
|
+
if (!item.label.startsWith('independent-')) continue;
|
|
490
|
+
const sameAs = known.get(squash(item.code));
|
|
491
|
+
if (sameAs) out.push({ label: item.label, sameAs });
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
|
|
474
496
|
|
|
475
497
|
/***/ }),
|
|
476
498
|
|
package/dist/9220.index.js
CHANGED
|
@@ -7,6 +7,7 @@ export const modules = {
|
|
|
7
7
|
|
|
8
8
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
9
|
/* harmony export */ computeDelta: () => (/* binding */ computeDelta),
|
|
10
|
+
/* harmony export */ createScopeFilter: () => (/* binding */ createScopeFilter),
|
|
10
11
|
/* harmony export */ persistStatus: () => (/* binding */ persistStatus),
|
|
11
12
|
/* harmony export */ renderStatusLine: () => (/* binding */ renderStatusLine),
|
|
12
13
|
/* harmony export */ watchProject: () => (/* binding */ watchProject)
|
|
@@ -17,6 +18,7 @@ export const modules = {
|
|
|
17
18
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
|
|
18
19
|
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(31174);
|
|
19
20
|
/* harmony import */ var _language_discovery_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(30951);
|
|
21
|
+
/* harmony import */ var _scope_inventory_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(58585);
|
|
20
22
|
// Watch mode — continuous incremental scan as the developer edits.
|
|
21
23
|
//
|
|
22
24
|
// Spawns a long-running scan watcher that:
|
|
@@ -44,6 +46,7 @@ export const modules = {
|
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
|
|
47
50
|
const STATUS_MD = 'watch-status.md';
|
|
48
51
|
const STATUS_JSON = 'watch-status.json';
|
|
49
52
|
const DEBOUNCE_MS = 350;
|
|
@@ -52,6 +55,25 @@ const MAX_BURST = 50; // ignore beyond this # of rapid events
|
|
|
52
55
|
const SCAN_EXT_RE = /\.(?:[jt]sx?|mjs|cjs|py|java|kt|go|rb|php|cs|c|cc|cpp|h|hpp|rs|sol|vy|swift|dart|toml|yml|yaml|json|tf|tfvars|bicep)$/i;
|
|
53
56
|
const IGNORE_DIR_RE = /(?:^|\/)(?:\.git|node_modules|\.bench-cache|dist|build|\.next|coverage|\.agentic-security)(?:$|\/)/;
|
|
54
57
|
|
|
58
|
+
// Under Git scope (PRD FR 01) the watcher asks the same inventory the scan uses, so a change to a Git-ignored file does not
|
|
59
|
+
// trigger a rescan, and a tracked file under a directory the default list would skip (`dist/`, `build/`) still does. A change
|
|
60
|
+
// to an ignore source rebuilds the inventory. A path the inventory has not seen (a new file) rebuilds it, at most once a second.
|
|
61
|
+
const IGNORE_SOURCE = /(?:^|\/)\.gitignore$|^\.git\/info\/exclude$/;
|
|
62
|
+
function createScopeFilter(scanRoot, scope = {}, { build = _scope_inventory_js__WEBPACK_IMPORTED_MODULE_5__/* .buildScanInventory */ .mP, now = Date.now } = {}) {
|
|
63
|
+
let included = null;
|
|
64
|
+
let builtAt = -Infinity;
|
|
65
|
+
const rebuild = () => { included = new Set(build(scanRoot, scope).included); builtAt = now(); };
|
|
66
|
+
rebuild();
|
|
67
|
+
return (rel) => {
|
|
68
|
+
if (!rel || /^\.agentic-security(?:\/|$)/.test(rel)) return false;
|
|
69
|
+
if (IGNORE_SOURCE.test(rel)) { rebuild(); return true; }
|
|
70
|
+
if (/^\.git(?:\/|$)/.test(rel)) return false;
|
|
71
|
+
if (included.has(rel)) return true;
|
|
72
|
+
if (now() - builtAt >= 1000) rebuild();
|
|
73
|
+
return included.has(rel);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
function _isScanable(rel) {
|
|
56
78
|
if (!rel) return false;
|
|
57
79
|
// An explicit export is evidence even though it lives in a build directory.
|
|
@@ -181,6 +203,7 @@ function readStatus(scanRoot) {
|
|
|
181
203
|
async function watchProject(scanRoot, onChange, opts = {}) {
|
|
182
204
|
if (process.env.AGENTIC_SECURITY_NO_WATCH === '1') return { stop: async () => {}, _disabled: true };
|
|
183
205
|
const recursive = opts.recursive !== false;
|
|
206
|
+
const eligible = typeof opts.isEligible === 'function' ? opts.isEligible : _isScanable;
|
|
184
207
|
const ac = new AbortController();
|
|
185
208
|
let timer = null;
|
|
186
209
|
const pending = new Set();
|
|
@@ -202,7 +225,7 @@ async function watchProject(scanRoot, onChange, opts = {}) {
|
|
|
202
225
|
for await (const evt of node_fs_promises__WEBPACK_IMPORTED_MODULE_0__.watch(scanRoot, { recursive, signal: ac.signal })) {
|
|
203
226
|
if (stopped) break;
|
|
204
227
|
const rel = String(evt.filename || '').replace(/\\/g, '/');
|
|
205
|
-
if (!
|
|
228
|
+
if (!eligible(rel)) continue;
|
|
206
229
|
pending.add(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel));
|
|
207
230
|
if (timer) clearTimeout(timer);
|
|
208
231
|
timer = setTimeout(flush, DEBOUNCE_MS);
|