rustmaninoff 0.2.0

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

use clap::Parser;

use super::{execute, request_from_args, Cli, Command};

fn dir(label: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let path = std::env::temp_dir().join(format!("rustmaninoff-cov-{label}-{nanos}"));
    fs::create_dir_all(&path).unwrap();
    path
}

fn run(args: &[&str]) -> anyhow::Result<i32> {
    let cli = Cli::try_parse_from(args).expect("args");
    execute(cli)
}

#[test]
fn scan_paths_frameworks_and_outputs() {
    let root = dir("scan");
    fs::write(
        root.join("main.tf"),
        "resource \"aws_ebs_volume\" \"disk\" {\n  encrypted = false\n}\n",
    )
    .unwrap();
    fs::write(
        root.join("main.tf.json"),
        r#"{"resource":{"aws_ebs_volume":{"json":{"encrypted":false}}}}"#,
    )
    .unwrap();
    fs::write(root.join("note.txt"), "ignore").unwrap();
    fs::write(
        root.join("pod.yaml"),
        "kind: Pod\napiVersion: v1\nmetadata:\n  name: web\nspec:\n  containers:\n    - name: web\n      image: nginx:latest\n      securityContext:\n        privileged: true\n",
    )
    .unwrap();
    fs::write(
        root.join("stack.yaml"),
        "Resources:\n  Bucket:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName: logs\n",
    )
    .unwrap();
    fs::write(
        root.join("Dockerfile"),
        "FROM nginx:latest\nUSER root\nADD src dest\n",
    )
    .unwrap();
    let skipped = root.join("target");
    fs::create_dir(&skipped).unwrap();
    fs::write(
        skipped.join("hidden.tf"),
        "resource \"aws_ebs_volume\" \"x\" { encrypted = false }\n",
    )
    .unwrap();
    let json_file = root.join("out.json");
    let sarif_file = root.join("out.sarif");
    let junit_file = root.join("out.xml");
    let code = run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--output",
        "cli,json,sarif,junit",
        "--json-file",
        json_file.to_str().unwrap(),
        "--sarif-file",
        sarif_file.to_str().unwrap(),
        "--junit-file",
        junit_file.to_str().unwrap(),
        "--show-unknown",
        "--compact",
        "--show-passed",
        "--fail-on",
        "LOW",
    ])
    .unwrap();
    assert_eq!(code, 1);
    assert!(json_file.is_file());
    assert!(sarif_file.is_file());
    assert!(junit_file.is_file());
    let only = run(&[
        "rustmaninoff",
        "scan",
        root.join("main.tf").to_str().unwrap(),
        "--framework",
        "terraform",
        "--check",
        "CKV_AWS_3",
        "--output",
        "json",
    ])
    .unwrap();
    assert_eq!(only, 1);
    let clean = dir("clean");
    fs::write(
        clean.join("main.tf"),
        "resource \"aws_ebs_volume\" \"disk\" {\n  encrypted = true\n}\n",
    )
    .unwrap();
    assert_eq!(
        run(&["rustmaninoff", "scan", clean.to_str().unwrap()]).unwrap(),
        0
    );
    assert_eq!(
        run(&[
            "rustmaninoff",
            "scan",
            root.join("main.tf").to_str().unwrap(),
            "--soft-fail"
        ])
        .unwrap(),
        0
    );
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--framework",
        "nope"
    ])
    .is_err());
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--fail-on",
        "nope"
    ])
    .is_err());
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--output",
        "html"
    ])
    .is_err());
    let k8s_only = run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--framework",
        "kubernetes",
        "--output",
        "text",
    ])
    .unwrap();
    assert_eq!(k8s_only, 1);
    let docker_only = run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--framework",
        "docker",
        "--check",
        "CKV_DOCKER_1",
        "--fail-on",
        "LOW",
        "--output",
        "text",
    ])
    .unwrap();
    assert_eq!(docker_only, 1);
    let missing = run(&[
        "rustmaninoff",
        "scan",
        root.join("missing").to_str().unwrap(),
    ])
    .unwrap();
    assert_eq!(missing, 0);
    let _ = fs::remove_dir_all(root);
    let _ = fs::remove_dir_all(clean);
}

