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