module Security.Advisories.Queries
  ( listVersionAffectedBy
  , listVersionRangeAffectedBy
  , isVersionAffectedBy
  , isVersionRangeAffectedBy
  )
where

import Control.Monad.IO.Class (MonadIO)
import Data.Text (Text)
import Distribution.Types.Version (Version)
import Distribution.Types.VersionInterval (asVersionIntervals)
import Distribution.Types.VersionRange (VersionRange, anyVersion, earlierVersion, intersectVersionRanges, noVersion, orLaterVersion, unionVersionRanges, withinRange)
import Validation (Validation(..))

import Security.Advisories.Core.Advisory
import Security.Advisories.Filesystem
import Security.Advisories.Parse

-- | Check whether a package and a version is concerned by an advisory
isVersionAffectedBy :: Text -> Version -> Advisory -> Bool
isVersionAffectedBy :: Text -> Version -> Advisory -> Bool
isVersionAffectedBy = (Version -> VersionRange -> Bool)
-> Text -> Version -> Advisory -> Bool
forall a.
(a -> VersionRange -> Bool) -> Text -> a -> Advisory -> Bool
isAffectedByHelper Version -> VersionRange -> Bool
withinRange

-- | Check whether a package and a version range is concerned by an advisory
isVersionRangeAffectedBy :: Text -> VersionRange -> Advisory -> Bool
isVersionRangeAffectedBy :: Text -> VersionRange -> Advisory -> Bool
isVersionRangeAffectedBy = (VersionRange -> VersionRange -> Bool)
-> Text -> VersionRange -> Advisory -> Bool
forall a.
(a -> VersionRange -> Bool) -> Text -> a -> Advisory -> Bool
isAffectedByHelper ((VersionRange -> VersionRange -> Bool)
 -> Text -> VersionRange -> Advisory -> Bool)
-> (VersionRange -> VersionRange -> Bool)
-> Text
-> VersionRange
-> Advisory
-> Bool
forall a b. (a -> b) -> a -> b
$
  \VersionRange
queryVersionRange VersionRange
affectedVersionRange ->
    VersionRange -> Bool
isSomeVersion (VersionRange
affectedVersionRange VersionRange -> VersionRange -> VersionRange
`intersectVersionRanges` VersionRange
queryVersionRange)
  where
    isSomeVersion :: VersionRange -> Bool
    isSomeVersion :: VersionRange -> Bool
isSomeVersion VersionRange
range
      | [] <- VersionRange -> [VersionInterval]
asVersionIntervals VersionRange
range = Bool
False
      | Bool
otherwise = Bool
True

-- | Helper function for 'isVersionAffectedBy' and 'isVersionRangeAffectedBy'
isAffectedByHelper :: (a -> VersionRange -> Bool) -> Text -> a -> Advisory -> Bool
isAffectedByHelper :: forall a.
(a -> VersionRange -> Bool) -> Text -> a -> Advisory -> Bool
isAffectedByHelper a -> VersionRange -> Bool
checkWithRange Text
queryPackageName a
queryVersionish =
    (Affected -> Bool) -> [Affected] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any Affected -> Bool
checkAffected ([Affected] -> Bool)
-> (Advisory -> [Affected]) -> Advisory -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Advisory -> [Affected]
advisoryAffected
    where
      checkAffected :: Affected -> Bool
      checkAffected :: Affected -> Bool
checkAffected Affected
affected =
        Text
queryPackageName Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Affected -> Text
affectedPackage Affected
affected
          Bool -> Bool -> Bool
&& a -> VersionRange -> Bool
checkWithRange a
queryVersionish (Affected -> VersionRange
fromAffected Affected
affected)

      fromAffected :: Affected -> VersionRange
      fromAffected :: Affected -> VersionRange
fromAffected = (AffectedVersionRange -> VersionRange -> VersionRange)
-> VersionRange -> [AffectedVersionRange] -> VersionRange
forall a b. (a -> b -> b) -> b -> [a] -> b
forall (t :: * -> *) a b.
Foldable t =>
(a -> b -> b) -> b -> t a -> b
foldr (VersionRange -> VersionRange -> VersionRange
unionVersionRanges (VersionRange -> VersionRange -> VersionRange)
-> (AffectedVersionRange -> VersionRange)
-> AffectedVersionRange
-> VersionRange
-> VersionRange
forall b c a. (b -> c) -> (a -> b) -> a -> c
. AffectedVersionRange -> VersionRange
fromAffectedVersionRange) VersionRange
noVersion ([AffectedVersionRange] -> VersionRange)
-> (Affected -> [AffectedVersionRange]) -> Affected -> VersionRange
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Affected -> [AffectedVersionRange]
affectedVersions

      fromAffectedVersionRange :: AffectedVersionRange -> VersionRange
      fromAffectedVersionRange :: AffectedVersionRange -> VersionRange
