ailint_core/rules/security/
tool_confirmation.rs1use serde::Deserialize;
6
7use crate::parser::{DocumentContent, ParsedDocument};
8use crate::rules::security::{line_of_offset, AIL203};
9use crate::rules::{dictionary_lines, Rule, RuleContext, RuleId, Severity, Violation};
10
11const DEFAULT_DESTRUCTIVE_PHRASES: &str = include_str!("destructive_phrases.txt");
12const DEFAULT_CONFIRMATION_PHRASES: &str = include_str!("confirmation_phrases.txt");
13
14#[derive(Debug, Default, Deserialize)]
15#[serde(default, deny_unknown_fields)]
16struct Options {
17 destructive_phrases: Option<Vec<String>>,
18 extra_destructive_phrases: Option<Vec<String>>,
19 confirmation_phrases: Option<Vec<String>>,
20 extra_confirmation_phrases: Option<Vec<String>>,
21}
22
23#[derive(Debug, Default)]
25pub struct ToolConfirmationRequiredRule;
26
27impl Rule for ToolConfirmationRequiredRule {
28 fn id(&self) -> RuleId {
29 AIL203
30 }
31
32 fn default_severity(&self) -> Severity {
33 Severity::Error
34 }
35
36 fn description(&self) -> &'static str {
37 "File describes destructive actions but has no confirmation constraint."
38 }
39
40 fn fix_hint(&self) -> &'static str {
41 "Require explicit confirmation (\"wait for human confirmation\") before destructive actions."
42 }
43
44 fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
45 match &doc.content {
46 DocumentContent::Markdown(_) | DocumentContent::Text => {}
47 _ => return Vec::new(),
48 };
49
50 let opts: Options = ctx
51 .options
52 .and_then(|v| serde_yaml::from_value(v.clone()).ok())
53 .unwrap_or_default();
54
55 let mut destructive_phrases: Vec<String> = match opts.destructive_phrases {
56 Some(p) => p,
57 None => dictionary_lines(DEFAULT_DESTRUCTIVE_PHRASES)
58 .into_iter()
59 .map(String::from)
60 .collect(),
61 };
62 if let Some(extra) = opts.extra_destructive_phrases {
63 destructive_phrases.extend(extra);
64 }
65 let mut confirmation_phrases: Vec<String> = match opts.confirmation_phrases {
66 Some(p) => p,
67 None => dictionary_lines(DEFAULT_CONFIRMATION_PHRASES)
68 .into_iter()
69 .map(String::from)
70 .collect(),
71 };
72 if let Some(extra) = opts.extra_confirmation_phrases {
73 confirmation_phrases.extend(extra);
74 }
75 for p in &mut destructive_phrases {
76 *p = p.to_lowercase();
77 }
78 for p in &mut confirmation_phrases {
79 *p = p.to_lowercase();
80 }
81
82 let lower_raw = doc.raw.to_lowercase();
83
84 let mut violations = Vec::new();
85 for d in &destructive_phrases {
86 if let Some(idx) = lower_raw.find(d.as_str()) {
87 if !confirmation_phrases
88 .iter()
89 .any(|c| lower_raw.contains(c.as_str()))
90 {
91 let line = line_of_offset(&doc.raw, idx);
92 let v = Violation::new(
93 AIL203,
94 ctx.severity,
95 doc.path.clone(),
96 "destructive action without confirmation constraint",
97 )
98 .at(line, 1)
99 .with_detail(d.clone());
100 violations.push(v);
101
102 break;
103 }
104 }
105 }
106
107 violations
108 }
109}