openclaw-scan 0.1.1

Security scanner for agentic AI framework installations (OpenClaw, Claude Code, and compatible)
Documentation
//! Integration tests for the secrets scanner.

use std::path::PathBuf;

use openclaw_scan::finding::{Category, Severity};
use openclaw_scan::paths::FrameworkHint;
use openclaw_scan::scanner::{secrets::SecretsScanner, ScanContext, Scanner};

fn fixture_dir() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}

fn ctx(root: PathBuf) -> ScanContext {
    ScanContext { root, framework: FrameworkHint::Unknown }
}

#[test]
fn detects_secrets_in_history_with_secrets_jsonl() {
    let dir = fixture_dir();
    // Copy fixture into a temp dir that looks like an install root
    let tmp = tempfile::tempdir().unwrap();
    std::fs::copy(
        dir.join("history_with_secrets.jsonl"),
        tmp.path().join("history.jsonl"),
    )
    .unwrap();

    let scanner = SecretsScanner;
    let findings = scanner.scan(&ctx(tmp.path().to_path_buf())).unwrap();

    assert!(!findings.is_empty(), "Should detect secrets in history.jsonl");
    assert!(
        findings.iter().any(|f| f.category == Category::SecretDetection),
        "Findings must be in SecretDetection category"
    );
    // Severity should be HIGH or CRITICAL
    assert!(
        findings
            .iter()
            .any(|f| f.severity >= Severity::High),
        "At least one HIGH or CRITICAL finding expected"
    );
}

#[test]
fn no_false_positives_on_clean_history() {
    let dir = fixture_dir();
    let tmp = tempfile::tempdir().unwrap();
    std::fs::copy(
        dir.join("history_clean.jsonl"),
        tmp.path().join("history.jsonl"),
    )
    .unwrap();

    let scanner = SecretsScanner;
    let findings = scanner.scan(&ctx(tmp.path().to_path_buf())).unwrap();
    assert!(
        findings.is_empty(),
        "Should not report false positives on clean history: {:?}",
        findings
    );
}

#[test]
fn detects_secrets_in_claude_md() {
    let dir = fixture_dir();
    let tmp = tempfile::tempdir().unwrap();
    std::fs::copy(
        dir.join("CLAUDE_with_secrets.md"),
        tmp.path().join("CLAUDE.md"),
    )
    .unwrap();

    let scanner = SecretsScanner;
    let findings = scanner.scan(&ctx(tmp.path().to_path_buf())).unwrap();
    assert!(!findings.is_empty(), "Should detect secrets in CLAUDE.md");
}

#[test]
fn evidence_is_always_redacted() {
    let dir = fixture_dir();
    let tmp = tempfile::tempdir().unwrap();
    std::fs::copy(
        dir.join("history_with_secrets.jsonl"),
        tmp.path().join("history.jsonl"),
    )
    .unwrap();

    let scanner = SecretsScanner;
    let findings = scanner.scan(&ctx(tmp.path().to_path_buf())).unwrap();

    for f in &findings {
        if let Some(ref evidence) = f.evidence {
            // Evidence must end with **** (redacted suffix)
            assert!(
                evidence.ends_with("****"),
                "Evidence should be redacted: {evidence}"
            );
        }
    }
}