4345 OS Vulnerabilities Backend (#16303)

#4345 

This backend feature branch includes the following PRs:

macOS Vuln Matching:
#15837 
#15990 
#16077 

Bugs / Issues:
#16004
 #15905 
#16226 

Windows Vuln Matching
#16047 
#16049 
#16085 
#16099 

API:
#16215
This commit is contained in:
Tim Lee 2024-01-24 12:18:57 -07:00 committed by GitHub
parent edee09e22a
commit 79b5baa297
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1904 additions and 346 deletions

View File

@ -0,0 +1,3 @@
- Fleet now detects operating system vulnerabilities for macOS (via National Vulnerabilities Database) and Windows (via the
Microsoft Security Resource Center). We are extending the `os_versions` API to include
vulnerabilities, as well as a new OS tab on the Software page.

View File

@ -275,6 +275,7 @@ func checkWinVulnerabilities(
"msg", "msrc-analysis-done",
"os name", o.Name,
"os version", o.Version,
"display version", o.DisplayVersion,
"elapsed", elapsed,
"found new", len(r))
results = append(results, r...)

View File

@ -494,6 +494,15 @@ func TestScanVulnerabilities(t *testing.T) {
},
}, nil
}
ds.ListOperatingSystemsForPlatformFunc = func(ctx context.Context, platform string) ([]fleet.OperatingSystem, error) {
return []fleet.OperatingSystem{}, nil
}
ds.DeleteOutOfDateOSVulnerabilitiesFunc = func(ctx context.Context, src fleet.VulnerabilitySource, d time.Duration) error {
return nil
}
ds.ListCVEsFunc = func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMeta, error) {
published := time.Date(2022, time.October, 26, 14, 15, 0, 0, time.UTC)

View File

@ -3,14 +3,14 @@
"enroll_secret": "{{ .EnrollSecret }}",
"host_details": {
"os_version": {
"build":"22000",
"build":"22621",
"major":"10",
"minor":"0",
"name":"Microsoft Windows 11 Enterprise",
"patch":"",
"platform":"windows",
"platform_like":"windows",
"version":"10.0.22000"
"version":"10.0.22621.2715"
},
"osquery_info": {
"build_distro": "10",
@ -70,11 +70,11 @@
[
{
"name":"Microsoft Windows 11 Enterprise",
"version":"10.0.22000",
"version":"10.0.22621.2715",
"major":"10",
"minor":"0",
"patch":"",
"build":"22000",
"build":"22621",
"platform":"windows",
"platform_like":"windows",
"codename":"Microsoft Windows 11 Enterprise",
@ -87,7 +87,8 @@
[
{
"name":"Microsoft Windows 11 Enterprise",
"version":"10.0.22000"
"display_version":"22H2",
"version": "10.0.22621.2715"
}
]
{{- end }}
@ -95,10 +96,11 @@
[
{
"name":"Microsoft Windows 11 Enterprise",
"display_version":"22H2",
"platform":"windows",
"arch":"x86_64",
"kernel_version":"10.0.22000.1098",
"version":"10.0.22000"
"kernel_version":"10.0.22621.2715",
"version":"10.0.22621"
}
]
{{- end }}

View File

@ -19,3 +19,8 @@ func (svc *Service) HostByIdentifier(ctx context.Context, identifier string, opt
opts.IncludePolicies = true
return svc.Service.HostByIdentifier(ctx, identifier, opts)
}
func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string, opts fleet.ListOptions, includeCVSS bool) (*fleet.OSVersions, int, *fleet.PaginationMetadata, error) {
// reuse OSVersions, but include premium options
return svc.Service.OSVersions(ctx, teamID, platform, name, version, opts, true)
}

View File

@ -487,7 +487,6 @@ var hostRefs = []string{
"host_display_names",
"windows_updates",
"host_disks",
"operating_system_vulnerabilities",
"host_updates",
"host_disk_encryption_keys",
"host_software_installed_paths",

View File

@ -3917,7 +3917,6 @@ func testTeamHostsExpiration(t *testing.T, ds *Datastore) {
assert.Len(t, deleted, 0)
_ = listHostsCheckCount(t, ds, filter, fleet.HostListOptions{}, 5)
}
func testHostsIncludesScheduledQueriesInPackStats(t *testing.T, ds *Datastore) {
@ -6151,6 +6150,7 @@ func testOSVersions(t *testing.T, ds *Datastore) {
osVersions, err = ds.OSVersions(ctx, nil, nil, nil, nil)
require.NoError(t, err)
expected = []fleet.OSVersion{
{HostsCount: 1, Name: "CentOS 8.0.0", NameOnly: "CentOS", Version: "8.0.0", Platform: "rhel"},
{HostsCount: 2, Name: "Ubuntu 20.4.0", NameOnly: "Ubuntu", Version: "20.4.0", Platform: "ubuntu"},
@ -6310,13 +6310,6 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
// Operating system vulnerabilities
_, err = ds.writer(context.Background()).Exec(
`INSERT INTO operating_system_vulnerabilities(host_id,operating_system_id,cve) VALUES (?,?,?)`,
host.ID, 1, "cve-1",
)
require.NoError(t, err)
_, err = ds.writer(context.Background()).Exec(`INSERT INTO host_software_installed_paths (host_id, software_id, installed_path) VALUES (?, ?, ?)`, host.ID, 1, "some_path")
require.NoError(t, err)

View File

@ -0,0 +1,27 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20231224070653, Down_20231224070653)
}
func Up_20231224070653(tx *sql.Tx) error {
_, err := tx.Exec(`
ALTER TABLE operating_system_vulnerabilities
ADD COLUMN resolved_in_version VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
ADD COLUMN updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
`)
if err != nil {
return fmt.Errorf("adding operating_system_vulnerabilities columns: %w", err)
}
return nil
}
func Down_20231224070653(tx *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,81 @@
package tables
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUp_20231224070653(t *testing.T) {
db := applyUpToPrev(t)
insertStmt := `
INSERT INTO operating_system_vulnerabilities
(host_id, operating_system_id, cve, source)
VALUES (?, ?, ?, ?)
`
_, err := db.Exec(insertStmt, 1, 1, "cve-1", 0)
require.NoError(t, err)
// Apply current migration.
applyNext(t, db)
selectStmt := `
SELECT host_id, operating_system_id, cve, source, resolved_in_version, updated_at, created_at
FROM operating_system_vulnerabilities
WHERE operating_system_id = ?
`
var osv struct {
HostID uint `db:"host_id"`
OperatingSystemID uint `db:"operating_system_id"`
CVE string `db:"cve"`
Source int `db:"source"`
ResolvedIn *string `db:"resolved_in_version"`
UpdatedAt string `db:"updated_at"`
CreatedAt string `db:"created_at"`
}
err = db.Get(&osv, selectStmt, 1)
require.NoError(t, err)
require.Equal(t, uint(1), osv.HostID)
require.Equal(t, uint(1), osv.OperatingSystemID)
require.Equal(t, "cve-1", osv.CVE)
require.Equal(t, 0, osv.Source)
require.Nil(t, osv.ResolvedIn)
require.NotEmpty(t, osv.UpdatedAt)
// Insert a new row.
newInsertStmt := `
INSERT INTO operating_system_vulnerabilities
(host_id, operating_system_id, cve, source, resolved_in_version)
VALUES (?, ?, ?, ?, ?)
`
_, err = db.Exec(newInsertStmt, 2, 2, "cve-2", 0, "1.2.3")
require.NoError(t, err)
err = db.Get(&osv, selectStmt, 2)
require.NoError(t, err)
require.Equal(t, uint(2), osv.HostID)
require.Equal(t, uint(2), osv.OperatingSystemID)
require.Equal(t, "cve-2", osv.CVE)
require.Equal(t, 0, osv.Source)
require.Equal(t, "1.2.3", *osv.ResolvedIn)
require.NotEmpty(t, osv.UpdatedAt)
updateStmt := `
UPDATE operating_system_vulnerabilities
SET resolved_in_version = ?
WHERE operating_system_id = ? AND cve = ? AND host_id = ?
`
_, err = db.Exec(updateStmt, "1.2.4", 2, "cve-2", 2)
require.NoError(t, err)
err = db.Get(&osv, selectStmt, 2)
require.NoError(t, err)
require.Equal(t, uint(2), osv.HostID)
require.Equal(t, uint(2), osv.OperatingSystemID)
require.Equal(t, "cve-2", osv.CVE)
require.Equal(t, 0, osv.Source)
require.Equal(t, "1.2.4", *osv.ResolvedIn)
require.NotEmpty(t, osv.UpdatedAt)
}

View File

@ -0,0 +1,42 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20240110134315, Down_20240110134315)
}
func Up_20240110134315(tx *sql.Tx) error {
addColumnStmt := `
ALTER TABLE operating_systems
ADD COLUMN display_version VARCHAR(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '';
`
if _, err := tx.Exec(addColumnStmt); err != nil {
return fmt.Errorf("adding operating_systems column: %w", err)
}
dropIndexStmt := `
ALTER TABLE operating_systems
DROP INDEX idx_unique_os;
`
if _, err := tx.Exec(dropIndexStmt); err != nil {
return fmt.Errorf("dropping operating_systems index: %w", err)
}
addIndexStmt := `
ALTER TABLE operating_systems
ADD UNIQUE INDEX idx_unique_os (name, version, arch, kernel_version, platform, display_version);
`
if _, err := tx.Exec(addIndexStmt); err != nil {
return fmt.Errorf("adding operating_systems index: %w", err)
}
return nil
}
func Down_20240110134315(tx *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,51 @@
package tables
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUp_20240110134315(t *testing.T) {
db := applyUpToPrev(t)
// Insert into OS table
insertStmt := `
INSERT INTO operating_systems (
name, version, arch, kernel_version, platform
)
VALUES (?, ?, ?, ?, ?)
`
_, err := db.Exec(insertStmt, "Windows", "10.0.19042", "x86_64", "10.0.19042.2482", "windows")
require.NoError(t, err)
applyNext(t, db)
// Check that the new column exists
var displayVersion string
err = db.Get(&displayVersion, "SELECT display_version FROM operating_systems LIMIT 1")
require.NoError(t, err)
require.Empty(t, displayVersion)
// Test unique constraint includes display_version
insertStmt1 := `
INSERT INTO operating_systems (
name, version, arch, kernel_version, platform, display_version
)
VALUES (?, ?, ?, ?, ?, ?)
`
// New record with display_version is not a duplicate
_, err = db.Exec(insertStmt1, "Windows", "10.0.19042", "x86_64", "10.0.19042.2482", "windows", "22H2")
require.NoError(t, err)
// Unique constraint error when display_version is empty
_, err = db.Exec(insertStmt1, "Windows", "10.0.19042", "x86_64", "10.0.19042.2482", "windows", "")
require.Error(t, err)
require.Contains(t, err.Error(), "Duplicate entry")
// Unique constraint violation when display_version is not NULL
_, err = db.Exec(insertStmt1, "Windows", "10.0.19042", "x86_64", "10.0.19042.2482", "windows", "22H2")
require.Error(t, err)
require.Contains(t, err.Error(), "Duplicate entry")
}

View File

@ -0,0 +1,59 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20240119091637, Down_20240119091637)
}
func Up_20240119091637(tx *sql.Tx) error {
// operating_system_vulnerabilities is not previously used
// truncating table is safe
stmt := `
TRUNCATE TABLE operating_system_vulnerabilities
`
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("truncating operating_system_vulnerabilities: %w", err)
}
stmt = `
ALTER TABLE operating_system_vulnerabilities
DROP INDEX idx_operating_system_vulnerabilities_unq_cve
`
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("dropping index idx_operating_system_vulnerabilities_unq_cve: %w", err)
}
stmt = `
ALTER TABLE operating_system_vulnerabilities
DROP INDEX idx_operating_system_vulnerabilities_operating_system_id_cve
`
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("dropping index idx_operating_system_vulnerabilities_operating_system_id_cve: %w", err)
}
stmt = `
ALTER TABLE operating_system_vulnerabilities
DROP COLUMN host_id
`
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("dropping host_id column from operating_system_vulnerabilities: %w", err)
}
stmt = `
ALTER TABLE operating_system_vulnerabilities
ADD UNIQUE INDEX idx_os_vulnerabilities_unq_os_id_cve (operating_system_id, cve)
`
if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("adding index idx_operating_system_vulnerabilities_unq_cve: %w", err)
}
return nil
}
func Down_20240119091637(tx *sql.Tx) error {
return nil
}

View File

@ -0,0 +1,45 @@
package tables
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUp_20240119091637(t *testing.T) {
db := applyUpToPrev(t)
stmt := `
INSERT INTO operating_system_vulnerabilities (host_id, operating_system_id, cve, source, resolved_in_version)
VALUES (1, 1, 'cve-1', 0, '1.0.0')
`
_, err := db.Exec(stmt)
require.NoError(t, err)
// Apply current migration.
applyNext(t, db)
// ensure table is truncated
stmt = `
SELECT COUNT(*) FROM operating_system_vulnerabilities
`
var count int
err = db.QueryRow(stmt).Scan(&count)
require.NoError(t, err)
require.Equal(t, 0, count)
// check new unique index
stmt = `
INSERT INTO operating_system_vulnerabilities (operating_system_id, cve, source, resolved_in_version)
VALUES (1, 'cve-1', 0, '1.0.0')
`
_, err = db.Exec(stmt)
require.NoError(t, err)
stmt = `
INSERT INTO operating_system_vulnerabilities (operating_system_id, cve, source, resolved_in_version)
VALUES (1, 'cve-1', 0, '1.0.0')
`
_, err = db.Exec(stmt)
require.Error(t, err)
}

View File

