linthis 0.17.2

A fast, cross-platform multi-language linter and formatter
Documentation
// Copyright 2024 zhlinh and linthis Project Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found at
//
// https://opensource.org/license/MIT
//
// The above copyright notice and this permission
// notice shall be included in all copies or
// substantial portions of the Software.

//! Vulnerability data structures and types.

use serde::{Deserialize, Serialize};
use std::fmt;

/// Severity level of a security vulnerability
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
    /// Unknown severity
    #[default]
    Unknown,
    /// Informational (no security impact)
    None,
    /// Low severity - limited impact
    Low,
    /// Medium severity - moderate impact
    Medium,
    /// High severity - significant impact
    High,
    /// Critical severity - severe impact
    Critical,
}

impl fmt::Display for Severity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Severity::Unknown => write!(f, "unknown"),
            Severity::None => write!(f, "none"),
            Severity::Low => write!(f, "low"),
            Severity::Medium => write!(f, "medium"),
            Severity::High => write!(f, "high"),
            Severity::Critical => write!(f, "critical"),
        }
    }
}

impl Severity {
    /// Parse severity from string (case-insensitive)
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(s: &str) -> Self {
        match s.to_lowercase().as_str() {
            "critical" => Severity::Critical,
            "high" => Severity::High,
            "medium" | "moderate" => Severity::Medium,
            "low" => Severity::Low,
            "none" | "info" | "informational" => Severity::None,
            _ => Severity::Unknown,
        }
    }

    /// Check if this severity meets or exceeds a threshold
    pub fn meets_threshold(&self, threshold: &Severity) -> bool {
        self >= threshold
    }

    /// Get ANSI color code for terminal output
    pub fn color_code(&self) -> &'static str {
        match self {
            Severity::Critical => "\x1b[1;31m", // Bold red
            Severity::High => "\x1b[31m",       // Red
            Severity::Medium => "\x1b[33m",     // Yellow
            Severity::Low => "\x1b[36m",        // Cyan
            Severity::None => "\x1b[37m",       // White
            Severity::Unknown => "\x1b[90m",    // Gray
        }
    }

    /// Get emoji representation
    pub fn emoji(&self) -> &'static str {
        match self {
            Severity::Critical => "🔴",
            Severity::High => "🟠",
            Severity::Medium => "🟡",
            Severity::Low => "🔵",
            Severity::None => "",
            Severity::Unknown => "",
        }
    }
}

/// Security advisory information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Advisory {
    /// Advisory ID (e.g., CVE-2024-1234, RUSTSEC-2024-0001)
    pub id: String,
    /// Alternative IDs (CVE, GHSA, etc.)
    pub aliases: Vec<String>,
    /// Advisory title/summary
    pub title: String,
    /// Detailed description
    pub description: String,
    /// Severity level
    pub severity: Severity,
    /// CVSS score (0.0 - 10.0)
    pub cvss_score: Option<f32>,
    /// CVSS vector string
    pub cvss_vector: Option<String>,
    /// URL for more information
    pub url: Option<String>,
    /// Date the advisory was published
    pub published: Option<String>,
    /// Date the advisory was last updated
    pub updated: Option<String>,
    /// CWE identifiers
    pub cwe_ids: Vec<String>,
    /// References and links
    pub references: Vec<String>,
}

impl Advisory {
    /// Get the primary CVE ID if available
    pub fn cve_id(&self) -> Option<&str> {
        if self.id.starts_with("CVE-") {
            return Some(&self.id);
        }
        self.aliases
            .iter()
            .find(|a| a.starts_with("CVE-"))
            .map(|s| s.as_str())
    }
}

/// Affected package information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AffectedPackage {
    /// Package name
    pub name: String,
    /// Currently installed version
    pub version: String,
    /// Ecosystem (npm, crates.io, pypi, etc.)
    pub ecosystem: String,
    /// Affected version ranges
    pub affected_versions: Vec<String>,
    /// Fixed versions (if available)
    pub patched_versions: Vec<String>,
    /// Recommended upgrade version
    pub recommended_version: Option<String>,
    /// Package path in the dependency tree
    pub path: Vec<String>,
    /// Whether this is a direct or transitive dependency
    pub is_direct: bool,
}

impl AffectedPackage {
    /// Get the recommended fix action
    pub fn fix_action(&self) -> String {
        if let Some(ref version) = self.recommended_version {
            format!("Upgrade {} from {} to {}", self.name, self.version, version)
        } else if !self.patched_versions.is_empty() {
            format!(
                "Upgrade {} from {} to one of: {}",
                self.name,
                self.version,
                self.patched_versions.join(", ")
            )
        } else {
            format!("No fix available for {} {}", self.name, self.version)
        }
    }
}

/// A detected vulnerability in a dependency
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
    /// The security advisory
    pub advisory: Advisory,
    /// Affected packages
    pub affected_packages: Vec<AffectedPackage>,
    /// Source tool that detected this vulnerability
    pub source: String,
    /// Language/ecosystem
    pub language: String,
    /// Whether a fix is available
    pub fix_available: bool,
    /// Suppression status (if ignored)
    pub suppressed: bool,
    /// Suppression reason (if suppressed)
    pub suppression_reason: Option<String>,
}

impl Vulnerability {
    /// Create a new vulnerability
    pub fn new(
        advisory: Advisory,
        affected_packages: Vec<AffectedPackage>,
        source: &str,
        language: &str,
    ) -> Self {
        let fix_available = affected_packages
            .iter()
            .any(|p| p.recommended_version.is_some() || !p.patched_versions.is_empty());

        Self {
            advisory,
            affected_packages,
            source: source.to_string(),
            language: language.to_string(),
            fix_available,
            suppressed: false,
            suppression_reason: None,
        }
    }

    /// Get the severity level
    pub fn severity(&self) -> Severity {
        self.advisory.severity
    }

    /// Get a short summary
    pub fn summary(&self) -> String {
        let packages: Vec<_> = self
            .affected_packages
            .iter()
            .map(|p| format!("{} {}", p.name, p.version))
            .collect();
        format!("{} in {}", self.advisory.id, packages.join(", "))
    }

    /// Check if this vulnerability meets severity threshold
    pub fn meets_severity_threshold(&self, threshold: &Severity) -> bool {
        self.advisory.severity.meets_threshold(threshold)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_severity_ordering() {
        assert!(Severity::Critical > Severity::High);
        assert!(Severity::High > Severity::Medium);
        assert!(Severity::Medium > Severity::Low);
        assert!(Severity::Low > Severity::None);
    }

    #[test]
    fn test_severity_from_str() {
        assert_eq!(Severity::from_str("critical"), Severity::Critical);
        assert_eq!(Severity::from_str("HIGH"), Severity::High);
        assert_eq!(Severity::from_str("moderate"), Severity::Medium);
        assert_eq!(Severity::from_str("unknown_value"), Severity::Unknown);
    }

    #[test]
    fn test_severity_threshold() {
        assert!(Severity::Critical.meets_threshold(&Severity::High));
        assert!(Severity::High.meets_threshold(&Severity::High));
        assert!(!Severity::Medium.meets_threshold(&Severity::High));
    }
}