mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
2 of 2: List Vulnerabilities API (#16695)
This commit is contained in:
parent
afb4b872b0
commit
f1eeaf42f2
10 changed files with 617 additions and 27 deletions
22
ee/server/service/vulnerabilities.go
Normal file
22
ee/server/service/vulnerabilities.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
var eeValidVulnSortColumns = []string{
|
||||
"cve",
|
||||
"host_count",
|
||||
"created_at",
|
||||
"cvss_score",
|
||||
"epss_probability",
|
||||
"published",
|
||||
}
|
||||
|
||||
func (svc *Service) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
|
||||
opt.ValidSortColumns = eeValidVulnSortColumns
|
||||
opt.IsEE = true
|
||||
return svc.Service.ListVulnerabilities(ctx, opt)
|
||||
}
|
||||
|
|
@ -11,7 +11,8 @@ import (
|
|||
)
|
||||
|
||||
func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
|
||||
selectStmt := `
|
||||
// Define base select statements for EE and Free versions
|
||||
eeSelectStmt := `
|
||||
SELECT
|
||||
vhc.cve,
|
||||
MIN(COALESCE(osv.created_at, sc.created_at, NOW())) AS created_at,
|
||||
|
|
@ -21,7 +22,8 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
|
|||
cm.cisa_known_exploit,
|
||||
cm.published,
|
||||
COALESCE(cm.description, '') AS description,
|
||||
vhc.host_count
|
||||
vhc.host_count,
|
||||
vhc.updated_at as host_count_updated_at
|
||||
FROM
|
||||
vulnerability_host_counts vhc
|
||||
LEFT JOIN cve_meta cm ON cm.cve = vhc.cve
|
||||
|
|
@ -29,8 +31,32 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
|
|||
LEFT JOIN software_cve sc ON sc.cve = vhc.cve
|
||||
WHERE vhc.host_count > 0
|
||||
`
|
||||
groupByAppend := ` GROUP BY
|
||||
freeSelectStmt := `
|
||||
SELECT
|
||||
vhc.cve,
|
||||
MIN(COALESCE(osv.created_at, sc.created_at, NOW())) AS created_at,
|
||||
COALESCE(osv.source, sc.source, 0) AS source,
|
||||
vhc.host_count,
|
||||
vhc.updated_at as host_count_updated_at
|
||||
FROM
|
||||
vulnerability_host_counts vhc
|
||||
LEFT JOIN operating_system_vulnerabilities osv ON osv.cve = vhc.cve
|
||||
LEFT JOIN software_cve sc ON sc.cve = vhc.cve
|
||||
WHERE vhc.host_count > 0
|
||||
`
|
||||
|
||||
// Choose the appropriate select statement based on EE or Free
|
||||
var selectStmt string
|
||||
if opt.IsEE {
|
||||
selectStmt = eeSelectStmt
|
||||
} else {
|
||||
selectStmt = freeSelectStmt
|
||||
}
|
||||
|
||||
// Define group by statements for EE and Free
|
||||
eeGroupBy := ` GROUP BY
|
||||
vhc.cve,
|
||||
source,
|
||||
cm.cvss_score,
|
||||
cm.epss_probability,
|
||||
cm.cisa_known_exploit,
|
||||
|
|
@ -38,15 +64,33 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
|
|||
description,
|
||||
vhc.host_count
|
||||
`
|
||||
freeGroupBy := " GROUP BY vhc.cve, source, vhc.host_count"
|
||||
|
||||
// Choose the appropriate group by statement based on EE or Free
|
||||
var groupBy string
|
||||
if opt.IsEE {
|
||||
groupBy = eeGroupBy
|
||||
} else {
|
||||
groupBy = freeGroupBy
|
||||
}
|
||||
|
||||
// Prepare arguments for the query
|
||||
var args []interface{}
|
||||
if opt.TeamID == 0 {
|
||||
selectStmt = selectStmt + " AND vhc.team_id = 0"
|
||||
selectStmt += " AND vhc.team_id = 0"
|
||||
} else {
|
||||
selectStmt = selectStmt + " AND vhc.team_id = ?"
|
||||
selectStmt += " AND vhc.team_id = ?"
|
||||
args = append(args, opt.TeamID)
|
||||
}
|
||||
|
||||
if opt.KnownExploit {
|
||||
selectStmt += " AND cm.cisa_known_exploit = 1"
|
||||
}
|
||||
|
||||
if match := opt.MatchQuery; match != "" {
|
||||
selectStmt, args = searchLike(selectStmt, args, match, "vhc.cve")
|
||||
}
|
||||
|
||||
if opt.KnownExploit {
|
||||
selectStmt = selectStmt + " AND cm.cisa_known_exploit = 1"
|
||||
}
|
||||
|
|
@ -55,17 +99,19 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
|
|||
selectStmt, args = searchLike(selectStmt, args, match, "vhc.cve")
|
||||
}
|
||||
|
||||
selectStmt = selectStmt + groupByAppend
|
||||
// Append group by statement
|
||||
selectStmt += groupBy
|
||||
|
||||
opt.ListOptions.IncludeMetadata = !(opt.ListOptions.UsesCursorPagination())
|
||||
|
||||
selectStmt, args = appendListOptionsWithCursorToSQL(selectStmt, args, &opt.ListOptions)
|
||||
|
||||
// Execute the query
|
||||
var vulns []fleet.VulnerabilityWithMetadata
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &vulns, selectStmt, args...); err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "list vulnerabilities")
|
||||
}
|
||||
|
||||
// Prepare metadata
|
||||
var metaData *fleet.PaginationMetadata
|
||||
if opt.ListOptions.IncludeMetadata {
|
||||
metaData = &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,35 @@ func testListVulnerabilities(t *testing.T, ds *Datastore) {
|
|||
Source: fleet.NVDSource,
|
||||
},
|
||||
}
|
||||
list, _, err = ds.ListVulnerabilities(context.Background(), opts)
|
||||
list, _, err = ds.ListVulnerabilities(context.Background(), fleet.VulnListOptions{IsEE: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 3)
|
||||
for _, vuln := range list {
|
||||
expectedVuln, ok := expected[vuln.CVE]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expectedVuln.CVEMeta, vuln.CVEMeta)
|
||||
require.Equal(t, expectedVuln.HostCount, vuln.HostCount)
|
||||
}
|
||||
|
||||
// Test Fleet Free
|
||||
expected = map[string]fleet.VulnerabilityWithMetadata{
|
||||
"CVE-2020-1234": {
|
||||
CVEMeta: fleet.CVEMeta{CVE: "CVE-2020-1234"},
|
||||
HostCount: 10,
|
||||
Source: fleet.MSRCSource,
|
||||
},
|
||||
"CVE-2020-1235": {
|
||||
CVEMeta: fleet.CVEMeta{CVE: "CVE-2020-1235"},
|
||||
HostCount: 15,
|
||||
Source: fleet.MSRCSource,
|
||||
},
|
||||
"CVE-2020-1236": {
|
||||
CVEMeta: fleet.CVEMeta{CVE: "CVE-2020-1236"},
|
||||
HostCount: 20,
|
||||
Source: fleet.NVDSource,
|
||||
},
|
||||
}
|
||||
list, _, err = ds.ListVulnerabilities(context.Background(), fleet.VulnListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 3)
|
||||
for _, vuln := range list {
|
||||
|
|
@ -185,6 +213,7 @@ func testListVulnerabilitiesSort(t *testing.T, ds *Datastore) {
|
|||
seedVulnerabilities(t, ds)
|
||||
|
||||
opts := fleet.VulnListOptions{
|
||||
IsEE: true,
|
||||
ListOptions: fleet.ListOptions{
|
||||
Page: 0,
|
||||
PerPage: 5,
|
||||
|
|
@ -219,6 +248,7 @@ func testVulnerabilitiesFilters(t *testing.T, ds *Datastore) {
|
|||
|
||||
// Test KnownExploit filter
|
||||
opts := fleet.VulnListOptions{
|
||||
IsEE: true,
|
||||
KnownExploit: true,
|
||||
}
|
||||
list, _, err := ds.ListVulnerabilities(context.Background(), opts)
|
||||
|
|
|
|||
|
|
@ -605,6 +605,14 @@ type Service interface {
|
|||
ListSoftwareTitles(ctx context.Context, opt SoftwareTitleListOptions) ([]SoftwareTitle, int, *PaginationMetadata, error)
|
||||
SoftwareTitleByID(ctx context.Context, id uint) (*SoftwareTitle, error)
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Vulnerabilities
|
||||
|
||||
// ListVulnerabilities returns a list of vulnerabilities based on the provided options.
|
||||
ListVulnerabilities(ctx context.Context, opt VulnListOptions) ([]VulnerabilityWithMetadata, *PaginationMetadata, error)
|
||||
// CountVulnerabilities returns the number of vulnerabilities based on the provided options.
|
||||
CountVulnerabilities(ctx context.Context, opt VulnListOptions) (uint, error)
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////
|
||||
// Team Policies
|
||||
|
||||
|
|
|
|||
|
|
@ -21,22 +21,22 @@ type CVE struct {
|
|||
}
|
||||
|
||||
type CVEMeta struct {
|
||||
CVE string `db:"cve"`
|
||||
CVE string `db:"cve" json:"cve"`
|
||||
// CVSSScore is the Common Vulnerability Scoring System (CVSS) base score v3. The base score ranges from 0 - 10 and
|
||||
// takes into account several different metrics.
|
||||
// See https://nvd.nist.gov/vuln-metrics/cvss.
|
||||
CVSSScore *float64 `db:"cvss_score"`
|
||||
CVSSScore *float64 `db:"cvss_score" json:"cvss_score,omitempty"`
|
||||
// EPSSProbability is the Exploit Prediction Scoring System (EPSS) score. It is the probability
|
||||
// that a software vulnerability will be exploited in the next 30 days.
|
||||
// See https://www.first.org/epss/.
|
||||
EPSSProbability *float64 `db:"epss_probability"`
|
||||
EPSSProbability *float64 `db:"epss_probability" json:"epss_probability,omitempty"`
|
||||
// CISAKnownExploit is whether the the software vulnerability is a known exploit according to CISA.
|
||||
// See https://www.cisa.gov/known-exploited-vulnerabilities.
|
||||
CISAKnownExploit *bool `db:"cisa_known_exploit"`
|
||||
CISAKnownExploit *bool `db:"cisa_known_exploit" json:"cisa_known_exploit,omitempty"`
|
||||
// Published is when the cve was published according to NIST.score
|
||||
Published *time.Time `db:"published"`
|
||||
Published *time.Time `db:"published" json:"published,omitempty"`
|
||||
// CVE text description
|
||||
Description string `db:"description"`
|
||||
Description string `db:"description" json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// SoftwareCPE represents an entry in the `software_cpe` table.
|
||||
|
|
@ -129,15 +129,29 @@ const (
|
|||
|
||||
type VulnerabilityWithMetadata struct {
|
||||
CVEMeta
|
||||
HostCount uint `db:"host_count"`
|
||||
HostCountUpdatedAt time.Time `db:"host_count_updated_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
Source VulnerabilitySource `db:"source"`
|
||||
HostCount uint `db:"host_count" json:"host_count"`
|
||||
HostCountUpdatedAt time.Time `db:"host_count_updated_at" json:"host_count_updated_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DetailsLink string `json:"details_link"`
|
||||
Source VulnerabilitySource `db:"source" json:"-"`
|
||||
}
|
||||
|
||||
type VulnListOptions struct {
|
||||
ListOptions
|
||||
IsEE bool
|
||||
ValidSortColumns []string
|
||||
TeamID uint
|
||||
KnownExploit bool
|
||||
TeamID uint `query:"team_id,optional"`
|
||||
KnownExploit bool `query:"exploit,optional"`
|
||||
}
|
||||
|
||||
func (opt VulnListOptions) HasValidSortColumn() bool {
|
||||
if opt.OrderKey == "" || len(opt.ValidSortColumns) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, c := range opt.ValidSortColumns {
|
||||
if c == opt.OrderKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -371,6 +371,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.GET("/api/_version_/fleet/software/titles", listSoftwareTitlesEndpoint, listSoftwareTitlesRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/titles/{id:[0-9]+}", getSoftwareTitleEndpoint, getSoftwareTitleRequest{})
|
||||
|
||||
// Vulnerabilities
|
||||
ue.GET("/api/_version_/fleet/vulnerabilities", listVulnerabilitiesEndpoint, listVulnerabilitiesRequest{})
|
||||
|
||||
// Hosts
|
||||
ue.GET("/api/_version_/fleet/host_summary", getHostSummaryEndpoint, getHostSummaryRequest{})
|
||||
ue.GET("/api/_version_/fleet/hosts", listHostsEndpoint, listHostsRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/delete", deleteHostsEndpoint, deleteHostsRequest{})
|
||||
|
|
|
|||
|
|
@ -7367,6 +7367,156 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() {
|
|||
require.Contains(t, errMsg, fleet.ErrMDMNotConfigured.Error())
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestListVulnerabilities() {
|
||||
t := s.T()
|
||||
var resp listVulnerabilitiesResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp)
|
||||
|
||||
// Invalid Order Key
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusBadRequest, &resp, "order_key", "foo", "order_direction", "asc")
|
||||
|
||||
// EE Order Key is an invalid order key
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusBadRequest, &resp, "order_key", "cvss_score", "order_direction", "asc")
|
||||
|
||||
// Exploit is an EE only filter
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusPaymentRequired, &resp, "exploit", "true")
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp)
|
||||
require.Len(s.T(), resp.Vulnerabilities, 0)
|
||||
|
||||
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
|
||||
OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
|
||||
UUID: t.Name() + "2",
|
||||
Hostname: t.Name() + "foo2.local",
|
||||
PrimaryIP: "192.168.1.2",
|
||||
PrimaryMac: "30-65-EC-6F-C4-59",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.UpdateHostOperatingSystem(context.Background(), host.ID, fleet.OperatingSystem{
|
||||
Name: "windows",
|
||||
Version: "10.0.19042.1234",
|
||||
Arch: "64bit",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
allos, err := s.ds.ListOperatingSystems(context.Background())
|
||||
require.NoError(t, err)
|
||||
var os fleet.OperatingSystem
|
||||
for _, o := range allos {
|
||||
if o.ID > os.ID {
|
||||
os = o
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
|
||||
OSID: os.ID,
|
||||
CVE: "CVE-2021-1234",
|
||||
}, fleet.MSRCSource)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := s.ds.UpdateHostSoftware(context.Background(), host.ID, []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sw := res.Inserted[0]
|
||||
|
||||
_, err = s.ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{
|
||||
SoftwareID: sw.ID,
|
||||
CVE: "CVE-2021-1235",
|
||||
}, fleet.NVDSource)
|
||||
require.NoError(t, err)
|
||||
|
||||
// insert CVEMeta
|
||||
mockTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
err = s.ds.InsertCVEMeta(context.Background(), []fleet.CVEMeta{
|
||||
{
|
||||
CVE: "CVE-2021-1234",
|
||||
CVSSScore: ptr.Float64(7.5),
|
||||
EPSSProbability: ptr.Float64(0.5),
|
||||
CISAKnownExploit: ptr.Bool(true),
|
||||
Published: ptr.Time(mockTime),
|
||||
Description: "Test CVE 2021-1234",
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2021-1235",
|
||||
CVSSScore: ptr.Float64(5.4),
|
||||
EPSSProbability: ptr.Float64(0.6),
|
||||
CISAKnownExploit: ptr.Bool(false),
|
||||
Published: ptr.Time(mockTime),
|
||||
Description: "Test CVE 2021-1235",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.UpdateVulnerabilityHostCounts(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp)
|
||||
require.Empty(t, resp.Err)
|
||||
require.Len(s.T(), resp.Vulnerabilities, 2)
|
||||
require.Equal(t, resp.Count, uint(2))
|
||||
require.False(t, resp.Meta.HasPreviousResults)
|
||||
require.False(t, resp.Meta.HasNextResults)
|
||||
|
||||
expected := map[string]struct {
|
||||
fleet.CVEMeta
|
||||
HostCount uint
|
||||
DetailsLink string
|
||||
Source fleet.VulnerabilitySource
|
||||
}{
|
||||
"CVE-2021-1234": {
|
||||
HostCount: 1,
|
||||
DetailsLink: "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234",
|
||||
},
|
||||
"CVE-2021-1235": {
|
||||
HostCount: 1,
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1235",
|
||||
},
|
||||
}
|
||||
|
||||
for _, vuln := range resp.Vulnerabilities {
|
||||
expectedVuln, ok := expected[vuln.CVE]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expectedVuln.HostCount, vuln.HostCount)
|
||||
require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink)
|
||||
require.Empty(t, vuln.CVSSScore)
|
||||
}
|
||||
|
||||
// Test Team Filter
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", "1")
|
||||
require.Len(s.T(), resp.Vulnerabilities, 0)
|
||||
|
||||
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
err = s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.UpdateVulnerabilityHostCounts(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", fmt.Sprintf("%d", team.ID))
|
||||
require.Len(t, resp.Vulnerabilities, 2)
|
||||
require.Equal(t, uint(2), resp.Count)
|
||||
require.False(t, resp.Meta.HasPreviousResults)
|
||||
require.False(t, resp.Meta.HasNextResults)
|
||||
require.Empty(t, resp.Err)
|
||||
|
||||
for _, vuln := range resp.Vulnerabilities {
|
||||
expectedVuln, ok := expected[vuln.CVE]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expectedVuln.HostCount, vuln.HostCount)
|
||||
require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink)
|
||||
require.Empty(t, vuln.CVSSScore)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestOSVersions() {
|
||||
t := s.T()
|
||||
|
||||
|
|
|
|||
|
|
@ -3133,6 +3133,174 @@ func (s *integrationEnterpriseTestSuite) TestListHosts() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestListVulnerabilities() {
|
||||
t := s.T()
|
||||
var resp listVulnerabilitiesResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp)
|
||||
|
||||
// Invalid Order Key
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusBadRequest, &resp, "order_key", "foo", "order_direction", "asc")
|
||||
|
||||
// EE Only Order Key
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "order_key", "cvss_score", "order_direction", "asc")
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp)
|
||||
require.Len(s.T(), resp.Vulnerabilities, 0)
|
||||
|
||||
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
|
||||
OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"),
|
||||
UUID: t.Name() + "2",
|
||||
Hostname: t.Name() + "foo2.local",
|
||||
PrimaryIP: "192.168.1.2",
|
||||
PrimaryMac: "30-65-EC-6F-C4-59",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.UpdateHostOperatingSystem(context.Background(), host.ID, fleet.OperatingSystem{
|
||||
Name: "windows",
|
||||
Version: "10.0.19042.1234",
|
||||
Arch: "64bit",
|
||||
Platform: "windows",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
allos, err := s.ds.ListOperatingSystems(context.Background())
|
||||
require.NoError(t, err)
|
||||
var os fleet.OperatingSystem
|
||||
for _, o := range allos {
|
||||
if o.ID > os.ID {
|
||||
os = o
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
|
||||
OSID: os.ID,
|
||||
CVE: "CVE-2021-1234",
|
||||
}, fleet.MSRCSource)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := s.ds.UpdateHostSoftware(context.Background(), host.ID, []fleet.Software{
|
||||
{Name: "foo", Version: "0.0.1", Source: "chrome_extensions"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sw := res.Inserted[0]
|
||||
|
||||
_, err = s.ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{
|
||||
SoftwareID: sw.ID,
|
||||
CVE: "CVE-2021-1235",
|
||||
}, fleet.NVDSource)
|
||||
require.NoError(t, err)
|
||||
|
||||
// insert CVEMeta
|
||||
mockTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
err = s.ds.InsertCVEMeta(context.Background(), []fleet.CVEMeta{
|
||||
{
|
||||
CVE: "CVE-2021-1234",
|
||||
CVSSScore: ptr.Float64(7.5),
|
||||
EPSSProbability: ptr.Float64(0.5),
|
||||
CISAKnownExploit: ptr.Bool(true),
|
||||
Published: ptr.Time(mockTime),
|
||||
Description: "Test CVE 2021-1234",
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2021-1235",
|
||||
CVSSScore: ptr.Float64(5.4),
|
||||
EPSSProbability: ptr.Float64(0.6),
|
||||
CISAKnownExploit: ptr.Bool(false),
|
||||
Published: ptr.Time(mockTime),
|
||||
Description: "Test CVE 2021-1235",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.UpdateVulnerabilityHostCounts(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp)
|
||||
require.Len(s.T(), resp.Vulnerabilities, 2)
|
||||
require.Equal(t, resp.Count, uint(2))
|
||||
require.False(t, resp.Meta.HasPreviousResults)
|
||||
require.False(t, resp.Meta.HasNextResults)
|
||||
require.Empty(t, resp.Err)
|
||||
|
||||
expected := map[string]struct {
|
||||
fleet.CVEMeta
|
||||
HostCount uint
|
||||
DetailsLink string
|
||||
Source fleet.VulnerabilitySource
|
||||
}{
|
||||
"CVE-2021-1234": {
|
||||
HostCount: 1,
|
||||
DetailsLink: "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234",
|
||||
CVEMeta: fleet.CVEMeta{
|
||||
CVE: "CVE-2021-1234",
|
||||
CVSSScore: ptr.Float64(7.5),
|
||||
EPSSProbability: ptr.Float64(0.5),
|
||||
CISAKnownExploit: ptr.Bool(true),
|
||||
Published: ptr.Time(mockTime),
|
||||
Description: "Test CVE 2021-1234",
|
||||
},
|
||||
},
|
||||
"CVE-2021-1235": {
|
||||
HostCount: 1,
|
||||
DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2021-1235",
|
||||
CVEMeta: fleet.CVEMeta{
|
||||
CVE: "CVE-2021-1235",
|
||||
CVSSScore: ptr.Float64(5.4),
|
||||
EPSSProbability: ptr.Float64(0.6),
|
||||
CISAKnownExploit: ptr.Bool(false),
|
||||
Published: ptr.Time(mockTime),
|
||||
Description: "Test CVE 2021-1235",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, vuln := range resp.Vulnerabilities {
|
||||
expectedVuln, ok := expected[vuln.CVE]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expectedVuln.HostCount, vuln.HostCount)
|
||||
require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink)
|
||||
require.Equal(t, expectedVuln.CVEMeta, vuln.CVEMeta)
|
||||
}
|
||||
|
||||
// EE Exploit Filter
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "exploit", "true")
|
||||
require.Len(t, resp.Vulnerabilities, 1)
|
||||
require.Equal(t, "CVE-2021-1234", resp.Vulnerabilities[0].CVE)
|
||||
|
||||
// Test Team Filter
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", "1")
|
||||
require.Len(s.T(), resp.Vulnerabilities, 0)
|
||||
|
||||
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
err = s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{host.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.ds.UpdateVulnerabilityHostCounts(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities", nil, http.StatusOK, &resp, "team_id", fmt.Sprintf("%d", team.ID))
|
||||
require.Len(t, resp.Vulnerabilities, 2)
|
||||
require.Equal(t, uint(2), resp.Count)
|
||||
require.False(t, resp.Meta.HasPreviousResults)
|
||||
require.False(t, resp.Meta.HasNextResults)
|
||||
require.Empty(t, resp.Err)
|
||||
|
||||
for _, vuln := range resp.Vulnerabilities {
|
||||
expectedVuln, ok := expected[vuln.CVE]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expectedVuln.HostCount, vuln.HostCount)
|
||||
require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink)
|
||||
require.Equal(t, expectedVuln.CVEMeta, vuln.CVEMeta)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestOSVersions() {
|
||||
t := s.T()
|
||||
|
||||
|
|
@ -3146,13 +3314,16 @@ func (s *integrationEnterpriseTestSuite) TestOSVersions() {
|
|||
|
||||
// set operating system information on a host
|
||||
require.NoError(t, s.ds.UpdateHostOperatingSystem(context.Background(), hosts[0].ID, testOS))
|
||||
var osID uint
|
||||
var osinfo struct {
|
||||
ID uint `db:"id"`
|
||||
OSVersionID uint `db:"os_version_id"`
|
||||
}
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(context.Background(), q, &osID,
|
||||
`SELECT id FROM operating_systems WHERE name = ? AND version = ? AND arch = ? AND kernel_version = ? AND platform = ?`,
|
||||
return sqlx.GetContext(context.Background(), q, &osinfo,
|
||||
`SELECT id, os_version_id FROM operating_systems WHERE name = ? AND version = ? AND arch = ? AND kernel_version = ? AND platform = ?`,
|
||||
testOS.Name, testOS.Version, testOS.Arch, testOS.KernelVersion, testOS.Platform)
|
||||
})
|
||||
require.Greater(t, osID, uint(0))
|
||||
require.Greater(t, osinfo.ID, uint(0))
|
||||
|
||||
resp = listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_name", testOS.Name, "os_version", testOS.Version)
|
||||
|
|
@ -3160,7 +3331,7 @@ func (s *integrationEnterpriseTestSuite) TestOSVersions() {
|
|||
|
||||
expected := resp.Hosts[0]
|
||||
resp = listHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_id", fmt.Sprintf("%d", osID))
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_id", fmt.Sprintf("%d", osinfo.ID))
|
||||
require.Len(t, resp.Hosts, 1)
|
||||
require.Equal(t, expected, resp.Hosts[0])
|
||||
|
||||
|
|
@ -3169,7 +3340,7 @@ func (s *integrationEnterpriseTestSuite) TestOSVersions() {
|
|||
|
||||
// insert OS Vulns
|
||||
_, err := s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
|
||||
OSID: osID,
|
||||
OSID: osinfo.ID,
|
||||
CVE: "CVE-2021-1234",
|
||||
}, fleet.MSRCSource)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -3205,7 +3376,7 @@ func (s *integrationEnterpriseTestSuite) TestOSVersions() {
|
|||
require.Equal(t, vulnMeta[0].Description, **osVersionsResp.OSVersions[0].Vulnerabilities[0].Description)
|
||||
|
||||
var osVersionResp getOSVersionResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/os_versions/%d", 1), nil, http.StatusOK, &osVersionResp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/os_versions/%d", osinfo.OSVersionID), nil, http.StatusOK, &osVersionResp)
|
||||
require.Equal(t, &osVersionsResp.OSVersions[0], osVersionResp.OSVersion)
|
||||
|
||||
// return empty json if UpdateOSVersions cron hasn't run yet for new team
|
||||
|
|
|
|||
92
server/service/vulnerabilities.go
Normal file
92
server/service/vulnerabilities.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
||||
var freeValidVulnSortColumns = []string{
|
||||
"cve",
|
||||
"host_count",
|
||||
"host_count_updated_at",
|
||||
"created_at",
|
||||
}
|
||||
|
||||
type listVulnerabilitiesRequest struct {
|
||||
fleet.VulnListOptions
|
||||
}
|
||||
|
||||
type listVulnerabilitiesResponse struct {
|
||||
Vulnerabilities []fleet.VulnerabilityWithMetadata `json:"vulnerabilities"`
|
||||
Count uint `json:"count"`
|
||||
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r listVulnerabilitiesResponse) error() error { return r.Err }
|
||||
|
||||
func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (errorer, error) {
|
||||
request := req.(*listVulnerabilitiesRequest)
|
||||
vulns, meta, err := svc.ListVulnerabilities(ctx, request.VulnListOptions)
|
||||
if err != nil {
|
||||
return listVulnerabilitiesResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
count, err := svc.CountVulnerabilities(ctx, request.VulnListOptions)
|
||||
if err != nil {
|
||||
return listVulnerabilitiesResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return listVulnerabilitiesResponse{
|
||||
Vulnerabilities: vulns,
|
||||
Meta: meta,
|
||||
Count: count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
||||
TeamID: &opt.TeamID,
|
||||
}, fleet.ActionRead); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(opt.ValidSortColumns) == 0 {
|
||||
opt.ValidSortColumns = freeValidVulnSortColumns
|
||||
}
|
||||
|
||||
if !opt.HasValidSortColumn() {
|
||||
return nil, nil, badRequest("invalid order key")
|
||||
}
|
||||
|
||||
if opt.KnownExploit && !opt.IsEE {
|
||||
return nil, nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
vulns, meta, err := svc.ds.ListVulnerabilities(ctx, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for i, vuln := range vulns {
|
||||
if vuln.Source == fleet.MSRCSource {
|
||||
vulns[i].DetailsLink = fmt.Sprintf("https://msrc.microsoft.com/update-guide/en-US/vulnerability/%s", vuln.CVE)
|
||||
} else {
|
||||
vulns[i].DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE)
|
||||
}
|
||||
}
|
||||
|
||||
return vulns, meta, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CountVulnerabilities(ctx context.Context, opts fleet.VulnListOptions) (uint, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
||||
TeamID: &opts.TeamID,
|
||||
}, fleet.ActionRead); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return svc.ds.CountVulnerabilities(ctx, opts)
|
||||
}
|
||||
53
server/service/vulnerabilities_test.go
Normal file
53
server/service/vulnerabilities_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListVulnerabilities(t *testing.T) {
|
||||
ds := new(mock.Store)
|
||||
svc, ctx := newTestService(t, ds, nil, nil)
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
|
||||
ds.ListVulnerabilitiesFunc = func(cxt context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
|
||||
return []fleet.VulnerabilityWithMetadata{
|
||||
{
|
||||
CVEMeta: fleet.CVEMeta{
|
||||
CVE: "CVE-2019-1234",
|
||||
Description: "A vulnerability",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
HostCount: 10,
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
t.Run("no list options", func(t *testing.T) {
|
||||
_, _, err := svc.ListVulnerabilities(ctx, fleet.VulnListOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("can only sort by supported columns", func(t *testing.T) {
|
||||
// invalid order key
|
||||
opts := fleet.VulnListOptions{ListOptions: fleet.ListOptions{
|
||||
OrderKey: "invalid",
|
||||
}, ValidSortColumns: freeValidVulnSortColumns}
|
||||
|
||||
_, _, err := svc.ListVulnerabilities(ctx, opts)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid order key")
|
||||
|
||||
// valid order key
|
||||
opts.OrderKey = "cve"
|
||||
_, _, err = svc.ListVulnerabilities(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue