From 27b8d1aa4b55dd5cefbe84ee6374d210c44a8661 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky <2685025+getvictor@users.noreply.github.com> Date: Sat, 25 Oct 2025 16:02:02 -0500 Subject: [PATCH] Fixed issue searching software versions (#34770) **Related issue:** Resolves #34713 Fix for unreleased but. Needs to be cherry picked into 4.76.0 # Checklist for submitter ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results --- server/datastore/mysql/software.go | 16 +++++++----- server/datastore/mysql/software_test.go | 34 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index 9d40cf58ef..281e91ba3b 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -1407,13 +1407,15 @@ func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, e goqu.I("software_cve").As("scv"), goqu.On(goqu.I("s.id").Eq(goqu.I("scv.software_id"))), ) - } else if !opts.WithoutVulnerabilityDetails || opts.IncludeCVEScores { - // Only LEFT JOIN software_cve if we need CVE details in the list OR if we need CVE scores. - // When WithoutVulnerabilityDetails=true AND IncludeCVEScores=false, we can skip this join - // entirely in the subquery. The outer query will fetch CVEs only for the paginated results, - // which is much more efficient. - // However, if IncludeCVEScores=true, we MUST include the join because we need to select - // scv.resolved_in_version and other scv columns for ordering/filtering. + } else if !opts.WithoutVulnerabilityDetails || opts.IncludeCVEScores || opts.ListOptions.MatchQuery != "" { + // LEFT JOIN software_cve if: + // 1. We need CVE details in the list (!WithoutVulnerabilityDetails), OR + // 2. We need CVE scores for ordering/filtering (IncludeCVEScores), OR + // 3. We have a search query (MatchQuery) that might be searching for CVEs + // + // When WithoutVulnerabilityDetails=true AND IncludeCVEScores=false AND MatchQuery is empty, + // we can skip this join entirely in the subquery. The outer query will fetch CVEs only for + // the paginated results, which is much more efficient. ds = ds. LeftJoin( goqu.I("software_cve").As("scv"), diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index b2838720b1..ab30e4dcec 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -786,6 +786,40 @@ func testSoftwareList(t *testing.T, ds *Datastore) { listSoftwareCheckCount(t, ds, 0, 0, opts, true) }) + t.Run("filters by CVE with WithoutVulnerabilityDetails", func(t *testing.T) { + // Regression test for https://github.com/fleetdm/fleet/issues/34713 + // When WithoutVulnerabilityDetails=true and IncludeCVEScores=false, + // the software_cve table is not joined in the subquery, but the WHERE clause + // still tries to reference scv.cve, causing "Unknown column 'scv.cve'" error. + opts := fleet.SoftwareListOptions{ + ListOptions: fleet.ListOptions{ + MatchQuery: "CVE-2022-0001", + }, + WithoutVulnerabilityDetails: true, + IncludeCVEScores: false, + } + software := listSoftwareCheckCount(t, ds, 1, 1, opts, true) + expectedFoo001 := fleet.Software{ + Name: "foo", + Version: "0.0.1", + Source: "chrome_extensions", + } + require.Len(t, software, 1) + require.Equal(t, expectedFoo001.Name, software[0].Name) + require.Equal(t, expectedFoo001.Version, software[0].Version) + require.Equal(t, expectedFoo001.Source, software[0].Source) + + // Test with partial CVE + opts.ListOptions.MatchQuery = "0002" + software = listSoftwareCheckCount(t, ds, 1, 1, opts, true) + require.Len(t, software, 1) + require.Equal(t, expectedFoo001.Name, software[0].Name) + + // Test with unknown CVE + opts.ListOptions.MatchQuery = "CVE-2022-0000" + listSoftwareCheckCount(t, ds, 0, 0, opts, true) + }) + t.Run("filters by query", func(t *testing.T) { // query by name (case insensitive) opts := fleet.SoftwareListOptions{