hsec-tools-0.3.0.1: src/Security/Advisories/Filesystem.hs
{-# LANGUAGE CPP #-}
{-|
Helpers for the /security-advisories/ file system.
Top-level functions that take a @FilePath@ expect the path to the
top-level directory of the /security-advisories/ repository (i.e.
it must have the @advisories/@ subdirectory).
-}
module Security.Advisories.Filesystem
(
dirNameAdvisories
, dirNameReserved
, dirNamePublished
, isSecurityAdvisoriesRepo
, getReservedIds
, getAdvisoryIds
, getAllocatedIds
, greatestId
, getGreatestId
, forReserved
, forAdvisory
, listAdvisories
, advisoryFromFile
) 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 qualified Data.Text.IO as T
import System.FilePath ((</>), dropExtension)
import System.Directory (doesDirectoryExist, listDirectory)
import Validation (Validation (..))
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))
dirNameAdvisories :: FilePath
dirNameAdvisories = "advisories"
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.
--
isSecurityAdvisoriesRepo :: FilePath -> IO Bool
isSecurityAdvisoriesRepo path =
doesDirectoryExist (path </> dirNameAdvisories)
-- | Get a list of reserved HSEC IDs. The order is unspecified.
--
getReservedIds :: FilePath -> IO [HsecId]
getReservedIds root =
forReserved root (\_ hsid -> pure [hsid])
-- | Get a list of used IDs (does not include reserved IDs)
-- There may be duplicates and the order is unspecified.
--
getAdvisoryIds :: FilePath -> IO [HsecId]
getAdvisoryIds root =
forAdvisory root (\_ hsid -> pure [hsid])
-- | Get all allocated IDs, including reserved IDs.
-- There may be duplicates and the order is unspecified.
--
getAllocatedIds :: FilePath -> IO [HsecId]
getAllocatedIds root =
liftA2 (<>)
(getAdvisoryIds root)
(getReservedIds root)
-- | Return the greatest ID in a collection of IDs. If the
-- collection is empty, return the 'placeholder'.
--
greatestId :: (Foldable t) => t HsecId -> HsecId
greatestId = getMax . foldr ((<>) . Max) (Max placeholder)
-- | Return the greatest ID in the database, including reserved IDs.
-- If there are IDs in the database, returns the 'placeholder'.
--
getGreatestId :: FilePath -> IO HsecId
getGreatestId = fmap greatestId . getAllocatedIds
-- | Invoke a callback for each HSEC ID in the reserved
-- directory. The results are combined monoidally.
--
forReserved
:: (MonadIO m, Monoid r)
=> FilePath -> (FilePath -> HsecId -> m r) -> m r
forReserved root =
_forFilesByYear (root </> dirNameAdvisories </> dirNameReserved)
-- | Invoke a callback for each HSEC ID under each of the advisory
-- subdirectories, excluding the @reserved@ directory. The results
-- are combined monoidally.
--
-- The same ID could appear multiple times. In particular, the callback
-- is invoked for symbolic links as well as regular files.
--
forAdvisory
:: (MonadIO m, Monoid r)
=> FilePath -> (FilePath -> HsecId -> m r) -> m r
forAdvisory root =
_forFilesByYear (root </> dirNameAdvisories </> dirNamePublished)
-- | List parsed Advisories
listAdvisories
:: (MonadIO m)
=> FilePath -> m (Validation [(FilePath, ParseAdvisoryError)] [Advisory])
listAdvisories root =
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 $
withExceptT GitHasNoOOB $ do
gitInfo <- ExceptT $ liftIO $ getAdvisoryGitInfo advisoryPath
pure OutOfBandAttributes
{ oobPublished = firstAppearanceCommitDate gitInfo
, oobModified = lastModificationCommitDate gitInfo
}
fileContent <- liftIO $ T.readFile advisoryPath
pure
$ either Failure Success
$ parseAdvisory NoOverrides oob fileContent
_forFilesByYear
:: (MonadIO m, Monoid r)
=> FilePath -- ^ (sub)directory name
-> (FilePath -> HsecId -> m r)
-> m r
_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