@ -4,31 +4,65 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/doug-martin/goqu/v9"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
)
func (ds *Datastore) ListOSVulnerabilities(ctx context.Context, hostIDs []uint) ([]fleet.OSVulnerability, error) {
func (ds *Datastore) ListOSVulnerabilitiesByOS(ctx context.Context, osID uint) ([]fleet.OSVulnerability, error) {
r := []fleet.OSVulnerability{}
stmt := dialect.
From(goqu.T("operating_system_vulnerabilities")).
Select(
goqu.I("host_id"),
goqu.I("operating_system_id"),
goqu.I("cve"),
).
Where(goqu.C("host_id").In(hostIDs))
stmt := `
SELECT
operating_system_id,
cve,
resolved_in_version
FROM operating_system_vulnerabilities
WHERE operating_system_id = ?
`
sql, args, err := stmt.ToSQL()
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "error generating SQL statement")
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &r, stmt, osID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "error executing SQL statement")
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &r, sql, args...); err != nil {
return r, nil
}
func (ds *Datastore) ListVulnsByOsNameAndVersion(ctx context.Context, name, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
r := fleet.Vulnerabilities{}
var sqlstmt string
if includeCVSS == true {
sqlstmt = `
SELECT DISTINCT
osv.cve,
cm.cvss_score,
cm.epss_probability,
cm.cisa_known_exploit,
cm.published as cve_published,
cm.description,
osv.resolved_in_version
FROM operating_system_vulnerabilities osv
LEFT JOIN cve_meta cm ON cm.cve = osv.cve
WHERE osv.operating_system_id IN (
SELECT id FROM operating_systems WHERE name = ? AND version = ?
)
`
} else {
sqlstmt = `
SELECT DISTINCT
osv.cve
FROM operating_system_vulnerabilities osv
WHERE osv.operating_system_id IN (
SELECT id FROM operating_systems WHERE name = ? AND version = ?
)
`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &r, sqlstmt, name, version); err != nil {
return nil, ctxerr.Wrap(ctx, err, "error executing SQL statement")
}
@ -43,10 +77,10 @@ func (ds *Datastore) InsertOSVulnerabilities(ctx context.Context, vulnerabilitie
}
values := strings.TrimSuffix(strings.Repeat("(?,?,?,?),", len(vulnerabilities)), ",")
sql := fmt.Sprintf(`INSERT IGNORE INTO operating_system_vulnerabilities (host_id, operating_system_id, cve, source) VALUES %s`, values)
sql := fmt.Sprintf(`INSERT IGNORE INTO operating_system_vulnerabilities (operating_system_id, cve, source, resolved_in_version) VALUES %s`, values)
for _, v := range vulnerabilities {
args = append(args, v.HostID, v.OSID, v.CVE, source)
args = append(args, v.OSID, v.CVE, source, v.ResolvedInVersion)
}
res, err := ds.writer(ctx).ExecContext(ctx, sql, args...)
if err != nil {
@ -57,22 +91,65 @@ func (ds *Datastore) InsertOSVulnerabilities(ctx context.Context, vulnerabilitie
return count, nil
}
func (ds *Datastore) InsertOSVulnerability(ctx context.Context, v fleet.OSVulnerability, s fleet.VulnerabilitySource) (bool, error) {
if v.CVE == "" {
return false, fmt.Errorf("inserting operating system vulnerability: CVE cannot be empty %#v", v)
}
var args []interface{}
// statement assumes a unique index on (host_id, cve)
sqlStmt := `
INSERT INTO operating_system_vulnerabilities (
operating_system_id,
cve,
source,
resolved_in_version
) VALUES (?,?,?,?)
ON DUPLICATE KEY UPDATE
operating_system_id = VALUES(operating_system_id),
source = VALUES(source),
resolved_in_version = VALUES(resolved_in_version),
updated_at = ?
`
args = append(args, v.OSID, v.CVE, s, v.ResolvedInVersion, time.Now().UTC())
res, err := ds.writer(ctx).ExecContext(ctx, sqlStmt, args...)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "insert operating system vulnerability")
}
return insertOnDuplicateDidInsert(res), nil
}
func (ds *Datastore) DeleteOSVulnerabilities(ctx context.Context, vulnerabilities []fleet.OSVulnerability) error {
if len(vulnerabilities) == 0 {
return nil
}
sql := fmt.Sprintf(
`DELETE FROM operating_system_vulnerabilities WHERE (host_id, cve) IN (%s)`,
`DELETE FROM operating_system_vulnerabilities WHERE (operating_system_id, cve) IN (%s)`,
strings.TrimSuffix(strings.Repeat("(?,?),", len(vulnerabilities)), ","),
)
var args []interface{}
for _, v := range vulnerabilities {
args = append(args, v.HostID, v.CVE)
args = append(args, v.OSID, v.CVE)
}
if _, err := ds.writer(ctx).ExecContext(ctx, sql, args...); err != nil {
return ctxerr.Wrapf(ctx, err, "deleting operating system vulnerabilities")
}
return nil
}
func (ds *Datastore) DeleteOutOfDateOSVulnerabilities(ctx context.Context, src fleet.VulnerabilitySource, d time.Duration) error {
deleteStmt := `
DELETE FROM operating_system_vulnerabilities
WHERE source = ? AND updated_at < ?
`
if _, err := ds.writer(ctx).ExecContext(ctx, deleteStmt, src, time.Now().UTC().Add(-d)); err != nil {
return ctxerr.Wrap(ctx, err, "deleting out of date operating system vulnerabilities")
}
return nil
}

View File

