mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #40054 # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - Included in previous PR ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Migrated logging infrastructure from external framework to standard library structured logging, enabling improved context-aware operations and error tracking across vulnerability detection and synchronization workflows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
591 lines
16 KiB
Go
591 lines
16 KiB
Go
package customcve
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mock"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMatchVersion(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
rule := CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
SourceMatch: "programs",
|
|
ResolvedInVersion: "16.0.17628.20144",
|
|
CVEs: []string{"CVE-2024-001", "CVE-2024-002"},
|
|
}
|
|
|
|
sw := []fleet.Software{
|
|
{
|
|
ID: 1,
|
|
Version: "16.0.17531.20152", // in range
|
|
},
|
|
{
|
|
ID: 2,
|
|
Version: "16.0.17425.20176", // in range
|
|
},
|
|
{
|
|
ID: 3,
|
|
Version: "16.0.17628.20144", // over range equals
|
|
},
|
|
{
|
|
ID: 4,
|
|
Version: "16.0.17628.20145", // over range
|
|
},
|
|
}
|
|
|
|
expected := []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-001",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-002",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-001",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-002",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
}
|
|
|
|
ds.ListSoftwareForVulnDetectionFunc = func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) {
|
|
return sw, nil
|
|
}
|
|
|
|
actual, err := rule.match(context.Background(), ds)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, actual)
|
|
}
|
|
|
|
func TestMatchExcludeIfNameContains(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
rule := CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
ExcludeIfNameContains: "companion",
|
|
SourceMatch: "programs",
|
|
ResolvedInVersion: "16.0.17628.20144",
|
|
CVEs: []string{"CVE-2024-001", "CVE-2024-002"},
|
|
}
|
|
|
|
sw := []fleet.Software{
|
|
{
|
|
ID: 1,
|
|
Name: "Microsoft 365 - en-us",
|
|
Version: "16.0.17000.00000",
|
|
},
|
|
{
|
|
ID: 2,
|
|
Name: "Microsoft 365 companion apps",
|
|
Version: "2.2601.6000.0",
|
|
},
|
|
{
|
|
ID: 3,
|
|
Name: "Microsoft 365 Companion Apps",
|
|
Version: "2.2601.6000.0",
|
|
},
|
|
{
|
|
ID: 4,
|
|
Name: "Microsoft 365 - fr-fr",
|
|
Version: "16.0.17000.00000",
|
|
},
|
|
}
|
|
|
|
expected := []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-001",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-002",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 4,
|
|
CVE: "CVE-2024-001",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 4,
|
|
CVE: "CVE-2024-002",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
}
|
|
|
|
ds.ListSoftwareForVulnDetectionFunc = func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) {
|
|
return sw, nil
|
|
}
|
|
|
|
actual, err := rule.match(context.Background(), ds)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, actual)
|
|
}
|
|
|
|
func TestMatchFilters(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
rule CVEMatchingRule
|
|
expectedFilter fleet.VulnSoftwareFilter
|
|
}{
|
|
{
|
|
name: "Match all",
|
|
rule: CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
SourceMatch: "programs",
|
|
ResolvedInVersion: "16.0.17628.20144",
|
|
CVEs: []string{"CVE-2024-001", "CVE-2024-002"},
|
|
},
|
|
expectedFilter: fleet.VulnSoftwareFilter{
|
|
Name: "Microsoft 365",
|
|
Source: "programs",
|
|
},
|
|
},
|
|
{
|
|
name: "Match only name",
|
|
rule: CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
ResolvedInVersion: "16.0.17628.20144",
|
|
CVEs: []string{"CVE-2024-001", "CVE-2024-002"},
|
|
},
|
|
expectedFilter: fleet.VulnSoftwareFilter{
|
|
Name: "Microsoft 365",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ds.ListSoftwareForVulnDetectionFunc = func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) {
|
|
require.Equal(t, tt.expectedFilter, filter)
|
|
return nil, nil
|
|
}
|
|
|
|
_, err := tt.rule.match(context.Background(), ds)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCVEMatchingRuleValidation(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
rule CVEMatchingRule
|
|
err error
|
|
}{
|
|
{
|
|
name: "Valid rule",
|
|
rule: CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
SourceMatch: "programs",
|
|
CVEs: []string{"CVE-123"},
|
|
ResolvedInVersion: "1.0.0",
|
|
},
|
|
},
|
|
{
|
|
name: "Valid rule with empty SourceMatch",
|
|
rule: CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
CVEs: []string{"CVE-123"},
|
|
ResolvedInVersion: "1.0.0",
|
|
},
|
|
},
|
|
{
|
|
name: "Empty CVEs",
|
|
rule: CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
SourceMatch: "programs",
|
|
ResolvedInVersion: "1.0.0",
|
|
},
|
|
err: MissingCVEsErr,
|
|
},
|
|
{
|
|
name: "Empty NameLikeMatch",
|
|
rule: CVEMatchingRule{
|
|
SourceMatch: "programs",
|
|
CVEs: []string{"CVE-123"},
|
|
ResolvedInVersion: "1.0.0",
|
|
},
|
|
err: MissingNameLikeMatch,
|
|
},
|
|
{
|
|
name: "Empty ResolvedInVersion",
|
|
rule: CVEMatchingRule{
|
|
NameLikeMatch: "Microsoft 365",
|
|
SourceMatch: "programs",
|
|
CVEs: []string{"CVE-123"},
|
|
},
|
|
err: MissingResolvedInVersionErr,
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.rule.validate()
|
|
if tt.err == nil {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Equal(t, tt.err, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateAll(t *testing.T) {
|
|
rules := getCVEMatchingRules()
|
|
err := rules.ValidateAll()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCheckCustomVulnerabilities(t *testing.T) {
|
|
ds := new(mock.Store)
|
|
sw := []fleet.Software{
|
|
// Very old version should match all custom matching rules.
|
|
{
|
|
ID: 1,
|
|
Name: "Microsoft 365 - en-us",
|
|
Version: "16.0.17531.20152",
|
|
Source: "programs",
|
|
},
|
|
// This version should match June matching rules but not July and August.
|
|
{
|
|
ID: 2,
|
|
Name: "Microsoft 365 - en-us",
|
|
Version: "16.0.17628.20144",
|
|
Source: "programs",
|
|
},
|
|
// This version should match June and July, but not August.
|
|
{
|
|
ID: 3,
|
|
Name: "Microsoft 365 - en-us",
|
|
Version: "16.0.17726.20161",
|
|
Source: "programs",
|
|
},
|
|
// This version should have no CVEs.
|
|
{
|
|
ID: 4,
|
|
Name: "Microsoft 365 - en-us",
|
|
Version: "16.0.17830.20167",
|
|
Source: "programs",
|
|
},
|
|
// git-gui vulnerable version should match gitk CVEs
|
|
{
|
|
ID: 5,
|
|
Name: "git-gui",
|
|
Version: "2.50.0",
|
|
Source: "homebrew_packages",
|
|
},
|
|
// git-gui patched version should not match gitk CVEs
|
|
{
|
|
ID: 6,
|
|
Name: "git-gui",
|
|
Version: "2.52.0",
|
|
Source: "homebrew_packages",
|
|
},
|
|
// Microsoft 365 companion apps should be excluded from all matching rules
|
|
{
|
|
ID: 7,
|
|
Name: "Microsoft 365 companion apps",
|
|
Version: "2.2601.6000.0",
|
|
Source: "programs",
|
|
},
|
|
// Windows Notepad vulnerable version should match CVE-2026-20841
|
|
{
|
|
ID: 8,
|
|
Name: "Microsoft.WindowsNotepad",
|
|
Version: "11.2409.10.0",
|
|
Source: "programs",
|
|
},
|
|
// Windows Notepad patched version should not match CVE-2026-20841
|
|
{
|
|
ID: 9,
|
|
Name: "Microsoft.WindowsNotepad",
|
|
Version: "11.2510.14.0",
|
|
Source: "programs",
|
|
},
|
|
}
|
|
|
|
t.Run("New Vulns return all inserted", func(t *testing.T) {
|
|
ds.ListSoftwareForVulnDetectionFunc = func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) {
|
|
if filter.Name == "Microsoft 365" && filter.Source == "programs" {
|
|
return []fleet.Software{sw[0], sw[1], sw[2], sw[3], sw[6]}, nil
|
|
}
|
|
if filter.Name == "git-gui" && filter.Source == "homebrew_packages" {
|
|
return []fleet.Software{sw[4], sw[5]}, nil
|
|
}
|
|
if filter.Name == "Microsoft.WindowsNotepad" && filter.Source == "programs" {
|
|
return []fleet.Software{sw[7], sw[8]}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
ds.InsertSoftwareVulnerabilitiesFunc = func(ctx context.Context, vulns []fleet.SoftwareVulnerability, source fleet.VulnerabilitySource) ([]fleet.SoftwareVulnerability, error) {
|
|
require.Equal(t, fleet.CustomSource, source)
|
|
for _, v := range vulns {
|
|
require.NotEqual(t, uint(7), v.SoftwareID, "Microsoft 365 companion apps should be excluded from CVE matching")
|
|
}
|
|
return vulns, nil // all inserted vulns are "new"
|
|
}
|
|
|
|
ds.DeleteOutOfDateVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error {
|
|
require.Equal(t, fleet.CustomSource, source)
|
|
return nil
|
|
}
|
|
|
|
ctx := context.Background()
|
|
vulns, err := CheckCustomVulnerabilities(ctx, ds, slog.New(slog.DiscardHandler), time.Now().UTC().Add(-time.Hour))
|
|
require.NoError(t, err)
|
|
require.Len(t, vulns, 35)
|
|
require.True(t, ds.DeleteOutOfDateVulnerabilitiesFuncInvoked)
|
|
|
|
expected := []fleet.SoftwareVulnerability{
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-30101",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-30102",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-30103",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-30104",
|
|
ResolvedInVersion: ptr.String("16.0.17628.20144"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2023-38545",
|
|
ResolvedInVersion: ptr.String("16.0.17726.20160"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38020",
|
|
ResolvedInVersion: ptr.String("16.0.17726.20160"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38021",
|
|
ResolvedInVersion: ptr.String("16.0.17726.20160"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38172",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38170",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38173",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38171",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38189",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38169",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 1,
|
|
CVE: "CVE-2024-38200",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2023-38545",
|
|
ResolvedInVersion: ptr.String("16.0.17726.20160"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38020",
|
|
ResolvedInVersion: ptr.String("16.0.17726.20160"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38021",
|
|
ResolvedInVersion: ptr.String("16.0.17726.20160"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38172",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38170",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38173",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38171",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38189",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38169",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 2,
|
|
CVE: "CVE-2024-38200",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 3,
|
|
CVE: "CVE-2024-38172",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 3,
|
|
CVE: "CVE-2024-38170",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 3,
|
|
CVE: "CVE-2024-38173",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 3,
|
|
CVE: "CVE-2024-38171",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 3,
|
|
CVE: "CVE-2024-38189",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 3,
|
|
CVE: "CVE-2024-38169",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 3,
|
|
CVE: "CVE-2024-38200",
|
|
ResolvedInVersion: ptr.String("16.0.17830.20166"),
|
|
},
|
|
{
|
|
SoftwareID: 5,
|
|
CVE: "CVE-2025-27613",
|
|
ResolvedInVersion: ptr.String("2.50.1"),
|
|
},
|
|
{
|
|
SoftwareID: 5,
|
|
CVE: "CVE-2025-27614",
|
|
ResolvedInVersion: ptr.String("2.50.1"),
|
|
},
|
|
{
|
|
SoftwareID: 5,
|
|
CVE: "CVE-2025-46835",
|
|
ResolvedInVersion: ptr.String("2.50.1"),
|
|
},
|
|
{
|
|
SoftwareID: 8,
|
|
CVE: "CVE-2026-20841",
|
|
ResolvedInVersion: ptr.String("11.2510"),
|
|
},
|
|
}
|
|
|
|
cmpSoftwareVulnerability := func(v []fleet.SoftwareVulnerability) func(i, j int) bool {
|
|
return func(i, j int) bool {
|
|
if v[i].SoftwareID <= v[j].SoftwareID {
|
|
if v[i].SoftwareID == v[j].SoftwareID {
|
|
return v[i].CVE < v[j].CVE
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
sort.Slice(expected, cmpSoftwareVulnerability(expected))
|
|
sort.Slice(vulns, cmpSoftwareVulnerability(vulns))
|
|
require.Equal(t, expected, vulns)
|
|
})
|
|
|
|
t.Run("Existing Vulns are not inserted", func(t *testing.T) {
|
|
ds.DeleteOutOfDateVulnerabilitiesFuncInvoked = false
|
|
|
|
ds.ListSoftwareForVulnDetectionFunc = func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) {
|
|
if filter.Name == "Microsoft 365" && filter.Source == "programs" {
|
|
return []fleet.Software{sw[0], sw[1], sw[2], sw[3], sw[6]}, nil
|
|
}
|
|
if filter.Name == "git-gui" && filter.Source == "homebrew_packages" {
|
|
return []fleet.Software{sw[4], sw[5]}, nil
|
|
}
|
|
if filter.Name == "Microsoft.WindowsNotepad" && filter.Source == "programs" {
|
|
return []fleet.Software{sw[7], sw[8]}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// Simulate all vulns already existing: InsertSoftwareVulnerabilities returns empty (no new vulns).
|
|
ds.InsertSoftwareVulnerabilitiesFunc = func(ctx context.Context, vulns []fleet.SoftwareVulnerability, source fleet.VulnerabilitySource) ([]fleet.SoftwareVulnerability, error) {
|
|
require.Equal(t, fleet.CustomSource, source)
|
|
for _, v := range vulns {
|
|
require.NotEqual(t, uint(7), v.SoftwareID, "Microsoft 365 companion apps should be excluded from CVE matching")
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
ds.DeleteOutOfDateVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error {
|
|
require.Equal(t, fleet.CustomSource, source)
|
|
return nil
|
|
}
|
|
|
|
ctx := t.Context()
|
|
vulns, err := CheckCustomVulnerabilities(ctx, ds, slog.New(slog.DiscardHandler), time.Now().UTC().Add(-time.Hour))
|
|
require.NoError(t, err)
|
|
require.True(t, ds.DeleteOutOfDateVulnerabilitiesFuncInvoked)
|
|
require.Len(t, vulns, 0)
|
|
})
|
|
}
|