use super::*;
use crate::core::types::{PolicyCheckResult, PolicyRuleType, PolicyViolation};
type FieldPresence = fn(&Resource) -> bool;
type FieldValue = fn(&Resource) -> Option<String>;
const FIELD_PRESENCE: &[(&str, FieldPresence)] = &[
("owner", |r| r.owner.is_some()),
("group", |r| r.group.is_some()),
("mode", |r| r.mode.is_some()),
("tags", |r| !r.tags.is_empty()),
("path", |r| r.path.is_some()),
("content", |r| r.content.is_some()),
("source", |r| r.source.is_some()),
("name", |r| r.name.is_some()),
("provider", |r| r.provider.is_some()),
("packages", |r| !r.packages.is_empty()),
("depends_on", |r| !r.depends_on.is_empty()),
("shell", |r| r.shell.is_some()),
("home", |r| r.home.is_some()),
("schedule", |r| r.schedule.is_some()),
("command", |r| r.command.is_some()),
("image", |r| r.image.is_some()),
("state", |r| r.state.is_some()),
("when", |r| r.when.is_some()),
];
pub(crate) fn resource_has_field(resource: &Resource, field: &str) -> bool {
FIELD_PRESENCE
.iter()
.find(|(name, _)| *name == field)
.is_some_and(|(_, present)| present(resource))
}
const FIELD_VALUES: &[(&str, FieldValue)] = &[
("owner", |r| r.owner.clone()),
("group", |r| r.group.clone()),
("mode", |r| r.mode.clone()),
("path", |r| r.path.clone()),
("content", |r| r.content.clone()),
("source", |r| r.source.clone()),
("name", |r| r.name.clone()),
("provider", |r| r.provider.clone()),
("state", |r| r.state.clone()),
("type", |r| Some(r.resource_type.to_string())),
("shell", |r| r.shell.clone()),
("home", |r| r.home.clone()),
("schedule", |r| r.schedule.clone()),
("command", |r| r.command.clone()),
("image", |r| r.image.clone()),
];
pub fn resource_field_value(resource: &Resource, field: &str) -> Option<String> {
FIELD_VALUES
.iter()
.find(|(name, _)| *name == field)
.and_then(|(_, value)| value(resource))
}
pub(crate) fn resource_field_count(resource: &Resource, field: &str) -> usize {
match field {
"tags" => resource.tags.len(),
"packages" => resource.packages.len(),
"depends_on" => resource.depends_on.len(),
_ => 0,
}
}
fn evaluate_rule(rule: &PolicyRule, resource: &Resource) -> bool {
match rule.rule_type {
PolicyRuleType::Require => violates_require(rule, resource),
PolicyRuleType::Deny | PolicyRuleType::Warn => violates_deny_or_warn(rule, resource),
PolicyRuleType::Assert => violates_assert(rule, resource),
PolicyRuleType::Limit => violates_limit(rule, resource),
}
}
fn violates_require(rule: &PolicyRule, resource: &Resource) -> bool {
if let Some(ref field) = rule.field {
!resource_has_field(resource, field)
} else {
false
}
}
fn violates_deny_or_warn(rule: &PolicyRule, resource: &Resource) -> bool {
if let (Some(ref field), Some(ref value)) = (&rule.condition_field, &rule.condition_value) {
resource_field_value(resource, field).as_deref() == Some(value.as_str())
} else {
false
}
}
fn violates_assert(rule: &PolicyRule, resource: &Resource) -> bool {
if let (Some(ref field), Some(ref expected)) = (&rule.condition_field, &rule.condition_value) {
resource_field_value(resource, field).as_deref() != Some(expected.as_str())
} else {
false
}
}
fn violates_limit(rule: &PolicyRule, resource: &Resource) -> bool {
if let Some(ref field) = rule.field {
let count = resource_field_count(resource, field);
let over_max = rule.max_count.is_some_and(|max| count > max);
let under_min = rule.min_count.is_some_and(|min| count < min);
over_max || under_min
} else {
false
}
}
pub(crate) fn matches_scope(rule: &PolicyRule, resource: &Resource) -> bool {
if let Some(ref rt) = rule.resource_type {
if resource.resource_type.to_string() != *rt {
return false;
}
}
if let Some(ref tag) = rule.tag {
if !resource.tags.contains(tag) {
return false;
}
}
true
}
pub fn evaluate_policies(config: &ForjarConfig) -> Vec<PolicyViolation> {
evaluate_policies_full(config).violations
}
pub fn violating_pairs(config: &ForjarConfig) -> Vec<(usize, String)> {
let mut out = Vec::new();
for (index, rule) in config.policies.iter().enumerate() {
for (id, resource) in &config.resources {
if matches_scope(rule, resource) && evaluate_rule(rule, resource) {
out.push((index, id.clone()));
}
}
}
out
}
pub fn evaluate_policies_full(config: &ForjarConfig) -> PolicyCheckResult {
let violations = violating_pairs(config)
.into_iter()
.map(|(index, resource_id)| {
let rule = &config.policies[index];
PolicyViolation {
rule_message: rule.message.clone(),
resource_id,
rule_type: rule.rule_type.clone(),
severity: rule.effective_severity(),
policy_id: rule.id.clone(),
remediation: rule.remediation.clone(),
compliance: rule.compliance.clone(),
}
})
.collect();
PolicyCheckResult {
violations,
rules_evaluated: config.policies.len(),
resources_checked: config.resources.len(),
}
}
pub fn policy_check_to_json(result: &PolicyCheckResult) -> String {
let violations_json: Vec<serde_json::Value> = result
.violations
.iter()
.map(|v| {
let compliance: Vec<serde_json::Value> = v
.compliance
.iter()
.map(|c| {
serde_json::json!({
"framework": c.framework,
"control": c.control,
})
})
.collect();
serde_json::json!({
"policy_id": v.policy_id,
"resource_id": v.resource_id,
"message": v.rule_message,
"severity": format!("{:?}", v.severity).to_lowercase(),
"rule_type": format!("{:?}", v.rule_type).to_lowercase(),
"remediation": v.remediation,
"compliance": compliance,
})
})
.collect();
let report = serde_json::json!({
"passed": !result.has_blocking_violations(),
"rules_evaluated": result.rules_evaluated,
"resources_checked": result.resources_checked,
"error_count": result.error_count(),
"warning_count": result.warning_count(),
"info_count": result.info_count(),
"violations": violations_json,
});
serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
}
pub fn policy_check_to_sarif(result: &PolicyCheckResult) -> String {
use crate::core::quality_gate::{checks::violation_to_finding, sarif::findings_to_sarif};
let findings: Vec<_> = result.violations.iter().map(violation_to_finding).collect();
let sarif = findings_to_sarif(&findings, "forjar.yaml");
serde_json::to_string_pretty(&sarif).unwrap_or_else(|_| "{}".to_string())
}