packages feed

hsec-tools 0.1.0.0 → 0.2.0.2

raw patch · 22 files changed

+1531/−701 lines, 22 filesdep +atom-conduitdep +conduitdep +conduit-extradep −feeddep ~Cabal-syntaxdep ~basedep ~bytestringnew-uploaderbinary-addedPVP ok

version bump matches the API change (PVP)

Dependencies added: atom-conduit, conduit, conduit-extra, data-default, file-embed, hedgehog, pandoc, pretty, prettyprinter, refined, resourcet, tasty-hedgehog, template-haskell, transformers, uri-bytestring, xml-conduit

Dependencies removed: feed

Dependency ranges changed: Cabal-syntax, base, bytestring, containers, cvss, extra, filepath, hsec-core, osv, tasty, time, toml-parser

API changes (from Hackage documentation)

- Security.Advisories.Parse: emptyOutOfBandAttributes :: OutOfBandAttributes
- Security.Advisories.Parse: instance GHC.Generics.Generic Security.Advisories.Parse.FrontMatter
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Distribution.Types.Version.Version
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Distribution.Types.VersionRange.Internal.VersionRange
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.Affected
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.AffectedVersionRange
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.Architecture
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.CAPEC
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.CWE
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.Keyword
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.OS
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.HsecId.HsecId
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Parse.AdvisoryMetadata
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.Advisories.Parse.FrontMatter
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.CVSS.CVSS
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.OSV.Reference
- Security.Advisories.Parse: instance Toml.Schema.FromValue.FromValue Security.OSV.ReferenceType
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToTable Security.Advisories.Core.Advisory.Affected
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToTable Security.Advisories.Core.Advisory.AffectedVersionRange
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToTable Security.Advisories.Parse.AdvisoryMetadata
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToTable Security.Advisories.Parse.FrontMatter
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToTable Security.OSV.Reference
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Distribution.Types.Version.Version
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Distribution.Types.VersionRange.Internal.VersionRange
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.Affected
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.AffectedVersionRange
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.Architecture
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.CAPEC
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.CWE
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.Keyword
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.OS
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.HsecId.HsecId
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Parse.AdvisoryMetadata
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.Advisories.Parse.FrontMatter
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.CVSS.CVSS
- Security.Advisories.Parse: instance Toml.Schema.ToValue.ToValue Security.OSV.Reference
+ Security.Advisories.Filesystem: advisoryFromFile :: MonadIO m => FilePath -> m (Validation ParseAdvisoryError Advisory)
+ Security.Advisories.Filesystem: parseComponentIdentifier :: forall (m :: Type -> Type). Monad m => FilePath -> ExceptT OOBError m (Maybe ComponentIdentifier)
+ Security.Advisories.Format: AdvisoryMetadata :: HsecId -> Maybe UTCTime -> Maybe UTCTime -> [CAPEC] -> [CWE] -> [Keyword] -> [Text] -> [Text] -> AdvisoryMetadata
+ Security.Advisories.Format: FrontMatter :: AdvisoryMetadata -> [Reference] -> [Affected] -> FrontMatter
+ Security.Advisories.Format: [amdAliases] :: AdvisoryMetadata -> [Text]
+ Security.Advisories.Format: [amdCAPECs] :: AdvisoryMetadata -> [CAPEC]
+ Security.Advisories.Format: [amdCWEs] :: AdvisoryMetadata -> [CWE]
+ Security.Advisories.Format: [amdId] :: AdvisoryMetadata -> HsecId
+ Security.Advisories.Format: [amdKeywords] :: AdvisoryMetadata -> [Keyword]
+ Security.Advisories.Format: [amdModified] :: AdvisoryMetadata -> Maybe UTCTime
+ Security.Advisories.Format: [amdPublished] :: AdvisoryMetadata -> Maybe UTCTime
+ Security.Advisories.Format: [amdRelated] :: AdvisoryMetadata -> [Text]
+ Security.Advisories.Format: [frontMatterAdvisory] :: FrontMatter -> AdvisoryMetadata
+ Security.Advisories.Format: [frontMatterAffected] :: FrontMatter -> [Affected]
+ Security.Advisories.Format: [frontMatterReferences] :: FrontMatter -> [Reference]
+ Security.Advisories.Format: class FromValue a
+ Security.Advisories.Format: class ToValue a => ToTable a
+ Security.Advisories.Format: class ToValue a
+ Security.Advisories.Format: data AdvisoryMetadata
+ Security.Advisories.Format: data FrontMatter
+ Security.Advisories.Format: fromAdvisory :: Advisory -> FrontMatter
+ Security.Advisories.Format: fromValue :: FromValue a => Value' l -> Matcher l a
+ Security.Advisories.Format: instance Commonmark.Types.HasAttributes Security.Advisories.Format.FirstSourceRange
+ Security.Advisories.Format: instance Commonmark.Types.IsBlock Security.Advisories.Format.FirstSourceRange Security.Advisories.Format.FirstSourceRange
+ Security.Advisories.Format: instance Commonmark.Types.IsInline Security.Advisories.Format.FirstSourceRange
+ Security.Advisories.Format: instance Commonmark.Types.Rangeable Security.Advisories.Format.FirstSourceRange
+ Security.Advisories.Format: instance GHC.Base.Monoid Security.Advisories.Format.FirstSourceRange
+ Security.Advisories.Format: instance GHC.Base.Semigroup Security.Advisories.Format.FirstSourceRange
+ Security.Advisories.Format: instance GHC.Generics.Generic Security.Advisories.Format.AdvisoryMetadata
+ Security.Advisories.Format: instance GHC.Generics.Generic Security.Advisories.Format.FrontMatter
+ Security.Advisories.Format: instance GHC.Show.Show Security.Advisories.Format.AdvisoryMetadata
+ Security.Advisories.Format: instance GHC.Show.Show Security.Advisories.Format.FirstSourceRange
+ Security.Advisories.Format: instance GHC.Show.Show Security.Advisories.Format.FrontMatter
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Distribution.Types.Version.Version
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Distribution.Types.VersionRange.Internal.VersionRange
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.Affected
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.AffectedVersionRange
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.Architecture
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.CAPEC
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.CWE
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.GHCComponent
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.Keyword
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.Advisory.OS
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Core.HsecId.HsecId
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Format.AdvisoryMetadata
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.Advisories.Format.FrontMatter
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.CVSS.CVSS
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.OSV.Reference
+ Security.Advisories.Format: instance Toml.Schema.FromValue.FromValue Security.OSV.ReferenceType
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToTable Security.Advisories.Core.Advisory.Affected
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToTable Security.Advisories.Core.Advisory.AffectedVersionRange
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToTable Security.Advisories.Format.AdvisoryMetadata
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToTable Security.Advisories.Format.FrontMatter
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToTable Security.OSV.Reference
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Distribution.Types.Version.Version
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Distribution.Types.VersionRange.Internal.VersionRange
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.Affected
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.AffectedVersionRange
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.Architecture
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.CAPEC
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.CWE
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.GHCComponent
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.Keyword
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.Advisory.OS
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Core.HsecId.HsecId
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Format.AdvisoryMetadata
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.Advisories.Format.FrontMatter
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.CVSS.CVSS
+ Security.Advisories.Format: instance Toml.Schema.ToValue.ToValue Security.OSV.Reference
+ Security.Advisories.Format: listFromValue :: FromValue a => Value' l -> Matcher l [a]
+ Security.Advisories.Format: toTable :: ToTable a => a -> Table
+ Security.Advisories.Format: toValue :: ToValue a => a -> Value
+ Security.Advisories.Format: toValueList :: ToValue a => [a] -> Value
+ Security.Advisories.Generate.Snapshot: createSnapshot :: FilePath -> FilePath -> IO ()
+ Security.Advisories.Generate.Snapshot: instance Data.Aeson.Types.ToJSON.ToJSON Security.Advisories.Generate.Snapshot.SnapshotMetadata
+ Security.Advisories.Generate.Snapshot: instance GHC.Generics.Generic Security.Advisories.Generate.Snapshot.SnapshotMetadata
+ Security.Advisories.Generate.TH: fileLocation :: FilePath -> (FilePath -> Q Exp) -> Q Exp
+ Security.Advisories.Generate.TH: readDirFilesTH :: FilePath -> Q Exp
+ Security.Advisories.Generate.TH: readFileTH :: FilePath -> Q Exp
+ Security.Advisories.Git: instance GHC.Classes.Eq Security.Advisories.Git.GitError
+ Security.Advisories.Git: instance GHC.Classes.Ord Security.Advisories.Git.GitError
+ Security.Advisories.Parse: GitHasNoOOB :: GitError -> OOBError
+ Security.Advisories.Parse: PathHasNoComponentIdentifier :: OOBError
+ Security.Advisories.Parse: StdInHasNoOOB :: OOBError
+ Security.Advisories.Parse: [oobComponentIdentifier] :: OutOfBandAttributes -> Maybe ComponentIdentifier
+ Security.Advisories.Parse: data OOBError
+ Security.Advisories.Parse: displayOOBError :: OOBError -> String
+ Security.Advisories.Parse: instance GHC.Classes.Eq Security.Advisories.Parse.OOBError
+ Security.Advisories.Parse: instance GHC.Exception.Type.Exception Security.Advisories.Parse.ParseAdvisoryError
+ Security.Advisories.Parse: instance GHC.Generics.Generic Security.Advisories.Parse.OOBError
+ Security.Advisories.Parse: instance GHC.Show.Show Security.Advisories.Parse.OOBError
+ Security.Advisories.Parse: type OOB = Either OOBError OutOfBandAttributes
+ Security.Advisories.Parse: validateComponentIdentifier :: MonadFail m => ComponentIdentifier -> [Affected] -> m ()
- Security.Advisories.Filesystem: listAdvisories :: MonadIO m => FilePath -> m (Validation [ParseAdvisoryError] [Advisory])
+ Security.Advisories.Filesystem: listAdvisories :: MonadIO m => FilePath -> m (Validation [(FilePath, ParseAdvisoryError)] [Advisory])
- Security.Advisories.Git: AdvisoryGitInfo :: ZonedTime -> ZonedTime -> AdvisoryGitInfo
+ Security.Advisories.Git: AdvisoryGitInfo :: UTCTime -> UTCTime -> AdvisoryGitInfo
- Security.Advisories.Git: [firstAppearanceCommitDate] :: AdvisoryGitInfo -> ZonedTime
+ Security.Advisories.Git: [firstAppearanceCommitDate] :: AdvisoryGitInfo -> UTCTime
- Security.Advisories.Git: [lastModificationCommitDate] :: AdvisoryGitInfo -> ZonedTime
+ Security.Advisories.Git: [lastModificationCommitDate] :: AdvisoryGitInfo -> UTCTime
- Security.Advisories.Parse: OutOfBandAttributes :: Maybe ZonedTime -> Maybe ZonedTime -> OutOfBandAttributes
+ Security.Advisories.Parse: OutOfBandAttributes :: UTCTime -> UTCTime -> Maybe ComponentIdentifier -> OutOfBandAttributes
- Security.Advisories.Parse: [oobModified] :: OutOfBandAttributes -> Maybe ZonedTime
+ Security.Advisories.Parse: [oobModified] :: OutOfBandAttributes -> UTCTime
- Security.Advisories.Parse: [oobPublished] :: OutOfBandAttributes -> Maybe ZonedTime
+ Security.Advisories.Parse: [oobPublished] :: OutOfBandAttributes -> UTCTime
- Security.Advisories.Parse: parseAdvisory :: AttributeOverridePolicy -> OutOfBandAttributes -> Text -> Either ParseAdvisoryError Advisory
+ Security.Advisories.Parse: parseAdvisory :: AttributeOverridePolicy -> OOB -> Text -> Either ParseAdvisoryError Advisory
- Security.Advisories.Queries: listVersionAffectedBy :: MonadIO m => FilePath -> Text -> Version -> m (Validation [ParseAdvisoryError] [Advisory])
+ Security.Advisories.Queries: listVersionAffectedBy :: MonadIO m => FilePath -> Text -> Version -> m QueryResult
- Security.Advisories.Queries: listVersionRangeAffectedBy :: MonadIO m => FilePath -> Text -> VersionRange -> m (Validation [ParseAdvisoryError] [Advisory])
+ Security.Advisories.Queries: listVersionRangeAffectedBy :: MonadIO m => FilePath -> Text -> VersionRange -> m QueryResult

Files

