Skip to main content

ai_coding_shield/scanner/
mod.rs

1mod workflow;
2mod skill;
3mod mcp;
4mod generic;
5pub mod vuln_db; // Public for analyzer usage
6
7use anyhow::Result;
8use std::path::PathBuf;
9use walkdir::WalkDir;
10
11use crate::types::Artifact;
12
13pub struct Scanner {
14    root_path: PathBuf,
15}
16
17impl Scanner {
18    pub fn new(root_path: PathBuf) -> Self {
19        Self { root_path }
20    }
21
22    pub fn scan(&self) -> Result<Vec<Artifact>> {
23        let mut artifacts = Vec::new();
24
25        for entry in WalkDir::new(&self.root_path)
26            .follow_links(false)
27            .into_iter()
28            .filter_map(|e| e.ok())
29        {
30            let path = entry.path();
31
32            // Skip directories
33            if !path.is_file() {
34                continue;
35            }
36
37            // Detect artifact type by path patterns
38            if let Some(artifact) = self.detect_and_parse(path)? {
39                artifacts.push(artifact);
40            }
41        }
42
43        Ok(artifacts)
44    }
45
46    fn detect_and_parse(&self, path: &std::path::Path) -> Result<Option<Artifact>> {
47        let path_str = path.to_string_lossy();
48        let ext = path.extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();
49
50        // Workflows: .agent/workflows/*.md
51        if path_str.contains("workflows") && ext == "md" {
52            return Ok(Some(workflow::parse_workflow(path)?));
53        }
54
55        // Skills: .agent/skills/*/SKILL.md
56        if path_str.contains("skills") && path.file_name().map_or(false, |n| n == "SKILL.md") {
57            return Ok(Some(skill::parse_skill(path)?));
58        }
59
60        // Rules: .agent/rules/*.md (if they exist)
61        if path_str.contains("rules") && ext == "md" {
62            return Ok(Some(workflow::parse_workflow(path)?));
63        }
64
65        // MCP configs: mcp.json or similar
66        if path_str.to_lowercase().contains("mcp") && ext == "json" {
67             // Try to parse using McpScanner logic which handles various formats
68             if let Some(artifact) = mcp::McpScanner::scan_file(path)? {
69                 return Ok(Some(artifact));
70             }
71        }
72        
73        // Generic Scripts (Phase 6 support): .sh, .py, .js, .ts, .rs, .go, .java, .txt, .pem
74        // We match against known extensions for our test fixtures/common scripts
75        let script_exts = ["sh", "bash", "py", "js", "ts", "rs", "go", "java", "pl", "rb", "php", "txt", "pem", "json"];
76        if script_exts.contains(&ext.as_str()) {
77             // Avoid parsing MCP JSONs twice if they fell through
78             if ext == "json" && path_str.contains("mcp") {
79                 // Already handled or ignored above
80             } else {
81                 return Ok(Some(generic::parse_generic_file(path)?));
82             }
83        }
84
85        Ok(None)
86    }
87}