kingfisher/src/scanner/summary.rs
2025-08-09 15:36:12 -07:00

211 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::{
io::{self, Write},
sync::{Arc, Mutex},
};
use http::StatusCode;
use indicatif::HumanBytes;
use serde_json::json;
use thousands::Separable;
use tokio::time::Instant;
use tracing::debug;
use crate::{
cli::{
commands::{output::ReportOutputFormat, scan},
global,
},
findings_store,
matcher::MatcherStats,
rule_profiling::ConcurrentRuleProfiler,
rules_database::RulesDatabase,
};
macro_rules! safe_println {
($($arg:tt)*) => {
if let Err(e) = writeln!(io::stdout(), $($arg)*) {
if e.kind() == io::ErrorKind::BrokenPipe {
// Silently exit: the consumer went away
std::process::exit(0);
} else {
// Unexpected I/O error keep the old behaviour
panic!("stdout error: {}", e);
}
}
};
}
pub fn print_scan_summary(
start_time: Instant,
datastore: &Arc<Mutex<findings_store::FindingsStore>>,
global_args: &global::GlobalArgs,
args: &scan::ScanArgs,
// inputs: &FilesystemEnumeratorResult,
rules_db: &RulesDatabase,
matcher_stats: &Mutex<MatcherStats>,
profiler: Option<&ConcurrentRuleProfiler>,
) {
if global_args.quiet {
if args.rule_stats {
if let Some(prof) = profiler {
let stats = prof.generate_report();
if !stats.is_empty() {
let name_w = stats.iter().map(|s| s.rule_name.len()).max().unwrap_or(4);
let id_w = stats.iter().map(|s| s.rule_id.len()).max().unwrap_or(2);
safe_println!("\n{:-^1$}", " Rule Performance Stats ", name_w + id_w + 47);
safe_println!(
"{: <name_w$} {: <id_w$} {: >8} {: >15} {: >15}",
"Rule",
"ID",
"Matches",
"Slowest",
"Average",
name_w = name_w,
id_w = id_w
);
safe_println!("{:-<width$}", "", width = name_w + id_w + 49);
for rs in stats {
safe_println!(
"{: <name_w$} {: <id_w$} {: >8} {: >15?} {: >15?}",
rs.rule_name,
rs.rule_id,
rs.total_matches,
rs.slowest_match_time,
rs.average_match_time,
name_w = name_w,
id_w = id_w
);
}
}
}
}
return;
}
let ds = datastore.lock().unwrap();
let num_rules = rules_db.num_rules();
let findings_by_rule = ds.get_summary();
let mut sorted_findings: Vec<_> = findings_by_rule.into_iter().collect();
sorted_findings.sort_by(|a, b| b.1.cmp(&a.1));
let duration = start_time.elapsed();
let all_matches = ds.get_matches();
let total_findings = if args.no_dedup {
all_matches.iter().fold(0, |count, msg| {
let (origin_set, _, match_item) = &**msg;
if match_item.validation_success {
count + origin_set.len()
} else {
count + 1
}
})
} else {
ds.get_num_matches()
};
let (successful_validations, failed_validations) =
all_matches.iter().fold((0, 0), |(success, fail), msg| {
let (origin_set, _, match_item) = &**msg;
if match_item.validation_success {
if match_item.validation_response_status != StatusCode::CONTINUE.as_u16() {
if args.no_dedup {
(success + origin_set.len(), fail)
} else {
(success + 1, fail)
}
} else {
(success, fail)
}
} else if match_item.validation_response_status != StatusCode::CONTINUE.as_u16() {
(success, fail + 1)
} else {
(success, fail)
}
});
let matcher_stats = matcher_stats.lock().unwrap();
if args.output_args.format == ReportOutputFormat::Json
|| args.output_args.format == ReportOutputFormat::Jsonl
{
let summary = json!({
"findings": total_findings,
"successful_validations": successful_validations,
"failed_validations": failed_validations,
"rules_applied": num_rules,
"blobs_scanned": matcher_stats.blobs_scanned,
"bytes_scanned": matcher_stats.bytes_scanned,
"scan_duration": duration.as_secs_f64(),
"findings_by_rule": sorted_findings
});
safe_println!("{}", summary.to_string());
} else if args.output_args.format == ReportOutputFormat::Pretty
|| args.output_args.output.is_some()
{
safe_println!("\n==========================================");
safe_println!("Scan Summary:");
safe_println!("==========================================");
safe_println!(" |Findings....................: {}", total_findings.separate_with_commas());
safe_println!(
" |__Successful Validations....: {}",
successful_validations.separate_with_commas()
);
safe_println!(
" |__Failed Validations........: {}",
failed_validations.separate_with_commas()
);
safe_println!(" |Rules Applied...............: {}", num_rules.separate_with_commas());
safe_println!(
" |__Blobs Scanned.............: {}",
matcher_stats.blobs_scanned.separate_with_commas()
);
safe_println!(
" |Bytes Scanned...............: {}",
HumanBytes(matcher_stats.bytes_scanned)
);
safe_println!(" |Scan Duration...............: {}", humantime::format_duration(duration));
}
if args.rule_stats {
if let Some(prof) = profiler {
let stats = prof.generate_report();
if !stats.is_empty() {
let name_w = stats.iter().map(|s| s.rule_name.len()).max().unwrap_or(4);
let id_w = stats.iter().map(|s| s.rule_id.len()).max().unwrap_or(2);
safe_println!("\n{:-^1$}", " Rule Performance Stats ", name_w + id_w + 47);
safe_println!(
"{: <name_w$} {: <id_w$} {: >8} {: >15} {: >15}",
"Rule",
"ID",
"Matches",
"Slowest",
"Average",
name_w = name_w,
id_w = id_w
);
safe_println!("{:-<width$}", "", width = name_w + id_w + 49);
for rs in stats {
safe_println!(
"{: <name_w$} {: <id_w$} {: >8} {: >15?} {: >15?}",
rs.rule_name,
rs.rule_id,
rs.total_matches,
rs.slowest_match_time,
rs.average_match_time,
name_w = name_w,
id_w = id_w
);
}
}
}
}
debug!("\nAll Rules with Matches:");
debug!("=======================");
let max_rule_length = sorted_findings.iter().map(|(rule, _)| rule.len()).max().unwrap_or(0);
for (rule, count) in sorted_findings {
debug!("{: <width$}: {}", rule, count, width = max_rule_length);
}
}