Module: Rubino::Security::PrefixDeriver
- Defined in:
- lib/rubino/security/prefix_deriver.rb
Overview
Derives the REUSABLE rule that an approval should be remembered as, instead of pinning memory to the exact “<tool>:<command>” string.
Mirrors the reference persistence unit: approve_session/is_approved key on a PATTERN KEY, not the raw command. For a dangerous command the pattern key IS the dangerous description, so approving it once covers the whole risk class for the session. For a plain command the reference allowlist is prefix-ish; we derive a leading-token prefix the same way CommandAllowlist matches (start_with?, command_allowlist.rb).
Pure derivation — no I/O, no persistence. Returns a small immutable Rule.
kind == :pattern -> remember a dangerous-pattern CLASS (value = key)
kind == :prefix -> remember a command PREFIX (value = "git")
kind == :command -> remember one EXACT command (value = "git status")
Defined Under Namespace
Classes: Rule
Constant Summary collapse
- WRAPPERS =
Wrapper commands whose first sub-token is part of the meaningful prefix (“bundle exec” / “npm run”), not the argument. Without this a naive “first token” prefix would collapse ‘bundle exec rspec` and `bundle install` into the same `bundle` rule.
{ "bundle" => %w[exec].freeze, "npm" => %w[run].freeze, "yarn" => %w[run].freeze, "pnpm" => %w[run].freeze, "rake" => [].freeze, "cargo" => %w[run].freeze }.freeze
Class Method Summary collapse
-
.command_prefix(command) ⇒ Object
Leading safe-token run of a plain command: “git status” -> “git” “bundle exec rspec” -> “bundle exec” “npm run test –watch” -> “npm run” A plain command keeps only its head; a wrapper command (bundle/npm/…) additionally keeps its declared verb (exec/run) so distinct wrapped tools don’t collapse into one rule.
- .command_rule(tool:, command:) ⇒ Object
-
.narrow_rule_for(tool:, command:, pattern_key: nil) ⇒ Object
The NARROW rule used by :session / :always_command for S3 so behavior stays stable: a dangerous command remembers its pattern class (matching the reference), everything else remembers the exact command.
-
.plain_word?(token) ⇒ Boolean
A “plain word” is a bare token: not a flag, no path/assignment/glob punctuation — the shape that can safely extend a prefix.
-
.rule_for(tool:, command:, pattern_key: nil) ⇒ Object
Builds the rule a (tool, command) approval should be remembered as.
Class Method Details
.command_prefix(command) ⇒ Object
Leading safe-token run of a plain command:
"git status" -> "git"
"bundle exec rspec" -> "bundle exec"
"npm run test --watch" -> "npm run"
A plain command keeps only its head; a wrapper command (bundle/npm/…) additionally keeps its declared verb (exec/run) so distinct wrapped tools don’t collapse into one rule. The run stops at the first flag or argument-shaped token, mirroring CommandAllowlist’s start_with? match.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/rubino/security/prefix_deriver.rb', line 102 def command_prefix(command) tokens = command.to_s.strip.split(/\s+/) return "" if tokens.empty? head = tokens.first prefix = [head] if WRAPPERS.key?(head) verb = tokens[1] prefix << verb if verb && plain_word?(verb) && WRAPPERS[head].include?(verb) end prefix.join(" ") end |
.command_rule(tool:, command:) ⇒ Object
88 89 90 91 92 |
# File 'lib/rubino/security/prefix_deriver.rb', line 88 def command_rule(tool:, command:) value = command.to_s.strip value = tool.to_s if value.empty? Rule.new(kind: :command, value: value) end |
.narrow_rule_for(tool:, command:, pattern_key: nil) ⇒ Object
The NARROW rule used by :session / :always_command for S3 so behavior stays stable: a dangerous command remembers its pattern class (matching the reference), everything else remembers the exact command. The broad :prefix rule is derivable via rule_for but only wired into a decision in S5.
80 81 82 83 84 85 86 |
# File 'lib/rubino/security/prefix_deriver.rb', line 80 def narrow_rule_for(tool:, command:, pattern_key: nil) cmd = command.to_s key = pattern_key || DangerousPatterns.detect(cmd)[1] return Rule.new(kind: :pattern, value: key) if key command_rule(tool: tool, command: cmd) end |
.plain_word?(token) ⇒ Boolean
A “plain word” is a bare token: not a flag, no path/assignment/glob punctuation — the shape that can safely extend a prefix.
119 120 121 |
# File 'lib/rubino/security/prefix_deriver.rb', line 119 def plain_word?(token) token.match?(/\A[A-Za-z0-9_:.-]+\z/) && !token.start_with?("-") end |
.rule_for(tool:, command:, pattern_key: nil) ⇒ Object
Builds the rule a (tool, command) approval should be remembered as.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/rubino/security/prefix_deriver.rb', line 56 def rule_for(tool:, command:, pattern_key: nil) cmd = command.to_s key = pattern_key || DangerousPatterns.detect(cmd)[1] return Rule.new(kind: :pattern, value: key) if key # The :prefix rule ("allow `<head>` commands") only makes sense for the # shell tool, where sibling commands genuinely share a leading # executable (git status / git diff). For structured-arg tools the # "command" is a file path (write/edit/read) or a code/arg fragment # (ruby), so a derived prefix is nonsense — "allow `output.txt` # commands", "allow `6` commands". Remember those by exact command # instead, so the CLI/web offer no bogus prefix choice. (B6) return command_rule(tool: tool, command: cmd) unless tool.to_s == "shell" prefix = command_prefix(cmd) return command_rule(tool: tool, command: cmd) if prefix.empty? Rule.new(kind: :prefix, value: prefix) end |