16475 vuln detail api (#16828)

This commit is contained in:
Tim Lee 2024-02-14 14:42:16 -07:00 committed by mostlikelee
parent de40fa6988
commit 97cc6b844f
13 changed files with 831 additions and 20 deletions

View File

@ -20,3 +20,7 @@ func (svc *Service) ListVulnerabilities(ctx context.Context, opt fleet.VulnListO
opt.IsEE = true
return svc.Service.ListVulnerabilities(ctx, opt)
}
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (*fleet.VulnerabilityWithMetadata, error) {
return svc.Service.Vulnerability(ctx, cve, teamID, true)
}

View File

@ -2,14 +2,167 @@ package mysql
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
)
func (ds *Datastore) Vulnerability(ctx context.Context, cve string, teamID *uint, includeCVEScores bool) (*fleet.VulnerabilityWithMetadata, error) {
var vuln fleet.VulnerabilityWithMetadata
eeSelectStmt := `
SELECT
vhc.cve,
MIN(COALESCE(osv.created_at, sc.created_at, NOW())) AS created_at,
COALESCE(osv.source, sc.source, 0) AS source,
cm.cvss_score,
cm.epss_probability,
cm.cisa_known_exploit,
cm.published,
COALESCE(cm.description, '') AS description,
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
LEFT JOIN operating_system_vulnerabilities osv ON osv.cve = vhc.cve
LEFT JOIN software_cve sc ON sc.cve = vhc.cve
WHERE vhc.cve = ?
`
eeGroupBy := " GROUP BY vhc.cve, source, cm.cvss_score, cm.epss_probability, cm.cisa_known_exploit, cm.published, description, vhc.host_count, host_count_updated_at"
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.cve = ?
`
freeGroupBy := " GROUP BY vhc.cve, source, vhc.host_count, host_count_updated_at"
var args []interface{}
args = append(args, cve)
if teamID != nil {
eeSelectStmt += " AND vhc.team_id = ?"
freeSelectStmt += " AND vhc.team_id = ?"
args = append(args, *teamID)
} else {
eeSelectStmt += " AND vhc.team_id = 0"
freeSelectStmt += " AND vhc.team_id = 0"
}
eeSelectStmt += eeGroupBy
freeSelectStmt += freeGroupBy
var selectStmt string
if includeCVEScores {
selectStmt = eeSelectStmt
} else {
selectStmt = freeSelectStmt
}
err := sqlx.GetContext(ctx, ds.reader(ctx), &vuln, selectStmt, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("Vulnerability").WithName(cve))
}
return nil, ctxerr.Wrap(ctx, err, "fetching vulnerability")
}
return &vuln, nil
}
func (ds *Datastore) OSVersionsByCVE(ctx context.Context, cve string, teamID *uint) (vos []*fleet.VulnerableOS, updatedAt time.Time, err error) {
osvs, err := ds.OSVersions(ctx, teamID, nil, nil, nil)
if err != nil {
return nil, updatedAt, ctxerr.Wrap(ctx, err, "fetching OS versions by CVE")
}
updatedAt = osvs.CountsUpdatedAt
var osVersionWithResolved []struct {
OSVersionID uint `db:"os_version_id"`
ResolvedVersion *string `db:"resolved_in_version"`
}
selectStmt := `
SELECT os.os_version_id, osv.resolved_in_version
FROM operating_system_vulnerabilities osv
JOIN operating_systems os ON os.id = osv.operating_system_id
WHERE osv.cve = ?
`
err = sqlx.SelectContext(ctx, ds.reader(ctx), &osVersionWithResolved, selectStmt, cve)
if err != nil {
if err == sql.ErrNoRows {
return nil, updatedAt, ctxerr.Wrap(ctx, notFound("Vulnerability").WithName(cve))
}
return vos, updatedAt, ctxerr.Wrap(ctx, err, "fetching OS versions by CVE")
}
for _, osv := range osvs.OSVersions {
for _, id := range osVersionWithResolved {
if osv.OSVersionID == id.OSVersionID {
vos = append(vos, &fleet.VulnerableOS{
OSVersion: osv,
ResolvedInVersion: id.ResolvedVersion,
})
}
}
}
return
}
func (ds *Datastore) SoftwareByCVE(ctx context.Context, cve string, teamID *uint) (vs []*fleet.VulnerableSoftware, updatedAt time.Time, err error) {
var args []interface{}
selectStmt := `
SELECT
s.id,
s.name,
s.version,
s.source,
s.browser,
COALESCE(scpe.cpe, '') as generated_cpe,
COALESCE(shc.hosts_count, 0) as hosts_count,
COALESCE(sc.resolved_in_version, '') as resolved_in_version
FROM software s
JOIN software_cve sc ON sc.software_id = s.id
LEFT JOIN software_cpe scpe ON scpe.software_id = s.id
LEFT JOIN software_host_counts shc ON shc.software_id = s.id
WHERE sc.cve = ?
`
args = append(args, cve)
if teamID != nil {
selectStmt += " AND shc.team_id = ?"
args = append(args, *teamID)
} else {
selectStmt += " AND shc.team_id = 0"
}
err = sqlx.SelectContext(ctx, ds.reader(ctx), &vs, selectStmt, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, updatedAt, ctxerr.Wrap(ctx, notFound("Vulnerability").WithName(cve))
}
return vs, updatedAt, ctxerr.Wrap(ctx, err, "fetching software by CVE")
}
return
}
func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
// Define base select statements for EE and Free versions
eeSelectStmt := `
@ -62,9 +215,10 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
cm.cisa_known_exploit,
cm.published,
description,
vhc.host_count
vhc.host_count,
host_count_updated_at
`
freeGroupBy := " GROUP BY vhc.cve, source, vhc.host_count"
freeGroupBy := " GROUP BY vhc.cve, source, vhc.host_count, host_count_updated_at"
// Choose the appropriate group by statement based on EE or Free
var groupBy string
@ -91,14 +245,6 @@ func (ds *Datastore) ListVulnerabilities(ctx context.Context, opt fleet.VulnList
selectStmt, args = searchLike(selectStmt, args, match, "vhc.cve")
}
if opt.KnownExploit {
selectStmt = selectStmt + " AND cm.cisa_known_exploit = 1"
}
if match := opt.MatchQuery; match != "" {
selectStmt, args = searchLike(selectStmt, args, match, "vhc.cve")
}
// Append group by statement
selectStmt += groupBy

View File

@ -20,6 +20,10 @@ func TestVulnerabilities(t *testing.T) {
fn func(t *testing.T, ds *Datastore)
}{
{"TestListVulnerabilities", testListVulnerabilities},
{"TestVulnerabilityWithOS", testVulnerabilityWithOS},
{"TestVulnerabilityWithSoftware", testVulnerabilityWithSoftware},
{"TestOSVersionsByCVE", testOSVersionsByCVE},
{"TestSoftwareByCVE", testSoftwareByCVE},
{"TestVulnerabilitiesPagination", testVulnerabilitiesPagination},
{"TestVulnerabilitiesTeamFilter", testVulnerabilitiesTeamFilter},
{"TestListVulnerabilitiesSort", testListVulnerabilitiesSort},
@ -157,6 +161,169 @@ func testListVulnerabilities(t *testing.T, ds *Datastore) {
}
}
func testVulnerabilityWithOS(t *testing.T, ds *Datastore) {
mockTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := context.Background()
v, err := ds.Vulnerability(ctx, "CVE-2020-1234", nil, false)
require.Nil(t, v)
require.Error(t, err)
var nfe *notFoundError
require.ErrorAs(t, err, &nfe)
// Insert Host Count
insertStmt := `
INSERT INTO vulnerability_host_counts (cve, team_id, host_count)
VALUES (?, ?, ?), (?, ?, ?)
`
_, err = ds.writer(context.Background()).Exec(insertStmt,
"CVE-2020-1234", 0, 10,
"CVE-2020-1234", 1, 4,
)
require.NoError(t, err)
// // insert OS Vuln
_, err = ds.InsertOSVulnerabilities(context.Background(), []fleet.OSVulnerability{
{
OSID: 1,
CVE: "CVE-2020-1234",
ResolvedInVersion: ptr.String("1.0.0"),
},
}, fleet.MSRCSource)
require.NoError(t, err)
// // insert CVEMeta
err = ds.InsertCVEMeta(context.Background(), []fleet.CVEMeta{
{
CVE: "CVE-2020-1234",
CVSSScore: ptr.Float64(7.5),
EPSSProbability: ptr.Float64(0.5),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(mockTime),
Description: "Test CVE 2020-1234",
},
})
require.NoError(t, err)
expected := fleet.VulnerabilityWithMetadata{
CVEMeta: fleet.CVEMeta{
CVE: "CVE-2020-1234",
},
HostCount: 10,
Source: fleet.MSRCSource,
}
// No CVSSScores
v, err = ds.Vulnerability(ctx, "CVE-2020-1234", nil, false)
require.NoError(t, err)
require.Equal(t, expected.CVEMeta, v.CVEMeta)
require.Equal(t, expected.HostCount, v.HostCount)
require.Equal(t, expected.Source, v.Source)
// Team 1
expected.HostCount = 4
v, err = ds.Vulnerability(ctx, "CVE-2020-1234", ptr.Uint(1), false)
require.NoError(t, err)
require.Equal(t, expected.CVEMeta, v.CVEMeta)
require.Equal(t, expected.HostCount, v.HostCount)
require.Equal(t, expected.Source, v.Source)
expected = fleet.VulnerabilityWithMetadata{
CVEMeta: fleet.CVEMeta{
CVE: "CVE-2020-1234",
CVSSScore: ptr.Float64(7.5),
EPSSProbability: ptr.Float64(0.5),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(mockTime),
Description: "Test CVE 2020-1234",
},
HostCount: 10,
Source: fleet.MSRCSource,
}
// With CVSSScores
v, err = ds.Vulnerability(ctx, "CVE-2020-1234", nil, true)
require.NoError(t, err)
require.Equal(t, expected.CVEMeta, v.CVEMeta)
require.Equal(t, expected.HostCount, v.HostCount)
require.Equal(t, expected.Source, v.Source)
}
func testVulnerabilityWithSoftware(t *testing.T, ds *Datastore) {
mockTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
ctx := context.Background()
v, err := ds.Vulnerability(ctx, "CVE-2020-1234", nil, false)
require.Nil(t, v)
require.Error(t, err)
var nfe *notFoundError
require.ErrorAs(t, err, &nfe)
// Insert Host Count
insertStmt := `
INSERT INTO vulnerability_host_counts (cve, team_id, host_count)
VALUES (?, ?, ?)
`
_, err = ds.writer(context.Background()).Exec(insertStmt, "CVE-2020-1234", 0, 10)
require.NoError(t, err)
// insert Software Vuln
_, err = ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{
SoftwareID: 1,
CVE: "CVE-2020-1234",
}, fleet.NVDSource)
require.NoError(t, err)
// insert CVEMeta
err = ds.InsertCVEMeta(context.Background(), []fleet.CVEMeta{
{
CVE: "CVE-2020-1234",
CVSSScore: ptr.Float64(7.5),
EPSSProbability: ptr.Float64(0.5),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(mockTime),
Description: "Test CVE 2020-1234",
},
})
require.NoError(t, err)
// No CVSSScores
expected := fleet.VulnerabilityWithMetadata{
CVEMeta: fleet.CVEMeta{
CVE: "CVE-2020-1234",
},
HostCount: 10,
Source: fleet.NVDSource,
}
v, err = ds.Vulnerability(ctx, "CVE-2020-1234", nil, false)
require.NoError(t, err)
require.Equal(t, expected.CVEMeta, v.CVEMeta)
require.Equal(t, expected.HostCount, v.HostCount)
require.Equal(t, expected.Source, v.Source)
// With CVSSScores
expected = fleet.VulnerabilityWithMetadata{
CVEMeta: fleet.CVEMeta{
CVE: "CVE-2020-1234",
CVSSScore: ptr.Float64(7.5),
EPSSProbability: ptr.Float64(0.5),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(mockTime),
Description: "Test CVE 2020-1234",
},
HostCount: 10,
Source: fleet.NVDSource,
}
v, err = ds.Vulnerability(ctx, "CVE-2020-1234", nil, true)
require.NoError(t, err)
require.Equal(t, expected.CVEMeta, v.CVEMeta)
require.Equal(t, expected.HostCount, v.HostCount)
require.Equal(t, expected.Source, v.Source)
}
func testVulnerabilitiesPagination(t *testing.T, ds *Datastore) {
seedVulnerabilities(t, ds)
@ -629,6 +796,80 @@ func testVulnerabilityHostCountBatchInserts(t *testing.T, ds *Datastore) {
}
}
func testOSVersionsByCVE(t *testing.T, ds *Datastore) {
seedVulnerabilities(t, ds)
// global
osv, _, err := ds.OSVersionsByCVE(context.Background(), "CVE-2020-1238", nil)
require.NoError(t, err)
expected := []fleet.VulnerableOS{
{
OSVersion: fleet.OSVersion{
Name: "Microsoft Windows 11 Enterprise 22H2 10.0.22621.2715",
NameOnly: "Microsoft Windows 11 Enterprise 22H2",
OSVersionID: 1,
Version: "10.0.22621.2715",
Platform: "windows",
HostsCount: 10,
},
ResolvedInVersion: ptr.String("1.0.0"),
},
}
require.Len(t, osv, 1)
require.Equal(t, osv[0].OSVersion, expected[0].OSVersion)
// team 1
expected[0].OSVersion.HostsCount = 4
osv, _, err = ds.OSVersionsByCVE(context.Background(), "CVE-2020-1238", ptr.Uint(1))
require.NoError(t, err)
require.Len(t, osv, 1)
require.Equal(t, osv[0].OSVersion, expected[0].OSVersion)
// team 2
expected[0].OSVersion.HostsCount = 3
osv, _, err = ds.OSVersionsByCVE(context.Background(), "CVE-2020-1238", ptr.Uint(2))
require.NoError(t, err)
require.Len(t, osv, 1)
require.Equal(t, osv[0].OSVersion, expected[0].OSVersion)
}
func testSoftwareByCVE(t *testing.T, ds *Datastore) {
seedVulnerabilities(t, ds)
// global
software, _, err := ds.SoftwareByCVE(context.Background(), "CVE-2020-1234", nil)
require.NoError(t, err)
expected := &fleet.VulnerableSoftware{
ID: 1,
Name: "Chrome",
Version: "1.0.0",
Source: "programs",
HostsCount: 5,
GenerateCPE: "cpe:2.3:a:google:chrome:1.0.0:*:*:*:*:*:*:*:*",
ResolvedInVersion: ptr.String("1.0.0"),
}
require.Len(t, software, 1)
require.Equal(t, expected, software[0])
// team 1
expected.HostsCount = 4
software, _, err = ds.SoftwareByCVE(context.Background(), "CVE-2020-1234", ptr.Uint(1))
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, expected, software[0])
// team 2
expected.HostsCount = 1
software, _, err = ds.SoftwareByCVE(context.Background(), "CVE-2020-1234", ptr.Uint(2))
require.NoError(t, err)
require.Len(t, software, 1)
require.Equal(t, expected, software[0])
}
func assertHostCounts(t *testing.T, expected []hostCount, actual []fleet.VulnerabilityWithMetadata) {
t.Helper()
require.Len(t, actual, len(expected))
@ -639,6 +880,88 @@ func assertHostCounts(t *testing.T, expected []hostCount, actual []fleet.Vulnera
}
func seedVulnerabilities(t *testing.T, ds *Datastore) {
// insert 20 hosts
var hostids []uint
for i := 0; i < 20; i++ {
host := test.NewHost(t, ds, fmt.Sprintf("host%d", i), fmt.Sprintf("192.168.0.%d", i), fmt.Sprintf("%d", i+1000), fmt.Sprintf("%d", i+1000), time.Now())
hostids = append(hostids, host.ID)
}
// update 15 hosts to windows
for i := 0; i < 10; i++ {
err := ds.UpdateHostOperatingSystem(context.Background(), hostids[i], fleet.OperatingSystem{
Name: "Microsoft Windows 11 Enterprise 22H2",
Version: "10.0.22621.2715",
Arch: "x86_64",
Platform: "windows",
})
require.NoError(t, err)
}
// update 5 hosts to macOS
for i := 10; i < 15; i++ {
err := ds.UpdateHostOperatingSystem(context.Background(), hostids[i], fleet.OperatingSystem{
Name: "macOS",
Version: "14.1.2",
Arch: "arm64",
Platform: "darwin",
})
require.NoError(t, err)
}
// move 4 windows hosts to team 1
team1, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(context.Background(), &team1.ID, hostids[:4])
require.NoError(t, err)
// move 3 windows hosts to team 2
team2, err := ds.NewTeam(context.Background(), &fleet.Team{Name: "team2"})
require.NoError(t, err)
err = ds.AddHostsToTeam(context.Background(), &team2.ID, hostids[4:7])
require.NoError(t, err)
// move 1 macOS host to team 2
err = ds.AddHostsToTeam(context.Background(), &team2.ID, []uint{hostids[10]})
require.NoError(t, err)
err = ds.UpdateOSVersions(context.Background())
require.NoError(t, err)
// State:
// 10 global windows hosts
// 5 global macOS hosts
// 4 windows hosts in team 1
// 3 windows hosts in team 2
// 1 macOS host in team 2
// add software to 5 windows hosts
// affects:
// 5 global windows hosts
// 4 windows hosts in team 1
// 1 windows host in team 2
for i := 0; i < 5; i++ {
_, err = ds.UpdateHostSoftware(context.Background(), hostids[i], []fleet.Software{
{
Name: "Chrome",
Version: "1.0.0",
Source: "programs",
},
})
require.NoError(t, err)
}
_, err = ds.UpsertSoftwareCPEs(context.Background(), []fleet.SoftwareCPE{
{
SoftwareID: 1,
CPE: "cpe:2.3:a:google:chrome:1.0.0:*:*:*:*:*:*:*:*",
},
})
require.NoError(t, err)
err = ds.SyncHostsSoftware(context.Background(), time.Now())
require.NoError(t, err)
softwareVulns := []fleet.SoftwareVulnerability{
{
SoftwareID: 1,
@ -817,7 +1140,7 @@ func seedVulnerabilities(t *testing.T, ds *Datastore) {
}
// Insert OS Vuln
_, err := ds.InsertOSVulnerabilities(context.Background(), osVulns, fleet.NVDSource)
_, err = ds.InsertOSVulnerabilities(context.Background(), osVulns, fleet.NVDSource)
require.NoError(t, err)
// Insert Software Vuln

View File

@ -310,6 +310,8 @@ type Datastore interface {
GetMDMSolution(ctx context.Context, mdmID uint) (*MDMSolution, error)
OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*OSVersions, error)
OSVersionsByCVE(ctx context.Context, cve string, teamID *uint) ([]*VulnerableOS, time.Time, error)
SoftwareByCVE(ctx context.Context, cve string, teamID *uint) ([]*VulnerableSoftware, time.Time, error)
OSVersion(ctx context.Context, osVersionID uint, teamID *uint) (*OSVersion, *time.Time, error)
UpdateOSVersions(ctx context.Context) error
@ -857,6 +859,8 @@ type Datastore interface {
// ListVulnerabilities returns a list of unique vulnerabilities based on the provided options.
ListVulnerabilities(ctx context.Context, opt VulnListOptions) ([]VulnerabilityWithMetadata, *PaginationMetadata, error)
// Vulnerability returns the vulnerability corresponding to the specified CVE ID
Vulnerability(ctx context.Context, cve string, teamID *uint, includeCVEScores bool) (*VulnerabilityWithMetadata, error)
// CountVulnerabilities returns the number of unique vulnerabilities based on the provided
// options.
CountVulnerabilities(ctx context.Context, opt VulnListOptions) (uint, error)

View File

@ -1125,6 +1125,11 @@ type OSVersions struct {
OSVersions []OSVersion `json:"os_versions"`
}
type VulnerableOS struct {
OSVersion
ResolvedInVersion *string `json:"resolved_in_version"`
}
type OSVersion struct {
// ID is the unique id of the operating system.
ID uint `json:"id,omitempty"`

View File

@ -610,8 +610,14 @@ 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)
// 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)
// /////////////////////////////////////////////////////////////////////////////
// Team Policies

View File

@ -103,6 +103,17 @@ func (s Software) ToUniqueStr() string {
return strings.Join(ss, SoftwareFieldSeparator)
}
type VulnerableSoftware struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Version string `json:"version" db:"version"`
Source string `json:"source" db:"source"`
Browser string `json:"browser" db:"browser"`
GenerateCPE string `json:"generated_cpe" db:"generated_cpe"`
HostsCount int `json:"hosts_count,omitempty" db:"hosts_count"`
ResolvedInVersion *string `json:"resolved_in_version" db:"resolved_in_version"`
}
type SliceString []string
func (c *SliceString) Scan(v interface{}) error {

View File

@ -244,6 +244,10 @@ type GetMDMSolutionFunc func(ctx context.Context, mdmID uint) (*fleet.MDMSolutio
type OSVersionsFunc func(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error)
type OSVersionsByCVEFunc func(ctx context.Context, cve string, teamID *uint) ([]*fleet.VulnerableOS, time.Time, error)
type SoftwareByCVEFunc func(ctx context.Context, cve string, teamID *uint) ([]*fleet.VulnerableSoftware, time.Time, error)
type OSVersionFunc func(ctx context.Context, osVersionID uint, teamID *uint) (*fleet.OSVersion, *time.Time, error)
type UpdateOSVersionsFunc func(ctx context.Context) error
@ -594,6 +598,8 @@ type DeleteOutOfDateOSVulnerabilitiesFunc func(ctx context.Context, source fleet
type ListVulnerabilitiesFunc func(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error)
type VulnerabilityFunc func(ctx context.Context, cve string, teamID *uint, includeCVEScores bool) (*fleet.VulnerabilityWithMetadata, error)
type CountVulnerabilitiesFunc func(ctx context.Context, opt fleet.VulnListOptions) (uint, error)
type UpdateVulnerabilityHostCountsFunc func(ctx context.Context) error
@ -1166,6 +1172,12 @@ type DataStore struct {
OSVersionsFunc OSVersionsFunc
OSVersionsFuncInvoked bool
OSVersionsByCVEFunc OSVersionsByCVEFunc
OSVersionsByCVEFuncInvoked bool
SoftwareByCVEFunc SoftwareByCVEFunc
SoftwareByCVEFuncInvoked bool
OSVersionFunc OSVersionFunc
OSVersionFuncInvoked bool
@ -1691,6 +1703,9 @@ type DataStore struct {
ListVulnerabilitiesFunc ListVulnerabilitiesFunc
ListVulnerabilitiesFuncInvoked bool
VulnerabilityFunc VulnerabilityFunc
VulnerabilityFuncInvoked bool
CountVulnerabilitiesFunc CountVulnerabilitiesFunc
CountVulnerabilitiesFuncInvoked bool
@ -2833,6 +2848,20 @@ func (s *DataStore) OSVersions(ctx context.Context, teamID *uint, platform *stri
return s.OSVersionsFunc(ctx, teamID, platform, name, version)
}
func (s *DataStore) OSVersionsByCVE(ctx context.Context, cve string, teamID *uint) ([]*fleet.VulnerableOS, time.Time, error) {
s.mu.Lock()
s.OSVersionsByCVEFuncInvoked = true
s.mu.Unlock()
return s.OSVersionsByCVEFunc(ctx, cve, teamID)
}
func (s *DataStore) SoftwareByCVE(ctx context.Context, cve string, teamID *uint) ([]*fleet.VulnerableSoftware, time.Time, error) {
s.mu.Lock()
s.SoftwareByCVEFuncInvoked = true
s.mu.Unlock()
return s.SoftwareByCVEFunc(ctx, cve, teamID)
}
func (s *DataStore) OSVersion(ctx context.Context, osVersionID uint, teamID *uint) (*fleet.OSVersion, *time.Time, error) {
s.mu.Lock()
s.OSVersionFuncInvoked = true
@ -4058,6 +4087,13 @@ func (s *DataStore) ListVulnerabilities(ctx context.Context, opt fleet.VulnListO
return s.ListVulnerabilitiesFunc(ctx, opt)
}
func (s *DataStore) Vulnerability(ctx context.Context, cve string, teamID *uint, includeCVEScores bool) (*fleet.VulnerabilityWithMetadata, error) {
s.mu.Lock()
s.VulnerabilityFuncInvoked = true
s.mu.Unlock()
return s.VulnerabilityFunc(ctx, cve, teamID, includeCVEScores)
}
func (s *DataStore) CountVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) (uint, error) {
s.mu.Lock()
s.CountVulnerabilitiesFuncInvoked = true

View File

@ -373,6 +373,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
// Vulnerabilities
ue.GET("/api/_version_/fleet/vulnerabilities", listVulnerabilitiesEndpoint, listVulnerabilitiesRequest{})
ue.GET("/api/_version_/fleet/vulnerabilities/{cve}", getVulnerabilityEndpoint, getVulnerabilityRequest{})
// Hosts
ue.GET("/api/_version_/fleet/host_summary", getHostSummaryEndpoint, getHostSummaryRequest{})

View File

@ -7400,9 +7400,8 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
require.NoError(t, err)
err = s.ds.UpdateHostOperatingSystem(context.Background(), host.ID, fleet.OperatingSystem{
Name: "windows",
Name: "Windows 11 Enterprise 22H2",
Version: "10.0.19042.1234",
Arch: "64bit",
Platform: "windows",
})
require.NoError(t, err)
@ -7415,18 +7414,33 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
}
}
err = s.ds.UpdateOSVersions(context.Background())
require.NoError(t, err)
_, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
OSID: os.ID,
CVE: "CVE-2021-1234",
OSID: os.ID,
CVE: "CVE-2021-1234",
ResolvedInVersion: *ptr.StringPtr("10.0.19043.2013"),
}, 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"},
{Name: "Google Chrome", Version: "0.0.1", Source: "programs"},
})
require.NoError(t, err)
sw := res.Inserted[0]
_, err = s.ds.UpsertSoftwareCPEs(context.Background(), []fleet.SoftwareCPE{
{
SoftwareID: sw.ID,
CPE: "cpe:2.3:a:google:chrome:1.0.0:*:*:*:*:*:*:*:*",
},
})
require.NoError(t, err)
err = s.ds.SyncHostsSoftware(context.Background(), time.Now())
require.NoError(t, err)
_, err = s.ds.InsertSoftwareVulnerability(context.Background(), fleet.SoftwareVulnerability{
SoftwareID: sw.ID,
CVE: "CVE-2021-1235",
@ -7515,6 +7529,42 @@ func (s *integrationTestSuite) TestListVulnerabilities() {
require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink)
require.Empty(t, vuln.CVSSScore)
}
var gResp getVulnerabilityResponse
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusOK, &gResp)
require.Empty(t, gResp.Err)
require.Equal(t, "CVE-2021-1234", gResp.Vulnerability.CVE)
require.Equal(t, uint(1), gResp.Vulnerability.HostCount)
require.Equal(t, "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234", gResp.Vulnerability.DetailsLink)
require.Empty(t, gResp.Vulnerability.Description)
require.Empty(t, gResp.Vulnerability.CVSSScore)
require.Empty(t, gResp.Vulnerability.CISAKnownExploit)
require.Empty(t, gResp.Vulnerability.EPSSProbability)
require.Empty(t, gResp.Vulnerability.Published)
require.Len(t, gResp.OSVersions, 1)
require.Equal(t, "Windows 11 Enterprise 22H2 10.0.19042.1234", gResp.OSVersions[0].Name)
require.Equal(t, "Windows 11 Enterprise 22H2", gResp.OSVersions[0].NameOnly)
require.Equal(t, "windows", gResp.OSVersions[0].Platform)
require.Equal(t, "10.0.19042.1234", gResp.OSVersions[0].Version)
require.Equal(t, 1, gResp.OSVersions[0].HostsCount)
require.Equal(t, "10.0.19043.2013", *gResp.OSVersions[0].ResolvedInVersion)
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1235", nil, http.StatusOK, &gResp)
require.Empty(t, gResp.Err)
require.Equal(t, "CVE-2021-1235", gResp.Vulnerability.CVE)
require.Equal(t, uint(1), gResp.Vulnerability.HostCount)
require.Equal(t, "https://nvd.nist.gov/vuln/detail/CVE-2021-1235", gResp.Vulnerability.DetailsLink)
require.Empty(t, gResp.Vulnerability.Description)
require.Empty(t, gResp.Vulnerability.CVSSScore)
require.Empty(t, gResp.Vulnerability.CISAKnownExploit)
require.Empty(t, gResp.Vulnerability.EPSSProbability)
require.Empty(t, gResp.Vulnerability.Published)
require.Len(t, gResp.Software, 1)
require.Equal(t, "Google Chrome", gResp.Software[0].Name)
require.Equal(t, "0.0.1", gResp.Software[0].Version)
require.Equal(t, "programs", gResp.Software[0].Source)
require.Equal(t, "cpe:2.3:a:google:chrome:1.0.0:*:*:*:*:*:*:*:*", gResp.Software[0].GenerateCPE)
require.Equal(t, 1, gResp.Software[0].HostsCount)
}
func (s *integrationTestSuite) TestOSVersions() {

View File

@ -3163,9 +3163,8 @@ func (s *integrationEnterpriseTestSuite) TestListVulnerabilities() {
require.NoError(t, err)
err = s.ds.UpdateHostOperatingSystem(context.Background(), host.ID, fleet.OperatingSystem{
Name: "windows",
Name: "Windows 11 Enterprise 22H2",
Version: "10.0.19042.1234",
Arch: "64bit",
Platform: "windows",
})
require.NoError(t, err)
@ -3178,9 +3177,13 @@ func (s *integrationEnterpriseTestSuite) TestListVulnerabilities() {
}
}
err = s.ds.UpdateOSVersions(context.Background())
require.NoError(t, err)
_, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
OSID: os.ID,
CVE: "CVE-2021-1234",
OSID: os.ID,
CVE: "CVE-2021-1234",
ResolvedInVersion: ptr.String("10.0.19043.2013"),
}, fleet.MSRCSource)
require.NoError(t, err)
@ -3299,6 +3302,25 @@ func (s *integrationEnterpriseTestSuite) TestListVulnerabilities() {
require.Equal(t, expectedVuln.DetailsLink, vuln.DetailsLink)
require.Equal(t, expectedVuln.CVEMeta, vuln.CVEMeta)
}
var gResp getVulnerabilityResponse
s.DoJSON("GET", "/api/latest/fleet/vulnerabilities/CVE-2021-1234", nil, http.StatusOK, &gResp)
require.Empty(t, gResp.Err)
require.Equal(t, "CVE-2021-1234", gResp.Vulnerability.CVE)
require.Equal(t, uint(1), gResp.Vulnerability.HostCount)
require.Equal(t, "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234", gResp.Vulnerability.DetailsLink)
require.Equal(t, "Test CVE 2021-1234", gResp.Vulnerability.Description)
require.Equal(t, ptr.Float64(7.5), gResp.Vulnerability.CVSSScore)
require.Equal(t, ptr.Bool(true), gResp.Vulnerability.CISAKnownExploit)
require.Equal(t, ptr.Float64(0.5), gResp.Vulnerability.EPSSProbability)
require.Equal(t, ptr.Time(mockTime), gResp.Vulnerability.Published)
require.Len(t, gResp.OSVersions, 1)
require.Equal(t, "Windows 11 Enterprise 22H2 10.0.19042.1234", gResp.OSVersions[0].Name)
require.Equal(t, "Windows 11 Enterprise 22H2", gResp.OSVersions[0].NameOnly)
require.Equal(t, "windows", gResp.OSVersions[0].Platform)
require.Equal(t, "10.0.19042.1234", gResp.OSVersions[0].Version)
require.Equal(t, 1, gResp.OSVersions[0].HostsCount)
require.Equal(t, "10.0.19043.2013", *gResp.OSVersions[0].ResolvedInVersion)
}
func (s *integrationEnterpriseTestSuite) TestOSVersions() {

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
)
@ -90,3 +91,79 @@ func (svc *Service) CountVulnerabilities(ctx context.Context, opts fleet.VulnLis
return svc.ds.CountVulnerabilities(ctx, opts)
}
type getVulnerabilityRequest struct {
CVE string `url:"cve"`
TeamID *uint `query:"team_id,optional"`
}
type getVulnerabilityResponse struct {
Vulnerability *fleet.VulnerabilityWithMetadata `json:"vulnerability"`
OSVersions []*fleet.VulnerableOS `json:"os_versions"`
Software []*fleet.VulnerableSoftware `json:"software"`
Err error `json:"error,omitempty"`
}
func (r getVulnerabilityResponse) error() error { return r.Err }
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)
if err != nil {
return getVulnerabilityResponse{Err: err}, nil
}
if vuln.Source == fleet.MSRCSource {
vuln.DetailsLink = fmt.Sprintf("https://msrc.microsoft.com/update-guide/en-US/vulnerability/%s", vuln.CVE)
} else {
vuln.DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE)
}
osVersions, _, err := svc.ListOSVersionsByCVE(ctx, vuln.CVE, request.TeamID)
if err != nil {
return getVulnerabilityResponse{Err: err}, nil
}
software, _, err := svc.ListSoftwareByCVE(ctx, vuln.CVE, request.TeamID)
if err != nil {
return getVulnerabilityResponse{Err: err}, nil
}
return getVulnerabilityResponse{
Vulnerability: vuln,
OSVersions: osVersions,
Software: software,
}, nil
}
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (*fleet.VulnerabilityWithMetadata, error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, err
}
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, err
}
vuln, err := svc.ds.Vulnerability(ctx, cve, teamID, useCVSScores)
if err != nil {
return nil, err
}
return vuln, nil
}
func (svc *Service) ListOSVersionsByCVE(ctx context.Context, cve string, teamID *uint) (result []*fleet.VulnerableOS, updatedAt time.Time, err error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, updatedAt, err
}
return svc.ds.OSVersionsByCVE(ctx, cve, teamID)
}
func (svc *Service) ListSoftwareByCVE(ctx context.Context, cve string, teamID *uint) (result []*fleet.VulnerableSoftware, updatedAt time.Time, err error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, updatedAt, err
}
return svc.ds.SoftwareByCVE(ctx, cve, teamID)
}

View File

@ -51,3 +51,129 @@ func TestListVulnerabilities(t *testing.T) {
require.NoError(t, err)
})
}
func TestVulnerabilitesAuth(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
ds.ListVulnerabilitiesFunc = func(cxt context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
return []fleet.VulnerabilityWithMetadata{}, &fleet.PaginationMetadata{}, nil
}
ds.VulnerabilityFunc = func(cxt context.Context, cve string, teamID *uint, includeCVEScores bool) (*fleet.VulnerabilityWithMetadata, error) {
return &fleet.VulnerabilityWithMetadata{}, nil
}
ds.CountVulnerabilitiesFunc = func(cxt context.Context, opt fleet.VulnListOptions) (uint, error) {
return 0, nil
}
for _, tc := range []struct {
name string
user *fleet.User
shouldFailGlobalRead bool
shouldFailTeamRead bool
}{
{
name: "global-admin",
user: &fleet.User{
ID: 1,
GlobalRole: ptr.String(fleet.RoleAdmin),
},
shouldFailGlobalRead: false,
shouldFailTeamRead: false,
},
{
name: "global-maintainer",
user: &fleet.User{
ID: 1,
GlobalRole: ptr.String(fleet.RoleMaintainer),
},
shouldFailGlobalRead: false,
shouldFailTeamRead: false,
},
{
name: "global-observer",
user: &fleet.User{
ID: 1,
GlobalRole: ptr.String(fleet.RoleObserver),
},
shouldFailGlobalRead: false,
shouldFailTeamRead: false,
},
{
name: "team-admin-belongs-to-team",
user: &fleet.User{
ID: 1,
Teams: []fleet.UserTeam{{
Team: fleet.Team{ID: 1},
Role: fleet.RoleAdmin,
}},
},
shouldFailGlobalRead: true,
shouldFailTeamRead: false,
},
{
name: "team-maintainer-belongs-to-team",
user: &fleet.User{
ID: 1,
Teams: []fleet.UserTeam{{
Team: fleet.Team{ID: 1},
Role: fleet.RoleMaintainer,
}},
},
shouldFailGlobalRead: true,
shouldFailTeamRead: false,
},
{
name: "team-observer-belongs-to-team",
user: &fleet.User{
ID: 1,
Teams: []fleet.UserTeam{{
Team: fleet.Team{ID: 1},
Role: fleet.RoleObserver,
}},
},
shouldFailGlobalRead: true,
shouldFailTeamRead: false,
},
{
name: "team-admin-does-not-belong-to-team",
user: &fleet.User{
ID: 1,
Teams: []fleet.UserTeam{{
Team: fleet.Team{ID: 2},
Role: fleet.RoleAdmin,
}},
},
shouldFailGlobalRead: true,
shouldFailTeamRead: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx = viewer.NewContext(ctx, viewer.Viewer{User: tc.user})
_, _, err := svc.ListVulnerabilities(ctx, fleet.VulnListOptions{})
checkAuthErr(t, tc.shouldFailGlobalRead, err)
_, _, err = svc.ListVulnerabilities(ctx, fleet.VulnListOptions{
TeamID: 1,
})
checkAuthErr(t, tc.shouldFailTeamRead, err)
_, err = svc.CountVulnerabilities(ctx, fleet.VulnListOptions{})
checkAuthErr(t, tc.shouldFailGlobalRead, err)
_, err = svc.CountVulnerabilities(ctx, fleet.VulnListOptions{
TeamID: 1,
})
checkAuthErr(t, tc.shouldFailTeamRead, err)
_, 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)
checkAuthErr(t, tc.shouldFailTeamRead, err)
})
}
}