hsec-tools-0.3.0.1: src/Security/Advisories/Format.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Security.Advisories.Format
( FrontMatter (..),
fromAdvisory,
AdvisoryMetadata (..),
Toml.ToTable (..),
Toml.ToValue (..),
Toml.FromValue (..),
)
where
import Control.Applicative ((<|>))
import Commonmark.Types (HasAttributes (..), IsBlock (..), IsInline (..), Rangeable (..), SourceRange (..))
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Data.Monoid (First (..))
import Data.List (intercalate)
import Data.Tuple (swap)
import GHC.Generics (Generic)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time (utc, UTCTime(..), zonedTimeToUTC, localTimeToUTC)
import Distribution.Parsec (eitherParsec)
import Distribution.Pretty (pretty)
import Distribution.Types.Version (Version)
import Distribution.Types.VersionRange (VersionRange)
import Network.URI (parseAbsoluteURI)
import qualified Text.PrettyPrint as Pretty
import qualified Toml
import qualified Toml.Schema as Toml
import Security.Advisories.Core.Advisory
import Security.Advisories.Core.HsecId
import qualified Security.CVSS as CVSS
import Security.OSV (Reference (..), ReferenceType, referenceTypes)
fromAdvisory :: Advisory -> FrontMatter
fromAdvisory advisory =
FrontMatter
{ frontMatterAdvisory =
AdvisoryMetadata
{ amdId = advisoryId advisory,
amdPublished = Just $ advisoryPublished advisory,
amdModified = Just $ advisoryModified advisory,
amdCAPECs = advisoryCAPECs advisory,
amdCWEs = advisoryCWEs advisory,
amdKeywords = advisoryKeywords advisory,
amdAliases = advisoryAliases advisory,
amdRelated = advisoryRelated advisory
},
frontMatterReferences = advisoryReferences advisory,
frontMatterAffected = advisoryAffected advisory
}
-- advisory markdown file.
data FrontMatter = FrontMatter {
frontMatterAdvisory :: AdvisoryMetadata,
frontMatterReferences :: [Reference],
frontMatterAffected :: [Affected]
} deriving (Show, Generic)
instance Toml.FromValue FrontMatter where
fromValue = Toml.parseTableFromValue $
do advisory <- Toml.reqKey "advisory"
affected <- Toml.reqKey "affected"
references <- fromMaybe [] <$> Toml.optKey "references"
pure FrontMatter {
frontMatterAdvisory = advisory,
frontMatterAffected = affected,
frontMatterReferences = references
}
instance Toml.ToValue FrontMatter where
toValue = Toml.defaultTableToValue
instance Toml.ToTable FrontMatter where
toTable x = Toml.table
[ "advisory" Toml..= frontMatterAdvisory x
, "affected" Toml..= frontMatterAffected x
, "references" Toml..= frontMatterReferences x
]
-- | Internal type corresponding to the @[advisory]@ subsection of the
-- TOML frontmatter in an advisory markdown file.
data AdvisoryMetadata = AdvisoryMetadata
{ amdId :: HsecId
, amdModified :: Maybe UTCTime
, amdPublished :: Maybe UTCTime
, amdCAPECs :: [CAPEC]
, amdCWEs :: [CWE]
, amdKeywords :: [Keyword]
, amdAliases :: [T.Text]
, amdRelated :: [T.Text]
}
deriving (Show, Generic)
instance Toml.FromValue AdvisoryMetadata where
fromValue = Toml.parseTableFromValue $
do identifier <- Toml.reqKey "id"
published <- Toml.optKeyOf "date" getDefaultedZonedTime
modified <- Toml.optKeyOf "modified" getDefaultedZonedTime
let optList key = fromMaybe [] <$> Toml.optKey key
capecs <- optList "capec"
cwes <- optList "cwe"
kwds <- optList "keywords"
aliases <- optList "aliases"
related <- optList "related"
pure AdvisoryMetadata
{ amdId = identifier
, amdModified = modified
, amdPublished = published
, amdCAPECs = capecs
, amdCWEs = cwes
, amdKeywords = kwds
, amdAliases = aliases
, amdRelated = related
}
instance Toml.ToValue AdvisoryMetadata where
toValue = Toml.defaultTableToValue
instance Toml.ToTable AdvisoryMetadata where
toTable x = Toml.table $
["id" Toml..= amdId x] ++
["modified" Toml..= y | Just y <- [amdModified x]] ++
["date" Toml..= y | Just y <- [amdPublished x]] ++
["capec" Toml..= amdCAPECs x | not (null (amdCAPECs x))] ++
["cwe" Toml..= amdCWEs x | not (null (amdCWEs x))] ++
["keywords" Toml..= amdKeywords x | not (null (amdKeywords x))] ++
["aliases" Toml..= amdAliases x | not (null (amdAliases x))] ++
["related" Toml..= amdRelated x | not (null (amdRelated x))]
instance Toml.FromValue GHCComponent where
fromValue v = case v of
Toml.Text' _ n
| Just c <- ghcComponentFromText n
-> pure c
| otherwise
-> Toml.failAt (Toml.valueAnn v) $
"Invalid ghc-component '"
<> T.unpack n
<> "', expected "
<> T.unpack (T.intercalate "|" componentNames)
_ -> Toml.failAt (Toml.valueAnn v) $
"Non-text ghc-component, expected"
<> T.unpack (T.intercalate "|" componentNames)
where
componentNames = map ghcComponentToText [minBound..maxBound]
instance Toml.ToValue GHCComponent where
toValue = Toml.Text' () . ghcComponentToText
instance Toml.FromValue Affected where
fromValue =
Toml.parseTableFromValue $ do
ecosystem <-
(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"
versions <- Toml.reqKey "versions"
pure $ Affected
{ affectedComponentIdentifier = ecosystem
, affectedCVSS = cvss
, affectedVersions = versions
, affectedArchitectures = arch
, affectedOS = os
, affectedDeclarations = decls
}
instance Toml.ToValue Affected where
toValue = Toml.defaultTableToValue
instance Toml.ToTable Affected where
toTable x = Toml.table $
ecosystem ++
[ "cvss" Toml..= affectedCVSS x
, "versions" Toml..= affectedVersions x
] ++
[ "os" Toml..= y | Just y <- [affectedOS x]] ++
[ "arch" Toml..= y | Just y <- [affectedArchitectures x]] ++
[ "declarations" Toml..= asTable (affectedDeclarations x) | not (null (affectedDeclarations x))]
where
ecosystem = case affectedComponentIdentifier x of
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]
instance Toml.FromValue AffectedVersionRange where
fromValue = Toml.parseTableFromValue $
do introduced <- Toml.reqKey "introduced"
fixed <- Toml.optKey "fixed"
pure AffectedVersionRange {
affectedVersionRangeIntroduced = introduced,
affectedVersionRangeFixed = fixed
}
instance Toml.ToValue AffectedVersionRange where
toValue = Toml.defaultTableToValue
instance Toml.ToTable AffectedVersionRange where
toTable x = Toml.table $
("introduced" Toml..= affectedVersionRangeIntroduced x) :
["fixed" Toml..= y | Just y <- [affectedVersionRangeFixed x]]
instance Toml.FromValue HsecId where
fromValue v =
do s <- Toml.fromValue v
case parseHsecId s of
Nothing -> Toml.failAt (Toml.valueAnn v) "invalid HSEC-ID: expected HSEC-[0-9]{4,}-[0-9]{4,}"
Just x -> pure x
instance Toml.ToValue HsecId where
toValue = Toml.toValue . printHsecId
instance Toml.FromValue CAPEC where
fromValue v = CAPEC <$> Toml.fromValue v
instance Toml.ToValue CAPEC where
toValue (CAPEC x) = Toml.toValue x
instance Toml.FromValue CWE where
fromValue v = CWE <$> Toml.fromValue v
instance Toml.ToValue CWE where
toValue (CWE x) = Toml.toValue x
instance Toml.FromValue Keyword where
fromValue v = Keyword <$> Toml.fromValue v
instance Toml.ToValue Keyword where
toValue (Keyword x) = Toml.toValue x
-- | Get a datetime with the timezone defaulted to UTC and the time defaulted to midnight
getDefaultedZonedTime :: Toml.Value' l -> Toml.Matcher l UTCTime
getDefaultedZonedTime (Toml.ZonedTime' _ x) = pure (zonedTimeToUTC x)
getDefaultedZonedTime (Toml.LocalTime' _ x) = pure (localTimeToUTC utc x)
getDefaultedZonedTime (Toml.Day' _ x) = pure (UTCTime x 0)
getDefaultedZonedTime v = Toml.failAt (Toml.valueAnn v) "expected a date with optional time and timezone"
instance Toml.FromValue Reference where
fromValue = Toml.parseTableFromValue $
do refType <- Toml.reqKey "type"
url <- Toml.reqKey "url"
pure (Reference refType url)
instance Toml.FromValue ReferenceType where
fromValue (Toml.Text' _ refTypeStr)
| Just a <- lookup refTypeStr (fmap swap referenceTypes) = pure a
fromValue v =
Toml.failAt (Toml.valueAnn v) $
"reference.type should be one of: " ++ intercalate ", " (T.unpack . snd <$> referenceTypes)
instance Toml.ToValue Reference where
toValue = Toml.defaultTableToValue
instance Toml.ToTable Reference where
toTable x = Toml.table
[ "type" Toml..= fromMaybe "UNKNOWN" (lookup (referencesType x) referenceTypes)
, "url" Toml..= referencesUrl x
]
instance Toml.FromValue OS where
fromValue v =
do s <- Toml.fromValue v
case s :: String of
"darwin" -> pure MacOS
"freebsd" -> pure FreeBSD
"linux" -> pure Linux
"linux-android" -> pure Android
"mingw32" -> pure Windows
"netbsd" -> pure NetBSD
"openbsd" -> pure OpenBSD
other -> Toml.failAt (Toml.valueAnn v) ("Invalid OS: " ++ show other)
instance Toml.ToValue OS where
toValue x =
Toml.toValue $
case x of
MacOS -> "darwin" :: String
FreeBSD -> "freebsd"
Linux -> "linux"
Android -> "linux-android"
Windows -> "mingw32"
NetBSD -> "netbsd"
OpenBSD -> "openbsd"
instance Toml.FromValue Architecture where
fromValue v =
do s <- Toml.fromValue v
case parseArchitecture s of
Just a -> pure a
Nothing -> Toml.failAt (Toml.valueAnn v) ("Invalid architecture: " ++ show s)
instance Toml.ToValue Architecture where
toValue = Toml.toValue . printArchitecture
printArchitecture :: Architecture -> Text
printArchitecture = T.toLower . T.pack . show
parseArchitecture :: Text -> Maybe Architecture
parseArchitecture = flip lookup [(printArchitecture arch, arch) | arch <- [minBound .. maxBound]]
instance Toml.FromValue Version where
fromValue v =
do s <- Toml.fromValue v
case eitherParsec s of
Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in version: " ++ err)
Right affected -> pure affected
instance Toml.ToValue Version where
toValue = Toml.toValue . Pretty.render . pretty
instance Toml.FromValue VersionRange where
fromValue v =
do s <- Toml.fromValue v
case eitherParsec s of
Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in version range: " ++ err)
Right affected -> pure affected
instance Toml.ToValue VersionRange where
toValue = Toml.toValue . Pretty.render . pretty
instance Toml.FromValue CVSS.CVSS where
fromValue v =
do s <- Toml.fromValue v
case CVSS.parseCVSS s of
Left err -> Toml.failAt (Toml.valueAnn v) ("parse error in cvss: " ++ show err)
Right cvss -> pure cvss
instance Toml.ToValue CVSS.CVSS where
toValue = Toml.toValue . CVSS.cvssVectorString
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
-- the first block element. We can use it to delete the lines
-- from the input.
--
newtype FirstSourceRange = FirstSourceRange (First SourceRange)
deriving (Show, Semigroup, Monoid)
instance Rangeable FirstSourceRange where
ranged range = (FirstSourceRange (First (Just range)) <>)
instance HasAttributes FirstSourceRange where
addAttributes _ = id
instance IsBlock FirstSourceRange FirstSourceRange where
paragraph _ = mempty
plain _ = mempty
thematicBreak = mempty
blockQuote _ = mempty
codeBlock _ = mempty
heading _ = mempty
rawBlock _ = mempty
referenceLinkDefinition _ = mempty
list _ = mempty
instance IsInline FirstSourceRange where
lineBreak = mempty
softBreak = mempty
str _ = mempty
entity _ = mempty
escapedChar _ = mempty
emph = id
strong = id
link _ _ _ = mempty
image _ _ _ = mempty
code _ = mempty
rawInline _ _ = mempty