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