agentshield/adapter/
crewai.rs1use std::path::Path;
2
3use crate::config::ScanPathFilter;
4use crate::error::Result;
5use crate::ir::*;
6
7pub struct CrewAiAdapter;
14
15impl super::Adapter for CrewAiAdapter {
16 fn framework(&self) -> Framework {
17 Framework::CrewAi
18 }
19
20 fn detect(&self, root: &Path) -> bool {
21 super::pipeline::detect_python_framework(
22 root,
23 &["crewai"],
24 |trimmed| {
25 trimmed == "crewai"
26 || trimmed.starts_with("crewai==")
27 || trimmed.starts_with("crewai>=")
28 || trimmed.starts_with("crewai[")
29 || trimmed == "crewai-tools"
30 || trimmed.starts_with("crewai-tools==")
31 || trimmed.starts_with("crewai-tools>=")
32 },
33 &[
34 "from crewai",
35 "import crewai",
36 "from crewai_tools",
37 "import crewai_tools",
38 ],
39 )
40 }
41
42 fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
43 let filter = ScanPathFilter::for_ignore_tests(ignore_tests);
44 self.load_with_filter(root, &filter)
45 }
46
47 fn load_with_filter(&self, root: &Path, filter: &ScanPathFilter) -> Result<Vec<ScanTarget>> {
48 super::pipeline::load_framework_target(
49 root,
50 filter,
51 Framework::CrewAi,
52 "crewai-project",
53 |lang| matches!(lang, Language::Python),
54 None,
55 )
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::adapter::Adapter;
63 use std::io::Write;
64 use std::path::PathBuf;
65 use tempfile::TempDir;
66
67 #[test]
68 fn test_detect_crewai_via_pyproject() {
69 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
70 let adapter = CrewAiAdapter;
71 assert!(adapter.detect(&dir));
72 }
73
74 #[test]
75 fn test_detect_non_crewai_project() {
76 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
77 .join("tests/fixtures/mcp_servers/safe_calculator");
78 let adapter = CrewAiAdapter;
79 assert!(!adapter.detect(&dir));
80 }
81
82 #[test]
83 fn test_load_crewai_finds_cmd_injection() {
84 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
85 let adapter = CrewAiAdapter;
86 let targets = adapter.load(&dir, false).unwrap();
87 assert_eq!(targets.len(), 1);
88
89 let target = &targets[0];
90 assert_eq!(target.framework, Framework::CrewAi);
91 assert_eq!(target.name, "crewai_project");
92
93 assert!(
95 !target.execution.commands.is_empty(),
96 "expected command execution findings from vuln_tool.py"
97 );
98 assert!(
100 target
101 .execution
102 .commands
103 .iter()
104 .any(|c| c.command_arg.is_tainted()),
105 "expected tainted command source from subprocess.run with user input"
106 );
107 }
108
109 #[test]
110 fn test_load_crewai_finds_ssrf() {
111 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
112 let adapter = CrewAiAdapter;
113 let targets = adapter.load(&dir, false).unwrap();
114 let target = &targets[0];
115
116 assert!(
118 !target.execution.network_operations.is_empty(),
119 "expected network operation findings from fetch_tool.py"
120 );
121 }
122
123 #[test]
124 fn test_load_crewai_parses_dependencies() {
125 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
126 let adapter = CrewAiAdapter;
127 let targets = adapter.load(&dir, false).unwrap();
128 let target = &targets[0];
129
130 assert!(
131 target
132 .dependencies
133 .dependencies
134 .iter()
135 .any(|d| d.name == "crewai"),
136 "expected crewai in dependencies"
137 );
138 }
139
140 #[test]
141 fn test_load_crewai_only_python_files() {
142 let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/crewai_project");
143 let adapter = CrewAiAdapter;
144 let targets = adapter.load(&dir, false).unwrap();
145 let target = &targets[0];
146
147 for sf in &target.source_files {
149 assert_eq!(
150 sf.language,
151 Language::Python,
152 "non-Python file found: {:?}",
153 sf.path
154 );
155 }
156 }
157
158 #[test]
159 fn test_detect_crewai_nested_python_import() {
160 let tmp_root = TempDir::new().unwrap();
161 let nested_dir = tmp_root.path().join("src/pkg");
162 std::fs::create_dir_all(&nested_dir).unwrap();
163
164 let mut file = std::fs::File::create(nested_dir.join("tool.py")).unwrap();
165 writeln!(file, "from crewai_tools import BaseTool").unwrap();
166
167 let adapter = CrewAiAdapter;
168 assert!(adapter.detect(tmp_root.path()));
169 }
170}