Skip to main content

syncable_cli/analyzer/tool_management/
installer.rs

1use crate::analyzer::dependency_parser::Language;
2use crate::analyzer::tool_management::{InstallationSource, ToolDetector};
3use crate::error::Result;
4use std::collections::HashMap;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum ToolInstallationError {
9    #[error("Installation failed: {0}")]
10    InstallationFailed(String),
11
12    #[error("Tool not supported on this platform: {0}")]
13    UnsupportedPlatform(String),
14
15    #[error("Command execution failed: {0}")]
16    CommandFailed(String),
17
18    #[error("IO error: {0}")]
19    Io(#[from] std::io::Error),
20}
21
22/// Tool installer for vulnerability scanning dependencies
23#[derive(Default)]
24pub struct ToolInstaller {
25    installed_tools: HashMap<String, bool>,
26    tool_detector: ToolDetector,
27}
28
29impl ToolInstaller {
30    pub fn new() -> Self {
31        Self {
32            installed_tools: HashMap::new(),
33            tool_detector: ToolDetector::new(),
34        }
35    }
36
37    /// Ensure all required tools for vulnerability scanning are available
38    pub fn ensure_tools_for_languages(&mut self, languages: &[Language]) -> Result<()> {
39        for language in languages {
40            match language {
41                Language::Rust => self.ensure_cargo_audit()?,
42                Language::JavaScript | Language::TypeScript => {
43                    if self.ensure_bun().is_err() {
44                        self.ensure_npm()?;
45                    }
46                }
47                Language::Python => self.ensure_pip_audit()?,
48                Language::Go => self.ensure_govulncheck()?,
49                Language::Java | Language::Kotlin => self.ensure_grype()?,
50                _ => {}
51            }
52        }
53        Ok(())
54    }
55
56    /// Check if a tool is installed and available
57    fn is_tool_installed(&mut self, tool: &str) -> bool {
58        let status = self.tool_detector.detect_tool(tool);
59        self.installed_tools
60            .insert(tool.to_string(), status.available);
61        status.available
62    }
63
64    /// Test if a tool is available by running version command
65    pub fn test_tool_availability(&mut self, tool: &str) -> bool {
66        self.is_tool_installed(tool)
67    }
68
69    /// Get installation status summary
70    pub fn get_tool_status(&self) -> HashMap<String, bool> {
71        self.installed_tools.clone()
72    }
73
74    /// Print tool installation status with detailed information
75    pub fn print_tool_status(&mut self, languages: &[Language]) {
76        // Skip printing in agent/quiet mode
77        if std::env::var("SYNCABLE_QUIET").is_ok() {
78            return;
79        }
80        println!("\nšŸ”§ Vulnerability Scanning Tools Status:");
81        println!("{}", "=".repeat(50));
82
83        let tool_statuses = self.tool_detector.detect_all_vulnerability_tools(languages);
84
85        for language in languages {
86            match language {
87                Language::Rust => {
88                    self.print_single_tool_status("cargo-audit", &tool_statuses, language);
89                }
90                Language::JavaScript | Language::TypeScript => {
91                    let js_tools = ["bun", "npm", "yarn", "pnpm"];
92                    for tool in &js_tools {
93                        if let Some(status) = tool_statuses.get(*tool) {
94                            self.print_js_tool_status(tool, status, language);
95                        }
96                    }
97                }
98                Language::Python => {
99                    self.print_single_tool_status("pip-audit", &tool_statuses, language);
100                }
101                Language::Go => {
102                    self.print_single_tool_status("govulncheck", &tool_statuses, language);
103                }
104                Language::Java | Language::Kotlin => {
105                    self.print_single_tool_status("grype", &tool_statuses, language);
106                }
107                _ => continue,
108            }
109        }
110        println!();
111    }
112
113    fn print_single_tool_status(
114        &self,
115        tool_name: &str,
116        tool_statuses: &HashMap<String, crate::analyzer::tool_management::ToolStatus>,
117        language: &Language,
118    ) {
119        if let Some(status) = tool_statuses.get(tool_name) {
120            let status_icon = if status.available { "āœ…" } else { "āŒ" };
121            print!(
122                "  {} {:?}: {} {}",
123                status_icon,
124                language,
125                tool_name,
126                if status.available {
127                    "installed"
128                } else {
129                    "missing"
130                }
131            );
132
133            if status.available {
134                if let Some(ref version) = status.version {
135                    print!(" (v{})", version);
136                }
137                if let Some(ref path) = status.path {
138                    print!(" at {}", path.display());
139                }
140                match &status.installation_source {
141                    InstallationSource::SystemPath => print!(" [system]"),
142                    InstallationSource::UserLocal => print!(" [user]"),
143                    InstallationSource::CargoHome => print!(" [cargo]"),
144                    InstallationSource::GoHome => print!(" [go]"),
145                    InstallationSource::PackageManager(pm) => print!(" [{}]", pm),
146                    InstallationSource::Manual => print!(" [manual]"),
147                    InstallationSource::NotFound => {}
148                }
149            } else {
150                print!(" - Install with: ");
151                match tool_name {
152                    "cargo-audit" => print!("cargo install cargo-audit"),
153                    "pip-audit" => print!("pip install pip-audit or pipx install pip-audit"),
154                    "govulncheck" => print!("go install golang.org/x/vuln/cmd/govulncheck@latest"),
155                    "grype" => print!("brew install grype or download from GitHub"),
156                    _ => print!("check documentation"),
157                }
158            }
159            println!();
160        }
161    }
162
163    fn print_js_tool_status(
164        &self,
165        tool_name: &str,
166        status: &crate::analyzer::tool_management::ToolStatus,
167        language: &Language,
168    ) {
169        let status_icon = if status.available { "āœ…" } else { "āŒ" };
170        print!(
171            "  {} {:?}: {} {}",
172            status_icon,
173            language,
174            tool_name,
175            if status.available {
176                "installed"
177            } else {
178                "missing"
179            }
180        );
181
182        if status.available {
183            if let Some(ref version) = status.version {
184                print!(" (v{})", version);
185            }
186            if let Some(ref path) = status.path {
187                print!(" at {}", path.display());
188            }
189            match &status.installation_source {
190                InstallationSource::SystemPath => print!(" [system]"),
191                InstallationSource::UserLocal => print!(" [user]"),
192                InstallationSource::CargoHome => print!(" [cargo]"),
193                InstallationSource::GoHome => print!(" [go]"),
194                InstallationSource::PackageManager(pm) => print!(" [{}]", pm),
195                InstallationSource::Manual => print!(" [manual]"),
196                InstallationSource::NotFound => {}
197            }
198        } else {
199            print!(" - Install with: ");
200            match tool_name {
201                "bun" => print!("curl -fsSL https://bun.sh/install | bash"),
202                "npm" => print!("Install Node.js from https://nodejs.org/"),
203                "yarn" => print!("npm install -g yarn"),
204                "pnpm" => print!("npm install -g pnpm"),
205                _ => print!("check documentation"),
206            }
207        }
208        println!();
209    }
210
211    // Installation methods - these will be moved to installers/ modules
212    fn ensure_cargo_audit(&mut self) -> Result<()> {
213        use crate::analyzer::tool_management::installers::rust::install_cargo_audit;
214        install_cargo_audit(&mut self.tool_detector, &mut self.installed_tools)
215    }
216
217    fn ensure_npm(&mut self) -> Result<()> {
218        use crate::analyzer::tool_management::installers::javascript::ensure_npm;
219        ensure_npm(&mut self.tool_detector, &mut self.installed_tools)
220    }
221
222    fn ensure_bun(&mut self) -> Result<()> {
223        use crate::analyzer::tool_management::installers::javascript::install_bun;
224        install_bun(&mut self.tool_detector, &mut self.installed_tools)
225    }
226
227    fn ensure_pip_audit(&mut self) -> Result<()> {
228        use crate::analyzer::tool_management::installers::python::install_pip_audit;
229        install_pip_audit(&mut self.tool_detector, &mut self.installed_tools)
230    }
231
232    fn ensure_govulncheck(&mut self) -> Result<()> {
233        use crate::analyzer::tool_management::installers::go::install_govulncheck;
234        install_govulncheck(&mut self.tool_detector, &mut self.installed_tools)
235    }
236
237    fn ensure_grype(&mut self) -> Result<()> {
238        use crate::analyzer::tool_management::installers::java::install_grype;
239        install_grype(&mut self.tool_detector, &mut self.installed_tools)
240    }
241}