packages feed

hsec-core-0.5.0.0: src/Security/Advisories/Core/Advisory.hs

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

module Security.Advisories.Core.Advisory
  ( Advisory (..),

    -- * Supporting types
    Affected (..),
    CAPEC (..),
    CWE (..),
    Architecture (..),
    AffectedVersionRange (..),
    OS (..),
    Keyword (..),
    AffectedApi (..),
    ComponentIdentifier (..),
    GHCComponent (..),
    RepositoryURL (..),
    RepositoryName (..),
    PackageName,
    mkPackageName,
    unPackageName,
    ghcComponentToText,
    ghcComponentFromText,
    hackage,

    -- * Queries
    isVersionAffectedBy,
    isVersionRangeAffectedBy,
  )
where

import Data.Text (Text)
import Data.Time (UTCTime)
import Distribution.Types.PackageName (PackageName, mkPackageName, unPackageName)
import Distribution.Types.Version (Version)
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 :: [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
  = 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, Ord, Enum, Bounded)

ghcComponentToText :: GHCComponent -> Text
ghcComponentToText c = case c of
  GHCCompiler -> "ghc"
  GHCi -> "ghci"
  GHCRTS -> "rts"
  GHCPkg -> "ghc-pkg"
  RunGHC -> "runghc"
  IServ -> "ghc-iserv"
  HP2PS -> "hp2ps"
  HPC -> "hpc"
  HSC2HS -> "hsc2hs"
  Haddock -> "haddock"

ghcComponentFromText :: Text -> Maybe GHCComponent
ghcComponentFromText c = case c of
  "ghc" -> Just GHCCompiler
  "ghci" -> Just GHCi
  "rts" -> Just GHCRTS
  "ghc-pkg" -> Just GHCPkg
  "runghc" -> Just RunGHC
  "ghc-iserv" -> Just IServ
  "hp2ps" -> Just HP2PS
  "hpc" -> Just HPC
  "hsc2hs" -> Just HSC2HS
  "haddock" -> Just Haddock
  _ -> Nothing

-- | 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)],
    affectedApi :: [AffectedApi]
  }
  deriving stock (Eq, Show)

newtype CAPEC = CAPEC {unCAPEC :: Integer}
  deriving stock (Eq, Show)

newtype CWE = CWE {unCWE :: Integer}
  deriving stock (Eq, Show)

data Architecture
  = AArch64
  | Alpha
  | Arm
  | HPPA
  | HPPA1_1
  | I386
  | IA64
  | M68K
  | MIPS
  | MIPSEB
  | MIPSEL
  | NIOS2
  | PowerPC
  | PowerPC64
  | PowerPC64LE
  | RISCV32
  | RISCV64
  | RS6000
  | S390
  | S390X
  | SH4
  | SPARC
  | SPARC64
  | VAX
  | X86_64
  deriving stock (Eq, Show, Enum, Bounded)

data OS
  = Windows
  | MacOS
  | Linux
  | FreeBSD
  | Android
  | NetBSD
  | OpenBSD
  deriving stock (Eq, Show, Enum, Bounded)

newtype Keyword = Keyword {unKeyword :: Text}
  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))