use std::path::PathBuf;
use anyhow::Result;
use aptu_core::{AppConfig, Finding, PatternEngine, SarifReport, SecurityScanner};
use walkdir::WalkDir;
use crate::cli::OutputFormat;
#[allow(clippy::unused_async)]
pub async fn run_scan_security_command(
path: PathBuf,
fail_on: Vec<String>,
exclude: Vec<String>,
output_format: OutputFormat,
_config: &AppConfig,
) -> Result<()> {
let scanner = SecurityScanner::default();
let mut findings: Vec<Finding> = Vec::new();
for entry in WalkDir::new(&path)
.follow_links(false)
.into_iter()
.filter_map(std::result::Result::ok)
{
if entry.file_type().is_dir() {
continue;
}
let file_path = entry.path();
let file_path_str = file_path.to_string_lossy();
if exclude
.iter()
.any(|prefix| file_path_str.starts_with(prefix.as_str()))
{
continue;
}
let Ok(content) = std::fs::read_to_string(file_path) else {
continue;
};
let file_findings = scanner.scan_file(&content, &file_path_str);
findings.extend(file_findings);
}
emit_output(output_format, &findings)?;
if !fail_on.is_empty() {
let fail_severities: Vec<String> = fail_on.iter().map(|s| s.to_lowercase()).collect();
let should_fail = findings
.iter()
.any(|f| fail_severities.iter().any(|s| s == f.severity.as_str()));
if should_fail {
return Err(anyhow::Error::new(crate::errors::ScanFindingsExit));
}
}
Ok(())
}
fn emit_output(output_format: OutputFormat, findings: &[Finding]) -> Result<()> {
match output_format {
OutputFormat::Sarif => {
let engine = PatternEngine::from_embedded_json()?;
let patterns = engine.definitions();
let report = SarifReport::with_rules(findings.to_vec(), &patterns);
let json = serde_json::to_string_pretty(&report)
.map_err(|e| anyhow::anyhow!("Failed to serialize SARIF: {e}"))?;
println!("{json}");
}
OutputFormat::GithubAnnotations => {
for f in findings {
println!(
"::error file={},line={},title={}::{}",
f.file_path, f.line_number, f.pattern_id, f.description
);
}
}
OutputFormat::Json => {
let json = serde_json::to_string_pretty(findings)
.map_err(|e| anyhow::anyhow!("Failed to serialize findings to JSON: {e}"))?;
println!("{json}");
}
OutputFormat::Yaml => {
let yaml = serde_saphyr::to_string(&findings.to_vec())
.map_err(|e| anyhow::anyhow!("Failed to serialize findings to YAML: {e}"))?;
println!("{yaml}");
}
OutputFormat::Text | OutputFormat::Markdown => {
if findings.is_empty() {
println!("No security findings.");
} else {
println!("Security findings ({}):", findings.len());
for f in findings {
println!(
" [{}] {} ({}:{}): {}",
format!("{:?}", f.severity).to_uppercase(),
f.pattern_id,
f.file_path,
f.line_number,
f.description
);
}
}
}
}
Ok(())
}