use crate::core::{Finding, Severity};
use crate::git::dangerous::DANGEROUS_COMPONENTS;
use crate::plugins::traits::{PluginError, PluginReport, ScanContext, ScanPhase, SecurityPlugin};
use async_trait::async_trait;
use std::path::Path;
use std::time::Instant;
pub struct GitInternalsScanner;
impl Default for GitInternalsScanner {
fn default() -> Self {
Self::new()
}
}
impl GitInternalsScanner {
pub fn new() -> Self {
Self
}
fn scan_for_hooks(&self, git_dir: &Path) -> Vec<Finding> {
let mut findings = Vec::new();
let hooks_dir = git_dir.join("hooks");
if !hooks_dir.exists() {
return findings;
}
if let Ok(entries) = std::fs::read_dir(&hooks_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
if DANGEROUS_COMPONENTS.hooks.contains(&name) {
findings.push(
Finding::new(
format!("GIT-HOOK-{}", name.to_uppercase()),
format!("Dangerous git hook found: {}", name),
Severity::Critical,
)
.with_file(path.clone())
.with_description(format!(
"Git hook '{}' was found after sanitization. \
This hook can execute arbitrary code and should be removed.",
name
)),
);
}
}
}
}
findings
}
fn scan_config(&self, git_dir: &Path) -> Vec<Finding> {
let mut findings = Vec::new();
let config_path = git_dir.join("config");
if !config_path.exists() {
return findings;
}
if let Ok(content) = std::fs::read_to_string(&config_path) {
for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim();
for dangerous_key in DANGEROUS_COMPONENTS.dangerous_config_keys {
let key_pattern = dangerous_key.replace("*", "");
if trimmed.to_lowercase().contains(&key_pattern.to_lowercase()) {
findings.push(
Finding::new(
format!("GIT-CFG-{:03}", line_num),
format!("Dangerous git config key: {}", dangerous_key),
Severity::High,
)
.with_file(config_path.clone())
.with_line((line_num + 1) as u32)
.with_evidence(trimmed.to_string())
.with_description(format!(
"Dangerous git config key '{}' found. \
This can be used to execute arbitrary code.",
dangerous_key
)),
);
}
}
}
}
findings
}
}
#[async_trait]
impl SecurityPlugin for GitInternalsScanner {
fn name(&self) -> &str {
"git-internals"
}
fn version(&self) -> &str {
"0.1.0"
}
fn description(&self) -> &str {
"Scan .git directory for threats and verify sanitization"
}
fn scan_phase(&self) -> ScanPhase {
ScanPhase::GitInternals
}
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());
let git_dir_buf;
let git_dir = if context.path.ends_with(".git") {
context.path
} else {
git_dir_buf = context.path.join(".git");
git_dir_buf.as_path()
};
if git_dir.exists() {
let mut all_findings = Vec::new();
all_findings.extend(self.scan_for_hooks(git_dir));
all_findings.extend(self.scan_config(git_dir));
report.findings = all_findings;
report.scanned_files = 1;
}
report.duration_ms = start.elapsed().as_millis() as u64;
Ok(report)
}
}