packages feed

cvss-0.3.0.0: src/Security/CVSS/Internal.hs

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}

module Security.CVSS.Internal
  ( CVSSDB (..),
    MetricGroup (..),
    MetricInfo (..),
    MetricValue (..),
    allMetrics,
    lookupMetricInfo,
    lookupMetricValueChar,
    lookupMetricValue,
    getMetricValue,
    getMetricValueOr,
    doCVSSInfo,
    roundup,
    validateUnique,
    validateKnown,
    validateRequired,
    hasTemporalMetrics,
    hasEnvironmentalMetrics,
    getModifiedMetricValue,
    changed,
    unchanged,
  )
where

import Data.Coerce (coerce)
import Data.Foldable (traverse_)
import Data.List (find, group, sort)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Text (Text)
import Data.Text qualified as Text
import Security.CVSS.Types

-- Constants used in place of Unchanged/Changed pattern synonyms
unchanged :: Float
unchanged = 6.42

changed :: Float
changed = 7.52

newtype CVSSDB = CVSSDB [MetricGroup]

data MetricGroup = MetricGroup
  { mgName :: Text,
    mgMetrics :: [MetricInfo]
  }

data MetricInfo = MetricInfo
  { miName :: Text,
    miShortName :: MetricShortName,
    miRequired :: Bool,
    miValues :: [MetricValue]
  }

data MetricValue = MetricValue
  { mvName :: Text,
    mvChar :: MetricValueChar,
    mvNum :: Float,
    mvNumChangedScope :: Maybe Float,
    mvDesc :: Text
  }

allMetrics :: CVSSDB -> [MetricInfo]
allMetrics (CVSSDB db) = concatMap mgMetrics db

lookupMetricInfo :: CVSSDB -> Text -> Maybe MetricInfo
lookupMetricInfo db name =
  find (\mi -> miName mi == name) (allMetrics db)

lookupMetricValueChar :: CVSSDB -> [Metric] -> Text -> Maybe MetricValueChar
lookupMetricValueChar db metrics name = do
  mi <- lookupMetricInfo db name
  Metric _ valueChar <- find (\metric -> miShortName mi == mName metric) metrics
  pure valueChar

lookupMetricValue :: CVSSDB -> [Metric] -> Float -> Text -> Maybe Float
lookupMetricValue db metrics scope name = do
  mi <- lookupMetricInfo db name
  valueChar <- lookupMetricValueChar db metrics name
  mv <- find (\mv -> mvChar mv == valueChar) (miValues mi)
  pure $ case mvNumChangedScope mv of
    Just value | scope /= unchanged -> value
    _ -> mvNum mv

getMetricValue :: CVSSDB -> [Metric] -> Float -> Text -> Float
getMetricValue db metrics scope name = case lookupMetricValue db metrics scope name of
  Nothing -> error $ "The impossible have happened, unknown metric: " <> Text.unpack name
  Just value -> value

getMetricValueOr :: CVSSDB -> [Metric] -> Float -> Float -> Text -> Float
getMetricValueOr db metrics defaultValue scope name = fromMaybe defaultValue $ lookupMetricValue db metrics scope name

doCVSSInfo :: CVSSDB -> [Metric] -> [Text]
doCVSSInfo (CVSSDB db) = map showMetricInfo
  where
    showMetricInfo metric = case mapMaybe (getInfo metric) db of
      [(mg, mi, mv)] ->
        mconcat [mgName mg, " ", miName mi, ": ", mvName mv, " (", mvDesc mv, ")"]
      _ -> error $ "The impossible have happened for " <> show metric
    getInfo metric mg = do
      mi <- find (\mi -> miShortName mi == mName metric) (mgMetrics mg)
      mv <- find (\mv -> mvChar mv == mChar metric) (miValues mi)
      pure (mg, mi, mv)

roundup :: Float -> Float
roundup input
  | int_input `mod` 10000 == 0 = fromIntegral int_input / 100000
  | otherwise = (fromIntegral (floor_int (fromIntegral int_input / 10000)) + 1) / 10
  where
    floor_int :: Float -> Int
    floor_int = floor
    int_input :: Int
    int_input = round (input * 100000)

validateUnique :: [Metric] -> Either CVSSError ()
validateUnique = traverse_ checkDouble . group . sort . map mName
  where
    checkDouble [] = error "The impossible have happened"
    checkDouble [_] = pure ()
    checkDouble (MetricShortName n : _) = Left (DuplicateMetric n)

validateKnown :: CVSSDB -> [Metric] -> Either CVSSError ()
validateKnown db = traverse_ checkKnown
  where
    checkKnown (Metric name char) = do
      mi <- case find (\mi -> miShortName mi == name) (allMetrics db) of
        Nothing -> Left (UnknownMetric (coerce name))
        Just m -> pure m
      case find (\mv -> mvChar mv == char) (miValues mi) of
        Nothing -> Left (UnknownValue (coerce name) (coerce char))
        Just _ -> pure ()

validateRequired :: CVSSDB -> [Metric] -> Either CVSSError ()
validateRequired db metrics = traverse_ checkRequired (allMetrics db)
  where
    checkRequired mi
      | miRequired mi,
        Nothing <- find (\metric -> miShortName mi == mName metric) metrics =
          Left (MissingRequiredMetric (miName mi))
      | otherwise = pure ()

hasTemporalMetrics :: [Metric] -> Bool
hasTemporalMetrics =
  any (\metric -> mName metric `elem` ["E", "RL", "RC"])

hasEnvironmentalMetrics :: [Metric] -> Bool
hasEnvironmentalMetrics =
  any
    ( \metric ->
        mName metric
          `elem` ["CR", "IR", "AR", "MAV", "MAC", "MPR", "MUI", "MS", "MC", "MI", "MA"]
    )

getModifiedMetricValue :: CVSSDB -> [Metric] -> Text -> Text -> Float -> Float
getModifiedMetricValue db ms modifiedName baseName scope =
  case lookupMetricValueChar db ms modifiedName of
    Just (MetricValueChar "X") -> getMetricValue db ms scope baseName
    Just _ -> getMetricValue db ms scope modifiedName
    Nothing -> getMetricValue db ms scope baseName