1use std::collections::HashMap;
2use std::path::Path;
3
4use rayon::prelude::*;
5
6use crate::error::Result;
7use crate::facts::{FactSpec, FactValues, evaluate_facts};
8use crate::registry::RuleRegistry;
9use crate::report::{FixItem, FixReport, FixRuleResult, FixStatus, Report};
10use crate::rule::{Context, FixContext, FixOutcome, Rule, RuleResult, Violation};
11use crate::walker::FileIndex;
12use crate::when::{WhenEnv, WhenExpr};
13
14#[derive(Debug)]
18pub struct RuleEntry {
19 pub rule: Box<dyn Rule>,
20 pub when: Option<WhenExpr>,
21}
22
23impl RuleEntry {
24 pub fn new(rule: Box<dyn Rule>) -> Self {
25 Self { rule, when: None }
26 }
27
28 #[must_use]
29 pub fn with_when(mut self, expr: WhenExpr) -> Self {
30 self.when = Some(expr);
31 self
32 }
33}
34
35#[derive(Debug)]
43pub struct Engine {
44 entries: Vec<RuleEntry>,
45 registry: RuleRegistry,
46 facts: Vec<FactSpec>,
47 vars: HashMap<String, String>,
48 fix_size_limit: Option<u64>,
49}
50
51impl Engine {
52 pub fn new(rules: Vec<Box<dyn Rule>>, registry: RuleRegistry) -> Self {
54 let entries = rules.into_iter().map(RuleEntry::new).collect();
55 Self {
56 entries,
57 registry,
58 facts: Vec::new(),
59 vars: HashMap::new(),
60 fix_size_limit: Some(1 << 20),
61 }
62 }
63
64 pub fn from_entries(entries: Vec<RuleEntry>, registry: RuleRegistry) -> Self {
66 Self {
67 entries,
68 registry,
69 facts: Vec::new(),
70 vars: HashMap::new(),
71 fix_size_limit: Some(1 << 20),
72 }
73 }
74
75 #[must_use]
76 pub fn with_fix_size_limit(mut self, limit: Option<u64>) -> Self {
77 self.fix_size_limit = limit;
78 self
79 }
80
81 #[must_use]
82 pub fn with_facts(mut self, facts: Vec<FactSpec>) -> Self {
83 self.facts = facts;
84 self
85 }
86
87 #[must_use]
88 pub fn with_vars(mut self, vars: HashMap<String, String>) -> Self {
89 self.vars = vars;
90 self
91 }
92
93 pub fn rule_count(&self) -> usize {
94 self.entries.len()
95 }
96
97 pub fn run(&self, root: &Path, index: &FileIndex) -> Result<Report> {
98 let fact_values = evaluate_facts(&self.facts, root, index)?;
99 let ctx = Context {
100 root,
101 index,
102 registry: Some(&self.registry),
103 facts: Some(&fact_values),
104 vars: Some(&self.vars),
105 };
106 let when_env = WhenEnv {
107 facts: &fact_values,
108 vars: &self.vars,
109 };
110 let results: Vec<RuleResult> = self
111 .entries
112 .par_iter()
113 .filter_map(|entry| run_entry(entry, &ctx, &when_env, &fact_values))
114 .collect();
115 Ok(Report { results })
116 }
117
118 pub fn fix(&self, root: &Path, index: &FileIndex, dry_run: bool) -> Result<FixReport> {
125 let fact_values = evaluate_facts(&self.facts, root, index)?;
126 let ctx = Context {
127 root,
128 index,
129 registry: Some(&self.registry),
130 facts: Some(&fact_values),
131 vars: Some(&self.vars),
132 };
133 let when_env = WhenEnv {
134 facts: &fact_values,
135 vars: &self.vars,
136 };
137 let fix_ctx = FixContext {
138 root,
139 dry_run,
140 fix_size_limit: self.fix_size_limit,
141 };
142
143 let mut results: Vec<FixRuleResult> = Vec::new();
144 for entry in &self.entries {
145 if let Some(expr) = &entry.when {
146 match expr.evaluate(&when_env) {
147 Ok(true) => {}
148 Ok(false) => continue,
149 Err(e) => {
150 results.push(FixRuleResult {
151 rule_id: entry.rule.id().to_string(),
152 level: entry.rule.level(),
153 items: vec![FixItem {
154 violation: Violation::new(format!("when evaluation error: {e}")),
155 status: FixStatus::Unfixable,
156 }],
157 });
158 continue;
159 }
160 }
161 }
162 let violations = match entry.rule.evaluate(&ctx) {
163 Ok(v) => v,
164 Err(e) => vec![Violation::new(format!("rule error: {e}"))],
165 };
166 if violations.is_empty() {
167 continue;
168 }
169 let fixer = entry.rule.fixer();
170 let items: Vec<FixItem> = violations
171 .into_iter()
172 .map(|v| {
173 let status = match fixer {
174 Some(f) => match f.apply(&v, &fix_ctx) {
175 Ok(FixOutcome::Applied(s)) => FixStatus::Applied(s),
176 Ok(FixOutcome::Skipped(s)) => FixStatus::Skipped(s),
177 Err(e) => FixStatus::Skipped(format!("fix error: {e}")),
178 },
179 None => FixStatus::Unfixable,
180 };
181 FixItem {
182 violation: v,
183 status,
184 }
185 })
186 .collect();
187 results.push(FixRuleResult {
188 rule_id: entry.rule.id().to_string(),
189 level: entry.rule.level(),
190 items,
191 });
192 }
193 Ok(FixReport { results })
194 }
195}
196
197fn run_entry(
198 entry: &RuleEntry,
199 ctx: &Context<'_>,
200 when_env: &WhenEnv<'_>,
201 _facts: &FactValues,
202) -> Option<RuleResult> {
203 if let Some(expr) = &entry.when {
204 match expr.evaluate(when_env) {
205 Ok(true) => {} Ok(false) => return None,
207 Err(e) => {
208 return Some(RuleResult {
209 rule_id: entry.rule.id().to_string(),
210 level: entry.rule.level(),
211 policy_url: entry.rule.policy_url().map(str::to_string),
212 violations: vec![Violation::new(format!("when evaluation error: {e}"))],
213 is_fixable: entry.rule.fixer().is_some(),
214 });
215 }
216 }
217 }
218 Some(run_one(entry.rule.as_ref(), ctx))
219}
220
221fn run_one(rule: &dyn Rule, ctx: &Context<'_>) -> RuleResult {
222 let violations = match rule.evaluate(ctx) {
223 Ok(v) => v,
224 Err(e) => vec![Violation::new(format!("rule error: {e}"))],
225 };
226 RuleResult {
227 rule_id: rule.id().to_string(),
228 level: rule.level(),
229 policy_url: rule.policy_url().map(str::to_string),
230 violations,
231 is_fixable: rule.fixer().is_some(),
232 }
233}