code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::fs;
use std::path::{Component, Path, PathBuf};

use serde_json::{Map, Value};

use crate::discovery::ProcessingDiagnostic;
use crate::findings::{Finding, Severity};

pub(crate) struct SarifIngestion {
    pub findings: Vec<Finding>,
    pub diagnostics: Vec<ProcessingDiagnostic>,
}

pub(crate) fn read(reports: &[String], root: &Path) -> SarifIngestion {
    let mut findings = Vec::new();
    let mut diagnostics = Vec::new();
    for report in reports {
        match read_report(Path::new(report), root) {
            Ok(mut imported) => findings.append(&mut imported),
            Err(message) => diagnostics.push(ProcessingDiagnostic {
                code: "sarif-ingestion-failed",
                severity: "warning",
                stage: "ingestion",
                message,
                path: report.clone(),
            }),
        }
    }
    findings.sort_by(|left, right| {
        left.path
            .cmp(&right.path)
            .then(left.line.cmp(&right.line))
            .then(left.code.cmp(&right.code))
    });
    SarifIngestion {
        findings,
        diagnostics,
    }
}

fn read_report(report: &Path, root: &Path) -> Result<Vec<Finding>, String> {
    let bytes = fs::read(report).map_err(|error| error.to_string())?;
    let document: Value = serde_json::from_slice(&bytes).map_err(|error| error.to_string())?;
    let runs = document
        .get("runs")
        .and_then(Value::as_array)
        .ok_or_else(|| "expected a SARIF runs array".to_owned())?;
    let mut findings = Vec::new();
    for run in runs {
        let Some(run) = run.as_object() else { continue };
        let provider = provider(run);
        let Some(results) = run.get("results").and_then(Value::as_array) else {
            continue;
        };
        findings.extend(
            results
                .iter()
                .filter_map(Value::as_object)
                .filter_map(|result| finding(result, &provider, root)),
        );
    }
    Ok(findings)
}

fn provider(run: &Map<String, Value>) -> String {
    let name = run
        .get("tool")
        .and_then(|tool| tool.get("driver"))
        .and_then(|driver| driver.get("name"))
        .and_then(Value::as_str)
        .filter(|name| !name.trim().is_empty());
    name.map(slug)
        .unwrap_or_else(|| "external-sarif".to_owned())
}

fn finding(result: &Map<String, Value>, provider: &str, root: &Path) -> Option<Finding> {
    let rule = result.get("ruleId")?.as_str()?;
    let physical = result
        .get("locations")?
        .as_array()?
        .first()?
        .get("physicalLocation")?;
    let uri = physical.get("artifactLocation")?.get("uri")?.as_str()?;
    let relative = safe_relative_path(uri)?;
    if !root.join(&relative).starts_with(root) {
        return None;
    }
    let region = physical.get("region");
    let line = region
        .and_then(|value| value.get("startLine"))
        .and_then(Value::as_u64)
        .and_then(|value| usize::try_from(value).ok())
        .unwrap_or(1);
    let end_line = region
        .and_then(|value| value.get("endLine"))
        .and_then(Value::as_u64)
        .and_then(|value| usize::try_from(value).ok())
        .unwrap_or(line);
    let message = result
        .get("message")
        .and_then(|value| value.get("text"))
        .and_then(Value::as_str)
        .unwrap_or("external SARIF finding")
        .to_owned();
    let severity = match result.get("level").and_then(Value::as_str) {
        Some("error") => Severity::Error,
        Some("warning") => Severity::Warn,
        _ => Severity::Info,
    };
    Some(Finding {
        code: format!("external:{provider}:{rule}"),
        severity,
        path: relative.to_string_lossy().replace('\\', "/"),
        line,
        end_line,
        message,
        confidence: "external".to_owned(),
        why: format!("Imported read-only from the attributed `{provider}` SARIF report."),
        suggestion: "Review remediation guidance from the originating analyzer.".to_owned(),
        related_files: Vec::new(),
        snippet: String::new(),
        code_flow: Vec::new(),
    })
}

fn safe_relative_path(uri: &str) -> Option<PathBuf> {
    if uri.is_empty() || uri.starts_with('/') || uri.starts_with('\\') {
        return None;
    }
    let normalized = uri.replace('\\', "/");
    let first_segment = normalized.split('/').next()?;
    if first_segment.contains(':') {
        return None;
    }
    let mut relative = PathBuf::new();
    for component in Path::new(&normalized).components() {
        match component {
            Component::Normal(segment) => relative.push(segment),
            Component::CurDir => {}
            Component::ParentDir => {
                if !relative.pop() {
                    return None;
                }
            }
            Component::Prefix(_) | Component::RootDir => return None,
        }
    }
    (!relative.as_os_str().is_empty()).then_some(relative)
}

fn slug(value: &str) -> String {
    let mut output = String::new();
    let mut separator = false;
    for character in value.to_ascii_lowercase().chars() {
        if character.is_ascii_alphanumeric() {
            if separator && !output.is_empty() {
                output.push('-');
            }
            output.push(character);
            separator = false;
        } else {
            separator = true;
        }
    }
    output
}