|
2 | 2 | // enforcement owner (hard deny at the top of its verdict path). |
3 | 3 |
|
4 | 4 | import { splitChainedCommand, tokenize } from "../permission/command.js"; |
| 5 | +import { |
| 6 | + peelTransparentCommand, |
| 7 | + programBasename, |
| 8 | +} from "./transparent-command.js"; |
| 9 | + |
| 10 | +export { programBasename } from "./transparent-command.js"; |
5 | 11 |
|
6 | 12 | function skipMatching( |
7 | 13 | tokens: readonly string[], |
@@ -318,16 +324,6 @@ const RECURSIVE_FLAG = /^(--recursive|-[A-Za-z]*[rR][A-Za-z]*)$/; |
318 | 324 | // Interpreters whose `-c` / `--command` payload is an independent shell subject. |
319 | 325 | // Exported so tests and callers share one explicit list with the peeler. |
320 | 326 | export const SHELL_INTERPRETERS = new Set(["bash", "sh", "zsh", "dash", "ksh"]); |
321 | | -// Transparent prefixes that sit in front of a real program without changing it. |
322 | | -const PREFIX_WRAPPERS = new Set([ |
323 | | - "command", |
324 | | - "env", |
325 | | - "builtin", |
326 | | - "time", |
327 | | - "nice", |
328 | | - "nohup", |
329 | | - "timeout", |
330 | | -]); |
331 | 327 | // Max recursive peel depth for nested wrappers. Exported so the depth cap is a |
332 | 328 | // named policy knob tests can assert against, not a magic number. |
333 | 329 | export const MAX_PEEL_DEPTH = 4; |
@@ -375,12 +371,6 @@ function isDangerousTarget(token: string): boolean { |
375 | 371 | return false; |
376 | 372 | } |
377 | 373 |
|
378 | | -export function programBasename(token: string): string { |
379 | | - const bare = token.replace(/['"]/g, ""); |
380 | | - const slash = bare.lastIndexOf("/"); |
381 | | - return slash >= 0 ? bare.slice(slash + 1) : bare; |
382 | | -} |
383 | | - |
384 | 374 | // Payload we cannot statically inspect: empty, a bare expansion, or a leading |
385 | 375 | // command substitution. Argument-position expansions like `rm -rf $HOME` stay |
386 | 376 | // parseable so catastrophic-target checks still fire. |
@@ -684,7 +674,8 @@ function finishEnvSplitPayload( |
684 | 674 | raw = payload; |
685 | 675 | } else { |
686 | 676 | if (isOpaquePayload(rest.join(" "))) return { kind: "opaque" }; |
687 | | - raw = rejoinTokens([payload, ...rest]); |
| 677 | + const trailing = rejoinTokens(rest); |
| 678 | + raw = trailing === null ? null : `${payload} ${trailing}`; |
688 | 679 | } |
689 | 680 | if (raw === null) return { kind: "opaque" }; |
690 | 681 | return peelEnvSplitUtility(raw); |
@@ -811,66 +802,14 @@ function skipEnvFlagsAndAssignments(tokens: string[], start: number): number { |
811 | 802 | // single segment. |
812 | 803 | function peelOnce(segment: string): PeelOutcome { |
813 | 804 | const tokens = tokenize(segment); |
814 | | - let i = 0; |
815 | | - i = skipMatching(tokens, i, (t) => ENV_ASSIGNMENT.test(t)); |
816 | | - |
817 | | - let strippedPrefix = false; |
818 | | - while (i < tokens.length) { |
819 | | - const current = tokens[i]; |
820 | | - if (current === undefined) break; |
821 | | - const base = programBasename(current); |
822 | | - if (base === "env") { |
823 | | - // Prefer split-string peel: the whole payload is one quoted argument |
824 | | - // that env re-splits itself, so the transparent-prefix path below |
825 | | - // would only rejoin `-S '…'` and leave the real command invisible. |
826 | | - const splitPeel = peelEnvSplitString(tokens, i + 1); |
827 | | - if (splitPeel.kind !== "none") return splitPeel; |
828 | | - strippedPrefix = true; |
829 | | - i = skipEnvFlagsAndAssignments(tokens, i + 1); |
830 | | - continue; |
831 | | - } |
832 | | - if (base === "timeout") { |
833 | | - strippedPrefix = true; |
834 | | - i++; |
835 | | - // Optional duration (10, 30s, 1m, …) and common long/short flags. |
836 | | - while (i < tokens.length) { |
837 | | - const t = tokens[i]; |
838 | | - if (t === undefined) break; |
839 | | - if (/^\d/.test(t)) { |
840 | | - i++; |
841 | | - continue; |
842 | | - } |
843 | | - if (t.startsWith("-") && t !== "-") { |
844 | | - // Flags that take a value: -k / --kill-after / -s / --signal. |
845 | | - if ( |
846 | | - t === "-k" || |
847 | | - t === "--kill-after" || |
848 | | - t === "-s" || |
849 | | - t === "--signal" || |
850 | | - t.startsWith("--kill-after=") || |
851 | | - t.startsWith("--signal=") |
852 | | - ) { |
853 | | - i++; |
854 | | - if (!t.includes("=") && i < tokens.length) { |
855 | | - const next = tokens[i]; |
856 | | - if (next !== undefined && !next.startsWith("-")) i++; |
857 | | - } |
858 | | - continue; |
859 | | - } |
860 | | - i++; |
861 | | - continue; |
862 | | - } |
863 | | - break; |
864 | | - } |
865 | | - continue; |
866 | | - } |
867 | | - if (PREFIX_WRAPPERS.has(base) && base !== "env" && base !== "timeout") { |
868 | | - strippedPrefix = true; |
869 | | - i++; |
870 | | - continue; |
871 | | - } |
872 | | - break; |
| 805 | + const transparent = peelTransparentCommand(tokens); |
| 806 | + for (const wrapperIndex of transparent.wrapperIndexes) { |
| 807 | + if (programBasename(tokens[wrapperIndex] ?? "") !== "env") continue; |
| 808 | + const splitPeel = peelEnvSplitString(tokens, wrapperIndex + 1); |
| 809 | + if (splitPeel.kind !== "none") return splitPeel; |
873 | 810 | } |
| 811 | + const i = transparent.executableIndex; |
| 812 | + const strippedPrefix = transparent.wrapperIndexes.length > 0; |
874 | 813 |
|
875 | 814 | if (i >= tokens.length) |
876 | 815 | return strippedPrefix ? { kind: "opaque" } : { kind: "none" }; |
|
0 commit comments