allsource_core/security/
automation.rs

1/// Security Automation and CI/CD Integration
2///
3/// Automated security scanning and monitoring for:
4/// - Dependency vulnerabilities (cargo audit)
5/// - Code security analysis (static analysis)
6/// - Secret detection
7/// - License compliance
8/// - Security policy enforcement
9
10use crate::error::Result;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::process::Command;
15
16/// Security scan configuration
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SecurityScanConfig {
19    /// Enable automatic security scanning
20    pub enabled: bool,
21
22    /// Scan frequency in hours
23    pub scan_frequency_hours: u32,
24
25    /// Enable dependency scanning
26    pub enable_dependency_scan: bool,
27
28    /// Enable secrets scanning
29    pub enable_secrets_scan: bool,
30
31    /// Enable SAST (Static Application Security Testing)
32    pub enable_sast: bool,
33
34    /// Enable license compliance checking
35    pub enable_license_check: bool,
36
37    /// Fail build on high severity issues
38    pub fail_on_high_severity: bool,
39
40    /// Fail build on medium severity issues
41    pub fail_on_medium_severity: bool,
42}
43
44impl Default for SecurityScanConfig {
45    fn default() -> Self {
46        Self {
47            enabled: true,
48            scan_frequency_hours: 24,
49            enable_dependency_scan: true,
50            enable_secrets_scan: true,
51            enable_sast: true,
52            enable_license_check: true,
53            fail_on_high_severity: true,
54            fail_on_medium_severity: false,
55        }
56    }
57}
58
59/// Security scan result
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SecurityScanResult {
62    /// Scan timestamp
63    pub timestamp: DateTime<Utc>,
64
65    /// Overall status
66    pub status: ScanStatus,
67
68    /// Findings by category
69    pub findings: HashMap<String, Vec<SecurityFinding>>,
70
71    /// Summary statistics
72    pub summary: ScanSummary,
73
74    /// Recommendations
75    pub recommendations: Vec<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79pub enum ScanStatus {
80    Pass,
81    Warning,
82    Fail,
83    Error,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct SecurityFinding {
88    /// Finding ID
89    pub id: String,
90
91    /// Title
92    pub title: String,
93
94    /// Description
95    pub description: String,
96
97    /// Severity
98    pub severity: Severity,
99
100    /// Category
101    pub category: FindingCategory,
102
103    /// Affected component
104    pub component: String,
105
106    /// Fix recommendation
107    pub fix: Option<String>,
108
109    /// CVE ID (if applicable)
110    pub cve: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
114pub enum Severity {
115    Critical,
116    High,
117    Medium,
118    Low,
119    Info,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub enum FindingCategory {
124    Dependency,
125    SecretLeak,
126    CodeVulnerability,
127    LicenseIssue,
128    ConfigurationIssue,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ScanSummary {
133    pub total_findings: usize,
134    pub critical: usize,
135    pub high: usize,
136    pub medium: usize,
137    pub low: usize,
138    pub info: usize,
139}
140
141/// Security scanner
142pub struct SecurityScanner {
143    config: SecurityScanConfig,
144    last_scan: Option<DateTime<Utc>>,
145    last_result: Option<SecurityScanResult>,
146}
147
148impl SecurityScanner {
149    /// Create new security scanner
150    pub fn new(config: SecurityScanConfig) -> Self {
151        Self {
152            config,
153            last_scan: None,
154            last_result: None,
155        }
156    }
157
158    /// Run full security scan
159    pub fn run_full_scan(&mut self) -> Result<SecurityScanResult> {
160        let mut all_findings: HashMap<String, Vec<SecurityFinding>> = HashMap::new();
161        let mut recommendations = Vec::new();
162
163        // Dependency scanning
164        if self.config.enable_dependency_scan {
165            match self.scan_dependencies() {
166                Ok(findings) => {
167                    if !findings.is_empty() {
168                        all_findings.insert("dependencies".to_string(), findings);
169                        recommendations.push("Run 'cargo update' to update vulnerable dependencies".to_string());
170                    }
171                }
172                Err(e) => {
173                    eprintln!("Dependency scan failed: {}", e);
174                }
175            }
176        }
177
178        // Secrets scanning
179        if self.config.enable_secrets_scan {
180            match self.scan_secrets() {
181                Ok(findings) => {
182                    if !findings.is_empty() {
183                        all_findings.insert("secrets".to_string(), findings);
184                        recommendations.push("Remove hardcoded secrets and use environment variables".to_string());
185                    }
186                }
187                Err(e) => {
188                    eprintln!("Secrets scan failed: {}", e);
189                }
190            }
191        }
192
193        // SAST
194        if self.config.enable_sast {
195            match self.run_static_analysis() {
196                Ok(findings) => {
197                    if !findings.is_empty() {
198                        all_findings.insert("code_analysis".to_string(), findings);
199                        recommendations.push("Review and fix code quality issues".to_string());
200                    }
201                }
202                Err(e) => {
203                    eprintln!("SAST failed: {}", e);
204                }
205            }
206        }
207
208        // License checking
209        if self.config.enable_license_check {
210            match self.check_licenses() {
211                Ok(findings) => {
212                    if !findings.is_empty() {
213                        all_findings.insert("licenses".to_string(), findings);
214                        recommendations.push("Review dependency licenses for compliance".to_string());
215                    }
216                }
217                Err(e) => {
218                    eprintln!("License check failed: {}", e);
219                }
220            }
221        }
222
223        // Calculate summary
224        let summary = self.calculate_summary(&all_findings);
225
226        // Determine overall status
227        let status = self.determine_status(&summary);
228
229        let result = SecurityScanResult {
230            timestamp: Utc::now(),
231            status,
232            findings: all_findings,
233            summary,
234            recommendations,
235        };
236
237        self.last_scan = Some(Utc::now());
238        self.last_result = Some(result.clone());
239
240        Ok(result)
241    }
242
243    /// Scan dependencies for known vulnerabilities
244    fn scan_dependencies(&self) -> Result<Vec<SecurityFinding>> {
245        let mut findings = Vec::new();
246
247        // Run cargo audit
248        let output = Command::new("cargo")
249            .args(&["audit", "--json"])
250            .output();
251
252        match output {
253            Ok(output) if output.status.success() => {
254                // Parse cargo audit output
255                if let Ok(output_str) = String::from_utf8(output.stdout) {
256                    // Simple parsing (in production, use proper JSON parsing)
257                    if output_str.contains("Crate:") || output_str.contains("ID:") {
258                        findings.push(SecurityFinding {
259                            id: "DEP-001".to_string(),
260                            title: "Vulnerable dependency detected".to_string(),
261                            description: "cargo audit found vulnerabilities".to_string(),
262                            severity: Severity::High,
263                            category: FindingCategory::Dependency,
264                            component: "dependencies".to_string(),
265                            fix: Some("Run 'cargo update' and review audit output".to_string()),
266                            cve: None,
267                        });
268                    }
269                }
270            }
271            Ok(_) => {
272                // cargo audit found issues (non-zero exit)
273                findings.push(SecurityFinding {
274                    id: "DEP-002".to_string(),
275                    title: "Dependency vulnerabilities found".to_string(),
276                    description: "cargo audit reported vulnerabilities".to_string(),
277                    severity: Severity::High,
278                    category: FindingCategory::Dependency,
279                    component: "Cargo dependencies".to_string(),
280                    fix: Some("Review 'cargo audit' output and update dependencies".to_string()),
281                    cve: None,
282                });
283            }
284            Err(_) => {
285                // cargo audit not installed or failed
286                eprintln!("cargo audit not available - install with: cargo install cargo-audit");
287            }
288        }
289
290        Ok(findings)
291    }
292
293    /// Scan for hardcoded secrets
294    fn scan_secrets(&self) -> Result<Vec<SecurityFinding>> {
295        let mut findings = Vec::new();
296
297        // Common secret patterns
298        let secret_patterns = vec![
299            (r"(?i)(api[_-]?key|apikey)\s*[:=]\s*[a-zA-Z0-9]{20,}", "API Key"),
300            (r"(?i)(password|passwd|pwd)\s*[:=]\s*[\w@#$%^&*]{8,}", "Password"),
301            (r"(?i)(secret[_-]?key)\s*[:=]\s*[a-zA-Z0-9]{20,}", "Secret Key"),
302            (r"(?i)(aws[_-]?access[_-]?key[_-]?id)\s*[:=]\s*[A-Z0-9]{20}", "AWS Access Key"),
303            (r"(?i)(private[_-]?key)\s*[:=]", "Private Key"),
304        ];
305
306        // Check common files (in production, scan all source files)
307        let files_to_check = vec![
308            ".env",
309            ".env.example",
310            "config.toml",
311            "Cargo.toml",
312        ];
313
314        for file in files_to_check {
315            if let Ok(content) = std::fs::read_to_string(file) {
316                for (pattern, secret_type) in &secret_patterns {
317                    if content.contains("password") || content.contains("secret") || content.contains("key") {
318                        findings.push(SecurityFinding {
319                            id: format!("SEC-{:03}", findings.len() + 1),
320                            title: format!("Potential {} found", secret_type),
321                            description: format!("Potential hardcoded {} detected in {}", secret_type, file),
322                            severity: Severity::High,
323                            category: FindingCategory::SecretLeak,
324                            component: file.to_string(),
325                            fix: Some("Remove hardcoded secrets, use environment variables or secret management".to_string()),
326                            cve: None,
327                        });
328                    }
329                }
330            }
331        }
332
333        Ok(findings)
334    }
335
336    /// Run static application security testing
337    fn run_static_analysis(&self) -> Result<Vec<SecurityFinding>> {
338        let mut findings = Vec::new();
339
340        // Run clippy with security lints
341        let output = Command::new("cargo")
342            .args(&["clippy", "--", "-W", "clippy::all"])
343            .output();
344
345        match output {
346            Ok(output) if !output.status.success() => {
347                let stderr = String::from_utf8_lossy(&output.stderr);
348                if stderr.contains("warning:") || stderr.contains("error:") {
349                    findings.push(SecurityFinding {
350                        id: "SAST-001".to_string(),
351                        title: "Code quality issues found".to_string(),
352                        description: "Clippy found potential code issues".to_string(),
353                        severity: Severity::Medium,
354                        category: FindingCategory::CodeVulnerability,
355                        component: "source code".to_string(),
356                        fix: Some("Run 'cargo clippy' and address warnings".to_string()),
357                        cve: None,
358                    });
359                }
360            }
361            _ => {}
362        }
363
364        Ok(findings)
365    }
366
367    /// Check dependency licenses
368    fn check_licenses(&self) -> Result<Vec<SecurityFinding>> {
369        let findings = Vec::new();
370
371        // Restricted licenses (example list)
372        let restricted_licenses = vec!["GPL-3.0", "AGPL-3.0", "SSPL"];
373
374        // In production, use cargo-license or similar tool
375        // For now, this is a placeholder
376
377        Ok(findings)
378    }
379
380    fn calculate_summary(&self, findings: &HashMap<String, Vec<SecurityFinding>>) -> ScanSummary {
381        let mut summary = ScanSummary {
382            total_findings: 0,
383            critical: 0,
384            high: 0,
385            medium: 0,
386            low: 0,
387            info: 0,
388        };
389
390        for findings_vec in findings.values() {
391            for finding in findings_vec {
392                summary.total_findings += 1;
393                match finding.severity {
394                    Severity::Critical => summary.critical += 1,
395                    Severity::High => summary.high += 1,
396                    Severity::Medium => summary.medium += 1,
397                    Severity::Low => summary.low += 1,
398                    Severity::Info => summary.info += 1,
399                }
400            }
401        }
402
403        summary
404    }
405
406    fn determine_status(&self, summary: &ScanSummary) -> ScanStatus {
407        if summary.critical > 0 {
408            return ScanStatus::Fail;
409        }
410
411        if self.config.fail_on_high_severity && summary.high > 0 {
412            return ScanStatus::Fail;
413        }
414
415        if self.config.fail_on_medium_severity && summary.medium > 0 {
416            return ScanStatus::Fail;
417        }
418
419        if summary.high > 0 || summary.medium > 0 {
420            return ScanStatus::Warning;
421        }
422
423        ScanStatus::Pass
424    }
425
426    /// Get last scan result
427    pub fn get_last_result(&self) -> Option<&SecurityScanResult> {
428        self.last_result.as_ref()
429    }
430
431    /// Check if scan is needed
432    pub fn should_scan(&self) -> bool {
433        if !self.config.enabled {
434            return false;
435        }
436
437        match self.last_scan {
438            None => true,
439            Some(last) => {
440                let elapsed = Utc::now() - last;
441                elapsed.num_hours() >= self.config.scan_frequency_hours as i64
442            }
443        }
444    }
445}
446
447/// CI/CD integration helper
448pub struct CiCdIntegration;
449
450impl CiCdIntegration {
451    /// Generate GitHub Actions workflow
452    pub fn generate_github_actions_workflow() -> String {
453        r#"name: Security Scan
454
455on:
456  push:
457    branches: [ main, develop ]
458  pull_request:
459    branches: [ main ]
460  schedule:
461    - cron: '0 0 * * *'  # Daily
462
463jobs:
464  security-scan:
465    runs-on: ubuntu-latest
466    steps:
467      - uses: actions/checkout@v3
468
469      - name: Install Rust
470        uses: actions-rs/toolchain@v1
471        with:
472          toolchain: stable
473          components: clippy
474
475      - name: Install cargo-audit
476        run: cargo install cargo-audit
477
478      - name: Dependency Audit
479        run: cargo audit
480
481      - name: Security Clippy
482        run: cargo clippy -- -D warnings
483
484      - name: Run Tests
485        run: cargo test --lib security
486
487      - name: Secret Scanning
488        uses: trufflesecurity/trufflehog@main
489        with:
490          path: ./
491          base: main
492          head: HEAD
493"#.to_string()
494    }
495
496    /// Generate GitLab CI configuration
497    pub fn generate_gitlab_ci_config() -> String {
498        r#"security-scan:
499  stage: test
500  image: rust:latest
501  script:
502    - cargo install cargo-audit
503    - cargo audit
504    - cargo clippy -- -D warnings
505    - cargo test --lib security
506  allow_failure: false
507"#.to_string()
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    #[test]
516    fn test_scanner_creation() {
517        let scanner = SecurityScanner::new(SecurityScanConfig::default());
518        assert!(scanner.last_result.is_none());
519        assert!(scanner.should_scan());
520    }
521
522    #[test]
523    fn test_scan_summary_calculation() {
524        let scanner = SecurityScanner::new(SecurityScanConfig::default());
525        let mut findings = HashMap::new();
526
527        findings.insert("test".to_string(), vec![
528            SecurityFinding {
529                id: "1".to_string(),
530                title: "Test".to_string(),
531                description: "Test".to_string(),
532                severity: Severity::Critical,
533                category: FindingCategory::Dependency,
534                component: "test".to_string(),
535                fix: None,
536                cve: None,
537            },
538            SecurityFinding {
539                id: "2".to_string(),
540                title: "Test2".to_string(),
541                description: "Test2".to_string(),
542                severity: Severity::High,
543                category: FindingCategory::Dependency,
544                component: "test".to_string(),
545                fix: None,
546                cve: None,
547            },
548        ]);
549
550        let summary = scanner.calculate_summary(&findings);
551        assert_eq!(summary.total_findings, 2);
552        assert_eq!(summary.critical, 1);
553        assert_eq!(summary.high, 1);
554    }
555
556    #[test]
557    fn test_status_determination() {
558        let scanner = SecurityScanner::new(SecurityScanConfig::default());
559
560        let summary_critical = ScanSummary {
561            total_findings: 1,
562            critical: 1,
563            high: 0,
564            medium: 0,
565            low: 0,
566            info: 0,
567        };
568        assert_eq!(scanner.determine_status(&summary_critical), ScanStatus::Fail);
569
570        let summary_clean = ScanSummary {
571            total_findings: 0,
572            critical: 0,
573            high: 0,
574            medium: 0,
575            low: 0,
576            info: 0,
577        };
578        assert_eq!(scanner.determine_status(&summary_clean), ScanStatus::Pass);
579    }
580
581    #[test]
582    fn test_github_actions_workflow_generation() {
583        let workflow = CiCdIntegration::generate_github_actions_workflow();
584        assert!(workflow.contains("cargo audit"));
585        assert!(workflow.contains("cargo clippy"));
586        assert!(workflow.contains("Security Scan"));
587    }
588}