@clear-capabilities/agentic-security-scanner 0.165.1 → 0.167.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 +61 -0
- package/bin/agentic-security.js +39 -4
- package/dist/2199.index.js +348 -0
- package/dist/3740.index.js +722 -0
- package/dist/4113.index.js +1 -1
- package/dist/4142.index.js +929 -0
- package/dist/5738.index.js +240 -0
- package/dist/7838.index.js +1 -1
- package/dist/808.index.js +1330 -0
- package/dist/818.index.js +93 -0
- package/dist/9800.index.js +245 -0
- package/dist/9999.index.js +2 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -4
- package/src/capabilities/CLAUDE.md +8 -0
- package/src/posture/artifact-registry.js +3 -0
- package/src/posture/proof-fabric/cli.js +115 -0
- package/src/posture/proof-fabric/fix-proof.js +73 -0
- package/src/posture/proof-fabric/oracles.js +101 -0
- package/src/posture/proof-fabric/orchestrator.js +129 -0
- package/src/posture/proof-fabric/reproducer.js +108 -0
- package/src/posture/proof-fabric/runner.js +243 -0
- package/src/posture/proof-fabric/states.js +53 -0
- package/src/posture/proof-fabric/store.js +83 -0
- package/src/posture/rule-compiler/cli.js +141 -0
- package/src/posture/rule-compiler/corpus.js +61 -0
- package/src/posture/rule-compiler/eligibility.js +25 -0
- package/src/posture/rule-compiler/ir.js +87 -0
- package/src/posture/rule-compiler/lifecycle.js +57 -0
- package/src/posture/rule-compiler/mutation.js +60 -0
- package/src/posture/rule-compiler/promotion.js +47 -0
- package/src/posture/rule-compiler/store.js +96 -0
- package/src/sandbox/backend-userspace.js +7 -1
- package/src/sandbox/supervise.js +26 -16
- package/src/sandbox/trust-boundary.js +2 -1
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
export const id = 3740;
|
|
2
|
+
export const ids = [3740];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 25517:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ $: () => (/* binding */ ORACLE_TYPES),
|
|
10
|
+
/* harmony export */ U: () => (/* binding */ evaluateOracle)
|
|
11
|
+
/* harmony export */ });
|
|
12
|
+
// Deterministic security oracles. A model may SELECT an oracle and propose its parameters; it may never decide that the
|
|
13
|
+
// oracle succeeded. Each oracle is a pure function of an Observation gathered by the TRUSTED runner (never by the model):
|
|
14
|
+
//
|
|
15
|
+
// Observation = {
|
|
16
|
+
// exitCode, signal, timedOut, stdout, stderr,
|
|
17
|
+
// canaries: { <name>: { exists, digestBefore, digestAfter } } // checked by the runner on disk, after the run
|
|
18
|
+
// callbacks: [{ nonce }] // hits recorded by the runner's loopback listener
|
|
19
|
+
// nonce // per-run random token the target cannot predict
|
|
20
|
+
// }
|
|
21
|
+
//
|
|
22
|
+
// An oracle returns { fired, reason }. `fired:false` with `inconclusive:true` means the observation could not decide
|
|
23
|
+
// (for example a timeout); it is never treated as "not exploitable".
|
|
24
|
+
const ORACLE_TYPES = Object.freeze([
|
|
25
|
+
'crash_sanitizer', 'command_execution', 'unauthorized_http_access', 'unauthorized_db_read', 'database_mutation',
|
|
26
|
+
'filesystem_mutation', 'outbound_callback', 'authentication_bypass', 'arbitrary_assertion',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SANITIZER_RE = /(AddressSanitizer|UndefinedBehaviorSanitizer|stack-buffer-overflow|heap-buffer-overflow|SEGV on unknown address|stack smashing detected|buffer overflow detected)/i;
|
|
30
|
+
const CRASH_SIGNALS = new Set(['SIGSEGV', 'SIGABRT', 'SIGBUS', 'SIGILL', 'SIGFPE']);
|
|
31
|
+
|
|
32
|
+
const out = (fired, reason, extra = {}) => ({ fired, reason, ...extra });
|
|
33
|
+
const inconclusive = (reason) => ({ fired: false, inconclusive: true, reason });
|
|
34
|
+
const has = (obs, text) => typeof text === 'string' && text.length > 0 && `${obs.stdout || ''}${obs.stderr || ''}`.includes(text);
|
|
35
|
+
|
|
36
|
+
const IMPL = {
|
|
37
|
+
crash_sanitizer(obs) {
|
|
38
|
+
if (SANITIZER_RE.test(obs.stderr || '')) return out(true, 'sanitizer or runtime memory-safety report on stderr');
|
|
39
|
+
if (obs.signal && CRASH_SIGNALS.has(obs.signal)) return out(true, `process terminated by ${obs.signal}`);
|
|
40
|
+
if (obs.timedOut) return inconclusive('run timed out before a crash could be judged');
|
|
41
|
+
return out(false, 'no crash signal and no sanitizer report');
|
|
42
|
+
},
|
|
43
|
+
// The injected payload asks the shell/interpreter to create a file named after the per-run nonce inside the
|
|
44
|
+
// watched canary directory. The target cannot create it unless attacker data reached a command.
|
|
45
|
+
command_execution(obs, p) {
|
|
46
|
+
const c = obs.canaries?.[p.canary || 'command'];
|
|
47
|
+
if (!c) return inconclusive('command canary was not armed by the runner');
|
|
48
|
+
if (c.exists && !c.existedBefore) return out(true, 'nonce-named canary file created by an injected command');
|
|
49
|
+
if (obs.timedOut) return inconclusive('run timed out before the canary could be judged');
|
|
50
|
+
return out(false, 'canary file not created');
|
|
51
|
+
},
|
|
52
|
+
// A secret planted behind authorization (never shown to the exploit client). Seeing the nonce-bearing secret in the
|
|
53
|
+
// client's output while the actor held no credential is the violation.
|
|
54
|
+
unauthorized_http_access(obs, p) {
|
|
55
|
+
const secret = obs.secrets?.[p.secret || 'http'];
|
|
56
|
+
if (!secret) return inconclusive('protected secret was not armed by the runner');
|
|
57
|
+
if (has(obs, secret)) return out(true, 'protected resource content returned to an unauthenticated actor');
|
|
58
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
59
|
+
return out(false, 'protected content not observed');
|
|
60
|
+
},
|
|
61
|
+
unauthorized_db_read(obs, p) {
|
|
62
|
+
const secret = obs.secrets?.[p.secret || 'db'];
|
|
63
|
+
if (!secret) return inconclusive('database canary row was not armed by the runner');
|
|
64
|
+
if (has(obs, secret)) return out(true, 'row the actor may not read was returned');
|
|
65
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
66
|
+
return out(false, 'canary row value not observed');
|
|
67
|
+
},
|
|
68
|
+
database_mutation(obs, p) {
|
|
69
|
+
const c = obs.canaries?.[p.canary || 'db'];
|
|
70
|
+
if (!c) return inconclusive('database canary was not armed by the runner');
|
|
71
|
+
if (c.digestBefore && c.digestAfter && c.digestBefore !== c.digestAfter) return out(true, 'protected database state changed');
|
|
72
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
73
|
+
return out(false, 'protected database state unchanged');
|
|
74
|
+
},
|
|
75
|
+
filesystem_mutation(obs, p) {
|
|
76
|
+
const c = obs.canaries?.[p.canary || 'fs'];
|
|
77
|
+
if (!c) return inconclusive('filesystem canary was not armed by the runner');
|
|
78
|
+
if ((c.exists && !c.existedBefore) || (c.digestBefore && c.digestAfter && c.digestBefore !== c.digestAfter)) return out(true, 'file outside the permitted area was created or modified');
|
|
79
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
80
|
+
return out(false, 'protected path unchanged');
|
|
81
|
+
},
|
|
82
|
+
outbound_callback(obs) {
|
|
83
|
+
if (obs.callbackUnavailable) return { fired: false, unsupported: true, reason: 'loopback callback listener is not available on this backend' };
|
|
84
|
+
const hit = (obs.callbacks || []).some((c) => c && c.nonce && c.nonce === obs.nonce);
|
|
85
|
+
if (hit) return out(true, 'nonce-bearing request reached the runner-controlled listener');
|
|
86
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
87
|
+
return out(false, 'no nonce-bearing callback received');
|
|
88
|
+
},
|
|
89
|
+
authentication_bypass(obs, p) {
|
|
90
|
+
const secret = obs.secrets?.[p.secret || 'auth'];
|
|
91
|
+
if (!secret) return inconclusive('protected secret was not armed by the runner');
|
|
92
|
+
if (has(obs, secret)) return out(true, 'privileged content returned without valid credentials');
|
|
93
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
94
|
+
return out(false, 'privileged content not observed without credentials');
|
|
95
|
+
},
|
|
96
|
+
// Declarative only: no code, no regex supplied by a model. Operators are a closed set.
|
|
97
|
+
arbitrary_assertion(obs, p) {
|
|
98
|
+
const a = p.assert;
|
|
99
|
+
if (!a || typeof a !== 'object') return inconclusive('assertion missing');
|
|
100
|
+
if (obs.timedOut) return inconclusive('run timed out');
|
|
101
|
+
if ('exitCode' in a) return a.exitCode === obs.exitCode ? out(true, `exit code was ${obs.exitCode}`) : out(false, `exit code was ${obs.exitCode}`);
|
|
102
|
+
if ('stdoutIncludes' in a) return has({ stdout: obs.stdout }, String(a.stdoutIncludes)) ? out(true, 'stdout contained the asserted text') : out(false, 'stdout did not contain the asserted text');
|
|
103
|
+
if ('stderrIncludes' in a) return has({ stderr: obs.stderr }, String(a.stderrIncludes)) ? out(true, 'stderr contained the asserted text') : out(false, 'stderr did not contain the asserted text');
|
|
104
|
+
return inconclusive('unsupported assertion operator');
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function evaluateOracle(spec, observation) {
|
|
109
|
+
if (!spec || !ORACLE_TYPES.includes(spec.type)) return { fired: false, unsupported: true, reason: `unknown oracle type: ${String(spec?.type)}` };
|
|
110
|
+
if (!observation || typeof observation !== 'object') return inconclusive('no observation');
|
|
111
|
+
try { return IMPL[spec.type](observation, spec.params || {}); } catch (e) { return inconclusive(`oracle error: ${e.message}`); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
/***/ }),
|
|
116
|
+
|
|
117
|
+
/***/ 3740:
|
|
118
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
119
|
+
|
|
120
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
121
|
+
/* harmony export */ kO: () => (/* binding */ proveFinding)
|
|
122
|
+
/* harmony export */ });
|
|
123
|
+
/* unused harmony exports independentVerify, applySeverityPolicy, _newRunId */
|
|
124
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
|
|
125
|
+
/* harmony import */ var _states_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12090);
|
|
126
|
+
/* harmony import */ var _runner_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2998);
|
|
127
|
+
/* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(37055);
|
|
128
|
+
/* harmony import */ var _proof_tier_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40216);
|
|
129
|
+
// Proof orchestration: finding + revision + reproducer + policy -> structured evidence.
|
|
130
|
+
//
|
|
131
|
+
// Rules that make this safe to trust:
|
|
132
|
+
// * The model may propose a reproducer and select an oracle. The oracle (oracles.js) judges. Never a model.
|
|
133
|
+
// * An attempt that did not complete (timeout, build failure, blocked, unsupported, error) is NOT a negative and NOT a
|
|
134
|
+
// positive: it is degraded evidence, it never advances the state, and it makes the result INCOMPLETE.
|
|
135
|
+
// * EXPLOIT_CONFIRMED needs `required` successes in `attempts` completed attempts AND a pass from an independent
|
|
136
|
+
// verifier that receives only {revision, reproducer, findingId, oracle} in a fresh environment.
|
|
137
|
+
// * "Not reproduced" is reported as such; it is never reported as safe.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
const PROOF_POLICY_DEFAULTS = Object.freeze({ attempts: 3, required: 3, independentReplay: true, timeoutMs: 20_000, cpus: 1, memoryMb: 512, network: 'none', requireContainer: false });
|
|
145
|
+
const RESULT_SCHEMA = 'agentic-security/proof-result';
|
|
146
|
+
|
|
147
|
+
function normalizePolicy(p = {}) {
|
|
148
|
+
const x = { ...PROOF_POLICY_DEFAULTS, ...p };
|
|
149
|
+
x.attempts = Math.max(1, Math.min(10, Math.trunc(Number(x.attempts)) || PROOF_POLICY_DEFAULTS.attempts));
|
|
150
|
+
x.required = Math.max(1, Math.min(x.attempts, Math.trunc(Number(x.required)) || x.attempts));
|
|
151
|
+
x.timeoutMs = Math.max(1000, Math.min(120_000, Number(x.timeoutMs) || PROOF_POLICY_DEFAULTS.timeoutMs));
|
|
152
|
+
return x;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const summarize = (a) => ({ status: a.status, fired: a.oracle?.fired === true, inconclusive: a.oracle?.inconclusive === true, reason: a.oracle?.reason ?? a.reasons?.[0] ?? null, durationMs: a.durationMs, degraded: a.degraded });
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Independent verifier. Takes a verifierView ONLY; any extra property (a model's reasoning, prior results) is rejected so
|
|
159
|
+
* the isolation of its inputs is checkable rather than a convention.
|
|
160
|
+
*/
|
|
161
|
+
async function independentVerify(view, opts = {}) {
|
|
162
|
+
const allowed = new Set(['revision', 'reproducer', 'findingId', 'oracle']);
|
|
163
|
+
const extra = Object.keys(view || {}).filter((k) => !allowed.has(k));
|
|
164
|
+
if (extra.length) return { status: 'rejected', reason: `verifier input must be exactly revision, reproducer, findingId, oracle; got extra: ${extra.join(', ')}` };
|
|
165
|
+
const attempt = await (opts.attemptFn ?? _runner_js__WEBPACK_IMPORTED_MODULE_2__/* .runAttempt */ .Tm)(structuredClone(view.reproducer), { ...opts, preferContainer: opts.preferContainer });
|
|
166
|
+
return { status: attempt.status === 'completed' ? (attempt.oracle?.fired ? 'confirmed' : 'not-reproduced') : 'incomplete', attempt: summarize(attempt), environment: attempt.environment ?? null };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {object} a
|
|
171
|
+
* @param {object} a.finding the finding (id/stableId, severity, proofTier ...)
|
|
172
|
+
* @param {string} [a.revision] commit the proof applies to
|
|
173
|
+
* @param {object} a.reproducer serialized reproducer
|
|
174
|
+
* @param {object} [a.policy]
|
|
175
|
+
* @param {object} [a.deps] test seams: { attemptFn }
|
|
176
|
+
*/
|
|
177
|
+
async function proveFinding({ finding, revision = null, reproducer, policy = {}, signal, deps = {}, sourceState = null, sourceRoot = null, overlay = null } = {}) {
|
|
178
|
+
const started = Date.now();
|
|
179
|
+
const pol = normalizePolicy(policy);
|
|
180
|
+
const attemptFn = deps.attemptFn ?? _runner_js__WEBPACK_IMPORTED_MODULE_2__/* .runAttempt */ .Tm;
|
|
181
|
+
const findingId = finding?.stableId ?? finding?.id ?? reproducer?.findingId ?? 'unknown';
|
|
182
|
+
const degradation = [];
|
|
183
|
+
let state = sourceState ?? (0,_states_js__WEBPACK_IMPORTED_MODULE_1__/* .fromProofTier */ .jn)((0,_proof_tier_js__WEBPACK_IMPORTED_MODULE_4__/* .proofTierOf */ .gx)(finding || {}));
|
|
184
|
+
if (state === 'REPRODUCED') state = 'STATICALLY_CORROBORATED'; // an old single-run marker proof must be re-earned here
|
|
185
|
+
const startState = state;
|
|
186
|
+
|
|
187
|
+
const result = (extra) => ({
|
|
188
|
+
schema: RESULT_SCHEMA, version: 1, findingId, revision, startState, state, staticCorroboration: (0,_states_js__WEBPACK_IMPORTED_MODULE_1__/* .stateRank */ .g3)(startState) >= 1, tier: (0,_states_js__WEBPACK_IMPORTED_MODULE_1__/* .toProofTier */ .KF)(state),
|
|
189
|
+
oracle: reproducer?.oracle?.type ?? null, policy: pol,
|
|
190
|
+
reproducerDigest: reproducer ? safeDigest(reproducer) : null,
|
|
191
|
+
degradation, timing: { proofMs: Date.now() - started }, ...extra,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const errs = reproducer ? (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_3__/* .validateReproducer */ .mC)(reproducer) : ['no reproducer supplied'];
|
|
195
|
+
if (errs.length) { degradation.push('invalid-reproducer'); return result({ completeness: 'INCOMPLETE', outcome: 'invalid-reproducer', reasons: errs, attempts: { required: pol.required, run: 0, successes: 0, incomplete: 0 }, independentReplay: { status: 'not-run' } }); }
|
|
196
|
+
|
|
197
|
+
// A valid reproducer is itself the REPRODUCER_GENERATED rung. Static corroboration is recorded, not required: an exploit
|
|
198
|
+
// that reproduces under an independent oracle is deterministic corroboration in its own right, and a pattern-only finding
|
|
199
|
+
// must not be unable to earn proof merely because the taint engine never saw it. The record says which it was.
|
|
200
|
+
const staticCorroboration = (0,_states_js__WEBPACK_IMPORTED_MODULE_1__/* .stateRank */ .g3)(startState) >= (0,_states_js__WEBPACK_IMPORTED_MODULE_1__/* .stateRank */ .g3)('STATICALLY_CORROBORATED');
|
|
201
|
+
if (!staticCorroboration) degradation.push('no-static-corroboration');
|
|
202
|
+
state = 'REPRODUCER_GENERATED';
|
|
203
|
+
|
|
204
|
+
const attempts = [];
|
|
205
|
+
for (let i = 0; i < pol.attempts; i++) {
|
|
206
|
+
if (signal?.aborted) { degradation.push('cancelled'); break; }
|
|
207
|
+
const a = await attemptFn(structuredClone(reproducer), { limits: { timeoutMs: pol.timeoutMs }, signal, requireContainer: pol.requireContainer, preferContainer: pol.preferContainer, force: pol.force, sourceRoot, overlay });
|
|
208
|
+
attempts.push(a);
|
|
209
|
+
// Stop early once the threshold can no longer be met only if every attempt is a decisive negative; incomplete runs continue.
|
|
210
|
+
}
|
|
211
|
+
const completed = attempts.filter((a) => a.status === 'completed' && !a.oracle?.inconclusive);
|
|
212
|
+
const successes = completed.filter((a) => a.oracle?.fired).length;
|
|
213
|
+
const incomplete = attempts.length - completed.length;
|
|
214
|
+
for (const a of attempts) for (const d of a.degraded || []) if (!degradation.includes(d)) degradation.push(d);
|
|
215
|
+
if (incomplete) degradation.push(`incomplete-attempts:${incomplete}`);
|
|
216
|
+
const env = attempts.find((a) => a.environment)?.environment ?? null;
|
|
217
|
+
const base = {
|
|
218
|
+
environment: env && { mode: env.mode, assurance: env.assurance, backend: env.backend ?? null, note: env.note ?? null },
|
|
219
|
+
build: attempts.some((a) => a.status === 'build-failed') ? 'failed' : attempts.length ? 'ok' : 'not-run',
|
|
220
|
+
attempts: { required: pol.required, run: attempts.length, successes, incomplete, detail: attempts.map(summarize) },
|
|
221
|
+
artifactHashes: { reproducer: safeDigest(reproducer), observations: (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_3__/* .sha256 */ .sc)((0,_reproducer_js__WEBPACK_IMPORTED_MODULE_3__/* .canonicalize */ .dw)(attempts.map((a) => a.observation ?? null))) },
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (successes > 0) {
|
|
225
|
+
const r = (0,_states_js__WEBPACK_IMPORTED_MODULE_1__/* .advance */ .np)(state, 'REPRODUCED'); if (r.ok) state = r.state;
|
|
226
|
+
}
|
|
227
|
+
if (!attempts.length) return result({ ...base, completeness: 'INCOMPLETE', outcome: 'not-run', independentReplay: { status: 'not-run' } });
|
|
228
|
+
if (successes === 0) {
|
|
229
|
+
const decisive = completed.length === attempts.length;
|
|
230
|
+
return result({ ...base, completeness: decisive ? 'FULL' : 'INCOMPLETE', outcome: decisive ? 'not-reproduced' : 'inconclusive', note: 'not reproduced is not proof of safety', independentReplay: { status: 'not-run' } });
|
|
231
|
+
}
|
|
232
|
+
if (successes < pol.required || incomplete) {
|
|
233
|
+
return result({ ...base, completeness: incomplete ? 'INCOMPLETE' : 'DEGRADED', outcome: 'reproduced-below-threshold', independentReplay: { status: 'not-run' } });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let independent = { status: 'skipped' };
|
|
237
|
+
if (pol.independentReplay) {
|
|
238
|
+
independent = await independentVerify((0,_reproducer_js__WEBPACK_IMPORTED_MODULE_3__/* .verifierView */ .IW)({ revision, reproducer, findingId, oracle: reproducer.oracle }), { attemptFn, limits: { timeoutMs: pol.timeoutMs }, signal, requireContainer: pol.requireContainer, force: pol.force, sourceRoot, overlay });
|
|
239
|
+
if (independent.status !== 'confirmed') degradation.push(`independent-replay:${independent.status}`);
|
|
240
|
+
} else degradation.push('independent-replay-disabled');
|
|
241
|
+
if (independent.status === 'confirmed') {
|
|
242
|
+
const r = (0,_states_js__WEBPACK_IMPORTED_MODULE_1__/* .advance */ .np)(state, 'EXPLOIT_CONFIRMED'); if (r.ok) state = r.state;
|
|
243
|
+
}
|
|
244
|
+
const full = state === 'EXPLOIT_CONFIRMED' && base.environment?.assurance !== 'none';
|
|
245
|
+
return result({ ...base, independentReplay: independent, completeness: full ? (base.environment.assurance === 'lower' ? 'DEGRADED' : 'FULL') : 'INCOMPLETE', outcome: full ? 'exploit-confirmed' : 'reproduced-unverified' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const safeDigest = (r) => { try { return (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_3__/* .reproducerDigest */ .we)(r); } catch { return null; } };
|
|
249
|
+
|
|
250
|
+
/** Severity policy: optional, never on by default. Returns the finding unchanged unless the policy asks to demote. */
|
|
251
|
+
function applySeverityPolicy(finding, evidenceState, policy) {
|
|
252
|
+
const req = policy?.criticalRequires;
|
|
253
|
+
if (!Array.isArray(req) || !req.length || finding?.severity !== 'critical') return finding;
|
|
254
|
+
const ok = req.some((s) => stateRankOf(evidenceState ?? 'CANDIDATE') >= stateRankOf(s));
|
|
255
|
+
return ok ? finding : { ...finding, severity: 'high', severityPolicy: { demotedFrom: 'critical', reason: `critical requires ${req.join(' or ')}; evidence state is ${evidenceState ?? 'CANDIDATE'}` } };
|
|
256
|
+
}
|
|
257
|
+
const _newRunId = () => randomBytes(6).toString('hex');
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
/***/ }),
|
|
261
|
+
|
|
262
|
+
/***/ 37055:
|
|
263
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
264
|
+
|
|
265
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
266
|
+
/* harmony export */ IW: () => (/* binding */ verifierView),
|
|
267
|
+
/* harmony export */ V2: () => (/* binding */ substitute),
|
|
268
|
+
/* harmony export */ Yj: () => (/* binding */ LANGUAGES),
|
|
269
|
+
/* harmony export */ dw: () => (/* binding */ canonicalize),
|
|
270
|
+
/* harmony export */ mC: () => (/* binding */ validateReproducer),
|
|
271
|
+
/* harmony export */ sc: () => (/* binding */ sha256),
|
|
272
|
+
/* harmony export */ we: () => (/* binding */ reproducerDigest)
|
|
273
|
+
/* harmony export */ });
|
|
274
|
+
/* unused harmony exports MAX_FILES, MAX_FILE_BYTES, isSafeRelPath */
|
|
275
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
|
|
276
|
+
/* harmony import */ var _oracles_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25517);
|
|
277
|
+
// Serialized reproducer: the ONLY thing an independent verifier receives (besides revision, finding id and oracle).
|
|
278
|
+
// It carries no reasoning, no model output and no prior results. Everything per-run (nonce, secrets, canary paths,
|
|
279
|
+
// callback URL) is a placeholder substituted fresh by the trusted runner, so a replay can never reuse a stale value
|
|
280
|
+
// and a target cannot learn the values in advance.
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
const REPRODUCER_SCHEMA = 'agentic-security/reproducer';
|
|
285
|
+
const REPRODUCER_VERSION = 1;
|
|
286
|
+
const LANGUAGES = Object.freeze(['javascript', 'typescript', 'python', 'java', 'c', 'cpp', 'csharp']);
|
|
287
|
+
const MAX_FILES = 64;
|
|
288
|
+
const MAX_FILE_BYTES = 256 * 1024;
|
|
289
|
+
const MAX_TOTAL_BYTES = 1024 * 1024;
|
|
290
|
+
const PLACEHOLDER_RE = /\{\{([A-Z_]+)(?::([a-z0-9_-]{1,32}))?\}\}/g;
|
|
291
|
+
const ALLOWED_PLACEHOLDERS = new Set(['NONCE', 'SECRET', 'CANARY', 'CALLBACK_URL', 'CALLBACK_PORT', 'ROOT', 'NODE', 'PYTHON', 'JAVA', 'JAVAC', 'CC']);
|
|
292
|
+
|
|
293
|
+
function isSafeRelPath(rel) {
|
|
294
|
+
if (typeof rel !== 'string' || !rel || rel.length > 200) return false;
|
|
295
|
+
if (rel.startsWith('/') || rel.includes('\0') || rel.includes('\\')) return false;
|
|
296
|
+
if (['canary', 'tmp'].includes(rel.split('/')[0])) return false;
|
|
297
|
+
return rel.split('/').every((seg) => seg && seg !== '.' && seg !== '..' && !seg.startsWith('__oracle_'));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Canonical JSON (sorted keys) so identical reproducers hash identically regardless of key order. */
|
|
301
|
+
function canonicalize(v) {
|
|
302
|
+
if (Array.isArray(v)) return `[${v.map(canonicalize).join(',')}]`;
|
|
303
|
+
if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
|
|
304
|
+
return JSON.stringify(v);
|
|
305
|
+
}
|
|
306
|
+
const sha256 = (s) => (0,node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash)('sha256').update(s).digest('hex');
|
|
307
|
+
const reproducerDigest = (r) => sha256(canonicalize(r));
|
|
308
|
+
|
|
309
|
+
function validateReproducer(r) {
|
|
310
|
+
const errs = [];
|
|
311
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) return ['reproducer must be an object'];
|
|
312
|
+
if (r.schema !== REPRODUCER_SCHEMA) errs.push('unknown schema');
|
|
313
|
+
if (r.version !== REPRODUCER_VERSION) errs.push('unsupported reproducer version');
|
|
314
|
+
if (typeof r.findingId !== 'string' || !r.findingId || r.findingId.length > 200) errs.push('findingId required');
|
|
315
|
+
if (!LANGUAGES.includes(r.lang)) errs.push(`unsupported language: ${String(r.lang)}`);
|
|
316
|
+
if (!r.oracle || !_oracles_js__WEBPACK_IMPORTED_MODULE_1__/* .ORACLE_TYPES */ .$.includes(r.oracle.type)) errs.push(`unknown oracle: ${String(r.oracle?.type)}`);
|
|
317
|
+
const files = r.files ?? {};
|
|
318
|
+
if (typeof files !== 'object' || Array.isArray(files)) errs.push('files must be an object');
|
|
319
|
+
else {
|
|
320
|
+
const names = Object.keys(files);
|
|
321
|
+
if (names.length > MAX_FILES) errs.push(`file count must be at most ${MAX_FILES}`);
|
|
322
|
+
let total = 0;
|
|
323
|
+
for (const n of names) {
|
|
324
|
+
if (!isSafeRelPath(n)) { errs.push(`unsafe file path: ${n}`); continue; }
|
|
325
|
+
if (typeof files[n] !== 'string') { errs.push(`file ${n} must be a string`); continue; }
|
|
326
|
+
const b = Buffer.byteLength(files[n]);
|
|
327
|
+
total += b;
|
|
328
|
+
if (b > MAX_FILE_BYTES) errs.push(`file ${n} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
329
|
+
}
|
|
330
|
+
if (total > MAX_TOTAL_BYTES) errs.push('reproducer exceeds total size limit');
|
|
331
|
+
}
|
|
332
|
+
const sourceNames = r.sources && typeof r.sources === 'object' && !Array.isArray(r.sources) ? Object.keys(r.sources) : [];
|
|
333
|
+
if (Object.keys(files).length + sourceNames.length === 0) errs.push('reproducer must declare at least one file or source');
|
|
334
|
+
for (const key of ['build', 'run']) {
|
|
335
|
+
const a = r[key];
|
|
336
|
+
if (a == null && key === 'build') continue;
|
|
337
|
+
if (!Array.isArray(a) || !a.length || !a.every((x) => typeof x === 'string' && x.length < 2000 && !x.includes('\0'))) errs.push(`${key} must be a non-empty argv array of strings (no shell string)`);
|
|
338
|
+
}
|
|
339
|
+
for (const text of [...Object.values(files || {}), ...(r.build || []), ...(r.run || [])]) {
|
|
340
|
+
if (typeof text !== 'string') continue;
|
|
341
|
+
for (const m of text.matchAll(PLACEHOLDER_RE)) if (!ALLOWED_PLACEHOLDERS.has(m[1])) errs.push(`unknown placeholder {{${m[1]}}}`);
|
|
342
|
+
}
|
|
343
|
+
if (r.watch != null) {
|
|
344
|
+
if (typeof r.watch !== 'object' || Array.isArray(r.watch)) errs.push('watch must be an object');
|
|
345
|
+
else for (const [k, v] of Object.entries(r.watch)) if (!/^[a-z0-9_-]{1,32}$/.test(k) || !isSafeRelPath(v)) errs.push(`bad watch entry: ${k}`);
|
|
346
|
+
}
|
|
347
|
+
if (r.env != null) {
|
|
348
|
+
if (typeof r.env !== 'object' || Array.isArray(r.env)) errs.push('env must be an object');
|
|
349
|
+
else for (const [k, v] of Object.entries(r.env)) {
|
|
350
|
+
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(k)) errs.push(`bad env var name: ${k}`);
|
|
351
|
+
if (typeof v !== 'string' || v.length > 4096) errs.push(`env value for ${k} must be a short string`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (r.sources != null) {
|
|
355
|
+
if (typeof r.sources !== 'object' || Array.isArray(r.sources)) errs.push('sources must be an object');
|
|
356
|
+
else for (const [dest, src] of Object.entries(r.sources)) if (!isSafeRelPath(dest) || !isSafeRelPath(src)) errs.push(`bad sources entry: ${dest}`);
|
|
357
|
+
}
|
|
358
|
+
if (r.network != null && !['none', 'callback'].includes(r.network)) errs.push('network must be none or callback');
|
|
359
|
+
return errs;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** The view handed to an independent verifier: exactly the reproducer, plus identifiers. Nothing else. */
|
|
363
|
+
function verifierView({ revision, reproducer, findingId, oracle }) {
|
|
364
|
+
return Object.freeze({ revision: revision ?? null, findingId, oracle, reproducer: structuredClone(reproducer) });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function substitute(text, ctx) {
|
|
368
|
+
return text.replace(PLACEHOLDER_RE, (whole, name, arg) => {
|
|
369
|
+
switch (name) {
|
|
370
|
+
case 'NONCE': return ctx.nonce;
|
|
371
|
+
case 'SECRET': return ctx.secret(arg || 'default');
|
|
372
|
+
case 'CANARY': return ctx.canary(arg || 'default');
|
|
373
|
+
case 'CALLBACK_URL': return ctx.callbackUrl ?? whole;
|
|
374
|
+
case 'CALLBACK_PORT': return String(ctx.callbackPort ?? '');
|
|
375
|
+
case 'ROOT': return ctx.root;
|
|
376
|
+
case 'NODE': return ctx.tools.node;
|
|
377
|
+
case 'PYTHON': return ctx.tools.python ?? whole;
|
|
378
|
+
case 'JAVA': return ctx.tools.java ?? whole;
|
|
379
|
+
case 'JAVAC': return ctx.tools.javac ?? whole;
|
|
380
|
+
case 'CC': return ctx.tools.cc ?? whole;
|
|
381
|
+
default: return whole;
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
/***/ }),
|
|
388
|
+
|
|
389
|
+
/***/ 2998:
|
|
390
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
391
|
+
|
|
392
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
393
|
+
/* harmony export */ Tm: () => (/* binding */ runAttempt),
|
|
394
|
+
/* harmony export */ aP: () => (/* binding */ selectEnvironment),
|
|
395
|
+
/* harmony export */ qV: () => (/* binding */ hostTools),
|
|
396
|
+
/* harmony export */ ql: () => (/* binding */ languageSupport)
|
|
397
|
+
/* harmony export */ });
|
|
398
|
+
/* unused harmony export DEFAULT_LIMITS */
|
|
399
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
400
|
+
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
|
|
401
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
|
|
402
|
+
/* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(31421);
|
|
403
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(77598);
|
|
404
|
+
/* harmony import */ var _sandbox_trust_boundary_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(43547);
|
|
405
|
+
/* harmony import */ var _sandbox_index_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(75778);
|
|
406
|
+
/* harmony import */ var _oracles_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(25517);
|
|
407
|
+
/* harmony import */ var _reproducer_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(37055);
|
|
408
|
+
// Hermetic execution of one reproducer attempt.
|
|
409
|
+
//
|
|
410
|
+
// Isolation preference: container (when a working runtime and a pre-pulled image exist) > local confined sandbox >
|
|
411
|
+
// unavailable. The local mode is LOWER assurance and says so in every result; it is never silently substituted for a
|
|
412
|
+
// container where a policy requires one (`requireContainer`). Nothing here ever executes target code unconfined.
|
|
413
|
+
//
|
|
414
|
+
// Defaults: outbound network denied; only a runner-controlled loopback callback listener may be reachable, and only when
|
|
415
|
+
// the reproducer asks for it; no privileged containers; bounded CPU, memory, disk, process count and wall time.
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
427
|
+
timeoutMs: 20_000, buildTimeoutMs: 60_000, graceMs: 1000, maxOutputBytes: 256 * 1024,
|
|
428
|
+
cpus: 1, memoryMb: 512, pids: 128, diskMb: 64,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const CONTAINER_IMAGES = Object.freeze({
|
|
432
|
+
javascript: 'node:24-slim', typescript: 'node:24-slim', python: 'python:3-slim', java: 'eclipse-temurin:21',
|
|
433
|
+
c: 'gcc:latest', cpp: 'gcc:latest', csharp: 'mcr.microsoft.com/dotnet/sdk:8.0',
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const SIGNALS = { 134: 'SIGABRT', 135: 'SIGBUS', 136: 'SIGFPE', 132: 'SIGILL', 139: 'SIGSEGV' };
|
|
437
|
+
|
|
438
|
+
function findTool(name, pathEnv = process.env.PATH || '') {
|
|
439
|
+
for (const dir of pathEnv.split(node_path__WEBPACK_IMPORTED_MODULE_2__.delimiter)) {
|
|
440
|
+
if (!dir) continue;
|
|
441
|
+
const p = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, name);
|
|
442
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_0__.accessSync(p, node_fs__WEBPACK_IMPORTED_MODULE_0__.constants.X_OK); if (node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(p).isFile()) return node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(p); } catch { /* next */ }
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function hostTools() {
|
|
448
|
+
return { node: process.execPath, python: findTool('python3'), java: findTool('java'), javac: findTool('javac'), cc: findTool('cc') || findTool('gcc'), dotnet: findTool('dotnet') };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Language -> tools it needs. Missing tools make the language `unsupported` with a typed reason, never a pass. */
|
|
452
|
+
function languageSupport(lang, tools = hostTools()) {
|
|
453
|
+
const need = { javascript: ['node'], typescript: ['node'], python: ['python'], java: ['java', 'javac'], c: ['cc'], cpp: ['cc'], csharp: ['dotnet'] }[lang];
|
|
454
|
+
if (!need) return { supported: false, reason: `unsupported language: ${lang}` };
|
|
455
|
+
const missing = need.filter((t) => !tools[t]);
|
|
456
|
+
return missing.length ? { supported: false, reason: `missing toolchain: ${missing.join(', ')}` } : { supported: true, reason: null };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Pick the execution environment. `docker` is injectable for tests. A container needs a responsive daemon AND the image
|
|
461
|
+
* already present: nothing is ever pulled (no network during proof).
|
|
462
|
+
*/
|
|
463
|
+
function selectEnvironment({ lang, preferContainer = true, requireContainer = false, docker = null, tools = hostTools(), force } = {}) {
|
|
464
|
+
const dockerBin = docker?.bin ?? findTool('docker');
|
|
465
|
+
if (preferContainer && dockerBin) {
|
|
466
|
+
const image = docker?.images?.[lang] ?? CONTAINER_IMAGES[lang];
|
|
467
|
+
const probe = (docker?.probe ?? defaultDockerProbe)(dockerBin, image);
|
|
468
|
+
if (probe.ok) return { mode: 'container', assurance: 'high', runtime: dockerBin, image, reason: null };
|
|
469
|
+
if (requireContainer) return { mode: 'unavailable', assurance: 'none', reason: `container required but unavailable: ${probe.reason}` };
|
|
470
|
+
const local = localEnv(force);
|
|
471
|
+
return { ...local, containerSkipped: probe.reason };
|
|
472
|
+
}
|
|
473
|
+
if (requireContainer) return { mode: 'unavailable', assurance: 'none', reason: 'container required but no container runtime found' };
|
|
474
|
+
return localEnv(force);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function localEnv(force) {
|
|
478
|
+
const backend = (0,_sandbox_index_js__WEBPACK_IMPORTED_MODULE_6__/* .detectBackend */ .kS)({ force });
|
|
479
|
+
if (backend === 'disabled') return { mode: 'unavailable', assurance: 'none', reason: 'no confinement backend works on this host; refusing to execute' };
|
|
480
|
+
return { mode: 'local-confined', assurance: 'lower', backend, reason: null, note: 'lower-assurance local confinement: not a container boundary' };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function defaultDockerProbe(bin, image) {
|
|
484
|
+
const info = (0,node_child_process__WEBPACK_IMPORTED_MODULE_3__.spawnSync)(bin, ['info', '--format', '{{.ServerVersion}}'], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL' });
|
|
485
|
+
if (info.status !== 0) return { ok: false, reason: 'container daemon not responding' };
|
|
486
|
+
const img = (0,node_child_process__WEBPACK_IMPORTED_MODULE_3__.spawnSync)(bin, ['image', 'inspect', image], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL' });
|
|
487
|
+
return img.status === 0 ? { ok: true } : { ok: false, reason: `image ${image} is not present locally (proofs never pull)` };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
const digestFile = (p) => { try { return (0,node_crypto__WEBPACK_IMPORTED_MODULE_4__.createHash)('sha256').update(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(p)).digest('hex'); } catch { return null; } };
|
|
493
|
+
|
|
494
|
+
const LISTENER_SRC = `
|
|
495
|
+
const http = require('node:http'); const fs = require('node:fs');
|
|
496
|
+
const out = process.argv[1];
|
|
497
|
+
const s = http.createServer((req, res) => {
|
|
498
|
+
const m = /^\\/([A-Za-z0-9_-]{8,64})/.exec(req.url || '');
|
|
499
|
+
if (m) fs.appendFileSync(out, JSON.stringify({ nonce: m[1] }) + '\\n');
|
|
500
|
+
req.resume(); res.end('ok');
|
|
501
|
+
});
|
|
502
|
+
s.on('error', () => process.exit(2));
|
|
503
|
+
s.listen(0, '127.0.0.1', () => { process.stdout.write(String(s.address().port) + '\\n'); });
|
|
504
|
+
setTimeout(() => process.exit(0), 120000).unref();
|
|
505
|
+
`;
|
|
506
|
+
|
|
507
|
+
async function startListener(dir) {
|
|
508
|
+
const hits = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, 'hits.jsonl');
|
|
509
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(hits, '');
|
|
510
|
+
const child = (0,node_child_process__WEBPACK_IMPORTED_MODULE_3__.spawn)(process.execPath, ['-e', LISTENER_SRC, hits], { stdio: ['ignore', 'pipe', 'ignore'], detached: false });
|
|
511
|
+
const port = await new Promise((resolve) => {
|
|
512
|
+
let buf = '';
|
|
513
|
+
const t = setTimeout(() => resolve(null), 3000);
|
|
514
|
+
child.stdout.on('data', (d) => { buf += d; const n = Number(buf.trim()); if (n > 0) { clearTimeout(t); resolve(n); } });
|
|
515
|
+
child.on('exit', () => { clearTimeout(t); resolve(null); });
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
port, child,
|
|
519
|
+
hits: () => { try { return node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(hits, 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return []; } },
|
|
520
|
+
stop: () => { try { child.kill('SIGKILL'); } catch { /* gone */ } },
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function trimOut(s, max) { s = String(s ?? ''); return s.length > max ? `${s.slice(0, max)}\n[truncated]` : s; }
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Run one attempt. Never throws. Result:
|
|
528
|
+
* { status, executed, environment, observation, oracle, durationMs, reasons[], degraded[], nonceDigest }
|
|
529
|
+
* status: 'completed' | 'build-failed' | 'blocked' | 'unsupported' | 'invalid' | 'error'
|
|
530
|
+
* Only `completed` carries an oracle verdict; everything else is incomplete evidence.
|
|
531
|
+
*/
|
|
532
|
+
async function runAttempt(reproducer, opts = {}) {
|
|
533
|
+
const started = Date.now();
|
|
534
|
+
const limits = { ...DEFAULT_LIMITS, ...(opts.limits || {}) };
|
|
535
|
+
const done = (o) => ({ executed: false, observation: null, oracle: null, degraded: [], reasons: [], ...o, durationMs: Date.now() - started });
|
|
536
|
+
|
|
537
|
+
const errs = (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_8__/* .validateReproducer */ .mC)(reproducer);
|
|
538
|
+
if (errs.length) return done({ status: 'invalid', reasons: errs });
|
|
539
|
+
const tools = opts.tools ?? hostTools();
|
|
540
|
+
const sup = languageSupport(reproducer.lang, tools);
|
|
541
|
+
if (!sup.supported) return done({ status: 'unsupported', reasons: [sup.reason] });
|
|
542
|
+
const env = opts.environment ?? selectEnvironment({ lang: reproducer.lang, requireContainer: opts.requireContainer, preferContainer: opts.preferContainer ?? true, force: opts.force });
|
|
543
|
+
if (env.mode === 'unavailable') return done({ status: 'blocked', environment: env, reasons: [env.reason] });
|
|
544
|
+
if (env.mode === 'container') {
|
|
545
|
+
return done({ status: 'blocked', environment: env, reasons: ['container execution is selected but the container runner is not enabled in this build; use requireContainer:false for local-confined proof'] });
|
|
546
|
+
}
|
|
547
|
+
if (reproducer.network === 'callback' && env.backend !== 'userspace') {
|
|
548
|
+
return done({ status: 'unsupported', environment: env, reasons: [`loopback callback needs the userspace backend; this host uses ${env.backend}`] });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const nonce = (0,node_crypto__WEBPACK_IMPORTED_MODULE_4__.randomBytes)(12).toString('hex');
|
|
552
|
+
const base = node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdtempSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.tmpdir(), 'proof-fabric-')));
|
|
553
|
+
const root = node_path__WEBPACK_IMPORTED_MODULE_2__.join(base, 'root');
|
|
554
|
+
const aux = node_path__WEBPACK_IMPORTED_MODULE_2__.join(base, 'aux');
|
|
555
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(root, { recursive: true });
|
|
556
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, 'canary'));
|
|
557
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, 'tmp'));
|
|
558
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(aux);
|
|
559
|
+
let listener = null;
|
|
560
|
+
try {
|
|
561
|
+
const secrets = {};
|
|
562
|
+
const canaries = {};
|
|
563
|
+
const ctx = {
|
|
564
|
+
nonce, root, tools,
|
|
565
|
+
secret: (n) => (secrets[n] ??= `SECRET-${nonce}-${n}`),
|
|
566
|
+
canary: (n) => { canaries[n] ??= { path: node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, 'canary', `${nonce}-${n}`) }; return canaries[n].path; },
|
|
567
|
+
};
|
|
568
|
+
if (reproducer.network === 'callback') {
|
|
569
|
+
listener = await startListener(aux);
|
|
570
|
+
if (!listener.port) return done({ status: 'error', environment: env, reasons: ['could not start the loopback callback listener'] });
|
|
571
|
+
ctx.callbackPort = listener.port;
|
|
572
|
+
ctx.callbackUrl = `http://127.0.0.1:${listener.port}/${nonce}`;
|
|
573
|
+
}
|
|
574
|
+
// Repository sources imported at proof time (so a patch can be overlaid). Read-only, size-bounded, confined to sourceRoot.
|
|
575
|
+
const imported = {};
|
|
576
|
+
if (reproducer.sources) {
|
|
577
|
+
if (!opts.sourceRoot) return done({ status: 'invalid', environment: env, reasons: ['reproducer imports repository sources but no sourceRoot was given'] });
|
|
578
|
+
const realRoot = node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(opts.sourceRoot);
|
|
579
|
+
for (const [dest, src] of Object.entries(reproducer.sources)) {
|
|
580
|
+
let text;
|
|
581
|
+
if (opts.overlay && Object.hasOwn(opts.overlay, src)) text = String(opts.overlay[src]);
|
|
582
|
+
else {
|
|
583
|
+
const abs = node_path__WEBPACK_IMPORTED_MODULE_2__.resolve(realRoot, src);
|
|
584
|
+
let real; try { real = node_fs__WEBPACK_IMPORTED_MODULE_0__.realpathSync(abs); } catch { return done({ status: 'invalid', environment: env, reasons: [`source not found: ${src}`] }); }
|
|
585
|
+
if (real !== realRoot && !real.startsWith(realRoot + node_path__WEBPACK_IMPORTED_MODULE_2__.sep)) return done({ status: 'invalid', environment: env, reasons: [`source escapes the repository: ${src}`] });
|
|
586
|
+
const st = node_fs__WEBPACK_IMPORTED_MODULE_0__.statSync(real);
|
|
587
|
+
if (!st.isFile() || st.size > 256 * 1024) return done({ status: 'invalid', environment: env, reasons: [`source is not a regular file within the size limit: ${src}`] });
|
|
588
|
+
text = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(real, 'utf8');
|
|
589
|
+
}
|
|
590
|
+
imported[dest] = text;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
for (const [rel, text] of Object.entries({ ...imported, ...reproducer.files })) {
|
|
594
|
+
const dest = node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, rel);
|
|
595
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(node_path__WEBPACK_IMPORTED_MODULE_2__.dirname(dest), { recursive: true });
|
|
596
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(dest, (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_8__/* .substitute */ .V2)(text, ctx));
|
|
597
|
+
}
|
|
598
|
+
const argv = (a) => a.map((x) => (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_8__/* .substitute */ .V2)(x, ctx));
|
|
599
|
+
const watch = {};
|
|
600
|
+
for (const [name, rel] of Object.entries(reproducer.watch || {})) {
|
|
601
|
+
const p = node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, rel);
|
|
602
|
+
watch[name] = { path: p, existedBefore: node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(p), digestBefore: digestFile(p) };
|
|
603
|
+
}
|
|
604
|
+
const runEnv = { PATH: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin', HOME: root, TMPDIR: node_path__WEBPACK_IMPORTED_MODULE_2__.join(root, 'tmp'), LANG: 'C.UTF-8', CI: '1' };
|
|
605
|
+
for (const [k, v] of Object.entries(reproducer.env || {})) runEnv[k] = (0,_reproducer_js__WEBPACK_IMPORTED_MODULE_8__/* .substitute */ .V2)(v, ctx);
|
|
606
|
+
const boundary = { root, timeoutMs: limits.timeoutMs, graceMs: limits.graceMs, maxOutputBytes: limits.maxOutputBytes, env: runEnv, force: opts.force, signal: opts.signal, controlProbes: opts.controlProbes };
|
|
607
|
+
|
|
608
|
+
const degraded = [];
|
|
609
|
+
if (env.assurance === 'lower') degraded.push('lower-assurance-local-confinement');
|
|
610
|
+
|
|
611
|
+
if (reproducer.build) {
|
|
612
|
+
const b = await (0,_sandbox_trust_boundary_js__WEBPACK_IMPORTED_MODULE_5__/* .runInBoundary */ .N)(argv(reproducer.build), { ...boundary, timeoutMs: limits.buildTimeoutMs, cwd: undefined });
|
|
613
|
+
if (b.blocked) return done({ status: 'blocked', environment: env, reasons: b.reasons, degraded });
|
|
614
|
+
if (b.timedOut || b.exitCode !== 0) return done({ status: 'build-failed', executed: true, environment: env, reasons: [b.timedOut ? 'build timed out' : `build exited ${b.exitCode}`], degraded, observation: { buildStderr: trimOut(b.targetOutput?.stderr, 2000) } });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const r = await (0,_sandbox_trust_boundary_js__WEBPACK_IMPORTED_MODULE_5__/* .runInBoundary */ .N)(argv(reproducer.run), { ...boundary, ...(listener ? { networkProxyPort: listener.port } : {}) });
|
|
618
|
+
if (r.blocked) return done({ status: 'blocked', environment: env, reasons: r.reasons, degraded });
|
|
619
|
+
if (!r.executed) return done({ status: 'error', environment: env, reasons: [`target did not execute (${r.status})`], degraded });
|
|
620
|
+
|
|
621
|
+
const exitCode = r.exitCode ?? null;
|
|
622
|
+
const observation = {
|
|
623
|
+
exitCode, signal: SIGNALS[exitCode] ?? null, timedOut: !!r.timedOut,
|
|
624
|
+
stdout: trimOut(r.targetOutput?.stdout, limits.maxOutputBytes), stderr: trimOut(r.targetOutput?.stderr, limits.maxOutputBytes),
|
|
625
|
+
nonce, secrets, canaries: {}, callbacks: listener ? listener.hits() : [],
|
|
626
|
+
};
|
|
627
|
+
for (const [n, c] of Object.entries(canaries)) observation.canaries[n] = { exists: node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(c.path), existedBefore: false };
|
|
628
|
+
for (const [n, w] of Object.entries(watch)) observation.canaries[n] = { exists: node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(w.path), existedBefore: w.existedBefore, digestBefore: w.digestBefore, digestAfter: digestFile(w.path) };
|
|
629
|
+
if (r.outputCapped) degraded.push('output-capped');
|
|
630
|
+
if (r.timedOut) degraded.push('timed-out');
|
|
631
|
+
if (r.termination?.survivors?.length) degraded.push('surviving-processes');
|
|
632
|
+
|
|
633
|
+
const oracle = (0,_oracles_js__WEBPACK_IMPORTED_MODULE_7__/* .evaluateOracle */ .U)(reproducer.oracle, observation);
|
|
634
|
+
// Raw secrets/nonce must not leave this function: persisted evidence carries digests only.
|
|
635
|
+
const safeObs = { ...observation, secrets: Object.fromEntries(Object.keys(secrets).map((k) => [k, '[redacted]'])), nonce: '[redacted]', callbacks: observation.callbacks.length };
|
|
636
|
+
safeObs.stdout = scrub(safeObs.stdout, nonce); safeObs.stderr = scrub(safeObs.stderr, nonce);
|
|
637
|
+
return done({
|
|
638
|
+
status: oracle.unsupported ? 'unsupported' : 'completed', executed: true, environment: { ...env, backend: r.backend ?? env.backend },
|
|
639
|
+
observation: safeObs, oracle, degraded, reasons: oracle.unsupported ? [oracle.reason] : [], controls: r.controls,
|
|
640
|
+
nonceDigest: (0,node_crypto__WEBPACK_IMPORTED_MODULE_4__.createHash)('sha256').update(nonce).digest('hex').slice(0, 16),
|
|
641
|
+
});
|
|
642
|
+
} catch (e) {
|
|
643
|
+
return done({ status: 'error', environment: env, reasons: [`runner error: ${e.message}`] });
|
|
644
|
+
} finally {
|
|
645
|
+
if (listener) listener.stop();
|
|
646
|
+
node_fs__WEBPACK_IMPORTED_MODULE_0__.rmSync(base, { recursive: true, force: true });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const scrub = (s, nonce) => String(s).split(nonce).join('[nonce]');
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
/***/ }),
|
|
654
|
+
|
|
655
|
+
/***/ 12090:
|
|
656
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
657
|
+
|
|
658
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
659
|
+
/* harmony export */ KF: () => (/* binding */ toProofTier),
|
|
660
|
+
/* harmony export */ g3: () => (/* binding */ stateRank),
|
|
661
|
+
/* harmony export */ jn: () => (/* binding */ fromProofTier),
|
|
662
|
+
/* harmony export */ np: () => (/* binding */ advance)
|
|
663
|
+
/* harmony export */ });
|
|
664
|
+
/* unused harmony exports EVIDENCE_STATES, COMPLETENESS */
|
|
665
|
+
// Evidence state ladder for the Universal Exploit Proof Fabric.
|
|
666
|
+
//
|
|
667
|
+
// Evidence state is SEPARATE from severity and confidence. It extends, never replaces, the vocabulary that already
|
|
668
|
+
// exists: `proof-tier.js` (execution-proven / proof-failed / taint-proven / unproven) and the fix-verify legs
|
|
669
|
+
// (`verifiedFull`, `degradedLegs`). `toProofTier` / `fromProofTier` are the explicit bridges.
|
|
670
|
+
//
|
|
671
|
+
// Transitions are guarded: a state can only be reached from the state directly before it (or by dropping back), so no
|
|
672
|
+
// caller can jump a finding to EXPLOIT_CONFIRMED without passing REPRODUCED, or to FIX_VERIFIED without
|
|
673
|
+
// FIX_REPLAY_BLOCKED. A degraded or incomplete run NEVER advances a state.
|
|
674
|
+
const EVIDENCE_STATES = Object.freeze([
|
|
675
|
+
'CANDIDATE',
|
|
676
|
+
'STATICALLY_CORROBORATED',
|
|
677
|
+
'REPRODUCER_GENERATED',
|
|
678
|
+
'REPRODUCED',
|
|
679
|
+
'EXPLOIT_CONFIRMED',
|
|
680
|
+
'FIX_REPLAY_BLOCKED',
|
|
681
|
+
'FIX_VERIFIED',
|
|
682
|
+
]);
|
|
683
|
+
|
|
684
|
+
const stateRank = (s) => EVIDENCE_STATES.indexOf(s);
|
|
685
|
+
|
|
686
|
+
const REQUIRES_PREDECESSOR = Object.freeze(Object.fromEntries(EVIDENCE_STATES.map((s, i) => [s, i === 0 ? null : EVIDENCE_STATES[i - 1]])));
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Advance `from` to `to`. Returns { ok, state, reason }. Moving up requires `to` to be exactly the next rung; moving
|
|
690
|
+
* down or staying is always allowed (evidence can be lost, never invented). Unknown states are rejected.
|
|
691
|
+
*/
|
|
692
|
+
function advance(from, to) {
|
|
693
|
+
if (!EVIDENCE_STATES.includes(from)) return { ok: false, state: 'CANDIDATE', reason: `unknown current state: ${String(from)}` };
|
|
694
|
+
if (!EVIDENCE_STATES.includes(to)) return { ok: false, state: from, reason: `unknown target state: ${String(to)}` };
|
|
695
|
+
if (stateRank(to) <= stateRank(from)) return { ok: true, state: to, reason: null };
|
|
696
|
+
if (REQUIRES_PREDECESSOR[to] !== from) return { ok: false, state: from, reason: `cannot reach ${to} from ${from}: ${REQUIRES_PREDECESSOR[to]} is required first` };
|
|
697
|
+
return { ok: true, state: to, reason: null };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Bridges to the older vocabulary so existing consumers keep working.
|
|
701
|
+
function toProofTier(state) {
|
|
702
|
+
const r = stateRank(state);
|
|
703
|
+
if (r >= stateRank('EXPLOIT_CONFIRMED')) return 'execution-proven';
|
|
704
|
+
if (r >= stateRank('STATICALLY_CORROBORATED')) return 'taint-proven';
|
|
705
|
+
return 'unproven';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function fromProofTier(tier) {
|
|
709
|
+
if (tier === 'taint-proven') return 'STATICALLY_CORROBORATED';
|
|
710
|
+
// execution-proven under the older single-run marker path is one run, not the 3/3 + independent replay the fabric
|
|
711
|
+
// requires, so it maps to REPRODUCED and never to EXPLOIT_CONFIRMED.
|
|
712
|
+
if (tier === 'execution-proven') return 'REPRODUCED';
|
|
713
|
+
return 'CANDIDATE';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Completeness vocabulary shared with fix-verify: FULL only when nothing was skipped.
|
|
717
|
+
const COMPLETENESS = Object.freeze(['FULL', 'DEGRADED', 'INCOMPLETE']);
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
/***/ })
|
|
721
|
+
|
|
722
|
+
};
|