use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
pub mod slsa;
pub mod eval;
#[cfg(feature = "rego")]
pub mod rego;
pub use slsa::{SlsaLevel, detect_slsa_level};
pub use eval::{evaluate_policy, PolicyEvaluationResult, RuleResult, PolicySummary};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Enforcement {
#[default]
Strict,
Report,
}
impl std::fmt::Display for Enforcement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Enforcement::Strict => write!(f, "strict"),
Enforcement::Report => write!(f, "report"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Policy {
#[serde(default)]
pub policy: PolicyMetadata,
#[serde(default)]
pub slsa: SlsaPolicy,
#[serde(default)]
pub signatures: SignaturePolicy,
#[serde(default)]
pub trusted_tools: HashMap<String, TrustedToolPolicy>,
#[serde(default)]
pub trusted_builders: HashMap<String, TrustedBuilderPolicy>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyMetadata {
#[serde(default = "default_policy_name")]
pub name: String,
#[serde(default = "default_policy_version")]
pub version: String,
#[serde(default)]
pub enforcement: Enforcement,
}
fn default_policy_name() -> String {
"default".to_string()
}
fn default_policy_version() -> String {
"1.0".to_string()
}
impl Default for PolicyMetadata {
fn default() -> Self {
Self {
name: default_policy_name(),
version: default_policy_version(),
enforcement: Enforcement::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlsaPolicy {
#[serde(default)]
pub minimum_level: u8,
#[serde(default)]
pub enforcement: Option<Enforcement>,
}
impl Default for SlsaPolicy {
fn default() -> Self {
Self {
minimum_level: 0,
enforcement: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignaturePolicy {
#[serde(default)]
pub require_root_signatures: bool,
#[serde(default)]
pub require_attestation_signatures: bool,
#[serde(default)]
pub max_attestation_age_days: Option<u64>,
#[serde(default)]
pub enforcement: Option<Enforcement>,
}
impl Default for SignaturePolicy {
fn default() -> Self {
Self {
require_root_signatures: false,
require_attestation_signatures: false,
max_attestation_age_days: None,
enforcement: None,
}
}
}
impl SignaturePolicy {
pub fn max_attestation_age(&self) -> Option<Duration> {
self.max_attestation_age_days
.map(|days| Duration::from_secs(days * 24 * 60 * 60))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedToolPolicy {
#[serde(default)]
pub min_version: Option<String>,
#[serde(default)]
pub max_version: Option<String>,
#[serde(default)]
pub required_hash: Option<String>,
#[serde(default)]
pub public_keys: Vec<TrustedPublicKeyConfig>,
#[serde(default)]
pub keyless: Option<KeylessConfig>,
#[serde(default)]
pub enforcement: Option<Enforcement>,
}
impl Default for TrustedToolPolicy {
fn default() -> Self {
Self {
min_version: None,
max_version: None,
required_hash: None,
public_keys: Vec::new(),
keyless: None,
enforcement: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedPublicKeyConfig {
pub algorithm: String,
pub key: String,
#[serde(default)]
pub key_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeylessConfig {
#[serde(default)]
pub oidc_issuers: Vec<String>,
#[serde(default)]
pub subjects: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedBuilderPolicy {
pub builder_id: String,
#[serde(default)]
pub oidc_issuer: Option<String>,
#[serde(default)]
pub allowed_repos: Vec<String>,
#[serde(default)]
pub enforcement: Option<Enforcement>,
}
#[derive(Debug, Clone)]
pub enum PolicyError {
ParseError(String),
IoError(String),
ValidationError(String),
}
impl std::fmt::Display for PolicyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PolicyError::ParseError(msg) => write!(f, "Policy parse error: {}", msg),
PolicyError::IoError(msg) => write!(f, "Policy I/O error: {}", msg),
PolicyError::ValidationError(msg) => write!(f, "Policy validation error: {}", msg),
}
}
}
impl std::error::Error for PolicyError {}
impl Policy {
pub fn permissive() -> Self {
Self {
policy: PolicyMetadata {
name: "permissive".to_string(),
version: "1.0".to_string(),
enforcement: Enforcement::Report,
},
slsa: SlsaPolicy::default(),
signatures: SignaturePolicy::default(),
trusted_tools: HashMap::new(),
trusted_builders: HashMap::new(),
}
}
pub fn strict() -> Self {
Self {
policy: PolicyMetadata {
name: "strict".to_string(),
version: "1.0".to_string(),
enforcement: Enforcement::Strict,
},
slsa: SlsaPolicy {
minimum_level: 2,
enforcement: Some(Enforcement::Strict),
},
signatures: SignaturePolicy {
require_root_signatures: true,
require_attestation_signatures: true,
max_attestation_age_days: Some(30),
enforcement: Some(Enforcement::Strict),
},
trusted_tools: HashMap::new(),
trusted_builders: HashMap::new(),
}
}
pub fn from_toml(toml_str: &str) -> Result<Self, PolicyError> {
toml::from_str(toml_str).map_err(|e| PolicyError::ParseError(e.to_string()))
}
pub fn from_toml_file(path: &str) -> Result<Self, PolicyError> {
let content = std::fs::read_to_string(path)
.map_err(|e| PolicyError::IoError(format!("{}: {}", path, e)))?;
Self::from_toml(&content)
}
pub fn to_toml(&self) -> Result<String, PolicyError> {
toml::to_string_pretty(self).map_err(|e| PolicyError::ParseError(e.to_string()))
}
pub fn effective_enforcement(&self, section_enforcement: Option<Enforcement>) -> Enforcement {
section_enforcement.unwrap_or(self.policy.enforcement)
}
pub fn add_trusted_tool(&mut self, name: impl Into<String>, tool: TrustedToolPolicy) {
self.trusted_tools.insert(name.into(), tool);
}
pub fn add_trusted_builder(&mut self, name: impl Into<String>, builder: TrustedBuilderPolicy) {
self.trusted_builders.insert(name.into(), builder);
}
pub fn is_tool_trusted(&self, name: &str) -> bool {
self.trusted_tools.contains_key(name)
}
pub fn get_trusted_tool(&self, name: &str) -> Option<&TrustedToolPolicy> {
self.trusted_tools.get(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_policy() {
let toml = r#"
[policy]
name = "test"
"#;
let policy = Policy::from_toml(toml).unwrap();
assert_eq!(policy.policy.name, "test");
assert_eq!(policy.slsa.minimum_level, 0);
}
#[test]
fn test_parse_full_policy() {
let toml = r#"
[policy]
name = "production"
version = "1.0"
enforcement = "strict"
[slsa]
minimum_level = 2
enforcement = "strict"
[signatures]
require_root_signatures = true
require_attestation_signatures = true
max_attestation_age_days = 30
[trusted_tools.loom]
min_version = "0.1.0"
public_keys = [{ algorithm = "ed25519", key = "abc123", key_id = "loom-prod" }]
[trusted_tools.wac]
min_version = "0.5.0"
keyless = { oidc_issuers = ["https://token.actions.githubusercontent.com"], subjects = ["https://github.com/bytecodealliance/*"] }
[trusted_builders.github-actions]
builder_id = "https://github.com/actions/runner"
oidc_issuer = "https://token.actions.githubusercontent.com"
allowed_repos = ["pulseengine/*"]
"#;
let policy = Policy::from_toml(toml).unwrap();
assert_eq!(policy.policy.name, "production");
assert_eq!(policy.policy.enforcement, Enforcement::Strict);
assert_eq!(policy.slsa.minimum_level, 2);
assert!(policy.signatures.require_root_signatures);
assert!(policy.signatures.require_attestation_signatures);
assert_eq!(policy.signatures.max_attestation_age_days, Some(30));
assert!(policy.trusted_tools.contains_key("loom"));
assert!(policy.trusted_tools.contains_key("wac"));
let loom = policy.trusted_tools.get("loom").unwrap();
assert_eq!(loom.min_version, Some("0.1.0".to_string()));
assert_eq!(loom.public_keys.len(), 1);
let wac = policy.trusted_tools.get("wac").unwrap();
assert!(wac.keyless.is_some());
assert!(policy.trusted_builders.contains_key("github-actions"));
}
#[test]
fn test_enforcement_default() {
let policy = Policy::permissive();
assert_eq!(policy.policy.enforcement, Enforcement::Report);
let policy = Policy::strict();
assert_eq!(policy.policy.enforcement, Enforcement::Strict);
}
#[test]
fn test_effective_enforcement() {
let mut policy = Policy::permissive();
policy.policy.enforcement = Enforcement::Report;
assert_eq!(
policy.effective_enforcement(None),
Enforcement::Report
);
assert_eq!(
policy.effective_enforcement(Some(Enforcement::Strict)),
Enforcement::Strict
);
}
#[test]
fn test_policy_to_toml() {
let policy = Policy::strict();
let toml = policy.to_toml().unwrap();
assert!(toml.contains("[policy]"));
assert!(toml.contains("strict"));
let parsed = Policy::from_toml(&toml).unwrap();
assert_eq!(parsed.policy.name, policy.policy.name);
}
#[test]
fn test_max_attestation_age() {
let mut policy = SignaturePolicy::default();
assert!(policy.max_attestation_age().is_none());
policy.max_attestation_age_days = Some(30);
let duration = policy.max_attestation_age().unwrap();
assert_eq!(duration, Duration::from_secs(30 * 24 * 60 * 60));
}
}