rustmaninoff 0.2.0

Fast IaC security scanner with Checkov-compatible policies
Documentation
use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

use rustmaninoff_engine::{Severity, Status};
use rustmaninoff_ir::Framework;
use rustmaninoff_report::{render_json, render_junit, render_sarif, render_text, RenderOptions};

use crate::{builtin_policies, scan, ScanRequest};

#[test]
fn builtin_pack_covers_at_least_160_policies_with_fixtures() {
    let policies = builtin_policies().expect("embedded policies");
    println!("builtin policies: {}", policies.len());
    assert!(
        policies.len() >= 175,
        "expected at least 175 builtin policies, found {}",
        policies.len()
    );
    for policy in &policies {
        let fixtures = policy
            .fixtures
            .as_ref()
            .unwrap_or_else(|| panic!("{} is missing fixtures", policy.id));
        let related: Vec<_> = policies
            .iter()
            .filter(|item| item.id == policy.id && item.framework == policy.framework)
            .cloned()
            .collect();
        println!(
            "check {} {}",
            policy.framework.map(|item| item.as_str()).unwrap_or("any"),
            policy.id
        );
        assert_fixture(policy, &related, &fixtures.fail, true);
        assert_fixture(policy, &related, &fixtures.pass, false);
    }
}

fn assert_fixture(
    policy: &rustmaninoff_engine::Policy,
    related: &[rustmaninoff_engine::Policy],
    source: &str,
    expect_fail: bool,
) {
    let framework = policy.framework.expect("framework");
    let name = match framework {
        Framework::Terraform => "fixture.tf",
        Framework::CloudFormation => "fixture.yaml",
        Framework::Kubernetes => "fixture.yaml",
        Framework::Docker => "Dockerfile",
    };
    let output = match framework {
        Framework::Terraform => rustmaninoff_parser_terraform::parse_file(Path::new(name), source),
        Framework::CloudFormation => rustmaninoff_parser_cfn::parse_file(Path::new(name), source),
        Framework::Kubernetes => rustmaninoff_parser_k8s::parse_file(Path::new(name), source),
        Framework::Docker => rustmaninoff_parser_docker::parse_file(Path::new(name), source),
    };
    assert!(
        output.diagnostics.is_empty(),
        "{} fixture parse error: {:?} source:\n{source}",
        policy.id,
        output.diagnostics
    );
    assert!(
        !output.resources.is_empty(),
        "{} fixture produced no resources:\n{source}",
        policy.id
    );
    let findings = rustmaninoff_engine::evaluate(&output.resources, related);
    let matched: Vec<_> = findings
        .iter()
        .filter(|finding| finding.check_id == policy.id)
        .collect();
    if expect_fail {
        assert!(
            matched
                .iter()
                .any(|finding| finding.status == Status::Failed),
            "{} fail fixture did not fail. statuses={:?}\n{source}",
            policy.id,
            matched
                .iter()
                .map(|finding| finding.status)
                .collect::<Vec<_>>()
        );
    } else {
        assert!(
            matched
                .iter()
                .any(|finding| finding.status == Status::Passed)
                || !output.resources.iter().any(|resource| {
                    policy
                        .resource_types
                        .iter()
                        .any(|resource_type| resource_type == &resource.resource_type)
                }),
            "{} pass fixture produced no passed result. statuses={:?}\n{source}",
            policy.id,
            matched
                .iter()
                .map(|finding| finding.status)
                .collect::<Vec<_>>()
        );
        assert!(
            matched
                .iter()
                .all(|finding| finding.status == Status::Passed),
            "{} pass fixture statuses={:?}\n{source}",
            policy.id,
            matched
                .iter()
                .map(|finding| finding.status)
                .collect::<Vec<_>>()
        );
    }
}

#[test]
fn scan_reports_text_json_sarif_and_junit() {
    let dir = std::env::temp_dir().join(format!(
        "rustmaninoff-scan-{}",
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    fs::create_dir_all(&dir).unwrap();
    fs::write(
        dir.join("main.tf"),
        r#"
resource "aws_ebs_volume" "bad" {
  availability_zone = "us-east-1a"
  size              = 8
  encrypted         = false
}
"#,
    )
    .unwrap();
    fs::write(
        dir.join("broken.tf"),
        "resource \"aws_ebs_volume\" \"bad\" {\n",
    )
    .unwrap();
    let request = ScanRequest {
        path: dir.clone(),
        frameworks: vec![
            Framework::Terraform,
            Framework::CloudFormation,
            Framework::Kubernetes,
            Framework::Docker,
        ],
        checks: vec!["CKV_AWS_3".into()],
        skip_checks: Vec::new(),
        fail_on: Severity::High,
        external_dirs: Vec::new(),
        show_unknown: false,
        show_passed: true,
        compact: false,
        soft_fail: false,
        outputs: vec!["cli".into()],
        json_file: None,
        sarif_file: None,
        junit_file: None,
        excluded: vec![".git".into()],
    };
    let result = scan(&request).expect("scan");
    assert_eq!(result.exit_code, 1);
    assert!(result
        .diagnostics
        .iter()
        .any(|item| item.file.ends_with("broken.tf")));
    assert!(result.findings.iter().any(
        |finding| finding.check_id.as_ref() == "CKV_AWS_3" && finding.status == Status::Failed
    ));
    let options = RenderOptions {
        show_passed: true,
        show_unknown: false,
        compact: false,
    };
    let text = render_text(&result.findings, &result.diagnostics, &options);
    let json = render_json(&result.findings, &result.diagnostics);
    let sarif = render_sarif(&result.findings);
    let junit = render_junit(&result.findings, &result.diagnostics);
    println!("{text}");
    assert!(text.contains("FAILED"));
    assert!(text.contains("CKV_AWS_3"));
    assert!(json.contains("\"failed_checks\""));
    assert!(sarif.contains("\"version\": \"2.1.0\""));
    assert!(junit.contains("<testsuite"));
    assert!(junit.contains("<failure"));
    let _ = fs::remove_dir_all(dir);
}

#[test]
fn skip_comment_suppresses_a_failure() {
    let source = r#"
# checkov:skip=CKV_AWS_3:development volume
resource "aws_ebs_volume" "bad" {
  encrypted = false
}
"#;
    let output = rustmaninoff_parser_terraform::parse_file(Path::new("skipped.tf"), source);
    let policies = builtin_policies().unwrap();
    let related: Vec<_> = policies
        .into_iter()
        .filter(|policy| {
            policy.id.as_ref() == "CKV_AWS_3" && policy.framework == Some(Framework::Terraform)
        })
        .collect();
    let findings = rustmaninoff_engine::evaluate(&output.resources, &related);
    assert!(
        findings
            .iter()
            .any(|finding| finding.status == Status::Skipped),
        "{findings:?}"
    );
    assert!(findings
        .iter()
        .all(|finding| finding.status != Status::Failed));
}