@clear-capabilities/agentic-security-scanner 0.172.0 → 0.173.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 +29 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1336.index.js +101 -101
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7405.index.js +23 -6
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +338 -48
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +28 -28
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +31 -6
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/adapters/haskell-hie.js +28 -1
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/canonical.js +25 -0
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +16 -10
- package/src/context-intelligence/config.js +4 -1
- package/src/context-intelligence/controls.js +108 -10
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/entrypoints.js +2 -2
- package/src/context-intelligence/evaluation/index.js +11 -4
- package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +10 -4
- package/src/context-intelligence/review.js +129 -16
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +272 -20
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/util/string-memo.js +38 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Supervised, resource-bounded analyzer execution (E6-05).
|
|
2
|
+
//
|
|
3
|
+
// A worker thread (analyzer-supervisor.js) can be terminated on a deadline, but it shares the process: it cannot be given a memory ceiling that
|
|
4
|
+
// does not take the scanner down with it, it cannot be CPU-limited, and anything it spawns outlives it. An analyzer that is expensive or runs
|
|
5
|
+
// code of uncertain behaviour belongs in its OWN PROCESS, and this module runs it there:
|
|
6
|
+
//
|
|
7
|
+
// * own process GROUP (detached), so a kill takes the analyzer and everything it forked; a test proves a forked grandchild dies too
|
|
8
|
+
// * wall-clock deadline, a resident-memory ceiling polled from outside (`maxRssMb`), a V8 heap ceiling (`maxHeapMb`, which turns exhaustion into an
|
|
9
|
+
// abort the supervisor classifies as `oom`), and an optional CPU-seconds ceiling (`cpuSeconds`, POSIX `ulimit -t`)
|
|
10
|
+
// * cancellation through an AbortSignal; a hard overall stop (`maxTotalMs`) so the run can never wait indefinitely
|
|
11
|
+
// * a BOUNDED queue: at most `concurrency` run at once, at most `maxQueue` wait, the rest are `rejected: queue_full`, never dropped silently
|
|
12
|
+
// * every task ends in exactly one terminal status. A failure is a result, not an exception: the other analyzers' results are kept
|
|
13
|
+
//
|
|
14
|
+
// `summarizeSupervised` turns the results into what the rest of the pipeline already understands: merged findings from the analyzers that
|
|
15
|
+
// completed, an `analyzerCoverage`-shaped object for computeScanHealth (so scan health degrades to `partial` and a strict assurance mode fails), and
|
|
16
|
+
// the exact list of analyzers that did not finish and why.
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const CHILD = path.join(HERE, 'supervised-child.js');
|
|
24
|
+
export const TERMINAL_STATUSES = Object.freeze(['completed', 'failed', 'timed_out', 'oom', 'rss_limit', 'cpu_limit', 'cancelled', 'rejected', 'overall_deadline']);
|
|
25
|
+
const DEFAULTS = Object.freeze({ concurrency: 2, maxQueue: 64, timeoutMs: 60_000, maxHeapMb: 1024, maxRssMb: 2048, cpuSeconds: 0, maxResultBytes: 8 * 1024 * 1024, maxTotalMs: 30 * 60_000, rssPollMs: 200 });
|
|
26
|
+
|
|
27
|
+
const killGroup = (pid) => { try { process.kill(-pid, 'SIGKILL'); } catch { try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ } } };
|
|
28
|
+
const rssMb = (pid) => { try { return parseInt(execFileSync('ps', ['-o', 'rss=', '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 }).trim(), 10) / 1024; } catch { return 0; } };
|
|
29
|
+
|
|
30
|
+
function runOne(task, o, state) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const t0 = Date.now();
|
|
33
|
+
const lim = { timeoutMs: task.timeoutMs ?? o.timeoutMs, maxHeapMb: task.maxHeapMb ?? o.maxHeapMb, maxRssMb: task.maxRssMb ?? o.maxRssMb, cpuSeconds: task.cpuSeconds ?? o.cpuSeconds };
|
|
34
|
+
// limits become command-line text, so each must be a plain positive number before it goes anywhere near a command line
|
|
35
|
+
for (const [k, v] of Object.entries(lim)) {
|
|
36
|
+
if (!(typeof v === 'number' && Number.isFinite(v) && v >= 0)) return resolve({ id: task.id, status: 'failed', ok: false, error: `invalid limit ${k}: must be a non-negative number`, durationMs: 0 });
|
|
37
|
+
}
|
|
38
|
+
const node = [`--max-old-space-size=${Math.ceil(lim.maxHeapMb)}`, CHILD];
|
|
39
|
+
const posix = process.platform !== 'win32';
|
|
40
|
+
// the CPU ceiling is applied with `ulimit` in a shell that then `exec`s node. The values are passed as positional parameters and quoted by the
|
|
41
|
+
// shell itself, so nothing from the task is ever parsed as shell syntax
|
|
42
|
+
const [cmd, args] = posix && lim.cpuSeconds > 0 ? ['/bin/sh', ['-c', 'ulimit -t "$1"; shift; exec "$@"', 'sh', String(Math.ceil(lim.cpuSeconds)), process.execPath, ...node]] : [process.execPath, node];
|
|
43
|
+
const child = spawn(cmd, args, { detached: posix, stdio: ['ignore', 'ignore', 'pipe', 'ipc'], env: { ...process.env, AGENTIC_SECURITY_SUPERVISED_SPEC: JSON.stringify({ modulePath: task.modulePath, exportName: task.exportName, args: task.args || [] }) } });
|
|
44
|
+
const pid = child.pid; state.pids.add(pid);
|
|
45
|
+
let status = null; let msg = null; let stderr = ''; let settled = false;
|
|
46
|
+
const done = (s, extra = {}) => {
|
|
47
|
+
if (settled) return; settled = true;
|
|
48
|
+
clearTimeout(timer); clearInterval(poll); state.kills.delete(kill);
|
|
49
|
+
killGroup(pid); state.pids.delete(pid);
|
|
50
|
+
resolve({ id: task.id, status: s, ok: s === 'completed', durationMs: Date.now() - t0, pid, ...extra });
|
|
51
|
+
};
|
|
52
|
+
const kill = (why) => { if (!status) status = why; killGroup(pid); };
|
|
53
|
+
state.kills.add(kill);
|
|
54
|
+
const timer = setTimeout(() => kill('timed_out'), lim.timeoutMs);
|
|
55
|
+
const poll = setInterval(() => { if (!status && lim.maxRssMb > 0 && rssMb(pid) > lim.maxRssMb) kill('rss_limit'); }, o.rssPollMs);
|
|
56
|
+
child.stderr.on('data', (d) => { if (stderr.length < 4000) stderr += d.toString(); });
|
|
57
|
+
child.on('message', (m) => { msg = m; });
|
|
58
|
+
child.on('error', (e) => done('failed', { error: `could not start: ${e.message}` }));
|
|
59
|
+
child.on('close', (code, signal) => {
|
|
60
|
+
if (status) return done(status, { error: status === 'timed_out' ? `exceeded ${lim.timeoutMs} ms` : status === 'rss_limit' ? `exceeded ${lim.maxRssMb} MB resident` : status, exitCode: code, signal });
|
|
61
|
+
if (msg && msg.ok) {
|
|
62
|
+
const bytes = Buffer.byteLength(JSON.stringify(msg.result ?? null));
|
|
63
|
+
return bytes > o.maxResultBytes ? done('failed', { error: `result of ${bytes} bytes exceeds the ${o.maxResultBytes} byte cap`, exitCode: code, signal }) : done('completed', { result: msg.result, exitCode: code, signal });
|
|
64
|
+
}
|
|
65
|
+
if (msg && !msg.ok) return done('failed', { error: msg.error, exitCode: code, signal });
|
|
66
|
+
if (/heap out of memory|allocation failed|ERR_WORKER_OUT_OF_MEMORY/i.test(stderr) || signal === 'SIGABRT' || code === 134) return done('oom', { error: 'ran out of memory', exitCode: code, signal });
|
|
67
|
+
if (signal === 'SIGXCPU' || (lim.cpuSeconds > 0 && (code === 152 || signal === 'SIGKILL'))) return done('cpu_limit', { error: `exceeded ${lim.cpuSeconds} CPU seconds`, exitCode: code, signal });
|
|
68
|
+
return done('failed', { error: `exited without a result (code ${code}, signal ${signal})${stderr ? `: ${stderr.trim().slice(0, 300)}` : ''}`, exitCode: code, signal });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {{id:string, modulePath:string, exportName:string, args?:any[], timeoutMs?:number, maxHeapMb?:number, maxRssMb?:number, cpuSeconds?:number}[]} tasks
|
|
75
|
+
* @param {{concurrency?, maxQueue?, timeoutMs?, maxHeapMb?, maxRssMb?, cpuSeconds?, maxResultBytes?, maxTotalMs?, signal?:AbortSignal}} opts
|
|
76
|
+
* @returns {Promise<object[]>} one result per task, in task order, each with exactly one terminal status
|
|
77
|
+
*/
|
|
78
|
+
export async function runSupervised(tasks, opts = {}) {
|
|
79
|
+
const o = { ...DEFAULTS, ...opts };
|
|
80
|
+
const state = { pids: new Set(), kills: new Set() };
|
|
81
|
+
const results = new Array(tasks.length);
|
|
82
|
+
const queue = []; let running = 0; let stopped = null;
|
|
83
|
+
const stop = (why) => { if (stopped) return; stopped = why; for (const k of [...state.kills]) k(why); };
|
|
84
|
+
const onAbort = () => stop('cancelled');
|
|
85
|
+
if (o.signal) { if (o.signal.aborted) stopped = 'cancelled'; else o.signal.addEventListener('abort', onAbort, { once: true }); }
|
|
86
|
+
const overall = setTimeout(() => stop('overall_deadline'), o.maxTotalMs);
|
|
87
|
+
tasks.forEach((t, i) => {
|
|
88
|
+
if (queue.length >= o.maxQueue + o.concurrency) results[i] = { id: t.id, status: 'rejected', ok: false, error: `queue_full: more than ${o.maxQueue} waiting`, durationMs: 0 };
|
|
89
|
+
else queue.push(i);
|
|
90
|
+
});
|
|
91
|
+
await new Promise((resolve) => {
|
|
92
|
+
const pump = () => {
|
|
93
|
+
while (running < o.concurrency && queue.length) {
|
|
94
|
+
const i = queue.shift();
|
|
95
|
+
if (stopped) { results[i] = { id: tasks[i].id, status: stopped, ok: false, error: `not started: ${stopped}`, durationMs: 0 }; continue; }
|
|
96
|
+
running++;
|
|
97
|
+
runOne(tasks[i], o, state).then((r) => { results[i] = r; }, (e) => { results[i] = { id: tasks[i].id, status: 'failed', ok: false, error: String(e && e.message || e), durationMs: 0 }; }).finally(() => { running--; pump(); });
|
|
98
|
+
}
|
|
99
|
+
if (!running && !queue.length) resolve();
|
|
100
|
+
};
|
|
101
|
+
pump();
|
|
102
|
+
});
|
|
103
|
+
clearTimeout(overall); if (o.signal) o.signal.removeEventListener('abort', onAbort);
|
|
104
|
+
// a task killed by the overall stop or a cancel reports that, not 'failed'
|
|
105
|
+
return results.map((r) => (stopped && !r.ok && ['timed_out', 'failed'].includes(r.status) && r.error === stopped ? { ...r, status: stopped } : r));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What the rest of the pipeline needs from a supervised run.
|
|
110
|
+
* @returns {{findings:object[], completed:string[], incomplete:{id:string,status:string,error:string}[], analyzerCoverage:object, conditions:string[]}}
|
|
111
|
+
*/
|
|
112
|
+
export function summarizeSupervised(results) {
|
|
113
|
+
const findings = []; const completed = []; const incomplete = [];
|
|
114
|
+
for (const r of results) {
|
|
115
|
+
if (r.status === 'completed') { completed.push(r.id); if (Array.isArray(r.result)) findings.push(...r.result); else if (r.result && Array.isArray(r.result.findings)) findings.push(...r.result.findings); }
|
|
116
|
+
else incomplete.push({ id: r.id, status: r.status, error: r.error || r.status });
|
|
117
|
+
}
|
|
118
|
+
const n = (s) => incomplete.filter((x) => x.status === s).length;
|
|
119
|
+
const analyzerCoverage = { analyzers: results.length, completed: completed.length, failed: incomplete.length, timedOut: n('timed_out') + n('overall_deadline'), outOfMemory: n('oom') + n('rss_limit'), cancelled: n('cancelled'), rejected: n('rejected'), skippedByPolicy: 0 };
|
|
120
|
+
const conditions = incomplete.map((x) => `analyzer ${x.id} did not finish (${x.status}): ${x.error}`);
|
|
121
|
+
return { findings, completed, incomplete, analyzerCoverage, conditions };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Is a process still alive? (test and diagnostics helper for "no orphan") */
|
|
125
|
+
export function pidAlive(pid) { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The child half of supervised-analyzers.js (E6-05). Runs ONE exported function in its own process and reports the result over the IPC channel. It is
|
|
2
|
+
// started by the supervisor, never imported: the supervisor owns the limits, the deadline and the kill, so a child that hangs, exhausts memory or
|
|
3
|
+
// forks more processes can be stopped from outside without its cooperation.
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const spec = JSON.parse(process.env.AGENTIC_SECURITY_SUPERVISED_SPEC || '{}');
|
|
8
|
+
try {
|
|
9
|
+
const mod = await import(pathToFileURL(spec.modulePath).href);
|
|
10
|
+
if (typeof mod[spec.exportName] !== 'function') throw new Error(`${spec.exportName} is not an exported function of ${spec.modulePath}`);
|
|
11
|
+
const result = await mod[spec.exportName](...(spec.args || []));
|
|
12
|
+
process.send({ ok: true, result }, () => process.exit(0));
|
|
13
|
+
} catch (e) {
|
|
14
|
+
process.send({ ok: false, error: String((e && e.message) || e).slice(0, 2000) }, () => process.exit(1));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
main();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Scan telemetry (E6-01): where the time and memory of ONE scan went, and which stage failed, as structured aggregates inside the scan result.
|
|
2
|
+
//
|
|
3
|
+
// LOCAL ONLY. Nothing here sends anything anywhere: the snapshot is a field of the scan object the caller already holds. There is no endpoint, no
|
|
4
|
+
// identifier of the user or the machine, no file path and no source text; stage names are fixed strings and an error carries a class and a short
|
|
5
|
+
// message, never code.
|
|
6
|
+
//
|
|
7
|
+
// WHAT IT RECORDS
|
|
8
|
+
// stages wall time and call count per named stage (file read, detectors, deep analysis, lineage, each annotator, ...)
|
|
9
|
+
// detectors time and call count per detector, accumulated by runDetector
|
|
10
|
+
// memory resident-set and heap high-water marks sampled at stage boundaries (a sample, not a profiler: a spike between two samples is missed)
|
|
11
|
+
// counters named integer counts (files scanned, files skipped, cache hits and misses)
|
|
12
|
+
// errors attributed failures: the stage, a class, a short message and a count, from the existing error arrays
|
|
13
|
+
// overhead the time this module itself spent bookkeeping, so its cost is measured, not asserted
|
|
14
|
+
//
|
|
15
|
+
// DETERMINISM. Under --deterministic, every duration and memory figure is reported as 0 and the stage list is sorted by name, so two runs of the same
|
|
16
|
+
// input give byte-identical telemetry; the counts, which are properties of the input, are kept.
|
|
17
|
+
const MB = 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
export function createTelemetry({ deterministic = false } = {}) {
|
|
20
|
+
const stages = new Map(); const detectors = new Map(); const counters = new Map(); const errors = new Map();
|
|
21
|
+
const mem = { rssHighWaterBytes: 0, heapHighWaterBytes: 0, samples: 0 };
|
|
22
|
+
let selfNs = 0n; const startedNs = process.hrtime.bigint();
|
|
23
|
+
const own = (fn) => { const a = process.hrtime.bigint(); try { return fn(); } finally { selfNs += process.hrtime.bigint() - a; } };
|
|
24
|
+
const bump = (map, key, ms) => own(() => { const c = map.get(key) || { ms: 0, calls: 0 }; c.ms += ms; c.calls += 1; map.set(key, c); });
|
|
25
|
+
const sample = () => own(() => {
|
|
26
|
+
if (deterministic) { mem.samples++; return; }
|
|
27
|
+
const m = process.memoryUsage();
|
|
28
|
+
if (m.rss > mem.rssHighWaterBytes) mem.rssHighWaterBytes = m.rss;
|
|
29
|
+
if (m.heapUsed > mem.heapHighWaterBytes) mem.heapHighWaterBytes = m.heapUsed;
|
|
30
|
+
mem.samples++;
|
|
31
|
+
});
|
|
32
|
+
const api = {
|
|
33
|
+
/** Time a synchronous function as stage `name`. Errors propagate; the time is still recorded. */
|
|
34
|
+
time(name, fn) { const a = process.hrtime.bigint(); try { return fn(); } finally { bump(stages, name, Number(process.hrtime.bigint() - a) / 1e6); sample(); } },
|
|
35
|
+
/** Time an async function as stage `name`. */
|
|
36
|
+
async timeAsync(name, fn) { const a = process.hrtime.bigint(); try { return await fn(); } finally { bump(stages, name, Number(process.hrtime.bigint() - a) / 1e6); sample(); } },
|
|
37
|
+
/** Record a duration measured elsewhere. */
|
|
38
|
+
record(name, ms) { bump(stages, name, Number.isFinite(ms) ? ms : 0); sample(); },
|
|
39
|
+
detector(name, ms) { bump(detectors, name, ms); },
|
|
40
|
+
count(name, n = 1) { own(() => counters.set(name, (counters.get(name) || 0) + n)); },
|
|
41
|
+
set(name, n) { own(() => counters.set(name, n)); },
|
|
42
|
+
sample,
|
|
43
|
+
/** Attribute a failure. `kind` is a short class (an error name or code); `message` is clipped and carries no source text. */
|
|
44
|
+
error(stage, kind, message) {
|
|
45
|
+
own(() => {
|
|
46
|
+
const key = `${stage}|${kind}`; const c = errors.get(key) || { stage, kind, count: 0, example: String(message ?? '').replace(/\s+/g, ' ').slice(0, 120) };
|
|
47
|
+
c.count += 1; errors.set(key, c);
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
snapshot() {
|
|
51
|
+
return own(() => {
|
|
52
|
+
sample();
|
|
53
|
+
const z = (v) => (deterministic ? 0 : Math.round(v * 100) / 100);
|
|
54
|
+
const byName = (a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
55
|
+
const rows = (map) => [...map].sort(deterministic ? byName : (a, b) => b[1].ms - a[1].ms || byName(a, b)).map(([name, v]) => ({ name, ms: z(v.ms), calls: v.calls }));
|
|
56
|
+
return {
|
|
57
|
+
schema: 'agentic-security/scan-telemetry', version: 1, deterministic,
|
|
58
|
+
totalMs: z(Number(process.hrtime.bigint() - startedNs) / 1e6),
|
|
59
|
+
stages: rows(stages),
|
|
60
|
+
detectors: rows(detectors).slice(0, 25),
|
|
61
|
+
memory: { rssHighWaterMb: z(mem.rssHighWaterBytes / MB), heapHighWaterMb: z(mem.heapHighWaterBytes / MB), samples: mem.samples },
|
|
62
|
+
counters: Object.fromEntries([...counters].sort(byName)),
|
|
63
|
+
errors: [...errors.values()].sort((a, b) => (a.stage + a.kind < b.stage + b.kind ? -1 : 1)),
|
|
64
|
+
overhead: { selfMs: z(Number(selfNs) / 1e6) },
|
|
65
|
+
outbound: 'none: this record stays in the scan result',
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return api;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fold the existing error arrays of a scan into telemetry errors, by stage. Reads the arrays; changes nothing. */
|
|
74
|
+
export function attributeErrors(tel, { annotatorErrors = [], detectorErrors = [], deepFailure = null, filesTimedOut = 0 } = {}) {
|
|
75
|
+
for (const e of annotatorErrors) tel.error(`annotator:${e.phase || e.annotator || 'unknown'}`, e.name || 'Error', e.message || e.err);
|
|
76
|
+
for (const e of detectorErrors) tel.error(`detector:${e.analyzer || 'unknown'}`, 'DetectorError', e.err);
|
|
77
|
+
if (deepFailure) tel.error('deep-analysis', 'DeepFailure', deepFailure);
|
|
78
|
+
if (filesTimedOut > 0) tel.error('per-file-timeout', 'Timeout', `${filesTimedOut} file(s) exceeded the per-file budget`);
|
|
79
|
+
}
|
|
@@ -118,8 +118,9 @@ export function aggregateCorpus(detail) {
|
|
|
118
118
|
* protocol is forced to `unmeasured`, and an absent report is `unmeasured` with
|
|
119
119
|
* the reason, never an empty pass. Pure.
|
|
120
120
|
*/
|
|
121
|
-
export function summarizeMeasurement(provenance, independent) {
|
|
121
|
+
export function summarizeMeasurement(provenance, independent, independentRun = null) {
|
|
122
122
|
const p = provenance || {};
|
|
123
|
+
const run = summarizeIndependentRun(independentRun, p);
|
|
123
124
|
const clean = p.worktreeClean === true;
|
|
124
125
|
const hist = independent && independent.engineVersion && independent.engineVersion !== p.engineVersion;
|
|
125
126
|
return {
|
|
@@ -128,14 +129,47 @@ export function summarizeMeasurement(provenance, independent) {
|
|
|
128
129
|
identity: clean
|
|
129
130
|
? 'the commit and bundle below identify exactly the tree that was measured'
|
|
130
131
|
: p.worktreeClean === false ? 'the worktree was dirty: the commit below does NOT identify what was measured, so this is not a clean measurement' : 'cleanliness of the measured tree was not recorded',
|
|
131
|
-
currentEngineIndependentAccuracy: 'unmeasured',
|
|
132
|
-
currentEngineIndependentReason:
|
|
132
|
+
currentEngineIndependentAccuracy: run && run.comparableToCurrentEngine ? (run.identity.measurementValid ? 'measured' : 'measured_not_release_grade') : 'unmeasured',
|
|
133
|
+
currentEngineIndependentReason: run && run.comparableToCurrentEngine
|
|
134
|
+
? (run.identity.measurementValid ? `measured by run ${run.runId} on a clean commit; the figures are for the development and validation splits only` : `run ${run.runId} measured this engine but the measurement is not valid for a release claim: ${run.identity.invalidReasons.join(', ') || 'no identity recorded'}`)
|
|
135
|
+
: run ? `the latest independent run (${run.runId}) measured engine ${run.engineVersion}, not the engine above; it is a historical record` : 'no adjudicated real-code population has been run against this engine; the independent record in this document, if any, describes an older engine',
|
|
136
|
+
independentRun: run,
|
|
133
137
|
historicalIndependentRecord: hist
|
|
134
138
|
? { engineVersion: independent.engineVersion, measuredAt: independent.measuredAt || null, usage: 'historical record only; not a measurement of the engine above and not combined with the corpus figures' }
|
|
135
139
|
: null,
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The latest clean-commit run of the independent population (bench/independent/runs/<id>/RESULT.json), reduced to what a reader may be shown.
|
|
145
|
+
* Only the localized definition is a claim. A language below the declared minimum is `insufficient_population` and shows counts, never a rate;
|
|
146
|
+
* a language in the manifest that was not scored at all is listed as unmeasured. Pure.
|
|
147
|
+
*/
|
|
148
|
+
export function summarizeIndependentRun(run, provenance) {
|
|
149
|
+
if (!run || !run.scoring) return null;
|
|
150
|
+
const loc = run.scoring.definitions.localized;
|
|
151
|
+
const d = run.scoring.denominators;
|
|
152
|
+
const id = run.identity || {};
|
|
153
|
+
const rate = (r) => (r && r.d > 0 ? { n: r.n, d: r.d, value: r.value } : { n: r?.n ?? 0, d: r?.d ?? 0, value: null });
|
|
154
|
+
const byLanguage = Object.entries(loc.byLanguage).map(([language, g]) => (g.status === 'reportable'
|
|
155
|
+
? { language, entries: g.entries, status: 'reportable', recall: rate(g.recall), precision: rate(g.precision), f1: g.f1, recallInterval: g.recallInterval, precisionInterval: g.precisionInterval, counts: { tp: g.tp, fp: g.fp, fn: g.fn, tn: g.tn } }
|
|
156
|
+
: { language, entries: g.entries, status: 'insufficient_population', counts: { tp: g.tp, fp: g.fp, fn: g.fn, tn: g.tn } }));
|
|
157
|
+
const unmeasuredLanguages = (run.population?.selection?.counts ? Object.keys(run.population.selection.counts) : []).filter((l) => !loc.byLanguage[l] || loc.byLanguage[l].status !== 'reportable');
|
|
158
|
+
return {
|
|
159
|
+
runId: run.runId, engineVersion: run.engineVersion || null, measuredAt: run.mergedAt || null,
|
|
160
|
+
comparableToCurrentEngine: !!run.engineVersion && run.engineVersion === (provenance || {}).engineVersion,
|
|
161
|
+
historical: !!run.engineVersion && run.engineVersion !== (provenance || {}).engineVersion,
|
|
162
|
+
identity: { sourceCommit: id.sourceCommit || null, bundleSha256: id.bundleSha256 || null, bundleSidecarSha256: id.bundleSidecarSha256 || null, measurementValid: id.measurementValid === true, invalidReasons: id.invalidReasons || [], lockfileSha256: id.lockfileSha256 || null, configSha256: id.configSha256 || null, modelEndpointMode: id.config?.modelEndpointMode || null, toolchain: id.toolchain ? { node: id.toolchain.node, os: id.toolchain.os } : null },
|
|
163
|
+
population: { role: run.population?.role || 'independent population', manifestSha256: run.population?.manifestSha256 || null, splitsProtocolHash: run.population?.splitsProtocolHash || run.splits?.protocolHash || null, splitNames: run.population?.selection?.splitNames || [], perLanguageCap: run.population?.selection?.perLanguage ?? null, sealedSplitTouched: run.population?.sealedSplitTouched === true },
|
|
164
|
+
configuration: run.configuration || null,
|
|
165
|
+
denominators: { inScope: d.inScope, scored: d.scored, unscored: d.unscored, unscoredByClass: d.unscoredByClass, unaccounted: d.unaccounted, reconciles: d.reconciles },
|
|
166
|
+
headline: run.headline ? { quotable: run.headline.quotable === true, reason: run.headline.reason || null, precision: run.headline.precision ? rate(run.headline.precision) : null, recall: run.headline.recall ? rate(run.headline.recall) : null, f1: run.headline.f1 ?? null, f1Interval: run.headline.f1Interval || null } : null,
|
|
167
|
+
byLanguage, unmeasuredLanguages,
|
|
168
|
+
minEntriesForRate: run.scoring.minEntriesForRate,
|
|
169
|
+
fixedVersionPressure: run.scoring.fixedVersionPressure || null,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
export function summarizeEvaluationGates(report) {
|
|
140
174
|
if (!report || typeof report !== 'object') {
|
|
141
175
|
return {
|
|
@@ -185,6 +219,17 @@ export function summarizeEvaluationGates(report) {
|
|
|
185
219
|
* such scan renders "not measured this run" rather than a
|
|
186
220
|
* fabricated rate. See PRD Success Metrics.
|
|
187
221
|
*/
|
|
222
|
+
/**
|
|
223
|
+
* What happened to one third-party target: `completed`, `not_attempted` (it never ran: no pinned commit, so there was nothing to clone) or `failed`
|
|
224
|
+
* (it ran and did not finish). A pre-existing summary records an unpinned target as `error`; the message is the evidence, so it is read from there.
|
|
225
|
+
*/
|
|
226
|
+
export function proofTargetOutcome(t) {
|
|
227
|
+
if (t && t.status === 'ok') return 'completed';
|
|
228
|
+
if (t && t.status === 'not_attempted') return 'not_attempted';
|
|
229
|
+
if (t && t.status === 'error' && /has no pinned commit/.test(String(t.error || ''))) return 'not_attempted';
|
|
230
|
+
return 'failed';
|
|
231
|
+
}
|
|
232
|
+
|
|
188
233
|
export function buildScorecard(inputs) {
|
|
189
234
|
const corpus = aggregateCorpus(inputs.corpusDetail);
|
|
190
235
|
const selfScan = inputs.selfScan || {};
|
|
@@ -324,9 +369,11 @@ export function buildScorecard(inputs) {
|
|
|
324
369
|
bundleSha: proof.bundleSha || null,
|
|
325
370
|
targetCount: proof.targetCount ?? null,
|
|
326
371
|
ok: proof.ok ?? null,
|
|
327
|
-
|
|
372
|
+
// `failed` in the committed summary counted targets that never ran; the three outcomes are separated here
|
|
373
|
+
failed: (proof.targets || []).filter(t => proofTargetOutcome(t) === 'failed').length,
|
|
374
|
+
notAttempted: (proof.targets || []).filter(t => proofTargetOutcome(t) === 'not_attempted').length,
|
|
328
375
|
targets: (proof.targets || []).map(t => ({
|
|
329
|
-
id: t.id, commit: t.commit, status: t.status,
|
|
376
|
+
id: t.id, commit: t.commit, status: t.status, outcome: proofTargetOutcome(t),
|
|
330
377
|
filesInScope: t.coverage?.totals?.inScope ?? null,
|
|
331
378
|
filesParsed: t.coverage?.totals?.parsed ?? null,
|
|
332
379
|
determinismChecked: t.determinism?.checked ?? null,
|
|
@@ -349,7 +396,7 @@ export function buildScorecard(inputs) {
|
|
|
349
396
|
// QA-001/QA-003: the real-code gates registered in the evaluation protocol. Relayed, never computed here.
|
|
350
397
|
evaluationGates: summarizeEvaluationGates(inputs.evaluation),
|
|
351
398
|
// QA-008.AC01: what this scorecard measured, stated once. `clean` is what lets the commit and bundle identify the measured tree.
|
|
352
|
-
measurement: summarizeMeasurement(inputs.provenance, committed.independent),
|
|
399
|
+
measurement: summarizeMeasurement(inputs.provenance, committed.independent, committed.independentRun),
|
|
353
400
|
};
|
|
354
401
|
}
|
|
355
402
|
|
|
@@ -403,7 +450,10 @@ export function renderScorecardMarkdown(m) {
|
|
|
403
450
|
}
|
|
404
451
|
if (m.measurement) {
|
|
405
452
|
L.push(`| Measurement identity | ${m.measurement.cleanMeasurement ? 'clean measurement: the commit and bundle above identify the measured tree' : 'NOT a clean measurement: the commit and bundle above do not identify the measured tree'} |`);
|
|
406
|
-
|
|
453
|
+
const ia = m.measurement.currentEngineIndependentAccuracy;
|
|
454
|
+
L.push(ia === 'unmeasured'
|
|
455
|
+
? '| Independent accuracy of THIS engine | unmeasured (no real-code run of this engine exists; see the historical record below, if present) |'
|
|
456
|
+
: `| Independent accuracy of THIS engine | ${ia === 'measured' ? 'measured on a clean commit' : 'measured, but NOT release grade'}: ${m.measurement.currentEngineIndependentReason} |`);
|
|
407
457
|
}
|
|
408
458
|
L.push(`| Node | ${p.nodeVersion || 'unknown'} |`);
|
|
409
459
|
L.push(`| Corpus entries | ${c.totalEntries} (${c.scoredEntries} scored) |`);
|
|
@@ -733,6 +783,45 @@ export function renderScorecardMarkdown(m) {
|
|
|
733
783
|
L.push('');
|
|
734
784
|
}
|
|
735
785
|
|
|
786
|
+
const run = m.measurement && m.measurement.independentRun;
|
|
787
|
+
if (run) {
|
|
788
|
+
const pc = (r) => (r && r.value !== null && r.value !== undefined ? `${(r.value * 100).toFixed(1)}% (${r.n}/${r.d})` : `n/a (${r ? r.n : 0}/${r ? r.d : 0})`);
|
|
789
|
+
const iv = (i) => (i && i.status === 'measured' ? `${(i.low * 100).toFixed(1)}% to ${(i.high * 100).toFixed(1)}%` : 'unmeasured');
|
|
790
|
+
L.push(run.comparableToCurrentEngine ? `## Independent accuracy of this engine (run ${run.runId})` : `## Independent run ${run.runId}: historical record (engine ${run.engineVersion})`);
|
|
791
|
+
L.push('');
|
|
792
|
+
L.push('This is the independent population, upstream code labelled by public advisories, run on the engine below at a named commit. It is a different');
|
|
793
|
+
L.push('instrument from the curated regression net above and the two figures are never combined. Only the **localized** definition (a matching finding landed on code the');
|
|
794
|
+
L.push('fix changed) is a claim; it is reported with numerator and denominator.');
|
|
795
|
+
L.push('');
|
|
796
|
+
L.push('| Field | Value |');
|
|
797
|
+
L.push('| --- | --- |');
|
|
798
|
+
L.push(`| Engine | ${run.engineVersion}${run.comparableToCurrentEngine ? '' : ' (NOT the engine this scorecard describes)'} |`);
|
|
799
|
+
L.push(`| Measured at commit | \`${run.identity.sourceCommit || 'unknown'}\` |`);
|
|
800
|
+
L.push(`| Bundle SHA-256 | \`${run.identity.bundleSha256 || 'unknown'}\` (sidecar ${run.identity.bundleSidecarSha256 && run.identity.bundleSidecarSha256 === run.identity.bundleSha256 ? 'matches' : 'does not match or is missing'}) |`);
|
|
801
|
+
L.push(`| Measurement | ${run.identity.measurementValid ? 'valid: clean tree, bundle matches its digest' : `NOT valid for a release claim: ${run.identity.invalidReasons.join(', ') || 'no identity recorded'}`} |`);
|
|
802
|
+
L.push(`| Configuration | ${run.configuration ? `deep=${run.configuration.deep}, model=${run.configuration.llm}` : 'unknown'}; model endpoint ${run.identity.modelEndpointMode || 'unknown'} |`);
|
|
803
|
+
L.push(`| Population | manifest \`${(run.population.manifestSha256 || 'unknown').slice(0, 16)}\`; splits protocol \`${(run.population.splitsProtocolHash || 'unknown').slice(0, 23)}\`; ${run.population.splitNames.join(' + ') || 'unknown'} only, at most ${run.population.perLanguageCap ?? 'all'} entries per language; sealed split touched: ${run.population.sealedSplitTouched ? 'YES' : 'no'} |`);
|
|
804
|
+
L.push(`| Date (UTC) | ${run.measuredAt || 'unknown'} |`);
|
|
805
|
+
L.push(`| Entries in scope | ${run.denominators.inScope}: scored ${run.denominators.scored}, unscored ${run.denominators.unscored}${Object.keys(run.denominators.unscoredByClass || {}).length ? ` (${Object.entries(run.denominators.unscoredByClass).map(([k, v]) => `${k} ${v}`).join(', ')})` : ''}, unaccounted ${run.denominators.unaccounted}; denominators ${run.denominators.reconciles ? 'reconcile' : 'DO NOT reconcile'} |`);
|
|
806
|
+
L.push('');
|
|
807
|
+
if (run.headline && run.headline.quotable) {
|
|
808
|
+
L.push(`**Headline (localized):** precision ${pc(run.headline.precision)}, recall ${pc(run.headline.recall)}, F1 ${run.headline.f1 === null ? 'n/a' : run.headline.f1.toFixed(3)}${run.headline.f1Interval && run.headline.f1Interval.status === 'measured' ? ` (95% interval ${iv(run.headline.f1Interval)}, resampling whole repositories)` : ' (interval unmeasured)'}.`);
|
|
809
|
+
} else {
|
|
810
|
+
L.push(`**No headline figure:** ${run.headline && run.headline.reason ? run.headline.reason : 'the run is not quotable'}.`);
|
|
811
|
+
}
|
|
812
|
+
L.push('');
|
|
813
|
+
L.push(`| Language | Scored | Recall | Precision | Recall 95% interval | Status |`);
|
|
814
|
+
L.push('| --- | --- | --- | --- | --- | --- |');
|
|
815
|
+
for (const g of run.byLanguage) {
|
|
816
|
+
if (g.status === 'reportable') L.push(`| ${g.language} | ${g.entries} | ${pc(g.recall)} | ${pc(g.precision)} | ${iv(g.recallInterval)} | reportable |`);
|
|
817
|
+
else L.push(`| ${g.language} | ${g.entries} | counts only (${g.counts.tp} true positive, ${g.counts.fn} missed) | | | insufficient_population (below ${run.minEntriesForRate}) |`);
|
|
818
|
+
}
|
|
819
|
+
L.push('');
|
|
820
|
+
if (run.unmeasuredLanguages.length) L.push(`Languages with no rate in this run: ${run.unmeasuredLanguages.join(', ')}. They are unmeasured here, not good or bad.`, '');
|
|
821
|
+
L.push('This run does not use the sealed split, so it is a baseline on development and validation data, not a held-out result, and it makes no claim about the 80% per-language target.');
|
|
822
|
+
L.push('');
|
|
823
|
+
}
|
|
824
|
+
|
|
736
825
|
const ind = m.committedInputs.independent;
|
|
737
826
|
if (ind && ind.overall) {
|
|
738
827
|
const hist = ind.historical === true;
|
|
@@ -874,7 +963,7 @@ export function renderScorecardMarkdown(m) {
|
|
|
874
963
|
const pc = m.committedInputs.proofCorpus;
|
|
875
964
|
if (pc) {
|
|
876
965
|
L.push(`- **Third-party repository run** (*committed artifact*, \`${pc.source}\`,`);
|
|
877
|
-
L.push(` bundle \`${pc.bundleSha}\`): ${pc.ok}
|
|
966
|
+
L.push(` bundle \`${pc.bundleSha}\`): ${pc.ok} of ${pc.targetCount} targets completed, ${pc.notAttempted ?? 0} not attempted (no pinned commit, so nothing was cloned), ${pc.failed ?? 0} failed after starting.`);
|
|
878
967
|
L.push(' Reported for scale and parse coverage only — these repositories have no');
|
|
879
968
|
L.push(' vulnerability ground truth, so no accuracy rate is derived from them.');
|
|
880
969
|
L.push('');
|
|
@@ -882,7 +971,7 @@ export function renderScorecardMarkdown(m) {
|
|
|
882
971
|
L.push('| --- | --- | --- | --- | --- | --- |');
|
|
883
972
|
for (const t of pc.targets) {
|
|
884
973
|
const det = t.determinismChecked ? (t.determinismIdentical ? 'identical' : 'checked, not byte-identical') : 'not checked';
|
|
885
|
-
L.push(`| ${t.id} | \`${String(t.commit || '').slice(0, 12)}\` | ${t.status} | ${formatRate(t.filesParsed ?? 0, t.filesInScope ?? 0)} | ${t.resultsEmitted ?? 'n/a'} | ${det} |`);
|
|
974
|
+
L.push(`| ${t.id} | \`${String(t.commit || '').slice(0, 12)}\` | ${t.outcome === 'not_attempted' ? 'not attempted' : t.status} | ${formatRate(t.filesParsed ?? 0, t.filesInScope ?? 0)} | ${t.resultsEmitted ?? 'n/a'} | ${det} |`);
|
|
886
975
|
}
|
|
887
976
|
}
|
|
888
977
|
L.push('');
|
|
@@ -144,6 +144,7 @@ export const ARTIFACT_REGISTRY = [
|
|
|
144
144
|
{ name: 'findings.csv', kind: 'file', classification: 'generated', retentionClass: 'scan' },
|
|
145
145
|
{ name: 'llm-cache', kind: 'dir', classification: 'generated', retentionClass: 'cache' },
|
|
146
146
|
{ name: 'fix-history', kind: 'dir', classification: 'generated', retentionClass: 'backup' },
|
|
147
|
+
{ name: 'fix-batches', kind: 'dir', classification: 'generated', retentionClass: 'backup' },
|
|
147
148
|
{ name: 'verifier-runs', kind: 'dir', classification: 'generated', retentionClass: 'evidence', source: 'src/posture/verifier.js (recordVerifierRun)', note: 'Adversarial premortem Q1 (2026-09-07): one JSON record per `agentic-security verify` run, added so module:verifier (nist-800-171-r3.json 03.12.01, nist-csf-2.json RC.RP) has a real artifact to check for instead of a name in the ARTIFACT table nothing ever wrote.' },
|
|
148
149
|
{ name: 'fix-plans', kind: 'dir', classification: 'generated', retentionClass: 'scan' },
|
|
149
150
|
// The following were confirmed missing from the old hardcoded WIPE/
|
|
@@ -172,6 +173,8 @@ export const ARTIFACT_REGISTRY = [
|
|
|
172
173
|
{ name: 'compliance', kind: 'dir', classification: 'generated', retentionClass: 'evidence', source: 'posture/auditor-walkthrough.js' },
|
|
173
174
|
{ name: 'attestations', kind: 'dir', classification: 'generated', retentionClass: 'evidence', source: 'posture/evidence-bundle.js' },
|
|
174
175
|
{ name: 'auditor-walkthroughs', kind: 'dir', classification: 'generated', retentionClass: 'evidence' },
|
|
176
|
+
{ name: 'compliance-applicability.json', kind: 'file', classification: 'operator-config', note: 'operator declarations that a framework control does not apply ({ <framework id>: { <control id>: { reason, declaredBy } } }); read by `compliance --report <framework> --truth`. The tool never infers non-applicability.' },
|
|
177
|
+
{ name: 'analysis-cache.jsonl', kind: 'file', classification: 'generated', retentionClass: 'cache', confidential: true, source: 'posture/scan-checkpoint.js (openCheckpoint persistent mode), engine.js', note: 'Opt-in (AGENTIC_SECURITY_ANALYSIS_CACHE=1) cross-scan cache of per-file detector results. Each record is authenticated with the install key, so a cache carried in from elsewhere is discarded. It holds findings with source excerpts, so it is treated like the source tree.' },
|
|
175
178
|
{ name: 'incremental', kind: 'dir', classification: 'generated', retentionClass: 'cache', source: 'dataflow/incremental.js (cache)' },
|
|
176
179
|
{ name: 'model-rescan', kind: 'dir', classification: 'generated', retentionClass: 'scan' },
|
|
177
180
|
{ name: 'sca-upgrade-history', kind: 'dir', classification: 'generated', retentionClass: 'scan' },
|
|
@@ -279,6 +282,8 @@ export const ARTIFACT_REGISTRY = [
|
|
|
279
282
|
// retention cannot reach it. Operator-config, not generated: these are
|
|
280
283
|
// hand-supplied eBPF/APM trace exports the scanner only ever reads.
|
|
281
284
|
{ name: 'runtime-trace.jsonl', kind: 'file', classification: 'operator-config', note: 'eBPF/APM runtime trace consumed by posture/runtime-correlation.js\'s own trace loader (deliberately not named by its literal export here — no-dead-modules.test.js scans note strings too, and naming it would make it look, wrongly, like a real call site) — operator-supplied, never scanner-written; $AGENTIC_SECURITY_RUNTIME_TRACE_PATH overrides the location entirely' },
|
|
285
|
+
{ name: 'runtime-policy.json', kind: 'file', classification: 'operator-config', note: 'opt-in policy for runtime observation ingestion (collect, kinds, retentionDays, sampleRate); read by posture/runtime-trace-ingest.js' },
|
|
286
|
+
{ name: 'runtime-trace.meta.json', kind: 'file', classification: 'generated', retentionClass: 'evidence', note: 'ingestion record beside runtime-trace.jsonl: declared sampling, retention, counts of dropped and rejected fields (counts only, never values)' },
|
|
282
287
|
{ name: 'runtime.jsonl', kind: 'file', classification: 'operator-config', note: 'alternate filename for runtime-trace.jsonl — see posture/runtime-correlation.js\'s DEFAULT_TRACE_NAMES' },
|
|
283
288
|
{ name: 'ebpf-trace.jsonl', kind: 'file', classification: 'operator-config', note: 'alternate filename for runtime-trace.jsonl — see posture/runtime-correlation.js\'s DEFAULT_TRACE_NAMES' },
|
|
284
289
|
];
|