ggen_cli_lib/cmds/audit/
security.rs

1//! Security vulnerability scanning and analysis.
2//!
3//! This module provides functionality to scan codebases for security vulnerabilities,
4//! analyze dependencies for known CVEs, and perform security audits. It integrates with
5//! cargo-make to perform comprehensive security analysis.
6//!
7//! # Examples
8//!
9//! ```bash
10//! ggen audit security scan --path ./src --verbose
11//! ggen audit security cve --package "serde" --version "1.0.0"
12//! ggen audit security audit --all
13//! ```
14//!
15//! # Errors
16//!
17//! Returns errors if the underlying cargo-make commands fail or if
18//! the specified paths don't exist.
19
20use clap::{Args, Subcommand};
21use ggen_utils::error::Result;
22// CLI output only - no library logging
23
24#[derive(Args, Debug)]
25pub struct SecurityArgs {
26    #[command(subcommand)]
27    pub action: SecurityAction,
28}
29
30#[derive(Subcommand, Debug)]
31pub enum SecurityAction {
32    /// Scan for security vulnerabilities
33    Scan(ScanArgs),
34
35    /// Check dependencies for known vulnerabilities
36    Dependencies(DependenciesArgs),
37
38    /// Audit configuration files
39    Config(ConfigArgs),
40}
41
42#[derive(Args, Debug)]
43pub struct ScanArgs {
44    /// Directory to scan [default: current directory]
45    #[arg(long, default_value = ".")]
46    pub path: String,
47
48    /// Output in JSON format
49    #[arg(long)]
50    pub json: bool,
51
52    /// Show detailed information
53    #[arg(long)]
54    pub verbose: bool,
55
56    /// Fix automatically where possible
57    #[arg(long)]
58    pub fix: bool,
59}
60
61#[derive(Args, Debug)]
62pub struct DependenciesArgs {
63    /// Check only direct dependencies
64    #[arg(long)]
65    pub direct_only: bool,
66
67    /// Output in JSON format
68    #[arg(long)]
69    pub json: bool,
70
71    /// Update vulnerable dependencies
72    #[arg(long)]
73    pub update: bool,
74}
75
76#[derive(Args, Debug)]
77pub struct ConfigArgs {
78    /// Configuration file to audit
79    #[arg(long)]
80    pub file: Option<String>,
81
82    /// Output in JSON format
83    #[arg(long)]
84    pub json: bool,
85
86    /// Fix configuration issues
87    #[arg(long)]
88    pub fix: bool,
89}
90
91pub async fn run(args: &SecurityArgs) -> Result<()> {
92    match &args.action {
93        SecurityAction::Scan(scan_args) => scan_security(scan_args).await,
94        SecurityAction::Dependencies(deps_args) => check_dependencies(deps_args).await,
95        SecurityAction::Config(config_args) => audit_config(config_args).await,
96    }
97}
98
99/// Validate and sanitize path input
100fn validate_path(path: &str) -> Result<()> {
101    // Validate path is not empty
102    if path.trim().is_empty() {
103        return Err(ggen_utils::error::Error::new("Path cannot be empty"));
104    }
105
106    // Validate path length
107    if path.len() > 1000 {
108        return Err(ggen_utils::error::Error::new(
109            "Path too long (max 1000 characters)",
110        ));
111    }
112
113    // Basic path traversal protection
114    if path.contains("..") {
115        return Err(ggen_utils::error::Error::new(
116            "Path traversal detected: path cannot contain '..'",
117        ));
118    }
119
120    Ok(())
121}
122
123/// Validate and sanitize file path input (if provided)
124fn validate_file_path(file: &Option<String>) -> Result<()> {
125    if let Some(file) = file {
126        // Validate file path is not empty
127        if file.trim().is_empty() {
128            return Err(ggen_utils::error::Error::new("File path cannot be empty"));
129        }
130
131        // Validate file path length
132        if file.len() > 1000 {
133            return Err(ggen_utils::error::Error::new(
134                "File path too long (max 1000 characters)",
135            ));
136        }
137
138        // Basic path traversal protection
139        if file.contains("..") {
140            return Err(ggen_utils::error::Error::new(
141                "Path traversal detected: file path cannot contain '..'",
142            ));
143        }
144
145        // Validate file path format (basic pattern check)
146        if !file.chars().all(|c| {
147            c.is_alphanumeric() || c == '.' || c == '/' || c == '-' || c == '_' || c == '\\'
148        }) {
149            return Err(ggen_utils::error::Error::new(
150                "Invalid file path format: only alphanumeric characters, dots, slashes, dashes, underscores, and backslashes allowed",
151            ));
152        }
153    }
154
155    Ok(())
156}
157
158async fn scan_security(args: &ScanArgs) -> Result<()> {
159    // Validate input
160    validate_path(&args.path)?;
161
162    println!("🔒 Scanning for security vulnerabilities");
163
164    let mut cmd = std::process::Command::new("cargo");
165    cmd.args(["make", "audit"]);
166
167    if args.json {
168        cmd.arg("--json");
169    }
170
171    if args.verbose {
172        cmd.arg("--verbose");
173    }
174
175    if args.fix {
176        cmd.arg("--fix");
177    }
178
179    cmd.arg("--path").arg(&args.path);
180
181    let output = cmd.output()?;
182
183    if !output.status.success() {
184        let stderr = String::from_utf8_lossy(&output.stderr);
185        return Err(ggen_utils::error::Error::new(&format!(
186            "Security scan failed: {}",
187            stderr
188        )));
189    }
190
191    let stdout = String::from_utf8_lossy(&output.stdout);
192    println!("{}", stdout);
193    Ok(())
194}
195
196async fn check_dependencies(args: &DependenciesArgs) -> Result<()> {
197    println!("📦 Checking dependencies for security vulnerabilities");
198
199    let mut cmd = std::process::Command::new("cargo");
200    cmd.args(["make", "audit-deps"]);
201
202    if args.direct_only {
203        cmd.arg("--direct-only");
204    }
205
206    if args.json {
207        cmd.arg("--json");
208    }
209
210    if args.update {
211        cmd.arg("--update");
212    }
213
214    let output = cmd.output()?;
215
216    if !output.status.success() {
217        let stderr = String::from_utf8_lossy(&output.stderr);
218        return Err(ggen_utils::error::Error::new(&format!(
219            "Dependency security check failed: {}",
220            stderr
221        )));
222    }
223
224    let stdout = String::from_utf8_lossy(&output.stdout);
225    println!("{}", stdout);
226    Ok(())
227}
228
229async fn audit_config(args: &ConfigArgs) -> Result<()> {
230    // Validate input
231    validate_file_path(&args.file)?;
232
233    println!("⚙️ Auditing configuration files for security issues");
234
235    let mut cmd = std::process::Command::new("cargo");
236    cmd.args(["make", "audit-config"]);
237
238    if let Some(file) = &args.file {
239        cmd.arg("--file").arg(file);
240    }
241
242    if args.json {
243        cmd.arg("--json");
244    }
245
246    if args.fix {
247        cmd.arg("--fix");
248    }
249
250    let output = cmd.output()?;
251
252    if !output.status.success() {
253        let stderr = String::from_utf8_lossy(&output.stderr);
254        return Err(ggen_utils::error::Error::new(&format!(
255            "Configuration audit failed: {}",
256            stderr
257        )));
258    }
259
260    let stdout = String::from_utf8_lossy(&output.stdout);
261    println!("{}", stdout);
262    Ok(())
263}