Skip to main content

agentshield/adapter/
crewai.rs

1use std::path::Path;
2
3use crate::config::ScanPathFilter;
4use crate::error::Result;
5use crate::ir::*;
6
7/// CrewAI framework adapter.
8///
9/// Detects CrewAI projects by looking for:
10/// - `pyproject.toml` with `crewai` dependency or `[tool.crewai]` section
11/// - `requirements.txt` containing `crewai`
12/// - Python files importing `from crewai` or `from crewai_tools`
13pub 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        // Should find command injection in vuln_tool.py
94        assert!(
95            !target.execution.commands.is_empty(),
96            "expected command execution findings from vuln_tool.py"
97        );
98        // Should find tainted command args
99        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        // Should find network operations in fetch_tool.py
117        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        // All source files should be Python
148        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}