packages feed

hsec-core 0.2.0.2 → 0.5.0.0

raw patch · 7 files changed

Files

CHANGELOG.md view
@@ -1,3 +1,23 @@+## 0.5.0.0++* Add `AffectedApi` type and `affectedApi` field in `Affected`++## 0.4.0.0++* Introduce `OsvId` and use it in `Advisory.{aliases,related}`++## 0.3.0.2++* Update `cvss` dependency bounds++## 0.3.0.0++* Add `Repository` and `ComponentIdentifier` in `Security.Advisories.Core.Advisory`++## 0.2.1.0++* Introduce `isVersionAffectedBy` and `isVersionRangeAffectedBy` in `Security.Advisories.Core`+ ## 0.2.0.2  * Update `osv` dependency bounds
README.md view
@@ -11,5 +11,5 @@ Run (and auto update) the golden test:  ```ShellSession-cabal test -O0 --test-show-details=direct --test-option=--accept+cabal test -O0 --test-show-details=direct ```
hsec-core.cabal view
@@ -1,43 +1,56 @@-cabal-version:      2.4-name:               hsec-core-version:            0.2.0.2+cabal-version:   2.4+name:            hsec-core+version:         0.5.0.0  -- A short (one-line) description of the package.-synopsis:           Core package representing Haskell advisories+synopsis:        Core package representing Haskell advisories  -- A longer description of the package.-description:        Core package representing Haskell advisories.+description:     Core package representing Haskell advisories.  -- A URL where users can report bugs. -- bug-reports:  -- The license under which the package is released.-license:            BSD-3-Clause-author:             Haskell Security Response Team-maintainer:         security-advisories@haskell.org+license:         BSD-3-Clause+author:          Haskell Security Response Team+maintainer:      security-advisories@haskell.org  -- A copyright notice. -- copyright:-category:           Data-extra-doc-files:    CHANGELOG.md, README.md+category:        Data+extra-doc-files:+  CHANGELOG.md+  README.md  tested-with:-  GHC ==8.10.7 || ==9.0.2 || ==9.2.8 || ==9.4.8 || ==9.6.6 || ==9.8.3 || ==9.10.1 || ==9.12.1+  GHC ==8.10.7+   || ==9.0.2+   || ==9.2.8+   || ==9.4.8+   || ==9.6.6+   || ==9.8.3+   || ==9.10.1+   || ==9.12.1  library   exposed-modules:     Security.Advisories.Core.Advisory     Security.Advisories.Core.HsecId+    Security.Advisories.Core.OsvId    build-depends:     , base          >=4.14    && <5     , Cabal-syntax  >=3.8.1.0 && <3.15-    , cvss >= 0.2 && < 0.3-    , osv >= 0.1 && < 0.3+    , containers    >=0.6     && <0.8+    , cvss          >=0.2     && <0.4+    , network-uri   >=2.6.3.0 && <2.8+    , osv           >=0.1     && <0.3     , pandoc-types  >=1.22    && <2     , safe          >=0.3     && <0.4     , text          >=1.2     && <3     , time          >=1.9     && <1.15+   hs-source-dirs:   src   default-language: Haskell2010   ghc-options:@@ -48,12 +61,14 @@   type:             exitcode-stdio-1.0   hs-source-dirs:   test   main-is:          Spec.hs+  other-modules:    Spec.QueriesSpec   build-depends:     , base+    , Cabal-syntax     , cvss     , hsec-core-    , tasty        <2-    , tasty-hunit  <0.11+    , tasty         <2+    , tasty-hunit   <0.11     , text    default-language: Haskell2010
src/Security/Advisories/Core/Advisory.hs view
@@ -1,59 +1,93 @@-{-# LANGUAGE DerivingVia, OverloadedStrings #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}  module Security.Advisories.Core.Advisory-  ( Advisory(..)+  ( Advisory (..),+     -- * Supporting types-  , Affected(..)-  , CAPEC(..)-  , CWE(..)-  , Architecture(..)-  , AffectedVersionRange(..)-  , OS(..)-  , Keyword(..)-  , ComponentIdentifier(..)-  , GHCComponent(..)-  , ghcComponentToText-  , ghcComponentFromText+    Affected (..),+    CAPEC (..),+    CWE (..),+    Architecture (..),+    AffectedVersionRange (..),+    OS (..),+    Keyword (..),+    AffectedApi (..),+    ComponentIdentifier (..),+    GHCComponent (..),+    RepositoryURL (..),+    RepositoryName (..),+    PackageName,+    mkPackageName,+    unPackageName,+    ghcComponentToText,+    ghcComponentFromText,+    hackage,++    -- * Queries+    isVersionAffectedBy,+    isVersionRangeAffectedBy,   )-  where+where  import Data.Text (Text) import Data.Time (UTCTime)+import Distribution.Types.PackageName (PackageName, mkPackageName, unPackageName) import Distribution.Types.Version (Version)-import Distribution.Types.VersionRange (VersionRange)--import Text.Pandoc.Definition (Pandoc)-+import Distribution.Types.VersionInterval (asVersionIntervals)+import Distribution.Types.VersionRange (VersionRange, anyVersion, earlierVersion, intersectVersionRanges, noVersion, orLaterVersion, unionVersionRanges, withinRange)+import Network.URI (URI)+import Network.URI.Static (uri) import Security.Advisories.Core.HsecId (HsecId)+import Security.Advisories.Core.OsvId (OsvId) import qualified Security.CVSS as CVSS import Security.OSV (Reference)+import Text.Pandoc.Definition (Pandoc)  data Advisory = Advisory-  { advisoryId :: HsecId-  , advisoryModified :: UTCTime-  , advisoryPublished :: UTCTime-  , advisoryCAPECs :: [CAPEC]-  , advisoryCWEs :: [CWE]-  , advisoryKeywords :: [Keyword]-  , advisoryAliases :: [Text]-  , advisoryRelated :: [Text]-  , advisoryAffected :: [Affected]-  , advisoryReferences :: [Reference]-  , advisoryPandoc :: Pandoc  -- ^ Parsed document, without TOML front matter-  , advisoryHtml :: Text-  , advisorySummary :: Text-    -- ^ A one-line, English textual summary of the vulnerability-  , advisoryDetails :: Text-    -- ^ Details of the vulnerability (CommonMark), without TOML front matter+  { advisoryId :: HsecId,+    advisoryModified :: UTCTime,+    advisoryPublished :: UTCTime,+    advisoryCAPECs :: [CAPEC],+    advisoryCWEs :: [CWE],+    advisoryKeywords :: [Keyword],+    advisoryAliases :: [OsvId],+    advisoryRelated :: [OsvId],+    advisoryAffected :: [Affected],+    advisoryReferences :: [Reference],+    -- | Parsed document, without TOML front matter+    advisoryPandoc :: Pandoc,+    advisoryHtml :: Text,+    -- | A one-line, English textual summary of the vulnerability+    advisorySummary :: Text,+    -- | Details of the vulnerability (CommonMark), without TOML front matter+    advisoryDetails :: Text   }   deriving stock (Show) -data ComponentIdentifier = Hackage Text | GHC GHCComponent+data ComponentIdentifier+  = Repository RepositoryURL RepositoryName PackageName+  | GHC GHCComponent   deriving stock (Show, Eq) +hackage :: PackageName -> ComponentIdentifier+hackage =+  Repository+    (RepositoryURL [uri|https://hackage.haskell.org|])+    (RepositoryName "hackage")++newtype RepositoryURL+  = RepositoryURL {unRepositoryURL :: URI}+  deriving stock (Eq, Ord, Show)++newtype RepositoryName+  = RepositoryName {unRepositoryName :: Text}+  deriving stock (Eq, Ord, Show)+ -- Keep this list in sync with the 'ghcComponentFromText' below data GHCComponent = GHCCompiler | GHCi | GHCRTS | GHCPkg | RunGHC | IServ | HP2PS | HPC | HSC2HS | Haddock-  deriving stock (Show, Eq, Enum, Bounded)+  deriving stock (Show, Eq, Ord, Enum, Bounded)  ghcComponentToText :: GHCComponent -> Text ghcComponentToText c = case c of@@ -85,12 +119,13 @@ -- | An affected package (or package component).  An 'Advisory' must -- mention one or more packages. data Affected = Affected-  { affectedComponentIdentifier :: ComponentIdentifier-  , affectedCVSS :: CVSS.CVSS-  , affectedVersions :: [AffectedVersionRange]-  , affectedArchitectures :: Maybe [Architecture]-  , affectedOS :: Maybe [OS]-  , affectedDeclarations :: [(Text, VersionRange)]+  { affectedComponentIdentifier :: ComponentIdentifier,+    affectedCVSS :: CVSS.CVSS,+    affectedVersions :: [AffectedVersionRange],+    affectedArchitectures :: Maybe [Architecture],+    affectedOS :: Maybe [OS],+    affectedDeclarations :: [(Text, VersionRange)],+    affectedApi :: [AffectedApi]   }   deriving stock (Eq, Show) @@ -142,8 +177,53 @@   deriving stock (Eq, Ord)   deriving (Show) via Text +data AffectedApi = AffectedApi+  { affectedApiModule :: Text,+    affectedApiName :: Text+  }+  deriving stock (Eq, Show)+ data AffectedVersionRange = AffectedVersionRange   { affectedVersionRangeIntroduced :: Version,     affectedVersionRangeFixed :: Maybe Version   }   deriving stock (Eq, Show)++-- * Queries++-- | Check whether a component and a version is concerned by an advisory+--+-- Since @0.2.1.0@+isVersionAffectedBy :: ComponentIdentifier -> Version -> Advisory -> Bool+isVersionAffectedBy = isAffectedByHelper withinRange++-- | Check whether a component and a version range is concerned by an advisory+--+-- Since @0.2.1.0@+isVersionRangeAffectedBy :: ComponentIdentifier -> VersionRange -> Advisory -> Bool+isVersionRangeAffectedBy = isAffectedByHelper $+  \queryVersionRange affectedVersionRange ->+    isSomeVersion (affectedVersionRange `intersectVersionRanges` queryVersionRange)+  where+    isSomeVersion :: VersionRange -> Bool+    isSomeVersion range+      | [] <- asVersionIntervals range = False+      | otherwise = True++-- | Helper function for 'isVersionAffectedBy' and 'isVersionRangeAffectedBy'+isAffectedByHelper :: (a -> VersionRange -> Bool) -> ComponentIdentifier -> a -> Advisory -> Bool+isAffectedByHelper checkWithRange queryComponent queryVersionish =+  any checkAffected . advisoryAffected+  where+    checkAffected :: Affected -> Bool+    checkAffected affected =+      affectedComponentIdentifier affected == queryComponent && checkWithRange queryVersionish (fromAffected affected)++    fromAffected :: Affected -> VersionRange+    fromAffected = foldr (unionVersionRanges . fromAffectedVersionRange) noVersion . affectedVersions++    fromAffectedVersionRange :: AffectedVersionRange -> VersionRange+    fromAffectedVersionRange avr =+      intersectVersionRanges+        (orLaterVersion (affectedVersionRangeIntroduced avr))+        (maybe anyVersion earlierVersion (affectedVersionRangeFixed avr))
+ src/Security/Advisories/Core/OsvId.hs view
@@ -0,0 +1,106 @@+{-# LANGUAGE OverloadedStrings #-}++module Security.Advisories.Core.OsvId+  ( OsvId,+    parseOsvId,+    printOsvId,+    osvIdPrefix,+  )+where++import qualified Data.List as L+import Data.Maybe (listToMaybe)+import qualified Data.Set as Set+import Data.Text (Text)+import qualified Data.Text as T++newtype OsvId = OsvId Text+  deriving (Eq, Ord, Show)++knownPrefixes :: Set.Set Text+knownPrefixes =+  Set.fromList+    [ "ASB-A",+      "PUB-A",+      "ALSA",+      "ALBA",+      "ALEA",+      "ALPINE",+      "BELL",+      "BIT",+      "CGA",+      "CleanStart",+      "CURL",+      "CVE",+      "DEBIAN",+      "DHI",+      "DRUPAL",+      "DSA",+      "DLA",+      "DTSA",+      "ECHO",+      "EEF",+      "ELA",+      "GHSA",+      "GO",+      "GSD",+      "HSEC",+      "JLSEC",+      "KUBE",+      "LBSEC",+      "LSN",+      "MGASA",+      "MAL",+      "MINI",+      "OESA",+      "OSEC",+      "OSV",+      "PHSA",+      "PSF",+      "PYSEC",+      "RHSA",+      "RHBA",+      "RHEA",+      "RLSA",+      "RXSA",+      "RSEC",+      "ROOT",+      "RUSTSEC",+      "SUSE-SU",+      "SUSE-RU",+      "SUSE-FU",+      "SUSE-OU",+      "openSUSE-SU",+      "UBUNTU",+      "USN",+      "V8"+    ]++sortedPrefixes :: [Text]+sortedPrefixes = L.reverse . L.sortOn T.length $ Set.toList knownPrefixes++parseOsvId :: Text -> Maybe OsvId+parseOsvId t = do+  prefix <- findPrefix t+  let rest = T.drop (T.length prefix + 1) t+  guard (not (T.null rest))+  pure (OsvId t)+  where+    findPrefix txt =+      listToMaybe $+        filter (\p -> T.isPrefixOf (p <> "-") txt) sortedPrefixes++    guard True = Just ()+    guard False = Nothing++printOsvId :: OsvId -> Text+printOsvId (OsvId t) = t++osvIdPrefix :: OsvId -> Text+osvIdPrefix (OsvId t) = case findPrefix t of+  Just p -> p+  Nothing -> T.takeWhile (/= '-') t+  where+    findPrefix txt =+      listToMaybe $+        filter (\p -> T.isPrefixOf (p <> "-") txt) sortedPrefixes
test/Spec.hs view
@@ -1,10 +1,11 @@ module Main where  import Test.Tasty+import qualified Spec.QueriesSpec as QueriesSpec  main :: IO () main =   defaultMain $     testGroup       "Tests"-      []+      [QueriesSpec.spec]
+ test/Spec/QueriesSpec.hs view
@@ -0,0 +1,195 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Spec.QueriesSpec (spec) where++import Data.Bifunctor (first)+import Data.Either (fromRight)+import Data.Maybe (fromMaybe)+import qualified Data.Maybe as Maybe+import Data.Text (Text)+import qualified Data.Text as T+import Distribution.Parsec (eitherParsec)+import Distribution.Types.Version (alterVersion, version0)+import Distribution.Types.VersionRange (VersionRange, VersionRangeF (..), anyVersion, projectVersionRange)+import Security.Advisories.Core.Advisory+import Security.Advisories.Core.HsecId+import Security.Advisories.Core.OsvId (parseOsvId)+import Security.CVSS (parseCVSS)+import Test.Tasty+import Test.Tasty.HUnit++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 component 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 = Maybe.fromJust . traverse parseOsvId $ ["CVE-2022-1234"],+      advisoryRelated = Maybe.fromJust . traverse parseOsvId $ ["CVE-2022-5678", "CVE-2022-9012"],+      advisoryAffected =+        [ Affected+            { affectedComponentIdentifier = component,+              affectedCVSS = cvss,+              affectedVersions = mkAffectedVersions versionRange,+              affectedArchitectures = Nothing,+              affectedOS = Nothing,+              affectedDeclarations = [],+              affectedApi = []+            }+        ],+      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+          ]++component :: ComponentIdentifier+component = hackage $ mkPackageName "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)