Added known_vulnerability to vulnerabilities endpoint. (#21136)

#19857 
For `GET /api/v1/fleet/vulnerabilities` endpoint, added
`known_vulnerability` field to the response. This field is present when
query is a valid CVE format and returns no results. It indicates whether
the vulnerability is in Fleet's DB.

# Checklist for submitter
- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Victor Lyuboslavsky 2024-08-08 21:37:25 +02:00 committed by GitHub
parent 1b4e4f44c5
commit b67017398b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 105 additions and 9 deletions

View file

@ -0,0 +1 @@
For GET /api/v1/fleet/vulnerabilities endpoint, added `known_vulnerability` field to the response. This field is present when query is a valid CVE format and returns no results. It indicates whether the vulnerability is in Fleet's DB.

View file

@ -3,6 +3,7 @@ package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
@ -496,3 +497,12 @@ func (ds *Datastore) batchInsertHostCounts(ctx context.Context, counts []hostCou
return nil
}
func (ds *Datastore) IsCVEKnownToFleet(ctx context.Context, cve string) (bool, error) {
var count uint
err := sqlx.GetContext(ctx, ds.reader(ctx), &count, "SELECT 1 FROM cve_meta WHERE cve = ?", cve)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false, err
}
return count > 0, nil
}

View file

@ -989,6 +989,8 @@ type Datastore interface {
CountVulnerabilities(ctx context.Context, opt VulnListOptions) (uint, error)
// UpdateVulnerabilityHostCounts updates hosts counts for all vulnerabilities.
UpdateVulnerabilityHostCounts(ctx context.Context) error
// IsCVEKnownToFleet checks if the provided CVE is known to Fleet.
IsCVEKnownToFleet(ctx context.Context, cve string) (bool, error)
///////////////////////////////////////////////////////////////////////////////
// Apple MDM

View file

@ -668,6 +668,8 @@ type Service interface {
ListOSVersionsByCVE(ctx context.Context, cve string, teamID *uint) (result []*VulnerableOS, updatedAt time.Time, err error)
// ListSoftwareByCVE returns a list of software affected by the provided CVE.
ListSoftwareByCVE(ctx context.Context, cve string, teamID *uint) (result []*VulnerableSoftware, updatedAt time.Time, err error)
// IsCVEKnownToFleet returns whether the provided CVE is known to Fleet.
IsCVEKnownToFleet(ctx context.Context, cve string) (bool, error)
// /////////////////////////////////////////////////////////////////////////////
// Team Policies

View file

@ -684,6 +684,8 @@ type CountVulnerabilitiesFunc func(ctx context.Context, opt fleet.VulnListOption
type UpdateVulnerabilityHostCountsFunc func(ctx context.Context) error
type IsCVEKnownToFleetFunc func(ctx context.Context, cve string) (bool, error)
type NewMDMAppleConfigProfileFunc func(ctx context.Context, p fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error)
type BulkUpsertMDMAppleConfigProfilesFunc func(ctx context.Context, payload []*fleet.MDMAppleConfigProfile) error
@ -2007,6 +2009,9 @@ type DataStore struct {
UpdateVulnerabilityHostCountsFunc UpdateVulnerabilityHostCountsFunc
UpdateVulnerabilityHostCountsFuncInvoked bool
IsCVEKnownToFleetFunc IsCVEKnownToFleetFunc
IsCVEKnownToFleetFuncInvoked bool
NewMDMAppleConfigProfileFunc NewMDMAppleConfigProfileFunc
NewMDMAppleConfigProfileFuncInvoked bool
@ -4823,6 +4828,13 @@ func (s *DataStore) UpdateVulnerabilityHostCounts(ctx context.Context) error {
return s.UpdateVulnerabilityHostCountsFunc(ctx)
}
func (s *DataStore) IsCVEKnownToFleet(ctx context.Context, cve string) (bool, error) {
s.mu.Lock()
s.IsCVEKnownToFleetFuncInvoked = true
s.mu.Unlock()
return s.IsCVEKnownToFleetFunc(ctx, cve)
}
func (s *DataStore) NewMDMAppleConfigProfile(ctx context.Context, p fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
s.mu.Lock()
s.NewMDMAppleConfigProfileFuncInvoked = true

View file

@ -8662,6 +8662,8 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
require.NoError(t, err)
// insert CVEMeta
knownCVEWoPrefix := "2021-1299"
knownCVE := "cve-" + knownCVEWoPrefix
mockTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
err = s.ds.InsertCVEMeta(context.Background(), []fleet.CVEMeta{
{
@ -8688,6 +8690,14 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
Published: ptr.Time(mockTime),
Description: "Test CVE 2021-1246",
},
{
CVE: knownCVE,
CVSSScore: ptr.Float64(6.4),
EPSSProbability: ptr.Float64(0.61),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(mockTime),
Description: fmt.Sprintf("Test %s", knownCVE),
},
})
require.NoError(t, err)
@ -8701,6 +8711,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
require.Equal(t, resp.Count, uint(3))
require.False(t, resp.Meta.HasPreviousResults)
require.False(t, resp.Meta.HasNextResults)
assert.Nil(t, resp.KnownVulnerability)
expected := map[string]struct {
fleet.CVEMeta
@ -8738,6 +8749,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
require.Equal(t, resp.Count, uint(2))
require.False(t, resp.Meta.HasPreviousResults)
require.False(t, resp.Meta.HasNextResults)
assert.Nil(t, resp.KnownVulnerability)
expected = map[string]struct {
fleet.CVEMeta
@ -8771,6 +8783,34 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
require.Equal(t, resp.Count, uint(0))
require.False(t, resp.Meta.HasPreviousResults)
require.False(t, resp.Meta.HasNextResults)
assert.Nil(t, resp.KnownVulnerability)
// test with a known CVE that does not match on software/OS
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", knownCVE)
require.Empty(t, resp.Err)
assert.Len(s.T(), resp.Vulnerabilities, 0)
assert.Equal(t, resp.Count, uint(0))
assert.False(t, resp.Meta.HasPreviousResults)
assert.False(t, resp.Meta.HasNextResults)
assert.Equal(t, ptr.Bool(true), resp.KnownVulnerability)
// test with a known CVE that does not match on software/OS, but without CVE- prefix
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "query", knownCVEWoPrefix)
require.Empty(t, resp.Err)
assert.Len(s.T(), resp.Vulnerabilities, 0)
assert.Equal(t, resp.Count, uint(0))
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)
assert.Len(s.T(), resp.Vulnerabilities, 0)
assert.Equal(t, resp.Count, uint(0))
assert.False(t, resp.Meta.HasPreviousResults)
assert.False(t, resp.Meta.HasNextResults)
assert.Equal(t, ptr.Bool(false), resp.KnownVulnerability)
// Team 1 Filter
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", "1")

View file

@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"regexp"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
@ -22,13 +23,17 @@ type listVulnerabilitiesRequest struct {
}
type listVulnerabilitiesResponse struct {
Vulnerabilities []fleet.VulnerabilityWithMetadata `json:"vulnerabilities"`
Count uint `json:"count"`
CountsUpdatedAt time.Time `json:"counts_updated_at"`
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
Err error `json:"error,omitempty"`
Vulnerabilities []fleet.VulnerabilityWithMetadata `json:"vulnerabilities"`
Count uint `json:"count"`
CountsUpdatedAt time.Time `json:"counts_updated_at"`
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
Err error `json:"error,omitempty"`
KnownVulnerability *bool `json:"known_vulnerability,omitempty"`
}
// Allow formats like: CVE-2017-12345, cve-2017-12345 or 2017-12345
var cveRegex = regexp.MustCompile(`(?i)^(CVE-)?\d{4}-\d{4}\d*$`)
func (r listVulnerabilitiesResponse) error() error { return r.Err }
func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (errorer, error) {
@ -50,11 +55,31 @@ func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc 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
query := request.ListOptions.MatchQuery
matches := cveRegex.FindStringSubmatch(query)
if matches != nil {
const cvePrefix = "CVE-"
if len(matches) > 1 && matches[1] == "" {
// 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
}
knownVulnerability = &known
}
}
return listVulnerabilitiesResponse{
Vulnerabilities: vulns,
Meta: meta,
Count: count,
CountsUpdatedAt: updatedAt,
Vulnerabilities: vulns,
Meta: meta,
Count: count,
CountsUpdatedAt: updatedAt,
KnownVulnerability: knownVulnerability,
}, nil
}
@ -99,6 +124,10 @@ func (svc *Service) CountVulnerabilities(ctx context.Context, opts fleet.VulnLis
return svc.ds.CountVulnerabilities(ctx, opts)
}
func (svc *Service) IsCVEKnownToFleet(ctx context.Context, cve string) (bool, error) {
return svc.ds.IsCVEKnownToFleet(ctx, cve)
}
type getVulnerabilityRequest struct {
CVE string `url:"cve"`
TeamID *uint `query:"team_id,optional"`