Module: Rubino::Security::HardlineGuard
- Defined in:
- lib/rubino/security/hardline_guard.rb
Overview
Hardline (unconditional) blocklist — a floor BELOW yolo.
Commands so catastrophic they must NEVER run via the agent, regardless of –yolo, skip-approvals mode, a permissions:allow rule, or a command_allowlist entry. Opting into yolo is the user trusting the agent to move fast on their files and services — NOT trusting it to wipe the disk or power the box off.
The list is deliberately TINY: only things with no recovery path —filesystem destruction rooted at / (or ~), raw block-device overwrites, filesystem format, kernel shutdown/reboot, and fork-bomb / kill-all DoS. Recoverable-but-costly operations (git reset –hard, rm -rf /tmp/x, chmod -R 777, curl|sh) DO NOT belong here — they stay in the dangerous- pattern layer where yolo/approval can pass them through. Adding anything recoverable here is a false-positive that blocks legitimate work.
Mirrors the reference approval module: HARDLINE_PATTERNS, detect_hardline_command, the sudo-stdin guard, and the “tiny, no recovery path” guidance.
Constant Summary collapse
- CMDPOS =
Start-of-command anchor: matches positions where a shell begins parsing a new command (start of string, after a separator, after a subshell opener), optionally consuming leading wrappers (sudo, env VAR=VAL, exec/nohup/setsid/time) so we don’t false-positive on “echo reboot” or “grep shutdown log”. Mirrors approval.py:_CMDPOS.
/(?:^|[;&|\n`]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?(?:env\s+(?:\w+=\S*\s+)*)?(?:(?:exec|nohup|setsid|time)\s+)*\s*/.source.freeze
- HARDLINE_PATTERNS =
[regex, human description]. Matched against the lowercased, whitespace- normalized command. KEEP TINY — unrecoverable only.
[ # rm -r/-rf targeting the root filesystem (/ or /*) [%r{\brm\s+(?:-\S*\s+)*(?:/|/\*)(?:\s|$)}, "recursive delete of root filesystem"], # rm -r/-rf targeting a protected system directory [%r{\brm\s+(?:-\S*\s+)*(?:/home|/root|/etc|/usr|/var|/bin|/sbin|/boot|/lib)(?:/\*)?(?:\s|$)}, "recursive delete of system directory"], # rm targeting the home directory (~ or $HOME) [%r{\brm\s+(?:-\S*\s+)*(?:~|\$home)(?:/?|/\*)?(?:\s|$)}, "recursive delete of home directory"], # Filesystem format [/\bmkfs(?:\.[a-z0-9]+)?\b/, "format filesystem (mkfs)"], # dd to a raw block device [%r{\bdd\b[^\n]*\bof=/dev/(?:sd|nvme|hd|mmcblk|vd|xvd|disk|loop)[a-z0-9]*}, "dd to raw block device"], # Redirect to a raw block device (echo x > /dev/sda) [%r{>\s*/dev/(?:sd|nvme|hd|mmcblk|vd|xvd|disk|loop)[a-z0-9]*\b}, "redirect to raw block device"], # chmod/chown -R on the root filesystem [%r{\b(?:chmod|chown)\s+(?:-\S*\s+)*-\S*r\S*\s+\S+\s+/(?:\s|$)}, "recursive chmod/chown of root filesystem"], # Fork bomb (classic shell form, whitespace-tolerant) [/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, "fork bomb"], # Kill every process on the system [/\bkill\s+(?:-\S+\s+)*-1\b/, "kill all processes"], # System shutdown / reboot / halt / poweroff (anchored to cmd position) [/#{CMDPOS}(?:shutdown|reboot|halt|poweroff)\b/, "system shutdown/reboot"], [/#{CMDPOS}init\s+[06]\b/, "init 0/6 (shutdown/reboot)"], [/#{CMDPOS}systemctl\s+(?:poweroff|reboot|halt|kexec)\b/, "systemctl poweroff/reboot"], [/#{CMDPOS}telinit\s+[06]\b/, "telinit 0/6 (shutdown/reboot)"] ].freeze
- SUDO_STDIN_RE =
sudo -S without a configured SUDO_PASSWORD is the model piping a guessed password via stdin — a brute-force vector. Unconditional block. Mirrors approval.py:_check_sudo_stdin_guard (:255).
/(?:^|[;&|`\n]|&&|\|\||\$\()\s*sudo\s+-s\b/
Class Method Summary collapse
-
.block_reason(command) ⇒ Object
Convenience predicate for the post-approval defense-in-depth check in ShellTool.
-
.detect(command) ⇒ Object
Returns [true, description] when the command hits the hardline floor (a HARDLINE_PATTERN or the sudo-stdin guard), else [false, nil].
-
.normalize(command) ⇒ Object
Minimal normalization: collapse runs of spaces/tabs (newlines kept so the command-separator anchors still fire), trim, and lowercase so trivial obfuscation (extra spaces, case) doesn’t slip through.
-
.sudo_stdin?(normalized) ⇒ Boolean
sudo -S only fires the guard when no SUDO_PASSWORD is configured — with one set, an internal transform legitimately injects -S elsewhere.
Class Method Details
.block_reason(command) ⇒ Object
Convenience predicate for the post-approval defense-in-depth check in ShellTool. Returns the description, or nil when the command is clear.
82 83 84 85 |
# File 'lib/rubino/security/hardline_guard.rb', line 82 def block_reason(command) blocked, description = detect(command) blocked ? description : nil end |
.detect(command) ⇒ Object
Returns [true, description] when the command hits the hardline floor (a HARDLINE_PATTERN or the sudo-stdin guard), else [false, nil].
70 71 72 73 74 75 76 77 78 |
# File 'lib/rubino/security/hardline_guard.rb', line 70 def detect(command) normalized = normalize(command) HARDLINE_PATTERNS.each do |regex, description| return [true, description] if normalized.match?(regex) end return [true, "sudo password guessing via stdin (sudo -S)"] if sudo_stdin?(normalized) [false, nil] end |
.normalize(command) ⇒ Object
Minimal normalization: collapse runs of spaces/tabs (newlines kept so the command-separator anchors still fire), trim, and lowercase so trivial obfuscation (extra spaces, case) doesn’t slip through. Deliberately NOT a full ANSI/Unicode normalizer — over-engineering for the hardline floor.
100 101 102 |
# File 'lib/rubino/security/hardline_guard.rb', line 100 def normalize(command) command.to_s.gsub(/[ \t]+/, " ").strip.downcase end |
.sudo_stdin?(normalized) ⇒ Boolean
sudo -S only fires the guard when no SUDO_PASSWORD is configured —with one set, an internal transform legitimately injects -S elsewhere.
89 90 91 92 93 |
# File 'lib/rubino/security/hardline_guard.rb', line 89 def sudo_stdin?(normalized) return false if ENV.key?("SUDO_PASSWORD") normalized.match?(SUDO_STDIN_RE) end |