@clear-capabilities/agentic-security-scanner 0.133.0 → 0.136.2
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 +299 -0
- package/bin/agentic-security-audit.js +2 -1
- package/bin/agentic-security-consistency.js +2 -1
- package/bin/agentic-security.js +348 -40
- package/dist/113.index.js +6 -6
- package/dist/117.index.js +3 -1
- package/dist/178.index.js +1 -1
- package/dist/220.index.js +5 -3
- package/dist/238.index.js +4 -4
- package/dist/317.index.js +270 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +13 -7
- package/dist/499.index.js +86 -0
- package/dist/513.index.js +7 -3
- package/dist/526.index.js +6 -6
- package/dist/609.index.js +741 -0
- package/dist/637.index.js +1 -1
- package/dist/675.index.js +7 -5
- package/dist/839.index.js +4 -3
- package/dist/905.index.js +1173 -0
- package/dist/agentic-security.mjs +56 -56
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +32 -0
- package/dist/compliance-frameworks/eu-ai-act.json +51 -0
- package/dist/compliance-frameworks/gdpr.json +45 -0
- package/dist/compliance-frameworks/hipaa-security-rule.json +56 -0
- package/dist/compliance-frameworks/nist-ai-600-1.json +51 -0
- package/dist/compliance-frameworks/nist-csf-2.json +73 -0
- package/dist/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +79 -0
- package/dist/compliance-frameworks/owasp-llm-top-10.json +69 -0
- package/package.json +14 -6
- package/src/badge.js +2 -1
- package/src/dataflow/cross-service-taint.js +2 -1
- package/src/dataflow/ifds-precise.js +6 -4
- package/src/dataflow/incremental.js +7 -5
- package/src/discovery/CLAUDE.md +48 -0
- package/src/discovery/confirm.js +47 -0
- package/src/discovery/disprove.js +79 -0
- package/src/discovery/hunter.js +116 -0
- package/src/discovery/index.js +331 -0
- package/src/discovery/judge.js +97 -0
- package/src/discovery/lenses.js +69 -0
- package/src/discovery/llm-invoke.js +120 -0
- package/src/discovery/memory.js +163 -0
- package/src/discovery/partition.js +92 -0
- package/src/engine.js +163 -7
- package/src/integrations/tickets.js +7 -6
- package/src/ir/ir-stats.js +1 -1
- package/src/ir/parser-cpp.js +1 -1
- package/src/leaderboard.js +3 -2
- package/src/llm-validator/consistency.js +6 -2
- package/src/llm-validator/index.js +30 -41
- package/src/llm-validator/providers.js +227 -0
- package/src/mcp/audit.js +2 -1
- package/src/mcp/tools.js +4 -3
- package/src/posture/CLAUDE.md +102 -1
- package/src/posture/accuracy-scorecard.js +59 -1
- package/src/posture/agents-memory.js +5 -3
- package/src/posture/auditor-walkthrough.js +9 -7
- package/src/posture/auth-posture-import.js +5 -4
- package/src/posture/autopilot.js +225 -0
- package/src/posture/comparison.js +181 -0
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/src/posture/compliance-policy.js +7 -9
- package/src/posture/custom-rules.js +7 -5
- package/src/posture/cve-alert-daemon.js +6 -5
- package/src/posture/dep-add-guard.js +2 -1
- package/src/posture/deterministic.js +3 -2
- package/src/posture/evidence-bundle.js +246 -0
- package/src/posture/execution-proof.js +25 -1
- package/src/posture/exploitability-probability.js +2 -1
- package/src/posture/feature-flags.js +3 -2
- package/src/posture/findings-memory.js +3 -3
- package/src/posture/fix-history.js +5 -2
- package/src/posture/fix-metrics.js +5 -5
- package/src/posture/fix-plan.js +2 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/grader-calibration.js +3 -4
- package/src/posture/intent-context.js +2 -1
- package/src/posture/learning.js +4 -3
- package/src/posture/license-attributions.js +5 -7
- package/src/posture/license-graph.js +2 -1
- package/src/posture/license-policy.js +2 -1
- package/src/posture/logic-claims.js +266 -0
- package/src/posture/model-rescan.js +4 -3
- package/src/posture/network-policy-import.js +3 -2
- package/src/posture/poc-inprocess.js +404 -2
- package/src/posture/pqc-migration-plan.js +7 -5
- package/src/posture/pr-augment.js +8 -5
- package/src/posture/privacy-framework.js +262 -0
- package/src/posture/proof-artifact.js +101 -0
- package/src/posture/prove-findings.js +28 -4
- package/src/posture/risk-dollars.js +2 -2
- package/src/posture/router.js +5 -4
- package/src/posture/ruleset-version.js +2 -2
- package/src/posture/runtime-correlation.js +2 -1
- package/src/posture/sbom-diff.js +12 -3
- package/src/posture/sca-policy.js +7 -4
- package/src/posture/scan-checkpoint.js +15 -0
- package/src/posture/state-dir.js +34 -0
- package/src/posture/telemetry-ingest.js +4 -3
- package/src/posture/threat-model-auto.js +4 -1
- package/src/posture/threat-model-grounding.js +11 -1
- package/src/posture/time-to-fix.js +3 -2
- package/src/posture/triage-memory.js +3 -2
- package/src/posture/waf-ingest.js +6 -5
- package/src/posture/watch-mode.js +4 -3
- package/src/report/index.js +9 -0
- package/src/sast/code-injection-multilang.js +29 -0
- package/src/sca/dep-confusion.js +2 -1
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
import * as fs from 'node:fs';
|
|
65
65
|
import * as path from 'node:path';
|
|
66
66
|
import * as crypto from 'node:crypto';
|
|
67
|
-
import {
|
|
67
|
+
import { ensureStateDir, safeWriteState, statePath } from '../posture/state-dir.js';
|
|
68
68
|
import { redactSecrets } from './redact.js';
|
|
69
69
|
import { signLastScan } from '../posture/integrity.js';
|
|
70
70
|
|
|
@@ -73,6 +73,7 @@ import { signLastScan } from '../posture/integrity.js';
|
|
|
73
73
|
// doesn't have to reach through the `_internal` underscore-prefixed export.
|
|
74
74
|
import { createCostLedger, parseCapUsd, renderCostCeiling } from './cost-ceiling.js';
|
|
75
75
|
import { localEndpointConfig } from './local-endpoint.js';
|
|
76
|
+
import { resolveProvider, buildProviderRequest, providerMatrix } from './providers.js';
|
|
76
77
|
|
|
77
78
|
// The output cap we request. Shared with the cost estimate so the ceiling
|
|
78
79
|
// charges exactly what we permit the model to produce.
|
|
@@ -83,7 +84,6 @@ const MAX_OUTPUT_TOKENS = 512;
|
|
|
83
84
|
let _localPresetRefusal = null;
|
|
84
85
|
|
|
85
86
|
export const PROMPT_VERSION = 'v2.0-hardened';
|
|
86
|
-
const CACHE_DIR = '.agentic-security/llm-cache';
|
|
87
87
|
|
|
88
88
|
// System preamble — embeds a per-request challenge token the model MUST
|
|
89
89
|
// echo, and a strict instruction-priority frame. {{challenge}} and {{nonce}}
|
|
@@ -124,49 +124,36 @@ Snippet (single line, trusted from scanner output): {{snippet}}
|
|
|
124
124
|
Reply now with the JSON object on the last line of your response. Nothing else after it.
|
|
125
125
|
`;
|
|
126
126
|
|
|
127
|
+
// Delegates to the provider seam (PRD Epic 3). Kept as a thin adapter rather
|
|
128
|
+
// than deleted: every call site, test and cost-ceiling path already speaks this
|
|
129
|
+
// shape, and changing a seam and all its consumers at once is how a refactor
|
|
130
|
+
// becomes a regression. `_localPresetRefusal` still carries a REFUSAL
|
|
131
|
+
// distinctly from "nothing configured" — the local preset declining a remote
|
|
132
|
+
// endpoint must not read as an absent config.
|
|
127
133
|
function endpointConfig() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return r.config;
|
|
142
|
-
}
|
|
143
|
-
// Explicit BYO endpoint always wins (unchanged behaviour).
|
|
144
|
-
const endpoint = process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
|
|
145
|
-
if (endpoint) {
|
|
146
|
-
return { endpoint, apiKey: process.env.AGENTIC_SECURITY_LLM_API_KEY, model: process.env.AGENTIC_SECURITY_LLM_MODEL || 'unknown', preset: null };
|
|
147
|
-
}
|
|
148
|
-
// #18 — first-class Anthropic preset. Opt-in via AGENTIC_SECURITY_LLM_PRESET=anthropic
|
|
149
|
-
// + a key (AGENTIC_SECURITY_LLM_API_KEY or ANTHROPIC_API_KEY): makes the FP-suppression
|
|
150
|
-
// validator reachable with just a key — no BYO endpoint URL or request-shape wrangling.
|
|
151
|
-
// Offline-degrading: no key → null (validator no-ops; no runtime cloud call by default).
|
|
152
|
-
if ((process.env.AGENTIC_SECURITY_LLM_PRESET || '').toLowerCase() === 'anthropic') {
|
|
153
|
-
const apiKey = process.env.AGENTIC_SECURITY_LLM_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
154
|
-
if (!apiKey) return null;
|
|
155
|
-
return {
|
|
156
|
-
endpoint: 'https://api.anthropic.com/v1/messages',
|
|
157
|
-
apiKey,
|
|
158
|
-
model: process.env.AGENTIC_SECURITY_LLM_MODEL || 'claude-haiku-4-5',
|
|
159
|
-
preset: 'anthropic',
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
134
|
+
const r = resolveProvider({ role: 'validate' });
|
|
135
|
+
if (!r.ok) { _localPresetRefusal = r.reason || null; return null; }
|
|
136
|
+
_localPresetRefusal = null;
|
|
137
|
+
const c = r.config;
|
|
138
|
+
return {
|
|
139
|
+
endpoint: c.endpoint,
|
|
140
|
+
apiKey: c.apiKey,
|
|
141
|
+
model: c.model,
|
|
142
|
+
preset: c.provider === 'anthropic' ? 'anthropic' : (c.provider === 'local' ? 'local' : null),
|
|
143
|
+
provider: c.provider,
|
|
144
|
+
egress: c.egress,
|
|
145
|
+
_shape: c.shape,
|
|
146
|
+
};
|
|
163
147
|
}
|
|
164
148
|
|
|
165
149
|
// Shape the request for the target: the Anthropic Messages API needs an
|
|
166
150
|
// x-api-key header (added by the caller), an anthropic-version header, and a
|
|
167
151
|
// {model, max_tokens, messages:[…]} body with the reply in content[].text. The
|
|
168
152
|
// generic path posts {prompt, model} with a Bearer header. Pure — no I/O.
|
|
169
|
-
function buildRequest(model, prompt, preset) {
|
|
153
|
+
function buildRequest(model, prompt, preset, shape) {
|
|
154
|
+
// A resolved provider carries its own wire shape; use it. The hand-written
|
|
155
|
+
// branches below remain for callers that pass only a preset string.
|
|
156
|
+
if (shape) return buildProviderRequest({ shape, model, apiKey: null }, prompt, MAX_OUTPUT_TOKENS);
|
|
170
157
|
if (preset === 'anthropic') {
|
|
171
158
|
return {
|
|
172
159
|
headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
@@ -347,8 +334,8 @@ function renderPrompt(finding, fileContents, challenge, nonce) {
|
|
|
347
334
|
.replace('{{context}}', sterileContext || '(no surrounding code available)');
|
|
348
335
|
}
|
|
349
336
|
|
|
350
|
-
async function callEndpoint(endpoint, apiKey, model, prompt, preset = null) {
|
|
351
|
-
const { headers, body, extractText, extractUsage } = buildRequest(model, prompt, preset);
|
|
337
|
+
async function callEndpoint(endpoint, apiKey, model, prompt, preset = null, shape = null) {
|
|
338
|
+
const { headers, body, extractText, extractUsage } = buildRequest(model, prompt, preset, shape);
|
|
352
339
|
if (apiKey) {
|
|
353
340
|
if (preset === 'anthropic') headers['x-api-key'] = apiKey;
|
|
354
341
|
else headers['Authorization'] = `Bearer ${apiKey}`;
|
|
@@ -520,7 +507,7 @@ export async function validateOne(finding, fileContents, scanRoot, ledger = null
|
|
|
520
507
|
}
|
|
521
508
|
}
|
|
522
509
|
|
|
523
|
-
const resp = await callEndpoint(cfg.endpoint, cfg.apiKey, cfg.model, prompt, cfg.preset);
|
|
510
|
+
const resp = await callEndpoint(cfg.endpoint, cfg.apiKey, cfg.model, prompt, cfg.preset, cfg._shape);
|
|
524
511
|
// Record actual usage when the endpoint reports it, else the estimate. An
|
|
525
512
|
// unreported call is never free — but the two are recorded DISTINCTLY, so
|
|
526
513
|
// the reported spend can say which it is. Presenting an upper bound as a
|
|
@@ -629,6 +616,8 @@ export async function validateMany(findings, { fileContents, scanRoot, concurren
|
|
|
629
616
|
findings.costCeiling = ledger.state();
|
|
630
617
|
findings.costCeilingSummary = renderCostCeiling(ledger.state());
|
|
631
618
|
}
|
|
619
|
+
// Which provider each role would use. No keys, ever — this is reported.
|
|
620
|
+
findings.providerMatrix = providerMatrix();
|
|
632
621
|
const _cs = cacheStats();
|
|
633
622
|
findings.validatorCache = _cs;
|
|
634
623
|
if (_cs.unverified > 0) {
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// PRD Epic 3 — the model-neutral seam.
|
|
2
|
+
//
|
|
3
|
+
// The deterministic engine has never depended on a model. The only coupling was
|
|
4
|
+
// here, in how an AI call is shaped: one hardcoded pair of request builders and
|
|
5
|
+
// one `endpointConfig` that knew about exactly two presets. This module makes
|
|
6
|
+
// the provider a parameter instead, so an OpenAI or Gemini shop — or a
|
|
7
|
+
// regulated org that cannot send code to any vendor — is a configuration
|
|
8
|
+
// change rather than a fork.
|
|
9
|
+
//
|
|
10
|
+
// WHAT A PROVIDER IS. Endpoint, auth header shape, request body, and how to
|
|
11
|
+
// read text and token usage back out. Nothing else. Prompt construction,
|
|
12
|
+
// redaction, response validation, the challenge/nonce cross-check and the cost
|
|
13
|
+
// ceiling all stay upstream and apply identically to every provider — those are
|
|
14
|
+
// the security properties, and they must not become per-vendor.
|
|
15
|
+
//
|
|
16
|
+
// PER-ROLE PINNING. The strong pipelines route cheap models at triage and
|
|
17
|
+
// frontier models at synthesis. `resolveProvider({ role })` reads a per-role
|
|
18
|
+
// override before the global one, so `AGENTIC_SECURITY_LLM_MODEL_VERIFY` can
|
|
19
|
+
// point at something cheap while `..._FIX` points at something capable. Roles
|
|
20
|
+
// are a closed set: an unknown role would silently fall back to the global
|
|
21
|
+
// model, which is how a "cheap verify" quietly becomes an expensive one.
|
|
22
|
+
//
|
|
23
|
+
// GRACEFUL DEGRADATION IS THE DEFAULT. No provider configured means no AI
|
|
24
|
+
// stages — the engine still returns its deterministic findings. That is the
|
|
25
|
+
// existing behaviour and it is load-bearing: adding providers must not make the
|
|
26
|
+
// scanner require one.
|
|
27
|
+
//
|
|
28
|
+
// THE LOCAL PATH KEEPS ITS GUARANTEE. `local` still routes through
|
|
29
|
+
// `local-endpoint.js`, which enforces loopback on the host literal. A provider
|
|
30
|
+
// abstraction must not become a way to smuggle a remote endpoint into the mode
|
|
31
|
+
// whose entire promise is that nothing leaves the machine.
|
|
32
|
+
|
|
33
|
+
import { localEndpointConfig } from './local-endpoint.js';
|
|
34
|
+
|
|
35
|
+
// Roles the pipeline dispatches under. Closed set on purpose — see above.
|
|
36
|
+
export const ROLES = Object.freeze([
|
|
37
|
+
'validate', // the FP-suppression validator (the original caller)
|
|
38
|
+
'verify', // adversarial verification (Epic 2) — cheap tier by default
|
|
39
|
+
'explain', // human-facing explanation
|
|
40
|
+
'fix', // patch synthesis
|
|
41
|
+
'poc', // proof-of-concept synthesis (Epic 1)
|
|
42
|
+
'logic', // cross-file business-logic reasoning (Epic 6)
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
46
|
+
|
|
47
|
+
// ── Request/response shapes, one per wire protocol ──────────────────────────
|
|
48
|
+
//
|
|
49
|
+
// Kept as data rather than subclasses: each is four small functions, and a flat
|
|
50
|
+
// table makes it obvious what a new provider must supply.
|
|
51
|
+
const SHAPES = {
|
|
52
|
+
anthropic: {
|
|
53
|
+
headers: () => ({ 'Content-Type': 'application/json', 'anthropic-version': ANTHROPIC_VERSION }),
|
|
54
|
+
auth: (h, key) => { if (key) h['x-api-key'] = key; },
|
|
55
|
+
body: (model, prompt, maxTokens) => ({ model, max_tokens: maxTokens, messages: [{ role: 'user', content: prompt }] }),
|
|
56
|
+
text: (j) => (Array.isArray(j?.content) ? j.content.filter(b => b?.type === 'text').map(b => b.text || '').join('') : ''),
|
|
57
|
+
usage: (j) => (j?.usage && Number.isFinite(j.usage.input_tokens)
|
|
58
|
+
? { inputTokens: j.usage.input_tokens, outputTokens: j.usage.output_tokens || 0 } : null),
|
|
59
|
+
},
|
|
60
|
+
// OpenAI-compatible: also what most local servers speak, which is why the
|
|
61
|
+
// `local` provider reuses it rather than inventing a third shape.
|
|
62
|
+
openai: {
|
|
63
|
+
headers: () => ({ 'Content-Type': 'application/json' }),
|
|
64
|
+
auth: (h, key) => { if (key) h.Authorization = `Bearer ${key}`; },
|
|
65
|
+
body: (model, prompt, maxTokens) => ({ model, max_tokens: maxTokens, messages: [{ role: 'user', content: prompt }] }),
|
|
66
|
+
text: (j) => j?.choices?.[0]?.message?.content || j?.choices?.[0]?.text || '',
|
|
67
|
+
usage: (j) => {
|
|
68
|
+
const u = j?.usage; if (!u) return null;
|
|
69
|
+
const inputTokens = u.prompt_tokens ?? u.input_tokens;
|
|
70
|
+
const outputTokens = u.completion_tokens ?? u.output_tokens ?? 0;
|
|
71
|
+
return Number.isFinite(inputTokens) ? { inputTokens, outputTokens } : null;
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
// The LEGACY generic shape: `{prompt, model}` with a permissive extractor.
|
|
75
|
+
// This is what BYO endpoints and local servers have always been sent, and
|
|
76
|
+
// existing deployments speak it. Assuming OpenAI-compatibility here would
|
|
77
|
+
// silently break every one of them, so the OpenAI shape is used ONLY when the
|
|
78
|
+
// operator explicitly asks for the openai preset.
|
|
79
|
+
generic: {
|
|
80
|
+
headers: () => ({ 'Content-Type': 'application/json' }),
|
|
81
|
+
auth: (h, key) => { if (key) h.Authorization = `Bearer ${key}`; },
|
|
82
|
+
body: (model, prompt) => ({ prompt, model }),
|
|
83
|
+
text: (j) => (j && (j.response || j.text || j.content || j.output
|
|
84
|
+
|| j.choices?.[0]?.message?.content || j.message?.content)) || '',
|
|
85
|
+
usage: (j) => {
|
|
86
|
+
const u = j?.usage; if (!u) return null;
|
|
87
|
+
const inputTokens = u.prompt_tokens ?? u.input_tokens;
|
|
88
|
+
const outputTokens = u.completion_tokens ?? u.output_tokens ?? 0;
|
|
89
|
+
return Number.isFinite(inputTokens) ? { inputTokens, outputTokens } : null;
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
gemini: {
|
|
93
|
+
headers: () => ({ 'Content-Type': 'application/json' }),
|
|
94
|
+
// Gemini takes the key on the query string; the caller appends it, so the
|
|
95
|
+
// header set stays empty rather than carrying a bearer token that the API
|
|
96
|
+
// would ignore.
|
|
97
|
+
auth: () => {},
|
|
98
|
+
body: (model, prompt, maxTokens) => ({
|
|
99
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
100
|
+
generationConfig: { maxOutputTokens: maxTokens },
|
|
101
|
+
}),
|
|
102
|
+
text: (j) => (j?.candidates?.[0]?.content?.parts || []).map(p => p?.text || '').join(''),
|
|
103
|
+
usage: (j) => {
|
|
104
|
+
const u = j?.usageMetadata; if (!u) return null;
|
|
105
|
+
return Number.isFinite(u.promptTokenCount)
|
|
106
|
+
? { inputTokens: u.promptTokenCount, outputTokens: u.candidatesTokenCount || 0 } : null;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const DEFAULT_MODEL = {
|
|
112
|
+
anthropic: 'claude-haiku-4-5',
|
|
113
|
+
openai: 'gpt-4o-mini',
|
|
114
|
+
gemini: 'gemini-2.0-flash',
|
|
115
|
+
local: 'local-model',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function _envKey(role, suffix) {
|
|
119
|
+
return `AGENTIC_SECURITY_LLM_${suffix}_${String(role).toUpperCase()}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Per-role override, falling back to the global setting. */
|
|
123
|
+
function _forRole(env, role, suffix) {
|
|
124
|
+
if (role && ROLES.includes(role)) {
|
|
125
|
+
const v = env[_envKey(role, suffix)];
|
|
126
|
+
if (v) return v;
|
|
127
|
+
}
|
|
128
|
+
return env[`AGENTIC_SECURITY_LLM_${suffix}`];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the provider for a role.
|
|
133
|
+
*
|
|
134
|
+
* @returns {{ok:true, config:object} | {ok:false, reason:string|null}}
|
|
135
|
+
* `ok:false` with `reason:null` means "no AI configured", which is the normal
|
|
136
|
+
* default and NOT an error. A non-null reason means a configuration was
|
|
137
|
+
* supplied and refused — those must be surfaced, not silently treated the
|
|
138
|
+
* same as "off".
|
|
139
|
+
*/
|
|
140
|
+
export function resolveProvider({ role = 'validate', env = process.env } = {}) {
|
|
141
|
+
const explicit = (_forRole(env, role, 'PRESET') || '').toLowerCase();
|
|
142
|
+
const model = _forRole(env, role, 'MODEL');
|
|
143
|
+
|
|
144
|
+
// 1. Local — checked first because it is the only mode that makes a promise
|
|
145
|
+
// about where data goes, and a stray endpoint must not override it.
|
|
146
|
+
if (explicit === 'local') {
|
|
147
|
+
const r = localEndpointConfig(env);
|
|
148
|
+
if (!r.ok) return { ok: false, reason: r.reason };
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
config: {
|
|
152
|
+
provider: 'local', shape: SHAPES.generic, endpoint: r.config.endpoint,
|
|
153
|
+
apiKey: r.config.apiKey, model: model || r.config.model,
|
|
154
|
+
egress: 'loopback-only', role,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Explicit BYO endpoint — checked BEFORE the vendor presets because that
|
|
160
|
+
// is the documented precedence: an operator who names an endpoint means
|
|
161
|
+
// that endpoint, even with a preset also set. Reversing it would silently
|
|
162
|
+
// redirect traffic to a vendor.
|
|
163
|
+
const byoEndpoint = _forRole(env, role, 'ENDPOINT');
|
|
164
|
+
if (byoEndpoint) {
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
config: {
|
|
168
|
+
provider: 'byo', shape: SHAPES.generic, endpoint: byoEndpoint,
|
|
169
|
+
apiKey: _forRole(env, role, 'API_KEY') || null,
|
|
170
|
+
model: model || 'unknown', egress: 'remote', role,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 3. Explicit vendor presets.
|
|
176
|
+
if (explicit === 'anthropic' || explicit === 'openai' || explicit === 'gemini') {
|
|
177
|
+
const apiKey = _forRole(env, role, 'API_KEY')
|
|
178
|
+
|| (explicit === 'anthropic' ? env.ANTHROPIC_API_KEY
|
|
179
|
+
: explicit === 'openai' ? env.OPENAI_API_KEY
|
|
180
|
+
: env.GEMINI_API_KEY || env.GOOGLE_API_KEY);
|
|
181
|
+
// No key -> the tier is off, not broken. Same offline-degrading rule the
|
|
182
|
+
// anthropic preset has always had.
|
|
183
|
+
if (!apiKey) return { ok: false, reason: null };
|
|
184
|
+
const m = model || DEFAULT_MODEL[explicit];
|
|
185
|
+
const endpoint = (
|
|
186
|
+
explicit === 'anthropic' ? 'https://api.anthropic.com/v1/messages'
|
|
187
|
+
: explicit === 'openai' ? 'https://api.openai.com/v1/chat/completions'
|
|
188
|
+
: `https://generativelanguage.googleapis.com/v1beta/models/${m}:generateContent?key=${encodeURIComponent(apiKey)}`
|
|
189
|
+
);
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
config: {
|
|
193
|
+
provider: explicit, shape: SHAPES[explicit], endpoint,
|
|
194
|
+
apiKey: explicit === 'gemini' ? null : apiKey, model: m, egress: 'remote', role,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 4. Nothing configured — deterministic engine only.
|
|
200
|
+
return { ok: false, reason: null };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Build the HTTP call for a resolved provider. Pure. */
|
|
204
|
+
export function buildProviderRequest(config, prompt, maxTokens) {
|
|
205
|
+
const headers = config.shape.headers();
|
|
206
|
+
config.shape.auth(headers, config.apiKey);
|
|
207
|
+
return {
|
|
208
|
+
headers,
|
|
209
|
+
body: config.shape.body(config.model, prompt, maxTokens),
|
|
210
|
+
extractText: config.shape.text,
|
|
211
|
+
extractUsage: config.shape.usage,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Which provider each role would use, for reporting. Never includes keys. */
|
|
216
|
+
export function providerMatrix(env = process.env) {
|
|
217
|
+
const out = {};
|
|
218
|
+
for (const role of ROLES) {
|
|
219
|
+
const r = resolveProvider({ role, env });
|
|
220
|
+
out[role] = r.ok
|
|
221
|
+
? { provider: r.config.provider, model: r.config.model, egress: r.config.egress }
|
|
222
|
+
: { provider: null, reason: r.reason || 'not configured' };
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const _internals = { SHAPES, DEFAULT_MODEL, _forRole };
|
package/src/mcp/audit.js
CHANGED
|
@@ -27,6 +27,7 @@ import * as path from 'node:path';
|
|
|
27
27
|
import * as crypto from 'node:crypto';
|
|
28
28
|
import { redactArgsBlob } from './redact.js';
|
|
29
29
|
|
|
30
|
+
import { stateDir } from '../posture/state-dir.js';
|
|
30
31
|
const MAX_ARG_BYTES = 1024;
|
|
31
32
|
const GENESIS = 'GENESIS';
|
|
32
33
|
const REMOTE_TIMEOUT_MS = 1500;
|
|
@@ -87,7 +88,7 @@ export function auditCall({ sessionRoot, tool, args, outcome, reason }) {
|
|
|
87
88
|
let hasMarker = false;
|
|
88
89
|
for (const m of MARKERS) { try { if (fs.existsSync(path.join(sessionRoot, m))) { hasMarker = true; break; } } catch {} }
|
|
89
90
|
if (!hasMarker) return;
|
|
90
|
-
const dir =
|
|
91
|
+
const dir = stateDir(sessionRoot);
|
|
91
92
|
fs.mkdirSync(dir, { recursive: true });
|
|
92
93
|
const logFile = path.join(dir, 'mcp-audit.log');
|
|
93
94
|
const entry = {
|
package/src/mcp/tools.js
CHANGED
|
@@ -157,7 +157,7 @@ function _scratchpadAbs(sessionRoot, relPath) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function _scratchpadTotalBytes(sessionRoot) {
|
|
160
|
-
const base =
|
|
160
|
+
const base = statePath(sessionRoot, 'agent-scratchpad');
|
|
161
161
|
if (!fs.existsSync(base)) return 0;
|
|
162
162
|
let total = 0;
|
|
163
163
|
const walk = (dir) => {
|
|
@@ -222,8 +222,8 @@ function _confine(sessionRoot, candidate, label) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
function _readLastScanVerified(sessionRoot, { allowUnsigned = false } = {}) {
|
|
225
|
-
const
|
|
226
|
-
const scanFile = path.join(
|
|
225
|
+
const stateDirPath = stateDir(sessionRoot);
|
|
226
|
+
const scanFile = path.join(stateDirPath, 'last-scan.json');
|
|
227
227
|
const sigFile = scanFile + '.sig';
|
|
228
228
|
if (!fs.existsSync(scanFile)) return { scan: null, status: 'missing' };
|
|
229
229
|
const body = fs.readFileSync(scanFile, 'utf8');
|
|
@@ -1013,6 +1013,7 @@ export const read_scratchpad = {
|
|
|
1013
1013
|
import { appendAgentsMemory as _appendAgentsMemory, readAgentsMemory as _readAgentsMemory } from '../posture/agents-memory.js';
|
|
1014
1014
|
import { lookupCve as _lookupCve } from '../posture/cve-lookup.js';
|
|
1015
1015
|
|
|
1016
|
+
import { stateDir, statePath } from '../posture/state-dir.js';
|
|
1016
1017
|
export const append_agents_memory = {
|
|
1017
1018
|
name: 'append_agents_memory',
|
|
1018
1019
|
description: 'Append a short narrative entry to AGENTS.md — agent-authored continual-learning notes. Use at session end to record "what worked / what didn\'t / what I\'d try differently next time" so the next agent can pick up the lesson. Bounded: 2 KB per entry, 20 KB total before rotation to AGENTS.md.archive. Use sparingly — narrative, not structured data.',
|
package/src/posture/CLAUDE.md
CHANGED
|
@@ -44,7 +44,7 @@ rather than creating a stray state dir outside a project.
|
|
|
44
44
|
|
|
45
45
|
**Agentic verification** — `verifier.js`, `verifier-target.js`, `verifier-ephemeral.js`, `harness-discovery.js`, `adversary-agent.js`, `defender-agent.js`, `auditor-agent.js`, `three-agent-pipeline.js`.
|
|
46
46
|
|
|
47
|
-
**Methodology additions (
|
|
47
|
+
**Methodology additions (Agentic Methodology PRD, removed post-implementation)** — default-on annotators/artifacts that layer the agentic-hunter methodology on the deterministic engine:
|
|
48
48
|
- `falsification.js` — default falsification pass. For each taint-style finding, tries to DISPROVE it (locate a context-matched control on the path, reusing `dataflow/sanitizer-proof.js`'s shape rules read-only); a blocked finding is demoted + `quarantined`, never removed and never severity-touched (recall-preserving, like `proof-gate`). Wired after `annotateProofGate`. Opt out: `AGENTIC_SECURITY_NO_FALSIFICATION=1`. Optional LLM tier over survivors when an endpoint is configured.
|
|
49
49
|
- `entrypoint-inventory.js` — attack-surface completeness ledger. Enumerates every entry point (HTTP/queue/cron/CLI/env/upload/webhook) with a disposition each; on `scan.entrypointInventory`.
|
|
50
50
|
- `root-cause-sweep.js` — from confirmed findings, finds sibling instances detectors missed with total-count accounting (`found === candidates + mitigated`); on `scan.rootCauseSweep`. Searches the corpus **once per distinct sink pattern**, not once per finding — findings deriving the same pattern share one walk and one set of (read-only) match records. The counts are always exact; the materialised `instances` list is a bounded sample (`INSTANCE_SAMPLE_LIMIT`, 100) and says so via `instancesTruncated`. Both properties are load-bearing on large corpora: the per-finding walk was O(findings × corpus-bytes) and the instance records were O(findings × matches), which together exhausted a 6 GB heap on a 40k-file suite. If you touch this module, keep the own-site exclusion **per pattern group** — resolving it globally makes a group subtract an exclusion it never matched and drives counts negative.
|
|
@@ -80,6 +80,31 @@ Wired in `bin/agentic-security.js` after every filter and after `makeDeterminist
|
|
|
80
80
|
|
|
81
81
|
**Rule lifecycle** — `custom-rules.js` (YAML pattern DSL), `rule-overrides.js` (`disable:` gated on signature), `rule-packs.js`, `rule-synthesis.js` (proposes suppressions from triage feedback), `ruleset-version.js`.
|
|
82
82
|
|
|
83
|
+
**NIST Privacy Framework 1.1 (`privacy-framework.js`)** — assessment + remediation
|
|
84
|
+
over the bundled `compliance-frameworks/nist-privacy-1-1.json` (all 104 controls).
|
|
85
|
+
Sits on top of `auditor-walkthrough.js`'s evaluator and adds the half a narrative
|
|
86
|
+
cannot give you: a gap becomes a FINDING (`family: privacy-compliance`,
|
|
87
|
+
`CWE-359`) carrying an actionable remediation, so it flows through triage and
|
|
88
|
+
`/fix`.
|
|
89
|
+
|
|
90
|
+
Four buckets, and the bucket is always stated: `gap` (mapped signal failing —
|
|
91
|
+
the ONLY bucket that emits a finding), `engine-gap` (NIST rates it code-testable
|
|
92
|
+
but this engine has no signal — disclosed by name, never a pass), `manual` (NIST
|
|
93
|
+
rates it not code-testable), `satisfied`. NIST's own `codeTestable` rating is
|
|
94
|
+
carried per control and is what separates "nobody checked" from "we checked and
|
|
95
|
+
it is fine" — 48 of 104 are governance controls no scanner can assess, and
|
|
96
|
+
reporting those as passed is the failure mode the module exists to prevent.
|
|
97
|
+
|
|
98
|
+
Two guards are load-bearing. **Findings are opt-in**
|
|
99
|
+
(`AGENTIC_SECURITY_PRIVACY_FRAMEWORK=1`); the assessment always lands on
|
|
100
|
+
`scan.privacyFramework` and at `.agentic-security/privacy-framework.{json,md}`,
|
|
101
|
+
but appending to `scan.findings` by default would change every severity count
|
|
102
|
+
and gate verdict downstream. And the **vacuous-satisfaction guard**: a
|
|
103
|
+
`family:`-mapped control clears when no findings of that family are open, which
|
|
104
|
+
is also true of a scan that read zero files — so when nothing was examined every
|
|
105
|
+
mapped control degrades to `engine-gap` instead of reporting as satisfied. That
|
|
106
|
+
one was caught by the module's own test, not in review.
|
|
107
|
+
|
|
83
108
|
**Posture artifacts** — `sbom.js`, `aibom.js`, `api-inventory.js`, `threat-model.js`, `trust-boundary-diagram.js`, `stack-playbook.js`, `deploy-platform.js`, `license-policy.js`, `material-change.js`, `mttr.js`, `streak.js`, `scorecard.js`, `security-trend.js`.
|
|
84
109
|
|
|
85
110
|
**Why this fired** — `why-fired.js`. Runs LAST so it reflects every annotation. Customer-facing provenance.
|
|
@@ -98,6 +123,31 @@ Wired in `bin/agentic-security.js` after every filter and after `makeDeterminist
|
|
|
98
123
|
credibility, orthogonal to `confidence`/`exploitability`: whether the bug was
|
|
99
124
|
*run*, not just reasoned about.
|
|
100
125
|
|
|
126
|
+
**The five proof classes** (`poc-inprocess.js`), and what each observes:
|
|
127
|
+
|
|
128
|
+
| Family | Evidence |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `command-injection`, `code-injection` | the injected payload itself writes the marker |
|
|
131
|
+
| `webhook-missing-signature-verification` | the handler is observed *accepting* an unsigned request |
|
|
132
|
+
| `sql-injection` | the payload reaches a stubbed driver inside the **SQL text** rather than as a bound parameter |
|
|
133
|
+
| `path-traversal` | a sentinel planted outside the served directory comes back out of the handler |
|
|
134
|
+
|
|
135
|
+
The last two need no running application, which is the point: the SQL question
|
|
136
|
+
("text or bound parameter?") is settled where the query crosses into the driver,
|
|
137
|
+
and the traversal question is settled by what the handler hands back. A
|
|
138
|
+
parameterised query and a `basename`-guarded read both reach `proof-failed` by
|
|
139
|
+
**execution**, not by a source pattern.
|
|
140
|
+
|
|
141
|
+
Classes deliberately absent, with reasons, are listed in the module header —
|
|
142
|
+
IDOR (needs two identities and a populated store; a PoC built on invented state
|
|
143
|
+
proves something about the invention), SSRF (the sandbox denies egress, so a
|
|
144
|
+
failed fetch is confinement talking), XSS (a marker file cannot observe a DOM).
|
|
145
|
+
|
|
146
|
+
`extraFiles` on a PoC carries support files that are **not** the vulnerable
|
|
147
|
+
source (the SQL driver stub). `mergePocFiles` merges it under `requires`, which
|
|
148
|
+
always wins — otherwise a template could replace the code it is supposed to
|
|
149
|
+
exploit and prove a fact about itself.
|
|
150
|
+
|
|
101
151
|
**The four tiers** (`PROOF_TIERS`, most-proven first):
|
|
102
152
|
|
|
103
153
|
- `execution-proven` — a generated PoC ran inside the sandbox and the sandbox
|
|
@@ -236,6 +286,57 @@ State lives at `<scanRoot>/.agentic-security/scan-checkpoint.jsonl`; like every
|
|
|
236
286
|
other module here, nothing throws — a failure to open, read or append degrades to
|
|
237
287
|
"no checkpoint", i.e. a normal full scan.
|
|
238
288
|
|
|
289
|
+
## The autonomous loop, the fleet, and the two things that judge them
|
|
290
|
+
|
|
291
|
+
**`autopilot.js`** is the chain — scan → prove → validate → fix → **re-verify** —
|
|
292
|
+
and nothing else. Every stage is injected, so the orchestration is testable
|
|
293
|
+
without an engine or a model; `scripts/autopilot.mjs` is where the real stages
|
|
294
|
+
get wired (a real scan, a real sandboxed exploit, a deterministic-then-model fix,
|
|
295
|
+
and the real gate).
|
|
296
|
+
|
|
297
|
+
The rule that makes it safe to automate: a fix is applied **only** if the PoC
|
|
298
|
+
that proved the bug no longer fires **and** the test suite still passes. Anything
|
|
299
|
+
else is `NEEDS_REVIEW` and is not written. A re-scan proves the *detector* went
|
|
300
|
+
quiet; only re-running the exploit proves the hole is shut. Gates are ON by
|
|
301
|
+
default — `apply` is an explicit opt-in — and the outcome set (`OUTCOMES`) is
|
|
302
|
+
closed, because the report groups on it and an undeclared value would vanish
|
|
303
|
+
from every count. `maxFindings` is reported as `capped`, never applied silently.
|
|
304
|
+
|
|
305
|
+
The CLI refuses to start with no confinement backend (the gate's verdict
|
|
306
|
+
requires executing something) and refuses a dirty git tree by default (the test
|
|
307
|
+
leg writes the candidate patch to disk and restores it in a `finally`, so a
|
|
308
|
+
clean tree is what makes a crash recoverable). A VERIFIED_FIXED reached with no
|
|
309
|
+
test runner detected is counted and reported separately — the exploit stopped
|
|
310
|
+
firing, but nothing checked the application still works.
|
|
311
|
+
|
|
312
|
+
**`fleet.js`** rolls many repositories into one offline page. `renderFleetHtml`
|
|
313
|
+
emits no scripts and no external references, and a repo that FAILED to scan
|
|
314
|
+
always forces a non-zero exit: an unscanned repo is unknown, not clean.
|
|
315
|
+
|
|
316
|
+
**`logic-claims.js` (PRD Epic 6)** is the business-logic tier's other half. The
|
|
317
|
+
deterministic side already existed (`sast/logic.js`, `posture/business-logic.js`);
|
|
318
|
+
what did not was any way to be *wrong* about a claim from the reviewing agent,
|
|
319
|
+
which is prose and was the only tier nothing could disagree with. Three offline
|
|
320
|
+
lenses can refute one: `citation` (the file exists and the line is inside it),
|
|
321
|
+
`quotation` (the quoted snippet is at the cited line ±3), and `corroboration`
|
|
322
|
+
(for kinds that assert something checkable — "this route has no authentication"
|
|
323
|
+
against a handler that plainly authenticates). Verdicts go through
|
|
324
|
+
`verification-separation.js`, so a lens can never vote on a claim it produced.
|
|
325
|
+
Recall-preserving: a refuted claim is `quarantined`, never deleted, never
|
|
326
|
+
severity-touched. Wired in `engine.js`, which reads
|
|
327
|
+
`.agentic-security/logic-claims.json` from the scan root and lands the results
|
|
328
|
+
on `scan.logicVulns` with a summary at `scan.logicClaims`.
|
|
329
|
+
|
|
330
|
+
**`comparison.js` (PRD Epic 7.2)** scores this engine head-to-head against
|
|
331
|
+
participants **the operator supplies** — the repository ships the harness and the
|
|
332
|
+
answer key, never a participant, and a test asserts no tool name appears in
|
|
333
|
+
either file. Two properties are the whole module: every rate is computed over the
|
|
334
|
+
**intersection** of entries *all* participants completed (two tools scored over
|
|
335
|
+
different subsets are not comparable, and the difference is invisible in the
|
|
336
|
+
output), and an entry a participant could not run is **unscored**, never counted
|
|
337
|
+
as a miss. Matching is CWE-only so nobody is scored on this engine's vocabulary.
|
|
338
|
+
Driver: `scripts/comparison.mjs`, over the CVE-replay corpus.
|
|
339
|
+
|
|
239
340
|
## Gotchas
|
|
240
341
|
|
|
241
342
|
- The seed `calibration-seed.json` is small (n < 30 for several families). Don't treat it as a held-out set — that's `holdout-eval.js`'s job, against an externally-supplied JSONL.
|
|
@@ -121,7 +121,14 @@ export function buildScorecard(inputs) {
|
|
|
121
121
|
notGeneralRecall: true,
|
|
122
122
|
notGeneralFalsePositiveRate: true,
|
|
123
123
|
f1Emitted: false,
|
|
124
|
-
|
|
124
|
+
// Was: "no labelled real-world population is available". That stopped
|
|
125
|
+
// being true when bench/independent/ was built, and a stale justification
|
|
126
|
+
// is worse than none — it argues against a measurement that now exists.
|
|
127
|
+
// No F1 is emitted FOR THE CURATED CORPUS, and the reason is narrower:
|
|
128
|
+
// its `post/` fixtures are authored here to be silent, so its precision
|
|
129
|
+
// denominator describes fixture design rather than engine behaviour.
|
|
130
|
+
// F1 over the independent population IS reported, in its own section.
|
|
131
|
+
f1OmissionReason: 'the curated corpus authors both its vulnerable and its fixed fixtures, so an F1 over it would measure fixture design, not accuracy; F1 over the labelled third-party population is reported separately from bench/independent/RESULT.json',
|
|
125
132
|
},
|
|
126
133
|
corpus: {
|
|
127
134
|
measuredThisRun: true,
|
|
@@ -138,6 +145,17 @@ export function buildScorecard(inputs) {
|
|
|
138
145
|
corpusBaseline: committed.corpusBaseline
|
|
139
146
|
? { source: 'bench/cve-replay/corpus-baseline.json', generatedAt: committed.corpusBaseline.generatedAt, total: committed.corpusBaseline.total, passing: committed.corpusBaseline.passing }
|
|
140
147
|
: null,
|
|
148
|
+
independent: committed.independent
|
|
149
|
+
? {
|
|
150
|
+
source: 'bench/independent/RESULT.json',
|
|
151
|
+
measuredAt: committed.independent.measuredAt || null,
|
|
152
|
+
engineVersion: committed.independent.engineVersion || null,
|
|
153
|
+
population: committed.independent.population || null,
|
|
154
|
+
overall: committed.independent.overall || null,
|
|
155
|
+
wide: committed.independent.wide || null,
|
|
156
|
+
byLanguage: committed.independent.byLanguage || null,
|
|
157
|
+
}
|
|
158
|
+
: null,
|
|
141
159
|
proofCorpus: proof
|
|
142
160
|
? {
|
|
143
161
|
source: 'bench/proof-corpus/results/summary.json',
|
|
@@ -307,6 +325,46 @@ export function renderScorecardMarkdown(m) {
|
|
|
307
325
|
}
|
|
308
326
|
L.push('Per-file counts are in `docs/scorecard.json`.');
|
|
309
327
|
L.push('');
|
|
328
|
+
const ind = m.committedInputs.independent;
|
|
329
|
+
if (ind && ind.overall) {
|
|
330
|
+
L.push('## Independent evaluation population — the number that matters');
|
|
331
|
+
L.push('');
|
|
332
|
+
L.push('Everything above is a **regression net**: its fixtures and its labels are both');
|
|
333
|
+
L.push('written here, which is why its detection rate sits at the ceiling by');
|
|
334
|
+
L.push('construction. `bench/independent/` is the other instrument — real upstream code');
|
|
335
|
+
L.push('at the commit where a vulnerability really existed, with the CWE assigned by a');
|
|
336
|
+
L.push('public advisory database rather than by this project.');
|
|
337
|
+
L.push('');
|
|
338
|
+
L.push(`**Measured ${ind.measuredAt} on engine ${ind.engineVersion}, ` +
|
|
339
|
+
`n=${ind.population?.scoredEntries}, ${ind.population?.unscored} unscored** ` +
|
|
340
|
+
'(*committed artifact*, `' + ind.source + '` — read, not re-run: scoring takes ~32 minutes).');
|
|
341
|
+
L.push('');
|
|
342
|
+
L.push('| | Advisory-local (**the claim**) | Wide (diagnostic) |');
|
|
343
|
+
L.push('| --- | --- | --- |');
|
|
344
|
+
L.push(`| Precision | **${formatRate(ind.overall.precision?.n, ind.overall.precision?.d)}** | ${formatRate(ind.wide?.precision?.n, ind.wide?.precision?.d)} |`);
|
|
345
|
+
L.push(`| Recall | **${formatRate(ind.overall.recall?.n, ind.overall.recall?.d)}** | ${formatRate(ind.wide?.recall?.n, ind.wide?.recall?.d)} |`);
|
|
346
|
+
L.push(`| F1 | **${ind.overall.f1 === null || ind.overall.f1 === undefined ? 'n/a' : ind.overall.f1.toFixed(3)}** | ${ind.wide?.f1 === null || ind.wide?.f1 === undefined ? 'n/a' : ind.wide.f1.toFixed(3)} |`);
|
|
347
|
+
L.push('');
|
|
348
|
+
if (ind.byLanguage) {
|
|
349
|
+
L.push('| Language | n | Recall | Precision |');
|
|
350
|
+
L.push('| --- | --- | --- | --- |');
|
|
351
|
+
for (const [k, v] of Object.entries(ind.byLanguage)) {
|
|
352
|
+
L.push(`| ${k} | ${v.entries} | ${formatRate(v.recall?.n, v.recall?.d)} | ${formatRate(v.precision?.n, v.precision?.d)} |`);
|
|
353
|
+
}
|
|
354
|
+
L.push('');
|
|
355
|
+
}
|
|
356
|
+
L.push('**Quote the advisory-local column.** "Wide" scores the same scans without');
|
|
357
|
+
L.push('restricting findings to the files the advisory\'s fix commit touched — it asks');
|
|
358
|
+
L.push('only whether the CWE appeared *anywhere* in the package. Over scopes holding up');
|
|
359
|
+
L.push('to 1740 findings that is close to asking whether the codebase contains the bug');
|
|
360
|
+
L.push('class at all, a question with a much easier yes. It is kept because it is the');
|
|
361
|
+
L.push('only way to tell whether a change moved the engine or moved the benchmark.');
|
|
362
|
+
L.push('');
|
|
363
|
+
L.push('Against ~100% on the curated corpus above. **That gap is the most useful number');
|
|
364
|
+
L.push('in this document**, and publishing it is the point of the exercise. The figure');
|
|
365
|
+
L.push('went DOWN when the benchmark was corrected, and is published that way.');
|
|
366
|
+
L.push('');
|
|
367
|
+
}
|
|
310
368
|
L.push('## Committed artifacts referenced (not re-run by this command)');
|
|
311
369
|
L.push('');
|
|
312
370
|
const cb = m.committedInputs.corpusBaseline;
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import * as fs from 'node:fs';
|
|
30
30
|
import * as path from 'node:path';
|
|
31
31
|
|
|
32
|
+
import { statePath, stateWritesEnabled } from './state-dir.js';
|
|
32
33
|
const MEMORY_FILE = '.agentic-security/AGENTS.md';
|
|
33
34
|
const ARCHIVE_FILE = '.agentic-security/AGENTS.md.archive';
|
|
34
35
|
const MAX_BYTES = 20 * 1024;
|
|
@@ -36,8 +37,8 @@ const MAX_ENTRY_BYTES = 2 * 1024;
|
|
|
36
37
|
const ARCHIVE_MAX_BYTES = 200 * 1024;
|
|
37
38
|
const HEADER = '# AGENTS.md\n\nAgent-authored continual-learning notes. Each entry: timestamp + agent name + one short paragraph. New entries appended at the bottom; oldest entries rotate to AGENTS.md.archive when this file exceeds 20 KB.\n\n';
|
|
38
39
|
|
|
39
|
-
function _resolve(scanRoot) { return
|
|
40
|
-
function _archivePath(scanRoot) { return
|
|
40
|
+
function _resolve(scanRoot) { return statePath(scanRoot, 'AGENTS.md'); }
|
|
41
|
+
function _archivePath(scanRoot) { return statePath(scanRoot, 'AGENTS.md.archive'); }
|
|
41
42
|
|
|
42
43
|
export function readAgentsMemory(scanRoot) {
|
|
43
44
|
const fp = _resolve(scanRoot);
|
|
@@ -65,7 +66,8 @@ export function appendAgentsMemory(scanRoot, { agent, body }) {
|
|
|
65
66
|
const entry = `\n## ${ts} agent: ${agent}\n\n${snippet}\n`;
|
|
66
67
|
try {
|
|
67
68
|
const fp = _resolve(scanRoot);
|
|
68
|
-
|
|
69
|
+
if (!stateWritesEnabled()) return;
|
|
70
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
69
71
|
if (!fs.existsSync(fp)) fs.writeFileSync(fp, HEADER);
|
|
70
72
|
fs.appendFileSync(fp, entry);
|
|
71
73
|
_maybeRotate(scanRoot);
|