use crate::cli::UI;
use crate::core::{Config, ScanEngine, Severity};
use crate::platform;
use crate::platform::types::CreateIssue;
use anyhow::Result;
use std::collections::HashSet;
use std::path::PathBuf;
pub async fn execute(
path: PathBuf,
fail_on: Option<String>,
include_git: bool,
create_issues: bool,
issue_threshold: String,
ui: &UI,
) -> Result<()> {
ui.header("Security Scan");
ui.blank();
ui.field("Target", path.display());
ui.field("Include .git", include_git);
ui.blank();
let config = Config::default();
let engine = ScanEngine::new(config);
let mut report = engine.scan_directory(&path).await?;
if include_git {
let git_dir = path.join(".git");
if git_dir.exists() {
let git_report = engine.scan_git_directory(&git_dir).await?;
report.merge(git_report);
}
}
if ui.json {
let json_value = serde_json::to_value(&report).unwrap_or_default();
ui.json_out(&json_value);
} else {
display_report(&report, ui);
}
if create_issues && !report.findings.is_empty() {
let issue_sev = Severity::parse_str(&issue_threshold).unwrap_or(Severity::High);
let qualifying: Vec<_> = report
.findings
.iter()
.filter(|f| f.severity >= issue_sev)
.collect();
if !qualifying.is_empty() {
ui.blank();
ui.section("Filing Issues");
match file_issues_from_findings(&qualifying, ui).await {
Ok((created, skipped)) => {
ui.blank();
ui.success(format!("{} issues created, {} skipped", created, skipped));
}
Err(e) => {
ui.blank();
ui.warning(format!("Could not file issues: {}", e));
}
}
}
}
let threshold = match fail_on.as_deref() {
Some("critical") => Severity::Critical,
Some("high") => Severity::High,
Some("medium") => Severity::Medium,
Some("low") => Severity::Low,
_ => Severity::High, };
if report.has_findings_at_or_above(threshold) {
anyhow::bail!(
"Scan failed: Found {} findings at or above {} severity",
report
.findings
.iter()
.filter(|f| f.severity >= threshold)
.count(),
threshold
);
}
Ok(())
}
async fn file_issues_from_findings(
findings: &[&crate::core::Finding],
ui: &UI,
) -> Result<(usize, usize)> {
let path = PathBuf::from(".");
let remote = platform::detect_remote(&path)?;
let token = platform::resolve_token(&remote.host)
.ok_or_else(|| anyhow::anyhow!("Not authenticated. Run: securegit auth login"))?;
let client = platform::create_client(&remote, token);
let mut seen = HashSet::new();
let mut created = 0usize;
let mut skipped = 0usize;
for finding in findings {
let file_str = finding
.file_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "unknown".to_string());
let dedup_key = format!("{}:{}", finding.title, file_str);
if !seen.insert(dedup_key) {
skipped += 1;
continue;
}
let issue_title = format!(
"[SecureGit] {}: {} in {}",
finding.severity, finding.title, file_str
);
let existing = client
.search_issues(&format!("[SecureGit] {} in {}", finding.title, file_str))
.await
.unwrap_or_default();
if existing
.iter()
.any(|i| i.title.contains(&finding.title) && i.state == "open")
{
ui.status_item(false, format!("Exists: {}", issue_title));
skipped += 1;
continue;
}
let mut body = "## Security Finding\n\n".to_string();
body.push_str(&format!("**Severity:** {}\n", finding.severity));
body.push_str(&format!("**File:** `{}`\n", file_str));
if let Some(line) = finding.line_start {
body.push_str(&format!("**Line:** {}\n", line));
}
body.push_str(&format!("\n### Description\n\n{}\n", finding.description));
if let Some(ref snippet) = finding.code_snippet {
body.push_str(&format!("\n### Evidence\n\n```\n{}\n```\n", snippet));
}
if let Some(ref remediation) = finding.remediation {
body.push_str(&format!("\n### Remediation\n\n{}\n", remediation));
}
if !finding.cwe_ids.is_empty() {
body.push_str("\n### References\n\n");
for cwe in &finding.cwe_ids {
body.push_str(&format!(
"- [CWE-{}](https://cwe.mitre.org/data/definitions/{}.html)\n",
cwe, cwe
));
}
}
body.push_str("\n---\n*Filed automatically by securegit*\n");
let severity_label = format!("{}", finding.severity).to_lowercase();
let labels = vec!["security".to_string(), severity_label];
match client
.create_issue(&CreateIssue {
title: issue_title.clone(),
body,
labels,
})
.await
{
Ok(issue) => {
ui.status_item(true, format!("Created #{}: {}", issue.number, issue_title));
created += 1;
}
Err(e) => {
ui.status_item(false, format!("Failed: {} ({})", issue_title, e));
skipped += 1;
}
}
}
Ok((created, skipped))
}
fn display_report(report: &crate::core::ScanReport, ui: &UI) {
ui.section("Scan Results");
ui.field("Files scanned", report.scanned_files);
ui.field(
"Data scanned",
format!("{} KB", report.scanned_bytes / 1024),
);
ui.field("Duration", format!("{} ms", report.duration_ms));
ui.blank();
let critical = report.count_by_severity(Severity::Critical);
let high = report.count_by_severity(Severity::High);
let medium = report.count_by_severity(Severity::Medium);
let low = report.count_by_severity(Severity::Low);
let info_count = report.count_by_severity(Severity::Info);
ui.severity_row(critical, high, medium, low, info_count);
if report.findings.is_empty() {
ui.result_banner(true, "No security issues found", &[]);
return;
}
ui.blank();
for severity in [
Severity::Critical,
Severity::High,
Severity::Medium,
Severity::Low,
] {
let findings: Vec<_> = report
.findings
.iter()
.filter(|f| f.severity == severity)
.collect();
if findings.is_empty() {
continue;
}
for finding in findings {
ui.finding(finding);
}
}
if !report.plugin_reports.is_empty() {
ui.section("Plugin Performance:");
for plugin_report in &report.plugin_reports {
if plugin_report.findings_count > 0 {
ui.list_item(format!(
"{} - {} findings in {}ms",
plugin_report.plugin_name,
plugin_report.findings_count,
plugin_report.duration_ms
));
}
}
ui.blank();
}
}