mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
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:
parent
edee09e22a
commit
79b5baa297
3
changes/4345-os-vulns-backend
Normal file
3
changes/4345-os-vulns-backend
Normal 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.
|
@ -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...)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 }}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user