packages feed

hsec-tools 0.2.0.2 → 0.3.0.1

raw patch · 23 files changed

+1270/−744 lines, 23 filesdep +lensdep +network-uridep −pathwalkdep −safedep −tasty-hunitdep ~hsec-coredep ~transformersPVP ok

version bump matches the API change (PVP)

Dependencies added: lens, network-uri

Dependencies removed: pathwalk, safe, tasty-hunit

Dependency ranges changed: hsec-core, transformers

API changes (from Hackage documentation)

- Security.Advisories.Filesystem: parseComponentIdentifier :: forall (m :: Type -> Type). Monad m => FilePath -> ExceptT OOBError m (Maybe ComponentIdentifier)
- Security.Advisories.Parse: PathHasNoComponentIdentifier :: OOBError
- Security.Advisories.Parse: [oobComponentIdentifier] :: OutOfBandAttributes -> Maybe ComponentIdentifier
- Security.Advisories.Queries: isVersionAffectedBy :: Text -> Version -> Advisory -> Bool
- Security.Advisories.Queries: isVersionRangeAffectedBy :: Text -> VersionRange -> Advisory -> Bool
+ Security.Advisories.Convert.OSV: AffectedLinks :: Text -> Text -> AffectedLinks
+ Security.Advisories.Convert.OSV: DbLinks :: Text -> Text -> Text -> DbLinks
+ Security.Advisories.Convert.OSV: [affectedLinksHumanLink] :: AffectedLinks -> Text
+ Security.Advisories.Convert.OSV: [affectedLinksOSV] :: AffectedLinks -> Text
+ Security.Advisories.Convert.OSV: [dbLinksHome] :: DbLinks -> Text
+ Security.Advisories.Convert.OSV: [dbLinksOSVs] :: DbLinks -> Text
+ Security.Advisories.Convert.OSV: [dbLinksRepository] :: DbLinks -> Text
+ Security.Advisories.Convert.OSV: convertWithLinks :: DbLinks -> Advisory -> Model DbLinks AffectedLinks Void Void
+ Security.Advisories.Convert.OSV: data AffectedLinks
+ Security.Advisories.Convert.OSV: data DbLinks
+ Security.Advisories.Convert.OSV: haskellLinks :: DbLinks
+ Security.Advisories.Convert.OSV: instance Data.Aeson.Types.ToJSON.ToJSON Security.Advisories.Convert.OSV.AffectedLinks
+ Security.Advisories.Convert.OSV: instance Data.Aeson.Types.ToJSON.ToJSON Security.Advisories.Convert.OSV.DbLinks
+ Security.Advisories.Filesystem: dirNamePublished :: FilePath
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Distribution.Types.PackageName.PackageName
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.RepositoryName
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.RepositoryURL
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Distribution.Types.PackageName.PackageName
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.RepositoryName
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.RepositoryURL
- Security.Advisories.Parse: OutOfBandAttributes :: UTCTime -> UTCTime -> Maybe ComponentIdentifier -> OutOfBandAttributes
+ Security.Advisories.Parse: OutOfBandAttributes :: UTCTime -> UTCTime -> OutOfBandAttributes
- Security.Advisories.Queries: listVersionAffectedBy :: MonadIO m => FilePath -> Text -> Version -> m QueryResult
+ Security.Advisories.Queries: listVersionAffectedBy :: MonadIO m => FilePath -> ComponentIdentifier -> Version -> m QueryResult
- Security.Advisories.Queries: listVersionRangeAffectedBy :: MonadIO m => FilePath -> Text -> VersionRange -> m QueryResult
+ Security.Advisories.Queries: listVersionRangeAffectedBy :: MonadIO m => FilePath -> ComponentIdentifier -> VersionRange -> m QueryResult

Files

