agentshield/adapter/
crewai.rs1use std::path::Path;
2
3use crate::config::ScanPathFilter;
4use crate::error::Result;
5use crate::ir::taint_builder::build_data_surface;
6use crate::ir::*;
7
8pub struct CrewAiAdapter;
15
16impl super::Adapter for CrewAiAdapter {
17 fn framework(&self) -> Framework {
18 Framework::CrewAi
19 }
20
21 fn detect(&self, root: &Path) -> bool {
22 let pyproject = root.join("pyproject.toml");
24 if pyproject.exists() {
25 if let Ok(content) = std::fs::read_to_string(&pyproject) {
26 if content.contains("crewai") {
27 return true;
28 }
29 }
30 }
31
32 let requirements = root.join("requirements.txt");
34 if requirements.exists() {
35 if let Ok(content) = std::fs::read_to_string(&requirements) {
36 if content.lines().any(|l| {
37 let trimmed = l.trim();
38 trimmed == "crewai"
39 || trimmed.starts_with("crewai==")
40 || trimmed.starts_with("crewai>=")
41 || trimmed.starts_with("crewai[")
42 || trimmed == "crewai-tools"
43 || trimmed.starts_with("crewai-tools==")
44 || trimmed.starts_with("crewai-tools>=")
45 }) {
46 return true;
47 }
48 }
49 }
50
51 if super::mcp::has_recursive_python_import(
53 root,
54 &[
55 "from crewai",
56 "import crewai",
57 "from crewai_tools",
58 "import crewai_tools",
59 ],
60 ) {
61 return true;
62 }
63
64 false
65 }
66
67 fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
68 let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
69 self.load_with_filter(root, &filter)
70 }
71
72 fn load_with_filter(&self, root: &Path, filter: &ScanPathFilter) -> Result<Vec<ScanTarget>> {
73 let name = root
74 .file_name()
75 .map(|n| n.to_string_lossy().to_string())
76 .unwrap_or_else(|| "crewai-project".into());
77
78 let mut source_files = Vec::new();
79 super::mcp::collect_source_files_with_filter(root, filter, &mut source_files)?;
81
82 source_files.retain(|sf| matches!(sf.language, Language::Python));
84
85 let execution = super::pipeline::build_execution_surface(&source_files);
86
87 let dependencies = super::mcp::parse_dependencies(root, filter);
89
90 let provenance = super::mcp::parse_provenance(root, filter);
92
93 let tools = vec![];
94 let data = build_data_surface(&tools, &execution);
95
96 Ok(vec![ScanTarget {
97 name,
98 framework: Framework::CrewAi,
99 root_path: root.to_path_buf(),
100 tools,
101 execution,
102 data,
103 dependencies,
104 provenance,
105 source_files,
106 }])
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::adapter::Adapter;
114 use std::io::Write;
115 use std::path::PathBuf;
116 use tempfile::TempDir;
117
118 #[test]
119 fn test_detect_crewai_via_pyproject() {
120 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
121 let adapter = CrewAiAdapter;
122 assert!(adapter.detect(&dir));
123 }
124
125 #[test]
126 fn test_detect_non_crewai_project() {
127 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
128 .join("tests/fixtures/mcp_servers/safe_calculator");
129 let adapter = CrewAiAdapter;
130 assert!(!adapter.detect(&dir));
131 }
132
133 #[test]
134 fn test_load_crewai_finds_cmd_injection() {
135 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
136 let adapter = CrewAiAdapter;
137 let targets = adapter.load(&dir, false).unwrap();
138 assert_eq!(targets.len(), 1);
139
140 let target = &targets[0];
141 assert_eq!(target.framework, Framework::CrewAi);
142 assert_eq!(target.name, "crewai_project");
143
144 assert!(
146 !target.execution.commands.is_empty(),
147 "expected command execution findings from vuln_tool.py"
148 );
149 assert!(
151 target
152 .execution
153 .commands
154 .iter()
155 .any(|c| c.command_arg.is_tainted()),
156 "expected tainted command source from subprocess.run with user input"
157 );
158 }
159
160 #[test]
161 fn test_load_crewai_finds_ssrf() {
162 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
163 let adapter = CrewAiAdapter;
164 let targets = adapter.load(&dir, false).unwrap();
165 let target = &targets[0];
166
167 assert!(
169 !target.execution.network_operations.is_empty(),
170 "expected network operation findings from fetch_tool.py"
171 );
172 }
173
174 #[test]
175 fn test_load_crewai_parses_dependencies() {
176 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
177 let adapter = CrewAiAdapter;
178 let targets = adapter.load(&dir, false).unwrap();
179 let target = &targets[0];
180
181 assert!(
182 target
183 .dependencies
184 .dependencies
185 .iter()
186 .any(|d| d.name == "crewai"),
187 "expected crewai in dependencies"
188 );
189 }
190
191 #[test]
192 fn test_load_crewai_only_python_files() {
193 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
194 let adapter = CrewAiAdapter;
195 let targets = adapter.load(&dir, false).unwrap();
196 let target = &targets[0];
197
198 for sf in &target.source_files {
200 assert_eq!(
201 sf.language,
202 Language::Python,
203 "non-Python file found: {:?}",
204 sf.path
205 );
206 }
207 }
208
209 #[test]
210 fn test_detect_crewai_nested_python_import() {
211 let tmp_root = TempDir::new().unwrap();
212 let nested_dir = tmp_root.path().join("src/pkg");
213 std::fs::create_dir_all(&nested_dir).unwrap();
214
215 let mut file = std::fs::File::create(nested_dir.join("tool.py")).unwrap();
216 writeln!(file, "from crewai_tools import BaseTool").unwrap();
217
218 let adapter = CrewAiAdapter;
219 assert!(adapter.detect(tmp_root.path()));
220 }
221}