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 +14/−0
- README.md +1/−1
- app/Main.hs +34/−12
- hsec-tools.cabal +18/−18
- src/Security/Advisories/Convert/OSV.hs +108/−31
- src/Security/Advisories/Filesystem.hs +36/−51
- src/Security/Advisories/Format.hs +32/−4
- src/Security/Advisories/Generate/HTML.hs +86/−75
- src/Security/Advisories/Generate/Snapshot.hs +20/−4
- src/Security/Advisories/Git.hs +4/−3
- src/Security/Advisories/Parse.hs +0/−9
- src/Security/Advisories/Queries.hs +8/−46
- test/Spec.hs +0/−3
- test/Spec/FormatSpec.hs +22/−2
- test/Spec/QueriesSpec.hs +0/−194
- test/golden/EXAMPLE_ADVISORY.md +0/−32
- test/golden/EXAMPLE_ADVISORY.md.golden +0/−259
- test/golden/EXAMPLE_ADVISORY_GHC.md +32/−0
- test/golden/EXAMPLE_ADVISORY_GHC.md.golden +259/−0
- test/golden/EXAMPLE_ADVISORY_HACKAGE.md +32/−0
- test/golden/EXAMPLE_ADVISORY_HACKAGE.md.golden +264/−0
- test/golden/EXAMPLE_ADVISORY_REPO.md +34/−0
- test/golden/EXAMPLE_ADVISORY_REPO.md.golden +266/−0
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 = "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"- </code></pre>- <h1>Advisory Template - Title Goes Here</h1>- <p>This is an example template.</p>- <ul>- <li>Markdown- </li>- <li>TOML "front matter".- </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 = "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"+ </code></pre>+ <h1>Advisory Template - Title Goes Here</h1>+ <p>This is an example template.</p>+ <ul>+ <li>Markdown+ </li>+ <li>TOML "front matter".+ </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 = "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"+ </code></pre>+ <h1>Advisory Template - Title Goes Here</h1>+ <p>This is an example template.</p>+ <ul>+ <li>Markdown+ </li>+ <li>TOML "front matter".+ </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 = "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"+ </code></pre>+ <h1>Advisory Template - Title Goes Here</h1>+ <p>This is an example template.</p>+ <ul>+ <li>Markdown+ </li>+ <li>TOML "front matter".+ </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"+}+