hsec-tools-0.3.0.1: test/Spec/FormatSpec.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
module Spec.FormatSpec (spec) where
import Control.Monad (replicateM)
import Data.Function (on)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
import Distribution.Types.Version
import Distribution.Types.VersionRange
import qualified Hedgehog as Gen
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Network.URI (URI(URI), URIAuth(URIAuth))
import qualified Prettyprinter as Pretty
import qualified Prettyprinter.Render.Text as Pretty
import Security.Advisories.Core.Advisory
import Security.Advisories.Core.HsecId
import Security.Advisories.Format
import Security.CVSS
import Security.OSV (Reference (..), ReferenceType (..))
import Test.Tasty
import Test.Tasty.Hedgehog
import qualified Toml
spec :: TestTree
spec =
testGroup
"Format"
[ testGroup
"FrontMatter"
[ testProperty "parse . render == id" $
Gen.property $ do
fm <- Gen.forAll genFrontMatter
let rendered =
Pretty.renderStrict $ Pretty.layoutPretty Pretty.defaultLayoutOptions $ Toml.encode fm
Gen.footnote $ T.unpack rendered
Toml.decode rendered Gen.=== Toml.Success mempty (FrontMatterEq fm)
]
]
newtype FrontMatterEq = FrontMatterEq {unFrontMatter :: FrontMatter}
deriving newtype (Show, FromValue)
instance Eq FrontMatterEq where
(==) = (==) `on` show . unFrontMatter
genFrontMatter :: Gen.Gen FrontMatter
genFrontMatter =
FrontMatter
<$> genAdvisoryMetadata
<*> Gen.list (Range.linear 0 10) genReference
<*> Gen.list (Range.linear 0 10) genAffected
genAdvisoryMetadata :: Gen.Gen AdvisoryMetadata
genAdvisoryMetadata =
AdvisoryMetadata
<$> genHsecId
<*> Gen.maybe genUTCTime
<*> Gen.maybe genUTCTime
<*> Gen.list (Range.linear 0 5) genCAPEC
<*> Gen.list (Range.linear 0 5) genCWE
<*> Gen.list (Range.linear 0 5) genKeyword
<*> Gen.list (Range.linear 0 5) genText
<*> Gen.list (Range.linear 0 5) genText
genAffected :: Gen.Gen Affected
genAffected =
Affected
<$> genComponentIdentifier
<*> genCVSS
<*> Gen.list (Range.linear 0 5) genAffectedVersionRange
<*> Gen.maybe (Gen.list (Range.linear 0 5) genArchitecture)
<*> Gen.maybe (Gen.list (Range.linear 0 5) genOS)
<*> (Map.toList . Map.fromList <$> Gen.list (Range.linear 0 5) ((,) <$> genText <*> genVersionRange))
genComponentIdentifier :: Gen.Gen ComponentIdentifier
genComponentIdentifier = Gen.choice $
[ Repository
<$> (RepositoryURL <$> genURI)
<*> (RepositoryName <$> genText)
<*> (mkPackageName . T.unpack <$> genText)
, hackage . mkPackageName . T.unpack <$> genText
, GHC <$> Gen.enumBounded
]
genURI :: Gen.Gen URI
genURI = do
host <- Gen.element ["example.com", "foo.org", "bar.net", "test.co"]
nPath <- Gen.int (Range.linear 0 2)
parts <- replicateM nPath (Gen.string (Range.linear 1 10) Gen.alphaNum)
let path = concatMap ('/':) parts
hasQ <- Gen.bool
query <- if not hasQ
then pure ""
else do
k <- Gen.string (Range.linear 1 6) Gen.alphaNum
v <- Gen.string (Range.linear 1 6) Gen.alphaNum
pure $ '?': k ++ "=" ++ v
pure $ URI "https:" (Just $ URIAuth "" host "") path query ""
genCVSS :: Gen.Gen CVSS
genCVSS =
Gen.choice $
map
(\x -> either (\e -> error $ "Cannot parse CVSS " <> show x <> " " <> show e) return $ parseCVSS x)
[ "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N",
"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",
"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N",
"CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
"CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",
"CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N",
"CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
"CVSS:3.0/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:L",
"AV:N/AC:L/Au:N/C:N/I:N/A:C",
"AV:N/AC:L/Au:N/C:C/I:C/A:C",
"AV:L/AC:H/Au:N/C:C/I:C/A:C"
]
genCAPEC :: Gen.Gen CAPEC
genCAPEC = CAPEC <$> Gen.integral (Range.linear 100 999)
genCWE :: Gen.Gen CWE
genCWE = CWE <$> Gen.integral (Range.linear 100 999)
genHsecId :: Gen.Gen HsecId
genHsecId = flip nextHsecId placeholder <$> Gen.integral (Range.linear 2024 2032)
genUTCTime :: Gen.Gen UTCTime
genUTCTime =
UTCTime
<$> genDay
<*> fmap secondsToDiffTime (Gen.integral $ Range.constant 0 86401)
genDay :: Gen.Gen Day
genDay = do
y <- toInteger <$> Gen.int (Range.constant 1968 2019)
m <- Gen.int (Range.constant 1 12)
d <- Gen.int (Range.constant 1 28)
pure $ fromGregorian y m d
genVersionRange :: Gen.Gen VersionRange
genVersionRange =
Gen.recursive
Gen.choice
[ pure anyVersion,
pure noVersion,
thisVersion <$> genVersion,
notThisVersion <$> genVersion,
laterVersion <$> genVersion,
earlierVersion <$> genVersion,
orLaterVersion <$> genVersion,
orEarlierVersion <$> genVersion,
withinVersion <$> genVersion,
majorBoundVersion <$> genVersion
]
[ Gen.subterm2 genVersionRange genVersionRange unionVersionRanges,
Gen.subterm2 genVersionRange genVersionRange intersectVersionRanges
]
genText :: Gen.Gen Text
genText = Gen.text (Range.linear 1 20) Gen.alphaNum
genAffectedVersionRange :: Gen.Gen AffectedVersionRange
genAffectedVersionRange = AffectedVersionRange <$> genVersion <*> Gen.maybe genVersion
genVersion :: Gen.Gen Version
genVersion = mkVersion <$> Gen.list (Range.linear 1 5) (Gen.integral (Range.linear 0 999))
genArchitecture :: Gen.Gen Architecture
genArchitecture = Gen.enumBounded
genOS :: Gen.Gen OS
genOS = Gen.enumBounded
genKeyword :: Gen.Gen Keyword
genKeyword = Keyword <$> genText
genReference :: Gen.Gen Reference
genReference = Reference <$> genReferenceType <*> genText
genReferenceType :: Gen.Gen ReferenceType
genReferenceType = Gen.enumBounded