use crate::cli::args::HookCommands;
use crate::cli::UI;
use crate::core::{PluginScanReport, ScanReport, Severity};
use crate::plugins::builtin::{patterns::PatternScanner, secrets::SecretsScanner};
use crate::plugins::traits::{ScanContext, ScanPhase, SecurityPlugin};
use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
pub async fn execute(action: HookCommands, ui: &UI) -> Result<()> {
match action {
HookCommands::Install {
pre_commit,
pre_push,
all,
} => install_hooks(pre_commit, pre_push, all, ui).await,
HookCommands::Uninstall { all } => uninstall_hooks(all, ui).await,
HookCommands::Status => show_status(ui).await,
}
}
async fn install_hooks(pre_commit: bool, pre_push: bool, all: bool, ui: &UI) -> Result<()> {
ui.header("Hook Installation");
ui.blank();
let git_dir = PathBuf::from(".git");
if !git_dir.exists() {
anyhow::bail!("Not in a git repository");
}
let hooks_dir = git_dir.join("hooks");
std::fs::create_dir_all(&hooks_dir)?;
let install_all = all || (!pre_commit && !pre_push);
if install_all || pre_commit {
let hook_path = hooks_dir.join("pre-commit");
let hook_content = generate_pre_commit_hook();
std::fs::write(&hook_path, hook_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755))?;
}
ui.status_item(true, "pre-commit hook installed");
}
if install_all || pre_push {
let hook_path = hooks_dir.join("pre-push");
let hook_content = generate_pre_push_hook();
std::fs::write(&hook_path, hook_content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755))?;
}
ui.status_item(true, "pre-push hook installed");
}
ui.blank();
ui.success("Hooks installed successfully");
ui.blank();
ui.info("These hooks will scan your code before commit/push");
ui.info("To bypass: git commit --no-verify");
ui.blank();
Ok(())
}
async fn uninstall_hooks(_all: bool, ui: &UI) -> Result<()> {
ui.header("Hook Removal");
ui.blank();
let hooks_dir = PathBuf::from(".git/hooks");
let hooks = vec!["pre-commit", "pre-push"];
for hook in hooks {
let hook_path = hooks_dir.join(hook);
if hook_path.exists() {
let content = std::fs::read_to_string(&hook_path)?;
if content.contains("securegit") {
std::fs::remove_file(&hook_path)?;
ui.status_item(true, format!("{} hook removed", hook));
}
}
}
ui.blank();
Ok(())
}
async fn show_status(ui: &UI) -> Result<()> {
ui.header("Hook Status");
ui.blank();
let hooks_dir = PathBuf::from(".git/hooks");
if !hooks_dir.exists() {
ui.info("Not in a git repository");
return Ok(());
}
let hooks = vec!["pre-commit", "pre-push"];
for hook in hooks {
let hook_path = hooks_dir.join(hook);
if hook_path.exists() {
let content = std::fs::read_to_string(&hook_path)?;
if content.contains("securegit") {
ui.status_item(true, hook);
} else {
ui.status_item(false, format!("{} (not securegit)", hook));
}
} else {
ui.status_item(false, format!("{} (not installed)", hook));
}
}
ui.blank();
Ok(())
}
pub async fn pre_commit(fail_on: String, ui: &UI) -> Result<()> {
ui.info("Running pre-commit scan...");
let threshold = Severity::parse_str(&fail_on).unwrap_or(Severity::High);
let repo = crate::ops::open_repo(&std::path::PathBuf::from("."))?;
let index = repo.index()?;
let mut plugins: Vec<Box<dyn SecurityPlugin>> = vec![
Box::new(SecretsScanner::new()),
Box::new(PatternScanner::new()),
];
for plugin in &mut plugins {
let _ = plugin.initialize().await;
}
let mut report = ScanReport::new();
for entry in index.iter() {
let path = String::from_utf8_lossy(&entry.path).to_string();
let path_buf = PathBuf::from(&path);
let blob = match repo.find_blob(entry.id) {
Ok(b) => b,
Err(_) => continue,
};
let content = blob.content().to_vec();
report.scanned_files += 1;
report.scanned_bytes += content.len() as u64;
let context = ScanContext {
path: &path_buf,
scan_phase: ScanPhase::PreCommit,
file_content: Some(&content),
metadata: HashMap::new(),
};
for plugin in &plugins {
if let Ok(plugin_report) = plugin.scan(&context).await {
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,
});
}
}
}
for plugin in &mut plugins {
let _ = plugin.cleanup().await;
}
let config = crate::core::Config::default();
report.findings.retain(|f| {
f.file_path
.as_ref()
.map(|p| !config.scan.is_allowlisted(p))
.unwrap_or(true)
});
if report.has_findings_at_or_above(threshold) {
let count = report
.findings
.iter()
.filter(|f| f.severity >= threshold)
.count();
ui.blank();
for finding in &report.findings {
if finding.severity >= threshold {
let path = finding
.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default();
ui.status_item(
false,
format!("[{}] {} {}", finding.severity, finding.title, path),
);
}
}
ui.blank();
anyhow::bail!(
"Pre-commit scan failed: {} finding(s) at or above {} severity",
count,
threshold
);
}
ui.success(format!(
"Pre-commit scan passed ({} files scanned)",
report.scanned_files
));
Ok(())
}
pub async fn pre_push(fail_on: String, ui: &UI) -> Result<()> {
ui.info("Running pre-push scan...");
let threshold = Severity::parse_str(&fail_on).unwrap_or(Severity::High);
let repo = crate::ops::open_repo(&std::path::PathBuf::from("."))?;
let head = repo.head()?.peel_to_commit()?;
let tree = head.tree()?;
let mut plugins: Vec<Box<dyn SecurityPlugin>> = vec![
Box::new(SecretsScanner::new()),
Box::new(PatternScanner::new()),
];
for plugin in &mut plugins {
let _ = plugin.initialize().await;
}
let mut entries: Vec<(String, git2::Oid)> = Vec::new();
tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
if entry.kind() != Some(git2::ObjectType::Blob) {
return git2::TreeWalkResult::Ok;
}
let name = match entry.name() {
Some(n) => n,
None => return git2::TreeWalkResult::Ok,
};
let path = if dir.is_empty() {
name.to_string()
} else {
format!("{}{}", dir, name)
};
entries.push((path, entry.id()));
git2::TreeWalkResult::Ok
})?;
let mut report = ScanReport::new();
for (path, oid) in &entries {
let blob = match repo.find_blob(*oid) {
Ok(b) => b,
Err(_) => continue,
};
let content = blob.content().to_vec();
let path_buf = PathBuf::from(path);
report.scanned_files += 1;
report.scanned_bytes += content.len() as u64;
let context = ScanContext {
path: &path_buf,
scan_phase: ScanPhase::PrePush,
file_content: Some(&content),
metadata: HashMap::new(),
};
for plugin in &plugins {
if let Ok(plugin_report) = plugin.scan(&context).await {
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,
});
}
}
}
for plugin in &mut plugins {
let _ = plugin.cleanup().await;
}
let config = crate::core::Config::default();
report.findings.retain(|f| {
f.file_path
.as_ref()
.map(|p| !config.scan.is_allowlisted(p))
.unwrap_or(true)
});
if report.has_findings_at_or_above(threshold) {
let count = report
.findings
.iter()
.filter(|f| f.severity >= threshold)
.count();
ui.blank();
for finding in &report.findings {
if finding.severity >= threshold {
let path = finding
.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_default();
ui.status_item(
false,
format!("[{}] {} {}", finding.severity, finding.title, path),
);
}
}
ui.blank();
anyhow::bail!(
"Pre-push scan failed: {} finding(s) at or above {} severity",
count,
threshold
);
}
ui.success(format!(
"Pre-push scan passed ({} files scanned)",
report.scanned_files
));
Ok(())
}
fn generate_pre_commit_hook() -> String {
r#"#!/bin/sh
# SecureGit pre-commit hook
# Auto-generated - do not edit manually
echo "Running SecureGit pre-commit scan..."
if command -v securegit > /dev/null 2>&1; then
securegit pre-commit --fail-on high
exit $?
else
echo "WARNING: securegit not found in PATH"
echo "Install securegit or remove this hook"
exit 1
fi
"#
.to_string()
}
fn generate_pre_push_hook() -> String {
r#"#!/bin/sh
# SecureGit pre-push hook
# Auto-generated - do not edit manually
echo "Running SecureGit pre-push scan..."
if command -v securegit > /dev/null 2>&1; then
securegit pre-push --fail-on high
exit $?
else
echo "WARNING: securegit not found in PATH"
echo "Install securegit or remove this hook"
exit 1
fi
"#
.to_string()
}