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