nyx-scanner 0.6.1

A multi-language static analysis tool for detecting security vulnerabilities
Documentation
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering::Relaxed};
use std::time::Instant;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ScanStage {
    Queued = 0,
    Discovering = 1,
    Indexing = 2,
    LoadingSummaries = 3,
    BuildingCallGraph = 4,
    Analyzing = 5,
    PostProcessing = 6,
    Complete = 7,
}

impl ScanStage {
    fn as_str(self) -> &'static str {
        match self {
            Self::Queued => "queued",
            Self::Discovering => "discovering",
            Self::Indexing => "indexing",
            Self::LoadingSummaries => "loading_summaries",
            Self::BuildingCallGraph => "building_call_graph",
            Self::Analyzing => "analyzing",
            Self::PostProcessing => "post_processing",
            Self::Complete => "complete",
        }
    }
}

/// Lock-free progress reporting from rayon workers during a scan.
#[derive(Debug)]
pub struct ScanProgress {
    /// See [`ScanStage`].
    stage: AtomicU8,
    files_discovered: AtomicU64,
    files_parsed: AtomicU64,
    files_analyzed: AtomicU64,
    files_skipped: AtomicU64,
    batches_total: AtomicU64,
    batches_completed: AtomicU64,
    current_file: Mutex<String>,
    started_at: Instant,
    walk_ms: AtomicU64,
    pass1_ms: AtomicU64,
    call_graph_ms: AtomicU64,
    pass2_ms: AtomicU64,
    post_process_ms: AtomicU64,
    languages: Mutex<HashMap<String, u64>>,
}

impl Default for ScanProgress {
    fn default() -> Self {
        Self::new()
    }
}

impl ScanProgress {
    pub fn new() -> Self {
        Self {
            stage: AtomicU8::new(ScanStage::Queued as u8),
            files_discovered: AtomicU64::new(0),
            files_parsed: AtomicU64::new(0),
            files_analyzed: AtomicU64::new(0),
            files_skipped: AtomicU64::new(0),
            batches_total: AtomicU64::new(0),
            batches_completed: AtomicU64::new(0),
            current_file: Mutex::new(String::new()),
            started_at: Instant::now(),
            walk_ms: AtomicU64::new(0),
            pass1_ms: AtomicU64::new(0),
            call_graph_ms: AtomicU64::new(0),
            pass2_ms: AtomicU64::new(0),
            post_process_ms: AtomicU64::new(0),
            languages: Mutex::new(HashMap::new()),
        }
    }

    pub fn set_stage(&self, stage: ScanStage) {
        self.stage.store(stage as u8, Relaxed);
    }

    pub fn set_files_discovered(&self, count: u64) {
        self.files_discovered.store(count, Relaxed);
    }

    pub fn inc_parsed(&self, n: u64) {
        self.files_parsed.fetch_add(n, Relaxed);
    }

    pub fn inc_analyzed(&self, n: u64) {
        self.files_analyzed.fetch_add(n, Relaxed);
    }

    pub fn set_files_skipped(&self, count: u64) {
        self.files_skipped.store(count, Relaxed);
    }

    pub fn inc_skipped(&self, n: u64) {
        self.files_skipped.fetch_add(n, Relaxed);
    }

    pub fn set_batches_total(&self, count: u64) {
        self.batches_total.store(count, Relaxed);
    }

    pub fn inc_batches_completed(&self, n: u64) {
        self.batches_completed.fetch_add(n, Relaxed);
    }

    pub fn set_current_file(&self, path: &str) {
        if let Ok(mut f) = self.current_file.try_lock() {
            f.clear();
            f.push_str(path);
        }
    }

    pub fn elapsed_ms(&self) -> u64 {
        self.started_at.elapsed().as_millis() as u64
    }

    pub fn record_walk_ms(&self, ms: u64) {
        self.walk_ms.fetch_add(ms, Relaxed);
    }

    pub fn record_pass1_ms(&self, ms: u64) {
        self.pass1_ms.fetch_add(ms, Relaxed);
    }

    pub fn record_call_graph_ms(&self, ms: u64) {
        self.call_graph_ms.fetch_add(ms, Relaxed);
    }

    pub fn record_pass2_ms(&self, ms: u64) {
        self.pass2_ms.fetch_add(ms, Relaxed);
    }

    pub fn record_post_process_ms(&self, ms: u64) {
        self.post_process_ms.fetch_add(ms, Relaxed);
    }

    pub fn record_language(&self, lang: &str) {
        if let Ok(mut langs) = self.languages.try_lock() {
            *langs.entry(lang.to_string()).or_insert(0) += 1;
        }
    }

