packages feed

hsec-tools-0.5.0.0: src/Security/Advisories/Convert/OSV.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module Security.Advisories.Convert.OSV
  ( convert,
    convertWithLinks,
    DbLinks (..),
    AffectedLinks (..),
    HsecEcosystemSpecific (..),
    haskellLinks,
  )
where

import Data.Aeson
import qualified Data.Text as T
import Data.Void
import Distribution.Pretty (prettyShow)
import Security.Advisories
import Security.Advisories.Core.OsvId (printOsvId)
import qualified Security.OSV as OSV

convert :: Advisory -> OSV.Model Void Void HsecEcosystemSpecific Void
convert adv =
  ( OSV.newModel'
      (T.pack . printHsecId $ advisoryId adv)
      (advisoryModified adv)
  )
    { OSV.modelPublished = Just $ advisoryPublished adv,
      OSV.modelAliases = map printOsvId (advisoryAliases adv),
      OSV.modelRelated = map printOsvId (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 HsecEcosystemSpecific Void
mkAffected aff =
  OSV.Affected
    { OSV.affectedPackage = mkPackage (affectedComponentIdentifier aff),
      OSV.affectedRanges = pure $ mkRange (affectedVersions aff),
      OSV.affectedSeverity = [OSV.Severity (affectedCVSS aff)],
      OSV.affectedEcosystemSpecific =
        if null (affectedApi aff)
          then Nothing
          else Just (HsecEcosystemSpecific (affectedApi aff)),
      OSV.affectedDatabaseSpecific = Nothing
    }

mkPackage :: ComponentIdentifier -> OSV.Package
mkPackage ecosystem =
  OSV.Package
    { OSV.packageName = packageName,
      OSV.packageEcosystem = ecosystemName,
      OSV.packagePurl = Nothing
    }
  where
    (ecosystemName, packageName) = case ecosystem of
      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
mkRange ranges =
  OSV.RangeEcosystem (foldMap mkEvs ranges) Nothing
  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)

convertWithLinks :: DbLinks -> Advisory -> OSV.Model DbLinks AffectedLinks HsecEcosystemSpecific 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
      ]

instance FromJSON DbLinks where
  parseJSON = withObject "DbLinks" $ \o -> do
    dbLinksRepository <- o .: "repository"
    dbLinksOSVs <- o .: "osvs"
    dbLinksHome <- o .: "home"
    pure DbLinks {..}

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
      ]

instance FromJSON AffectedLinks where
  parseJSON = withObject "AffectedLinks" $ \o -> do
    affectedLinksOSV <- o .: "osv"
    affectedLinksHumanLink <- o .: "human_link"
    pure AffectedLinks {..}

newtype HsecEcosystemSpecific = HsecEcosystemSpecific
  { hsecEcosystemAffectedApi :: [AffectedApi]
  }
  deriving stock (Eq, Show)

instance ToJSON HsecEcosystemSpecific where
  toJSON (HsecEcosystemSpecific apis) =
    object
      [ "affected_api" .= apis
      ]

instance FromJSON HsecEcosystemSpecific where
  parseJSON = withObject "HsecEcosystemSpecific" $ \o -> do
    hsecEcosystemAffectedApi <- o .: "affected_api"
    pure HsecEcosystemSpecific {..}

instance ToJSON AffectedApi where
  toJSON AffectedApi {..} =
    object
      [ "module" .= affectedApiModule,
        "name" .= affectedApiName
      ]

instance FromJSON AffectedApi where
  parseJSON = withObject "AffectedApi" $ \o -> do
    affectedApiModule <- o .: "module"
    affectedApiName <- o .: "name"
    pure AffectedApi {..}

mkAffectedWithLinks :: DbLinks -> HsecId -> Affected -> OSV.Affected AffectedLinks HsecEcosystemSpecific 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"