Skip to main content

openclaw_scan/scanner/
mod.rs

1//! Scanner orchestration — trait definition and parallel runner.
2
3pub mod config;
4pub mod dependencies;
5pub mod history;
6pub mod hooks;
7pub mod network;
8pub mod permissions;
9pub mod secrets;
10
11use anyhow::Result;
12use rayon::prelude::*;
13
14use crate::finding::Finding;
15use crate::paths::{FrameworkHint, InstallRoot};
16
17// ── ScanContext ───────────────────────────────────────────────────────────────
18
19/// Shared context passed to every scanner.
20#[derive(Debug, Clone)]
21pub struct ScanContext {
22    /// Resolved installation root (any supported framework).
23    pub root: std::path::PathBuf,
24    /// Which framework was detected — informational only.
25    #[allow(dead_code)]
26    pub framework: FrameworkHint,
27}
28
29impl ScanContext {
30    #[allow(dead_code)]
31    pub fn from_root(root: &InstallRoot) -> Self {
32        ScanContext {
33            root: root.path.clone(),
34            framework: root.framework,
35        }
36    }
37
38    /// Return `root / sub` if it exists, otherwise `None`.
39    #[allow(dead_code)]
40    pub fn subpath(&self, sub: &str) -> Option<std::path::PathBuf> {
41        let p = self.root.join(sub);
42        if p.exists() {
43            Some(p)
44        } else {
45            None
46        }
47    }
48}
49
50// ── Scanner trait ─────────────────────────────────────────────────────────────
51
52/// Every scanner implements this trait.
53pub trait Scanner: Send + Sync {
54    fn name(&self) -> &'static str;
55    fn scan(&self, ctx: &ScanContext) -> Result<Vec<Finding>>;
56}
57
58// ── run_all ───────────────────────────────────────────────────────────────────
59
60/// Run all scanners in parallel and collect every finding.
61///
62/// Scanner errors are logged to stderr and treated as empty results so a
63/// single broken scanner never blocks the full report.
64pub fn run_all(ctx: &ScanContext) -> Vec<Finding> {
65    let scanners: Vec<Box<dyn Scanner>> = vec![
66        Box::new(secrets::SecretsScanner),
67        Box::new(permissions::PermissionsScanner),
68        Box::new(config::ConfigScanner),
69        Box::new(network::NetworkScanner),
70        Box::new(hooks::HooksScanner),
71        Box::new(dependencies::DependenciesScanner),
72        Box::new(history::HistoryScanner),
73    ];
74
75    scanners
76        .par_iter()
77        .flat_map(|s| match s.scan(ctx) {
78            Ok(findings) => findings,
79            Err(e) => {
80                eprintln!("ocls: scanner '{}' error: {}", s.name(), e);
81                vec![]
82            }
83        })
84        .collect()
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use std::path::PathBuf;
91
92    #[test]
93    fn scan_context_subpath_nonexistent() {
94        let ctx = ScanContext {
95            root: PathBuf::from("/tmp/__ocls_test_nonexistent__"),
96            framework: FrameworkHint::Unknown,
97        };
98        assert!(ctx.subpath("settings.json").is_none());
99    }
100
101    #[test]
102    fn scan_context_subpath_existing() {
103        let dir = tempfile::tempdir().unwrap();
104        std::fs::write(dir.path().join("settings.json"), b"{}").unwrap();
105        let ctx = ScanContext {
106            root: dir.path().to_path_buf(),
107            framework: FrameworkHint::Unknown,
108        };
109        assert!(ctx.subpath("settings.json").is_some());
110    }
111}