[go: up one dir, main page]

cvss/v4/
score.rs

1//! CVSS v4 scores
2
3#[cfg(feature = "std")]
4use crate::v4::scoring::ScoringVector;
5use crate::{Error, severity::Severity, v4::Vector};
6use alloc::borrow::ToOwned;
7#[cfg(feature = "serde")]
8use alloc::string::String;
9#[cfg(feature = "serde")]
10use alloc::string::ToString;
11use core::{fmt, fmt::Display, str::FromStr};
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize, de, ser};
14
15/// CVSS v4 scores
16///
17/// It consists of a floating point value, and a nomenclature indicating
18/// the type of metrics used to calculate the score as recommended by the
19/// specification.
20///
21/// Described in CVSS v4.0 Specification: Section 1.3
22///
23/// > This nomenclature should be used wherever a numerical CVSS value is displayed or communicated.
24#[derive(Clone, Debug, PartialEq)]
25pub struct Score {
26    value: f64,
27    nomenclature: Nomenclature,
28}
29
30/// > Numerical CVSS Scores have very different meanings based on the metrics
31/// > used to calculate them. Regarding prioritization, the usefulness of a
32/// > numerical CVSS score is directly proportional to the CVSS metrics
33/// > leveraged to generate that score. Therefore, numerical CVSS scores should
34/// > be labeled using nomenclature that communicates the metrics used in its
35/// > generation.
36#[derive(Clone, Debug, PartialEq)]
37pub enum Nomenclature {
38    ///Base metrics
39    CvssB,
40    ///  Base and Environmental metrics
41    CvssBE,
42    /// Base and Threat metrics
43    CvssBT,
44    /// Base, Threat, Environmental metrics
45    CvssBTE,
46}
47
48impl Display for Nomenclature {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::CvssB => write!(f, "CVSS-B"),
52            Self::CvssBE => write!(f, "CVSS-BE"),
53            Self::CvssBT => write!(f, "CVSS-BT"),
54            Self::CvssBTE => write!(f, "CVSS-BTE"),
55        }
56    }
57}
58
59impl FromStr for Nomenclature {
60    type Err = Error;
61
62    fn from_str(s: &str) -> crate::Result<Self> {
63        match s {
64            "CVSS-B" => Ok(Self::CvssB),
65            "CVSS-BE" => Ok(Self::CvssBE),
66            "CVSS-BT" => Ok(Self::CvssBT),
67            "CVSS-BTE" => Ok(Self::CvssBTE),
68            _ => Err(Error::InvalidNomenclatureV4 {
69                nomenclature: s.to_owned(),
70            }),
71        }
72    }
73}
74
75impl From<&Vector> for Nomenclature {
76    fn from(vector: &Vector) -> Self {
77        let has_threat = vector.e.is_some();
78        let has_environmental = vector.ar.is_some()
79            || vector.cr.is_some()
80            || vector.ir.is_some()
81            || vector.mac.is_some()
82            || vector.mat.is_some()
83            || vector.mav.is_some()
84            || vector.mpr.is_some()
85            || vector.msa.is_some()
86            || vector.msc.is_some()
87            || vector.msi.is_some()
88            || vector.mui.is_some()
89            || vector.mva.is_some()
90            || vector.mvc.is_some()
91            || vector.mvi.is_some();
92
93        match (has_threat, has_environmental) {
94            (true, true) => Nomenclature::CvssBTE,
95            (true, false) => Nomenclature::CvssBT,
96            (false, true) => Nomenclature::CvssBE,
97            (false, false) => Nomenclature::CvssB,
98        }
99    }
100}
101
102#[cfg(feature = "serde")]
103#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
104impl<'de> Deserialize<'de> for Nomenclature {
105    fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
106        String::deserialize(deserializer)?
107            .parse()
108            .map_err(de::Error::custom)
109    }
110}
111
112#[cfg(feature = "serde")]
113#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
114impl Serialize for Nomenclature {
115    fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
116        self.to_string().serialize(serializer)
117    }
118}
119
120#[cfg(feature = "std")]
121impl From<&Vector> for Score {
122    fn from(vector: &Vector) -> Self {
123        let nomenclature = Nomenclature::from(vector);
124        let scoring = ScoringVector::from(vector);
125        let value = Self::round_v4(scoring.score());
126
127        Self {
128            value,
129            nomenclature,
130        }
131    }
132}
133
134impl Score {
135    /// Create a new score
136    pub fn new(value: f64, nomenclature: Nomenclature) -> Self {
137        Self {
138            value,
139            nomenclature,
140        }
141    }
142
143    /// The specification only states that the score should be rounded to one decimal.
144    ///
145    /// In order to stay compatible with Red Hat's test suite, so use the same
146    /// rounding method.
147    ///
148    /// ```python
149    /// from decimal import Decimal as D, ROUND_HALF_UP
150    /// EPSILON = 10**-6
151    /// return float(D(x + EPSILON).quantize(D("0.1"), rounding=ROUND_HALF_UP))
152    /// ```
153    #[cfg(feature = "std")]
154    pub(crate) fn round_v4(value: f64) -> f64 {
155        let value = f64::clamp(value, 0.0, 10.0);
156        const EPSILON: f64 = 10e-6;
157        ((value + EPSILON) * 10.).round() / 10.
158    }
159
160    /// Get the score as a floating point value
161    pub fn value(self) -> f64 {
162        self.value
163    }
164
165    /// Convert the numeric score into a `Severity`
166    pub fn severity(self) -> Severity {
167        if self.value < 0.1 {
168            Severity::None
169        } else if self.value < 4.0 {
170            Severity::Low
171        } else if self.value < 7.0 {
172            Severity::Medium
173        } else if self.value < 9.0 {
174            Severity::High
175        } else {
176            Severity::Critical
177        }
178    }
179}
180
181/// There is no defined or recommended format in the specification, nor in existing implementations.
182///
183/// Using "4.5 (CVSS-BT)".
184impl Display for Score {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        // Always show exactly one decimal
187        write!(f, "{:.1} ({})", self.value, self.nomenclature)
188    }
189}
190
191impl From<Score> for f64 {
192    fn from(score: Score) -> f64 {
193        score.value()
194    }
195}
196
197impl From<Score> for Severity {
198    fn from(score: Score) -> Severity {
199        score.severity()
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use alloc::string::ToString;
207
208    #[test]
209    fn new_score() {
210        let score = Score::new(5.5, Nomenclature::CvssB);
211        assert_eq!(score.value(), 5.5);
212    }
213
214    #[test]
215    #[cfg(feature = "std")]
216    fn round_v4_round() {
217        // 8.6 - 7.15 = 1.4499999999999993 (float) => 1.5
218        assert_eq!(Score::round_v4(8.6 - 7.15), 1.5);
219        assert_eq!(Score::round_v4(5.12345), 5.1);
220    }
221
222    #[test]
223    fn into_severity() {
224        let score = Score::new(5.0, Nomenclature::CvssB);
225        let severity: Severity = score.into();
226        assert_eq!(severity, Severity::Medium);
227    }
228
229    #[test]
230    fn display_score() {
231        let score = Score::new(4.5, Nomenclature::CvssB);
232        assert_eq!(score.to_string(), "4.5 (CVSS-B)");
233    }
234}