use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use crate::utils::{parse_metrics::parse_metric, prefix};
use crate::{ParseError, Severity as UnifiedSeverity, Version};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CvssV2 {
pub vector_string: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub severity: Option<Severity>,
pub base_score: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub temporal_score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environmental_score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_vector: Option<AccessVector>,
#[serde(skip_serializing_if = "Option::is_none")]
pub access_complexity: Option<AccessComplexity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<Authentication>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidentiality_impact: Option<Impact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrity_impact: Option<Impact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub availability_impact: Option<Impact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exploitability: Option<Exploitability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remediation_level: Option<RemediationLevel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub report_confidence: Option<ReportConfidence>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collateral_damage_potential: Option<CollateralDamagePotential>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_distribution: Option<TargetDistribution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidentiality_requirement: Option<SecurityRequirement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub integrity_requirement: Option<SecurityRequirement>,
#[serde(skip_serializing_if = "Option::is_none")]
pub availability_requirement: Option<SecurityRequirement>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum Severity {
Low,
Medium,
High,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AccessVector {
#[strum(serialize = "N")]
Network,
#[strum(serialize = "A")]
AdjacentNetwork,
#[strum(serialize = "L")]
Local,
}
impl AccessVector {
pub fn score(&self) -> f64 {
match self {
AccessVector::Network => 1.0,
AccessVector::AdjacentNetwork => 0.646,
AccessVector::Local => 0.395,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "UPPERCASE")]
pub enum AccessComplexity {
#[strum(serialize = "H")]
High,
#[strum(serialize = "M")]
Medium,
#[strum(serialize = "L")]
Low,
}
impl AccessComplexity {
pub fn score(&self) -> f64 {
match self {
AccessComplexity::High => 0.35,
AccessComplexity::Medium => 0.61,
AccessComplexity::Low => 0.71,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "UPPERCASE")]
pub enum Authentication {
#[strum(serialize = "M")]
Multiple,
#[strum(serialize = "S")]
Single,
#[strum(serialize = "N")]
None,
}
impl Authentication {
pub fn score(&self) -> f64 {
match self {
Authentication::Multiple => 0.45,
Authentication::Single => 0.56,
Authentication::None => 0.704,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "UPPERCASE")]
pub enum Impact {
#[strum(serialize = "N")]
None,
#[strum(serialize = "P")]
Partial,
#[strum(serialize = "C")]
Complete,
}
impl Impact {
pub fn score(&self) -> f64 {
match self {
Impact::None => 0.0,
Impact::Partial => 0.275,
Impact::Complete => 0.660,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Exploitability {
#[strum(serialize = "U")]
Unproven,
#[strum(serialize = "POC")]
ProofOfConcept,
#[strum(serialize = "F")]
Functional,
#[strum(serialize = "H")]
High,
#[strum(serialize = "ND")]
NotDefined,
}
impl Exploitability {
pub fn score(&self) -> f64 {
match self {
Exploitability::Unproven => 0.85,
Exploitability::ProofOfConcept => 0.9,
Exploitability::Functional => 0.95,
Exploitability::High => 1.0,
Exploitability::NotDefined => 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RemediationLevel {
#[strum(serialize = "OF")]
OfficialFix,
#[strum(serialize = "TF")]
TemporaryFix,
#[strum(serialize = "W")]
Workaround,
#[strum(serialize = "U")]
Unavailable,
#[strum(serialize = "ND")]
NotDefined,
}
impl RemediationLevel {
pub fn score(&self) -> f64 {
match self {
RemediationLevel::OfficialFix => 0.87,
RemediationLevel::TemporaryFix => 0.90,
RemediationLevel::Workaround => 0.95,
RemediationLevel::Unavailable => 1.0,
RemediationLevel::NotDefined => 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ReportConfidence {
#[strum(serialize = "UC")]
Unconfirmed,
#[strum(serialize = "UR")]
Uncorroborated,
#[strum(serialize = "C")]
Confirmed,
#[strum(serialize = "ND")]
NotDefined,
}
impl ReportConfidence {
pub fn score(&self) -> f64 {
match self {
ReportConfidence::Unconfirmed => 0.90,
ReportConfidence::Uncorroborated => 0.95,
ReportConfidence::Confirmed => 1.0,
ReportConfidence::NotDefined => 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CollateralDamagePotential {
#[strum(serialize = "N")]
None,
#[strum(serialize = "L")]
Low,
#[strum(serialize = "LM")]
LowMedium,
#[strum(serialize = "MH")]
MediumHigh,
#[strum(serialize = "H")]
High,
#[strum(serialize = "ND")]
NotDefined,
}
impl CollateralDamagePotential {
pub fn score(&self) -> f64 {
match self {
CollateralDamagePotential::None => 0.0,
CollateralDamagePotential::Low => 0.1,
CollateralDamagePotential::LowMedium => 0.3,
CollateralDamagePotential::MediumHigh => 0.4,
CollateralDamagePotential::High => 0.5,
CollateralDamagePotential::NotDefined => 0.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TargetDistribution {
#[strum(serialize = "N")]
None,
#[strum(serialize = "L")]
Low,
#[strum(serialize = "M")]
Medium,
#[strum(serialize = "H")]
High,
#[strum(serialize = "ND")]
NotDefined,
}
impl TargetDistribution {
pub fn score(&self) -> f64 {
match self {
TargetDistribution::None => 0.0,
TargetDistribution::Low => 0.25,
TargetDistribution::Medium => 0.75,
TargetDistribution::High => 1.0,
TargetDistribution::NotDefined => 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SecurityRequirement {
#[strum(serialize = "L")]
Low,
#[strum(serialize = "M")]
Medium,
#[strum(serialize = "H")]
High,
#[strum(serialize = "ND")]
NotDefined,
}
impl SecurityRequirement {
pub fn score(&self) -> f64 {
match self {
SecurityRequirement::Low => 0.5,
SecurityRequirement::Medium => 1.0,
SecurityRequirement::High => 1.51,
SecurityRequirement::NotDefined => 1.0,
}
}
}
fn round_to_first_decimal(value: f64) -> f64 {
(value * 10.0).round() / 10.0
}
enum ImpactKind {
WithImpact,
WithAdjustedImpact,
}
impl CvssV2 {
pub fn vector_string(&self) -> &str {
&self.vector_string
}
pub fn base_score(&self) -> f64 {
self.base_score
}
pub fn base_severity(&self) -> Option<UnifiedSeverity> {
self.severity.as_ref().map(|s| match s {
Severity::Low => UnifiedSeverity::Low,
Severity::Medium => UnifiedSeverity::Medium,
Severity::High => UnifiedSeverity::High,
})
}
pub fn calculated_base_score(&self) -> Option<f64> {
self.calculate_base_score(ImpactKind::WithImpact)
}
fn calculate_base_score(&self, impact_kind: ImpactKind) -> Option<f64> {
let ci = self.confidentiality_impact.as_ref()?;
let ii = self.integrity_impact.as_ref()?;
let ai = self.availability_impact.as_ref()?;
let av = self.access_vector.as_ref()?;
let ac = self.access_complexity.as_ref()?;
let au = self.authentication.as_ref()?;
let impact_score = match impact_kind {
ImpactKind::WithImpact => {
10.41 * (1.0 - (1.0 - ci.score()) * (1.0 - ii.score()) * (1.0 - ai.score()))
}
ImpactKind::WithAdjustedImpact => {
let cr = self
.confidentiality_requirement
.as_ref()
.map_or(1.0, |v| v.score());
let ir = self
.integrity_requirement
.as_ref()
.map_or(1.0, |v| v.score());
let ar = self
.availability_requirement
.as_ref()
.map_or(1.0, |v| v.score());
(10.41
* (1.0
- (1.0 - ci.score() * cr)
* (1.0 - ii.score() * ir)
* (1.0 - ai.score() * ar)))
.min(10.0)
}
};
let exploitability = 20.0 * av.score() * ac.score() * au.score();
let f_impact = if impact_score == 0.0 { 0.0 } else { 1.176 };
let score = ((0.6 * impact_score) + (0.4 * exploitability) - 1.5) * f_impact;
Some(round_to_first_decimal(score))
}
pub fn calculated_temporal_score(&self) -> Option<f64> {
self.calculate_temporal_score(ImpactKind::WithImpact)
}
fn calculate_temporal_score(&self, impact: ImpactKind) -> Option<f64> {
let base_score = self.calculate_base_score(impact)?;
let exploitability = self.exploitability.as_ref().map_or(1.0, |e| e.score());
let remediation_level = self.remediation_level.as_ref().map_or(1.0, |r| r.score());
let report_confidence = self.report_confidence.as_ref().map_or(1.0, |r| r.score());
let temporal_score = base_score * exploitability * remediation_level * report_confidence;
Some(round_to_first_decimal(temporal_score))
}
pub fn calculated_environmental_score(&self) -> Option<f64> {
let adjusted_temporal = self.calculate_temporal_score(ImpactKind::WithAdjustedImpact)?;
let cdp = self
.collateral_damage_potential
.as_ref()
.map_or(0.0, |v| v.score());
let td = self.target_distribution.as_ref().map_or(1.0, |v| v.score());
let environmental_score = (adjusted_temporal + (10.0 - adjusted_temporal) * cdp) * td;
Some(round_to_first_decimal(environmental_score))
}
}
impl FromStr for CvssV2 {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (version_opt, components_str) = prefix::extract_version_from_optional_prefix(s)?;
if let Some(version) = version_opt {
prefix::validate_allowed_prefix_version(&version, &[Version::V2])?;
}
let mut cvss = CvssV2 {
vector_string: s.to_string(),
severity: None,
base_score: 0.0,
temporal_score: None,
environmental_score: None,
access_vector: None,
access_complexity: None,
authentication: None,
confidentiality_impact: None,
integrity_impact: None,
availability_impact: None,
exploitability: None,
remediation_level: None,
report_confidence: None,
collateral_damage_potential: None,
target_distribution: None,
confidentiality_requirement: None,
integrity_requirement: None,
availability_requirement: None,
};
for component in components_str.split('/') {
if component.is_empty() {
continue;
}
let mut parts = component.split(':');
let key = parts
.next()
.ok_or_else(|| ParseError::InvalidComponent {
component: component.to_string(),
})?
.to_ascii_uppercase();
let value = parts
.next()
.ok_or_else(|| ParseError::InvalidComponent {
component: component.to_string(),
})?
.to_ascii_uppercase();
if parts.next().is_some() {
return Err(ParseError::InvalidComponent {
component: component.to_string(),
});
}
match key.as_str() {
"AV" => parse_metric(&mut cvss.access_vector, &value, &key)?,
"AC" => parse_metric(&mut cvss.access_complexity, &value, &key)?,
"AU" => parse_metric(&mut cvss.authentication, &value, &key)?,
"C" => parse_metric(&mut cvss.confidentiality_impact, &value, &key)?,
"I" => parse_metric(&mut cvss.integrity_impact, &value, &key)?,
"A" => parse_metric(&mut cvss.availability_impact, &value, &key)?,
"E" => parse_metric(&mut cvss.exploitability, &value, &key)?,
"RL" => parse_metric(&mut cvss.remediation_level, &value, &key)?,
"RC" => parse_metric(&mut cvss.report_confidence, &value, &key)?,
"CDP" => parse_metric(&mut cvss.collateral_damage_potential, &value, &key)?,
"TD" => parse_metric(&mut cvss.target_distribution, &value, &key)?,
"CR" => parse_metric(&mut cvss.confidentiality_requirement, &value, &key)?,
"IR" => parse_metric(&mut cvss.integrity_requirement, &value, &key)?,
"AR" => parse_metric(&mut cvss.availability_requirement, &value, &key)?,
_ => return Err(ParseError::UnknownMetric { metric: key }),
}
}
Ok(cvss)
}
}
impl fmt::Display for CvssV2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AV:")?;
if let Some(av) = &self.access_vector {
write!(f, "{}", av)?;
}
if let Some(ac) = &self.access_complexity {
write!(f, "/AC:{}", ac)?;
}
if let Some(au) = &self.authentication {
write!(f, "/Au:{}", au)?;
}
if let Some(c) = &self.confidentiality_impact {
write!(f, "/C:{}", c)?;
}
if let Some(i) = &self.integrity_impact {
write!(f, "/I:{}", i)?;
}
if let Some(a) = &self.availability_impact {
write!(f, "/A:{}", a)?;
}
Ok(())
}
}