Skip to main content

cc_audit/discovery/
patterns.rs

1//! File patterns for scan target discovery.
2
3use std::path::Path;
4
5/// A file pattern specification for matching scan targets.
6#[derive(Debug, Clone)]
7pub struct FilePattern {
8    /// Root directories to search in.
9    pub root_dirs: &'static [&'static str],
10    /// File extensions to match.
11    pub extensions: &'static [&'static str],
12    /// Specific file names to match.
13    pub file_names: &'static [&'static str],
14}
15
16impl FilePattern {
17    /// Check if a path matches this pattern.
18    pub fn matches(&self, path: &Path) -> bool {
19        // Check specific file names first
20        if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
21            && self.file_names.contains(&file_name)
22        {
23            return true;
24        }
25
26        // Check extensions
27        if !self.extensions.is_empty() {
28            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
29                return self.extensions.contains(&ext);
30            }
31            return false;
32        }
33
34        true
35    }
36
37    /// Check if a path is within one of the root directories.
38    pub fn is_in_root_dir(&self, path: &Path, base: &Path) -> bool {
39        if self.root_dirs.is_empty() {
40            return true;
41        }
42
43        for root in self.root_dirs {
44            let root_path = base.join(root);
45            if path.starts_with(&root_path) {
46                return true;
47            }
48        }
49
50        false
51    }
52}
53
54/// Patterns for SKILL.md and CLAUDE.md files.
55pub static SKILL_PATTERNS: FilePattern = FilePattern {
56    root_dirs: &[".claude", "scripts"],
57    extensions: &["md"],
58    file_names: &["SKILL.md", "CLAUDE.md"],
59};
60
61/// Patterns for command definition files.
62pub static COMMAND_PATTERNS: FilePattern = FilePattern {
63    root_dirs: &[".claude/commands", "commands"],
64    extensions: &["md"],
65    file_names: &[],
66};
67
68/// Patterns for MCP configuration files.
69pub static MCP_PATTERNS: FilePattern = FilePattern {
70    root_dirs: &[".claude"],
71    extensions: &["json"],
72    file_names: &["mcp.json", ".mcp.json"],
73};
74
75/// Patterns for dependency manifest files.
76pub static DEPENDENCY_PATTERNS: FilePattern = FilePattern {
77    root_dirs: &[],
78    extensions: &["json", "toml", "txt", "lock"],
79    file_names: &[
80        "package.json",
81        "package-lock.json",
82        "Cargo.toml",
83        "Cargo.lock",
84        "requirements.txt",
85        "Pipfile",
86        "Pipfile.lock",
87        "pyproject.toml",
88        "poetry.lock",
89        "go.mod",
90        "go.sum",
91        "Gemfile",
92        "Gemfile.lock",
93        "composer.json",
94        "composer.lock",
95        "pom.xml",
96        "build.gradle",
97        "build.gradle.kts",
98    ],
99};
100
101/// Patterns for Docker-related files.
102pub static DOCKER_PATTERNS: FilePattern = FilePattern {
103    root_dirs: &[],
104    extensions: &["yml", "yaml"],
105    file_names: &[
106        "Dockerfile",
107        "dockerfile",
108        "docker-compose.yml",
109        "docker-compose.yaml",
110        "compose.yml",
111        "compose.yaml",
112    ],
113};
114
115/// Patterns for hook configuration files.
116pub static HOOK_PATTERNS: FilePattern = FilePattern {
117    root_dirs: &[".claude"],
118    extensions: &["json"],
119    file_names: &["settings.json"],
120};
121
122/// Patterns for subagent definition files.
123pub static SUBAGENT_PATTERNS: FilePattern = FilePattern {
124    root_dirs: &[".claude/agents"],
125    extensions: &["md", "yaml", "yml"],
126    file_names: &[],
127};
128
129/// Patterns for rules directory files.
130pub static RULES_DIR_PATTERNS: FilePattern = FilePattern {
131    root_dirs: &[".claude/rules", "rules"],
132    extensions: &["md", "yaml", "yml"],
133    file_names: &[],
134};
135
136/// Patterns for plugin manifest files.
137pub static PLUGIN_PATTERNS: FilePattern = FilePattern {
138    root_dirs: &[],
139    extensions: &["json"],
140    file_names: &["plugin.json", "marketplace.json"],
141};
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_skill_patterns_matches_skill_md() {
149        assert!(SKILL_PATTERNS.matches(Path::new("SKILL.md")));
150        assert!(SKILL_PATTERNS.matches(Path::new("path/to/SKILL.md")));
151    }
152
153    #[test]
154    fn test_skill_patterns_matches_claude_md() {
155        assert!(SKILL_PATTERNS.matches(Path::new("CLAUDE.md")));
156        assert!(SKILL_PATTERNS.matches(Path::new(".claude/CLAUDE.md")));
157    }
158
159    #[test]
160    fn test_skill_patterns_matches_md_extension() {
161        assert!(SKILL_PATTERNS.matches(Path::new("readme.md")));
162        assert!(!SKILL_PATTERNS.matches(Path::new("file.txt")));
163    }
164
165    #[test]
166    fn test_command_patterns() {
167        assert!(COMMAND_PATTERNS.matches(Path::new("test.md")));
168        assert!(!COMMAND_PATTERNS.matches(Path::new("test.txt")));
169    }
170
171    #[test]
172    fn test_mcp_patterns() {
173        assert!(MCP_PATTERNS.matches(Path::new("mcp.json")));
174        assert!(MCP_PATTERNS.matches(Path::new(".mcp.json")));
175        assert!(!MCP_PATTERNS.matches(Path::new("config.yaml")));
176    }
177
178    #[test]
179    fn test_dependency_patterns() {
180        assert!(DEPENDENCY_PATTERNS.matches(Path::new("package.json")));
181        assert!(DEPENDENCY_PATTERNS.matches(Path::new("Cargo.toml")));
182        assert!(DEPENDENCY_PATTERNS.matches(Path::new("requirements.txt")));
183        assert!(!DEPENDENCY_PATTERNS.matches(Path::new("README.md")));
184    }
185
186    #[test]
187    fn test_docker_patterns() {
188        assert!(DOCKER_PATTERNS.matches(Path::new("Dockerfile")));
189        assert!(DOCKER_PATTERNS.matches(Path::new("docker-compose.yml")));
190        assert!(DOCKER_PATTERNS.matches(Path::new("compose.yaml")));
191    }
192
193    #[test]
194    fn test_is_in_root_dir() {
195        let base = Path::new("/project");
196
197        assert!(SKILL_PATTERNS.is_in_root_dir(Path::new("/project/.claude/CLAUDE.md"), base));
198        assert!(SKILL_PATTERNS.is_in_root_dir(Path::new("/project/scripts/helper.md"), base));
199        assert!(!SKILL_PATTERNS.is_in_root_dir(Path::new("/project/other/file.md"), base));
200    }
201}