Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FLORA-232] Advisory Search #805

Merged
merged 2 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -42,7 +42,7 @@
}

mkURL :: SearchAction -> Positive Word -> Text
mkURL ListAllPackages pageNumber =

Check warning on line 45 in src/web/FloraWeb/Components/PaginationNav.hs

View workflow job for this annotation

GitHub Actions / Backend_tests (9.6.6, ubuntu-latest)

Pattern match(es) are non-exhaustive
"/" <> toUrlPiece (Links.packageIndexLink pageNumber)
mkURL (ListAllPackagesInNamespace namespace) pageNumber =
Links.namespacePage namespace pageNumber
Expand All @@ -52,6 +52,8 @@
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 @@
, showDependencies
, showDependents
, showPackageSecurityPage
, advisoriesListing
) where

import Control.Monad (when)
Expand Down Expand Up @@ -383,7 +384,7 @@

showAll :: Target -> Maybe Version -> Namespace -> PackageName -> FloraHTML
showAll target mVersion namespace packageName = do
let resource = case target of

Check warning on line 387 in src/web/FloraWeb/Pages/Templates/Packages.hs

View workflow job for this annotation

GitHub Actions / Backend_tests (9.6.6, ubuntu-latest)

Pattern match(es) are non-exhaustive
Dependents -> Links.dependentsPage namespace packageName (PositiveUnsafe 1)
Dependencies -> Links.dependenciesPage namespace packageName (fromJust mVersion)
Versions -> Links.versionsPage namespace packageName
Expand Down Expand Up @@ -566,13 +567,17 @@
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
Loading