agentshield/adapter/
crewai.rs1use std::path::{Path, PathBuf};
2
3use crate::analysis::cross_file::apply_cross_file_sanitization;
4use crate::config::ScanPathFilter;
5use crate::error::Result;
6use crate::ir::taint_builder::build_data_surface;
7use crate::ir::*;
8use crate::parser;
9
10pub struct CrewAiAdapter;
17
18impl super::Adapter for CrewAiAdapter {
19 fn framework(&self) -> Framework {
20 Framework::CrewAi
21 }
22
23 fn detect(&self, root: &Path) -> bool {
24 let pyproject = root.join("pyproject.toml");
26 if pyproject.exists() {
27 if let Ok(content) = std::fs::read_to_string(&pyproject) {
28 if content.contains("crewai") {
29 return true;
30 }
31 }
32 }
33
34 let requirements = root.join("requirements.txt");
36 if requirements.exists() {
37 if let Ok(content) = std::fs::read_to_string(&requirements) {
38 if content.lines().any(|l| {
39 let trimmed = l.trim();
40 trimmed == "crewai"
41 || trimmed.starts_with("crewai==")
42 || trimmed.starts_with("crewai>=")
43 || trimmed.starts_with("crewai[")
44 || trimmed == "crewai-tools"
45 || trimmed.starts_with("crewai-tools==")
46 || trimmed.starts_with("crewai-tools>=")
47 }) {
48 return true;
49 }
50 }
51 }
52
53 if let Ok(entries) = std::fs::read_dir(root) {
55 for entry in entries.flatten() {
56 let path = entry.path();
57 if path.extension().is_some_and(|e| e == "py") {
58 if let Ok(content) = std::fs::read_to_string(&path) {
59 if content.contains("from crewai")
60 || content.contains("import crewai")
61 || content.contains("from crewai_tools")
62 || content.contains("import crewai_tools")
63 {
64 return true;
65 }
66 }
67 }
68 }
69 }
70
71 let src_dir = root.join("src");
73 if src_dir.is_dir() {
74 if let Ok(entries) = std::fs::read_dir(&src_dir) {
75 for entry in entries.flatten() {
76 let path = entry.path();
77 if path.extension().is_some_and(|e| e == "py") {
78 if let Ok(content) = std::fs::read_to_string(&path) {
79 if content.contains("from crewai")
80 || content.contains("import crewai")
81 || content.contains("from crewai_tools")
82 || content.contains("import crewai_tools")
83 {
84 return true;
85 }
86 }
87 }
88 }
89 }
90 }
91
92 false
93 }
94
95 fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
96 let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
97 self.load_with_filter(root, &filter)
98 }
99
100 fn load_with_filter(&self, root: &Path, filter: &ScanPathFilter) -> Result<Vec<ScanTarget>> {
101 let name = root
102 .file_name()
103 .map(|n| n.to_string_lossy().to_string())
104 .unwrap_or_else(|| "crewai-project".into());
105
106 let mut source_files = Vec::new();
107 let mut execution = execution_surface::ExecutionSurface::default();
108
109 super::mcp::collect_source_files_with_filter(root, filter, &mut source_files)?;
111
112 source_files.retain(|sf| matches!(sf.language, Language::Python));
114
115 let mut parsed_files: Vec<(PathBuf, parser::ParsedFile)> = Vec::new();
117 for sf in &source_files {
118 if let Some(parser) = parser::parser_for_language(sf.language) {
119 if let Ok(parsed) = parser.parse_file(&sf.path, &sf.content) {
120 parsed_files.push((sf.path.clone(), parsed));
121 }
122 }
123 }
124
125 apply_cross_file_sanitization(&mut parsed_files);
127
128 for (_, parsed) in parsed_files {
130 execution.commands.extend(parsed.commands);
131 execution.file_operations.extend(parsed.file_operations);
132 execution
133 .network_operations
134 .extend(parsed.network_operations);
135 execution.env_accesses.extend(parsed.env_accesses);
136 execution.dynamic_exec.extend(parsed.dynamic_exec);
137 }
138
139 let dependencies = super::mcp::parse_dependencies(root, filter);
141
142 let provenance = super::mcp::parse_provenance(root, filter);
144
145 let tools = vec![];
146 let data = build_data_surface(&tools, &execution);
147
148 Ok(vec![ScanTarget {
149 name,
150 framework: Framework::CrewAi,
151 root_path: root.to_path_buf(),
152 tools,
153 execution,
154 data,
155 dependencies,
156 provenance,
157 source_files,
158 }])
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::adapter::Adapter;
166
167 #[test]
168 fn test_detect_crewai_via_pyproject() {
169 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
170 let adapter = CrewAiAdapter;
171 assert!(adapter.detect(&dir));
172 }
173
174 #[test]
175 fn test_detect_non_crewai_project() {
176 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
177 .join("tests/fixtures/mcp_servers/safe_calculator");
178 let adapter = CrewAiAdapter;
179 assert!(!adapter.detect(&dir));
180 }
181
182 #[test]
183 fn test_load_crewai_finds_cmd_injection() {
184 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
185 let adapter = CrewAiAdapter;
186 let targets = adapter.load(&dir, false).unwrap();
187 assert_eq!(targets.len(), 1);
188
189 let target = &targets[0];
190 assert_eq!(target.framework, Framework::CrewAi);
191 assert_eq!(target.name, "crewai_project");
192
193 assert!(
195 !target.execution.commands.is_empty(),
196 "expected command execution findings from vuln_tool.py"
197 );
198 assert!(
200 target
201 .execution
202 .commands
203 .iter()
204 .any(|c| c.command_arg.is_tainted()),
205 "expected tainted command source from subprocess.run with user input"
206 );
207 }
208
209 #[test]
210 fn test_load_crewai_finds_ssrf() {
211 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
212 let adapter = CrewAiAdapter;
213 let targets = adapter.load(&dir, false).unwrap();
214 let target = &targets[0];
215
216 assert!(
218 !target.execution.network_operations.is_empty(),
219 "expected network operation findings from fetch_tool.py"
220 );
221 }
222
223 #[test]
224 fn test_load_crewai_parses_dependencies() {
225 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
226 let adapter = CrewAiAdapter;
227 let targets = adapter.load(&dir, false).unwrap();
228 let target = &targets[0];
229
230 assert!(
231 target
232 .dependencies
233 .dependencies
234 .iter()
235 .any(|d| d.name == "crewai"),
236 "expected crewai in dependencies"
237 );
238 }
239
240 #[test]
241 fn test_load_crewai_only_python_files() {
242 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
243 let adapter = CrewAiAdapter;
244 let targets = adapter.load(&dir, false).unwrap();
245 let target = &targets[0];
246
247 for sf in &target.source_files {
249 assert_eq!(
250 sf.language,
251 Language::Python,
252 "non-Python file found: {:?}",
253 sf.path
254 );
255 }
256 }
257}