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}