1use serde::{Deserialize, Serialize};
4
5use super::agent::Lifecycle;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct PolicyRule {
10 pub id: String,
12 pub rule_type: PolicyRuleType,
14 pub severity: Severity,
16 pub scope: PolicyScope,
18 pub description: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum PolicyRuleType {
26 RequireAsset { asset_id: String },
28 RequireRole { role_id: String },
30 MaxStale { threshold: u32 },
32 TrustBoundary {
34 max_mutation: bool,
35 max_egress: bool,
36 max_credentials: bool,
37 },
38 LifecycleGate { min_stage: Lifecycle },
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum Severity {
46 Critical,
48 Warning,
50 Info,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum PolicyScope {
58 All,
60 NamePattern(String),
62 Team(String),
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct PolicyViolation {
69 pub rule: PolicyRule,
71 pub workspace: String,
73 pub asset_id: Option<String>,
75 pub first_detected: String,
77 pub details: String,
79 pub remediation: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PolicyEvaluation {
86 pub workspace: String,
88 pub results: Vec<RuleResult>,
90 pub compliance_score: f64,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct RuleResult {
97 pub rule_id: String,
99 pub passed: bool,
101 pub details: Option<String>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Suppression {
108 pub rule_id: String,
110 pub workspace: String,
112 pub reason: String,
114 pub approver: String,
116 pub expires: String,
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn make_rule(severity: Severity) -> PolicyRule {
125 PolicyRule {
126 id: "test-rule".to_string(),
127 rule_type: PolicyRuleType::RequireAsset {
128 asset_id: "some-asset".to_string(),
129 },
130 severity,
131 scope: PolicyScope::All,
132 description: "Test rule".to_string(),
133 }
134 }
135
136 #[test]
137 fn severity_round_trip() {
138 for s in [Severity::Critical, Severity::Warning, Severity::Info] {
139 let json = serde_json::to_string(&s).unwrap();
140 let decoded: Severity = serde_json::from_str(&json).unwrap();
141 assert_eq!(decoded, s);
142 }
143 }
144
145 #[test]
146 fn severity_serialized_strings() {
147 assert_eq!(
148 serde_json::to_string(&Severity::Critical).unwrap(),
149 "\"critical\""
150 );
151 assert_eq!(
152 serde_json::to_string(&Severity::Warning).unwrap(),
153 "\"warning\""
154 );
155 assert_eq!(serde_json::to_string(&Severity::Info).unwrap(), "\"info\"");
156 }
157
158 #[test]
159 fn severity_ordering() {
160 assert!(Severity::Critical < Severity::Warning);
163 assert!(Severity::Warning < Severity::Info);
164 }
165
166 #[test]
167 fn policy_rule_round_trip() {
168 let rule = make_rule(Severity::Critical);
169 let json = serde_json::to_string(&rule).unwrap();
170 let decoded: PolicyRule = serde_json::from_str(&json).unwrap();
171 assert_eq!(decoded.id, "test-rule");
172 assert_eq!(decoded.severity, Severity::Critical);
173 }
174
175 #[test]
176 fn policy_rule_type_require_role_round_trip() {
177 let rt = PolicyRuleType::RequireRole {
178 role_id: "cloud-security-engineer".to_string(),
179 };
180 let json = serde_json::to_string(&rt).unwrap();
181 let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
182 assert_eq!(decoded, rt);
183 }
184
185 #[test]
186 fn policy_rule_type_max_stale_round_trip() {
187 let rt = PolicyRuleType::MaxStale { threshold: 5 };
188 let json = serde_json::to_string(&rt).unwrap();
189 let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
190 assert_eq!(decoded, rt);
191 }
192
193 #[test]
194 fn policy_rule_type_trust_boundary_round_trip() {
195 let rt = PolicyRuleType::TrustBoundary {
196 max_mutation: false,
197 max_egress: true,
198 max_credentials: true,
199 };
200 let json = serde_json::to_string(&rt).unwrap();
201 let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
202 assert_eq!(decoded, rt);
203 }
204
205 #[test]
206 fn policy_rule_type_lifecycle_gate_round_trip() {
207 let rt = PolicyRuleType::LifecycleGate {
208 min_stage: Lifecycle::Stable,
209 };
210 let json = serde_json::to_string(&rt).unwrap();
211 let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
212 assert_eq!(decoded, rt);
213 }
214
215 #[test]
216 fn policy_scope_round_trip() {
217 for scope in [
218 PolicyScope::All,
219 PolicyScope::NamePattern("production-*".to_string()),
220 PolicyScope::Team("platform-security".to_string()),
221 ] {
222 let json = serde_json::to_string(&scope).unwrap();
223 let decoded: PolicyScope = serde_json::from_str(&json).unwrap();
224 assert_eq!(
226 serde_json::to_string(&decoded).unwrap(),
227 serde_json::to_string(&scope).unwrap()
228 );
229 }
230 }
231
232 #[test]
233 fn policy_violation_round_trip() {
234 let v = PolicyViolation {
235 rule: make_rule(Severity::Warning),
236 workspace: "prod".to_string(),
237 asset_id: Some("aws-iam-scanner".to_string()),
238 first_detected: "2025-01-01T00:00:00.000Z".to_string(),
239 details: "Asset not found".to_string(),
240 remediation: "Install the asset".to_string(),
241 };
242 let json = serde_json::to_string(&v).unwrap();
243 let decoded: PolicyViolation = serde_json::from_str(&json).unwrap();
244 assert_eq!(decoded.workspace, "prod");
245 assert_eq!(decoded.asset_id, Some("aws-iam-scanner".to_string()));
246 }
247
248 #[test]
249 fn policy_evaluation_round_trip() {
250 let eval = PolicyEvaluation {
251 workspace: "staging".to_string(),
252 results: vec![RuleResult {
253 rule_id: "test-rule".to_string(),
254 passed: true,
255 details: None,
256 }],
257 compliance_score: 100.0,
258 };
259 let json = serde_json::to_string(&eval).unwrap();
260 let decoded: PolicyEvaluation = serde_json::from_str(&json).unwrap();
261 assert_eq!(decoded.workspace, "staging");
262 assert!((decoded.compliance_score - 100.0).abs() < f64::EPSILON);
263 }
264
265 #[test]
266 fn suppression_round_trip() {
267 let s = Suppression {
268 rule_id: "no-mutation-mcp".to_string(),
269 workspace: "staging-infra".to_string(),
270 reason: "Approved exception".to_string(),
271 approver: "security-lead@example.com".to_string(),
272 expires: "2025-06-01".to_string(),
273 };
274 let json = serde_json::to_string(&s).unwrap();
275 let decoded: Suppression = serde_json::from_str(&json).unwrap();
276 assert_eq!(decoded.rule_id, "no-mutation-mcp");
277 assert_eq!(decoded.expires, "2025-06-01");
278 }
279}