Skip to content

Commit

Permalink
Added known_vulnerability to vulnerabilities endpoint. (#21136)
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
getvictor authored Aug 8, 2024
1 parent 1b4e4f4 commit b670173
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 9 deletions.
1 change: 1 addition & 0 deletions changes/19857-known_vulnerability
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions server/datastore/mysql/vulnerabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
Expand Down Expand Up @@ -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
}
2 changes: 2 additions & 0 deletions server/fleet/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions server/fleet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions server/mock/datastore_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2007,6 +2009,9 @@ type DataStore struct {
UpdateVulnerabilityHostCountsFunc UpdateVulnerabilityHostCountsFunc
UpdateVulnerabilityHostCountsFuncInvoked bool

IsCVEKnownToFleetFunc IsCVEKnownToFleetFunc
IsCVEKnownToFleetFuncInvoked bool

NewMDMAppleConfigProfileFunc NewMDMAppleConfigProfileFunc
NewMDMAppleConfigProfileFuncInvoked bool

Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions server/service/integration_core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
47 changes: 38 additions & 9 deletions server/service/vulnerabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"regexp"
"time"

"github.com/fleetdm/fleet/v4/server/authz"
Expand All @@ -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) {
Expand All @@ -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
}

Expand Down Expand Up @@ -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"`
Expand Down

0 comments on commit b670173

Please sign in to comment.