use super::helpers::*;
use crate::core::{resolver, types};
use std::path::Path;
pub(crate) fn cmd_validate_check_idempotency(file: &Path, json: bool) -> Result<(), String> {
let config = parse_and_validate(file)?;
let order = resolver::build_execution_order(&config)?;
let mut non_idempotent = Vec::new();
for name in &order {
if let Some(res) = config.resources.get(name) {
let rt = format!("{:?}", res.resource_type);
if rt == "Unknown" {
non_idempotent.push(format!("{name}: unknown resource type"));
}
}
}
if json {
println!(
"{{\"check_idempotency\":true,\"issues\":{},\"ok\":{}}}",
non_idempotent.len(),
non_idempotent.is_empty()
);
} else if non_idempotent.is_empty() {
println!(
"{} All {} resources produce idempotent scripts.",
green("✓"),
order.len()
);
} else {
println!(
"{} {} potential idempotency issue(s):",
red("✗"),
non_idempotent.len()
);
for issue in &non_idempotent {
println!(" - {issue}");
}
}
Ok(())
}
pub(crate) fn cmd_validate_check_drift_coverage(file: &Path, json: bool) -> Result<(), String> {
let config = parse_and_validate(file)?;
let total = config.resources.len();
let covered = total;
if json {
println!(
"{{\"check_drift_coverage\":true,\"total\":{total},\"covered\":{covered},\"ok\":true}}"
);
} else {
println!(
"{} All {}/{} resources have drift detection coverage.",
green("✓"),
covered,
total
);
}
Ok(())
}
pub(crate) fn cmd_validate_check_complexity(file: &Path, json: bool) -> Result<(), String> {
let config = parse_and_validate(file)?;
let threshold = 5;
let mut warnings: Vec<(String, usize)> = Vec::new();
for (name, res) in &config.resources {
let fan_out = res.depends_on.len();
if fan_out >= threshold {
warnings.push((name.clone(), fan_out));
}
}
collect_fan_in_warnings(&config, threshold, &mut warnings);
warnings.sort_by(|a, b| b.1.cmp(&a.1));
if json {
let entries: Vec<String> = warnings
.iter()
.map(|(name, count)| format!(r#"{{"resource":"{name}","fan_out_or_in":{count}}}"#))
.collect();
println!("[{}]", entries.join(","));
} else if warnings.is_empty() {
println!(
"{} No high-complexity resources found (threshold: {})",
green("✓"),
threshold
);
} else {
println!("Complexity warnings (threshold: {threshold}):\n");
for (name, count) in &warnings {
println!(
" {} {} — {} dependencies/dependents",
yellow("⚠"),
name,
count
);
}
}
Ok(())
}
fn collect_fan_in_warnings(
config: &types::ForjarConfig,
threshold: usize,
warnings: &mut Vec<(String, usize)>,
) {
let mut fan_in: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for res in config.resources.values() {
for dep in &res.depends_on {
*fan_in.entry(dep.clone()).or_insert(0) += 1;
}
}
for (name, count) in &fan_in {
if *count >= threshold && !warnings.iter().any(|(n, _)| n == name) {
warnings.push((name.clone(), *count));
}
}
}
fn check_world_writable_mode(
name: &str,
res: &types::Resource,
warnings: &mut Vec<(String, String)>,
) {
let mode = match res.mode {
Some(ref m) => m,
None => return,
};
if !mode.ends_with("7") && !mode.ends_with("6") {
return;
}
let last_three = if mode.len() >= 3 {
&mode[mode.len() - 3..]
} else {
mode.as_str()
};
if let Some(c) = last_three.chars().last() {
if c == '7' || c == '6' {
warnings.push((name.to_string(), format!("world-writable mode: {mode}")));
}
}
}
fn check_privileged_port(name: &str, res: &types::Resource, warnings: &mut Vec<(String, String)>) {
if res.resource_type != types::ResourceType::Network {
return;
}
if let Some(ref port_str) = res.port {
if let Ok(port) = port_str.parse::<u16>() {
if port < 1024 {
warnings.push((name.to_string(), format!("privileged port: {port}")));
}
}
}
}
fn check_resource_security(
name: &str,
res: &types::Resource,
warnings: &mut Vec<(String, String)>,
) {
check_world_writable_mode(name, res, warnings);
check_root_ownership_security(name, res, warnings);
check_privileged_port(name, res, warnings);
}
fn check_root_ownership_security(
name: &str,
res: &types::Resource,
warnings: &mut Vec<(String, String)>,
) {
if let Some(ref owner) = res.owner {
if owner == "root" {
if let Some(ref path) = res.path {
if path.starts_with("/tmp") || path.starts_with("/var/tmp") {
warnings.push((
name.to_string(),
format!("root-owned file in temp directory: {path}"),
));
}
}
}
}
}
pub(crate) fn cmd_validate_check_security(file: &Path, json: bool) -> Result<(), String> {
let config = parse_and_validate(file)?;
let mut warnings: Vec<(String, String)> = Vec::new();
for (name, res) in &config.resources {
check_resource_security(name, res, &mut warnings);
}
if json {
let entries: Vec<String> = warnings
.iter()
.map(|(name, warning)| format!(r#"{{"resource":"{name}","warning":"{warning}"}}"#))
.collect();
println!("[{}]", entries.join(","));
} else if warnings.is_empty() {
println!("{} No security issues found.", green("✓"));
} else {
println!("Security warnings:\n");
for (name, warning) in &warnings {
println!(" {} {} — {}", yellow("⚠"), name, warning);
}
}
Ok(())
}
pub(crate) fn cmd_validate_check_deprecation(file: &Path, json: bool) -> Result<(), String> {
let config = parse_and_validate(file)?;
let mut warnings: Vec<(String, String)> = Vec::new();
let deprecated_types = ["legacy", "raw_shell"];
for (name, res) in &config.resources {
let type_str = format!("{:?}", res.resource_type).to_lowercase();
for dep_type in &deprecated_types {
if type_str.contains(dep_type) {
warnings.push((name.clone(), format!("deprecated type: {type_str}")));
}
}
if res
.content
.as_ref()
.is_some_and(|c| c.contains("#!/bin/sh"))
{
warnings.push((
name.clone(),
"content contains #!/bin/sh shebang — consider using check scripts instead"
.to_string(),
));
}
}
if json {
let entries: Vec<String> = warnings
.iter()
.map(|(name, msg)| format!(r#"{{"resource":"{name}","warning":"{msg}"}}"#))
.collect();
println!("[{}]", entries.join(","));
} else if warnings.is_empty() {
println!("{} No deprecated patterns found.", green("✓"));
} else {
println!("Deprecation warnings:\n");
for (name, msg) in &warnings {
println!(" {} {} — {}", yellow("⚠"), name, msg);
}
}
Ok(())
}