syncable_cli/agent/tools/
security.rs

1//! Security and vulnerability scanning tools using Rig's Tool trait
2
3use rig::completion::ToolDefinition;
4use rig::tool::Tool;
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use std::path::PathBuf;
8
9use crate::analyzer::security::turbo::{ScanMode, TurboConfig, TurboSecurityAnalyzer};
10
11// ============================================================================
12// Security Scan Tool
13// ============================================================================
14
15#[derive(Debug, Deserialize)]
16pub struct SecurityScanArgs {
17    pub mode: Option<String>,
18    pub path: Option<String>,
19}
20
21#[derive(Debug, thiserror::Error)]
22#[error("Security scan error: {0}")]
23pub struct SecurityScanError(String);
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct SecurityScanTool {
27    project_path: PathBuf,
28}
29
30impl SecurityScanTool {
31    pub fn new(project_path: PathBuf) -> Self {
32        Self { project_path }
33    }
34}
35
36impl Tool for SecurityScanTool {
37    const NAME: &'static str = "security_scan";
38
39    type Error = SecurityScanError;
40    type Args = SecurityScanArgs;
41    type Output = String;
42
43    async fn definition(&self, _prompt: String) -> ToolDefinition {
44        ToolDefinition {
45            name: Self::NAME.to_string(),
46            description: "Perform a security scan to detect potential secrets, API keys, passwords, and sensitive data that might be accidentally committed.".to_string(),
47            parameters: json!({
48                "type": "object",
49                "properties": {
50                    "mode": {
51                        "type": "string",
52                        "enum": ["lightning", "fast", "balanced", "thorough", "paranoid"],
53                        "description": "Scan mode: lightning (fast), balanced (recommended), thorough, or paranoid"
54                    },
55                    "path": {
56                        "type": "string",
57                        "description": "Optional subdirectory path to scan"
58                    }
59                }
60            }),
61        }
62    }
63
64    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
65        let path = match args.path {
66            Some(subpath) => self.project_path.join(subpath),
67            None => self.project_path.clone(),
68        };
69
70        let scan_mode = match args.mode.as_deref() {
71            Some("lightning") => ScanMode::Lightning,
72            Some("fast") => ScanMode::Fast,
73            Some("thorough") => ScanMode::Thorough,
74            Some("paranoid") => ScanMode::Paranoid,
75            _ => ScanMode::Balanced,
76        };
77
78        let config = TurboConfig {
79            scan_mode,
80            ..TurboConfig::default()
81        };
82
83        let scanner = TurboSecurityAnalyzer::new(config)
84            .map_err(|e| SecurityScanError(format!("Failed to create scanner: {}", e)))?;
85
86        let report = scanner
87            .analyze_project(&path)
88            .map_err(|e| SecurityScanError(format!("Scan failed: {}", e)))?;
89
90        let result = json!({
91            "total_findings": report.total_findings,
92            "overall_score": report.overall_score,
93            "risk_level": format!("{:?}", report.risk_level),
94            "files_scanned": report.files_scanned,
95            "findings": report.findings.iter().take(50).map(|f| {
96                json!({
97                    "title": f.title,
98                    "description": f.description,
99                    "severity": format!("{:?}", f.severity),
100                    "category": format!("{:?}", f.category),
101                    "file_path": f.file_path.as_ref().map(|p| p.display().to_string()),
102                    "line_number": f.line_number,
103                    "evidence": f.evidence.as_ref().map(|e| e.chars().take(100).collect::<String>()),
104                })
105            }).collect::<Vec<_>>(),
106            "recommendations": report.recommendations.iter().take(10).collect::<Vec<_>>(),
107            "scan_mode": args.mode.as_deref().unwrap_or("balanced"),
108        });
109
110        serde_json::to_string_pretty(&result)
111            .map_err(|e| SecurityScanError(format!("Failed to serialize: {}", e)))
112    }
113}
114
115// ============================================================================
116// Vulnerabilities Tool
117// ============================================================================
118
119#[derive(Debug, Deserialize)]
120pub struct VulnerabilitiesArgs {
121    pub path: Option<String>,
122}
123
124#[derive(Debug, thiserror::Error)]
125#[error("Vulnerability check error: {0}")]
126pub struct VulnerabilitiesError(String);
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct VulnerabilitiesTool {
130    project_path: PathBuf,
131}
132
133impl VulnerabilitiesTool {
134    pub fn new(project_path: PathBuf) -> Self {
135        Self { project_path }
136    }
137}
138
139impl Tool for VulnerabilitiesTool {
140    const NAME: &'static str = "check_vulnerabilities";
141
142    type Error = VulnerabilitiesError;
143    type Args = VulnerabilitiesArgs;
144    type Output = String;
145
146    async fn definition(&self, _prompt: String) -> ToolDefinition {
147        ToolDefinition {
148            name: Self::NAME.to_string(),
149            description:
150                "Check the project's dependencies for known security vulnerabilities (CVEs)."
151                    .to_string(),
152            parameters: json!({
153                "type": "object",
154                "properties": {
155                    "path": {
156                        "type": "string",
157                        "description": "Optional subdirectory path to check"
158                    }
159                }
160            }),
161        }
162    }
163
164    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
165        let path = match args.path {
166            Some(subpath) => self.project_path.join(subpath),
167            None => self.project_path.clone(),
168        };
169
170        let parser = crate::analyzer::dependency_parser::DependencyParser::new();
171        let dependencies = parser
172            .parse_all_dependencies(&path)
173            .map_err(|e| VulnerabilitiesError(format!("Failed to parse dependencies: {}", e)))?;
174
175        if dependencies.is_empty() {
176            return Ok(json!({
177                "message": "No dependencies found in project",
178                "total_vulnerabilities": 0
179            })
180            .to_string());
181        }
182
183        let checker = crate::analyzer::vulnerability::VulnerabilityChecker::new();
184        let report = checker
185            .check_all_dependencies(&dependencies, &path)
186            .await
187            .map_err(|e| VulnerabilitiesError(format!("Vulnerability check failed: {}", e)))?;
188
189        let result = json!({
190            "total_vulnerabilities": report.total_vulnerabilities,
191            "critical_count": report.critical_count,
192            "high_count": report.high_count,
193            "medium_count": report.medium_count,
194            "low_count": report.low_count,
195            "vulnerable_dependencies": report.vulnerable_dependencies.iter().take(20).map(|dep| {
196                json!({
197                    "name": dep.name,
198                    "version": dep.version,
199                    "language": dep.language.as_str(),
200                    "vulnerabilities": dep.vulnerabilities.iter().map(|v| {
201                        json!({
202                            "id": v.id,
203                            "title": v.title,
204                            "severity": format!("{:?}", v.severity),
205                            "cve": v.cve,
206                            "patched_versions": v.patched_versions,
207                        })
208                    }).collect::<Vec<_>>()
209                })
210            }).collect::<Vec<_>>()
211        });
212
213        serde_json::to_string_pretty(&result)
214            .map_err(|e| VulnerabilitiesError(format!("Failed to serialize: {}", e)))
215    }
216}