@deftai/directive-core 0.98.1 → 0.100.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/dist/authz/actions.d.ts +6 -0
- package/dist/authz/actions.js +3 -0
- package/dist/authz/classify.js +401 -75
- package/dist/authz/decompose-apply.d.ts +67 -0
- package/dist/authz/decompose-apply.js +302 -0
- package/dist/authz/index.d.ts +3 -1
- package/dist/authz/index.js +3 -1
- package/dist/authz/store.d.ts +53 -0
- package/dist/authz/store.js +219 -1
- package/dist/authz/types.d.ts +19 -2
- package/dist/authz/types.js +5 -1
- package/dist/consumer-check-contract/evaluate.d.ts +40 -0
- package/dist/consumer-check-contract/evaluate.js +188 -3
- package/dist/consumer-check-contract/index.d.ts +1 -1
- package/dist/consumer-check-contract/index.js +1 -1
- package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
- package/dist/content-contracts/skills/greptile-detector.js +202 -4
- package/dist/decision/index.d.ts +17 -0
- package/dist/decision/index.js +35 -0
- package/dist/decision/list.d.ts +47 -0
- package/dist/decision/list.js +250 -0
- package/dist/decision/schema.d.ts +88 -0
- package/dist/decision/schema.js +293 -0
- package/dist/decision/write.d.ts +82 -0
- package/dist/decision/write.js +427 -0
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +72 -79
- package/dist/doctor/which.d.ts +20 -1
- package/dist/doctor/which.js +67 -1
- package/dist/eval/report.d.ts +29 -0
- package/dist/eval/report.js +69 -0
- package/dist/eval/run.d.ts +9 -0
- package/dist/eval/run.js +40 -4
- package/dist/eval/version-pin.d.ts +99 -0
- package/dist/eval/version-pin.js +181 -0
- package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
- package/dist/finish-loop/pr-finish-loop.js +116 -5
- package/dist/finish-loop/types.d.ts +1 -1
- package/dist/hooks/dispatcher.d.ts +9 -5
- package/dist/hooks/dispatcher.js +23 -15
- package/dist/hooks/readonly.d.ts +6 -3
- package/dist/hooks/readonly.js +44 -25
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/intake/issue-ingest.js +29 -0
- package/dist/lifecycle/completed-consistency.d.ts +60 -0
- package/dist/lifecycle/completed-consistency.js +443 -0
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +52 -0
- package/dist/literal-acceptance/capture.js +658 -0
- package/dist/literal-acceptance/evaluate.d.ts +47 -0
- package/dist/literal-acceptance/evaluate.js +193 -0
- package/dist/literal-acceptance/index.d.ts +12 -0
- package/dist/literal-acceptance/index.js +12 -0
- package/dist/literal-acceptance/run.d.ts +38 -0
- package/dist/literal-acceptance/run.js +216 -0
- package/dist/literal-acceptance/safety.d.ts +24 -0
- package/dist/literal-acceptance/safety.js +251 -0
- package/dist/literal-acceptance/types.d.ts +82 -0
- package/dist/literal-acceptance/types.js +26 -0
- package/dist/platform/host-content-surface.d.ts +74 -0
- package/dist/platform/host-content-surface.js +214 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/ceremony-dial.d.ts +289 -0
- package/dist/policy/ceremony-dial.js +980 -0
- package/dist/policy/deft-directive-disable.js +12 -2
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +16 -1
- package/dist/policy/merge-approval-head.d.ts +129 -0
- package/dist/policy/merge-approval-head.js +461 -0
- package/dist/policy/require-human-merge.d.ts +5 -0
- package/dist/policy/require-human-merge.js +5 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
- package/dist/pr-merge-readiness/ci-gate.js +65 -5
- package/dist/pr-merge-readiness/compute.d.ts +13 -0
- package/dist/pr-merge-readiness/compute.js +99 -14
- package/dist/pr-merge-readiness/evaluate.js +10 -0
- package/dist/pr-merge-readiness/gh.d.ts +49 -0
- package/dist/pr-merge-readiness/gh.js +268 -0
- package/dist/pr-merge-readiness/index.d.ts +2 -2
- package/dist/pr-merge-readiness/index.js +2 -2
- package/dist/pr-merge-readiness/mergeability.js +5 -0
- package/dist/pr-merge-readiness/output.js +2 -0
- package/dist/pr-merge-readiness/parse.js +4 -0
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
- package/dist/pr-merge-readiness/types.d.ts +6 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
- package/dist/pr-wait-mergeable/cascade.js +115 -1
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +4 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
- package/dist/pr-wait-mergeable/wrappers.js +7 -1
- package/dist/preflight/evaluate.d.ts +15 -1
- package/dist/preflight/evaluate.js +33 -2
- package/dist/render/framework-commands.js +4 -0
- package/dist/scope/acceptance-evidence.d.ts +76 -0
- package/dist/scope/acceptance-evidence.js +348 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.js +125 -11
- package/dist/scope/effort-activate-gate.d.ts +28 -0
- package/dist/scope/effort-activate-gate.js +64 -0
- package/dist/scope/index.d.ts +4 -0
- package/dist/scope/index.js +4 -0
- package/dist/scope/parent-lineage.d.ts +125 -0
- package/dist/scope/parent-lineage.js +626 -0
- package/dist/scope/transition.d.ts +8 -0
- package/dist/scope/transition.js +69 -2
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/effort-budget.d.ts +130 -0
- package/dist/session/effort-budget.js +334 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/session-start.d.ts +30 -1
- package/dist/session/session-start.js +234 -26
- package/dist/session/verify-session-ritual.d.ts +14 -0
- package/dist/session/verify-session-ritual.js +33 -0
- package/dist/story-ready/evaluate.d.ts +11 -0
- package/dist/story-ready/evaluate.js +41 -3
- package/dist/swarm/index.d.ts +2 -0
- package/dist/swarm/index.js +2 -0
- package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
- package/dist/swarm/pre-dispatch-cli.js +143 -0
- package/dist/swarm/pre-dispatch.d.ts +87 -0
- package/dist/swarm/pre-dispatch.js +373 -0
- package/dist/triage/actions/candidates-log.d.ts +1 -2
- package/dist/triage/actions/candidates-log.js +37 -6
- package/dist/vbrief-activate/activate.js +6 -0
- package/dist/vbrief-validate/constants.d.ts +2 -0
- package/dist/vbrief-validate/constants.js +2 -0
- package/dist/vbrief-validate/schema.js +4 -1
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +19 -3
package/dist/authz/classify.js
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* (CodeQL js/polynomial-redos).
|
|
8
8
|
*/
|
|
9
9
|
import { classifyMcpTool, classifyShellCommand, listShellOps, } from "../policy/runtime-authority.js";
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Split on whitespace without nested quantifiers (O(n)).
|
|
12
|
+
* Newlines become `;` segment breaks so compound lists like
|
|
13
|
+
* `scp …authz…\necho ok` keep the scp dest (#3213 Greptile residual).
|
|
14
|
+
* Bare space/tab/CR still only separate tokens.
|
|
15
|
+
*/
|
|
11
16
|
function shellTokens(command) {
|
|
12
17
|
const out = [];
|
|
13
18
|
let cur = "";
|
|
@@ -15,13 +20,24 @@ function shellTokens(command) {
|
|
|
15
20
|
const c = command[i];
|
|
16
21
|
if (c === undefined)
|
|
17
22
|
break;
|
|
18
|
-
if (c === " " || c === "\t" || c === "\
|
|
23
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
19
24
|
if (cur.length > 0) {
|
|
20
25
|
out.push(cur);
|
|
21
26
|
cur = "";
|
|
22
27
|
}
|
|
23
28
|
continue;
|
|
24
29
|
}
|
|
30
|
+
if (c === "\n") {
|
|
31
|
+
if (cur.length > 0) {
|
|
32
|
+
out.push(cur);
|
|
33
|
+
cur = "";
|
|
34
|
+
}
|
|
35
|
+
// Emit a segment break once (avoid runs of `;` from blank lines).
|
|
36
|
+
if (out.length > 0 && out[out.length - 1] !== ";") {
|
|
37
|
+
out.push(";");
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
25
41
|
cur += c;
|
|
26
42
|
}
|
|
27
43
|
if (cur.length > 0)
|
|
@@ -163,14 +179,55 @@ const POLICY_AUTHORITY_MUTATORS = new Set([
|
|
|
163
179
|
/** Kill-switch / permanent opt-out basenames agents must not plant under UAT (#3186 / #3039). */
|
|
164
180
|
const KILL_SWITCH_BASENAMES = [".deft-directive-disable", ".no-deft-directive"];
|
|
165
181
|
/**
|
|
166
|
-
* Downloaders / decoders that can plant files without shell
|
|
167
|
-
* Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
|
|
168
|
-
* `curl $URL` as a store write via opaque-expansion heuristics.
|
|
182
|
+
* Downloaders / decoders / remote-copy tools that can plant files without shell
|
|
183
|
+
* redirects (#3206 / #3213 / #3245). Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
|
|
184
|
+
* and would classify bare `curl $URL` as a store write via opaque-expansion heuristics.
|
|
169
185
|
*/
|
|
170
|
-
const DOWNLOADER_DECODER_BINS = new Set([
|
|
186
|
+
const DOWNLOADER_DECODER_BINS = new Set([
|
|
187
|
+
"curl",
|
|
188
|
+
"wget",
|
|
189
|
+
"xxd",
|
|
190
|
+
"openssl",
|
|
191
|
+
// #3213 residual after #3206: alternate downloaders / remote copy.
|
|
192
|
+
"scp",
|
|
193
|
+
"aria2c",
|
|
194
|
+
"certutil",
|
|
195
|
+
// #3245 residual after #3213: archive extractors + further alt downloaders.
|
|
196
|
+
"tar",
|
|
197
|
+
"bsdtar",
|
|
198
|
+
"unzip",
|
|
199
|
+
"7z",
|
|
200
|
+
"7za",
|
|
201
|
+
"7zr",
|
|
202
|
+
"rclone",
|
|
203
|
+
"axel",
|
|
204
|
+
"fetch",
|
|
205
|
+
"socat",
|
|
206
|
+
"lftp",
|
|
207
|
+
]);
|
|
208
|
+
/**
|
|
209
|
+
* Archive extractors / alt writers that can plant via pathish operands without
|
|
210
|
+
* shell redirects (#3245). Used for pathish authz/kill scans (prefer deny) and
|
|
211
|
+
* write-shape residual under UAT — not bare curl-class URL fetches.
|
|
212
|
+
*/
|
|
213
|
+
const ARCHIVE_ALT_WRITE_BINS = new Set([
|
|
214
|
+
"tar",
|
|
215
|
+
"bsdtar",
|
|
216
|
+
"unzip",
|
|
217
|
+
"7z",
|
|
218
|
+
"7za",
|
|
219
|
+
"7zr",
|
|
220
|
+
"rclone",
|
|
221
|
+
"axel",
|
|
222
|
+
"fetch",
|
|
223
|
+
"socat",
|
|
224
|
+
"lftp",
|
|
225
|
+
]);
|
|
171
226
|
/**
|
|
172
227
|
* File destination flags for downloaders/decoders (#3206).
|
|
173
228
|
* normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
|
|
229
|
+
* scp: `-o` is an SSH option — excluded in isDownloaderDestFlag.
|
|
230
|
+
* 7z: `-oDIR` is attached-only (no separate value token).
|
|
174
231
|
*/
|
|
175
232
|
const DOWNLOADER_FILE_DEST_FLAGS = new Set([
|
|
176
233
|
"-o",
|
|
@@ -179,9 +236,32 @@ const DOWNLOADER_FILE_DEST_FLAGS = new Set([
|
|
|
179
236
|
"-out",
|
|
180
237
|
"--out",
|
|
181
238
|
]);
|
|
182
|
-
/** Directory destination flags (curl --output-dir / wget -P); bin-scoped below. */
|
|
239
|
+
/** Directory destination flags (curl --output-dir / wget -P / aria2c -d); bin-scoped below. */
|
|
183
240
|
const CURL_DIR_DEST_FLAGS = new Set(["--output-dir"]);
|
|
184
241
|
const WGET_DIR_DEST_FLAGS = new Set(["-p", "--directory-prefix"]);
|
|
242
|
+
const ARIA2C_DIR_DEST_FLAGS = new Set(["-d", "--dir"]);
|
|
243
|
+
/**
|
|
244
|
+
* tar/bsdtar extract directory (#3245).
|
|
245
|
+
* Case-sensitive: POSIX tar uses capital `-C` for chdir; lower `-c` is create and is NOT dest.
|
|
246
|
+
*/
|
|
247
|
+
const TAR_DIR_DEST_FLAGS_EXACT = new Set(["-C"]);
|
|
248
|
+
const TAR_DIR_DEST_FLAGS_LOWER = new Set(["--directory"]);
|
|
249
|
+
/** unzip extract directory (#3245). */
|
|
250
|
+
const UNZIP_DIR_DEST_FLAGS = new Set(["-d"]);
|
|
251
|
+
/**
|
|
252
|
+
* Symlink / hard-link plant bins (#3213). Absent from prior killWriteBins →
|
|
253
|
+
* `ln -sf … .deft-directive-disable` classified empty → UAT fail-open.
|
|
254
|
+
*/
|
|
255
|
+
const SYMLINK_PLANT_BINS = new Set(["ln", "link", "mklink"]);
|
|
256
|
+
/** socat address prefixes that open a write target (#3245). */
|
|
257
|
+
const SOCAT_WRITE_ADDR_PREFIXES = [
|
|
258
|
+
"open:",
|
|
259
|
+
"create:",
|
|
260
|
+
"creat:",
|
|
261
|
+
"append:",
|
|
262
|
+
"owronly:",
|
|
263
|
+
"oappend:",
|
|
264
|
+
];
|
|
185
265
|
/** Final path segment of a token (path-qualified bins / .exe). */
|
|
186
266
|
function binBareName(token) {
|
|
187
267
|
const pathish = token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
@@ -195,19 +275,96 @@ function isDownloaderDecoderBin(token) {
|
|
|
195
275
|
return false;
|
|
196
276
|
return DOWNLOADER_DECODER_BINS.has(binBareName(token));
|
|
197
277
|
}
|
|
198
|
-
|
|
278
|
+
/**
|
|
279
|
+
* True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
|
|
280
|
+
* tar `-C` must use raw case: lowercased `-c` is create-archive, not chdir (#3245).
|
|
281
|
+
* 7z uses attached `-oDIR` only — separate `-o PATH` is not a 7z dest form (handled elsewhere).
|
|
282
|
+
*/
|
|
283
|
+
function isDownloaderDestFlag(flag, bin, rawFlag) {
|
|
284
|
+
// scp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
|
|
285
|
+
if (bin === "scp")
|
|
286
|
+
return false;
|
|
287
|
+
// 7z family: only attached `-oDIR` (parsed in attached-short branch), not separate `-o PATH`.
|
|
288
|
+
if (bin === "7z" || bin === "7za" || bin === "7zr") {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
199
291
|
if (DOWNLOADER_FILE_DEST_FLAGS.has(flag))
|
|
200
292
|
return true;
|
|
201
293
|
if (bin === "curl" && CURL_DIR_DEST_FLAGS.has(flag))
|
|
202
294
|
return true;
|
|
203
295
|
if (bin === "wget" && WGET_DIR_DEST_FLAGS.has(flag))
|
|
204
296
|
return true;
|
|
297
|
+
if (bin === "aria2c" && ARIA2C_DIR_DEST_FLAGS.has(flag))
|
|
298
|
+
return true;
|
|
299
|
+
if (bin === "tar" || bin === "bsdtar") {
|
|
300
|
+
if (rawFlag !== undefined && TAR_DIR_DEST_FLAGS_EXACT.has(rawFlag))
|
|
301
|
+
return true;
|
|
302
|
+
if (TAR_DIR_DEST_FLAGS_LOWER.has(flag))
|
|
303
|
+
return true;
|
|
304
|
+
// Exact match on raw when normalize collapsed case for long flags only.
|
|
305
|
+
if (rawFlag !== undefined && TAR_DIR_DEST_FLAGS_LOWER.has(normalizeToken(rawFlag)))
|
|
306
|
+
return true;
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
if (bin === "unzip" && UNZIP_DIR_DEST_FLAGS.has(flag))
|
|
310
|
+
return true;
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* OpenSSH/scp flags that take a separate value token (`-o ProxyCommand=…`, `-i key`, `-P port`).
|
|
315
|
+
* Not dest flags — must skip so value tokens are not mistaken for write destinations (#3213).
|
|
316
|
+
* Case-sensitive where needed: scp `-P` (port) takes a value; `-p` (preserve) does not.
|
|
317
|
+
*/
|
|
318
|
+
const SCP_VALUE_FLAGS_LOWER = new Set(["-o", "-i", "-c", "-s", "-j", "-f", "-l", "-b"]);
|
|
319
|
+
const SCP_VALUE_FLAGS_EXACT = new Set(["-P", "-F", "-S", "-J"]);
|
|
320
|
+
/** Shell metacharacters that end a command segment (compound lists / pipelines). */
|
|
321
|
+
function isShellSegmentBreak(token) {
|
|
322
|
+
const t = token.trim();
|
|
323
|
+
if (t.length === 0)
|
|
324
|
+
return false;
|
|
325
|
+
if (t === ";" || t === "|" || t === "||" || t === "&&" || t === "&")
|
|
326
|
+
return true;
|
|
327
|
+
// Trailing operator glued to a prior token is handled by pathish stripping; bare ops here.
|
|
205
328
|
return false;
|
|
206
329
|
}
|
|
207
330
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
331
|
+
* First unquoted, unescaped shell-list operator (`;` `&` `|`) in a token, or -1.
|
|
332
|
+
* Quoted operators (e.g. `'file;name'`) and escaped unquoted ops (e.g. `path\;file`)
|
|
333
|
+
* are literal data and must not end scp segments (#3213 Greptile residual).
|
|
334
|
+
* O(n). Unquoted / double-quoted `\` escapes the next char; single-quoted `\` is literal.
|
|
335
|
+
*/
|
|
336
|
+
function firstUnquotedShellOpIndex(raw) {
|
|
337
|
+
let inSingle = false;
|
|
338
|
+
let inDouble = false;
|
|
339
|
+
for (let k = 0; k < raw.length; k++) {
|
|
340
|
+
const ch = raw[k];
|
|
341
|
+
// Outside single quotes, backslash escapes the next character (POSIX-ish).
|
|
342
|
+
if (ch === "\\" && k + 1 < raw.length && !inSingle) {
|
|
343
|
+
k++;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (ch === "'" && !inDouble) {
|
|
347
|
+
inSingle = !inSingle;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (ch === '"' && !inSingle) {
|
|
351
|
+
inDouble = !inDouble;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (inSingle || inDouble)
|
|
355
|
+
continue;
|
|
356
|
+
if (ch === ";" || ch === "&" || ch === "|")
|
|
357
|
+
return k;
|
|
358
|
+
}
|
|
359
|
+
return -1;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Destinations from curl/wget/xxd/openssl/scp/aria2c/certutil via -o/--output/-O/-out/
|
|
363
|
+
* --output-dir/-P/-d/--dir (separate, =value, or attached short form), xxd -r path-like
|
|
364
|
+
* write positionals (#3206), positional dests for scp/certutil (#3213), and #3245 residual
|
|
365
|
+
* archive/alt bins (tar -C, unzip -d, 7z -oDIR, rclone positionals, socat OPEN:, axel/fetch -o).
|
|
210
366
|
* openssl uses flags only (no positional dests — avoids treating -in paths as writes).
|
|
367
|
+
* Segment stops at shell operators so compound `scp …authz…; echo` cannot overwrite dests.
|
|
211
368
|
* O(n) token walk — no nested-quantifier regex on untrusted input.
|
|
212
369
|
*/
|
|
213
370
|
function downloaderDecoderDestinations(tokens) {
|
|
@@ -222,9 +379,33 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
222
379
|
i++;
|
|
223
380
|
// xxd reverse mode writes; without -r, path positionals are dump inputs (read).
|
|
224
381
|
let xxdReverse = false;
|
|
382
|
+
// scp / certutil / rclone / archive extractors: collect pathish operands in this segment.
|
|
383
|
+
// Fail-closed under UAT: any pathish mentioning protected store/kill paths is a dest
|
|
384
|
+
// candidate (scp source-or-dest of `.deft/authz` is treated as settings — prefer deny
|
|
385
|
+
// over source/dest thrash). Last pathish remains the ordinary write dest.
|
|
386
|
+
// Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
|
|
387
|
+
let lastPositionalPath = null;
|
|
388
|
+
const protectedPathish = [];
|
|
389
|
+
const collectsProtectedPositionals = bin === "scp" ||
|
|
390
|
+
bin === "certutil" ||
|
|
391
|
+
bin === "rclone" ||
|
|
392
|
+
bin === "tar" ||
|
|
393
|
+
bin === "bsdtar" ||
|
|
394
|
+
bin === "unzip" ||
|
|
395
|
+
bin === "7z" ||
|
|
396
|
+
bin === "7za" ||
|
|
397
|
+
bin === "7zr" ||
|
|
398
|
+
bin === "socat" ||
|
|
399
|
+
bin === "lftp";
|
|
225
400
|
while (i < tokens.length) {
|
|
226
401
|
const raw = tokens[i];
|
|
227
402
|
const n = normalizeToken(raw);
|
|
403
|
+
// Bare shell ops end this bin's segment. Do NOT use normalizeToken here —
|
|
404
|
+
// quoted `';'` becomes `;` after strip and would cut before the real authz dest
|
|
405
|
+
// (Greptile P1 residual #3213). Glued ops are handled by firstUnquotedShellOpIndex.
|
|
406
|
+
if (isShellSegmentBreak(raw)) {
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
228
409
|
// New bare bin starts another command segment (not pathish ./wget operands).
|
|
229
410
|
if (!n.startsWith("-") &&
|
|
230
411
|
!raw.includes("/") &&
|
|
@@ -247,18 +428,36 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
247
428
|
i++;
|
|
248
429
|
continue;
|
|
249
430
|
}
|
|
250
|
-
//
|
|
431
|
+
// scp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
|
|
432
|
+
if (bin === "scp" &&
|
|
433
|
+
(SCP_VALUE_FLAGS_LOWER.has(n) ||
|
|
434
|
+
SCP_VALUE_FLAGS_EXACT.has(raw) ||
|
|
435
|
+
SCP_VALUE_FLAGS_EXACT.has(n))) {
|
|
436
|
+
const next = tokens[i + 1];
|
|
437
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
438
|
+
i += 2;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
i++;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
// --flag=value (and rare -out=value); scp -oOption=Value attached form also lands here.
|
|
445
|
+
// tar --directory=PATH also lands here.
|
|
251
446
|
if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
|
|
252
447
|
const eq = raw.indexOf("=");
|
|
253
|
-
const
|
|
254
|
-
|
|
448
|
+
const flagRaw = raw.slice(0, eq);
|
|
449
|
+
const flag = normalizeToken(flagRaw);
|
|
450
|
+
if (isDownloaderDestFlag(flag, bin, flagRaw)) {
|
|
255
451
|
dests.push(pathishToken(raw.slice(eq + 1)));
|
|
256
452
|
}
|
|
453
|
+
// scp attached -oProxyCommand=… is not a file dest — skip without recording.
|
|
257
454
|
i++;
|
|
258
455
|
continue;
|
|
259
456
|
}
|
|
260
457
|
// Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
|
|
261
|
-
|
|
458
|
+
// Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
|
|
459
|
+
// 7z requires attached -oDIR (#3245).
|
|
460
|
+
if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
|
|
262
461
|
if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output")) {
|
|
263
462
|
dests.push(pathishToken(raw.slice(4)));
|
|
264
463
|
i++;
|
|
@@ -275,11 +474,29 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
275
474
|
i++;
|
|
276
475
|
continue;
|
|
277
476
|
}
|
|
477
|
+
// aria2c attached -dDIR (#3213)
|
|
478
|
+
if (bin === "aria2c" && n.startsWith("-d") && n.length > 2) {
|
|
479
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
480
|
+
i++;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
// unzip attached -dDIR (#3245)
|
|
484
|
+
if (bin === "unzip" && n.startsWith("-d") && n.length > 2) {
|
|
485
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
486
|
+
i++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
// tar attached -CDIR (rare; capital C required)
|
|
490
|
+
if ((bin === "tar" || bin === "bsdtar") && raw.startsWith("-C") && raw.length > 2) {
|
|
491
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
492
|
+
i++;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
278
495
|
}
|
|
279
|
-
// Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P
|
|
280
|
-
if (isDownloaderDestFlag(n, bin)) {
|
|
496
|
+
// Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P / -d / -C DIR
|
|
497
|
+
if (isDownloaderDestFlag(n, bin, raw)) {
|
|
281
498
|
const next = tokens[i + 1];
|
|
282
|
-
if (next !== undefined && !String(next).startsWith("-")) {
|
|
499
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
283
500
|
dests.push(pathishToken(next));
|
|
284
501
|
i += 2;
|
|
285
502
|
continue;
|
|
@@ -297,8 +514,52 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
297
514
|
dests.push(p);
|
|
298
515
|
}
|
|
299
516
|
}
|
|
517
|
+
// socat write address forms: OPEN:path, CREATE:path, APPEND:path (#3245).
|
|
518
|
+
if (bin === "socat" && !n.startsWith("-")) {
|
|
519
|
+
const p = pathishToken(raw);
|
|
520
|
+
for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
|
|
521
|
+
if (p.startsWith(prefix) && p.length > prefix.length) {
|
|
522
|
+
const dest = p.slice(prefix.length);
|
|
523
|
+
dests.push(dest);
|
|
524
|
+
if (dest.includes(".deft/authz") ||
|
|
525
|
+
dest.includes(".deft-directive-disable") ||
|
|
526
|
+
dest.includes(".no-deft-directive")) {
|
|
527
|
+
protectedPathish.push(dest);
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// scp / certutil / rclone / archive extractors: pathish operands (quote-aware glued-op cut).
|
|
534
|
+
// Under UAT: any `.deft/authz` / kill-switch pathish is fail-closed settings
|
|
535
|
+
// (read vs write thrash deferred — prefer deny over dest-parser perfection; #3213 / #3245).
|
|
536
|
+
if (collectsProtectedPositionals && !n.startsWith("-")) {
|
|
537
|
+
const cut = firstUnquotedShellOpIndex(raw);
|
|
538
|
+
const cleaned = cut >= 0 ? raw.slice(0, cut) : raw;
|
|
539
|
+
const p = pathishToken(cleaned);
|
|
540
|
+
if (p.length > 0) {
|
|
541
|
+
lastPositionalPath = p;
|
|
542
|
+
// Fail-closed: protected store/kill basenames anywhere in pathish.
|
|
543
|
+
if (p.includes(".deft/authz") ||
|
|
544
|
+
p.includes(".deft-directive-disable") ||
|
|
545
|
+
p.includes(".no-deft-directive")) {
|
|
546
|
+
protectedPathish.push(p);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (cut >= 0) {
|
|
550
|
+
i++;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
300
554
|
i++;
|
|
301
555
|
}
|
|
556
|
+
// Prefer protected pathish (fail-closed) then last ordinary dest.
|
|
557
|
+
for (const p of protectedPathish) {
|
|
558
|
+
dests.push(p);
|
|
559
|
+
}
|
|
560
|
+
if (lastPositionalPath !== null && !protectedPathish.includes(lastPositionalPath)) {
|
|
561
|
+
dests.push(lastPositionalPath);
|
|
562
|
+
}
|
|
302
563
|
}
|
|
303
564
|
return dests;
|
|
304
565
|
}
|
|
@@ -377,52 +638,75 @@ function hasPolicyAuthorityMutator(tokens) {
|
|
|
377
638
|
}
|
|
378
639
|
return false;
|
|
379
640
|
}
|
|
641
|
+
/** True when pathish token names a kill-switch basename (quote-strip resistant). */
|
|
642
|
+
function pathishMentionsKillSwitch(pathish) {
|
|
643
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
644
|
+
if (pathish === name || pathish.endsWith(`/${name}`) || pathish.includes(name)) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
380
650
|
/**
|
|
381
|
-
* Shell write targeting `.deft-directive-disable` / `.no-deft-directive` (#3186 / #3039).
|
|
651
|
+
* Shell write targeting `.deft-directive-disable` / `.no-deft-directive` (#3186 / #3039 / #3213).
|
|
382
652
|
* Planting the kill-switch under UAT would full-bypass subsequent gates without operator recovery.
|
|
653
|
+
* Includes symlink plant bins (`ln`/`link`/`mklink`) and quote-split-resistant basename checks.
|
|
383
654
|
*/
|
|
384
655
|
function hasKillSwitchShellWrite(command, tokens) {
|
|
385
656
|
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
657
|
+
// Quote-stripped form so `'.deft'-directive-disable`-class splits still match (#3213).
|
|
658
|
+
const stripped = lower.replace(/['"]/g, "");
|
|
386
659
|
let mentionsKill = false;
|
|
387
660
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
388
|
-
if (lower.includes(name)) {
|
|
661
|
+
if (lower.includes(name) || stripped.includes(name)) {
|
|
389
662
|
mentionsKill = true;
|
|
390
663
|
break;
|
|
391
664
|
}
|
|
392
665
|
}
|
|
666
|
+
if (!mentionsKill) {
|
|
667
|
+
for (const t of tokens) {
|
|
668
|
+
if (pathishMentionsKillSwitch(pathishToken(t))) {
|
|
669
|
+
mentionsKill = true;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
393
674
|
if (!mentionsKill)
|
|
394
675
|
return false;
|
|
395
|
-
// Redirect dest region after each `>` / `>>` (O(n)).
|
|
396
|
-
for (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
j
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
676
|
+
// Redirect dest region after each `>` / `>>` (O(n)); check raw + quote-stripped.
|
|
677
|
+
for (const hay of [lower, stripped]) {
|
|
678
|
+
for (let i = 0; i < hay.length; i++) {
|
|
679
|
+
if (hay[i] !== ">")
|
|
680
|
+
continue;
|
|
681
|
+
let j = i + 1;
|
|
682
|
+
if (j < hay.length && hay[j] === ">")
|
|
683
|
+
j++;
|
|
684
|
+
let end = j;
|
|
685
|
+
while (end < hay.length &&
|
|
686
|
+
hay[end] !== "|" &&
|
|
687
|
+
hay[end] !== ";" &&
|
|
688
|
+
hay[end] !== "&" &&
|
|
689
|
+
hay[end] !== "\n") {
|
|
690
|
+
end++;
|
|
691
|
+
}
|
|
692
|
+
const dest = hay.slice(j, end);
|
|
693
|
+
for (const name of KILL_SWITCH_BASENAMES) {
|
|
694
|
+
if (dest.includes(name))
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
414
697
|
}
|
|
415
698
|
}
|
|
416
|
-
// Downloader/decoder destinations: curl -o .deft-directive-disable,
|
|
699
|
+
// Downloader/decoder destinations: curl -o .deft-directive-disable, scp, … (#3206 / #3213).
|
|
417
700
|
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
701
|
+
if (pathishMentionsKillSwitch(dest))
|
|
702
|
+
return true;
|
|
422
703
|
}
|
|
423
|
-
// Write/destructive bins with a kill-switch path
|
|
704
|
+
// Write/destructive + symlink plant + archive/alt-write bins with a kill-switch path
|
|
705
|
+
// argument (#3213 ln/link/mklink; #3245 tar/axel/rclone/socat residual).
|
|
424
706
|
const killWriteBins = new Set([
|
|
425
707
|
...INDIRECT_WRITE_BINS,
|
|
708
|
+
...SYMLINK_PLANT_BINS,
|
|
709
|
+
...ARCHIVE_ALT_WRITE_BINS,
|
|
426
710
|
"touch",
|
|
427
711
|
"new-item",
|
|
428
712
|
"ni",
|
|
@@ -431,26 +715,34 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
431
715
|
"type",
|
|
432
716
|
]);
|
|
433
717
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
434
|
-
|
|
718
|
+
const binTok = normalizeToken(tokens[ti]);
|
|
719
|
+
const bare = binBareName(tokens[ti]);
|
|
720
|
+
if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
|
|
435
721
|
continue;
|
|
436
722
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
437
723
|
const p = pathishToken(tokens[tj]);
|
|
438
|
-
|
|
439
|
-
|
|
724
|
+
if (pathishMentionsKillSwitch(p))
|
|
725
|
+
return true;
|
|
726
|
+
// socat OPEN:.deft-directive-disable — path after address prefix (#3245).
|
|
727
|
+
for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
|
|
728
|
+
if (p.startsWith(prefix) && pathishMentionsKillSwitch(p.slice(prefix.length))) {
|
|
440
729
|
return true;
|
|
730
|
+
}
|
|
441
731
|
}
|
|
442
732
|
}
|
|
443
733
|
}
|
|
444
|
-
// Bare `touch .deft-directive-disable`
|
|
734
|
+
// Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` — path later.
|
|
445
735
|
for (const t of tokens) {
|
|
446
736
|
const p = pathishToken(t);
|
|
447
737
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
448
738
|
// Exact basename or ends with /basename
|
|
449
739
|
if (p === name || p.endsWith(`/${name}`)) {
|
|
450
|
-
// Require some write shape (redirect already handled; touch/ni/echo/…)
|
|
740
|
+
// Require some write shape (redirect already handled; touch/ni/echo/ln/…)
|
|
451
741
|
if (hasWriteShape(command, tokens) ||
|
|
452
742
|
lower.includes("touch") ||
|
|
453
|
-
lower.includes("new-item")
|
|
743
|
+
lower.includes("new-item") ||
|
|
744
|
+
SYMLINK_PLANT_BINS.has(binBareName(tokens[0])) ||
|
|
745
|
+
tokens.some((tok) => SYMLINK_PLANT_BINS.has(binBareName(tok)))) {
|
|
454
746
|
return true;
|
|
455
747
|
}
|
|
456
748
|
}
|
|
@@ -623,47 +915,76 @@ function pathishToken(token) {
|
|
|
623
915
|
return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
624
916
|
}
|
|
625
917
|
/**
|
|
626
|
-
*
|
|
918
|
+
* True when a pathish string targets `.deft/authz` (after quote strip / slash normalize).
|
|
919
|
+
* Quote-split forms like `'.deft/'authz'/grants/x'` become `.deft/authz/grants/x` via pathishToken.
|
|
920
|
+
*/
|
|
921
|
+
function pathishIsAuthzDir(pathish) {
|
|
922
|
+
return pathish.includes(".deft/authz");
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Shell **write** targeting `.deft/authz/` (#3110 AC-3 / #3206 / #3213).
|
|
627
926
|
* Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
|
|
628
927
|
* Redirects only count when the destination region contains `.deft/authz`.
|
|
928
|
+
* Pathish/token checks run even when the raw command lacks contiguous `.deft/authz`
|
|
929
|
+
* text (quote-split residual: `cp x '.deft/'authz'/grants/y'`).
|
|
629
930
|
*/
|
|
630
931
|
function hasAuthzDirShellWrite(command, tokens) {
|
|
631
932
|
const lower = command.toLowerCase().replace(/\\/g, "/");
|
|
632
|
-
|
|
633
|
-
|
|
933
|
+
// Quote-stripped contiguous form for redirect dest checks (#3213).
|
|
934
|
+
const stripped = lower.replace(/['"]/g, "");
|
|
634
935
|
// Redirect dest region after each `>` / `>>` (O(n); no nested-quantifier regex).
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
j
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
936
|
+
// Check both raw and quote-stripped so quote-split dests still match.
|
|
937
|
+
for (const hay of [lower, stripped]) {
|
|
938
|
+
for (let i = 0; i < hay.length; i++) {
|
|
939
|
+
if (hay[i] !== ">")
|
|
940
|
+
continue;
|
|
941
|
+
let j = i + 1;
|
|
942
|
+
if (j < hay.length && hay[j] === ">")
|
|
943
|
+
j++;
|
|
944
|
+
// Dest until pipe/semicolon/ampersand/newline.
|
|
945
|
+
let end = j;
|
|
946
|
+
while (end < hay.length &&
|
|
947
|
+
hay[end] !== "|" &&
|
|
948
|
+
hay[end] !== ";" &&
|
|
949
|
+
hay[end] !== "&" &&
|
|
950
|
+
hay[end] !== "\n") {
|
|
951
|
+
end++;
|
|
952
|
+
}
|
|
953
|
+
if (hay.slice(j, end).includes(".deft/authz"))
|
|
954
|
+
return true;
|
|
649
955
|
}
|
|
650
|
-
if (lower.slice(j, end).includes(".deft/authz"))
|
|
651
|
-
return true;
|
|
652
956
|
}
|
|
653
|
-
// Write/destructive bins with an authz path argument
|
|
957
|
+
// Write/destructive + symlink plant + archive/alt-write bins with an authz path argument
|
|
958
|
+
// (pathish = quote-strip resistant). Always run — do not gate on contiguous `.deft/authz`
|
|
959
|
+
// in the raw command (#3213 / #3245). SYMLINK_PLANT_BINS: `ln -s forged .deft/authz/grants/x`
|
|
960
|
+
// must not fail-open as unclassifiable (SLizard residual). ARCHIVE_ALT_WRITE_BINS: tar/rclone
|
|
961
|
+
// residual pathish without dest-flag perfection thrash.
|
|
654
962
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
655
|
-
|
|
963
|
+
const bare = binBareName(tokens[ti]);
|
|
964
|
+
const n = normalizeToken(tokens[ti]);
|
|
965
|
+
if (!INDIRECT_WRITE_BINS.has(n) &&
|
|
966
|
+
!INDIRECT_WRITE_BINS.has(bare) &&
|
|
967
|
+
!SYMLINK_PLANT_BINS.has(bare) &&
|
|
968
|
+
!ARCHIVE_ALT_WRITE_BINS.has(bare)) {
|
|
656
969
|
continue;
|
|
970
|
+
}
|
|
657
971
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
658
|
-
|
|
972
|
+
const p = pathishToken(tokens[tj]);
|
|
973
|
+
if (pathishIsAuthzDir(p))
|
|
659
974
|
return true;
|
|
975
|
+
for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
|
|
976
|
+
if (p.startsWith(prefix) && pathishIsAuthzDir(p.slice(prefix.length))) {
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
660
980
|
}
|
|
661
981
|
}
|
|
662
|
-
// Downloader/decoder destinations under .deft/authz (#3206
|
|
982
|
+
// Downloader/decoder destinations under .deft/authz (#3206 / #3213 / #3245 archive+alt).
|
|
663
983
|
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
664
|
-
if (dest
|
|
984
|
+
if (pathishIsAuthzDir(dest))
|
|
665
985
|
return true;
|
|
666
986
|
}
|
|
987
|
+
// Contiguous mention without write shape stays false (reads like cat .deft/authz/…).
|
|
667
988
|
return false;
|
|
668
989
|
}
|
|
669
990
|
/** Write/destructive shell bins (token match after normalizeToken). */
|
|
@@ -740,7 +1061,12 @@ function hasWriteShape(command, tokens) {
|
|
|
740
1061
|
if (command.includes(">"))
|
|
741
1062
|
return true;
|
|
742
1063
|
for (const t of tokens) {
|
|
743
|
-
|
|
1064
|
+
const bare = binBareName(t);
|
|
1065
|
+
if (INDIRECT_WRITE_BINS.has(normalizeToken(t)) || INDIRECT_WRITE_BINS.has(bare))
|
|
1066
|
+
return true;
|
|
1067
|
+
// #3245: archive extractors / alt downloaders are write-shaped (defense-in-depth for
|
|
1068
|
+
// unclassifiable plant paths under UAT — not bare curl $URL which stays non-write-shaped).
|
|
1069
|
+
if (ARCHIVE_ALT_WRITE_BINS.has(bare))
|
|
744
1070
|
return true;
|
|
745
1071
|
}
|
|
746
1072
|
return false;
|