feat(interp): add AllowedCommandPatterns argv-prefix allowlist#222
Draft
feat(interp): add AllowedCommandPatterns argv-prefix allowlist#222
Conversation
Introduces a second permit axis alongside AllowedCommands. Each pattern
is a token list; an invocation whose argv begins with the same tokens
is allowed. Patterns are matched after shell expansion, so command
substitution cannot bypass enforcement — the matcher inspects the
resolved argv at execve time.
The two axes are joined by union: a command runs if its name is in
AllowedCommands OR its argv prefix-matches any pattern. This lets
operators express "allow kubectl get but not kubectl delete" without
having to allow kubectl wholesale.
Threaded through the existing builtin policy callback by extending
CommandAllowed(name string, args []string), so find -exec/-execdir
also enforce patterns at both parse time (with unsubstituted argv) and
eval time (with the substituted argv). help passes a single-element
argv; multi-token-pattern-only commands therefore don't appear in the
help listing — documented cosmetic limitation.
Adds:
- interp.AllowedCommandPatterns([][]string) RunnerOption
- --allowed-command-patterns CLI flag (comma-separated patterns,
space-separated tokens)
- 13 unit tests + 6 YAML scenarios, including the architectural
substitution-defeat case
- examples/command_patterns.sh with 8 curated invocations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ndSpec
Replaces the original argv-prefix matcher with a spec-driven structural
matcher. A pattern is now shaped (command [, subcommand_path...]) and
matched against the LEADING STRUCTURAL TOKENS of argv[1..] (where
"structural" means non-flag tokens, classified per a registered
CommandSpec). This closes a semantic hole in the previous matcher:
positional argument values that happened to equal a pattern subcommand
token would satisfy the pattern even when the operation wasn't the one
the policy author intended.
Architectural model:
- CommandSpec describes a command's flag conventions (BooleanFlags,
ValueFlags). The matcher uses the spec to skip flag tokens (and
their values) when extracting structural tokens from argv.
- Multi-token patterns require a CommandSpec for their command;
single-token patterns do not (they only check argv[0]).
- Validation runs at New() after all options applied, so option
order doesn't matter.
- The built-in registry ships only `ip` (the only rshell builtin
with multi-token subcommand structure). Integrators register
additional specs via interp.CommandSpecs(...).
Behaviour vs. previous (presence-based) matcher:
- "ip route show" matches (ip, route): SAME.
- "ip -4 route show" matches (ip, route): SAME (flag interleaving).
- "ip addr show" rejected by (ip, route): SAME.
- "ip addr show route" rejected by (ip, route): NEW — previously
matched because "route" appeared anywhere in argv. Now blocked
because the leading structural token after argv[0] is "addr".
CLI/API surface:
- New: interp.CommandSpec, interp.CommandSpecs(...) RunnerOption.
- New: --allowed-command-patterns CLI flag was added in the prior
commit on this branch; nothing to change there.
- Examples (examples/command_patterns.sh) now use `ip` as the
canonical multi-token-pattern target.
- YAML scenarios rewritten to use `ip`.
- 24 unit tests covering option validation, structural matching,
flag classification (boolean, value, key=value forms, unknown
flags), and the architectural substitution-defeat case.
Allowlist of strings.Contains added to analysis/symbols_interp.go for
the "--key=value" detection in extractStructuralTokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an invocation is rejected by the policy gate, print the FULL
attempted invocation (not just the command name) plus a hint listing
the patterns configured for that command name. This turns
rshell: ip: command not allowed
into
rshell: ip addr show: invocation not permitted by policy (command name: ip)
hint: allowed patterns for "ip": 'ip route'
so operators can immediately see what was attempted and what would
have been admissible. The bare-command case (script "ip" with no
args) keeps the original short format for back-compat readability.
Updates the few test scenarios and unit tests that asserted the old
exact-string error to match the new format. strings.TrimRight added
to the analysis allowlist (used in runCmd's error wrapping).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third pattern axis alongside AllowedCommands and
AllowedCommandPatterns: DeniedCommandPatterns blocks invocations whose
argv satisfies any of the given patterns, evaluated BEFORE every allow
rule. A deny match short-circuits the gate to a refusal even when
AllowAllCommands, a name allowlist, or an allow pattern would
otherwise admit the call.
Use case: an operator can allow `ip` wholesale via AllowedCommands but
carve out `ip route` specifically via DeniedCommandPatterns. ip addr
and ip link still admit; ip route is refused at the gate.
Same pattern shape and matching semantics as AllowedCommandPatterns:
each pattern is a (command [, subcommand_path...]) token list, matched
against the argv's leading structural tokens using the same
spec-driven flag classification. Multi-token deny patterns require a
registered CommandSpec; option-time validation (renamed
validatePatternList, factored to cover both axes) rejects them
otherwise.
Architectural property carries over: a substitution that resolves at
runtime to a denied argv is blocked even though the literal command
text was opaque. Denies are inspected post-expansion at the same call
sites as allows.
Error messages now distinguish "blocked by deny pattern X" from "no
allow rule matched" so operators see which side rejected. The
help-suggestion footer is suppressed for deny denials since 'help'
won't reveal why the deny fired.
Adds:
- interp.DeniedCommandPatterns([][]string) RunnerOption
- --denied-command-patterns CLI flag
- 9 new unit tests covering option validation, deny-overrides-name,
deny-overrides-allow-pattern, post-expansion enforcement, and
structural-position correctness (deny doesn't match positional
values like the allow side doesn't)
- 4 YAML scenarios under tests/scenarios/shell/denied_command_patterns/
- 3 examples script cases (cases 11-13)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces a second command permit axis alongside
AllowedCommands. EachAllowedCommandPatternsentry is a token list; an invocation whose argv begins with the same tokens is allowed. Patterns are matched after shell expansion, so command substitution cannot bypass enforcement — the matcher inspects the resolved argv at execve time.The two axes are joined by union: a command runs if its name is in
AllowedCommandsOR its argv prefix-matches any pattern. This lets operators express "allowkubectl getbut notkubectl delete" without having to allowkubectlwholesale.Why
Built as a POC for an rshell permission-model RFC. The motivating example is the canonical substitution attack:
The literal command text is opaque to any static caller. Only enforcement that runs after expansion (i.e. on the node, not the backend) can defend against this; this PR adds that primitive in the OSS interpreter.
What changed
Library API
interp.AllowedCommandPatterns([][]string) RunnerOptionwith validation (non-empty patterns, non-empty tokens).(*Runner).argvMatchesAllowedPattern([]string) bool— six-line linear-scan, exact-token equality, no regex.builtins.CallContext.CommandAllowedcallback signature extended fromfunc(name string) booltofunc(name string, args []string) bool. All consumers (find, help) updated; pattern matching now flows throughfind -exec/-execdirat both parse-time (unsubstituted argv with{}placeholders preserved) and eval-time (fully substituted argv).helppasses single-element argv[]string{name}; documented cosmetic limitation: pattern-only multi-token commands don't appear in the help listing.Enforcement points (all four)
runner_exec.go:call(top-level dispatch).runner_exec.go:runCmd(recursive builtin dispatch; reconstructs full argv fromcmdName + cmdArgsbefore consulting matcher).CommandAllowedcallback definitions inrunner_exec.go(handed to builtins).CLI
--allowed-command-patternsflag: comma-separates patterns, whitespace-separates tokens within a pattern. e.g.--allowed-command-patterns "kubectl get,ls,echo hello".--allowed-commandsand--allow-all-commandsunchanged.Tests
interp/allowed_command_patterns_test.go: option validation, basic matching, length edge cases, union semantics withAllowedCommands, and the architectural testTestPatternsBlockSubstitutionEscapethat proves substitution can't bypass.tests/scenarios/shell/allowed_command_patterns/covering the same cases through the existingTestShellScenariosharness.CommandAllowedsignature.Docs + examples
README.mdSecurity Model table updated with the new option.SHELL_FEATURES.mdlistsAllowedCommandPatterns.examples/command_patterns.sh— 8 curated invocations stepping through allow/block/substitution-defeat/find -exec/union semantics.Semantics summary
AllowedCommands: ["rshell:cat"]catallowed for any args (existing).AllowedCommandPatterns: [["kubectl","get"]]kubectl get *allowed;kubectl delete *blocked.allow-all-commandsTest plan
go test ./...— 45/45 packages pass, no failures.bash examples/command_patterns.sh— all 8 cases produce documented outcomes.find -execflows pattern matching through both parse-time and eval-time gates.CommandAllowedsignature change doesn't affect any consumer outsidefindandhelp(grepped, but worth a fresh pass).Out of scope / follow-ups
helplistings when only patterns are configured.🤖 Generated with Claude Code