From 84ee756b1bf6ac3cb69fb7401535929b87aaeb4d Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 16 Aug 2024 14:09:52 +0200 Subject: [PATCH] Include known_vulnerability field when query is a CVE. (#21363) Update to #19857 after customer feedback. If the search query is in CVE format (CVE-YYYY-<4+digits>), we always return if that exact match is a CVE known to Fleet. # Checklist for submitter - [x] Added/updated tests - [x] Manual QA for all new/changed functionality --- server/service/integration_core_test.go | 46 +++++++++++++++++-------- server/service/vulnerabilities.go | 21 ++++++++--- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 7c7e70d1fc..851d0c3970 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -8661,7 +8661,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() { _, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{ OSID: os.ID, - CVE: "CVE-2021-1234", + CVE: "CVE-2021-12345", ResolvedInVersion: *ptr.StringPtr("10.0.19043.2013"), }, fleet.MSRCSource) require.NoError(t, err) @@ -8718,17 +8718,17 @@ func (s *integrationTestSuite) TestListVulnerabilities() { require.NoError(t, err) // insert CVEMeta - knownCVEWoPrefix := "2021-1299" + knownCVEWoPrefix := "2021-12999" knownCVE := "cve-" + knownCVEWoPrefix mockTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) err = s.ds.InsertCVEMeta(context.Background(), []fleet.CVEMeta{ { - CVE: "CVE-2021-1234", + CVE: "CVE-2021-12345", CVSSScore: ptr.Float64(7.5), EPSSProbability: ptr.Float64(0.5), CISAKnownExploit: ptr.Bool(true), Published: ptr.Time(mockTime), - Description: "Test CVE 2021-1234", + Description: "Test CVE 2021-12345", }, { CVE: "CVE-2021-1235", @@ -8775,9 +8775,9 @@ func (s *integrationTestSuite) TestListVulnerabilities() { DetailsLink string Source fleet.VulnerabilitySource }{ - "CVE-2021-1234": { + "CVE-2021-12345": { HostCount: 1, - DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1234", + DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-12345", }, "CVE-2021-1235": { HostCount: 1, @@ -8791,7 +8791,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() { for _, vuln := range resp.Vulnerabilities { expectedVuln, ok := expected[vuln.CVE.CVE] - require.True(t, ok) + require.True(t, ok, vuln.CVE.CVE) require.Equal(t, expectedVuln.HostCount, vuln.HostsCount) require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink) require.Empty(t, vuln.CVSSScore) @@ -8813,9 +8813,9 @@ func (s *integrationTestSuite) TestListVulnerabilities() { DetailsLink string Source fleet.VulnerabilitySource }{ - "CVE-2021-1234": { + "CVE-2021-12345": { HostCount: 1, - DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1234", + DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-12345", }, "CVE-2021-1235": { HostCount: 1, @@ -8859,6 +8859,24 @@ func (s *integrationTestSuite) TestListVulnerabilities() { assert.False(t, resp.Meta.HasNextResults) assert.Equal(t, ptr.Bool(true), resp.KnownVulnerability) + // test with a substring of a known CVE -- results are returned but the exact match is not known to Fleet + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", "CVE-2021-1234") + require.Empty(t, resp.Err) + assert.Len(s.T(), resp.Vulnerabilities, 1) + assert.Equal(t, resp.Count, uint(1)) + assert.False(t, resp.Meta.HasPreviousResults) + assert.False(t, resp.Meta.HasNextResults) + assert.Equal(t, ptr.Bool(false), resp.KnownVulnerability) + + // test with exact match of a known CVE -- results are returned and CVE is known to Fleet + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", "2021-12345") + require.Empty(t, resp.Err) + assert.Len(s.T(), resp.Vulnerabilities, 1) + assert.Equal(t, resp.Count, uint(1)) + assert.False(t, resp.Meta.HasPreviousResults) + assert.False(t, resp.Meta.HasNextResults) + assert.Equal(t, ptr.Bool(true), resp.KnownVulnerability) + // test with a unknown CVE that does not match on software/OS s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", knownCVE+"1") require.Empty(t, resp.Err) @@ -8924,17 +8942,17 @@ func (s *integrationTestSuite) TestListVulnerabilities() { s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1246", nil, http.StatusOK, &gResp, "team_id", "0") // Valid CVD not in "no team" scope - s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusNotFound, &gResp, "team_id", "0") + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-12345", nil, http.StatusNotFound, &gResp, "team_id", "0") // Invalid TeamID - s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusForbidden, &gResp, "team_id", "100") + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-12345", nil, http.StatusForbidden, &gResp, "team_id", "100") // Valid Global Request - s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusOK, &gResp) + s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-12345", nil, http.StatusOK, &gResp) require.Empty(t, gResp.Err) - require.Equal(t, "CVE-2021-1234", gResp.Vulnerability.CVE.CVE) + require.Equal(t, "CVE-2021-12345", gResp.Vulnerability.CVE.CVE) require.Equal(t, uint(1), gResp.Vulnerability.HostsCount) - require.Equal(t, "https://nvd.nist.gov/vuln/detail/CVE-2021-1234", gResp.Vulnerability.DetailsLink) + require.Equal(t, "https://nvd.nist.gov/vuln/detail/CVE-2021-12345", gResp.Vulnerability.DetailsLink) require.Empty(t, gResp.Vulnerability.Description) require.Empty(t, gResp.Vulnerability.CVSSScore) require.Empty(t, gResp.Vulnerability.CISAKnownExploit) diff --git a/server/service/vulnerabilities.go b/server/service/vulnerabilities.go index e4085eb43d..74b55610d1 100644 --- a/server/service/vulnerabilities.go +++ b/server/service/vulnerabilities.go @@ -55,9 +55,9 @@ func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet } } + // Check whether the query was for a vulnerability known to fleet var knownVulnerability *bool - if len(vulns) == 0 && len(request.ListOptions.MatchQuery) > 0 { - // If no vulnerabilities are returned, we need to check if the query was for a vulnerability known to fleet + if len(request.ListOptions.MatchQuery) > 0 { query := request.ListOptions.MatchQuery matches := cveRegex.FindStringSubmatch(query) if matches != nil { @@ -66,9 +66,20 @@ func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet // If CVE prefix was missing, we add it query = cvePrefix + query } - known, err := svc.IsCVEKnownToFleet(ctx, query) - if err != nil { - return listVulnerabilitiesResponse{Err: err}, nil + // As an optimization, we first check if the CVE was one of the ones returned + // by the query. If it was, we already know it's known to Fleet. + var known bool + for _, vuln := range vulns { + if vuln.CVE.CVE == query { + known = true + break + } + } + if !known { + known, err = svc.IsCVEKnownToFleet(ctx, query) + if err != nil { + return listVulnerabilitiesResponse{Err: err}, nil + } } knownVulnerability = &known }