Skip to content

Commit 024d41c

Browse files
TristanCacquerayblackheaven
authored andcommitted
Add CVSSError data type
1 parent 547fc4c commit 024d41c

File tree

1 file changed

+92
-49
lines changed

1 file changed

+92
-49
lines changed

code/cvss/src/Security/CVSS.hs

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{-# LANGUAGE DerivingStrategies #-}
2+
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
13
{-# LANGUAGE ImportQualifiedPost #-}
24
{-# LANGUAGE OverloadedStrings #-}
35
{-# LANGUAGE PatternSynonyms #-}
@@ -10,7 +12,10 @@ module Security.CVSS (
1012
CVSS (cvssVersion),
1113
CVSSVersion (..),
1214
Rating (..),
15+
16+
-- * Parser
1317
parseCVSS,
18+
CVSSError (..),
1419

1520
-- * Helpers
1621
cvssVectorString,
@@ -19,9 +24,11 @@ module Security.CVSS (
1924
cvssInfo,
2025
) where
2126

27+
import Data.Coerce (coerce)
2228
import Data.Foldable (traverse_)
2329
import Data.List (find, group, sort)
2430
import Data.Maybe (mapMaybe)
31+
import Data.String (IsString)
2532
import Data.Text (Text)
2633
import Data.Text qualified as Text
2734
import GHC.Float (powerFloat)
@@ -55,28 +62,60 @@ toRating score
5562
| score < 9 = High
5663
| otherwise = Critical
5764

58-
type Metric = (Text, Char)
65+
data CVSSError
66+
= UnknownVersion
67+
| EmptyComponent
68+
| MissingValue Text
69+
| DuplicateMetric Text
70+
| MissingRequiredMetric Text
71+
| UnknownMetric Text
72+
| UnknownValue Text Char
73+
74+
instance Show CVSSError where
75+
show = Text.unpack . showCVSSError
76+
77+
showCVSSError :: CVSSError -> Text
78+
showCVSSError e = case e of
79+
UnknownVersion -> "Unknown CVSS version"
80+
EmptyComponent -> "Empty component"
81+
MissingValue name -> "Missing value for \"" <> name <> "\""
82+
DuplicateMetric name -> "Duplicate metric for \"" <> name <> "\""
83+
MissingRequiredMetric name -> "Missing required metric \"" <> name <> "\""
84+
UnknownMetric name -> "Unknown metric \"" <> name <> "\""
85+
UnknownValue name value -> "Unknown value '" <> Text.pack (show value) <> "' for \"" <> name <> "\""
86+
87+
newtype MetricShortName = MetricShortName Text
88+
deriving newtype (Eq, IsString, Ord, Show)
89+
90+
newtype MetricValueChar = MetricValueChar Char
91+
deriving newtype (Eq, Ord, Show)
92+
93+
data Metric = Metric
94+
{ mName :: MetricShortName
95+
, mChar :: MetricValueChar
96+
}
97+
deriving (Show)
5998

6099
-- example CVSS string: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N
61100

62101
-- | Parse a CVSS string.
63-
parseCVSS :: Text -> Either Text CVSS
102+
parseCVSS :: Text -> Either CVSSError CVSS
64103
parseCVSS txt
65104
| "CVSS:3.1/" `Text.isPrefixOf` txt = parseCVSS31
66-
| otherwise = Left "Unknown CVSS version"
105+
| otherwise = Left UnknownVersion
67106
where
68107
parseCVSS31 =
69108
CVSS CVSS31 <$> do
70109
metrics <- traverse splitComponent components
71110
validateCvss31 metrics
72111

73112
components = drop 1 $ Text.split (== '/') txt
74-
splitComponent :: Text -> Either Text Metric
113+
splitComponent :: Text -> Either CVSSError Metric
75114
splitComponent componentTxt = case Text.unsnoc componentTxt of
76-
Nothing -> Left "Empty component"
115+
Nothing -> Left EmptyComponent
77116
Just (rest, c) -> case Text.unsnoc rest of
78-
Just (name, ':') -> Right (name, c)
79-
_ -> Left "Expected :"
117+
Just (name, ':') -> Right (Metric (MetricShortName name) (MetricValueChar c))
118+
_ -> Left (MissingValue componentTxt)
80119

81120
-- | Compute the base score.
82121
cvssScore :: CVSS -> (Rating, Float)
@@ -100,12 +139,13 @@ cvssShow :: Bool -> CVSS -> Text
100139
cvssShow ordered cvss = case cvssVersion cvss of
101140
CVSS31 -> Text.intercalate "/" ("CVSS:3.1" : map toComponent (cvss31Order (cvssMetrics cvss)))
102141
where
103-
toComponent (name, value) = Text.snoc (name <> ":") value
104-
cvss31Order xs
142+
toComponent :: Metric -> Text
143+
toComponent (Metric (MetricShortName name) (MetricValueChar value)) = Text.snoc (name <> ":") value
144+
cvss31Order metrics
105145
| ordered = mapMaybe getMetric allMetrics
106-
| otherwise = xs
146+
| otherwise = metrics
107147
where
108-
getMetric mi = find (\(name, _) -> miShortName mi == name) xs
148+
getMetric mi = find (\metric -> miShortName mi == mName metric) metrics
109149

110150
-- | Description of a metric group.
111151
data MetricGroup = MetricGroup
@@ -116,15 +156,15 @@ data MetricGroup = MetricGroup
116156
-- | Description of a single metric.
117157
data MetricInfo = MetricInfo
118158
{ miName :: Text
119-
, miShortName :: Text
159+
, miShortName :: MetricShortName
120160
, miRequired :: Bool
121161
, miValues :: [MetricValue]
122162
}
123163

124164
-- | Description of a single metric value
125165
data MetricValue = MetricValue
126166
{ mvName :: Text
127-
, mvChar :: Char
167+
, mvChar :: MetricValueChar
128168
, mvNum :: Float
129169
, mvNumChangedScope :: Maybe Float
130170
, mvDesc :: Text
@@ -143,40 +183,40 @@ cvss31 =
143183
"Attack Vector"
144184
"AV"
145185
True
146-
[ MetricValue "Network" 'N' 0.85 Nothing "The vulnerable component is bound to the network stack and the set of possible attackers extends beyond the other options listed below, up to and including the entire Internet."
147-
, MetricValue "Adjacent" 'A' 0.62 Nothing "The vulnerable component is bound to the network stack, but the attack is limited at the protocol level to a logically adjacent topology."
148-
, MetricValue "Local" 'L' 0.55 Nothing "The vulnerable component is not bound to the network stack and the attacker’s path is via read/write/execute capabilities."
149-
, MetricValue "Physical" 'P' 0.2 Nothing "The attack requires the attacker to physically touch or manipulate the vulnerable component."
186+
[ MetricValue "Network" (C 'N') 0.85 Nothing "The vulnerable component is bound to the network stack and the set of possible attackers extends beyond the other options listed below, up to and including the entire Internet."
187+
, MetricValue "Adjacent" (C 'A') 0.62 Nothing "The vulnerable component is bound to the network stack, but the attack is limited at the protocol level to a logically adjacent topology."
188+
, MetricValue "Local" (C 'L') 0.55 Nothing "The vulnerable component is not bound to the network stack and the attacker’s path is via read/write/execute capabilities."
189+
, MetricValue "Physical" (C 'P') 0.2 Nothing "The attack requires the attacker to physically touch or manipulate the vulnerable component."
150190
]
151191
, MetricInfo
152192
"Attack Complexity"
153193
"AC"
154194
True
155-
[ MetricValue "Low" 'L' 0.77 Nothing "Specialized access conditions or extenuating circumstances do not exist."
156-
, MetricValue "High" 'H' 0.44 Nothing "A successful attack depends on conditions beyond the attacker's control."
195+
[ MetricValue "Low" (C 'L') 0.77 Nothing "Specialized access conditions or extenuating circumstances do not exist."
196+
, MetricValue "High" (C 'H') 0.44 Nothing "A successful attack depends on conditions beyond the attacker's control."
157197
]
158198
, MetricInfo
159199
"Privileges Required"
160200
"PR"
161201
True
162-
[ MetricValue "None" 'N' 0.85 Nothing "The attacker is unauthorized prior to attack, and therefore does not require any access to settings or files of the vulnerable system to carry out an attack."
163-
, MetricValue "Low" 'L' 0.62 (Just 0.68) "The attacker requires privileges that provide basic user capabilities that could normally affect only settings and files owned by a user."
164-
, MetricValue "High" 'H' 0.27 (Just 0.5) "The attacker requires privileges that provide significant (e.g., administrative) control over the vulnerable component allowing access to component-wide settings and files."
202+
[ MetricValue "None" (C 'N') 0.85 Nothing "The attacker is unauthorized prior to attack, and therefore does not require any access to settings or files of the vulnerable system to carry out an attack."
203+
, MetricValue "Low" (C 'L') 0.62 (Just 0.68) "The attacker requires privileges that provide basic user capabilities that could normally affect only settings and files owned by a user."
204+
, MetricValue "High" (C 'H') 0.27 (Just 0.5) "The attacker requires privileges that provide significant (e.g., administrative) control over the vulnerable component allowing access to component-wide settings and files."
165205
]
166206
, MetricInfo
167207
"User Interaction"
168208
"UI"
169209
True
170-
[ MetricValue "None" 'N' 0.85 Nothing "The vulnerable system can be exploited without interaction from any user."
171-
, MetricValue "Required" 'R' 0.62 Nothing "Successful exploitation of this vulnerability requires a user to take some action before the vulnerability can be exploited."
210+
[ MetricValue "None" (C 'N') 0.85 Nothing "The vulnerable system can be exploited without interaction from any user."
211+
, MetricValue "Required" (C 'R') 0.62 Nothing "Successful exploitation of this vulnerability requires a user to take some action before the vulnerability can be exploited."
172212
]
173213
, MetricInfo
174214
"Scope"
175215
"S"
176216
True
177217
[ -- Note: not defined as contants in specification
178-
MetricValue "Unchanged" 'U' Unchanged Nothing "An exploited vulnerability can only affect resources managed by the same security authority."
179-
, MetricValue "Changed" 'C' Changed Nothing "An exploited vulnerability can affect resources beyond the security scope managed by the security authority of the vulnerable component."
218+
MetricValue "Unchanged" (C 'U') Unchanged Nothing "An exploited vulnerability can only affect resources managed by the same security authority."
219+
, MetricValue "Changed" (C 'C') Changed Nothing "An exploited vulnerability can affect resources beyond the security scope managed by the security authority of the vulnerable component."
180220
]
181221
, MetricInfo
182222
"Confidentiality Impact"
@@ -203,13 +243,16 @@ cvss31 =
203243
, mkNone "There is no impact to availability within the impacted component."
204244
]
205245
]
206-
mkHigh = MetricValue "High" 'H' 0.56 Nothing
207-
mkLow = MetricValue "Low" 'L' 0.22 Nothing
208-
mkNone = MetricValue "None" 'N' 0 Nothing
246+
mkHigh = MetricValue "High" (C 'H') 0.56 Nothing
247+
mkLow = MetricValue "Low" (C 'L') 0.22 Nothing
248+
mkNone = MetricValue "None" (C 'N') 0 Nothing
209249
-- TODOs
210250
temporalMetrics = []
211251
environmentalMetrics = []
212252

253+
pattern C :: Char -> MetricValueChar
254+
pattern C c = MetricValueChar c
255+
213256
pattern Unchanged :: Float
214257
pattern Unchanged = 6.42
215258
pattern Changed :: Float
@@ -222,9 +265,9 @@ cvss31info = map showMetricInfo
222265
[(mg, mi, mv)] ->
223266
mconcat [mgName mg, " ", miName mi, ": ", mvName mv, " (", mvDesc mv, ")"]
224267
_ -> error $ "The impossible have happened for " <> show metric
225-
getInfo (name, value) mg = do
226-
mi <- find (\mi -> miShortName mi == name) (mgMetrics mg)
227-
mv <- find (\mv -> mvChar mv == value) (miValues mi)
268+
getInfo metric mg = do
269+
mi <- find (\mi -> miShortName mi == mName metric) (mgMetrics mg)
270+
mv <- find (\mv -> mvChar mv == mChar metric) (miValues mi)
228271
pure (mg, mi, mv)
229272

230273
allMetrics :: [MetricInfo]
@@ -263,58 +306,58 @@ cvss31score metrics = (toRating score, score)
263306
getMetric :: Text -> Maybe Float
264307
getMetric name = do
265308
mi <- find (\mi -> miName mi == name) allMetrics
266-
valueChar <- lookup (miShortName mi) metrics
309+
Metric _ valueChar <- find (\metric -> miShortName mi == mName metric) metrics
267310
mv <- find (\mv -> mvChar mv == valueChar) (miValues mi)
268311
pure $ case mvNumChangedScope mv of
269312
Just value | scope /= Unchanged -> value
270313
_ -> mvNum mv
271314

272-
validateCvss31 :: [Metric] -> Either Text [Metric]
315+
validateCvss31 :: [Metric] -> Either CVSSError [Metric]
273316
validateCvss31 metrics = do
274317
traverse_ (\t -> t metrics) [validateUnique, validateKnown, validateRequired]
275318
pure metrics
276319

277320
{- | Check for duplicates metric
278321
279-
>>> validateUnique [("AV", 'N'), ("AC", 'L'), ("AV", 'L')]
322+
>>> validateUnique [("AV", (C 'N')), ("AC", (C 'L')), ("AV", (C 'L'))]
280323
Left "Duplicated \"AV\""
281324
-}
282-
validateUnique :: [Metric] -> Either Text ()
283-
validateUnique = traverse_ checkDouble . group . sort . map fst
325+
validateUnique :: [Metric] -> Either CVSSError ()
326+
validateUnique = traverse_ checkDouble . group . sort . map mName
284327
where
285328
checkDouble [] = error "The impossible have happened"
286329
checkDouble [_] = pure ()
287-
checkDouble (n : _) = Left $ "Duplicated \"" <> n <> "\""
330+
checkDouble (MetricShortName n : _) = Left (DuplicateMetric n)
288331

289332
{- | Check for unknown metric
290333
291-
>>> validateKnown [("AV", 'M')]
292-
Left "Unknown value: 'M'"
334+
>>> validateKnown [("AV", (C 'M'))]
335+
Left "Unknown value: (C 'M')"
293336
294-
>>> validateKnown [("AW", 'L')]
337+
>>> validateKnown [("AW", (C 'L'))]
295338
Left "Unknown metric: \"AW\""
296339
-}
297-
validateKnown :: [Metric] -> Either Text ()
340+
validateKnown :: [Metric] -> Either CVSSError ()
298341
validateKnown = traverse_ checkKnown
299342
where
300-
checkKnown (name, value) = do
343+
checkKnown (Metric name char) = do
301344
mi <- case find (\mi -> miShortName mi == name) allMetrics of
302-
Nothing -> Left $ "Unknown metric: \"" <> name <> "\""
345+
Nothing -> Left (UnknownMetric (coerce name))
303346
Just m -> pure m
304-
case find (\mv -> mvChar mv == value) (miValues mi) of
305-
Nothing -> Left $ "Unknown value: '" <> Text.pack (show value) <> "'"
347+
case find (\mv -> mvChar mv == char) (miValues mi) of
348+
Nothing -> Left (UnknownValue (coerce name) (coerce char))
306349
Just _ -> pure ()
307350

308351
{- | Check for required metric
309352
310353
>>> validateRequired []
311354
Left "Missing \"Attack Vector\""
312355
-}
313-
validateRequired :: [Metric] -> Either Text ()
356+
validateRequired :: [Metric] -> Either CVSSError ()
314357
validateRequired metrics = traverse_ checkRequired allMetrics
315358
where
316359
checkRequired mi
317360
| miRequired mi
318-
, Nothing <- lookup (miShortName mi) metrics =
319-
Left $ "Missing \"" <> miName mi <> "\""
361+
, Nothing <- find (\metric -> miShortName mi == mName metric) metrics =
362+
Left (MissingRequiredMetric (miName mi))
320363
| otherwise = pure ()

0 commit comments

Comments
 (0)