CHANGELOG.md view
@@ -0,0 +1,12 @@+## 0.2.0.2++* Update `tasty` dependency bounds+* Update `osv` dependency bounds++## 0.2.0.1++- Rework HTML/Atom generation, use `atom-conduit` instead of `feed`++## 0.1.1.0++- Redesign index
+ README.md view
@@ -0,0 +1,15 @@+# hsec-tools++`hesc-tools` aims to support [Haskell advisories database](https://github.com/haskell/security-advisories).++## Building++We aim to support both regular cabal-based and nix-based builds.++## Testing++Run (and auto update) the golden test:++```ShellSession+cabal test -O0 --test-show-details=direct --test-option=--accept+```
+ app/Command/NextID.hs view
@@ -0,0 +1,10 @@+module Command.NextID where++import Security.Advisories.Core.HsecId (printHsecId, getNextHsecId)+import Security.Advisories.Filesystem (getGreatestId)++import Util (ensureRepo)++runNextIDCommand :: Maybe FilePath -> IO ()+runNextIDCommand mPath =+  ensureRepo mPath >>= getGreatestId >>= getNextHsecId >>= putStrLn . printHsecId
app/Command/Reserve.hs view
@@ -2,8 +2,7 @@  module Command.Reserve where -import Control.Monad (unless, when)-import Data.Maybe (fromMaybe)+import Control.Monad (when) import System.Exit (die) import System.FilePath ((</>), (<.>)) @@ -11,7 +10,6 @@   ( add   , commit   , explainGitError-  , getRepoRoot   ) import Security.Advisories.Core.HsecId   ( placeholder@@ -21,10 +19,11 @@ import Security.Advisories.Filesystem   ( dirNameAdvisories   , dirNameReserved-  , isSecurityAdvisoriesRepo   , getGreatestId   ) +import Util (ensureRepo)+ -- | How to choose IDs when creating advisories or -- reservations. data IdMode@@ -40,14 +39,7 @@  runReserveCommand :: Maybe FilePath -> IdMode -> CommitFlag -> IO () runReserveCommand mPath idMode commitFlag = do-  let-    path = fromMaybe "." mPath-  repoPath <- getRepoRoot path >>= \case-    Left _ -> die "Not a git repo"-    Right a -> pure a-  isRepo <- isSecurityAdvisoriesRepo repoPath-  unless isRepo $-    die "Not a security-advisories repo"+  repoPath <- ensureRepo mPath    hsid <- case idMode of     IdModePlaceholder -> pure placeholder
app/Main.hs view
@@ -1,39 +1,42 @@-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}  module Main where  import Control.Monad (forM_, join, void, when)+import Control.Monad.Trans.Except (runExceptT, ExceptT (ExceptT), withExceptT, throwE)+import Control.Monad.IO.Class (liftIO)+import qualified Data.Aeson import qualified Data.ByteString.Lazy as L-import Data.Maybe (fromMaybe) import Data.Foldable (for_)-import Data.Functor ((<&>)) import Data.List (intercalate, isPrefixOf)-import Distribution.Parsec (eitherParsec)-import Distribution.Types.VersionRange (VersionRange, anyVersion)-import System.Exit (die, exitFailure, exitSuccess)-import System.IO (hPrint, hPutStrLn, stderr)-import System.FilePath (takeBaseName)-import Validation (Validation(..))--import qualified Data.Aeson+import Data.Maybe (fromMaybe) import qualified Data.Text as T import qualified Data.Text.IO as T+import Control.Exception (Exception(displayException))+import Distribution.Parsec (eitherParsec)+import Distribution.Types.VersionRange (VersionRange, anyVersion) import Options.Applicative- import Security.Advisories import qualified Security.Advisories.Convert.OSV as OSV+import Security.Advisories.Generate.HTML+import Security.Advisories.Generate.Snapshot import Security.Advisories.Git import Security.Advisories.Queries (listVersionRangeAffectedBy)-import Security.Advisories.Generate.HTML+import Security.Advisories.Filesystem (parseComponentIdentifier)+import System.Exit (die, exitFailure, exitSuccess)+import System.FilePath (takeBaseName)+import System.IO (hPrint, hPutStrLn, stderr)+import Validation (Validation (..))  import qualified Command.Reserve+import qualified Command.NextID  main :: IO ()-main = join $-  customExecParser-    (prefs showHelpOnEmpty)-    cliOpts+main =+  join $+    customExecParser+      (prefs showHelpOnEmpty)+      cliOpts  cliOpts :: ParserInfo (IO ()) cliOpts = info (commandsParser <**> helper) (fullDesc <> header "Haskell Advisories tools")@@ -41,55 +44,63 @@     commandsParser :: Parser (IO ())     commandsParser =       hsubparser-        (  command "check" (info commandCheck (progDesc "Syntax check a single advisory"))-        <> command "reserve" (info commandReserve (progDesc "Reserve an HSEC ID"))-        <> command "osv" (info commandOsv (progDesc "Convert a single advisory to OSV"))-        <> command "render" (info commandRender (progDesc "Render a single advisory as HTML"))-        <> command "generate-index" (info commandGenerateIndex (progDesc "Generate an HTML index"))-        <> command "query" (info commandQuery (progDesc "Run various queries against the database"))-        <> command "help" (info commandHelp (progDesc "Show command help"))+        ( command "check" (info commandCheck (progDesc "Syntax check a single advisory"))+            <> command "next-id" (info commandNextID (progDesc "Print the next available HSEC ID"))+            <> command "reserve" (info commandReserve (progDesc "Reserve an HSEC ID"))+            <> command "osv" (info commandOsv (progDesc "Convert a single advisory to OSV"))+            <> command "render" (info commandRender (progDesc "Render a single advisory as HTML"))+            <> command "generate-index" (info commandGenerateIndex (progDesc "Generate an HTML index"))+            <> command "generate-snapshot" (info commandGenerateSnapshot (progDesc "Generate a snapshot from a Git repository"))+            <> command "query" (info commandQuery (progDesc "Run various queries against the database"))+            <> command "help" (info commandHelp (progDesc "Show command help"))         )  -- | Create an option with a fixed set of values multiOption :: [(String, a)] -> Mod OptionFields a -> Parser a multiOption kvs m = option rdr (m <> metavar choices)   where-  choices = "{" <> intercalate "|" (fmap fst kvs) <> "}"-  errMsg = "must be one of " <> choices-  rdr = eitherReader (maybe (Left errMsg) Right . flip lookup kvs)+    choices = "{" <> intercalate "|" (fmap fst kvs) <> "}"+    errMsg = "must be one of " <> choices+    rdr = eitherReader (maybe (Left errMsg) Right . flip lookup kvs)  commandReserve :: Parser (IO ()) commandReserve =   Command.Reserve.runReserveCommand-  <$> optional (argument str (metavar "REPO"))-  <*> multiOption-        [ ("placeholder", Command.Reserve.IdModePlaceholder)-        , ("auto",        Command.Reserve.IdModeAuto)-        ]-        ( long "id-mode" <> help "How to assign IDs" )-  <*> flag-        Command.Reserve.DoNotCommit -- default value-        Command.Reserve.Commit      -- active value-        ( long "commit"-        <> help "Commit the reservation file"-        )+    <$> optional (argument str (metavar "REPO"))+    <*> multiOption+      [ ("placeholder", Command.Reserve.IdModePlaceholder),+        ("auto", Command.Reserve.IdModeAuto)+      ]+      (long "id-mode" <> help "How to assign IDs")+    <*> flag+      Command.Reserve.DoNotCommit -- default value+      Command.Reserve.Commit -- active value+      ( long "commit"+          <> help "Commit the reservation file"+      ) +commandNextID :: Parser (IO ())+commandNextID =+  Command.NextID.runNextIDCommand+    <$> optional (argument str (metavar "REPO"))+ commandCheck :: Parser (IO ()) commandCheck =   withAdvisory go-  <$> optional (argument str (metavar "FILE"))+    <$> optional (argument str (metavar "FILE"))   where     go mPath advisory = do       for_ mPath $ \path -> do         let base = takeBaseName path         when ("HSEC-" `isPrefixOf` base && base /= printHsecId (advisoryId advisory)) $-          die $ "Filename does not match advisory ID: " <> path+          die $+            "Filename does not match advisory ID: " <> path       T.putStrLn "no error"  commandOsv :: Parser (IO ()) commandOsv =   withAdvisory go-  <$> optional (argument str (metavar "FILE"))+    <$> optional (argument str (metavar "FILE"))   where     go _ adv = do       L.putStr (Data.Aeson.encode (OSV.convert adv))@@ -98,7 +109,7 @@ commandRender :: Parser (IO ()) commandRender =   withAdvisory (\_ -> T.putStrLn . advisoryHtml)-  <$> optional (argument str (metavar "FILE"))+    <$> optional (argument str (metavar "FILE"))  commandQuery :: Parser (IO ()) commandQuery =@@ -112,22 +123,23 @@         <$> argument str (metavar "PACKAGE")         <*> 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-              let versionRange' = fromMaybe anyVersion versionRange-              maybeAffectedAdvisories <- listVersionRangeAffectedBy (fromMaybe "." advisoriesPath) packageName versionRange'-              case maybeAffectedAdvisories of-                Validation.Failure errors -> do-                  T.hPutStrLn stderr "Cannot parse some advisories"-                  forM_ errors $-                    hPrint stderr-                  exitFailure-                Validation.Success [] -> putStrLn "Not affected"-                Validation.Success affectedAdvisories -> do-                  hPutStrLn stderr "Affected by:"-                  forM_ affectedAdvisories $ \advisory ->-                    T.hPutStrLn stderr $ "* [" <> T.pack (printHsecId $ advisoryId advisory) <> "] " <> advisorySummary advisory-                  exitFailure+      where+        go :: T.Text -> Maybe VersionRange -> Maybe FilePath -> IO ()+        go packageName versionRange advisoriesPath = do+          let versionRange' = fromMaybe anyVersion versionRange+          maybeAffectedAdvisories <- listVersionRangeAffectedBy (fromMaybe "." advisoriesPath) packageName versionRange'+          case maybeAffectedAdvisories of+            Validation.Failure errors -> do+              T.hPutStrLn stderr "Cannot parse some advisories"+              forM_ errors $+                hPrint stderr+              exitFailure+            Validation.Success [] -> putStrLn "Not affected"+            Validation.Success affectedAdvisories -> do+              hPutStrLn stderr "Affected by:"+              forM_ affectedAdvisories $ \advisory ->+                T.hPutStrLn stderr $ "* [" <> T.pack (printHsecId $ advisoryId advisory) <> "] " <> advisorySummary advisory+              exitFailure  commandGenerateIndex :: Parser (IO ()) commandGenerateIndex =@@ -135,16 +147,25 @@       renderAdvisoriesIndex src dst       T.putStrLn "Index generated"   )-  <$> argument str (metavar "SOURCE-DIR")-  <*> argument str (metavar "DESTINATION-DIR")+    <$> argument str (metavar "SOURCE-DIR")+    <*> argument str (metavar "DESTINATION-DIR") +commandGenerateSnapshot :: Parser (IO ())+commandGenerateSnapshot =+  ( \src dst -> do+      createSnapshot src dst+      T.putStrLn "Snapshot generated"+  )+    <$> argument str (metavar "SOURCE-DIR")+    <*> argument str (metavar "DESTINATION-DIR")+ commandHelp :: Parser (IO ()) commandHelp =   ( \mCmd ->       let args = maybe id (:) mCmd ["-h"]-      in void $ handleParseResult $ execParserPure defaultPrefs cliOpts args+       in void $ handleParseResult $ execParserPure defaultPrefs cliOpts args   )-  <$> optional (argument str (metavar "COMMAND"))+    <$> optional (argument str (metavar "COMMAND"))  versionRangeReader :: ReadM VersionRange versionRangeReader = eitherReader eitherParsec@@ -153,24 +174,21 @@ withAdvisory go file = do   input <- maybe T.getContents T.readFile file -  oob <- ($ emptyOutOfBandAttributes) <$> case file of-    Nothing -> pure id-    Just path ->-      getAdvisoryGitInfo path <&> \case-        Left _ -> id-        Right gitInfo -> \oob -> oob-          { oobPublished = Just (firstAppearanceCommitDate gitInfo)-          , oobModified = Just (lastModificationCommitDate gitInfo)-          }+  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     Left e -> do-      T.hPutStrLn stderr $-        case e of-          MarkdownError _ explanation -> "Markdown parsing error:\n" <> explanation-          MarkdownFormatError explanation -> "Markdown structure error:\n" <> explanation-          TomlError _ explanation -> "Couldn't parse front matter as TOML:\n" <> explanation-          AdvisoryError _ explanation -> "Advisory structure error:\n" <> explanation+      hPutStrLn stderr (displayException e)       exitFailure     Right advisory -> do       go file advisory
+ app/Util.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE LambdaCase #-}++module Util where++import Data.Maybe (fromMaybe)+import System.Exit (die)++import Security.Advisories.Filesystem (isSecurityAdvisoriesRepo)+import Security.Advisories.Git (getRepoRoot)++-- | Ensure the given path (or current directory "." if @Nothing@)+-- is an advisory Git repo.  Return the (valid) repo root, or die+-- with an error message.+--+ensureRepo :: Maybe FilePath -> IO FilePath+ensureRepo mPath =+  getRepoRoot (fromMaybe "." mPath) >>= \case+    Left _          -> die "Not a git repo"+    Right repoPath  -> isSecurityAdvisoriesRepo repoPath >>= \case+      False -> die "Not a security-advisories repo"+      True  -> pure repoPath
+ assets/css/default.css view
@@ -0,0 +1,156 @@+:root{+    --bg-color:#FFFFFF;+    --text-color:#333;+    --outline-color:#DB83ED;+    --header-color:#5E5184;+    --anchor-color:#9E358F;+    --anchor-visited-color:#6F5F9C;+    --code-bg-color:#FAFAFA;+    --filename-bg:#EAEAEA;+    --code-color:#383a42;+    --code-bg-color:#fafafa;+    --code-comment-color:#a0a1a7;+    --code-kw-color:#af005f;+    --code-name-color:#e45649;+    --code-literal-color:#268bd2;+    --code-string-color:#cb4b16;+    --code-attr-color:#986801;+    --code-constructor-color:#5f5faf;+    --code-symbol-color:#4078f2;+    --code-record-field-color:#c18401;+    --code-pragma-color:#2aa198+}+@media (prefers-color-scheme:dark){+    :root{+        --bg-color:#333;+        --text-color:#C9D1D9;+        --header-color:#BBA1FF;+        --anchor-color:#EB82DC;+        --anchor-visited-color:#D5C5FF;+        --code-bg-color:transparent;+        --filename-bg:#2C2C2C;+        --code-color:#C9D1D9;+        --code-bg-color:#333;+        --code-comment-color:#a0a1a7;+        --code-kw-color:#BBA1FF;+        --code-name-color:#e45649;+        --code-literal-color:#268bd2;+        --code-string-color:#cb4b16;+        --code-attr-color:#986801;+        --code-constructor-color:#d079c9;+        --code-symbol-color:var(--code-color);+        --code-record-field-color:#c18401;+        --code-pragma-color:#2aa198+    }+}+*:focus-visible{+    outline-color:var(--outline-color)+}+body{+    color:var(--text-color);+    background-color:var(--bg-color)+}+a{+    color:var(--anchor-color)+}+a:visited{+    color:var(--anchor-visited-color)+}+h1,h2,h3,h4,h5,h6{+    color:var(--header-color)+}+input{+    background-color:rgba(255,255,255,0.06);+    color:var(--text-color)+}+.nav-bar{+    text-align: right;+}+.nav-bar ul{+    display: inline-block;+    list-style: none;+    margin: 0;+    padding: 0;+}+.nav-bar li{+    display: inline-block;+    vertical-align: middle;+    padding: 0;+    margin: 0;+    height: 100%;+    position: relative;+}+ *:focus-visible{+    outline-offset:4px;+    outline-width:1px+}+body{+    font-size:1.6rem;+    margin:0 auto;+    max-width:120rem+}+footer{+    margin-top:3rem;+    padding:1.2rem 0;+    border-top:0.2rem solid #000;+    font-size:1.2rem;+    color:#555+}+h1{+    font-size:2.4rem+}+h2{+    font-size:2rem+}+html{+    font-size:62.5%;+    font-family:Helvetica,sans-serif+}+table tbody td{+    padding:5px+}+footer{+    padding: 0 2%;+    text-align: center;+}+footer .HF{+    height:50px;+    line-height:50px;+    display:inline-block;+    background-repeat:no-repeat;+    background-image:url('../images/hf-logo.png');+    background-size:50px;+    background-position:left center;+    padding-left:60px+}+@media (max-width:319px){+    .nav-bar{+        margin:0 1.5rem 0 0;+    }+    .nav-bar a{+        display:block;+        line-height:1.6+    }+}+@media (min-width:320px){+    .nav-bar{+        margin:0 2rem 0 0;+    }+    .nav-bar a{+        display:inline;+        margin:0 0.6rem+    }+}+@media (min-width:640px){+    .nav-bar{+        margin:0 3rem 0 0;+    }+    .nav-bar a{+        margin:0 0 0 1.2rem;+        display:inline+    }+}++#advisory dt {+    margin-top: 0.75em;+}
+ assets/images/hf-logo.png view

binary file changed (absent → 12105 bytes)

hsec-tools.cabal view
@@ -1,6 +1,6 @@-cabal-version:      2.4+cabal-version:      3.0 name:               hsec-tools-version:            0.1.0.0+version:            0.2.0.2  -- A short (one-line) description of the package. synopsis:@@ -21,13 +21,15 @@ -- A copyright notice. -- copyright: category:           Data-extra-doc-files:    CHANGELOG.md+extra-doc-files:    CHANGELOG.md, README.md extra-source-files:+  assets/css/*.css+  assets/images/*.png   test/golden/*.golden   test/golden/*.md  tested-with:-  GHC ==8.10.7 || ==9.0.2 || ==9.2.8 || ==9.4.8 || ==9.6.3 || ==9.8.1+  GHC ==8.10.7 || ==9.0.2 || ==9.2.8 || ==9.4.8 || ==9.6.6 || ==9.8.3 || ==9.10.1 || ==9.12.1  library   exposed-modules:@@ -35,34 +37,55 @@     Security.Advisories.Convert.OSV     Security.Advisories.Filesystem     Security.Advisories.Generate.HTML+    Security.Advisories.Generate.Snapshot+    Security.Advisories.Generate.TH     Security.Advisories.Git+    Security.Advisories.Format     Security.Advisories.Parse     Security.Advisories.Queries +  other-modules:+      Paths_hsec_tools+  autogen-modules:+      Paths_hsec_tools+   build-depends:     , aeson                 >=2.0.1.0  && <3-    , base                  >=4.14     && <4.20-    , Cabal-syntax          >=3.8.1.0  && <3.11+    , atom-conduit          >=0.9      && <0.10+    , base                  >=4.14     && <5+    , bytestring            >=0.10     && <0.14+    , Cabal-syntax          >=3.8.1.0  && <3.15     , commonmark            ^>=0.2.2     , commonmark-pandoc     >=0.2      && <0.3-    , containers            >=0.6      && <0.7-    , cvss+    , conduit               >=1.3      && <1.4+    , conduit-extra         >=1.3      && <1.4+    , containers            >=0.6      && <0.8+    , cvss                  >= 0.2     && < 0.3+    , data-default          >=0.7      && <0.8     , directory             <2-    , extra                 ^>=1.7.5-    , filepath              >=1.4      && <1.5-    , hsec-core             >= 0.1     && < 0.2-    , feed                  ==1.3.*+    , extra                 >=1.7      && <1.9+    , filepath              >=1.4      && <1.6+    , hsec-core             ^>= 0.2+    , file-embed            >=0.0.13.0 && <0.0.17     , lucid                 >=2.9.0    && < 3     , mtl                   >=2.2      && <2.4-    , osv                   >= 0.1     && < 0.2+    , osv                   >=0.1      && <0.3+    , pandoc                >=2.0      && <3.8     , pandoc-types          >=1.22     && <2     , parsec                >=3        && <4-    , pathwalk              >=0.3      && < 0.4+    , pathwalk              >=0.3      && <0.4+    , pretty                >=1.0      && <1.2+    , prettyprinter         >=1.7      && <1.8     , process               >=1.6      && <1.7-    , safe                  >=0.3      && < 0.4+    , refined               >=0.7      && <0.9+    , resourcet             >=1.2      && <1.4+    , safe                  >=0.3      && <0.4     , text                  >=1.2      && <3-    , time                  >=1.9      && <1.14-    , toml-parser           ^>=2.0.0.0+    , template-haskell      >=2.16.0.0 && <2.24+    , time                  >=1.9      && <1.15+    , toml-parser           >=2.0.0.0  && <2.1+    , uri-bytestring        >=0.3      && <0.5+    , xml-conduit           >=1.9      && <1.11     , validation-selective  >=0.1      && <1    hs-source-dirs:   src@@ -74,6 +97,8 @@ executable hsec-tools   main-is:          Main.hs   other-modules:    Command.Reserve+                    , Command.NextID+                    , Util    -- Modules included in this executable, other than Main.   -- other-modules:@@ -82,14 +107,15 @@   -- other-extensions:   build-depends:     , aeson                 >=2.0.1.0 && <3-    , base                  >=4.14    && <4.20+    , base                  >=4.14    && <5     , bytestring            >=0.10    && <0.13-    , Cabal-syntax          >=3.8.1.0 && <3.11-    , filepath              >=1.4     && <1.5-    , hsec-core             >= 0.1    && < 0.2+    , Cabal-syntax          >=3.8.1.0 && <3.15+    , filepath              >=1.4     && <1.6+    , hsec-core             ^>= 0.2     , hsec-tools     , optparse-applicative  >=0.17    && <0.19     , text                  >=1.2     && <3+    , transformers     , validation-selective  >=0.1     && <1    hs-source-dirs:   app@@ -102,21 +128,32 @@   type:             exitcode-stdio-1.0   hs-source-dirs:   test   main-is:          Spec.hs-  other-modules:    Spec.QueriesSpec+  autogen-modules:+    Paths_hsec_tools+  other-modules:+    Paths_hsec_tools+    Spec.FormatSpec+    Spec.QueriesSpec   build-depends:     , aeson-pretty   <2-    , base           <5+    , base     , Cabal-syntax+    , containers     , cvss     , directory+    , hedgehog       <2     , hsec-core     , hsec-tools+    , osv     , pretty-simple  <5-    , tasty          <1.5+    , prettyprinter+    , tasty          <2     , tasty-golden   <2.4+    , tasty-hedgehog <2     , tasty-hunit    <0.11     , text     , time+    , toml-parser    default-language: Haskell2010   ghc-options:
src/Security/Advisories/Convert/OSV.hs view
@@ -6,7 +6,6 @@   where  import qualified Data.Text as T-import Data.Time (zonedTimeToUTC) import Data.Void import Distribution.Pretty (prettyShow) @@ -17,9 +16,9 @@ convert adv =   ( OSV.newModel'     (T.pack . printHsecId $ advisoryId adv)-    (zonedTimeToUTC $ advisoryModified adv)+    (advisoryModified adv)   )-  { OSV.modelPublished = Just $ zonedTimeToUTC (advisoryPublished adv)+  { OSV.modelPublished = Just $ advisoryPublished adv   , OSV.modelAliases = advisoryAliases adv   , OSV.modelRelated = advisoryRelated adv   , OSV.modelSummary = Just $ advisorySummary adv@@ -31,19 +30,23 @@ mkAffected :: Affected -> OSV.Affected Void Void Void mkAffected aff =   OSV.Affected-    { OSV.affectedPackage = mkPackage (affectedPackage aff)+    { OSV.affectedPackage = mkPackage (affectedComponentIdentifier aff)     , OSV.affectedRanges = pure $ mkRange (affectedVersions aff)     , OSV.affectedSeverity = [OSV.Severity (affectedCVSS aff)]     , OSV.affectedEcosystemSpecific = Nothing     , OSV.affectedDatabaseSpecific = Nothing     } -mkPackage :: T.Text -> OSV.Package-mkPackage name = OSV.Package-  { OSV.packageName = name-  , OSV.packageEcosystem = "Hackage"+mkPackage :: ComponentIdentifier -> OSV.Package+mkPackage ecosystem = OSV.Package+  { OSV.packageName = packageName+  , OSV.packageEcosystem = ecosystemName   , OSV.packagePurl = Nothing   }+  where+    (ecosystemName, packageName) = case ecosystem of+        Hackage n -> ("Hackage", n)+        GHC c -> ("GHC", ghcComponentToText c)  mkRange :: [AffectedVersionRange] -> OSV.Range Void mkRange ranges =
src/Security/Advisories/Filesystem.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE LambdaCase #-}- {-|  Helpers for the /security-advisories/ file system.@@ -22,26 +20,31 @@   , forReserved   , forAdvisory   , listAdvisories+  , advisoryFromFile+  , parseComponentIdentifier   ) where  import Control.Applicative (liftA2) import Data.Bifunctor (bimap) import Data.Foldable (fold)-import Data.Functor ((<&>)) import Data.Semigroup (Max(Max, getMax)) import Data.Traversable (for)  import Control.Monad.IO.Class (MonadIO, 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)+import System.FilePath ((</>), takeBaseName, splitDirectories) import System.Directory (doesDirectoryExist, pathIsSymbolicLink) import System.Directory.PathWalk-import Validation (Validation, eitherToValidation)+import Validation (Validation (..)) -import Security.Advisories (Advisory, AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), ParseAdvisoryError, emptyOutOfBandAttributes, parseAdvisory)+import Security.Advisories (Advisory, AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), ParseAdvisoryError, parseAdvisory, ComponentIdentifier(..)) 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)   dirNameAdvisories :: FilePath@@ -121,24 +124,35 @@ -- | List deduplicated parsed Advisories listAdvisories   :: (MonadIO m)-  => FilePath -> m (Validation [ParseAdvisoryError] [Advisory])+  => FilePath -> m (Validation [(FilePath, ParseAdvisoryError)] [Advisory]) listAdvisories root =   forAdvisory root $ \advisoryPath _advisoryId -> do     isSym <- liftIO $ pathIsSymbolicLink advisoryPath     if isSym       then return $ pure []-      else do-        oob <--          liftIO (getAdvisoryGitInfo advisoryPath) <&> \case-            Left _ -> emptyOutOfBandAttributes-            Right gitInfo ->-              emptyOutOfBandAttributes-                { oobPublished = Just (firstAppearanceCommitDate gitInfo),-                  oobModified = Just (lastModificationCommitDate gitInfo)-                }-        fileContent <- liftIO $ T.readFile advisoryPath-        return $ eitherToValidation $ bimap return return $ parseAdvisory NoOverrides oob fileContent+      else+        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+   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.@@ -161,3 +175,10 @@       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
+ src/Security/Advisories/Format.hs view
@@ -0,0 +1,376 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Security.Advisories.Format+  ( FrontMatter (..),+    fromAdvisory,+    AdvisoryMetadata (..),+    Toml.ToTable (..),+    Toml.ToValue (..),+    Toml.FromValue (..),+  )+where++import Control.Applicative ((<|>))+import Commonmark.Types (HasAttributes (..), IsBlock (..), IsInline (..), Rangeable (..), SourceRange (..))+import qualified Data.Map as Map+import Data.Maybe (fromMaybe)+import Data.Monoid (First (..))+import Data.List (intercalate)+import Data.Tuple (swap)+import GHC.Generics (Generic)++import Data.Text (Text)+import qualified Data.Text as T+import Data.Time (utc, UTCTime(..), zonedTimeToUTC, localTimeToUTC)+import Distribution.Parsec (eitherParsec)+import Distribution.Pretty (pretty)+import Distribution.Types.Version (Version)+import Distribution.Types.VersionRange (VersionRange)+import qualified Text.PrettyPrint as Pretty+import qualified Toml+import qualified Toml.Schema as Toml++import Security.Advisories.Core.Advisory+import Security.Advisories.Core.HsecId+import qualified Security.CVSS as CVSS+import Security.OSV (Reference (..), ReferenceType, referenceTypes)++fromAdvisory :: Advisory -> FrontMatter+fromAdvisory advisory =+  FrontMatter+    { frontMatterAdvisory =+        AdvisoryMetadata+          { amdId = advisoryId advisory,+            amdPublished = Just $ advisoryPublished advisory,+            amdModified = Just $ advisoryModified advisory,+            amdCAPECs = advisoryCAPECs advisory,+            amdCWEs = advisoryCWEs advisory,+            amdKeywords = advisoryKeywords advisory,+            amdAliases = advisoryAliases advisory,+            amdRelated = advisoryRelated advisory+          },+      frontMatterReferences = advisoryReferences advisory,+      frontMatterAffected = advisoryAffected advisory+    }++-- advisory markdown file.+data FrontMatter = FrontMatter {+  frontMatterAdvisory :: AdvisoryMetadata,+  frontMatterReferences :: [Reference],+  frontMatterAffected :: [Affected]+} deriving (Show, Generic)++instance Toml.FromValue FrontMatter where+  fromValue = Toml.parseTableFromValue $+   do advisory   <- Toml.reqKey "advisory"+      affected   <- Toml.reqKey "affected"+      references <- fromMaybe [] <$> Toml.optKey "references"+      pure FrontMatter {+        frontMatterAdvisory = advisory,+        frontMatterAffected = affected,+        frontMatterReferences = references+        }++instance Toml.ToValue FrontMatter where+  toValue = Toml.defaultTableToValue++instance Toml.ToTable FrontMatter where+  toTable x = Toml.table+    [ "advisory" Toml..= frontMatterAdvisory x+    , "affected" Toml..= frontMatterAffected x+    , "references" Toml..= frontMatterReferences x+    ]++-- | Internal type corresponding to the @[advisory]@ subsection of the+-- TOML frontmatter in an advisory markdown file.+data AdvisoryMetadata = AdvisoryMetadata+  { amdId         :: HsecId+  , amdModified   :: Maybe UTCTime+  , amdPublished  :: Maybe UTCTime+  , amdCAPECs     :: [CAPEC]+  , amdCWEs       :: [CWE]+  , amdKeywords   :: [Keyword]+  , amdAliases    :: [T.Text]+  , amdRelated    :: [T.Text]+  }+  deriving (Show, Generic)++instance Toml.FromValue AdvisoryMetadata where+  fromValue = Toml.parseTableFromValue $+   do identifier  <- Toml.reqKey "id"+      published   <- Toml.optKeyOf "date" getDefaultedZonedTime+      modified    <- Toml.optKeyOf "modified"  getDefaultedZonedTime+      let optList key = fromMaybe [] <$> Toml.optKey key+      capecs      <- optList "capec"+      cwes        <- optList "cwe"+      kwds        <- optList "keywords"+      aliases     <- optList "aliases"+      related     <- optList "related"+      pure AdvisoryMetadata+        { amdId = identifier+        , amdModified = modified+        , amdPublished = published+        , amdCAPECs = capecs+        , amdCWEs = cwes+        , amdKeywords = kwds+        , amdAliases = aliases+        , amdRelated = related+        }++instance Toml.ToValue AdvisoryMetadata where+  toValue = Toml.defaultTableToValue++instance Toml.ToTable AdvisoryMetadata where+  toTable x = Toml.table $+    ["id"        Toml..= amdId x] +++    ["modified"  Toml..= y | Just y <- [amdModified x]] +++    ["date"      Toml..= y | Just y <- [amdPublished x]] +++    ["capec"     Toml..= amdCAPECs x | not (null (amdCAPECs x))] +++    ["cwe"       Toml..= amdCWEs x | not (null (amdCWEs x))] +++    ["keywords"  Toml..= amdKeywords x | not (null (amdKeywords x))] +++    ["aliases"   Toml..= amdAliases x | not (null (amdAliases x))] +++    ["related"   Toml..= amdRelated x | not (null (amdRelated x))]++instance Toml.FromValue GHCComponent where+  fromValue v = case v of+    Toml.Text' _ n+      | Just c <- ghcComponentFromText n+      -> pure c+      | otherwise+      -> Toml.failAt (Toml.valueAnn v) $+          "Invalid ghc-component '"+          <> T.unpack n+          <> "', expected "+          <> T.unpack (T.intercalate "|" componentNames)+    _ -> Toml.failAt (Toml.valueAnn v) $+          "Non-text ghc-component, expected"+          <> T.unpack (T.intercalate "|" componentNames)+   where+     componentNames = map ghcComponentToText [minBound..maxBound]++instance Toml.ToValue GHCComponent where+  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+      os        <- Toml.optKey "os"+      arch      <- Toml.optKey "arch"+      decls     <- maybe [] Map.toList <$> Toml.optKey "declarations"+      versions  <- Toml.reqKey "versions"+      pure $ Affected+        { affectedComponentIdentifier = ecosystem+        , affectedCVSS = cvss+        , affectedVersions = versions+        , affectedArchitectures = arch+        , affectedOS = os+        , affectedDeclarations = decls+        }++instance Toml.ToValue Affected where+  toValue = Toml.defaultTableToValue++instance Toml.ToTable Affected where+  toTable x = Toml.table $+    ecosystem +++    [ "cvss"    Toml..= affectedCVSS x+    , "versions" Toml..= affectedVersions x+    ] +++    [ "os"   Toml..= y | Just y <- [affectedOS x]] +++    [ "arch" Toml..= y | Just y <- [affectedArchitectures x]] +++    [ "declarations" Toml..= asTable (affectedDeclarations x) | not (null (affectedDeclarations x))]+    where+      ecosystem = case affectedComponentIdentifier x of+        Hackage pkg -> ["package" Toml..= pkg]+        GHC c -> ["ghc-component" Toml..= c]+      asTable kvs = Map.fromList [(T.unpack k, v) | (k,v) <- kvs]++instance Toml.FromValue AffectedVersionRange where+  fromValue = Toml.parseTableFromValue $+   do introduced <- Toml.reqKey "introduced"+      fixed      <- Toml.optKey "fixed"+      pure AffectedVersionRange {+        affectedVersionRangeIntroduced = introduced,+        affectedVersionRangeFixed = fixed+        }++instance Toml.ToValue AffectedVersionRange where+  toValue = Toml.defaultTableToValue++instance Toml.ToTable AffectedVersionRange where+  toTable x = Toml.table $+    ("introduced" Toml..= affectedVersionRangeIntroduced x) :+    ["fixed" Toml..= y | Just y <- [affectedVersionRangeFixed x]]+++instance Toml.FromValue HsecId where+  fromValue v =+   do s <- Toml.fromValue v+      case parseHsecId s of+        Nothing -> Toml.failAt (Toml.valueAnn v) "invalid HSEC-ID: expected HSEC-[0-9]{4,}-[0-9]{4,}"+        Just x -> pure x++instance Toml.ToValue HsecId where+  toValue = Toml.toValue . printHsecId++instance Toml.FromValue CAPEC where+  fromValue v = CAPEC <$> Toml.fromValue v++instance Toml.ToValue CAPEC where+  toValue (CAPEC x) = Toml.toValue x++instance Toml.FromValue CWE where+  fromValue v = CWE <$> Toml.fromValue v++instance Toml.ToValue CWE where+  toValue (CWE x) = Toml.toValue x++instance Toml.FromValue Keyword where+  fromValue v = Keyword <$> Toml.fromValue v++instance Toml.ToValue Keyword where+  toValue (Keyword x) = Toml.toValue x++-- | Get a datetime with the timezone defaulted to UTC and the time defaulted to midnight+getDefaultedZonedTime :: Toml.Value' l -> Toml.Matcher l UTCTime+getDefaultedZonedTime (Toml.ZonedTime' _ x) = pure (zonedTimeToUTC x)+getDefaultedZonedTime (Toml.LocalTime' _ x) = pure (localTimeToUTC utc x)+getDefaultedZonedTime (Toml.Day' _       x) = pure (UTCTime x 0)+getDefaultedZonedTime v                     = Toml.failAt (Toml.valueAnn v) "expected a date with optional time and timezone"++instance Toml.FromValue Reference where+  fromValue = Toml.parseTableFromValue $+   do refType <- Toml.reqKey "type"+      url     <- Toml.reqKey "url"+      pure (Reference refType url)++instance Toml.FromValue ReferenceType where+  fromValue (Toml.Text' _ refTypeStr)+    | Just a <- lookup refTypeStr (fmap swap referenceTypes) = pure a+  fromValue v =+    Toml.failAt (Toml.valueAnn v) $+      "reference.type should be one of: " ++ intercalate ", " (T.unpack . snd <$> referenceTypes)++instance Toml.ToValue Reference where+  toValue = Toml.defaultTableToValue++instance Toml.ToTable Reference where+  toTable x = Toml.table+    [ "type" Toml..= fromMaybe "UNKNOWN" (lookup (referencesType x) referenceTypes)+    , "url" Toml..= referencesUrl x+    ]++instance Toml.FromValue OS where+  fromValue v =+   do s <- Toml.fromValue v+      case s :: String of+        "darwin" -> pure MacOS+        "freebsd" -> pure FreeBSD+        "linux" -> pure Linux+        "linux-android" -> pure Android+        "mingw32" -> pure Windows+        "netbsd" -> pure NetBSD+        "openbsd" -> pure OpenBSD+        other -> Toml.failAt (Toml.valueAnn v) ("Invalid OS: " ++ show other)++instance Toml.ToValue OS where+  toValue x =+    Toml.toValue $+    case x of+      MacOS -> "darwin" :: String+      FreeBSD -> "freebsd"+      Linux -> "linux"+      Android -> "linux-android"+      Windows -> "mingw32"+      NetBSD -> "netbsd"+      OpenBSD -> "openbsd"++instance Toml.FromValue Architecture where+  fromValue v =+   do s <- Toml.fromValue v+      case parseArchitecture s of+        Just a -> pure a+        Nothing -> Toml.failAt (Toml.valueAnn v) ("Invalid architecture: " ++ show s)++instance Toml.ToValue Architecture where+  toValue = Toml.toValue . printArchitecture++printArchitecture :: Architecture -> Text+printArchitecture = T.toLower . T.pack . show++parseArchitecture :: Text -> Maybe Architecture+parseArchitecture = flip lookup [(printArchitecture arch, arch) | arch <- [minBound .. maxBound]]++instance Toml.FromValue Version where+  fromValue v =+   do s <- Toml.fromValue v+      case eitherParsec s of+        Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in version: " ++ err)+        Right affected -> pure affected++instance Toml.ToValue Version where+  toValue = Toml.toValue . Pretty.render . pretty++instance Toml.FromValue VersionRange where+  fromValue v =+   do s <- Toml.fromValue v+      case eitherParsec s of+        Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in version range: " ++ err)+        Right affected -> pure affected++instance Toml.ToValue VersionRange where+  toValue = Toml.toValue . Pretty.render . pretty++instance Toml.FromValue CVSS.CVSS where+  fromValue v =+    do s <- Toml.fromValue v+       case CVSS.parseCVSS s of+         Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in cvss: " ++ show err)+         Right cvss -> pure cvss++instance Toml.ToValue CVSS.CVSS where+  toValue = Toml.toValue . CVSS.cvssVectorString++-- | A solution to an awkward problem: how to delete the TOML+-- block.  We parse into this type to get the source range of+-- the first block element.  We can use it to delete the lines+-- from the input.+--+newtype FirstSourceRange = FirstSourceRange (First SourceRange)+  deriving (Show, Semigroup, Monoid)++instance Rangeable FirstSourceRange where+  ranged range = (FirstSourceRange (First (Just range)) <>)++instance HasAttributes FirstSourceRange where+  addAttributes _ = id++instance IsBlock FirstSourceRange FirstSourceRange where+  paragraph _ = mempty+  plain _ = mempty+  thematicBreak = mempty+  blockQuote _ = mempty+  codeBlock _ = mempty+  heading _ = mempty+  rawBlock _ = mempty+  referenceLinkDefinition _ = mempty+  list _ = mempty++instance IsInline FirstSourceRange where+  lineBreak = mempty+  softBreak = mempty+  str _ = mempty+  entity _ = mempty+  escapedChar _ = mempty+  emph = id+  strong = id+  link _ _ _ = mempty+  image _ _ _ = mempty+  code _ = mempty+  rawInline _ _ = mempty
src/Security/Advisories/Generate/HTML.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}  module Security.Advisories.Generate.HTML   ( renderAdvisoriesIndex,@@ -8,30 +9,44 @@ where  import Control.Monad (forM_)+import qualified Data.ByteString.Char8 as BS8 import Data.List (sortOn) import Data.List.Extra (groupSort) import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe) import Data.Ord (Down (..)) import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.Encoding as BSE import qualified Data.Text.IO as T-import qualified Data.Text.Lazy as TL-import Data.Time (ZonedTime, zonedTimeToUTC)-import Data.Time.Format.ISO8601+import Data.Time (UTCTime, formatTime, defaultTimeLocale) import System.Directory (createDirectoryIfMissing) import System.Exit (exitFailure)-import System.FilePath ((</>))-import System.IO (hPrint, stderr)+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 Distribution.Pretty (prettyShow)+import Distribution.Types.VersionRange (earlierVersion, intersectVersionRanges, orLaterVersion) import Lucid-import Safe (maximumMay)-import qualified Text.Atom.Feed as Feed-import qualified Text.Atom.Feed.Export as FeedExport+import Refined (refineTH)+import qualified Text.Atom.Types as Feed+import qualified Text.Atom.Conduit.Render as FeedExport+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 @@ -48,7 +63,7 @@         return advisories    let renderHTMLToFile path content = do-        putStrLn $ "Rendering " <> path+        hPutStrLn stderr $ "Rendering " <> path         renderToFile path content    createDirectoryIfMissing False dst@@ -58,27 +73,35 @@    let advisoriesDir = dst </> "advisory"   createDirectoryIfMissing False advisoriesDir-  forM_ advisories $ \advisory ->-    renderHTMLToFile (advisoriesDir </> advisoryHtmlFilename (Advisories.advisoryId advisory)) $-      inPage PageAdvisory $-        div_ [class_ "pure-u-1"] $-          toHtmlRaw (Advisories.advisoryHtml advisory)+  forM_ advisories $ \advisory -> do+    let advisoryPath = advisoriesDir </> advisoryHtmlFilename (Advisories.advisoryId advisory)+    hPutStrLn stderr $ "Rendering " <> advisoryPath+    renderHTMLToFile advisoryPath $+      inPage (T.pack $ Advisories.printHsecId $ Advisories.advisoryId advisory) PageAdvisory $+        renderAdvisory advisory -  putStrLn $ "Rendering " <> (dst </> "atom.xml")-  writeFile (dst </> "atom.xml") $ T.unpack $ renderFeed advisories+  hPutStrLn stderr $ "Rendering " <> (dst </> "atom.xml")+  Conduit.runConduitRes $ renderFeed advisories Conduit..| ConduitBinary.sinkFile (dst </> "atom.xml") +  putStrLn "Copying assets"+  let assetsDir = dst </> "assets"+  forM_ $(readDirFilesTH "assets") $ \(path, content) -> do+    createDirectoryIfMissing True $ assetsDir </> takeDirectory path+    putStrLn $ "Copying " <> (assetsDir </> path)+    BS8.writeFile (assetsDir </> path) content+ -- * Rendering types  data AdvisoryR = AdvisoryR   { advisoryId :: Advisories.HsecId,     advisorySummary :: Text,     advisoryAffected :: [AffectedPackageR],-    advisoryModified :: ZonedTime+    advisoryModified :: UTCTime   }   deriving stock (Show)  data AffectedPackageR = AffectedPackageR-  { packageName :: Text,+  { ecosystem :: ComponentIdentifier,     introduced :: Text,     fixed :: Maybe Text   }@@ -88,63 +111,150 @@  listByDates :: [AdvisoryR] -> Html () listByDates advisories =-  inPage PageListByDates $-    div_ [class_ "pure-u-1"] $ do+  inPage "Advisories list" PageListByDates $ do+    indexDescription+    div_ [class_ "advisories"] $ do+      table_ [class_ "pure-table pure-table-horizontal"] $ do+        thead_ $ do+          tr_ $ do+            th_ "#"+            th_ "Package(s)"+            th_ "Summary"++        tbody_ $ do+          let sortedAdvisories =+                zip+                  (sortOn (Down . advisoryId) advisories)+                  (cycle [[], [class_ "pure-table-odd"]])+          forM_ sortedAdvisories $ \(advisory, trClasses) ->+            tr_ trClasses $ do+              td_ [class_ "advisory-id"] $ a_ [href_ $ advisoryLink (advisoryId advisory)] $ toHtml (Advisories.printHsecId (advisoryId advisory))+              td_ [class_ "advisory-packages"] $ toHtml $ T.intercalate "," $ packageName <$> advisoryAffected advisory+              td_ [class_ "advisory-summary"] $ toHtml $ advisorySummary advisory++packageName :: AffectedPackageR -> Text+packageName af = case ecosystem af of+  Hackage n -> n+  GHC c -> "ghc:" <> ghcComponentToText c++listByPackages :: [AdvisoryR] -> Html ()+listByPackages advisories =+  inPage "Advisories list" PageListByPackages $ do+    indexDescription++    let byPackage :: Map.Map Text [(AdvisoryR, AffectedPackageR)]+        byPackage =+          Map.fromList $+            groupSort+              [ (packageName package, (advisory, package))+                | advisory <- advisories,+                  package <- advisoryAffected advisory+              ]++    forM_ (Map.toList byPackage) $ \(currentPackageName, perPackageAdvisory) -> do+      h2_ $ toHtml currentPackageName       div_ [class_ "advisories"] $ do-        table_ [class_ "pure-table pure-table-horizontal"] $ do+        table_ [] $ do           thead_ $ do             tr_ $ do               th_ "#"-              th_ "Package(s)"+              th_ "Introduced"+              th_ "Fixed"               th_ "Summary"            tbody_ $ do             let sortedAdvisories =-                  zip-                    (sortOn (Down . advisoryId) advisories)-                    (cycle [[], [class_ "pure-table-odd"]])-            forM_ sortedAdvisories $ \(advisory, trClasses) ->-              tr_ trClasses $ do-                td_ [class_ "advisory-id"] $ a_ [href_ $ advisoryLink (advisoryId advisory)] $ toHtml (Advisories.printHsecId (advisoryId advisory))-                td_ [class_ "advisory-packages"] $ toHtml $ T.intercalate "," $ packageName <$> advisoryAffected advisory+                    sortOn (Down . advisoryId . fst) perPackageAdvisory+            forM_ sortedAdvisories $ \(advisory, package) -> do+              tr_ $ do+                td_ [class_ "advisory-id"] $+                  a_ [href_ $ advisoryLink $ advisoryId advisory] $+                    toHtml (Advisories.printHsecId $ advisoryId advisory)+                td_ [class_ "advisory-introduced"] $ toHtml $ introduced package+                td_ [class_ "advisory-fixed"] $ maybe (return ()) toHtml $ fixed package                 td_ [class_ "advisory-summary"] $ toHtml $ advisorySummary advisory -listByPackages :: [AdvisoryR] -> Html ()-listByPackages advisories =-  inPage PageListByPackages $-    div_ [class_ "pure-u-1"] $ do-      let byPackage :: Map.Map Text [(AdvisoryR, AffectedPackageR)]-          byPackage =-            Map.fromList $-              groupSort-                [ (packageName package, (advisory, package))-                  | advisory <- advisories,-                    package <- advisoryAffected advisory-                ]+indexDescription :: Html ()+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+      "It is generated from "+      a_ [href_ "https://github.com/haskell/security-advisories/", target_ "_blank", rel_ "noopener noreferrer"] "Haskell Security Advisory Database"+      ". "+      "Feel free to "+      a_ [href_ "https://github.com/haskell/security-advisories/blob/main/PROCESS.md", target_ "_blank", rel_ "noopener noreferrer"] "report new or historic security issues"+      "." -      forM_ (Map.toList byPackage) $ \(currentPackageName, perPackageAdvisory) -> do-        h2_ $ toHtml currentPackageName-        div_ [class_ "advisories"] $ do-          table_ [class_ "pure-table pure-table-horizontal"] $ do-            thead_ $ do-              tr_ $ do-                th_ "#"-                th_ "Introduced"-                th_ "Fixed"-                th_ "Summary"+renderAdvisory :: Advisories.Advisory -> Html ()+renderAdvisory advisory =+  div_ [id_ "advisory"] $ do+    let renderedDescription = unsafePerformIO $ runIOorExplode $ writeHtml5String def $ Advisories.advisoryPandoc advisory+    toHtmlRaw renderedDescription -            tbody_ $ do-              let sortedAdvisories =-                    zip-                      (sortOn (Down . advisoryId . fst) perPackageAdvisory)-                      (cycle [[], [class_ "pure-table-odd"]])-              forM_ sortedAdvisories $ \((advisory, package), trClasses) ->-                tr_ trClasses $ do-                  td_ [class_ "advisory-id"] $ a_ [href_ $ advisoryLink $ advisoryId advisory] $ toHtml (Advisories.printHsecId $ advisoryId advisory)-                  td_ [class_ "advisory-introduced"] $ toHtml $ introduced package-                  td_ [class_ "advisory-fixed"] $ maybe (return ()) toHtml $ fixed package-                  td_ [class_ "advisory-summary"] $ toHtml $ advisorySummary advisory+    let placeholderWhenEmptyOr :: [a] -> ([a] -> Html ()) -> Html ()+        placeholderWhenEmptyOr xs f = if null xs then dd_ [] $ i_ "< none >" else f xs +    h3_ [] "Info"+    dl_ [] $ do+      dt_ "Published"+      dd_ [] $ toHtml $ formatTime defaultTimeLocale "%B %d, %Y" $ Advisories.advisoryPublished advisory+      dt_ "Modified"+      dd_ [] $ toHtml $ formatTime defaultTimeLocale "%B %d, %Y" $ Advisories.advisoryModified advisory+      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+      dt_ "CWEs"+      placeholderWhenEmptyOr (Advisories.advisoryCWEs advisory) $ \cwes ->+        forM_ cwes $ \(Advisories.CWE cwe) ->+          dd_ [] $ a_ [href_ $ "https://cwe.mitre.org/data/definitions/" <> T.pack (show cwe) <> ".html"] $ toHtml $ show cwe+      dt_ "Keywords"+      placeholderWhenEmptyOr (Advisories.advisoryKeywords advisory) $ dd_ [] . toHtml . T.intercalate ", " . map Advisories.unKeyword+      dt_ "Aliases"+      placeholderWhenEmptyOr (Advisories.advisoryAliases advisory) $ dd_ [] . toHtml . T.intercalate ", "+      dt_ "Related"+      placeholderWhenEmptyOr (Advisories.advisoryRelated advisory) $ dd_ [] . toHtml . T.intercalate ", "+      dt_ "References"+      placeholderWhenEmptyOr (Advisories.advisoryReferences advisory) $ \references ->+        forM_ references $ \reference ->+          dd_ [] $ a_ [href_ $ OSV.referencesUrl reference] $ toHtml $ "[" <> fromMaybe "WEB" (lookup (OSV.referencesType reference) OSV.referenceTypes) <> "] " <> OSV.referencesUrl reference++    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++      dl_ [] $ do+        dt_ "CVSS"+        dd_ [] $ toHtml $ T.pack $ show $ Advisories.affectedCVSS affected+        dt_ "Versions"+        forM_ (Advisories.affectedVersions affected) $ \affectedVersionRange ->+          dd_ [] $+            code_ [] $+              toHtml $+                T.pack $+                  prettyShow $+                    let introducedVersionRange = orLaterVersion $ Advisories.affectedVersionRangeIntroduced affectedVersionRange+                    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+        forM_ (Advisories.affectedOS affected) $ \oses -> do+          dt_ "OSes"+          dd_ [] $ toHtml $ T.intercalate ", " $ T.toLower . T.pack . show <$> oses+        dt_ "Declarations"+        placeholderWhenEmptyOr (Advisories.affectedDeclarations affected) $ \declarations ->+          forM_ declarations $ \(declaration, versionRange) ->+            dd_ [] $ do+              code_ [] $ toHtml declaration+              ": "+              code_ [] $ toHtml $ T.pack $ prettyShow versionRange+ -- * Utils  data NavigationPage@@ -159,48 +269,36 @@   PageListByPackages -> "."   PageAdvisory -> ".." -inPage :: NavigationPage -> Html () -> Html ()-inPage page content =+inPage :: Text -> NavigationPage -> Html () -> Html ()+inPage title page content =   doctypehtml_ $-    html_ $ do+    html_ [lang_ "en"] $ do       head_ $ do         meta_ [charset_ "UTF-8"]         base_ [href_ $ baseUrlForPage page]         link_ [rel_ "alternate", type_ "application/atom+xml", href_ atomFeedUrl]-        link_ [rel_ "stylesheet", href_ "https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css", integrity_ "sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls", crossorigin_ "anonymous"]+        link_ [rel_ "stylesheet", href_ "assets/css/default.css"]         meta_ [name_ "viewport", content_ "width=device-width, initial-scale=1"]-        title_ "Haskell Security.Advisories.Core"-        style_ $-          T.intercalate-            "\n"-            [ ".advisories, .content {",-              "    margin: 1em;",-              "}",-              "a {",-              "    text-decoration: none;",-              "}",-              "a:visited {",-              "    text-decoration: none;",-              "    color: darkblue;",-              "}",-              "pre {",-              "    background: lightgrey;",-              "}"-            ]+        meta_ [name_ "description", content_ "Haskell Security advisories"]+        title_ "Haskell Security advisories"       body_ $ do-        div_ [class_ "pure-u-1"] $ do-          div_ [class_ "pure-menu pure-menu-horizontal"] $ do-            let selectedOn p cls =-                  if page == p-                    then cls <> " pure-menu-selected"-                    else cls-            span_ [class_ "pure-menu-heading pure-menu-link"] "Advisories list"-            ul_ [class_ "pure-menu-list"] $ do-              li_ [class_ $ selectedOn PageListByDates "pure-menu-item"] $-                a_ [href_ "by-dates.html", class_ "pure-menu-link"] "by date"-              li_ [class_ $ selectedOn PageListByPackages "pure-menu-item"] $-                a_ [href_ "by-packages.html", class_ "pure-menu-link"] "by package"+        div_ [class_ "nav-bar"] $ do+          let selectedOn p =+                if page == p+                  then "selected"+                  else ""+          ul_ [class_ "items"] $ do+            li_ [class_ $ selectedOn PageListByDates] $+              a_ [href_ "by-dates.html"] "by date"+            li_ [class_ $ selectedOn PageListByPackages] $+              a_ [href_ "by-packages.html"] "by package"+        h1_ [] $ toHtml title         div_ [class_ "content"] content+        footer_ [] $ do+          div_ [class_ "HF"] $ do+            "This site is a project of "+            a_ [href_ "https://haskell.foundation", target_ "_blank", rel_ "noopener noreferrer"] "The Haskell Foundation"+            "."  advisoryHtmlFilename :: Advisories.HsecId -> FilePath advisoryHtmlFilename advisoryId' = Advisories.printHsecId advisoryId' <> ".html"@@ -221,46 +319,89 @@     toAffectedPackageR p =       flip map (Advisories.affectedVersions p) $ \versionRange ->         AffectedPackageR-          { packageName = Advisories.affectedPackage p,+          { ecosystem = Advisories.affectedComponentIdentifier p,             introduced = T.pack $ prettyShow $ Advisories.affectedVersionRangeIntroduced versionRange,             fixed = T.pack . prettyShow <$> Advisories.affectedVersionRangeFixed versionRange           }  -- * Atom/RSS feed -feed :: [Advisories.Advisory] -> Feed.Feed+feed :: [Advisories.Advisory] -> Feed.AtomFeed feed advisories =-  ( Feed.nullFeed-      atomFeedUrl-      (Feed.TextString "Haskell Security Advisory DB") -- Title-      (maybe "" (T.pack . iso8601Show) . maximumMay . fmap (zonedTimeToUTC . Advisories.advisoryModified) $ advisories)-  )-    { Feed.feedEntries = fmap toEntry advisories-    , Feed.feedLinks = [(Feed.nullLink atomFeedUrl) { Feed.linkRel = Just (Left "self") }]-    , Feed.feedAuthors = [Feed.nullPerson { Feed.personName = "Haskell Security Response Team" }]+  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.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.nullEntry-        (toUrl advisory)-        (mkSummary advisory)-        (T.pack . iso8601Show $ Advisories.advisoryModified advisory)-      )-        { Feed.entryLinks = [(Feed.nullLink (toUrl advisory)) { Feed.linkRel = Just (Left "alternate") }]-        , Feed.entryContent = Just (Feed.HTMLContent (Advisories.advisoryHtml 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         } -    mkSummary advisory =-      Feed.TextString $-        T.pack (Advisories.printHsecId (Advisories.advisoryId advisory))+    mkTitle advisory =+      T.pack (Advisories.printHsecId (Advisories.advisoryId advisory))         <> " - "         <> Advisories.advisorySummary advisory+     toUrl advisory = advisoriesRootUrl <> "/" <> advisoryLink (Advisories.advisoryId advisory) -renderFeed :: [Advisories.Advisory] -> Text+    toAtomURI =+      Feed.AtomURI+        . either (error . show) id+        . URI.parseURI URI.strictURIParserOptions+        . BSE.encodeUtf8++    hsrt =+        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 =-  maybe (error "Cannot render atom feed") TL.toStrict-    . FeedExport.textFeed+  (Conduit..| ConduitXML.renderBytes def)+    . FeedExport.renderAtomFeed     . feed  advisoriesRootUrl :: T.Text
+ src/Security/Advisories/Generate/Snapshot.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module Security.Advisories.Generate.Snapshot+  ( createSnapshot,+  )+where++import Data.Aeson (ToJSON, encodeFile)+import Data.Default (def)+import qualified Data.Text.IO as T+import Data.Time (UTCTime)+import Data.Version (Version)+import GHC.Generics (Generic)+import Paths_hsec_tools (version)+import qualified Prettyprinter as Pretty+import qualified Prettyprinter.Render.Text as Pretty+import Security.Advisories.Core.Advisory+import Security.Advisories.Filesystem (advisoryFromFile, forAdvisory, forReserved)+import Security.Advisories.Format (fromAdvisory)+import System.Directory (copyFileWithMetadata, createDirectoryIfMissing)+import System.FilePath (takeDirectory, (</>))+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 (..))++-- * Actions++createSnapshot :: FilePath -> FilePath -> IO ()+createSnapshot src dst = do+  let toDstFilePath orig = dst </> drop (length src + 1) orig++  forReserved src $ \p _ -> do+    createDirectoryIfMissing True $ takeDirectory $ toDstFilePath p+    hPutStrLn stderr $ "Copying '" <> p <> "' to '" <> toDstFilePath p <> "'"+    copyFileWithMetadata p $ toDstFilePath p++  advisoriesLatestUpdates <-+    forAdvisory src $ \p _ -> do+      createDirectoryIfMissing True $ takeDirectory $ toDstFilePath p+      hPutStrLn stderr $ "Taking a snapshot of '" <> p <> "' to '" <> toDstFilePath p <> "'"+      advisoryFromFile p+        >>= \case+          Failure e -> do+            hPrint stderr e+            return []+          Success advisory -> do+            let pandoc =+                  Pandoc+                    nullMeta+                    ( CodeBlock+                        ("", ["toml"], [])+                        ( Pretty.renderStrict $+                            Pretty.layoutPretty Pretty.defaultLayoutOptions $+                              Toml.encode $+                                fromAdvisory advisory+                        )+                        : blocks (advisoryPandoc advisory)+                    )+                blocks (Pandoc _ xs) = xs+            rendered <- runIOorExplode $ writeCommonMark def pandoc+            T.writeFile (toDstFilePath p) rendered+            return [advisoryModified advisory]++  let metadataPath = dst </> "snapshot.json"+      metadata =+        SnapshotMetadata+          { latestUpdate = maximum advisoriesLatestUpdates,+            snapshotVersion = version+          }+  hPutStrLn stderr $ "Writing snapshot metadata to '" <> metadataPath <> "'"+  encodeFile metadataPath metadata++data SnapshotMetadata = SnapshotMetadata+  { latestUpdate :: UTCTime,+    snapshotVersion :: Version+  }+  deriving stock (Generic)+  deriving anyclass (ToJSON)
+ src/Security/Advisories/Generate/TH.hs view
@@ -0,0 +1,22 @@+module Security.Advisories.Generate.TH ( +  readFileTH,+  readDirFilesTH,+  fileLocation,+  ) where++import Control.Monad.IO.Class (liftIO)+import Data.ByteString.Char8 as BS8+import Data.FileEmbed (embedDir, makeRelativeToLocationPredicate)+import Language.Haskell.TH (Exp (LitE), Lit (StringL), Q)++-- | Read file at compile-time.+readFileTH :: FilePath -> Q Exp+readFileTH p = fileLocation p $ \p' -> LitE . StringL . BS8.unpack <$> liftIO (BS8.readFile p')++-- | Read files in (sub-)directory at compile-time.+-- Gives a [(FilePath, ByteString)]+readDirFilesTH :: FilePath -> Q Exp+readDirFilesTH p = fileLocation p embedDir++fileLocation :: FilePath -> (FilePath -> Q Exp) -> Q Exp+fileLocation fp act = makeRelativeToLocationPredicate (== "hsec-tools.cabal") fp >>= act
src/Security/Advisories/Git.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE DerivingStrategies #-}  {-| @@ -18,21 +19,22 @@  import Data.Char (isSpace) import Data.List (dropWhileEnd)-import Data.Time (ZonedTime)+import Data.Time (UTCTime, zonedTimeToUTC) import Data.Time.Format.ISO8601 (iso8601ParseM) import System.Exit (ExitCode(ExitSuccess)) import System.FilePath (splitFileName) import System.Process (readProcessWithExitCode)+import Control.Applicative ((<|>))  data AdvisoryGitInfo = AdvisoryGitInfo-  { firstAppearanceCommitDate :: ZonedTime-  , lastModificationCommitDate :: ZonedTime+  { firstAppearanceCommitDate :: UTCTime+  , lastModificationCommitDate :: UTCTime   }  data GitError   = GitProcessError ExitCode String String -- ^ exit code, stdout and stderr   | GitTimeParseError String -- ^ unable to parse this input as a datetime-  deriving (Show)+  deriving stock (Eq, Ord, Show)  explainGitError :: GitError -> String explainGitError = \case@@ -117,4 +119,7 @@       -- the same as `ExitFailure`       pure . Left $ GitProcessError status stdout stderr   where-    parseTime s = maybe (Left $ GitTimeParseError s) Right $ iso8601ParseM s+    parseTime :: String -> Either GitError UTCTime+    parseTime s = maybe (Left $ GitTimeParseError s) Right $+       iso8601ParseM s+         <|> zonedTimeToUTC <$> iso8601ParseM s
src/Security/Advisories/Parse.hs view
@@ -1,35 +1,41 @@+{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-orphans #-}-module Security.Advisories.Parse-  ( parseAdvisory-  , OutOfBandAttributes(..)-  , emptyOutOfBandAttributes-  , AttributeOverridePolicy(..)-  , ParseAdvisoryError(..)-  )-  where + module Security.Advisories.Parse+ ( parseAdvisory+ , OOB+ , OOBError (..)+ , OutOfBandAttributes(..)+ , displayOOBError+ , AttributeOverridePolicy(..)+ , ParseAdvisoryError(..)+ , validateComponentIdentifier+ )+where++import Control.Exception (Exception(displayException)) import Data.Bifunctor (first) import Data.Foldable (toList)-import Data.List (intercalate) import Data.Maybe (fromMaybe) import Data.Monoid (First(..))+ import Data.Tuple (swap)+import Control.Applicative ((<|>))+ import GHC.Generics (Generic) -import qualified Data.Map as Map import Data.Sequence (Seq((:<|)))+import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Lazy as T (toStrict)-import Data.Time (ZonedTime(..), LocalTime (LocalTime), midnight, utc)-import Distribution.Parsec (eitherParsec)-import Distribution.Types.Version (Version)-import Distribution.Types.VersionRange (VersionRange)+import Data.Time (UTCTime(..))  import Commonmark.Html (Html, renderHtml) import qualified Commonmark.Parser as Commonmark@@ -43,30 +49,25 @@ import Text.Pandoc.Walk (query) import Text.Parsec.Pos (sourceLine) -import Security.Advisories.Core.HsecId import Security.Advisories.Core.Advisory-import Security.OSV (Reference(..), ReferenceType, referenceTypes)-import qualified Security.CVSS as CVSS+import Security.Advisories.Format (FrontMatter(..), AdvisoryMetadata(..))+import Security.Advisories.Git (GitError, explainGitError)++-- | if there are no out of band attributes, attach a reason why that's the case+--+-- @since 0.2.0.0+type OOB = Either OOBError OutOfBandAttributes+ -- | A source of attributes supplied out of band from the advisory -- content.  Values provided out of band are treated according to -- the 'AttributeOverridePolicy'.------ The convenient way to construct a value of this type is to start--- with 'emptyOutOfBandAttributes', then use the record accessors to--- set particular fields.--- data OutOfBandAttributes = OutOfBandAttributes-  { oobModified :: Maybe ZonedTime-  , oobPublished :: Maybe ZonedTime+  { oobModified :: UTCTime+  , oobPublished :: UTCTime+  , oobComponentIdentifier :: Maybe ComponentIdentifier   }   deriving (Show) -emptyOutOfBandAttributes :: OutOfBandAttributes-emptyOutOfBandAttributes = OutOfBandAttributes-  { oobModified = Nothing-  , oobPublished = Nothing-  }- data AttributeOverridePolicy   = PreferInBand   | PreferOutOfBand@@ -74,18 +75,39 @@   deriving (Show, Eq)  data ParseAdvisoryError-  = MarkdownError Commonmark.ParseError T.Text-  | MarkdownFormatError T.Text-  | TomlError String T.Text-  | AdvisoryError [Toml.MatchMessage Toml.Position] T.Text-  deriving stock (Eq, Show, Generic)+    = MarkdownError Commonmark.ParseError Text+    | MarkdownFormatError Text+    | TomlError String Text+    | AdvisoryError [Toml.MatchMessage Toml.Position] T.Text+    deriving stock (Eq, Show, Generic) --- | The main parsing function.  'OutOfBandAttributes' are handled--- according to the 'AttributeOverridePolicy'.+-- | @since 0.2.0.0+instance Exception ParseAdvisoryError where+  displayException = T.unpack . \case+    MarkdownError _ explanation -> "Markdown parsing error:\n" <> explanation+    MarkdownFormatError explanation -> "Markdown structure error:\n" <> explanation+    TomlError _ explanation -> "Couldn't parse front matter as TOML:\n" <> explanation+    AdvisoryError _ explanation -> "Advisory structure error:\n" <> explanation++-- | errors that may occur while ingesting oob data --+-- @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+ parseAdvisory   :: AttributeOverridePolicy-  -> OutOfBandAttributes+  -> OOB   -> T.Text -- ^ input (CommonMark with TOML header)   -> Either ParseAdvisoryError Advisory parseAdvisory policy attrs raw = do@@ -117,7 +139,6 @@         -- no block elements?  empty range list?         -- these shouldn't happen, but better be total         raw-   -- Re-parse input as HTML.  This will probably go away; we now store the   -- Pandoc doc and can render that instead, where needed.   html <-@@ -131,26 +152,26 @@    where     firstPretty-      :: (e -> T.Text -> ParseAdvisoryError)-      -> (e -> T.Text)+      :: (e -> Text -> ParseAdvisoryError)+      -> (e -> Text)       -> Either e a       -> Either ParseAdvisoryError a     firstPretty ctr pretty = first $ mkPretty ctr pretty      mkPretty-      :: (e -> T.Text -> ParseAdvisoryError)-      -> (e -> T.Text)+      :: (e -> Text -> ParseAdvisoryError)+      -> (e -> Text)       -> e       -> ParseAdvisoryError     mkPretty ctr pretty x = ctr x $ pretty x  parseAdvisoryTable-  :: OutOfBandAttributes+  :: OOB   -> AttributeOverridePolicy   -> Pandoc -- ^ parsed document (without frontmatter)-  -> T.Text -- ^ summary-  -> T.Text -- ^ details-  -> T.Text -- ^ rendered HTML+  -> Text -- ^ summary+  -> Text -- ^ details+  -> Text -- ^ rendered HTML   -> Toml.Table' Toml.Position   -> Either [Toml.MatchMessage Toml.Position] Advisory parseAdvisoryTable oob policy doc summary details html tab =@@ -158,15 +179,20 @@    do fm <- Toml.fromValue (Toml.Table' Toml.startPos tab)       published <-         mergeOobMandatory policy-          (oobPublished oob)+          (oobPublished <$> oob)+          displayOOBError           "advisory.date"           (amdPublished (frontMatterAdvisory fm))       modified <-         fromMaybe published <$>           mergeOobOptional policy-            (oobPublished oob)+            (oobPublished <$> oob)             "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@@ -176,7 +202,7 @@         , advisoryKeywords = amdKeywords (frontMatterAdvisory fm)         , advisoryAliases = amdAliases (frontMatterAdvisory fm)         , advisoryRelated = amdRelated (frontMatterAdvisory fm)-        , advisoryAffected = frontMatterAffected fm+        , advisoryAffected = affected         , advisoryReferences = frontMatterReferences fm         , advisoryPandoc = doc         , advisoryHtml = html@@ -184,412 +210,123 @@         , advisoryDetails = details         } --- | Internal type corresponding to the complete raw TOML content of an--- advisory markdown file.-data FrontMatter = FrontMatter {-  frontMatterAdvisory :: AdvisoryMetadata,-  frontMatterReferences :: [Reference],-  frontMatterAffected :: [Affected]-} deriving (Generic)--instance Toml.FromValue FrontMatter where-  fromValue = Toml.parseTableFromValue $-   do advisory   <- Toml.reqKey "advisory"-      affected   <- Toml.reqKey "affected"-      references <- fromMaybe [] <$> Toml.optKey "references"-      pure FrontMatter {-        frontMatterAdvisory = advisory,-        frontMatterAffected = affected,-        frontMatterReferences = references-        }--instance Toml.ToValue FrontMatter where-  toValue = Toml.defaultTableToValue--instance Toml.ToTable FrontMatter where-  toTable x = Toml.table-    [ "advisory" Toml..= frontMatterAdvisory x-    , "affected" Toml..= frontMatterAffected x-    , "references" Toml..= frontMatterReferences x-    ]---- | Internal type corresponding to the @[advisory]@ subsection of the--- TOML frontmatter in an advisory markdown file.-data AdvisoryMetadata = AdvisoryMetadata-  { amdId         :: HsecId-  , amdModified   :: Maybe ZonedTime-  , amdPublished  :: Maybe ZonedTime-  , amdCAPECs     :: [CAPEC]-  , amdCWEs       :: [CWE]-  , amdKeywords   :: [Keyword]-  , amdAliases    :: [T.Text]-  , amdRelated    :: [T.Text]-  }--instance Toml.FromValue AdvisoryMetadata where-  fromValue = Toml.parseTableFromValue $-   do identifier  <- Toml.reqKey "id"-      published   <- Toml.optKeyOf "date" getDefaultedZonedTime-      modified    <- Toml.optKeyOf "modified"  getDefaultedZonedTime-      let optList key = fromMaybe [] <$> Toml.optKey key-      capecs      <- optList "capec"-      cwes        <- optList "cwe"-      kwds        <- optList "keywords"-      aliases     <- optList "aliases"-      related     <- optList "related"-      pure AdvisoryMetadata-        { amdId = identifier-        , amdModified = modified-        , amdPublished = published-        , amdCAPECs = capecs-        , amdCWEs = cwes-        , amdKeywords = kwds-        , amdAliases = aliases-        , amdRelated = related-        }--instance Toml.ToValue AdvisoryMetadata where-  toValue = Toml.defaultTableToValue--instance Toml.ToTable AdvisoryMetadata where-  toTable x = Toml.table $-    ["id"        Toml..= amdId x] ++-    ["modified"  Toml..= y | Just y <- [amdModified x]] ++-    ["date"      Toml..= y | Just y <- [amdPublished x]] ++-    ["capec"     Toml..= amdCAPECs x | not (null (amdCAPECs x))] ++-    ["cwe"       Toml..= amdCWEs x | not (null (amdCWEs x))] ++-    ["keywords"  Toml..= amdKeywords x | not (null (amdKeywords x))] ++-    ["aliases"   Toml..= amdAliases x | not (null (amdAliases x))] ++-    ["Related"   Toml..= amdRelated x | not (null (amdRelated x))]--instance Toml.FromValue Affected where-  fromValue = Toml.parseTableFromValue $-   do package   <- Toml.reqKey "package"-      cvss      <- Toml.reqKey "cvss" -- TODO validate CVSS format-      os        <- Toml.optKey "os"-      arch      <- Toml.optKey "arch"-      decls     <- maybe [] Map.toList <$> Toml.optKey "declarations"-      versions  <- Toml.reqKey "versions"-      pure $ Affected-        { affectedPackage = package-        , affectedCVSS = cvss-        , affectedVersions = versions-        , affectedArchitectures = arch-        , affectedOS = os-        , affectedDeclarations = decls-        }--instance Toml.ToValue Affected where-  toValue = Toml.defaultTableToValue--instance Toml.ToTable Affected where-  toTable x = Toml.table $-    [ "package" Toml..= affectedPackage x-    , "cvss"    Toml..= affectedCVSS x-    , "versions" Toml..= affectedVersions x-    ] ++-    [ "os"   Toml..= y | Just y <- [affectedOS x]] ++-    [ "arch" Toml..= y | Just y <- [affectedArchitectures x]] ++-    [ "declarations" Toml..= asTable (affectedDeclarations x) | not (null (affectedDeclarations x))]-    where-      asTable kvs = Map.fromList [(T.unpack k, v) | (k,v) <- kvs]--instance Toml.FromValue AffectedVersionRange where-  fromValue = Toml.parseTableFromValue $-   do introduced <- Toml.reqKey "introduced"-      fixed      <- Toml.optKey "fixed"-      pure AffectedVersionRange {-        affectedVersionRangeIntroduced = introduced,-        affectedVersionRangeFixed = fixed-        }--instance Toml.ToValue AffectedVersionRange where-  toValue = Toml.defaultTableToValue--instance Toml.ToTable AffectedVersionRange where-  toTable x = Toml.table $-    ("introduced" Toml..= affectedVersionRangeIntroduced x) :-    ["fixed" Toml..= y | Just y <- [affectedVersionRangeFixed x]]---instance Toml.FromValue HsecId where-  fromValue v =-   do s <- Toml.fromValue v-      case parseHsecId s of-        Nothing -> Toml.failAt (Toml.valueAnn v) "invalid HSEC-ID: expected HSEC-[0-9]{4,}-[0-9]{4,}"-        Just x -> pure x--instance Toml.ToValue HsecId where-  toValue = Toml.toValue . printHsecId--instance Toml.FromValue CAPEC where-  fromValue v = CAPEC <$> Toml.fromValue v--instance Toml.ToValue CAPEC where-  toValue (CAPEC x) = Toml.toValue x--instance Toml.FromValue CWE where-  fromValue v = CWE <$> Toml.fromValue v--instance Toml.ToValue CWE where-  toValue (CWE x) = Toml.toValue x--instance Toml.FromValue Keyword where-  fromValue v = Keyword <$> Toml.fromValue v--instance Toml.ToValue Keyword where-  toValue (Keyword x) = Toml.toValue x---- | Get a datetime with the timezone defaulted to UTC and the time defaulted to midnight-getDefaultedZonedTime :: Toml.Value' l -> Toml.Matcher l ZonedTime-getDefaultedZonedTime (Toml.ZonedTime' _ x) = pure x-getDefaultedZonedTime (Toml.LocalTime' _ x) = pure (ZonedTime x utc)-getDefaultedZonedTime (Toml.Day' _       x) = pure (ZonedTime (LocalTime x midnight) utc)-getDefaultedZonedTime v                     = Toml.failAt (Toml.valueAnn v) "expected a date with optional time and timezone"+-- | Make sure one of the affected match the ecosystem+validateComponentIdentifier :: MonadFail m => ComponentIdentifier -> [Affected] -> m ()+validateComponentIdentifier ecosystem xs+  | any (\affected -> affectedComponentIdentifier affected == ecosystem) xs = pure ()+  | otherwise = fail $ "Expected an affected to match the ecosystem: " <> show ecosystem -advisoryDoc :: Blocks -> Either T.Text (T.Text, [Block])+advisoryDoc :: Blocks -> Either Text (Text, [Block]) advisoryDoc (Many blocks) = case blocks of-  CodeBlock (_, classes, _) frontMatter :<| t-    | "toml" `elem` classes-    -> pure (frontMatter, toList t)-  _-    -> Left "Does not have toml code block as first element"+    CodeBlock (_, classes, _) frontMatter :<| t+        | "toml" `elem` classes ->+            pure (frontMatter, toList t)+    _ ->+        Left "Does not have toml code block as first element" -parseAdvisorySummary :: Pandoc -> Either T.Text T.Text+parseAdvisorySummary :: Pandoc -> Either Text Text parseAdvisorySummary = fmap inlineText . firstHeading -firstHeading :: Pandoc -> Either T.Text [Inline]+firstHeading :: Pandoc -> Either Text [Inline] firstHeading (Pandoc _ xs) = go xs   where-  go [] = Left "Does not have summary heading"-  go (Header _ _ ys : _) = Right ys-  go (_ : t) = go t+    go [] = Left "Does not have summary heading"+    go (Header _ _ ys : _) = Right ys+    go (_ : t) = go t  -- yield "plain" terminal inline content; discard formatting-inlineText :: [Inline] -> T.Text+inlineText :: [Inline] -> Text inlineText = query f   where-  f inl = case inl of-    Str s -> s-    Code _ s -> s-    Space -> " "-    SoftBreak -> " "-    LineBreak -> "\n"-    Math _ s -> s-    RawInline _ s -> s-    _ -> ""--instance Toml.FromValue Reference where-  fromValue = Toml.parseTableFromValue $-   do refType <- Toml.reqKey "type"-      url     <- Toml.reqKey "url"-      pure (Reference refType url)--instance Toml.FromValue ReferenceType where-  fromValue (Toml.Text' _ refTypeStr)-    | Just a <- lookup refTypeStr (fmap swap referenceTypes) = pure a-  fromValue v =-    Toml.failAt (Toml.valueAnn v) $-      "reference.type should be one of: " ++ intercalate ", " (T.unpack . snd <$> referenceTypes)--instance Toml.ToValue Reference where-  toValue = Toml.defaultTableToValue--instance Toml.ToTable Reference where-  toTable x = Toml.table-    [ "type" Toml..= fromMaybe "UNKNOWN" (lookup (referencesType x) referenceTypes)-    , "url" Toml..= referencesUrl x-    ]--instance Toml.FromValue OS where-  fromValue v =-   do s <- Toml.fromValue v-      case s :: String of-        "darwin" -> pure MacOS-        "freebsd" -> pure FreeBSD-        "linux" -> pure Linux-        "linux-android" -> pure Android-        "mingw32" -> pure Windows-        "netbsd" -> pure NetBSD-        "openbsd" -> pure OpenBSD-        other -> Toml.failAt (Toml.valueAnn v) ("Invalid OS: " ++ show other)--instance Toml.ToValue OS where-  toValue x =-    Toml.toValue $-    case x of-      MacOS -> "darwin" :: String-      FreeBSD -> "freebsd"-      Linux -> "linux"-      Android -> "linux-android"-      Windows -> "mingw32"-      NetBSD -> "netbsd"-      OpenBSD -> "openbsd"--instance Toml.FromValue Architecture where-  fromValue v =-   do s <- Toml.fromValue v-      case s :: String of-        "aarch64" -> pure AArch64-        "alpha" -> pure Alpha-        "arm" -> pure Arm-        "hppa" -> pure HPPA-        "hppa1_1" -> pure HPPA1_1-        "i386" -> pure I386-        "ia64" -> pure IA64-        "m68k" -> pure M68K-        "mips" -> pure MIPS-        "mipseb" -> pure MIPSEB-        "mipsel" -> pure MIPSEL-        "nios2" -> pure NIOS2-        "powerpc" -> pure PowerPC-        "powerpc64" -> pure PowerPC64-        "powerpc64le" -> pure PowerPC64LE-        "riscv32" -> pure RISCV32-        "riscv64" -> pure RISCV64-        "rs6000" -> pure RS6000-        "s390" -> pure S390-        "s390x" -> pure S390X-        "sh4" -> pure SH4-        "sparc" -> pure SPARC-        "sparc64" -> pure SPARC64-        "vax" -> pure VAX-        "x86_64" -> pure X86_64-        other -> Toml.failAt (Toml.valueAnn v) ("Invalid architecture: " ++ show other)--instance Toml.ToValue Architecture where-  toValue x =-    Toml.toValue $-    case x of-        AArch64 -> "aarch64" :: String-        Alpha -> "alpha"-        Arm -> "arm"-        HPPA -> "hppa"-        HPPA1_1 -> "hppa1_1"-        I386 -> "i386"-        IA64 -> "ia64"-        M68K -> "m68k"-        MIPS -> "mips"-        MIPSEB -> "mipseb"-        MIPSEL -> "mipsel"-        NIOS2 -> "nios2"-        PowerPC -> "powerpc"-        PowerPC64 -> "powerpc64"-        PowerPC64LE -> "powerpc64le"-        RISCV32 -> "riscv32"-        RISCV64 -> "riscv64"-        RS6000 -> "rs6000"-        S390 -> "s390"-        S390X -> "s390x"-        SH4 -> "sh4"-        SPARC -> "sparc"-        SPARC64 -> "sparc64"-        VAX -> "vax"-        X86_64 -> "x86_64"--instance Toml.FromValue Version where-  fromValue v =-   do s <- Toml.fromValue v-      case eitherParsec s of-        Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in version range: " ++ err)-        Right affected -> pure affected--instance Toml.ToValue Version where-  toValue = Toml.toValue . show--instance Toml.FromValue VersionRange where-  fromValue v =-   do s <- Toml.fromValue v-      case eitherParsec s of-        Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in version range: " ++ err)-        Right affected -> pure affected--instance Toml.ToValue VersionRange where-  toValue = Toml.toValue . show--instance Toml.FromValue CVSS.CVSS where-  fromValue v =-    do s <- Toml.fromValue v-       case CVSS.parseCVSS s of-         Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in cvss: " ++ show err)-         Right cvss -> pure cvss--instance Toml.ToValue CVSS.CVSS where-  toValue = Toml.toValue . CVSS.cvssVectorString+    f inl = case inl of+        Str s -> s+        Code _ s -> s+        Space -> " "+        SoftBreak -> " "+        LineBreak -> "\n"+        Math _ s -> s+        RawInline _ s -> s+        _ -> ""  mergeOob   :: MonadFail m   => AttributeOverridePolicy-  -> Maybe a  -- ^ out-of-band value+  -> Either e a  -- ^ out-of-band value   -> String  -- ^ key   -> Maybe a -- ^ in-band-value-  -> m b  -- ^ when key and out-of-band value absent+  -> (e -> m b)  -- ^ when key and out-of-band value absent   -> (a -> m b) -- ^ when value present   -> m b mergeOob policy oob k ib absent present = do   case (oob, ib) of-    (Just l, Just r) -> case policy of+    (Right l, Just r) -> case policy of       NoOverrides -> fail ("illegal out of band override: " ++ k)       PreferOutOfBand -> present l       PreferInBand -> present r-    (Just a, Nothing) -> present a-    (Nothing, Just a) -> present a-    (Nothing, Nothing) -> absent+    (Right a, Nothing) -> present a+    (Left _, Just a) -> present a+    (Left e, Nothing) -> absent e  mergeOobOptional   :: MonadFail m   => AttributeOverridePolicy-  -> Maybe a  -- ^ out-of-band value+  -> Either e a  -- ^ out-of-band value   -> String -- ^ key   -> Maybe a -- ^ in-band-value   -> m (Maybe a) mergeOobOptional policy oob k ib =-  mergeOob policy oob k ib (pure Nothing) (pure . Just)+  mergeOob policy oob k ib (const $ pure Nothing) (pure . Just)  mergeOobMandatory   :: MonadFail m   => AttributeOverridePolicy-  -> Maybe a  -- ^ out-of-band value+  -> Either e a  -- ^ out-of-band value+  -> (e -> String) -- ^ how to display information about a missing out of band value   -> String  -- ^ key   -> Maybe a -- ^ in-band value   -> m a-mergeOobMandatory policy oob k ib =-  mergeOob policy oob k ib (fail ("missing mandatory key: " ++ k)) pure+mergeOobMandatory policy eoob doob k ib =+  mergeOob policy eoob k ib everythingFailed pure+    where+      everythingFailed e = fail $ unlines+        [ "while trying to lookup mandatory key " <> show k <> ":"+        , doob e+        ] --- | A solution to an awkward problem: how to delete the TOML--- block.  We parse into this type to get the source range of--- the first block element.  We can use it to delete the lines--- from the input.---+{- | A solution to an awkward problem: how to delete the TOML+ block.  We parse into this type to get the source range of+ the first block element.  We can use it to delete the lines+ from the input.+-} newtype FirstSourceRange = FirstSourceRange (First SourceRange)-  deriving (Show, Semigroup, Monoid)+    deriving (Show, Semigroup, Monoid)  instance Rangeable FirstSourceRange where-  ranged range = (FirstSourceRange (First (Just range)) <>)+    ranged range = (FirstSourceRange (First (Just range)) <>)  instance HasAttributes FirstSourceRange where-  addAttributes _ = id+    addAttributes _ = id  instance IsBlock FirstSourceRange FirstSourceRange where-  paragraph _ = mempty-  plain _ = mempty-  thematicBreak = mempty-  blockQuote _ = mempty-  codeBlock _ = mempty-  heading _ = mempty-  rawBlock _ = mempty-  referenceLinkDefinition _ = mempty-  list _ = mempty+    paragraph _ = mempty+    plain _ = mempty+    thematicBreak = mempty+    blockQuote _ = mempty+    codeBlock _ = mempty+    heading _ = mempty+    rawBlock _ = mempty+    referenceLinkDefinition _ = mempty+    list _ = mempty  instance IsInline FirstSourceRange where-  lineBreak = mempty-  softBreak = mempty-  str _ = mempty-  entity _ = mempty-  escapedChar _ = mempty-  emph = id-  strong = id-  link _ _ _ = mempty-  image _ _ _ = mempty-  code _ = mempty-  rawInline _ _ = mempty+    lineBreak = mempty+    softBreak = mempty+    str _ = mempty+    entity _ = mempty+    escapedChar _ = mempty+    emph = id+    strong = id+    link _ _ _ = mempty+    image _ _ _ = mempty+    code _ = mempty+    rawInline _ _ = mempty
src/Security/Advisories/Queries.hs view
@@ -38,9 +38,10 @@     any checkAffected . advisoryAffected     where       checkAffected :: Affected -> Bool-      checkAffected affected =-        queryPackageName == affectedPackage affected-          && checkWithRange queryVersionish (fromAffected affected)+      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@@ -50,16 +51,24 @@         (orLaterVersion (affectedVersionRangeIntroduced avr))         (maybe anyVersion earlierVersion (affectedVersionRangeFixed avr)) +type QueryResult = Validation [(FilePath, ParseAdvisoryError)] [Advisory]+ -- | List the advisories matching a package name and a version-listVersionAffectedBy :: MonadIO m => FilePath -> Text -> Version -> m (Validation [ParseAdvisoryError] [Advisory])+listVersionAffectedBy+  :: MonadIO m+  => FilePath -> Text -> Version -> m QueryResult listVersionAffectedBy = listAffectedByHelper isVersionAffectedBy  -- | List the advisories matching a package name and a version range-listVersionRangeAffectedBy :: MonadIO m => FilePath -> Text -> VersionRange -> m (Validation [ParseAdvisoryError] [Advisory])+listVersionRangeAffectedBy+  :: (MonadIO m)+  => FilePath -> Text -> VersionRange -> m QueryResult listVersionRangeAffectedBy = listAffectedByHelper isVersionRangeAffectedBy  -- | Helper function for 'listVersionAffectedBy' and 'listVersionRangeAffectedBy'-listAffectedByHelper :: MonadIO m => (Text -> a -> Advisory -> Bool) -> FilePath -> Text -> a -> m (Validation [ParseAdvisoryError] [Advisory])+listAffectedByHelper+  :: (MonadIO m)+  => (Text -> a -> Advisory -> Bool) -> FilePath -> Text -> a -> m QueryResult listAffectedByHelper checkAffectedBy root queryPackageName queryVersionish =   fmap (filter (checkAffectedBy queryPackageName queryVersionish)) <$>     listAdvisories root
test/Spec.hs view
@@ -7,28 +7,31 @@ import qualified Data.Text.IO as T import qualified Data.Text.Lazy as LText import qualified Data.Text.Lazy.Encoding as LText+import Data.Time (UTCTime(UTCTime)) import Data.Time.Calendar.OrdinalDate (fromOrdinalDate)-import Data.Time.LocalTime-import System.Directory (listDirectory)-import Test.Tasty-import Test.Tasty.Golden (goldenVsString)-import Text.Pretty.Simple (pShowNoColor)-+import Paths_hsec_tools (getDataFileName) 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)+import Text.Pretty.Simple (pShowNoColor)  main :: IO () main = do     goldenFiles <- listGoldenFiles     defaultMain $-      testGroup "Tests"-        [ goldenTestsSpec goldenFiles-        , QueriesSpec.spec-        ]+        testGroup+            "Tests"+            [ goldenTestsSpec goldenFiles+            , QueriesSpec.spec+            , FormatSpec.spec+            ]  listGoldenFiles :: IO [FilePath]-listGoldenFiles = map (mappend dpath) . filter (not . isSuffixOf ".golden") <$> listDirectory dpath+listGoldenFiles = map (mappend dpath) . filter (not . isSuffixOf ".golden") <$> (getDataFileName dpath >>= listDirectory)   where     dpath = "test/golden/" @@ -40,14 +43,14 @@   where     doCheck :: IO LText.Text     doCheck = do-        input <- T.readFile fp-        let fakeDate = ZonedTime (LocalTime (fromOrdinalDate 1970 0) midnight) utc-            attr =-                emptyOutOfBandAttributes-                    { oobPublished = Just fakeDate-                    , oobModified = Just fakeDate-                    }-            res = parseAdvisory NoOverrides attr input+        input <- getDataFileName fp >>= T.readFile+        let fakeDate = UTCTime (fromOrdinalDate 1970 0) 0+            attr = OutOfBandAttributes+              { oobPublished = fakeDate+              , oobModified = fakeDate+              , oobComponentIdentifier = Nothing+              }+            res = parseAdvisory NoOverrides (Right attr) input             osvExport = case res of                 Right adv ->                     let osv = OSV.convert adv
+ test/Spec/FormatSpec.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++module Spec.FormatSpec (spec) where++import Data.Fixed (Fixed (MkFixed))+import Data.Function (on)+import qualified Data.Map.Strict as Map+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time+import Distribution.Types.Version+import Distribution.Types.VersionRange+import qualified Hedgehog as Gen+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import qualified Prettyprinter as Pretty+import qualified Prettyprinter.Render.Text as Pretty+import Security.Advisories.Core.Advisory+import Security.Advisories.Core.HsecId+import Security.Advisories.Format+import Security.CVSS+import Security.OSV (Reference (..), ReferenceType (..))+import Test.Tasty+import Test.Tasty.Hedgehog+import qualified Toml++spec :: TestTree+spec =+  testGroup+    "Format"+    [ testGroup+        "FrontMatter"+        [ testProperty "parse . render == id" $+            Gen.property $ do+              fm <- Gen.forAll genFrontMatter+              let rendered =+                    Pretty.renderStrict $ Pretty.layoutPretty Pretty.defaultLayoutOptions $ Toml.encode fm+              Gen.footnote $ T.unpack rendered+              Toml.decode rendered Gen.=== Toml.Success mempty (FrontMatterEq fm)+        ]+    ]++newtype FrontMatterEq = FrontMatterEq {unFrontMatter :: FrontMatter}+  deriving newtype (Show, FromValue)++instance Eq FrontMatterEq where+  (==) = (==) `on` show . unFrontMatter++genFrontMatter :: Gen.Gen FrontMatter+genFrontMatter =+  FrontMatter+    <$> genAdvisoryMetadata+    <*> Gen.list (Range.linear 0 10) genReference+    <*> Gen.list (Range.linear 0 10) genAffected++genAdvisoryMetadata :: Gen.Gen AdvisoryMetadata+genAdvisoryMetadata =+  AdvisoryMetadata+    <$> genHsecId+    <*> Gen.maybe genUTCTime+    <*> Gen.maybe genUTCTime+    <*> Gen.list (Range.linear 0 5) genCAPEC+    <*> Gen.list (Range.linear 0 5) genCWE+    <*> Gen.list (Range.linear 0 5) genKeyword+    <*> Gen.list (Range.linear 0 5) genText+    <*> Gen.list (Range.linear 0 5) genText++genAffected :: Gen.Gen Affected+genAffected =+  Affected+    <$> genComponentIdentifier+    <*> genCVSS+    <*> Gen.list (Range.linear 0 5) genAffectedVersionRange+    <*> Gen.maybe (Gen.list (Range.linear 0 5) genArchitecture)+    <*> Gen.maybe (Gen.list (Range.linear 0 5) genOS)+    <*> (Map.toList . Map.fromList <$> Gen.list (Range.linear 0 5) ((,) <$> genText <*> genVersionRange))++genComponentIdentifier :: Gen.Gen ComponentIdentifier+genComponentIdentifier = Gen.choice $+  [ Hackage <$> genText+  , GHC <$> Gen.enumBounded+  ]++genCVSS :: Gen.Gen CVSS+genCVSS =+  Gen.choice $+    map+      (\x -> either (\e -> error $ "Cannot parse CVSS " <> show x <> " " <> show e) return $ parseCVSS x)+      [ "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N",+        "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",+        "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N",+        "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",+        "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N",+        "CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N",+        "CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",+        "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",+        "CVSS:3.0/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:L",+        "AV:N/AC:L/Au:N/C:N/I:N/A:C",+        "AV:N/AC:L/Au:N/C:C/I:C/A:C",+        "AV:L/AC:H/Au:N/C:C/I:C/A:C"+      ]++genCAPEC :: Gen.Gen CAPEC+genCAPEC = CAPEC <$> Gen.integral (Range.linear 100 999)++genCWE :: Gen.Gen CWE+genCWE = CWE <$> Gen.integral (Range.linear 100 999)++genHsecId :: Gen.Gen HsecId+genHsecId = flip nextHsecId placeholder <$> Gen.integral (Range.linear 2024 2032)++genUTCTime :: Gen.Gen UTCTime+genUTCTime =+  UTCTime+    <$> genDay+    <*> fmap secondsToDiffTime (Gen.integral $ Range.constant 0 86401)++genDay :: Gen.Gen Day+genDay = do+  y <- toInteger <$> Gen.int (Range.constant 1968 2019)+  m <- Gen.int (Range.constant 1 12)+  d <- Gen.int (Range.constant 1 28)+  pure $ fromGregorian y m d++genVersionRange :: Gen.Gen VersionRange+genVersionRange =+  Gen.recursive+    Gen.choice+    [ pure anyVersion,+      pure noVersion,+      thisVersion <$> genVersion,+      notThisVersion <$> genVersion,+      laterVersion <$> genVersion,+      earlierVersion <$> genVersion,+      orLaterVersion <$> genVersion,+      orEarlierVersion <$> genVersion,+      withinVersion <$> genVersion,+      majorBoundVersion <$> genVersion+    ]+    [ Gen.subterm2 genVersionRange genVersionRange unionVersionRanges,+      Gen.subterm2 genVersionRange genVersionRange intersectVersionRanges+    ]++genText :: Gen.Gen Text+genText = Gen.text (Range.linear 1 20) Gen.alphaNum++genAffectedVersionRange :: Gen.Gen AffectedVersionRange+genAffectedVersionRange = AffectedVersionRange <$> genVersion <*> Gen.maybe genVersion++genVersion :: Gen.Gen Version+genVersion = mkVersion <$> Gen.list (Range.linear 1 5) (Gen.integral (Range.linear 0 999))++genArchitecture :: Gen.Gen Architecture+genArchitecture = Gen.enumBounded++genOS :: Gen.Gen OS+genOS = Gen.enumBounded++genKeyword :: Gen.Gen Keyword+genKeyword = Keyword <$> genText++genReference :: Gen.Gen Reference+genReference = Reference <$> genReferenceType <*> genText++genReferenceType :: Gen.Gen ReferenceType+genReferenceType = Gen.enumBounded
test/Spec/QueriesSpec.hs view
@@ -115,7 +115,7 @@      , advisoryRelated = [ "CVE-2022-YYYY" , "CVE-2022-ZZZZ" ]      , advisoryAffected =          [ Affected-             { affectedPackage = packageName+             { affectedComponentIdentifier = Hackage packageName              , affectedCVSS = cvss              , affectedVersions = mkAffectedVersions versionRange              , affectedArchitectures = Nothing
test/golden/EXAMPLE_ADVISORY.md.golden view
@@ -17,7 +17,7 @@             ]         , advisoryAffected =             [ Affected-                { affectedPackage = "package-name"+                { 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