mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 16:39:01 +00:00
**Related issue:** Resolves #35281 - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## 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 * **Bug Fixes** * Fixed false positives when detecting security vulnerabilities in Microsoft 365 companion apps by improving targeting accuracy. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
567 lines
15 KiB
Go
567 lines
15 KiB
Go
package customcve
|
|
|
|
import (
|
|
"context"
|
|
"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/go-kit/log"
|
|
"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",
|
|
},
|
|
}
|
|
|
|
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
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
var insertCount int
|
|
ds.InsertSoftwareVulnerabilityFunc = func(ctx context.Context, vuln fleet.SoftwareVulnerability, source fleet.VulnerabilitySource) (bool, error) {
|
|
insertCount++
|
|
require.Equal(t, fleet.CustomSource, source)
|
|
require.NotEqual(t, uint(7), vuln.SoftwareID, "Microsoft 365 companion apps should be excluded from CVE matching")
|
|
return true, nil
|
|
}
|
|
|
|
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, log.NewNopLogger(), time.Now().UTC().Add(-time.Hour))
|
|
require.NoError(t, err)
|
|
require.Equal(t, 34, insertCount)
|
|
require.Len(t, vulns, 34)
|
|
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"),
|
|
},
|
|
}
|
|
|
|
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
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
var insertCount int
|
|
ds.InsertSoftwareVulnerabilityFunc = func(ctx context.Context, vuln fleet.SoftwareVulnerability, source fleet.VulnerabilitySource) (bool, error) {
|
|
insertCount++
|
|
require.Equal(t, fleet.CustomSource, source)
|
|
require.NotEqual(t, uint(7), vuln.SoftwareID, "Microsoft 365 companion apps should be excluded from CVE matching")
|
|
return false, nil
|
|
}
|
|
|
|
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, log.NewNopLogger(), time.Now().UTC().Add(-time.Hour))
|
|
require.NoError(t, err)
|
|
require.True(t, ds.DeleteOutOfDateVulnerabilitiesFuncInvoked)
|
|
require.Equal(t, 34, insertCount)
|
|
require.Len(t, vulns, 0)
|
|
})
|
|
}
|