module Ecluse.Core.Cve.Internal (
AdvisoryRange (..),
CveDbRejected (..),
openHardenedConnection,
probeQuery,
advisoriesQuery,
provenanceQuery,
) where
import Database.SQLite.Simple (Connection, Only (..), SQLError, close, execute_, open, query, query_)
import UnliftIO.Exception (onException, try)
import Ecluse.Core.Ecosystem (Ecosystem, ecosystemName)
import Ecluse.Core.Osv.Schema (ColumnSpec (..), MetaKey (MetaEcosystem), TableSpec (..), osvSchemaEpoch, osvTableSpecs, renderMetaKey)
data AdvisoryRange = AdvisoryRange
{ AdvisoryRange -> Text
arCveId :: Text
, AdvisoryRange -> Maybe Double
arSeverity :: Maybe Double
, AdvisoryRange -> Maybe Text
arIntroduced :: Maybe Text
, AdvisoryRange -> Maybe Text
arFixed :: Maybe Text
, AdvisoryRange -> Maybe Text
arLastAffected :: Maybe Text
}
deriving stock (AdvisoryRange -> AdvisoryRange -> Bool
(AdvisoryRange -> AdvisoryRange -> Bool)
-> (AdvisoryRange -> AdvisoryRange -> Bool) -> Eq AdvisoryRange
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: AdvisoryRange -> AdvisoryRange -> Bool
== :: AdvisoryRange -> AdvisoryRange -> Bool
$c/= :: AdvisoryRange -> AdvisoryRange -> Bool
/= :: AdvisoryRange -> AdvisoryRange -> Bool
Eq, Int -> AdvisoryRange -> ShowS
[AdvisoryRange] -> ShowS
AdvisoryRange -> String
(Int -> AdvisoryRange -> ShowS)
-> (AdvisoryRange -> String)
-> ([AdvisoryRange] -> ShowS)
-> Show AdvisoryRange
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> AdvisoryRange -> ShowS
showsPrec :: Int -> AdvisoryRange -> ShowS
$cshow :: AdvisoryRange -> String
show :: AdvisoryRange -> String
$cshowList :: [AdvisoryRange] -> ShowS
showList :: [AdvisoryRange] -> ShowS
Show)
data CveDbRejected
=
CveDbWrongEpoch Int
|
CveDbIntegrityFailed [Text]
|
CveDbSchemaNonConformant Text
|
CveDbEcosystemMismatch (Maybe Text)
deriving stock (CveDbRejected -> CveDbRejected -> Bool
(CveDbRejected -> CveDbRejected -> Bool)
-> (CveDbRejected -> CveDbRejected -> Bool) -> Eq CveDbRejected
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CveDbRejected -> CveDbRejected -> Bool
== :: CveDbRejected -> CveDbRejected -> Bool
$c/= :: CveDbRejected -> CveDbRejected -> Bool
/= :: CveDbRejected -> CveDbRejected -> Bool
Eq, Int -> CveDbRejected -> ShowS
[CveDbRejected] -> ShowS
CveDbRejected -> String
(Int -> CveDbRejected -> ShowS)
-> (CveDbRejected -> String)
-> ([CveDbRejected] -> ShowS)
-> Show CveDbRejected
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CveDbRejected -> ShowS
showsPrec :: Int -> CveDbRejected -> ShowS
$cshow :: CveDbRejected -> String
show :: CveDbRejected -> String
$cshowList :: [CveDbRejected] -> ShowS
showList :: [CveDbRejected] -> ShowS
Show)
openHardenedConnection :: Ecosystem -> FilePath -> IO (Either CveDbRejected Connection)
openHardenedConnection :: Ecosystem -> String -> IO (Either CveDbRejected Connection)
openHardenedConnection Ecosystem
eco String
dbFile = do
conn <- String -> IO Connection
open String
dbFile
let hardenAndAccept = do
Connection -> Query -> IO ()
execute_ Connection
conn Query
"PRAGMA trusted_schema = OFF"
Connection -> Query -> IO ()
execute_ Connection
conn Query
"PRAGMA query_only = ON"
Connection -> Query -> IO ()
execute_ Connection
conn Query
"PRAGMA cell_size_check = ON"
Connection -> Query -> IO ()
execute_ Connection
conn Query
"PRAGMA mmap_size = 0"
Ecosystem -> Connection -> IO (Either CveDbRejected ())
acceptArtifact Ecosystem
eco Connection
conn
accepted <- hardenAndAccept `onException` close conn
case accepted of
Left CveDbRejected
rejection -> do
Connection -> IO ()
close Connection
conn
Either CveDbRejected Connection
-> IO (Either CveDbRejected Connection)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (CveDbRejected -> Either CveDbRejected Connection
forall a b. a -> Either a b
Left CveDbRejected
rejection)
Right () -> Either CveDbRejected Connection
-> IO (Either CveDbRejected Connection)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Connection -> Either CveDbRejected Connection
forall a b. b -> Either a b
Right Connection
conn)
acceptArtifact :: Ecosystem -> Connection -> IO (Either CveDbRejected ())
acceptArtifact :: Ecosystem -> Connection -> IO (Either CveDbRejected ())
acceptArtifact Ecosystem
eco Connection
conn = ExceptT CveDbRejected IO () -> IO (Either CveDbRejected ())
forall e (m :: * -> *) a. ExceptT e m a -> m (Either e a)
runExceptT (ExceptT CveDbRejected IO () -> IO (Either CveDbRejected ()))
-> ExceptT CveDbRejected IO () -> IO (Either CveDbRejected ())
forall a b. (a -> b) -> a -> b
$ do
IO (Either CveDbRejected ()) -> ExceptT CveDbRejected IO ()
forall e (m :: * -> *) a. m (Either e a) -> ExceptT e m a
ExceptT (Connection -> IO (Either CveDbRejected ())
checkEpochStamp Connection
conn)
IO (Either CveDbRejected ()) -> ExceptT CveDbRejected IO ()
forall e (m :: * -> *) a. m (Either e a) -> ExceptT e m a
ExceptT (Connection -> IO (Either CveDbRejected ())
checkIntegrity Connection
conn)
(TableSpec -> ExceptT CveDbRejected IO ())
-> [TableSpec] -> ExceptT CveDbRejected IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ (IO (Either CveDbRejected ()) -> ExceptT CveDbRejected IO ()
forall e (m :: * -> *) a. m (Either e a) -> ExceptT e m a
ExceptT (IO (Either CveDbRejected ()) -> ExceptT CveDbRejected IO ())
-> (TableSpec -> IO (Either CveDbRejected ()))
-> TableSpec
-> ExceptT CveDbRejected IO ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Connection -> TableSpec -> IO (Either CveDbRejected ())
checkTableConformance Connection
conn) [TableSpec]
osvTableSpecs
IO (Either CveDbRejected ()) -> ExceptT CveDbRejected IO ()
forall e (m :: * -> *) a. m (Either e a) -> ExceptT e m a
ExceptT (Ecosystem -> Connection -> IO (Either CveDbRejected ())
checkMetaEcosystem Ecosystem
eco Connection
conn)
checkEpochStamp :: Connection -> IO (Either CveDbRejected ())
checkEpochStamp :: Connection -> IO (Either CveDbRejected ())
checkEpochStamp Connection
conn = do
stamped <- IO [Only Int] -> IO (Either SQLError [Only Int])
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
m a -> m (Either e a)
try (Connection -> Query -> IO [Only Int]
forall r. FromRow r => Connection -> Query -> IO [r]
query_ Connection
conn Query
"PRAGMA user_version") :: IO (Either SQLError [Only Int])
pure $ case stamped of
Left SQLError
err -> CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left ([Text] -> CveDbRejected
CveDbIntegrityFailed [Text
"not a valid SQLite database: " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> SQLError -> Text
forall b a. (Show a, IsString b) => a -> b
show SQLError
err])
Right [Only Int]
rows -> case (Only Int -> Int) -> [Only Int] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map Only Int -> Int
forall a. Only a -> a
fromOnly [Only Int]
rows of
[Int
epoch]
| Int
epoch Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
osvSchemaEpoch -> () -> Either CveDbRejected ()
forall a b. b -> Either a b
Right ()
| Bool
otherwise -> CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left (Int -> CveDbRejected
CveDbWrongEpoch Int
epoch)
[Int]
_ -> CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left (Int -> CveDbRejected
CveDbWrongEpoch Int
0)
checkIntegrity :: Connection -> IO (Either CveDbRejected ())
checkIntegrity :: Connection -> IO (Either CveDbRejected ())
checkIntegrity Connection
conn = do
result <- IO [Only Text] -> IO (Either SQLError [Only Text])
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
m a -> m (Either e a)
try (Connection -> Query -> IO [Only Text]
forall r. FromRow r => Connection -> Query -> IO [r]
query_ Connection
conn Query
"PRAGMA quick_check") :: IO (Either SQLError [Only Text])
pure $ case result of
Left SQLError
err -> CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left ([Text] -> CveDbRejected
CveDbIntegrityFailed [SQLError -> Text
forall b a. (Show a, IsString b) => a -> b
show SQLError
err])
Right [Only Text]
report -> case (Only Text -> Text) -> [Only Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Only Text -> Text
forall a. Only a -> a
fromOnly [Only Text]
report of
[Text
"ok"] -> () -> Either CveDbRejected ()
forall a b. b -> Either a b
Right ()
[Text]
problems -> CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left ([Text] -> CveDbRejected
CveDbIntegrityFailed [Text]
problems)
checkTableConformance :: Connection -> TableSpec -> IO (Either CveDbRejected ())
checkTableConformance :: Connection -> TableSpec -> IO (Either CveDbRejected ())
checkTableConformance Connection
conn TableSpec
spec = do
listed <- IO [(Maybe Text, Maybe Int)]
-> IO (Either SQLError [(Maybe Text, Maybe Int)])
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
m a -> m (Either e a)
try (Connection -> Query -> Only Text -> IO [(Maybe Text, Maybe Int)]
forall q r.
(ToRow q, FromRow r) =>
Connection -> Query -> q -> IO [r]
query Connection
conn Query
"SELECT type, strict FROM pragma_table_list WHERE name = ?" (Text -> Only Text
forall a. a -> Only a
Only (TableSpec -> Text
tableName TableSpec
spec))) :: IO (Either SQLError [(Maybe Text, Maybe Int)])
columns <- try (query conn "SELECT name, type, \"notnull\" FROM pragma_table_xinfo(?)" (Only (tableName spec))) :: IO (Either SQLError [(Maybe Text, Maybe Text, Maybe Int)])
pure $ case (listed, columns) of
(Right [(Just Text
"table", Just Int
1)], Right [(Maybe Text, Maybe Text, Maybe Int)]
cols)
| (ColumnSpec -> Bool) -> [ColumnSpec] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all ([(Maybe Text, Maybe Text, Maybe Int)] -> ColumnSpec -> Bool
hasConformingColumn [(Maybe Text, Maybe Text, Maybe Int)]
cols) (TableSpec -> [ColumnSpec]
tableColumns TableSpec
spec) -> () -> Either CveDbRejected ()
forall a b. b -> Either a b
Right ()
(Either SQLError [(Maybe Text, Maybe Int)],
Either SQLError [(Maybe Text, Maybe Text, Maybe Int)])
_ -> CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left (Text -> CveDbRejected
CveDbSchemaNonConformant (TableSpec -> Text
tableName TableSpec
spec))
hasConformingColumn :: [(Maybe Text, Maybe Text, Maybe Int)] -> ColumnSpec -> Bool
hasConformingColumn :: [(Maybe Text, Maybe Text, Maybe Int)] -> ColumnSpec -> Bool
hasConformingColumn [(Maybe Text, Maybe Text, Maybe Int)]
cols ColumnSpec
spec = ((Maybe Text, Maybe Text, Maybe Int) -> Bool)
-> [(Maybe Text, Maybe Text, Maybe Int)] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Maybe Text, Maybe Text, Maybe Int) -> Bool
forall {a}.
(Eq a, Num a) =>
(Maybe Text, Maybe Text, Maybe a) -> Bool
conforms [(Maybe Text, Maybe Text, Maybe Int)]
cols
where
conforms :: (Maybe Text, Maybe Text, Maybe a) -> Bool
conforms (Maybe Text
name, Maybe Text
declaredType, Maybe a
notnull) =
Maybe Text
name Maybe Text -> Maybe Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text -> Maybe Text
forall a. a -> Maybe a
Just (ColumnSpec -> Text
colName ColumnSpec
spec)
Bool -> Bool -> Bool
&& Maybe Text
declaredType Maybe Text -> Maybe Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text -> Maybe Text
forall a. a -> Maybe a
Just (ColumnSpec -> Text
colDeclaredType ColumnSpec
spec)
Bool -> Bool -> Bool
&& (Bool -> Bool
not (ColumnSpec -> Bool
colNotNull ColumnSpec
spec) Bool -> Bool -> Bool
|| Maybe a
notnull Maybe a -> Maybe a -> Bool
forall a. Eq a => a -> a -> Bool
== a -> Maybe a
forall a. a -> Maybe a
Just a
1)
checkMetaEcosystem :: Ecosystem -> Connection -> IO (Either CveDbRejected ())
checkMetaEcosystem :: Ecosystem -> Connection -> IO (Either CveDbRejected ())
checkMetaEcosystem Ecosystem
eco Connection
conn = do
named <- IO [Only Text] -> IO (Either SQLError [Only Text])
forall (m :: * -> *) e a.
(MonadUnliftIO m, Exception e) =>
m a -> m (Either e a)
try (Connection -> Query -> Only Text -> IO [Only Text]
forall q r.
(ToRow q, FromRow r) =>
Connection -> Query -> q -> IO [r]
query Connection
conn Query
"SELECT value FROM meta WHERE key = ?" (Text -> Only Text
forall a. a -> Only a
Only (MetaKey -> Text
renderMetaKey MetaKey
MetaEcosystem))) :: IO (Either SQLError [Only Text])
pure $ case named of
Left SQLError
_ -> CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left (Maybe Text -> CveDbRejected
CveDbEcosystemMismatch Maybe Text
forall a. Maybe a
Nothing)
Right [Only Text]
rows ->
let found :: Maybe Text
found = Only Text -> Text
forall a. Only a -> a
fromOnly (Only Text -> Text) -> Maybe (Only Text) -> Maybe Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Only Text] -> Maybe (Only Text)
forall a. [a] -> Maybe a
listToMaybe [Only Text]
rows
in if Maybe Text
found Maybe Text -> Maybe Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text -> Maybe Text
forall a. a -> Maybe a
Just (Ecosystem -> Text
ecosystemName Ecosystem
eco)
then () -> Either CveDbRejected ()
forall a b. b -> Either a b
Right ()
else CveDbRejected -> Either CveDbRejected ()
forall a b. a -> Either a b
Left (Maybe Text -> CveDbRejected
CveDbEcosystemMismatch Maybe Text
found)
probeQuery :: Connection -> Text -> Text -> IO Bool
probeQuery :: Connection -> Text -> Text -> IO Bool
probeQuery Connection
conn Text
name Text
version = do
hits <- Connection -> Query -> (Text, Text) -> IO [Only Int]
forall q r.
(ToRow q, FromRow r) =>
Connection -> Query -> q -> IO [r]
query Connection
conn Query
"SELECT 1 FROM package_vulnerability_ranges WHERE package_name = ? AND fixed_version = ? LIMIT 1" (Text
name, Text
version) :: IO [Only Int]
pure (not (null hits))
advisoriesQuery :: Connection -> Text -> IO [AdvisoryRange]
advisoriesQuery :: Connection -> Text -> IO [AdvisoryRange]
advisoriesQuery Connection
conn Text
name = do
rows <- Connection
-> Query
-> Only Text
-> IO [(Text, Maybe Text, Maybe Text, Maybe Text, Maybe Double)]
forall q r.
(ToRow q, FromRow r) =>
Connection -> Query -> q -> IO [r]
query Connection
conn Query
"SELECT cve_id, introduced_version, fixed_version, last_affected_version, severity FROM package_vulnerability_ranges WHERE package_name = ?" (Text -> Only Text
forall a. a -> Only a
Only Text
name)
pure (map toRange rows)
where
toRange :: (Text, Maybe Text, Maybe Text, Maybe Text, Maybe Double)
-> AdvisoryRange
toRange (Text
cveId, Maybe Text
intro, Maybe Text
fixed, Maybe Text
lastAffected, Maybe Double
severity) =
AdvisoryRange
{ arCveId :: Text
arCveId = Text
cveId
, arIntroduced :: Maybe Text
arIntroduced = Maybe Text
intro
, arFixed :: Maybe Text
arFixed = Maybe Text
fixed
, arLastAffected :: Maybe Text
arLastAffected = Maybe Text
lastAffected
, arSeverity :: Maybe Double
arSeverity = Maybe Double
severity
}
provenanceQuery :: Connection -> IO [(Text, Text)]
provenanceQuery :: Connection -> IO [(Text, Text)]
provenanceQuery Connection
conn = Connection -> Query -> IO [(Text, Text)]
forall r. FromRow r => Connection -> Query -> IO [r]
query_ Connection
conn Query
"SELECT key, value FROM meta ORDER BY key"