@clear-capabilities/agentic-security-scanner 0.132.0 → 0.134.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 +228 -0
- package/bin/agentic-security.js +103 -1
- package/dist/113.index.js +3 -3
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/499.index.js +86 -0
- package/dist/526.index.js +3 -3
- package/dist/609.index.js +741 -0
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +56 -56
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +9 -4
- package/src/discovery/CLAUDE.md +38 -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 +159 -0
- package/src/discovery/judge.js +97 -0
- package/src/discovery/lenses.js +69 -0
- package/src/discovery/llm-invoke.js +31 -0
- package/src/discovery/partition.js +92 -0
- package/src/engine.js +151 -1
- package/src/llm-validator/cost-ceiling.js +199 -0
- package/src/llm-validator/index.js +254 -35
- package/src/llm-validator/local-endpoint.js +90 -0
- package/src/llm-validator/providers.js +227 -0
- package/src/posture/CLAUDE.md +76 -0
- package/src/posture/accuracy-scorecard.js +37 -6
- package/src/posture/autopilot.js +225 -0
- package/src/posture/comparison.js +181 -0
- package/src/posture/corpus-match.js +29 -14
- package/src/posture/execution-proof.js +25 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/integrity.js +42 -9
- package/src/posture/learning.js +8 -1
- package/src/posture/logic-claims.js +266 -0
- package/src/posture/model-routing.js +26 -0
- package/src/posture/model-trust.js +174 -0
- package/src/posture/poc-inprocess.js +567 -0
- package/src/posture/proof-artifact.js +101 -0
- package/src/posture/prove-findings.js +172 -0
- package/src/posture/rule-overrides.js +64 -3
- package/src/posture/state-dir.js +25 -0
- package/src/posture/vuln-archaeology.js +231 -0
- package/src/report/index.js +16 -0
- package/src/sandbox/CLAUDE.md +27 -5
- package/src/sandbox/backend-namespace.js +39 -11
- package/src/sandbox/backend-userspace.js +4 -0
- package/src/sast/CLAUDE.md +4 -0
- package/src/sast/crypto-specialist.js +247 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyMeasuredTrust } from './model-trust.js';
|
|
1
2
|
// Capability-based model routing for cost-sensitive subagent dispatch.
|
|
2
3
|
//
|
|
3
4
|
// A declarative CWE/severity → model policy. When the orchestrator is about to
|
|
@@ -105,6 +106,31 @@ export function routeModelForFinding(finding) {
|
|
|
105
106
|
reason: `${cwe ? `${cwe} at ` : ''}${severity || 'low'} severity is a simple / hardening class — ${LABEL[MODEL_CHEAPEST]} at low effort.` };
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// R13 — measured trust, layered over the capability policy above.
|
|
110
|
+
//
|
|
111
|
+
// The policy is a set of hand-written beliefs about which classes are hard.
|
|
112
|
+
// This lets a class be downgraded to the cheapest model ONLY once that model's
|
|
113
|
+
// measured miss rate clears a Wilson 95% upper bound for that class. The
|
|
114
|
+
// capability route is the floor: measured evidence can permit a downgrade the
|
|
115
|
+
// policy already allows, never promote a class the policy thinks is hard.
|
|
116
|
+
//
|
|
117
|
+
// Without a ledger the behaviour is byte-identical to before — no evidence is
|
|
118
|
+
// not permission, and the default remains the stronger model.
|
|
119
|
+
export function trustKeyFor(finding) {
|
|
120
|
+
return `${MODEL_CHEAPEST}::${parseCwe(finding?.cwe) || 'no-cwe'}::${(finding?.severity || 'unknown').toLowerCase()}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function routeModelWithTrust(finding, ledger = null) {
|
|
124
|
+
const route = routeModelForFinding(finding);
|
|
125
|
+
if (!ledger || route.model === MODEL_CHEAPEST) return { ...route, trust: null };
|
|
126
|
+
return applyMeasuredTrust(
|
|
127
|
+
route,
|
|
128
|
+
{ model: MODEL_CHEAPEST, effort: 'low' },
|
|
129
|
+
ledger,
|
|
130
|
+
trustKeyFor(finding),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
// Route a list of findings. Returns [{ finding, model, effort, reason }, …].
|
|
109
135
|
export function routeModelForFindings(findings) {
|
|
110
136
|
const list = Array.isArray(findings) ? findings : [];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// R13 — multi-model routing gated on MEASURED trust.
|
|
2
|
+
//
|
|
3
|
+
// `model-routing.js` already routes by capability: crypto and auth go to the
|
|
4
|
+
// strongest model, hardening to the cheapest. That policy is a set of
|
|
5
|
+
// hand-written beliefs about which classes are hard. It has never been checked
|
|
6
|
+
// against what the cheap model actually gets wrong.
|
|
7
|
+
//
|
|
8
|
+
// This module supplies the check. A decision class may be downgraded to a
|
|
9
|
+
// cheaper model only once that model's measured MISS RATE clears a statistical
|
|
10
|
+
// bound — specifically the Wilson 95% UPPER bound, not the point estimate.
|
|
11
|
+
//
|
|
12
|
+
// WHY THE UPPER BOUND, AND WHY THAT IS THE WHOLE IDEA. "0 misses in 5 samples"
|
|
13
|
+
// has a point estimate of 0% and looks perfect. Its Wilson upper bound is
|
|
14
|
+
// about 52%: the data is equally consistent with a model that misses half the
|
|
15
|
+
// time. Routing on the point estimate would let five lucky samples hand a
|
|
16
|
+
// security decision to a model that fails constantly. The upper bound is what
|
|
17
|
+
// converts "we have not seen it fail" into "we have enough evidence that it
|
|
18
|
+
// rarely fails", and it is precisely what makes small-n evidence unusable
|
|
19
|
+
// rather than flattering. Small n cannot clear a tight threshold at all — that
|
|
20
|
+
// is the feature, not a limitation to engineer around.
|
|
21
|
+
//
|
|
22
|
+
// FAIL CLOSED. No evidence for a class means no downgrade. The expensive model
|
|
23
|
+
// is the safe default, so absence of data must never read as permission. This
|
|
24
|
+
// is the same rule as everywhere else here: an unverified check is not a pass.
|
|
25
|
+
//
|
|
26
|
+
// A MISS IS ASYMMETRIC. `record()` takes agreement with the trusted model, and
|
|
27
|
+
// what is counted is DISAGREEMENT WHERE THE CHEAP MODEL WAS WRONG — a missed
|
|
28
|
+
// true positive. A cheap model that is merely noisier (extra false positives)
|
|
29
|
+
// costs triage time; one that misses real vulnerabilities costs a breach. Only
|
|
30
|
+
// the second gates routing.
|
|
31
|
+
|
|
32
|
+
// STATUS — MECHANISM ONLY. Nothing in `src/` or `bin/` constructs a ledger, and
|
|
33
|
+
// nothing calls `record()`: there is no adjudication source comparing a cheap
|
|
34
|
+
// model's verdicts against a trusted model's, so no class ever accumulates
|
|
35
|
+
// evidence. The consequence is benign, because the design fails closed — with
|
|
36
|
+
// no observations every class stays on the stronger model — but this is a
|
|
37
|
+
// mechanism waiting for data, not a working routing capability. Do not describe
|
|
38
|
+
// it as one until something feeds it.
|
|
39
|
+
|
|
40
|
+
import { wilsonInterval } from './calibration.js';
|
|
41
|
+
|
|
42
|
+
// The default bar a cheap model must clear: its true miss rate must be below
|
|
43
|
+
// 5% with 95% confidence. Deliberately strict — this gates security decisions,
|
|
44
|
+
// and the cost of being wrong is a missed vulnerability, not a wasted token.
|
|
45
|
+
export const DEFAULT_MAX_MISS_RATE = 0.05;
|
|
46
|
+
|
|
47
|
+
// Below this, no bound is tight enough to matter and we refuse on principle
|
|
48
|
+
// rather than letting an unusually clean run through on a technicality.
|
|
49
|
+
export const MIN_SAMPLES = 30;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* An observation ledger, keyed by decision class.
|
|
53
|
+
*
|
|
54
|
+
* A "decision class" is whatever granularity routing decides at — this module
|
|
55
|
+
* does not impose one. `model-routing.js` decides by CWE family and severity,
|
|
56
|
+
* so `${model}::${cwe}` is the natural key there.
|
|
57
|
+
*/
|
|
58
|
+
export function createTrustLedger({ maxMissRate = DEFAULT_MAX_MISS_RATE, minSamples = MIN_SAMPLES } = {}) {
|
|
59
|
+
/** @type {Map<string, {n:number, misses:number}>} */
|
|
60
|
+
const classes = new Map();
|
|
61
|
+
|
|
62
|
+
function _get(key) {
|
|
63
|
+
let c = classes.get(key);
|
|
64
|
+
if (!c) { c = { n: 0, misses: 0 }; classes.set(key, c); }
|
|
65
|
+
return c;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
maxMissRate,
|
|
70
|
+
minSamples,
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Record one adjudicated decision.
|
|
74
|
+
* @param {string} key decision class
|
|
75
|
+
* @param {boolean} missed did the cheap model MISS something the trusted
|
|
76
|
+
* model caught? Extra false positives are not misses.
|
|
77
|
+
*/
|
|
78
|
+
record(key, missed) {
|
|
79
|
+
if (typeof key !== 'string' || !key) return false;
|
|
80
|
+
const c = _get(key);
|
|
81
|
+
c.n++;
|
|
82
|
+
if (missed) c.misses++;
|
|
83
|
+
return true;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
observations(key) {
|
|
87
|
+
const c = classes.get(key);
|
|
88
|
+
return c ? { ...c } : { n: 0, misses: 0 };
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* May this class be routed to the cheaper model?
|
|
93
|
+
* @returns {{allowed:boolean, reason:string, n:number, misses:number,
|
|
94
|
+
* missRate:number|null, upperBound:number|null}}
|
|
95
|
+
*/
|
|
96
|
+
verdict(key) {
|
|
97
|
+
const { n, misses } = this.observations(key);
|
|
98
|
+
if (n < minSamples) {
|
|
99
|
+
return {
|
|
100
|
+
allowed: false, n, misses, missRate: n ? misses / n : null, upperBound: null,
|
|
101
|
+
reason: `only ${n} adjudicated sample(s) for '${key}'; ${minSamples} are required before a `
|
|
102
|
+
+ 'downgrade can be justified. No evidence is not permission — the stronger model stands.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// wilsonInterval is written for a SUCCESS count; the miss rate's interval
|
|
106
|
+
// is the same computation with misses as the successes.
|
|
107
|
+
const [, upper] = wilsonInterval(misses, n);
|
|
108
|
+
const missRate = misses / n;
|
|
109
|
+
if (upper > maxMissRate) {
|
|
110
|
+
return {
|
|
111
|
+
allowed: false, n, misses, missRate, upperBound: upper,
|
|
112
|
+
reason: `measured miss rate ${(missRate * 100).toFixed(1)}% over ${n} samples, but the 95% upper `
|
|
113
|
+
+ `bound is ${(upper * 100).toFixed(1)}% — above the ${(maxMissRate * 100).toFixed(1)}% bar. `
|
|
114
|
+
+ 'The point estimate is not the claim; the data is still consistent with a worse model.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
allowed: true, n, misses, missRate, upperBound: upper,
|
|
119
|
+
reason: `${misses} miss(es) in ${n} samples; 95% upper bound ${(upper * 100).toFixed(1)}% is within the `
|
|
120
|
+
+ `${(maxMissRate * 100).toFixed(1)}% bar, so the cheaper model is justified for '${key}'.`,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/** Every class with its verdict, for reporting. */
|
|
125
|
+
report() {
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const key of [...classes.keys()].sort()) out[key] = this.verdict(key);
|
|
128
|
+
return out;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Apply measured trust on top of a capability route.
|
|
135
|
+
*
|
|
136
|
+
* The capability route is the FLOOR, never overridden upward by this module:
|
|
137
|
+
* measured trust can only permit a downgrade that policy already proposed, it
|
|
138
|
+
* cannot promote a class the policy thinks is hard. Evidence about a cheap
|
|
139
|
+
* model's miss rate says nothing about whether a hard class deserves a strong
|
|
140
|
+
* one.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} route from routeModelForFinding: {model, effort, reason}
|
|
143
|
+
* @param {object} proposal the cheaper alternative: {model, effort}
|
|
144
|
+
* @param {object} ledger createTrustLedger()
|
|
145
|
+
* @param {string} key decision class
|
|
146
|
+
*/
|
|
147
|
+
export function applyMeasuredTrust(route, proposal, ledger, key) {
|
|
148
|
+
if (!route || !proposal || !ledger) return { ...route, trust: null };
|
|
149
|
+
const v = ledger.verdict(key);
|
|
150
|
+
if (!v.allowed) {
|
|
151
|
+
return {
|
|
152
|
+
...route,
|
|
153
|
+
trust: { ...v, applied: false },
|
|
154
|
+
reason: `${route.reason} Downgrade withheld: ${v.reason}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
model: proposal.model,
|
|
159
|
+
effort: proposal.effort ?? route.effort,
|
|
160
|
+
trust: { ...v, applied: true },
|
|
161
|
+
reason: `Downgraded to ${proposal.model} on measured evidence: ${v.reason}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** One-line summary of a trust report. */
|
|
166
|
+
export function renderTrustSummary(report) {
|
|
167
|
+
const keys = Object.keys(report || {});
|
|
168
|
+
if (!keys.length) return null;
|
|
169
|
+
const allowed = keys.filter(k => report[k].allowed);
|
|
170
|
+
const short = keys.filter(k => report[k].n < MIN_SAMPLES);
|
|
171
|
+
return `measured-trust routing: ${allowed.length}/${keys.length} class(es) cleared the `
|
|
172
|
+
+ `95% upper-bound bar; ${short.length} still below the ${MIN_SAMPLES}-sample minimum `
|
|
173
|
+
+ '(those keep the stronger model).';
|
|
174
|
+
}
|