use forjar::core::cis_ubuntu_pack::{cis_ubuntu_2204_pack, cis_ubuntu_yaml, severity_summary};
use forjar::core::compliance_pack::{evaluate_pack, ComplianceCheck};
use forjar::core::plugin_dispatch::{
available_plugin_types, dispatch_apply, dispatch_check, dispatch_destroy, is_plugin_type,
parse_plugin_type,
};
use std::collections::HashMap;
#[test]
fn cis_pack_name() {
let pack = cis_ubuntu_2204_pack();
assert_eq!(pack.name, "cis-ubuntu-22.04");
}
#[test]
fn cis_pack_version() {
let pack = cis_ubuntu_2204_pack();
assert_eq!(pack.version, "1.0.0");
}
#[test]
fn cis_pack_framework() {
let pack = cis_ubuntu_2204_pack();
assert_eq!(pack.framework, "CIS");
}
#[test]
fn cis_pack_has_description() {
let pack = cis_ubuntu_2204_pack();
assert!(pack.description.is_some());
assert!(pack.description.as_ref().unwrap().contains("CIS"));
}
#[test]
fn cis_pack_has_24_rules() {
let pack = cis_ubuntu_2204_pack();
assert_eq!(pack.rules.len(), 24, "expected 24 CIS rules");
}
#[test]
fn cis_rule_ids_all_prefixed() {
let pack = cis_ubuntu_2204_pack();
for rule in &pack.rules {
assert!(
rule.id.starts_with("CIS-"),
"rule {} should start with CIS-",
rule.id
);
}
}
#[test]
fn cis_rule_ids_unique() {
let pack = cis_ubuntu_2204_pack();
let mut ids: Vec<&str> = pack.rules.iter().map(|r| r.id.as_str()).collect();
let original_len = ids.len();
ids.sort();
ids.dedup();
assert_eq!(ids.len(), original_len, "duplicate rule IDs found");
}
#[test]
fn cis_all_rules_have_controls() {
let pack = cis_ubuntu_2204_pack();
for rule in &pack.rules {
assert!(
!rule.controls.is_empty(),
"rule {} has no controls",
rule.id
);
}
}
#[test]
fn cis_all_rules_have_titles() {
let pack = cis_ubuntu_2204_pack();
for rule in &pack.rules {
assert!(!rule.title.is_empty(), "rule {} has empty title", rule.id);
}
}
#[test]
fn cis_severity_errors_count() {
let pack = cis_ubuntu_2204_pack();
let (errors, _, _) = severity_summary(&pack);
assert!(errors >= 12, "expected >=12 error rules, got {errors}");
}
#[test]
fn cis_severity_warnings_count() {
let pack = cis_ubuntu_2204_pack();
let (_, warnings, _) = severity_summary(&pack);
assert!(warnings >= 8, "expected >=8 warning rules, got {warnings}");
}
#[test]
fn cis_severity_info_count() {
let pack = cis_ubuntu_2204_pack();
let (_, _, info) = severity_summary(&pack);
assert!(info >= 1, "expected >=1 info rule, got {info}");
}
#[test]
fn cis_severity_sum_equals_total() {
let pack = cis_ubuntu_2204_pack();
let (errors, warnings, info) = severity_summary(&pack);
assert_eq!(errors + warnings + info, 24);
}
#[test]
fn cis_has_assert_rules() {
let pack = cis_ubuntu_2204_pack();
let assert_count = pack
.rules
.iter()
.filter(|r| matches!(r.check, ComplianceCheck::Assert { .. }))
.count();
assert!(
assert_count >= 5,
"expected >=5 Assert rules, got {assert_count}"
);
}
#[test]
fn cis_has_deny_rules() {
let pack = cis_ubuntu_2204_pack();
let deny_count = pack
.rules
.iter()
.filter(|r| matches!(r.check, ComplianceCheck::Deny { .. }))
.count();
assert!(deny_count >= 5, "expected >=5 Deny rules, got {deny_count}");
}
#[test]
fn cis_has_require_rules() {
let pack = cis_ubuntu_2204_pack();
let require_count = pack
.rules
.iter()
.filter(|r| matches!(r.check, ComplianceCheck::Require { .. }))
.count();
assert!(
require_count >= 5,
"expected >=5 Require rules, got {require_count}"
);
}
#[test]
fn cis_has_require_tag_rules() {
let pack = cis_ubuntu_2204_pack();
let tag_count = pack
.rules
.iter()
.filter(|r| matches!(r.check, ComplianceCheck::RequireTag { .. }))
.count();
assert!(
tag_count >= 2,
"expected >=2 RequireTag rules, got {tag_count}"
);
}
#[test]
fn cis_covers_section_1_filesystem() {
let pack = cis_ubuntu_2204_pack();
assert!(pack.rules.iter().any(|r| r.id.starts_with("CIS-1.")));
}
#[test]
fn cis_covers_section_2_services() {
let pack = cis_ubuntu_2204_pack();
assert!(pack.rules.iter().any(|r| r.id.starts_with("CIS-2.")));
}
#[test]
fn cis_covers_section_3_network() {
let pack = cis_ubuntu_2204_pack();
assert!(pack.rules.iter().any(|r| r.id.starts_with("CIS-3.")));
}
#[test]
fn cis_covers_section_4_access() {
let pack = cis_ubuntu_2204_pack();
assert!(pack.rules.iter().any(|r| r.id.starts_with("CIS-4.")));
}
#[test]
fn cis_covers_section_5_auth() {
let pack = cis_ubuntu_2204_pack();
assert!(pack.rules.iter().any(|r| r.id.starts_with("CIS-5.")));
}
#[test]
fn cis_covers_section_6_maintenance() {
let pack = cis_ubuntu_2204_pack();
assert!(pack.rules.iter().any(|r| r.id.starts_with("CIS-6.")));
}
#[test]
fn cis_cross_maps_to_stig() {
let pack = cis_ubuntu_2204_pack();
let stig_rules: Vec<_> = pack
.rules
.iter()
.filter(|r| r.controls.iter().any(|c| c.starts_with("STIG")))
.collect();
assert!(
!stig_rules.is_empty(),
"at least one rule should cross-map to STIG"
);
}
#[test]
fn cis_yaml_serialization() {
let yaml = cis_ubuntu_yaml().unwrap();
assert!(yaml.contains("cis-ubuntu-22.04"));
assert!(yaml.contains("CIS-1.1.1"));
assert!(yaml.contains("CIS-6.2.1"));
}
#[test]
fn cis_yaml_roundtrip() {
let yaml = cis_ubuntu_yaml().unwrap();
let parsed: forjar::core::compliance_pack::CompliancePack =
serde_yaml_ng::from_str(&yaml).unwrap();
assert_eq!(parsed.name, "cis-ubuntu-22.04");
assert_eq!(parsed.rules.len(), 24);
}
#[test]
fn cis_yaml_contains_all_sections() {
let yaml = cis_ubuntu_yaml().unwrap();
for section in ["1.1", "2.1", "3.1", "4.1", "5.1", "6.1"] {
assert!(
yaml.contains(&format!("CIS-{section}")),
"YAML should contain section {section}"
);
}
}
#[test]
fn cis_evaluate_passing_file() {
let pack = cis_ubuntu_2204_pack();
let mut resources = HashMap::new();
let mut fields = HashMap::new();
fields.insert("type".into(), "file".into());
fields.insert("owner".into(), "root".into());
fields.insert("group".into(), "root".into());
fields.insert("mode".into(), "0644".into());
fields.insert("tags".into(), "system,environment".into());
resources.insert("config-file".into(), fields);
let result = evaluate_pack(&pack, &resources);
assert!(result.passed_count() > 0);
}
#[test]
fn cis_evaluate_world_writable_fails() {
let pack = cis_ubuntu_2204_pack();
let mut resources = HashMap::new();
let mut fields = HashMap::new();
fields.insert("type".into(), "file".into());
fields.insert("mode".into(), "777".into());
resources.insert("bad-file".into(), fields);
let result = evaluate_pack(&pack, &resources);
let failed_ids: Vec<_> = result
.results
.iter()
.filter(|r| !r.passed)
.map(|r| r.rule_id.as_str())
.collect();
assert!(
failed_ids.contains(&"CIS-5.3.1"),
"CIS-5.3.1 should fail for mode 777, failures: {failed_ids:?}"
);
}
#[test]
fn cis_evaluate_ssh_root_login_fails() {
let pack = cis_ubuntu_2204_pack();
let mut resources = HashMap::new();
let mut fields = HashMap::new();
fields.insert("type".into(), "file".into());
fields.insert("content".into(), "PermitRootLogin yes".into());
resources.insert("sshd-config".into(), fields);
let result = evaluate_pack(&pack, &resources);
let failed_ids: Vec<_> = result
.results
.iter()
.filter(|r| !r.passed)
.map(|r| r.rule_id.as_str())
.collect();
assert!(
failed_ids.contains(&"CIS-5.2.1"),
"CIS-5.2.1 should fail for PermitRootLogin yes"
);
}
#[test]
fn cis_evaluate_xinetd_denied() {
let pack = cis_ubuntu_2204_pack();
let mut resources = HashMap::new();
let mut fields = HashMap::new();
fields.insert("type".into(), "package".into());
fields.insert("name".into(), "xinetd".into());
resources.insert("bad-pkg".into(), fields);
let result = evaluate_pack(&pack, &resources);
let failed_ids: Vec<_> = result
.results
.iter()
.filter(|r| !r.passed)
.map(|r| r.rule_id.as_str())
.collect();
assert!(
failed_ids.contains(&"CIS-2.1.1"),
"CIS-2.1.1 should fail for xinetd"
);
}
#[test]
fn parse_plugin_type_valid_name() {
assert_eq!(parse_plugin_type("plugin:nginx"), Some("nginx"));
}
#[test]
fn parse_plugin_type_with_dash() {
assert_eq!(parse_plugin_type("plugin:my-custom"), Some("my-custom"));
}
#[test]
fn parse_plugin_type_with_underscore() {
assert_eq!(parse_plugin_type("plugin:my_plugin"), Some("my_plugin"));
}
#[test]
fn parse_plugin_type_not_plugin() {
assert_eq!(parse_plugin_type("package"), None);
assert_eq!(parse_plugin_type("file"), None);
assert_eq!(parse_plugin_type("service"), None);
}
#[test]
fn parse_plugin_type_bare_plugin() {
assert_eq!(parse_plugin_type("plugin"), None);
}
#[test]
fn parse_plugin_type_empty_name() {
assert_eq!(parse_plugin_type("plugin:"), Some(""));
}
#[test]
fn is_plugin_type_true() {
assert!(is_plugin_type("plugin:foo"));
assert!(is_plugin_type("plugin:bar-baz"));
}
#[test]
fn is_plugin_type_false() {
assert!(!is_plugin_type("package"));
assert!(!is_plugin_type("file"));
assert!(!is_plugin_type("plugin"));
assert!(!is_plugin_type(""));
}
#[test]
fn available_plugins_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let types = available_plugin_types(dir.path());
assert!(types.is_empty());
}
#[test]
fn dispatch_check_missing() {
let dir = tempfile::tempdir().unwrap();
let config = serde_json::json!({"key": "value"});
let result = dispatch_check(dir.path(), "nonexistent", &config);
assert!(!result.success);
assert_eq!(result.operation, "check");
assert_eq!(result.plugin_name, "nonexistent");
}
#[test]
fn dispatch_apply_missing() {
let dir = tempfile::tempdir().unwrap();
let config = serde_json::json!({});
let result = dispatch_apply(dir.path(), "nonexistent", &config);
assert!(!result.success);
assert_eq!(result.operation, "apply");
}
#[test]
fn dispatch_destroy_missing() {
let dir = tempfile::tempdir().unwrap();
let config = serde_json::json!({});
let result = dispatch_destroy(dir.path(), "nonexistent", &config);
assert!(!result.success);
assert_eq!(result.operation, "destroy");
}