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::{TurboConfig, TurboSecurityAnalyzer, ScanMode};
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 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#[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}