use std::process;
use clap::Parser;
mod cli;
mod finding;
mod output;
mod paths;
mod report;
mod scanner;
use cli::Cli;
use finding::Severity;
use output::OutputConfig;
use paths::resolve;
use report::Report;
use scanner::run_all;
fn main() {
match run() {
Ok(exit_code) => process::exit(exit_code),
Err(err) => {
eprintln!("error: {err:#}");
process::exit(2);
}
}
}
fn run() -> anyhow::Result<i32> {
let cli = Cli::parse();
let roots = resolve(cli.paths.clone())?;
let mut all_findings = Vec::new();
let mut scanned_paths: Vec<String> = Vec::new();
for root in &roots {
let ctx = scanner::ScanContext {
root: root.path.clone(),
framework: root.framework,
};
let mut findings = run_all(&ctx);
if let Some(cat_filter) = cli.category {
let cat = cat_filter.to_category();
findings.retain(|f| f.category == cat);
}
scanned_paths.push(root.path.display().to_string());
all_findings.extend(findings);
}
let report = Report::build(all_findings, scanned_paths, env!("CARGO_PKG_VERSION"));
let threshold: Severity = cli.min_severity.to_severity();
let visible_report = filter_report(report, threshold);
let cfg = OutputConfig {
json: cli.json,
quiet: cli.quiet,
verbose: cli.verbose,
color: !cli.no_color,
};
output::render(&visible_report, &cfg)?;
let has_findings = visible_report.has_findings_at(threshold);
Ok(if has_findings { 1 } else { 0 })
}
fn filter_report(mut report: Report, min: Severity) -> Report {
report.findings.retain(|f| f.severity >= min);
report.total_critical = report
.findings
.iter()
.filter(|f| f.severity == Severity::Critical)
.count();
report.total_high = report
.findings
.iter()
.filter(|f| f.severity == Severity::High)
.count();
report.total_medium = report
.findings
.iter()
.filter(|f| f.severity == Severity::Medium)
.count();
report.total_low = report
.findings
.iter()
.filter(|f| f.severity == Severity::Low)
.count();
report.total_info = report
.findings
.iter()
.filter(|f| f.severity == Severity::Info)
.count();
report
}
#[cfg(test)]
mod tests {
use super::*;
use crate::finding::{Category, Finding};
fn make_report(findings: Vec<Finding>) -> Report {
Report::build(findings, vec![], env!("CARGO_PKG_VERSION"))
}
#[test]
fn filter_report_removes_low_when_threshold_is_medium() {
let findings = vec![
Finding::new(Severity::Low, Category::DataExposure, "Low", "D", "/f", "R"),
Finding::new(
Severity::Medium,
Category::ConfigSecurity,
"Medium",
"D",
"/f",
"R",
),
Finding::new(
Severity::Critical,
Category::SecretDetection,
"Critical",
"D",
"/f",
"R",
),
];
let report = make_report(findings);
let filtered = filter_report(report, Severity::Medium);
assert_eq!(filtered.findings.len(), 2);
assert!(filtered
.findings
.iter()
.all(|f| f.severity >= Severity::Medium));
}
#[test]
fn filter_report_keeps_all_when_threshold_is_info() {
let findings = vec![
Finding::new(Severity::Info, Category::DataExposure, "I", "D", "/f", "R"),
Finding::new(
Severity::Critical,
Category::SecretDetection,
"C",
"D",
"/f",
"R",
),
];
let count = findings.len();
let report = make_report(findings);
let filtered = filter_report(report, Severity::Info);
assert_eq!(filtered.findings.len(), count);
}
#[test]
fn filter_report_recomputes_totals() {
let findings = vec![
Finding::new(Severity::Low, Category::DataExposure, "L", "D", "/f", "R"),
Finding::new(
Severity::Critical,
Category::SecretDetection,
"C",
"D",
"/f",
"R",
),
];
let report = make_report(findings);
let filtered = filter_report(report, Severity::Medium);
assert_eq!(filtered.total_critical, 1);
assert_eq!(filtered.total_low, 0);
}
#[test]
fn filter_report_removes_everything_when_threshold_is_critical_and_no_criticals() {
let findings = vec![Finding::new(
Severity::High,
Category::ConfigSecurity,
"H",
"D",
"/f",
"R",
)];
let report = make_report(findings);
let filtered = filter_report(report, Severity::Critical);
assert!(filtered.findings.is_empty());
}
}