use std::sync::{Mutex, OnceLock};
use crate::audit::{AuditContext, AuditRule, FnScan};
use crate::types::AuditReport;
pub type BoxedRule = Box<dyn AuditRule + Send + Sync>;
pub struct RuleRegistry {
rules: Vec<BoxedRule>,
}
impl Default for RuleRegistry {
fn default() -> Self {
Self::new()
}
}
impl RuleRegistry {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn register_rule(&mut self, rule: BoxedRule) {
if self.rules.iter().any(|r| r.id() == rule.id()) {
return;
}
self.rules.push(rule);
}
pub fn register_builtin_rules(&mut self) {
self.register_rule(Box::new(crate::rules::Auth001) as BoxedRule);
self.register_rule(Box::new(crate::rules::Auth002) as BoxedRule);
self.register_rule(Box::new(crate::rules::Auth003) as BoxedRule);
self.register_rule(Box::new(crate::rules::Auth004) as BoxedRule);
self.register_rule(Box::new(crate::rules::Move001) as BoxedRule);
self.register_rule(Box::new(crate::rules::Math001) as BoxedRule);
self.register_rule(Box::new(crate::rules::Cei001) as BoxedRule);
}
pub fn registered_rules(&self) -> &[BoxedRule] {
&self.rules
}
pub fn run_all(
&self,
scans: &[FnScan],
ctx: &AuditContext,
disabled: &[&str],
report: &mut AuditReport,
) {
for rule in &self.rules {
if disabled.contains(&rule.id()) {
continue;
}
rule.check(scans, ctx, report);
}
}
}
static GLOBAL: OnceLock<Mutex<RuleRegistry>> = OnceLock::new();
fn global() -> &'static Mutex<RuleRegistry> {
GLOBAL.get_or_init(|| Mutex::new(RuleRegistry::new()))
}
pub fn register_rule(rule: BoxedRule) {
global()
.lock()
.expect("audit registry poisoned")
.register_rule(rule);
}
pub fn register_builtin_rules() {
global()
.lock()
.expect("audit registry poisoned")
.register_builtin_rules();
}
pub fn run_registered(
scans: &[FnScan],
ctx: &AuditContext,
disabled: &[&str],
report: &mut AuditReport,
) {
register_builtin_rules();
let reg = global().lock().expect("audit registry poisoned");
reg.run_all(scans, ctx, disabled, report);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Severity;
struct Stub {
id: &'static str,
}
impl AuditRule for Stub {
fn id(&self) -> &'static str {
self.id
}
fn severity(&self) -> Severity {
Severity::Info
}
fn description(&self) -> &'static str {
"stub"
}
fn check(&self, _s: &[FnScan], _c: &AuditContext, r: &mut AuditReport) {
r.add(crate::types::Finding {
file: None,
rule_id: self.id.to_string(),
severity: Severity::Info,
message: "stub fired".into(),
location: None,
});
}
}
#[test]
fn register_adds_rule() {
let mut reg = RuleRegistry::new();
assert_eq!(reg.registered_rules().len(), 0);
reg.register_rule(Box::new(Stub { id: "STUB-1" }) as BoxedRule);
assert_eq!(reg.registered_rules().len(), 1);
}
#[test]
fn duplicate_registration_is_ignored() {
let mut reg = RuleRegistry::new();
reg.register_rule(Box::new(Stub { id: "STUB-1" }) as BoxedRule);
reg.register_rule(Box::new(Stub { id: "STUB-1" }) as BoxedRule);
assert_eq!(
reg.registered_rules().len(),
1,
"duplicate id must be ignored"
);
}
#[test]
fn ordering_preserved() {
let mut reg = RuleRegistry::new();
reg.register_rule(Box::new(Stub { id: "B" }) as BoxedRule);
reg.register_rule(Box::new(Stub { id: "A" }) as BoxedRule);
let ids: Vec<&str> = reg.registered_rules().iter().map(|r| r.id()).collect();
assert_eq!(ids, vec!["B", "A"]);
}
#[test]
fn builtin_registration_count() {
let mut reg = RuleRegistry::new();
reg.register_builtin_rules();
assert_eq!(reg.registered_rules().len(), 7, "seven built-in rules");
let ids: Vec<&str> = reg.registered_rules().iter().map(|r| r.id()).collect();
assert_eq!(
ids,
vec!["AUTH-001", "AUTH-002", "AUTH-003", "AUTH-004", "MOVE-001", "MATH-001", "CEI-001"]
);
}
#[test]
fn run_all_executes_every_rule() {
let mut reg = RuleRegistry::new();
reg.register_rule(Box::new(Stub { id: "X" }) as BoxedRule);
reg.register_rule(Box::new(Stub { id: "Y" }) as BoxedRule);
let scans: Vec<FnScan> = Vec::new();
let ctx = AuditContext { spec: None };
let mut report = AuditReport::default();
reg.run_all(&scans, &ctx, &[], &mut report);
assert_eq!(report.summary.total, 2);
assert!(report.findings.iter().any(|f| f.rule_id == "X"));
assert!(report.findings.iter().any(|f| f.rule_id == "Y"));
}
#[test]
fn run_all_respects_disabled() {
let mut reg = RuleRegistry::new();
reg.register_rule(Box::new(Stub { id: "X" }) as BoxedRule);
let scans: Vec<FnScan> = Vec::new();
let ctx = AuditContext { spec: None };
let mut report = AuditReport::default();
reg.run_all(&scans, &ctx, &["X"], &mut report);
assert_eq!(report.summary.total, 0);
}
}