ailint_core/rules/security/
unrestricted_tool.rs1use regex::{Regex, RegexBuilder, RegexSetBuilder};
7use serde::Deserialize;
8
9use crate::parser::ParsedDocument;
10use crate::rules::security::{line_of_offset, truncate_chars, AIL201};
11use crate::rules::{dictionary_lines, Rule, RuleContext, RuleId, Severity, Violation};
12
13const BUILTIN_PATTERNS: &str = include_str!("unrestricted_tool_patterns.txt");
14
15#[derive(Debug, Default, Deserialize)]
16struct Options {
17 #[serde(default)]
18 patterns: Option<Vec<String>>,
19 #[serde(default)]
20 extra_patterns: Option<Vec<String>>,
21}
22
23#[derive(Debug, Default)]
25pub struct NoUnrestrictedToolGrantRule;
26
27impl Rule for NoUnrestrictedToolGrantRule {
28 fn id(&self) -> RuleId {
29 AIL201
30 }
31
32 fn default_severity(&self) -> Severity {
33 Severity::Warning
34 }
35
36 fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
37 let opts: Options = ctx
38 .options
39 .and_then(|v| serde_yaml::from_value(v.clone()).ok())
40 .unwrap_or_default();
41
42 let base: Vec<String> = match opts.patterns {
43 Some(p) => p,
44 None => dictionary_lines(BUILTIN_PATTERNS)
45 .into_iter()
46 .map(String::from)
47 .collect(),
48 };
49 let extras = opts.extra_patterns.unwrap_or_default();
50
51 let compiled: Vec<(&String, Regex)> = base
54 .iter()
55 .chain(extras.iter())
56 .filter_map(|p| {
57 RegexBuilder::new(p)
58 .case_insensitive(true)
59 .build()
60 .ok()
61 .map(|re| (p, re))
62 })
63 .collect();
64 let set = RegexSetBuilder::new(compiled.iter().map(|(p, _)| p.as_str()))
65 .case_insensitive(true)
66 .build()
67 .ok();
68 let matched: Vec<usize> = match &set {
69 Some(s) => s.matches(&doc.raw).into_iter().collect(),
70 None => (0..compiled.len()).collect(),
71 };
72
73 let mut out = Vec::new();
74 for idx in matched {
75 let (_, re) = &compiled[idx];
76 for m in re.find_iter(&doc.raw) {
77 let line = line_of_offset(&doc.raw, m.start());
78 let matched = truncate_chars(m.as_str(), 60);
79 let mut v = Violation::new(
80 AIL201,
81 self.default_severity(),
82 doc.path.clone(),
83 format!("unrestricted tool/permission grant: '{}'", matched),
84 )
85 .at(line, 1);
86 v.fix_hint = Some(
87 "scope tool access explicitly (list allowed tools or commands) rather than granting blanket access"
88 .to_string(),
89 );
90 out.push(v);
91 }
92 }
93 out
94 }
95}