Skip to main content

ailint_core/rules/security/
tool_confirmation.rs

1//! AIL203 `tool-confirmation-required` — Ensure critical/destructive rules include "Wait for human confirmation" phrasing.
2//!
3//! See: `docs/rules/security/AIL203.md`
4
5use crate::parser::{DocumentContent, ParsedDocument};
6use crate::rules::security::{line_of_offset, AIL203};
7use crate::rules::{Rule, RuleContext, RuleId, Severity, Violation};
8
9/// AIL203 tool-confirmation-required: destructive actions need a confirmation step.
10#[derive(Debug, Default)]
11pub struct ToolConfirmationRequiredRule;
12
13impl Rule for ToolConfirmationRequiredRule {
14    fn id(&self) -> RuleId {
15        AIL203
16    }
17
18    fn default_severity(&self) -> Severity {
19        Severity::Error
20    }
21
22    fn description(&self) -> &'static str {
23        "File describes destructive actions but has no confirmation constraint."
24    }
25
26    fn fix_hint(&self) -> &'static str {
27        "Require explicit confirmation (\"wait for human confirmation\") before destructive actions."
28    }
29
30    fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
31        match &doc.content {
32            DocumentContent::Markdown(_) | DocumentContent::Text => {}
33            _ => return Vec::new(),
34        };
35
36        let destructive_phrases = [
37            "delete data",
38            "rm -rf",
39            "drop table",
40            "truncate table",
41            "irreversible",
42            "destructive action",
43        ];
44        let confirmation_phrases = [
45            "ask",
46            "confirm",
47            "human",
48            "wait",
49            "permission",
50            "consent",
51            "approval",
52        ];
53
54        let lower_raw = doc.raw.to_lowercase();
55
56        let mut violations = Vec::new();
57        for d in &destructive_phrases {
58            if let Some(idx) = lower_raw.find(d) {
59                if !confirmation_phrases.iter().any(|c| lower_raw.contains(c)) {
60                    let line = line_of_offset(&doc.raw, idx);
61                    let v = Violation::new(
62                        AIL203,
63                        ctx.severity,
64                        doc.path.clone(),
65                        "destructive action without confirmation constraint",
66                    )
67                    .at(line, 1)
68                    .with_detail((*d).to_string());
69                    violations.push(v);
70
71                    break;
72                }
73            }
74        }
75
76        violations
77    }
78}