/** * Break a command line into the commands it actually runs. * * ## The hole this closes * * A pattern rule tests a regular expression against an argument's value. When * that value is a command line, the value and the command are not the same * thing: `git push origin main` is one command, and * `true; git push origin main` is two, of which the second is the one the rule * was written about. An anchored pattern sees only the first. * * Measured against the gate, with the rule from this repository's own * documentation (`^git push`, deny): * * git push origin main -> deny * echo hi && git push origin main -> did not match * true; git push origin main -> did not match * bash -c "git push origin main" -> did not match * * A rule that fails to match reaches the permission mode, and a turn with no * terminal resolves that to `auto`. So an operator's prohibition was bypassed * by typing four characters in front of it, in exactly the unattended case the * prohibition exists for. The `bash` tool's own description tells the model to * "use `&&` / `;` chaining for compound commands", so the evading form is not * an exotic input — it is the documented one. * * ## Why splitting alone would make things worse * * Applied naively this widens `allow` in the same motion it fixes `deny`. An * allow rule matching `^git status` would go on matching the first segment of * `git status && rm -rf ~` and hand back `allow` for the whole line. So the * caller must read the two decisions differently, and {@link evaluateRule} * does: * * - **deny** matches when ANY segment matches. One prohibited command poisons * the line it rides on. * - **allow** matches only when EVERY segment matches, and never when the line * is {@link CommandLineDecomposition.opaque}. Permission is a claim about the * whole line, and a claim that cannot be checked is not granted. * * That asymmetry is the same one `refuse-do-not-degrade` describes: when the * analysis is uncertain, the uncertainty spends against the permissive answer. * * ## What `opaque` means * * Some lines contain text that is not the command that runs. Command * substitution (`$(…)`, backticks, `<(…)`) executes something whose text is * not in the line at all, and `eval` runs a string assembled at runtime. No * decomposition of the source can be a decomposition of what ran, so the line * is marked opaque and `allow` declines it. `deny` still tests what is visible, * because a deny that matches too much costs a prompt and a deny that matches * too little costs the thing it was written to prevent. * * ## What it deliberately does not do * * A value with no chain operator, no nested shell and nothing opaque comes back * as itself, byte for byte. That keeps every rule about a non-command argument * — a path, a number, a URL — behaving exactly as it did, and confines this * machinery to the case that motivated it. * * It is a decomposition, not a shell. `xargs sh -c`, a command read from a * file, and a shell invoked through an interpreter it does not recognise all * pass through as ordinary text. Each of those either denies as before or, for * an allow rule, fails to match every segment and so declines. The failure mode * is a prompt, never a silent grant. */ /** The commands a line runs, and whether that list can be trusted as complete. */ export interface CommandLineDecomposition { /** * The individual commands, in source order. Never empty: a line that * decomposes to nothing yields the original. */ readonly segments: readonly string[]; /** * True when the line runs something this decomposition cannot see, so * `segments` is a lower bound rather than the whole story. */ readonly opaque: boolean; } export declare function decomposeCommandLine(command: string): CommandLineDecomposition; //# sourceMappingURL=command-line.d.ts.map