mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] sweep after phase 25: 2 deferred items resolved
This commit is contained in:
parent
76d0037073
commit
4228be2db6
3 changed files with 95 additions and 18 deletions
|
|
@ -467,15 +467,15 @@ pub fn handle(
|
|||
let idx = Indexer::from_pool(&project_name, &pool)?;
|
||||
idx.vacuum()?;
|
||||
}
|
||||
// Indexed scan path: Phase 25 chain composer needs a
|
||||
// SurfaceMap. The indexed pipeline does not yet thread one
|
||||
// out — Phase 23's CLI loads it from SQLite when needed. For
|
||||
// now return an empty map so chain emission produces no
|
||||
// chains; this matches pre-Phase-25 behaviour for indexed
|
||||
// scans.
|
||||
// Indexed scan path: persist + return the SurfaceMap so the
|
||||
// Phase 25 chain composer can walk it. `scan_with_index_parallel_observer`
|
||||
// already builds and persists the map into the `surface_map`
|
||||
// SQLite table; reload it through the same pool so the indexed
|
||||
// chain emission matches the non-indexed branch.
|
||||
let scan_pool = Arc::clone(&pool);
|
||||
let diags = scan_with_index_parallel_observer(
|
||||
&project_name,
|
||||
pool,
|
||||
scan_pool,
|
||||
config,
|
||||
show_progress,
|
||||
&scan_path,
|
||||
|
|
@ -484,7 +484,11 @@ pub fn handle(
|
|||
None,
|
||||
Some(&preview_tier_seen),
|
||||
)?;
|
||||
(diags, crate::surface::SurfaceMap::new())
|
||||
let surface_map = {
|
||||
let idx = Indexer::from_pool(&project_name, &pool)?;
|
||||
idx.load_surface_map()?.unwrap_or_default()
|
||||
};
|
||||
(diags, surface_map)
|
||||
};
|
||||
|
||||
// Print the Preview-tier banner to stderr once, after file enumeration
|
||||
|
|
@ -646,7 +650,12 @@ pub fn handle(
|
|||
tracing::debug!("Printing to console");
|
||||
print!(
|
||||
"{}",
|
||||
crate::fmt::render_console(&diags, &project_name, Some(&stats))
|
||||
crate::fmt::render_console(
|
||||
&diags_for_output,
|
||||
&project_name,
|
||||
Some(&stats),
|
||||
&chains,
|
||||
)
|
||||
);
|
||||
if let Some(ref diff) = verdict_diff {
|
||||
println!("\nBaseline comparison:");
|
||||
|
|
|
|||
74
src/fmt.rs
74
src/fmt.rs
|
|
@ -4,6 +4,7 @@
|
|||
//! severity hierarchy, normalised taint flow rendering, and stable wrapping.
|
||||
#![allow(clippy::collapsible_if)]
|
||||
|
||||
use crate::chain::finding::ChainFinding;
|
||||
use crate::commands::scan::{Diag, SuppressionStats};
|
||||
use crate::patterns::Severity;
|
||||
use console::style;
|
||||
|
|
@ -17,14 +18,26 @@ const DEFAULT_WIDTH: usize = 100;
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Render all diagnostics as grouped, formatted console output with a summary.
|
||||
///
|
||||
/// `chains` is the list of composed exploit chains emitted alongside
|
||||
/// `diags`. When non-empty, a `Chains` section is printed ahead of the
|
||||
/// per-file findings. Callers that have already gated constituent
|
||||
/// findings on `[output] show_chain_constituents` should pass the
|
||||
/// filtered `diags` slice so the constituent listing matches the JSON /
|
||||
/// SARIF emitters.
|
||||
pub fn render_console(
|
||||
diags: &[Diag],
|
||||
project_name: &str,
|
||||
suppression_stats: Option<&SuppressionStats>,
|
||||
chains: &[ChainFinding],
|
||||
) -> String {
|
||||
let width = terminal_width();
|
||||
let mut out = String::new();
|
||||
|
||||
if !chains.is_empty() {
|
||||
out.push_str(&render_chains(chains, width));
|
||||
}
|
||||
|
||||
let mut grouped: BTreeMap<&str, Vec<&Diag>> = BTreeMap::new();
|
||||
for d in diags {
|
||||
grouped.entry(&d.path).or_default().push(d);
|
||||
|
|
@ -240,6 +253,61 @@ const LOGO: &[&str] = &[
|
|||
/// Indentation for body/evidence lines (spaces).
|
||||
const BODY_INDENT: usize = 6;
|
||||
|
||||
/// Render the `Chains` header section. Each chain is summarised on
|
||||
/// two lines: severity + impact + score header, then sink location +
|
||||
/// constituent count.
|
||||
fn render_chains(chains: &[ChainFinding], _width: usize) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!(
|
||||
"{}\n",
|
||||
style(format!("Chains ({})", chains.len())).bold().underlined()
|
||||
));
|
||||
for c in chains {
|
||||
let sev = chain_severity_tag(c.severity);
|
||||
let impact = format!("{:?}", c.implied_impact);
|
||||
let header = format!(
|
||||
" {} [{}] {} (score: {:.1}, {} members)",
|
||||
sev,
|
||||
impact,
|
||||
style(&c.sink.function_name).bold(),
|
||||
c.score,
|
||||
c.members.len()
|
||||
);
|
||||
out.push_str(&format!("{header}\n"));
|
||||
out.push_str(&format!(
|
||||
" {} {}:{}:{}\n",
|
||||
style("sink:").dim(),
|
||||
c.sink.file,
|
||||
c.sink.line,
|
||||
c.sink.col
|
||||
));
|
||||
for m in &c.members {
|
||||
out.push_str(&format!(
|
||||
" {} {} {}:{}:{}\n",
|
||||
style("via:").dim(),
|
||||
style(&m.rule_id).dim(),
|
||||
m.location.file,
|
||||
m.location.line,
|
||||
m.location.col
|
||||
));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Render a chain severity tag with the same shape as the per-diag
|
||||
/// severity tag so chain output reads consistently next to findings.
|
||||
fn chain_severity_tag(s: crate::chain::finding::ChainSeverity) -> String {
|
||||
use crate::chain::finding::ChainSeverity;
|
||||
match s {
|
||||
ChainSeverity::Critical => format!("{} {}", style("✖").red().bold(), style("[CRITICAL]").red().bold()),
|
||||
ChainSeverity::High => format!("{} {}", style("✖").red(), style("[HIGH]").red()),
|
||||
ChainSeverity::Medium => format!("{} {}", style("⚠").yellow(), style("[MEDIUM]").yellow()),
|
||||
ChainSeverity::Low => format!("{} {}", style("●").dim(), style("[LOW]").dim()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a single diagnostic block.
|
||||
fn render_diag(d: &Diag, width: usize) -> String {
|
||||
let mut out = String::new();
|
||||
|
|
@ -882,7 +950,7 @@ mod tests {
|
|||
stable_hash: 0,
|
||||
},
|
||||
];
|
||||
let output = render_console(&diags, "test-project", None);
|
||||
let output = render_console(&diags, "test-project", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(stripped.contains("src/a.rs"));
|
||||
assert!(stripped.contains("src/b.rs"));
|
||||
|
|
@ -917,7 +985,7 @@ mod tests {
|
|||
alternative_finding_ids: Vec::new(),
|
||||
stable_hash: 0,
|
||||
}];
|
||||
let output = render_console(&diags, "proj", None);
|
||||
let output = render_console(&diags, "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(stripped.contains("Source:"), "should contain Source label");
|
||||
assert!(stripped.contains("Sink:"), "should contain Sink label");
|
||||
|
|
@ -976,7 +1044,7 @@ mod tests {
|
|||
stable_hash: 0,
|
||||
},
|
||||
];
|
||||
let output = render_console(&diags, "proj", None);
|
||||
let output = render_console(&diags, "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
// There should be a blank line between the two findings
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ fn diag_with_verdict(status: VerifyStatus) -> Diag {
|
|||
#[test]
|
||||
fn console_confirmed_shows_payload_id() {
|
||||
let diag = diag_with_verdict(VerifyStatus::Confirmed);
|
||||
let output = render_console(&[diag], "proj", None);
|
||||
let output = render_console(&[diag], "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(
|
||||
stripped.contains("[DYN: confirmed via sqli-tautology]"),
|
||||
|
|
@ -138,7 +138,7 @@ fn console_confirmed_shows_payload_id() {
|
|||
#[test]
|
||||
fn console_not_confirmed_shows_annotation() {
|
||||
let diag = diag_with_verdict(VerifyStatus::NotConfirmed);
|
||||
let output = render_console(&[diag], "proj", None);
|
||||
let output = render_console(&[diag], "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(
|
||||
stripped.contains("[DYN: not confirmed]"),
|
||||
|
|
@ -149,7 +149,7 @@ fn console_not_confirmed_shows_annotation() {
|
|||
#[test]
|
||||
fn console_unsupported_shows_reason() {
|
||||
let diag = diag_with_verdict(VerifyStatus::Unsupported);
|
||||
let output = render_console(&[diag], "proj", None);
|
||||
let output = render_console(&[diag], "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(
|
||||
stripped.contains("[DYN: unsupported (no payloads for cap)]"),
|
||||
|
|
@ -160,7 +160,7 @@ fn console_unsupported_shows_reason() {
|
|||
#[test]
|
||||
fn console_inconclusive_shows_reason() {
|
||||
let diag = diag_with_verdict(VerifyStatus::Inconclusive);
|
||||
let output = render_console(&[diag], "proj", None);
|
||||
let output = render_console(&[diag], "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(
|
||||
stripped.contains("[DYN: inconclusive (build failed)]"),
|
||||
|
|
@ -171,7 +171,7 @@ fn console_inconclusive_shows_reason() {
|
|||
#[test]
|
||||
fn console_no_annotation_when_no_dynamic_verdict() {
|
||||
let diag = base_diag();
|
||||
let output = render_console(&[diag], "proj", None);
|
||||
let output = render_console(&[diag], "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(
|
||||
!stripped.contains("[DYN:"),
|
||||
|
|
@ -183,7 +183,7 @@ fn console_no_annotation_when_no_dynamic_verdict() {
|
|||
fn console_no_annotation_when_evidence_has_no_verdict() {
|
||||
let mut diag = base_diag();
|
||||
diag.evidence = Some(Evidence::default());
|
||||
let output = render_console(&[diag], "proj", None);
|
||||
let output = render_console(&[diag], "proj", None, &[]);
|
||||
let stripped = strip_ansi(&output);
|
||||
assert!(
|
||||
!stripped.contains("[DYN:"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue