secfinding 0.3.0

Universal security finding types for vulnerability scanners.
Documentation
//! Validation logic for finding fields.

use super::error::FindingBuildError;
use super::types::FindingConfig;

fn validate_non_empty_field(
    value: &str,
    error: FindingBuildError,
) -> Result<(), FindingBuildError> {
    if value.is_empty() {
        return Err(error);
    }
    Ok(())
}

fn validate_max_len(value: &str, field: &'static str, max: usize) -> Result<(), FindingBuildError> {
    if value.len() > max {
        return Err(FindingBuildError::FieldTooLong { field, max });
    }
    Ok(())
}

fn validate_string_content(value: &str, field: &'static str) -> Result<(), FindingBuildError> {
    if value.contains('\0') {
        return Err(FindingBuildError::InvalidField {
            field,
            reason: "cannot contain null bytes",
        });
    }
    if value.contains('\u{FFFD}') {
        return Err(FindingBuildError::InvalidField {
            field,
            reason: "cannot contain Unicode replacement character (U+FFFD)",
        });
    }
    Ok(())
}

/// Validate scanner field.
pub(crate) fn validate_scanner(
    scanner: &str,
    config: &FindingConfig,
) -> Result<(), FindingBuildError> {
    validate_non_empty_field(scanner, FindingBuildError::EmptyScanner)?;
    validate_max_len(scanner, "scanner", config.max_scanner_len)?;
    validate_string_content(scanner, "scanner")
}

/// Validate target field.
pub(crate) fn validate_target(
    target: &str,
    config: &FindingConfig,
) -> Result<(), FindingBuildError> {
    validate_non_empty_field(target, FindingBuildError::EmptyTarget)?;
    validate_max_len(target, "target", config.max_target_len)?;
    validate_string_content(target, "target")
}

/// Validate title field.
pub(crate) fn validate_title(title: &str, config: &FindingConfig) -> Result<(), FindingBuildError> {
    validate_non_empty_field(title, FindingBuildError::EmptyTitle)?;
    validate_max_len(title, "title", config.max_title_len)?;
    validate_string_content(title, "title")
}

/// Validate detail field.
pub(crate) fn validate_detail(
    detail: &str,
    config: &FindingConfig,
) -> Result<(), FindingBuildError> {
    validate_max_len(detail, "detail", config.max_detail_len)?;
    validate_string_content(detail, "detail")
}

/// Validate CVE identifier format.
pub(crate) fn validate_cve(cve: &str) -> Result<(), FindingBuildError> {
    if !cve.starts_with("CVE-") || cve.len() > 30 || cve.len() < 8 {
        return Err(FindingBuildError::InvalidCveFormat(cve.to_string()));
    }
    validate_string_content(cve, "cve_ids")
}

/// Validate CWE identifier format.
pub(crate) fn validate_cwe(cwe: &str) -> Result<(), FindingBuildError> {
    if !cwe.starts_with("CWE-") || cwe.len() > 30 || cwe.len() < 5 {
        return Err(FindingBuildError::InvalidCweFormat(cwe.to_string()));
    }
    validate_string_content(cwe, "cwe_ids")
}

/// Validate confidence score is finite and within `[0.0, 1.0]`.
///
/// Out-of-range values return an error; silently clamping was an
/// audit finding (`verify_confidence_rejects_out_of_range`) — a
/// caller asking for `confidence(1.5)` has a bug, not a UI hint.
pub(crate) fn validate_confidence(
    confidence: Option<f64>,
) -> Result<Option<f64>, FindingBuildError> {
    match confidence {
        Some(conf) if !conf.is_finite() => Err(FindingBuildError::InvalidConfidence),
        Some(conf) if !(0.0..=1.0).contains(&conf) => Err(FindingBuildError::InvalidConfidence),
        Some(conf) => Ok(Some(conf)),
        None => Ok(None),
    }
}

/// Validate CVSS score is finite and within `[0.0, 10.0]`.
///
/// Out-of-range values return an error; silently clamping was an
/// audit finding (`verify_cvss_rejects_out_of_range`).
pub(crate) fn validate_cvss_score(
    cvss_score: Option<f64>,
) -> Result<Option<f64>, FindingBuildError> {
    match cvss_score {
        Some(score) if !score.is_finite() => Err(FindingBuildError::InvalidCvssScore),
        Some(score) if !(0.0..=10.0).contains(&score) => Err(FindingBuildError::InvalidCvssScore),
        Some(score) => Ok(Some(score)),
        None => Ok(None),
    }
}