#[allow(unused_imports)]
use crate::core::{codegen, executor, migrate, parser, planner, resolver, secrets, state, types};
use std::path::Path;
pub(crate) fn cmd_validate_check_resource_naming_convention(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let violations = find_naming_convention_violations(&config);
if json {
let items: Vec<String> = violations
.iter()
.map(|(n, reason)| format!("{{\"resource\":\"{n}\",\"issue\":\"{reason}\"}}"))
.collect();
println!("{{\"naming_convention_violations\":[{}]}}", items.join(","));
} else if violations.is_empty() {
println!("All resources follow naming conventions.");
} else {
println!("Naming convention violations:");
for (n, reason) in &violations {
println!(" {n} — {reason}");
}
}
Ok(())
}
pub(super) fn find_naming_convention_violations(
config: &types::ForjarConfig,
) -> Vec<(String, String)> {
let mut violations = Vec::new();
for name in config.resources.keys() {
if name.chars().any(|c| c.is_uppercase()) {
violations.push((name.clone(), "contains uppercase characters".to_string()));
} else if name.contains(' ') {
violations.push((name.clone(), "contains spaces".to_string()));
} else if name.starts_with('-') || name.ends_with('-') {
violations.push((name.clone(), "starts or ends with hyphen".to_string()));
} else if name.contains("__") {
violations.push((name.clone(), "contains double underscore".to_string()));
}
}
violations.sort_by(|a, b| a.0.cmp(&b.0));
violations
}
pub(crate) fn cmd_validate_check_resource_idempotency(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig =
serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let warnings = find_idempotency_concerns(&config);
if json {
let items: Vec<String> = warnings
.iter()
.map(|(n, reason)| format!("{{\"resource\":\"{n}\",\"concern\":\"{reason}\"}}"))
.collect();
println!("{{\"idempotency_concerns\":[{}]}}", items.join(","));
} else if warnings.is_empty() {
println!("All resources appear idempotent-safe.");
} else {
println!("Idempotency concerns:");
for (n, reason) in &warnings {
println!(" {n} — {reason}");
}
}
Ok(())
}
pub(super) fn find_idempotency_concerns(config: &types::ForjarConfig) -> Vec<(String, String)> {
let mut concerns = Vec::new();
for (name, resource) in &config.resources {
if let Some(ref content) = resource.content {
if content.contains("$(date") || content.contains("$(hostname") {
concerns.push((
name.clone(),
"content uses dynamic shell substitution".to_string(),
));
}
}
if let Some(ref st) = resource.state {
if st == "absent" && !resource.triggers.is_empty() {
concerns.push((name.clone(), "absent resource has triggers".to_string()));
}
}
}
concerns.sort_by(|a, b| a.0.cmp(&b.0));
concerns
}
pub(crate) fn cmd_validate_check_resource_documentation(
file: &Path,
json: bool,
) -> Result<(), String> {
let raw = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig = serde_yaml_ng::from_str(&raw).map_err(|e| e.to_string())?;
let undocumented = find_undocumented_resources(&config);
if json {
let items: Vec<String> = undocumented.iter().map(|n| format!("\"{n}\"")).collect();
println!("{{\"undocumented_resources\":[{}]}}", items.join(","));
} else if undocumented.is_empty() {
println!("All resources have documentation.");
} else {
println!("Resources missing documentation:");
for name in &undocumented {
println!(" {name}");
}
}
Ok(())
}
pub(super) fn find_undocumented_resources(config: &types::ForjarConfig) -> Vec<String> {
let mut missing: Vec<String> = config
.resources
.keys()
.filter(|name| {
let r = &config.resources[*name];
r.tags.is_empty() && r.content.is_none()
})
.cloned()
.collect();
missing.sort();
missing
}
pub(crate) fn cmd_validate_check_resource_ownership(file: &Path, json: bool) -> Result<(), String> {
let raw = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig = serde_yaml_ng::from_str(&raw).map_err(|e| e.to_string())?;
let unowned = find_unowned_resources(&config);
if json {
let items: Vec<String> = unowned.iter().map(|n| format!("\"{n}\"")).collect();
println!("{{\"unowned_resources\":[{}]}}", items.join(","));
} else if unowned.is_empty() {
println!("All resources have ownership assigned.");
} else {
println!("Resources missing ownership (no tags or resource_group):");
for name in &unowned {
println!(" {name}");
}
}
Ok(())
}
pub(super) fn find_unowned_resources(config: &types::ForjarConfig) -> Vec<String> {
let mut missing: Vec<String> = config
.resources
.keys()
.filter(|name| {
let r = &config.resources[*name];
r.tags.is_empty() && r.resource_group.is_none()
})
.cloned()
.collect();
missing.sort();
missing
}
pub(crate) fn cmd_validate_check_resource_secret_exposure(
file: &Path,
json: bool,
) -> Result<(), String> {
let raw = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig = serde_yaml_ng::from_str(&raw).map_err(|e| e.to_string())?;
let exposures = find_secret_exposures(&config);
if json {
let items: Vec<String> = exposures
.iter()
.map(|(n, reason)| format!("{{\"resource\":\"{n}\",\"issue\":\"{reason}\"}}"))
.collect();
println!("{{\"secret_exposures\":[{}]}}", items.join(","));
} else if exposures.is_empty() {
println!("No secret exposures detected.");
} else {
println!("Potential secret exposures:");
for (n, reason) in &exposures {
println!(" {n} — {reason}");
}
}
Ok(())
}
pub(super) fn find_secret_exposures(config: &types::ForjarConfig) -> Vec<(String, String)> {
let patterns = [
"password",
"secret",
"api_key",
"apikey",
"token",
"private_key",
];
let mut exposures = Vec::new();
for (name, resource) in &config.resources {
if let Some(ref content) = resource.content {
let lower = content.to_lowercase();
for pat in &patterns {
if lower.contains(pat) {
exposures.push((name.clone(), format!("content may contain '{pat}'")));
break;
}
}
}
}
exposures.sort_by(|a, b| a.0.cmp(&b.0));
exposures
}
pub(crate) fn cmd_validate_check_resource_tag_standards(
file: &Path,
json: bool,
) -> Result<(), String> {
let raw = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let config: types::ForjarConfig = serde_yaml_ng::from_str(&raw).map_err(|e| e.to_string())?;
let violations = find_tag_standard_violations(&config);
if json {
let items: Vec<String> = violations
.iter()
.map(|(n, tag, reason)| {
format!("{{\"resource\":\"{n}\",\"tag\":\"{tag}\",\"issue\":\"{reason}\"}}")
})
.collect();
println!("{{\"tag_standard_violations\":[{}]}}", items.join(","));
} else if violations.is_empty() {
println!("All resource tags follow naming standards.");
} else {
println!("Tag naming standard violations:");
for (n, tag, reason) in &violations {
println!(" {n} tag '{tag}' — {reason}");
}
}
Ok(())
}
pub(super) fn find_tag_standard_violations(
config: &types::ForjarConfig,
) -> Vec<(String, String, String)> {
let mut violations = Vec::new();
for (name, resource) in &config.resources {
for tag in &resource.tags {
if tag.chars().any(|c| c.is_uppercase()) {
violations.push((name.clone(), tag.clone(), "contains uppercase".to_string()));
} else if tag.contains(' ') {
violations.push((name.clone(), tag.clone(), "contains spaces".to_string()));
}
}
}
violations.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
violations
}
pub(crate) fn cmd_validate_check_resource_privilege_escalation(
file: &Path,
json: bool,
) -> Result<(), String> {
let content = std::fs::read_to_string(file).map_err(|e| e.to_string())?;
let cfg: types::ForjarConfig = serde_yaml_ng::from_str(&content).map_err(|e| e.to_string())?;
let risks = find_privilege_escalation_risks(&cfg);
if json {
let items: Vec<String> = risks
.iter()
.map(|(n, r)| format!("{{\"resource\":\"{n}\",\"risk\":\"{r}\"}}"))
.collect();
println!("{{\"privilege_escalation_risks\":[{}]}}", items.join(","));
} else if risks.is_empty() {
println!("No privilege escalation risks detected.");
} else {
println!("Privilege escalation risks:");
for (n, r) in &risks {
println!(" {n} — {r}");
}
}
Ok(())
}
pub(super) fn find_privilege_escalation_risks(cfg: &types::ForjarConfig) -> Vec<(String, String)> {
let mut risks = Vec::new();
let priv_patterns = [
"chmod +s",
"setuid",
"setgid",
"sudoers",
"NOPASSWD",
"cap_sys_admin",
];
for (name, res) in &cfg.resources {
if let Some(ref content) = res.content {
for pat in &priv_patterns {
if content.contains(pat) {
risks.push((name.clone(), format!("contains '{pat}'")));
}
}
}
if let Some(ref path) = res.path {
let p = path.to_lowercase();
if p.contains("sudoers") || p.contains("/etc/shadow") {
risks.push((name.clone(), format!("targets sensitive path '{path}'")));
}
}
}
risks.sort_by(|a, b| a.0.cmp(&b.0));
risks
}
pub(super) use super::validate_ownership_b::*;