nyx-scanner 0.3.0

A CLI security scanner for automating vulnerability checks
Documentation
use crate::commands::scan::Diag;
use crate::patterns::{self, Severity};
use once_cell::sync::Lazy;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::path::Path;

/// Lazily-built global map: pattern ID → description from all language registries.
static PATTERN_DESCRIPTIONS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
    let mut map = HashMap::new();
    for lang in &[
        "rust",
        "c",
        "cpp",
        "java",
        "go",
        "php",
        "python",
        "ruby",
        "javascript",
        "typescript",
    ] {
        for p in patterns::load(lang) {
            map.entry(p.id).or_insert(p.description);
        }
    }
    map
});

/// CFG rule descriptions for rules not in the pattern registry.
fn cfg_rule_description(id: &str) -> Option<&'static str> {
    match id {
        "cfg-unguarded-sink" => Some("Dangerous sink reachable without prior guard or sanitizer"),
        "cfg-unreachable-sink" => Some("Sink in unreachable code"),
        "cfg-auth-gap" => Some("Entry-point handler reaches sink without authentication check"),
        "cfg-error-fallthrough" => {
            Some("Error check does not terminate; dangerous call follows on error path")
        }
        "cfg-resource-leak" => Some("Resource acquired but not released on all exit paths"),
        "cfg-lock-not-released" => Some("Lock acquired but not released on all exit paths"),
        _ => None,
    }
}

/// Look up a human-readable description for any rule ID.
fn rule_description(id: &str) -> &str {
    // Strip taint-specific suffix for lookup (e.g. "taint-unsanitised-flow:foo.rs:42" → base)
    let base_id = if id.starts_with("taint-") {
        "taint-unsanitised-flow"
    } else {
        id
    };

    if let Some(desc) = PATTERN_DESCRIPTIONS.get(base_id) {
        return desc;
    }
    if let Some(desc) = cfg_rule_description(base_id) {
        return desc;
    }
    if base_id == "taint-unsanitised-flow" {
        return "Unsanitised data flows from source to sink";
    }
    id
}

fn severity_to_level(sev: Severity) -> &'static str {
    match sev {
        Severity::High => "error",
        Severity::Medium => "warning",
        Severity::Low => "note",
    }
}

/// Build a SARIF 2.1.0 JSON value from a list of diagnostics.
pub fn build_sarif(diags: &[Diag], scan_root: &Path) -> Value {
    // Deduplicate rule IDs and build rules array.
    let mut rule_ids: Vec<String> = Vec::new();
    let mut rule_index_map: HashMap<String, usize> = HashMap::new();

    for d in diags {
        let base = if d.id.starts_with("taint-") {
            "taint-unsanitised-flow".to_string()
        } else {
            d.id.clone()
        };
        if !rule_index_map.contains_key(&base) {
            let idx = rule_ids.len();
            rule_index_map.insert(base.clone(), idx);
            rule_ids.push(base);
        }
    }

    let rules: Vec<Value> = rule_ids
        .iter()
        .map(|id| {
            json!({
                "id": id,
                "shortDescription": { "text": rule_description(id) },
            })
        })
        .collect();

    let results: Vec<Value> = diags
        .iter()
        .map(|d| {
            let base = if d.id.starts_with("taint-") {
                "taint-unsanitised-flow"
            } else {
                &d.id
            };
            let rule_index = rule_index_map[base];

            // Make path relative to scan root if possible
            let uri = Path::new(&d.path)
                .strip_prefix(scan_root)
                .map(|p| p.to_string_lossy().to_string())
                .unwrap_or_else(|_| d.path.clone());

            json!({
                "ruleId": base,
                "ruleIndex": rule_index,
                "level": severity_to_level(d.severity),
                "message": { "text": rule_description(base) },
                "locations": [{
                    "physicalLocation": {
                        "artifactLocation": { "uri": uri },
                        "region": {
                            "startLine": d.line,
                            "startColumn": d.col
                        }
                    }
                }]
            })
        })
        .collect();

    json!({
        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
        "version": "2.1.0",
        "runs": [{
            "tool": {
                "driver": {
                    "name": "nyx",
                    "version": env!("CARGO_PKG_VERSION"),
                    "informationUri": env!("CARGO_PKG_HOMEPAGE"),
                    "rules": rules
                }
            },
            "results": results
        }]
    })
}