@ -3,8 +3,10 @@ package mysql
import (
"context"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/require"
)
@ -15,11 +17,14 @@ func TestOperatingSystemVulnerabilities(t *testing.T) {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"ListOSVulnerabilitiesEmpty", testListOSVulnerabilitiesEmpty},
{"ListOSVulnerabilities", testListOSVulnerabilities},
{"ListOSVulnerabilitiesEmpty", testListOSVulnerabilitiesByOSEmpty},
{"ListOSVulnerabilities", testListOSVulnerabilitiesByOS},
{"ListVulnssByOsNameAndVersion", testListVulnsByOsNameAndVersion},
{"InsertOSVulnerabilities", testInsertOSVulnerabilities},
{"InsertSingleOSVulnerability", testInsertOSVulnerability},
{"DeleteOSVulnerabilitiesEmpty", testDeleteOSVulnerabilitiesEmpty},
{"DeleteOSVulnerabilities", testDeleteOSVulnerabilities},
{"DeleteOutOfDateOSVulnerabilities", testDeleteOutOfDateOSVulnerabilities},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@ -29,80 +34,218 @@ func TestOperatingSystemVulnerabilities(t *testing.T) {
}
}
func testListOSVulnerabilitiesEmpty(t *testing.T, ds *Datastore) {
func testListOSVulnerabilitiesByOSEmpty(t *testing.T, ds *Datastore) {
ctx := context.Background()
actual, err := ds.ListOSVulnerabilities(ctx, []uint{4})
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
require.NoError(t, err)
require.Empty(t, actual)
}
func testListOSVulnerabilities(t *testing.T, ds *Datastore) {
func testListOSVulnerabilitiesByOS(t *testing.T, ds *Datastore) {
ctx := context.Background()
vulns := []fleet.OSVulnerability{
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-3", OSID: 1},
{HostID: 2, CVE: "cve-2", OSID: 1},
{CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.3")},
{CVE: "cve-3", OSID: 1, ResolvedInVersion: ptr.String("10.14.2")},
{CVE: "cve-2", OSID: 1, ResolvedInVersion: ptr.String("8.123.1")},
{CVE: "cve-1", OSID: 2, ResolvedInVersion: ptr.String("1.2.3")},
{CVE: "cve-5", OSID: 2, ResolvedInVersion: ptr.String("10.14.2")},
}
for _, v := range vulns {
_, err := ds.writer(ctx).Exec(
`INSERT INTO operating_system_vulnerabilities(host_id,operating_system_id,cve) VALUES (?,?,?)`,
v.HostID, v.OSID, v.CVE,
)
_, err := ds.InsertOSVulnerability(ctx, v, fleet.MSRCSource)
require.NoError(t, err)
}
t.Run("none matching", func(t *testing.T) {
actual, err := ds.ListOSVulnerabilities(ctx, []uint{3})
require.NoError(t, err)
require.Empty(t, actual)
})
t.Run("returns matching", func(t *testing.T) {
expected := []fleet.OSVulnerability{
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-3", OSID: 1},
{CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.3")},
{CVE: "cve-3", OSID: 1, ResolvedInVersion: ptr.String("10.14.2")},
{CVE: "cve-2", OSID: 1, ResolvedInVersion: ptr.String("8.123.1")},
}
actual, err := ds.ListOSVulnerabilities(ctx, []uint{1})
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
require.NoError(t, err)
require.ElementsMatch(t, expected, actual)
})
}
func testListVulnsByOsNameAndVersion(t *testing.T, ds *Datastore) {
ctx := context.Background()
seedOS := []fleet.OperatingSystem{
{
Name: "Microsoft Windows 11 Pro 21H2",
Version: "10.0.22000.795",
Arch: "64-bit",
KernelVersion: "10.0.22000.795",
Platform: "windows",
DisplayVersion: "21H2",
},
{
Name: "Microsoft Windows 11 Pro 21H2",
Version: "10.0.22000.795",
Arch: "ARM 64-bit",
KernelVersion: "10.0.22000.795",
Platform: "windows",
DisplayVersion: "21H2",
},
{
Name: "Microsoft Windows 11 Pro 22H2",
Version: "10.0.22621.890",
Arch: "64-bit",
KernelVersion: "10.0.22621.890",
Platform: "windows",
DisplayVersion: "22H2",
},
}
dbOS := []fleet.OperatingSystem{}
for _, seed := range seedOS {
os, err := newOperatingSystemDB(context.Background(), ds.writer(context.Background()), seed)
require.NoError(t, err)
dbOS = append(dbOS, *os)
}
cves, err := ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", false)
require.NoError(t, err)
require.Empty(t, cves)
mockTime := time.Date(2024, time.January, 18, 10, 0, 0, 0, time.UTC)
cveMeta := []fleet.CVEMeta{
{
CVE: "CVE-2021-1234",
CVSSScore: ptr.Float64(9.7),
EPSSProbability: ptr.Float64(4.2),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(mockTime),
Description: "A bad vulnerability",
},
{
CVE: "CVE-2021-1235",
CVSSScore: ptr.Float64(9.8),
EPSSProbability: ptr.Float64(0.1),
CISAKnownExploit: ptr.Bool(false),
Published: ptr.Time(mockTime),
Description: "A worse vulnerability",
},
{
CVE: "CVE-2021-1236",
CVSSScore: ptr.Float64(9.8),
EPSSProbability: ptr.Float64(0.1),
CISAKnownExploit: ptr.Bool(false),
Published: ptr.Time(mockTime),
Description: "A terrible vulnerability",
},
}
err = ds.InsertCVEMeta(ctx, cveMeta)
require.NoError(t, err)
// add CVEs for each OS with different architectures
vulns := []fleet.OSVulnerability{
{CVE: "CVE-2021-1234", OSID: dbOS[0].ID, ResolvedInVersion: ptr.String("1.2.3")},
{CVE: "CVE-2021-1234", OSID: dbOS[1].ID, ResolvedInVersion: ptr.String("1.2.3")}, // same OS, different arch
{CVE: "CVE-2021-1235", OSID: dbOS[1].ID, ResolvedInVersion: ptr.String("10.14.2")},
{CVE: "CVE-2021-1236", OSID: dbOS[2].ID, ResolvedInVersion: ptr.String("103.2.1")},
}
_, err = ds.InsertOSVulnerabilities(ctx, vulns, fleet.MSRCSource)
require.NoError(t, err)
// test without CVS meta
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", false)
require.NoError(t, err)
expected := fleet.Vulnerabilities{
{CVE: "CVE-2021-1234"},
{CVE: "CVE-2021-1235"},
}
require.Len(t, cves, 2)
require.ElementsMatch(t, expected, cves)
// test with CVS meta
cves, err = ds.ListVulnsByOsNameAndVersion(ctx, "Microsoft Windows 11 Pro 21H2", "10.0.22000.795", true)
require.NoError(t, err)
require.Len(t, cves, 2)
require.Equal(t, cveMeta[0].CVE, cves[0].CVE)
require.Equal(t, &cveMeta[0].CVSSScore, cves[0].CVSSScore)
require.Equal(t, &cveMeta[0].EPSSProbability, cves[0].EPSSProbability)
require.Equal(t, &cveMeta[0].CISAKnownExploit, cves[0].CISAKnownExploit)
require.Equal(t, cveMeta[0].Published, *cves[0].CVEPublished)
require.Equal(t, cveMeta[0].Description, **cves[0].Description)
require.Equal(t, cveMeta[1].CVE, cves[1].CVE)
require.Equal(t, &cveMeta[1].CVSSScore, cves[1].CVSSScore)
require.Equal(t, &cveMeta[1].EPSSProbability, cves[1].EPSSProbability)
require.Equal(t, &cveMeta[1].CISAKnownExploit, cves[1].CISAKnownExploit)
require.Equal(t, cveMeta[1].Published, *cves[1].CVEPublished)
require.Equal(t, cveMeta[1].Description, **cves[1].Description)
}
func testInsertOSVulnerabilities(t *testing.T, ds *Datastore) {
ctx := context.Background()
vulns := []fleet.OSVulnerability{
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-3", OSID: 1},
{HostID: 2, CVE: "cve-2", OSID: 1},
{CVE: "cve-1", OSID: 1},
{CVE: "cve-3", OSID: 1},
{CVE: "cve-2", OSID: 1},
}
c, err := ds.InsertOSVulnerabilities(ctx, vulns, fleet.MSRCSource)
require.NoError(t, err)
require.Equal(t, int64(3), c)
expected := []fleet.OSVulnerability{
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-3", OSID: 1},
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
require.NoError(t, err)
require.ElementsMatch(t, vulns, actual)
}
func testInsertOSVulnerability(t *testing.T, ds *Datastore) {
ctx := context.Background()
vulns := fleet.OSVulnerability{
CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.3"),
}
actual, err := ds.ListOSVulnerabilities(ctx, []uint{1})
vulnsUpdate := fleet.OSVulnerability{
CVE: "cve-1", OSID: 1, ResolvedInVersion: ptr.String("1.2.4"),
}
vulnNoCVE := fleet.OSVulnerability{
OSID: 1, ResolvedInVersion: ptr.String("1.2.4"),
}
// Inserting a vulnerability with no CVE should not insert anything
didInsert, err := ds.InsertOSVulnerability(ctx, vulnNoCVE, fleet.MSRCSource)
require.Error(t, err)
require.False(t, didInsert)
// Inserting a vulnerability with a CVE should insert
didInsert, err = ds.InsertOSVulnerability(ctx, vulns, fleet.MSRCSource)
require.NoError(t, err)
require.ElementsMatch(t, expected, actual)
require.True(t, didInsert)
// Inserting the same vulnerability should not insert
didInsert, err = ds.InsertOSVulnerability(ctx, vulnsUpdate, fleet.MSRCSource)
require.NoError(t, err)
require.Equal(t, false, didInsert)
list1, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
require.NoError(t, err)
require.Len(t, list1, 1)
require.Equal(t, vulnsUpdate, list1[0])
}
func testDeleteOSVulnerabilitiesEmpty(t *testing.T, ds *Datastore) {
ctx := context.Background()
vulns := []fleet.OSVulnerability{
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-3", OSID: 1},
{HostID: 2, CVE: "cve-2", OSID: 1},
{CVE: "cve-1", OSID: 1},
{CVE: "cve-1", OSID: 1},
{CVE: "cve-3", OSID: 1},
{CVE: "cve-2", OSID: 1},
}
err := ds.DeleteOSVulnerabilities(ctx, vulns)
@ -113,10 +256,9 @@ func testDeleteOSVulnerabilities(t *testing.T, ds *Datastore) {
ctx := context.Background()
vulns := []fleet.OSVulnerability{
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-3", OSID: 1},
{HostID: 2, CVE: "cve-2", OSID: 1},
{CVE: "cve-1", OSID: 1},
{CVE: "cve-2", OSID: 1},
{CVE: "cve-3", OSID: 1},
}
c, err := ds.InsertOSVulnerabilities(ctx, vulns, fleet.MSRCSource)
@ -124,20 +266,47 @@ func testDeleteOSVulnerabilities(t *testing.T, ds *Datastore) {
require.Equal(t, int64(3), c)
toDelete := []fleet.OSVulnerability{
{HostID: 2, CVE: "cve-2", OSID: 1},
{CVE: "cve-2", OSID: 1},
}
err = ds.DeleteOSVulnerabilities(ctx, toDelete)
require.NoError(t, err)
actual, err := ds.ListOSVulnerabilities(ctx, []uint{1})
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
require.NoError(t, err)
require.ElementsMatch(t, []fleet.OSVulnerability{
{HostID: 1, CVE: "cve-1", OSID: 1},
{HostID: 1, CVE: "cve-3", OSID: 1},
{CVE: "cve-1", OSID: 1},
{CVE: "cve-3", OSID: 1},
}, actual)
actual, err = ds.ListOSVulnerabilities(ctx, []uint{2})
require.NoError(t, err)
require.Empty(t, actual)
}
func testDeleteOutOfDateOSVulnerabilities(t *testing.T, ds *Datastore) {
ctx := context.Background()
yesterday := time.Now().Add(-3 * time.Hour).Format("2006-01-02 15:04:05")
oldVuln := fleet.OSVulnerability{
CVE: "cve-1", OSID: 1,
}
newVuln := fleet.OSVulnerability{
CVE: "cve-2", OSID: 1,
}
_, err := ds.InsertOSVulnerability(ctx, oldVuln, fleet.NVDSource)
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx, "UPDATE operating_system_vulnerabilities SET updated_at = ?", yesterday)
require.NoError(t, err)
_, err = ds.InsertOSVulnerability(ctx, newVuln, fleet.NVDSource)
require.NoError(t, err)
// Delete out of date vulns
err = ds.DeleteOutOfDateOSVulnerabilities(ctx, fleet.NVDSource, 2*time.Hour)
require.NoError(t, err)
actual, err := ds.ListOSVulnerabilitiesByOS(ctx, 1)
require.NoError(t, err)
require.Len(t, actual, 1)
require.ElementsMatch(t, []fleet.OSVulnerability{newVuln}, actual)
}

View File

@ -16,12 +16,25 @@ func (ds *Datastore) ListOperatingSystems(ctx context.Context) ([]fleet.Operatin
func listOperatingSystemsDB(ctx context.Context, tx sqlx.QueryerContext) ([]fleet.OperatingSystem, error) {
var os []fleet.OperatingSystem
if err := sqlx.SelectContext(ctx, tx, &os, `SELECT id, name, version, arch, kernel_version FROM operating_systems`); err != nil {
if err := sqlx.SelectContext(ctx, tx, &os, `SELECT id, name, version, arch, kernel_version, platform, display_version FROM operating_systems`); err != nil {
return nil, err
}
return os, nil
}
func (ds *Datastore) ListOperatingSystemsForPlatform(ctx context.Context, platform string) ([]fleet.OperatingSystem, error) {
var oses []fleet.OperatingSystem
sqlStatement := `
SELECT id, name, version, arch, kernel_version, platform, display_version
FROM operating_systems
WHERE platform = ?
`
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &oses, sqlStatement, platform); err != nil {
return nil, err
}
return oses, nil
}
func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error {
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
os, err := getOrGenerateOperatingSystemDB(ctx, tx, hostOS)
@ -51,8 +64,8 @@ func getOrGenerateOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hos
// `newOperatingSystemDB` inserts a record for the given operating system and
// returns the record including the newly associated ID.
func newOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostOS fleet.OperatingSystem) (*fleet.OperatingSystem, error) {
stmt := "INSERT IGNORE INTO operating_systems (name, version, arch, kernel_version, platform) VALUES (?, ?, ?, ?, ?)"
if _, err := tx.ExecContext(ctx, stmt, hostOS.Name, hostOS.Version, hostOS.Arch, hostOS.KernelVersion, hostOS.Platform); err != nil {
stmt := "INSERT IGNORE INTO operating_systems (name, version, arch, kernel_version, platform, display_version) VALUES (?, ?, ?, ?, ?, ?)"
if _, err := tx.ExecContext(ctx, stmt, hostOS.Name, hostOS.Version, hostOS.Arch, hostOS.KernelVersion, hostOS.Platform, hostOS.DisplayVersion); err != nil {
return nil, ctxerr.Wrap(ctx, err, "insert new operating system")
}
@ -73,8 +86,8 @@ func newOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostOS fleet.
// If found, it returns the record including the associated ID.
func getOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostOS fleet.OperatingSystem) (*fleet.OperatingSystem, error) {
var os fleet.OperatingSystem
stmt := "SELECT id, name, version, arch, kernel_version FROM operating_systems WHERE name = ? AND version = ? AND arch = ? AND kernel_version = ?"
if err := sqlx.GetContext(ctx, tx, &os, stmt, hostOS.Name, hostOS.Version, hostOS.Arch, hostOS.KernelVersion); err != nil {
stmt := "SELECT id, name, version, arch, kernel_version, platform, display_version FROM operating_systems WHERE name = ? AND version = ? AND arch = ? AND kernel_version = ? AND platform = ? AND display_version = ?"
if err := sqlx.GetContext(ctx, tx, &os, stmt, hostOS.Name, hostOS.Version, hostOS.Arch, hostOS.KernelVersion, hostOS.Platform, hostOS.DisplayVersion); err != nil {
return nil, err
}
return &os, nil
@ -118,7 +131,7 @@ func getIDHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID
// of the `host_operating_system` table.
func getHostOperatingSystemDB(ctx context.Context, tx sqlx.ExtContext, hostID uint) (*fleet.OperatingSystem, error) {
var os fleet.OperatingSystem
stmt := "SELECT id, name, version, arch, kernel_version FROM operating_systems WHERE id = (SELECT os_id FROM host_operating_system WHERE host_id = ?)"
stmt := "SELECT id, name, version, arch, kernel_version, platform, display_version FROM operating_systems WHERE id = (SELECT os_id FROM host_operating_system WHERE host_id = ?)"
if err := sqlx.GetContext(ctx, tx, &os, stmt, hostID); err != nil {
return nil, err
}

View File

@ -38,6 +38,28 @@ func TestListOperatingSystems(t *testing.T) {
}
}
func TestListOperatingSystemsForPlatform(t *testing.T) {
ctx := context.Background()
ds := CreateMySQLDS(t)
// no os records
list, err := ds.ListOperatingSystemsForPlatform(ctx, "windows")
require.NoError(t, err)
require.Len(t, list, 0)
// with os records
seedByID := seedOperatingSystems(t, ds)
list, err = ds.ListOperatingSystemsForPlatform(ctx, "windows")
require.NoError(t, err)
require.Len(t, list, 1)
require.Equal(t, seedByID[list[0].ID], list[0])
// OS does not exist
list, err = ds.ListOperatingSystemsForPlatform(ctx, "foo")
require.NoError(t, err)
require.Len(t, list, 0)
}
func TestUpdateHostOperatingSystem(t *testing.T) {
ctx := context.Background()
ds := CreateMySQLDS(t)
@ -380,11 +402,12 @@ func TestCleanupHostOperatingSystems(t *testing.T) {
func seedOperatingSystems(t *testing.T, ds *Datastore) map[uint]fleet.OperatingSystem {
osSeeds := []fleet.OperatingSystem{
{
Name: "Microsoft Windows 11 Enterprise Evaluation",
Version: "21H2",
Arch: "64-bit",
KernelVersion: "10.0.22000.795",
Platform: "windows",
Name: "Microsoft Windows 11 Enterprise Evaluation",
Version: "10.0.22000.795",
Arch: "64-bit",
KernelVersion: "10.0.22000.795",
Platform: "windows",
DisplayVersion: "21H2",
},
{
Name: "macOS",
@ -426,5 +449,5 @@ func seedOperatingSystems(t *testing.T, ds *Datastore) map[uint]fleet.OperatingS
}
func isSameOS(t *testing.T, os1 fleet.OperatingSystem, os2 fleet.OperatingSystem) bool {
return assert.ElementsMatch(t, []string{os1.Name, os1.Version, os1.Arch, os1.KernelVersion}, []string{os2.Name, os2.Version, os2.Arch, os2.KernelVersion})
return assert.ElementsMatch(t, []string{os1.Name, os1.Version, os1.Arch, os1.KernelVersion, os1.Platform}, []string{os2.Name, os2.Version, os2.Arch, os2.KernelVersion, os2.Platform})
}

File diff suppressed because one or more lines are too long

View File

@ -511,6 +511,9 @@ type Datastore interface {
// ListOperationsSystems returns all operating systems (id, name, version)
ListOperatingSystems(ctx context.Context) ([]OperatingSystem, error)
// ListOperatingSystemsForPlatform returns all operating systems for the given platform.
// Supported values for platform are: "darwin" and "windows"
ListOperatingSystemsForPlatform(ctx context.Context, platform string) ([]OperatingSystem, error)
// UpdateHostOperatingSystem updates the `host_operating_system` table
// for the given host ID with the ID of the operating system associated
// with the given name, version, arch, and kernel version in the
@ -822,9 +825,17 @@ type Datastore interface {
///////////////////////////////////////////////////////////////////////////////
// OperatingSystemVulnerabilities Store
ListOSVulnerabilities(ctx context.Context, hostID []uint) ([]OSVulnerability, error)
ListOSVulnerabilitiesByOS(ctx context.Context, osID uint) ([]OSVulnerability, error)
ListVulnsByOsNameAndVersion(ctx context.Context, name, version string, includeCVSS bool) (Vulnerabilities, error)
InsertOSVulnerabilities(ctx context.Context, vulnerabilities []OSVulnerability, source VulnerabilitySource) (int64, error)
DeleteOSVulnerabilities(ctx context.Context, vulnerabilities []OSVulnerability) error
// InsertOSVulnerability will either insert a new vulnerability in the datastore (in which
// case it will return true) or if a matching record already exists it will update its
// updated_at timestamp (in which case it will return false).
InsertOSVulnerability(ctx context.Context, vuln OSVulnerability, source VulnerabilitySource) (bool, error)
// DeleteOutOfDateVulnerabilities deletes 'operating_system_vulnerabilities' entries from the provided source where
// the updated_at timestamp is older than the provided duration
DeleteOutOfDateOSVulnerabilities(ctx context.Context, source VulnerabilitySource, duration time.Duration) error
///////////////////////////////////////////////////////////////////////////////
// Apple MDM

View File

@ -1118,6 +1118,8 @@ type OSVersions struct {
}
type OSVersion struct {
// ID is the unique id of the operating system.
ID uint `json:"id,omitempty"`
// HostsCount is the number of hosts that have reported the operating system.
HostsCount int `json:"hosts_count"`
// Name is the name and alphanumeric version of the operating system. e.g., "Microsoft Windows 11 Enterprise",
@ -1130,8 +1132,12 @@ type OSVersion struct {
Version string `json:"version"`
// Platform is the platform of the operating system, e.g., "windows", "ubuntu", or "darwin".
Platform string `json:"platform"`
// ID is the unique id of the operating system.
ID uint `json:"os_id,omitempty"`
// GeneratedCPE is the Common Platform Enumeration (CPE) name for the operating system.
// It is currently only generated for Operating Systems scanned for vulnerabilities
// in NVD (macOS only)
GeneratedCPEs []string `json:"generated_cpes,omitempty"`
// Vulnerabilities are the vulnerabilities associated with the operating system.
Vulnerabilities Vulnerabilities `json:"vulnerabilities"`
}
type HostDetailOptions struct {

View File

@ -7,14 +7,18 @@ type OperatingSystem struct {
ID uint `json:"id" db:"id"`
// Name is the name of the operating system, e.g., "Debian/GNU Linus", "Ubuntu", or "Microsoft Windows 11 Enterprise"
Name string `json:"name" db:"name"`
// Version is the version of the operating system, e.g., "10.0.0", "22.04 LTS", "21H2"
// Version is the version of the operating system, e.g., "14.1.2"(macOS), "22.04 LTS"(Ubuntu)
// On Windows, this is the build number, which will always match KernelVersion e.g., "10.0.19042.1348"
Version string `json:"version" db:"version"`
// Arch is the architecture of the operating system, e.g., "x86_64" or "64-bit"
Arch string `json:"arch,omitempty" db:"arch"`
// KernelVersion is the kernel version of the operating system, e.g., "5.10.76-linuxkit" or "10.0.22000.795"
// KernelVersion is the kernel version of the operating system, e.g., "5.10.76-linuxkit"
// On Windows, this is the build number, which will always match Version e.g., "10.0.19042.1348"
KernelVersion string `json:"kernel_version,omitempty" db:"kernel_version"`
// Platform is the platform of the operating system, e.g., "darwin" or "rhel"
Platform string `json:"platform" db:"platform"`
// DisplayVersion is the display version of a Windows operating system, e.g. "22H2"
DisplayVersion string `json:"display_version" db:"display_version"`
}
// IsWindows returns whether the OperatingSystem record references a Windows OS

View File

@ -384,7 +384,7 @@ type Service interface {
// OSVersions returns a list of operating systems and associated host counts, which may be
// filtered using the following optional criteria: team id, platform, or name and version.
// Name cannot be used without version, and conversely, version cannot be used without name.
OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*OSVersions, error)
OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string, opts ListOptions, includeCVSS bool) (*OSVersions, int, *PaginationMetadata, error)
// /////////////////////////////////////////////////////////////////////////////
// AppConfigService provides methods for configuring the Fleet application

View File

@ -77,21 +77,24 @@ func (sv SoftwareVulnerability) Affected() uint {
// OSVulnerability is a vulnerability on a OS.
// Represents an entry in the `os_vulnerabilities` table.
type OSVulnerability struct {
OSID uint `db:"operating_system_id"`
HostID uint `db:"host_id"`
CVE string `db:"cve"`
OSID uint `db:"operating_system_id"`
CVE string `db:"cve"`
// Source is the source of the vulnerability.
Source VulnerabilitySource `db:"source"`
// ResolvedInVersion is the version of the OS that resolves the vulnerability.
ResolvedInVersion *string `db:"resolved_in_version"`
}
// String implements fmt.Stringer.
func (ov OSVulnerability) String() string {
return fmt.Sprintf("{%d,%d,%s}", ov.OSID, ov.HostID, ov.CVE)
return fmt.Sprintf("{%d,%s}", ov.OSID, ov.CVE)
}
// Key returns a string representation of the os vulnerability.
// If we have a list of os vulnerabilities, the Key can be used
// as a discrimator for unique entries.
func (ov OSVulnerability) Key() string {
return fmt.Sprintf("os:%d:%d:%s", ov.OSID, ov.HostID, ov.CVE)
return fmt.Sprintf("os:%d:%s", ov.OSID, ov.CVE)
}
func (ov OSVulnerability) GetCVE() string {
@ -99,7 +102,7 @@ func (ov OSVulnerability) GetCVE() string {
}
func (ov OSVulnerability) Affected() uint {
return ov.HostID
return ov.OSID
}
// Represents a vulnerability, e.g. an OS or a Software vulnerability.

View File

@ -380,6 +380,8 @@ type ListCVEsFunc func(ctx context.Context, maxAge time.Duration) ([]fleet.CVEMe
type ListOperatingSystemsFunc func(ctx context.Context) ([]fleet.OperatingSystem, error)
type ListOperatingSystemsForPlatformFunc func(ctx context.Context, platform string) ([]fleet.OperatingSystem, error)
type UpdateHostOperatingSystemFunc func(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error
type CleanupHostOperatingSystemsFunc func(ctx context.Context) error
@ -564,12 +566,18 @@ type ListWindowsUpdatesByHostIDFunc func(ctx context.Context, hostID uint) ([]fl
type InsertWindowsUpdatesFunc func(ctx context.Context, hostID uint, updates []fleet.WindowsUpdate) error
type ListOSVulnerabilitiesFunc func(ctx context.Context, hostID []uint) ([]fleet.OSVulnerability, error)
type ListOSVulnerabilitiesByOSFunc func(ctx context.Context, osID uint) ([]fleet.OSVulnerability, error)
type ListVulnsByOsNameAndVersionFunc func(ctx context.Context, name string, version string, includeCVSS bool) (fleet.Vulnerabilities, error)
type InsertOSVulnerabilitiesFunc func(ctx context.Context, vulnerabilities []fleet.OSVulnerability, source fleet.VulnerabilitySource) (int64, error)
type DeleteOSVulnerabilitiesFunc func(ctx context.Context, vulnerabilities []fleet.OSVulnerability) error
type InsertOSVulnerabilityFunc func(ctx context.Context, vuln fleet.OSVulnerability, source fleet.VulnerabilitySource) (bool, error)
type DeleteOutOfDateOSVulnerabilitiesFunc func(ctx context.Context, source fleet.VulnerabilitySource, duration time.Duration) error
type NewMDMAppleConfigProfileFunc func(ctx context.Context, p fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error)
type BulkUpsertMDMAppleConfigProfilesFunc func(ctx context.Context, payload []*fleet.MDMAppleConfigProfile) error
@ -1332,6 +1340,9 @@ type DataStore struct {
ListOperatingSystemsFunc ListOperatingSystemsFunc
ListOperatingSystemsFuncInvoked bool
ListOperatingSystemsForPlatformFunc ListOperatingSystemsForPlatformFunc
ListOperatingSystemsForPlatformFuncInvoked bool
UpdateHostOperatingSystemFunc UpdateHostOperatingSystemFunc
UpdateHostOperatingSystemFuncInvoked bool
@ -1608,8 +1619,11 @@ type DataStore struct {
InsertWindowsUpdatesFunc InsertWindowsUpdatesFunc
InsertWindowsUpdatesFuncInvoked bool
ListOSVulnerabilitiesFunc ListOSVulnerabilitiesFunc
ListOSVulnerabilitiesFuncInvoked bool
ListOSVulnerabilitiesByOSFunc ListOSVulnerabilitiesByOSFunc
ListOSVulnerabilitiesByOSFuncInvoked bool
ListVulnsByOsNameAndVersionFunc ListVulnsByOsNameAndVersionFunc
ListVulnsByOsNameAndVersionFuncInvoked bool
InsertOSVulnerabilitiesFunc InsertOSVulnerabilitiesFunc
InsertOSVulnerabilitiesFuncInvoked bool
@ -1617,6 +1631,12 @@ type DataStore struct {
DeleteOSVulnerabilitiesFunc DeleteOSVulnerabilitiesFunc
DeleteOSVulnerabilitiesFuncInvoked bool
InsertOSVulnerabilityFunc InsertOSVulnerabilityFunc
InsertOSVulnerabilityFuncInvoked bool
DeleteOutOfDateOSVulnerabilitiesFunc DeleteOutOfDateOSVulnerabilitiesFunc
DeleteOutOfDateOSVulnerabilitiesFuncInvoked bool
NewMDMAppleConfigProfileFunc NewMDMAppleConfigProfileFunc
NewMDMAppleConfigProfileFuncInvoked bool
@ -3214,6 +3234,13 @@ func (s *DataStore) ListOperatingSystems(ctx context.Context) ([]fleet.Operating
return s.ListOperatingSystemsFunc(ctx)
}
func (s *DataStore) ListOperatingSystemsForPlatform(ctx context.Context, platform string) ([]fleet.OperatingSystem, error) {
s.mu.Lock()
s.ListOperatingSystemsForPlatformFuncInvoked = true
s.mu.Unlock()
return s.ListOperatingSystemsForPlatformFunc(ctx, platform)
}
func (s *DataStore) UpdateHostOperatingSystem(ctx context.Context, hostID uint, hostOS fleet.OperatingSystem) error {
s.mu.Lock()
s.UpdateHostOperatingSystemFuncInvoked = true
@ -3858,11 +3885,18 @@ func (s *DataStore) InsertWindowsUpdates(ctx context.Context, hostID uint, updat
return s.InsertWindowsUpdatesFunc(ctx, hostID, updates)
}
func (s *DataStore) ListOSVulnerabilities(ctx context.Context, hostID []uint) ([]fleet.OSVulnerability, error) {
func (s *DataStore) ListOSVulnerabilitiesByOS(ctx context.Context, osID uint) ([]fleet.OSVulnerability, error) {
s.mu.Lock()
s.ListOSVulnerabilitiesFuncInvoked = true
s.ListOSVulnerabilitiesByOSFuncInvoked = true
s.mu.Unlock()
return s.ListOSVulnerabilitiesFunc(ctx, hostID)
return s.ListOSVulnerabilitiesByOSFunc(ctx, osID)
}
func (s *DataStore) ListVulnsByOsNameAndVersion(ctx context.Context, name string, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
s.mu.Lock()
s.ListVulnsByOsNameAndVersionFuncInvoked = true
s.mu.Unlock()
return s.ListVulnsByOsNameAndVersionFunc(ctx, name, version, includeCVSS)
}
func (s *DataStore) InsertOSVulnerabilities(ctx context.Context, vulnerabilities []fleet.OSVulnerability, source fleet.VulnerabilitySource) (int64, error) {
@ -3879,6 +3913,20 @@ func (s *DataStore) DeleteOSVulnerabilities(ctx context.Context, vulnerabilities
return s.DeleteOSVulnerabilitiesFunc(ctx, vulnerabilities)
}
func (s *DataStore) InsertOSVulnerability(ctx context.Context, vuln fleet.OSVulnerability, source fleet.VulnerabilitySource) (bool, error) {
s.mu.Lock()
s.InsertOSVulnerabilityFuncInvoked = true
s.mu.Unlock()
return s.InsertOSVulnerabilityFunc(ctx, vuln, source)
}
func (s *DataStore) DeleteOutOfDateOSVulnerabilities(ctx context.Context, source fleet.VulnerabilitySource, duration time.Duration) error {
s.mu.Lock()
s.DeleteOutOfDateOSVulnerabilitiesFuncInvoked = true
s.mu.Unlock()
return s.DeleteOutOfDateOSVulnerabilitiesFunc(ctx, source, duration)
}
func (s *DataStore) NewMDMAppleConfigProfile(ctx context.Context, p fleet.MDMAppleConfigProfile) (*fleet.MDMAppleConfigProfile, error) {
s.mu.Lock()
s.NewMDMAppleConfigProfileFuncInvoked = true

View File

@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
@ -1741,16 +1742,19 @@ func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}
type osVersionsRequest struct {
fleet.ListOptions
TeamID *uint `query:"team_id,optional"`
Platform *string `query:"platform,optional"`
Name *string `query:"os_name,optional"`
Version *string `query:"os_name,optional"`
Version *string `query:"os_version,optional"`
}
type osVersionsResponse struct {
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
OSVersions []fleet.OSVersion `json:"os_versions"`
Err error `json:"error,omitempty"`
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
Count int `json:"count"`
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
OSVersions []fleet.OSVersion `json:"os_versions"`
Err error `json:"error,omitempty"`
}
func (r osVersionsResponse) error() error { return r.Err }
@ -1758,7 +1762,7 @@ func (r osVersionsResponse) error() error { return r.Err }
func osVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*osVersionsRequest)
osVersions, err := svc.OSVersions(ctx, req.TeamID, req.Platform, req.Name, req.Version)
osVersions, count, metadata, err := svc.OSVersions(ctx, req.TeamID, req.Platform, req.Name, req.Version, req.ListOptions, false)
if err != nil {
return &osVersionsResponse{Err: err}, nil
}
@ -1766,20 +1770,27 @@ func osVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Serv
return &osVersionsResponse{
CountsUpdatedAt: &osVersions.CountsUpdatedAt,
OSVersions: osVersions.OSVersions,
Meta: metadata,
Count: count,
}, nil
}
func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error) {
func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *string, name *string, version *string, opts fleet.ListOptions, includeCVSS bool) (*fleet.OSVersions, int, *fleet.PaginationMetadata, error) {
var count int
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
return nil, err
return nil, count, nil, err
}
if name != nil && version == nil {
return nil, &fleet.BadRequestError{Message: "Cannot specify os_name without os_version"}
return nil, count, nil, &fleet.BadRequestError{Message: "Cannot specify os_name without os_version"}
}
if name == nil && version != nil {
return nil, &fleet.BadRequestError{Message: "Cannot specify os_version without os_name"}
return nil, count, nil, &fleet.BadRequestError{Message: "Cannot specify os_version without os_name"}
}
if opts.OrderKey != "" && opts.OrderKey != "hosts_count" {
return nil, count, nil, &fleet.BadRequestError{Message: "Invalid order key"}
}
osVersions, err := svc.ds.OSVersions(ctx, teamID, platform, name, version)
@ -1789,16 +1800,83 @@ func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *stri
// most of the time, team should exist so checking here saves unnecessary db calls
_, err := svc.ds.Team(ctx, *teamID)
if err != nil {
return nil, err
return nil, count, nil, err
}
}
// if team exists but stats have not yet been gathered, return empty JSON array
osVersions = &fleet.OSVersions{}
} else if err != nil {
return nil, err
return nil, count, nil, err
}
return osVersions, nil
for i, os := range osVersions.OSVersions {
// populate OSVersion.GeneratedCPEs
if os.Platform == "darwin" {
osVersions.OSVersions[i].GeneratedCPEs = []string{
fmt.Sprintf("cpe:2.3:o:apple:macos:%s:*:*:*:*:*:*:*", os.Version),
fmt.Sprintf("cpe:2.3:o:apple:mac_os_x:%s:*:*:*:*:*:*:*", os.Version),
}
}
// populate OSVersion.Vulnerabilities
vulns, err := svc.ds.ListVulnsByOsNameAndVersion(ctx, os.NameOnly, os.Version, includeCVSS)
if err != nil {
return nil, count, nil, err
}
osVersions.OSVersions[i].Vulnerabilities = make(fleet.Vulnerabilities, 0) // avoid null in JSON
for _, vuln := range vulns {
switch os.Platform {
case "darwin":
vuln.DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE)
case "windows":
vuln.DetailsLink = fmt.Sprintf("https://msrc.microsoft.com/update-guide/en-US/vulnerability/%s", vuln.CVE)
}
osVersions.OSVersions[i].Vulnerabilities = append(osVersions.OSVersions[i].Vulnerabilities, vuln)
}
}
if opts.OrderKey == "hosts_count" && opts.OrderDirection == fleet.OrderAscending {
sort.Slice(osVersions.OSVersions, func(i, j int) bool {
return osVersions.OSVersions[i].HostsCount < osVersions.OSVersions[j].HostsCount
})
} else {
sort.Slice(osVersions.OSVersions, func(i, j int) bool {
return osVersions.OSVersions[i].HostsCount > osVersions.OSVersions[j].HostsCount
})
}
count = len(osVersions.OSVersions)
var metaData *fleet.PaginationMetadata
osVersions.OSVersions, metaData = paginateOSVersions(osVersions.OSVersions, opts)
return osVersions, count, metaData, nil
}
func paginateOSVersions(slice []fleet.OSVersion, opts fleet.ListOptions) ([]fleet.OSVersion, *fleet.PaginationMetadata) {
metaData := &fleet.PaginationMetadata{
HasPreviousResults: opts.Page > 0,
}
if opts.PerPage == 0 {
return slice, metaData
}
start := opts.Page * opts.PerPage
if start >= uint(len(slice)) {
return []fleet.OSVersion{}, metaData
}
end := start + opts.PerPage
if end >= uint(len(slice)) {
end = uint(len(slice))
} else {
metaData.HasNextResults = true
}
return slice[start:end], metaData
}
////////////////////////////////////////////////////////////////////////////////

View File

@ -989,27 +989,112 @@ func TestEmptyTeamOSVersions(t *testing.T) {
return nil, newNotFoundError()
}
ds.ListVulnsByOsNameAndVersionFunc = func(ctx context.Context, name, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
return fleet.Vulnerabilities{}, nil
}
// team exists with stats
vers, err := svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(1), ptr.String("darwin"), nil, nil)
vers, _, _, err := svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(1), ptr.String("darwin"), nil, nil, fleet.ListOptions{}, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 1)
// team exists but no stats
vers, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(2), ptr.String("darwin"), nil, nil)
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(2), ptr.String("darwin"), nil, nil, fleet.ListOptions{}, false)
require.NoError(t, err)
assert.Empty(t, vers.OSVersions)
// team does not exist
_, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(3), ptr.String("darwin"), nil, nil)
_, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(3), ptr.String("darwin"), nil, nil, fleet.ListOptions{}, false)
require.Error(t, err)
require.Equal(t, "not found", fmt.Sprint(err))
// some unknown error
_, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(4), ptr.String("darwin"), nil, nil)
_, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), ptr.Uint(4), ptr.String("darwin"), nil, nil, fleet.ListOptions{}, false)
require.Error(t, err)
require.Equal(t, "some unknown error", fmt.Sprint(err))
}
func TestOSVersionsListOptions(t *testing.T) {
ds := new(mock.Store)
svc, ctx := newTestService(t, ds, nil, nil)
testVersions := []fleet.OSVersion{
{HostsCount: 4, NameOnly: "Windows 11 Pro 22H2", Platform: "windows"},
{HostsCount: 1, NameOnly: "macOS 12.1", Platform: "darwin"},
{HostsCount: 2, NameOnly: "macOS 12.2", Platform: "darwin"},
{HostsCount: 3, NameOnly: "Windows 11 Pro 21H2", Platform: "windows"},
{HostsCount: 5, NameOnly: "Ubuntu 20.04", Platform: "ubuntu"},
{HostsCount: 6, NameOnly: "Ubuntu 21.04", Platform: "ubuntu"},
}
ds.OSVersionsFunc = func(ctx context.Context, teamID *uint, platform *string, name *string, version *string) (*fleet.OSVersions, error) {
return &fleet.OSVersions{CountsUpdatedAt: time.Now(), OSVersions: testVersions}, nil
}
ds.ListVulnsByOsNameAndVersionFunc = func(ctx context.Context, name, version string, includeCVSS bool) (fleet.Vulnerabilities, error) {
return fleet.Vulnerabilities{}, nil
}
// test default descending count sort
opts := fleet.ListOptions{}
vers, _, _, err := svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 6)
assert.Equal(t, "Ubuntu 21.04", vers.OSVersions[0].NameOnly)
assert.Equal(t, "Ubuntu 20.04", vers.OSVersions[1].NameOnly)
assert.Equal(t, "Windows 11 Pro 22H2", vers.OSVersions[2].NameOnly)
assert.Equal(t, "Windows 11 Pro 21H2", vers.OSVersions[3].NameOnly)
assert.Equal(t, "macOS 12.2", vers.OSVersions[4].NameOnly)
assert.Equal(t, "macOS 12.1", vers.OSVersions[5].NameOnly)
// test ascending count sort
opts = fleet.ListOptions{OrderKey: "hosts_count", OrderDirection: fleet.OrderAscending}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 6)
assert.Equal(t, "macOS 12.1", vers.OSVersions[0].NameOnly)
assert.Equal(t, "macOS 12.2", vers.OSVersions[1].NameOnly)
assert.Equal(t, "Windows 11 Pro 21H2", vers.OSVersions[2].NameOnly)
assert.Equal(t, "Windows 11 Pro 22H2", vers.OSVersions[3].NameOnly)
assert.Equal(t, "Ubuntu 20.04", vers.OSVersions[4].NameOnly)
assert.Equal(t, "Ubuntu 21.04", vers.OSVersions[5].NameOnly)
// pagination
opts = fleet.ListOptions{Page: 0, PerPage: 2}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 2)
assert.Equal(t, "Ubuntu 21.04", vers.OSVersions[0].NameOnly)
assert.Equal(t, "Ubuntu 20.04", vers.OSVersions[1].NameOnly)
opts = fleet.ListOptions{Page: 1, PerPage: 2}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 2)
assert.Equal(t, "Windows 11 Pro 22H2", vers.OSVersions[0].NameOnly)
assert.Equal(t, "Windows 11 Pro 21H2", vers.OSVersions[1].NameOnly)
// pagination + ascending hosts_count sort
opts = fleet.ListOptions{Page: 0, PerPage: 2, OrderKey: "hosts_count", OrderDirection: fleet.OrderAscending}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 2)
assert.Equal(t, "macOS 12.1", vers.OSVersions[0].NameOnly)
assert.Equal(t, "macOS 12.2", vers.OSVersions[1].NameOnly)
// per page too high
opts = fleet.ListOptions{Page: 0, PerPage: 1000}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 6)
// Page number too high
opts = fleet.ListOptions{Page: 1000, PerPage: 2}
vers, _, _, err = svc.OSVersions(test.UserContext(ctx, test.UserAdmin), nil, nil, nil, nil, opts, false)
require.NoError(t, err)
assert.Len(t, vers.OSVersions, 0)
}
func TestHostEncryptionKey(t *testing.T) {
cases := []struct {
name string

View File

@ -3834,7 +3834,6 @@ func (s *integrationTestSuite) TestListHostsByLabel() {
hostsJson, _ := json.MarshalIndent(hostsResp, "", " ")
labelsJson, _ := json.MarshalIndent(labelsResp, "", " ")
assert.Equal(t, string(hostsJson), string(labelsJson))
}
func (s *integrationTestSuite) TestLabelSpecs() {
@ -7351,41 +7350,133 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() {
func (s *integrationTestSuite) TestOSVersions() {
t := s.T()
testOS := fleet.OperatingSystem{Name: "barOS", Version: "4.2", Arch: "64bit", KernelVersion: "13.37", Platform: "foo"}
testOSes := []fleet.OperatingSystem{
{Name: "macOS", Version: "14.1.2", Arch: "64bit", KernelVersion: "13.37", Platform: "darwin"},
{Name: "macOS", Version: "13.2.1", Arch: "64bit", KernelVersion: "18.12", Platform: "darwin"},
{Name: "macOS", Version: "13.2.1", Arch: "64bit", KernelVersion: "18.12", Platform: "darwin"},
{Name: "Windows 11 Pro 21H2", Version: "10.0.22000.1", Arch: "64bit", KernelVersion: "10.0.22000.1", Platform: "windows"},
{Name: "Windows 11 Pro 21H2", Version: "10.0.22000.1", Arch: "64bit", KernelVersion: "10.0.22000.1", Platform: "windows"},
{Name: "Windows 11 Pro 21H2", Version: "10.0.22000.1", Arch: "64bit", KernelVersion: "10.0.22000.1", Platform: "windows"},
{Name: "Windows 11 Pro 21H2", Version: "10.0.22000.2", Arch: "64bit", KernelVersion: "10.0.22000.2", Platform: "windows"},
{Name: "Windows 11 Pro 21H2", Version: "10.0.22000.2", Arch: "64bit", KernelVersion: "10.0.22000.2", Platform: "windows"},
{Name: "Windows 11 Pro 21H2", Version: "10.0.22000.2", Arch: "ARM64", KernelVersion: "10.0.22000.2", Platform: "windows"},
{Name: "Windows 11 Pro 21H2", Version: "10.0.22000.2", Arch: "ARM64", KernelVersion: "10.0.22000.2", Platform: "windows"},
}
hosts := s.createHosts(t)
var platforms []string
for _, os := range testOSes {
platforms = append(platforms, os.Platform)
}
hosts := s.createHosts(t, platforms...)
var resp listHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp)
require.Len(t, resp.Hosts, len(hosts))
// set operating system information on a host
require.NoError(t, s.ds.UpdateHostOperatingSystem(context.Background(), hosts[0].ID, testOS))
var osID uint
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 = ?`,
testOS.Name, testOS.Version, testOS.Arch, testOS.KernelVersion, testOS.Platform)
})
require.Greater(t, osID, uint(0))
for i, os := range testOSes {
require.NoError(t, s.ds.UpdateHostOperatingSystem(context.Background(), hosts[i].ID, os))
}
// get OS versions
osv, err := s.ds.ListOperatingSystems(context.Background())
require.NoError(t, err)
require.Len(t, osv, 6) // includes fooOS from another test
osvMap := make(map[string]fleet.OperatingSystem)
for _, os := range osv {
key := fmt.Sprintf("%s %s %s", os.Name, os.Version, os.Arch)
osvMap[key] = os
}
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_name", testOS.Name, "os_version", testOS.Version)
require.Len(t, resp.Hosts, 1)
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_name", testOSes[1].Name, "os_version", testOSes[1].Version)
require.Len(t, resp.Hosts, 2)
expected := resp.Hosts[0]
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_id", fmt.Sprintf("%d", osID))
require.Len(t, resp.Hosts, 1)
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_id", fmt.Sprintf("%d", osvMap["macOS 13.2.1 64bit"].ID))
require.Len(t, resp.Hosts, 2)
require.Equal(t, expected, resp.Hosts[0])
// generate aggregated stats
require.NoError(t, s.ds.UpdateOSVersions(context.Background()))
// insert Vuln for Win x64
_, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
OSID: osvMap["Windows 11 Pro 21H2 10.0.22000.2 64bit"].ID,
CVE: "CVE-2021-1234",
}, fleet.MSRCSource)
require.NoError(t, err)
// insert duplicate Vuln for Win ARM64
_, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
OSID: osvMap["Windows 11 Pro 21H2 10.0.22000.2 ARM64"].ID,
CVE: "CVE-2021-1234",
}, fleet.MSRCSource)
require.NoError(t, err)
// insert different Vuln for Win ARM64
_, err = s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
OSID: osvMap["Windows 11 Pro 21H2 10.0.22000.2 ARM64"].ID,
CVE: "CVE-2021-5678",
}, fleet.MSRCSource)
require.NoError(t, err)
var osVersionsResp osVersionsResponse
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusOK, &osVersionsResp)
require.Len(t, osVersionsResp.OSVersions, 1)
require.Equal(t, fleet.OSVersion{HostsCount: 1, Name: fmt.Sprintf("%s %s", testOS.Name, testOS.Version), NameOnly: testOS.Name, Version: testOS.Version, Platform: testOS.Platform}, osVersionsResp.OSVersions[0])
require.Len(t, osVersionsResp.OSVersions, 4) // different archs are grouped together
// Default sort is by hosts count, descending
require.Equal(t, fleet.OSVersion{
HostsCount: 4,
Name: "Windows 11 Pro 21H2 10.0.22000.2",
NameOnly: "Windows 11 Pro 21H2",
Version: "10.0.22000.2",
Platform: "windows",
Vulnerabilities: fleet.Vulnerabilities{
{
CVE: "CVE-2021-1234",
DetailsLink: "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234",
},
{
CVE: "CVE-2021-5678", // vulns are aggregated by OS name and version
DetailsLink: "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-5678",
},
},
}, osVersionsResp.OSVersions[0])
// name without version
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusBadRequest, &osVersionsResp, "os_name", "Windows 11 Pro 21H2")
// version without name
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusBadRequest, &osVersionsResp, "os_version", "10.0.22000.1")
// invalid order key
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusBadRequest, &osVersionsResp, "order_key", "nosuchkey")
// ascending order by hosts count
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusOK, &osVersionsResp, "order_key", "hosts_count", "order_direction", "asc")
require.Equal(t, 1, osVersionsResp.OSVersions[0].HostsCount)
require.Equal(t, "macOS 14.1.2", osVersionsResp.OSVersions[0].Name)
// test pagination
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusOK, &osVersionsResp, "page", "0", "per_page", "2")
require.Len(t, osVersionsResp.OSVersions, 2)
require.Equal(t, "Windows 11 Pro 21H2 10.0.22000.2", osVersionsResp.OSVersions[0].Name)
require.Equal(t, "Windows 11 Pro 21H2 10.0.22000.1", osVersionsResp.OSVersions[1].Name)
require.Equal(t, 4, osVersionsResp.Count)
require.True(t, osVersionsResp.Meta.HasNextResults)
require.False(t, osVersionsResp.Meta.HasPreviousResults)
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusOK, &osVersionsResp, "page", "1", "per_page", "2")
require.Len(t, osVersionsResp.OSVersions, 2)
require.Equal(t, "macOS 13.2.1", osVersionsResp.OSVersions[0].Name)
require.Equal(t, "macOS 14.1.2", osVersionsResp.OSVersions[1].Name)
require.Equal(t, 4, osVersionsResp.Count)
require.False(t, osVersionsResp.Meta.HasNextResults)
require.True(t, osVersionsResp.Meta.HasPreviousResults)
}
func (s *integrationTestSuite) TestPingEndpoints() {

View File

@ -3134,6 +3134,88 @@ func (s *integrationEnterpriseTestSuite) TestListHosts() {
}
}
func (s *integrationEnterpriseTestSuite) TestOSVersions() {
t := s.T()
testOS := fleet.OperatingSystem{Name: "Windows 11 Pro", Version: "10.0.22621.2861", Arch: "x86_64", KernelVersion: "10.0.22621.2861", Platform: "windows"}
hosts := s.createHosts(t)
var resp listHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp)
require.Len(t, resp.Hosts, len(hosts))
// set operating system information on a host
require.NoError(t, s.ds.UpdateHostOperatingSystem(context.Background(), hosts[0].ID, testOS))
var osID uint
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 = ?`,
testOS.Name, testOS.Version, testOS.Arch, testOS.KernelVersion, testOS.Platform)
})
require.Greater(t, osID, uint(0))
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_name", testOS.Name, "os_version", testOS.Version)
require.Len(t, resp.Hosts, 1)
expected := resp.Hosts[0]
resp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &resp, "os_id", fmt.Sprintf("%d", osID))
require.Len(t, resp.Hosts, 1)
require.Equal(t, expected, resp.Hosts[0])
// generate aggregated stats
require.NoError(t, s.ds.UpdateOSVersions(context.Background()))
// insert OS Vulns
_, err := s.ds.InsertOSVulnerability(context.Background(), fleet.OSVulnerability{
OSID: osID,
CVE: "CVE-2021-1234",
}, fleet.MSRCSource)
require.NoError(t, err)
// insert CVE MEta
vulnMeta := []fleet.CVEMeta{
{
CVE: "CVE-2021-1234",
CVSSScore: ptr.Float64(5.4),
EPSSProbability: ptr.Float64(0.5),
CISAKnownExploit: ptr.Bool(true),
Published: ptr.Time(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
Description: "a long description of the cve",
},
}
require.NoError(t, s.ds.InsertCVEMeta(context.Background(), vulnMeta))
var osVersionsResp osVersionsResponse
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusOK, &osVersionsResp)
require.Len(t, osVersionsResp.OSVersions, 1)
require.Equal(t, 1, osVersionsResp.OSVersions[0].HostsCount)
require.Equal(t, fmt.Sprintf("%s %s", testOS.Name, testOS.Version), osVersionsResp.OSVersions[0].Name)
require.Equal(t, testOS.Name, osVersionsResp.OSVersions[0].NameOnly)
require.Equal(t, testOS.Version, osVersionsResp.OSVersions[0].Version)
require.Equal(t, testOS.Platform, osVersionsResp.OSVersions[0].Platform)
require.Len(t, osVersionsResp.OSVersions[0].Vulnerabilities, 1)
require.Equal(t, "CVE-2021-1234", osVersionsResp.OSVersions[0].Vulnerabilities[0].CVE)
require.Equal(t, "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-1234", osVersionsResp.OSVersions[0].Vulnerabilities[0].DetailsLink)
require.Equal(t, *vulnMeta[0].CVSSScore, **osVersionsResp.OSVersions[0].Vulnerabilities[0].CVSSScore)
require.Equal(t, *vulnMeta[0].EPSSProbability, **osVersionsResp.OSVersions[0].Vulnerabilities[0].EPSSProbability)
require.Equal(t, *vulnMeta[0].CISAKnownExploit, **osVersionsResp.OSVersions[0].Vulnerabilities[0].CISAKnownExploit)
require.Equal(t, *vulnMeta[0].Published, **osVersionsResp.OSVersions[0].Vulnerabilities[0].CVEPublished)
require.Equal(t, vulnMeta[0].Description, **osVersionsResp.OSVersions[0].Vulnerabilities[0].Description)
// return empty json if UpdateOSVersions cron hasn't run yet for new team
team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "new team"})
require.NoError(t, err)
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), &team.ID, []uint{hosts[0].ID}))
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusOK, &osVersionsResp, "team_id", fmt.Sprintf("%d", team.ID))
require.Len(t, osVersionsResp.OSVersions, 0)
// return err if team_id is invalid
s.DoJSON("GET", "/api/latest/fleet/os_versions", nil, http.StatusBadRequest, &osVersionsResp, "team_id", "invalid")
}
func (s *integrationEnterpriseTestSuite) TestMDMNotConfiguredEndpoints() {
t := s.T()
@ -6374,3 +6456,26 @@ func checkWindowsOSUpdatesProfile(t *testing.T, ds *mysql.Datastore, teamID *uin
return prof.ProfileUUID
}
func (s *integrationEnterpriseTestSuite) createHosts(t *testing.T, platforms ...string) []*fleet.Host {
var hosts []*fleet.Host
if len(platforms) == 0 {
platforms = []string{"debian", "rhel", "linux", "windows", "darwin"}
}
for i, platform := range platforms {
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-time.Duration(i) * time.Minute),
OsqueryHostID: ptr.String(fmt.Sprintf("%s%d", t.Name(), i)),
NodeKey: ptr.String(fmt.Sprintf("%s%d", t.Name(), i)),
UUID: uuid.New().String(),
Hostname: fmt.Sprintf("%sfoo.local%d", t.Name(), i),
Platform: platform,
})
require.NoError(t, err)
hosts = append(hosts, host)
}
return hosts
}

