2026-01-12 04:53:31 +00:00

573 lines
20 KiB
Rust

//! Policy Validation System
//!
//! Validates Cedar policies for syntax, semantics, and compliance.
use std::collections::HashMap;
use std::path::Path;
use cedar_policy::{Policy, PolicyId, PolicySet};
use serde::{Deserialize, Serialize};
use crate::config::PolicyConfig;
use crate::error::{http, policy, ControlCenterError, Result};
/// Policy validation results
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
pub suggestions: Vec<ValidationSuggestion>,
pub metrics: ValidationMetrics,
}
/// Validation error details
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub error_type: ValidationErrorType,
pub message: String,
pub line: Option<usize>,
pub column: Option<usize>,
pub policy_id: Option<String>,
}
/// Types of validation errors
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationErrorType {
SyntaxError,
SemanticError,
ComplianceViolation,
SecurityIssue,
PerformanceIssue,
}
/// Validation warning details
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWarning {
pub warning_type: ValidationWarningType,
pub message: String,
pub policy_id: Option<String>,
pub severity: WarningSeverity,
}
/// Types of validation warnings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationWarningType {
DeprecatedSyntax,
PerformanceImpact,
SecurityBestPractice,
MaintenanceIssue,
}
/// Warning severity levels
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WarningSeverity {
Low,
Medium,
High,
}
/// Validation suggestions for improvement
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationSuggestion {
pub suggestion_type: SuggestionType,
pub message: String,
pub proposed_change: Option<String>,
pub policy_id: Option<String>,
}
/// Types of validation suggestions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SuggestionType {
Optimization,
Security,
Readability,
Maintenance,
}
/// Validation metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationMetrics {
pub total_policies: usize,
pub valid_policies: usize,
pub invalid_policies: usize,
pub total_lines: usize,
pub complexity_score: f64,
pub validation_time_ms: u64,
}
/// Policy validator
pub struct PolicyValidator {
config: PolicyConfig,
compliance_rules: HashMap<String, Vec<ComplianceRule>>,
security_patterns: Vec<SecurityPattern>,
}
/// Compliance rule definition
#[derive(Debug, Clone)]
pub struct ComplianceRule {
pub id: String,
pub name: String,
pub description: String,
pub required: bool,
pub pattern: String,
pub compliance_framework: String,
}
/// Security pattern for validation
#[derive(Debug, Clone)]
pub struct SecurityPattern {
pub id: String,
pub name: String,
pub pattern: String,
pub severity: WarningSeverity,
pub description: String,
}
impl PolicyValidator {
/// Create new policy validator
pub fn new(config: &PolicyConfig) -> Self {
let mut validator = Self {
config: config.clone(),
compliance_rules: HashMap::new(),
security_patterns: Vec::new(),
};
validator.initialize_compliance_rules();
validator.initialize_security_patterns();
validator
}
/// Initialize compliance rules for various frameworks
fn initialize_compliance_rules(&mut self) {
// SOC2 compliance rules
let soc2_rules = vec![
ComplianceRule {
id: "soc2-access-control".to_string(),
name: "Access Control Requirements".to_string(),
description: "Policies must implement proper access controls".to_string(),
required: true,
pattern: r#"permit.*when.*has.*"role""#.to_string(),
compliance_framework: "SOC2".to_string(),
},
ComplianceRule {
id: "soc2-audit-logging".to_string(),
name: "Audit Logging Requirements".to_string(),
description: "All access decisions must be logged".to_string(),
required: true,
pattern: r#"@audit.*true"#.to_string(),
compliance_framework: "SOC2".to_string(),
},
];
// HIPAA compliance rules
let hipaa_rules = vec![
ComplianceRule {
id: "hipaa-minimum-necessary".to_string(),
name: "Minimum Necessary Access".to_string(),
description: "Access must be limited to minimum necessary".to_string(),
required: true,
pattern: r#"permit.*when.*has.*"data_classification".*"phi""#.to_string(),
compliance_framework: "HIPAA".to_string(),
},
ComplianceRule {
id: "hipaa-authorization-tracking".to_string(),
name: "Authorization Tracking".to_string(),
description: "All PHI access must be tracked".to_string(),
required: true,
pattern: r#"@track.*"phi_access""#.to_string(),
compliance_framework: "HIPAA".to_string(),
},
];
self.compliance_rules.insert("SOC2".to_string(), soc2_rules);
self.compliance_rules
.insert("HIPAA".to_string(), hipaa_rules);
}
/// Initialize security patterns for validation
fn initialize_security_patterns(&mut self) {
self.security_patterns = vec![
SecurityPattern {
id: "overly-permissive".to_string(),
name: "Overly Permissive Policy".to_string(),
pattern: r#"permit\s*\(\s*\*\s*,\s*\*\s*,\s*\*\s*\)"#.to_string(),
severity: WarningSeverity::High,
description: "Policy allows all principals, actions, and resources".to_string(),
},
SecurityPattern {
id: "missing-time-constraints".to_string(),
name: "Missing Time Constraints".to_string(),
pattern: r#"permit.*when.*!.*time"#.to_string(),
severity: WarningSeverity::Medium,
description: "Consider adding time-based constraints for security".to_string(),
},
SecurityPattern {
id: "hardcoded-credentials".to_string(),
name: "Hardcoded Credentials".to_string(),
pattern: r#"(password|secret|key|token)\s*==\s*"[^"]*""#.to_string(),
severity: WarningSeverity::High,
description: "Avoid hardcoding credentials in policies".to_string(),
},
];
}
/// Validate policy content
pub fn validate_policy_content(&self, content: &str) -> Result<ValidationResult> {
let start_time = std::time::Instant::now();
let mut result = ValidationResult {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
suggestions: Vec::new(),
metrics: ValidationMetrics {
total_policies: 1,
valid_policies: 0,
invalid_policies: 0,
total_lines: content.lines().count(),
complexity_score: 0.0,
validation_time_ms: 0,
},
};
// Parse policy for syntax validation
let policy_id = PolicyId::new("validation_check");
match Policy::parse(Some(policy_id), content) {
Ok(policy) => {
result.metrics.valid_policies = 1;
// Run semantic validation
self.validate_semantics(&policy, &mut result);
// Run security validation
self.validate_security(content, &mut result);
// Run compliance validation
self.validate_compliance(content, &mut result);
// Calculate complexity score
result.metrics.complexity_score = self.calculate_complexity_score(content);
}
Err(parse_error) => {
result.valid = false;
result.metrics.invalid_policies = 1;
result.errors.push(ValidationError {
error_type: ValidationErrorType::SyntaxError,
message: parse_error.to_string(),
line: None, // Cedar doesn't provide line numbers in parse errors
column: None,
policy_id: Some("validation_check".to_string()),
});
}
}
result.metrics.validation_time_ms = start_time.elapsed().as_millis() as u64;
Ok(result)
}
/// Validate directory of policies
pub fn validate_directory(&self, dir: &Path) -> Result<ValidationResult> {
let start_time = std::time::Instant::now();
let mut combined_result = ValidationResult {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
suggestions: Vec::new(),
metrics: ValidationMetrics {
total_policies: 0,
valid_policies: 0,
invalid_policies: 0,
total_lines: 0,
complexity_score: 0.0,
validation_time_ms: 0,
},
};
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("cedar") {
let content = std::fs::read_to_string(&path)?;
let file_result = self.validate_policy_content(&content)?;
// Merge results
combined_result.valid &= file_result.valid;
combined_result.errors.extend(file_result.errors);
combined_result.warnings.extend(file_result.warnings);
combined_result.suggestions.extend(file_result.suggestions);
combined_result.metrics.total_policies += file_result.metrics.total_policies;
combined_result.metrics.valid_policies += file_result.metrics.valid_policies;
combined_result.metrics.invalid_policies += file_result.metrics.invalid_policies;
combined_result.metrics.total_lines += file_result.metrics.total_lines;
combined_result.metrics.complexity_score += file_result.metrics.complexity_score;
}
}
// Average complexity score
if combined_result.metrics.total_policies > 0 {
combined_result.metrics.complexity_score /=
combined_result.metrics.total_policies as f64;
}
combined_result.metrics.validation_time_ms = start_time.elapsed().as_millis() as u64;
Ok(combined_result)
}
/// Validate policy semantics
fn validate_semantics(&self, _policy: &Policy, result: &mut ValidationResult) {
// Implementation would check for semantic issues
// For now, just add placeholder validations
// Check for common semantic issues
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::MaintenanceIssue,
message: "Consider adding comments for complex conditions".to_string(),
policy_id: Some("semantic_check".to_string()),
severity: WarningSeverity::Low,
});
}
/// Validate security patterns
fn validate_security(&self, content: &str, result: &mut ValidationResult) {
use regex::Regex;
for pattern in &self.security_patterns {
if let Ok(regex) = Regex::new(&pattern.pattern) {
if regex.is_match(content) {
result.warnings.push(ValidationWarning {
warning_type: ValidationWarningType::SecurityBestPractice,
message: format!("{}: {}", pattern.name, pattern.description),
policy_id: Some(pattern.id.clone()),
severity: pattern.severity.clone(),
});
}
}
}
}
/// Validate compliance requirements
fn validate_compliance(&self, content: &str, result: &mut ValidationResult) {
use regex::Regex;
// Check enabled compliance frameworks
for (framework, rules) in &self.compliance_rules {
for rule in rules {
let Ok(regex) = Regex::new(&rule.pattern) else {
continue;
};
if rule.required && !regex.is_match(content) {
result.errors.push(ValidationError {
error_type: ValidationErrorType::ComplianceViolation,
message: format!("{} violation: {}", framework, rule.description),
line: None,
column: None,
policy_id: Some(rule.id.clone()),
});
result.valid = false;
}
}
}
}
/// Calculate complexity score for policy
fn calculate_complexity_score(&self, content: &str) -> f64 {
let lines = content.lines().count() as f64;
let conditions = content.matches("when").count() as f64;
let operators = content.matches("&&").count() + content.matches("||").count();
let nested_levels = self.count_nested_levels(content) as f64;
// Simple complexity calculation
(lines * 0.1) + (conditions * 0.5) + (operators as f64 * 0.3) + (nested_levels * 0.8)
}
/// Count nested levels in policy content
fn count_nested_levels(&self, content: &str) -> usize {
let mut max_depth = 0usize;
let mut current_depth = 0usize;
for char in content.chars() {
match char {
'(' | '{' => {
current_depth += 1;
max_depth = max_depth.max(current_depth);
}
')' | '}' => {
current_depth = current_depth.saturating_sub(1);
}
_ => {}
}
}
max_depth
}
/// Generate validation report
pub fn generate_report(&self, result: &ValidationResult, format: &str) -> Result<String> {
match format.to_lowercase().as_str() {
"json" => Ok(serde_json::to_string_pretty(result)?),
"text" => self.generate_text_report(result),
"html" => self.generate_html_report(result),
_ => Err(ControlCenterError::Http(http::HttpError::Validation(
format!("Unsupported report format: {}", format),
))),
}
}
/// Generate text report
fn generate_text_report(&self, result: &ValidationResult) -> Result<String> {
let mut report = String::new();
report.push_str("Policy Validation Report\n");
report.push_str("========================\n\n");
report.push_str(&format!(
"Status: {}\n",
if result.valid { "VALID" } else { "INVALID" }
));
report.push_str(&format!(
"Total Policies: {}\n",
result.metrics.total_policies
));
report.push_str(&format!(
"Valid Policies: {}\n",
result.metrics.valid_policies
));
report.push_str(&format!(
"Invalid Policies: {}\n",
result.metrics.invalid_policies
));
report.push_str(&format!(
"Complexity Score: {:.2}\n",
result.metrics.complexity_score
));
report.push_str(&format!(
"Validation Time: {}ms\n\n",
result.metrics.validation_time_ms
));
if !result.errors.is_empty() {
report.push_str("ERRORS:\n");
for error in &result.errors {
report.push_str(&format!(" - {}: {}\n", error.error_type, error.message));
}
report.push('\n');
}
if !result.warnings.is_empty() {
report.push_str("WARNINGS:\n");
for warning in &result.warnings {
report.push_str(&format!(
" - [{}] {}: {}\n",
warning.severity, warning.warning_type, warning.message
));
}
report.push('\n');
}
if !result.suggestions.is_empty() {
report.push_str("SUGGESTIONS:\n");
for suggestion in &result.suggestions {
report.push_str(&format!(
" - {}: {}\n",
suggestion.suggestion_type, suggestion.message
));
}
}
Ok(report)
}
/// Generate HTML report
fn generate_html_report(&self, result: &ValidationResult) -> Result<String> {
let status_color = if result.valid { "green" } else { "red" };
let status_text = if result.valid { "VALID" } else { "INVALID" };
let html = format!(
r#"
<html>
<head>
<title>Policy Validation Report</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background-color: #f5f5f5; padding: 15px; border-radius: 5px; }}
.status {{ color: {}; font-weight: bold; }}
.section {{ margin: 20px 0; }}
.error {{ color: red; }}
.warning {{ color: orange; }}
.suggestion {{ color: blue; }}
</style>
</head>
<body>
<div class="header">
<h1>Policy Validation Report</h1>
<p><strong>Status:</strong> <span class="status">{}</span></p>
<p><strong>Total Policies:</strong> {}</p>
<p><strong>Valid Policies:</strong> {}</p>
<p><strong>Invalid Policies:</strong> {}</p>
<p><strong>Complexity Score:</strong> {:.2}</p>
<p><strong>Validation Time:</strong> {}ms</p>
</div>
<!-- Additional HTML content would be generated here -->
</body>
</html>
"#,
status_color,
status_text,
result.metrics.total_policies,
result.metrics.valid_policies,
result.metrics.invalid_policies,
result.metrics.complexity_score,
result.metrics.validation_time_ms
);
Ok(html)
}
}
// Implement Display for enum types
impl std::fmt::Display for ValidationErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationErrorType::SyntaxError => write!(f, "Syntax Error"),
ValidationErrorType::SemanticError => write!(f, "Semantic Error"),
ValidationErrorType::ComplianceViolation => write!(f, "Compliance Violation"),
ValidationErrorType::SecurityIssue => write!(f, "Security Issue"),
ValidationErrorType::PerformanceIssue => write!(f, "Performance Issue"),
}
}
}
impl std::fmt::Display for ValidationWarningType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationWarningType::DeprecatedSyntax => write!(f, "Deprecated Syntax"),
ValidationWarningType::PerformanceImpact => write!(f, "Performance Impact"),
ValidationWarningType::SecurityBestPractice => write!(f, "Security Best Practice"),
ValidationWarningType::MaintenanceIssue => write!(f, "Maintenance Issue"),
}
}
}
impl std::fmt::Display for WarningSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WarningSeverity::Low => write!(f, "Low"),
WarningSeverity::Medium => write!(f, "Medium"),
WarningSeverity::High => write!(f, "High"),
}
}
}
impl std::fmt::Display for SuggestionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SuggestionType::Optimization => write!(f, "Optimization"),
SuggestionType::Security => write!(f, "Security"),
SuggestionType::Readability => write!(f, "Readability"),
SuggestionType::Maintenance => write!(f, "Maintenance"),
}
}
}