@clear-capabilities/agentic-security-scanner 0.152.0 → 0.153.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/dist/1336.index.js +16010 -0
- package/dist/agentic-security.mjs +3 -3
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -3
- package/src/dataflow/backward.js +104 -7
- package/src/dataflow/cross-service-taint.js +154 -11
- package/src/dataflow/engine.js +23 -1
- package/src/dataflow/exploit-prover.js +111 -23
- package/src/dataflow/index.js +12 -2
- package/src/dataflow/string-domain.js +36 -0
- package/src/engine.js +10 -6
- package/src/ir/class-hierarchy.js +160 -1
- package/src/report/index.js +12 -0
- package/src/dataflow/smt-feasibility.js +0 -189
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
// SMT path feasibility — Recommendation #3 of the world-class roadmap.
|
|
2
|
-
//
|
|
3
|
-
// For top-N findings per scan, generate SMT constraints from the IR
|
|
4
|
-
// representing the conditions that must hold along the call-graph path
|
|
5
|
-
// from source to sink. Discharge via a Z3 solver. If UNSAT, the
|
|
6
|
-
// finding is provably infeasible and gets demoted to 'info' severity
|
|
7
|
-
// with `pathFeasibility: 'unsat'`. If SAT, we emit a sample witness
|
|
8
|
-
// (a concrete tainted input that triggers the sink) which is gold-standard
|
|
9
|
-
// evidence for the developer.
|
|
10
|
-
//
|
|
11
|
-
// Solver backend: prefers `z3-solver` (Z3 WASM published on npm) when
|
|
12
|
-
// installed; falls back to a constraint-emission-only mode that still
|
|
13
|
-
// records the SMT-LIB script so a CI step can discharge it offline.
|
|
14
|
-
//
|
|
15
|
-
// Gating: opt-in via AGENTIC_SECURITY_SMT_FEASIBILITY=1. Always bounded
|
|
16
|
-
// at top-MAX_PROOF_OBLIGATIONS findings per scan to keep wall-clock
|
|
17
|
-
// under PROOF_BUDGET_MS.
|
|
18
|
-
//
|
|
19
|
-
// IMPORTANT — this module is NOT a generic symbolic executor. It targets
|
|
20
|
-
// a narrow shape: "does there exist an input that flows from source S
|
|
21
|
-
// through path P to sink K?" That's enough to prove or refute the
|
|
22
|
-
// reachability claim on a finding the engine already produced. We do
|
|
23
|
-
// NOT attempt to prove arbitrary safety properties.
|
|
24
|
-
|
|
25
|
-
const PROOF_BUDGET_MS_DEFAULT = 30_000;
|
|
26
|
-
const MAX_PROOF_OBLIGATIONS_DEFAULT = 50;
|
|
27
|
-
const PER_QUERY_TIMEOUT_MS_DEFAULT = 5_000;
|
|
28
|
-
|
|
29
|
-
// Lazy-load Z3. The module is permitted to be absent — when it is, we
|
|
30
|
-
// fall back to constraint-emission-only mode (the SMT-LIB script is
|
|
31
|
-
// attached to the finding for offline discharge).
|
|
32
|
-
let _z3Mod = null;
|
|
33
|
-
let _z3LoadAttempted = false;
|
|
34
|
-
async function _loadZ3() {
|
|
35
|
-
if (_z3LoadAttempted) return _z3Mod;
|
|
36
|
-
_z3LoadAttempted = true;
|
|
37
|
-
try {
|
|
38
|
-
_z3Mod = await import('z3-solver');
|
|
39
|
-
if (typeof _z3Mod.init === 'function') await _z3Mod.init();
|
|
40
|
-
} catch { _z3Mod = null; }
|
|
41
|
-
return _z3Mod;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── Constraint emission ───────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Encode a single IR predicate (one node along the path) into an SMT-LIB
|
|
48
|
-
* assertion. Predicates supported in v1:
|
|
49
|
-
* - `var = source(name)` — declares var as a free symbolic string
|
|
50
|
-
* - `var = const(literal)` — equality with a constant
|
|
51
|
-
* - `var = concat(a, b)` — string concatenation
|
|
52
|
-
* - `var = sanitize(x, kind)` — applies a sanitizer; encoded as
|
|
53
|
-
* `var = "safe"` (forces concrete)
|
|
54
|
-
* - `assert reach(line N)` — terminal predicate: this line must be
|
|
55
|
-
* reachable
|
|
56
|
-
* - `guard(cond)` — a path condition (free-form text)
|
|
57
|
-
*/
|
|
58
|
-
function encodePredicate(p, idx) {
|
|
59
|
-
switch (p.kind) {
|
|
60
|
-
case 'source':
|
|
61
|
-
return `(declare-const ${p.var} String)`;
|
|
62
|
-
case 'const':
|
|
63
|
-
return `(assert (= ${p.var} ${JSON.stringify(p.value)}))`;
|
|
64
|
-
case 'concat':
|
|
65
|
-
return `(assert (= ${p.var} (str.++ ${p.a} ${p.b})))`;
|
|
66
|
-
case 'sanitize':
|
|
67
|
-
return `(assert (= ${p.var} "safe-${p.kind}-${idx}"))`;
|
|
68
|
-
case 'reach':
|
|
69
|
-
// Symbolic "this line is reached" — we don't really model reachability,
|
|
70
|
-
// we just record the obligation. The presence of the path is what
|
|
71
|
-
// matters; SAT just means "some input satisfies the path conditions."
|
|
72
|
-
return `; reach(${p.file}:${p.line})`;
|
|
73
|
-
case 'guard':
|
|
74
|
-
return `(assert ${p.smtCond || `(= ${p.var} ${JSON.stringify(p.value)})`})`;
|
|
75
|
-
default:
|
|
76
|
-
return `; unsupported predicate kind: ${p.kind}`;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Emit a complete SMT-LIB script for one finding. The script declares
|
|
82
|
-
* source variables, asserts every predicate, asks (check-sat). On SAT
|
|
83
|
-
* we (get-model) for the witness; on UNSAT the finding is infeasible.
|
|
84
|
-
*/
|
|
85
|
-
export function emitSmtScript(predicates, opts = {}) {
|
|
86
|
-
const lines = [];
|
|
87
|
-
lines.push('; SMT-LIB script — emitted by scanner/src/dataflow/smt-feasibility.js');
|
|
88
|
-
lines.push(`(set-logic QF_S)`);
|
|
89
|
-
lines.push(`(set-option :timeout ${opts.timeoutMs || PER_QUERY_TIMEOUT_MS_DEFAULT})`);
|
|
90
|
-
predicates.forEach((p, i) => lines.push(encodePredicate(p, i)));
|
|
91
|
-
lines.push('(check-sat)');
|
|
92
|
-
lines.push('(get-model)');
|
|
93
|
-
return lines.join('\n');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Z3 discharge ──────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* dischargeFinding(predicates, opts) — encode + solve. Returns one of:
|
|
100
|
-
* { verdict: 'sat', witness: { var: value } }
|
|
101
|
-
* { verdict: 'unsat' }
|
|
102
|
-
* { verdict: 'unknown', reason: '<why>' }
|
|
103
|
-
* { verdict: 'pending', script: '<smt-lib text>' } // when Z3 unavailable
|
|
104
|
-
*/
|
|
105
|
-
export async function dischargeFinding(predicates, opts = {}) {
|
|
106
|
-
if (!predicates || !predicates.length) return { verdict: 'unknown', reason: 'no-predicates' };
|
|
107
|
-
const script = emitSmtScript(predicates, opts);
|
|
108
|
-
const z3 = await _loadZ3();
|
|
109
|
-
if (!z3) return { verdict: 'pending', script };
|
|
110
|
-
try {
|
|
111
|
-
const { Context } = z3;
|
|
112
|
-
const ctx = new Context('main');
|
|
113
|
-
const solver = new ctx.Solver();
|
|
114
|
-
// Feed the script via parse — z3-solver supports SMT-LIB ingestion.
|
|
115
|
-
try { solver.fromString(script); }
|
|
116
|
-
catch (e) {
|
|
117
|
-
return { verdict: 'unknown', reason: 'parse-error: ' + String(e && e.message), script };
|
|
118
|
-
}
|
|
119
|
-
const start = Date.now();
|
|
120
|
-
const result = await Promise.race([
|
|
121
|
-
solver.check(),
|
|
122
|
-
new Promise(resolve => setTimeout(() => resolve('timeout'), opts.timeoutMs || PER_QUERY_TIMEOUT_MS_DEFAULT)),
|
|
123
|
-
]);
|
|
124
|
-
const elapsed = Date.now() - start;
|
|
125
|
-
if (result === 'unsat') return { verdict: 'unsat', elapsedMs: elapsed };
|
|
126
|
-
if (result === 'timeout' || result === 'unknown') return { verdict: 'unknown', reason: result, elapsedMs: elapsed, script };
|
|
127
|
-
if (result === 'sat') {
|
|
128
|
-
// Best-effort witness extraction.
|
|
129
|
-
let witness = {};
|
|
130
|
-
try {
|
|
131
|
-
const model = solver.model();
|
|
132
|
-
for (const decl of model.decls()) witness[decl.name()] = String(model.get(decl));
|
|
133
|
-
} catch { /* no model */ }
|
|
134
|
-
return { verdict: 'sat', witness, elapsedMs: elapsed };
|
|
135
|
-
}
|
|
136
|
-
return { verdict: 'unknown', reason: String(result), elapsedMs: elapsed };
|
|
137
|
-
} catch (e) {
|
|
138
|
-
return { verdict: 'unknown', reason: String(e && e.message || e), script };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ── Finding-level integration ─────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Annotate the top-N findings with their feasibility verdict. Modifies
|
|
146
|
-
* findings in place — each gets a `pathFeasibility` field and (when
|
|
147
|
-
* SAT) a `feasibilityWitness` object. Findings whose verdict is UNSAT
|
|
148
|
-
* are demoted to 'info' severity.
|
|
149
|
-
*/
|
|
150
|
-
export async function annotatePathFeasibility(findings, opts = {}) {
|
|
151
|
-
if (!Array.isArray(findings)) return { annotated: 0, demoted: 0 };
|
|
152
|
-
const budget = opts.budgetMs || PROOF_BUDGET_MS_DEFAULT;
|
|
153
|
-
const max = opts.maxObligations || MAX_PROOF_OBLIGATIONS_DEFAULT;
|
|
154
|
-
// Prioritize: critical/high findings with concrete chains first.
|
|
155
|
-
const sorted = [...findings]
|
|
156
|
-
.filter(f => f.severity === 'critical' || f.severity === 'high')
|
|
157
|
-
.filter(f => Array.isArray(f.chain) || Array.isArray(f.taintPath))
|
|
158
|
-
.slice(0, max);
|
|
159
|
-
const start = Date.now();
|
|
160
|
-
let annotated = 0, demoted = 0;
|
|
161
|
-
for (const f of sorted) {
|
|
162
|
-
if (Date.now() - start > budget) {
|
|
163
|
-
f.pathFeasibility = 'unknown';
|
|
164
|
-
f.feasibilityReason = 'budget-exceeded';
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const predicates = (f.chain || f.taintPath || []).map((step, i) => ({
|
|
168
|
-
kind: i === 0 ? 'source' : (step.kind || 'concat'),
|
|
169
|
-
var: `v${i}`,
|
|
170
|
-
a: `v${Math.max(0, i - 1)}`, b: '""',
|
|
171
|
-
value: step.value || '',
|
|
172
|
-
file: step.file, line: step.line,
|
|
173
|
-
}));
|
|
174
|
-
const r = await dischargeFinding(predicates, { timeoutMs: Math.min(5_000, budget) });
|
|
175
|
-
f.pathFeasibility = r.verdict;
|
|
176
|
-
if (r.witness) f.feasibilityWitness = r.witness;
|
|
177
|
-
if (r.script) f._smtScript = r.script.slice(0, 4000);
|
|
178
|
-
annotated++;
|
|
179
|
-
if (r.verdict === 'unsat') {
|
|
180
|
-
const before = f.severity;
|
|
181
|
-
f.severity = 'info';
|
|
182
|
-
f._pathFeasibilityDemoted = before;
|
|
183
|
-
demoted++;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return { annotated, demoted, elapsedMs: Date.now() - start };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export const _internals = { encodePredicate, emitSmtScript, PROOF_BUDGET_MS_DEFAULT, MAX_PROOF_OBLIGATIONS_DEFAULT };
|