killer 2.0.1

A Rust security platform: static analysis, the .klr test language, a parallel test framework, project intelligence, code review, and a CI gate.
Documentation
//! End-to-end tests: scan the fixture projects and assert on the findings.

use std::path::{Path, PathBuf};

use killer::analyzer::{Analyzer, Category, Finding, Rule, Severity};
use killer::config::Config;
use killer::report::Report;
use killer::results::Verdict;
use killer::scanner::{self, FileData, Language};

fn fixture(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join(name)
}

fn analyze(dir: &Path) -> (killer::scanner::ScanResult, Vec<Finding>) {
    let config = Config::load(dir).expect("config loads");
    let scan = scanner::scan(dir, &config);
    let findings = Analyzer::with_default_rules(&config).analyze(&scan);
    (scan, findings)
}

#[test]
fn scanner_collects_files_and_languages() {
    let (scan, _) = analyze(&fixture("vulnerable_project"));
    assert!(scan.stats.files >= 2, "should find the fixture files");
    assert!(scan.stats.lines_of_code > 0);
    assert!(scan.stats.languages.contains(&"JavaScript".to_string()));
    assert!(scan.stats.languages.contains(&"Python".to_string()));
}

#[test]
fn vulnerable_project_triggers_security_rules() {
    let (_, findings) = analyze(&fixture("vulnerable_project"));

    // At least one hardcoded secret (JS API key / AWS key).
    assert!(
        findings.iter().any(|f| f.rule == "hardcoded-secret"),
        "expected a hardcoded-secret finding"
    );

    // At least one dangerous command (os.system / subprocess / eval).
    assert!(
        findings.iter().any(|f| f.rule == "dangerous-command"),
        "expected a dangerous-command finding"
    );

    // Security findings should be present and high-severity.
    assert!(findings
        .iter()
        .any(|f| f.category == Category::Security && f.severity <= Severity::High));
}

#[test]
fn vulnerable_project_tracks_todo_and_fixme() {
    let (_, findings) = analyze(&fixture("vulnerable_project"));
    assert!(
        findings.iter().any(|f| f.rule == "todo-tracker"),
        "expected TODO/FIXME markers to be tracked"
    );
}

#[test]
fn clean_project_has_no_blocking_issues() {
    let dir = fixture("clean_project");
    let (scan, findings) = analyze(&dir);
    let report = Report::new("clean".into(), scan.stats, findings);
    assert!(
        !report.has_blocking_issues(),
        "clean project should have no critical/high issues, got: {:#?}",
        report.findings
    );
    assert_eq!(report.score(), 100, "clean project should score 100");
}

#[test]
fn report_renders_and_scores() {
    let (scan, findings) = analyze(&fixture("vulnerable_project"));
    let report = Report::new("vulnerable".into(), scan.stats, findings);

    // Score is reduced by the findings but stays within bounds.
    assert!(report.score() <= 100);
    assert!(report.has_blocking_issues());

    // The rendered report contains the header and a score line.
    let text = report.render_terminal();
    assert!(text.contains("KILLER REPORT"));
    assert!(text.contains("Score:"));
}

#[test]
fn respects_config_rule_toggle() {
    // With secret detection disabled, no hardcoded-secret findings appear.
    let dir = fixture("vulnerable_project");
    let mut config = Config::default();
    config.rules.secret_detection = false;

    let scan = scanner::scan(&dir, &config);
    let findings = Analyzer::with_default_rules(&config).analyze(&scan);

    assert!(
        !findings.iter().any(|f| f.rule == "hardcoded-secret"),
        "disabling secret_detection should remove those findings"
    );
    // Other rules still fire.
    assert!(findings.iter().any(|f| f.rule == "dangerous-command"));
}

// ---------------------------------------------------------------------------
// Public-API shape. These tests live outside the library crate, so they are the
// only place the `#[non_exhaustive]` rules actually bite: they fail to compile
// if `Finding` loses its constructor, if `Rule` grows a defaultless method, or
// if a match on one of the open enums stops accepting a wildcard arm.

/// A rule written the way a third-party crate would write one: only the two
/// required trait methods, and a `Finding` built through the constructor
/// because struct-literal syntax is not available out of crate.
struct OutOfCrateRule;

impl Rule for OutOfCrateRule {
    fn id(&self) -> &str {
        "out-of-crate"
    }

    fn check(&self, file: &FileData) -> Vec<Finding> {
        if file.language != Language::Python {
            return Vec::new();
        }
        vec![Finding::new(
            self.id(),
            "Python file",
            Category::Quality,
            Severity::Info,
            &file.path,
            0,
            "seen by a third-party rule",
        )
        .with_suggestion("nothing to do")]
    }
}

#[test]
fn third_party_rule_needs_only_id_and_check() {
    let dir = fixture("vulnerable_project");
    let config = Config::load(&dir).expect("config loads");
    let scan = scanner::scan(&dir, &config);
    let findings = Analyzer::new(vec![Box::new(OutOfCrateRule)]).analyze(&scan);

    assert!(
        !findings.is_empty(),
        "the rule should have fired on the Python fixture"
    );
    assert_eq!(findings[0].rule, "out-of-crate");
    assert_eq!(findings[0].suggestion.as_deref(), Some("nothing to do"));

    // The defaulted trait methods, as an out-of-crate caller sees them.
    assert_eq!(OutOfCrateRule.name(), "out-of-crate");
    assert_eq!(OutOfCrateRule.description(), "");
    assert_eq!(OutOfCrateRule.category(), Category::Quality);
}

/// The fold direction a downstream catch-all has to take: only an explicit
/// `Secure` counts as secure, so a variant added later cannot silently pass.
fn treat_as_secure(verdict: Verdict) -> bool {
    matches!(verdict, Verdict::Secure)
}

#[test]
fn unknown_verdicts_are_not_secure() {
    assert!(treat_as_secure(Verdict::Secure));
    assert!(!treat_as_secure(Verdict::Vulnerable));
    assert!(!treat_as_secure(Verdict::Inconclusive));
    assert!(!treat_as_secure(Verdict::Errored));
}

#[test]
fn open_enums_accept_a_wildcard_arm() {
    // Severity: an unrecognized level must not be treated as blocking.
    let blocking = |s: Severity| matches!(s, Severity::Critical | Severity::High);
    assert!(blocking(Severity::Critical));
    assert!(!blocking(Severity::Info));

    let category = match Category::Security {
        Category::Security => "security",
        _ => "other",
    };
    assert_eq!(category, "security");

    // Language: a language this caller does not know about folds into the same
    // branch as `Other`.
    let label = match Language::from_path(Path::new("a.rs")) {
        Language::Rust => "rust",
        _ => "other",
    };
    assert_eq!(label, "rust");
}