use anyhow::Result;
use colored::Colorize;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyConfig {
pub enabled: bool,
pub default_action: PolicyAction,
pub rules: Vec<PolicyRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRule {
pub name: String,
pub description: String,
pub pattern: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub regex: Option<String>,
pub action: PolicyAction,
pub enabled: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PolicyAction {
Block,
Warn,
RequireConfirmation,
Allow,
}
impl Default for PolicyConfig {
fn default() -> Self {
Self {
enabled: true,
default_action: PolicyAction::Allow,
rules: default_rules(),
}
}
}
fn default_rules() -> Vec<PolicyRule> {
vec![
PolicyRule {
name: "auth_files".to_string(),
description: "Authentication and authorization files".to_string(),
pattern: "**/auth/**/*".to_string(),
regex: None,
action: PolicyAction::Block,
enabled: true,
},
PolicyRule {
name: "env_files".to_string(),
description: "Environment files with secrets".to_string(),
pattern: "**/.env*".to_string(),
regex: None,
action: PolicyAction::Block,
enabled: true,
},
PolicyRule {
name: "terraform_state".to_string(),
description: "Terraform state files".to_string(),
pattern: "**/*.tfstate*".to_string(),
regex: None,
action: PolicyAction::Block,
enabled: true,
},
PolicyRule {
name: "db_migrations".to_string(),
description: "Database migration files".to_string(),
pattern: "**/migrations/**/*".to_string(),
regex: None,
action: PolicyAction::RequireConfirmation,
enabled: true,
},
PolicyRule {
name: "encryption_keys".to_string(),
description: "Encryption key files".to_string(),
pattern: "**/*.key".to_string(),
regex: None,
action: PolicyAction::Block,
enabled: true,
},
]
}
pub struct PolicyEngine {
config: PolicyConfig,
compiled_patterns: Vec<(PolicyRule, Regex)>,
}
#[derive(Debug)]
pub struct Violation {
pub rule: PolicyRule,
pub file_path: String,
pub action: PolicyAction,
}
impl PolicyEngine {
pub fn new(config: PolicyConfig) -> Result<Self> {
let mut compiled_patterns = Vec::new();
for rule in &config.rules {
if !rule.enabled {
continue;
}
let pattern = if let Some(ref regex) = rule.regex {
regex.clone()
} else {
glob_to_regex(&rule.pattern)?
};
match Regex::new(&pattern) {
Ok(regex) => {
compiled_patterns.push((rule.clone(), regex));
}
Err(e) => {
eprintln!("Warning: Invalid pattern '{}': {}", rule.pattern, e);
continue;
}
}
}
Ok(Self {
config,
compiled_patterns,
})
}
pub fn check_file(&self, file_path: &Path) -> Option<Violation> {
if !self.config.enabled {
return None;
}
let path_str = file_path.to_string_lossy();
for (rule, regex) in &self.compiled_patterns {
if regex.is_match(&path_str) {
return Some(Violation {
rule: rule.clone(),
file_path: path_str.to_string(),
action: rule.action,
});
}
}
None
}
}
fn glob_to_regex(glob: &str) -> Result<String> {
let mut regex = String::new();
if let Some(rest) = glob.strip_prefix("**/") {
regex.push_str("(^|.*/)");
regex.push_str(&convert_glob_pattern(rest)?);
} else if let Some(rest) = glob.strip_prefix("**") {
regex.push_str(".*");
regex.push_str(&convert_glob_pattern(rest)?);
} else {
regex.push('^');
regex.push_str(&convert_glob_pattern(glob)?);
}
regex.push('$');
Ok(regex)
}
fn convert_glob_pattern(pattern: &str) -> Result<String> {
let mut regex = String::new();
let mut chars = pattern.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'*' => {
if chars.peek() == Some(&'*') {
chars.next(); if chars.peek() == Some(&'/') {
chars.next(); regex.push_str(".*"); } else {
regex.push_str(".*"); }
} else {
regex.push_str("[^/]*"); }
}
'?' => regex.push('.'),
'.' => regex.push_str(r"\."),
'/' => regex.push('/'),
_ => {
if "+()^$|[]{}".contains(ch) {
regex.push('\\');
}
regex.push(ch);
}
}
}
Ok(regex)
}
impl Violation {
pub fn format_message(&self) -> String {
let action_str = match self.action {
PolicyAction::Block => "BLOCKED".red().bold().to_string(),
PolicyAction::Warn => "WARNING".yellow().bold().to_string(),
PolicyAction::RequireConfirmation => "REQUIRES CONFIRMATION".cyan().bold().to_string(),
PolicyAction::Allow => "ALLOWED".green().to_string(),
};
format!(
"{}: {}\n Rule: {}\n Description: {}",
action_str, self.file_path, self.rule.name, self.rule.description
)
}
}