Skip to main content

cc_audit/discovery/
targets.rs

1//! Scan target definitions.
2
3use std::path::PathBuf;
4
5/// The kind of scan target.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum TargetKind {
8    /// SKILL.md or CLAUDE.md file
9    Skill,
10    /// Command definition file (.claude/commands/*.md)
11    Command,
12    /// MCP configuration file (mcp.json, .mcp.json)
13    Mcp,
14    /// Hook configuration file (settings.json)
15    Hook,
16    /// Dependency manifest file (package.json, Cargo.toml, etc.)
17    Dependency,
18    /// Docker-related file (Dockerfile, docker-compose.yml)
19    Docker,
20    /// Plugin manifest file (plugin.json, marketplace.json)
21    Plugin,
22    /// Subagent definition file (.claude/agents/*.md)
23    Subagent,
24    /// Rules directory file (.claude/rules/*.md)
25    RulesDir,
26    /// Generic text file
27    TextFile,
28    /// Unknown file type
29    Unknown,
30}
31
32impl TargetKind {
33    /// Get the target kind from a file path.
34    pub fn from_path(path: &std::path::Path) -> Self {
35        let file_name = path
36            .file_name()
37            .and_then(|n| n.to_str())
38            .unwrap_or_default();
39
40        // Check specific file names first
41        match file_name {
42            "SKILL.md" | "CLAUDE.md" => return Self::Skill,
43            "mcp.json" | ".mcp.json" => return Self::Mcp,
44            "settings.json" => return Self::Hook,
45            "plugin.json" | "marketplace.json" => return Self::Plugin,
46            "Dockerfile" | "dockerfile" => return Self::Docker,
47            "package.json" | "Cargo.toml" | "requirements.txt" | "Pipfile" | "pyproject.toml"
48            | "go.mod" | "Gemfile" | "composer.json" | "pom.xml" => return Self::Dependency,
49            _ => {}
50        }
51
52        // Check by path components
53        let path_str = path.to_string_lossy();
54
55        if (path_str.contains(".claude/commands/")
56            || path_str.contains("/commands/")
57            || path_str.starts_with("commands/"))
58            && file_name.ends_with(".md")
59        {
60            return Self::Command;
61        }
62
63        if path_str.contains(".claude/agents/") || path_str.starts_with(".claude/agents/") {
64            return Self::Subagent;
65        }
66
67        if path_str.contains(".claude/rules/")
68            || path_str.contains("/rules/")
69            || path_str.starts_with("rules/")
70            || path_str.starts_with(".claude/rules/")
71        {
72            return Self::RulesDir;
73        }
74
75        // Check by extension
76        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
77            match ext {
78                "md" => {
79                    if path_str.contains("scripts/") || path_str.contains(".claude/") {
80                        return Self::Skill;
81                    }
82                    return Self::TextFile;
83                }
84                "json" => return Self::Unknown,
85                "yml" | "yaml" => {
86                    if file_name.contains("docker-compose") || file_name.contains("compose") {
87                        return Self::Docker;
88                    }
89                    return Self::Unknown;
90                }
91                _ => return Self::Unknown,
92            }
93        }
94
95        Self::Unknown
96    }
97}
98
99/// A scan target representing a file or directory to be scanned.
100#[derive(Debug, Clone)]
101pub struct ScanTarget {
102    /// The path to the target.
103    pub path: PathBuf,
104    /// The kind of target.
105    pub kind: TargetKind,
106    /// The discovery source (how this target was found).
107    pub source: DiscoverySource,
108}
109
110/// How a scan target was discovered.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum DiscoverySource {
113    /// Explicitly specified by user (CLI argument).
114    UserSpecified,
115    /// Discovered via file system traversal.
116    FileSystem,
117    /// Discovered via client auto-detection (Claude, Cursor, etc.).
118    ClientDetection,
119    /// Discovered via remote URL.
120    Remote,
121}
122
123impl ScanTarget {
124    /// Create a new scan target.
125    pub fn new(path: PathBuf, kind: TargetKind, source: DiscoverySource) -> Self {
126        Self { path, kind, source }
127    }
128
129    /// Create a scan target from a path, auto-detecting the kind.
130    pub fn from_path(path: PathBuf, source: DiscoverySource) -> Self {
131        let kind = TargetKind::from_path(&path);
132        Self { path, kind, source }
133    }
134
135    /// Create a user-specified scan target.
136    pub fn user_specified(path: PathBuf) -> Self {
137        Self::from_path(path, DiscoverySource::UserSpecified)
138    }
139
140    /// Create a file system discovered scan target.
141    pub fn discovered(path: PathBuf) -> Self {
142        Self::from_path(path, DiscoverySource::FileSystem)
143    }
144
145    /// Create a client-detected scan target.
146    pub fn from_client(path: PathBuf) -> Self {
147        Self::from_path(path, DiscoverySource::ClientDetection)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::path::Path;
155
156    #[test]
157    fn test_target_kind_skill() {
158        assert_eq!(
159            TargetKind::from_path(Path::new("SKILL.md")),
160            TargetKind::Skill
161        );
162        assert_eq!(
163            TargetKind::from_path(Path::new("CLAUDE.md")),
164            TargetKind::Skill
165        );
166        assert_eq!(
167            TargetKind::from_path(Path::new(".claude/CLAUDE.md")),
168            TargetKind::Skill
169        );
170    }
171
172    #[test]
173    fn test_target_kind_command() {
174        assert_eq!(
175            TargetKind::from_path(Path::new(".claude/commands/test.md")),
176            TargetKind::Command
177        );
178        assert_eq!(
179            TargetKind::from_path(Path::new("commands/deploy.md")),
180            TargetKind::Command
181        );
182    }
183
184    #[test]
185    fn test_target_kind_mcp() {
186        assert_eq!(
187            TargetKind::from_path(Path::new("mcp.json")),
188            TargetKind::Mcp
189        );
190        assert_eq!(
191            TargetKind::from_path(Path::new(".mcp.json")),
192            TargetKind::Mcp
193        );
194    }
195
196    #[test]
197    fn test_target_kind_dependency() {
198        assert_eq!(
199            TargetKind::from_path(Path::new("package.json")),
200            TargetKind::Dependency
201        );
202        assert_eq!(
203            TargetKind::from_path(Path::new("Cargo.toml")),
204            TargetKind::Dependency
205        );
206        assert_eq!(
207            TargetKind::from_path(Path::new("requirements.txt")),
208            TargetKind::Dependency
209        );
210    }
211
212    #[test]
213    fn test_target_kind_docker() {
214        assert_eq!(
215            TargetKind::from_path(Path::new("Dockerfile")),
216            TargetKind::Docker
217        );
218        assert_eq!(
219            TargetKind::from_path(Path::new("docker-compose.yml")),
220            TargetKind::Docker
221        );
222    }
223
224    #[test]
225    fn test_target_kind_plugin() {
226        assert_eq!(
227            TargetKind::from_path(Path::new("plugin.json")),
228            TargetKind::Plugin
229        );
230        assert_eq!(
231            TargetKind::from_path(Path::new("marketplace.json")),
232            TargetKind::Plugin
233        );
234    }
235
236    #[test]
237    fn test_target_kind_subagent() {
238        assert_eq!(
239            TargetKind::from_path(Path::new(".claude/agents/helper.md")),
240            TargetKind::Subagent
241        );
242    }
243
244    #[test]
245    fn test_target_kind_rules_dir() {
246        assert_eq!(
247            TargetKind::from_path(Path::new(".claude/rules/custom.md")),
248            TargetKind::RulesDir
249        );
250        assert_eq!(
251            TargetKind::from_path(Path::new("rules/security.md")),
252            TargetKind::RulesDir
253        );
254    }
255
256    #[test]
257    fn test_scan_target_creation() {
258        let target = ScanTarget::user_specified(PathBuf::from("SKILL.md"));
259        assert_eq!(target.kind, TargetKind::Skill);
260        assert_eq!(target.source, DiscoverySource::UserSpecified);
261
262        let target = ScanTarget::discovered(PathBuf::from("package.json"));
263        assert_eq!(target.kind, TargetKind::Dependency);
264        assert_eq!(target.source, DiscoverySource::FileSystem);
265
266        let target = ScanTarget::from_client(PathBuf::from(".claude/CLAUDE.md"));
267        assert_eq!(target.kind, TargetKind::Skill);
268        assert_eq!(target.source, DiscoverySource::ClientDetection);
269    }
270}