From 0a16ceabd4bb509cc34e73abb1760aa6de64838f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri=20de=20Tarl=C3=A9?= Date: Fri, 27 Dec 2024 12:21:39 +0100 Subject: [PATCH 1/2] [FLORA-232] Advisory Search --- assets/css/3-screens/1-package/5-security.css | 33 ++++---- docs/docs/search.md | 3 +- flora.cabal | 27 +++++- ghc-tags.yaml | 1 + .../Advisories/Model/Affected/Query.hs | 84 ++++++++++++++++++- src/core/Flora/Model/Release/Query.hs | 2 +- src/{core => search}/Flora/Search.hs | 22 +++++ src/web/FloraWeb/Components/PaginationNav.hs | 2 + src/web/FloraWeb/Links.hs | 11 +++ src/web/FloraWeb/Pages/Server/Search.hs | 4 + src/web/FloraWeb/Pages/Templates/Packages.hs | 25 +++--- .../Pages/Templates/Screens/Search.hs | 17 +++- 12 files changed, 198 insertions(+), 33 deletions(-) rename src/{core => search}/Flora/Search.hs (90%) diff --git a/assets/css/3-screens/1-package/5-security.css b/assets/css/3-screens/1-package/5-security.css index 325c9ad8..2f63ce70 100644 --- a/assets/css/3-screens/1-package/5-security.css +++ b/assets/css/3-screens/1-package/5-security.css @@ -1,25 +1,26 @@ -.advisory-list { - display: table; - border-collapse: separate; - border-spacing: 12px; -} - .advisory-list__head { - display: table-header-group; - border-inline: solid; - font-size: 1.25rem; -} - -.advisory-list__body { - display: table-row-group; + display: none; } @media only screen and (--viewport-md) { + .advisory-list { + display: table; + border-collapse: separate; + border-spacing: 12px; + } + + .advisory-list__head { + display: table-header-group; + border-inline: solid; + font-size: 1.25rem; + } + + .advisory-list__body { + display: table-row-group; + } + .advisory-list__header { display: table-cell; } } -.advisory-list__header { - display: none; -} diff --git a/docs/docs/search.md b/docs/docs/search.md index 2447c615..268316f4 100644 --- a/docs/docs/search.md +++ b/docs/docs/search.md @@ -3,13 +3,14 @@ title: Search features slug: search-features --- -While searching for packages you may want to refine the search terms with modifiers. +While searching for packages you may want to refine the search terms with modifiers. Currently, the following modifiers are available: * `depends:<@namespace>/`: Shows the dependents page for a package * `in:<@namespace> `: Searches for a package name in the specified namespace * `in:<@namespace>`: Lists packages in a namespace * `exe:`: Search for packages that have an executable component with `` as the search string +* `hsec:`: Search for security advisories in the HSEC Database. These modifiers must be placed at the very beginning of the search query, otherwise they will be interpreted as a search term. diff --git a/flora.cabal b/flora.cabal index 1f6471b8..6ad8dd18 100644 --- a/flora.cabal +++ b/flora.cabal @@ -147,7 +147,6 @@ library Flora.Model.User.Query Flora.Model.User.Update Flora.QRCode - Flora.Search Flora.Tracing JSON Log.Backend.File @@ -228,6 +227,29 @@ library ghc-options: -fplugin=Effectful.Plugin +library flora-search + import: common-extensions + import: common-ghc-options + hs-source-dirs: ./src/search + + -- cabal-fmt: expand src/search + exposed-modules: Flora.Search + build-depends: + , aeson + , base + , effectful-core + , flora + , flora-advisories + , log-base + , log-effectful + , monad-time-effectful + , pg-transact-effectful + , text + , text-display + , tracing + , tracing-effectful + , vector + library flora-advisories import: common-extensions import: common-ghc-options @@ -369,6 +391,7 @@ library flora-web , flora , flora-advisories , flora-jobs + , flora-search , haddock-library , htmx-lucid , http-api-data @@ -517,6 +540,7 @@ executable flora-cli , filepath , flora , flora-advisories + , flora-search , flora-web , hsec-core , log-base @@ -562,6 +586,7 @@ test-suite flora-test , filepath , flora , flora-advisories + , flora-search , flora-web , hedgehog , http-client diff --git a/ghc-tags.yaml b/ghc-tags.yaml index 24e60c74..893cc798 100644 --- a/ghc-tags.yaml +++ b/ghc-tags.yaml @@ -5,6 +5,7 @@ exclude_paths: - dist - dist-newstyle - assets +- _build extensions: - BangPatterns - BlockArguments diff --git a/src/advisories/Advisories/Model/Affected/Query.hs b/src/advisories/Advisories/Model/Affected/Query.hs index 48a3b2fb..88df575c 100644 --- a/src/advisories/Advisories/Model/Affected/Query.hs +++ b/src/advisories/Advisories/Model/Affected/Query.hs @@ -2,11 +2,12 @@ module Advisories.Model.Affected.Query where +import Data.Text (Text) import Data.Vector (Vector) import Database.PostgreSQL.Entity -import Database.PostgreSQL.Entity.DBT (QueryNature (..), query) +import Database.PostgreSQL.Entity.DBT (QueryNature (..), query, queryOne) import Database.PostgreSQL.Entity.Types (field) -import Database.PostgreSQL.Simple (Only (..)) +import Database.PostgreSQL.Simple (Only (..), Query) import Database.PostgreSQL.Simple.SqlQQ import Effectful import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) @@ -57,9 +58,86 @@ SELECT s0.hsec_id , s0.published , a1.cvss FROM security_advisories AS s0 - INNER JOIN affected_packages AS a1 ON s0.advisory_id = a1.advisory_id + INNER JOIN affected_packages AS a1 ON s0.advisory_id = a1.advisory_id INNER JOIN affected_version_ranges AS a2 ON a1.affected_package_id = a2.affected_package_id INNER JOIN packages AS p3 ON a1.package_id = p3.package_id WHERE a1.package_id = ? |] (Only packageId) + +searchInAdvisories :: DB :> es => (Word, Word) -> Text -> Eff es (Vector PackageAdvisoryPreview) +searchInAdvisories (offset, limit) searchTerm = + dbtToEff $ + query + Select + searchAdvisoriesQuery + (searchTerm, searchTerm, offset, limit) + +searchAdvisoriesQuery :: Query +searchAdvisoriesQuery = + [sql| +WITH results AS ( + SELECT s0.hsec_id + , s0.summary + , CASE + WHEN a2.fixed_version IS NULL + THEN FALSE + ELSE TRUE + END as fixed + , s0.published + , a1.cvss + , word_similarity(s0.summary, ?) as rating + FROM security_advisories AS s0 + INNER JOIN affected_packages AS a1 ON s0.advisory_id = a1.advisory_id + INNER JOIN affected_version_ranges AS a2 ON a1.affected_package_id = a2.affected_package_id + INNER JOIN packages AS p3 ON a1.package_id = p3.package_id + WHERE ? <% s0.summary + ORDER BY rating desc, s0.summary asc + OFFSET ? + LIMIT ? +) + +SELECT r0.hsec_id + , r0.summary + , r0.fixed + , r0.published + , r0.cvss +FROM results as r0 + |] + +countAdvisorySearchResults :: DB :> es => Text -> Eff es Word +countAdvisorySearchResults searchTerm = + dbtToEff $ do + (result :: Maybe (Only Int)) <- + queryOne + Select + countAdvisorySearchResultsQuery + (searchTerm, searchTerm) + case result of + Just (Only n) -> pure $ fromIntegral n + Nothing -> pure 0 + +countAdvisorySearchResultsQuery :: Query +countAdvisorySearchResultsQuery = + [sql| +WITH results AS ( + SELECT s0.hsec_id + , s0.summary + , CASE + WHEN a2.fixed_version IS NULL + THEN FALSE + ELSE TRUE + END as fixed + , s0.published + , a1.cvss + , word_similarity(s0.summary, ?) as rating + FROM security_advisories AS s0 + INNER JOIN affected_packages AS a1 ON s0.advisory_id = a1.advisory_id + INNER JOIN affected_version_ranges AS a2 ON a1.affected_package_id = a2.affected_package_id + INNER JOIN packages AS p3 ON a1.package_id = p3.package_id + WHERE ? <% s0.summary + ORDER BY rating desc, s0.summary asc +) + +SELECT COUNT(*) FROM results as r0 + |] diff --git a/src/core/Flora/Model/Release/Query.hs b/src/core/Flora/Model/Release/Query.hs index 25f2941a..592de54b 100644 --- a/src/core/Flora/Model/Release/Query.hs +++ b/src/core/Flora/Model/Release/Query.hs @@ -30,8 +30,8 @@ import Data.Vector qualified as Vector import Database.PostgreSQL.Entity import Database.PostgreSQL.Entity.DBT (QueryNature (..), query, queryOne, queryOne_, query_) import Database.PostgreSQL.Entity.Types (field) -import Database.PostgreSQL.Simple (In (..), Only (..), Query) import Database.PostgreSQL.Simple.SqlQQ +import Database.PostgreSQL.Simple.Types (In (..), Only (..), Query) import Distribution.Orphans.Version () import Distribution.Version (Version) import Effectful diff --git a/src/core/Flora/Search.hs b/src/search/Flora/Search.hs similarity index 90% rename from src/core/Flora/Search.hs rename to src/search/Flora/Search.hs index 35412d6a..5e66340c 100644 --- a/src/core/Flora/Search.hs +++ b/src/search/Flora/Search.hs @@ -17,8 +17,12 @@ import Effectful import Effectful.Log (Log) import Effectful.PostgreSQL.Transact.Effect (DB) import Effectful.Time (Time) +import Effectful.Trace import Log qualified +import Monitor.Tracing qualified as Tracing +import Advisories.Model.Affected.Query qualified as Query +import Advisories.Model.Affected.Types (PackageAdvisoryPreview) import Flora.Logging import Flora.Model.Package ( Namespace (..) @@ -44,6 +48,7 @@ data SearchAction -- ^ Search within the package | SearchInNamespace Namespace PackageName | SearchExecutable Text + | SearchInAdvisories Text deriving (Eq, Ord, Show) instance Display SearchAction where @@ -60,6 +65,8 @@ instance Display SearchAction where "Package " <> displayBuilder namespace <> "/" <> displayBuilder packageName displayBuilder (SearchExecutable executableName) = "Executable " <> displayBuilder executableName + displayBuilder (SearchInAdvisories searchTerm) = + "Search in Advisories: " <> displayBuilder searchTerm searchPackageByName :: (DB :> es, Log :> es, Time :> es) @@ -158,6 +165,20 @@ searchExecutable (offset, limit) queryString = do ] pure (count, results) +searchInAdvisories + :: (DB :> es, Trace :> es) + => (Word, Word) + -> Text + -> Eff es (Word, Vector PackageAdvisoryPreview) +searchInAdvisories (offset, limit) queryString = do + results <- + Tracing.childSpan "Query.searchInAdvisories" $ + Query.searchInAdvisories (offset, limit) queryString + count <- + Tracing.childSpan "Query.countAdvisorySearchResults" $ + Query.countAdvisorySearchResults queryString + pure (count, results) + dependencyInfoToPackageInfo :: DependencyInfo -> PackageInfo dependencyInfoToPackageInfo dep = PackageInfo @@ -228,6 +249,7 @@ parseSearchQuery = \case Just $ ListAllPackagesInNamespace namespace _ -> Just $ SearchPackages rest (Text.stripPrefix "exe:" -> Just rest) -> Just $ SearchExecutable rest + (Text.stripPrefix "hsec:" -> Just rest) -> Just $ SearchInAdvisories rest e -> Just $ SearchPackages e -- Determine if the string is diff --git a/src/web/FloraWeb/Components/PaginationNav.hs b/src/web/FloraWeb/Components/PaginationNav.hs index b2be4bc6..d6b6454b 100644 --- a/src/web/FloraWeb/Components/PaginationNav.hs +++ b/src/web/FloraWeb/Components/PaginationNav.hs @@ -52,6 +52,8 @@ mkURL (DependentsOf namespace packageName mbSearchString) pageNumber = Links.dependentsPage namespace packageName pageNumber <> "q=" <> toUrlPiece mbSearchString mkURL (SearchExecutable searchString) pageNumber = "/" <> toUrlPiece (Links.packageWithExecutable pageNumber searchString) +mkURL (SearchInAdvisories searchString) pageNumber = + "/" <> toUrlPiece (Links.searchInAdvisories pageNumber searchString) paginate :: Word diff --git a/src/web/FloraWeb/Links.hs b/src/web/FloraWeb/Links.hs index cc8e4803..1ab6abba 100644 --- a/src/web/FloraWeb/Links.hs +++ b/src/web/FloraWeb/Links.hs @@ -117,6 +117,17 @@ packageWithExecutable pageNumber search = /: Just search /: Just pageNumber +searchInAdvisories + :: Positive Word + -> Text + -> Link +searchInAdvisories pageNumber search = + links + // Web.search + // Search.displaySearch + /: Just search + /: Just pageNumber + packageSecurity :: Namespace -> PackageName -> Link packageSecurity namespace packageName = links diff --git a/src/web/FloraWeb/Pages/Server/Search.hs b/src/web/FloraWeb/Pages/Server/Search.hs index 4ae4236a..c52b2e89 100644 --- a/src/web/FloraWeb/Pages/Server/Search.hs +++ b/src/web/FloraWeb/Pages/Server/Search.hs @@ -68,3 +68,7 @@ searchHandler (Headers session _) (Just searchString) pageParam = do (count, results) <- Search.searchExecutable pagination executableName render templateEnv $ Search.showExecutableResults searchString count pageNumber results + Just (SearchInAdvisories searchTerm) -> do + (count, results) <- Search.searchInAdvisories pagination searchTerm + render templateEnv $ + Search.showAdvisorySearchResults searchString count pageNumber results diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index 178cb83d..0f84c140 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -24,6 +24,7 @@ module FloraWeb.Pages.Templates.Packages , showDependencies , showDependents , showPackageSecurityPage + , advisoriesListing ) where import Control.Monad (when) @@ -566,13 +567,17 @@ showPackageSecurityPage showPackageSecurityPage namespace packageName advisoryPreviews = do div_ [class_ "container"] $ do presentationHeaderForAdvisories namespace packageName - if Vector.null advisoryPreviews - then p_ [] "No advisories found for this package." - else div_ [class_ "advisory-list"] $ do - div_ [class_ "advisory-list__head"] $ do - div_ [class_ "advisory-list__header"] "ID" - div_ [class_ "advisory-list__header"] "Summary" - div_ [class_ "advisory-list__header"] "Published" - div_ [class_ "advisory-list__header"] "Attributes" - div_ [class_ "advisory-list__body"] $ - Vector.forM_ advisoryPreviews (\preview -> advisoryListRow preview) + advisoriesListing advisoryPreviews + +advisoriesListing :: Vector PackageAdvisoryPreview -> FloraHTML +advisoriesListing advisoryPreviews = + if Vector.null advisoryPreviews + then p_ [] "No advisories found for this package." + else div_ [class_ "advisory-list"] $ do + div_ [class_ "advisory-list__head"] $ do + div_ [class_ "advisory-list__header"] "ID" + div_ [class_ "advisory-list__header"] "Summary" + div_ [class_ "advisory-list__header"] "Published" + div_ [class_ "advisory-list__header"] "Attributes" + div_ [class_ "advisory-list__body"] $ + Vector.forM_ advisoryPreviews (\preview -> advisoryListRow preview) diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Search.hs b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs index b4d7e595..04095bdc 100644 --- a/src/web/FloraWeb/Pages/Templates/Screens/Search.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs @@ -7,12 +7,13 @@ import Data.Text.Display (display) import Data.Vector (Vector) import Lucid +import Advisories.Model.Affected.Types import Flora.Model.Package (Namespace, PackageInfo (..), PackageInfoWithExecutables (..)) import Flora.Search (SearchAction (..)) import FloraWeb.Components.PackageListHeader (presentationHeader) import FloraWeb.Components.PaginationNav (paginationNav) import FloraWeb.Pages.Templates -import FloraWeb.Pages.Templates.Packages (packageListing, packageWithExecutableListing) +import FloraWeb.Pages.Templates.Packages (advisoriesListing, packageListing, packageWithExecutableListing) showAllPackages :: Word -> Positive Word -> Vector PackageInfo -> FloraHTML showAllPackages count currentPage packagesInfo = do @@ -63,3 +64,17 @@ showExecutableResults executableName count currentPage results = do packageWithExecutableListing results when (count > 30) $ paginationNav count currentPage (SearchExecutable executableName) + +showAdvisorySearchResults + :: Text + -> Word + -> Positive Word + -> Vector PackageAdvisoryPreview + -- ^ Results + -> FloraHTML +showAdvisorySearchResults searchTerm count currentPage results = do + div_ [class_ "container"] $ do + presentationHeader searchTerm "" count + advisoriesListing results + when (count > 30) $ + paginationNav count currentPage (SearchInAdvisories searchTerm) From 279340ad463a588621f79604a8bf6c690c918eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri=20de=20Tarl=C3=A9?= Date: Fri, 27 Dec 2024 12:23:50 +0100 Subject: [PATCH 2/2] Add changelog entry --- changelog.d/805 | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/805 diff --git a/changelog.d/805 b/changelog.d/805 new file mode 100644 index 00000000..5ec4cd51 --- /dev/null +++ b/changelog.d/805 @@ -0,0 +1,2 @@ +synopsis: Search in security advisories with the `hsec:` qualifier +prs: #805