Updated /api/v1/fleet/vulnerabilities/{cve} endpoint (#21463)
main task: #19857 subtask: #21392 - For GET /api/v1/fleet/vulnerabilities/{cve} endpoint, added validation of CVE format, and added a 204 response. The 204 response indicates that the vulnerability is known to Fleet but not present on any hosts. - Removed the previous known_vulnerability field implementation # 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:
@@ -1,2 +1,2 @@
|
||||
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.
|
||||
For GET /api/v1/fleet/vulnerabilities/{cve} endpoint, added validation of CVE format, and added a 204 response. The 204 response indicates that the vulnerability is known to Fleet but not present on any hosts.
|
||||
For the UI, add new empty states for searching vulnerabilities: invalid CVE format searched, a known CVE serached but not present on hosts, not a known CVE searched, exploited vulnerability empty state, operating systems empty state, new icons
|
||||
|
||||
@@ -21,6 +21,7 @@ func (svc *Service) ListVulnerabilities(ctx context.Context, opt fleet.VulnListO
|
||||
return svc.Service.ListVulnerabilities(ctx, opt)
|
||||
}
|
||||
|
||||
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (*fleet.VulnerabilityWithMetadata, error) {
|
||||
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (vuln *fleet.VulnerabilityWithMetadata,
|
||||
known bool, err error) {
|
||||
return svc.Service.Vulnerability(ctx, cve, teamID, true)
|
||||
}
|
||||
|
||||
@@ -661,15 +661,13 @@ type Service interface {
|
||||
// ListVulnerabilities returns a list of vulnerabilities based on the provided options.
|
||||
ListVulnerabilities(ctx context.Context, opt VulnListOptions) ([]VulnerabilityWithMetadata, *PaginationMetadata, error)
|
||||
// ListVulnerability returns a vulnerability based on the provided CVE.
|
||||
Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (*VulnerabilityWithMetadata, error)
|
||||
Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (vuln *VulnerabilityWithMetadata, known bool, err error)
|
||||
// CountVulnerabilities returns the number of vulnerabilities based on the provided options.
|
||||
CountVulnerabilities(ctx context.Context, opt VulnListOptions) (uint, error)
|
||||
// ListOSVersionsByCVE returns a list of OS versions affected by the provided CVE.
|
||||
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
|
||||
|
||||
@@ -8718,8 +8718,7 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
|
||||
require.NoError(t, err)
|
||||
|
||||
// insert CVEMeta
|
||||
knownCVEWoPrefix := "2021-12999"
|
||||
knownCVE := "cve-" + knownCVEWoPrefix
|
||||
knownCVE := "cve-2021-12999"
|
||||
mockTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
err = s.ds.InsertCVEMeta(context.Background(), []fleet.CVEMeta{
|
||||
{
|
||||
@@ -8767,7 +8766,6 @@ 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
|
||||
@@ -8805,7 +8803,6 @@ 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
|
||||
@@ -8839,7 +8836,6 @@ 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)
|
||||
@@ -8848,43 +8844,15 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
|
||||
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 substring of a known CVE -- results are returned but the exact match is not known to Fleet
|
||||
// test with a substring of a known CVE -- results are returned
|
||||
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)
|
||||
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)
|
||||
_ = s.Do("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusNotFound)
|
||||
|
||||
// Team 1 Filter
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", "1")
|
||||
@@ -8933,19 +8901,20 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
|
||||
|
||||
var gResp getVulnerabilityResponse
|
||||
// invalid cve
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/foobar", nil, http.StatusNotFound, &gResp)
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/foobar", nil, http.StatusBadRequest, &gResp)
|
||||
|
||||
// Valid CVE but not in team scope
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1246", nil, http.StatusNotFound, &gResp, "team_id", fmt.Sprintf("%d", team.ID))
|
||||
s.Do("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1246", nil, http.StatusNoContent, "team_id",
|
||||
fmt.Sprintf("%d", team.ID))
|
||||
|
||||
// Valid CVE in "no team" scope
|
||||
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-12345", nil, http.StatusNotFound, &gResp, "team_id", "0")
|
||||
// Valid CVE not in "no team" scope
|
||||
s.Do("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-12345", nil, http.StatusNoContent, "team_id", "0")
|
||||
|
||||
// Invalid TeamID
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-12345", nil, http.StatusForbidden, &gResp, "team_id", "100")
|
||||
s.Do("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-12345", nil, http.StatusForbidden, "team_id", "100")
|
||||
|
||||
// Valid Global Request
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-12345", nil, http.StatusOK, &gResp)
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
@@ -18,21 +19,32 @@ var freeValidVulnSortColumns = []string{
|
||||
"created_at",
|
||||
}
|
||||
|
||||
type cveNotFoundError struct{}
|
||||
|
||||
var _ fleet.NotFoundError = (*cveNotFoundError)(nil)
|
||||
|
||||
func (p cveNotFoundError) Error() string {
|
||||
return "This is not known CVE. None of Fleet’s vulnerability sources are aware of this CVE."
|
||||
}
|
||||
|
||||
func (p cveNotFoundError) IsNotFound() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type listVulnerabilitiesRequest struct {
|
||||
fleet.VulnListOptions
|
||||
}
|
||||
|
||||
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"`
|
||||
KnownVulnerability *bool `json:"known_vulnerability,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"`
|
||||
}
|
||||
|
||||
// Allow formats like: CVE-2017-12345, cve-2017-12345 or 2017-12345
|
||||
var cveRegex = regexp.MustCompile(`(?i)^(CVE-)?\d{4}-\d{4}\d*$`)
|
||||
// Allow formats like: CVE-2017-12345, cve-2017-12345
|
||||
var cveRegex = regexp.MustCompile(`(?i)^CVE-\d{4}-\d{4}\d*$`)
|
||||
|
||||
func (r listVulnerabilitiesResponse) error() error { return r.Err }
|
||||
|
||||
@@ -55,42 +67,11 @@ 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(request.ListOptions.MatchQuery) > 0 {
|
||||
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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
return listVulnerabilitiesResponse{
|
||||
Vulnerabilities: vulns,
|
||||
Meta: meta,
|
||||
Count: count,
|
||||
CountsUpdatedAt: updatedAt,
|
||||
KnownVulnerability: knownVulnerability,
|
||||
Vulnerabilities: vulns,
|
||||
Meta: meta,
|
||||
Count: count,
|
||||
CountsUpdatedAt: updatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -149,17 +130,29 @@ type getVulnerabilityResponse struct {
|
||||
OSVersions []*fleet.VulnerableOS `json:"os_versions"`
|
||||
Software []*fleet.VulnerableSoftware `json:"software"`
|
||||
Err error `json:"error,omitempty"`
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (r getVulnerabilityResponse) error() error { return r.Err }
|
||||
|
||||
func (r getVulnerabilityResponse) Status() int {
|
||||
if r.statusCode == 0 {
|
||||
return http.StatusOK
|
||||
}
|
||||
return r.statusCode
|
||||
}
|
||||
|
||||
func getVulnerabilityEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (errorer, error) {
|
||||
request := req.(*getVulnerabilityRequest)
|
||||
|
||||
vuln, err := svc.Vulnerability(ctx, request.CVE, request.TeamID, false)
|
||||
vuln, known, err := svc.Vulnerability(ctx, request.CVE, request.TeamID, false)
|
||||
if err != nil {
|
||||
return getVulnerabilityResponse{Err: err}, nil
|
||||
}
|
||||
if vuln == nil && known {
|
||||
// Return 204 status code if the vulnerability is known to Fleet but does not match any host software/OS
|
||||
return getVulnerabilityResponse{statusCode: http.StatusNoContent}, nil
|
||||
}
|
||||
|
||||
vuln.DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE.CVE)
|
||||
|
||||
@@ -180,30 +173,47 @@ func getVulnerabilityEndpoint(ctx context.Context, req interface{}, svc fleet.Se
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (*fleet.VulnerabilityWithMetadata, error) {
|
||||
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (vuln *fleet.VulnerabilityWithMetadata,
|
||||
known bool, err error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if !cveRegex.Match([]byte(cve)) {
|
||||
return nil, false, badRequest("That vulnerability (CVE) is not valid. Try updating your search to use CVE format: \"CVE-YYYY-<4 or more digits>\"")
|
||||
}
|
||||
|
||||
if teamID != nil && *teamID != 0 {
|
||||
exists, err := svc.ds.TeamExists(ctx, *teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "checking if team exists")
|
||||
return nil, false, ctxerr.Wrap(ctx, err, "checking if team exists")
|
||||
} else if !exists {
|
||||
return nil, authz.ForbiddenWithInternal("team does not exist", nil, nil, nil)
|
||||
return nil, false, authz.ForbiddenWithInternal("team does not exist", nil, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
vuln, err := svc.ds.Vulnerability(ctx, cve, teamID, useCVSScores)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
vuln, err = svc.ds.Vulnerability(ctx, cve, teamID, useCVSScores)
|
||||
switch {
|
||||
case fleet.IsNotFound(err):
|
||||
var errKnown error
|
||||
known, errKnown = svc.ds.IsCVEKnownToFleet(ctx, cve)
|
||||
if errKnown != nil {
|
||||
return nil, false, errKnown
|
||||
}
|
||||
if !known {
|
||||
return nil, false, cveNotFoundError{}
|
||||
}
|
||||
case err != nil:
|
||||
return nil, false, err
|
||||
default:
|
||||
known = true
|
||||
}
|
||||
|
||||
return vuln, nil
|
||||
return vuln, known, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ListOSVersionsByCVE(ctx context.Context, cve string, teamID *uint) (result []*fleet.VulnerableOS, updatedAt time.Time, err error) {
|
||||
|
||||
@@ -173,10 +173,10 @@ func TestVulnerabilitesAuth(t *testing.T) {
|
||||
})
|
||||
checkAuthErr(t, tc.shouldFailTeamRead, err)
|
||||
|
||||
_, err = svc.Vulnerability(ctx, "CVE-2019-1234", nil, false)
|
||||
_, _, err = svc.Vulnerability(ctx, "CVE-2019-1234", nil, false)
|
||||
checkAuthErr(t, tc.shouldFailGlobalRead, err)
|
||||
|
||||
_, err = svc.Vulnerability(ctx, "CVE-2019-1234", ptr.Uint(1), false)
|
||||
_, _, err = svc.Vulnerability(ctx, "CVE-2019-1234", ptr.Uint(1), false)
|
||||
checkAuthErr(t, tc.shouldFailTeamRead, err)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user