Skip to content

Commit 46e3a7f

Browse files
TristanCacquerayblackheaven
authored andcommitted
Add CVSS 2.0
1 parent 024d41c commit 46e3a7f

File tree

2 files changed

+134
-34
lines changed

2 files changed

+134
-34
lines changed

code/cvss/src/Security/CVSS.hs

Lines changed: 129 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{-# LANGUAGE ImportQualifiedPost #-}
44
{-# LANGUAGE OverloadedStrings #-}
55
{-# LANGUAGE PatternSynonyms #-}
6+
{-# LANGUAGE TypeApplications #-}
67

78
{- | This module provides a CVSS parser and utility functions
89
adapted from https://www.first.org/cvss/v3.1/specification-document
@@ -37,6 +38,8 @@ import GHC.Float (powerFloat)
3738
data CVSSVersion
3839
= -- | Version 3.1: https://www.first.org/cvss/v3-1/
3940
CVSS31
41+
| -- | Version 2.0: https://www.first.org/cvss/v2/
42+
CVSS20
4043

4144
-- | Parsed CVSS string obtained with 'parseCVSS'.
4245
data CVSS = CVSS
@@ -101,13 +104,13 @@ data Metric = Metric
101104
-- | Parse a CVSS string.
102105
parseCVSS :: Text -> Either CVSSError CVSS
103106
parseCVSS txt
104-
| "CVSS:3.1/" `Text.isPrefixOf` txt = parseCVSS31
107+
| "CVSS:3.1/" `Text.isPrefixOf` txt = CVSS CVSS31 <$> validateComponents validateCvss31
108+
| "CVSS:2.0/" `Text.isPrefixOf` txt = CVSS CVSS20 <$> validateComponents validateCvss20
105109
| otherwise = Left UnknownVersion
106110
where
107-
parseCVSS31 =
108-
CVSS CVSS31 <$> do
109-
metrics <- traverse splitComponent components
110-
validateCvss31 metrics
111+
validateComponents validator = do
112+
metrics <- traverse splitComponent components
113+
validator metrics
111114

112115
components = drop 1 $ Text.split (== '/') txt
113116
splitComponent :: Text -> Either CVSSError Metric
@@ -121,11 +124,11 @@ parseCVSS txt
121124
cvssScore :: CVSS -> (Rating, Float)
122125
cvssScore cvss = case cvssVersion cvss of
123126
CVSS31 -> cvss31score (cvssMetrics cvss)
127+
CVSS20 -> cvss20score (cvssMetrics cvss)
124128

125129
-- | Explain the CVSS metrics.
126130
cvssInfo :: CVSS -> [Text]
127-
cvssInfo cvss = case cvssVersion cvss of
128-
CVSS31 -> cvss31info (cvssMetrics cvss)
131+
cvssInfo cvss = doCVSSInfo (cvssDB (cvssVersion cvss)) (cvssMetrics cvss)
129132

130133
-- | Format the CVSS back to its original string.
131134
cvssVectorString :: CVSS -> Text
@@ -137,16 +140,24 @@ cvssVectorStringOrdered = cvssShow True
137140

138141
cvssShow :: Bool -> CVSS -> Text
139142
cvssShow ordered cvss = case cvssVersion cvss of
140-
CVSS31 -> Text.intercalate "/" ("CVSS:3.1" : map toComponent (cvss31Order (cvssMetrics cvss)))
143+
CVSS31 -> Text.intercalate "/" ("CVSS:3.1" : map toComponent (cvssOrder (cvssMetrics cvss)))
144+
CVSS20 -> Text.intercalate "/" ("CVSS:2.0" : map toComponent (cvssOrder (cvssMetrics cvss)))
141145
where
142146
toComponent :: Metric -> Text
143147
toComponent (Metric (MetricShortName name) (MetricValueChar value)) = Text.snoc (name <> ":") value
144-
cvss31Order metrics
145-
| ordered = mapMaybe getMetric allMetrics
148+
cvssOrder metrics
149+
| ordered = mapMaybe getMetric (allMetrics (cvssDB (cvssVersion cvss)))
146150
| otherwise = metrics
147151
where
148152
getMetric mi = find (\metric -> miShortName mi == mName metric) metrics
149153

154+
newtype CVSSDB = CVSSDB [MetricGroup]
155+
156+
cvssDB :: CVSSVersion -> CVSSDB
157+
cvssDB v = case v of
158+
CVSS31 -> cvss31
159+
CVSS20 -> cvss20
160+
150161
-- | Description of a metric group.
151162
data MetricGroup = MetricGroup
152163
{ mgName :: Text
@@ -171,12 +182,13 @@ data MetricValue = MetricValue
171182
}
172183

173184
-- | CVSS3.1 metrics pulled from section 2. "Base Metrics" and section section 7.4. "Metric Values"
174-
cvss31 :: [MetricGroup]
185+
cvss31 :: CVSSDB
175186
cvss31 =
176-
[ MetricGroup "Base" baseMetrics
177-
, MetricGroup "Temporal" temporalMetrics
178-
, MetricGroup "Environmental" environmentalMetrics
179-
]
187+
CVSSDB
188+
[ MetricGroup "Base" baseMetrics
189+
, MetricGroup "Temporal" temporalMetrics
190+
, MetricGroup "Environmental" environmentalMetrics
191+
]
180192
where
181193
baseMetrics =
182194
[ MetricInfo
@@ -258,10 +270,10 @@ pattern Unchanged = 6.42
258270
pattern Changed :: Float
259271
pattern Changed = 7.52
260272

261-
cvss31info :: [Metric] -> [Text]
262-
cvss31info = map showMetricInfo
273+
doCVSSInfo :: CVSSDB -> [Metric] -> [Text]
274+
doCVSSInfo (CVSSDB db) = map showMetricInfo
263275
where
264-
showMetricInfo metric = case mapMaybe (getInfo metric) cvss31 of
276+
showMetricInfo metric = case mapMaybe (getInfo metric) db of
265277
[(mg, mi, mv)] ->
266278
mconcat [mgName mg, " ", miName mi, ": ", mvName mv, " (", mvDesc mv, ")"]
267279
_ -> error $ "The impossible have happened for " <> show metric
@@ -270,8 +282,8 @@ cvss31info = map showMetricInfo
270282
mv <- find (\mv -> mvChar mv == mChar metric) (miValues mi)
271283
pure (mg, mi, mv)
272284

273-
allMetrics :: [MetricInfo]
274-
allMetrics = concatMap mgMetrics cvss31
285+
allMetrics :: CVSSDB -> [MetricInfo]
286+
allMetrics (CVSSDB db) = concatMap mgMetrics db
275287

276288
-- | Implementation of the Appendix A - "Floating Point Rounding"
277289
roundup :: Float -> Float
@@ -300,12 +312,15 @@ cvss31score metrics = (toRating score, score)
300312
scope = gm "Scope"
301313

302314
gm :: Text -> Float
303-
gm name = case getMetric name of
304-
Nothing -> error $ "The impossible have happened, unknown metric: " <> Text.unpack name
305-
Just v -> v
306-
getMetric :: Text -> Maybe Float
307-
getMetric name = do
308-
mi <- find (\mi -> miName mi == name) allMetrics
315+
gm = getMetricValue cvss31 metrics scope
316+
317+
getMetricValue :: CVSSDB -> [Metric] -> Float -> Text -> Float
318+
getMetricValue db metrics scope name = case mValue of
319+
Nothing -> error $ "The impossible have happened, unknown metric: " <> Text.unpack name
320+
Just v -> v
321+
where
322+
mValue = do
323+
mi <- find (\mi -> miName mi == name) (allMetrics db)
309324
Metric _ valueChar <- find (\metric -> miShortName mi == mName metric) metrics
310325
mv <- find (\mv -> mvChar mv == valueChar) (miValues mi)
311326
pure $ case mvNumChangedScope mv of
@@ -314,9 +329,91 @@ cvss31score metrics = (toRating score, score)
314329

315330
validateCvss31 :: [Metric] -> Either CVSSError [Metric]
316331
validateCvss31 metrics = do
317-
traverse_ (\t -> t metrics) [validateUnique, validateKnown, validateRequired]
332+
traverse_ (\t -> t metrics) [validateUnique, validateKnown cvss31, validateRequired cvss31]
333+
pure metrics
334+
335+
cvss20 :: CVSSDB
336+
cvss20 =
337+
CVSSDB
338+
[ MetricGroup "Base" baseMetrics
339+
]
340+
where
341+
baseMetrics =
342+
[ MetricInfo
343+
"Access Vector"
344+
"AV"
345+
True
346+
[ MetricValue "Local" (C 'L') 0.395 Nothing "A vulnerability exploitable with only local access requires the attacker to have either physical access to the vulnerable system or a local (shell) account."
347+
, MetricValue "Adjacent Network" (C 'A') 0.646 Nothing "A vulnerability exploitable with adjacent network access requires the attacker to have access to either the broadcast or collision domain of the vulnerable software."
348+
, MetricValue "Network" (C 'N') 1.0 Nothing "A vulnerability exploitable with network access means the vulnerable software is bound to the network stack and the attacker does not require local network access or local access."
349+
]
350+
, MetricInfo
351+
"Access Complexity"
352+
"AC"
353+
True
354+
[ MetricValue "High" (C 'H') 0.35 Nothing "Specialized access conditions exist."
355+
, MetricValue "Medium" (C 'M') 0.61 Nothing "The access conditions are somewhat specialized."
356+
, MetricValue "Low" (C 'L') 0.71 Nothing "Specialized access conditions or extenuating circumstances do not exist."
357+
]
358+
, MetricInfo
359+
"Authentication"
360+
"Au"
361+
True
362+
[ MetricValue "Multiple" (C 'M') 0.45 Nothing "Exploiting the vulnerability requires that the attacker authenticate two or more times, even if the same credentials are used each time."
363+
, MetricValue "Single" (C 'S') 0.56 Nothing "The vulnerability requires an attacker to be logged into the system (such as at a command line or via a desktop session or web interface)."
364+
, MetricValue "None" (C 'N') 0.704 Nothing "Authentication is not required to exploit the vulnerability."
365+
]
366+
, MetricInfo
367+
"Confidentiality Impact"
368+
"C"
369+
True
370+
[ mkNone "There is no impact to the confidentiality of the system."
371+
, mkPartial "There is considerable informational disclosure."
372+
, mkComplete "There is total information disclosure, resulting in all system files being revealed."
373+
]
374+
, MetricInfo
375+
"Integrity Impact"
376+
"I"
377+
True
378+
[ mkNone "There is no impact to the integrity of the system."
379+
, mkPartial "Modification of some system files or information is possible, but the attacker does not have control over what can be modified, or the scope of what the attacker can affect is limited."
380+
, mkComplete "There is a total compromise of system integrity."
381+
]
382+
, MetricInfo
383+
"Availability Impact"
384+
"A"
385+
True
386+
[ mkNone "There is no impact to the availability of the system."
387+
, mkPartial "There is reduced performance or interruptions in resource availability."
388+
, mkComplete "There is a total shutdown of the affected resource."
389+
]
390+
]
391+
mkNone = MetricValue "None" (C 'N') 0 Nothing
392+
mkPartial = MetricValue "Partial" (C 'P') 0.275 Nothing
393+
mkComplete = MetricValue "Complete" (C 'C') 0.660 Nothing
394+
395+
validateCvss20 :: [Metric] -> Either CVSSError [Metric]
396+
validateCvss20 metrics = do
397+
traverse_ (\t -> t metrics) [validateUnique, validateKnown cvss20, validateRequired cvss20]
318398
pure metrics
319399

400+
-- | Implementation of section 3.2.1. "Base Equation"
401+
cvss20score :: [Metric] -> (Rating, Float)
402+
cvss20score metrics = (toRating score, score)
403+
where
404+
score = round_to_1_decimal ((0.6 * impact + 0.4 * exploitability - 1.5) * fImpact)
405+
impact = 10.41 * (1 - (1 - gm "Confidentiality Impact") * (1 - gm "Integrity Impact") * (1 - gm "Availability Impact"))
406+
exploitability = 20 * gm "Access Vector" * gm "Access Complexity" * gm "Authentication"
407+
fImpact
408+
| impact == 0 = 0
409+
| otherwise = 1.176
410+
411+
round_to_1_decimal :: Float -> Float
412+
round_to_1_decimal x = fromIntegral @Int (round (x * 10)) / 10
413+
414+
gm :: Text -> Float
415+
gm = getMetricValue cvss20 metrics 0
416+
320417
{- | Check for duplicates metric
321418
322419
>>> validateUnique [("AV", (C 'N')), ("AC", (C 'L')), ("AV", (C 'L'))]
@@ -337,11 +434,11 @@ validateUnique = traverse_ checkDouble . group . sort . map mName
337434
>>> validateKnown [("AW", (C 'L'))]
338435
Left "Unknown metric: \"AW\""
339436
-}
340-
validateKnown :: [Metric] -> Either CVSSError ()
341-
validateKnown = traverse_ checkKnown
437+
validateKnown :: CVSSDB -> [Metric] -> Either CVSSError ()
438+
validateKnown db = traverse_ checkKnown
342439
where
343440
checkKnown (Metric name char) = do
344-
mi <- case find (\mi -> miShortName mi == name) allMetrics of
441+
mi <- case find (\mi -> miShortName mi == name) (allMetrics db) of
345442
Nothing -> Left (UnknownMetric (coerce name))
346443
Just m -> pure m
347444
case find (\mv -> mvChar mv == char) (miValues mi) of
@@ -353,8 +450,8 @@ validateKnown = traverse_ checkKnown
353450
>>> validateRequired []
354451
Left "Missing \"Attack Vector\""
355452
-}
356-
validateRequired :: [Metric] -> Either CVSSError ()
357-
validateRequired metrics = traverse_ checkRequired allMetrics
453+
validateRequired :: CVSSDB -> [Metric] -> Either CVSSError ()
454+
validateRequired db metrics = traverse_ checkRequired (allMetrics db)
358455
where
359456
checkRequired mi
360457
| miRequired mi

code/cvss/test/Spec.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module Main where
44

55
import Control.Monad
6-
import Data.Text (Text, unpack)
6+
import Data.Text (Text)
77
import qualified Security.CVSS as CVSS
88
import Test.Tasty
99
import Test.Tasty.HUnit
@@ -13,7 +13,7 @@ main = defaultMain $
1313
testCase "Security.CVSS" $ do
1414
forM_ examples $ \(cvssString, score, rating) -> do
1515
case CVSS.parseCVSS cvssString of
16-
Left e -> assertFailure (unpack e)
16+
Left e -> assertFailure (show e)
1717
Right cvss -> do
1818
CVSS.cvssScore cvss @?= (rating, score)
1919
CVSS.cvssVectorString cvss @?= cvssString
@@ -24,4 +24,7 @@ examples =
2424
[ ("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N", 5.8, CVSS.Medium)
2525
, ("CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N", 6.4, CVSS.Medium)
2626
, ("CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N", 3.1, CVSS.Low)
27+
, ("CVSS:2.0/AV:N/AC:L/Au:N/C:N/I:N/A:C", 7.8, CVSS.High)
28+
, ("CVSS:2.0/AV:N/AC:L/Au:N/C:C/I:C/A:C", 10, CVSS.Critical)
29+
, ("CVSS:2.0/AV:L/AC:H/Au:N/C:C/I:C/A:C", 6.2, CVSS.Medium)
2730
]

0 commit comments

Comments
 (0)