
























@@ -1,4 +1,10 @@
11import { splitShellArgs } from "../../utils/shell-argv.js";
2+import {
3+COMMAND_CARRIER_EXECUTABLES,
4+isEnvAssignmentToken,
5+resolveCarrierCommandArgv,
6+SOURCE_EXECUTABLES,
7+} from "../command-carriers.js";
28import { unwrapKnownDispatchWrapperInvocation } from "../dispatch-wrapper-resolution.js";
39import type { ExecCommandSegment } from "../exec-approvals-analysis.js";
410import { normalizeExecutableToken } from "../exec-wrapper-resolution.js";
@@ -8,9 +14,7 @@ import {
814} from "../shell-wrapper-resolution.js";
915import { detectInterpreterInlineEvalArgv, type InterpreterInlineEvalHit } from "./inline-eval.js";
101611-export const COMMAND_CARRIER_EXECUTABLES = new Set(["sudo", "doas", "env", "command", "builtin"]);
12-13-export const SOURCE_EXECUTABLES = new Set([".", "source"]);
17+export { COMMAND_CARRIER_EXECUTABLES, resolveCarrierCommandArgv, SOURCE_EXECUTABLES };
14181519export type CommandCarrierHit = {
1620command: string;
@@ -19,340 +23,6 @@ export type CommandCarrierHit = {
19232024export type CarriedShellBuiltinHit = { kind: "eval" } | { kind: "source"; command: string };
212522-const MAX_ENV_SPLIT_PAYLOAD_DEPTH = 32;
23-24-const COMMAND_EXECUTING_OPTIONS = new Set(["-p"]);
25-const COMMAND_QUERY_OPTIONS = new Set(["-v", "-V"]);
26-const ENV_OPTIONS_WITH_VALUE = new Set([
27-"-C",
28-"-S",
29-"-u",
30-"--argv0",
31-"--block-signal",
32-"--chdir",
33-"--default-signal",
34-"--ignore-signal",
35-"--split-string",
36-"--unset",
37-]);
38-const ENV_SPLIT_STRING_OPTIONS = new Set(["-S", "--split-string"]);
39-const ENV_STANDALONE_OPTIONS = new Set(["-0", "-i", "--ignore-environment", "--null"]);
40-const SUDO_OPTIONS_WITH_VALUE = new Set([
41-"-C",
42-"-D",
43-"-g",
44-"-h",
45-"-p",
46-"-R",
47-"-T",
48-"-U",
49-"-u",
50-"--chdir",
51-"--close-from",
52-"--group",
53-"--host",
54-"--other-user",
55-"--prompt",
56-"--role",
57-"--type",
58-"--user",
59-]);
60-const SUDO_STANDALONE_OPTIONS = new Set([
61-"-A",
62-"-b",
63-"-E",
64-"-H",
65-"-n",
66-"-P",
67-"-S",
68-"--askpass",
69-"--background",
70-"--login",
71-"--non-interactive",
72-"--preserve-env",
73-"--reset-home",
74-"--stdin",
75-]);
76-const SUDO_NON_EXEC_OPTIONS = new Set([
77-"-K",
78-"-k",
79-"-l",
80-"-V",
81-"-v",
82-"-e",
83-"--edit",
84-"--help",
85-"--list",
86-"--remove-timestamp",
87-"--reset-timestamp",
88-"--validate",
89-"--version",
90-]);
91-const DOAS_OPTIONS_WITH_VALUE = new Set(["-a", "-C", "-u"]);
92-const DOAS_STANDALONE_OPTIONS = new Set(["-L", "-n", "-s"]);
93-const EXEC_OPTIONS_WITH_VALUE = new Set(["-a"]);
94-const EXEC_STANDALONE_OPTIONS = new Set(["-c", "-l"]);
95-96-function isEnvAssignmentToken(token: string): boolean {
97-return /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(token);
98-}
99-100-function optionName(token: string): string {
101-return token.split("=", 1)[0] ?? token;
102-}
103-104-type ParsedCarrierOption = {
105-name: string;
106-hasInlineValue: boolean;
107-inlineValue?: string;
108-};
109-110-function parseCarrierOptionToken(
111-token: string,
112-standaloneOptions: ReadonlySet<string>,
113-optionsWithValue: ReadonlySet<string>,
114-nonExecutingOptions: ReadonlySet<string> = new Set(),
115-): ParsedCarrierOption[] | null {
116-if (token.startsWith("--")) {
117-const name = optionName(token);
118-if (
119-standaloneOptions.has(name) ||
120-optionsWithValue.has(name) ||
121-nonExecutingOptions.has(name)
122-) {
123-const valueDelimiter = token.indexOf("=");
124-return [
125-{
126- name,
127-hasInlineValue: valueDelimiter >= 0,
128-inlineValue: valueDelimiter >= 0 ? token.slice(valueDelimiter + 1) : undefined,
129-},
130-];
131-}
132-return null;
133-}
134-135-if (!/^-[A-Za-z0-9]/u.test(token)) {
136-return null;
137-}
138-139-const options: ParsedCarrierOption[] = [];
140-for (let index = 1; index < token.length; index += 1) {
141-const name = `-${token[index] ?? ""}`;
142-if (optionsWithValue.has(name)) {
143-options.push({
144- name,
145-hasInlineValue: index < token.length - 1,
146-inlineValue: index < token.length - 1 ? token.slice(index + 1) : undefined,
147-});
148-return options;
149-}
150-if (standaloneOptions.has(name) || nonExecutingOptions.has(name)) {
151-options.push({ name, hasInlineValue: false });
152-continue;
153-}
154-return null;
155-}
156-return options.length > 0 ? options : null;
157-}
158-159-function knownCarrierOptionConsumesNextValue(
160-options: readonly ParsedCarrierOption[],
161-optionsWithValue: ReadonlySet<string>,
162-nonExecutingOptions: ReadonlySet<string> = new Set(),
163-): boolean | null {
164-let consumesNextValue = false;
165-for (const option of options) {
166-if (nonExecutingOptions.has(option.name)) {
167-return null;
168-}
169-if (optionsWithValue.has(option.name)) {
170-consumesNextValue = !option.hasInlineValue;
171-}
172-}
173-return consumesNextValue;
174-}
175-176-function findParsedCarrierOption(
177-options: readonly ParsedCarrierOption[],
178-names: ReadonlySet<string>,
179-): ParsedCarrierOption | undefined {
180-return options.find((option) => names.has(option.name));
181-}
182-183-function resolveEnvSplitPayload(
184-payload: string,
185-trailingArgv: string[],
186-depth: number,
187-): string[] | null {
188-const innerArgv = splitShellArgs(payload);
189-if (!innerArgv || innerArgv.length === 0) {
190-return null;
191-}
192-const carriedArgv = [...innerArgv, ...trailingArgv];
193-return resolveEnvCarriedArgv(["env", ...carriedArgv], depth + 1) ?? carriedArgv;
194-}
195-196-function resolveEnvCarriedArgv(argv: string[], depth = 0): string[] | null {
197-if (depth > MAX_ENV_SPLIT_PAYLOAD_DEPTH || normalizeExecutableToken(argv[0] ?? "") !== "env") {
198-return null;
199-}
200-for (let index = 1; index < argv.length; index += 1) {
201-const token = argv[index] ?? "";
202-if (!token) {
203-return null;
204-}
205-if (isEnvAssignmentToken(token)) {
206-continue;
207-}
208-if (token === "--") {
209-return argv.slice(index + 1);
210-}
211-if (token.startsWith("-")) {
212-const option = parseCarrierOptionToken(token, ENV_STANDALONE_OPTIONS, ENV_OPTIONS_WITH_VALUE);
213-if (!option) {
214-return null;
215-}
216-const splitStringOption = findParsedCarrierOption(option, ENV_SPLIT_STRING_OPTIONS);
217-if (splitStringOption) {
218-const payloadIndex = splitStringOption.inlineValue === undefined ? index + 1 : index;
219-const payload = splitStringOption.inlineValue ?? argv[payloadIndex];
220-return typeof payload === "string"
221- ? resolveEnvSplitPayload(payload, argv.slice(payloadIndex + 1), depth)
222- : null;
223-}
224-const consumeNextValue = knownCarrierOptionConsumesNextValue(option, ENV_OPTIONS_WITH_VALUE);
225-if (consumeNextValue) {
226-index += 1;
227-}
228-continue;
229-}
230-return argv.slice(index);
231-}
232-return null;
233-}
234-235-function resolveCommandBuiltinCarriedArgv(argv: string[]): string[] | null {
236-const executable = normalizeExecutableToken(argv[0] ?? "");
237-if (executable !== "command" && executable !== "builtin") {
238-return null;
239-}
240-for (let index = 1; index < argv.length; index += 1) {
241-const token = argv[index] ?? "";
242-if (token === "--") {
243-return argv.slice(index + 1);
244-}
245-if (!token.startsWith("-")) {
246-return argv.slice(index);
247-}
248-const normalized = optionName(token);
249-if (COMMAND_QUERY_OPTIONS.has(normalized)) {
250-return null;
251-}
252-if (!COMMAND_EXECUTING_OPTIONS.has(normalized)) {
253-return null;
254-}
255-}
256-return null;
257-}
258-259-function resolveSudoLikeCarriedArgv(argv: string[]): string[] | null {
260-const executable = normalizeExecutableToken(argv[0] ?? "");
261-const standaloneOptions =
262-executable === "sudo"
263- ? SUDO_STANDALONE_OPTIONS
264- : executable === "doas"
265- ? DOAS_STANDALONE_OPTIONS
266- : null;
267-const optionsWithValue =
268-executable === "sudo"
269- ? SUDO_OPTIONS_WITH_VALUE
270- : executable === "doas"
271- ? DOAS_OPTIONS_WITH_VALUE
272- : null;
273-if (!standaloneOptions || !optionsWithValue) {
274-return null;
275-}
276-for (let index = 1; index < argv.length; index += 1) {
277-const token = argv[index] ?? "";
278-if (token === "--") {
279-return argv.slice(index + 1);
280-}
281-if (!token.startsWith("-")) {
282-return argv.slice(index);
283-}
284-const option = parseCarrierOptionToken(
285-token,
286-standaloneOptions,
287-optionsWithValue,
288-executable === "sudo" ? SUDO_NON_EXEC_OPTIONS : undefined,
289-);
290-if (!option) {
291-return null;
292-}
293-const consumeNextValue = knownCarrierOptionConsumesNextValue(
294-option,
295-optionsWithValue,
296-executable === "sudo" ? SUDO_NON_EXEC_OPTIONS : undefined,
297-);
298-if (consumeNextValue === null) {
299-return null;
300-}
301-if (consumeNextValue) {
302-index += 1;
303-}
304-continue;
305-}
306-return null;
307-}
308-309-export function resolveCarrierCommandArgv(
310-argv: string[],
311-depth = 0,
312-options?: { includeExec?: boolean },
313-): string[] | null {
314-const executable = normalizeExecutableToken(argv[0] ?? "");
315-switch (executable) {
316-case "env":
317-return resolveEnvCarriedArgv(argv, depth);
318-case "command":
319-case "builtin":
320-return resolveCommandBuiltinCarriedArgv(argv);
321-case "sudo":
322-case "doas":
323-return resolveSudoLikeCarriedArgv(argv);
324-case "exec":
325-return options?.includeExec ? resolveExecCarriedArgv(argv) : null;
326-default:
327-return null;
328-}
329-}
330-331-function resolveExecCarriedArgv(argv: string[]): string[] | null {
332-if (normalizeExecutableToken(argv[0] ?? "") !== "exec") {
333-return null;
334-}
335-for (let index = 1; index < argv.length; index += 1) {
336-const token = argv[index] ?? "";
337-if (token === "--") {
338-return argv.slice(index + 1);
339-}
340-if (!token.startsWith("-")) {
341-return argv.slice(index);
342-}
343-const option = parseCarrierOptionToken(token, EXEC_STANDALONE_OPTIONS, EXEC_OPTIONS_WITH_VALUE);
344-if (!option) {
345-return null;
346-}
347-const consumeNextValue = knownCarrierOptionConsumesNextValue(option, EXEC_OPTIONS_WITH_VALUE);
348-if (consumeNextValue) {
349-index += 1;
350-}
351-continue;
352-}
353-return null;
354-}
355-35626function commandArgvKey(argv: readonly string[]): string {
35727return argv.join("\0");
35828}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。