Skip to content

Commit

Permalink
Add Advisory.Ecosystem to support GHC's advisory
Browse files Browse the repository at this point in the history
This change updates the affected schema to support GHC ecosystem
with the "ghc-component" key.
This change also implements a new OOB attribute to validate that
the advisory path matchs at least one affected.
  • Loading branch information
TristanCacqueray committed Jul 3, 2024
1 parent 0666c38 commit aef7bcb
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 47 deletions.
28 changes: 26 additions & 2 deletions code/hsec-core/src/Security/Advisories/Core/Advisory.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DerivingVia, OverloadedStrings #-}

module Security.Advisories.Core.Advisory
( Advisory(..)
Expand All @@ -10,6 +10,10 @@ module Security.Advisories.Core.Advisory
, AffectedVersionRange(..)
, OS(..)
, Keyword(..)
, Ecosystem(..)
, GHCComponent(..)
, ghcComponentToText
, ghcComponentFromText
)
where

Expand Down Expand Up @@ -44,10 +48,30 @@ data Advisory = Advisory
}
deriving stock (Show)

data Ecosystem = Hackage Text | GHC GHCComponent
deriving stock (Show, Eq)

-- Keep this list in sync with the 'ghcComponentFromText' below
data GHCComponent = GHCCompiler | GHCi | GHCRTS
deriving stock (Show, Eq)

ghcComponentToText :: GHCComponent -> Text
ghcComponentToText c = case c of
GHCCompiler -> "compiler"
GHCi -> "ghci"
GHCRTS -> "rts"

ghcComponentFromText :: Text -> Maybe GHCComponent
ghcComponentFromText c = case c of
"compiler" -> Just GHCCompiler
"ghci" -> Just GHCi
"rts" -> Just GHCRTS
_ -> Nothing

-- | An affected package (or package component). An 'Advisory' must
-- mention one or more packages.
data Affected = Affected
{ affectedPackage :: Text
{ affectedEcosystem :: Ecosystem
, affectedCVSS :: CVSS.CVSS
, affectedVersions :: [AffectedVersionRange]
, affectedArchitectures :: Maybe [Architecture]
Expand Down
8 changes: 6 additions & 2 deletions code/hsec-tools/app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import qualified Security.Advisories.Convert.OSV as OSV
import Security.Advisories.Git
import Security.Advisories.Queries (listVersionRangeAffectedBy)
import Security.Advisories.Generate.HTML
import Security.Advisories.Filesystem (parseEcosystem)

import qualified Command.Reserve

Expand Down Expand Up @@ -156,11 +157,14 @@ withAdvisory go file = do

oob <- runExceptT $ case file of
Nothing -> throwE StdInHasNoOOB
Just path -> withExceptT GitHasNoOOB $ do
gitInfo <- ExceptT $ liftIO $ getAdvisoryGitInfo path
Just path -> do
ecosystem <- parseEcosystem path
withExceptT GitHasNoOOB $ do
gitInfo <- ExceptT $ liftIO $ getAdvisoryGitInfo path
pure OutOfBandAttributes
{ oobPublished = firstAppearanceCommitDate gitInfo
, oobModified = lastModificationCommitDate gitInfo
, oobEcosystem = ecosystem
}

case parseAdvisory NoOverrides oob input of
Expand Down
14 changes: 9 additions & 5 deletions code/hsec-tools/src/Security/Advisories/Convert/OSV.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,23 @@ convert adv =
mkAffected :: Affected -> OSV.Affected Void Void Void
mkAffected aff =
OSV.Affected
{ OSV.affectedPackage = mkPackage (affectedPackage aff)
{ OSV.affectedPackage = mkPackage (affectedEcosystem aff)
, OSV.affectedRanges = pure $ mkRange (affectedVersions aff)
, OSV.affectedSeverity = [OSV.Severity (affectedCVSS aff)]
, OSV.affectedEcosystemSpecific = Nothing
, OSV.affectedDatabaseSpecific = Nothing
}

mkPackage :: T.Text -> OSV.Package
mkPackage name = OSV.Package
{ OSV.packageName = name
, OSV.packageEcosystem = "Hackage"
mkPackage :: Ecosystem -> OSV.Package
mkPackage ecosystem = OSV.Package
{ OSV.packageName = packageName
, OSV.packageEcosystem = ecosystemName
, OSV.packagePurl = Nothing
}
where
(ecosystemName, packageName) = case ecosystem of
Hackage n -> ("Hackage", n)
GHC c -> ("GHC", ghcComponentToText c)

mkRange :: [AffectedVersionRange] -> OSV.Range Void
mkRange ranges =
Expand Down
35 changes: 24 additions & 11 deletions code/hsec-tools/src/Security/Advisories/Filesystem.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Security.Advisories.Filesystem
, forReserved
, forAdvisory
, listAdvisories
, parseEcosystem
) where

import Control.Applicative (liftA2)
Expand All @@ -29,17 +30,19 @@ import Data.Traversable (for)

import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Writer.Strict (execWriterT, tell)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import System.FilePath ((</>), takeBaseName)
import System.FilePath ((</>), takeBaseName, splitDirectories)
import System.Directory (doesDirectoryExist, pathIsSymbolicLink)
import System.Directory.PathWalk
import Validation (Validation (..))

import Security.Advisories (Advisory, AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), ParseAdvisoryError, parseAdvisory)
import Security.Advisories (Advisory, AttributeOverridePolicy (NoOverrides), OutOfBandAttributes (..), ParseAdvisoryError, parseAdvisory, Ecosystem(..))
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))
import Security.Advisories.Parse (OOBError(GitHasNoOOB, PathHasNoEcosystem))
import Security.Advisories.Core.Advisory (ghcComponentFromText)


