use crate::rules::types::{Category, Confidence, Rule, Severity};
use regex::Regex;
pub fn rules() -> Vec<Rule> {
vec![
ps_001(),
ps_003(),
ps_004(),
ps_005(),
ps_006(),
ps_007(),
ps_008(),
ps_009(),
]
}
fn ps_001() -> Rule {
Rule {
id: "PS-001",
name: "Crontab manipulation",
description: "Detects crontab commands which could be used to establish persistence",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r"\bcrontab\s+").expect("PS-001: invalid regex"),
Regex::new(r"/etc/cron").expect("PS-001: invalid regex"),
Regex::new(r"/var/spool/cron").expect("PS-001: invalid regex"),
],
exclusions: vec![
Regex::new(r"crontab\s+-l\b").expect("PS-001: invalid regex"), ],
message: "Persistence mechanism: crontab manipulation detected",
recommendation: "Skills should not modify scheduled tasks. Review the necessity of cron access",
fix_hint: Some(
"Remove crontab commands. Use explicit user interaction for scheduled tasks",
),
cwe_ids: &["CWE-912"],
}
}
fn ps_003() -> Rule {
Rule {
id: "PS-003",
name: "Shell profile modification",
description: "Detects modifications to shell profiles (.bashrc, .zshrc, etc.) for persistence",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r">>\s*.*\.(bashrc|bash_profile|zshrc|profile|zprofile)")
.expect("PS-003: invalid regex"),
Regex::new(r">\s*.*\.(bashrc|bash_profile|zshrc|profile|zprofile)")
.expect("PS-003: invalid regex"),
Regex::new(r"echo\s+.*\.(bashrc|bash_profile|zshrc|profile|zprofile)")
.expect("PS-003: invalid regex"),
Regex::new(r"tee\s+.*\.(bashrc|bash_profile|zshrc|profile|zprofile)")
.expect("PS-003: invalid regex"),
],
exclusions: vec![],
message: "Persistence mechanism: shell profile modification detected",
recommendation: "Skills should not modify shell startup files",
fix_hint: Some(
"Remove shell profile modifications. Document required env vars for manual setup",
),
cwe_ids: &["CWE-912"],
}
}
fn ps_004() -> Rule {
Rule {
id: "PS-004",
name: "System service registration",
description: "Detects registration of system services (systemd, launchd) for persistence",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Tentative,
patterns: vec![
Regex::new(r"systemctl\s+(enable|start|daemon-reload)").expect("PS-004: invalid regex"),
Regex::new(r"/etc/systemd/system/").expect("PS-004: invalid regex"),
Regex::new(r"\.service\b").expect("PS-004: invalid regex"),
Regex::new(r"launchctl\s+(load|bootstrap|enable)").expect("PS-004: invalid regex"),
Regex::new(r"~/Library/LaunchAgents/").expect("PS-004: invalid regex"),
Regex::new(r"/Library/Launch(Agents|Daemons)/").expect("PS-004: invalid regex"),
Regex::new(r"\.plist\b").expect("PS-004: invalid regex"),
],
exclusions: vec![
Regex::new(r"systemctl\s+status").expect("PS-004: invalid regex"),
Regex::new(r"launchctl\s+list").expect("PS-004: invalid regex"),
],
message: "Persistence mechanism: system service registration detected",
recommendation: "Skills should not register system services without explicit approval",
fix_hint: Some(
"Remove service registration. Provide manual installation instructions instead",
),
cwe_ids: &["CWE-912"],
}
}
fn ps_005() -> Rule {
Rule {
id: "PS-005",
name: "SSH authorized_keys modification",
description: "Detects modifications to authorized_keys which could grant persistent SSH access",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Certain,
patterns: vec![
Regex::new(r">>\s*.*authorized_keys").expect("PS-005: invalid regex"),
Regex::new(r">\s*.*authorized_keys").expect("PS-005: invalid regex"),
Regex::new(r"echo\s+.*authorized_keys").expect("PS-005: invalid regex"),
Regex::new(r"tee\s+.*authorized_keys").expect("PS-005: invalid regex"),
Regex::new(r"cat\s+.*>\s*.*authorized_keys").expect("PS-005: invalid regex"),
],
exclusions: vec![],
message: "Persistence mechanism: authorized_keys modification detected",
recommendation: "Skills should never modify SSH authorized_keys",
fix_hint: Some("Remove authorized_keys modification. Never automate SSH key management"),
cwe_ids: &["CWE-912", "CWE-522"],
}
}
fn ps_006() -> Rule {
Rule {
id: "PS-006",
name: "Delayed/background execution",
description: "Detects commands that schedule delayed or background execution to evade detection",
severity: Severity::High,
category: Category::Persistence,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r"\bat\s+(now|midnight|noon|\d)").expect("PS-006: invalid regex"),
Regex::new(r"\|\s*at\s+(now|midnight|\d)").expect("PS-006: invalid regex"),
Regex::new(r"\bbatch\b").expect("PS-006: invalid regex"),
Regex::new(r"screen\s+-[dDmS]+.*(-c|bash|sh|curl|wget|nc)")
.expect("PS-006: invalid regex"),
Regex::new(r"tmux\s+(new-session|new)\s+-d").expect("PS-006: invalid regex"),
Regex::new(r"nohup\s+.*\b(curl|wget|nc|netcat|bash|sh)\b")
.expect("PS-006: invalid regex"),
Regex::new(r"nohup\s+.*\b(python3?|node|ruby|perl)\b").expect("PS-006: invalid regex"),
Regex::new(r"&\s*;\s*disown").expect("PS-006: invalid regex"),
Regex::new(r"disown\s+-h").expect("PS-006: invalid regex"),
Regex::new(r"setsid\s+.*\b(curl|wget|nc|bash|sh)\b").expect("PS-006: invalid regex"),
Regex::new(r"systemd-run\s+(--scope|--user).*\b(curl|wget|bash|sh|python|node)\b")
.expect("PS-006: invalid regex"),
Regex::new(r"timeout\s+.*&\s*$").expect("PS-006: invalid regex"),
Regex::new(r"sleep\s+\d+\s*;\s*(curl|wget|bash|sh|nc)").expect("PS-006: invalid regex"),
Regex::new(r"sleep\s+\d+\s*&&\s*(curl|wget|bash|sh|nc)")
.expect("PS-006: invalid regex"),
Regex::new(r":\(\)\s*\{\s*:\|\:&\s*\}").expect("PS-006: invalid regex"),
Regex::new(r"\bstart-stop-daemon\s+--start").expect("PS-006: invalid regex"),
Regex::new(r"&>\s*/dev/null\s*&").expect("PS-006: invalid regex"),
Regex::new(r">\s*/dev/null\s+2>&1\s*&").expect("PS-006: invalid regex"),
],
exclusions: vec![
Regex::new(r"^\s*#").expect("PS-006: invalid regex"),
Regex::new(r"screen\s+-r").expect("PS-006: invalid regex"),
Regex::new(r"tmux\s+attach").expect("PS-006: invalid regex"),
],
message: "Delayed or background execution detected. This can be used to evade detection or establish persistence.",
recommendation: "Avoid scheduling background tasks. Use explicit, foreground execution that users can observe.",
fix_hint: Some("Remove delayed execution. Execute commands directly in the foreground."),
cwe_ids: &["CWE-912"],
}
}
fn ps_007() -> Rule {
Rule {
id: "PS-007",
name: "Init system manipulation",
description: "Detects manipulation of init systems and startup scripts for persistence",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r">\s*/etc/rc\.local").expect("PS-007: invalid regex"),
Regex::new(r">>\s*/etc/rc\.local").expect("PS-007: invalid regex"),
Regex::new(r">\s*/etc/init\.d/").expect("PS-007: invalid regex"),
Regex::new(r"update-rc\.d").expect("PS-007: invalid regex"),
Regex::new(r"chkconfig\s+--add").expect("PS-007: invalid regex"),
Regex::new(r"\.config/autostart/.*\.desktop").expect("PS-007: invalid regex"),
Regex::new(r"/etc/xdg/autostart/").expect("PS-007: invalid regex"),
Regex::new(r"HKEY.*\\Run").expect("PS-007: invalid regex"),
Regex::new(r"defaults\s+write.*LoginHook").expect("PS-007: invalid regex"),
],
exclusions: vec![Regex::new(r"^\s*#").expect("PS-007: invalid regex")],
message: "Init system manipulation detected. This is commonly used to establish boot-time persistence.",
recommendation: "Skills should not modify system startup scripts or init configurations.",
fix_hint: Some("Remove init system modifications. Provide manual setup instructions."),
cwe_ids: &["CWE-912"],
}
}
fn ps_008() -> Rule {
Rule {
id: "PS-008",
name: "Systemd service creation",
description: "Detects creation of systemd service unit files for establishing persistence on Linux systems",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Certain,
patterns: vec![
Regex::new(r">\s*/etc/systemd/system/[^/]+\.service").expect("PS-008: invalid regex"),
Regex::new(r">>\s*/etc/systemd/system/").expect("PS-008: invalid regex"),
Regex::new(r"tee\s+.*\.service").expect("PS-008: invalid regex"),
Regex::new(r"~/.config/systemd/user/").expect("PS-008: invalid regex"),
Regex::new(r"\$HOME/.config/systemd/user/").expect("PS-008: invalid regex"),
Regex::new(r"\[Service\]").expect("PS-008: invalid regex"),
Regex::new(r"ExecStart\s*=").expect("PS-008: invalid regex"),
Regex::new(r"WantedBy\s*=\s*(multi-user|default)\.target")
.expect("PS-008: invalid regex"),
Regex::new(r">\s*/etc/systemd/system/[^/]+\.timer").expect("PS-008: invalid regex"),
Regex::new(r"\[Timer\]").expect("PS-008: invalid regex"),
Regex::new(r"OnBootSec\s*=|OnUnitActiveSec\s*=").expect("PS-008: invalid regex"),
],
exclusions: vec![
Regex::new(r"^\s*#").expect("PS-008: invalid regex"),
Regex::new(r"systemctl\s+(status|show|cat)").expect("PS-008: invalid regex"),
],
message: "Systemd service creation detected. This establishes persistent execution on system boot.",
recommendation: "Skills should not create systemd services. Provide manual installation instructions.",
fix_hint: Some(
"Remove systemd service creation. Document how users can manually install if needed.",
),
cwe_ids: &["CWE-912"],
}
}
fn ps_009() -> Rule {
Rule {
id: "PS-009",
name: "Launchd plist creation (macOS)",
description: "Detects creation of launchd plist files for establishing persistence on macOS",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Certain,
patterns: vec![
Regex::new(r">\s*~/Library/LaunchAgents/[^/]+\.plist").expect("PS-009: invalid regex"),
Regex::new(r">\s*\$HOME/Library/LaunchAgents/").expect("PS-009: invalid regex"),
Regex::new(r"tee\s+.*LaunchAgents.*\.plist").expect("PS-009: invalid regex"),
Regex::new(r">\s*/Library/LaunchDaemons/").expect("PS-009: invalid regex"),
Regex::new(r">\s*/Library/LaunchAgents/").expect("PS-009: invalid regex"),
Regex::new(r"<key>ProgramArguments</key>").expect("PS-009: invalid regex"),
Regex::new(r"<key>RunAtLoad</key>").expect("PS-009: invalid regex"),
Regex::new(r"<key>KeepAlive</key>").expect("PS-009: invalid regex"),
Regex::new(r"<key>StartInterval</key>").expect("PS-009: invalid regex"),
Regex::new(r"<key>StartCalendarInterval</key>").expect("PS-009: invalid regex"),
Regex::new(r"plutil\s+-insert.*LaunchAgents").expect("PS-009: invalid regex"),
Regex::new(r"defaults\s+write.*LaunchAgents").expect("PS-009: invalid regex"),
],
exclusions: vec![
Regex::new(r"^\s*#").expect("PS-009: invalid regex"),
Regex::new(r"launchctl\s+list").expect("PS-009: invalid regex"),
Regex::new(r"plutil\s+-lint").expect("PS-009: invalid regex"),
],
message: "Launchd plist creation detected. This establishes persistent execution on macOS.",
recommendation: "Skills should not create launchd plists. Provide manual installation instructions.",
fix_hint: Some(
"Remove launchd plist creation. Document how users can manually install if needed.",
),
cwe_ids: &["CWE-912"],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ps_001_detects_crontab() {
let rule = ps_001();
let test_cases = vec![
("crontab -e", true),
("crontab /tmp/cron.txt", true),
("echo '* * * * * /tmp/backdoor' >> /etc/cron.d/evil", true),
("crontab -l", false), ];
for (input, should_match) in test_cases {
let matched = rule.patterns.iter().any(|p| p.is_match(input));
let excluded = rule.exclusions.iter().any(|e| e.is_match(input));
let result = matched && !excluded;
assert_eq!(result, should_match, "Failed for input: {}", input);
}
}
#[test]
fn test_ps_003_detects_shell_profile_mod() {
let rule = ps_003();
let test_cases = vec![
("echo 'export PATH=...' >> ~/.bashrc", true),
("echo 'alias ll=ls' >> ~/.zshrc", true),
("tee -a ~/.bash_profile", true),
("cat ~/.bashrc", false), ("echo 'hello' > file.txt", false),
];
for (input, should_match) in test_cases {
let matched = rule.patterns.iter().any(|p| p.is_match(input));
assert_eq!(matched, should_match, "Failed for input: {}", input);
}
}
#[test]
fn test_ps_004_detects_service_registration() {
let rule = ps_004();
let test_cases = vec![
("systemctl enable myservice", true),
("systemctl start backdoor.service", true),
("cp evil.service /etc/systemd/system/", true),
("launchctl load ~/Library/LaunchAgents/com.evil.plist", true),
("cp agent.plist ~/Library/LaunchAgents/", true),
("systemctl status nginx", false), ("launchctl list", false), ];
for (input, should_match) in test_cases {
let matched = rule.patterns.iter().any(|p| p.is_match(input));
let excluded = rule.exclusions.iter().any(|e| e.is_match(input));
let result = matched && !excluded;
assert_eq!(result, should_match, "Failed for input: {}", input);
}
}
#[test]
fn test_ps_005_detects_authorized_keys_mod() {
let rule = ps_005();
let test_cases = vec![
("echo 'ssh-rsa AAAA...' >> ~/.ssh/authorized_keys", true),
("cat key.pub >> /home/user/.ssh/authorized_keys", true),
("tee -a ~/.ssh/authorized_keys", true),
("cat ~/.ssh/authorized_keys", false), ];
for (input, should_match) in test_cases {
let matched = rule.patterns.iter().any(|p| p.is_match(input));
assert_eq!(matched, should_match, "Failed for input: {}", input);
}
}
#[test]
fn snapshot_ps_001() {
let rule = ps_001();
let content = include_str!("../../../tests/fixtures/rules/ps_001.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_001", findings);
}
#[test]
fn snapshot_ps_003() {
let rule = ps_003();
let content = include_str!("../../../tests/fixtures/rules/ps_003.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_003", findings);
}
#[test]
fn snapshot_ps_004() {
let rule = ps_004();
let content = include_str!("../../../tests/fixtures/rules/ps_004.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_004", findings);
}
#[test]
fn snapshot_ps_005() {
let rule = ps_005();
let content = include_str!("../../../tests/fixtures/rules/ps_005.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_005", findings);
}
#[test]
fn snapshot_ps_006() {
let rule = ps_006();
let content = include_str!("../../../tests/fixtures/rules/ps_006.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_006", findings);
}
#[test]
fn snapshot_ps_007() {
let rule = ps_007();
let content = include_str!("../../../tests/fixtures/rules/ps_007.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_007", findings);
}
}