Module: Rubino::Security::DangerousPatterns
- Defined in:
- lib/rubino/security/dangerous_patterns.rb
Overview
Dangerous (recoverable-but-risky) command patterns — the layer ABOVE the hardline floor. These are operations that can lose work, rewrite shared history, escalate privilege, or touch system/credential files, but which a user might legitimately want to run with confirmation. Unlike HardlineGuard (catastrophic, no recovery path, never runs), a dangerous match is meant to drive an :ask — yolo/approval CAN pass it through.
This is deliberately DISTINCT from HardlineGuard: there is NO overlap. Hardline owns “rm -rf /”, “mkfs”, “dd to /dev/sd*”, shutdown/reboot, fork bomb, kill-all, sudo -S guessing. DangerousPatterns owns the recoverable cousins: recursive rm of NON-root paths, git force-push / reset –hard, curl|sh, broad chmod/chown, writes into /etc, sudo with privilege flags, find -delete, etc.
Mirrors the reference approval module: DANGEROUS_PATTERNS and detect_dangerous_command. A faithful CORE subset of the reference ~47 patterns — the important risk classes, not an exhaustive copy.
Constant Summary collapse
- SYSTEM_CONFIG_PATH =
Sensitive write targets (system config, block devices, ssh/credential files). Mirrors approval.py:_SENSITIVE_WRITE_TARGET (:152) in spirit, kept compact. /etc plus its macOS /private/etc mirror.
%r{(?:/etc/|/private/(?:etc|var|tmp)/)}.source.freeze
- SENSITIVE_WRITE_TARGET =
%r{(?:#{SYSTEM_CONFIG_PATH}|/dev/sd|(?:~|\$home)/\.ssh/|(?:~|\$home)/\.(?:netrc|pgpass|npmrc|pypirc)\b)}.source.freeze
- PATTERNS =
[regex, human description “pattern key”]. Matched against the lowercased, whitespace-normalized command. The description doubles as the persisted approval key in later slices (mirrors the reference pattern_key).
[ # --- Recursive / forced delete of NON-root paths (root is hardline). # -\S*r catches both -rf and the long --recursive form. --- [/\brm\s+-\S*r/, "recursive delete"], # --- Broad permission / ownership changes --- [/\bchmod\s+(?:-\S*\s+)*(?:777|666|o\+[rwx]*w|a\+[rwx]*w)\b/, "world/other-writable permissions"], [/\bchown\s+(?:-\S*)?r\s+root/, "recursive chown to root"], # --- Privilege escalation: sudo with non-interactive privilege flags --- # Plain `sudo cmd` is TTY-bound and excluded; these flags (stdin/ # askpass/shell/list) are the agent-reachable escalation forms. # (sudo -S WITHOUT a configured password is hardline; this is the # broader, recoverable privilege-flag class.) [/\bsudo\b[^;|&\n]*?\s+(?:--stdin\b|-a\b|--askpass\b|-s\b)/, "sudo with privilege flag (stdin/askpass/shell/list)"], # --- Pipe remote content to a shell (curl|sh, wget|bash) --- [%r{\b(?:curl|wget)\b.*\|\s*(?:[/\w]*/)?(?:ba)?sh(?:\s|$|-c)}, "pipe remote content to shell"], [/\b(?:bash|sh|zsh|ksh)\s+<\s*<?\s*\(\s*(?:curl|wget)\b/, "execute remote script via process substitution"], # --- Write / overwrite into system or credential files --- [/>>?\s*["']?#{SENSITIVE_WRITE_TARGET}/, "overwrite system file via redirection"], [/\btee\b.*["']?#{SENSITIVE_WRITE_TARGET}/, "overwrite system file via tee"], [/\b(?:cp|mv|install)\b.*\s#{SYSTEM_CONFIG_PATH}/, "copy/move file into system config path"], [/\bsed\s+-\S*i.*\s#{SYSTEM_CONFIG_PATH}/, "in-place edit of system config"], # --- Service control --- [/\bsystemctl\s+(?:-\S+\s+)*(?:stop|restart|disable|mask)\b/, "stop/restart system service"], # --- Force-kill process sweeps (kill-all -1 is hardline) --- [/\bpkill\s+-9\b/, "force kill processes"], [/\bkillall\s+(?:-\S*\s+)*-(?:9|kill|sigkill)\b/, "force kill processes (killall -KILL)"], [/\bkillall\s+(?:-\S*\s+)*-r\b/, "kill processes by regex (killall -r)"], # --- find that deletes --- [%r{\bfind\b.*-exec(?:dir)?\s+(?:/\S*/)?rm\b}, "find -exec/-execdir rm"], [/\bfind\b.*-delete\b/, "find -delete"], [/\bxargs\s+.*\brm\b/, "xargs with rm"], # --- Git destructive / history-rewriting operations --- [/\bgit\s+reset\s+--hard\b/, "git reset --hard (destroys uncommitted changes)"], [/\bgit\s+push\b.*--force\b/, "git force push (rewrites remote history)"], [/\bgit\s+push\b.*\s-f\b/, "git force push short flag (rewrites remote history)"], [/\bgit\s+clean\s+-\S*f/, "git clean with force (deletes untracked files)"], [/\bgit\s+branch\s+-d\b/, "git branch force delete"], # --- Filesystem format / raw disk copy (the recoverable framings; # mkfs and dd-to-/dev/sd* themselves are hardline) --- [/\bdd\s+.*if=/, "disk copy"], # --- Destructive SQL --- [/\bdrop\s+(?:table|database)\b/, "SQL DROP"], [/\bdelete\s+from\b(?![^\n]*\bwhere\b)/, "SQL DELETE without WHERE"], [/\btruncate\s+(?:table)?\s*\w/, "SQL TRUNCATE"] ].freeze
Class Method Summary collapse
-
.dangerous?(command) ⇒ Boolean
Convenience predicate: true when the command hits a dangerous pattern.
-
.detect(command) ⇒ Object
Returns [true, pattern_key, description] when the command matches a dangerous pattern, else [false, nil, nil].
-
.normalize(command) ⇒ Object
Same normalization idiom as HardlineGuard: collapse spaces/tabs (keep newlines so separator anchors fire), trim, lowercase.
Class Method Details
.dangerous?(command) ⇒ Boolean
Convenience predicate: true when the command hits a dangerous pattern.
106 107 108 |
# File 'lib/rubino/security/dangerous_patterns.rb', line 106 def dangerous?(command) detect(command).first end |
.detect(command) ⇒ Object
Returns [true, pattern_key, description] when the command matches a dangerous pattern, else [false, nil, nil]. The pattern_key and description are the same string (the human-readable key) — the tuple arity mirrors the reference detect_dangerous_command so later slices can persist the key.
97 98 99 100 101 102 103 |
# File 'lib/rubino/security/dangerous_patterns.rb', line 97 def detect(command) normalized = normalize(command) PATTERNS.each do |regex, description| return [true, description, description] if normalized.match?(regex) end [false, nil, nil] end |
.normalize(command) ⇒ Object
Same normalization idiom as HardlineGuard: collapse spaces/tabs (keep newlines so separator anchors fire), trim, lowercase. Trivial obfuscation (extra spaces, case) doesn’t slip through.
113 114 115 |
# File 'lib/rubino/security/dangerous_patterns.rb', line 113 def normalize(command) command.to_s.gsub(/[ \t]+/, " ").strip.downcase end |