fromAffectedVersionRange AffectedVersionRange
avr = VersionRange -> VersionRange -> VersionRange
intersectVersionRanges
        (Version -> VersionRange
orLaterVersion (AffectedVersionRange -> Version
affectedVersionRangeIntroduced AffectedVersionRange
avr))
        (VersionRange
-> (Version -> VersionRange) -> Maybe Version -> VersionRange
forall b a. b -> (a -> b) -> Maybe a -> b
maybe VersionRange
anyVersion Version -> VersionRange
earlierVersion (AffectedVersionRange -> Maybe Version
affectedVersionRangeFixed AffectedVersionRange
avr))

-- | List the advisories matching a package name and a version
listVersionAffectedBy :: MonadIO m => FilePath -> Text -> Version -> m (Validation [ParseAdvisoryError] [Advisory])
listVersionAffectedBy :: forall (m :: * -> *).
MonadIO m =>
FilePath
-> Text
-> Version
-> m (Validation [ParseAdvisoryError] [Advisory])
listVersionAffectedBy = (Text -> Version -> Advisory -> Bool)
-> FilePath
-> Text
-> Version
-> m (Validation [ParseAdvisoryError] [Advisory])
forall (m :: * -> *) a.
MonadIO m =>
(Text -> a -> Advisory -> Bool)
-> FilePath
-> Text
-> a
-> m (Validation [ParseAdvisoryError] [Advisory])
listAffectedByHelper Text -> Version -> Advisory -> Bool
isVersionAffectedBy

-- | List the advisories matching a package name and a version range
listVersionRangeAffectedBy :: MonadIO m => FilePath -> Text -> VersionRange -> m (Validation [ParseAdvisoryError] [Advisory])
listVersionRangeAffectedBy :: forall (m :: * -> *).
MonadIO m =>
FilePath
-> Text
-> VersionRange
-> m (Validation [ParseAdvisoryError] [Advisory])
listVersionRangeAffectedBy = (Text -> VersionRange -> Advisory -> Bool)
-> FilePath
-> Text
-> VersionRange
-> m (Validation [ParseAdvisoryError] [Advisory])
forall (m :: * -> *) a.
MonadIO m =>
(Text -> a -> Advisory -> Bool)
-> FilePath
-> Text
-> a
-> m (Validation [ParseAdvisoryError] [Advisory])
listAffectedByHelper Text -> VersionRange -> Advisory -> Bool
isVersionRangeAffectedBy

-- | Helper function for 'listVersionAffectedBy' and 'listVersionRangeAffectedBy'
listAffectedByHelper :: MonadIO m => (Text -> a -> Advisory -> Bool) -> FilePath -> Text -> a -> m (Validation [ParseAdvisoryError] [Advisory])
listAffectedByHelper :: forall (m :: * -> *) a.
MonadIO m =>
(Text -> a -> Advisory -> Bool)
-> FilePath
-> Text
-> a
-> m (Validation [ParseAdvisoryError] [Advisory])
listAffectedByHelper Text -> a -> Advisory -> Bool
checkAffectedBy FilePath
root Text
queryPackageName a
queryVersionish =
  ([Advisory] -> [Advisory])
-> Validation [ParseAdvisoryError] [Advisory]
-> Validation [ParseAdvisoryError] [Advisory]
forall a b.
(a -> b)
-> Validation [ParseAdvisoryError] a
-> Validation [ParseAdvisoryError] b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((Advisory -> Bool) -> [Advisory] -> [Advisory]
forall a. (a -> Bool) -> [a] -> [a]
filter (Text -> a -> Advisory -> Bool
checkAffectedBy Text
queryPackageName a
queryVersionish)) (Validation [ParseAdvisoryError] [Advisory]
 -> Validation [ParseAdvisoryError] [Advisory])
-> m (Validation [ParseAdvisoryError] [Advisory])
-> m (Validation [ParseAdvisoryError] [Advisory])
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$>
    FilePath -> m (Validation [ParseAdvisoryError] [Advisory])
forall (m :: * -> *).
MonadIO m =>
FilePath -> m (Validation [ParseAdvisoryError] [Advisory])
listAdvisories FilePath
root