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