dirNameAdvisories :: FilePath
Expand Down Expand Up @@ -126,15 +129,18 @@ listAdvisories root =
if isSym
then return $ pure []
else do
oob <- runExceptT $ withExceptT GitHasNoOOB $ do
gitInfo <- ExceptT $ liftIO $ getAdvisoryGitInfo advisoryPath
pure OutOfBandAttributes
{ oobPublished = firstAppearanceCommitDate gitInfo
, oobModified = lastModificationCommitDate gitInfo
}
oob <- runExceptT $ do
ecosystem <- parseEcosystem advisoryPath
withExceptT GitHasNoOOB $ do
gitInfo <- ExceptT $ liftIO $ getAdvisoryGitInfo advisoryPath
pure OutOfBandAttributes
{ oobPublished = firstAppearanceCommitDate gitInfo
, oobModified = lastModificationCommitDate gitInfo
, oobEcosystem = ecosystem
}
fileContent <- liftIO $ T.readFile advisoryPath
pure
$ either (Failure . (: [])) (Success . (: []))
pure
$ either (Failure . (: [])) (Success . (: []))
$ parseAdvisory NoOverrides oob fileContent

-- | Get names (not paths) of subdirectories of the given directory
Expand All @@ -159,3 +165,10 @@ _forFiles root go =
case parseHsecId (takeBaseName file) of
Nothing -> pure mempty
Just hsid -> go (dir </> file) hsid

parseEcosystem :: Monad m => FilePath -> ExceptT OOBError m (Maybe Ecosystem)
parseEcosystem 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 PathHasNoEcosystem
_ -> pure Nothing
10 changes: 8 additions & 2 deletions code/hsec-tools/src/Security/Advisories/Generate/HTML.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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 (Ecosystem (..), ghcComponentToText)

-- * Actions

Expand Down Expand Up @@ -87,7 +88,7 @@ data AdvisoryR = AdvisoryR
deriving stock (Show)

