syncable_cli/agent/tools/
security.rs1use 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#[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#[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}