Skip to content

Commit

Permalink
[FLORA-232] Advisory Search (#805)
Browse files Browse the repository at this point in the history
* [FLORA-232] Advisory Search

* Add changelog entry
  • Loading branch information
tchoutri authored Dec 27, 2024
1 parent c153e51 commit 864e194
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 33 deletions.
33 changes: 17 additions & 16 deletions assets/css/3-screens/1-package/5-security.css
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions changelog.d/805
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
synopsis: Search in security advisories with the `hsec:` qualifier
prs: #805
3 changes: 2 additions & 1 deletion docs/docs/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>/<packagename>`: Shows the dependents page for a package
* `in:<@namespace> <packagename>`: Searches for a package name in the specified namespace
* `in:<@namespace>`: Lists packages in a namespace
* `exe:<executable name>`: Search for packages that have an executable component with `<executable name>` as the search string
* `hsec:<search term>`: 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.

Expand Down
27 changes: 26 additions & 1 deletion flora.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ library
Flora.Model.User.Query
Flora.Model.User.Update
Flora.QRCode
Flora.Search
Flora.Tracing
JSON
Log.Backend.File
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -369,6 +391,7 @@ library flora-web
, flora
, flora-advisories
, flora-jobs
, flora-search
, haddock-library
, htmx-lucid
, http-api-data
Expand Down Expand Up @@ -517,6 +540,7 @@ executable flora-cli
, filepath
, flora
, flora-advisories
, flora-search
, flora-web
, hsec-core
, log-base
Expand Down Expand Up @@ -562,6 +586,7 @@ test-suite flora-test
, filepath
, flora
, flora-advisories
, flora-search
, flora-web
, hedgehog
, http-client
Expand Down
1 change: 1 addition & 0 deletions ghc-tags.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exclude_paths:
- dist
- dist-newstyle
- assets
- _build
extensions:
- BangPatterns
- BlockArguments
Expand Down
84 changes: 81 additions & 3 deletions src/advisories/Advisories/Model/Affected/Query.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
|]
2 changes: 1 addition & 1 deletion src/core/Flora/Model/Release/Query.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions src/core/Flora/Search.hs → src/search/Flora/Search.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (..)
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/web/FloraWeb/Components/PaginationNav.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/web/FloraWeb/Links.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/web/FloraWeb/Pages/Server/Search.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 15 additions & 10 deletions src/web/FloraWeb/Pages/Templates/Packages.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module FloraWeb.Pages.Templates.Packages
, showDependencies
, showDependents
, showPackageSecurityPage
, advisoriesListing
) where

import Control.Monad (when)
Expand Down Expand Up @@ -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)
Loading

0 comments on commit 864e194

Please sign in to comment.