@deftai/directive-core 0.104.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 +1108 -67
- package/dist/check/cached-orchestrator.d.ts +1 -1
- package/dist/check/cached-orchestrator.js +2 -2
- package/dist/check/named-cause.d.ts +1 -1
- package/dist/check/named-cause.js +34 -4
- package/dist/codebase/default-extractor.d.ts +5 -0
- package/dist/codebase/default-extractor.js +7 -15
- package/dist/deposit/copy-tree.js +60 -0
- package/dist/deposit/python-free.js +26 -18
- package/dist/doctor/checks.d.ts +8 -0
- package/dist/doctor/checks.js +37 -25
- package/dist/doctor/doctor-state.d.ts +4 -0
- package/dist/doctor/doctor-state.js +7 -1
- package/dist/doctor/main.js +9 -19
- package/dist/doctor/types.d.ts +9 -0
- 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/fs/contained-write.d.ts +115 -1
- package/dist/fs/contained-write.js +241 -5
- package/dist/fs/mutation-ledger.d.ts +54 -0
- package/dist/fs/mutation-ledger.js +133 -0
- package/dist/fs/non-product-dirs.d.ts +79 -0
- package/dist/fs/non-product-dirs.js +96 -0
- package/dist/hooks/dispatcher.d.ts +3 -1
- package/dist/hooks/dispatcher.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +18 -16
- package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
- package/dist/init-deposit/core-guard-pin-content.js +393 -0
- package/dist/init-deposit/gitignore.js +9 -0
- package/dist/init-deposit/hygiene.d.ts +47 -11
- package/dist/init-deposit/hygiene.js +172 -37
- package/dist/init-deposit/legacy-detect.js +6 -2
- package/dist/init-deposit/refresh.d.ts +21 -1
- package/dist/init-deposit/refresh.js +106 -9
- package/dist/init-deposit/scaffold.js +28 -279
- package/dist/init-deposit/slash-deposit.js +13 -7
- package/dist/init-deposit/xbrief-projections.d.ts +8 -1
- package/dist/init-deposit/xbrief-projections.js +24 -11
- package/dist/intake/clause-derivation.d.ts +47 -1
- package/dist/intake/clause-derivation.js +238 -9
- package/dist/intake/issue-ingest.d.ts +3 -1
- package/dist/intake/issue-ingest.js +35 -6
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
- 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.d.ts +10 -0
- package/dist/literal-acceptance/capture.js +226 -77
- package/dist/literal-acceptance/evaluate.d.ts +29 -4
- package/dist/literal-acceptance/evaluate.js +70 -2
- package/dist/literal-acceptance/index.d.ts +3 -3
- package/dist/literal-acceptance/index.js +3 -3
- package/dist/literal-acceptance/safety.d.ts +41 -0
- package/dist/literal-acceptance/safety.js +126 -2
- package/dist/literal-acceptance/types.d.ts +8 -0
- 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/orphan-active/evaluate.d.ts +7 -1
- package/dist/orphan-active/evaluate.js +102 -23
- package/dist/platform/platform-capabilities.js +2 -3
- package/dist/policy/base-branch.d.ts +37 -0
- package/dist/policy/base-branch.js +114 -0
- package/dist/policy/branch-sync.d.ts +80 -0
- package/dist/policy/branch-sync.js +211 -0
- package/dist/policy/ceremony-dial-escalation.js +1 -0
- package/dist/policy/ceremony-dial.js +11 -21
- package/dist/policy/delivery-branch.d.ts +5 -3
- package/dist/policy/delivery-branch.js +7 -3
- package/dist/policy/file-size-thresholds.d.ts +24 -0
- package/dist/policy/file-size-thresholds.js +23 -0
- package/dist/policy/forge-outage-retry.d.ts +47 -0
- package/dist/policy/forge-outage-retry.js +157 -0
- package/dist/policy/host-hooks.d.ts +20 -0
- package/dist/policy/host-hooks.js +121 -2
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/index.js +66 -7
- 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/project-invariants.d.ts +50 -0
- package/dist/policy/project-invariants.js +205 -0
- 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/sync-default.d.ts +129 -0
- package/dist/policy/sync-default.js +452 -0
- package/dist/policy/sync-max-files.d.ts +93 -0
- package/dist/policy/sync-max-files.js +192 -0
- package/dist/policy/value-feedback.js +7 -8
- package/dist/pr-monitor/absent-required.d.ts +11 -0
- package/dist/pr-monitor/absent-required.js +61 -0
- package/dist/pr-monitor/constants.d.ts +7 -0
- package/dist/pr-monitor/constants.js +7 -0
- package/dist/pr-monitor/index.d.ts +2 -1
- package/dist/pr-monitor/index.js +2 -1
- package/dist/pr-monitor/main.js +4 -0
- package/dist/pr-monitor/monitor.js +24 -1
- package/dist/pr-wait-mergeable/classify.js +6 -0
- package/dist/preflight/evaluate.d.ts +4 -0
- package/dist/preflight/evaluate.js +35 -1
- package/dist/preflight/index.d.ts +4 -0
- package/dist/preflight/index.js +2 -0
- package/dist/preflight/intended-placement.d.ts +62 -0
- package/dist/preflight/intended-placement.js +275 -0
- package/dist/preflight/project-invariants-gate.d.ts +29 -0
- package/dist/preflight/project-invariants-gate.js +82 -0
- 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/acceptance-resolver.d.ts +128 -0
- package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
- package/dist/product-first-done-gate/acceptance.d.ts +7 -4
- package/dist/product-first-done-gate/acceptance.js +79 -8
- package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
- package/dist/product-first-done-gate/empty-resolution.js +12 -1
- package/dist/product-first-done-gate/evaluate.d.ts +16 -0
- package/dist/product-first-done-gate/evaluate.js +298 -34
- package/dist/product-first-done-gate/index.d.ts +2 -0
- package/dist/product-first-done-gate/index.js +1 -0
- package/dist/release/build-dist.d.ts +101 -0
- package/dist/release/build-dist.js +428 -46
- 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/emit.d.ts +16 -10
- package/dist/run-summary/emit.js +26 -15
- package/dist/run-summary/index.d.ts +3 -2
- package/dist/run-summary/index.js +2 -1
- package/dist/run-summary/session-id.d.ts +21 -0
- package/dist/run-summary/session-id.js +92 -0
- package/dist/run-summary/types.d.ts +33 -4
- package/dist/run-summary/types.js +1 -0
- package/dist/scope/acceptance-evidence.d.ts +19 -7
- package/dist/scope/acceptance-evidence.js +38 -11
- package/dist/scope/delivery-evidence.d.ts +3 -3
- package/dist/scope/delivery-evidence.js +22 -27
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.js +15 -5
- package/dist/scope/parent-lineage.js +7 -6
- package/dist/scope/project-invariant-coverage.d.ts +54 -0
- package/dist/scope/project-invariant-coverage.js +322 -0
- package/dist/scope/promote-from-issue.js +5 -5
- package/dist/scope/transition.js +1 -1
- package/dist/scope-provenance/compare-intent.d.ts +27 -0
- package/dist/scope-provenance/compare-intent.js +233 -0
- package/dist/scope-provenance/digest.d.ts +18 -1
- package/dist/scope-provenance/digest.js +16 -11
- package/dist/scope-provenance/evaluate.d.ts +5 -1
- package/dist/scope-provenance/evaluate.js +67 -7
- package/dist/scope-provenance/extract-intent.d.ts +43 -0
- package/dist/scope-provenance/extract-intent.js +262 -0
- package/dist/scope-provenance/index.d.ts +7 -1
- package/dist/scope-provenance/index.js +7 -1
- package/dist/scope-provenance/intent-digest.d.ts +12 -0
- package/dist/scope-provenance/intent-digest.js +33 -0
- package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
- package/dist/scope-provenance/intent-evaluate.js +149 -0
- package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
- package/dist/scope-provenance/json-tokenizer.js +225 -0
- package/dist/scope-provenance/known-machine.d.ts +38 -0
- package/dist/scope-provenance/known-machine.js +157 -0
- package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
- package/dist/scope-provenance/mint-artifacts.js +425 -0
- package/dist/session/ac-pass-banking.d.ts +9 -36
- package/dist/session/ac-pass-banking.js +33 -79
- package/dist/session/ac-pass-reuse.d.ts +53 -0
- package/dist/session/ac-pass-reuse.js +95 -0
- package/dist/session/git.js +19 -4
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.js +4 -0
- package/dist/session/occupancy-stress-worker.d.ts +7 -0
- package/dist/session/occupancy-stress-worker.js +36 -0
- package/dist/session/occupancy.d.ts +97 -0
- package/dist/session/occupancy.js +422 -0
- package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
- package/dist/session/process-cost.d.ts +32 -10
- package/dist/session/process-cost.js +189 -1
- package/dist/session/product-state-hash.d.ts +26 -0
- package/dist/session/product-state-hash.js +700 -0
- package/dist/session/ritual-entrypoint.js +16 -1
- package/dist/session/session-ready.d.ts +2 -0
- package/dist/session/session-ready.js +52 -30
- package/dist/session/session-start.d.ts +13 -0
- package/dist/session/session-start.js +103 -9
- package/dist/session/verify-ac-session-cache.d.ts +42 -0
- package/dist/session/verify-ac-session-cache.js +126 -0
- package/dist/session/verify-session-ritual.d.ts +28 -0
- package/dist/session/verify-session-ritual.js +97 -5
- package/dist/slash/openclaw-deposit.js +13 -9
- package/dist/slice/constants.d.ts +2 -2
- package/dist/slice/constants.js +2 -2
- package/dist/slice/existing.js +9 -3
- package/dist/slice/lock.d.ts +16 -1
- package/dist/slice/lock.js +137 -11
- package/dist/story-ready/evaluate.d.ts +2 -0
- package/dist/story-ready/evaluate.js +13 -0
- package/dist/swarm/complete-cohort.d.ts +3 -0
- package/dist/swarm/complete-cohort.js +59 -11
- 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 +47 -4
- package/dist/swarm/launch.d.ts +22 -0
- package/dist/swarm/launch.js +155 -0
- package/dist/swarm/routing-set-cli.d.ts +11 -1
- package/dist/swarm/routing-set-cli.js +15 -3
- package/dist/swarm/routing-verify.d.ts +13 -1
- package/dist/swarm/routing-verify.js +60 -21
- package/dist/swarm/routing.d.ts +14 -0
- package/dist/swarm/routing.js +53 -3
- package/dist/telemetry-coverage/fake-trial.js +10 -0
- package/dist/telemetry-coverage/kinds.js +2 -0
- package/dist/triage/actions/index.js +4 -1
- 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/ts-check-lane/index.d.ts +3 -0
- package/dist/ts-check-lane/index.js +2 -0
- package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
- package/dist/ts-check-lane/progress-reporter.js +38 -0
- package/dist/ts-check-lane/progress.d.ts +32 -0
- package/dist/ts-check-lane/progress.js +57 -0
- package/dist/ts-check-lane/run-lane.d.ts +3 -1
- package/dist/ts-check-lane/run-lane.js +8 -6
- package/dist/validate-content/validate-links.d.ts +8 -0
- package/dist/validate-content/validate-links.js +9 -15
- package/dist/value/feedback-file.js +12 -2
- package/dist/value/readback.d.ts +4 -1
- package/dist/value/readback.js +51 -12
- package/dist/vbrief-build/index.d.ts +1 -0
- package/dist/vbrief-build/index.js +1 -0
- package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
- package/dist/vbrief-build/project-invariants-io.js +41 -0
- package/dist/vbrief-reconcile/graph.js +18 -1
- package/dist/vbrief-reconcile/index.d.ts +1 -1
- package/dist/vbrief-reconcile/index.js +1 -1
- package/dist/vbrief-reconcile/types.d.ts +13 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
- package/dist/vbrief-reconcile/umbrellas.js +261 -26
- package/dist/vbrief-validate/index.d.ts +1 -1
- package/dist/vbrief-validate/index.js +1 -1
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +68 -0
- package/dist/vbrief-validate/schema.d.ts +4 -0
- package/dist/vbrief-validate/schema.js +1 -1
- package/dist/verify-ac/evaluate.d.ts +7 -1
- package/dist/verify-ac/evaluate.js +34 -26
- package/dist/verify-ac/flag.d.ts +4 -0
- package/dist/verify-ac/flag.js +16 -0
- package/dist/verify-ac/index.d.ts +2 -2
- package/dist/verify-ac/index.js +2 -2
- 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/dist/verify-source/contained-writes.js +2 -0
- package/dist/verify-source/verify-stubs.d.ts +15 -1
- package/dist/verify-source/verify-stubs.js +20 -16
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
- package/dist/xbrief-migrate/migrate-project.js +2 -5
- 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
|
|
@@ -262,10 +293,67 @@ const DOWNLOADER_DECODER_BINS = new Set([
|
|
|
262
293
|
"sqlite3",
|
|
263
294
|
"crane",
|
|
264
295
|
"objcopy",
|
|
296
|
+
// #3382 residual after #3354: dest forms that are not generic -o/--output/--outfile.
|
|
297
|
+
"cmake",
|
|
298
|
+
"script",
|
|
299
|
+
"gallery-dl",
|
|
300
|
+
"megadl",
|
|
301
|
+
"ncftpget",
|
|
302
|
+
"svn",
|
|
303
|
+
"fossil",
|
|
304
|
+
"bzr",
|
|
305
|
+
"cvs",
|
|
306
|
+
"darcs",
|
|
307
|
+
"ed",
|
|
308
|
+
"nvim",
|
|
309
|
+
"nano",
|
|
310
|
+
"vim",
|
|
311
|
+
"vi",
|
|
312
|
+
// #3421 residual after #3382: dest forms the harvest missed.
|
|
313
|
+
"git",
|
|
314
|
+
"ex",
|
|
315
|
+
"dos2unix",
|
|
316
|
+
"unix2dos",
|
|
317
|
+
"mac2unix",
|
|
318
|
+
"unix2mac",
|
|
319
|
+
"aws",
|
|
320
|
+
"pijul",
|
|
321
|
+
"pg_dump",
|
|
322
|
+
"pg_restore",
|
|
323
|
+
"pg_dumpall",
|
|
324
|
+
"convert",
|
|
325
|
+
"magick",
|
|
326
|
+
"mogrify",
|
|
327
|
+
"fallocate",
|
|
328
|
+
"new-item",
|
|
329
|
+
// #3459 residual after #3421: GNU g* peers, forge clone, iwr, fsutil.
|
|
330
|
+
"iwr",
|
|
331
|
+
"invoke-webrequest",
|
|
332
|
+
"fsutil",
|
|
333
|
+
"gh",
|
|
334
|
+
"glab",
|
|
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,
|
|
265
353
|
]);
|
|
266
354
|
/**
|
|
267
355
|
* Archive extractors / alt writers that can plant via pathish operands without
|
|
268
|
-
* shell redirects (#3245 / #3288 / #3311 / #3336 / #3354). Used for pathish authz/kill scans (prefer deny)
|
|
356
|
+
* shell redirects (#3245 / #3288 / #3311 / #3336 / #3354 / #3382). Used for pathish authz/kill scans (prefer deny)
|
|
269
357
|
* and write-shape residual under UAT — not bare curl-class URL fetches.
|
|
270
358
|
*/
|
|
271
359
|
const ARCHIVE_ALT_WRITE_BINS = new Set([
|
|
@@ -338,9 +426,45 @@ const ARCHIVE_ALT_WRITE_BINS = new Set([
|
|
|
338
426
|
"sqlite3",
|
|
339
427
|
"crane",
|
|
340
428
|
"objcopy",
|
|
429
|
+
// #3382 residual: dedicated dest-form writers (not general-purpose cmake/git/editors).
|
|
430
|
+
"gallery-dl",
|
|
431
|
+
"megadl",
|
|
432
|
+
"ncftpget",
|
|
433
|
+
// #3421 residual: dedicated dest-form writers (not general-purpose git/aws/editors).
|
|
434
|
+
"dos2unix",
|
|
435
|
+
"unix2dos",
|
|
436
|
+
"mac2unix",
|
|
437
|
+
"unix2mac",
|
|
438
|
+
"pijul",
|
|
439
|
+
"pg_dump",
|
|
440
|
+
"pg_restore",
|
|
441
|
+
"pg_dumpall",
|
|
442
|
+
"convert",
|
|
443
|
+
"magick",
|
|
444
|
+
"mogrify",
|
|
445
|
+
"fallocate",
|
|
446
|
+
// #3459 residual: dest writers that are not general-purpose gh/cmd.
|
|
447
|
+
"iwr",
|
|
448
|
+
"invoke-webrequest",
|
|
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,
|
|
341
465
|
]);
|
|
342
466
|
/**
|
|
343
|
-
* Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354).
|
|
467
|
+
* Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354 / #3382).
|
|
344
468
|
* Prefer a Set over a long `||` chain so coverage counts one membership check, not N branches.
|
|
345
469
|
*/
|
|
346
470
|
const PROTECTED_POSITIONAL_BINS = new Set([
|
|
@@ -403,7 +527,149 @@ const PROTECTED_POSITIONAL_BINS = new Set([
|
|
|
403
527
|
"sqlite3",
|
|
404
528
|
"crane",
|
|
405
529
|
"objcopy",
|
|
530
|
+
// #3382 residual: positional dest writers (cmake copy / script / VCS / editors).
|
|
531
|
+
"cmake",
|
|
532
|
+
"script",
|
|
533
|
+
"gallery-dl",
|
|
534
|
+
"megadl",
|
|
535
|
+
"ncftpget",
|
|
536
|
+
"svn",
|
|
537
|
+
"fossil",
|
|
538
|
+
"bzr",
|
|
539
|
+
"cvs",
|
|
540
|
+
"darcs",
|
|
541
|
+
"ed",
|
|
542
|
+
"nvim",
|
|
543
|
+
"nano",
|
|
544
|
+
"vim",
|
|
545
|
+
"vi",
|
|
546
|
+
// #3421 residual: positional dest writers (aws, convert, New-Item).
|
|
547
|
+
// git clone/worktree/submodule dests are harvested only on those write subcommands.
|
|
548
|
+
"ex",
|
|
549
|
+
"dos2unix",
|
|
550
|
+
"unix2dos",
|
|
551
|
+
"mac2unix",
|
|
552
|
+
"unix2mac",
|
|
553
|
+
"aws",
|
|
554
|
+
"pijul",
|
|
555
|
+
"pg_dump",
|
|
556
|
+
"pg_restore",
|
|
557
|
+
"pg_dumpall",
|
|
558
|
+
"convert",
|
|
559
|
+
"magick",
|
|
560
|
+
"mogrify",
|
|
561
|
+
"fallocate",
|
|
562
|
+
"new-item",
|
|
563
|
+
// #3459 residual: positional dest writers (fsutil createnew, iwr dest flags).
|
|
564
|
+
// gh/glab/hub clone dests are harvested only on the clone subcommand.
|
|
565
|
+
"iwr",
|
|
566
|
+
"invoke-webrequest",
|
|
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,
|
|
585
|
+
]);
|
|
586
|
+
/**
|
|
587
|
+
* Bins whose dest is the last pathish operand (src … dest). A protected
|
|
588
|
+
* source that writes elsewhere is not a dest plant (#3421 residual).
|
|
589
|
+
*/
|
|
590
|
+
const LAST_POSITIONAL_DEST_BINS = new Set(["aws", "convert", "magick"]);
|
|
591
|
+
/** Wrappers / assignments before the dest-owning bin (`env aws`, `FOO=1 convert`). */
|
|
592
|
+
const COMMAND_WRAPPER_BINS = new Set([
|
|
593
|
+
"env",
|
|
594
|
+
"command",
|
|
595
|
+
"nice",
|
|
596
|
+
"nohup",
|
|
597
|
+
"sudo",
|
|
598
|
+
"doas",
|
|
599
|
+
"time",
|
|
600
|
+
"stdbuf",
|
|
601
|
+
"timeout",
|
|
602
|
+
"ionice",
|
|
603
|
+
]);
|
|
604
|
+
function isWrapperOrAssignmentToken(raw, n) {
|
|
605
|
+
if (COMMAND_WRAPPER_BINS.has(writeBinName(raw)))
|
|
606
|
+
return true;
|
|
607
|
+
return n.includes("=") && !n.startsWith("-") && !raw.includes("/") && !raw.includes("\\");
|
|
608
|
+
}
|
|
609
|
+
/** Wrapper flags that take a separate value (`env -C DIR`, `nice -n N`). */
|
|
610
|
+
const WRAPPER_VALUE_FLAGS = new Set([
|
|
611
|
+
"-c",
|
|
612
|
+
"--chdir",
|
|
613
|
+
"-u",
|
|
614
|
+
"--unset",
|
|
615
|
+
"-n",
|
|
616
|
+
"--adjustment",
|
|
617
|
+
"-o",
|
|
618
|
+
"--output",
|
|
619
|
+
"-i",
|
|
620
|
+
"--input",
|
|
621
|
+
"-e",
|
|
622
|
+
"--error",
|
|
623
|
+
"-s",
|
|
624
|
+
"--signal",
|
|
625
|
+
"-k",
|
|
626
|
+
"--kill-after",
|
|
406
627
|
]);
|
|
628
|
+
/** In-place ImageMagick writer: every protected operand is a dest, not last-only. */
|
|
629
|
+
const MAGICK_FAMILY_BINS = new Set(["convert", "magick", "mogrify"]);
|
|
630
|
+
const MAGICK_WRITE_DEST_FLAGS = new Set(["-write"]);
|
|
631
|
+
/** True when this token is a subcommand of a last-positional dest bin (`aws s3 cp`). */
|
|
632
|
+
function tokenEndsShellSegment(token) {
|
|
633
|
+
return isShellSegmentBreak(token) || firstUnquotedShellOpIndex(token) >= 0;
|
|
634
|
+
}
|
|
635
|
+
function segmentStartedByLastPositionalDest(tokens, tokenIndex) {
|
|
636
|
+
let start = tokenIndex;
|
|
637
|
+
while (start > 0 && !tokenEndsShellSegment(tokens[start - 1]))
|
|
638
|
+
start--;
|
|
639
|
+
let skipNext = false;
|
|
640
|
+
let afterWrapper = false;
|
|
641
|
+
for (let k = start; k < tokenIndex; k++) {
|
|
642
|
+
const raw = tokens[k];
|
|
643
|
+
if (isShellSegmentBreak(raw))
|
|
644
|
+
continue;
|
|
645
|
+
const n = normalizeToken(raw);
|
|
646
|
+
const bare = binBareName(raw);
|
|
647
|
+
if (skipNext) {
|
|
648
|
+
skipNext = false;
|
|
649
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare))
|
|
650
|
+
return true;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (n.startsWith("-")) {
|
|
654
|
+
if (!n.includes("=") && WRAPPER_VALUE_FLAGS.has(n))
|
|
655
|
+
skipNext = true;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (isWrapperOrAssignmentToken(raw, n)) {
|
|
659
|
+
afterWrapper = COMMAND_WRAPPER_BINS.has(bare);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare))
|
|
663
|
+
return true;
|
|
664
|
+
// timeout 5 aws / similar: skip one non-bin operand after a wrapper.
|
|
665
|
+
if (afterWrapper) {
|
|
666
|
+
afterWrapper = false;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
407
673
|
/** wget family (directory-prefix dest flags). */
|
|
408
674
|
const WGET_FAMILY_BINS = new Set(["wget", "wget2"]);
|
|
409
675
|
/** aria2 family (dir dest flags). */
|
|
@@ -419,6 +685,82 @@ const SQLITE3_OUTPUT_META = [".output", ".once"];
|
|
|
419
685
|
const TAR_FAMILY_BINS = new Set(["tar", "bsdtar", "gtar", "star", "gnutar"]);
|
|
420
686
|
/** xh / httpie family (download-dir dest flags) (#3311). */
|
|
421
687
|
const XH_FAMILY_BINS = new Set(["xh", "httpie"]);
|
|
688
|
+
/** gallery-dl dest-dir flags (#3382). */
|
|
689
|
+
const GALLERY_DL_FAMILY_BINS = new Set(["gallery-dl", "gallery_dl"]);
|
|
690
|
+
const GALLERY_DL_DIR_DEST_FLAGS = new Set(["-d", "--destination", "--dest"]);
|
|
691
|
+
/** megadl dest-path flags (#3382). */
|
|
692
|
+
const MEGADL_FAMILY_BINS = new Set(["megadl"]);
|
|
693
|
+
const MEGADL_PATH_DEST_FLAGS = new Set(["--path"]);
|
|
694
|
+
/**
|
|
695
|
+
* Extra dest flags harvested on unknown write-shaped bins when dest is protected (#3382 / #3421).
|
|
696
|
+
* Not merged into DOWNLOADER_FILE_DEST_FLAGS: curl `-d` is POST data, not a file dest.
|
|
697
|
+
*/
|
|
698
|
+
const GENERIC_PROTECTED_EXTRA_DEST_FLAGS = new Set([
|
|
699
|
+
"-d",
|
|
700
|
+
"--dir",
|
|
701
|
+
"--destination",
|
|
702
|
+
"--dest",
|
|
703
|
+
"--path",
|
|
704
|
+
"--directory",
|
|
705
|
+
"--workdir",
|
|
706
|
+
"--file",
|
|
707
|
+
"--separate-git-dir",
|
|
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",
|
|
722
|
+
]);
|
|
723
|
+
/** Bins whose `--file` / `-f` operand is an input, not a dest plant. */
|
|
724
|
+
const READ_SHAPED_FILE_FLAG_BINS = new Set([
|
|
725
|
+
"grep",
|
|
726
|
+
"egrep",
|
|
727
|
+
"fgrep",
|
|
728
|
+
"rg",
|
|
729
|
+
"ag",
|
|
730
|
+
"ack",
|
|
731
|
+
"get-content",
|
|
732
|
+
"gc",
|
|
733
|
+
"cat",
|
|
734
|
+
"type",
|
|
735
|
+
"less",
|
|
736
|
+
"more",
|
|
737
|
+
"head",
|
|
738
|
+
"tail",
|
|
739
|
+
]);
|
|
740
|
+
const READ_INPUT_FILE_FLAGS = new Set(["--file", "-f"]);
|
|
741
|
+
/** PowerShell New-Item dest flags (not generic: Get-Content -Path is a read). */
|
|
742
|
+
const NEW_ITEM_PATH_DEST_FLAGS = new Set(["-path", "-literalpath", "--literalpath"]);
|
|
743
|
+
/** git subcommands whose positionals can plant a dest (#3421). */
|
|
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
|
+
]);
|
|
757
|
+
/** Forge CLIs whose `clone` dest is harvested like git clone (#3459). */
|
|
758
|
+
const FORGE_CLONE_BINS = new Set(["gh", "glab", "hub"]);
|
|
759
|
+
/** fossil dest-dir flags (#3382 PATH form + #3421 --workdir=). */
|
|
760
|
+
const FOSSIL_WORKDIR_DEST_FLAGS = new Set(["--workdir"]);
|
|
761
|
+
/** pg_dump family dest-file flags (#3421). */
|
|
762
|
+
const PG_DUMP_FAMILY_BINS = new Set(["pg_dump", "pg_restore", "pg_dumpall"]);
|
|
763
|
+
const PG_DUMP_FILE_DEST_FLAGS = new Set(["-f", "--file"]);
|
|
422
764
|
/**
|
|
423
765
|
* File destination flags for downloaders/decoders (#3206).
|
|
424
766
|
* normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
|
|
@@ -433,6 +775,8 @@ const DOWNLOADER_FILE_DEST_FLAGS = new Set([
|
|
|
433
775
|
"-out",
|
|
434
776
|
"--out",
|
|
435
777
|
"--outfile",
|
|
778
|
+
// PowerShell iwr/Invoke-WebRequest -OutFile (single-dash long form) (#3459).
|
|
779
|
+
"-outfile",
|
|
436
780
|
// unar single-dash long form; GNU/others use --output-directory.
|
|
437
781
|
"-output-directory",
|
|
438
782
|
"--output-directory",
|
|
@@ -479,12 +823,46 @@ function binBareName(token) {
|
|
|
479
823
|
const base = pathish.includes("/") ? pathish.slice(pathish.lastIndexOf("/") + 1) : pathish;
|
|
480
824
|
return base.endsWith(".exe") ? base.slice(0, -4) : base;
|
|
481
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* GNU coreutils g* prefixes (Homebrew `ginstall`, `gcp`).
|
|
828
|
+
* Stems only — `git` / `gh` / `gpg` do not map to `it` / `h` / `pg` (#3459).
|
|
829
|
+
*/
|
|
830
|
+
const GNU_COREUTILS_WRITE_STEMS = new Set([
|
|
831
|
+
"cp",
|
|
832
|
+
"mv",
|
|
833
|
+
"rm",
|
|
834
|
+
"rmdir",
|
|
835
|
+
"unlink",
|
|
836
|
+
"shred",
|
|
837
|
+
"truncate",
|
|
838
|
+
"chmod",
|
|
839
|
+
"chown",
|
|
840
|
+
"dd",
|
|
841
|
+
"tee",
|
|
842
|
+
"install",
|
|
843
|
+
"ln",
|
|
844
|
+
"link",
|
|
845
|
+
"mkdir",
|
|
846
|
+
"touch",
|
|
847
|
+
]);
|
|
848
|
+
function writeBinName(token) {
|
|
849
|
+
const bare = binBareName(token);
|
|
850
|
+
if (bare.length > 1 && bare.startsWith("g")) {
|
|
851
|
+
const stem = bare.slice(1);
|
|
852
|
+
if (GNU_COREUTILS_WRITE_STEMS.has(stem))
|
|
853
|
+
return stem;
|
|
854
|
+
}
|
|
855
|
+
return bare;
|
|
856
|
+
}
|
|
482
857
|
/** True when token names a downloader/decoder bin (bare or path-qualified). */
|
|
483
858
|
function isDownloaderDecoderBin(token) {
|
|
484
859
|
const n = normalizeToken(token);
|
|
485
860
|
if (n.startsWith("-"))
|
|
486
861
|
return false;
|
|
487
|
-
return DOWNLOADER_DECODER_BINS.has(
|
|
862
|
+
return DOWNLOADER_DECODER_BINS.has(writeBinName(token));
|
|
863
|
+
}
|
|
864
|
+
function isScpFamilyBin(bin) {
|
|
865
|
+
return bin === "scp" || bin === "pscp";
|
|
488
866
|
}
|
|
489
867
|
/**
|
|
490
868
|
* True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
|
|
@@ -492,8 +870,8 @@ function isDownloaderDecoderBin(token) {
|
|
|
492
870
|
* 7z uses attached `-oDIR` only — separate `-o PATH` is not a 7z dest form (handled elsewhere).
|
|
493
871
|
*/
|
|
494
872
|
function isDownloaderDestFlag(flag, bin, rawFlag) {
|
|
495
|
-
// scp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
|
|
496
|
-
if (bin
|
|
873
|
+
// scp/pscp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
|
|
874
|
+
if (isScpFamilyBin(bin))
|
|
497
875
|
return false;
|
|
498
876
|
// 7z family: only attached `-oDIR` (parsed in attached-short branch), not separate `-o PATH`.
|
|
499
877
|
if (SEVEN_Z_FAMILY_BINS.has(bin)) {
|
|
@@ -532,6 +910,18 @@ function isDownloaderDestFlag(flag, bin, rawFlag) {
|
|
|
532
910
|
return true;
|
|
533
911
|
if (ATOOL_FAMILY_BINS.has(bin) && ATOOL_DIR_DEST_FLAGS.has(flag))
|
|
534
912
|
return true;
|
|
913
|
+
if (GALLERY_DL_FAMILY_BINS.has(bin) && GALLERY_DL_DIR_DEST_FLAGS.has(flag))
|
|
914
|
+
return true;
|
|
915
|
+
if (MEGADL_FAMILY_BINS.has(bin) && MEGADL_PATH_DEST_FLAGS.has(flag))
|
|
916
|
+
return true;
|
|
917
|
+
if (bin === "fossil" && FOSSIL_WORKDIR_DEST_FLAGS.has(flag))
|
|
918
|
+
return true;
|
|
919
|
+
if (PG_DUMP_FAMILY_BINS.has(bin) && PG_DUMP_FILE_DEST_FLAGS.has(flag))
|
|
920
|
+
return true;
|
|
921
|
+
if (bin === "new-item" && NEW_ITEM_PATH_DEST_FLAGS.has(flag))
|
|
922
|
+
return true;
|
|
923
|
+
if (MAGICK_FAMILY_BINS.has(bin) && MAGICK_WRITE_DEST_FLAGS.has(flag))
|
|
924
|
+
return true;
|
|
535
925
|
return false;
|
|
536
926
|
}
|
|
537
927
|
/**
|
|
@@ -582,6 +972,28 @@ function firstUnquotedShellOpIndex(raw) {
|
|
|
582
972
|
}
|
|
583
973
|
return -1;
|
|
584
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
|
+
}
|
|
585
997
|
/**
|
|
586
998
|
* Destinations from curl/wget/xxd/openssl/scp/aria2c/certutil via -o/--output/-O/-out/
|
|
587
999
|
* --output-dir/-P/-d/--dir (separate, =value, or attached short form), xxd -r path-like
|
|
@@ -599,7 +1011,7 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
599
1011
|
i++;
|
|
600
1012
|
continue;
|
|
601
1013
|
}
|
|
602
|
-
const bin =
|
|
1014
|
+
const bin = writeBinName(tokens[i]);
|
|
603
1015
|
i++;
|
|
604
1016
|
// xxd reverse mode writes; without -r, path positionals are dump inputs (read).
|
|
605
1017
|
let xxdReverse = false;
|
|
@@ -610,7 +1022,11 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
610
1022
|
// Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
|
|
611
1023
|
let lastPositionalPath = null;
|
|
612
1024
|
const protectedPathish = [];
|
|
613
|
-
const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin)
|
|
1025
|
+
const collectsProtectedPositionals = (PROTECTED_POSITIONAL_BINS.has(bin) && residualPositionalBinIsWriteShaped(bin, tokens, i)) ||
|
|
1026
|
+
(bin === "git" && gitHasWriteSubcommand(tokens, i)) ||
|
|
1027
|
+
(bin === "git" && gitHasBundleCreate(tokens, i)) ||
|
|
1028
|
+
(bin === "jj" && jjHasWriteSubcommand(tokens, i)) ||
|
|
1029
|
+
(FORGE_CLONE_BINS.has(bin) && forgeHasCloneSubcommand(tokens, i));
|
|
614
1030
|
while (i < tokens.length) {
|
|
615
1031
|
const raw = tokens[i];
|
|
616
1032
|
const n = normalizeToken(raw);
|
|
@@ -642,8 +1058,8 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
642
1058
|
i++;
|
|
643
1059
|
continue;
|
|
644
1060
|
}
|
|
645
|
-
// scp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
|
|
646
|
-
if (bin
|
|
1061
|
+
// scp/pscp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
|
|
1062
|
+
if (isScpFamilyBin(bin) &&
|
|
647
1063
|
(SCP_VALUE_FLAGS_LOWER.has(n) ||
|
|
648
1064
|
SCP_VALUE_FLAGS_EXACT.has(raw) ||
|
|
649
1065
|
SCP_VALUE_FLAGS_EXACT.has(n))) {
|
|
@@ -669,10 +1085,11 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
669
1085
|
continue;
|
|
670
1086
|
}
|
|
671
1087
|
// Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
|
|
672
|
-
// 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).
|
|
673
1089
|
// 7z requires attached -oDIR (#3245).
|
|
674
|
-
if (bin
|
|
675
|
-
|
|
1090
|
+
if (!isScpFamilyBin(bin) && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
|
|
1091
|
+
// `-outfile` is PowerShell -OutFile (next token is dest), not attached -outFILE (#3459).
|
|
1092
|
+
if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output") && n !== "-outfile") {
|
|
676
1093
|
dests.push(pathishToken(raw.slice(4)));
|
|
677
1094
|
i++;
|
|
678
1095
|
continue;
|
|
@@ -698,6 +1115,12 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
698
1115
|
i++;
|
|
699
1116
|
continue;
|
|
700
1117
|
}
|
|
1118
|
+
// gallery-dl attached -dDIR (#3382)
|
|
1119
|
+
if (GALLERY_DL_FAMILY_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
|
|
1120
|
+
dests.push(pathishToken(raw.slice(2)));
|
|
1121
|
+
i++;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
701
1124
|
// unzip / cabextract attached -dDIR (#3245 / #3336)
|
|
702
1125
|
if (UNZIP_DIR_DEST_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
|
|
703
1126
|
dests.push(pathishToken(raw.slice(2)));
|
|
@@ -751,9 +1174,7 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
751
1174
|
if (p.startsWith(prefix) && p.length > prefix.length) {
|
|
752
1175
|
const dest = p.slice(prefix.length);
|
|
753
1176
|
dests.push(dest);
|
|
754
|
-
if (dest
|
|
755
|
-
dest.includes(".deft-directive-disable") ||
|
|
756
|
-
dest.includes(".no-deft-directive")) {
|
|
1177
|
+
if (pathishIsProtectedDest(dest)) {
|
|
757
1178
|
protectedPathish.push(dest);
|
|
758
1179
|
}
|
|
759
1180
|
break;
|
|
@@ -765,7 +1186,7 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
765
1186
|
const meta = sqlite3MetaDest(raw, tokens[i + 1]);
|
|
766
1187
|
if (meta !== null) {
|
|
767
1188
|
dests.push(meta.dest);
|
|
768
|
-
if (
|
|
1189
|
+
if (pathishIsProtectedDest(meta.dest)) {
|
|
769
1190
|
protectedPathish.push(meta.dest);
|
|
770
1191
|
}
|
|
771
1192
|
i += meta.consumedNext ? 2 : 1;
|
|
@@ -773,18 +1194,16 @@ function downloaderDecoderDestinations(tokens) {
|
|
|
773
1194
|
}
|
|
774
1195
|
}
|
|
775
1196
|
// scp / certutil / rclone / archive extractors: pathish operands (quote-aware glued-op cut).
|
|
776
|
-
// Under UAT: any `.deft/authz` / kill-switch pathish is
|
|
777
|
-
//
|
|
1197
|
+
// Under UAT: any `.deft/authz` / approved-scope / kill-switch pathish is
|
|
1198
|
+
// fail-closed settings (prefer deny over dest-parser perfection; #3213 / #3421).
|
|
1199
|
+
// Last-positional dest bins (aws/convert/magick): only the last operand is dest.
|
|
778
1200
|
if (collectsProtectedPositionals && !n.startsWith("-")) {
|
|
779
1201
|
const cut = firstUnquotedShellOpIndex(raw);
|
|
780
1202
|
const cleaned = cut >= 0 ? raw.slice(0, cut) : raw;
|
|
781
1203
|
const p = pathishToken(cleaned);
|
|
782
1204
|
if (p.length > 0) {
|
|
783
1205
|
lastPositionalPath = p;
|
|
784
|
-
|
|
785
|
-
if (p.includes(".deft/authz") ||
|
|
786
|
-
p.includes(".deft-directive-disable") ||
|
|
787
|
-
p.includes(".no-deft-directive")) {
|
|
1206
|
+
if (pathishIsProtectedDest(p) && !LAST_POSITIONAL_DEST_BINS.has(bin)) {
|
|
788
1207
|
protectedPathish.push(p);
|
|
789
1208
|
}
|
|
790
1209
|
}
|
|
@@ -886,13 +1305,316 @@ function hasPolicyAuthorityMutator(tokens) {
|
|
|
886
1305
|
}
|
|
887
1306
|
/** True when pathish token names a kill-switch basename (quote-strip resistant). */
|
|
888
1307
|
function pathishIsProtectedDest(pathish) {
|
|
889
|
-
return pathishIsAuthzDir(pathish) ||
|
|
1308
|
+
return (pathishIsAuthzDir(pathish) ||
|
|
1309
|
+
pathishIsApprovedScopeDir(pathish) ||
|
|
1310
|
+
pathishMentionsKillSwitch(pathish));
|
|
1311
|
+
}
|
|
1312
|
+
function isGenericProtectedDestFlag(flag) {
|
|
1313
|
+
return DOWNLOADER_FILE_DEST_FLAGS.has(flag) || GENERIC_PROTECTED_EXTRA_DEST_FLAGS.has(flag);
|
|
890
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
|
+
]);
|
|
891
1394
|
/**
|
|
892
|
-
*
|
|
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
|
+
}
|
|
1488
|
+
function isReadShapedInputFileFlag(bin, flag) {
|
|
1489
|
+
return READ_SHAPED_FILE_FLAG_BINS.has(bin) && READ_INPUT_FILE_FLAGS.has(flag);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Git globals that do not take a separate value. Unlisted globals consume the
|
|
1493
|
+
* next token so a later dest writer is still seen (#3421 residual).
|
|
1494
|
+
*/
|
|
1495
|
+
const GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
1496
|
+
"--no-pager",
|
|
1497
|
+
"-p",
|
|
1498
|
+
"--paginate",
|
|
1499
|
+
"-P",
|
|
1500
|
+
"--bare",
|
|
1501
|
+
"--help",
|
|
1502
|
+
"-h",
|
|
1503
|
+
"--version",
|
|
1504
|
+
"--literal-pathspecs",
|
|
1505
|
+
"--glob-pathspecs",
|
|
1506
|
+
"--noglob-pathspecs",
|
|
1507
|
+
"--icase-pathspecs",
|
|
1508
|
+
"--no-replace-objects",
|
|
1509
|
+
"--no-optional-locks",
|
|
1510
|
+
"--no-lazy-fetch",
|
|
1511
|
+
"--no-advice",
|
|
1512
|
+
"--quiet",
|
|
1513
|
+
"-q",
|
|
1514
|
+
]);
|
|
1515
|
+
/**
|
|
1516
|
+
* True only when the VCS *subcommand* (first non-flag token) is a dest writer.
|
|
1517
|
+
* Later operands named clone/worktree/submodule (e.g. `git log worktree -- …`)
|
|
1518
|
+
* are not write subcommands (#3423 residual).
|
|
1519
|
+
* Boolean globals (`--no-pager`) do not consume the next token. Other globals
|
|
1520
|
+
* consume it even when that token spells a subcommand (#3421 residual).
|
|
1521
|
+
*/
|
|
1522
|
+
function hasWriteSubcommand(tokens, start, writeSubs, booleanFlags) {
|
|
1523
|
+
let i = start;
|
|
1524
|
+
while (i < tokens.length) {
|
|
1525
|
+
const raw = tokens[i];
|
|
1526
|
+
if (isShellSegmentBreak(raw))
|
|
1527
|
+
return false;
|
|
1528
|
+
const n = normalizeToken(raw);
|
|
1529
|
+
if (n.startsWith("-")) {
|
|
1530
|
+
if (!n.includes("=") && !booleanFlags.has(n) && !n.startsWith("--no-")) {
|
|
1531
|
+
const next = tokens[i + 1];
|
|
1532
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1533
|
+
i += 2;
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
i++;
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
return writeSubs.has(n);
|
|
1541
|
+
}
|
|
1542
|
+
return false;
|
|
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
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* True when this forge-CLI segment is a clone dest writer
|
|
1582
|
+
* (`gh repo clone`, `glab repo clone`, `hub clone`) (#3459).
|
|
1583
|
+
* `clone` as a later flag value (`gh pr create --title clone`) is not a dest writer.
|
|
1584
|
+
*/
|
|
1585
|
+
function forgeHasCloneSubcommand(tokens, start) {
|
|
1586
|
+
const nonFlags = [];
|
|
1587
|
+
let i = start;
|
|
1588
|
+
while (i < tokens.length) {
|
|
1589
|
+
const raw = tokens[i];
|
|
1590
|
+
if (isShellSegmentBreak(raw))
|
|
1591
|
+
break;
|
|
1592
|
+
const n = normalizeToken(raw);
|
|
1593
|
+
if (n.startsWith("-")) {
|
|
1594
|
+
if (!n.includes("=") && (GH_VALUE_FLAGS.has(raw) || GH_VALUE_FLAGS.has(n))) {
|
|
1595
|
+
const next = tokens[i + 1];
|
|
1596
|
+
if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
|
|
1597
|
+
i += 2;
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
i++;
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
nonFlags.push(n);
|
|
1605
|
+
i++;
|
|
1606
|
+
}
|
|
1607
|
+
if (nonFlags[0] === "clone")
|
|
1608
|
+
return true;
|
|
1609
|
+
return nonFlags[0] === "repo" && nonFlags[1] === "clone";
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Fail-closed dest harvest for write-shaped Shell under UAT (#3354 / #3382 / #3421).
|
|
893
1613
|
* Named-bin parsers above are not the only path: any token that looks like
|
|
894
|
-
* `-o` / `--output` / `--outfile` / `--output-file`
|
|
895
|
-
*
|
|
1614
|
+
* `-o` / `--output` / `--outfile` / `--output-file` / `-d` / `--dir` /
|
|
1615
|
+
* `--destination` / `--path` / `--directory` / `--workdir` / `--file` / `-f`
|
|
1616
|
+
* (or attached `-oDIR` / `-dDIR`) whose dest is authz/kill-switch/approved-scope
|
|
1617
|
+
* is collected even when the bin is unknown.
|
|
896
1618
|
* scp `-o` (OpenSSH option) and cpio `-o` (copy-out) stay excluded.
|
|
897
1619
|
*/
|
|
898
1620
|
function genericProtectedDests(tokens) {
|
|
@@ -907,29 +1629,54 @@ function genericProtectedDests(tokens) {
|
|
|
907
1629
|
currentBin = "";
|
|
908
1630
|
continue;
|
|
909
1631
|
}
|
|
910
|
-
if (!n.startsWith("-")) {
|
|
1632
|
+
if (!n.startsWith("-") && !isEnvAssign(raw)) {
|
|
911
1633
|
if (isDownloaderDecoderBin(raw)) {
|
|
912
|
-
currentBin =
|
|
1634
|
+
currentBin = writeBinName(raw);
|
|
913
1635
|
}
|
|
914
1636
|
else if (currentBin.length === 0 &&
|
|
915
1637
|
!raw.includes("/") &&
|
|
916
1638
|
!raw.includes("\\") &&
|
|
917
1639
|
n.length > 0) {
|
|
918
|
-
currentBin =
|
|
1640
|
+
currentBin = writeBinName(raw);
|
|
919
1641
|
}
|
|
920
1642
|
}
|
|
921
|
-
if (currentBin
|
|
1643
|
+
if (isScpFamilyBin(currentBin) || currentBin === "cpio")
|
|
922
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
|
+
}
|
|
923
1659
|
if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
|
|
924
1660
|
const eq = raw.indexOf("=");
|
|
925
1661
|
const flag = normalizeToken(raw.slice(0, eq));
|
|
926
|
-
if (
|
|
1662
|
+
if (isGenericProtectedDestFlag(flag) && !isReadShapedInputFileFlag(currentBin, flag)) {
|
|
927
1663
|
const dest = pathishToken(raw.slice(eq + 1));
|
|
928
1664
|
if (pathishIsProtectedDest(dest))
|
|
929
1665
|
dests.push(dest);
|
|
930
1666
|
}
|
|
931
1667
|
continue;
|
|
932
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
|
+
}
|
|
933
1680
|
if (n.startsWith("-") &&
|
|
934
1681
|
!n.startsWith("--") &&
|
|
935
1682
|
n.startsWith("-o") &&
|
|
@@ -940,13 +1687,10 @@ function genericProtectedDests(tokens) {
|
|
|
940
1687
|
dests.push(dest);
|
|
941
1688
|
continue;
|
|
942
1689
|
}
|
|
943
|
-
if (
|
|
944
|
-
const
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
if (pathishIsProtectedDest(dest))
|
|
948
|
-
dests.push(dest);
|
|
949
|
-
}
|
|
1690
|
+
if (n.startsWith("-") && !n.startsWith("--") && n.startsWith("-d") && n.length > 2) {
|
|
1691
|
+
const dest = pathishToken(raw.slice(2));
|
|
1692
|
+
if (pathishIsProtectedDest(dest))
|
|
1693
|
+
dests.push(dest);
|
|
950
1694
|
}
|
|
951
1695
|
}
|
|
952
1696
|
return dests;
|
|
@@ -969,8 +1713,9 @@ function sqlite3MetaDest(raw, next) {
|
|
|
969
1713
|
return null;
|
|
970
1714
|
}
|
|
971
1715
|
function pathishMentionsKillSwitch(pathish) {
|
|
1716
|
+
const p = canonicalizePathish(pathish);
|
|
972
1717
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
973
|
-
if (
|
|
1718
|
+
if (p === name || p.endsWith(`/${name}`) || p.includes(name)) {
|
|
974
1719
|
return true;
|
|
975
1720
|
}
|
|
976
1721
|
}
|
|
@@ -1045,9 +1790,13 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
1045
1790
|
]);
|
|
1046
1791
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1047
1792
|
const binTok = normalizeToken(tokens[ti]);
|
|
1048
|
-
const bare =
|
|
1793
|
+
const bare = writeBinName(tokens[ti]);
|
|
1049
1794
|
if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
|
|
1050
1795
|
continue;
|
|
1796
|
+
// Last-positional dest bins (and their subcommands): dest harvest owns dest vs source.
|
|
1797
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare) || segmentStartedByLastPositionalDest(tokens, ti)) {
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1051
1800
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
1052
1801
|
const p = pathishToken(tokens[tj]);
|
|
1053
1802
|
if (pathishMentionsKillSwitch(p))
|
|
@@ -1061,8 +1810,14 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
1061
1810
|
}
|
|
1062
1811
|
}
|
|
1063
1812
|
// Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` — path later.
|
|
1064
|
-
|
|
1065
|
-
|
|
1813
|
+
// Skip tokens in a last-positional dest segment (dest harvest owns dest vs source).
|
|
1814
|
+
// Other segments still scan so `aws … ; touch .deft-directive-disable` is settings.
|
|
1815
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1816
|
+
if (LAST_POSITIONAL_DEST_BINS.has(writeBinName(tokens[ti])) ||
|
|
1817
|
+
segmentStartedByLastPositionalDest(tokens, ti)) {
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
const p = pathishToken(tokens[ti]);
|
|
1066
1821
|
for (const name of KILL_SWITCH_BASENAMES) {
|
|
1067
1822
|
// Exact basename or ends with /basename
|
|
1068
1823
|
if (p === name || p.endsWith(`/${name}`)) {
|
|
@@ -1070,8 +1825,8 @@ function hasKillSwitchShellWrite(command, tokens) {
|
|
|
1070
1825
|
if (hasWriteShape(command, tokens) ||
|
|
1071
1826
|
lower.includes("touch") ||
|
|
1072
1827
|
lower.includes("new-item") ||
|
|
1073
|
-
SYMLINK_PLANT_BINS.has(
|
|
1074
|
-
tokens.some((tok) => SYMLINK_PLANT_BINS.has(
|
|
1828
|
+
SYMLINK_PLANT_BINS.has(writeBinName(tokens[0])) ||
|
|
1829
|
+
tokens.some((tok) => SYMLINK_PLANT_BINS.has(writeBinName(tok)))) {
|
|
1075
1830
|
return true;
|
|
1076
1831
|
}
|
|
1077
1832
|
}
|
|
@@ -1099,17 +1854,39 @@ function isProgrammaticWriteBinToken(token) {
|
|
|
1099
1854
|
bare === "nodejs" ||
|
|
1100
1855
|
bare === "perl" ||
|
|
1101
1856
|
bare === "ruby" ||
|
|
1857
|
+
bare === "jruby" ||
|
|
1858
|
+
bare === "pypy" ||
|
|
1859
|
+
bare === "pypy3" ||
|
|
1102
1860
|
bare === "pwsh" ||
|
|
1103
|
-
bare === "powershell"
|
|
1861
|
+
bare === "powershell" ||
|
|
1862
|
+
bare === "tsx" ||
|
|
1863
|
+
bare === "ts-node" ||
|
|
1864
|
+
bare === "tsnode" ||
|
|
1865
|
+
bare === "bun" ||
|
|
1866
|
+
bare === "deno" ||
|
|
1867
|
+
bare === "php" ||
|
|
1868
|
+
bare === "lua" ||
|
|
1869
|
+
bare === "luajit") {
|
|
1104
1870
|
return true;
|
|
1105
1871
|
}
|
|
1106
|
-
// Versioned: python3.11, python3.12, node18, …
|
|
1872
|
+
// Versioned: python3.11, python3.12, node18, php8.3, lua5.4, …
|
|
1107
1873
|
if (bare.startsWith("python3.") || bare.startsWith("python2."))
|
|
1108
1874
|
return true;
|
|
1109
1875
|
if (/^python\d+(\.\d+)*$/.test(bare))
|
|
1110
1876
|
return true;
|
|
1111
1877
|
if (/^node\d+$/.test(bare))
|
|
1112
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;
|
|
1113
1890
|
return false;
|
|
1114
1891
|
}
|
|
1115
1892
|
/**
|
|
@@ -1117,6 +1894,74 @@ function isProgrammaticWriteBinToken(token) {
|
|
|
1117
1894
|
* Escaped quotes (`\'` / `\"`) stay inside the string. Used so `print('.write(')` is not
|
|
1118
1895
|
* a write API while `open(p,'w').write('x')` still matches `.write(` (Greptile conf residual).
|
|
1119
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
|
+
}
|
|
1120
1965
|
function includesOutsideQuotes(haystack, needle) {
|
|
1121
1966
|
if (needle.length === 0 || haystack.length < needle.length)
|
|
1122
1967
|
return false;
|
|
@@ -1143,6 +1988,85 @@ function includesOutsideQuotes(haystack, needle) {
|
|
|
1143
1988
|
}
|
|
1144
1989
|
return false;
|
|
1145
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
|
+
}
|
|
1146
2070
|
/**
|
|
1147
2071
|
* True when command uses a programmatic bin with a **write** API, optionally with
|
|
1148
2072
|
* path-obfuscation markers (base64/bytes/chr). Pure reads / print-only stay unclassifiable.
|
|
@@ -1173,6 +2097,8 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1173
2097
|
const writeApi = lower.includes("writefilesync") ||
|
|
1174
2098
|
lower.includes("writefile") ||
|
|
1175
2099
|
lower.includes("writetext") ||
|
|
2100
|
+
includesIdentCall(lower, "file_put_contents") ||
|
|
2101
|
+
interpreterScriptPayloads(tokens).some((p) => includesIdentCall(p.toLowerCase(), "file_put_contents")) ||
|
|
1176
2102
|
lower.includes("set-content") ||
|
|
1177
2103
|
lower.includes("out-file") ||
|
|
1178
2104
|
lower.includes("fs.write") ||
|
|
@@ -1180,6 +2106,17 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1180
2106
|
lower.includes("path.write(") ||
|
|
1181
2107
|
lower.includes("file.write(") ||
|
|
1182
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) ||
|
|
1183
2120
|
lower.includes(">>") ||
|
|
1184
2121
|
lower.includes("unlink(") ||
|
|
1185
2122
|
lower.includes("rmsync") ||
|
|
@@ -1217,7 +2154,13 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1217
2154
|
lower.includes("mode='a") ||
|
|
1218
2155
|
lower.includes('mode="a') ||
|
|
1219
2156
|
lower.includes("mode=w"));
|
|
1220
|
-
|
|
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;
|
|
1221
2164
|
// Path construction that hides the destination from literal classifiers.
|
|
1222
2165
|
const obfuscatedPath = lower.includes("base64") ||
|
|
1223
2166
|
lower.includes("fromcharcode") ||
|
|
@@ -1243,17 +2186,89 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
|
|
|
1243
2186
|
function pathishToken(token) {
|
|
1244
2187
|
return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
|
|
1245
2188
|
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Collapse `//`, `/./`, and `/../` lexically so equivalent dests still match
|
|
2191
|
+
* (`.deft/foo/../approved-scope/x`) without following symlinks or matching
|
|
2192
|
+
* prefix siblings. O(n) — no nested-quantifier regex.
|
|
2193
|
+
*/
|
|
2194
|
+
function canonicalizePathish(pathish) {
|
|
2195
|
+
const parts = [];
|
|
2196
|
+
let cur = "";
|
|
2197
|
+
const abs = pathish.startsWith("/");
|
|
2198
|
+
for (let i = 0; i <= pathish.length; i++) {
|
|
2199
|
+
const c = i < pathish.length ? pathish[i] : "/";
|
|
2200
|
+
if (c === "/" || i === pathish.length) {
|
|
2201
|
+
if (cur.length === 0 || cur === ".") {
|
|
2202
|
+
// drop empty and `.`
|
|
2203
|
+
}
|
|
2204
|
+
else if (cur === "..") {
|
|
2205
|
+
if (parts.length > 0 && parts[parts.length - 1] !== "..") {
|
|
2206
|
+
parts.pop();
|
|
2207
|
+
}
|
|
2208
|
+
else if (!abs) {
|
|
2209
|
+
parts.push("..");
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
else {
|
|
2213
|
+
parts.push(cur);
|
|
2214
|
+
}
|
|
2215
|
+
cur = "";
|
|
2216
|
+
}
|
|
2217
|
+
else {
|
|
2218
|
+
cur += c;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
const joined = parts.join("/");
|
|
2222
|
+
return abs ? `/${joined}` : joined;
|
|
2223
|
+
}
|
|
1246
2224
|
/**
|
|
1247
2225
|
* True when a pathish string targets `.deft/authz` (after quote strip / slash normalize).
|
|
1248
2226
|
* Quote-split forms like `'.deft/'authz'/grants/x'` become `.deft/authz/grants/x` via pathishToken.
|
|
1249
2227
|
*/
|
|
2228
|
+
/** True when `dir` is a path segment (left-bounded; `foo.deft/authz` is not `.deft/authz`). */
|
|
2229
|
+
function pathishHasDirSegment(pathish, dir) {
|
|
2230
|
+
const p = canonicalizePathish(pathish.trim());
|
|
2231
|
+
if (p === dir || p.startsWith(`${dir}/`) || p.startsWith(`${dir}\\`))
|
|
2232
|
+
return true;
|
|
2233
|
+
for (let i = 0; i < p.length; i++) {
|
|
2234
|
+
const c = p[i];
|
|
2235
|
+
if (c !== "/" && c !== "\\" && c !== ":" && c !== "=")
|
|
2236
|
+
continue;
|
|
2237
|
+
const rest = p.slice(i + 1);
|
|
2238
|
+
if (rest === dir || rest.startsWith(`${dir}/`) || rest.startsWith(`${dir}\\`))
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
return false;
|
|
2242
|
+
}
|
|
1250
2243
|
function pathishIsAuthzDir(pathish) {
|
|
1251
|
-
return pathish
|
|
2244
|
+
return pathishHasDirSegment(pathish, ".deft/authz");
|
|
2245
|
+
}
|
|
2246
|
+
/** True when a pathish string targets `.deft/approved-scope` (#3421 / #3410 mint). */
|
|
2247
|
+
function pathishIsApprovedScopeDir(pathish) {
|
|
2248
|
+
return pathishHasDirSegment(pathish, ".deft/approved-scope");
|
|
2249
|
+
}
|
|
2250
|
+
function pathishIsSettingsStoreDir(pathish) {
|
|
2251
|
+
return pathishIsAuthzDir(pathish) || pathishIsApprovedScopeDir(pathish);
|
|
2252
|
+
}
|
|
2253
|
+
/** True when dest is a settings-store path or a split-path `authz` segment after `.deft`. */
|
|
2254
|
+
function destMentionsAuthzSegment(dest) {
|
|
2255
|
+
const p = canonicalizePathish(pathishToken(dest).trim());
|
|
2256
|
+
if (pathishIsSettingsStoreDir(p))
|
|
2257
|
+
return true;
|
|
2258
|
+
const segs = p.split("/").filter((s) => s.length > 0);
|
|
2259
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2260
|
+
if (segs[i] !== "authz")
|
|
2261
|
+
continue;
|
|
2262
|
+
const prev = i > 0 ? segs[i - 1] : "";
|
|
2263
|
+
if (prev === ".deft" || prev.length === 0)
|
|
2264
|
+
return true;
|
|
2265
|
+
}
|
|
2266
|
+
return false;
|
|
1252
2267
|
}
|
|
1253
2268
|
/**
|
|
1254
|
-
* Shell **write** targeting `.deft/authz/`
|
|
2269
|
+
* Shell **write** targeting `.deft/authz/` or `.deft/approved-scope/` (#3110 / #3421).
|
|
1255
2270
|
* Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
|
|
1256
|
-
* Redirects only count when the destination region contains
|
|
2271
|
+
* Redirects only count when the destination region contains a settings-store dir.
|
|
1257
2272
|
* Pathish/token checks run even when the raw command lacks contiguous `.deft/authz`
|
|
1258
2273
|
* text (quote-split residual: `cp x '.deft/'authz'/grants/y'`).
|
|
1259
2274
|
*/
|
|
@@ -1279,7 +2294,7 @@ function hasAuthzDirShellWrite(command, tokens) {
|
|
|
1279
2294
|
hay[end] !== "\n") {
|
|
1280
2295
|
end++;
|
|
1281
2296
|
}
|
|
1282
|
-
if (hay.slice(j, end)
|
|
2297
|
+
if (pathishIsSettingsStoreDir(hay.slice(j, end)))
|
|
1283
2298
|
return true;
|
|
1284
2299
|
}
|
|
1285
2300
|
}
|
|
@@ -1289,7 +2304,7 @@ function hasAuthzDirShellWrite(command, tokens) {
|
|
|
1289
2304
|
// must not fail-open as unclassifiable (SLizard residual). ARCHIVE_ALT_WRITE_BINS: tar/rclone
|
|
1290
2305
|
// residual pathish without dest-flag perfection thrash.
|
|
1291
2306
|
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1292
|
-
const bare =
|
|
2307
|
+
const bare = writeBinName(tokens[ti]);
|
|
1293
2308
|
const n = normalizeToken(tokens[ti]);
|
|
1294
2309
|
if (!INDIRECT_WRITE_BINS.has(n) &&
|
|
1295
2310
|
!INDIRECT_WRITE_BINS.has(bare) &&
|
|
@@ -1297,20 +2312,24 @@ function hasAuthzDirShellWrite(command, tokens) {
|
|
|
1297
2312
|
!ARCHIVE_ALT_WRITE_BINS.has(bare)) {
|
|
1298
2313
|
continue;
|
|
1299
2314
|
}
|
|
2315
|
+
// Last-positional dest bins (and their subcommands): dest harvest owns dest vs source.
|
|
2316
|
+
if (LAST_POSITIONAL_DEST_BINS.has(bare) || segmentStartedByLastPositionalDest(tokens, ti)) {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
1300
2319
|
for (let tj = ti + 1; tj < tokens.length; tj++) {
|
|
1301
2320
|
const p = pathishToken(tokens[tj]);
|
|
1302
|
-
if (
|
|
2321
|
+
if (pathishIsSettingsStoreDir(p))
|
|
1303
2322
|
return true;
|
|
1304
2323
|
for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
|
|
1305
|
-
if (p.startsWith(prefix) &&
|
|
2324
|
+
if (p.startsWith(prefix) && pathishIsSettingsStoreDir(p.slice(prefix.length))) {
|
|
1306
2325
|
return true;
|
|
1307
2326
|
}
|
|
1308
2327
|
}
|
|
1309
2328
|
}
|
|
1310
2329
|
}
|
|
1311
|
-
// Downloader/decoder destinations under
|
|
2330
|
+
// Downloader/decoder destinations under settings-store dirs (#3206 / #3421).
|
|
1312
2331
|
for (const dest of downloaderDecoderDestinations(tokens)) {
|
|
1313
|
-
if (
|
|
2332
|
+
if (pathishIsSettingsStoreDir(dest))
|
|
1314
2333
|
return true;
|
|
1315
2334
|
}
|
|
1316
2335
|
// Contiguous mention without write shape stays false (reads like cat .deft/authz/…).
|
|
@@ -1347,8 +2366,38 @@ const INDIRECT_WRITE_BINS = new Set([
|
|
|
1347
2366
|
"remove-item",
|
|
1348
2367
|
"ri",
|
|
1349
2368
|
"ni",
|
|
2369
|
+
"new-item",
|
|
1350
2370
|
"sc",
|
|
1351
2371
|
"mi",
|
|
2372
|
+
// #3459 residual: Windows copy / fsutil / touch / TS runners.
|
|
2373
|
+
"copy",
|
|
2374
|
+
"fsutil",
|
|
2375
|
+
"touch",
|
|
2376
|
+
"tsx",
|
|
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,
|
|
1352
2401
|
]);
|
|
1353
2402
|
/**
|
|
1354
2403
|
* O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
|
|
@@ -1390,7 +2439,7 @@ function hasWriteShape(command, tokens) {
|
|
|
1390
2439
|
if (command.includes(">"))
|
|
1391
2440
|
return true;
|
|
1392
2441
|
for (const t of tokens) {
|
|
1393
|
-
const bare =
|
|
2442
|
+
const bare = writeBinName(t);
|
|
1394
2443
|
if (INDIRECT_WRITE_BINS.has(normalizeToken(t)) || INDIRECT_WRITE_BINS.has(bare))
|
|
1395
2444
|
return true;
|
|
1396
2445
|
// #3245: archive extractors / alt downloaders are write-shaped (defense-in-depth for
|
|
@@ -1551,15 +2600,7 @@ function hasProgrammaticAuthzEnvWrite(command, tokens) {
|
|
|
1551
2600
|
const lower = command.toLowerCase();
|
|
1552
2601
|
let hasProg = false;
|
|
1553
2602
|
for (const t of tokens) {
|
|
1554
|
-
|
|
1555
|
-
if (n === "python" ||
|
|
1556
|
-
n === "python3" ||
|
|
1557
|
-
n === "node" ||
|
|
1558
|
-
n === "nodejs" ||
|
|
1559
|
-
n === "perl" ||
|
|
1560
|
-
n === "ruby" ||
|
|
1561
|
-
n === "pwsh" ||
|
|
1562
|
-
n === "powershell") {
|
|
2603
|
+
if (isProgrammaticWriteBinToken(t)) {
|
|
1563
2604
|
hasProg = true;
|
|
1564
2605
|
break;
|
|
1565
2606
|
}
|
|
@@ -1748,7 +2789,7 @@ export function classifyShellAuthzOps(command) {
|
|
|
1748
2789
|
lower[end] !== "\n") {
|
|
1749
2790
|
end++;
|
|
1750
2791
|
}
|
|
1751
|
-
if (lower.slice(j, end)
|
|
2792
|
+
if (destMentionsAuthzSegment(lower.slice(j, end))) {
|
|
1752
2793
|
found.add("settings");
|
|
1753
2794
|
break;
|
|
1754
2795
|
}
|