@clear-capabilities/agentic-security-scanner 0.185.0 → 0.186.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 +34 -0
- package/bin/agentic-security.js +66 -18
- package/dist/1128.index.js +25 -21
- package/dist/{5817.index.js → 1602.index.js} +378 -29
- package/dist/2584.index.js +1 -1
- package/dist/4171.index.js +2 -2
- package/dist/{0b301d45b231f144a1b4.json → 4d06c8944ae121d7085c.json} +8 -8
- package/dist/5752.index.js +466 -0
- package/dist/7405.index.js +1 -1
- package/dist/8158.index.js +1713 -0
- package/dist/8798.index.js +1 -1
- package/dist/9382.index.js +2 -2
- package/dist/9560.index.js +1 -1
- package/dist/9579.index.js +1 -1
- package/dist/agentic-security.mjs +29 -29
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -8
- package/src/dataflow/CLAUDE.md +6 -3
- package/src/dataflow/access-paths.js +12 -2
- package/src/dataflow/demand-query.js +279 -40
- package/src/dataflow/dep-fingerprint.js +1 -1
- package/src/dataflow/exploit-prover.js +6 -2
- package/src/dataflow/ir-cache.js +268 -0
- package/src/dataflow/privacy-taint.js +7 -0
- package/src/engine.js +62 -5
- package/src/ir/CLAUDE.md +4 -0
- package/src/ir/callgraph.js +36 -8
- package/src/ir/index.js +18 -4
- package/src/ir/parser-js.js +10 -0
- package/src/language/CLAUDE.md +13 -0
- package/src/language/nix-option-types.js +170 -0
- package/src/language/nix-script-taint.js +46 -3
- package/src/language/nix-secrets.js +20 -2
- package/src/lineage/CLAUDE.md +2 -1
- package/src/lineage/smt-orchestrator.js +47 -11
- package/src/lineage/smt-z3-backend.js +294 -0
- package/src/posture/CLAUDE.md +5 -1
- package/src/posture/business-logic.js +16 -7
- package/src/posture/dag-commands.js +442 -0
- package/src/posture/finding-defaults.js +19 -0
- package/src/posture/git-history.js +91 -1
- package/src/posture/proof-fabric/cli.js +1 -1
- package/src/posture/proof-fabric/container.js +2 -2
- package/src/posture/proof-fabric/generate-haskell.js +205 -0
- package/src/posture/proof-fabric/generate.js +6 -3
- package/src/posture/proof-fabric/reproducer.js +4 -2
- package/src/posture/proof-fabric/runner.js +4 -4
- package/src/sast/CLAUDE.md +2 -0
- package/src/sast/_call-args.js +70 -0
- package/src/sast/init-scope.js +87 -0
- package/src/sast/listen-bind-address.js +49 -0
- package/src/sast/redirect-target.js +106 -0
- package/src/sast/reflected-input.js +76 -0
- package/src/sast/send-file-root.js +27 -0
- package/src/sast/sensitive-data-logging.js +38 -1
- package/dist/119.index.js +0 -197
- package/dist/128.index.js +0 -129
- package/dist/1293.index.js +0 -5689
- package/dist/1336.index.js +0 -16331
- package/dist/1351.index.js +0 -133
- package/dist/1387.index.js +0 -271
- package/dist/1437.index.js +0 -23
- package/dist/1445.index.js +0 -110
- package/dist/1505.index.js +0 -151
- package/dist/1557.index.js +0 -293
- package/dist/1621.index.js +0 -311
- package/dist/1751.index.js +0 -160
- package/dist/1765.index.js +0 -727
- package/dist/1861.index.js +0 -690
- package/dist/1906.index.js +0 -433
- package/dist/208.index.js +0 -1631
- package/dist/2223.index.js +0 -159
- package/dist/2238.index.js +0 -295
- package/dist/2530.index.js +0 -152
- package/dist/2565.index.js +0 -116
- package/dist/2574.index.js +0 -127
- package/dist/2620.index.js +0 -345
- package/dist/2773.index.js +0 -98
- package/dist/2775.index.js +0 -32
- package/dist/2966.index.js +0 -171
- package/dist/2972.index.js +0 -87
- package/dist/2991.index.js +0 -131
- package/dist/3063.index.js +0 -833
- package/dist/3075.index.js +0 -227
- package/dist/3121.index.js +0 -297
- package/dist/3314.index.js +0 -263
- package/dist/3480.index.js +0 -1822
- package/dist/3481.index.js +0 -24
- package/dist/3506.index.js +0 -268
- package/dist/3533.index.js +0 -591
- package/dist/3653.index.js +0 -450
- package/dist/3740.index.js +0 -722
- package/dist/3822.index.js +0 -131
- package/dist/3868.index.js +0 -292
- package/dist/3918.index.js +0 -192
- package/dist/4090.index.js +0 -288
- package/dist/4113.index.js +0 -1286
- package/dist/4121.index.js +0 -1190
- package/dist/4142.index.js +0 -690
- package/dist/4168.index.js +0 -509
- package/dist/4281.index.js +0 -993
- package/dist/4336.index.js +0 -436
- package/dist/4387.index.js +0 -532
- package/dist/4413.index.js +0 -302
- package/dist/4476.index.js +0 -4510
- package/dist/4491.index.js +0 -133
- package/dist/4538.index.js +0 -185
- package/dist/4684.index.js +0 -319
- package/dist/4740.index.js +0 -165
- package/dist/4780.index.js +0 -209
- package/dist/4802.index.js +0 -225
- package/dist/4812.index.js +0 -379
- package/dist/4835.index.js +0 -126
- package/dist/494.index.js +0 -226
- package/dist/4991.index.js +0 -549
- package/dist/5199.index.js +0 -305
- package/dist/5216.index.js +0 -109
- package/dist/5439.index.js +0 -301
- package/dist/5443.index.js +0 -97
- package/dist/5496.index.js +0 -377
- package/dist/5738.index.js +0 -240
- package/dist/5801.index.js +0 -588
- package/dist/5858.index.js +0 -223
- package/dist/5933.index.js +0 -143
- package/dist/601.index.js +0 -23
- package/dist/6173.index.js +0 -273
- package/dist/6291.index.js +0 -1871
- package/dist/6312.index.js +0 -666
- package/dist/634.index.js +0 -16182
- package/dist/644.index.js +0 -87
- package/dist/6508.index.js +0 -283
- package/dist/6560.index.js +0 -645
- package/dist/6576.index.js +0 -422
- package/dist/6579.index.js +0 -271
- package/dist/6655.index.js +0 -3221
- package/dist/6745.index.js +0 -305
- package/dist/6818.index.js +0 -21642
- package/dist/6932.index.js +0 -1190
- package/dist/6972.index.js +0 -167
- package/dist/6995.index.js +0 -163
- package/dist/7249.index.js +0 -191
- package/dist/7372.index.js +0 -1197
- package/dist/7546.index.js +0 -541
- package/dist/76.index.js +0 -448
- package/dist/7879.index.js +0 -163
- package/dist/8025.index.js +0 -130
- package/dist/8054.index.js +0 -248
- package/dist/808.index.js +0 -1330
- package/dist/8097.index.js +0 -201
- package/dist/8126.index.js +0 -191
- package/dist/8178.index.js +0 -78
- package/dist/8382.index.js +0 -93
- package/dist/8408.index.js +0 -261
- package/dist/8540.index.js +0 -174
- package/dist/8635.index.js +0 -802
- package/dist/8688.index.js +0 -410
- package/dist/8699.index.js +0 -779
- package/dist/8891.index.js +0 -457
- package/dist/8916.index.js +0 -885
- package/dist/9146.index.js +0 -170
- package/dist/9168.index.js +0 -841
- package/dist/9292.index.js +0 -369
- package/dist/9422.index.js +0 -866
- package/dist/9440.index.js +0 -453
- package/dist/9479.index.js +0 -152
- package/dist/9481.index.js +0 -220
- package/dist/9539.index.js +0 -100
- package/dist/9547.index.js +0 -103
- package/dist/9692.index.js +0 -90
- package/dist/9800.index.js +0 -462
- package/dist/9899.index.js +0 -138
- package/dist/9981.index.js +0 -206
- package/dist/999.index.js +0 -117
- package/dist/9999.index.js +0 -1698
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
//
|
|
2
|
+
// smt-z3-backend.js: a real solver for the SMT orchestrator (DAG-04, R04.3 / R04.4).
|
|
3
|
+
//
|
|
4
|
+
// The orchestrator (smt-orchestrator.js) is built against an injectable backend object
|
|
5
|
+
// { name, version?, available?, check(problem, { signal }) -> Promise<{ result, reason? }> }.
|
|
6
|
+
// This module is that backend, on top of the optional `z3-solver` package. Rules it keeps:
|
|
7
|
+
//
|
|
8
|
+
// * LAZY and OPTIONAL. `z3-solver` is imported on the first query, never at module load, and never by a
|
|
9
|
+
// default scan (the whole path is behind `pathEvidence` and `AGENTIC_SECURITY_LINEAGE_SMT=z3`). A package
|
|
10
|
+
// that is not installed, fails to load or fails to initialise makes the backend `unsupported`
|
|
11
|
+
// (`available === false`): never a crash and never an `unsat`.
|
|
12
|
+
// * ONLY A VERIFIED ENCODING MAY SAY `unsat`. `encodeProblem` refuses (result `unknown`, with a reason) any
|
|
13
|
+
// problem it cannot translate faithfully: a variable used with two sorts, a number SMT-LIB cannot write
|
|
14
|
+
// exactly, an unknown literal or operator. An opaque (unmodeled) predicate is encoded as `true`, which can
|
|
15
|
+
// only make the formula LARGER, so `unsat` stays sound; for the same reason a `sat` that leaned on an opaque
|
|
16
|
+
// predicate is reported `unknown`, not as a model.
|
|
17
|
+
// * Numbers are Real, not Int, so a comparison such as `0.5 < x < 1` is not wrongly called infeasible.
|
|
18
|
+
// * BOUNDED. A per-query timeout (`timeoutMs`), a process memory cap (`memoryMb`, Z3's `memory_max_size`),
|
|
19
|
+
// and the orchestrator's own deadline racing the query. A timeout is `timeout`, memory exhaustion and any
|
|
20
|
+
// other solver failure is `unknown`. None of them ever reduces anything.
|
|
21
|
+
// * DETERMINISTIC. Variables are renamed v0..vN in sorted order, assertions keep their order, the solver
|
|
22
|
+
// random seed is fixed, and the SMT-LIB text is the cache key. Only definitive answers (`sat`, `unsat`)
|
|
23
|
+
// are cached; a timeout depends on the machine and is not.
|
|
24
|
+
//
|
|
25
|
+
// `unsat` here means "the encoded conditions cannot hold together", relative to the model stated in
|
|
26
|
+
// path-predicates.js. It is not a proof about the running program, and `sat` never implies exploitability.
|
|
27
|
+
|
|
28
|
+
import crypto from 'node:crypto';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
|
|
33
|
+
export const Z3_BACKEND_VERSION = 'smt-z3/1';
|
|
34
|
+
export const Z3_DEFAULT_TIMEOUT_MS = 1000;
|
|
35
|
+
export const Z3_DEFAULT_MEMORY_MB = 256;
|
|
36
|
+
const CACHE_MAX = 256;
|
|
37
|
+
const HARD_GRACE_MS = 500;
|
|
38
|
+
// 1e21 and 1e-7 are where String(number) switches to exponent form, which SMT-LIB has no literal for.
|
|
39
|
+
const MAX_PLAIN = 1e15;
|
|
40
|
+
|
|
41
|
+
// ---- encoding ------------------------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const OPS = { '<': '<', '<=': '<=', '>': '>', '>=': '>=', '==': '=', '===': '=', '!=': 'distinct', '!==': 'distinct' };
|
|
44
|
+
|
|
45
|
+
/** A finite JS number as an SMT-LIB Real literal, or null when it cannot be written exactly in plain decimal. */
|
|
46
|
+
function realLiteral(n) {
|
|
47
|
+
if (typeof n !== 'number' || !Number.isFinite(n) || Math.abs(n) >= MAX_PLAIN) return null;
|
|
48
|
+
const s = String(n);
|
|
49
|
+
if (/e/i.test(s)) return null;
|
|
50
|
+
const abs = Math.abs(n);
|
|
51
|
+
const body = Number.isInteger(abs) ? `${abs}.0` : String(abs);
|
|
52
|
+
return n < 0 ? `(- ${body})` : body;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A JS string as an SMT-LIB 2.6 string literal: only printable ASCII is written raw. */
|
|
56
|
+
function stringLiteral(s) {
|
|
57
|
+
let out = '';
|
|
58
|
+
for (const ch of String(s)) {
|
|
59
|
+
const cp = ch.codePointAt(0);
|
|
60
|
+
if (ch === '"') out += '""';
|
|
61
|
+
else if (cp >= 0x20 && cp <= 0x7e && ch !== '\\') out += ch;
|
|
62
|
+
else out += `\\u{${cp.toString(16)}}`;
|
|
63
|
+
}
|
|
64
|
+
return `"${out}"`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Translate the solver-neutral problem (path-predicates.js `toSolverProblem`) into SMT-LIB 2 text.
|
|
69
|
+
*
|
|
70
|
+
* @returns {{ok:true, text:string, digest:string, unmodeled:number, sorts:object, literals:number, assertions:number}
|
|
71
|
+
* | {ok:false, reason:string}}
|
|
72
|
+
*/
|
|
73
|
+
export function encodeProblem(problem) {
|
|
74
|
+
if (!problem || typeof problem !== 'object' || !Array.isArray(problem.assertions) || !Array.isArray(problem.vars)) {
|
|
75
|
+
return { ok: false, reason: 'encoding-unverified:malformed-problem' };
|
|
76
|
+
}
|
|
77
|
+
const sorts = new Map();
|
|
78
|
+
const claim = (v, sort) => {
|
|
79
|
+
if (typeof v !== 'string') return false;
|
|
80
|
+
if (sorts.has(v) && sorts.get(v) !== sort) return false;
|
|
81
|
+
sorts.set(v, sort);
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
const constants = problem.constants && typeof problem.constants === 'object' ? problem.constants : {};
|
|
85
|
+
let literals = 0; let unmodeled = 0;
|
|
86
|
+
const alt = [];
|
|
87
|
+
for (const a of problem.assertions) {
|
|
88
|
+
if (!a || !Array.isArray(a.alternatives)) return { ok: false, reason: 'encoding-unverified:malformed-assertion' };
|
|
89
|
+
for (const conj of a.alternatives) {
|
|
90
|
+
if (!Array.isArray(conj)) return { ok: false, reason: 'encoding-unverified:malformed-assertion' };
|
|
91
|
+
for (const l of conj) {
|
|
92
|
+
literals += 1;
|
|
93
|
+
if (!l || typeof l !== 'object') return { ok: false, reason: 'encoding-unverified:malformed-literal' };
|
|
94
|
+
if (l.t === 'num') {
|
|
95
|
+
if (!OPS[l.op]) return { ok: false, reason: 'encoding-unverified:unknown-operator' };
|
|
96
|
+
for (const s of [l.left, l.right]) {
|
|
97
|
+
if (!s || typeof s !== 'object') return { ok: false, reason: 'encoding-unverified:malformed-literal' };
|
|
98
|
+
if ('var' in s) { if (!claim(s.var, 'Real')) return { ok: false, reason: 'encoding-unverified:sort-conflict' }; }
|
|
99
|
+
else if (realLiteral(s.const) === null) return { ok: false, reason: 'encoding-unverified:number-not-encodable' };
|
|
100
|
+
}
|
|
101
|
+
} else if (l.t === 'eq') {
|
|
102
|
+
if (typeof l.value !== 'string') return { ok: false, reason: 'encoding-unverified:non-string-equality' };
|
|
103
|
+
if (!claim(l.var, 'String')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
|
|
104
|
+
} else if (l.t === 'bool') {
|
|
105
|
+
if (!claim(l.var, 'Bool')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
|
|
106
|
+
} else if (l.t === 'opaque') {
|
|
107
|
+
unmodeled += 1;
|
|
108
|
+
} else {
|
|
109
|
+
return { ok: false, reason: 'encoding-unverified:unknown-literal' };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const constEntries = [];
|
|
115
|
+
for (const [k, v] of Object.entries(constants)) {
|
|
116
|
+
if (!sorts.has(k)) continue; // a constant for a variable no condition reads constrains nothing
|
|
117
|
+
if (typeof v === 'number') {
|
|
118
|
+
if (!claim(k, 'Real')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
|
|
119
|
+
if (realLiteral(v) === null) return { ok: false, reason: 'encoding-unverified:number-not-encodable' };
|
|
120
|
+
} else if (typeof v === 'string') {
|
|
121
|
+
if (!claim(k, 'String')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
|
|
122
|
+
} else return { ok: false, reason: 'encoding-unverified:constant-type' };
|
|
123
|
+
constEntries.push([k, v]);
|
|
124
|
+
}
|
|
125
|
+
// Rename to v0..vN in sorted order: no escaping hazard, and two distinct names can never merge.
|
|
126
|
+
const names = [...sorts.keys()].sort();
|
|
127
|
+
const id = new Map(names.map((n, i) => [n, `v${i}`]));
|
|
128
|
+
const lines = ['(set-logic ALL)'];
|
|
129
|
+
for (const n of names) lines.push(`(declare-const ${id.get(n)} ${sorts.get(n)})`);
|
|
130
|
+
const side = (s) => ('var' in s ? id.get(s.var) : realLiteral(s.const));
|
|
131
|
+
let asserted = 0;
|
|
132
|
+
for (const a of problem.assertions) {
|
|
133
|
+
const alts = a.alternatives.map((conj) => {
|
|
134
|
+
const parts = conj.map((l) => {
|
|
135
|
+
if (l.t === 'num') return `(${OPS[l.op]} ${side(l.left)} ${side(l.right)})`;
|
|
136
|
+
if (l.t === 'eq') return l.equal ? `(= ${id.get(l.var)} ${stringLiteral(l.value)})` : `(distinct ${id.get(l.var)} ${stringLiteral(l.value)})`;
|
|
137
|
+
if (l.t === 'bool') return l.truth ? id.get(l.var) : `(not ${id.get(l.var)})`;
|
|
138
|
+
return 'true'; // opaque: no constraint
|
|
139
|
+
});
|
|
140
|
+
return parts.length === 0 ? 'true' : parts.length === 1 ? parts[0] : `(and ${parts.join(' ')})`;
|
|
141
|
+
});
|
|
142
|
+
lines.push(`(assert ${alts.length === 0 ? 'false' : alts.length === 1 ? alts[0] : `(or ${alts.join(' ')})`})`);
|
|
143
|
+
asserted += 1;
|
|
144
|
+
}
|
|
145
|
+
for (const [k, v] of constEntries.sort((x, y) => (x[0] < y[0] ? -1 : 1))) {
|
|
146
|
+
lines.push(`(assert (= ${id.get(k)} ${typeof v === 'number' ? realLiteral(v) : stringLiteral(v)}))`);
|
|
147
|
+
asserted += 1;
|
|
148
|
+
}
|
|
149
|
+
lines.push('(check-sat)');
|
|
150
|
+
const text = lines.join('\n');
|
|
151
|
+
// Structural self-check of the translation: every assertion of the problem became exactly one assert.
|
|
152
|
+
const emitted = lines.filter((l) => l.startsWith('(assert ')).length;
|
|
153
|
+
if (emitted !== problem.assertions.length + constEntries.length || asserted !== emitted) {
|
|
154
|
+
return { ok: false, reason: 'encoding-unverified:assertion-count' };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
ok: true, text, unmodeled, literals, assertions: emitted,
|
|
158
|
+
sorts: Object.fromEntries(names.map((n) => [n, sorts.get(n)])),
|
|
159
|
+
digest: crypto.createHash('sha256').update(text).digest('hex').slice(0, 16),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- the backend ---------------------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The default loader. Loaded through a createRequire call at first use, the way the optional tree-sitter
|
|
167
|
+
* runtime is: a bundler keeps it external, and a build without the package fails here (reported as
|
|
168
|
+
* unsupported by the caller), never at start-up.
|
|
169
|
+
*/
|
|
170
|
+
async function defaultLoader() {
|
|
171
|
+
return require('z3-solver');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build a z3 backend.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} [o]
|
|
178
|
+
* @param {number} [o.timeoutMs] per-query solver timeout (Z3 `timeout`)
|
|
179
|
+
* @param {number} [o.memoryMb] process memory cap handed to Z3 (`memory_max_size`)
|
|
180
|
+
* @param {()=>Promise<object>} [o.loader] returns the `z3-solver` module; injected by tests
|
|
181
|
+
*/
|
|
182
|
+
export function createZ3Solver({ timeoutMs = Z3_DEFAULT_TIMEOUT_MS, memoryMb = Z3_DEFAULT_MEMORY_MB, loader = defaultLoader } = {}) {
|
|
183
|
+
const state = { initP: null, failed: null, api: null, ctx: null, version: null, queue: Promise.resolve() };
|
|
184
|
+
const cache = new Map();
|
|
185
|
+
const stats = { queries: 0, cacheHits: 0, loads: 0 };
|
|
186
|
+
|
|
187
|
+
function init() {
|
|
188
|
+
if (state.initP) return state.initP;
|
|
189
|
+
state.initP = (async () => {
|
|
190
|
+
stats.loads += 1;
|
|
191
|
+
const mod = await loader();
|
|
192
|
+
const initFn = mod?.init ?? mod?.default?.init;
|
|
193
|
+
if (typeof initFn !== 'function') throw new Error('z3-solver has no init()');
|
|
194
|
+
const api = await initFn();
|
|
195
|
+
try { api.Z3.global_param_set('memory_max_size', String(memoryMb)); } catch { /* an older build without the knob */ }
|
|
196
|
+
state.api = api;
|
|
197
|
+
state.ctx = api.Context('lineage-smt');
|
|
198
|
+
try { state.version = api.getVersionString?.() ?? null; } catch { state.version = null; }
|
|
199
|
+
return state;
|
|
200
|
+
})().catch((e) => {
|
|
201
|
+
state.failed = String(e?.code || e?.name || 'load-error');
|
|
202
|
+
state.initP = null;
|
|
203
|
+
return null;
|
|
204
|
+
});
|
|
205
|
+
return state.initP;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function run(enc, signal) {
|
|
209
|
+
const ctx = state.ctx;
|
|
210
|
+
const solver = new ctx.Solver();
|
|
211
|
+
let abortHook;
|
|
212
|
+
try {
|
|
213
|
+
solver.set('timeout', timeoutMs);
|
|
214
|
+
try { solver.set('random_seed', 0); } catch { /* not a parameter of this build */ }
|
|
215
|
+
solver.fromString(enc.text);
|
|
216
|
+
if (signal) {
|
|
217
|
+
abortHook = () => { try { state.api.Z3.interrupt(ctx.ptr); } catch { /* best effort */ } };
|
|
218
|
+
signal.addEventListener('abort', abortHook, { once: true });
|
|
219
|
+
}
|
|
220
|
+
// Z3 honours `timeout`, but a wedged wasm call must not hold the queue forever: past a grace period the
|
|
221
|
+
// backend gives up on this context and reports itself unavailable from then on.
|
|
222
|
+
let hardTimer;
|
|
223
|
+
const hung = new Promise((resolve) => { hardTimer = setTimeout(() => resolve('hung'), timeoutMs + HARD_GRACE_MS); });
|
|
224
|
+
const verdict = await Promise.race([solver.check(), hung]).finally(() => clearTimeout(hardTimer));
|
|
225
|
+
if (verdict === 'hung') {
|
|
226
|
+
try { state.api.Z3.interrupt(ctx.ptr); } catch { /* best effort */ }
|
|
227
|
+
state.failed = 'hung';
|
|
228
|
+
return { result: 'timeout', reason: 'z3-hung' };
|
|
229
|
+
}
|
|
230
|
+
if (verdict === 'unsat') return { result: 'unsat' };
|
|
231
|
+
if (verdict === 'sat') return { result: 'sat' };
|
|
232
|
+
const why = String(solver.reasonUnknown?.() ?? '');
|
|
233
|
+
if (/timeout/i.test(why)) return { result: 'timeout', reason: 'z3-timeout' };
|
|
234
|
+
if (/memout|memory/i.test(why)) return { result: 'unknown', reason: 'z3-memory' };
|
|
235
|
+
if (/cancel/i.test(why)) return { result: 'unknown', reason: 'z3-cancelled' };
|
|
236
|
+
return { result: 'unknown', reason: 'z3-unknown' };
|
|
237
|
+
} finally {
|
|
238
|
+
if (abortHook && signal) signal.removeEventListener('abort', abortHook);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const backend = {
|
|
243
|
+
name: 'z3',
|
|
244
|
+
get version() { return state.version ? `${state.version}+${Z3_BACKEND_VERSION}` : Z3_BACKEND_VERSION; },
|
|
245
|
+
/** False only after a load/initialisation failure; true (unknown yet) before the first query. */
|
|
246
|
+
get available() { return state.failed === null; },
|
|
247
|
+
get loadFailure() { return state.failed; },
|
|
248
|
+
stats() { return { ...stats, cacheSize: cache.size }; },
|
|
249
|
+
/** Never throws. `unsupported` when z3 is missing, `unknown` when the encoding cannot be verified. */
|
|
250
|
+
async check(problem, { signal } = {}) {
|
|
251
|
+
stats.queries += 1;
|
|
252
|
+
const enc = encodeProblem(problem);
|
|
253
|
+
if (!enc.ok) return { result: 'unknown', reason: enc.reason };
|
|
254
|
+
const key = `${timeoutMs}|${enc.text}`;
|
|
255
|
+
const hit = cache.get(key);
|
|
256
|
+
if (hit) { stats.cacheHits += 1; cache.delete(key); cache.set(key, hit); return finish(hit, enc, true); }
|
|
257
|
+
const ready = await init();
|
|
258
|
+
if (!ready || state.failed) return { result: 'unsupported', reason: `z3-unavailable:${state.failed}` };
|
|
259
|
+
// One query at a time: the wasm context is single-threaded.
|
|
260
|
+
const turn = state.queue.then(() => run(enc, signal)).catch(() => ({ result: 'unknown', reason: 'z3-error' }));
|
|
261
|
+
state.queue = turn.then(() => {}, () => {});
|
|
262
|
+
const raw = await turn;
|
|
263
|
+
if (raw.result === 'sat' || raw.result === 'unsat') {
|
|
264
|
+
cache.set(key, raw);
|
|
265
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
266
|
+
}
|
|
267
|
+
return finish(raw, enc, false);
|
|
268
|
+
},
|
|
269
|
+
/** Release the wasm worker threads. The next query re-initialises. */
|
|
270
|
+
async shutdown() {
|
|
271
|
+
const api = state.api;
|
|
272
|
+
state.initP = null; state.api = null; state.ctx = null;
|
|
273
|
+
if (!api) return;
|
|
274
|
+
try { const kt = (await loader())?.killThreads ?? null; if (kt && api.em) kt(api.em); } catch { /* nothing to release */ }
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
return backend;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function finish(raw, enc, cached) {
|
|
281
|
+
// A model that leaned on an unmodeled predicate says nothing about the real conditions.
|
|
282
|
+
if (raw.result === 'sat' && enc.unmodeled > 0) return { result: 'unknown', reason: 'sat-with-unmodeled-predicates', encodingDigest: enc.digest, cached };
|
|
283
|
+
return { ...raw, encodingDigest: enc.digest, cached };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let _shared = null;
|
|
287
|
+
/** The process-wide backend used when AGENTIC_SECURITY_LINEAGE_SMT=z3. */
|
|
288
|
+
export function sharedZ3Solver() {
|
|
289
|
+
if (!_shared) _shared = createZ3Solver();
|
|
290
|
+
return _shared;
|
|
291
|
+
}
|
|
292
|
+
export function smtBackendEnabled(env = process.env) {
|
|
293
|
+
return env.AGENTIC_SECURITY_LINEAGE_SMT === 'z3';
|
|
294
|
+
}
|
package/src/posture/CLAUDE.md
CHANGED
|
@@ -1030,7 +1030,11 @@ A sibling of `evidence-bundle.js` for one source-to-sink PATH rather than one fi
|
|
|
1030
1030
|
- **Proof binding.** An external proof reference must name this bundle's sink node, data element, graph digest and configuration digest; a stale or mismatched one is listed under `exploit.rejected` with reasons and is never re-attached.
|
|
1031
1031
|
- **Partial stays partial.** Completion fields are required (a missing one is invalid, never "complete"), the bundle is JSON-normalized at build so serialization cannot change it, and replay re-derives the path terminal, the truncation marks and `overall`.
|
|
1032
1032
|
- **Redaction.** Identities become `idt_` tokens (`lineage/identity-token.js`), trace/session ids become `sref_` references, free text goes through `mcp/redact.js`, unknown input fields are dropped and only counted.
|
|
1033
|
-
- **Limits.** Building needs the hop stream, which a scan does not persist;
|
|
1033
|
+
- **Limits.** Building needs the hop stream, which a scan does not persist; `witness build` (`dag-commands.js`) therefore re-runs the field analysis rather than reading a stored stream. Source files are bound only when the caller supplies their text (the rest are listed `unboundSourceFiles` and replay warns). The graph-edge to provenance-edge relation is the flow binding (`graphBinding.flowId`), checked for existence, not re-derived. Cross-service replay depends on DAG-08.
|
|
1034
|
+
|
|
1035
|
+
## Opt-in DAG commands: `dag-commands.js` (DAG-01, 02, 06, 08, 09, 10)
|
|
1036
|
+
|
|
1037
|
+
CLI entry points for capabilities that were library-only: `dataflow view` (typed graph view plus SCC condensation), `dataflow contracts` (boundary contracts via `runBoundaryTaint`), `dataflow sink-query` (demand reverse taint), the completion contract of `dataflow plan-cuts` (`planContract`; the command body is still in `bin/agentic-security.js`), and `witness build|verify`. Dispatched lazily from `bin/agentic-security.js`, so a default scan never imports it and its output is unchanged. Every report carries a `completion` record in the shared vocabulary and `negativeClaimAllowed` is false unless the engine's own envelope earned it. Exit codes: 0 report produced, 1 nothing usable or verification failed, 2 usage. Suite: `test/cli/dag-commands.test.js` (in `test:lineage`). User docs: `docs/security-graph/graph-precision.md` (Commands section).
|
|
1034
1038
|
|
|
1035
1039
|
## Gotchas
|
|
1036
1040
|
|
|
@@ -154,19 +154,28 @@ function collectStateSets(fileContents) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
function extractStateMachine(fileContents) {
|
|
157
|
-
// Then look for direct writes like `.status = "<not in set>"` and flag.
|
|
157
|
+
// Then look for direct writes like `.status = "<not in set>"` and flag. A write is
|
|
158
|
+
// compared only against the status sets declared in the SAME file: a set in one
|
|
159
|
+
// module says nothing about what another module's `.status` may hold, and matching
|
|
160
|
+
// every `.status =` in the repository against the first set found anywhere flags
|
|
161
|
+
// unrelated objects. Test files are skipped (they assign arbitrary states on purpose).
|
|
158
162
|
const stateSets = collectStateSets(fileContents);
|
|
159
163
|
if (!stateSets.length) return [];
|
|
164
|
+
const byFile = new Map();
|
|
165
|
+
for (const s of stateSets) {
|
|
166
|
+
if (!byFile.has(s.file)) byFile.set(s.file, []);
|
|
167
|
+
byFile.get(s.file).push(s);
|
|
168
|
+
}
|
|
160
169
|
const findings = [];
|
|
161
|
-
for (const [fp,
|
|
162
|
-
|
|
170
|
+
for (const [fp, sets] of byFile) {
|
|
171
|
+
const c = fileContents[fp];
|
|
172
|
+
if (typeof c !== 'string' || /(^|\/)(test|tests|__tests__|fixtures?)\//i.test(fp) || /\.(test|spec)\.[cm]?[jt]sx?$/i.test(fp)) continue;
|
|
163
173
|
const writeRe = /\.\s*(?:status|state|phase)\s*=\s*['"]([^'"]+)['"]/g;
|
|
164
174
|
let m;
|
|
165
175
|
while ((m = writeRe.exec(c))) {
|
|
166
176
|
const value = m[1];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (validSet.values.has(value)) continue;
|
|
177
|
+
if (sets.some(s => s.values.has(value))) continue;
|
|
178
|
+
const validSet = sets[0];
|
|
170
179
|
const line = _lineOf(c, m.index);
|
|
171
180
|
findings.push({
|
|
172
181
|
id: `state-machine:${fp}:${line}:${value}`,
|
|
@@ -179,7 +188,7 @@ function extractStateMachine(fileContents) {
|
|
|
179
188
|
parser: 'STATE-MACHINE',
|
|
180
189
|
confidence: 0.5,
|
|
181
190
|
snippet: m[0].slice(0, 200),
|
|
182
|
-
remediation: `The statuses recognized by
|
|
191
|
+
remediation: `The statuses recognized by this file appear to be {${[...validSet.values].join(', ')}}. This write sets a different value ('${value}'). If the new value is legitimate, add it to the set; otherwise treat the write as a state-machine bypass and reject it.`,
|
|
183
192
|
});
|
|
184
193
|
}
|
|
185
194
|
}
|