mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
Python fp and docs updtes (#58)
* refactor: Update comments for clarity and add expectations.json files for performance metrics * feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks * feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks * refactor: Simplify code formatting for better readability in multiple files * refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration * feat: Update Java and Python patterns to include new security rules * refactor: Improve comment clarity and consistency across multiple Rust files * refactor: Simplify code formatting for improved readability in integration tests and module files * refactor: Improve comment formatting and enhance clarity in assertions across multiple files
This commit is contained in:
parent
4db0805de6
commit
a438886217
291 changed files with 9485 additions and 3851 deletions
|
|
@ -72,7 +72,7 @@ pub struct AppState {
|
|||
pub findings_cache: Arc<RwLock<Option<CachedFindings>>>,
|
||||
}
|
||||
|
||||
/// 50 MiB cap on request bodies — generous for config uploads, tight
|
||||
/// 50 MiB cap on request bodies, generous for config uploads, tight
|
||||
/// enough to prevent OOM from a rogue client.
|
||||
const MAX_BODY_BYTES: usize = 50 * 1024 * 1024;
|
||||
|
||||
|
|
@ -286,7 +286,7 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Panic inside a thread that holds a write guard on the shared config lock.
|
||||
/// With `parking_lot::RwLock`, the lock must remain usable afterwards —
|
||||
/// With `parking_lot::RwLock`, the lock must remain usable afterwards ,
|
||||
/// this is the poison-recovery contract we rely on in every route handler.
|
||||
#[tokio::test]
|
||||
async fn config_lock_survives_panic_in_write_guard() {
|
||||
|
|
|
|||
|
|
@ -782,7 +782,7 @@ pub struct FuncSummaryView {
|
|||
/// Enclosing container path (class / impl / module / outer function).
|
||||
/// Empty for free top-level functions.
|
||||
pub container: String,
|
||||
/// Structural [`crate::symbol::FuncKind`] slug — `"fn"`, `"method"`,
|
||||
/// Structural [`crate::symbol::FuncKind`] slug, `"fn"`, `"method"`,
|
||||
/// `"closure"`, etc. Lets the UI distinguish anonymous closures from
|
||||
/// named functions for filtering.
|
||||
pub func_kind: String,
|
||||
|
|
@ -934,10 +934,10 @@ pub struct PointerView {
|
|||
pub locations: Vec<PointerLocationView>,
|
||||
pub values: Vec<PointerValueView>,
|
||||
/// Field reads attributed to params/receiver via the field-points-to
|
||||
/// extractor (Phase 5).
|
||||
/// extractor.
|
||||
pub field_reads: Vec<PointerFieldEntryView>,
|
||||
/// Field writes attributed to params/receiver via the field-points-to
|
||||
/// extractor (Phase 5).
|
||||
/// extractor.
|
||||
pub field_writes: Vec<PointerFieldEntryView>,
|
||||
/// Number of distinct interned locations beyond the reserved Top sentinel.
|
||||
pub location_count: usize,
|
||||
|
|
@ -998,7 +998,7 @@ impl PointerView {
|
|||
});
|
||||
}
|
||||
|
||||
// Per-value pt sets — emit only values with non-empty sets to keep
|
||||
// Per-value pt sets, emit only values with non-empty sets to keep
|
||||
// the payload focused on interesting facts.
|
||||
let mut values: Vec<PointerValueView> = Vec::new();
|
||||
for v in 0..ssa.num_values() as u32 {
|
||||
|
|
@ -1064,12 +1064,12 @@ pub struct TypeFactDetailView {
|
|||
pub ssa_value: u32,
|
||||
pub var_name: Option<String>,
|
||||
pub line: usize,
|
||||
/// Type kind tag — matches the [`TypeKind`] discriminant
|
||||
/// Type kind tag, matches the [`TypeKind`] discriminant
|
||||
/// (`String`, `Int`, `HttpClient`, `Dto`, …).
|
||||
pub kind: String,
|
||||
/// True when the value is allowed to be null/None.
|
||||
pub nullable: bool,
|
||||
/// Container/class name — set for `HttpClient`, `DatabaseConnection`,
|
||||
/// Container/class name, set for `HttpClient`, `DatabaseConnection`,
|
||||
/// `Dto`, etc. Mirrors [`TypeKind::container_name`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub container: Option<String>,
|
||||
|
|
@ -1437,7 +1437,7 @@ pub fn function_list(analysis: &FileAnalysis) -> Vec<FunctionInfo> {
|
|||
/// Lower a single function to SSA and optimize it.
|
||||
///
|
||||
/// Returns the per-function body graph alongside the SSA. SSA is lowered
|
||||
/// against `body.graph`, whose `NodeIndex` space is body-local — the file's
|
||||
/// against `body.graph`, whose `NodeIndex` space is body-local, the file's
|
||||
/// top-level CFG (`analysis.cfg()`) has a different index space, so any
|
||||
/// downstream analysis that indexes by `inst.cfg_node` must use the returned
|
||||
/// `&Cfg`, not `analysis.cfg()`.
|
||||
|
|
@ -1638,7 +1638,7 @@ pub fn analyse_file_summaries(
|
|||
/// Run the file-level authorization extraction pipeline for the debug UI.
|
||||
///
|
||||
/// Returns the structured `AuthorizationModel` (routes, units, sensitive
|
||||
/// operations, auth checks) plus the file bytes and an `enabled` flag —
|
||||
/// operations, auth checks) plus the file bytes and an `enabled` flag ,
|
||||
/// the bytes drive line-number resolution in the view, and `enabled`
|
||||
/// surfaces "auth analysis is off for this language" without conflating
|
||||
/// it with an empty result.
|
||||
|
|
@ -1651,7 +1651,7 @@ pub fn analyse_file_auth(
|
|||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::BAD_REQUEST)?;
|
||||
// Determine whether the auth rules were actually enabled for this
|
||||
// file's language — `extract_auth_model_for_debug` returns an empty
|
||||
// file's language, `extract_auth_model_for_debug` returns an empty
|
||||
// model both when the rules are disabled and when the file just
|
||||
// happens to have no routes. The view distinguishes the two so the
|
||||
// UI can show "analysis disabled" instead of "no routes found".
|
||||
|
|
@ -2122,7 +2122,7 @@ fn main() {
|
|||
// Belt-and-suspenders: assert that calling with the wrong (top-level)
|
||||
// CFG would have panicked. We can't catch the panic across rayon
|
||||
// worker threads here, but we can confirm at least one `inst.cfg_node`
|
||||
// index lies outside `analysis.cfg()`'s range — that's what triggers
|
||||
// index lies outside `analysis.cfg()`'s range, that's what triggers
|
||||
// the OOB indexing inside `transfer_inst`.
|
||||
let toplevel_count = analysis.cfg().node_count();
|
||||
let max_inst_idx = ssa
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Health-score scoring engine — v3.5.
|
||||
//! Health-score scoring engine, v3.5.
|
||||
//!
|
||||
//! Pure-function scoring over a `HealthInputs` struct. Documented in
|
||||
//! `docs/health-score-audit.md` (calibration, rationale) and
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
//!
|
||||
//! 2. **HIGH-count guardrails.** The *qualitative* axis: HIGH counts
|
||||
//! cap the maximum grade and floor "no HIGH" to at least C. These
|
||||
//! are non-negotiable promises — even a perfect-everywhere-else
|
||||
//! are non-negotiable promises, even a perfect-everywhere-else
|
||||
//! repo with 6 confirmed HIGHs grades F.
|
||||
//!
|
||||
//! Modifiers (triage, trend, stale, regression, suppression hygiene)
|
||||
|
|
@ -27,17 +27,17 @@
|
|||
//! * Verdict-weighted credibility (`Confirmed > NotAttempted >
|
||||
//! Inconclusive > Infeasible`). This is the structural protection
|
||||
//! against false-positive-driven F grades while the scanner is
|
||||
//! still maturing — it auto-tightens as symex coverage grows.
|
||||
//! still maturing, it auto-tightens as symex coverage grows.
|
||||
//! * Cross-file vs intra-file vs AST-only weighting via
|
||||
//! `context_factor`.
|
||||
//! * Test-path downweight (0.3×) — a HIGH in a test fixture is
|
||||
//! * Test-path downweight (0.3×), a HIGH in a test fixture is
|
||||
//! genuinely less concerning than one in a request handler.
|
||||
//! * Effective HIGH count for ceilings — the HIGH-count caps key on
|
||||
//! * Effective HIGH count for ceilings, the HIGH-count caps key on
|
||||
//! credibility-adjusted HIGHs, not raw HIGHs. A repo with 5
|
||||
//! low-confidence HIGHs that got `NotAttempted` from symex doesn't
|
||||
//! pay the same ceiling cost as a repo with 5 `Confirmed` HIGHs.
|
||||
//! * Tighter modifier ranges so they can't flip a band.
|
||||
//! * No `parse_success_rate` (it's actually a cache-miss metric —
|
||||
//! * No `parse_success_rate` (it's actually a cache-miss metric ,
|
||||
//! see `project_parse_success_rate_misnomer.md`).
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
|
|
@ -48,11 +48,11 @@ use crate::server::models::{BacklogStats, FindingSummary, HealthComponent, Healt
|
|||
// ── Tunables ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Calibrated for v0.5.0 scanner FP rate. As Nyx symex coverage and
|
||||
// rule precision improve, the HIGH ceilings should tighten — see
|
||||
// rule precision improve, the HIGH ceilings should tighten, see
|
||||
// `docs/health-score-audit.md` "Calibration trajectory" for the
|
||||
// roadmap.
|
||||
|
||||
/// Below this file count, we floor the size divisor at 1.0 — tiny
|
||||
/// Below this file count, we floor the size divisor at 1.0, tiny
|
||||
/// repos can't claim infinite per-LOC dilution from one finding.
|
||||
const FILES_FLOOR: f64 = 100.0;
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ const QUALITY_DRAG_PER_FINDING: f64 = 0.05;
|
|||
const QUALITY_DRAG_CAP: f64 = 15.0;
|
||||
|
||||
/// Below this finding count, the Triage component contributes
|
||||
/// weight 0 — we don't punish fresh users for not having triaged
|
||||
/// weight 0, we don't punish fresh users for not having triaged
|
||||
/// what didn't need triaging.
|
||||
const TRIAGE_FLOOR: usize = 20;
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ const STALE_PENALTY_CAP: f64 = 10.0;
|
|||
// ── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Pure inputs to the health-score calculation. No app state, no DB
|
||||
/// handles — those upstream concerns are flattened into primitives the
|
||||
/// handles, those upstream concerns are flattened into primitives the
|
||||
/// scorer actually consumes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HealthInputs<'a> {
|
||||
|
|
@ -120,7 +120,7 @@ pub fn compute(inp: &HealthInputs<'_>) -> HealthScore {
|
|||
let quality_drag = quality_drag(weighted.quality_count);
|
||||
let base_after_drag = (base_score - quality_drag).clamp(0.0, 100.0);
|
||||
|
||||
// Step 5: HIGH-count guardrails — keyed on *effective* HIGH count
|
||||
// Step 5: HIGH-count guardrails, keyed on *effective* HIGH count
|
||||
// (credibility-weighted), not raw count. This is what protects
|
||||
// users from FP-driven F grades while the scanner is maturing.
|
||||
let ceiling = high_total_ceiling(weighted.effective_high);
|
||||
|
|
@ -161,9 +161,9 @@ struct WeightedAggregate {
|
|||
/// context_factor` across security findings. Quality lints are
|
||||
/// handled separately via `quality_drag`.
|
||||
raw_weight: f64,
|
||||
/// Number of `*.quality.*` findings — drives `quality_drag`.
|
||||
/// Number of `*.quality.*` findings, drives `quality_drag`.
|
||||
quality_count: usize,
|
||||
/// Credibility-adjusted HIGH count (rounded) — drives the HIGH
|
||||
/// Credibility-adjusted HIGH count (rounded), drives the HIGH
|
||||
/// ceiling and floor. A low-confidence + Inconclusive HIGH might
|
||||
/// contribute 0.2; five of them would round to 1.
|
||||
effective_high: usize,
|
||||
|
|
@ -171,10 +171,10 @@ struct WeightedAggregate {
|
|||
raw_high: usize,
|
||||
raw_medium: usize,
|
||||
raw_low_security: usize,
|
||||
/// Confidence rate (high+medium*0.5)/total — drives the
|
||||
/// Confidence rate (high+medium*0.5)/total, drives the
|
||||
/// confidence component. 100 if no findings.
|
||||
confidence_rate: f64,
|
||||
/// Symex coverage — % of taint findings with any non-NotAttempted
|
||||
/// Symex coverage, % of taint findings with any non-NotAttempted
|
||||
/// verdict. Surfaced in component detail; not currently in score.
|
||||
symex_coverage: f64,
|
||||
}
|
||||
|
|
@ -218,7 +218,7 @@ fn aggregate_findings(findings: &[Diag]) -> WeightedAggregate {
|
|||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Symex coverage tracking — only meaningful for findings with
|
||||
// Symex coverage tracking, only meaningful for findings with
|
||||
// taint-flow evidence (the ones symex even attempts).
|
||||
if let Some(ev) = f.evidence.as_ref()
|
||||
&& ev.symbolic.is_some()
|
||||
|
|
@ -294,7 +294,7 @@ fn context_factor(f: &Diag) -> f64 {
|
|||
return 0.3;
|
||||
}
|
||||
let Some(ev) = f.evidence.as_ref() else {
|
||||
return 0.75; // No evidence at all — pattern match
|
||||
return 0.75; // No evidence at all, pattern match
|
||||
};
|
||||
if ev.flow_steps.is_empty() {
|
||||
return 0.75;
|
||||
|
|
@ -351,7 +351,7 @@ fn quality_drag(quality_count: usize) -> f64 {
|
|||
(quality_count as f64 * QUALITY_DRAG_PER_FINDING).min(QUALITY_DRAG_CAP)
|
||||
}
|
||||
|
||||
// ── HIGH guardrails — calibrated for v0.5.0 FP rate ──────────────────────────
|
||||
// ── HIGH guardrails, calibrated for v0.5.0 FP rate ──────────────────────────
|
||||
|
||||
/// Final-score ceiling keyed on *effective* HIGH count (credibility-
|
||||
/// weighted, not raw). See module docstring for the rationale.
|
||||
|
|
@ -398,7 +398,7 @@ fn build_components(
|
|||
let sev_score = base_after_drag.round().clamp(0.0, 100.0) as u8;
|
||||
let sev_detail = severity_detail(weighted, size_divisor, inp.repo_files, inp.backlog);
|
||||
|
||||
// Confidence component — high-conf rate scaled into 0..=100.
|
||||
// Confidence component, high-conf rate scaled into 0..=100.
|
||||
let conf_score = weighted.confidence_rate.round().clamp(0.0, 100.0) as u8;
|
||||
let conf_detail = format!(
|
||||
"High-confidence rate {:.0}% across {} security finding{}",
|
||||
|
|
@ -407,7 +407,7 @@ fn build_components(
|
|||
plural_s(total - weighted.quality_count)
|
||||
);
|
||||
|
||||
// Trend component — only contributes weight when has_history.
|
||||
// Trend component, only contributes weight when has_history.
|
||||
let net = inp.fixed_since_last as i64 - inp.new_since_last as i64;
|
||||
let trend_score = (50 + net * 5).clamp(0, 100) as u8;
|
||||
let trend_weight = if inp.has_history { 0.20 } else { 0.0 };
|
||||
|
|
@ -420,7 +420,7 @@ fn build_components(
|
|||
"Not applicable: no prior scan to compare against (re-scan to populate)".into()
|
||||
};
|
||||
|
||||
// Triage — drops out when total < TRIAGE_FLOOR.
|
||||
// Triage, drops out when total < TRIAGE_FLOOR.
|
||||
let triage_active = total >= TRIAGE_FLOOR;
|
||||
let triage_score = (inp.triage_coverage * 100.0).round().clamp(0.0, 100.0) as u8;
|
||||
let triage_weight = if triage_active { 0.20 } else { 0.0 };
|
||||
|
|
@ -470,7 +470,7 @@ fn build_components(
|
|||
HealthComponent {
|
||||
label: "Severity pressure".into(),
|
||||
score: sev_score,
|
||||
weight: 1.0, // Severity is the *base*, not a modifier — full weight in the blend.
|
||||
weight: 1.0, // Severity is the *base*, not a modifier, full weight in the blend.
|
||||
detail: sev_detail,
|
||||
},
|
||||
HealthComponent {
|
||||
|
|
@ -770,7 +770,7 @@ mod tests {
|
|||
.collect();
|
||||
let s = summary_of(&findings);
|
||||
let h = compute(&first_scan(&s, &findings, 0.0, 100));
|
||||
// The score reflects credibility — should NOT crater to F.
|
||||
// The score reflects credibility, should NOT crater to F.
|
||||
assert!(
|
||||
h.score >= 60,
|
||||
"low-credibility HIGHs shouldn't crater to F, got {}",
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ pub struct JobManager {
|
|||
job_order: Mutex<Vec<String>>,
|
||||
active_job_id: Mutex<Option<String>>,
|
||||
max_jobs: usize,
|
||||
/// Dedicated rayon pool for scans — keeps the global pool (and tokio
|
||||
/// Dedicated rayon pool for scans, keeps the global pool (and tokio
|
||||
/// worker threads) free so the web UI stays responsive during a scan.
|
||||
scan_pool: rayon::ThreadPool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -632,7 +632,7 @@ pub struct HealthScore {
|
|||
pub struct HealthComponent {
|
||||
/// Human label (e.g. "Severity pressure", "Trend", "Triage").
|
||||
pub label: String,
|
||||
/// 0–100 — already inverted so higher = healthier.
|
||||
/// 0–100, already inverted so higher = healthier.
|
||||
pub score: u8,
|
||||
/// Weight applied when blending into the final score (0.0–1.0).
|
||||
pub weight: f64,
|
||||
|
|
@ -662,7 +662,7 @@ pub struct BacklogStats {
|
|||
pub median_age_days: Option<u32>,
|
||||
/// Findings older than 30 days that remain open.
|
||||
pub stale_count: usize,
|
||||
/// Histogram buckets (label, count) — fixed 5 buckets.
|
||||
/// Histogram buckets (label, count), fixed 5 buckets.
|
||||
pub age_buckets: Vec<OverviewCount>,
|
||||
}
|
||||
|
||||
|
|
@ -691,12 +691,12 @@ pub struct ConfidenceDistribution {
|
|||
pub struct ScannerQuality {
|
||||
pub files_scanned: u64,
|
||||
pub files_skipped: u64,
|
||||
/// 0.0–1.0 — files_scanned / (files_scanned + files_skipped).
|
||||
/// 0.0–1.0, files_scanned / (files_scanned + files_skipped).
|
||||
pub parse_success_rate: f64,
|
||||
pub functions_analyzed: u64,
|
||||
pub call_edges: u64,
|
||||
pub unresolved_calls: u64,
|
||||
/// 0.0–1.0 — call_edges / (call_edges + unresolved_calls).
|
||||
/// 0.0–1.0, call_edges / (call_edges + unresolved_calls).
|
||||
pub call_resolution_rate: f64,
|
||||
/// % of taint findings that received a symbolic verdict (Confirmed|Infeasible|Inconclusive).
|
||||
pub symex_verified_rate: f64,
|
||||
|
|
@ -712,7 +712,7 @@ pub struct IssueCategoryBucket {
|
|||
pub count: usize,
|
||||
}
|
||||
|
||||
/// "Hot sink" — a single callee that absorbs many findings.
|
||||
/// "Hot sink", a single callee that absorbs many findings.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct HotSink {
|
||||
/// Callee name (best-effort; from flow_steps last Sink).
|
||||
|
|
@ -723,7 +723,7 @@ pub struct HotSink {
|
|||
/// One OWASP Top-10 (2021) bucket.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct OwaspBucket {
|
||||
/// "A01:2021 — Broken Access Control" etc.
|
||||
/// "A01:2021, Broken Access Control" etc.
|
||||
pub code: String,
|
||||
pub label: String,
|
||||
pub count: usize,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ pub async fn observe(mut request: Request, next: Next) -> Response {
|
|||
response.headers_mut().insert(REQUEST_ID_HEADER, value);
|
||||
}
|
||||
|
||||
// Skip noisy SSE channel — long-lived stream pollutes logs.
|
||||
// Skip noisy SSE channel, long-lived stream pollutes logs.
|
||||
if path != "/api/events" {
|
||||
if status.is_server_error() {
|
||||
tracing::error!(
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
//! Static rule-id → OWASP Top-10 (2021) mapping for the dashboard.
|
||||
//!
|
||||
//! Rule IDs follow the convention `{lang}.{family}.{name}` (e.g. `js.xss.outer_html`).
|
||||
//! The family segment is what determines the bucket. Conservative — when in doubt,
|
||||
//! The family segment is what determines the bucket. Conservative, when in doubt,
|
||||
//! map to the closest fit; rules with no obvious bucket are left unbucketed.
|
||||
|
||||
use crate::server::models::OwaspBucket;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Extract the family token from a rule ID. Handles two ID shapes:
|
||||
/// 1. `lang.family.name` — typical (e.g. `js.xss.outer_html`)
|
||||
/// 2. `family-subname` or single-segment — engine-emitted (e.g.
|
||||
/// 1. `lang.family.name`, typical (e.g. `js.xss.outer_html`)
|
||||
/// 2. `family-subname` or single-segment, engine-emitted (e.g.
|
||||
/// `state-resource-leak`, `taint-unsanitised-flow`, `cfg-error-fallthrough`)
|
||||
fn extract_family(rule_id: &str) -> &str {
|
||||
if let Some(idx) = rule_id.find('.') {
|
||||
|
|
@ -33,23 +33,23 @@ pub fn owasp_bucket_for(rule_id: &str) -> Option<(&'static str, &'static str)> {
|
|||
}
|
||||
|
||||
Some(match family {
|
||||
// A01 — Broken Access Control
|
||||
// A01, Broken Access Control
|
||||
"auth" | "csrf" | "mass_assign" | "path" | "redirect" => ("A01", "Broken Access Control"),
|
||||
// A02 — Cryptographic Failures
|
||||
// A02, Cryptographic Failures
|
||||
"crypto" | "secrets" => ("A02", "Cryptographic Failures"),
|
||||
// A03 — Injection (covers SQLi, XSS, command, code-eval, template, NoSQL, LDAP, reflection,
|
||||
// A03, Injection (covers SQLi, XSS, command, code-eval, template, NoSQL, LDAP, reflection,
|
||||
// and engine-level taint findings without a more specific family tag).
|
||||
"sqli" | "xss" | "cmdi" | "code_exec" | "template" | "nosql" | "ldap" | "reflection"
|
||||
| "taint" => ("A03", "Injection"),
|
||||
// A05 — Security Misconfiguration (TLS verify off, cookie flags, prototype pollution)
|
||||
// A05, Security Misconfiguration (TLS verify off, cookie flags, prototype pollution)
|
||||
"config" | "transport" | "prototype" => ("A05", "Security Misconfiguration"),
|
||||
// A08 — Software and Data Integrity Failures
|
||||
// A08, Software and Data Integrity Failures
|
||||
"deser" => ("A08", "Software and Data Integrity Failures"),
|
||||
// A09 — Logging & Monitoring Failures
|
||||
// A09, Logging & Monitoring Failures
|
||||
"log" => ("A09", "Logging and Monitoring Failures"),
|
||||
// A10 — SSRF
|
||||
// A10, SSRF
|
||||
"ssrf" => ("A10", "Server-Side Request Forgery"),
|
||||
// Memory-safety + state-machine resource lifecycle bugs — closest OWASP fit is
|
||||
// Memory-safety + state-machine resource lifecycle bugs, closest OWASP fit is
|
||||
// A04 Insecure Design (defensive depth).
|
||||
"memory" | "state" => ("A04", "Insecure Design"),
|
||||
// Quality findings (e.g. rs.quality.unwrap) and CFG structural issues
|
||||
|
|
@ -162,7 +162,7 @@ mod tests {
|
|||
fn malformed_rule_returns_none() {
|
||||
// single-segment "not" → family "not" → unmapped → None
|
||||
assert_eq!(owasp_bucket_for("not-a-rule"), None);
|
||||
// "js.onlytwo" — family is "onlytwo" which is unmapped
|
||||
// "js.onlytwo", family is "onlytwo" which is unmapped
|
||||
assert_eq!(owasp_bucket_for("js.onlytwo"), None);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ async fn remove_terminator(
|
|||
// ── Sources / Sinks / Sanitizers (by kind) ───────────────────────────────────
|
||||
|
||||
fn list_by_kind(state: &AppState, target_kind: &str) -> Vec<LabelEntryView> {
|
||||
// Built-in rules live on /api/rules — keep this endpoint focused on the
|
||||
// Built-in rules live on /api/rules, keep this endpoint focused on the
|
||||
// user's own additions in nyx.local.
|
||||
let target_rule_kind = match target_kind {
|
||||
"source" => RuleKind::Source,
|
||||
|
|
|
|||
|
|
@ -306,8 +306,8 @@ async fn get_type_facts(
|
|||
}
|
||||
|
||||
/// GET /api/debug/auth?file=<path>
|
||||
/// Return the file-scoped authorization model — routes, units,
|
||||
/// sensitive operations, and auth checks — for the debug UI.
|
||||
/// Return the file-scoped authorization model, routes, units,
|
||||
/// sensitive operations, and auth checks, for the debug UI.
|
||||
async fn get_auth(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<FileQuery>,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ struct TreeEntry {
|
|||
struct SymbolEntry {
|
||||
name: String,
|
||||
/// Legacy display kind (`"function"` / `"method"`) used by existing CSS
|
||||
/// classes in the frontend. Kept for backward-compat — new consumers
|
||||
/// classes in the frontend. Kept for backward-compat, new consumers
|
||||
/// should prefer `func_kind`.
|
||||
kind: String,
|
||||
/// Structural [`crate::symbol::FuncKind`] slug (`"fn"`, `"method"`,
|
||||
|
|
@ -291,7 +291,7 @@ async fn get_symbols(
|
|||
let entries: Vec<SymbolEntry> = symbols
|
||||
.into_iter()
|
||||
.map(|(name, arity, _lang, namespace, container, func_kind)| {
|
||||
// Legacy `kind` field — still used by existing CSS classes
|
||||
// Legacy `kind` field, still used by existing CSS classes
|
||||
// (`symbol-kind-method`, `symbol-kind-function`). Map any
|
||||
// method-like FuncKind onto `"method"` and everything else
|
||||
// onto `"function"` so the rendered icon stays sensible.
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ fn load_latest_findings_internal(state: &AppState) -> LoadedFindings {
|
|||
/// Build (or fetch from cache) the per-scan derived views.
|
||||
///
|
||||
/// Returns clones of `Arc`s so callers can drop the lock immediately and work
|
||||
/// without contention. Triage state is *not* baked into the cached views — it
|
||||
/// without contention. Triage state is *not* baked into the cached views, it
|
||||
/// changes on a different cadence and is overlaid per request.
|
||||
fn cached_for_latest(state: &AppState) -> CachedFindings {
|
||||
let loaded = load_latest_findings_internal(state);
|
||||
|
|
@ -85,7 +85,7 @@ fn cached_for_latest(state: &AppState) -> CachedFindings {
|
|||
}
|
||||
}
|
||||
|
||||
// Slow path: rebuild. Guard against concurrent rebuilds of the same key —
|
||||
// Slow path: rebuild. Guard against concurrent rebuilds of the same key ,
|
||||
// a second writer that finds the cache already populated for our key
|
||||
// simply returns it.
|
||||
let mut guard = state.findings_cache.write();
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub fn routes() -> Router<AppState> {
|
|||
.route("/overview/baseline/{scan_id}", post(set_baseline_path))
|
||||
}
|
||||
|
||||
/// GET /api/overview — aggregated dashboard data.
|
||||
/// GET /api/overview, aggregated dashboard data.
|
||||
async fn overview(State(state): State<AppState>) -> Json<OverviewResponse> {
|
||||
// 1. Load latest findings (in-memory → DB fallback)
|
||||
let findings = crate::server::routes::findings::load_latest_findings(&state);
|
||||
|
|
@ -121,7 +121,7 @@ async fn overview(State(state): State<AppState>) -> Json<OverviewResponse> {
|
|||
new_since_last,
|
||||
fixed_since_last,
|
||||
reintroduced: reintroduced_count,
|
||||
// Files-scanned proxy for repo size — used for size-aware
|
||||
// Files-scanned proxy for repo size, used for size-aware
|
||||
// severity dampening in `health::compute`. See
|
||||
// `docs/health-score-audit.md` for calibration data.
|
||||
repo_files: scanner_quality
|
||||
|
|
@ -129,10 +129,10 @@ async fn overview(State(state): State<AppState>) -> Json<OverviewResponse> {
|
|||
.map(|q| q.files_scanned)
|
||||
.filter(|&f| f > 0),
|
||||
backlog: backlog.as_ref(),
|
||||
// Trend is meaningless without ≥2 completed scans —
|
||||
// Trend is meaningless without ≥2 completed scans ,
|
||||
// matches the first-scan check `compare_to_current` uses.
|
||||
has_history: history.scans.len() >= 2,
|
||||
// Suppression-hygiene modifier — populated when the
|
||||
// Suppression-hygiene modifier, populated when the
|
||||
// suppression panel was computable for this scan.
|
||||
blanket_suppression_rate: suppression_hygiene.as_ref().map(|s| s.blanket_rate),
|
||||
},
|
||||
|
|
@ -173,7 +173,7 @@ async fn overview(State(state): State<AppState>) -> Json<OverviewResponse> {
|
|||
})
|
||||
}
|
||||
|
||||
/// GET /api/overview/trends — scan-over-scan finding counts.
|
||||
/// GET /api/overview/trends, scan-over-scan finding counts.
|
||||
async fn overview_trends(State(state): State<AppState>) -> Json<Vec<TrendPoint>> {
|
||||
let mut points = Vec::new();
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ struct BaselineBody {
|
|||
scan_id: String,
|
||||
}
|
||||
|
||||
/// POST /api/overview/baseline { scan_id } — pin a scan as the baseline for drift comparison.
|
||||
/// POST /api/overview/baseline { scan_id }, pin a scan as the baseline for drift comparison.
|
||||
async fn set_baseline(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<BaselineBody>,
|
||||
|
|
@ -226,7 +226,7 @@ async fn set_baseline(
|
|||
set_baseline_inner(&state, &body.scan_id)
|
||||
}
|
||||
|
||||
/// POST /api/overview/baseline/:scan_id — convenience path-form for clients without a JSON body.
|
||||
/// POST /api/overview/baseline/:scan_id, convenience path-form for clients without a JSON body.
|
||||
async fn set_baseline_path(
|
||||
State(state): State<AppState>,
|
||||
AxPath(scan_id): AxPath<String>,
|
||||
|
|
@ -248,7 +248,7 @@ fn set_baseline_inner(state: &AppState, scan_id: &str) -> Result<StatusCode, Sta
|
|||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// DELETE /api/overview/baseline — clear the pinned baseline.
|
||||
/// DELETE /api/overview/baseline, clear the pinned baseline.
|
||||
async fn clear_baseline(State(state): State<AppState>) -> Result<StatusCode, StatusCode> {
|
||||
let pool = state
|
||||
.db_pool
|
||||
|
|
@ -381,7 +381,7 @@ impl ScanHistory {
|
|||
(new_count, fixed_count, reintroduced)
|
||||
}
|
||||
|
||||
/// Trend slope across the last N totals — 1.0 means strictly improving,
|
||||
/// Trend slope across the last N totals, 1.0 means strictly improving,
|
||||
/// -1.0 strictly regressing, 0.0 unchanged. Returns None with <3 points.
|
||||
fn trend_slope(&self) -> Option<f64> {
|
||||
if self.scans.len() < 3 {
|
||||
|
|
@ -712,7 +712,7 @@ fn compute_cross_file_ratio(findings: &[Diag]) -> f64 {
|
|||
cross as f64 / findings.len() as f64
|
||||
}
|
||||
|
||||
/// Hot sinks are *only* meaningful for taint findings — counting AST rule IDs
|
||||
/// Hot sinks are *only* meaningful for taint findings, counting AST rule IDs
|
||||
/// (e.g. `rs.quality.unwrap`) here just duplicates the Top Rules table. So we
|
||||
/// deliberately require a real Sink-step callee (or a parsable sink snippet)
|
||||
/// and skip everything else. Empty result → frontend hides the card.
|
||||
|
|
@ -751,7 +751,7 @@ fn compute_hot_sinks(findings: &[Diag], limit: usize) -> Vec<HotSink> {
|
|||
rows
|
||||
}
|
||||
|
||||
/// Pull the leading identifier from a sink snippet — a best-effort heuristic
|
||||
/// Pull the leading identifier from a sink snippet, a best-effort heuristic
|
||||
/// for the dashboard's "hot sinks" list.
|
||||
fn extract_callee_from_snippet(s: &str) -> String {
|
||||
let trimmed = s.trim();
|
||||
|
|
@ -932,7 +932,7 @@ fn compute_suppression_hygiene(state: &AppState, findings: &[Diag]) -> Suppressi
|
|||
}
|
||||
|
||||
fn compute_backlog(state: &AppState, findings: &[Diag], history: &ScanHistory) -> BacklogStats {
|
||||
// No useful aging data on the first scan — every fingerprint was first-seen
|
||||
// No useful aging data on the first scan, every fingerprint was first-seen
|
||||
// today by definition. Avoid the misleading "0d / 0d / 0" display.
|
||||
if history.scans.len() <= 1 {
|
||||
return BacklogStats {
|
||||
|
|
@ -1046,7 +1046,7 @@ fn build_posture(
|
|||
current_total: usize,
|
||||
) -> PostureSummary {
|
||||
// First-scan case: no prior data to diff against. Saying "stable / no change"
|
||||
// is misleading — we genuinely don't know yet.
|
||||
// is misleading, we genuinely don't know yet.
|
||||
if history.scans.len() <= 1 {
|
||||
return PostureSummary {
|
||||
trend: "unknown".into(),
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ fn build_rule_list(state: &AppState) -> Vec<RuleInfo> {
|
|||
rules
|
||||
}
|
||||
|
||||
/// GET /api/rules — list all rules with finding counts.
|
||||
/// GET /api/rules, list all rules with finding counts.
|
||||
async fn list_rules(State(state): State<AppState>) -> Json<Vec<RuleListItem>> {
|
||||
let rules = build_rule_list(&state);
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ async fn list_rules(State(state): State<AppState>) -> Json<Vec<RuleListItem>> {
|
|||
Json(items)
|
||||
}
|
||||
|
||||
/// GET /api/rules/:id — full detail for one rule.
|
||||
/// GET /api/rules/:id, full detail for one rule.
|
||||
async fn get_rule(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
|
|
@ -140,7 +140,7 @@ async fn get_rule(
|
|||
}))
|
||||
}
|
||||
|
||||
/// POST /api/rules/:id/toggle — enable/disable a rule.
|
||||
/// POST /api/rules/:id/toggle, enable/disable a rule.
|
||||
async fn toggle_rule(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<String>,
|
||||
|
|
@ -162,7 +162,7 @@ async fn toggle_rule(
|
|||
Ok(Json(serde_json::json!({ "status": "ok", "rule_id": id })))
|
||||
}
|
||||
|
||||
/// POST /api/rules/clone — clone a built-in rule to custom.
|
||||
/// POST /api/rules/clone, clone a built-in rule to custom.
|
||||
async fn clone_rule(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ async fn delete_scan(
|
|||
Json(serde_json::json!({ "error": msg })),
|
||||
));
|
||||
}
|
||||
// "Scan not found" in memory is fine — may be DB-only
|
||||
// "Scan not found" in memory is fine, may be DB-only
|
||||
}
|
||||
|
||||
// Delete from DB (CASCADE handles metrics + logs)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
//! This file is designed to be committed to version control so that triage
|
||||
//! decisions travel with the code and are shared across team members.
|
||||
//!
|
||||
//! The file uses **portable fingerprints** — computed with paths relative to the
|
||||
//! project root — so they match across machines regardless of where the repo is
|
||||
//! The file uses **portable fingerprints**, computed with paths relative to the
|
||||
//! project root, so they match across machines regardless of where the repo is
|
||||
//! checked out.
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue