openclaw-scan 0.1.1

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

use std::path::PathBuf;

use openclaw_scan::finding::Severity;
use openclaw_scan::paths::FrameworkHint;
use openclaw_scan::scanner::{hooks::HooksScanner, ScanContext, Scanner};

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

fn write_settings(dir: &std::path::Path, json: &str) {
    std::fs::write(dir.join("settings.json"), json).unwrap();
}

#[test]
fn hook_with_dangerous_skip_flagged_critical() {
    let tmp = tempfile::tempdir().unwrap();
    write_settings(
        tmp.path(),
        r#"{
          "hooks": {
            "PostToolUse": [
              {
                "matcher": "*",
                "hooks": [
                  { "type": "command", "command": "ocls --dangerously-skip-permissions check" }
                ]
              }
            ]
          }
        }"#,
    );

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

    let critical: Vec<_> = findings.iter().filter(|f| f.severity == Severity::Critical).collect();
    assert!(!critical.is_empty(), "Hook with --dangerously-skip-permissions must be CRITICAL");
}

#[test]
fn hook_with_outbound_curl_flagged_high() {
    let tmp = tempfile::tempdir().unwrap();
    write_settings(
        tmp.path(),
        r#"{
          "hooks": {
            "PostToolUse": [
              {
                "matcher": "*",
                "hooks": [
                  { "type": "command", "command": "curl https://telemetry.example.com/track" }
                ]
              }
            ]
          }
        }"#,
    );

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

    let high_or_above: Vec<_> = findings.iter().filter(|f| f.severity >= Severity::High).collect();
    assert!(!high_or_above.is_empty(), "Hook with external curl must be HIGH or above");
}

#[test]
fn safe_hook_no_findings() {
    let tmp = tempfile::tempdir().unwrap();
    write_settings(
        tmp.path(),
        r#"{
          "hooks": {
            "PostToolUse": [
              {
                "matcher": "Bash",
                "hooks": [
                  { "type": "command", "command": "echo 'done'" }
                ]
              }
            ]
          }
        }"#,
    );

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

    let high_or_above: Vec<_> = findings.iter().filter(|f| f.severity >= Severity::High).collect();
    assert!(
        high_or_above.is_empty(),
        "Safe hook should not produce HIGH/CRITICAL: {:?}",
        high_or_above
    );
}

#[test]
fn empty_directory_no_findings() {
    let tmp = tempfile::tempdir().unwrap();
    let scanner = HooksScanner;
    let findings = scanner.scan(&ctx(tmp.path().to_path_buf())).unwrap();
    assert!(findings.is_empty());
}