data AffectedPackageR = AffectedPackageR
{ packageName :: Text,
{ ecosystem :: Ecosystem,
introduced :: Text,
fixed :: Maybe Text
}
Expand Down Expand Up @@ -118,6 +119,11 @@ listByDates advisories =
td_ [class_ "advisory-packages"] $ toHtml $ T.intercalate "," $ packageName <$> advisoryAffected advisory
td_ [class_ "advisory-summary"] $ toHtml $ advisorySummary advisory

packageName :: AffectedPackageR -> Text
packageName af = case ecosystem af of
Hackage n -> n
GHC c -> "ghc:" <> ghcComponentToText c

listByPackages :: [AdvisoryR] -> Html ()
listByPackages advisories =
inPage PageListByPackages $ do
Expand Down Expand Up @@ -228,7 +234,7 @@ toAdvisoryR x =
toAffectedPackageR p =
flip map (Advisories.affectedVersions p) $ \versionRange ->
AffectedPackageR
{ packageName = Advisories.affectedPackage p,
{ ecosystem = Advisories.affectedEcosystem p,
introduced = T.pack $ prettyShow $ Advisories.affectedVersionRangeIntroduced versionRange,
fixed = T.pack . prettyShow <$> Advisories.affectedVersionRangeFixed versionRange
}
Expand Down
59 changes: 40 additions & 19 deletions code/hsec-tools/src/Security/Advisories/Parse.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Data.List (intercalate)
import Data.Maybe (fromMaybe)
import Data.Monoid (First(..))
import Data.Tuple (swap)
import Control.Applicative ((<|>))
import GHC.Generics (Generic)

import qualified Data.Map as Map
Expand Down Expand Up @@ -61,14 +62,10 @@ type OOB = Either OOBError OutOfBandAttributes
-- | A source of attributes supplied out of band from the advisory
-- content. Values provided out of band are treated according to
-- the 'AttributeOverridePolicy'.
--
-- The convenient way to construct a value of this type is to start
-- with 'emptyOutOfBandAttributes', then use the record accessors to
-- set particular fields.
--
data OutOfBandAttributes = OutOfBandAttributes
{ oobModified :: UTCTime
, oobPublished :: UTCTime
, oobEcosystem :: Maybe Ecosystem
}
deriving (Show)

Expand All @@ -86,8 +83,8 @@ data ParseAdvisoryError
deriving stock (Eq, Show, Generic)

-- | @since 0.2.0.0
instance Exception ParseAdvisoryError where
displayException = T.unpack . \case
instance Exception ParseAdvisoryError where
displayException = T.unpack . \case
MarkdownError _ explanation -> "Markdown parsing error:\n" <> explanation
MarkdownFormatError explanation -> "Markdown structure error:\n" <> explanation
TomlError _ explanation -> "Couldn't parse front matter as TOML:\n" <> explanation
Expand All @@ -96,14 +93,16 @@ instance Exception ParseAdvisoryError where
-- | errors that may occur while ingesting oob data
--
-- @since 0.2.0.0
data OOBError
data OOBError
= StdInHasNoOOB -- ^ we obtain the advisory via stdin and can hence not parse git history
| PathHasNoEcosystem -- ^ 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
displayOOBError :: OOBError -> String
displayOOBError = \case
StdInHasNoOOB -> "stdin doesn't provide out of band information"
PathHasNoEcosystem -> "the path is missing 'hackage' or 'ghc' directory"
GitHasNoOOB gitErr -> "no out of band information obtained with git error:\n"
<> explainGitError gitErr

Expand Down Expand Up @@ -195,6 +194,10 @@ parseAdvisoryTable oob policy doc summary details html tab =
(oobPublished <$> oob)
"advisory.modified"
(amdModified (frontMatterAdvisory fm))
let affected = frontMatterAffected fm
case oob of
Right (OutOfBandAttributes _ _ (Just ecosystem)) -> validateEcosystem ecosystem affected
_ -> pure ()
pure Advisory
{ advisoryId = amdId (frontMatterAdvisory fm)
, advisoryPublished = published
Expand All @@ -204,14 +207,20 @@ parseAdvisoryTable oob policy doc summary details html tab =
, advisoryKeywords = amdKeywords (frontMatterAdvisory fm)
, advisoryAliases = amdAliases (frontMatterAdvisory fm)
, advisoryRelated = amdRelated (frontMatterAdvisory fm)
, advisoryAffected = frontMatterAffected fm
, advisoryAffected = affected
, advisoryReferences = frontMatterReferences fm
, advisoryPandoc = doc
, advisoryHtml = html
, advisorySummary = summary
, advisoryDetails = details
}

-- | Make sure one of the affected match the ecosystem
validateEcosystem :: MonadFail m => Ecosystem -> [Affected] -> m ()
validateEcosystem ecosystem xs
| any (\affected -> affectedEcosystem affected == ecosystem) xs = pure ()
| otherwise = fail $ "Expected an affected to match the ecosystem: " <> show ecosystem

-- | Internal type corresponding to the complete raw TOML content of an
-- advisory markdown file.
data FrontMatter = FrontMatter {
Expand Down Expand Up @@ -290,16 +299,25 @@ instance Toml.ToTable AdvisoryMetadata where
["aliases" Toml..= amdAliases x | not (null (amdAliases x))] ++
["Related" Toml..= amdRelated x | not (null (amdRelated x))]

instance Toml.FromValue GHCComponent where
fromValue = \case
Toml.Text' _ n
| Just c <- ghcComponentFromText n -> pure c
v -> Toml.failAt (Toml.valueAnn v) "Invalid component, expected compiler|ghci|rts"

instance Toml.ToValue GHCComponent where
toValue = Toml.Text' () . ghcComponentToText

instance Toml.FromValue Affected where
fromValue = Toml.parseTableFromValue $
do package <- Toml.reqKey "package"
do ecosystem <- (Hackage <$> Toml.reqKey "package") <|> (GHC <$> Toml.reqKey "ghc-component")
cvss <- Toml.reqKey "cvss" -- TODO validate CVSS format
os <- Toml.optKey "os"
arch <- Toml.optKey "arch"
decls <- maybe [] Map.toList <$> Toml.optKey "declarations"
versions <- Toml.reqKey "versions"
pure $ Affected
{ affectedPackage = package
{ affectedEcosystem = ecosystem
, affectedCVSS = cvss
, affectedVersions = versions
, affectedArchitectures = arch
Expand All @@ -312,14 +330,17 @@ instance Toml.ToValue Affected where

instance Toml.ToTable Affected where
toTable x = Toml.table $
[ "package" Toml..= affectedPackage x
, "cvss" Toml..= affectedCVSS x
ecosystem ++
[ "cvss" Toml..= affectedCVSS x
, "versions" Toml..= affectedVersions x
] ++
[ "os" Toml..= y | Just y <- [affectedOS x]] ++
[ "arch" Toml..= y | Just y <- [affectedArchitectures x]] ++
[ "declarations" Toml..= asTable (affectedDeclarations x) | not (null (affectedDeclarations x))]
where
ecosystem = case affectedEcosystem x of
Hackage pkg -> ["package" Toml..= pkg]
GHC c -> ["ghc-component" Toml..= c]
asTable kvs = Map.fromList [(T.unpack k, v) | (k,v) <- kvs]

instance Toml.FromValue AffectedVersionRange where
Expand Down Expand Up @@ -584,10 +605,10 @@ mergeOobMandatory
-> m a
mergeOobMandatory policy eoob doob k ib =
mergeOob policy eoob k ib everythingFailed pure
where
everythingFailed e = fail $ unlines
[ "while trying to lookup mandatory key " <> show k <> ":"
, doob e
where
everythingFailed e = fail $ unlines
[ "while trying to lookup mandatory key " <> show k <> ":"
, doob e
]

-- | A solution to an awkward problem: how to delete the TOML
Expand Down
7 changes: 4 additions & 3 deletions code/hsec-tools/src/Security/Advisories/Queries.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ isAffectedByHelper checkWithRange queryPackageName queryVersionish =
any checkAffected . advisoryAffected
where
checkAffected :: Affected -> Bool
checkAffected affected =
queryPackageName == affectedPackage affected
&& checkWithRange queryVersionish (fromAffected affected)
checkAffected affected = case affectedEcosystem 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
Expand Down
3 changes: 2 additions & 1 deletion code/hsec-tools/test/Spec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ doGoldenTest fp = goldenVsString fp (fp <> ".golden") (LText.encodeUtf8 <$> doCh
doCheck = do
input <- T.readFile fp
let fakeDate = UTCTime (fromOrdinalDate 1970 0) 0
attr = OutOfBandAttributes
attr = OutOfBandAttributes
{ oobPublished = fakeDate
, oobModified = fakeDate
, oobEcosystem = Nothing
}
res = parseAdvisory NoOverrides (Right attr) input
osvExport = case res of
Expand Down
2 changes: 1 addition & 1 deletion code/hsec-tools/test/Spec/QueriesSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ mkAdvisory versionRange =
, advisoryRelated = [ "CVE-2022-YYYY" , "CVE-2022-ZZZZ" ]
, advisoryAffected =
[ Affected
{ affectedPackage = packageName
{ affectedEcosystem = Hackage packageName
, affectedCVSS = cvss
, affectedVersions = mkAffectedVersions versionRange
, affectedArchitectures = Nothing
Expand Down
Loading

0 comments on commit aef7bcb

Please sign in to comment.