use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};
#[must_use]
pub fn create_pack() -> Pack {
Pack {
id: "infrastructure.ansible".to_string(),
name: "Ansible",
description: "Protects against destructive Ansible operations like dangerous shell \
commands and unchecked playbook runs",
keywords: &["ansible", "playbook"],
safe_patterns: create_safe_patterns(),
destructive_patterns: create_destructive_patterns(),
keyword_matcher: None,
safe_regex_set: None,
safe_regex_set_is_complete: false,
}
}
fn create_safe_patterns() -> Vec<SafePattern> {
vec![
safe_pattern!(
"ansible-check",
r"ansible(?:-playbook)?\b[^\n;&|]*--check(?:\s|$)[^\n;&|]*$"
),
safe_pattern!(
"ansible-list-hosts",
r"ansible(?:-playbook)?\b[^\n;&|]*--list-hosts(?:\s|$)[^\n;&|]*$"
),
safe_pattern!(
"ansible-list-tasks",
r"ansible(?:-playbook)?\b[^\n;&|]*--list-tasks(?:\s|$)[^\n;&|]*$"
),
safe_pattern!(
"ansible-syntax",
r"ansible(?:-playbook)?\b[^\n;&|]*--syntax-check(?:\s|$)[^\n;&|]*$"
),
safe_pattern!("ansible-inventory", r"ansible-inventory\b[^\n;&|]*$"),
safe_pattern!("ansible-doc", r"ansible-doc\b[^\n;&|]*$"),
safe_pattern!("ansible-config", r"ansible-config\b[^\n;&|]*$"),
]
}
fn create_destructive_patterns() -> Vec<DestructivePattern> {
vec![
destructive_pattern!(
"shell-rm-rf",
r"ansible\s+.*-m\s+(?:shell|command)\s+.*rm\s+-rf",
"Ansible shell/command with 'rm -rf' is destructive. Review carefully.",
Critical,
"Running 'rm -rf' via Ansible shell or command module executes destructive deletion \
across all targeted hosts simultaneously. This multiplies the impact compared to \
running it locally:\n\n\
- Files deleted on every host in inventory or pattern\n\
- No confirmation or dry-run by default\n\
- Parallel execution means rapid, widespread destruction\n\
- Cannot be undone without backups on each host\n\n\
Safer alternatives:\n\
- Use file module with state=absent for managed deletion\n\
- Add --check flag to preview which hosts would be affected\n\
- Use --limit to restrict to specific hosts first\n\
- Verify target path with debug module before deletion"
),
destructive_pattern!(
"shell-reboot",
r"ansible\s+.*-m\s+(?:shell|command)\s+.*(?:reboot|shutdown|poweroff)",
"Ansible shell/command with reboot/shutdown affects system availability.",
High,
"Rebooting or shutting down systems via Ansible affects all targeted hosts at once. \
This can cause service outages across your infrastructure:\n\n\
- All services on affected hosts become unavailable\n\
- No graceful draining of connections or traffic\n\
- Database transactions may be interrupted\n\
- Load balancer health checks will fail\n\n\
Safer alternatives:\n\
- Use the reboot module which handles wait_for_connection\n\
- Add serial: 1 to playbook for rolling reboots\n\
- Use --limit to test on single host first\n\
- Coordinate with maintenance windows"
),
destructive_pattern!(
"playbook-all-hosts",
r"ansible-playbook\s+(?!.*(?:--check(?:\s|$)|--limit)).*-i\s+\S+\s+\S+\.ya?ml",
"ansible-playbook without --check or --limit may affect all hosts. Use --check first.",
High,
"Running ansible-playbook without --check or --limit applies changes to all hosts in \
the inventory. This is risky for destructive playbooks:\n\n\
- Every host matching the play's pattern is affected\n\
- Mistakes propagate to entire infrastructure at once\n\
- No opportunity to verify changes before they're applied\n\
- Rollback may be difficult or impossible\n\n\
Safer alternatives:\n\
- Add --check --diff to preview changes\n\
- Use --limit host1,host2 to test on subset\n\
- Add serial: 1 to playbook for rolling updates\n\
- Review the playbook tasks before running"
),
destructive_pattern!(
"extra-vars-delete",
r#"ansible\s+.*-e\s+['\"].*(?:delete|remove|destroy|drop)"#,
"Ansible extra-vars contains potentially destructive keywords. Review carefully.",
Medium,
"Extra variables containing destructive keywords like 'delete', 'remove', 'destroy', \
or 'drop' may trigger conditional destructive actions in playbooks:\n\n\
- Variables may control whether resources are deleted\n\
- Typos in variable values could trigger unintended paths\n\
- Variable precedence may override safer defaults\n\n\
Safer alternatives:\n\
- Review playbook to understand how variables are used\n\
- Add --check to see what tasks would run\n\
- Use --limit to test on single host first\n\
- Consider using vault-encrypted vars for destructive flags"
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packs::test_helpers::*;
#[test]
fn ansible_safe_dry_run_modes() {
let pack = create_pack();
assert_allows(&pack, "ansible --check -i inventory.ini all -m ping");
assert_allows(&pack, "ansible-playbook --check deploy.yml");
assert_allows(&pack, "ansible-playbook --check --diff site.yml");
assert_allows(&pack, "ansible-playbook --list-hosts site.yml");
assert_allows(&pack, "ansible-playbook --list-tasks site.yml");
assert_allows(&pack, "ansible-playbook --syntax-check site.yml");
}
#[test]
fn ansible_safe_info_commands() {
let pack = create_pack();
assert_allows(&pack, "ansible-inventory --list");
assert_allows(&pack, "ansible-doc file");
assert_allows(&pack, "ansible-config dump");
}
#[test]
fn ansible_blocks_shell_rm_rf() {
let pack = create_pack();
assert_blocks(
&pack,
"ansible all -m shell -a 'rm -rf /var/data'",
"rm -rf",
);
assert_blocks(
&pack,
"ansible webservers -m command -a 'rm -rf /tmp/cache'",
"rm -rf",
);
}
#[test]
fn ansible_blocks_shell_reboot_shutdown() {
let pack = create_pack();
assert_blocks(&pack, "ansible all -m shell -a 'reboot'", "reboot");
assert_blocks(
&pack,
"ansible dbservers -m command -a 'shutdown -h now'",
"shutdown",
);
assert_blocks(
&pack,
"ansible all -m shell -a 'poweroff'",
"reboot/shutdown",
);
}
#[test]
fn ansible_blocks_playbook_without_check_or_limit() {
let pack = create_pack();
assert_blocks(
&pack,
"ansible-playbook -i production deploy.yml",
"without --check or --limit",
);
}
#[test]
fn ansible_allows_playbook_with_check_or_limit() {
let pack = create_pack();
assert_allows(&pack, "ansible-playbook --check -i production deploy.yml");
assert_allows(
&pack,
"ansible-playbook --limit web1 -i production deploy.yml",
);
}
#[test]
fn ansible_diff_alone_does_not_suppress_destructive_patterns() {
let pack = create_pack();
assert_no_safe_match(&pack, "ansible-playbook --diff -i production deploy.yml");
assert_blocks_with_pattern(
&pack,
"ansible-playbook --diff -i production deploy.yml",
"playbook-all-hosts",
);
assert_no_safe_match(&pack, "ansible all --diff -m shell -a 'reboot'");
assert_blocks_with_pattern(
&pack,
"ansible all --diff -m shell -a 'reboot'",
"shell-reboot",
);
assert_no_safe_match(&pack, r"ansible all --diff -e 'action=delete' -m debug");
assert_blocks_with_pattern(
&pack,
r"ansible all --diff -e 'action=delete' -m debug",
"extra-vars-delete",
);
}
#[test]
fn ansible_safe_modes_do_not_allow_compound_destructive_commands() {
let pack = create_pack();
let checked_then_destructive =
"ansible-playbook --check site.yml; ansible all -m shell -a 'rm -rf /data'";
assert_no_safe_match(&pack, checked_then_destructive);
assert_blocks_with_pattern(&pack, checked_then_destructive, "shell-rm-rf");
let inventory_then_destructive =
"ansible-inventory --list && ansible all -m shell -a 'reboot'";
assert_no_safe_match(&pack, inventory_then_destructive);
assert_blocks_with_pattern(&pack, inventory_then_destructive, "shell-reboot");
}
#[test]
fn ansible_blocks_extra_vars_destructive_keywords() {
let pack = create_pack();
assert_blocks(
&pack,
r"ansible all -e 'action=delete' -m shell -a 'echo hi'",
"destructive keywords",
);
assert_blocks(
&pack,
r#"ansible all -e "state=destroy" -m debug"#,
"destructive keywords",
);
}
#[test]
fn ansible_allows_benign_extra_vars() {
let pack = create_pack();
assert_allows(
&pack,
"ansible all -e 'version=1.2.3' -m debug -a 'var=version'",
);
}
#[test]
fn ansible_safe_check_overrides_destructive() {
let pack = create_pack();
assert_allows(&pack, "ansible all --check -m shell -a 'rm -rf /data'");
}
}