Skip to main content

rippy_cli/
risk.rs

1//! Risk classification for command groups.
2
3use serde::Serialize;
4
5use crate::allowlists;
6
7const CRITICAL_COMMANDS: &[&str] = &[
8    "sudo",
9    "su",
10    "doas",
11    "eval",
12    "exec",
13    "source",
14    "chmod",
15    "chown",
16    "chgrp",
17    "mkfs",
18    "dd",
19    "fdisk",
20    "iptables",
21    "systemctl",
22    "service",
23];
24
25const HIGH_COMMANDS: &[&str] = &[
26    "rm", "rmdir", "mv", "cp", "install", "docker", "podman", "kubectl", "pip", "pip3", "npm",
27    "yarn", "pnpm", "gem", "curl", "wget", "ssh", "scp", "rsync", "kill", "killall", "pkill",
28    "mount", "umount",
29];
30
31/// Read-only subcommands of tools that are otherwise medium/high risk.
32const SAFE_SUBCOMMANDS: &[&str] = &[
33    "git status",
34    "git log",
35    "git diff",
36    "git show",
37    "git branch",
38    "git remote",
39    "git stash list",
40    "git tag",
41    "docker ps",
42    "docker images",
43    "docker inspect",
44    "cargo check",
45    "cargo test",
46    "cargo clippy",
47    "cargo fmt",
48    "cargo build",
49    "cargo doc",
50    "npm test",
51    "npm run",
52    "kubectl get",
53    "kubectl describe",
54];
55
56/// Risk level for a suggested rule.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
58#[serde(rename_all = "lowercase")]
59pub enum RiskLevel {
60    Low,
61    Medium,
62    High,
63    Critical,
64}
65
66impl RiskLevel {
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Self::Low => "low",
70            Self::Medium => "medium",
71            Self::High => "high",
72            Self::Critical => "critical",
73        }
74    }
75}
76
77/// Classify a command group key into a risk level.
78#[must_use]
79pub fn classify(group_key: &str) -> RiskLevel {
80    // Check safe subcommands first (e.g. "docker ps" is low even though "docker" is high).
81    if SAFE_SUBCOMMANDS.contains(&group_key) {
82        return RiskLevel::Low;
83    }
84
85    let cmd = group_key.split_whitespace().next().unwrap_or("");
86
87    if CRITICAL_COMMANDS.contains(&cmd) {
88        return RiskLevel::Critical;
89    }
90    if HIGH_COMMANDS.contains(&cmd) {
91        return RiskLevel::High;
92    }
93    if allowlists::is_simple_safe(cmd) {
94        return RiskLevel::Low;
95    }
96    RiskLevel::Medium
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn critical_commands() {
105        assert_eq!(classify("sudo"), RiskLevel::Critical);
106        assert_eq!(classify("eval"), RiskLevel::Critical);
107        assert_eq!(classify("chmod"), RiskLevel::Critical);
108    }
109
110    #[test]
111    fn high_commands() {
112        assert_eq!(classify("rm"), RiskLevel::High);
113        assert_eq!(classify("docker"), RiskLevel::High);
114        assert_eq!(classify("curl"), RiskLevel::High);
115        assert_eq!(classify("npm"), RiskLevel::High);
116    }
117
118    #[test]
119    fn low_simple_safe() {
120        assert_eq!(classify("ls"), RiskLevel::Low);
121        assert_eq!(classify("cat"), RiskLevel::Low);
122        assert_eq!(classify("grep"), RiskLevel::Low);
123    }
124
125    #[test]
126    fn low_safe_subcommands() {
127        assert_eq!(classify("git status"), RiskLevel::Low);
128        assert_eq!(classify("git log"), RiskLevel::Low);
129        assert_eq!(classify("cargo test"), RiskLevel::Low);
130        assert_eq!(classify("docker ps"), RiskLevel::Low);
131    }
132
133    #[test]
134    fn medium_default() {
135        assert_eq!(classify("make"), RiskLevel::Medium);
136        assert_eq!(classify("git push"), RiskLevel::Medium);
137        assert_eq!(classify("unknown-tool"), RiskLevel::Medium);
138    }
139
140    #[test]
141    fn ordering_low_to_critical() {
142        assert!(RiskLevel::Low < RiskLevel::Medium);
143        assert!(RiskLevel::Medium < RiskLevel::High);
144        assert!(RiskLevel::High < RiskLevel::Critical);
145    }
146}