cvss-0.3.0.0: src/Security/CVSS.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Security.CVSS
-- Description : Main entry point for CVSS v2.0, v3.0, v3.1, and v4.0 scoring
--
-- Provides a unified interface for parsing, validating, and scoring
-- Common Vulnerability Scoring System (CVSS) vector strings across
-- all supported versions:
--
-- * __CVSS v2.0__ — https:\/\/www.first.org\/cvss\/v2\/guide
-- * __CVSS v3.0__ — https:\/\/www.first.org\/cvss\/v3-0\/
-- * __CVSS v3.1__ — https:\/\/www.first.org\/cvss\/v3-1\/
-- * __CVSS v4.0__ — https:\/\/www.first.org\/cvss\/v4.0\/specification-document
--
-- Vector string formats:
--
-- * v4.0: @CVSS:4.0\/AV:N\/AC:L\/...@
-- * v3.1: @CVSS:3.1\/AV:N\/AC:L\/...@
-- * v3.0: @CVSS:3.0\/AV:N\/AC:L\/...@
-- * v2.0: @AV:N\/AC:L\/Au:N\/...@ (no prefix)
module Security.CVSS
( -- * Types
CVSS (..),
CVSSVersion (..),
CVSSNomenclature (..),
Rating (..),
CVSSError (..),
Metric (..),
MetricShortName,
MetricValueChar,
showCVSSError,
toRating,
toRating20,
-- * Parser
parseCVSS,
-- * Helpers
cvssVectorString,
cvssVectorStringOrdered,
cvssScore,
cvss20TemporalScore,
cvss20EnvironmentalScore,
cvss30TemporalScore,
cvss30EnvironmentalScore,
cvss31TemporalScore,
cvss31EnvironmentalScore,
cvss40score,
cvss40BaseScore,
cvss40EnvironmentalScore,
cvssInfo,
cvssSupplementalInfo,
-- * Nomenclature
determineNomenclature,
cvssScoreWithNomenclature,
showCVSSWithNomenclature,
)
where
import Data.List (find)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text qualified as Text
import Security.CVSS.Internal (CVSSDB (..), allMetrics, doCVSSInfo, miShortName)
import Security.CVSS.Types (CVSS (..), CVSSError (..), CVSSNomenclature (..), CVSSVersion (..), Metric (..), MetricShortName (..), MetricValueChar (..), Rating (..), showCVSSError, toRating, toRating20)
import Security.CVSS.V20 (cvss20DB, cvss20EnvironmentalScore, cvss20TemporalScore, cvss20score, validateCvss20)
import Security.CVSS.V30 (cvss30DB, cvss30EnvironmentalScore, cvss30TemporalScore, cvss30score, validateCvss30)
import Security.CVSS.V31 (cvss31DB, cvss31EnvironmentalScore, cvss31TemporalScore, cvss31score, validateCvss31)
import Security.CVSS.V40 (cvss40BaseScore, cvss40DB, cvss40EnvironmentalScore, cvss40SupplementalInfo, cvss40score, hasEnvironmentalMetrics40, hasThreatMetrics40, validateCvss40)
-- | Parse a CVSS vector string into a 'CVSS' value.
--
-- The version is detected from the prefix:
--
-- * @CVSS:4.0\/...@ → CVSS40
-- * @CVSS:3.1\/...@ → CVSS31
-- * @CVSS:3.0\/...@ → CVSS30
-- * Any other string without a @CVSS:@ prefix → CVSS20
-- * @CVSS:@ with an unknown version → 'Left' 'UnknownVersion'
parseCVSS :: Text -> Either CVSSError CVSS
parseCVSS txt
| "CVSS:4.0/" `Text.isPrefixOf` txt = CVSS CVSS40 <$> validateComponents True validateCvss40
| "CVSS:3.1/" `Text.isPrefixOf` txt = CVSS CVSS31 <$> validateComponents True validateCvss31
| "CVSS:3.0/" `Text.isPrefixOf` txt = CVSS CVSS30 <$> validateComponents True validateCvss30
| "CVSS:" `Text.isPrefixOf` txt = Left UnknownVersion
| otherwise = CVSS CVSS20 <$> validateComponents False validateCvss20
where
validateComponents withPrefix validator = do
metrics <- traverse splitComponent $ components withPrefix
_ <- validator metrics
pure metrics
components withPrefix = (if withPrefix then drop 1 else id) $ Text.split (== '/') txt
splitComponent :: Text -> Either CVSSError Metric
splitComponent componentTxt = case Text.breakOnEnd ":" componentTxt of
("", _) -> Left EmptyComponent
(_, "") -> Left (MissingValue componentTxt)
(nameWithColon, valueText) ->
let name = Text.init nameWithColon
in Right (Metric (MetricShortName name) (MetricValueChar valueText))
-- | Compute the CVSS score for any supported version.
-- Dispatches to the appropriate version-specific scorer.
cvssScore :: CVSS -> (Rating, Float)
cvssScore cvss = case cvssVersion cvss of
CVSS40 -> cvss40score (cvssMetrics cvss)
CVSS31 -> cvss31score (cvssMetrics cvss)
CVSS30 -> cvss30score (cvssMetrics cvss)
CVSS20 -> cvss20score (cvssMetrics cvss)
-- | Look up human-readable descriptions for each metric in the vector.
cvssInfo :: CVSS -> [Text]
cvssInfo cvss = doCVSSInfo (cvssDB (cvssVersion cvss)) (cvssMetrics cvss)
-- | Render the CVSS vector string preserving the original metric order.
cvssVectorString :: CVSS -> Text
cvssVectorString = cvssShow False
-- | Render the CVSS vector string in canonical (specification-defined) metric order.
cvssVectorStringOrdered :: CVSS -> Text
cvssVectorStringOrdered = cvssShow True
-- | Internal: render a CVSS vector string, optionally ordering metrics
-- according to the specification.
cvssShow :: Bool -> CVSS -> Text
cvssShow ordered cvss = case cvssVersion cvss of
CVSS40 -> Text.intercalate "/" ("CVSS:4.0" : components)
CVSS31 -> Text.intercalate "/" ("CVSS:3.1" : components)
CVSS30 -> Text.intercalate "/" ("CVSS:3.0" : components)
CVSS20 -> Text.intercalate "/" components
where
components = map toComponent (cvssOrder (cvssMetrics cvss))
toComponent :: Metric -> Text
toComponent (Metric (MetricShortName name) (MetricValueChar value)) = name <> ":" <> value
cvssOrder metrics
| ordered = mapMaybe getMetric (allMetrics (cvssDB (cvssVersion cvss)))
| otherwise = metrics
where
getMetric mi = find (\metric -> miShortName mi == mName metric) metrics
-- | Retrieve supplemental metric info for CVSS v4.0 vectors.
-- Returns 'Nothing' for all other CVSS versions.
cvssSupplementalInfo :: CVSS -> Maybe Text
cvssSupplementalInfo cvss = case cvssVersion cvss of
CVSS40 -> Security.CVSS.V40.cvss40SupplementalInfo (cvssMetrics cvss)
_ -> Nothing
-- | Get the metric database for a given CVSS version.
cvssDB :: CVSSVersion -> CVSSDB
cvssDB v = case v of
CVSS40 -> cvss40DB
CVSS31 -> cvss31DB
CVSS30 -> cvss30DB
CVSS20 -> cvss20DB
-- | Determine the CVSS nomenclature (CVSS-B, CVSS-BT, CVSS-BE, CVSS-BTE)
-- for a CVSS v4.0 vector based on which metric groups are present.
-- For v2.x and v3.x, always returns 'CVSS_B'.
determineNomenclature :: CVSS -> CVSSNomenclature
determineNomenclature cvss = case cvssVersion cvss of
CVSS40 ->
let metrics = cvssMetrics cvss
hasT = hasThreatMetrics40 metrics
hasE = hasEnvironmentalMetrics40 metrics
in case (hasT, hasE) of
(False, False) -> CVSS_B
(True, False) -> CVSS_BT
(False, True) -> CVSS_BE
(True, True) -> CVSS_BTE
_ -> CVSS_B
-- | Compute the score along with the CVSS nomenclature.
cvssScoreWithNomenclature :: CVSS -> (Rating, Float, CVSSNomenclature)
cvssScoreWithNomenclature cvss =
let (rating, score) = cvssScore cvss
nomen = determineNomenclature cvss
in (rating, score, nomen)
-- | Render a CVSS vector with its nomenclature, rating, and score
-- in the format @CVSS-B:Medium\/6.5@ (or CVSS-BT, CVSS-BE, CVSS-BTE).
showCVSSWithNomenclature :: CVSS -> Text
showCVSSWithNomenclature cvss =
let (rating, score, nomen) = cvssScoreWithNomenclature cvss
nomenStr = case nomen of
CVSS_B -> "CVSS-B"
CVSS_BT -> "CVSS-BT"
CVSS_BE -> "CVSS-BE"
CVSS_BTE -> "CVSS-BTE"
in nomenStr <> ":" <> Text.pack (show rating) <> "/" <> Text.pack (show score)