CHANGELOG.md view
@@ -1,3 +1,17 @@+## 0.3.0.1++* Bump `hsec-core` `0.3.0.0`++## 0.3.0.0++* Move `isVersionAffectedBy` and `isVersionRangeAffectedBy` to `Security.Advisories.Core` (`hsec-core`)+* Add support for GHC component in `query is-affected`+* Add `model.database_specific.{repository,osvs,home}` and `model.affected.database_specific.{osv,human_link}` in OSV exports+* Adapt to new security-advisories layout+* Drop `Security.Advisories.Filesystem.parseComponentIdentifier`+* Drop `Security.Advisories.Parse.OutOfBandAttributes.oobComponentIdentifier`+* Drop `Security.Advisories.Parse.OOBError.PathHasNoComponentIdentifier`+ ## 0.2.0.2  * Update `tasty` dependency bounds
README.md view
@@ -1,6 +1,6 @@ # hsec-tools -`hesc-tools` aims to support [Haskell advisories database](https://github.com/haskell/security-advisories).+`hsec-tools` aims to support [Haskell advisories database](https://github.com/haskell/security-advisories).  ## Building 
app/Main.hs view
@@ -15,6 +15,7 @@ import Control.Exception (Exception(displayException)) import Distribution.Parsec (eitherParsec) import Distribution.Types.VersionRange (VersionRange, anyVersion)+import Network.URI (nullURI) import Options.Applicative import Security.Advisories import qualified Security.Advisories.Convert.OSV as OSV@@ -22,7 +23,6 @@ import Security.Advisories.Generate.Snapshot import Security.Advisories.Git import Security.Advisories.Queries (listVersionRangeAffectedBy)-import Security.Advisories.Filesystem (parseComponentIdentifier) import System.Exit (die, exitFailure, exitSuccess) import System.FilePath (takeBaseName) import System.IO (hPrint, hPutStrLn, stderr)@@ -99,11 +99,27 @@  commandOsv :: Parser (IO ()) commandOsv =-  withAdvisory go-    <$> optional (argument str (metavar "FILE"))+  withAdvisory . go+    <$> dbLinksParser+    <*> optional (argument str (metavar "FILE"))   where-    go _ adv = do-      L.putStr (Data.Aeson.encode (OSV.convert adv))+    dbLinksParser :: Parser OSV.DbLinks+    dbLinksParser =+      OSV.DbLinks+        <$> url "repository" (OSV.dbLinksRepository OSV.haskellLinks) "Repository URL"+        <*> url "osvs" (OSV.dbLinksOSVs OSV.haskellLinks) "OSVs link"+        <*> url "home" (OSV.dbLinksHome OSV.haskellLinks) "Home page URL"+      where+        url :: String -> T.Text -> String -> Parser T.Text+        url name def desc =+          T.pack <$> strOption+            ( long name <> metavar "URL"+           <> help desc+           <> value (T.unpack def)+           <> showDefault+            )+    go links _ adv = do+      L.putStr (Data.Aeson.encode (OSV.convertWithLinks links adv))       putChar '\n'  commandRender :: Parser (IO ())@@ -113,21 +129,29 @@  commandQuery :: Parser (IO ()) commandQuery =-  subparser+  hsubparser     ( 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")+        <$> argument (parseComponent <$> str) (metavar "PACKAGE|REPO:PACKAGE|GHC:COMPONENT")         <*> optional (option versionRangeReader (metavar "VERSION-RANGE" <> short 'v' <> long "version-range"))         <*> optional (option str (metavar "ADVISORIES-PATH" <> short 'p' <> long "advisories-path"))       where-        go :: T.Text -> Maybe VersionRange -> Maybe FilePath -> IO ()-        go packageName versionRange advisoriesPath = do+        parseComponent raw =+          case T.breakOn ":" raw of+            (pkg, "") -> hackage $ mkPackageName $ T.unpack pkg+            (p, pkg) ->+              let pkgName = mkPackageName $ T.unpack pkg+              in if T.toCaseFold p == T.toCaseFold "ghc"+                  then fromMaybe (hackage pkgName) $ GHC <$> ghcComponentFromText p+                  else Repository (RepositoryURL nullURI) (RepositoryName p) pkgName+        go :: ComponentIdentifier -> Maybe VersionRange -> Maybe FilePath -> IO ()+        go component versionRange advisoriesPath = do           let versionRange' = fromMaybe anyVersion versionRange-          maybeAffectedAdvisories <- listVersionRangeAffectedBy (fromMaybe "." advisoriesPath) packageName versionRange'+          maybeAffectedAdvisories <- listVersionRangeAffectedBy (fromMaybe "." advisoriesPath) component versionRange'           case maybeAffectedAdvisories of             Validation.Failure errors -> do               T.hPutStrLn stderr "Cannot parse some advisories"@@ -177,13 +201,11 @@   oob <- runExceptT $ case file of     Nothing -> throwE StdInHasNoOOB     Just path -> do-     ecosystem <- parseComponentIdentifier path      withExceptT GitHasNoOOB $ do       gitInfo <- ExceptT $ liftIO $ getAdvisoryGitInfo path       pure OutOfBandAttributes         { oobPublished = firstAppearanceCommitDate gitInfo         , oobModified = lastModificationCommitDate gitInfo-        , oobComponentIdentifier = ecosystem         }    case parseAdvisory NoOverrides oob input of
hsec-tools.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               hsec-tools-version:            0.2.0.2+version:            0.3.0.1  -- A short (one-line) description of the package. synopsis:@@ -60,26 +60,26 @@     , conduit               >=1.3      && <1.4     , conduit-extra         >=1.3      && <1.4     , containers            >=0.6      && <0.8-    , cvss                  >= 0.2     && < 0.3+    , cvss                  >=0.2      && <0.3     , data-default          >=0.7      && <0.8     , directory             <2     , extra                 >=1.7      && <1.9     , filepath              >=1.4      && <1.6-    , hsec-core             ^>= 0.2+    , hsec-core             >= 0.3.0.0 && <0.4     , file-embed            >=0.0.13.0 && <0.0.17-    , lucid                 >=2.9.0    && < 3+    , lens                  >=5.1.0    && <5.4+    , lucid                 >=2.9.0    && <3     , mtl                   >=2.2      && <2.4+    , network-uri           >=2.6.3.0  && <2.8     , osv                   >=0.1      && <0.3     , pandoc                >=2.0      && <3.8     , pandoc-types          >=1.22     && <2     , parsec                >=3        && <4-    , pathwalk              >=0.3      && <0.4     , pretty                >=1.0      && <1.2     , prettyprinter         >=1.7      && <1.8     , process               >=1.6      && <1.7     , refined               >=0.7      && <0.9     , resourcet             >=1.2      && <1.4-    , safe                  >=0.3      && <0.4     , text                  >=1.2      && <3     , template-haskell      >=2.16.0.0 && <2.24     , time                  >=1.9      && <1.15@@ -106,17 +106,18 @@   -- LANGUAGE extensions used by modules in this package.   -- other-extensions:   build-depends:-    , aeson                 >=2.0.1.0 && <3-    , base                  >=4.14    && <5-    , bytestring            >=0.10    && <0.13-    , Cabal-syntax          >=3.8.1.0 && <3.15-    , filepath              >=1.4     && <1.6-    , hsec-core             ^>= 0.2+    , aeson                 >=2.0.1.0  && <3+    , base                  >=4.14     && <5+    , bytestring            >=0.10     && <0.13+    , Cabal-syntax          >=3.8.1.0  && <3.15+    , filepath              >=1.4      && <1.6+    , hsec-core             >= 0.3.0.0 && <0.4     , hsec-tools-    , optparse-applicative  >=0.17    && <0.19-    , text                  >=1.2     && <3-    , transformers-    , validation-selective  >=0.1     && <1+    , network-uri           >=2.6.3.0  && <2.8+    , optparse-applicative  >=0.17     && <0.19+    , text                  >=1.2      && <3+    , transformers          >=0.5      && <0.7+    , validation-selective  >=0.1      && <1    hs-source-dirs:   app   default-language: Haskell2010@@ -133,7 +134,6 @@   other-modules:     Paths_hsec_tools     Spec.FormatSpec-    Spec.QueriesSpec   build-depends:     , aeson-pretty   <2     , base@@ -144,13 +144,13 @@     , hedgehog       <2     , hsec-core     , hsec-tools+    , network-uri     , osv     , pretty-simple  <5     , prettyprinter     , tasty          <2     , tasty-golden   <2.4     , tasty-hedgehog <2-    , tasty-hunit    <0.11     , text     , time     , toml-parser
src/Security/Advisories/Convert/OSV.hs view
@@ -1,10 +1,16 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} -module Security.Advisories.Convert.OSV-  ( convert-  )-  where+module Security.Advisories.Convert.OSV (+    convert,+    convertWithLinks,+    DbLinks (..),+    AffectedLinks (..),+    haskellLinks,+)+where +import Data.Aeson import qualified Data.Text as T import Data.Void import Distribution.Pretty (prettyShow)@@ -14,38 +20,41 @@  convert :: Advisory -> OSV.Model Void Void Void Void convert adv =-  ( OSV.newModel'-    (T.pack . printHsecId $ advisoryId adv)-    (advisoryModified adv)-  )-  { OSV.modelPublished = Just $ advisoryPublished adv-  , OSV.modelAliases = advisoryAliases adv-  , OSV.modelRelated = advisoryRelated adv-  , OSV.modelSummary = Just $ advisorySummary adv-  , OSV.modelDetails = Just $ advisoryDetails adv-  , OSV.modelReferences = advisoryReferences adv-  , OSV.modelAffected = fmap mkAffected (advisoryAffected adv)-  }+    ( OSV.newModel'+        (T.pack . printHsecId $ advisoryId adv)+        (advisoryModified adv)+    )+        { OSV.modelPublished = Just $ advisoryPublished adv+        , OSV.modelAliases = advisoryAliases adv+        , OSV.modelRelated = advisoryRelated adv+        , OSV.modelSummary = Just $ advisorySummary adv+        , OSV.modelDetails = Just $ advisoryDetails adv+        , OSV.modelReferences = advisoryReferences adv+        , OSV.modelAffected = fmap mkAffected (advisoryAffected adv)+        }  mkAffected :: Affected -> OSV.Affected Void Void Void mkAffected aff =-  OSV.Affected-    { OSV.affectedPackage = mkPackage (affectedComponentIdentifier aff)-    , OSV.affectedRanges = pure $ mkRange (affectedVersions aff)-    , OSV.affectedSeverity = [OSV.Severity (affectedCVSS aff)]-    , OSV.affectedEcosystemSpecific = Nothing-    , OSV.affectedDatabaseSpecific = Nothing-    }+    OSV.Affected+        { OSV.affectedPackage = mkPackage (affectedComponentIdentifier aff)+        , OSV.affectedRanges = pure $ mkRange (affectedVersions aff)+        , OSV.affectedSeverity = [OSV.Severity (affectedCVSS aff)]+        , OSV.affectedEcosystemSpecific = Nothing+        , OSV.affectedDatabaseSpecific = Nothing+        }  mkPackage :: ComponentIdentifier -> OSV.Package-mkPackage ecosystem = OSV.Package-  { OSV.packageName = packageName-  , OSV.packageEcosystem = ecosystemName-  , OSV.packagePurl = Nothing-  }+mkPackage ecosystem =+    OSV.Package+        { OSV.packageName = packageName+        , OSV.packageEcosystem = ecosystemName+        , OSV.packagePurl = Nothing+        }   where     (ecosystemName, packageName) = case ecosystem of-        Hackage n -> ("Hackage", n)+        Repository _ repoName pkg+          | ecosystem == hackage pkg -> ("Hackage", T.pack $ unPackageName pkg)+          | otherwise -> (unRepositoryName repoName, T.pack $ unPackageName pkg)         GHC c -> ("GHC", ghcComponentToText c)  mkRange :: [AffectedVersionRange] -> OSV.Range Void@@ -54,5 +63,73 @@   where     mkEvs :: AffectedVersionRange -> [OSV.Event T.Text]     mkEvs range =-      OSV.EventIntroduced (T.pack $ prettyShow $ affectedVersionRangeIntroduced range)-      : maybe [] (pure . OSV.EventFixed . T.pack . prettyShow) (affectedVersionRangeFixed range)+        OSV.EventIntroduced (T.pack $ prettyShow $ affectedVersionRangeIntroduced range)+            : maybe [] (pure . OSV.EventFixed . T.pack . prettyShow) (affectedVersionRangeFixed range)++convertWithLinks :: DbLinks -> Advisory -> OSV.Model DbLinks AffectedLinks Void Void+convertWithLinks links adv =+    OSV.Model+        { OSV.modelDatabaseSpecific = Just links+        , OSV.modelAffected = mkAffectedWithLinks links (advisoryId adv) <$> advisoryAffected adv+        , ..+        }+  where+    OSV.Model{..} = convert adv++data DbLinks = DbLinks+    { dbLinksRepository :: T.Text+    , dbLinksOSVs :: T.Text+    , dbLinksHome :: T.Text+    }++instance ToJSON DbLinks where+    toJSON DbLinks{..} =+        object+            [ "repository" .= dbLinksRepository+            , "osvs" .= dbLinksOSVs+            , "home" .= dbLinksHome+            ]++haskellLinks :: DbLinks+haskellLinks =+    DbLinks+        { dbLinksRepository = "https://github.com/haskell/security-advisories"+        , dbLinksOSVs = "https://raw.githubusercontent.com/haskell/security-advisories/refs/heads/generated/osv-export"+        , dbLinksHome = "https://github.com/haskell/security-advisories"+        }++data AffectedLinks = AffectedLinks+    { affectedLinksOSV :: T.Text+    , affectedLinksHumanLink :: T.Text+    }++instance ToJSON AffectedLinks where+    toJSON AffectedLinks{..} =+        object+            [ "osv" .= affectedLinksOSV+            , "human_link" .= affectedLinksHumanLink+            ]++mkAffectedWithLinks :: DbLinks -> HsecId -> Affected -> OSV.Affected AffectedLinks Void Void+mkAffectedWithLinks links hsecId aff =+    OSV.Affected+        { OSV.affectedDatabaseSpecific =+            Just+                AffectedLinks+                    { affectedLinksOSV = osvLink+                    , affectedLinksHumanLink = humanLink+                    }+        , ..+        }+  where+    OSV.Affected{..} = mkAffected aff+    stripSlash = T.dropWhileEnd (== '/')+    osvLink =+      stripSlash (dbLinksOSVs links)+      <> "/" <> T.pack (show $ hsecIdYear hsecId)+      <> "/" <> T.pack (printHsecId hsecId) <> ".json"+    humanLink =+      stripSlash (dbLinksHome links)+      <> "/tree/main/advisories/published/"+      <> T.pack (show $ hsecIdYear hsecId)+      <> "/" <> T.pack (printHsecId hsecId) <> ".md"
src/Security/Advisories/Filesystem.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE CPP #-}+ {-|  Helpers for the /security-advisories/ file system.@@ -11,6 +13,7 @@   (     dirNameAdvisories   , dirNameReserved+  , dirNamePublished   , isSecurityAdvisoriesRepo   , getReservedIds   , getAdvisoryIds@@ -21,31 +24,28 @@   , forAdvisory   , listAdvisories   , advisoryFromFile-  , parseComponentIdentifier   ) where +#if MIN_VERSION_base(4,18,0)+#else import Control.Applicative (liftA2)+#endif import Data.Bifunctor (bimap) import Data.Foldable (fold) import Data.Semigroup (Max(Max, getMax)) import Data.Traversable (for)  import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad.Writer.Strict (execWriterT, tell)-import qualified Data.Text as T import qualified Data.Text.IO as T-import System.FilePath ((</>), takeBaseName, splitDirectories)-import System.Directory (doesDirectoryExist, pathIsSymbolicLink)-import System.Directory.PathWalk+import System.FilePath ((</>), dropExtension)+import System.Directory (doesDirectoryExist, listDirectory) import Validation (Validation (..)) -import Security.Advisories (Advisory, AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), ParseAdvisoryError, parseAdvisory, ComponentIdentifier(..))+import Security.Advisories (Advisory, AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), ParseAdvisoryError, parseAdvisory) import Security.Advisories.Core.HsecId (HsecId, parseHsecId, placeholder) import Security.Advisories.Git(firstAppearanceCommitDate, getAdvisoryGitInfo, lastModificationCommitDate) import Control.Monad.Except (runExceptT, ExceptT (ExceptT), withExceptT)-import Security.Advisories.Parse (OOBError(GitHasNoOOB, PathHasNoComponentIdentifier))-import Security.Advisories.Core.Advisory (ghcComponentFromText)-+import Security.Advisories.Parse (OOBError(GitHasNoOOB))  dirNameAdvisories :: FilePath dirNameAdvisories = "advisories"@@ -53,6 +53,9 @@ dirNameReserved :: FilePath dirNameReserved = "reserved" +dirNamePublished :: FilePath+dirNamePublished = "published"+ -- | Check whether the directory appears to be the root of a -- /security-advisories/ filesystem.  Only checks that the -- @advisories@ subdirectory exists.@@ -104,7 +107,7 @@   :: (MonadIO m, Monoid r)   => FilePath -> (FilePath -> HsecId -> m r) -> m r forReserved root =-  _forFiles (root </> dirNameAdvisories </> dirNameReserved)+  _forFilesByYear (root </> dirNameAdvisories </> dirNameReserved)  -- | Invoke a callback for each HSEC ID under each of the advisory -- subdirectories, excluding the @reserved@ directory.  The results@@ -116,69 +119,51 @@ forAdvisory   :: (MonadIO m, Monoid r)   => FilePath -> (FilePath -> HsecId -> m r) -> m r-forAdvisory root go = do-  let dir = root </> dirNameAdvisories-  subdirs <- filter (/= dirNameReserved) <$> _getSubdirs dir-  fmap fold $ for subdirs $ \subdir -> _forFiles (dir </> subdir) go+forAdvisory root =+  _forFilesByYear (root </> dirNameAdvisories </> dirNamePublished) --- | List deduplicated parsed Advisories+-- | List parsed Advisories listAdvisories   :: (MonadIO m)   => FilePath -> m (Validation [(FilePath, ParseAdvisoryError)] [Advisory]) listAdvisories root =-  forAdvisory root $ \advisoryPath _advisoryId -> do-    isSym <- liftIO $ pathIsSymbolicLink advisoryPath-    if isSym-      then return $ pure []-      else-        bimap (\err -> [(advisoryPath, err)]) pure-        <$> advisoryFromFile advisoryPath+  forAdvisory root $ \advisoryPath _advisoryId ->+    bimap (\err -> [(advisoryPath, err)]) pure+    <$> advisoryFromFile advisoryPath  -- | Parse an advisory from a file system path advisoryFromFile   :: (MonadIO m)   => FilePath -> m (Validation ParseAdvisoryError Advisory) advisoryFromFile advisoryPath = do-  oob <- runExceptT $ do-   ecosystem <- parseComponentIdentifier advisoryPath+  oob <- runExceptT $    withExceptT GitHasNoOOB $ do     gitInfo <- ExceptT $ liftIO $ getAdvisoryGitInfo advisoryPath     pure OutOfBandAttributes       { oobPublished = firstAppearanceCommitDate gitInfo       , oobModified = lastModificationCommitDate gitInfo-      , oobComponentIdentifier = ecosystem       }   fileContent <- liftIO $ T.readFile advisoryPath   pure     $ either Failure Success     $ 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.----_getSubdirs :: (MonadIO m) => FilePath -> m [FilePath]-_getSubdirs root =-  execWriterT $-    pathWalkInterruptible root $ \_ subdirs _ -> do-      tell subdirs-      pure Stop--_forFiles+_forFilesByYear   :: (MonadIO m, Monoid r)   => FilePath  -- ^ (sub)directory name   -> (FilePath -> HsecId -> m r)   -> m r-_forFiles root go =-  pathWalkAccumulate root $ \dir _ files ->-    fmap fold $ for files $ \file ->-      case parseHsecId (takeBaseName file) of-        Nothing -> pure mempty-        Just hsid -> go (dir </> file) hsid--parseComponentIdentifier :: Monad m => FilePath -> ExceptT OOBError m (Maybe ComponentIdentifier)-parseComponentIdentifier fp = ExceptT . pure $ case drop 1 $ reverse $ splitDirectories fp of-  package : "hackage" : _ -> pure (Just $ Hackage $ T.pack package)-  component : "ghc" : _ | Just ghc <- ghcComponentFromText (T.pack component) -> pure (Just $ GHC ghc)-  _ : _ : "advisories" : _ -> Left PathHasNoComponentIdentifier-  _ -> pure Nothing+_forFilesByYear root go = do+  yearsFile <- liftIO $ listDirectory root+  fmap (foldMap fold) $+    for yearsFile $ \year -> do+      let yearDir = root </> year+      isYear <- liftIO $ doesDirectoryExist yearDir+      if isYear+        then do+          files <- liftIO $ listDirectory yearDir+          for files $ \file ->+            case parseHsecId (dropExtension file) of+              Nothing -> pure mempty+              Just hsid -> go (yearDir </> file) hsid+        else pure mempty
src/Security/Advisories/Format.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-orphans #-}@@ -31,6 +32,7 @@ import Distribution.Pretty (pretty) import Distribution.Types.Version (Version) import Distribution.Types.VersionRange (VersionRange)+import Network.URI (parseAbsoluteURI) import qualified Text.PrettyPrint as Pretty import qualified Toml import qualified Toml.Schema as Toml@@ -157,9 +159,13 @@   toValue = Toml.Text' () . ghcComponentToText  instance Toml.FromValue Affected where-  fromValue = Toml.parseTableFromValue $-   do ecosystem   <- (Hackage <$> Toml.reqKey "package") <|> (GHC <$> Toml.reqKey "ghc-component")-      cvss      <- Toml.reqKey "cvss" -- TODO validate CVSS format+  fromValue =+   Toml.parseTableFromValue $ do+      ecosystem   <-+        (Repository <$> Toml.reqKey "repository-url" <*> Toml.reqKey "repository-name" <*> Toml.reqKey "package" )+          <|> (hackage <$> Toml.reqKey "package")+          <|> (GHC <$> Toml.reqKey "ghc-component")+      cvss      <- Toml.reqKey "cvss"       os        <- Toml.optKey "os"       arch      <- Toml.optKey "arch"       decls     <- maybe [] Map.toList <$> Toml.optKey "declarations"@@ -187,7 +193,9 @@     [ "declarations" Toml..= asTable (affectedDeclarations x) | not (null (affectedDeclarations x))]     where       ecosystem = case affectedComponentIdentifier x of-        Hackage pkg -> ["package" Toml..= pkg]+        Repository repoUrl repoName pkg+          | affectedComponentIdentifier x == hackage pkg -> ["package" Toml..= pkg]+          | otherwise -> ["repository-url" Toml..= repoUrl, "repository-name" Toml..= repoName, "package" Toml..= pkg]         GHC c -> ["ghc-component" Toml..= c]       asTable kvs = Map.fromList [(T.unpack k, v) | (k,v) <- kvs] @@ -336,6 +344,26 @@  instance Toml.ToValue CVSS.CVSS where   toValue = Toml.toValue . CVSS.cvssVectorString++instance Toml.ToValue PackageName where+  toValue = Toml.toValue . unPackageName++instance Toml.FromValue PackageName where+  fromValue = fmap mkPackageName . Toml.fromValue++instance Toml.ToValue RepositoryURL where+  toValue = Toml.toValue . show . unRepositoryURL++instance Toml.FromValue RepositoryURL where+  fromValue got = do+    lit <- Toml.fromValue got+    case parseAbsoluteURI lit of+      Nothing -> Toml.failAt (Toml.valueAnn got) "expected absolute URL"+      Just url -> pure $ RepositoryURL url++deriving newtype instance Toml.ToValue RepositoryName++deriving newtype instance Toml.FromValue RepositoryName  -- | A solution to an awkward problem: how to delete the TOML -- block.  We parse into this type to get the source range of
src/Security/Advisories/Generate/HTML.hs view
@@ -8,8 +8,13 @@   ) where +import Control.Lens.Combinators (set) import Control.Monad (forM_)+import Control.Monad.Trans.Resource (ResourceT) import qualified Data.ByteString.Char8 as BS8+import qualified Data.Conduit as Conduit+import qualified Data.Conduit.Binary as ConduitBinary+import Data.Default (def) import Data.List (sortOn) import Data.List.Extra (groupSort) import qualified Data.Map.Strict as Map@@ -19,35 +24,31 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as BSE import qualified Data.Text.IO as T-import Data.Time (UTCTime, formatTime, defaultTimeLocale)-import System.Directory (createDirectoryIfMissing)-import System.Exit (exitFailure)-import System.FilePath ((</>), takeDirectory)-import System.IO (hPrint, hPutStrLn, stderr)-import System.IO.Unsafe (unsafePerformIO)--import Control.Monad.Trans.Resource (ResourceT)-import qualified Data.Conduit as Conduit-import qualified Data.Conduit.Binary as ConduitBinary-import Data.Default (def)+import Data.Time (UTCTime, defaultTimeLocale, formatTime) import Distribution.Pretty (prettyShow) import Distribution.Types.VersionRange (earlierVersion, intersectVersionRanges, orLaterVersion) import Lucid+import Network.URI (nullURI, relativeTo)+import Network.URI.Lens (uriPathLens) import Refined (refineTH)-import qualified Text.Atom.Types as Feed+import qualified Security.Advisories as Advisories+import Security.Advisories.Core.Advisory (ComponentIdentifier (..), ghcComponentToText)+import Security.Advisories.Filesystem (listAdvisories)+import Security.Advisories.Generate.TH (readDirFilesTH)+import qualified Security.OSV as OSV+import System.Directory (createDirectoryIfMissing)+import System.Exit (exitFailure)+import System.FilePath (takeDirectory, (</>))+import System.IO (hPrint, hPutStrLn, stderr)+import System.IO.Unsafe (unsafePerformIO) import qualified Text.Atom.Conduit.Render as FeedExport+import qualified Text.Atom.Types as Feed import Text.Pandoc (runIOorExplode) import Text.Pandoc.Writers (writeHtml5String) import qualified Text.XML.Stream.Render as ConduitXML import qualified URI.ByteString as URI import Validation (Validation (..)) -import qualified Security.Advisories as Advisories-import Security.Advisories.Filesystem (listAdvisories)-import Security.Advisories.Generate.TH (readDirFilesTH)-import Security.Advisories.Core.Advisory (ComponentIdentifier (..), ghcComponentToText)-import qualified Security.OSV as OSV- -- * Actions  renderAdvisoriesIndex :: FilePath -> FilePath -> IO ()@@ -134,7 +135,7 @@  packageName :: AffectedPackageR -> Text packageName af = case ecosystem af of-  Hackage n -> n+  Repository _ repoName n -> "@" <> Advisories.unRepositoryName repoName <> "/" <> T.pack (Advisories.unPackageName n)   GHC c -> "ghc:" <> ghcComponentToText c  listByPackages :: [AdvisoryR] -> Html ()@@ -164,7 +165,7 @@            tbody_ $ do             let sortedAdvisories =-                    sortOn (Down . advisoryId . fst) perPackageAdvisory+                  sortOn (Down . advisoryId . fst) perPackageAdvisory             forM_ sortedAdvisories $ \(advisory, package) -> do               tr_ $ do                 td_ [class_ "advisory-id"] $@@ -178,7 +179,7 @@ indexDescription =   div_ [class_ "description"] $ do     p_ "The Haskell Security Advisory Database is a repository of security advisories filed against packages published via Hackage."-    p_  $ do+    p_ $ do       "It is generated from "       a_ [href_ "https://github.com/haskell/security-advisories/", target_ "_blank", rel_ "noopener noreferrer"] "Haskell Security Advisory Database"       ". "@@ -204,7 +205,7 @@       dt_ "CAPECs"       placeholderWhenEmptyOr (Advisories.advisoryCAPECs advisory) $ \capecs ->         forM_ capecs $ \(Advisories.CAPEC capec) ->-            dd_ [] $ a_ [href_ $ "https://capec.mitre.org/data/definitions/" <> T.pack (show capec) <> ".html"] $ toHtml $ show capec+          dd_ [] $ a_ [href_ $ "https://capec.mitre.org/data/definitions/" <> T.pack (show capec) <> ".html"] $ toHtml $ show capec       dt_ "CWEs"       placeholderWhenEmptyOr (Advisories.advisoryCWEs advisory) $ \cwes ->         forM_ cwes $ \(Advisories.CWE cwe) ->@@ -223,9 +224,19 @@     h4_ [] "Affected"     forM_ (Advisories.advisoryAffected advisory) $ \affected -> do       h5_ [] $-        case Advisories.affectedComponentIdentifier affected of-          Hackage package -> a_ [href_ $ "https://hackage.haskell.org/package/" <> package] $ code_ [] $ toHtml package-          GHC component -> code_ [] $ toHtml $ Advisories.ghcComponentToText component+        let (url, title) =+              case Advisories.affectedComponentIdentifier affected of+                Repository repoUrl repoName package ->+                  let name = Advisories.unPackageName package+                  in+                    ( T.pack $ show $ set uriPathLens ("package/" <> name) nullURI `relativeTo` Advisories.unRepositoryURL repoUrl,+                      "@" <> Advisories.unRepositoryName repoName <> "/" <> T.pack name+                    )+                GHC component ->+                  ( "https://gitlab.haskell.org/ghc/ghc",+                    Advisories.ghcComponentToText component+                  )+         in a_ [href_ url] $ code_ [] $ toHtml title        dl_ [] $ do         dt_ "CVSS"@@ -238,9 +249,9 @@                 T.pack $                   prettyShow $                     let introducedVersionRange = orLaterVersion $ Advisories.affectedVersionRangeIntroduced affectedVersionRange-                    in case Advisories.affectedVersionRangeFixed affectedVersionRange of-                      Nothing -> introducedVersionRange-                      Just fixedVersion -> introducedVersionRange `intersectVersionRanges` earlierVersion fixedVersion+                     in case Advisories.affectedVersionRangeFixed affectedVersionRange of+                          Nothing -> introducedVersionRange+                          Just fixedVersion -> introducedVersionRange `intersectVersionRanges` earlierVersion fixedVersion         forM_ (Advisories.affectedArchitectures affected) $ \architectures -> do           dt_ "Architectures"           dd_ [] $ toHtml $ T.intercalate ", " $ T.toLower . T.pack . show <$> architectures@@ -329,53 +340,53 @@ feed :: [Advisories.Advisory] -> Feed.AtomFeed feed advisories =   Feed.AtomFeed-    { Feed.feedAuthors      = [ hsrt ]-    , Feed.feedCategories   = []-    , Feed.feedContributors = []-    , Feed.feedEntries      = toEntry <$> advisories-    , Feed.feedGenerator    = Nothing-    , Feed.feedIcon         = Nothing-    , Feed.feedId           = "73b10e73-16bc-4bf2-a56c-ad7d09213e45"-    , Feed.feedLinks        = [-          Feed.AtomLink-            { Feed.linkHref   = toAtomURI atomFeedUrl-            , Feed.linkRel    = "self"-            , Feed.linkType   = ""-            , Feed.linkLang   = ""-            , Feed.linkTitle  = ""-            , Feed.linkLength = ""+    { Feed.feedAuthors = [hsrt],+      Feed.feedCategories = [],+      Feed.feedContributors = [],+      Feed.feedEntries = toEntry <$> advisories,+      Feed.feedGenerator = Nothing,+      Feed.feedIcon = Nothing,+      Feed.feedId = "73b10e73-16bc-4bf2-a56c-ad7d09213e45",+      Feed.feedLinks =+        [ Feed.AtomLink+            { Feed.linkHref = toAtomURI atomFeedUrl,+              Feed.linkRel = "self",+              Feed.linkType = "",+              Feed.linkLang = "",+              Feed.linkTitle = "",+              Feed.linkLength = ""             }-    ]-    , Feed.feedLogo         = Nothing-    , Feed.feedRights       = Nothing-    , Feed.feedSubtitle     = Nothing-    , Feed.feedTitle        = Feed.AtomPlainText Feed.TypeText "Haskell Security Advisory DB"-    , Feed.feedUpdated      = maximum $ Advisories.advisoryModified <$> advisories+        ],+      Feed.feedLogo = Nothing,+      Feed.feedRights = Nothing,+      Feed.feedSubtitle = Nothing,+      Feed.feedTitle = Feed.AtomPlainText Feed.TypeText "Haskell Security Advisory DB",+      Feed.feedUpdated = maximum $ Advisories.advisoryModified <$> advisories     }   where     toEntry advisory =       Feed.AtomEntry-        { Feed.entryAuthors      = [hsrt]-        , Feed.entryCategories   = []-        , Feed.entryContent      = Just $ Feed.AtomContentInlineText Feed.TypeHTML $ Advisories.advisoryHtml advisory-        , Feed.entryContributors = []-        , Feed.entryId           = T.pack $ Advisories.printHsecId $ Advisories.advisoryId advisory-        , Feed.entryLinks        = [-            Feed.AtomLink-              { Feed.linkHref   = toAtomURI $ toUrl advisory-              , Feed.linkRel    = "alternate"-              , Feed.linkType   = ""-              , Feed.linkLang   = ""-              , Feed.linkTitle  = ""-              , Feed.linkLength = ""-              }-          ]-        , Feed.entryPublished    = Just $ Advisories.advisoryPublished advisory-        , Feed.entryRights       = Nothing-        , Feed.entrySource       = Nothing-        , Feed.entrySummary      = Nothing-        , Feed.entryTitle        = Feed.AtomPlainText Feed.TypeText $ mkTitle advisory-        , Feed.entryUpdated      = Advisories.advisoryModified advisory+        { Feed.entryAuthors = [hsrt],+          Feed.entryCategories = [],+          Feed.entryContent = Just $ Feed.AtomContentInlineText Feed.TypeHTML $ Advisories.advisoryHtml advisory,+          Feed.entryContributors = [],+          Feed.entryId = T.pack $ Advisories.printHsecId $ Advisories.advisoryId advisory,+          Feed.entryLinks =+            [ Feed.AtomLink+                { Feed.linkHref = toAtomURI $ toUrl advisory,+                  Feed.linkRel = "alternate",+                  Feed.linkType = "",+                  Feed.linkLang = "",+                  Feed.linkTitle = "",+                  Feed.linkLength = ""+                }+            ],+          Feed.entryPublished = Just $ Advisories.advisoryPublished advisory,+          Feed.entryRights = Nothing,+          Feed.entrySource = Nothing,+          Feed.entrySummary = Nothing,+          Feed.entryTitle = Feed.AtomPlainText Feed.TypeText $ mkTitle advisory,+          Feed.entryUpdated = Advisories.advisoryModified advisory         }      mkTitle advisory =@@ -392,11 +403,11 @@         . BSE.encodeUtf8      hsrt =-        Feed.AtomPerson-          { Feed.personName = $$(refineTH "Haskell Security Response Team")-          , Feed.personEmail = "security-advisories@haskell.org"-          , Feed.personUri = Nothing-          }+      Feed.AtomPerson+        { Feed.personName = $$(refineTH "Haskell Security Response Team"),+          Feed.personEmail = "security-advisories@haskell.org",+          Feed.personUri = Nothing+        }  renderFeed :: [Advisories.Advisory] -> Conduit.ConduitT () BS8.ByteString (ResourceT IO) () renderFeed =
src/Security/Advisories/Generate/Snapshot.hs view
@@ -9,6 +9,7 @@   ) where +import Control.Monad (forM_) import Data.Aeson (ToJSON, encodeFile) import Data.Default (def) import qualified Data.Text.IO as T@@ -22,12 +23,13 @@ import Security.Advisories.Filesystem (advisoryFromFile, forAdvisory, forReserved) import Security.Advisories.Format (fromAdvisory) import System.Directory (copyFileWithMetadata, createDirectoryIfMissing)-import System.FilePath (takeDirectory, (</>))+import System.FilePath (takeDirectory, (</>), takeFileName) import System.IO (hPrint, hPutStrLn, stderr) import Text.Pandoc (Block (CodeBlock), Pandoc (Pandoc), nullMeta, runIOorExplode) import Text.Pandoc.Writers (writeCommonMark) import qualified Toml import Validation (Validation (..))+import qualified Data.Text as T  -- * Actions @@ -42,8 +44,7 @@    advisoriesLatestUpdates <-     forAdvisory src $ \p _ -> do-      createDirectoryIfMissing True $ takeDirectory $ toDstFilePath p-      hPutStrLn stderr $ "Taking a snapshot of '" <> p <> "' to '" <> toDstFilePath p <> "'"+      hPutStrLn stderr $ "Taking a snapshot of '" <> p <> "'"       advisoryFromFile p         >>= \case           Failure e -> do@@ -64,7 +65,22 @@                     )                 blocks (Pandoc _ xs) = xs             rendered <- runIOorExplode $ writeCommonMark def pandoc-            T.writeFile (toDstFilePath p) rendered++            let targetFiles =+                  concat+                    [ [toDstFilePath p],+                      legacyComponentFile . affectedComponentIdentifier <$> advisoryAffected advisory+                    ]+                advisoryFilename = takeFileName p+                legacyComponentFile =+                  \case+                    Repository _ repoName pkg -> dst </> T.unpack (unRepositoryName repoName) </> unPackageName pkg </> advisoryFilename+                    GHC comp -> dst </> "ghc" </> T.unpack (ghcComponentToText comp) </> advisoryFilename+            forM_ targetFiles $ \targetFile -> do+              hPutStrLn stderr $ " * Writing it to '" <> targetFile <> "'"+              createDirectoryIfMissing True $ takeDirectory targetFile+              T.writeFile targetFile rendered+             return [advisoryModified advisory]    let metadataPath = dst </> "snapshot.json"
src/Security/Advisories/Git.hs view
@@ -19,6 +19,7 @@  import Data.Char (isSpace) import Data.List (dropWhileEnd)+import qualified Data.List.NonEmpty as NE import Data.Time (UTCTime, zonedTimeToUTC) import Data.Time.Format.ISO8601 (iso8601ParseM) import System.Exit (ExitCode(ExitSuccess))@@ -110,10 +111,10 @@     "" -- standard input   let timestamps = filter (not . null) $ lines stdout   case status of-    ExitSuccess | not (null timestamps) ->+    ExitSuccess | Just timestamps' <- NE.nonEmpty timestamps ->       pure $ AdvisoryGitInfo-        <$> parseTime (last timestamps)  -- first commit is last line-        <*> parseTime (head timestamps)  -- most recent commit is first line+        <$> parseTime (NE.last timestamps')  -- first commit is last line+        <*> parseTime (NE.head timestamps')  -- most recent commit is first line     _ ->       -- `null lines` should not happen, but if it does we treat it       -- the same as `ExitFailure`
src/Security/Advisories/Parse.hs view
@@ -26,9 +26,6 @@ import Data.Maybe (fromMaybe) import Data.Monoid (First(..)) -import Data.Tuple (swap)-import Control.Applicative ((<|>))- import GHC.Generics (Generic)  import Data.Sequence (Seq((:<|)))@@ -64,7 +61,6 @@ data OutOfBandAttributes = OutOfBandAttributes   { oobModified :: UTCTime   , oobPublished :: UTCTime-  , oobComponentIdentifier :: Maybe ComponentIdentifier   }   deriving (Show) @@ -94,14 +90,12 @@ -- @since 0.2.0.0 data OOBError   = StdInHasNoOOB -- ^ we obtain the advisory via stdin and can hence not parse git history-  | PathHasNoComponentIdentifier -- ^ the path is missing 'hackage' or 'ghc' directory   | GitHasNoOOB GitError -- ^ processing oob info via git failed   deriving stock (Eq, Show, Generic)  displayOOBError :: OOBError -> String displayOOBError = \case   StdInHasNoOOB -> "stdin doesn't provide out of band information"-  PathHasNoComponentIdentifier -> "the path is missing 'hackage' or 'ghc' directory"   GitHasNoOOB gitErr -> "no out of band information obtained with git error:\n"     <> explainGitError gitErr @@ -190,9 +184,6 @@             "advisory.modified"             (amdModified (frontMatterAdvisory fm))       let affected = frontMatterAffected fm-      case oob of-        Right (OutOfBandAttributes _ _ (Just ecosystem)) -> validateComponentIdentifier ecosystem affected-        _ -> pure ()       pure Advisory         { advisoryId = amdId (frontMatterAdvisory fm)         , advisoryPublished = published
src/Security/Advisories/Queries.hs view
@@ -1,74 +1,36 @@ 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 Distribution.Types.VersionRange (VersionRange) 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 = isAffectedByHelper withinRange---- | Check whether a package and a version range is concerned by an advisory-isVersionRangeAffectedBy :: Text -> 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) -> Text -> a -> Advisory -> Bool-isAffectedByHelper checkWithRange queryPackageName queryVersionish =-    any checkAffected . advisoryAffected-    where-      checkAffected :: Affected -> Bool-      checkAffected affected = case affectedComponentIdentifier affected of-        Hackage pkg -> queryPackageName == pkg && checkWithRange queryVersionish (fromAffected affected)-        -- TODO: support GHC ecosystem query, e.g. by adding a cli flag-        _ -> False--      fromAffected :: Affected -> VersionRange-      fromAffected = foldr (unionVersionRanges . fromAffectedVersionRange) noVersion . affectedVersions--      fromAffectedVersionRange :: AffectedVersionRange -> VersionRange-      fromAffectedVersionRange avr = intersectVersionRanges-        (orLaterVersion (affectedVersionRangeIntroduced avr))-        (maybe anyVersion earlierVersion (affectedVersionRangeFixed avr))- type QueryResult = Validation [(FilePath, ParseAdvisoryError)] [Advisory] --- | List the advisories matching a package name and a version+-- | List the advisories matching a component and a version listVersionAffectedBy   :: MonadIO m-  => FilePath -> Text -> Version -> m QueryResult+  => FilePath -> ComponentIdentifier -> Version -> m QueryResult listVersionAffectedBy = listAffectedByHelper isVersionAffectedBy --- | List the advisories matching a package name and a version range+-- | List the advisories matching a component and a version range listVersionRangeAffectedBy   :: (MonadIO m)-  => FilePath -> Text -> VersionRange -> m QueryResult+  => FilePath -> ComponentIdentifier -> VersionRange -> m QueryResult listVersionRangeAffectedBy = listAffectedByHelper isVersionRangeAffectedBy  -- | Helper function for 'listVersionAffectedBy' and 'listVersionRangeAffectedBy' listAffectedByHelper   :: (MonadIO m)-  => (Text -> a -> Advisory -> Bool) -> FilePath -> Text -> a -> m QueryResult-listAffectedByHelper checkAffectedBy root queryPackageName queryVersionish =-  fmap (filter (checkAffectedBy queryPackageName queryVersionish)) <$>+  => (ComponentIdentifier -> a -> Advisory -> Bool) -> FilePath -> ComponentIdentifier -> a -> m QueryResult+listAffectedByHelper checkAffectedBy root queryComponent queryVersionish =+  fmap (filter (checkAffectedBy queryComponent queryVersionish)) <$>     listAdvisories root
test/Spec.hs view
@@ -13,7 +13,6 @@ import qualified Security.Advisories.Convert.OSV as OSV import Security.Advisories.Parse import qualified Spec.FormatSpec as FormatSpec-import qualified Spec.QueriesSpec as QueriesSpec import System.Directory (listDirectory) import Test.Tasty (defaultMain, testGroup, TestTree) import Test.Tasty.Golden (goldenVsString)@@ -26,7 +25,6 @@         testGroup             "Tests"             [ goldenTestsSpec goldenFiles-            , QueriesSpec.spec             , FormatSpec.spec             ] @@ -48,7 +46,6 @@             attr = OutOfBandAttributes               { oobPublished = fakeDate               , oobModified = fakeDate-              , oobComponentIdentifier = Nothing               }             res = parseAdvisory NoOverrides (Right attr) input             osvExport = case res of
test/Spec/FormatSpec.hs view
@@ -4,7 +4,7 @@  module Spec.FormatSpec (spec) where -import Data.Fixed (Fixed (MkFixed))+import Control.Monad (replicateM) import Data.Function (on) import qualified Data.Map.Strict as Map import Data.Text (Text)@@ -15,6 +15,7 @@ import qualified Hedgehog as Gen import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range+import Network.URI (URI(URI), URIAuth(URIAuth)) import qualified Prettyprinter as Pretty import qualified Prettyprinter.Render.Text as Pretty import Security.Advisories.Core.Advisory@@ -79,9 +80,28 @@  genComponentIdentifier :: Gen.Gen ComponentIdentifier genComponentIdentifier = Gen.choice $-  [ Hackage <$> genText+  [ Repository+      <$> (RepositoryURL <$> genURI)+      <*> (RepositoryName <$> genText)+      <*> (mkPackageName . T.unpack <$> genText)+  , hackage . mkPackageName . T.unpack <$> genText   , GHC <$> Gen.enumBounded   ]++genURI :: Gen.Gen URI+genURI = do+  host   <- Gen.element ["example.com", "foo.org", "bar.net", "test.co"]+  nPath  <- Gen.int (Range.linear 0 2)+  parts  <- replicateM nPath (Gen.string (Range.linear 1 10) Gen.alphaNum)+  let path = concatMap ('/':) parts+  hasQ   <- Gen.bool+  query  <- if not hasQ+              then pure ""+              else do+                k <- Gen.string (Range.linear 1 6) Gen.alphaNum+                v <- Gen.string (Range.linear 1 6) Gen.alphaNum+                pure $ '?': k ++ "=" ++ v+  pure $ URI "https:" (Just $ URIAuth "" host "") path query ""  genCVSS :: Gen.Gen CVSS genCVSS =
− test/Spec/QueriesSpec.hs
@@ -1,194 +0,0 @@-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}--module Spec.QueriesSpec (spec) where--import Data.Bifunctor (first)-import Data.Either (fromRight)-import Data.Maybe (fromMaybe)-import Data.Text (Text)-import qualified Data.Text as T-import Distribution.Parsec (eitherParsec)-import Distribution.Types.Version (version0, alterVersion)-import Distribution.Types.VersionRange (VersionRange, VersionRangeF(..), anyVersion, projectVersionRange)-import Test.Tasty-import Test.Tasty.HUnit--import Security.CVSS (parseCVSS)-import Security.Advisories.Core.Advisory-import Security.Advisories.Core.HsecId-import Security.Advisories.Queries--spec :: TestTree-spec =-  testGroup "Queries" [-    testGroup "isAffectedBy" $-      flip concatMap cases $ map $ \(actual, query, expected) ->-        let title x y =-              if expected-                then show x <> " is vulnerable to " <> show y-                else show x <> " is not vulnerable to " <> show y-            versionRange x =-              either (\e -> error $ "Cannot parse version range " <> show x <> " : " <> show e) id $-                parseVersionRange $-                  if x == ""-                    then Nothing-                    else Just x-         in testCase (title actual query) $-              let query' = versionRange query-                  affectedVersion' = versionRange actual-              in isVersionRangeAffectedBy packageName query' (mkAdvisory affectedVersion')-                    @?= expected-  ]--cases :: [[(Text, Text, Bool)]]-cases =-  [-    reversible ("", "", True)-  , reversible ("", "==1", True)-  , reversible ("==1.1", "<=2", True)-  , reversible ("==1.1", ">1", True)-  , reversible ("==1", "==1", True)-  , reversible ("==2||==1", "==1", True)-  , reversible ("==1.1", "<=2&&>1", True)-  , reversible ("^>=1", ">1&&<1.2", True)-  , reversible (">=1", "==2", True)-  , reversible (">=1", "==1", True)-  , reversible ("==2", ">=2", True)-  , reversible ("==2", ">1", True)-  , reversible (">5", ">=2", True)-  , reversible (">5", ">2", True)-  , reversible ("==5", ">=2", True)-  , reversible ("==5", ">2", True)-  , reversible (">=5", ">2", True)-  , reversible ("<=5", ">2", True)-  , reversible ("<=5", "<=2", True)-  , reversible ("<5", ">=2", True)-  , reversible (">=2", "==5", True)-  , reversible (">2", "==5", True)-  , reversible (">5", ">=5", True)-  , reversible ("^>=1.1", ">1", True)-  , reversible ("^>=1.1", "<2", True)-  , reversible ("^>=1.1", "<=1.2", True)-  , reversible ("^>=1.1", ">1.1", True)-  , reversible ("^>=1.1", ">=1.1.5", True)-  , reversible ("^>=1.1", ">=1", True)-  , reversible ("==1.1", "<1", False)-  , reversible ("==2.1", "<=2", False)-  , reversible ("==1", ">1.1", False)-  , reversible ("==2", "==1", False)-  , reversible ("==2||==1", ">3", False)-  , notReversible ("<=2&&>1", "==3", True)-  , reversible (">=2", "==1.1", False)-  , reversible (">=1.1", "==1", False)-  , reversible ("==2", ">=2.1", False)-  , notReversible (">1", "==1", False)-  , reversible ("<2", ">=2", False)-  , reversible ("==2", ">=5", False)-  , reversible ("==2", ">5", False)-  , reversible ("<=2", ">5", False)-  , reversible ("<=2", ">5", False)-  , reversible ("<2", ">=5", False)-  , reversible (">=2", "==1.1", False)-  , reversible ("<2", "==5", False)-  , reversible ("<5", ">=5", False)-  , reversible ("^>=1.1", "<1", False)-  , reversible ("^>=1.1", "<1.1", False)-  , reversible ("^>=1.1", ">=1.2", False)-  , reversible ("^>=1.1", "<=1", False)-  , reversible ("^>=1.1", ">2", False)-  , reversible ("^>=1", ">=2", False)-  ]-  where reversible (query, affectedVersion, expected) = [(query, affectedVersion, expected), (query, affectedVersion, expected)]-        notReversible (query, affectedVersion, expected) = [(query, affectedVersion, expected), (affectedVersion, query, not expected)]--mkAdvisory :: VersionRange -> Advisory-mkAdvisory versionRange =-   Advisory-     { advisoryId = fromMaybe (error "Cannot mkHsecId") $ mkHsecId 2023 42-     , advisoryModified = read "2023-01-01T00:00:00"-     , advisoryPublished = read "2023-01-01T00:00:00"-     , advisoryCAPECs = []-     , advisoryCWEs = []-     , advisoryKeywords = []-     , advisoryAliases = [ "CVE-2022-XXXX" ]-     , advisoryRelated = [ "CVE-2022-YYYY" , "CVE-2022-ZZZZ" ]-     , advisoryAffected =-         [ Affected-             { affectedComponentIdentifier = Hackage packageName-             , affectedCVSS = cvss-             , affectedVersions = mkAffectedVersions versionRange-             , affectedArchitectures = Nothing-             , affectedOS = Nothing-             , affectedDeclarations = []-             }-         ]-     , advisoryReferences = []-     , advisoryPandoc = mempty-     , advisoryHtml = ""-     , advisorySummary = ""-     , advisoryDetails = ""-     }-  where-    cvss = fromRight (error "Cannot parseCVSS") (parseCVSS "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")--mkAffectedVersions :: VersionRange -> [AffectedVersionRange]-mkAffectedVersions vr =-  let-    fixed from to =-      AffectedVersionRange-        { affectedVersionRangeIntroduced = from-        , affectedVersionRangeFixed = Just to-        }-    onlyFixed to =-      AffectedVersionRange-        { affectedVersionRangeIntroduced = version0-        , affectedVersionRangeFixed = Just to-        }-    vulnerable from =-      AffectedVersionRange-        { affectedVersionRangeIntroduced = from-        , affectedVersionRangeFixed = Nothing-        }-    nextMinor =-      \case-        [] -> [1]-        [x] -> [x, 1]-        [x, y] -> [x, y, 1]-        [x, y, z] -> [x, y, z, 1]-        [w, x, y, z] -> [w, x, y, z + 1]-        xs -> xs ++ [1]-    previousMinor =-      \case-        [] -> [0]-        [x] -> [x - 1 , 99]-        [x, y] -> [x, y - 1, 99]-        [x, y, z] -> [x, y, z - 1, 99]-        [w, x, y, z] -> [w, x, y, z - 1]-        _ -> error "TODO"-    mkMajorBoundVersion =-      \case-        [] -> [0]-        [x] -> [x, 1]-        (x:y:_) -> [x, y + 1]-  in-  case projectVersionRange vr of-    ThisVersionF x -> [fixed x $ alterVersion (<> [0,0,1]) x]-    LaterVersionF x -> [vulnerable $ alterVersion nextMinor x]-    OrLaterVersionF x -> [vulnerable x]-    EarlierVersionF x -> [onlyFixed $ alterVersion previousMinor x]-    OrEarlierVersionF x -> [onlyFixed x]-    MajorBoundVersionF x -> [fixed x $ alterVersion mkMajorBoundVersion x]-    UnionVersionRangesF x y -> mkAffectedVersions x <> mkAffectedVersions y-    IntersectVersionRangesF x y ->-      [ low { affectedVersionRangeFixed = affectedVersionRangeFixed high }-      | low <- mkAffectedVersions x-      , high <- mkAffectedVersions y-      ]--packageName :: Text-packageName = "package-name"---- | Parse 'VersionRange' as given to the CLI-parseVersionRange :: Maybe Text -> Either Text VersionRange-parseVersionRange  = maybe (return anyVersion) (first T.pack . eitherParsec . T.unpack)
− test/golden/EXAMPLE_ADVISORY.md
@@ -1,32 +0,0 @@-```toml--[advisory]-id = "HSEC-0000-0000"-cwe = []-keywords = ["example", "freeform", "keywords"]-aliases = ["CVE-2022-XXXX"]-related = ["CVE-2022-YYYY", "CVE-2022-ZZZZ"]--[[affected]]-package = "package-name"-cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"--[[affected.versions]]-introduced = "1.0.8"-fixed = "1.1"-[[affected.versions]]-introduced = "1.1.2"--[[references]]-type = "ARTICLE"-url = "https://example.com"-```--# Advisory Template - Title Goes Here--This is an example template.-- * Markdown- * TOML "front matter".-- > Acme Broken.
− test/golden/EXAMPLE_ADVISORY.md.golden
@@ -1,259 +0,0 @@-Right-    ( Advisory-        { advisoryId = HSEC-0000-0000-        , advisoryModified = 1970-01-01 00:00:00 UTC-        , advisoryPublished = 1970-01-01 00:00:00 UTC-        , advisoryCAPECs = []-        , advisoryCWEs = []-        , advisoryKeywords =-            [ "example"-            , "freeform"-            , "keywords"-            ]-        , advisoryAliases = [ "CVE-2022-XXXX" ]-        , advisoryRelated =-            [ "CVE-2022-YYYY"-            , "CVE-2022-ZZZZ"-            ]-        , advisoryAffected =-            [ Affected-                { affectedComponentIdentifier = Hackage "package-name"-                , affectedCVSS = CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H-                , affectedVersions =-                    [ AffectedVersionRange-                        { affectedVersionRangeIntroduced = mkVersion-                            [ 1-                            , 0-                            , 8-                            ]-                        , affectedVersionRangeFixed = Just-                            ( mkVersion-                                [ 1-                                , 1-                                ]-                            )-                        }-                    , AffectedVersionRange-                        { affectedVersionRangeIntroduced = mkVersion-                            [ 1-                            , 1-                            , 2-                            ]-                        , affectedVersionRangeFixed = Nothing-                        }-                    ]-                , affectedArchitectures = Nothing-                , affectedOS = Nothing-                , affectedDeclarations = []-                }-            ]-        , advisoryReferences =-            [ Reference-                { referencesType = ReferenceTypeArticle-                , referencesUrl = "https://example.com"-                }-            ]-        , advisoryPandoc = Pandoc-            ( Meta-                { unMeta = fromList [] }-            )-            [ Header 1-                ( ""-                , []-                , []-                )-                [ Str "Advisory"-                , Space-                , Str "Template"-                , Space-                , Str "-"-                , Space-                , Str "Title"-                , Space-                , Str "Goes"-                , Space-                , Str "Here"-                ]-            , Para-                [ Str "This"-                , Space-                , Str "is"-                , Space-                , Str "an"-                , Space-                , Str "example"-                , Space-                , Str "template."-                ]-            , BulletList-                [-                    [ Plain-                        [ Str "Markdown" ]-                    ]-                ,-                    [ Plain-                        [ Str "TOML"-                        , Space-                        , Str ""front"-                        , Space-                        , Str "matter"."-                        ]-                    ]-                ]-            , BlockQuote-                [ Para-                    [ Str "Acme"-                    , Space-                    , Str "Broken."-                    ]-                ]-            ]-        , advisoryHtml = "<pre><code class="language-toml">-          [advisory]-          id = &quot;HSEC-0000-0000&quot;-          cwe = []-          keywords = [&quot;example&quot;, &quot;freeform&quot;, &quot;keywords&quot;]-          aliases = [&quot;CVE-2022-XXXX&quot;]-          related = [&quot;CVE-2022-YYYY&quot;, &quot;CVE-2022-ZZZZ&quot;]--          [[affected]]-          package = &quot;package-name&quot;-          cvss = &quot;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&quot;--          [[affected.versions]]-          introduced = &quot;1.0.8&quot;-          fixed = &quot;1.1&quot;-          [[affected.versions]]-          introduced = &quot;1.1.2&quot;--          [[references]]-          type = &quot;ARTICLE&quot;-          url = &quot;https://example.com&quot;-          </code></pre>-          <h1>Advisory Template - Title Goes Here</h1>-          <p>This is an example template.</p>-          <ul>-          <li>Markdown-          </li>-          <li>TOML &quot;front matter&quot;.-          </li>-          </ul>-          <blockquote>-          <p>Acme Broken.</p>-          </blockquote>-          "-        , advisorySummary = "Advisory Template - Title Goes Here"-        , advisoryDetails = "# Advisory Template - Title Goes Here--          This is an example template.--           * Markdown-           * TOML "front matter".--           > Acme Broken.-          "-        }-    )-Model-    { modelSchemaVersion = "1.5.0"-    , modelId = "HSEC-0000-0000"-    , modelModified = 1970-01-01 00:00:00 UTC-    , modelPublished = Just 1970-01-01 00:00:00 UTC-    , modelWithdrawn = Nothing-    , modelAliases = [ "CVE-2022-XXXX" ]-    , modelRelated =-        [ "CVE-2022-YYYY"-        , "CVE-2022-ZZZZ"-        ]-    , modelSummary = Just "Advisory Template - Title Goes Here"-    , modelDetails = Just "# Advisory Template - Title Goes Here--      This is an example template.--       * Markdown-       * TOML "front matter".--       > Acme Broken.-      "-    , modelSeverity = []-    , modelAffected =-        [ Affected-            { affectedRanges =-                [ RangeEcosystem-                    [ EventIntroduced "1.0.8"-                    , EventFixed "1.1"-                    , EventIntroduced "1.1.2"-                    ] Nothing-                ]-            , affectedPackage = Package-                { packageName = "package-name"-                , packageEcosystem = "Hackage"-                , packagePurl = Nothing-                }-            , affectedSeverity =-                [ Severity CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H ]-            , affectedEcosystemSpecific = Nothing-            , affectedDatabaseSpecific = Nothing-            }-        ]-    , modelReferences =-        [ Reference-            { referencesType = ReferenceTypeArticle-            , referencesUrl = "https://example.com"-            }-        ]-    , modelCredits = []-    , modelDatabaseSpecific = Nothing-    }-{-    "affected": [-        {-            "package": {-                "ecosystem": "Hackage",-                "name": "package-name"-            },-            "ranges": [-                {-                    "events": [-                        {-                            "introduced": "1.0.8"-                        },-                        {-                            "fixed": "1.1"-                        },-                        {-                            "introduced": "1.1.2"-                        }-                    ],-                    "type": "ECOSYSTEM"-                }-            ],-            "severity": [-                {-                    "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",-                    "type": "CVSS_V3"-                }-            ]-        }-    ],-    "aliases": [-        "CVE-2022-XXXX"-    ],-    "details": "# Advisory Template - Title Goes Here\n\nThis is an example template.\n\n * Markdown\n * TOML \"front matter\".\n\n > Acme Broken.\n",-    "id": "HSEC-0000-0000",-    "modified": "1970-01-01T00:00:00Z",-    "published": "1970-01-01T00:00:00Z",-    "references": [-        {-            "type": "ARTICLE",-            "url": "https://example.com"-        }-    ],-    "related": [-        "CVE-2022-YYYY",-        "CVE-2022-ZZZZ"-    ],-    "schema_version": "1.5.0",-    "summary": "Advisory Template - Title Goes Here"-}-
+ test/golden/EXAMPLE_ADVISORY_GHC.md view
@@ -0,0 +1,32 @@+```toml++[advisory]+id = "HSEC-0000-0000"+cwe = []+keywords = ["example", "freeform", "keywords"]+aliases = ["CVE-2022-XXXX"]+related = ["CVE-2022-YYYY", "CVE-2022-ZZZZ"]++[[affected]]+ghc-component = "ghci"+cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"++[[affected.versions]]+introduced = "1.0.8"+fixed = "1.1"+[[affected.versions]]+introduced = "1.1.2"++[[references]]+type = "ARTICLE"+url = "https://example.com"+```++# Advisory Template - Title Goes Here++This is an example template.++ * Markdown+ * TOML "front matter".++ > Acme Broken.
+ test/golden/EXAMPLE_ADVISORY_GHC.md.golden view
@@ -0,0 +1,259 @@+Right+    ( Advisory+        { advisoryId = HSEC-0000-0000+        , advisoryModified = 1970-01-01 00:00:00 UTC+        , advisoryPublished = 1970-01-01 00:00:00 UTC+        , advisoryCAPECs = []+        , advisoryCWEs = []+        , advisoryKeywords =+            [ "example"+            , "freeform"+            , "keywords"+            ]+        , advisoryAliases = [ "CVE-2022-XXXX" ]+        , advisoryRelated =+            [ "CVE-2022-YYYY"+            , "CVE-2022-ZZZZ"+            ]+        , advisoryAffected =+            [ Affected+                { affectedComponentIdentifier = GHC GHCi+                , affectedCVSS = CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H+                , affectedVersions =+                    [ AffectedVersionRange+                        { affectedVersionRangeIntroduced = mkVersion+                            [ 1+                            , 0+                            , 8+                            ]+                        , affectedVersionRangeFixed = Just+                            ( mkVersion+                                [ 1+                                , 1+                                ]+                            )+                        }+                    , AffectedVersionRange+                        { affectedVersionRangeIntroduced = mkVersion+                            [ 1+                            , 1+                            , 2+                            ]+                        , affectedVersionRangeFixed = Nothing+                        }+                    ]+                , affectedArchitectures = Nothing+                , affectedOS = Nothing+                , affectedDeclarations = []+                }+            ]+        , advisoryReferences =+            [ Reference+                { referencesType = ReferenceTypeArticle+                , referencesUrl = "https://example.com"+                }+            ]+        , advisoryPandoc = Pandoc+            ( Meta+                { unMeta = fromList [] }+            )+            [ Header 1+                ( ""+                , []+                , []+                )+                [ Str "Advisory"+                , Space+                , Str "Template"+                , Space+                , Str "-"+                , Space+                , Str "Title"+                , Space+                , Str "Goes"+                , Space+                , Str "Here"+                ]+            , Para+                [ Str "This"+                , Space+                , Str "is"+                , Space+                , Str "an"+                , Space+                , Str "example"+                , Space+                , Str "template."+                ]+            , BulletList+                [+                    [ Plain+                        [ Str "Markdown" ]+                    ]+                ,+                    [ Plain+                        [ Str "TOML"+                        , Space+                        , Str ""front"+                        , Space+                        , Str "matter"."+                        ]+                    ]+                ]+            , BlockQuote+                [ Para+                    [ Str "Acme"+                    , Space+                    , Str "Broken."+                    ]+                ]+            ]+        , advisoryHtml = "<pre><code class="language-toml">+          [advisory]+          id = &quot;HSEC-0000-0000&quot;+          cwe = []+          keywords = [&quot;example&quot;, &quot;freeform&quot;, &quot;keywords&quot;]+          aliases = [&quot;CVE-2022-XXXX&quot;]+          related = [&quot;CVE-2022-YYYY&quot;, &quot;CVE-2022-ZZZZ&quot;]++          [[affected]]+          ghc-component = &quot;ghci&quot;+          cvss = &quot;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&quot;++          [[affected.versions]]+          introduced = &quot;1.0.8&quot;+          fixed = &quot;1.1&quot;+          [[affected.versions]]+          introduced = &quot;1.1.2&quot;++          [[references]]+          type = &quot;ARTICLE&quot;+          url = &quot;https://example.com&quot;+          </code></pre>+          <h1>Advisory Template - Title Goes Here</h1>+          <p>This is an example template.</p>+          <ul>+          <li>Markdown+          </li>+          <li>TOML &quot;front matter&quot;.+          </li>+          </ul>+          <blockquote>+          <p>Acme Broken.</p>+          </blockquote>+          "+        , advisorySummary = "Advisory Template - Title Goes Here"+        , advisoryDetails = "# Advisory Template - Title Goes Here++          This is an example template.++           * Markdown+           * TOML "front matter".++           > Acme Broken.+          "+        }+    )+Model+    { modelSchemaVersion = "1.5.0"+    , modelId = "HSEC-0000-0000"+    , modelModified = 1970-01-01 00:00:00 UTC+    , modelPublished = Just 1970-01-01 00:00:00 UTC+    , modelWithdrawn = Nothing+    , modelAliases = [ "CVE-2022-XXXX" ]+    , modelRelated =+        [ "CVE-2022-YYYY"+        , "CVE-2022-ZZZZ"+        ]+    , modelSummary = Just "Advisory Template - Title Goes Here"+    , modelDetails = Just "# Advisory Template - Title Goes Here++      This is an example template.++       * Markdown+       * TOML "front matter".++       > Acme Broken.+      "+    , modelSeverity = []+    , modelAffected =+        [ Affected+            { affectedRanges =+                [ RangeEcosystem+                    [ EventIntroduced "1.0.8"+                    , EventFixed "1.1"+                    , EventIntroduced "1.1.2"+                    ] Nothing+                ]+            , affectedPackage = Package+                { packageName = "ghci"+                , packageEcosystem = "GHC"+                , packagePurl = Nothing+                }+            , affectedSeverity =+                [ Severity CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H ]+            , affectedEcosystemSpecific = Nothing+            , affectedDatabaseSpecific = Nothing+            }+        ]+    , modelReferences =+        [ Reference+            { referencesType = ReferenceTypeArticle+            , referencesUrl = "https://example.com"+            }+        ]+    , modelCredits = []+    , modelDatabaseSpecific = Nothing+    }+{+    "affected": [+        {+            "package": {+                "ecosystem": "GHC",+                "name": "ghci"+            },+            "ranges": [+                {+                    "events": [+                        {+                            "introduced": "1.0.8"+                        },+                        {+                            "fixed": "1.1"+                        },+                        {+                            "introduced": "1.1.2"+                        }+                    ],+                    "type": "ECOSYSTEM"+                }+            ],+            "severity": [+                {+                    "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",+                    "type": "CVSS_V3"+                }+            ]+        }+    ],+    "aliases": [+        "CVE-2022-XXXX"+    ],+    "details": "# Advisory Template - Title Goes Here\n\nThis is an example template.\n\n * Markdown\n * TOML \"front matter\".\n\n > Acme Broken.\n",+    "id": "HSEC-0000-0000",+    "modified": "1970-01-01T00:00:00Z",+    "published": "1970-01-01T00:00:00Z",+    "references": [+        {+            "type": "ARTICLE",+            "url": "https://example.com"+        }+    ],+    "related": [+        "CVE-2022-YYYY",+        "CVE-2022-ZZZZ"+    ],+    "schema_version": "1.5.0",+    "summary": "Advisory Template - Title Goes Here"+}+
+ test/golden/EXAMPLE_ADVISORY_HACKAGE.md view
@@ -0,0 +1,32 @@+```toml++[advisory]+id = "HSEC-0000-0000"+cwe = []+keywords = ["example", "freeform", "keywords"]+aliases = ["CVE-2022-XXXX"]+related = ["CVE-2022-YYYY", "CVE-2022-ZZZZ"]++[[affected]]+package = "package-name"+cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"++[[affected.versions]]+introduced = "1.0.8"+fixed = "1.1"+[[affected.versions]]+introduced = "1.1.2"++[[references]]+type = "ARTICLE"+url = "https://example.com"+```++# Advisory Template - Title Goes Here++This is an example template.++ * Markdown+ * TOML "front matter".++ > Acme Broken.
+ test/golden/EXAMPLE_ADVISORY_HACKAGE.md.golden view
@@ -0,0 +1,264 @@+Right+    ( Advisory+        { advisoryId = HSEC-0000-0000+        , advisoryModified = 1970-01-01 00:00:00 UTC+        , advisoryPublished = 1970-01-01 00:00:00 UTC+        , advisoryCAPECs = []+        , advisoryCWEs = []+        , advisoryKeywords =+            [ "example"+            , "freeform"+            , "keywords"+            ]+        , advisoryAliases = [ "CVE-2022-XXXX" ]+        , advisoryRelated =+            [ "CVE-2022-YYYY"+            , "CVE-2022-ZZZZ"+            ]+        , advisoryAffected =+            [ Affected+                { affectedComponentIdentifier = Repository+                    ( RepositoryURL { unRepositoryURL = https://hackage.haskell.org } )+                    ( RepositoryName+                        { unRepositoryName = "hackage" }+                    )+                    ( PackageName "package-name" )+                , affectedCVSS = CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H+                , affectedVersions =+                    [ AffectedVersionRange+                        { affectedVersionRangeIntroduced = mkVersion+                            [ 1+                            , 0+                            , 8+                            ]+                        , affectedVersionRangeFixed = Just+                            ( mkVersion+                                [ 1+                                , 1+                                ]+                            )+                        }+                    , AffectedVersionRange+                        { affectedVersionRangeIntroduced = mkVersion+                            [ 1+                            , 1+                            , 2+                            ]+                        , affectedVersionRangeFixed = Nothing+                        }+                    ]+                , affectedArchitectures = Nothing+                , affectedOS = Nothing+                , affectedDeclarations = []+                }+            ]+        , advisoryReferences =+            [ Reference+                { referencesType = ReferenceTypeArticle+                , referencesUrl = "https://example.com"+                }+            ]+        , advisoryPandoc = Pandoc+            ( Meta+                { unMeta = fromList [] }+            )+            [ Header 1+                ( ""+                , []+                , []+                )+                [ Str "Advisory"+                , Space+                , Str "Template"+                , Space+                , Str "-"+                , Space+                , Str "Title"+                , Space+                , Str "Goes"+                , Space+                , Str "Here"+                ]+            , Para+                [ Str "This"+                , Space+                , Str "is"+                , Space+                , Str "an"+                , Space+                , Str "example"+                , Space+                , Str "template."+                ]+            , BulletList+                [+                    [ Plain+                        [ Str "Markdown" ]+                    ]+                ,+                    [ Plain+                        [ Str "TOML"+                        , Space+                        , Str ""front"+                        , Space+                        , Str "matter"."+                        ]+                    ]+                ]+            , BlockQuote+                [ Para+                    [ Str "Acme"+                    , Space+                    , Str "Broken."+                    ]+                ]+            ]+        , advisoryHtml = "<pre><code class="language-toml">+          [advisory]+          id = &quot;HSEC-0000-0000&quot;+          cwe = []+          keywords = [&quot;example&quot;, &quot;freeform&quot;, &quot;keywords&quot;]+          aliases = [&quot;CVE-2022-XXXX&quot;]+          related = [&quot;CVE-2022-YYYY&quot;, &quot;CVE-2022-ZZZZ&quot;]++          [[affected]]+          package = &quot;package-name&quot;+          cvss = &quot;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&quot;++          [[affected.versions]]+          introduced = &quot;1.0.8&quot;+          fixed = &quot;1.1&quot;+          [[affected.versions]]+          introduced = &quot;1.1.2&quot;++          [[references]]+          type = &quot;ARTICLE&quot;+          url = &quot;https://example.com&quot;+          </code></pre>+          <h1>Advisory Template - Title Goes Here</h1>+          <p>This is an example template.</p>+          <ul>+          <li>Markdown+          </li>+          <li>TOML &quot;front matter&quot;.+          </li>+          </ul>+          <blockquote>+          <p>Acme Broken.</p>+          </blockquote>+          "+        , advisorySummary = "Advisory Template - Title Goes Here"+        , advisoryDetails = "# Advisory Template - Title Goes Here++          This is an example template.++           * Markdown+           * TOML "front matter".++           > Acme Broken.+          "+        }+    )+Model+    { modelSchemaVersion = "1.5.0"+    , modelId = "HSEC-0000-0000"+    , modelModified = 1970-01-01 00:00:00 UTC+    , modelPublished = Just 1970-01-01 00:00:00 UTC+    , modelWithdrawn = Nothing+    , modelAliases = [ "CVE-2022-XXXX" ]+    , modelRelated =+        [ "CVE-2022-YYYY"+        , "CVE-2022-ZZZZ"+        ]+    , modelSummary = Just "Advisory Template - Title Goes Here"+    , modelDetails = Just "# Advisory Template - Title Goes Here++      This is an example template.++       * Markdown+       * TOML "front matter".++       > Acme Broken.+      "+    , modelSeverity = []+    , modelAffected =+        [ Affected+            { affectedRanges =+                [ RangeEcosystem+                    [ EventIntroduced "1.0.8"+                    , EventFixed "1.1"+                    , EventIntroduced "1.1.2"+                    ] Nothing+                ]+            , affectedPackage = Package+                { packageName = "package-name"+                , packageEcosystem = "Hackage"+                , packagePurl = Nothing+                }+            , affectedSeverity =+                [ Severity CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H ]+            , affectedEcosystemSpecific = Nothing+            , affectedDatabaseSpecific = Nothing+            }+        ]+    , modelReferences =+        [ Reference+            { referencesType = ReferenceTypeArticle+            , referencesUrl = "https://example.com"+            }+        ]+    , modelCredits = []+    , modelDatabaseSpecific = Nothing+    }+{+    "affected": [+        {+            "package": {+                "ecosystem": "Hackage",+                "name": "package-name"+            },+            "ranges": [+                {+                    "events": [+                        {+                            "introduced": "1.0.8"+                        },+                        {+                            "fixed": "1.1"+                        },+                        {+                            "introduced": "1.1.2"+                        }+                    ],+                    "type": "ECOSYSTEM"+                }+            ],+            "severity": [+                {+                    "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",+                    "type": "CVSS_V3"+                }+            ]+        }+    ],+    "aliases": [+        "CVE-2022-XXXX"+    ],+    "details": "# Advisory Template - Title Goes Here\n\nThis is an example template.\n\n * Markdown\n * TOML \"front matter\".\n\n > Acme Broken.\n",+    "id": "HSEC-0000-0000",+    "modified": "1970-01-01T00:00:00Z",+    "published": "1970-01-01T00:00:00Z",+    "references": [+        {+            "type": "ARTICLE",+            "url": "https://example.com"+        }+    ],+    "related": [+        "CVE-2022-YYYY",+        "CVE-2022-ZZZZ"+    ],+    "schema_version": "1.5.0",+    "summary": "Advisory Template - Title Goes Here"+}+
+ test/golden/EXAMPLE_ADVISORY_REPO.md view
@@ -0,0 +1,34 @@+```toml++[advisory]+id = "HSEC-0000-0000"+cwe = []+keywords = ["example", "freeform", "keywords"]+aliases = ["CVE-2022-XXXX"]+related = ["CVE-2022-YYYY", "CVE-2022-ZZZZ"]++[[affected]]+repository-url = "https://hackage.example.org/"+repository-name = "example"+package = "package-name"+cvss = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"++[[affected.versions]]+introduced = "1.0.8"+fixed = "1.1"+[[affected.versions]]+introduced = "1.1.2"++[[references]]+type = "ARTICLE"+url = "https://example.com"+```++# Advisory Template - Title Goes Here++This is an example template.++ * Markdown+ * TOML "front matter".++ > Acme Broken.
+ test/golden/EXAMPLE_ADVISORY_REPO.md.golden view
@@ -0,0 +1,266 @@+Right+    ( Advisory+        { advisoryId = HSEC-0000-0000+        , advisoryModified = 1970-01-01 00:00:00 UTC+        , advisoryPublished = 1970-01-01 00:00:00 UTC+        , advisoryCAPECs = []+        , advisoryCWEs = []+        , advisoryKeywords =+            [ "example"+            , "freeform"+            , "keywords"+            ]+        , advisoryAliases = [ "CVE-2022-XXXX" ]+        , advisoryRelated =+            [ "CVE-2022-YYYY"+            , "CVE-2022-ZZZZ"+            ]+        , advisoryAffected =+            [ Affected+                { affectedComponentIdentifier = Repository+                    ( RepositoryURL { unRepositoryURL = https://hackage.example.org/ } )+                    ( RepositoryName+                        { unRepositoryName = "example" }+                    )+                    ( PackageName "package-name" )+                , affectedCVSS = CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H+                , affectedVersions =+                    [ AffectedVersionRange+                        { affectedVersionRangeIntroduced = mkVersion+                            [ 1+                            , 0+                            , 8+                            ]+                        , affectedVersionRangeFixed = Just+                            ( mkVersion+                                [ 1+                                , 1+                                ]+                            )+                        }+                    , AffectedVersionRange+                        { affectedVersionRangeIntroduced = mkVersion+                            [ 1+                            , 1+                            , 2+                            ]+                        , affectedVersionRangeFixed = Nothing+                        }+                    ]+                , affectedArchitectures = Nothing+                , affectedOS = Nothing+                , affectedDeclarations = []+                }+            ]+        , advisoryReferences =+            [ Reference+                { referencesType = ReferenceTypeArticle+                , referencesUrl = "https://example.com"+                }+            ]+        , advisoryPandoc = Pandoc+            ( Meta+                { unMeta = fromList [] }+            )+            [ Header 1+                ( ""+                , []+                , []+                )+                [ Str "Advisory"+                , Space+                , Str "Template"+                , Space+                , Str "-"+                , Space+                , Str "Title"+                , Space+                , Str "Goes"+                , Space+                , Str "Here"+                ]+            , Para+                [ Str "This"+                , Space+                , Str "is"+                , Space+                , Str "an"+                , Space+                , Str "example"+                , Space+                , Str "template."+                ]+            , BulletList+                [+                    [ Plain+                        [ Str "Markdown" ]+                    ]+                ,+                    [ Plain+                        [ Str "TOML"+                        , Space+                        , Str ""front"+                        , Space+                        , Str "matter"."+                        ]+                    ]+                ]+            , BlockQuote+                [ Para+                    [ Str "Acme"+                    , Space+                    , Str "Broken."+                    ]+                ]+            ]+        , advisoryHtml = "<pre><code class="language-toml">+          [advisory]+          id = &quot;HSEC-0000-0000&quot;+          cwe = []+          keywords = [&quot;example&quot;, &quot;freeform&quot;, &quot;keywords&quot;]+          aliases = [&quot;CVE-2022-XXXX&quot;]+          related = [&quot;CVE-2022-YYYY&quot;, &quot;CVE-2022-ZZZZ&quot;]++          [[affected]]+          repository-url = &quot;https://hackage.example.org/&quot;+          repository-name = &quot;example&quot;+          package = &quot;package-name&quot;+          cvss = &quot;CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H&quot;++          [[affected.versions]]+          introduced = &quot;1.0.8&quot;+          fixed = &quot;1.1&quot;+          [[affected.versions]]+          introduced = &quot;1.1.2&quot;++          [[references]]+          type = &quot;ARTICLE&quot;+          url = &quot;https://example.com&quot;+          </code></pre>+          <h1>Advisory Template - Title Goes Here</h1>+          <p>This is an example template.</p>+          <ul>+          <li>Markdown+          </li>+          <li>TOML &quot;front matter&quot;.+          </li>+          </ul>+          <blockquote>+          <p>Acme Broken.</p>+          </blockquote>+          "+        , advisorySummary = "Advisory Template - Title Goes Here"+        , advisoryDetails = "# Advisory Template - Title Goes Here++          This is an example template.++           * Markdown+           * TOML "front matter".++           > Acme Broken.+          "+        }+    )+Model+    { modelSchemaVersion = "1.5.0"+    , modelId = "HSEC-0000-0000"+    , modelModified = 1970-01-01 00:00:00 UTC+    , modelPublished = Just 1970-01-01 00:00:00 UTC+    , modelWithdrawn = Nothing+    , modelAliases = [ "CVE-2022-XXXX" ]+    , modelRelated =+        [ "CVE-2022-YYYY"+        , "CVE-2022-ZZZZ"+        ]+    , modelSummary = Just "Advisory Template - Title Goes Here"+    , modelDetails = Just "# Advisory Template - Title Goes Here++      This is an example template.++       * Markdown+       * TOML "front matter".++       > Acme Broken.+      "+    , modelSeverity = []+    , modelAffected =+        [ Affected+            { affectedRanges =+                [ RangeEcosystem+                    [ EventIntroduced "1.0.8"+                    , EventFixed "1.1"+                    , EventIntroduced "1.1.2"+                    ] Nothing+                ]+            , affectedPackage = Package+                { packageName = "package-name"+                , packageEcosystem = "example"+                , packagePurl = Nothing+                }+            , affectedSeverity =+                [ Severity CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H ]+            , affectedEcosystemSpecific = Nothing+            , affectedDatabaseSpecific = Nothing+            }+        ]+    , modelReferences =+        [ Reference+            { referencesType = ReferenceTypeArticle+            , referencesUrl = "https://example.com"+            }+        ]+    , modelCredits = []+    , modelDatabaseSpecific = Nothing+    }+{+    "affected": [+        {+            "package": {+                "ecosystem": "example",+                "name": "package-name"+            },+            "ranges": [+                {+                    "events": [+                        {+                            "introduced": "1.0.8"+                        },+                        {+                            "fixed": "1.1"+                        },+                        {+                            "introduced": "1.1.2"+                        }+                    ],+                    "type": "ECOSYSTEM"+                }+            ],+            "severity": [+                {+                    "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",+                    "type": "CVSS_V3"+                }+            ]+        }+    ],+    "aliases": [+        "CVE-2022-XXXX"+    ],+    "details": "# Advisory Template - Title Goes Here\n\nThis is an example template.\n\n * Markdown\n * TOML \"front matter\".\n\n > Acme Broken.\n",+    "id": "HSEC-0000-0000",+    "modified": "1970-01-01T00:00:00Z",+    "published": "1970-01-01T00:00:00Z",+    "references": [+        {+            "type": "ARTICLE",+            "url": "https://example.com"+        }+    ],+    "related": [+        "CVE-2022-YYYY",+        "CVE-2022-ZZZZ"+    ],+    "schema_version": "1.5.0",+    "summary": "Advisory Template - Title Goes Here"+}+