hsec-tools-0.1.0.0: test/Spec/QueriesSpec.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Spec.QueriesSpec (spec) where
import Data.Bifunctor (first)
import Data.Either (fromRight)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Distribution.Parsec (eitherParsec)
import Distribution.Types.Version (version0, alterVersion)
import Distribution.Types.VersionRange (VersionRange, VersionRangeF(..), anyVersion, projectVersionRange)
import Test.Tasty
import Test.Tasty.HUnit
import Security.CVSS (parseCVSS)
import Security.Advisories.Core.Advisory
import Security.Advisories.Core.HsecId
import Security.Advisories.Queries
spec :: TestTree
spec =
testGroup "Queries" [
testGroup "isAffectedBy" $
flip concatMap cases $ map $ \(actual, query, expected) ->
let title x y =
if expected
then show x <> " is vulnerable to " <> show y
else show x <> " is not vulnerable to " <> show y
versionRange x =
either (\e -> error $ "Cannot parse version range " <> show x <> " : " <> show e) id $
parseVersionRange $
if x == ""
then Nothing
else Just x
in testCase (title actual query) $
let query' = versionRange query
affectedVersion' = versionRange actual
in isVersionRangeAffectedBy packageName query' (mkAdvisory affectedVersion')
@?= expected
]
cases :: [[(Text, Text, Bool)]]
cases =
[
reversible ("", "", True)
, reversible ("", "==1", True)
, reversible ("==1.1", "<=2", True)
, reversible ("==1.1", ">1", True)
, reversible ("==1", "==1", True)
, reversible ("==2||==1", "==1", True)
, reversible ("==1.1", "<=2&&>1", True)
, reversible ("^>=1", ">1&&<1.2", True)
, reversible (">=1", "==2", True)
, reversible (">=1", "==1", True)
, reversible ("==2", ">=2", True)
, reversible ("==2", ">1", True)
, reversible (">5", ">=2", True)
, reversible (">5", ">2", True)
, reversible ("==5", ">=2", True)
, reversible ("==5", ">2", True)
, reversible (">=5", ">2", True)
, reversible ("<=5", ">2", True)
, reversible ("<=5", "<=2", True)
, reversible ("<5", ">=2", True)
, reversible (">=2", "==5", True)
, reversible (">2", "==5", True)
, reversible (">5", ">=5", True)
, reversible ("^>=1.1", ">1", True)
, reversible ("^>=1.1", "<2", True)
, reversible ("^>=1.1", "<=1.2", True)
, reversible ("^>=1.1", ">1.1", True)
, reversible ("^>=1.1", ">=1.1.5", True)
, reversible ("^>=1.1", ">=1", True)
, reversible ("==1.1", "<1", False)
, reversible ("==2.1", "<=2", False)
, reversible ("==1", ">1.1", False)
, reversible ("==2", "==1", False)
, reversible ("==2||==1", ">3", False)
, notReversible ("<=2&&>1", "==3", True)
, reversible (">=2", "==1.1", False)
, reversible (">=1.1", "==1", False)
, reversible ("==2", ">=2.1", False)
, notReversible (">1", "==1", False)
, reversible ("<2", ">=2", False)
, reversible ("==2", ">=5", False)
, reversible ("==2", ">5", False)
, reversible ("<=2", ">5", False)
, reversible ("<=2", ">5", False)
, reversible ("<2", ">=5", False)
, reversible (">=2", "==1.1", False)
, reversible ("<2", "==5", False)
, reversible ("<5", ">=5", False)
, reversible ("^>=1.1", "<1", False)
, reversible ("^>=1.1", "<1.1", False)
, reversible ("^>=1.1", ">=1.2", False)
, reversible ("^>=1.1", "<=1", False)
, reversible ("^>=1.1", ">2", False)
, reversible ("^>=1", ">=2", False)
]
where reversible (query, affectedVersion, expected) = [(query, affectedVersion, expected), (query, affectedVersion, expected)]
notReversible (query, affectedVersion, expected) = [(query, affectedVersion, expected), (affectedVersion, query, not expected)]
mkAdvisory :: VersionRange -> Advisory
mkAdvisory versionRange =
Advisory
{ advisoryId = fromMaybe (error "Cannot mkHsecId") $ mkHsecId 2023 42
, advisoryModified = read "2023-01-01T00:00:00"
, advisoryPublished = read "2023-01-01T00:00:00"
, advisoryCAPECs = []
, advisoryCWEs = []
, advisoryKeywords = []
, advisoryAliases = [ "CVE-2022-XXXX" ]
, advisoryRelated = [ "CVE-2022-YYYY" , "CVE-2022-ZZZZ" ]
, advisoryAffected =
[ Affected
{ affectedPackage = packageName
, affectedCVSS = cvss
, affectedVersions = mkAffectedVersions versionRange
, affectedArchitectures = Nothing
, affectedOS = Nothing
, affectedDeclarations = []
}
]
, advisoryReferences = []
, advisoryPandoc = mempty
, advisoryHtml = ""
, advisorySummary = ""
, advisoryDetails = ""
}
where
cvss = fromRight (error "Cannot parseCVSS") (parseCVSS "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
mkAffectedVersions :: VersionRange -> [AffectedVersionRange]
mkAffectedVersions vr =
let
fixed from to =
AffectedVersionRange
{ affectedVersionRangeIntroduced = from
, affectedVersionRangeFixed = Just to
}
onlyFixed to =
AffectedVersionRange
{ affectedVersionRangeIntroduced = version0
, affectedVersionRangeFixed = Just to
}
vulnerable from =
AffectedVersionRange
{ affectedVersionRangeIntroduced = from
, affectedVersionRangeFixed = Nothing
}
nextMinor =
\case
[] -> [1]
[x] -> [x, 1]
[x, y] -> [x, y, 1]
[x, y, z] -> [x, y, z, 1]
[w, x, y, z] -> [w, x, y, z + 1]
xs -> xs ++ [1]
previousMinor =
\case
[] -> [0]
[x] -> [x - 1 , 99]
[x, y] -> [x, y - 1, 99]
[x, y, z] -> [x, y, z - 1, 99]
[w, x, y, z] -> [w, x, y, z - 1]
_ -> error "TODO"
mkMajorBoundVersion =
\case
[] -> [0]
[x] -> [x, 1]
(x:y:_) -> [x, y + 1]
in
case projectVersionRange vr of
ThisVersionF x -> [fixed x $ alterVersion (<> [0,0,1]) x]
LaterVersionF x -> [vulnerable $ alterVersion nextMinor x]
OrLaterVersionF x -> [vulnerable x]
EarlierVersionF x -> [onlyFixed $ alterVersion previousMinor x]
OrEarlierVersionF x -> [onlyFixed x]
MajorBoundVersionF x -> [fixed x $ alterVersion mkMajorBoundVersion x]
UnionVersionRangesF x y -> mkAffectedVersions x <> mkAffectedVersions y
IntersectVersionRangesF x y ->
[ low { affectedVersionRangeFixed = affectedVersionRangeFixed high }
| low <- mkAffectedVersions x
, high <- mkAffectedVersions y
]
packageName :: Text
packageName = "package-name"
-- | Parse 'VersionRange' as given to the CLI
parseVersionRange :: Maybe Text -> Either Text VersionRange
parseVersionRange = maybe (return anyVersion) (first T.pack . eitherParsec . T.unpack)