diff --git a/code/hsec-tools/app/Main.hs b/code/hsec-tools/app/Main.hs index 54f83816..f64a29ee 100644 --- a/code/hsec-tools/app/Main.hs +++ b/code/hsec-tools/app/Main.hs @@ -3,22 +3,24 @@ module Main where -import Control.Monad (join, void, when) +import Control.Monad (forM_, join, void, when) import qualified Data.ByteString.Lazy as L import Data.Foldable (for_) import Data.Functor ((<&>)) import Data.List (intercalate, isPrefixOf) -import qualified Data.Text.IO as T -import Options.Applicative import System.Exit (die, exitFailure, exitSuccess) -import System.IO (stderr) +import System.IO (hPutStrLn, stderr) import System.FilePath (takeBaseName) import qualified Data.Aeson +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Options.Applicative import Security.Advisories import qualified Security.Advisories.Convert.OSV as OSV import Security.Advisories.Git +import Security.Advisories.Queries (listAffectedBy, parseVersionRange) import Security.Advisories.Generate.HTML import qualified Command.Reserve @@ -37,6 +39,7 @@ cliOpts = info (commandsParser <**> helper) (fullDesc <> header "Haskell Advisor <> command "osv" (info commandOsv (progDesc "Convert a single advisory to OSV")) <> command "render" (info commandRender (progDesc "Render a single advisory as HTML")) <> command "generate-index" (info commandGenerateIndex (progDesc "Generate an HTML index")) + <> command "query" (info commandQuery (progDesc "Run various queries against the database")) <> command "help" (info commandHelp (progDesc "Show command help")) ) @@ -94,6 +97,34 @@ commandRender = <$> optional (argument str (metavar "FILE")) <**> helper +commandQuery :: Parser (IO ()) +commandQuery = + subparser + ( command "is-affected" (info isAffected (progDesc "Check if a package/version range is marked vulnerable")) + ) + where + isAffected :: Parser (IO ()) + isAffected = + go + <$> argument str (metavar "PACKAGE") + <*> optional (option str (metavar "VERSION-SPEC" <> short 'v' <> long "version-spec")) + <**> helper + where go :: T.Text -> Maybe T.Text -> IO () + go packageName versionRange = + case parseVersionRange versionRange of + Left e -> do + T.hPutStrLn stderr $ "Cannot parse '--version-spec': " <> e + exitFailure + Right versionRange' -> do + affectedAdvisories <- listAffectedBy "." packageName versionRange' + case affectedAdvisories of + [] -> putStrLn "Not affected" + _ -> do + hPutStrLn stderr "Affected by:" + forM_ affectedAdvisories $ \advisory -> + T.hPutStrLn stderr $ "* [" <> T.pack (printHsecId $ advisoryId advisory) <> "] " <> advisorySummary advisory + exitFailure + commandGenerateIndex :: Parser (IO ()) commandGenerateIndex = ( \src dst -> do diff --git a/code/hsec-tools/hsec-tools.cabal b/code/hsec-tools/hsec-tools.cabal index 95f9210d..391e0a65 100644 --- a/code/hsec-tools/hsec-tools.cabal +++ b/code/hsec-tools/hsec-tools.cabal @@ -3,7 +3,8 @@ name: hsec-tools version: 0.1.0.0 -- A short (one-line) description of the package. -synopsis: Tools for working with the Haskell security advisory database +synopsis: + Tools for working with the Haskell security advisory database -- A longer description of the package. description: @@ -19,97 +20,95 @@ maintainer: david@davidchristiansen.dk -- A copyright notice. -- copyright: -category: Data -extra-doc-files: CHANGELOG.md -extra-source-files: test/golden/*.md - test/golden/*.golden +category: Data +extra-doc-files: CHANGELOG.md +extra-source-files: + test/golden/*.golden + test/golden/*.md tested-with: GHC ==8.10.7 || ==9.0.2 || ==9.2.7 || ==9.4.5 || ==9.6.2 library - exposed-modules: Security.Advisories - , Security.Advisories.Definition - , Security.Advisories.Filesystem - , Security.Advisories.Git - , Security.Advisories.HsecId - , Security.Advisories.Parse - , Security.Advisories.Convert.OSV - , Security.Advisories.Generate.HTML - , Security.OSV - build-depends: base >=4.14 && < 4.19, - directory < 2, - extra ^>=1.7.5, - filepath >= 1.4 && < 1.5, - lucid >= 2.9.0, - process >= 1.6 && < 1.7, - text >= 1.2 && < 3, - time >= 1.9 && < 1.14, - Cabal-syntax >= 3.8.1.0 && < 3.11, - mtl >= 2.2 && < 2.4, - containers >= 0.6 && < 0.7, - commonmark ^>= 0.2.2, - toml-reader ^>= 0.1 || ^>= 0.2, - aeson >= 2.0.1.0 && < 3, - pandoc-types >= 1.22 && < 2, - pathwalk >= 0.3, - parsec >= 3 && < 4, - commonmark-pandoc >= 0.2 && < 0.3 - , safe >= 0.3 - hs-source-dirs: src - default-language: Haskell2010 - ghc-options: -Wall - -Wcompat - -Widentities - -Wincomplete-record-updates - -Wincomplete-uni-patterns - -Wpartial-fields - -Wredundant-constraints + exposed-modules: + Security.Advisories + Security.Advisories.Convert.OSV + Security.Advisories.Definition + Security.Advisories.Filesystem + Security.Advisories.Generate.HTML + Security.Advisories.Git + Security.Advisories.HsecId + Security.Advisories.Parse + Security.Advisories.Queries + Security.OSV + + build-depends: + , aeson >=2.0.1.0 && <3 + , base >=4.14 && <4.19 + , Cabal-syntax >=3.8.1.0 && <3.11 + , commonmark ^>=0.2.2 + , commonmark-pandoc >=0.2 && <0.3 + , containers >=0.6 && <0.7 + , directory <2 + , extra ^>=1.7.5 + , filepath >=1.4 && <1.5 + , lucid >=2.9.0 + , mtl >=2.2 && <2.4 + , pandoc-types >=1.22 && <2 + , parsec >=3 && <4 + , pathwalk >=0.3 + , process >=1.6 && <1.7 + , safe >=0.3 + , text >=1.2 && <3 + , time >=1.9 && <1.14 + , toml-reader >=0.1 && <0.2 || ^>=0.2 + , validation-selective >=0.1 && <1 + + hs-source-dirs: src + default-language: Haskell2010 + ghc-options: + -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints executable hsec-tools - main-is: Main.hs - other-modules: - Command.Reserve + main-is: Main.hs + other-modules: Command.Reserve - -- Modules included in this executable, other than Main. - -- other-modules: + -- Modules included in this executable, other than Main. + -- other-modules: - -- LANGUAGE extensions used by modules in this package. - -- other-extensions: - build-depends: hsec-tools, - base >=4.14 && < 4.19, - aeson >= 2.0.1.0 && < 3, - bytestring >= 0.10 && < 0.12, - filepath >= 1.4 && < 1.5, - optparse-applicative == 0.17.* || == 0.18.*, - text >= 1.2 && < 3 - hs-source-dirs: app - default-language: Haskell2010 - ghc-options: -Wall - -Wcompat - -Widentities - -Wincomplete-record-updates - -Wincomplete-uni-patterns - -Wpartial-fields - -Wredundant-constraints + -- LANGUAGE extensions used by modules in this package. + -- other-extensions: + build-depends: + , aeson >=2.0.1.0 && <3 + , base >=4.14 && <4.19 + , bytestring >=0.10 && <0.12 + , filepath >=1.4 && <1.5 + , hsec-tools + , optparse-applicative >=0.17 && <0.19 + , text >=1.2 && <3 + + hs-source-dirs: app + default-language: Haskell2010 + ghc-options: + -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints test-suite spec - type: exitcode-stdio-1.0 - hs-source-dirs: test - main-is: Spec.hs - build-depends: base < 5 - , directory - , hsec-tools - , pretty-simple < 5 - , tasty < 1.5 - , tasty-golden < 2.4 - , time - , text - default-language: Haskell2010 - ghc-options: -Wall - -Wcompat - -Widentities - -Wincomplete-record-updates - -Wincomplete-uni-patterns - -Wpartial-fields - -Wredundant-constraints + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + build-depends: + , base <5 + , directory + , hsec-tools + , pretty-simple <5 + , tasty <1.5 + , tasty-golden <2.4 + , text + , time + + default-language: Haskell2010 + ghc-options: + -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints diff --git a/code/hsec-tools/src/Security/Advisories/Convert/OSV.hs b/code/hsec-tools/src/Security/Advisories/Convert/OSV.hs index fb181369..2983c6ac 100644 --- a/code/hsec-tools/src/Security/Advisories/Convert/OSV.hs +++ b/code/hsec-tools/src/Security/Advisories/Convert/OSV.hs @@ -8,6 +8,7 @@ module Security.Advisories.Convert.OSV import qualified Data.Text as T import Data.Time (zonedTimeToUTC) import Data.Void +import Distribution.Pretty (prettyShow) import Security.Advisories import qualified Security.OSV as OSV @@ -54,8 +55,10 @@ mkSeverity s = case T.take 6 s of _ -> [] -- unexpected; don't include severity mkRange :: [AffectedVersionRange] -> OSV.Range Void -mkRange ranges = OSV.RangeEcosystem (foldMap mkEvs ranges) Nothing +mkRange ranges = + OSV.RangeEcosystem (foldMap mkEvs ranges) Nothing where - mkEvs range = - OSV.EventIntroduced (affectedVersionRangeIntroduced range) - : maybe [] (pure . OSV.EventFixed) (affectedVersionRangeFixed range) + mkEvs :: AffectedVersionRange -> [OSV.Event T.Text] + mkEvs range = + OSV.EventIntroduced (T.pack $ prettyShow $ affectedVersionRangeIntroduced range) + : maybe [] (pure . OSV.EventFixed . T.pack . prettyShow) (affectedVersionRangeFixed range) diff --git a/code/hsec-tools/src/Security/Advisories/Definition.hs b/code/hsec-tools/src/Security/Advisories/Definition.hs index 293d725c..14ea7538 100644 --- a/code/hsec-tools/src/Security/Advisories/Definition.hs +++ b/code/hsec-tools/src/Security/Advisories/Definition.hs @@ -14,6 +14,7 @@ module Security.Advisories.Definition import Data.Text (Text) import Data.Time (ZonedTime) +import Distribution.Types.Version (Version) import Distribution.Types.VersionRange (VersionRange) import Text.Pandoc.Definition (Pandoc) @@ -98,7 +99,7 @@ newtype Keyword = Keyword Text deriving (Show) via Text data AffectedVersionRange = AffectedVersionRange - { affectedVersionRangeIntroduced :: Text, - affectedVersionRangeFixed :: Maybe Text + { affectedVersionRangeIntroduced :: Version, + affectedVersionRangeFixed :: Maybe Version } deriving stock (Show) diff --git a/code/hsec-tools/src/Security/Advisories/Filesystem.hs b/code/hsec-tools/src/Security/Advisories/Filesystem.hs index 56bb0c27..adbe61d1 100644 --- a/code/hsec-tools/src/Security/Advisories/Filesystem.hs +++ b/code/hsec-tools/src/Security/Advisories/Filesystem.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE LambdaCase #-} + {-| Helpers for the /security-advisories/ file system. @@ -19,20 +21,27 @@ module Security.Advisories.Filesystem , getGreatestId , forReserved , forAdvisory + , listAdvisories ) where import Control.Applicative (liftA2) +import Data.Bifunctor (bimap) import Data.Foldable (fold) +import Data.Functor ((<&>)) import Data.Semigroup (Max(Max, getMax)) import Data.Traversable (for) -import Control.Monad.IO.Class (MonadIO) +import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Writer.Strict (execWriterT, tell) +import qualified Data.Text.IO as T import System.FilePath ((), takeBaseName) -import System.Directory (doesDirectoryExist) +import System.Directory (doesDirectoryExist, pathIsSymbolicLink) import System.Directory.PathWalk +import Validation (Validation, eitherToValidation) +import Security.Advisories (Advisory, AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), ParseAdvisoryError, emptyOutOfBandAttributes, parseAdvisory) import Security.Advisories.HsecId (HsecId, parseHsecId, placeholder) +import Security.Advisories.Git(firstAppearanceCommitDate, getAdvisoryGitInfo, lastModificationCommitDate) dirNameAdvisories :: FilePath @@ -109,6 +118,27 @@ forAdvisory root go = do subdirs <- filter (/= dirNameReserved) <$> _getSubdirs dir fmap fold $ for subdirs $ \subdir -> _forFiles (dir subdir) go +-- | List deduplicated parsed Advisories +listAdvisories + :: (MonadIO m) + => FilePath -> m (Validation [ParseAdvisoryError] [Advisory]) +listAdvisories root = + forAdvisory root $ \advisoryPath _advisoryId -> do + isSym <- liftIO $ pathIsSymbolicLink advisoryPath + if isSym + then return $ pure [] + else do + oob <- + liftIO (getAdvisoryGitInfo advisoryPath) <&> \case + Left _ -> emptyOutOfBandAttributes + Right gitInfo -> + emptyOutOfBandAttributes + { oobPublished = Just (firstAppearanceCommitDate gitInfo), + oobModified = Just (lastModificationCommitDate gitInfo) + } + fileContent <- liftIO $ T.readFile advisoryPath + return $ eitherToValidation $ bimap return return $ parseAdvisory NoOverrides oob fileContent + -- | Get names (not paths) of subdirectories of the given directory -- (one level). There's no monoidal, interruptible variant of -- @pathWalk@ so we use @WriterT@ to smuggle the result out. @@ -126,8 +156,8 @@ _forFiles -> (FilePath -> HsecId -> m r) -> m r _forFiles root go = - pathWalkAccumulate root $ \_ _ files -> + pathWalkAccumulate root $ \dir _ files -> fmap fold $ for files $ \file -> case parseHsecId (takeBaseName file) of Nothing -> pure mempty - Just hsid -> go (root file) hsid + Just hsid -> go (dir file) hsid diff --git a/code/hsec-tools/src/Security/Advisories/Generate/HTML.hs b/code/hsec-tools/src/Security/Advisories/Generate/HTML.hs index 58683f24..ba541973 100644 --- a/code/hsec-tools/src/Security/Advisories/Generate/HTML.hs +++ b/code/hsec-tools/src/Security/Advisories/Generate/HTML.hs @@ -7,49 +7,40 @@ module Security.Advisories.Generate.HTML ) where -import Control.Monad (filterM, forM_) -import Control.Monad.Extra (mapMaybeM) -import Data.Either.Extra (eitherToMaybe) -import Data.Functor ((<&>)) -import Data.List (isPrefixOf, isSuffixOf, sortOn) +import Control.Monad (forM_) +import Data.List (sortOn) import Data.List.Extra (groupSort) import qualified Data.Map.Strict as Map import Data.Ord (Down (..)) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T +import System.Exit (exitFailure) +import System.IO (stderr, hPrint) + +import Distribution.Pretty (prettyShow) import Lucid -import Security.Advisories (AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), emptyOutOfBandAttributes, parseAdvisory) +import Validation (Validation(..)) + import qualified Security.Advisories as Advisories -import Security.Advisories.Git -import System.Directory (createDirectoryIfMissing, pathIsSymbolicLink) -import System.Directory.Extra (listFilesRecursive) -import System.FilePath (takeFileName, ()) +import System.Directory (createDirectoryIfMissing) +import System.FilePath (()) +import Security.Advisories.Filesystem (listAdvisories) -- * Actions renderAdvisoriesIndex :: FilePath -> FilePath -> IO () renderAdvisoriesIndex src dst = do - let okToLoad path = do - isSym <- pathIsSymbolicLink path - let fileName = takeFileName path - pure $ - not isSym -- ignore symlinks (avoid duplicates entries in index) - && "HSEC-" `isPrefixOf` fileName - && ".md" `isSuffixOf` fileName - readAdvisory path = do - oob <- - getAdvisoryGitInfo path <&> \case - Left _ -> emptyOutOfBandAttributes - Right gitInfo -> - emptyOutOfBandAttributes - { oobPublished = Just (firstAppearanceCommitDate gitInfo), - oobModified = Just (lastModificationCommitDate gitInfo) - } - fileContent <- T.readFile path - return $ eitherToMaybe $ parseAdvisory NoOverrides oob fileContent - advisoriesFileName <- listFilesRecursive src >>= filterM okToLoad - advisories <- mapMaybeM readAdvisory advisoriesFileName + advisories <- + listAdvisories src >>= \case + Failure errors -> do + T.hPutStrLn stderr "Cannot parse some advisories" + forM_ errors $ + hPrint stderr + exitFailure + Success advisories -> + return advisories + let renderToFile' path content = do putStrLn $ "Rendering " <> path renderToFile path content @@ -219,6 +210,6 @@ toAdvisoryR x = flip map (Advisories.affectedVersions p) $ \versionRange -> AffectedPackageR { packageName = Advisories.affectedPackage p, - introduced = Advisories.affectedVersionRangeIntroduced versionRange, - fixed = Advisories.affectedVersionRangeFixed versionRange + introduced = T.pack $ prettyShow $ Advisories.affectedVersionRangeIntroduced versionRange, + fixed = T.pack . prettyShow <$> Advisories.affectedVersionRangeFixed versionRange } diff --git a/code/hsec-tools/src/Security/Advisories/Parse.hs b/code/hsec-tools/src/Security/Advisories/Parse.hs index 7f78d7cf..01d179fb 100644 --- a/code/hsec-tools/src/Security/Advisories/Parse.hs +++ b/code/hsec-tools/src/Security/Advisories/Parse.hs @@ -39,6 +39,7 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as T (toStrict) import Data.Time (LocalTime(..), ZonedTime(..), midnight, utc) import Distribution.Parsec (eitherParsec) +import Distribution.Types.Version (Version) import Distribution.Types.VersionRange (VersionRange) import Commonmark.Html (Html, renderHtml) @@ -227,8 +228,8 @@ parseAffected v = do parseAffectedVersionRange :: TOML.Value -> TableParser AffectedVersionRange parseAffectedVersionRange v = do tbl <- isTable v - introduced <- mandatory tbl "introduced" isString - fixed <- optional tbl "fixed" isString + introduced <- mandatory tbl "introduced" version + fixed <- optional tbl "fixed" version pure $ AffectedVersionRange introduced fixed advisoryDoc :: Blocks -> Either T.Text (T.Text, [Block]) @@ -316,6 +317,13 @@ architecture = \case "x86_64" -> pure X86_64 other -> throwError $ InvalidArchitecture other +version :: TOML.Value -> TableParser Version +version = + isString >=> \v -> + case eitherParsec (T.unpack v) of + Left err -> throwError $ UnderlyingParserError (T.pack err) + Right affected -> pure affected + versionRange :: TOML.Value -> TableParser VersionRange versionRange = isString >=> \v -> diff --git a/code/hsec-tools/src/Security/Advisories/Queries.hs b/code/hsec-tools/src/Security/Advisories/Queries.hs new file mode 100644 index 00000000..655e3d67 --- /dev/null +++ b/code/hsec-tools/src/Security/Advisories/Queries.hs @@ -0,0 +1,96 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Security.Advisories.Queries + ( isAffectedBy + , parseVersionRange + , listAffectedBy + ) +where + +import Control.Monad (forM_) +import Data.Bifunctor (first) +import System.Exit (exitFailure) +import System.IO (stderr, hPrint) + +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import Distribution.Parsec (eitherParsec) +import Distribution.Types.VersionRange (VersionRange, VersionRangeF(..), anyVersion, earlierVersion, intersectVersionRanges, majorUpperBound, orLaterVersion, projectVersionRange) +import Validation (Validation(..)) + +import Security.Advisories.Definition +import Security.Advisories.Filesystem + +-- | Check whether a package and a version range is concerned by an advisory +isAffectedBy :: Text -> VersionRange -> Advisory -> Bool +isAffectedBy queryPackageName queryVersionRange = + any checkAffected . advisoryAffected + where checkAffected :: Affected -> Bool + checkAffected affected = + queryPackageName == affectedPackage affected + && any + (intersectsWith (projectVersionRange queryVersionRange) . projectVersionRange . mkVersionRange) + (affectedVersions affected) + mkVersionRange :: AffectedVersionRange -> VersionRange + mkVersionRange x = + case affectedVersionRangeFixed x of + Nothing -> + orLaterVersion (affectedVersionRangeIntroduced x) + Just affectedVersionRangeFixed' -> + orLaterVersion (affectedVersionRangeIntroduced x) `intersectVersionRanges` earlierVersion affectedVersionRangeFixed' + intersectsWith :: VersionRangeF VersionRange -> VersionRangeF VersionRange -> Bool + intersectsWith left right = + case (left, right) of + (ThisVersionF x, ThisVersionF y) -> x == y + (ThisVersionF x, LaterVersionF y) -> x < y + (ThisVersionF x, OrLaterVersionF y) -> x >= y + (ThisVersionF x, EarlierVersionF y) -> x < y + (ThisVersionF x, OrEarlierVersionF y) -> x <= y + (LaterVersionF x, ThisVersionF y) -> x < y + (LaterVersionF _, LaterVersionF _) -> True + (LaterVersionF _, OrLaterVersionF _) -> True + (LaterVersionF x, EarlierVersionF y) -> x < y -- TODO + (LaterVersionF x, OrEarlierVersionF y) -> x < y + (OrLaterVersionF x, ThisVersionF y) -> x <= y + (OrLaterVersionF _, LaterVersionF _) -> True + (OrLaterVersionF _, OrLaterVersionF _) -> True + (OrLaterVersionF x, EarlierVersionF y) -> x < y + (OrLaterVersionF x, OrEarlierVersionF y) -> x <= y + (EarlierVersionF x, ThisVersionF y) -> x > y + (EarlierVersionF x, LaterVersionF y) -> x > y -- TODO + (EarlierVersionF x, OrLaterVersionF y) -> x > y + (EarlierVersionF _, EarlierVersionF _) -> True + (EarlierVersionF _, OrEarlierVersionF _) -> True + (OrEarlierVersionF x, ThisVersionF y) -> x >= y + (OrEarlierVersionF x, LaterVersionF y) -> x > y + (OrEarlierVersionF x, OrLaterVersionF y) -> x >= y + (OrEarlierVersionF _, EarlierVersionF _) -> True + (OrEarlierVersionF _, OrEarlierVersionF _) -> True + (MajorBoundVersionF x, _) -> intersectsWith (OrLaterVersionF x) right && intersectsWith (EarlierVersionF $ majorUpperBound x) right + (UnionVersionRangesF x y, _) -> intersectsWith (projectVersionRange x) right || intersectsWith (projectVersionRange y) right + (IntersectVersionRangesF x y, _) -> intersectsWith (projectVersionRange x) right && intersectsWith (projectVersionRange y) right + (_, UnionVersionRangesF x y) -> intersectsWith left (projectVersionRange x) || intersectsWith left (projectVersionRange y) + (_, IntersectVersionRangesF x y) -> intersectsWith left (projectVersionRange x) && intersectsWith left (projectVersionRange y) + (_, MajorBoundVersionF x) -> intersectsWith left (OrLaterVersionF x) && intersectsWith left (EarlierVersionF $ majorUpperBound x) + +-- | Parse 'VersionRange' as given to the CLI +parseVersionRange :: Maybe Text -> Either Text VersionRange +parseVersionRange = maybe (return anyVersion) (first T.pack . eitherParsec . T.unpack) + +-- | List the advisories matching package/version range +listAffectedBy :: FilePath -> Text -> VersionRange -> IO [Advisory] +listAffectedBy root queryPackageName queryVersionRange = do + advisories <- + listAdvisories root >>= \case + Failure errors -> do + T.hPutStrLn stderr "Cannot parse some advisories" + forM_ errors $ + hPrint stderr + exitFailure + Success advisories -> + return advisories + + + return $ filter (isAffectedBy queryPackageName queryVersionRange) advisories