keyhog 0.5.73

GPU-accelerated secret scanner for code, Git history, cloud, containers, browser assets, and live credential verification
//! Contract: `--format github-annotations` emits GitHub workflow commands.

use crate::support::{binary, write_temp_file};
use std::process::Command;

fn scan(path: &std::path::Path) -> std::process::Output {
    Command::new(binary())
        .args([
            "scan",
            "--daemon=off",
            "--backend",
            "simd",
            "--format",
            "github-annotations",
        ])
        .arg(path)
        .output()
        .expect("spawn keyhog scan --format github-annotations")
}

fn scan_directory_with_cap(path: &std::path::Path) -> std::process::Output {
    Command::new(binary())
        .args([
            "scan",
            "--daemon=off",
            "--backend",
            "simd",
            "--max-file-size",
            "15B",
            "--format",
            "github-annotations",
        ])
        .arg(path)
        .output()
        .expect("spawn capped keyhog scan --format github-annotations")
}

#[test]
fn clean_scan_emits_success_status_notice() {
    let (_dir, path) = write_temp_file("clean.env", "no secrets here\n");
    let output = scan(&path);
    assert_eq!(
        output.status.code(),
        Some(0),
        "clean scan with GitHub annotations must exit 0"
    );
    assert_eq!(
        String::from_utf8_lossy(&output.stdout),
        "::notice title=keyhog scan::scan status: success\n",
        "clean annotation output must carry an explicit terminal status"
    );
}

#[test]
fn planted_secret_emits_github_error_annotation() {
    let plaintext = "AKIAKPQXRMSNTBVWYZBN";
    let (_dir, path) = write_temp_file(
        "secret.env",
        &format!("clean line\nAWS_ACCESS_KEY_ID={plaintext}\n"),
    );
    let output = scan(&path);
    assert_eq!(
        output.status.code(),
        Some(1),
        "planted unverified secret must exit 1"
    );

    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.starts_with("::error "),
        "high-severity AWS finding must render as a GitHub error annotation: {stdout:?}"
    );
    assert!(
        stdout.contains("file=") && stdout.contains(",line=2,"),
        "annotation must carry file and one-based line number: {stdout:?}"
    );
    assert!(
        stdout.contains("title=keyhog"),
        "annotation must carry a title property: {stdout:?}"
    );
    assert!(
        stdout.contains("redacted=AK...BN"),
        "annotation message must carry the redacted credential: {stdout:?}"
    );
    assert!(
        !stdout.contains(plaintext),
        "annotation output must not leak plaintext credentials"
    );
}

#[test]
fn partial_scan_emits_github_coverage_warning() {
    let dir = tempfile::tempdir().expect("tempdir");
    std::fs::write(dir.path().join("small.txt"), "plain text\n").expect("small fixture");
    std::fs::write(dir.path().join("large.txt"), "this file exceeds the cap\n")
        .expect("large fixture");

    let output = scan_directory_with_cap(dir.path());
    assert_eq!(
        output.status.code(),
        Some(13),
        "partial scan must retain the coverage-gap exit code"
    );
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(
        stdout.contains("::warning title=keyhog coverage::partial scan coverage:")
            && stdout.contains("exceeded a configured size cap (--max-file-size or the matching --limit-*-bytes)=1"),
        "GitHub annotations must surface partial coverage in the job log: {stdout:?}"
    );
}