use serde::{Deserialize, Serialize};
use super::agent::Lifecycle;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRule {
pub id: String,
pub rule_type: PolicyRuleType,
pub severity: Severity,
pub scope: PolicyScope,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PolicyRuleType {
RequireAsset { asset_id: String },
RequireRole { role_id: String },
MaxStale { threshold: u32 },
TrustBoundary {
max_mutation: bool,
max_egress: bool,
max_credentials: bool,
},
LifecycleGate { min_stage: Lifecycle },
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Critical,
Warning,
Info,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyScope {
All,
NamePattern(String),
Team(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyViolation {
pub rule: PolicyRule,
pub workspace: String,
pub asset_id: Option<String>,
pub first_detected: String,
pub details: String,
pub remediation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyEvaluation {
pub workspace: String,
pub results: Vec<RuleResult>,
pub compliance_score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleResult {
pub rule_id: String,
pub passed: bool,
pub details: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Suppression {
pub rule_id: String,
pub workspace: String,
pub reason: String,
pub approver: String,
pub expires: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rule(severity: Severity) -> PolicyRule {
PolicyRule {
id: "test-rule".to_string(),
rule_type: PolicyRuleType::RequireAsset {
asset_id: "some-asset".to_string(),
},
severity,
scope: PolicyScope::All,
description: "Test rule".to_string(),
}
}
#[test]
fn severity_round_trip() {
for s in [Severity::Critical, Severity::Warning, Severity::Info] {
let json = serde_json::to_string(&s).unwrap();
let decoded: Severity = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, s);
}
}
#[test]
fn severity_serialized_strings() {
assert_eq!(
serde_json::to_string(&Severity::Critical).unwrap(),
"\"critical\""
);
assert_eq!(
serde_json::to_string(&Severity::Warning).unwrap(),
"\"warning\""
);
assert_eq!(serde_json::to_string(&Severity::Info).unwrap(), "\"info\"");
}
#[test]
fn severity_ordering() {
assert!(Severity::Critical < Severity::Warning);
assert!(Severity::Warning < Severity::Info);
}
#[test]
fn policy_rule_round_trip() {
let rule = make_rule(Severity::Critical);
let json = serde_json::to_string(&rule).unwrap();
let decoded: PolicyRule = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.id, "test-rule");
assert_eq!(decoded.severity, Severity::Critical);
}
#[test]
fn policy_rule_type_require_role_round_trip() {
let rt = PolicyRuleType::RequireRole {
role_id: "cloud-security-engineer".to_string(),
};
let json = serde_json::to_string(&rt).unwrap();
let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, rt);
}
#[test]
fn policy_rule_type_max_stale_round_trip() {
let rt = PolicyRuleType::MaxStale { threshold: 5 };
let json = serde_json::to_string(&rt).unwrap();
let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, rt);
}
#[test]
fn policy_rule_type_trust_boundary_round_trip() {
let rt = PolicyRuleType::TrustBoundary {
max_mutation: false,
max_egress: true,
max_credentials: true,
};
let json = serde_json::to_string(&rt).unwrap();
let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, rt);
}
#[test]
fn policy_rule_type_lifecycle_gate_round_trip() {
let rt = PolicyRuleType::LifecycleGate {
min_stage: Lifecycle::Stable,
};
let json = serde_json::to_string(&rt).unwrap();
let decoded: PolicyRuleType = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, rt);
}
#[test]
fn policy_scope_round_trip() {
for scope in [
PolicyScope::All,
PolicyScope::NamePattern("production-*".to_string()),
PolicyScope::Team("platform-security".to_string()),
] {
let json = serde_json::to_string(&scope).unwrap();
let decoded: PolicyScope = serde_json::from_str(&json).unwrap();
assert_eq!(
serde_json::to_string(&decoded).unwrap(),
serde_json::to_string(&scope).unwrap()
);
}
}
#[test]
fn policy_violation_round_trip() {
let v = PolicyViolation {
rule: make_rule(Severity::Warning),
workspace: "prod".to_string(),
asset_id: Some("aws-iam-scanner".to_string()),
first_detected: "2025-01-01T00:00:00.000Z".to_string(),
details: "Asset not found".to_string(),
remediation: "Install the asset".to_string(),
};
let json = serde_json::to_string(&v).unwrap();
let decoded: PolicyViolation = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.workspace, "prod");
assert_eq!(decoded.asset_id, Some("aws-iam-scanner".to_string()));
}
#[test]
fn policy_evaluation_round_trip() {
let eval = PolicyEvaluation {
workspace: "staging".to_string(),
results: vec![RuleResult {
rule_id: "test-rule".to_string(),
passed: true,
details: None,
}],
compliance_score: 100.0,
};
let json = serde_json::to_string(&eval).unwrap();
let decoded: PolicyEvaluation = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.workspace, "staging");
assert!((decoded.compliance_score - 100.0).abs() < f64::EPSILON);
}
#[test]
fn suppression_round_trip() {
let s = Suppression {
rule_id: "no-mutation-mcp".to_string(),
workspace: "staging-infra".to_string(),
reason: "Approved exception".to_string(),
approver: "security-lead@example.com".to_string(),
expires: "2025-06-01".to_string(),
};
let json = serde_json::to_string(&s).unwrap();
let decoded: Suppression = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.rule_id, "no-mutation-mcp");
assert_eq!(decoded.expires, "2025-06-01");
}
}