module Ecluse.Core.Cve (
CveDb (..),
openCveDb,
CveDbRejected (..),
CveLookup (..),
AdvisoryRange (..),
CveQueryFault (..),
PackageAdvisories,
packageAdvisories,
keepAdvisories,
affecting,
fixedAt,
MissingScorePolicy (..),
scoreAtLeast,
) where
import Data.Map.Strict qualified as Map
import UnliftIO.Exception (catch, catchAny, onException, throwIO)
import Ecluse.Core.Cve.Internal (AdvisoryRange (..), CveDbRejected (..), advisoriesQuery, coveredNamesQuery, openHardenedConnection, provenanceQuery)
import Ecluse.Core.Ecosystem (Ecosystem)
import Ecluse.Core.Osv.Provenance (AdvisoryProvenance, decodeProvenance)
import Ecluse.Core.Osv.Schema (EpssRequirement)
import Ecluse.Core.Osv.Types (UpperBound (..))
import Ecluse.Core.Version (Version, VersionKey, parseVersionKey, renderVersion, versionKeyIn)
import Database.SQLite.Simple (Connection, SQLError, close)
data CveLookup = CveLookup
{ CveLookup -> Text -> IO [AdvisoryRange]
cveAdvisoriesFor :: Text -> IO [AdvisoryRange]
, CveLookup -> IO [Text]
cveCoveredNames :: IO [Text]
}
data CveQueryFault = CveQueryFault
{ CveQueryFault -> Text
cqfQuery :: Text
, CveQueryFault -> Text
cqfDetail :: Text
}
deriving stock (CveQueryFault -> CveQueryFault -> Bool
(CveQueryFault -> CveQueryFault -> Bool)
-> (CveQueryFault -> CveQueryFault -> Bool) -> Eq CveQueryFault
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CveQueryFault -> CveQueryFault -> Bool
== :: CveQueryFault -> CveQueryFault -> Bool
$c/= :: CveQueryFault -> CveQueryFault -> Bool
/= :: CveQueryFault -> CveQueryFault -> Bool
Eq, Int -> CveQueryFault -> ShowS
[CveQueryFault] -> ShowS
CveQueryFault -> String
(Int -> CveQueryFault -> ShowS)
-> (CveQueryFault -> String)
-> ([CveQueryFault] -> ShowS)
-> Show CveQueryFault
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CveQueryFault -> ShowS
showsPrec :: Int -> CveQueryFault -> ShowS
$cshow :: CveQueryFault -> String
show :: CveQueryFault -> String
$cshowList :: [CveQueryFault] -> ShowS
showList :: [CveQueryFault] -> ShowS
Show)
instance Exception CveQueryFault
data CveDb = CveDb
{ CveDb -> CveLookup
cveDbLookup :: CveLookup
, CveDb -> IO ()
cveDbClose :: IO ()
, CveDb -> [(Text, Text)]
cveDbMeta :: [(Text, Text)]
, CveDb -> AdvisoryProvenance
cveDbProvenance :: AdvisoryProvenance
}
openCveDb :: Ecosystem -> EpssRequirement -> FilePath -> IO (Either CveDbRejected CveDb)
openCveDb :: Ecosystem
-> EpssRequirement -> String -> IO (Either CveDbRejected CveDb)
openCveDb Ecosystem
eco EpssRequirement
epssRequirement String
dbFile =
Ecosystem
-> EpssRequirement
-> String
-> IO (Either CveDbRejected Connection)
openHardenedConnection Ecosystem
eco EpssRequirement
epssRequirement String
dbFile IO (Either CveDbRejected Connection)
-> (Either CveDbRejected Connection
-> IO (Either CveDbRejected CveDb))
-> IO (Either CveDbRejected CveDb)
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Left CveDbRejected
rejection -> Either CveDbRejected CveDb -> IO (Either CveDbRejected CveDb)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (CveDbRejected -> Either CveDbRejected CveDb
forall a b. a -> Either a b
Left CveDbRejected
rejection)
Right Connection
conn -> do
meta <- Connection -> IO [(Text, Text)]
provenanceQuery Connection
conn IO [(Text, Text)] -> IO () -> IO [(Text, Text)]
forall (m :: * -> *) a b. MonadUnliftIO m => m a -> m b -> m a
`onException` Connection -> IO ()
close Connection
conn
pure (Right (mkCveDb conn meta))
mkCveDb :: Connection -> [(Text, Text)] -> CveDb
mkCveDb :: Connection -> [(Text, Text)] -> CveDb
mkCveDb Connection
conn [(Text, Text)]
meta =
CveDb
{ cveDbLookup :: CveLookup
cveDbLookup =
CveLookup
{ cveAdvisoriesFor :: Text -> IO [AdvisoryRange]
cveAdvisoriesFor = Text -> IO [AdvisoryRange] -> IO [AdvisoryRange]
forall a. Text -> IO a -> IO a
taggedQuery Text
"advisories-for" (IO [AdvisoryRange] -> IO [AdvisoryRange])
-> (Text -> IO [AdvisoryRange]) -> Text -> IO [AdvisoryRange]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Connection -> Text -> IO [AdvisoryRange]
advisoriesQuery Connection
conn
, cveCoveredNames :: IO [Text]
cveCoveredNames = Text -> IO [Text] -> IO [Text]
forall a. Text -> IO a -> IO a
taggedQuery Text
"covered-names" (Connection -> IO [Text]
coveredNamesQuery Connection
conn)
}
, cveDbClose :: IO ()
cveDbClose = Connection -> IO ()
close Connection
conn IO () -> (SomeException -> IO ()) -> IO ()
forall (m :: * -> *) a.
MonadUnliftIO m =>
m a -> (SomeException -> m a) -> m a
`catchAny` IO () -> SomeException -> IO ()
forall a b. a -> b -> a
const IO ()
forall (f :: * -> *). Applicative f => f ()
pass
, cveDbMeta :: [(Text, Text)]
cveDbMeta = [(Text, Text)]
meta
, cveDbProvenance :: AdvisoryProvenance
cveDbProvenance = [(Text, Text)] -> AdvisoryProvenance
decodeProvenance [(Text, Text)]
meta
}
taggedQuery :: Text -> IO a -> IO a
taggedQuery :: forall a. Text -> IO a -> IO a
taggedQuery Text
tag IO a
act = IO a
act IO a -> (SQLError -> IO a) -> IO a
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
m a -> (e -> m a) -> m a
`catch` \(SQLError
err :: SQLError) -> CveQueryFault -> IO a
forall (m :: * -> *) e a. (MonadIO m, Exception e) => e -> m a
throwIO (Text -> Text -> CveQueryFault
CveQueryFault Text
tag (SQLError -> Text
forall b a. (Show a, IsString b) => a -> b
show SQLError
err))
data PackageAdvisories = PackageAdvisories
{ PackageAdvisories -> Ecosystem
paEcosystem :: Ecosystem
, PackageAdvisories -> [Segment]
paSegments :: [Segment]
,
PackageAdvisories -> Map Text [AdvisoryRange]
paFixes :: ~(Map Text [AdvisoryRange])
}
data Segment = Segment
{ Segment -> AdvisoryRange
segRange :: AdvisoryRange
,
Segment -> SegmentBounds
segBounds :: ~SegmentBounds
}
data SegmentBounds
=
OnlyText Text
|
Ordered (Maybe VersionKey) UpperKey
data UpperKey = Below VersionKey | AtMost VersionKey | NoUpper
packageAdvisories :: Ecosystem -> [AdvisoryRange] -> PackageAdvisories
packageAdvisories :: Ecosystem -> [AdvisoryRange] -> PackageAdvisories
packageAdvisories Ecosystem
eco = Ecosystem -> [Segment] -> PackageAdvisories
fromSegments Ecosystem
eco ([Segment] -> PackageAdvisories)
-> ([AdvisoryRange] -> [Segment])
-> [AdvisoryRange]
-> PackageAdvisories
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (AdvisoryRange -> Segment) -> [AdvisoryRange] -> [Segment]
forall a b. (a -> b) -> [a] -> [b]
map (\AdvisoryRange
ar -> AdvisoryRange -> SegmentBounds -> Segment
Segment AdvisoryRange
ar (Ecosystem -> AdvisoryRange -> SegmentBounds
segmentBounds Ecosystem
eco AdvisoryRange
ar))
keepAdvisories :: (AdvisoryRange -> Bool) -> PackageAdvisories -> PackageAdvisories
keepAdvisories :: (AdvisoryRange -> Bool) -> PackageAdvisories -> PackageAdvisories
keepAdvisories AdvisoryRange -> Bool
keep PackageAdvisories
advisories = Ecosystem -> [Segment] -> PackageAdvisories
fromSegments (PackageAdvisories -> Ecosystem
paEcosystem PackageAdvisories
advisories) ((Segment -> Bool) -> [Segment] -> [Segment]
forall a. (a -> Bool) -> [a] -> [a]
filter (AdvisoryRange -> Bool
keep (AdvisoryRange -> Bool)
-> (Segment -> AdvisoryRange) -> Segment -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Segment -> AdvisoryRange
segRange) (PackageAdvisories -> [Segment]
paSegments PackageAdvisories
advisories))
fromSegments :: Ecosystem -> [Segment] -> PackageAdvisories
fromSegments :: Ecosystem -> [Segment] -> PackageAdvisories
fromSegments Ecosystem
eco [Segment]
segments =
PackageAdvisories
{ paEcosystem :: Ecosystem
paEcosystem = Ecosystem
eco
, paSegments :: [Segment]
paSegments = [Segment]
segments
, paFixes :: Map Text [AdvisoryRange]
paFixes = ([AdvisoryRange] -> [AdvisoryRange] -> [AdvisoryRange])
-> [(Text, [AdvisoryRange])] -> Map Text [AdvisoryRange]
forall k a. Ord k => (a -> a -> a) -> [(k, a)] -> Map k a
Map.fromListWith (([AdvisoryRange] -> [AdvisoryRange] -> [AdvisoryRange])
-> [AdvisoryRange] -> [AdvisoryRange] -> [AdvisoryRange]
forall a b c. (a -> b -> c) -> b -> a -> c
flip [AdvisoryRange] -> [AdvisoryRange] -> [AdvisoryRange]
forall a. Semigroup a => a -> a -> a
(<>)) [(Text
fixed, [Segment -> AdvisoryRange
segRange Segment
s]) | Segment
s <- [Segment]
segments, FixedBefore Text
fixed <- [AdvisoryRange -> UpperBound
arUpperBound (Segment -> AdvisoryRange
segRange Segment
s)]]
}
fixedAt :: PackageAdvisories -> Version -> [AdvisoryRange]
fixedAt :: PackageAdvisories -> Version -> [AdvisoryRange]
fixedAt PackageAdvisories
advisories Version
version = [AdvisoryRange]
-> Text -> Map Text [AdvisoryRange] -> [AdvisoryRange]
forall k a. Ord k => a -> k -> Map k a -> a
Map.findWithDefault [] (Version -> Text
renderVersion Version
version) (PackageAdvisories -> Map Text [AdvisoryRange]
paFixes PackageAdvisories
advisories)
affecting :: PackageAdvisories -> Version -> [AdvisoryRange]
affecting :: PackageAdvisories -> Version -> [AdvisoryRange]
affecting PackageAdvisories
advisories Version
version = [Segment -> AdvisoryRange
segRange Segment
s | Segment
s <- PackageAdvisories -> [Segment]
paSegments PackageAdvisories
advisories, SegmentBounds -> Bool
holds (Segment -> SegmentBounds
segBounds Segment
s)]
where
key :: Maybe VersionKey
key = Ecosystem -> Version -> Maybe VersionKey
versionKeyIn (PackageAdvisories -> Ecosystem
paEcosystem PackageAdvisories
advisories) Version
version
holds :: SegmentBounds -> Bool
holds = \case
OnlyText Text
only -> Version -> Text
renderVersion Version
version Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
only
Ordered Maybe VersionKey
lower UpperKey
upper -> Bool -> (VersionKey -> Bool) -> Maybe VersionKey -> Bool
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Bool
True (\VersionKey
k -> VersionKey -> Maybe VersionKey -> Bool
atOrAbove VersionKey
k Maybe VersionKey
lower Bool -> Bool -> Bool
&& VersionKey -> UpperKey -> Bool
withinUpper VersionKey
k UpperKey
upper) Maybe VersionKey
key
atOrAbove :: VersionKey -> Maybe VersionKey -> Bool
atOrAbove :: VersionKey -> Maybe VersionKey -> Bool
atOrAbove VersionKey
k = Bool -> (VersionKey -> Bool) -> Maybe VersionKey -> Bool
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Bool
True (VersionKey
k VersionKey -> VersionKey -> Bool
forall a. Ord a => a -> a -> Bool
>=)
withinUpper :: VersionKey -> UpperKey -> Bool
withinUpper :: VersionKey -> UpperKey -> Bool
withinUpper VersionKey
k = \case
Below VersionKey
fixed -> VersionKey
k VersionKey -> VersionKey -> Bool
forall a. Ord a => a -> a -> Bool
< VersionKey
fixed
AtMost VersionKey
lastAffected -> VersionKey
k VersionKey -> VersionKey -> Bool
forall a. Ord a => a -> a -> Bool
<= VersionKey
lastAffected
UpperKey
NoUpper -> Bool
True
segmentBounds :: Ecosystem -> AdvisoryRange -> SegmentBounds
segmentBounds :: Ecosystem -> AdvisoryRange -> SegmentBounds
segmentBounds Ecosystem
eco AdvisoryRange
ar = case (AdvisoryRange -> Maybe Text
arIntroduced AdvisoryRange
ar, AdvisoryRange -> UpperBound
arUpperBound AdvisoryRange
ar) of
(Just Text
introduced, LastAffected Text
lastAffected)
| Text
introduced Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
lastAffected -> SegmentBounds
-> (VersionKey -> SegmentBounds)
-> Maybe VersionKey
-> SegmentBounds
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (Text -> SegmentBounds
OnlyText Text
introduced) (\VersionKey
k -> Maybe VersionKey -> UpperKey -> SegmentBounds
Ordered (VersionKey -> Maybe VersionKey
forall a. a -> Maybe a
Just VersionKey
k) (VersionKey -> UpperKey
AtMost VersionKey
k)) Maybe VersionKey
introducedKey
(Maybe Text
_, UpperBound
upper) -> Maybe VersionKey -> UpperKey -> SegmentBounds
Ordered Maybe VersionKey
introducedKey (UpperBound -> UpperKey
upperKey UpperBound
upper)
where
keyOf :: Text -> Maybe VersionKey
keyOf = Either VersionError VersionKey -> Maybe VersionKey
forall l r. Either l r -> Maybe r
rightToMaybe (Either VersionError VersionKey -> Maybe VersionKey)
-> (Text -> Either VersionError VersionKey)
-> Text
-> Maybe VersionKey
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Ecosystem -> Text -> Either VersionError VersionKey
parseVersionKey Ecosystem
eco
introducedKey :: Maybe VersionKey
introducedKey = Text -> Maybe VersionKey
keyOf (Text -> Maybe VersionKey) -> Maybe Text -> Maybe VersionKey
forall (m :: * -> *) a b. Monad m => (a -> m b) -> m a -> m b
=<< AdvisoryRange -> Maybe Text
arIntroduced AdvisoryRange
ar
upperKey :: UpperBound -> UpperKey
upperKey = \case
FixedBefore Text
fixed -> UpperKey
-> (VersionKey -> UpperKey) -> Maybe VersionKey -> UpperKey
forall b a. b -> (a -> b) -> Maybe a -> b
maybe UpperKey
NoUpper VersionKey -> UpperKey
Below (Text -> Maybe VersionKey
keyOf Text
fixed)
LastAffected Text
lastAffected -> UpperKey
-> (VersionKey -> UpperKey) -> Maybe VersionKey -> UpperKey
forall b a. b -> (a -> b) -> Maybe a -> b
maybe UpperKey
NoUpper VersionKey -> UpperKey
AtMost (Text -> Maybe VersionKey
keyOf Text
lastAffected)
UpperBound
Unbounded -> UpperKey
NoUpper
data MissingScorePolicy
=
DenyMissingScore
|
AbstainMissingScore
scoreAtLeast :: MissingScorePolicy -> Double -> Maybe Double -> Bool
scoreAtLeast :: MissingScorePolicy -> Double -> Maybe Double -> Bool
scoreAtLeast MissingScorePolicy
missing Double
threshold = Bool -> (Double -> Bool) -> Maybe Double -> Bool
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Bool
absent (Double -> Double -> Bool
forall a. Ord a => a -> a -> Bool
>= Double
threshold)
where
absent :: Bool
absent = case MissingScorePolicy
missing of
MissingScorePolicy
DenyMissingScore -> Bool
True
MissingScorePolicy
AbstainMissingScore -> Bool
False