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::{TurboSecurityAnalyzer, TurboConfig, ScanMode};
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.analyze_project(&path)
87            .map_err(|e| SecurityScanError(format!("Scan failed: {}", e)))?;
88
89        let result = json!({
90            "total_findings": report.total_findings,
91            "overall_score": report.overall_score,
92            "risk_level": format!("{:?}", report.risk_level),
93            "files_scanned": report.files_scanned,
94            "findings": report.findings.iter().take(50).map(|f| {
95                json!({
96                    "title": f.title,
97                    "description": f.description,
98                    "severity": format!("{:?}", f.severity),
99                    "category": format!("{:?}", f.category),
100                    "file_path": f.file_path.as_ref().map(|p| p.display().to_string()),
101                    "line_number": f.line_number,
102                    "evidence": f.evidence.as_ref().map(|e| e.chars().take(100).collect::<String>()),
103                })
104            }).collect::<Vec<_>>(),
105            "recommendations": report.recommendations.iter().take(10).collect::<Vec<_>>(),
106            "scan_mode": args.mode.as_deref().unwrap_or("balanced"),
107        });
108
109        serde_json::to_string_pretty(&result)
110            .map_err(|e| SecurityScanError(format!("Failed to serialize: {}", e)))
111    }
112}
113
114// ============================================================================
115// Vulnerabilities Tool
116// ============================================================================
117
118#[derive(Debug, Deserialize)]
119pub struct VulnerabilitiesArgs {
120    pub path: Option<String>,
121}
122
123#[derive(Debug, thiserror::Error)]
124#[error("Vulnerability check error: {0}")]
125pub struct VulnerabilitiesError(String);
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct VulnerabilitiesTool {
129    project_path: PathBuf,
130}
131
132impl VulnerabilitiesTool {
133    pub fn new(project_path: PathBuf) -> Self {
134        Self { project_path }
135    }
136}
137
138impl Tool for VulnerabilitiesTool {
139    const NAME: &'static str = "check_vulnerabilities";
140
141    type Error = VulnerabilitiesError;
142    type Args = VulnerabilitiesArgs;
143    type Output = String;
144
145    async fn definition(&self, _prompt: String) -> ToolDefinition {
146        ToolDefinition {
147            name: Self::NAME.to_string(),
148            description: "Check the project's dependencies for known security vulnerabilities (CVEs).".to_string(),
149            parameters: json!({
150                "type": "object",
151                "properties": {
152                    "path": {
153                        "type": "string",
154                        "description": "Optional subdirectory path to check"
155                    }
156                }
157            }),
158        }
159    }
160
161    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
162        let path = match args.path {
163            Some(subpath) => self.project_path.join(subpath),
164            None => self.project_path.clone(),
165        };
166
167        let parser = crate::analyzer::dependency_parser::DependencyParser::new();
168        let dependencies = parser
169            .parse_all_dependencies(&path)
170            .map_err(|e| VulnerabilitiesError(format!("Failed to parse dependencies: {}", e)))?;
171
172        if dependencies.is_empty() {
173            return Ok(json!({
174                "message": "No dependencies found in project",
175                "total_vulnerabilities": 0
176            }).to_string());
177        }
178
179        let checker = crate::analyzer::vulnerability::VulnerabilityChecker::new();
180        let report = checker
181            .check_all_dependencies(&dependencies, &path)
182            .await
183            .map_err(|e| VulnerabilitiesError(format!("Vulnerability check failed: {}", e)))?;
184
185        let result = json!({
186            "total_vulnerabilities": report.total_vulnerabilities,
187            "critical_count": report.critical_count,
188            "high_count": report.high_count,
189            "medium_count": report.medium_count,
190            "low_count": report.low_count,
191            "vulnerable_dependencies": report.vulnerable_dependencies.iter().take(20).map(|dep| {
192                json!({
193                    "name": dep.name,
194                    "version": dep.version,
195                    "language": dep.language.as_str(),
196                    "vulnerabilities": dep.vulnerabilities.iter().map(|v| {
197                        json!({
198                            "id": v.id,
199                            "title": v.title,
200                            "severity": format!("{:?}", v.severity),
201                            "cve": v.cve,
202                            "patched_versions": v.patched_versions,
203                        })
204                    }).collect::<Vec<_>>()
205                })
206            }).collect::<Vec<_>>()
207        });
208
209        serde_json::to_string_pretty(&result)
210            .map_err(|e| VulnerabilitiesError(format!("Failed to serialize: {}", e)))
211    }
212}