#[test]
fn config_filters_and_external_checks() {
    let root = dir("config");
    fs::write(
        root.join("main.tf"),
        "resource \"aws_ebs_volume\" \"disk\" {\n  encrypted = false\n}\n",
    )
    .unwrap();
    fs::write(
        root.join(".rustmaninoff.yaml"),
        "frameworks: [terraform]\nskip_checks: [CKV_AWS_999]\nfail_on: HIGH\nexcluded_paths: [vendor]\nshow_unknown: true\ncompact: true\n",
    )
    .unwrap();
    fs::create_dir(root.join("vendor")).unwrap();
    fs::write(
        root.join("vendor/skip.tf"),
        "resource \"aws_ebs_volume\" \"x\" { encrypted = false }\n",
    )
    .unwrap();
    let external = root.join("external");
    fs::create_dir(&external).unwrap();
    fs::write(
        external.join("extra.yaml"),
        "metadata:\n  id: EXT_1\n  name: extra\n  severity: LOW\nresource_types: [aws_ebs_volume]\ndefinition:\n  cond_type: attribute\n  attribute: encrypted\n  operator: equals\n  value: true\n",
    )
    .unwrap();
    let code = run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--skip-check",
        "CKV_AWS_3",
        "--external-checks-dir",
        external.to_str().unwrap(),
        "--fail-on",
        "LOW",
    ])
    .unwrap();
    assert_eq!(code, 1);
    let bad = root.join("bad.yaml");
    fs::write(&bad, "[\n").unwrap();
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--config",
        bad.to_str().unwrap()
    ])
    .is_err());
    let deep = root.join("deep.yaml");
    fs::write(&deep, "{".repeat(200)).unwrap();
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--config",
        deep.to_str().unwrap()
    ])
    .is_err());
    let huge = root.join("huge.yaml");
    let mut file = fs::File::create(&huge).unwrap();
    file.write_all(b"frameworks: []\n").unwrap();
    file.set_len(1_000_001).unwrap();
    drop(file);
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--config",
        huge.to_str().unwrap()
    ])
    .is_err());
    #[cfg(unix)]
    {
        let link = root.join("link.yaml");
        std::os::unix::fs::symlink(&bad, &link).unwrap();
        assert!(run(&[
            "rustmaninoff",
            "scan",
            root.to_str().unwrap(),
            "--config",
            link.to_str().unwrap()
        ])
        .is_err());
    }
    let cli = Cli::try_parse_from(["rustmaninoff", "scan"]).unwrap();
    let Command::Scan(args) = cli.command;
    let request = request_from_args(args).unwrap();
    assert!(!request.frameworks.is_empty());
    let blocked = root.join("blocked");
    fs::create_dir(&blocked).unwrap();
    let err = run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--external-checks-dir",
        blocked.join("missing").to_str().unwrap(),
        "--check",
        "CKV_AWS_3",
    ]);
    assert!(err.is_err());
    let _ = fs::remove_dir_all(root);
}

#[test]
fn write_failure_is_reported() {
    let root = dir("write");
    fs::write(
        root.join("main.tf"),
        "resource \"aws_ebs_volume\" \"disk\" { encrypted = true }\n",
    )
    .unwrap();
    let err = run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--output",
        "json",
        "--json-file",
        root.to_str().unwrap(),
    ]);
    assert!(err.is_err());
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--output",
        "junit",
        "--junit-file",
        root.to_str().unwrap(),
    ])
    .is_err());
    assert!(run(&[
        "rustmaninoff",
        "scan",
        root.to_str().unwrap(),
        "--output",
        "sarif",
        "--sarif-file",
        root.to_str().unwrap(),
    ])
    .is_err());
    #[cfg(unix)]
    {
        let link = root.join("link.tf");
        std::os::unix::fs::symlink(root.join("main.tf"), &link).unwrap();
        assert_eq!(
            run(&["rustmaninoff", "scan", link.to_str().unwrap()]).unwrap(),
            0
        );
        assert!(
            super::classify_and_parse(&link, &[rustmaninoff_ir::Framework::Terraform]).is_none()
        );
    }
    let _ = fs::remove_dir_all(root);
}