@deftai/directive-core 0.105.0 → 0.106.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/classify.js +591 -34
- package/dist/check/named-cause.js +1 -1
- package/dist/forward-coverage/diff-coverage.d.ts +39 -0
- package/dist/forward-coverage/diff-coverage.js +112 -0
- package/dist/forward-coverage/diff-lines.d.ts +19 -0
- package/dist/forward-coverage/diff-lines.js +173 -0
- package/dist/forward-coverage/evaluate.d.ts +24 -9
- package/dist/forward-coverage/evaluate.js +125 -19
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +1 -1
- package/dist/intake/clause-derivation.d.ts +9 -0
- package/dist/intake/clause-derivation.js +30 -3
- package/dist/lifecycle-visible/evaluate.d.ts +98 -0
- package/dist/lifecycle-visible/evaluate.js +710 -0
- package/dist/lifecycle-visible/index.d.ts +2 -0
- package/dist/lifecycle-visible/index.js +2 -0
- package/dist/literal-acceptance/capture.js +3 -3
- package/dist/orchestration/judgment-policy.d.ts +13 -0
- package/dist/orchestration/judgment-policy.js +15 -0
- package/dist/orchestration/probe-session.js +13 -8
- package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
- package/dist/orchestration/verify-judgment-gates.js +43 -11
- package/dist/policy/ceremony-dial.js +7 -7
- package/dist/policy/host-hooks.d.ts +20 -0
- package/dist/policy/host-hooks.js +121 -2
- package/dist/policy/org-force-on-migration.js +2 -1
- package/dist/policy/plan-extensions.d.ts +1 -1
- package/dist/policy/plan-extensions.js +2 -0
- package/dist/policy/product-signal.js +6 -7
- package/dist/policy/require-human-merge.js +8 -6
- package/dist/policy/resolve.d.ts +7 -2
- package/dist/policy/resolve.js +25 -8
- package/dist/policy/value-feedback.js +7 -8
- package/dist/preflight-cache/evaluate.d.ts +12 -0
- package/dist/preflight-cache/evaluate.js +15 -1
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/product-first-done-gate/evaluate.d.ts +4 -0
- package/dist/product-first-done-gate/evaluate.js +46 -5
- package/dist/release/closed-verb-gate.d.ts +12 -0
- package/dist/release/closed-verb-gate.js +51 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -0
- package/dist/release/pipeline.js +7 -0
- package/dist/release/types.d.ts +12 -0
- package/dist/render/constants.d.ts +2 -1
- package/dist/render/constants.js +2 -1
- package/dist/render/framework-commands.js +4 -0
- package/dist/render/spec-validate.js +23 -12
- package/dist/run-summary/types.d.ts +4 -0
- package/dist/scope/main.js +15 -5
- package/dist/scope/promote-from-issue.js +5 -5
- package/dist/scope-provenance/evaluate.js +3 -3
- package/dist/session/ac-pass-banking.d.ts +5 -0
- package/dist/session/ac-pass-banking.js +3 -2
- package/dist/session/process-cost.d.ts +32 -10
- package/dist/session/process-cost.js +189 -1
- package/dist/session/product-state-hash.js +41 -24
- package/dist/session/ritual-entrypoint.js +16 -1
- package/dist/session/session-start.d.ts +6 -0
- package/dist/session/session-start.js +32 -2
- package/dist/session/verify-session-ritual.d.ts +24 -1
- package/dist/session/verify-session-ritual.js +89 -6
- package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
- package/dist/swarm/finalize-cohort-cli.js +165 -35
- package/dist/swarm/finalize-cohort.d.ts +1 -0
- package/dist/swarm/finalize-cohort.js +14 -2
- package/dist/swarm/routing-set-cli.js +2 -0
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +2 -2
- package/dist/triage/bootstrap/index.js +6 -6
- package/dist/triage/help/registry-data.d.ts +1 -1
- package/dist/triage/help/registry-data.js +1 -1
- package/dist/triage/welcome/writers.d.ts +1 -1
- package/dist/triage/welcome/writers.js +10 -5
- package/dist/value/readback.d.ts +4 -1
- package/dist/value/readback.js +51 -12
- package/dist/vbrief-validate/schema.d.ts +4 -0
- package/dist/vbrief-validate/schema.js +1 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
- package/dist/verify-env/agent-hook-readiness.js +12 -8
- package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
- package/dist/verify-env/agent-hooks-live-probe.js +26 -5
- package/dist/verify-env/agent-hooks.js +3 -4
- package/package.json +7 -3
package/dist/authz/classify.js
CHANGED
|
@@ -178,6 +178,37 @@ const POLICY_AUTHORITY_MUTATORS = new Set([
|
|
|
178
178
|
]);
|
|
179
179
|
/** Kill-switch / permanent opt-out basenames agents must not plant under UAT (#3186 / #3039). */
|
|
180
180
|
const KILL_SWITCH_BASENAMES = [".deft-directive-disable", ".no-deft-directive"];
|
|
181
|
+
/**
|
|
182
|
+
* Dest-form writers #3529 harvest missed (#3545). Shared across downloader,
|
|
183
|
+
* archive-alt, positional, and indirect-write membership so a named-peer
|
|
184
|
+
* allowlist cannot be the only fail-closed path.
|
|
185
|
+
*/
|
|
186
|
+
const UAT_RESIDUAL_DEST_WRITE_BINS_3545 = [
|
|
187
|
+
"fromdos",
|
|
188
|
+
"todos",
|
|
189
|
+
"emacsclient",
|
|
190
|
+
"pico",
|
|
191
|
+
"pdftk",
|
|
192
|
+
"gs",
|
|
193
|
+
"dpkg",
|
|
194
|
+
"degit",
|
|
195
|
+
"composer",
|
|
196
|
+
"ddrescue",
|
|
197
|
+
"dc3dd",
|
|
198
|
+
"sg_dd",
|
|
199
|
+
];
|
|
200
|
+
/** In-place residual writers: any protected operand is a dest. Extract/output bins stay dest-flag gated. */
|
|
201
|
+
const UAT_RESIDUAL_INPLACE_WRITE_BINS_3545 = [
|
|
202
|
+
"fromdos",
|
|
203
|
+
"todos",
|
|
204
|
+
"emacsclient",
|
|
205
|
+
"pico",
|
|
206
|
+
"degit",
|
|
207
|
+
"composer",
|
|
208
|
+
"ddrescue",
|
|
209
|
+
"dc3dd",
|
|
210
|
+
"sg_dd",
|
|
211
|
+
];
|
|
181
212
|
/**
|
|
182
213
|
* Downloaders / decoders / remote-copy tools that can plant files without shell
|
|
183
214
|
* redirects (#3206 / #3213 / #3245). Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
|
|
@@ -302,6 +333,23 @@ const DOWNLOADER_DECODER_BINS = new Set([
|
|
|
302
333
|
"gh",
|
|
303
334
|
"glab",
|
|
304
335
|
"hub",
|
|
336
|
+
// #3529 residual after #3459: dest-form writers harvest missed.
|
|
337
|
+
"xcopy",
|
|
338
|
+
"robocopy",
|
|
339
|
+
"bitsadmin",
|
|
340
|
+
"start-bitstransfer",
|
|
341
|
+
"expand-archive",
|
|
342
|
+
"tee-object",
|
|
343
|
+
"export-csv",
|
|
344
|
+
"pscp",
|
|
345
|
+
"jj",
|
|
346
|
+
"sponge",
|
|
347
|
+
"expand",
|
|
348
|
+
"extrac32",
|
|
349
|
+
"makecab",
|
|
350
|
+
"replace",
|
|
351
|
+
// #3545 residual after #3529: dest-form writers harvest missed.
|
|
352
|
+
...UAT_RESIDUAL_DEST_WRITE_BINS_3545,
|
|
305
353
|
]);
|
|
306
354
|
/**
|
|
307
355
|
* Archive extractors / alt writers that can plant via pathish operands without
|
|
@@ -399,6 +447,21 @@ const ARCHIVE_ALT_WRITE_BINS = new Set([
|
|
|
399
447
|
"iwr",
|
|
400
448
|
"invoke-webrequest",
|
|
401
449
|
"fsutil",
|
|
450
|
+
// #3529 residual: dest writers that are not general-purpose cmd/git.
|
|
451
|
+
"xcopy",
|
|
452
|
+
"robocopy",
|
|
453
|
+
"bitsadmin",
|
|
454
|
+
"start-bitstransfer",
|
|
455
|
+
"expand-archive",
|
|
456
|
+
"tee-object",
|
|
457
|
+
"export-csv",
|
|
458
|
+
"sponge",
|
|
459
|
+
"expand",
|
|
460
|
+
"extrac32",
|
|
461
|
+
"makecab",
|
|
462
|
+
"replace",
|
|
463
|
+
// #3545 residual: dest writers that are not general-purpose cmd/git.
|
|
464
|
+
...UAT_RESIDUAL_INPLACE_WRITE_BINS_3545,
|
|
402
465
|
]);
|
|
403
466
|
/**
|
|
404
467
|
* Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354 / #3382).
|
|
@@ -502,6 +565,23 @@ const PROTECTED_POSITIONAL_BINS = new Set([
|
|
|
502
565
|
"iwr",
|
|
503
566
|
"invoke-webrequest",
|
|
504
567
|
"fsutil",
|
|
568
|
+
// #3529 residual: positional dest writers (xcopy/robocopy/BitsTransfer/pscp).
|
|
569
|
+
// jj clone dests are harvested only on the clone subcommand.
|
|
570
|
+
"xcopy",
|
|
571
|
+
"robocopy",
|
|
572
|
+
"bitsadmin",
|
|
573
|
+
"start-bitstransfer",
|
|
574
|
+
"expand-archive",
|
|
575
|
+
"tee-object",
|
|
576
|
+
"export-csv",
|
|
577
|
+
"pscp",
|
|
578
|
+
"sponge",
|
|
579
|
+
"expand",
|
|
580
|
+
"extrac32",
|
|
581
|
+
"makecab",
|
|
582
|
+
"replace",
|
|
583
|
+
// #3545 residual: positional dest writers (fromdos/dpkg/degit/ddrescue).
|
|
584
|
+
...UAT_RESIDUAL_DEST_WRITE_BINS_3545,
|
|
505
585
|
]);
|
|
506
586
|
/**
|
|
507
587
|
* Bins whose dest is the last pathish operand (src … dest). A protected
|
|
@@ -626,6 +706,19 @@ const GENERIC_PROTECTED_EXTRA_DEST_FLAGS = new Set([
|
|
|
626
706
|
"--file",
|
|
627
707
|
"--separate-git-dir",
|
|
628
708
|
"--pack-destination",
|
|
709
|
+
// #3529 residual dest flags (PowerShell single-dash + extra dir dests).
|
|
710
|
+
"--output-dir",
|
|
711
|
+
"--outdir",
|
|
712
|
+
"--target-directory",
|
|
713
|
+
"--destdir",
|
|
714
|
+
"-destination",
|
|
715
|
+
"-destinationpath",
|
|
716
|
+
"-filepath",
|
|
717
|
+
"--prefix",
|
|
718
|
+
// #3545 residual dest flags (VCS repo dest + ghostscript output).
|
|
719
|
+
"--repodir",
|
|
720
|
+
"--repo-dir",
|
|
721
|
+
"-soutputfile",
|
|
629
722
|
]);
|
|
630
723
|
/** Bins whose `--file` / `-f` operand is an input, not a dest plant. */
|
|
631
724
|
const READ_SHAPED_FILE_FLAG_BINS = new Set([
|
|
@@ -649,6 +742,18 @@ const READ_INPUT_FILE_FLAGS = new Set(["--file", "-f"]);
|
|
|
649
742
|
const NEW_ITEM_PATH_DEST_FLAGS = new Set(["-path", "-literalpath", "--literalpath"]);
|
|
650
743
|
/** git subcommands whose positionals can plant a dest (#3421). */
|
|
651
744
|
const GIT_WRITE_SUBCOMMANDS = new Set(["clone", "worktree", "submodule"]);
|
|
745
|
+
/** git bundle subcommands that write a bundle file (#3529). Reads: verify / list-heads / unbundle. */
|
|
746
|
+
const GIT_BUNDLE_WRITE_SUBCOMMANDS = new Set(["create"]);
|
|
747
|
+
/** jj subcommands whose positionals can plant a dest (#3529). */
|
|
748
|
+
const JJ_WRITE_SUBCOMMANDS = new Set(["clone"]);
|
|
749
|
+
const JJ_PRE_SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
750
|
+
"--help",
|
|
751
|
+
"-h",
|
|
752
|
+
"--quiet",
|
|
753
|
+
"-q",
|
|
754
|
+
"--ignore-immutable",
|
|
755
|
+
"--no-pager",
|
|
756
|
+
]);
|
|
652
757
|
/** Forge CLIs whose `clone` dest is harvested like git clone (#3459). */
|
|
653
758
|
const FORGE_CLONE_BINS = new Set(["gh", "glab", "hub"]);
|
|
654
759
|
/** fossil dest-dir flags (#3382 PATH form + #3421 --workdir=). */
|
|
@@ -756,14 +861,17 @@ function isDownloaderDecoderBin(token) {
|
|
|
756
861
|
return false;
|
|
757
862
|
return DOWNLOADER_DECODER_BINS.has(writeBinName(token));
|
|
758
863
|
}
|
|
864
|
+
function isScpFamilyBin(bin) {
|
|
865
|
+
return bin === "scp" || bin === "pscp";
|
|
866
|
+
}
|
|
759
867
|
/**
|
|
760
868
|
* True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
|
|
761
869
|
* tar `-C` must use raw case: lowercased `-c` is create-archive, not chdir (#3245).
|
|
762
870
|
* 7z uses attached `-oDIR` only — separate `-o PATH` is not a 7z dest form (handled elsewhere).
|
|
763
871
|
*/
|
|
764
872
|
function isDownloaderDestFlag(flag, bin, rawFlag) {
|
|
765
|
-
// scp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
|
|
766
|
-
if (bin
|
|
873
|
+
// scp/pscp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
|
|
874
|
+
if (isScpFamilyBin(bin))
|
|
767
875
|
return false;
|
|
768
876
|
// 7z family: only attached `-oDIR` (parsed in attached-short branch), not separate `-o PATH`.
|
|
769
877
|
if (SEVEN_Z_FAMILY_BINS.has(bin)) {
|
|
@@ -864,6 +972,28 @@ function firstUnquotedShellOpIndex(raw) {
|
|
|
864
972
|
}
|
|
865
973
|
return -1;
|
|
866
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* dpkg/pdftk/gs only harvest protected positionals on write forms
|
|
977
|
+
* (`dpkg -x`, `pdftk … output`, gs dest flags). Inspect/read forms stay unclassifiable.
|
|
978
|
+
*/
|
|
979
|
+
function residualPositionalBinIsWriteShaped(bin, tokens, start) {
|
|
980
|
+
if (bin !== "dpkg" && bin !== "pdftk" && bin !== "gs")
|
|
981
|
+
return true;
|
|
982
|
+
for (let k = start; k < tokens.length; k++) {
|
|
983
|
+
const raw = tokens[k];
|
|
984
|
+
if (tokenEndsShellSegment(raw))
|
|
985
|
+
break;
|
|
986
|
+
const n = normalizeToken(raw);
|
|
987
|
+
if (bin === "dpkg" && (n === "-x" || n === "--extract" || n === "--vextract")) {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
if (bin === "pdftk" && n === "output")
|
|
991
|
+
return true;
|
|
992
|
+
if (bin === "gs" && (n.startsWith("-soutputfile") || n === "-soutputfile"))
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
867
997
|
/**
|
|
868
998
|
* Destinations from curl/wget/xxd/openssl/scp/aria2c/certutil via -o/--output/-O/-out/
|
|
869
999
|
* --output-dir/-P/-d/--dir (separate, =value, or attached short form), xxd -r path-like
|
|
@@ -892,8 +1022,10 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
892
1022
|
// Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
|
|
893
1023
|
let lastPositionalPath = null;
|
|
894
1024
|
const protectedPathish = [];
|
|
895
|
-
const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin) ||
|
|
1025
|
+
const collectsProtectedPositionals = (PROTECTED_POSITIONAL_BINS.has(bin) && residualPositionalBinIsWriteShaped(bin, tokens, i)) ||
|
|
896
1026
|
(bin === "git" && gitHasWriteSubcommand(tokens, i)) ||
|
|
1027
|
+
(bin === "git" && gitHasBundleCreate(tokens, i)) ||
|
|
1028
|
+
(bin === "jj" && jjHasWriteSubcommand(tokens, i)) ||
|
|
897
1029
|
(FORGE_CLONE_BINS.has(bin) && forgeHasCloneSubcommand(tokens, i));
|
|
898
1030
|
while (i < tokens.length) {
|
|
899
1031
|
const raw = tokens[i];
|
|
@@ -926,8 +1058,8 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
926
1058
|
i++;
|
|
927
1059
|
continue;
|
|
928
1060
|
}
|
|
929
|
-
// scp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
|
|
930
|
-
if (bin
|
|
1061
|
+
// scp/pscp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
|
|
1062
|
+
if (isScpFamilyBin(bin) &&
|
|
931
1063
|
(SCP_VALUE_FLAGS_LOWER.has(n) ||
|
|
932
1064
|
SCP_VALUE_FLAGS_EXACT.has(raw) ||
|
|
933
1065
|
SCP_VALUE_FLAGS_EXACT.has(n))) {
|
|
@@ -953,9 +1085,9 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
953
1085
|
continue;
|
|
954
1086
|
}
|
|
955
1087
|
// Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
|
|
956
|
-
// Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
|
|
1088
|
+
// Skip for scp/pscp (OpenSSH -oOption=Value attached forms are not file dests).
|
|
957
1089
|
// 7z requires attached -oDIR (#3245).
|
|
958
|
-
if (bin
|
|
1090
|
+
if (!isScpFamilyBin(bin) && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
|
|
959
1091
|
// `-outfile` is PowerShell -OutFile (next token is dest), not attached -outFILE (#3459).
|
|
960
1092
|
if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output") && n !== "-outfile") {
|
|
961
1093
|
dests.push(pathishToken(raw.slice(4)));
|
|
@@ -1180,6 +1312,179 @@ function pathishIsProtectedDest(pathish) {
|
|
|
1180
1312
|
function isGenericProtectedDestFlag(flag) {
|
|
1181
1313
|
return DOWNLOADER_FILE_DEST_FLAGS.has(flag) || GENERIC_PROTECTED_EXTRA_DEST_FLAGS.has(flag);
|
|
1182
1314
|
}
|
|
1315
|
+
/** Env-style dest keys harvested even without a leading `--` (`make DESTDIR=`). */
|
|
1316
|
+
const DEST_ASSIGNMENT_KEYS = new Set(["destdir", "prefix", "install_root", "dest_dir"]);
|
|
1317
|
+
/** Bins whose DESTDIR=/PREFIX= assignment is a dest plant (not `echo DESTDIR=…`). */
|
|
1318
|
+
const DEST_ASSIGNMENT_OWNER_BINS = new Set(["make", "gmake"]);
|
|
1319
|
+
/**
|
|
1320
|
+
* First-command bins that print/read: a later `make` token is data, not the writer
|
|
1321
|
+
* (`echo DESTDIR=… make`, `git log make DESTDIR=…`). Also the first command of a
|
|
1322
|
+
* search-launcher `-exec` / `-c` payload (`find -exec echo make DESTDIR=`).
|
|
1323
|
+
* Search/launchers themselves are not in this set — `find -exec make DESTDIR=`
|
|
1324
|
+
* still harvests the payload writer.
|
|
1325
|
+
*/
|
|
1326
|
+
const DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS = new Set([
|
|
1327
|
+
"echo",
|
|
1328
|
+
"printf",
|
|
1329
|
+
"true",
|
|
1330
|
+
"false",
|
|
1331
|
+
":",
|
|
1332
|
+
"git",
|
|
1333
|
+
"gh",
|
|
1334
|
+
"hg",
|
|
1335
|
+
"svn",
|
|
1336
|
+
"cat",
|
|
1337
|
+
"ls",
|
|
1338
|
+
"grep",
|
|
1339
|
+
"egrep",
|
|
1340
|
+
"fgrep",
|
|
1341
|
+
"rg",
|
|
1342
|
+
"head",
|
|
1343
|
+
"tail",
|
|
1344
|
+
"less",
|
|
1345
|
+
"more",
|
|
1346
|
+
"wc",
|
|
1347
|
+
"diff",
|
|
1348
|
+
"stat",
|
|
1349
|
+
"file",
|
|
1350
|
+
]);
|
|
1351
|
+
/**
|
|
1352
|
+
* Search/launchers whose first token is not the writer. Harvest make/DESTDIR
|
|
1353
|
+
* only from `-exec` / `--exec` / `-c` payloads (`find -exec make DESTDIR=`).
|
|
1354
|
+
* The payload's first command still uses NON_WRITER (`find -exec echo make`).
|
|
1355
|
+
* `xargs make DESTDIR=` stays a wrapper (not in the non-writer set).
|
|
1356
|
+
*/
|
|
1357
|
+
const DEST_ASSIGNMENT_SEARCH_LAUNCHER_BINS = new Set(["find"]);
|
|
1358
|
+
const DEST_ASSIGNMENT_EXEC_PAYLOAD_FLAGS = new Set([
|
|
1359
|
+
"-exec",
|
|
1360
|
+
"--exec",
|
|
1361
|
+
"-execdir",
|
|
1362
|
+
"--execdir",
|
|
1363
|
+
"-c",
|
|
1364
|
+
]);
|
|
1365
|
+
/** find `-exec` payload terminator (`\;` or `+`). Resets so a later `-exec` is still operative. */
|
|
1366
|
+
function isFindExecPayloadTerminator(n) {
|
|
1367
|
+
return n === ";" || n === "+";
|
|
1368
|
+
}
|
|
1369
|
+
/** make targets that do not apply DESTDIR (avoid denying `make DESTDIR=… clean`). */
|
|
1370
|
+
const DEST_ASSIGNMENT_NON_WRITE_TARGETS = new Set([
|
|
1371
|
+
"clean",
|
|
1372
|
+
"distclean",
|
|
1373
|
+
"mostlyclean",
|
|
1374
|
+
"maintainer-clean",
|
|
1375
|
+
"check",
|
|
1376
|
+
"test",
|
|
1377
|
+
"tests",
|
|
1378
|
+
"help",
|
|
1379
|
+
]);
|
|
1380
|
+
/** make flags that consume the next token (`-C dir`, `-f Makefile`). */
|
|
1381
|
+
const MAKE_VALUE_FLAGS = new Set([
|
|
1382
|
+
"-c",
|
|
1383
|
+
"--directory",
|
|
1384
|
+
"-f",
|
|
1385
|
+
"--file",
|
|
1386
|
+
"--makefile",
|
|
1387
|
+
"--include-dir",
|
|
1388
|
+
"-j",
|
|
1389
|
+
"--jobs",
|
|
1390
|
+
"-l",
|
|
1391
|
+
"--load-average",
|
|
1392
|
+
"--eval",
|
|
1393
|
+
]);
|
|
1394
|
+
/**
|
|
1395
|
+
* True when a DESTDIR/PREFIX assignment is an operative make write.
|
|
1396
|
+
* Looks for make/gmake anywhere after wrappers (including unknown wrappers
|
|
1397
|
+
* such as `xargs`), unless the first command is a print/read bin.
|
|
1398
|
+
* Search/launchers (`find`) still harvest from `-exec` / `--exec` / `-c`
|
|
1399
|
+
* payloads unless that payload's first command is a print/read bin.
|
|
1400
|
+
* Payload state resets at `\;` / `+` so a later `-exec make DESTDIR=` is
|
|
1401
|
+
* still operative (`find -exec echo hi \; -exec make DESTDIR= install`).
|
|
1402
|
+
* Non-writing targets (`clean`) are not operative.
|
|
1403
|
+
*/
|
|
1404
|
+
function segmentDestAssignmentIsOperative(tokens, tokenIndex) {
|
|
1405
|
+
let start = tokenIndex;
|
|
1406
|
+
while (start > 0 && !tokenEndsShellSegment(tokens[start - 1]))
|
|
1407
|
+
start--;
|
|
1408
|
+
let skipNext = false;
|
|
1409
|
+
let firstCommand = "";
|
|
1410
|
+
let inExecPayload = false;
|
|
1411
|
+
let execPayloadFirstCommand = "";
|
|
1412
|
+
let sawMake = false;
|
|
1413
|
+
const targets = [];
|
|
1414
|
+
for (let k = start; k < tokens.length; k++) {
|
|
1415
|
+
const raw = tokens[k];
|
|
1416
|
+
if (tokenEndsShellSegment(raw))
|
|
1417
|
+
break;
|
|
1418
|
+
if (skipNext) {
|
|
1419
|
+
skipNext = false;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const n = normalizeToken(raw);
|
|
1423
|
+
if (inExecPayload && isFindExecPayloadTerminator(n)) {
|
|
1424
|
+
inExecPayload = false;
|
|
1425
|
+
execPayloadFirstCommand = "";
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
// Print/read `-exec` payload: skip until terminator so a later `-exec` is seen.
|
|
1429
|
+
if (inExecPayload &&
|
|
1430
|
+
execPayloadFirstCommand.length > 0 &&
|
|
1431
|
+
DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS.has(execPayloadFirstCommand)) {
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (n.startsWith("-")) {
|
|
1435
|
+
// find -exec / -c opens the payload; do not consume the next token as a
|
|
1436
|
+
// wrapper value. After make is seen, -C still skips its directory.
|
|
1437
|
+
if (DEST_ASSIGNMENT_EXEC_PAYLOAD_FLAGS.has(n) &&
|
|
1438
|
+
DEST_ASSIGNMENT_SEARCH_LAUNCHER_BINS.has(firstCommand) &&
|
|
1439
|
+
!sawMake) {
|
|
1440
|
+
inExecPayload = true;
|
|
1441
|
+
execPayloadFirstCommand = "";
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (!n.includes("=") && (WRAPPER_VALUE_FLAGS.has(n) || MAKE_VALUE_FLAGS.has(n))) {
|
|
1445
|
+
skipNext = true;
|
|
1446
|
+
}
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (isEnvAssign(raw))
|
|
1450
|
+
continue;
|
|
1451
|
+
const bare = writeBinName(raw);
|
|
1452
|
+
if (COMMAND_WRAPPER_BINS.has(bare))
|
|
1453
|
+
continue;
|
|
1454
|
+
if (bare.length > 0 && bare[0] !== undefined && bare[0] >= "0" && bare[0] <= "9") {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (firstCommand.length === 0) {
|
|
1458
|
+
firstCommand = bare;
|
|
1459
|
+
if (DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS.has(bare))
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
if (inExecPayload && execPayloadFirstCommand.length === 0) {
|
|
1463
|
+
execPayloadFirstCommand = bare;
|
|
1464
|
+
}
|
|
1465
|
+
if (inExecPayload && DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS.has(execPayloadFirstCommand)) {
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
if (DEST_ASSIGNMENT_OWNER_BINS.has(bare)) {
|
|
1469
|
+
if (DEST_ASSIGNMENT_SEARCH_LAUNCHER_BINS.has(firstCommand) && !inExecPayload) {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
sawMake = true;
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (sawMake)
|
|
1476
|
+
targets.push(n);
|
|
1477
|
+
}
|
|
1478
|
+
if (!sawMake)
|
|
1479
|
+
return false;
|
|
1480
|
+
if (targets.length === 0)
|
|
1481
|
+
return true;
|
|
1482
|
+
for (const t of targets) {
|
|
1483
|
+
if (!DEST_ASSIGNMENT_NON_WRITE_TARGETS.has(t))
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1183
1488
|
function isReadShapedInputFileFlag(bin, flag) {
|
|
1184
1489
|
return READ_SHAPED_FILE_FLAG_BINS.has(bin) && READ_INPUT_FILE_FLAGS.has(flag);
|
|
1185
1490
|
}
|
|
@@ -1208,13 +1513,13 @@ const GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
1208
1513
|
"-q",
|
|
1209
1514
|
]);
|
|
1210
1515
|
/**
|
|
1211
|
-
* True only when the
|
|
1516
|
+
* True only when the VCS *subcommand* (first non-flag token) is a dest writer.
|
|
1212
1517
|
* Later operands named clone/worktree/submodule (e.g. `git log worktree -- …`)
|
|
1213
1518
|
* are not write subcommands (#3423 residual).
|
|
1214
1519
|
* Boolean globals (`--no-pager`) do not consume the next token. Other globals
|
|
1215
1520
|
* consume it even when that token spells a subcommand (#3421 residual).
|
|
1216
1521
|
*/
|
|
1217
|
-
function
|
|
1522
|
+
function hasWriteSubcommand(tokens, start, writeSubs, booleanFlags) {
|
|
1218
1523
|
let i = start;
|
|
1219
1524
|
while (i < tokens.length) {
|
|
1220
1525
|
const raw = tokens[i];
|
|
@@ -1222,7 +1527,7 @@ function gitHasWriteSubcommand(tokens, start) {
|
|
|
1222
1527
|
return false;
|
|
1223
1528
|
const n = normalizeToken(raw);
|
|
1224
1529
|
if (n.startsWith("-")) {
|
|
1225
|
-
if (!n.includes("=") && !
|
|
1530
|
+
if (!n.includes("=") && !booleanFlags.has(n) && !n.startsWith("--no-")) {
|
|
1226
1531
|
const next = tokens[i + 1];
|
|
1227
1532
|
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1228
1533
|
i += 2;
|
|
@@ -1232,10 +1537,46 @@ function gitHasWriteSubcommand(tokens, start) {
|
|
|
1232
1537
|
i++;
|
|
1233
1538
|
continue;
|
|
1234
1539
|
}
|
|
1235
|
-
return
|
|
1540
|
+
return writeSubs.has(n);
|
|
1236
1541
|
}
|
|
1237
1542
|
return false;
|
|
1238
1543
|
}
|
|
1544
|
+
function gitHasWriteSubcommand(tokens, start) {
|
|
1545
|
+
return hasWriteSubcommand(tokens, start, GIT_WRITE_SUBCOMMANDS, GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS);
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* True when this git segment is `bundle create` (write), not `bundle verify` / `list-heads`
|
|
1549
|
+
* / `unbundle` (read of an existing bundle) (#3529 Greptile P1).
|
|
1550
|
+
*/
|
|
1551
|
+
function gitHasBundleCreate(tokens, start) {
|
|
1552
|
+
const nonFlags = [];
|
|
1553
|
+
let i = start;
|
|
1554
|
+
while (i < tokens.length) {
|
|
1555
|
+
const raw = tokens[i];
|
|
1556
|
+
if (isShellSegmentBreak(raw))
|
|
1557
|
+
break;
|
|
1558
|
+
const n = normalizeToken(raw);
|
|
1559
|
+
if (n.startsWith("-")) {
|
|
1560
|
+
if (!n.includes("=") && !GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS.has(n) && !n.startsWith("--no-")) {
|
|
1561
|
+
const next = tokens[i + 1];
|
|
1562
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1563
|
+
i += 2;
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
i++;
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
nonFlags.push(n);
|
|
1571
|
+
if (nonFlags.length >= 2)
|
|
1572
|
+
break;
|
|
1573
|
+
i++;
|
|
1574
|
+
}
|
|
1575
|
+
return nonFlags[0] === "bundle" && GIT_BUNDLE_WRITE_SUBCOMMANDS.has(nonFlags[1] ?? "");
|
|
1576
|
+
}
|
|
1577
|
+
function jjHasWriteSubcommand(tokens, start) {
|
|
1578
|
+
return hasWriteSubcommand(tokens, start, JJ_WRITE_SUBCOMMANDS, JJ_PRE_SUBCOMMAND_BOOLEAN_FLAGS);
|
|
1579
|
+
}
|
|
1239
1580
|
/**
|
|
1240
1581
|
* True when this forge-CLI segment is a clone dest writer
|
|
1241
1582
|
* (`gh repo clone`, `glab repo clone`, `hub clone`) (#3459).
|
|
@@ -1288,7 +1629,7 @@ function genericProtectedDests(tokens) {
|
|
|
1288
1629
|
currentBin = "";
|
|
1289
1630
|
continue;
|
|
1290
1631
|
}
|
|
1291
|
-
if (!n.startsWith("-")) {
|
|
1632
|
+
if (!n.startsWith("-") && !isEnvAssign(raw)) {
|
|
1292
1633
|
if (isDownloaderDecoderBin(raw)) {
|
|
1293
1634
|
currentBin = writeBinName(raw);
|
|
1294
1635
|
}
|
|
@@ -1299,8 +1640,22 @@ function genericProtectedDests(tokens) {
|
|
|
1299
1640
|
currentBin = writeBinName(raw);
|
|
1300
1641
|
}
|
|
1301
1642
|
}
|
|
1302
|
-
if (currentBin
|
|
1643
|
+
if (isScpFamilyBin(currentBin) || currentBin === "cpio")
|
|
1303
1644
|
continue;
|
|
1645
|
+
// Env-style dest assignments (`DESTDIR=`, `PREFIX=`) without a leading dash (#3545).
|
|
1646
|
+
if (n.includes("=") && !n.startsWith("-")) {
|
|
1647
|
+
const eq = raw.indexOf("=");
|
|
1648
|
+
const keyRaw = raw.slice(0, eq);
|
|
1649
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(keyRaw)) {
|
|
1650
|
+
const key = normalizeToken(keyRaw);
|
|
1651
|
+
if (DEST_ASSIGNMENT_KEYS.has(key) && segmentDestAssignmentIsOperative(tokens, i)) {
|
|
1652
|
+
const dest = pathishToken(raw.slice(eq + 1));
|
|
1653
|
+
if (pathishIsProtectedDest(dest))
|
|
1654
|
+
dests.push(dest);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1304
1659
|
if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
|
|
1305
1660
|
const eq = raw.indexOf("=");
|
|
1306
1661
|
const flag = normalizeToken(raw.slice(0, eq));
|
|
@@ -1311,6 +1666,17 @@ function genericProtectedDests(tokens) {
|
|
|
1311
1666
|
}
|
|
1312
1667
|
continue;
|
|
1313
1668
|
}
|
|
1669
|
+
// Named dest flags (`-Destination`, `--output-dir`) take the next token.
|
|
1670
|
+
// Check before attached `-oDIR` / `-dDIR` so `-destination` is not `-d` + `estination`.
|
|
1671
|
+
if (isGenericProtectedDestFlag(n) && !isReadShapedInputFileFlag(currentBin, n)) {
|
|
1672
|
+
const next = tokens[i + 1];
|
|
1673
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1674
|
+
const dest = pathishToken(next);
|
|
1675
|
+
if (pathishIsProtectedDest(dest))
|
|
1676
|
+
dests.push(dest);
|
|
1677
|
+
}
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1314
1680
|
if (n.startsWith("-") &&
|
|
1315
1681
|
!n.startsWith("--") &&
|
|
1316
1682
|
n.startsWith("-o") &&
|
|
@@ -1325,15 +1691,6 @@ function genericProtectedDests(tokens) {
|
|
|
1325
1691
|
const dest = pathishToken(raw.slice(2));
|
|
1326
1692
|
if (pathishIsProtectedDest(dest))
|
|
1327
1693
|
dests.push(dest);
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
if (isGenericProtectedDestFlag(n) && !isReadShapedInputFileFlag(currentBin, n)) {
|
|
1331
|
-
const next = tokens[i + 1];
|
|
1332
|
-
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1333
|
-
const dest = pathishToken(next);
|
|
1334
|
-
if (pathishIsProtectedDest(dest))
|
|
1335
|
-
dests.push(dest);
|
|
1336
|
-
}
|
|
1337
1694
|
}
|
|
1338
1695
|
}
|
|
1339
1696
|
return dests;
|
|
@@ -1497,20 +1854,39 @@ function isProgrammaticWriteBinToken(token) {
|
|
|
1497
1854
|
bare === "nodejs" ||
|
|
1498
1855
|
bare === "perl" ||
|
|
1499
1856
|
bare === "ruby" ||
|
|
1857
|
+
bare === "jruby" ||
|
|
1858
|
+
bare === "pypy" ||
|
|
1859
|
+
bare === "pypy3" ||
|
|
1500
1860
|
bare === "pwsh" ||
|
|
1501
1861
|
bare === "powershell" ||
|
|
1502
1862
|
bare === "tsx" ||
|
|
1503
1863
|
bare === "ts-node" ||
|
|
1504
|
-
bare === "tsnode"
|
|
1864
|
+
bare === "tsnode" ||
|
|
1865
|
+
bare === "bun" ||
|
|
1866
|
+
bare === "deno" ||
|
|
1867
|
+
bare === "php" ||
|
|
1868
|
+
bare === "lua" ||
|
|
1869
|
+
bare === "luajit") {
|
|
1505
1870
|
return true;
|
|
1506
1871
|
}
|
|
1507
|
-
// Versioned: python3.11, python3.12, node18, …
|
|
1872
|
+
// Versioned: python3.11, python3.12, node18, php8.3, lua5.4, …
|
|
1508
1873
|
if (bare.startsWith("python3.") || bare.startsWith("python2."))
|
|
1509
1874
|
return true;
|
|
1510
1875
|
if (/^python\d+(\.\d+)*$/.test(bare))
|
|
1511
1876
|
return true;
|
|
1512
1877
|
if (/^node\d+$/.test(bare))
|
|
1513
1878
|
return true;
|
|
1879
|
+
if (/^php\d+(\.\d+)*$/.test(bare))
|
|
1880
|
+
return true;
|
|
1881
|
+
if (/^lua\d+(\.\d+)*$/.test(bare))
|
|
1882
|
+
return true;
|
|
1883
|
+
// #3545 residual: versioned ruby / jruby / pypy (bare `ruby` already matched).
|
|
1884
|
+
if (/^ruby\d+(\.\d+)*$/.test(bare))
|
|
1885
|
+
return true;
|
|
1886
|
+
if (/^jruby\d+(\.\d+)*$/.test(bare))
|
|
1887
|
+
return true;
|
|
1888
|
+
if (bare.startsWith("pypy3.") || /^pypy\d+(\.\d+)*$/.test(bare))
|
|
1889
|
+
return true;
|
|
1514
1890
|
return false;
|
|
1515
1891
|
}
|
|
1516
1892
|
/**
|
|
@@ -1518,6 +1894,74 @@ function isProgrammaticWriteBinToken(token) {
|
|
|
1518
1894
|
* Escaped quotes (`\'` / `\"`) stay inside the string. Used so `print('.write(')` is not
|
|
1519
1895
|
* a write API while `open(p,'w').write('x')` still matches `.write(` (Greptile conf residual).
|
|
1520
1896
|
*/
|
|
1897
|
+
/**
|
|
1898
|
+
* True when `ident` is followed by optional whitespace then `(` (PHP `file_put_contents (`).
|
|
1899
|
+
* Skips matches inside quotes so `echo "file_put_contents ($x)"` is not a write.
|
|
1900
|
+
* O(n) — no nested-quantifier regex.
|
|
1901
|
+
*/
|
|
1902
|
+
function includesIdentCall(haystack, ident) {
|
|
1903
|
+
if (ident.length === 0 || haystack.length < ident.length)
|
|
1904
|
+
return false;
|
|
1905
|
+
let inSingle = false;
|
|
1906
|
+
let inDouble = false;
|
|
1907
|
+
for (let i = 0; i < haystack.length; i++) {
|
|
1908
|
+
const c = haystack[i];
|
|
1909
|
+
if (c === "\\" && i + 1 < haystack.length && (inSingle || inDouble)) {
|
|
1910
|
+
i++;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (c === "'" && !inDouble) {
|
|
1914
|
+
inSingle = !inSingle;
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
if (c === '"' && !inSingle) {
|
|
1918
|
+
inDouble = !inDouble;
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
if (inSingle || inDouble)
|
|
1922
|
+
continue;
|
|
1923
|
+
if (!haystack.startsWith(ident, i))
|
|
1924
|
+
continue;
|
|
1925
|
+
let j = i + ident.length;
|
|
1926
|
+
while (j < haystack.length) {
|
|
1927
|
+
const w = haystack[j];
|
|
1928
|
+
if (w === " " || w === "\t" || w === "\n" || w === "\r") {
|
|
1929
|
+
j++;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
if (j < haystack.length && haystack[j] === "(")
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
/** Strip matching wrapping quotes from a `-r`/`-e` payload. */
|
|
1940
|
+
function unwrapQuotedPayload(token) {
|
|
1941
|
+
if (token.length >= 2) {
|
|
1942
|
+
const a = token[0];
|
|
1943
|
+
const b = token[token.length - 1];
|
|
1944
|
+
if ((a === '"' && b === '"') || (a === "'" && b === "'")) {
|
|
1945
|
+
return token.slice(1, -1);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return token;
|
|
1949
|
+
}
|
|
1950
|
+
/** Script bodies after interpreter `-r`/`-e`/`-c` (PHP/node one-liners). */
|
|
1951
|
+
function interpreterScriptPayloads(tokens) {
|
|
1952
|
+
const out = [];
|
|
1953
|
+
const flags = new Set(["-r", "-e", "-c", "--run", "--eval", "--execute"]);
|
|
1954
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1955
|
+
const n = normalizeToken(tokens[i]);
|
|
1956
|
+
if (!flags.has(n))
|
|
1957
|
+
continue;
|
|
1958
|
+
const next = tokens[i + 1];
|
|
1959
|
+
if (next === undefined || isShellSegmentBreak(next) || String(next).startsWith("-"))
|
|
1960
|
+
continue;
|
|
1961
|
+
out.push(unwrapQuotedPayload(next));
|
|
1962
|
+
}
|
|
1963
|
+
return out;
|
|
1964
|
+
}
|
|
1521
1965
|
function includesOutsideQuotes(haystack, needle) {
|
|
1522
1966
|
if (needle.length === 0 || haystack.length < needle.length)
|
|
1523
1967
|
return false;
|
|
@@ -1544,6 +1988,85 @@ function includesOutsideQuotes(haystack, needle) {
|
|
|
1544
1988
|
}
|
|
1545
1989
|
return false;
|
|
1546
1990
|
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Perl `open F,">dest"` / `open FH, '>'` without parens (#3545).
|
|
1993
|
+
* Quote-aware so `print 'open F,>'` is not a write. O(n).
|
|
1994
|
+
*/
|
|
1995
|
+
function hasPerlBareOpenWrite(haystack) {
|
|
1996
|
+
let inSingle = false;
|
|
1997
|
+
let inDouble = false;
|
|
1998
|
+
for (let i = 0; i < haystack.length; i++) {
|
|
1999
|
+
const c = haystack[i];
|
|
2000
|
+
if (c === "\\" && i + 1 < haystack.length && (inSingle || inDouble)) {
|
|
2001
|
+
i++;
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
if (c === "'" && !inDouble) {
|
|
2005
|
+
inSingle = !inSingle;
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (c === '"' && !inSingle) {
|
|
2009
|
+
inDouble = !inDouble;
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
if (inSingle || inDouble)
|
|
2013
|
+
continue;
|
|
2014
|
+
if (!haystack.startsWith("open", i))
|
|
2015
|
+
continue;
|
|
2016
|
+
if (i > 0) {
|
|
2017
|
+
const prev = haystack[i - 1];
|
|
2018
|
+
if ((prev >= "a" && prev <= "z") || (prev >= "0" && prev <= "9") || prev === "_")
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
let j = i + 4;
|
|
2022
|
+
while (j < haystack.length) {
|
|
2023
|
+
const w = haystack[j];
|
|
2024
|
+
if (w === " " || w === "\t" || w === "\n" || w === "\r") {
|
|
2025
|
+
j++;
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2030
|
+
const identStart = j;
|
|
2031
|
+
while (j < haystack.length) {
|
|
2032
|
+
const ch = haystack[j];
|
|
2033
|
+
if ((ch >= "a" && ch <= "z") ||
|
|
2034
|
+
(ch >= "A" && ch <= "Z") ||
|
|
2035
|
+
(ch >= "0" && ch <= "9") ||
|
|
2036
|
+
ch === "_") {
|
|
2037
|
+
j++;
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
break;
|
|
2041
|
+
}
|
|
2042
|
+
if (j === identStart)
|
|
2043
|
+
continue;
|
|
2044
|
+
while (j < haystack.length) {
|
|
2045
|
+
const w = haystack[j];
|
|
2046
|
+
if (w === " " || w === "\t") {
|
|
2047
|
+
j++;
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
if (j >= haystack.length || haystack[j] !== ",")
|
|
2053
|
+
continue;
|
|
2054
|
+
j++;
|
|
2055
|
+
while (j < haystack.length) {
|
|
2056
|
+
const w = haystack[j];
|
|
2057
|
+
if (w === " " || w === "\t") {
|
|
2058
|
+
j++;
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
const q = haystack[j];
|
|
2064
|
+
if ((q === "'" || q === '"') && j + 1 < haystack.length && haystack[j + 1] === ">") {
|
|
2065
|
+
return true;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return false;
|
|
2069
|
+
}
|
|
1547
2070
|
/**
|
|
1548
2071
|
* True when command uses a programmatic bin with a **write** API, optionally with
|
|
1549
2072
|
* path-obfuscation markers (base64/bytes/chr). Pure reads / print-only stay unclassifiable.
|
|
@@ -1574,6 +2097,8 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1574
2097
|
const writeApi = lower.includes("writefilesync") ||
|
|
1575
2098
|
lower.includes("writefile") ||
|
|
1576
2099
|
lower.includes("writetext") ||
|
|
2100
|
+
includesIdentCall(lower, "file_put_contents") ||
|
|
2101
|
+
interpreterScriptPayloads(tokens).some((p) => includesIdentCall(p.toLowerCase(), "file_put_contents")) ||
|
|
1577
2102
|
lower.includes("set-content") ||
|
|
1578
2103
|
lower.includes("out-file") ||
|
|
1579
2104
|
lower.includes("fs.write") ||
|
|
@@ -1581,6 +2106,17 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1581
2106
|
lower.includes("path.write(") ||
|
|
1582
2107
|
lower.includes("file.write(") ||
|
|
1583
2108
|
lower.includes("spurt") ||
|
|
2109
|
+
interpreterScriptPayloads(tokens).some((p) => {
|
|
2110
|
+
const pl = p.toLowerCase();
|
|
2111
|
+
return (includesIdentCall(pl, "write_file") ||
|
|
2112
|
+
includesIdentCall(pl, "spew") ||
|
|
2113
|
+
includesOutsideQuotes(pl, "->spew") ||
|
|
2114
|
+
hasPerlBareOpenWrite(pl));
|
|
2115
|
+
}) ||
|
|
2116
|
+
includesIdentCall(lower, "write_file") ||
|
|
2117
|
+
includesIdentCall(lower, "spew") ||
|
|
2118
|
+
includesOutsideQuotes(lower, "->spew") ||
|
|
2119
|
+
hasPerlBareOpenWrite(lower) ||
|
|
1584
2120
|
lower.includes(">>") ||
|
|
1585
2121
|
lower.includes("unlink(") ||
|
|
1586
2122
|
lower.includes("rmsync") ||
|
|
@@ -1618,7 +2154,13 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1618
2154
|
lower.includes("mode='a") ||
|
|
1619
2155
|
lower.includes('mode="a') ||
|
|
1620
2156
|
lower.includes("mode=w"));
|
|
1621
|
-
|
|
2157
|
+
// Perl `open F,">dest"` — `open(` is absent; `-e` payloads with spaces split tokens (#3545).
|
|
2158
|
+
const perlBareOpenWrite = (lower.includes("open ") || lower.includes("open\t")) &&
|
|
2159
|
+
(lower.includes(",'>") ||
|
|
2160
|
+
lower.includes(',">') ||
|
|
2161
|
+
lower.includes(", '>") ||
|
|
2162
|
+
lower.includes(', ">'));
|
|
2163
|
+
const writeish = writeApi || openWriteMode || perlBareOpenWrite;
|
|
1622
2164
|
// Path construction that hides the destination from literal classifiers.
|
|
1623
2165
|
const obfuscatedPath = lower.includes("base64") ||
|
|
1624
2166
|
lower.includes("fromcharcode") ||
|
|
@@ -1833,6 +2375,29 @@ const INDIRECT_WRITE_BINS = new Set([
|
|
|
1833
2375
|
"touch",
|
|
1834
2376
|
"tsx",
|
|
1835
2377
|
"ts-node",
|
|
2378
|
+
// #3529 residual: Windows dest writers / rename / moreutils sponge / runtimes.
|
|
2379
|
+
"xcopy",
|
|
2380
|
+
"robocopy",
|
|
2381
|
+
"move",
|
|
2382
|
+
"ren",
|
|
2383
|
+
"rename",
|
|
2384
|
+
"rename-item",
|
|
2385
|
+
"sponge",
|
|
2386
|
+
"bun",
|
|
2387
|
+
"deno",
|
|
2388
|
+
"php",
|
|
2389
|
+
"lua",
|
|
2390
|
+
"start-bitstransfer",
|
|
2391
|
+
"bitsadmin",
|
|
2392
|
+
"expand-archive",
|
|
2393
|
+
"tee-object",
|
|
2394
|
+
"export-csv",
|
|
2395
|
+
"expand",
|
|
2396
|
+
"extrac32",
|
|
2397
|
+
"makecab",
|
|
2398
|
+
"replace",
|
|
2399
|
+
// #3545 residual: dest writers (fromdos/degit/ddrescue). dpkg/pdftk/gs stay dest-flag gated.
|
|
2400
|
+
...UAT_RESIDUAL_INPLACE_WRITE_BINS_3545,
|
|
1836
2401
|
]);
|
|
1837
2402
|
/**
|
|
1838
2403
|
* O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
|
|
@@ -2035,15 +2600,7 @@ function hasProgrammaticAuthzEnvWrite(command, tokens) {
|
|
|
2035
2600
|
const lower = command.toLowerCase();
|
|
2036
2601
|
let hasProg = false;
|
|
2037
2602
|
for (const t of tokens) {
|
|
2038
|
-
|
|
2039
|
-
if (n === "python" ||
|
|
2040
|
-
n === "python3" ||
|
|
2041
|
-
n === "node" ||
|
|
2042
|
-
n === "nodejs" ||
|
|
2043
|
-
n === "perl" ||
|
|
2044
|
-
n === "ruby" ||
|
|
2045
|
-
n === "pwsh" ||
|
|
2046
|
-
n === "powershell") {
|
|
2603
|
+
if (isProgrammaticWriteBinToken(t)) {
|
|
2047
2604
|
hasProg = true;
|
|
2048
2605
|
break;
|
|
2049
2606
|
}
|