Skip to main content

agentshield/adapter/
openclaw.rs

1use 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
9/// OpenClaw Skills adapter.
10///
11/// Detects by presence of `SKILL.md` file.
12pub struct OpenClawAdapter;
13
14impl super::Adapter for OpenClawAdapter {
15    fn framework(&self) -> Framework {
16        Framework::OpenClaw
17    }
18
19    fn detect(&self, root: &Path) -> bool {
20        root.join("SKILL.md").exists()
21    }
22
23    fn load(&self, root: &Path, ignore_tests: bool) -> Result<Vec<ScanTarget>> {
24        let name = root
25            .file_name()
26            .map(|n| n.to_string_lossy().to_string())
27            .unwrap_or_else(|| "openclaw-skill".into());
28
29        let mut source_files = Vec::new();
30        let mut execution = execution_surface::ExecutionSurface::default();
31
32        // Collect source files (Python and Shell scripts)
33        let walker = ignore::WalkBuilder::new(root)
34            .hidden(true)
35            .git_ignore(true)
36            .max_depth(Some(3))
37            .build();
38
39        for entry in walker.flatten() {
40            let path = entry.path();
41            if !path.is_file() {
42                continue;
43            }
44
45            if ignore_tests && super::mcp::is_test_file(path) {
46                continue;
47            }
48
49            let ext = path
50                .extension()
51                .map(|e| e.to_string_lossy().to_string())
52                .unwrap_or_default();
53            let lang = Language::from_extension(&ext);
54
55            if !matches!(
56                lang,
57                Language::Python | Language::Shell | Language::Markdown
58            ) {
59                continue;
60            }
61
62            let metadata = std::fs::metadata(path)?;
63            if metadata.len() > 1_048_576 {
64                continue;
65            }
66
67            if let Ok(content) = std::fs::read_to_string(path) {
68                use sha2::Digest;
69                let hash = format!(
70                    "{:x}",
71                    sha2::Sha256::new()
72                        .chain_update(content.as_bytes())
73                        .finalize()
74                );
75                source_files.push(SourceFile {
76                    path: path.to_path_buf(),
77                    language: lang,
78                    size_bytes: metadata.len(),
79                    content_hash: hash,
80                    content,
81                });
82            }
83        }
84
85        // Phase 1: Parse source files
86        let mut parsed_files: Vec<(PathBuf, parser::ParsedFile)> = Vec::new();
87        for sf in &source_files {
88            if let Some(parser) = parser::parser_for_language(sf.language) {
89                if let Ok(parsed) = parser.parse_file(&sf.path, &sf.content) {
90                    parsed_files.push((sf.path.clone(), parsed));
91                }
92            }
93        }
94
95        // Phase 2: Cross-file sanitizer-aware analysis
96        apply_cross_file_sanitization(&mut parsed_files);
97
98        // Phase 3: Merge into execution surface
99        for (_, parsed) in parsed_files {
100            execution.commands.extend(parsed.commands);
101            execution.file_operations.extend(parsed.file_operations);
102            execution
103                .network_operations
104                .extend(parsed.network_operations);
105            execution.env_accesses.extend(parsed.env_accesses);
106            execution.dynamic_exec.extend(parsed.dynamic_exec);
107        }
108
109        let tools = vec![];
110        let data = build_data_surface(&tools, &execution);
111
112        Ok(vec![ScanTarget {
113            name,
114            framework: Framework::OpenClaw,
115            root_path: root.to_path_buf(),
116            tools,
117            execution,
118            data,
119            dependencies: Default::default(),
120            provenance: Default::default(),
121            source_files,
122        }])
123    }
124}