mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
fix(cli): apply repository triage file during scans
This commit is contained in:
parent
991c84a1eb
commit
1148e65f36
42 changed files with 571 additions and 20 deletions
|
|
@ -82,6 +82,13 @@ nyx scan [PATH] [OPTIONS]
|
|||
| `--rollup-examples <N>` | `5` | Number of example locations in rollup findings |
|
||||
| `--show-instances <RULE>` | *(none)* | Expand all instances of a specific rule (bypass rollup) |
|
||||
|
||||
`nyx scan` automatically reads `.nyx/triage.json` from the scan root when the
|
||||
file exists. Terminal triage states written by `nyx serve` (`false_positive`,
|
||||
`accepted_risk`, `suppressed`, and `fixed`) are hidden from CLI output and do
|
||||
not trigger `--fail-on` by default. Use `--show-suppressed` to include them in
|
||||
console, JSON, or SARIF output with their `triage_state` and optional
|
||||
`triage_note`.
|
||||
|
||||
**Severity expression formats**:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -282,6 +282,25 @@ Without `--fail-on` or `--gate`, Nyx always exits `0` on a successful scan regar
|
|||
|
||||
---
|
||||
|
||||
## Repository Triage
|
||||
|
||||
`nyx scan` and `nyx serve` share `.nyx/triage.json` in the scan root. The file
|
||||
uses portable fingerprints so committed triage decisions survive different
|
||||
checkout paths in local runs and CI.
|
||||
|
||||
When the file exists, CLI scans apply it automatically:
|
||||
|
||||
- `open` and `investigating` findings remain active.
|
||||
- `false_positive`, `accepted_risk`, `suppressed`, and `fixed` findings are
|
||||
excluded from output and `--fail-on` checks by default.
|
||||
- `--show-suppressed` includes terminal triage findings and emits
|
||||
`triage_state` plus `triage_note` when present.
|
||||
|
||||
`nyx serve` continues to read and write the same file when triage sync is
|
||||
enabled, so browser triage and CI gating use the same decisions.
|
||||
|
||||
---
|
||||
|
||||
## Severity Levels
|
||||
|
||||
| Level | Description | Typical rules |
|
||||
|
|
|
|||
10
src/ast.rs
10
src/ast.rs
|
|
@ -99,6 +99,8 @@ fn parse_timeout_diag(path: &Path, timeout_ms: u64) -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -711,6 +713,8 @@ fn build_taint_diag(
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: finding.finding_id.clone(),
|
||||
alternative_finding_ids: finding.alternative_finding_ids.to_vec(),
|
||||
|
|
@ -1398,6 +1402,8 @@ impl<'a> ParsedSource<'a> {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -2042,6 +2048,8 @@ impl<'a> ParsedFile<'a> {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -2123,6 +2131,8 @@ impl<'a> ParsedFile<'a> {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1046,6 +1046,8 @@ fn auth_finding_to_diag(finding: &checks::AuthFinding, tree: &Tree, file_path: &
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -406,6 +406,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -173,6 +173,20 @@ pub struct Diag {
|
|||
/// Metadata about the suppression directive, if suppressed.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub suppression: Option<crate::suppress::SuppressionMeta>,
|
||||
/// Triage state applied from `.nyx/triage.json`.
|
||||
///
|
||||
/// `open` is the default and is omitted from serialized output. Terminal
|
||||
/// states (`false_positive`, `accepted_risk`, `suppressed`, `fixed`) are
|
||||
/// hidden from CLI output and `--fail-on` by default, mirroring the web
|
||||
/// UI's triage attention queue.
|
||||
#[serde(
|
||||
default = "default_triage_state",
|
||||
skip_serializing_if = "is_default_triage_state"
|
||||
)]
|
||||
pub triage_state: String,
|
||||
/// Optional note carried with a triage decision.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub triage_note: String,
|
||||
/// Rollup data when multiple occurrences are grouped into one finding.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rollup: Option<RollupData>,
|
||||
|
|
@ -200,6 +214,25 @@ fn is_zero_u64(v: &u64) -> bool {
|
|||
*v == 0
|
||||
}
|
||||
|
||||
pub fn default_triage_state() -> String {
|
||||
"open".to_string()
|
||||
}
|
||||
|
||||
pub fn is_default_triage_state(state: &str) -> bool {
|
||||
state == "open"
|
||||
}
|
||||
|
||||
pub fn is_terminal_triage_state(state: &str) -> bool {
|
||||
matches!(
|
||||
state,
|
||||
"false_positive" | "accepted_risk" | "suppressed" | "fixed"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_inactive_for_cli(diag: &Diag) -> bool {
|
||||
diag.suppressed || is_terminal_triage_state(&diag.triage_state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Default for Diag {
|
||||
fn default() -> Self {
|
||||
|
|
@ -220,6 +253,8 @@ impl Default for Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: default_triage_state(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
@ -726,7 +761,27 @@ pub fn handle(
|
|||
// ── Apply inline suppressions ───────────────────────────────────
|
||||
apply_suppressions(&mut diags);
|
||||
if !show_suppressed {
|
||||
diags.retain(|d| !d.suppressed);
|
||||
let triage_summary =
|
||||
crate::server::triage_sync::apply_triage_file_to_diags(&mut diags, &scan_path)
|
||||
.map_err(|e| crate::errors::NyxError::Msg(format!("triage sync failed: {e}")))?;
|
||||
if !suppress_status
|
||||
&& triage_summary.decisions_applied + triage_summary.suppression_rules_applied > 0
|
||||
{
|
||||
eprintln!(
|
||||
"Applied {} triage decision{} from .nyx/triage.json.",
|
||||
triage_summary.decisions_applied + triage_summary.suppression_rules_applied,
|
||||
if triage_summary.decisions_applied + triage_summary.suppression_rules_applied == 1
|
||||
{
|
||||
""
|
||||
} else {
|
||||
"s"
|
||||
}
|
||||
);
|
||||
}
|
||||
diags.retain(|d| !is_inactive_for_cli(d));
|
||||
} else {
|
||||
crate::server::triage_sync::apply_triage_file_to_diags(&mut diags, &scan_path)
|
||||
.map_err(|e| crate::errors::NyxError::Msg(format!("triage sync failed: {e}")))?;
|
||||
}
|
||||
|
||||
// ── Prioritization: category filter, rollup, LOW budgets ─────────
|
||||
|
|
@ -923,7 +978,7 @@ pub fn handle(
|
|||
if let Some(threshold) = fail_on {
|
||||
let breached = diags
|
||||
.iter()
|
||||
.any(|d| !d.suppressed && d.severity <= threshold);
|
||||
.any(|d| !is_inactive_for_cli(d) && d.severity <= threshold);
|
||||
if breached {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
|
@ -3530,6 +3585,8 @@ fn rollup_findings(
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: Some(RollupData {
|
||||
count: total,
|
||||
occurrences: examples,
|
||||
|
|
@ -3762,6 +3819,8 @@ mod dedup_taint_flow_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -3930,6 +3989,8 @@ mod scc_tagging_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -4222,6 +4283,8 @@ fn severity_filter_applied_at_output_stage() {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -4244,6 +4307,8 @@ fn severity_filter_applied_at_output_stage() {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -4293,6 +4358,8 @@ mod prioritize_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -4724,6 +4791,8 @@ mod prioritize_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: Some(RollupData {
|
||||
count: 38,
|
||||
occurrences: vec![Location { line: 10, col: 1 }, Location { line: 20, col: 5 }],
|
||||
|
|
@ -4814,6 +4883,8 @@ mod stable_hash_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -1093,6 +1093,8 @@ pub mod index {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1602,6 +1602,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
86
src/fmt.rs
86
src/fmt.rs
|
|
@ -61,17 +61,20 @@ pub fn render_console(
|
|||
));
|
||||
}
|
||||
|
||||
let suppressed_count = diags.iter().filter(|d| d.suppressed).count();
|
||||
let active_count = diags.len() - suppressed_count;
|
||||
let inactive_count = diags
|
||||
.iter()
|
||||
.filter(|d| crate::commands::scan::is_inactive_for_cli(d))
|
||||
.count();
|
||||
let active_count = diags.len() - inactive_count;
|
||||
|
||||
if suppressed_count > 0 {
|
||||
if inactive_count > 0 {
|
||||
out.push_str(&format!(
|
||||
"{} '{}' generated {} {} ({} suppressed).\n\n",
|
||||
"{} '{}' generated {} {} ({} suppressed/triaged).\n\n",
|
||||
style("warning").yellow().bold(),
|
||||
style(project_name).white().bold(),
|
||||
style(active_count).bold(),
|
||||
if active_count == 1 { "issue" } else { "issues" },
|
||||
suppressed_count,
|
||||
inactive_count,
|
||||
));
|
||||
} else {
|
||||
out.push_str(&format!(
|
||||
|
|
@ -328,6 +331,8 @@ fn render_diag(d: &Diag, width: usize) -> String {
|
|||
let loc = format!("{}:{}", d.line, d.col);
|
||||
let sev = if d.suppressed {
|
||||
format!("{} {}", style("○").dim(), style("[SUPPRESSED]").dim(),)
|
||||
} else if crate::commands::scan::is_terminal_triage_state(&d.triage_state) {
|
||||
triage_state_tag(&d.triage_state)
|
||||
} else {
|
||||
severity_tag(d.severity)
|
||||
};
|
||||
|
|
@ -383,14 +388,25 @@ fn render_diag(d: &Diag, width: usize) -> String {
|
|||
} else {
|
||||
String::new()
|
||||
};
|
||||
let triage_suffix = if !crate::commands::scan::is_default_triage_state(&d.triage_state)
|
||||
&& !crate::commands::scan::is_terminal_triage_state(&d.triage_state)
|
||||
{
|
||||
format!(
|
||||
" {}",
|
||||
style(format!("[triage: {}]", d.triage_state.replace('_', " "))).cyan()
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
out.push_str(&format!(
|
||||
" {} {} {}{}{}{}\n",
|
||||
" {} {} {}{}{}{}{}\n",
|
||||
style(&loc).dim(),
|
||||
sev,
|
||||
style(&d.id).dim(),
|
||||
meta_suffix,
|
||||
engine_notes_suffix,
|
||||
alt_suffix,
|
||||
triage_suffix,
|
||||
));
|
||||
|
||||
// ── Rollup body ─────────────────────────────────────────────────────
|
||||
|
|
@ -427,6 +443,21 @@ fn render_diag(d: &Diag, width: usize) -> String {
|
|||
out.push_str(&format!("{indent_str}{wrapped}\n"));
|
||||
}
|
||||
|
||||
if !crate::commands::scan::is_default_triage_state(&d.triage_state) {
|
||||
let label = d.triage_state.replace('_', " ");
|
||||
let note = if d.triage_note.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" — {}", d.triage_note)
|
||||
};
|
||||
let wrapped = wrap_text(&format!("{label}{note}"), width, BODY_INDENT + 8);
|
||||
out.push_str(&format!(
|
||||
"{indent_str}{} {}\n",
|
||||
style("Triage:").dim(),
|
||||
style(wrapped).dim(),
|
||||
));
|
||||
}
|
||||
|
||||
// ── Evidence labels (Source, Sink, Path guard) ───────────────────────
|
||||
if !d.labels.is_empty() {
|
||||
out.push('\n');
|
||||
|
|
@ -663,6 +694,21 @@ fn severity_tag(sev: Severity) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn triage_state_tag(state: &str) -> String {
|
||||
let label = state.replace('_', " ").to_ascii_uppercase();
|
||||
match state {
|
||||
"false_positive" | "suppressed" | "fixed" => {
|
||||
format!("{} {}", style("○").dim(), style(format!("[{label}]")).dim())
|
||||
}
|
||||
"accepted_risk" => format!(
|
||||
"{} {}",
|
||||
style("●").yellow(),
|
||||
style(format!("[{label}]")).yellow(),
|
||||
),
|
||||
_ => format!("{} {}", style("○").dim(), style(format!("[{label}]")).dim()),
|
||||
}
|
||||
}
|
||||
|
||||
// Text utilities
|
||||
|
||||
/// Collapse spacing artefacts in method chains.
|
||||
|
|
@ -941,6 +987,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -963,6 +1011,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -999,6 +1049,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1035,6 +1087,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1057,6 +1111,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1091,6 +1147,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1122,6 +1180,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1157,6 +1217,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1251,6 +1313,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1298,6 +1362,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1331,6 +1397,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1368,6 +1436,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1401,6 +1471,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
@ -1448,6 +1520,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -226,6 +226,12 @@ pub fn build_sarif_with_chains(diags: &[Diag], chains: &[ChainFinding], scan_roo
|
|||
if let Some(conf) = d.confidence {
|
||||
props.insert("confidence".into(), json!(conf.to_string()));
|
||||
}
|
||||
if !crate::commands::scan::is_default_triage_state(&d.triage_state) {
|
||||
props.insert("triage_state".into(), json!(d.triage_state));
|
||||
if !d.triage_note.is_empty() {
|
||||
props.insert("triage_note".into(), json!(d.triage_note));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(field) = d
|
||||
.evidence
|
||||
|
|
@ -391,6 +397,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ pub fn scan_ejs_file(path: &Path, bytes: &[u8]) -> Vec<Diag> {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -423,6 +423,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -612,6 +612,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -292,6 +292,19 @@ impl JobManager {
|
|||
for d in &mut diags {
|
||||
d.stable_hash = scan::compute_stable_hash(d);
|
||||
}
|
||||
if config.server.triage_sync
|
||||
&& let Some(ref pool) = db_pool
|
||||
{
|
||||
match crate::server::triage_sync::sync_from_file(pool, &diags, &scan_root) {
|
||||
Some(applied) if applied > 0 => log_collector.info(
|
||||
format!(
|
||||
"Imported {applied} triage decisions from .nyx/triage.json"
|
||||
),
|
||||
None,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let dynamic_summary = scan::DynamicVerificationSummary::from_diags(&diags);
|
||||
if !dynamic_summary.is_empty() {
|
||||
log_collector.info(
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ pub fn collect_filter_values(findings: &[Diag]) -> FilterValues {
|
|||
languages.insert(lang);
|
||||
}
|
||||
rules.insert(d.id.clone());
|
||||
statuses.insert(status_for_diag(d).to_string());
|
||||
statuses.insert(status_for_diag(d));
|
||||
verification_statuses.insert(
|
||||
dynamic_status_for_diag(d)
|
||||
.unwrap_or("Unverified")
|
||||
|
|
@ -279,13 +279,15 @@ pub fn lang_for_finding_path(path: &str) -> Option<String> {
|
|||
}
|
||||
|
||||
/// Compute the status string for a diagnostic.
|
||||
fn status_for_diag(d: &Diag) -> &'static str {
|
||||
if d.suppressed {
|
||||
"suppressed"
|
||||
fn status_for_diag(d: &Diag) -> String {
|
||||
if !crate::commands::scan::is_default_triage_state(&d.triage_state) {
|
||||
d.triage_state.clone()
|
||||
} else if d.suppressed {
|
||||
"suppressed".to_string()
|
||||
} else if d.path_validated {
|
||||
"validated"
|
||||
"validated".to_string()
|
||||
} else {
|
||||
"open"
|
||||
"open".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -332,9 +334,9 @@ pub fn finding_from_diag(index: usize, d: &Diag) -> FindingView {
|
|||
path_validated: d.path_validated,
|
||||
suppressed: d.suppressed,
|
||||
language: lang_for_finding_path(&d.path),
|
||||
status: status_for_diag(d).to_string(),
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
status: status_for_diag(d),
|
||||
triage_state: d.triage_state.clone(),
|
||||
triage_note: d.triage_note.clone(),
|
||||
code_context: None,
|
||||
evidence: None,
|
||||
dynamic_verdict: d
|
||||
|
|
@ -937,6 +939,8 @@ mod tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
//! project root, so they match across machines regardless of where the repo is
|
||||
//! checked out.
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::commands::scan::{Diag, is_terminal_triage_state};
|
||||
use crate::database::index::Indexer;
|
||||
use crate::server::models::compute_portable_fingerprint;
|
||||
use crate::server::models::{compute_fingerprint, compute_portable_fingerprint};
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -73,6 +73,14 @@ fn default_suppressed() -> String {
|
|||
"suppressed".to_string()
|
||||
}
|
||||
|
||||
/// Summary of a triage file applied to a set of current findings.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct TriageApplySummary {
|
||||
pub decisions_applied: usize,
|
||||
pub suppression_rules_applied: usize,
|
||||
pub inactive_findings: usize,
|
||||
}
|
||||
|
||||
/// Path to the triage sync file for a given scan root.
|
||||
pub fn triage_file_path(scan_root: &Path) -> Result<PathBuf, String> {
|
||||
let root = canonical_scan_root(scan_root)?;
|
||||
|
|
@ -171,6 +179,98 @@ pub fn save_triage_file(scan_root: &Path, file: &TriageFile) -> Result<(), Strin
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_triage_state(state: &str) -> Result<(), String> {
|
||||
if crate::server::models::is_valid_triage_state(state) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("invalid triage state in .nyx/triage.json: {state}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn diag_relative_path(d: &Diag, scan_root: &Path) -> String {
|
||||
d.path
|
||||
.strip_prefix(scan_root.to_string_lossy().as_ref())
|
||||
.unwrap_or(&d.path)
|
||||
.trim_start_matches('/')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn suppression_rule_matches(
|
||||
rule: &TriageSuppressionRule,
|
||||
d: &Diag,
|
||||
scan_root: &Path,
|
||||
portable_fp: &str,
|
||||
) -> bool {
|
||||
let rel_path = diag_relative_path(d, scan_root);
|
||||
match rule.by.as_str() {
|
||||
// Prefer portable fingerprints for committed triage files, but accept
|
||||
// local fingerprints for hand-written files and older exports.
|
||||
"fingerprint" => rule.value == portable_fp || rule.value == compute_fingerprint(d),
|
||||
"rule" => rule.value == d.id,
|
||||
"file" => rule.value == d.path || rule.value == rel_path,
|
||||
"rule_in_file" => {
|
||||
rule.value == format!("{}:{}", d.id, d.path)
|
||||
|| rule.value == format!("{}:{rel_path}", d.id)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a loaded triage file directly to diagnostics.
|
||||
///
|
||||
/// This is the CLI-facing equivalent of [`import_triage`]: it uses the same
|
||||
/// portable fingerprint format as the server sync file, but annotates the
|
||||
/// in-memory findings instead of first writing through SQLite.
|
||||
pub fn apply_triage_to_diags(
|
||||
findings: &mut [Diag],
|
||||
scan_root: &Path,
|
||||
file: &TriageFile,
|
||||
) -> Result<TriageApplySummary, String> {
|
||||
let mut decisions: HashMap<&str, &TriageDecision> = HashMap::new();
|
||||
for decision in &file.decisions {
|
||||
validate_triage_state(&decision.state)?;
|
||||
decisions.insert(decision.fingerprint.as_str(), decision);
|
||||
}
|
||||
for rule in &file.suppression_rules {
|
||||
validate_triage_state(&rule.state)?;
|
||||
}
|
||||
|
||||
let mut summary = TriageApplySummary::default();
|
||||
for d in findings {
|
||||
let portable_fp = compute_portable_fingerprint(d, scan_root);
|
||||
if let Some(decision) = decisions.get(portable_fp.as_str()) {
|
||||
d.triage_state = decision.state.clone();
|
||||
d.triage_note = decision.note.clone();
|
||||
summary.decisions_applied += 1;
|
||||
} else if let Some(rule) = file
|
||||
.suppression_rules
|
||||
.iter()
|
||||
.find(|rule| suppression_rule_matches(rule, d, scan_root, &portable_fp))
|
||||
{
|
||||
d.triage_state = rule.state.clone();
|
||||
d.triage_note = rule.note.clone();
|
||||
summary.suppression_rules_applied += 1;
|
||||
}
|
||||
|
||||
if is_terminal_triage_state(&d.triage_state) {
|
||||
summary.inactive_findings += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
/// Load `.nyx/triage.json`, if present, and apply it to diagnostics.
|
||||
pub fn apply_triage_file_to_diags(
|
||||
findings: &mut [Diag],
|
||||
scan_root: &Path,
|
||||
) -> Result<TriageApplySummary, String> {
|
||||
let Some(file) = load_triage_file_checked(scan_root)? else {
|
||||
return Ok(TriageApplySummary::default());
|
||||
};
|
||||
apply_triage_to_diags(findings, scan_root, &file)
|
||||
}
|
||||
|
||||
fn read_bounded_text_file(path: &Path, max_bytes: u64) -> Result<String, String> {
|
||||
let file = std::fs::File::open(path).map_err(|e| format!("failed to open file: {e}"))?;
|
||||
let metadata = file
|
||||
|
|
@ -271,6 +371,7 @@ pub fn import_triage(
|
|||
|
||||
// Import decisions
|
||||
for decision in &file.decisions {
|
||||
validate_triage_state(&decision.state)?;
|
||||
if let Some(local_fp) = portable_to_local.get(&decision.fingerprint) {
|
||||
let _ = idx.set_triage_state(local_fp, &decision.state, &decision.note, "import");
|
||||
applied += 1;
|
||||
|
|
@ -279,6 +380,7 @@ pub fn import_triage(
|
|||
|
||||
// Import suppression rules
|
||||
for rule in &file.suppression_rules {
|
||||
validate_triage_state(&rule.state)?;
|
||||
let _ = idx.add_suppression_rule(&rule.by, &rule.value, &rule.state, &rule.note);
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +414,16 @@ pub fn sync_to_file(
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_diag(root: &Path, path: &str, rule_id: &str) -> Diag {
|
||||
Diag {
|
||||
path: root.join(path).to_string_lossy().to_string(),
|
||||
id: rule_id.to_string(),
|
||||
line: 10,
|
||||
col: 2,
|
||||
..Diag::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_triage_files_are_rejected() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
|
@ -340,6 +452,81 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_triage_to_diags_matches_portable_fingerprints() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut findings = vec![test_diag(root.path(), "src/app.js", "js.security.eval")];
|
||||
let fingerprint = compute_portable_fingerprint(&findings[0], root.path());
|
||||
let file = TriageFile {
|
||||
version: 1,
|
||||
decisions: vec![TriageDecision {
|
||||
fingerprint,
|
||||
state: "false_positive".to_string(),
|
||||
note: "framework sanitizer handles this".to_string(),
|
||||
rule_id: "js.security.eval".to_string(),
|
||||
path: "src/app.js".to_string(),
|
||||
}],
|
||||
suppression_rules: vec![],
|
||||
};
|
||||
|
||||
let summary = apply_triage_to_diags(&mut findings, root.path(), &file).unwrap();
|
||||
|
||||
assert_eq!(summary.decisions_applied, 1);
|
||||
assert_eq!(summary.inactive_findings, 1);
|
||||
assert_eq!(findings[0].triage_state, "false_positive");
|
||||
assert_eq!(findings[0].triage_note, "framework sanitizer handles this");
|
||||
assert!(crate::commands::scan::is_inactive_for_cli(&findings[0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_triage_to_diags_matches_suppression_rules_by_portable_path() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut findings = vec![
|
||||
test_diag(root.path(), "src/app.js", "js.security.eval"),
|
||||
test_diag(root.path(), "src/other.js", "js.security.eval"),
|
||||
];
|
||||
let file = TriageFile {
|
||||
version: 1,
|
||||
decisions: vec![],
|
||||
suppression_rules: vec![TriageSuppressionRule {
|
||||
by: "rule_in_file".to_string(),
|
||||
value: "js.security.eval:src/app.js".to_string(),
|
||||
state: "suppressed".to_string(),
|
||||
note: "test-only shim".to_string(),
|
||||
}],
|
||||
};
|
||||
|
||||
let summary = apply_triage_to_diags(&mut findings, root.path(), &file).unwrap();
|
||||
|
||||
assert_eq!(summary.suppression_rules_applied, 1);
|
||||
assert_eq!(summary.inactive_findings, 1);
|
||||
assert_eq!(findings[0].triage_state, "suppressed");
|
||||
assert_eq!(findings[0].triage_note, "test-only shim");
|
||||
assert_eq!(findings[1].triage_state, "open");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_triage_to_diags_rejects_invalid_states() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut findings = vec![test_diag(root.path(), "src/app.js", "js.security.eval")];
|
||||
let fingerprint = compute_portable_fingerprint(&findings[0], root.path());
|
||||
let file = TriageFile {
|
||||
version: 1,
|
||||
decisions: vec![TriageDecision {
|
||||
fingerprint,
|
||||
state: "maybe_later".to_string(),
|
||||
note: String::new(),
|
||||
rule_id: String::new(),
|
||||
path: String::new(),
|
||||
}],
|
||||
suppression_rules: vec![],
|
||||
};
|
||||
|
||||
let err = apply_triage_to_diags(&mut findings, root.path(), &file).unwrap_err();
|
||||
|
||||
assert!(err.contains("invalid triage state"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn load_triage_file_rejects_symlink_escape() {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ fn make_diag(
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ fn diag_with_caps(path: &str, line: usize, caps: Cap) -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ fn fixture_findings() -> Vec<Diag> {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@
|
|||
//! reproducible.
|
||||
|
||||
use assert_cmd::Command;
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
use predicates::prelude::*;
|
||||
use serde_json::Value;
|
||||
use serde_json::{Value, json};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Build a scan command with a fresh config dir and a writable tempdir as
|
||||
|
|
@ -197,6 +198,91 @@ fn scan_json_stdout_is_machine_clean_when_tracing_warns() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_respects_committed_triage_file_for_cli_output_and_fail_on() {
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
let target = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
target.path().join("app.js"),
|
||||
b"const q = req.query.x;\neval(q);\n",
|
||||
)
|
||||
.unwrap();
|
||||
let canonical_target = target.path().canonicalize().unwrap();
|
||||
|
||||
let scan_args = [
|
||||
"--format",
|
||||
"json",
|
||||
"--quiet",
|
||||
"--index",
|
||||
"off",
|
||||
"--no-verify",
|
||||
"--all",
|
||||
"--include-quality",
|
||||
"--parse-timeout-ms",
|
||||
"0",
|
||||
];
|
||||
let (mut first_cmd, _) = scan_cmd(home.path(), target.path());
|
||||
first_cmd.args(scan_args);
|
||||
let first = first_cmd.assert().success();
|
||||
let first_json = assert_stdout_is_json_from_byte_zero(
|
||||
&first.get_output().stdout,
|
||||
"initial nyx scan --format json",
|
||||
);
|
||||
let findings = first_json["findings"]
|
||||
.as_array()
|
||||
.expect("scan JSON must include findings");
|
||||
assert!(
|
||||
!findings.is_empty(),
|
||||
"fixture should emit at least one finding"
|
||||
);
|
||||
|
||||
let decisions: Vec<Value> = findings
|
||||
.iter()
|
||||
.map(|finding| {
|
||||
let diag: Diag = serde_json::from_value(finding.clone()).unwrap();
|
||||
json!({
|
||||
"fingerprint": nyx_scanner::server::models::compute_portable_fingerprint(
|
||||
&diag,
|
||||
&canonical_target,
|
||||
),
|
||||
"state": "false_positive",
|
||||
"note": "fixture triaged by committed file",
|
||||
"rule_id": diag.id,
|
||||
"path": diag.path.strip_prefix(canonical_target.to_string_lossy().as_ref())
|
||||
.unwrap_or(&diag.path)
|
||||
.trim_start_matches('/')
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let nyx_dir = target.path().join(".nyx");
|
||||
std::fs::create_dir(&nyx_dir).unwrap();
|
||||
std::fs::write(
|
||||
nyx_dir.join("triage.json"),
|
||||
serde_json::to_vec_pretty(&json!({
|
||||
"version": 1,
|
||||
"decisions": decisions,
|
||||
"suppression_rules": []
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (mut second_cmd, _) = scan_cmd(home.path(), target.path());
|
||||
second_cmd.args(scan_args).args(["--fail-on", "HIGH"]);
|
||||
let second = second_cmd.assert().success();
|
||||
let second_json = assert_stdout_is_json_from_byte_zero(
|
||||
&second.get_output().stdout,
|
||||
"triaged nyx scan --format json",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
second_json["findings"].as_array().unwrap().len(),
|
||||
0,
|
||||
"terminal triage decisions from .nyx/triage.json should be hidden by default"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_sarif_stdout_is_machine_clean_when_tracing_warns() {
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -970,6 +970,8 @@ fn make_diag(path: &Path, func: &str, cap: Cap, sink_line: u32) -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ fn base_diag() -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ fn deny_diag(stable_hash: u64) -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
@ -312,6 +314,8 @@ fn confirmed_run_is_byte_identical_across_runs() {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ mod parity_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ mod verify_e2e {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
@ -111,6 +113,8 @@ mod verify_e2e {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ fn high_confidence_taint_diag(path: &str, line: u32) -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -454,6 +454,8 @@ mod go_fixture_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ fn diag(severity: Severity, id: &str, conf: Option<Confidence>) -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -452,6 +452,8 @@ mod java_fixture_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -447,6 +447,8 @@ mod js_fixture_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ fn base_diag() -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ mod lang_detect {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -442,6 +442,8 @@ mod php_fixture_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ fn empty_diag() -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -930,6 +930,8 @@ mod python_fixture_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -281,6 +281,8 @@ mod rust_fixture_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -754,6 +754,8 @@ mod hardening_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
@ -947,6 +949,8 @@ mod hardening_tests {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -649,6 +649,8 @@ finally:
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
@ -787,6 +789,8 @@ finally:
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ fn base_diag() -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: "deadbeef01234567".into(),
|
||||
alternative_finding_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ fn make_diag(id: &str, path: &str, line: usize) -> Diag {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ mod spec_strategies {
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ fn make_diag(path: &str, handler: &str, line: usize, cap: Cap, rule_id: &str) ->
|
|||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
triage_state: "open".to_string(),
|
||||
triage_note: String::new(),
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue