use crate::core::{Config, PluginScanReport, ScanReport};
use crate::plugins::builtin::{
binary::BinaryScanner, cicd::CicdScanner, container::ContainerScanner,
dangerous_files::DangerousFilesScanner, deserialization::DeserializationScanner,
encoding::EncodingScanner, entropy::EntropyScanner, git_internals::GitInternalsScanner,
iac::IacScanner, patterns::PatternScanner, secrets::SecretsScanner,
supply_chain::SupplyChainScanner,
};
use crate::plugins::traits::{ScanContext, ScanPhase, SecurityPlugin};
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use std::time::Instant;
use walkdir::WalkDir;
pub struct ScanEngine {
config: Config,
}
impl ScanEngine {
pub fn new(config: Config) -> Self {
Self { config }
}
pub async fn scan_directory(&self, path: &Path) -> Result<ScanReport> {
let start = Instant::now();
let mut report = ScanReport::new();
let mut plugins: Vec<Box<dyn SecurityPlugin>> = vec![
Box::new(SecretsScanner::new()),
Box::new(PatternScanner::new()),
Box::new(EntropyScanner::new()),
Box::new(BinaryScanner::new()),
Box::new(CicdScanner::new()),
Box::new(SupplyChainScanner::new()),
Box::new(ContainerScanner::new()),
Box::new(IacScanner::new()),
Box::new(DeserializationScanner::new()),
Box::new(DangerousFilesScanner::new()),
Box::new(EncodingScanner::new()),
];
if let Some(config_dir) = dirs::config_dir() {
let plugins_dir = config_dir.join("securegit").join("plugins");
if plugins_dir.exists() {
for entry in std::fs::read_dir(&plugins_dir)
.into_iter()
.flatten()
.flatten()
{
let path = entry.path();
if path.is_file() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(&path) {
if metadata.permissions().mode() & 0o111 != 0 {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let mut plugin = crate::plugins::ExternalPlugin::new(
name.to_string(),
path.clone(),
format!("External plugin: {}", name),
crate::plugins::traits::ScanPhase::PostExtract,
);
if plugin.initialize().await.is_ok() {
plugins.push(Box::new(plugin));
}
}
}
}
}
#[cfg(not(unix))]
{
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let mut plugin = crate::plugins::ExternalPlugin::new(
name.to_string(),
path.clone(),
format!("External plugin: {}", name),
crate::plugins::traits::ScanPhase::PostExtract,
);
if plugin.initialize().await.is_ok() {
plugins.push(Box::new(plugin));
}
}
}
}
}
}
}
for plugin in &mut plugins {
let _ = plugin.initialize().await;
}
for entry in WalkDir::new(path)
.follow_links(false)
.into_iter()
.filter_map(|e| e.ok())
{
let entry_path = entry.path();
if !entry_path.is_file() {
continue;
}
if self.should_skip_path(entry_path) {
continue;
}
let max_bytes = self.config.scan.max_scan_file_mb * 1024 * 1024;
if max_bytes > 0 {
if let Ok(meta) = std::fs::metadata(entry_path) {
if meta.len() > max_bytes {
report.warnings.push(format!(
"Skipped {} ({}MB exceeds {}MB limit)",
entry_path.display(),
meta.len() / (1024 * 1024),
self.config.scan.max_scan_file_mb
));
continue;
}
}
}
if let Ok(content) = std::fs::read(entry_path) {
report.scanned_files += 1;
report.scanned_bytes += content.len() as u64;
let context = ScanContext {
path: entry_path,
scan_phase: ScanPhase::PostExtract,
file_content: Some(&content),
metadata: HashMap::new(),
};
for plugin in &plugins {
match plugin.scan(&context).await {
Ok(plugin_report) => {
let mut filtered_findings = plugin_report.findings;
filtered_findings.retain(|f| {
f.file_path
.as_ref()
.map(|p| !self.is_allowlisted(p))
.unwrap_or(true)
});
let findings_count = filtered_findings.len();
report.findings.extend(filtered_findings);
report.plugin_reports.push(PluginScanReport {
plugin_name: plugin.name().to_string(),
findings_count,
duration_ms: plugin_report.duration_ms,
});
}
Err(e) => {
report.warnings.push(format!(
"Plugin '{}' failed on {}: {}",
plugin.name(),
entry_path.display(),
e
));
}
}
}
}
}
report.duration_ms = start.elapsed().as_millis() as u64;
for plugin in &mut plugins {
let _ = plugin.cleanup().await;
}
Ok(report)
}
pub async fn scan_file(&self, file_path: &Path) -> Result<ScanReport> {
let start = Instant::now();
let mut report = ScanReport::new();
let mut plugins: Vec<Box<dyn SecurityPlugin>> = vec![
Box::new(SecretsScanner::new()),
Box::new(PatternScanner::new()),
Box::new(EntropyScanner::new()),
Box::new(BinaryScanner::new()),
Box::new(CicdScanner::new()),
Box::new(SupplyChainScanner::new()),
Box::new(ContainerScanner::new()),
Box::new(IacScanner::new()),
Box::new(DeserializationScanner::new()),
Box::new(DangerousFilesScanner::new()),
Box::new(EncodingScanner::new()),
];
for plugin in &mut plugins {
let _ = plugin.initialize().await;
}
if let Ok(content) = std::fs::read(file_path) {
report.scanned_files = 1;
report.scanned_bytes = content.len() as u64;
let context = ScanContext {
path: file_path,
scan_phase: ScanPhase::PostExtract,
file_content: Some(&content),
metadata: HashMap::new(),
};
for plugin in &plugins {
match plugin.scan(&context).await {
Ok(plugin_report) => {
let findings_count = plugin_report.findings.len();
report.findings.extend(plugin_report.findings);
report.plugin_reports.push(PluginScanReport {
plugin_name: plugin.name().to_string(),
findings_count,
duration_ms: plugin_report.duration_ms,
});
}
Err(e) => {
report.warnings.push(format!(
"Plugin '{}' failed on {}: {}",
plugin.name(),
file_path.display(),
e
));
}
}
}
}
report.findings.retain(|f| {
f.file_path
.as_ref()
.map(|p| !self.is_allowlisted(p))
.unwrap_or(true)
});
report.duration_ms = start.elapsed().as_millis() as u64;
for plugin in &mut plugins {
let _ = plugin.cleanup().await;
}
Ok(report)
}
pub async fn scan_git_directory(&self, path: &Path) -> Result<ScanReport> {
let start = Instant::now();
let mut report = ScanReport::new();
let mut plugin = GitInternalsScanner::new();
plugin.initialize().await?;
let context = ScanContext {
path,
scan_phase: ScanPhase::GitInternals,
file_content: None,
metadata: HashMap::new(),
};
match plugin.scan(&context).await {
Ok(plugin_report) => {
let findings_count = plugin_report.findings.len();
report.findings.extend(plugin_report.findings);
report.scanned_files += plugin_report.scanned_files;
report.plugin_reports.push(PluginScanReport {
plugin_name: plugin.name().to_string(),
findings_count,
duration_ms: plugin_report.duration_ms,
});
}
Err(e) => {
report.warnings.push(format!(
"Plugin '{}' failed on git directory {}: {}",
plugin.name(),
path.display(),
e
));
}
}
report.duration_ms = start.elapsed().as_millis() as u64;
plugin.cleanup().await?;
Ok(report)
}
fn is_allowlisted(&self, path: &Path) -> bool {
self.config.scan.is_allowlisted(path)
}
fn should_skip_path(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for skip_pattern in &self.config.scan.skip_paths {
if path_str.contains(skip_pattern.trim_end_matches("/**")) {
return true;
}
}
false
}
}