use regex::{Regex, RegexBuilder, RegexSetBuilder};
use serde::Deserialize;
use crate::parser::ParsedDocument;
use crate::rules::security::{line_of_offset, truncate_chars, AIL201};
use crate::rules::{dictionary_lines, Rule, RuleContext, RuleId, Severity, Violation};
const BUILTIN_PATTERNS: &str = include_str!("unrestricted_tool_patterns.txt");
#[derive(Debug, Default, Deserialize)]
struct Options {
#[serde(default)]
patterns: Option<Vec<String>>,
#[serde(default)]
extra_patterns: Option<Vec<String>>,
}
#[derive(Debug, Default)]
pub struct NoUnrestrictedToolGrantRule;
impl Rule for NoUnrestrictedToolGrantRule {
fn id(&self) -> RuleId {
AIL201
}
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn description(&self) -> &'static str {
"File grants a tool or permission without scoping."
}
fn fix_hint(&self) -> &'static str {
"Scope the grant to specific tools, paths, or actions instead of blanket access."
}
fn run(&self, doc: &ParsedDocument, ctx: &RuleContext<'_>) -> Vec<Violation> {
let opts: Options = ctx
.options
.and_then(|v| serde_yaml::from_value(v.clone()).ok())
.unwrap_or_default();
let base: Vec<String> = match opts.patterns {
Some(p) => p,
None => dictionary_lines(BUILTIN_PATTERNS)
.into_iter()
.map(String::from)
.collect(),
};
let extras = opts.extra_patterns.unwrap_or_default();
let compiled: Vec<(&String, Regex)> = base
.iter()
.chain(extras.iter())
.filter_map(|p| {
RegexBuilder::new(p)
.case_insensitive(true)
.build()
.ok()
.map(|re| (p, re))
})
.collect();
let set = RegexSetBuilder::new(compiled.iter().map(|(p, _)| p.as_str()))
.case_insensitive(true)
.build()
.ok();
let matched: Vec<usize> = match &set {
Some(s) => s.matches(&doc.raw).into_iter().collect(),
None => (0..compiled.len()).collect(),
};
let mut out = Vec::new();
for idx in matched {
let (_, re) = &compiled[idx];
for m in re.find_iter(&doc.raw) {
let line = line_of_offset(&doc.raw, m.start());
let matched = truncate_chars(m.as_str(), 60);
let v = Violation::new(
AIL201,
self.default_severity(),
doc.path.clone(),
"unrestricted tool/permission grant",
)
.at(line, 1)
.with_detail(matched);
out.push(v);
}
}
out
}
}