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