    pub fn snapshot(&self) -> ScanProgressSnapshot {
        let stage = match self.stage.load(Relaxed) {
            x if x == ScanStage::Queued as u8 => ScanStage::Queued.as_str(),
            x if x == ScanStage::Discovering as u8 => ScanStage::Discovering.as_str(),
            x if x == ScanStage::Indexing as u8 => ScanStage::Indexing.as_str(),
            x if x == ScanStage::LoadingSummaries as u8 => ScanStage::LoadingSummaries.as_str(),
            x if x == ScanStage::BuildingCallGraph as u8 => ScanStage::BuildingCallGraph.as_str(),
            x if x == ScanStage::Analyzing as u8 => ScanStage::Analyzing.as_str(),
            x if x == ScanStage::PostProcessing as u8 => ScanStage::PostProcessing.as_str(),
            x if x == ScanStage::Complete as u8 => ScanStage::Complete.as_str(),
            _ => "unknown",
        }
        .to_string();

        let current_file = self
            .current_file
            .try_lock()
            .map(|f| f.clone())
            .unwrap_or_default();

        let languages = self
            .languages
            .try_lock()
            .map(|l| l.clone())
            .unwrap_or_default();

        ScanProgressSnapshot {
            stage,
            files_discovered: self.files_discovered.load(Relaxed),
            files_parsed: self.files_parsed.load(Relaxed),
            files_analyzed: self.files_analyzed.load(Relaxed),
            files_skipped: self.files_skipped.load(Relaxed),
            batches_total: self.batches_total.load(Relaxed),
            batches_completed: self.batches_completed.load(Relaxed),
            current_file,
            elapsed_ms: self.elapsed_ms(),
            timing: TimingBreakdown {
                walk_ms: self.walk_ms.load(Relaxed),
                pass1_ms: self.pass1_ms.load(Relaxed),
                call_graph_ms: self.call_graph_ms.load(Relaxed),
                pass2_ms: self.pass2_ms.load(Relaxed),
                post_process_ms: self.post_process_ms.load(Relaxed),
            },
            languages,
        }
    }
}

/// Serializable snapshot of scan progress.
#[derive(Debug, Clone, Serialize)]
pub struct ScanProgressSnapshot {
    pub stage: String,
    pub files_discovered: u64,
    pub files_parsed: u64,
    pub files_analyzed: u64,
    pub files_skipped: u64,
    pub batches_total: u64,
    pub batches_completed: u64,
    pub current_file: String,
    pub elapsed_ms: u64,
    pub timing: TimingBreakdown,
    pub languages: HashMap<String, u64>,
}

/// Timing breakdown for each scan phase.
#[derive(Debug, Clone, Serialize, serde::Deserialize, Default)]
pub struct TimingBreakdown {
    pub walk_ms: u64,
    pub pass1_ms: u64,
    pub call_graph_ms: u64,
    pub pass2_ms: u64,
    pub post_process_ms: u64,
}

/// Engine-level metrics collected during a scan.
#[derive(Debug)]
pub struct ScanMetrics {
    pub cfg_nodes: AtomicU64,
    pub call_edges: AtomicU64,
    pub functions_analyzed: AtomicU64,
    pub summaries_reused: AtomicU64,
    pub unresolved_calls: AtomicU64,
}

impl Default for ScanMetrics {
    fn default() -> Self {
        Self::new()
    }
}

impl ScanMetrics {
    pub fn new() -> Self {
        Self {
            cfg_nodes: AtomicU64::new(0),
            call_edges: AtomicU64::new(0),
            functions_analyzed: AtomicU64::new(0),
            summaries_reused: AtomicU64::new(0),
            unresolved_calls: AtomicU64::new(0),
        }
    }

    pub fn snapshot(&self) -> ScanMetricsSnapshot {
        ScanMetricsSnapshot {
            cfg_nodes: self.cfg_nodes.load(Relaxed),
            call_edges: self.call_edges.load(Relaxed),
            functions_analyzed: self.functions_analyzed.load(Relaxed),
            summaries_reused: self.summaries_reused.load(Relaxed),
            unresolved_calls: self.unresolved_calls.load(Relaxed),
        }
    }
}

/// Serializable snapshot of engine metrics.
#[derive(Debug, Clone, Serialize, Default)]
pub struct ScanMetricsSnapshot {
    pub cfg_nodes: u64,
    pub call_edges: u64,
    pub functions_analyzed: u64,
    pub summaries_reused: u64,
    pub unresolved_calls: u64,
}