use crate::graph::GraphNode;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PolicyConfig {
pub allowed_ecosystems: Vec<String>,
pub blocked_packages: Vec<String>,
pub require_sandbox: bool,
pub allow_network: bool,
pub require_provenance: bool,
pub max_artifact_size_mb: u64,
}
impl Default for PolicyConfig {
fn default() -> Self {
Self {
allowed_ecosystems: vec![
"python".to_string(),
"rust".to_string(),
"node".to_string(),
"go".to_string(),
"gradle".to_string(),
],
blocked_packages: Vec::new(),
require_sandbox: true,
allow_network: false,
require_provenance: true,
max_artifact_size_mb: 500,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum PolicyStatus {
Pass,
Warn,
Fail,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PolicyViolation {
pub rule: String,
pub severity: String, pub message: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PolicyEvaluationResult {
pub status: PolicyStatus,
pub violations: Vec<PolicyViolation>,
}
pub struct PolicyEngine {
pub config: PolicyConfig,
}
impl PolicyEngine {
pub fn new(config: PolicyConfig) -> Self {
Self { config }
}
pub fn load_from_file(policy_path: &Path) -> Result<Self> {
if !policy_path.exists() {
return Ok(Self::new(PolicyConfig::default()));
}
let content = fs::read_to_string(policy_path)
.with_context(|| format!("Failed to read policy file at {}", policy_path.display()))?;
let config: PolicyConfig = toml::from_str(&content)
.with_context(|| format!("Failed to parse policy TOML at {}", policy_path.display()))?;
Ok(Self::new(config))
}
pub fn evaluate_graph(&self, nodes: &[GraphNode]) -> PolicyEvaluationResult {
let mut violations = Vec::new();
for node in nodes {
if !self.config.allowed_ecosystems.is_empty()
&& !self.config.allowed_ecosystems.contains(&node.ecosystem)
{
violations.push(PolicyViolation {
rule: "allowed_ecosystems".to_string(),
severity: "ERROR".to_string(),
message: format!(
"Ecosystem [{}] is not permitted by policy for package [{}]",
node.ecosystem, node.package_name
),
});
}
if self.config.blocked_packages.contains(&node.package_name) {
violations.push(PolicyViolation {
rule: "blocked_packages".to_string(),
severity: "ERROR".to_string(),
message: format!(
"Package [{}] is explicitly blocked by organization policy",
node.package_name
),
});
}
}
let has_errors = violations.iter().any(|v| v.severity == "ERROR");
let has_warnings = violations.iter().any(|v| v.severity == "WARNING");
let status = if has_errors {
PolicyStatus::Fail
} else if has_warnings {
PolicyStatus::Warn
} else {
PolicyStatus::Pass
};
PolicyEvaluationResult { status, violations }
}
}