vsec 0.0.1

Detect secrets and in Rust codebases
Documentation
// src/models/score.rs

use std::fmt;

/// Risk score with full breakdown
#[derive(Debug, Clone)]
pub struct Score {
    /// Final calculated score
    pub total: i32,

    /// Individual factor contributions
    pub factors: Vec<ScoreFactor>,

    /// Severity level based on score
    pub severity: Severity,

    /// Whether this was killed (score <= threshold due to negative factors)
    pub killed: bool,

    /// The factor that killed it (if any)
    pub killed_by: Option<String>,
}

impl Score {
    pub fn new(factors: Vec<ScoreFactor>) -> Self {
        let total: i32 = factors.iter().map(|f| f.contribution).sum();
        let killed = factors.iter().any(|f| f.contribution <= -100);
        let killed_by = if killed {
            factors
                .iter()
                .filter(|f| f.contribution <= -100)
                .map(|f| f.name.clone())
                .next()
        } else {
            None
        };

        let severity = Severity::from_score(total);

        Self {
            total,
            factors,
            severity,
            killed,
            killed_by,
        }
    }

    /// Check if this score passes the threshold
    pub fn passes_threshold(&self, threshold: i32) -> bool {
        !self.killed && self.total >= threshold
    }

    /// Create a score from just a total (for testing)
    pub fn from_total(total: i32) -> Self {
        Self {
            total,
            factors: Vec::new(),
            severity: Severity::from_score(total),
            killed: false,
            killed_by: None,
        }
    }
}

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

impl fmt::Display for Score {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({})", self.total, self.severity)
    }
}

/// Individual scoring factor
#[derive(Debug, Clone)]
pub struct ScoreFactor {
    /// Name of this factor
    pub name: String,

    /// Category of factor
    pub category: FactorCategory,

    /// Score contribution (can be negative)
    pub contribution: i32,

    /// Human-readable reason
    pub reason: String,

    /// The evidence that triggered this factor
    pub evidence: Option<String>,
}

impl ScoreFactor {
    pub fn new(
        name: impl Into<String>,
        category: FactorCategory,
        contribution: i32,
        reason: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            category,
            contribution,
            reason: reason.into(),
            evidence: None,
        }
    }

    pub fn with_evidence(mut self, evidence: impl Into<String>) -> Self {
        self.evidence = Some(evidence.into());
        self
    }

    /// Create a "kill" factor (large negative score)
    pub fn kill(name: impl Into<String>, reason: impl Into<String>) -> Self {
        Self::new(name, FactorCategory::Suppression, -100, reason)
    }
}

/// Categories of scoring factors
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FactorCategory {
    /// Base score for finding type
    Base,

    /// Name-based factors
    Name,

    /// Context-based factors (test, scope, etc.)
    Context,

    /// Consequence analysis factors
    Consequence,

    /// RHS analysis factors
    RightHandSide,

    /// Entropy/pattern factors
    ValueAnalysis,

    /// Pattern match factors
    Pattern,

    /// Factors that suppress findings
    Suppression,

    /// Custom rule factors
    Custom,
}

/// Severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Severity {
    #[default]
    None,
    Info,
    Low,
    Medium,
    High,
    Critical,
}

impl Severity {
    pub fn from_score(score: i32) -> Self {
        match score {
            s if s <= 0 => Self::None,
            s if s < 30 => Self::Info,
            s if s < 50 => Self::Low,
            s if s < 70 => Self::Medium,
            s if s < 90 => Self::High,
            _ => Self::Critical,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Info => "info",
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
            Self::Critical => "critical",
        }
    }
}

impl fmt::Display for Severity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}