View File

@ -173,11 +173,13 @@ var hostDetailQueries = map[string]DetailQuery{
},
"os_version_windows": {
Query: `
SELECT
os.name,
os.version
FROM
os_version os`,
SELECT os.name, r.data as display_version, k.version
FROM
registry r,
os_version os,
kernel_info k
WHERE r.path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'
`,
Platforms: []string{"windows"},
IngestFunc: func(ctx context.Context, logger log.Logger, host *fleet.Host, rows []map[string]string) error {
if len(rows) != 1 {
@ -186,17 +188,11 @@ var hostDetailQueries = map[string]DetailQuery{
return nil
}
version := rows[0]["version"]
if version == "" {
level.Debug(logger).Log(
"msg", "unable to identify windows version",
"host", host.Hostname,
)
}
s := fmt.Sprintf("%v %v", rows[0]["name"], version)
s := fmt.Sprintf("%s %s", rows[0]["name"], rows[0]["display_version"])
// Shorten "Microsoft Windows" to "Windows" to facilitate display and sorting in UI
s = strings.Replace(s, "Microsoft Windows", "Windows", 1)
s = strings.TrimSpace(s)
s += " " + rows[0]["version"]
host.OSVersion = s
return nil
@ -526,10 +522,14 @@ var extraDetailQueries = map[string]DetailQuery{
os.platform,
os.arch,
k.version as kernel_version,
os.version
os.version,
r.data as display_version
FROM
os_version os,
kernel_info k`,
kernel_info k,
registry r
WHERE
r.path = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\DisplayVersion'`,
Platforms: []string{"windows"},
DirectIngestFunc: directIngestOSWindows,
},
@ -1008,16 +1008,14 @@ func directIngestOSWindows(ctx context.Context, logger log.Logger, host *fleet.H
Arch: rows[0]["arch"],
KernelVersion: rows[0]["kernel_version"],
Platform: rows[0]["platform"],
Version: rows[0]["kernel_version"],
}
version := rows[0]["version"]
if version == "" {
level.Debug(logger).Log(
"msg", "unable to identify windows version",
"host", host.Hostname,
)
displayVersion := rows[0]["display_version"]
if displayVersion != "" {
hostOS.Name += " " + displayVersion
hostOS.DisplayVersion = displayVersion
}
hostOS.Version = version
if err := ds.UpdateHostOperatingSystem(ctx, host.ID, hostOS); err != nil {
return ctxerr.Wrap(ctx, err, "directIngestOSWindows update host operating system")

View File

@ -421,7 +421,7 @@ func TestDetailQueriesOSVersionWindows(t *testing.T) {
))
assert.NoError(t, ingest(context.Background(), log.NewNopLogger(), &host, rows))
assert.Equal(t, "Windows 11 Enterprise 10.0.22000", host.OSVersion)
assert.Equal(t, "Windows 11 Enterprise 21H2 10.0.22000", host.OSVersion)
require.NoError(t, json.Unmarshal([]byte(`
[{
@ -852,13 +852,26 @@ func TestDirectIngestOSWindows(t *testing.T) {
}{
{
expected: fleet.OperatingSystem{
Name: "Microsoft Windows 11 Enterprise",
Version: "21H2",
Arch: "64-bit",
KernelVersion: "10.0.22000.795",
Name: "Microsoft Windows 11 Enterprise 21H2",
Version: "10.0.22000.795",
Arch: "64-bit",
KernelVersion: "10.0.22000.795",
DisplayVersion: "21H2",
},
data: []map[string]string{
{"name": "Microsoft Windows 11 Enterprise", "version": "21H2", "release_id": "", "arch": "64-bit", "kernel_version": "10.0.22000.795"},
{"name": "Microsoft Windows 11 Enterprise", "display_version": "21H2", "version": "10.0.22000.795", "release_id": "", "arch": "64-bit", "kernel_version": "10.0.22000.795"},
},
},
{
expected: fleet.OperatingSystem{
Name: "Microsoft Windows 10 Enterprise", // no display_version
Version: "10.0.17763.2183",
Arch: "64-bit",
KernelVersion: "10.0.17763.2183",
DisplayVersion: "",
},
data: []map[string]string{
{"name": "Microsoft Windows 10 Enterprise", "display_version": "", "version": "10.0.17763", "release_id": "1809", "arch": "64-bit", "kernel_version": "10.0.17763.2183"},
},
},
}

View File

@ -2,8 +2,13 @@ package msrc
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/io"
msrc "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/parsed"
@ -11,8 +16,7 @@ import (
)
const (
hostsBatchSize = 500
vulnBatchSize = 500
vulnBatchSize = 500
)
func Analyze(
@ -28,12 +32,13 @@ func Analyze(
}
// Find matching products inside the bulletin
osProduct := msrc.NewProductFromOS(os)
matchingPIDs := make(map[string]bool)
for pID, p := range bulletin.Products {
if p.Matches(osProduct) {
matchingPIDs[pID] = true
}
pID, err := bulletin.Products.GetMatchForOS(ctx, os)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "Analyzing MSRC vulnerabilities")
}
if pID != "" {
matchingPIDs[pID] = true
}
if len(matchingPIDs) == 0 {
@ -43,62 +48,39 @@ func Analyze(
toInsert := make(map[string]fleet.OSVulnerability)
toDelete := make(map[string]fleet.OSVulnerability)
var offset int
for {
hIDs, err := ds.HostIDsByOSID(ctx, os.ID, offset, hostsBatchSize)
if err != nil {
return nil, err
}
// Run vulnerability detection for all hosts in this batch (hIDs)
// and store the results in 'found'.
var found []fleet.OSVulnerability
if len(hIDs) == 0 {
break
for cve, v := range bulletin.Vulnerabities {
// Check if this vulnerability targets the OS
if !utils.ProductIDsIntersect(v.ProductIDs, matchingPIDs) {
continue
}
offset += len(hIDs)
// Run vulnerability detection for all hosts in this batch (hIDs)
// and store the results in 'found'.
found := make(map[uint][]fleet.OSVulnerability, len(hIDs))
for _, hID := range hIDs {
updates, err := ds.ListWindowsUpdatesByHostID(ctx, hID)
if err != nil {
return nil, err
}
var vs []fleet.OSVulnerability
for cve, v := range bulletin.Vulnerabities {
// Check if this vulnerability targets the OS
if !utils.ProductIDsIntersect(v.ProductIDs, matchingPIDs) {
continue
}
if patched(os, bulletin, v, matchingPIDs, updates) {
continue
}
vs = append(vs, fleet.OSVulnerability{OSID: os.ID, HostID: hID, CVE: cve})
}
found[hID] = vs
// Check if the vulnerability is patched by referencing the OS version number
if patched(os, bulletin, v, matchingPIDs) {
continue
}
found = append(found, fleet.OSVulnerability{OSID: os.ID, CVE: cve})
}
// Fetch all stored vulnerabilities for the current batch
osVulns, err := ds.ListOSVulnerabilities(ctx, hIDs)
if err != nil {
return nil, err
}
existing := make(map[uint][]fleet.OSVulnerability)
for _, osv := range osVulns {
existing[osv.HostID] = append(existing[osv.HostID], osv)
}
// Fetch all stored vulnerabilities for the current batch
osVulns, err := ds.ListOSVulnerabilitiesByOS(ctx, os.ID)
if err != nil {
return nil, err
}
var existing []fleet.OSVulnerability
for _, osv := range osVulns {
existing = append(existing, osv)
}
// Compute what needs to be inserted/deleted for this batch
for _, hID := range hIDs {
insrt, del := utils.VulnsDelta(found[hID], existing[hID])
for _, i := range insrt {
toInsert[i.Key()] = i
}
for _, d := range del {
toDelete[d.Key()] = d
}
}
// Compute what needs to be inserted/deleted for this batch
insrt, del := utils.VulnsDelta(found, existing)
for _, i := range insrt {
toInsert[i.Key()] = i
}
for _, d := range del {
toDelete[d.Key()] = d
}
err = utils.BatchProcess(toDelete, func(v []fleet.OSVulnerability) error {
@ -139,16 +121,7 @@ func patched(
b *msrc.SecurityBulletin,
v msrc.Vulnerability,
matchingPIDs map[string]bool,
updates []fleet.WindowsUpdate,
) bool {
// check if any update directly remediates the vulnerability,
// this will be much faster than walking the forest of vendor fixes.
for _, u := range updates {
if v.RemediatedBy[u.KBID] {
return true
}
}
for KBID := range v.RemediatedBy {
fix := b.VendorFixes[KBID]
@ -157,16 +130,19 @@ func patched(
continue
}
// Check if the kernel build already contains the fix
if utils.Rpmvercmp(os.KernelVersion, fix.FixedBuild) >= 0 {
return true
// An empty FixBuild is a bug in the MSRC feed, last
// seen around apr-2021. Ignoring it to avoid false
// positive vulnerabilities.
if fix.FixedBuild == "" {
continue
}
// If not, walk the forest
for _, u := range updates {
if b.KBIDsConnected(KBID, u.KBID) {
return true
}
isGreater, err := winBuildVersionGreaterOrEqual(fix.FixedBuild, os.KernelVersion)
if err != nil {
continue
}
if isGreater {
return true
}
}
@ -185,3 +161,45 @@ func loadBulletin(os fleet.OperatingSystem, dir string) (*msrc.SecurityBulletin,
return msrc.UnmarshalBulletin(latest)
}
func winBuildVersionGreaterOrEqual(feed, os string) (bool, error) {
if feed == "" {
return false, errors.New("empty feed version")
}
feedBuild, feedParts, err := getBuildNumber(feed)
if err != nil {
return false, fmt.Errorf("invalid feed version: %w", err)
}
osBuild, osParts, err := getBuildNumber(os)
if err != nil {
return false, fmt.Errorf("invalid os version: %w", err)
}
for i := 0; i < 3; i++ {
if feedParts[i] != osParts[i] {
return false, fmt.Errorf("comparing different product versions: %s, %s", feed, os)
}
}
return osBuild >= feedBuild, nil
}
func getBuildNumber(version string) (int, []string, error) {
if version == "" {
return 0, nil, fmt.Errorf("empty version string %s", version)
}
parts := strings.Split(version, ".")
if len(parts) != 4 {
return 0, nil, fmt.Errorf("parts count mismatch %s", version)
}
build, err := strconv.Atoi(parts[3])
if err != nil {
return 0, nil, fmt.Errorf("unable to parse build number %s", version)
}
return build, parts, nil
}

View File

@ -16,39 +16,24 @@ import (
func TestAnalyzer(t *testing.T) {
op := fleet.OperatingSystem{
Name: "Microsoft Windows 11 Enterprise Evaluation",
Version: "21H2",
Arch: "64-bit",
KernelVersion: "10.0.22000.795",
Platform: "windows",
Name: "Microsoft Windows 11 Enterprise Evaluation",
DisplayVersion: "21H2",
Version: "10.0.22000.795",
Arch: "64-bit",
KernelVersion: "10.0.22000.795",
Platform: "windows",
}
prod := parsed.NewProductFromOS(op)
t.Run("#patched", func(t *testing.T) {
// TODO: this tests a nil vulnerability, which should never happen
// maybe return an error instead?
t.Run("no updates", func(t *testing.T) {
b := parsed.NewSecurityBulletin(prod.Name())
b.Products["123"] = prod
b.Vulnerabities["cve-123"] = parsed.NewVulnerability(nil)
pIDs := map[string]bool{"123": true}
require.False(t, patched(op, b, b.Vulnerabities["cve-123"], pIDs, nil))
})
t.Run("directly remediated", func(t *testing.T) {
b := parsed.NewSecurityBulletin(prod.Name())
b.Products["123"] = prod
vuln := parsed.NewVulnerability(nil)
vuln.RemediatedBy[123] = true
b.Vulnerabities["cve-123"] = vuln
pIDs := map[string]bool{"123": true}
updates := []fleet.WindowsUpdate{
{KBID: 123},
{KBID: 456},
}
require.True(t, patched(op, b, b.Vulnerabities["cve-123"], pIDs, updates))
require.False(t, patched(op, b, b.Vulnerabities["cve-123"], pIDs))
})
t.Run("remediated by build", func(t *testing.T) {
@ -65,37 +50,7 @@ func TestAnalyzer(t *testing.T) {
vfA.ProductIDs["123"] = true
b.VendorFixes[456] = vfA
updates := []fleet.WindowsUpdate{
{KBID: 789},
}
require.True(t, patched(op, b, b.Vulnerabities["cve-123"], pIDs, updates))
})
t.Run("remediated by a cumulative update", func(t *testing.T) {
b := parsed.NewSecurityBulletin(prod.Name())
b.Products["123"] = prod
pIDs := map[string]bool{"123": true}
vuln := parsed.NewVulnerability(nil)
vuln.RemediatedBy[456] = true
b.Vulnerabities["cve-123"] = vuln
vfA := parsed.NewVendorFix("10.0.22000.796")
vfA.Supersedes = ptr.Uint(123)
vfA.ProductIDs["123"] = true
b.VendorFixes[456] = vfA
vfB := parsed.NewVendorFix("10.0.22000.796")
vfB.Supersedes = ptr.Uint(456)
vfB.ProductIDs["123"] = true
b.VendorFixes[789] = vfA
updates := []fleet.WindowsUpdate{
{KBID: 789},
}
require.True(t, patched(op, b, b.Vulnerabities["cve-123"], pIDs, updates))
require.True(t, patched(op, b, b.Vulnerabities["cve-123"], pIDs))
})
})
@ -128,3 +83,83 @@ func TestAnalyzer(t *testing.T) {
})
})
}
func TestWinBuildVersionGreaterOrEqual(t *testing.T) {
tc := []struct {
name string
feed string
os string
result bool
errMessage string
}{
{
name: "equal",
feed: "10.0.22000.795",
os: "10.0.22000.795",
result: true,
errMessage: "",
},
{
name: "greater",
feed: "10.0.22000.795",
os: "10.0.22000.796",
result: true,
errMessage: "",
},
{
name: "less",
feed: "10.0.22000.795",
os: "10.0.22000.794",
result: false,
errMessage: "",
},
{
name: "too many parts in feed version",
feed: "10.0.22000.795.9999",
os: "10.0.22000.794",
result: false,
errMessage: "invalid feed version",
},
{
name: "too many parts in os version",
feed: "10.0.22000.795",
os: "10.0.22000.794.9999",
result: false,
errMessage: "invalid os version",
},
{
name: "too few parts in feed version",
feed: "10.0.22000",
os: "10.0.22000.794",
result: false,
errMessage: "invalid feed version",
},
{
name: "empty feed version",
feed: "",
os: "10.0.22000.794",
result: false,
errMessage: "empty feed version",
},
{
name: "comparing different product versions",
feed: "10.0.22000.795",
os: "10.0.22621.795",
result: false,
errMessage: "comparing different product versions",
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
result, err := winBuildVersionGreaterOrEqual(c.feed, c.os)
require.Equal(t, c.result, result)
if c.errMessage != "" {
require.Error(t, err)
require.ErrorContains(t, err, c.errMessage)
} else {
require.NoError(t, err)
}
})
}
}

View File

@ -1,9 +1,12 @@
package parsed
import (
"context"
"errors"
"fmt"
"strings"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
)
@ -12,12 +15,55 @@ import (
// (if any) and its version (if any).
type Product string
type Products map[string]Product
var ErrNoMatch = errors.New("no product matches")
func (p Products) GetMatchForOS(ctx context.Context, os fleet.OperatingSystem) (string, error) {
var dvMatch, noDvMatch string
for pID, product := range p {
normalizedOS := NewProductFromOS(os)
if product.Name() != normalizedOS.Name() {
continue
}
archMatch := product.Arch() == "all" || normalizedOS.Arch() == "all" || product.Arch() == normalizedOS.Arch()
if !archMatch {
continue
}
if product.HasDisplayVersion() && os.DisplayVersion != "" && strings.Index(string(product), os.DisplayVersion) != -1 {
dvMatch = pID
break
}
// Ensure a match against an unknown or blank os.DisplayVersion to
// a MSRC product that does not have a display version (eg. The initial release
// of Windows 11 is 21H2, which does not appear in the MSRC data)
if !product.HasDisplayVersion() {
noDvMatch = pID
continue
}
}
if dvMatch == "" && noDvMatch == "" {
return "", ctxerr.Wrap(ctx, ErrNoMatch)
}
if dvMatch == "" {
return noDvMatch, nil
}
return dvMatch, nil
}
func NewProductFromFullName(fullName string) Product {
return Product(fullName)
}
func NewProductFromOS(os fleet.OperatingSystem) Product {
return Product(fmt.Sprintf("%s %s for %s", os.Name, os.Version, os.Arch))
return Product(fmt.Sprintf("%s for %s", os.Name, os.Arch))
}
// Arch returns the archicture for the current Microsoft product, if none can
@ -28,6 +74,9 @@ func NewProductFromOS(os fleet.OperatingSystem) Product {
func (p Product) Arch() string {
val := string(p)
switch {
case strings.Index(val, "ARM 64-bit") != -1 ||
strings.Index(val, "ARM64") != -1:
return "arm64"
case strings.Index(val, "x64") != -1 ||
strings.Index(val, "64-bit") != -1 ||
strings.Index(val, "x86_64") != -1:
@ -35,8 +84,6 @@ func (p Product) Arch() string {
case strings.Index(val, "32-bit") != -1 ||
strings.Index(val, "x86") != -1:
return "32-bit"
case strings.Index(val, "ARM64") != -1:
return "arm64"
case strings.Index(val, "Itanium-Based") != -1:
return "itanium"
default:
@ -44,6 +91,22 @@ func (p Product) Arch() string {
}
}
// HasDisplayVersion returns true if the current Microsoft product
// has a display version in the name.
// Display Version refers to the version of the product that is
// displayed to the user: eg. 22H2
// Year/Half refers to the year and half of the year that the product
// was released: eg. 2nd Half of 2022
func (p Product) HasDisplayVersion() bool {
keywords := []string{"version", "edition"}
for _, k := range keywords {
if strings.Index(strings.ToLower(string(p)), k) != -1 {
return true
}
}
return false
}
// Name returns the name for the current Microsoft product, if none can
// be found then "" is returned.
// eg:

View File

@ -1,6 +1,7 @@
package parsed
import (
"context"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
@ -431,3 +432,115 @@ func TestFullProductName(t *testing.T) {
}
})
}
func TestProductHasDisplayVersion(t *testing.T) {
tc := []struct {
name Product
result bool
}{
{
name: "Windows 11 for x64-based Systems",
result: false,
},
{
name: "Windows 11 Version 22H2 for x64-based Systems",
result: true,
},
{
name: "Windows Server 2022, 23H2 Edition (Server Core installation)",
result: true,
},
{
name: "Windows Server 2022 (Server Core installation)",
result: false,
},
{
name: "Windows Server 2022",
result: false,
},
{
name: "Windows Server, version 1803 (Server Core Installation)",
result: true,
},
}
for _, tt := range tc {
require.Equal(t, tt.result, tt.name.HasDisplayVersion(), tt.name)
}
}
var msrcWinProducts = Products{
"11926": "Windows 11 for x64-based Systems",
"11927": "Windows 11 for ARM64-based Systems",
"12085": "Windows 11 Version 22H2 for ARM64-based Systems",
"12086": "Windows 11 Version 22H2 for x64-based Systems",
"12242": "Windows 11 Version 23H2 for ARM64-based Systems",
"12243": "Windows 11 Version 23H2 for x64-based Systems",
"11923": "Windows Server 2022",
"11924": "Windows Server 2022 (Server Core installation)",
"12244": "Windows Server 2022, 23H2 Edition (Server Core installation)",
}
func TestMatchesOperatingSystem(t *testing.T) {
ctx := context.Background()
tc := []struct {
name string
os fleet.OperatingSystem
want string
err error
}{
{
name: "OS with known Display Version Match x64",
os: fleet.OperatingSystem{
Name: "Windows 11 Pro 22H2",
Arch: "x86_64",
DisplayVersion: "22H2",
},
want: "12086",
err: nil,
},
{
name: "OS with known Display Version Match ARM64",
os: fleet.OperatingSystem{
Name: "Windows 11 Pro 22H2",
Arch: "ARM 64-bit Processor",
DisplayVersion: "22H2",
},
want: "12085",
err: nil,
},
{
name: "OS with no Display Version",
os: fleet.OperatingSystem{
Name: "Windows 11 Pro",
Arch: "64-bit",
},
want: "11926",
},
{
name: "Product contains 'Edition' keyword",
os: fleet.OperatingSystem{
Name: "Windows Server 2022 23H2",
Arch: "64-bit",
DisplayVersion: "23H2",
},
want: "12244",
err: nil,
},
{
name: "unknown OS",
os: fleet.OperatingSystem{
Name: "Windows Foo Bar",
Arch: "arm64",
},
want: "",
err: ErrNoMatch,
},
}
for _, tt := range tc {
match, err := msrcWinProducts.GetMatchForOS(ctx, tt.os)
require.ErrorIs(t, err, tt.err, tt.name)
require.Equal(t, tt.want, match, tt.name)
}
}

View File

@ -15,7 +15,7 @@ type SecurityBulletin struct {
// We can have many different 'products' under a single name, for example, for 'Windows 10':
// - Windows 10 Version 1809 for 32-bit Systems
// - Windows 10 Version 1909 for x64-based Systems
Products map[string]Product
Products Products
// All vulnerabilities contained in this bulletin, by CVE
Vulnerabities map[string]Vulnerability
// All vendor fixes for remediating the vulnerabilities contained in this bulletin, by KBID

View File

@ -40,7 +40,7 @@ func TestParser(t *testing.T) {
require.NoError(t, err)
// All the products we expect to see, grouped by their product name
expectedProducts := map[string]map[string]parsed.Product{
expectedProducts := map[string]parsed.Products{
"Windows 10": {
"11568": parsed.NewProductFromFullName("Windows 10 Version 1809 for 32-bit Systems"),
"11569": parsed.NewProductFromFullName("Windows 10 Version 1809 for x64-based Systems"),
@ -1195,7 +1195,7 @@ func TestParser(t *testing.T) {
t.Run("each bulletin should have the right products", func(t *testing.T) {
for _, g := range bulletins {
require.Equal(t, g.Products, expectedProducts[g.ProductName], g.ProductName)
require.Equal(t, expectedProducts[g.ProductName], g.Products, g.ProductName)
}
})

View File

@ -124,11 +124,38 @@ func getNVDCVEFeedFiles(vulnPath string) ([]string, error) {
return files, nil
}
// interface for items with NVD Meta Data
type itemWithNVDMeta interface {
GetMeta() *wfn.Attributes
GetID() uint
}
type softwareCPEWithNVDMeta struct {
fleet.SoftwareCPE
meta *wfn.Attributes
}
func (s softwareCPEWithNVDMeta) GetMeta() *wfn.Attributes {
return s.meta
}
func (s softwareCPEWithNVDMeta) GetID() uint {
return s.SoftwareID
}
type osCPEWithNVDMeta struct {
fleet.OperatingSystem
meta *wfn.Attributes
}
func (o osCPEWithNVDMeta) GetMeta() *wfn.Attributes {
return o.meta
}
func (o osCPEWithNVDMeta) GetID() uint {
return o.ID
}
// TranslateCPEToCVE maps the CVEs found in NVD archive files in the
// vulnerabilities database folder to software CPEs in the fleet database.
// If collectVulns is true, returns a list of any new software vulnerabilities found.
@ -168,10 +195,23 @@ func TranslateCPEToCVE(
})
}
if len(parsed) == 0 {
cpes, err := GetMacOSCPEs(ctx, ds)
if err != nil {
return nil, err
}
if len(parsed) == 0 && len(cpes) == 0 {
return nil, nil
}
var interfaceParsed []itemWithNVDMeta
for _, p := range parsed {
interfaceParsed = append(interfaceParsed, p)
}
for _, c := range cpes {
interfaceParsed = append(interfaceParsed, c)
}
knownNVDBugRules, err := GetKnownNVDBugRules()
if err != nil {
return nil, err
@ -179,14 +219,15 @@ func TranslateCPEToCVE(
// we are using a map here to remove any duplicates - a vulnerability can be present in more than one
// NVD feed file.
vulns := make(map[string]fleet.SoftwareVulnerability)
softwareVulns := make(map[string]fleet.SoftwareVulnerability)
osVulns := make(map[string]fleet.OSVulnerability)
for _, file := range files {
foundVulns, err := checkCVEs(
foundSoftwareVulns, foundOSVulns, err := checkCVEs(
ctx,
ds,
logger,
parsed,
interfaceParsed,
file,
collectVulns,
knownNVDBugRules,
@ -195,13 +236,16 @@ func TranslateCPEToCVE(
return nil, err
}
for _, e := range foundVulns {
vulns[e.Key()] = e
for _, e := range foundSoftwareVulns {
softwareVulns[e.Key()] = e
}
for _, e := range foundOSVulns {
osVulns[e.Key()] = e
}
}
var newVulns []fleet.SoftwareVulnerability
for _, vuln := range vulns {
for _, vuln := range softwareVulns {
ok, err := ds.InsertSoftwareVulnerability(ctx, vuln, fleet.NVDSource)
if err != nil {
level.Error(logger).Log("cpe processing", "error", "err", err)
@ -216,6 +260,14 @@ func TranslateCPEToCVE(
}
}
for _, vuln := range osVulns {
_, err := ds.InsertOSVulnerability(ctx, vuln, fleet.NVDSource)
if err != nil {
level.Error(logger).Log("cpe processing", "error", "err", err)
continue
}
}
// Delete any stale vulnerabilities. A vulnerability is stale iff the last time it was
// updated was more than `2 * periodicity` ago. This assumes that the whole vulnerability
// process completes in less than `periodicity` units of time.
@ -224,10 +276,54 @@ func TranslateCPEToCVE(
if err = ds.DeleteOutOfDateVulnerabilities(ctx, fleet.NVDSource, 2*periodicity); err != nil {
level.Error(logger).Log("msg", "error deleting out of date vulnerabilities", "err", err)
}
if err = ds.DeleteOutOfDateOSVulnerabilities(ctx, fleet.NVDSource, 2*periodicity); err != nil {
level.Error(logger).Log("msg", "error deleting out of date OS vulnerabilities", "err", err)
}
return newVulns, nil
}
// GetMacOSCPEs translates all found macOS Operating Systems to CPEs.
func GetMacOSCPEs(ctx context.Context, ds fleet.Datastore) ([]osCPEWithNVDMeta, error) {
var cpes []osCPEWithNVDMeta
oses, err := ds.ListOperatingSystemsForPlatform(ctx, "darwin")
if err != nil {
return cpes, ctxerr.Wrap(ctx, err, "list operating systems")
}
if len(oses) == 0 {
return cpes, nil
}
// variants of macOS found in the NVD feed
macosVariants := []string{"macos", "mac_os_x"}
for _, os := range oses {
for _, variant := range macosVariants {
cpe := osCPEWithNVDMeta{
OperatingSystem: os,
meta: &wfn.Attributes{
Part: "o",
Vendor: "apple",
Product: variant,
Version: os.Version,
Update: wfn.Any,
Edition: wfn.Any,
SWEdition: wfn.Any,
TargetSW: wfn.Any,
TargetHW: wfn.Any,
Other: wfn.Any,
Language: wfn.Any,
},
}
cpes = append(cpes, cpe)
}
}
return cpes, nil
}
func matchesExactTargetSW(softwareCPETargetSW string, targetSWs []string, configs []*wfn.Attributes) bool {
for _, targetSW := range targetSWs {
if softwareCPETargetSW == targetSW {
@ -245,25 +341,27 @@ func checkCVEs(
ctx context.Context,
ds fleet.Datastore,
logger kitlog.Logger,
softwareCPEs []softwareCPEWithNVDMeta,
CPEItems []itemWithNVDMeta,
jsonFile string,
collectVulns bool,
knownNVDBugRules CPEMatchingRules,
) ([]fleet.SoftwareVulnerability, error) {
) ([]fleet.SoftwareVulnerability, []fleet.OSVulnerability, error) {
dict, err := cvefeed.LoadJSONDictionary(jsonFile)
if err != nil {
return nil, err
return nil, nil, err
}
cache := cvefeed.NewCache(dict).SetRequireVersion(true).SetMaxSize(-1)
// This index consumes too much RAM
// cache.Idx = cvefeed.NewIndex(dict)
softwareCPECh := make(chan softwareCPEWithNVDMeta)
var foundVulns []fleet.SoftwareVulnerability
CPEItemCh := make(chan itemWithNVDMeta)
var foundSoftwareVulns []fleet.SoftwareVulnerability
var foundOSVulns []fleet.OSVulnerability
var wg sync.WaitGroup
var mu sync.Mutex
var softwareMu sync.Mutex
var osMu sync.Mutex
logger = log.With(logger, "json_file", jsonFile)
@ -278,13 +376,13 @@ func checkCVEs(
for {
select {
case softwareCPE, more := <-softwareCPECh:
case CPEItem, more := <-CPEItemCh:
if !more {
level.Debug(logger).Log("msg", "done")
return
}
cacheHits := cache.Get([]*wfn.Attributes{softwareCPE.meta})
cacheHits := cache.Get([]*wfn.Attributes{CPEItem.GetMeta()})
for _, matches := range cacheHits {
if len(matches.CPEs) == 0 {
continue
@ -293,7 +391,7 @@ func checkCVEs(
if rule, ok := knownNVDBugRules.FindMatch(
matches.CVE.ID(),
); ok {
if !rule.CPEMatches(softwareCPE.meta) {
if !rule.CPEMatches(CPEItem.GetMeta()) {
continue
}
}
@ -305,9 +403,9 @@ func checkCVEs(
// with target_sw == "*", meaning the client application is vulnerable on all operating systems.
// Such rules we want to ignore here to prevent many false positives that do not apply to the
// Chrome or Firefox environment.
if softwareCPE.meta.TargetSW == "chrome" || softwareCPE.meta.TargetSW == "firefox" {
if CPEItem.GetMeta().TargetSW == "chrome" || CPEItem.GetMeta().TargetSW == "firefox" {
if !matchesExactTargetSW(
softwareCPE.meta.TargetSW,
CPEItem.GetMeta().TargetSW,
[]string{"chrome", "firefox"},
matches.CVE.Config(),
) {
@ -315,20 +413,34 @@ func checkCVEs(
}
}
resolvedVersion, err := getMatchingVersionEndExcluding(ctx, matches.CVE.ID(), softwareCPE.meta, dict, logger)
resolvedVersion, err := getMatchingVersionEndExcluding(ctx, matches.CVE.ID(), CPEItem.GetMeta(), dict, logger)
if err != nil {
level.Debug(logger).Log("err", err)
}
vuln := fleet.SoftwareVulnerability{
SoftwareID: softwareCPE.SoftwareID,
CVE: matches.CVE.ID(),
ResolvedInVersion: ptr.String(resolvedVersion),
}
if _, ok := CPEItem.(softwareCPEWithNVDMeta); ok {
mu.Lock()
foundVulns = append(foundVulns, vuln)
mu.Unlock()
vuln := fleet.SoftwareVulnerability{
SoftwareID: CPEItem.GetID(),
CVE: matches.CVE.ID(),
ResolvedInVersion: ptr.String(resolvedVersion),
}
softwareMu.Lock()
foundSoftwareVulns = append(foundSoftwareVulns, vuln)
softwareMu.Unlock()
} else if _, ok := CPEItem.(osCPEWithNVDMeta); ok {
vuln := fleet.OSVulnerability{
OSID: CPEItem.GetID(),
CVE: matches.CVE.ID(),
ResolvedInVersion: ptr.String(resolvedVersion),
}
osMu.Lock()
foundOSVulns = append(foundOSVulns, vuln)
osMu.Unlock()
}
}
case <-ctx.Done():
@ -341,14 +453,14 @@ func checkCVEs(
level.Debug(logger).Log("msg", "pushing cpes")
for _, cpe := range softwareCPEs {
softwareCPECh <- cpe
for _, cpe := range CPEItems {
CPEItemCh <- cpe
}
close(softwareCPECh)
close(CPEItemCh)
level.Debug(logger).Log("msg", "cpes pushed")
wg.Wait()
return foundVulns, nil
return foundSoftwareVulns, foundOSVulns, nil
}
// Returns the versionEndExcluding string for the given CVE and host software meta

View File

@ -263,6 +263,79 @@ func TestTranslateCPEToCVE(t *testing.T) {
},
}
cveOSTests := []struct {
platform string
version string
osID uint
includedCVEs []string
}{
{
platform: "darwin",
version: "14.1.2",
osID: 1,
includedCVEs: []string{
"CVE-2023-45866",
"CVE-2023-42886",
"CVE-2023-42891",
"CVE-2023-42906",
"CVE-2023-42910",
"CVE-2023-42924",
"CVE-2023-42883",
"CVE-2023-42894",
"CVE-2023-42926",
"CVE-2023-42932",
"CVE-2023-42907",
"CVE-2023-42922",
"CVE-2023-42904",
"CVE-2023-42901",
"CVE-2023-42898",
"CVE-2023-42903",
"CVE-2023-42902",
"CVE-2023-42909",
"CVE-2023-42914",
"CVE-2023-42874",
"CVE-2023-42882",
"CVE-2023-42912",
"CVE-2023-42911",
"CVE-2023-42890",
"CVE-2023-42905",
"CVE-2023-42919",
"CVE-2023-42900",
"CVE-2023-42899",
"CVE-2023-42908",
"CVE-2023-42884",
},
},
{
platform: "darwin",
version: "13.6.2",
osID: 2,
// This is a subset of vulnerabilities for macOS 13.6.2
includedCVEs: []string{
"CVE-2023-32361",
"CVE-2023-35990",
"CVE-2023-40541",
"CVE-2023-40400",
"CVE-2023-41980",
"CVE-2023-38615",
"CVE-2023-39233",
"CVE-2023-40402",
"CVE-2023-40450",
"CVE-2023-42891",
"CVE-2023-41079",
"CVE-2023-42932",
"CVE-2023-38586",
"CVE-2023-41067",
"CVE-2023-40407",
"CVE-2023-42924",
"CVE-2023-40395",
"CVE-2023-38596",
"CVE-2023-32396",
"CVE-2023-29497",
},
},
}
t.Run("find_vulns_on_cpes", func(t *testing.T) {
t.Parallel()
@ -280,6 +353,20 @@ func TestTranslateCPEToCVE(t *testing.T) {
return softwareCPEs, nil
}
var osIDs []uint
ds.ListOperatingSystemsForPlatformFunc = func(ctx context.Context, p string) ([]fleet.OperatingSystem, error) {
var oss []fleet.OperatingSystem
for _, os := range cveOSTests {
oss = append(oss, fleet.OperatingSystem{
ID: os.osID,
Platform: os.platform,
Version: os.version,
})
osIDs = append(osIDs, os.osID)
}
return oss, nil
}
cveLock := &sync.Mutex{}
cvesFound := make(map[string][]cve)
ds.InsertSoftwareVulnerabilityFunc = func(ctx context.Context, vuln fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) (bool, error) {
@ -297,14 +384,30 @@ func TestTranslateCPEToCVE(t *testing.T) {
cvesFound[cpe] = append(cvesFound[cpe], cve)
return false, nil
}
osCVELock := &sync.Mutex{}
osCVEsFound := make(map[uint][]string)
ds.InsertOSVulnerabilityFunc = func(ctx context.Context, vuln fleet.OSVulnerability, src fleet.VulnerabilitySource) (bool, error) {
osCVELock.Lock()
defer osCVELock.Unlock()
osCVEsFound[vuln.OSID] = append(osCVEsFound[vuln.OSID], vuln.CVE)
return false, nil
}
ds.DeleteOutOfDateVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, duration time.Duration) error {
return nil
}
ds.DeleteOutOfDateOSVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, duration time.Duration) error {
return nil
}
_, err := TranslateCPEToCVE(ctx, ds, tempDir, kitlog.NewNopLogger(), false, 1*time.Hour)
require.NoError(t, err)
require.True(t, ds.DeleteOutOfDateVulnerabilitiesFuncInvoked)
require.True(t, ds.DeleteOutOfDateOSVulnerabilitiesFuncInvoked)
for cpe, tc := range cveTests {
if tc.continuesToUpdate {
@ -323,6 +426,12 @@ func TestTranslateCPEToCVE(t *testing.T) {
require.NotContains(t, cvesFound[cpe], cve, tc.cpe)
}
}
for _, tc := range cveOSTests {
for _, cve := range tc.includedCVEs {
require.Contains(t, osCVEsFound[tc.osID], cve)
}
}
})
t.Run("recent_vulns", func(t *testing.T) {
@ -342,10 +451,16 @@ func TestTranslateCPEToCVE(t *testing.T) {
ds.ListSoftwareCPEsFunc = func(ctx context.Context) ([]fleet.SoftwareCPE, error) {
return softwareCPEs, nil
}
ds.InsertSoftwareVulnerabilityFunc = func(ctx context.Context, vuln fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) (bool, error) {
return true, nil
}
ds.ListOperatingSystemsForPlatformFunc = func(ctx context.Context, p string) ([]fleet.OperatingSystem, error) {
return nil, nil
}
ds.DeleteOutOfDateOSVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, duration time.Duration) error {
return nil
}
recent, err := TranslateCPEToCVE(ctx, safeDS, tempDir, kitlog.NewNopLogger(), true, 1*time.Hour)
require.NoError(t, err)
@ -518,6 +633,66 @@ func TestPreprocessVersion(t *testing.T) {
}
}
func TestGetMacOSCPEs(t *testing.T) {
ctx := context.Background()
ds := new(mock.Store)
os := fleet.OperatingSystem{
ID: 1,
Name: "macOS",
Version: "11.6.2",
Arch: "x86_64",
KernelVersion: "20.6.0",
Platform: "darwin",
}
ds.ListOperatingSystemsForPlatformFunc = func(ctx context.Context, p string) ([]fleet.OperatingSystem, error) {
return []fleet.OperatingSystem{os}, nil
}
CVEs, err := GetMacOSCPEs(ctx, ds)
require.NoError(t, err)
require.Len(t, CVEs, 2)
expected := map[osCPEWithNVDMeta]struct{}{
{
OperatingSystem: os,
meta: &wfn.Attributes{
Part: "o",
Vendor: "apple",
Product: "mac_os_x",
Version: CVEs[0].Version,
Update: wfn.Any,
Edition: wfn.Any,
SWEdition: wfn.Any,
TargetSW: wfn.Any,
TargetHW: wfn.Any,
Other: wfn.Any,
Language: wfn.Any,
},
}: {},
{
OperatingSystem: os,
meta: &wfn.Attributes{
Part: "o",
Vendor: "apple",
Product: "macos",
Version: CVEs[0].Version,
Update: wfn.Any,
Edition: wfn.Any,
SWEdition: wfn.Any,
TargetSW: wfn.Any,
TargetHW: wfn.Any,
Other: wfn.Any,
Language: wfn.Any,
},
}: {},
}
for _, cve := range CVEs {
require.Contains(t, expected, cve)
}
}
// loadDict loads a cvefeed.Dictionary from a JSON NVD feed file.
func loadDict(t *testing.T, path string) cvefeed.Dictionary {
dict, err := cvefeed.LoadJSONDictionary(path)