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::{version::VersionV3, ParseError, Severity as UnifiedSeverity, Version};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CvssV3 {
pub vector_string: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<VersionV3>,
pub base_score: f64,
pub base_severity: Severity,
#[serde(skip_serializing_if = "Option::is_none")]
pub attack_vector: Option<AttackVector>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attack_complexity: Option<AttackComplexity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileges_required: Option<PrivilegesRequired>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_interaction: Option<UserInteraction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<Scope>,
#[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 temporal_score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temporal_severity: Option<Severity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exploit_code_maturity: Option<ExploitCodeMaturity>,
#[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 environmental_score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environmental_severity: Option<Severity>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_attack_vector: Option<AttackVector>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_attack_complexity: Option<AttackComplexity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_privileges_required: Option<PrivilegesRequired>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_user_interaction: Option<UserInteraction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_scope: Option<Scope>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_confidentiality_impact: Option<Impact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_integrity_impact: Option<Impact>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_availability_impact: Option<Impact>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Severity {
None,
Low,
Medium,
High,
Critical,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AttackVector {
#[strum(serialize = "N")]
Network,
#[strum(serialize = "A")]
AdjacentNetwork,
#[strum(serialize = "L")]
Local,
#[strum(serialize = "P")]
Physical,
#[strum(serialize = "X")]
NotDefined,
}
impl AttackVector {
pub fn score(&self) -> f64 {
match self {
AttackVector::Network => 0.85,
AttackVector::AdjacentNetwork => 0.62,
AttackVector::Local => 0.55,
AttackVector::Physical => 0.20,
AttackVector::NotDefined => 0.85, }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AttackComplexity {
#[strum(serialize = "L")]
Low,
#[strum(serialize = "H")]
High,
#[strum(serialize = "X")]
NotDefined,
}
impl AttackComplexity {
pub fn score(&self) -> f64 {
match self {
AttackComplexity::Low => 0.77,
AttackComplexity::High => 0.44,
AttackComplexity::NotDefined => 0.77, }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "UPPERCASE")]
pub enum PrivilegesRequired {
#[strum(serialize = "N")]
None,
#[strum(serialize = "L")]
Low,
#[strum(serialize = "H")]
High,
#[strum(serialize = "X")]
NotDefined,
}
impl PrivilegesRequired {
pub fn score(&self, scope_changed: bool) -> f64 {
match self {
PrivilegesRequired::None => 0.85,
PrivilegesRequired::Low => {
if scope_changed {
0.68
} else {
0.62
}
}
PrivilegesRequired::High => {
if scope_changed {
0.50
} else {
0.27
}
}
PrivilegesRequired::NotDefined => 0.85, }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum UserInteraction {
#[strum(serialize = "N")]
None,
#[strum(serialize = "R")]
Required,
#[strum(serialize = "X")]
NotDefined,
}
impl UserInteraction {
pub fn score(&self) -> f64 {
match self {
UserInteraction::None => 0.85,
UserInteraction::Required => 0.62,
UserInteraction::NotDefined => 0.85, }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Scope {
#[strum(serialize = "U")]
Unchanged,
#[strum(serialize = "C")]
Changed,
#[strum(serialize = "X")]
NotDefined,
}
impl Scope {
pub fn is_changed(&self) -> bool {
matches!(self, Scope::Changed)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Impact {
#[strum(serialize = "H")]
High,
#[strum(serialize = "L")]
Low,
#[strum(serialize = "N")]
None,
#[strum(serialize = "X")]
NotDefined,
}
impl Impact {
pub fn score(&self) -> f64 {
match self {
Impact::High => 0.56,
Impact::Low => 0.22,
Impact::None => 0.0,
Impact::NotDefined => 0.56, }
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ExploitCodeMaturity {
#[strum(serialize = "U")]
Unproven,
#[strum(serialize = "P")]
ProofOfConcept,
#[strum(serialize = "F")]
Functional,
#[strum(serialize = "H")]
High,
#[strum(serialize = "X")]
NotDefined,
}
impl ExploitCodeMaturity {
pub fn score(&self) -> f64 {
match self {
ExploitCodeMaturity::Unproven => 0.91,
ExploitCodeMaturity::ProofOfConcept => 0.94,
ExploitCodeMaturity::Functional => 0.97,
ExploitCodeMaturity::High => 1.0,
ExploitCodeMaturity::NotDefined => 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumString, Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RemediationLevel {
#[strum(serialize = "O")]
OfficialFix,
#[strum(serialize = "T")]
TemporaryFix,
#[strum(serialize = "W")]
Workaround,
#[strum(serialize = "U")]
Unavailable,
#[strum(serialize = "X")]
NotDefined,
}
impl RemediationLevel {
pub fn score(&self) -> f64 {
match self {
RemediationLevel::OfficialFix => 0.95,
RemediationLevel::TemporaryFix => 0.96,
RemediationLevel::Workaround => 0.97,
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 = "U")]
Unknown,
#[strum(serialize = "R")]
Reasonable,
#[strum(serialize = "C")]
Confirmed,
#[strum(serialize = "X")]
NotDefined,
}
impl ReportConfidence {
pub fn score(&self) -> f64 {
match self {
ReportConfidence::Unknown => 0.92,
ReportConfidence::Reasonable => 0.96,
ReportConfidence::Confirmed => 1.0,
ReportConfidence::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 = "X")]
NotDefined,
}
impl SecurityRequirement {
pub fn score(&self) -> f64 {
match self {
SecurityRequirement::Low => 0.5,
SecurityRequirement::Medium => 1.0,
SecurityRequirement::High => 1.5,
SecurityRequirement::NotDefined => 1.0,
}
}
}
impl CvssV3 {
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> {
Some(match self.base_severity {
Severity::None => UnifiedSeverity::None,
Severity::Low => UnifiedSeverity::Low,
Severity::Medium => UnifiedSeverity::Medium,
Severity::High => UnifiedSeverity::High,
Severity::Critical => UnifiedSeverity::Critical,
})
}
pub fn calculated_base_score(&self) -> Option<f64> {
let av = self.attack_vector.as_ref()?;
let ac = self.attack_complexity.as_ref()?;
let pr = self.privileges_required.as_ref()?;
let ui = self.user_interaction.as_ref()?;
let scope = self.scope.as_ref()?;
let c = self.confidentiality_impact.as_ref()?;
let i = self.integrity_impact.as_ref()?;
let a = self.availability_impact.as_ref()?;
let scope_changed = scope.is_changed();
let exploitability = 8.22 * av.score() * ac.score() * pr.score(scope_changed) * ui.score();
let impact_sub = 1.0 - ((1.0 - c.score()) * (1.0 - i.score()) * (1.0 - a.score()));
let iss = if scope_changed {
7.52 * (impact_sub - 0.029) - 3.25 * (impact_sub - 0.02).powf(15.0)
} else {
6.42 * impact_sub
};
let score = if iss <= 0.0 {
0.0
} else if scope_changed {
Self::roundup(f64::min(1.08 * (exploitability + iss), 10.0))
} else {
Self::roundup(f64::min(exploitability + iss, 10.0))
};
Some(score)
}
pub fn calculated_temporal_score(&self) -> Option<f64> {
let base_score = self.calculated_base_score()?;
let e = self
.exploit_code_maturity
.as_ref()
.map(|m| m.score())
.unwrap_or(1.0);
let rl = self
.remediation_level
.as_ref()
.map(|m| m.score())
.unwrap_or(1.0);
let rc = self
.report_confidence
.as_ref()
.map(|m| m.score())
.unwrap_or(1.0);
let score = Self::roundup(base_score * e * rl * rc);
Some(score)
}
pub fn calculated_environmental_score(&self) -> Option<f64> {
let av = self.attack_vector.as_ref()?;
let ac = self.attack_complexity.as_ref()?;
let pr = self.privileges_required.as_ref()?;
let ui = self.user_interaction.as_ref()?;
let scope = self.scope.as_ref()?;
let c = self.confidentiality_impact.as_ref()?;
let i = self.integrity_impact.as_ref()?;
let a = self.availability_impact.as_ref()?;
let mav = self
.modified_attack_vector
.as_ref()
.filter(|v| !matches!(v, AttackVector::NotDefined))
.unwrap_or(av);
let mac = self
.modified_attack_complexity
.as_ref()
.filter(|v| !matches!(v, AttackComplexity::NotDefined))
.unwrap_or(ac);
let mpr = self
.modified_privileges_required
.as_ref()
.filter(|v| !matches!(v, PrivilegesRequired::NotDefined))
.unwrap_or(pr);
let mui = self
.modified_user_interaction
.as_ref()
.filter(|v| !matches!(v, UserInteraction::NotDefined))
.unwrap_or(ui);
let ms = self
.modified_scope
.as_ref()
.filter(|v| !matches!(v, Scope::NotDefined))
.unwrap_or(scope);
let mc = self
.modified_confidentiality_impact
.as_ref()
.filter(|v| !matches!(v, Impact::NotDefined))
.unwrap_or(c);
let mi = self
.modified_integrity_impact
.as_ref()
.filter(|v| !matches!(v, Impact::NotDefined))
.unwrap_or(i);
let ma = self
.modified_availability_impact
.as_ref()
.filter(|v| !matches!(v, Impact::NotDefined))
.unwrap_or(a);
let cr = self
.confidentiality_requirement
.as_ref()
.map(|r| r.score())
.unwrap_or(1.0);
let ir = self
.integrity_requirement
.as_ref()
.map(|r| r.score())
.unwrap_or(1.0);
let ar = self
.availability_requirement
.as_ref()
.map(|r| r.score())
.unwrap_or(1.0);
let scope_changed = ms.is_changed();
let m_exploitability =
8.22 * mav.score() * mac.score() * mpr.score(scope_changed) * mui.score();
let m_impact_sub = f64::min(
1.0 - ((1.0 - cr * mc.score()) * (1.0 - ir * mi.score()) * (1.0 - ar * ma.score())),
0.915,
);
let m_iss = if scope_changed {
match self.version {
Some(VersionV3::V3_1) => {
7.52 * (m_impact_sub - 0.029) - 3.25 * (m_impact_sub * 0.9731 - 0.02).powf(13.0)
}
_ => {
7.52 * (m_impact_sub - 0.029) - 3.25 * (m_impact_sub - 0.02).powf(15.0)
}
}
} else {
6.42 * m_impact_sub
};
let score = if m_iss <= 0.0 {
0.0
} else {
let e = self
.exploit_code_maturity
.as_ref()
.map(|m| m.score())
.unwrap_or(1.0);
let rl = self
.remediation_level
.as_ref()
.map(|m| m.score())
.unwrap_or(1.0);
let rc = self
.report_confidence
.as_ref()
.map(|m| m.score())
.unwrap_or(1.0);
if scope_changed {
Self::roundup(
Self::roundup(f64::min(1.08 * (m_exploitability + m_iss), 10.0)) * e * rl * rc,
)
} else {
Self::roundup(Self::roundup(f64::min(m_exploitability + m_iss, 10.0)) * e * rl * rc)
}
};
Some(score)
}
fn roundup(value: f64) -> f64 {
let int_input = (value * 100000.0).round() as i64;
let normalized = int_input as f64 / 100000.0;
(normalized * 10.0).ceil() / 10.0
}
}
impl FromStr for CvssV3 {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (version, components_str) = prefix::extract_version_from_required_prefix(s)?;
prefix::validate_allowed_prefix_version(&version, &[Version::V3_0, Version::V3_1])?;
let parsed_version = match version {
Version::V3_0 => VersionV3::V3_0,
Version::V3_1 => VersionV3::V3_1,
_ => unreachable!("validated above"),
};
let mut cvss = CvssV3 {
vector_string: s.to_string(),
version: Some(parsed_version),
base_score: 0.0,
base_severity: Severity::None,
attack_vector: None,
attack_complexity: None,
privileges_required: None,
user_interaction: None,
scope: None,
confidentiality_impact: None,
integrity_impact: None,
availability_impact: None,
temporal_score: None,
temporal_severity: None,
exploit_code_maturity: None,
remediation_level: None,
report_confidence: None,
environmental_score: None,
environmental_severity: None,
confidentiality_requirement: None,
integrity_requirement: None,
availability_requirement: None,
modified_attack_vector: None,
modified_attack_complexity: None,
modified_privileges_required: None,
modified_user_interaction: None,
modified_scope: None,
modified_confidentiality_impact: None,
modified_integrity_impact: None,
modified_availability_impact: 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.attack_vector, &value, &key)?,
"AC" => parse_metric(&mut cvss.attack_complexity, &value, &key)?,
"PR" => parse_metric(&mut cvss.privileges_required, &value, &key)?,
"UI" => parse_metric(&mut cvss.user_interaction, &value, &key)?,
"S" => parse_metric(&mut cvss.scope, &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.exploit_code_maturity, &value, &key)?,
"RL" => parse_metric(&mut cvss.remediation_level, &value, &key)?,
"RC" => parse_metric(&mut cvss.report_confidence, &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)?,
"MAV" => parse_metric(&mut cvss.modified_attack_vector, &value, &key)?,
"MAC" => parse_metric(&mut cvss.modified_attack_complexity, &value, &key)?,
"MPR" => parse_metric(&mut cvss.modified_privileges_required, &value, &key)?,
"MUI" => parse_metric(&mut cvss.modified_user_interaction, &value, &key)?,
"MS" => parse_metric(&mut cvss.modified_scope, &value, &key)?,
"MC" => parse_metric(&mut cvss.modified_confidentiality_impact, &value, &key)?,
"MI" => parse_metric(&mut cvss.modified_integrity_impact, &value, &key)?,
"MA" => parse_metric(&mut cvss.modified_availability_impact, &value, &key)?,
_ => {
return Err(ParseError::UnknownMetric { metric: key });
}
}
}
Ok(cvss)
}
}
impl fmt::Display for CvssV3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let version = if self.vector_string.starts_with("CVSS:3.0") {
"3.0"
} else {
"3.1"
};
write!(f, "CVSS:{}", version)?;
if let Some(av) = &self.attack_vector {
write!(f, "/AV:{}", av)?;
}
if let Some(ac) = &self.attack_complexity {
write!(f, "/AC:{}", ac)?;
}
if let Some(pr) = &self.privileges_required {
write!(f, "/PR:{}", pr)?;
}
if let Some(ui) = &self.user_interaction {
write!(f, "/UI:{}", ui)?;
}
if let Some(s) = &self.scope {
write!(f, "/S:{}", s)?;
}
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)?;
}
if let Some(e) = &self.exploit_code_maturity {
write!(f, "/E:{}", e)?;
}
if let Some(rl) = &self.remediation_level {
write!(f, "/RL:{}", rl)?;
}
if let Some(rc) = &self.report_confidence {
write!(f, "/RC:{}", rc)?;
}
if let Some(cr) = &self.confidentiality_requirement {
write!(f, "/CR:{}", cr)?;
}
if let Some(ir) = &self.integrity_requirement {
write!(f, "/IR:{}", ir)?;
}
if let Some(ar) = &self.availability_requirement {
write!(f, "/AR:{}", ar)?;
}
if let Some(mav) = &self.modified_attack_vector {
write!(f, "/MAV:{}", mav)?;
}
if let Some(mac) = &self.modified_attack_complexity {
write!(f, "/MAC:{}", mac)?;
}
if let Some(mpr) = &self.modified_privileges_required {
write!(f, "/MPR:{}", mpr)?;
}
if let Some(mui) = &self.modified_user_interaction {
write!(f, "/MUI:{}", mui)?;
}
if let Some(ms) = &self.modified_scope {
write!(f, "/MS:{}", ms)?;
}
if let Some(mc) = &self.modified_confidentiality_impact {
write!(f, "/MC:{}", mc)?;
}
if let Some(mi) = &self.modified_integrity_impact {
write!(f, "/MI:{}", mi)?;
}
if let Some(ma) = &self.modified_availability_impact {
write!(f, "/MA:{}", ma)?;
}
Ok(())
}
}