Skip to content

Commit 2e89aa5

Browse files
lib/advisories: add filesystem helpers
Add the `Security.Advisories.Filesystem` module with helpers to: - check that a directory appears to be the *security-advisories* repo - list reserved, in-use or all allocated HSEC IDs - return the greatest allocated ID in the database - iterate over reserved IDs, or in-use advisory IDs Also export values for common path components so that we do not need to hardcoded them in other places (with associated risk of typos). These helpers will support the creation of CLI commands that perform checks or modify the *security-advisories* database.
1 parent 015e6b6 commit 2e89aa5

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

code/hsec-tools/hsec-tools.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ tested-with:
3030
library
3131
exposed-modules: Security.Advisories
3232
, Security.Advisories.Definition
33+
, Security.Advisories.Filesystem
3334
, Security.Advisories.Git
3435
, Security.Advisories.HsecId
3536
, Security.Advisories.Parse
@@ -51,6 +52,7 @@ library
5152
toml-reader ^>= 0.1 || ^>= 0.2,
5253
aeson >= 2.0.1.0 && < 3,
5354
pandoc-types >= 1.22 && < 2,
55+
pathwalk >= 0.3,
5456
parsec >= 3 && < 4,
5557
commonmark-pandoc >= 0.2 && < 0.3
5658
, safe >= 0.3
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
{-|
2+
3+
Helpers for the /security-advisories/ file system.
4+
5+
Top-level functions that take a @FilePath@ expect the path to the
6+
top-level directory of the /security-advisories/ repository (i.e.
7+
it must have the @advisories/@ subdirectory).
8+
9+
-}
10+
module Security.Advisories.Filesystem
11+
(
12+
dirNameAdvisories
13+
, dirNameReserved
14+
, isSecurityAdvisoriesRepo
15+
, getReservedIds
16+
, getAdvisoryIds
17+
, getAllocatedIds
18+
, greatestId
19+
, getGreatestId
20+
, forReserved
21+
, forAdvisory
22+
) where
23+
24+
import Control.Applicative (liftA2)
25+
import Data.Foldable (fold)
26+
import Data.Semigroup (Max(Max, getMax))
27+
import Data.Traversable (for)
28+
29+
import Control.Monad.IO.Class (MonadIO)
30+
import Control.Monad.Writer.Strict (execWriterT, tell)
31+
import System.FilePath ((</>), takeBaseName)
32+
import System.Directory (doesDirectoryExist)
33+
import System.Directory.PathWalk
34+
35+
import Security.Advisories.HsecId (HsecId, parseHsecId, placeholder)
36+
37+
38+
dirNameAdvisories :: FilePath
39+
dirNameAdvisories = "advisories"
40+
41+
dirNameReserved :: FilePath
42+
dirNameReserved = "reserved"
43+
44+
-- | Check whether the directory appears to be the root of a
45+
-- /security-advisories/ filesystem. Only checks that the
46+
-- @advisories@ subdirectory exists.
47+
--
48+
isSecurityAdvisoriesRepo :: FilePath -> IO Bool
49+
isSecurityAdvisoriesRepo path =
50+
doesDirectoryExist (path </> dirNameAdvisories)
51+
52+
53+
-- | Get a list of reserved HSEC IDs. The order is unspecified.
54+
--
55+
getReservedIds :: FilePath -> IO [HsecId]
56+
getReservedIds root =
57+
forReserved root (\_ hsid -> pure [hsid])
58+
59+
-- | Get a list of used IDs (does not include reserved IDs)
60+
-- There may be duplicates and the order is unspecified.
61+
--
62+
getAdvisoryIds :: FilePath -> IO [HsecId]
63+
getAdvisoryIds root =
64+
forAdvisory root (\_ hsid -> pure [hsid])
65+
66+
-- | Get all allocated IDs, including reserved IDs.
67+
-- There may be duplicates and the order is unspecified.
68+
--
69+
getAllocatedIds :: FilePath -> IO [HsecId]
70+
getAllocatedIds root =
71+
liftA2 (<>)
72+
(getAdvisoryIds root)
73+
(getReservedIds root)
74+
75+
-- | Return the greatest ID in a collection of IDs. If the
76+
-- collection is empty, return the 'placeholder'.
77+
--
78+
greatestId :: (Foldable t) => t HsecId -> HsecId
79+
greatestId = getMax . foldr ((<>) . Max) (Max placeholder)
80+
81+
-- | Return the greatest ID in the database, including reserved IDs.
82+
-- If there are IDs in the database, returns the 'placeholder'.
83+
--
84+
getGreatestId :: FilePath -> IO HsecId
85+
getGreatestId = fmap greatestId . getAllocatedIds
86+
87+
88+
-- | Invoke a callback for each HSEC ID in the reserved
89+
-- directory. The results are combined monoidally.
90+
--
91+
forReserved
92+
:: (MonadIO m, Monoid r)
93+
=> FilePath -> (FilePath -> HsecId -> m r) -> m r
94+
forReserved root =
95+
_forFiles (root </> dirNameAdvisories </> dirNameReserved)
96+
97+
-- | Invoke a callback for each HSEC ID under each of the advisory
98+
-- subdirectories, excluding the @reserved@ directory. The results
99+
-- are combined monoidally.
100+
--
101+
-- The same ID could appear multiple times. In particular, the callback
102+
-- is invoked for symbolic links as well as regular files.
103+
--
104+
forAdvisory
105+
:: (MonadIO m, Monoid r)
106+
=> FilePath -> (FilePath -> HsecId -> m r) -> m r
107+
forAdvisory root go = do
108+
let dir = root </> dirNameAdvisories
109+
subdirs <- filter (/= dirNameReserved) <$> _getSubdirs dir
110+
fmap fold $ for subdirs $ \subdir -> _forFiles (dir </> subdir) go
111+
112+
-- | Get names (not paths) of subdirectories of the given directory
113+
-- (one level). There's no monoidal, interruptible variant of
114+
-- @pathWalk@ so we use @WriterT@ to smuggle the result out.
115+
--
116+
_getSubdirs :: (MonadIO m) => FilePath -> m [FilePath]
117+
_getSubdirs root =
118+
execWriterT $
119+
pathWalkInterruptible root $ \_ subdirs _ -> do
120+
tell subdirs
121+
pure Stop
122+
123+
_forFiles
124+
:: (MonadIO m, Monoid r)
125+
=> FilePath -- ^ (sub)directory name
126+
-> (FilePath -> HsecId -> m r)
127+
-> m r
128+
_forFiles root go =
129+
pathWalkAccumulate root $ \_ _ files ->
130+
fmap fold $ for files $ \file ->
131+
case parseHsecId (takeBaseName file) of
132+
Nothing -> pure mempty
133+
Just hsid -> go (root </> file) hsid

0 commit comments

Comments
 (0)