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 std::collections::HashMap;
use std::time::Instant;

pub struct EntropyScanner {
    threshold: f64,
}

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

impl EntropyScanner {
    pub fn new() -> Self {
        Self { threshold: 4.5 }
    }

    fn calculate_entropy(&self, data: &[u8]) -> f64 {
        if data.is_empty() {
            return 0.0;
        }

        let mut frequency: HashMap<u8, usize> = HashMap::new();
        for &byte in data {
            *frequency.entry(byte).or_insert(0) += 1;
        }

        let len = data.len() as f64;
        let mut entropy = 0.0;

        for count in frequency.values() {
            let probability = *count as f64 / len;
            entropy -= probability * probability.log2();
        }

        entropy
    }
}

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

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

    fn description(&self) -> &str {
        "Detect high-entropy data (encrypted/obfuscated content)"
    }

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

    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 {
            // Skip binary files
            if !content.is_empty() && content.contains(&0) {
                return Ok(report);
            }

            let entropy = self.calculate_entropy(content);

            if entropy > self.threshold {
                let finding = Finding::new(
                    "ENT-001".to_string(),
                    "High entropy content detected".to_string(),
                    if entropy > 7.0 {
                        Severity::High
                    } else {
                        Severity::Medium
                    },
                )
                .with_file(context.path.to_path_buf())
                .with_description(format!(
                    "File has entropy of {:.2} (threshold: {:.2}). \
                     May contain encrypted, compressed, or obfuscated data.",
                    entropy, self.threshold
                ))
                .with_evidence(format!("Entropy: {:.2}", entropy));

                report.findings.push(finding);
            }

            report.scanned_files = 1;
        }

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

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

    #[tokio::test]
    async fn test_high_entropy_detection() {
        let scanner = EntropyScanner::new();
        let content = b"aB3$kL9#mN2@pQ5&rS8*tU1!vX4^yZ7aB3$kL9#mN2@pQ5&rS8*tU1!vX4^yZ7aB3$kL9#mN2@pQ5&rS8*tU1!vX4^yZ7";
        let context = ScanContext {
            path: std::path::Path::new("test.txt"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: std::collections::HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert!(!report.findings.is_empty());
    }

    #[tokio::test]
    async fn test_low_entropy_passes() {
        let scanner = EntropyScanner::new();
        let content = b"aaaaaaaaaa";
        let context = ScanContext {
            path: std::path::Path::new("test.txt"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: std::collections::HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert_eq!(report.findings.len(), 0);
    }

    #[tokio::test]
    async fn test_empty_content() {
        let scanner = EntropyScanner::new();
        let content = b"";
        let context = ScanContext {
            path: std::path::Path::new("test.txt"),
            scan_phase: ScanPhase::PostExtract,
            file_content: Some(content),
            metadata: std::collections::HashMap::new(),
        };
        let report = scanner.scan(&context).await.unwrap();
        assert_eq!(report.findings.len(), 0);
    }
}