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