Skip to main content

cac_core/
policy.rs

1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::path::Path;
4use thiserror::Error;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum PolicySeverity {
9    Critical,
10    High,
11    Medium,
12    Low,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum RuleKind {
18    SecretPattern,
19    ForbiddenFile,
20    RequiredAnnotation,
21    RequiredCall,
22    CustomRegex,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PolicyRule {
27    pub id: String,
28    pub kind: RuleKind,
29    pub description: String,
30    pub severity: PolicySeverity,
31    #[serde(default)]
32    pub pattern: Option<String>,
33    #[serde(default)]
34    pub file_glob: Option<String>,
35    #[serde(default)]
36    pub annotation: Option<String>,
37    #[serde(default)]
38    pub required_call: Option<String>,
39    #[serde(default)]
40    pub message: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Policy {
45    pub id: String,
46    pub name: String,
47    pub framework: String,
48    pub description: String,
49    pub rules: Vec<PolicyRule>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct PolicyPack {
54    pub version: String,
55    pub policies: Vec<Policy>,
56}
57
58#[derive(Debug, Error)]
59pub enum PolicyError {
60    #[error("failed to read policy file: {0}")]
61    Io(#[from] std::io::Error),
62    #[error("failed to parse policy YAML: {0}")]
63    Yaml(#[from] serde_yaml::Error),
64    #[error("invalid regex in rule {rule_id}: {source}")]
65    InvalidRegex {
66        rule_id: String,
67        #[source]
68        source: regex::Error,
69    },
70}
71
72impl PolicyPack {
73    pub fn load_dir(dir: &Path) -> Result<Self, PolicyError> {
74        let mut policies = Vec::new();
75        if !dir.exists() {
76            return Ok(Self {
77                version: "1.0".into(),
78                policies,
79            });
80        }
81
82        for entry in std::fs::read_dir(dir)? {
83            let entry = entry?;
84            let path = entry.path();
85            if path.extension().and_then(|e| e.to_str()) == Some("yaml")
86                || path.extension().and_then(|e| e.to_str()) == Some("yml")
87            {
88                let content = std::fs::read_to_string(&path)?;
89                let policy: Policy = serde_yaml::from_str(&content)?;
90                policies.push(policy);
91            }
92        }
93
94        policies.sort_by(|a, b| a.id.cmp(&b.id));
95        Ok(Self {
96            version: "1.0".into(),
97            policies,
98        })
99    }
100
101    pub fn compile_rules(&self) -> Result<Vec<CompiledRule>, PolicyError> {
102        let mut compiled = Vec::new();
103        for policy in &self.policies {
104            for rule in &policy.rules {
105                let regex = match rule.kind {
106                    RuleKind::SecretPattern | RuleKind::CustomRegex => {
107                        let pattern = rule.pattern.as_ref().ok_or_else(|| {
108                            PolicyError::InvalidRegex {
109                                rule_id: rule.id.clone(),
110                                source: regex::Error::Syntax("missing pattern".into()),
111                            }
112                        })?;
113                        Some(Regex::new(pattern).map_err(|source| PolicyError::InvalidRegex {
114                            rule_id: rule.id.clone(),
115                            source,
116                        })?)
117                    }
118                    RuleKind::RequiredAnnotation | RuleKind::RequiredCall => {
119                        rule.pattern.as_ref().map(|p| {
120                            Regex::new(p).map_err(|source| PolicyError::InvalidRegex {
121                                rule_id: rule.id.clone(),
122                                source,
123                            })
124                        }).transpose()?
125                    }
126                    RuleKind::ForbiddenFile => None,
127                };
128
129                compiled.push(CompiledRule {
130                    policy_id: policy.id.clone(),
131                    policy_name: policy.name.clone(),
132                    framework: policy.framework.clone(),
133                    rule: rule.clone(),
134                    regex,
135                });
136            }
137        }
138        Ok(compiled)
139    }
140}
141
142#[derive(Debug, Clone)]
143pub struct CompiledRule {
144    pub policy_id: String,
145    pub policy_name: String,
146    pub framework: String,
147    pub rule: PolicyRule,
148    pub regex: Option<Regex>,
149}
150
151impl CompiledRule {
152    pub fn violation_message(&self) -> String {
153        self.rule
154            .message
155            .clone()
156            .unwrap_or_else(|| self.rule.description.clone())
157    }
158}