agent-shield 1.0.2

Security scanner for AI agent extensions - offline-first, multi-framework, SARIF output
Documentation
use std::path::PathBuf;

use crate::adapter::auto_detect_and_load;
use crate::ir::{
    DataSurface, DependencySurface, ExecutionSurface, Framework, Language, ProvenanceSurface,
    ScanTarget, SourceFile, SourceLocation,
};
use crate::rules::{Finding, RuleEngine};

pub(crate) fn loc() -> SourceLocation {
    loc_at("test.py", 5)
}

pub(crate) fn loc_at(file: &str, line: usize) -> SourceLocation {
    SourceLocation {
        file: PathBuf::from(file),
        line,
        column: 0,
        end_line: None,
        end_column: None,
    }
}

pub(crate) fn empty_target() -> ScanTarget {
    ScanTarget {
        name: "test".into(),
        framework: Framework::Mcp,
        root_path: PathBuf::from("/test"),
        tools: Vec::new(),
        execution: ExecutionSurface::default(),
        data: DataSurface::default(),
        dependencies: DependencySurface::default(),
        provenance: ProvenanceSurface::default(),
        source_files: Vec::new(),
    }
}

pub(crate) fn make_target_from_files(files: Vec<(&str, Language, &str)>) -> ScanTarget {
    let mut target = empty_target();
    target.source_files = files
        .into_iter()
        .map(|(path, language, content)| SourceFile {
            path: PathBuf::from(path),
            language,
            size_bytes: content.len() as u64,
            content_hash: "hash".into(),
            content: content.into(),
        })
        .collect();
    target
}

pub(crate) fn make_target_with_source(filename: &str, content: &str, lang: Language) -> ScanTarget {
    make_target_from_files(vec![(filename, lang, content)])
}

pub(crate) fn fixture_findings(name: &str) -> Vec<Finding> {
    let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/mcp_servers")
        .join(name);
    let engine = RuleEngine::new();

    auto_detect_and_load(&fixture_path, false)
        .unwrap_or_else(|err| panic!("failed to load fixture {name}: {err}"))
        .iter()
        .flat_map(|target| engine.run(target))
        .collect()
}

#[test]
fn vuln_ts_cmd_inject_fixture_triggers_invariant_rules() {
    let findings = fixture_findings("vuln_ts_cmd_inject");

    for expected_rule in ["SHIELD-001", "SHIELD-003", "SHIELD-004", "SHIELD-011"] {
        assert!(
            findings.iter().any(|f| f.rule_id == expected_rule),
            "expected vuln_ts_cmd_inject fixture to trigger {expected_rule}, got rules: {:?}",
            findings
                .iter()
                .map(|f| f.rule_id.as_str())
                .collect::<Vec<_>>()
        );
    }

    #[cfg(feature = "typescript")]
    assert!(
        findings.iter().any(|f| f.rule_id == "SHIELD-002"),
        "expected vuln_ts_cmd_inject fixture to trigger SHIELD-002 with typescript AST enabled"
    );
}