package nvd import ( "context" "fmt" "log/slog" "net/http" "net/http/httptest" "os" "strings" "sync" "testing" "time" "github.com/fleetdm/fleet/v4/pkg/nettest" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/cvefeed" "github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd/tools/wfn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // firefox93WindowsVulnerabilities was manually generated by visiting: // https://nvd.nist.gov/vuln/search/results?form_type=Advanced&results_type=overview&isCpeNameSearch=true&seach_type=all&query=cpe:2.3:a:mozilla:firefox:93.0:*:*:*:*:*:*:* type cve struct { ID string resolvedInVersion string } var firefox93WindowsVulnerabilities = []cve{ {ID: "CVE-2021-43540", resolvedInVersion: "95.0"}, {ID: "CVE-2021-38503", resolvedInVersion: "94.0"}, {ID: "CVE-2021-38504", resolvedInVersion: "94.0"}, {ID: "CVE-2021-38506", resolvedInVersion: "94.0"}, {ID: "CVE-2021-38507", resolvedInVersion: "94.0"}, {ID: "CVE-2021-38508", resolvedInVersion: "94.0"}, {ID: "CVE-2021-38509", resolvedInVersion: "94.0"}, {ID: "CVE-2021-43534", resolvedInVersion: "94.0"}, {ID: "CVE-2021-43532", resolvedInVersion: "94.0"}, {ID: "CVE-2021-43531", resolvedInVersion: "94.0"}, {ID: "CVE-2021-43533", resolvedInVersion: "94.0"}, {ID: "CVE-2021-43538", resolvedInVersion: "95.0"}, {ID: "CVE-2021-43542", resolvedInVersion: "95.0"}, {ID: "CVE-2021-43543", resolvedInVersion: "95.0"}, {ID: "CVE-2021-30547"}, {ID: "CVE-2021-43546", resolvedInVersion: "95.0"}, {ID: "CVE-2021-43537", resolvedInVersion: "95.0"}, {ID: "CVE-2021-43541", resolvedInVersion: "95.0"}, {ID: "CVE-2021-43536", resolvedInVersion: "95.0"}, {ID: "CVE-2021-43545", resolvedInVersion: "95.0"}, {ID: "CVE-2021-43539", resolvedInVersion: "95.0"}, {ID: "CVE-2022-34480", resolvedInVersion: "102.0"}, {ID: "CVE-2022-26387", resolvedInVersion: "98.0"}, {ID: "CVE-2022-22759", resolvedInVersion: "97.0"}, {ID: "CVE-2022-28281", resolvedInVersion: "99.0"}, {ID: "CVE-2022-45415", resolvedInVersion: "107.0"}, {ID: "CVE-2022-42930", resolvedInVersion: "106.0"}, {ID: "CVE-2022-0511", resolvedInVersion: "97.0"}, {ID: "CVE-2022-22763", resolvedInVersion: "96.0"}, {ID: "CVE-2022-22737", resolvedInVersion: "96.0"}, {ID: "CVE-2022-22751", resolvedInVersion: "96.0"}, {ID: "CVE-2022-38478", resolvedInVersion: "104.0"}, {ID: "CVE-2022-22761", resolvedInVersion: "97.0"}, {ID: "CVE-2022-34482", resolvedInVersion: "102.0"}, {ID: "CVE-2022-26486", resolvedInVersion: "97.0.2"}, {ID: "CVE-2022-22739", resolvedInVersion: "96.0"}, {ID: "CVE-2022-22755", resolvedInVersion: "97.0"}, {ID: "CVE-2022-22757", resolvedInVersion: "97.0"}, {ID: "CVE-2022-1097", resolvedInVersion: "99.0"}, {ID: "CVE-2022-22754", resolvedInVersion: "97.0"}, {ID: "CVE-2022-22748", resolvedInVersion: "96.0"}, {ID: "CVE-2022-22736", resolvedInVersion: "96.0"}, {ID: "CVE-2022-22745", resolvedInVersion: "96.0"}, {ID: "CVE-2022-26385", resolvedInVersion: "98.0"}, {ID: "CVE-2022-26383", resolvedInVersion: "98.0"}, {ID: "CVE-2022-3266", resolvedInVersion: "105.0"}, {ID: "CVE-2022-34468", resolvedInVersion: "102.0"}, {ID: "CVE-2022-34481", resolvedInVersion: "102.0"}, {ID: "CVE-2022-28289", resolvedInVersion: "99.0"}, {ID: "CVE-2022-22741", resolvedInVersion: "96.0"}, {ID: "CVE-2022-28284", resolvedInVersion: "99.0"}, {ID: "CVE-2022-34484", resolvedInVersion: "102.0"}, {ID: "CVE-2022-22752", resolvedInVersion: "96.0"}, {ID: "CVE-2022-26485", resolvedInVersion: "97.0.2"}, {ID: "CVE-2022-28286", resolvedInVersion: "99.0"}, {ID: "CVE-2022-28283", resolvedInVersion: "99.0"}, {ID: "CVE-2022-28285", resolvedInVersion: "99.0"}, {ID: "CVE-2022-0843", resolvedInVersion: "97.0"}, {ID: "CVE-2022-29909", resolvedInVersion: "100.0"}, {ID: "CVE-2022-22749", resolvedInVersion: "96.0"}, {ID: "CVE-2022-26384", resolvedInVersion: "98.0"}, {ID: "CVE-2022-28282", resolvedInVersion: "99.0"}, {ID: "CVE-2022-28287", resolvedInVersion: "99.0"}, {ID: "CVE-2022-40956", resolvedInVersion: "105.0"}, {ID: "CVE-2022-22740", resolvedInVersion: "96.0"}, {ID: "CVE-2022-22743", resolvedInVersion: "96.0"}, {ID: "CVE-2022-22764", resolvedInVersion: "97.0"}, {ID: "CVE-2022-22738", resolvedInVersion: "96.0"}, {ID: "CVE-2022-1529", resolvedInVersion: "100.0.2"}, {ID: "CVE-2022-22760", resolvedInVersion: "97.0"}, {ID: "CVE-2022-29916", resolvedInVersion: "100.0"}, {ID: "CVE-2022-29917", resolvedInVersion: "100.0"}, {ID: "CVE-2022-22747", resolvedInVersion: "96.0"}, {ID: "CVE-2022-26382", resolvedInVersion: "98.0"}, {ID: "CVE-2022-22742", resolvedInVersion: "96.0"}, {ID: "CVE-2022-28288", resolvedInVersion: "99.0"}, {ID: "CVE-2022-22756", resolvedInVersion: "97.0"}, {ID: "CVE-2022-26381", resolvedInVersion: "98.0"}, {ID: "CVE-2022-1802", resolvedInVersion: "100.0.2"}, {ID: "CVE-2022-34483", resolvedInVersion: "102.0"}, {ID: "CVE-2022-29915", resolvedInVersion: "100.0"}, } type threadSafeDSMock struct { mu sync.Mutex *mock.Store } func (d *threadSafeDSMock) ListSoftwareCPEs(ctx context.Context) ([]fleet.SoftwareCPE, error) { d.mu.Lock() defer d.mu.Unlock() return d.Store.ListSoftwareCPEs(ctx) } func (d *threadSafeDSMock) InsertSoftwareVulnerability(ctx context.Context, vuln fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) (bool, error) { d.mu.Lock() defer d.mu.Unlock() return d.Store.InsertSoftwareVulnerability(ctx, vuln, src) } func (d *threadSafeDSMock) InsertSoftwareVulnerabilities(ctx context.Context, vulns []fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) ([]fleet.SoftwareVulnerability, error) { d.mu.Lock() defer d.mu.Unlock() return d.Store.InsertSoftwareVulnerabilities(ctx, vulns, src) } func TestTranslateCPEToCVE(t *testing.T) { t.Parallel() ctx := context.Background() // NVD_TEST_VULNDB_DIR can be used to speed up development (sync vulnerability data only once). tempDir := os.Getenv("NVD_TEST_VULNDB_DIR") if tempDir == "" { // download the CVEs once for all sub-tests, and then disable syncing tempDir = t.TempDir() err := nettest.RunWithNetRetry(t, func() error { return DownloadCVEFeed(tempDir, "", false, slog.New(slog.DiscardHandler)) }) require.NoError(t, err) } else { require.DirExists(t, tempDir) t.Logf("Using %s as database path", tempDir) } cveTests := map[string]struct { cpe string excludedCVEs []string includedCVEs []cve // continuesToUpdate indicates if the product/software // continues to register new CVE vulnerabilities. continuesToUpdate bool }{ "cpe:2.3:a:1password:1password:3.9.9:*:*:*:*:macos:*:*": { includedCVEs: []cve{ {ID: "CVE-2012-6369"}, }, continuesToUpdate: false, }, "cpe:2.3:a:1password:1password:3.9.9:*:*:*:*:*:*:*": { includedCVEs: []cve{ {ID: "CVE-2012-6369"}, }, continuesToUpdate: false, }, "cpe:2.3:a:pypa:pip:9.0.3:*:*:*:*:python:*:*": { includedCVEs: []cve{ {ID: "CVE-2019-20916", resolvedInVersion: "19.2"}, {ID: "CVE-2021-3572", resolvedInVersion: "21.1"}, {ID: "CVE-2025-8869", resolvedInVersion: "25.3"}, {ID: "CVE-2023-5752", resolvedInVersion: "23.3"}, {ID: "CVE-2026-1703", resolvedInVersion: "26.0"}, }, continuesToUpdate: true, }, "cpe:2.3:a:mozilla:firefox:93.0:*:*:*:*:windows:*:*": { includedCVEs: firefox93WindowsVulnerabilities, continuesToUpdate: true, }, "cpe:2.3:a:mozilla:firefox:93.0.100:*:*:*:*:windows:*:*": { includedCVEs: firefox93WindowsVulnerabilities, continuesToUpdate: true, }, "cpe:2.3:a:apple:icloud:1.0:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2017-13797", "CVE-2017-2383", "CVE-2017-2366", "CVE-2016-4613", "CVE-2016-4692", "CVE-2016-4743", "CVE-2016-7578", "CVE-2016-7583", "CVE-2016-7586", "CVE-2016-7587", "CVE-2016-7589", "CVE-2016-7592", "CVE-2016-7598", "CVE-2016-7599", "CVE-2016-7610", "CVE-2016-7611", "CVE-2016-7614", "CVE-2016-7632", "CVE-2016-7635", "CVE-2016-7639", "CVE-2016-7640", "CVE-2016-7641", "CVE-2016-7642", "CVE-2016-7645", "CVE-2016-7646", "CVE-2016-7648", "CVE-2016-7649", "CVE-2016-7652", "CVE-2016-7654", "CVE-2016-7656", "CVE-2017-2383", }, continuesToUpdate: true, }, "cpe:2.3:a:clickstudios:passwordstate:9.5.8.4:*:*:*:*:chrome:*:*": { includedCVEs: []cve{ {ID: "CVE-2022-4610", resolvedInVersion: ""}, {ID: "CVE-2022-4611", resolvedInVersion: ""}, {ID: "CVE-2022-4613", resolvedInVersion: ""}, {ID: "CVE-2022-4612", resolvedInVersion: ""}, }, continuesToUpdate: true, }, "cpe:2.3:a:apple:garageband:10.4.11:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2024-54559"}, continuesToUpdate: true, }, "cpe:2.3:o:apple:macos:15.1.1:*:*:*:*:*:*:*": { includedCVEs: []cve{{ID: "CVE-2024-54559", resolvedInVersion: "15.2"}}, continuesToUpdate: true, }, "cpe:2.3:a:avira:password_manager:2.18.4.38471:*:*:*:*:firefox:*:*": { includedCVEs: []cve{ {ID: "CVE-2022-28795"}, }, continuesToUpdate: true, }, "cpe:2.3:a:zoom:zoom:5.0.4301.0407:*:*:*:*:chrome:*:*": { excludedCVEs: []string{"CVE-2021-28133"}, // CVE-2021-28133 is a vulnerability in the Zoom application, not the extension. continuesToUpdate: true, }, "cpe:2.3:a:bitwarden:bitwarden:1.55.0:*:*:*:*:firefox:*:*": { excludedCVEs: []string{"CVE-2023-38840"}, // CVE-2023-38840 is a vulnerability in the Bitwarden application, not the extension. continuesToUpdate: true, }, // No Version Start "cpe:2.3:a:python:setuptools:64:*:*:*:*:*:*:*": { includedCVEs: []cve{ {ID: "CVE-2022-40897", resolvedInVersion: "65.5.1"}, }, continuesToUpdate: true, }, "cpe:2.3:a:mozilla:firefox:93.0.100:*:*:*:*:*:*:*": { includedCVEs: []cve{ // CVE matches multiple products {ID: "CVE-2022-40956", resolvedInVersion: "105.0"}, }, continuesToUpdate: true, }, // Multiple product matches with different version ranges "cpe:2.3:o:apple:macos:14.1:*:*:*:*:*:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-42919", resolvedInVersion: "14.2"}, }, continuesToUpdate: true, }, "cpe:2.3:a:microsoft:windows_subsystem_for_linux:0.63.10:*:*:*:*:visual_studio_code:*:*": { includedCVEs: []cve{ {ID: "CVE-2021-43907", resolvedInVersion: "0.63.11"}, }, continuesToUpdate: false, }, "cpe:2.3:a:github:pull_requests_and_issues:0.66.1:*:*:*:*:visual_studio_code:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-36867", resolvedInVersion: "0.66.2"}, }, continuesToUpdate: false, }, "cpe:2.3:a:microsoft:python_extension:2020.9.1:*:*:*:*:visual_studio_code:*:*": { includedCVEs: []cve{ {ID: "CVE-2020-17163", resolvedInVersion: "2020.9.2"}, {ID: "CVE-2024-49050", resolvedInVersion: "2024.18.2"}, {ID: "CVE-2025-49714", resolvedInVersion: "2025.8.1"}, }, continuesToUpdate: false, }, "cpe:2.3:a:microsoft:jupyter:2023.10.10:*:*:*:*:visual_studio_code:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-36018", resolvedInVersion: "2023.10.1100000000"}, }, continuesToUpdate: false, }, "cpe:2.3:a:microsoft:jupyter:2024.2.0:*:*:*:*:visual_studio_code:*:*": { includedCVEs: []cve{}, continuesToUpdate: false, }, "cpe:2.3:a:microsoft:eslint:2.0.0:*:*:*:*:visual_studio_code:*:*": { includedCVEs: []cve{ {ID: "CVE-2020-1481", resolvedInVersion: "2.1.7"}, }, continuesToUpdate: false, }, "cpe:2.3:a:microsoft:python_extension:2020.4.0:*:*:*:*:visual_studio_code:*:*": { includedCVEs: []cve{ {ID: "CVE-2020-1171", resolvedInVersion: "2020.5.0"}, {ID: "CVE-2020-1192", resolvedInVersion: "2020.5.0"}, {ID: "CVE-2020-17163", resolvedInVersion: "2020.9.2"}, {ID: "CVE-2024-49050", resolvedInVersion: "2024.18.2"}, {ID: "CVE-2025-49714", resolvedInVersion: "2025.8.1"}, }, continuesToUpdate: false, }, // #34323 "cpe:2.3:a:valvesoftware:dota_2:1.0:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2020-7949", "CVE-2020-7950", "CVE-2020-7951", "CVE-2020-7952", "CVE-2020-9005", }, }, "cpe:2.3:a:valvesoftware:dota_2:1.0.0:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2020-7949", "CVE-2020-7950", "CVE-2020-7951", "CVE-2020-7952", "CVE-2020-9005", }, }, "cpe:2.3:a:valvesoftware:dota_2:1.337:*:*:*:*:macos:*:*": { includedCVEs: []cve{ {ID: "CVE-2020-9005" /*resolvedInVersion: "2020-02-17"*/}, // we don't do non-semver resolved-in {ID: "CVE-2020-7950", resolvedInVersion: "7.23f"}, }, continuesToUpdate: true, }, // end of #34323 "cpe:2.3:a:adobe:animate:*:*:*:*:*:macos:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-44325"}, }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:17.0:*:*:*:*:macos:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-42852", resolvedInVersion: "17.1"}, {ID: "CVE-2023-42950", resolvedInVersion: "17.2"}, {ID: "CVE-2024-23273", resolvedInVersion: "17.4"}, }, excludedCVEs: []string{ "CVE-2023-28205", // This vulnerability is for Safari 16.4.0 "CVE-2024-23252", // Rejected CVE }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:16.4.0:*:*:*:*:macos:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-28205", resolvedInVersion: "16.4.1"}, }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:16.2:*:*:*:*:macos:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-28205", resolvedInVersion: "16.4.1"}, }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:16.4.1:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2023-28205", }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:16.5:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2023-28205", }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:16.6:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2023-28205", }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:15.6.1:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2023-28205", }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:14.1.2:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2023-28205", }, continuesToUpdate: true, }, "cpe:2.3:a:apple:safari:13.1.3:*:*:*:*:macos:*:*": { excludedCVEs: []string{ "CVE-2023-28205", }, continuesToUpdate: true, }, "cpe:2.3:a:microsoft:365_apps:16.0.17628.20144:*:*:*:*:windows:*:*": { includedCVEs: []cve{ {ID: "CVE-2024-21402"}, }, excludedCVEs: []string{"CVE-2011-5049"}, // OS vulnerability continuesToUpdate: true, }, "cpe:2.3:a:citrix:workspace:2309.0:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2024-6286"}, continuesToUpdate: true, }, "cpe:2.3:a:citrix:workspace:2203.1:*:*:*:ltsr:windows:*:*": { includedCVEs: []cve{ {ID: "CVE-2024-6286", resolvedInVersion: "2402"}, }, continuesToUpdate: true, }, "cpe:2.3:a:citrix:workspace:2311.1:*:*:*:*:windows:*:*": { includedCVEs: []cve{ {ID: "CVE-2024-6286", resolvedInVersion: "2403.1"}, }, continuesToUpdate: true, }, "cpe:2.3:a:python:python:3.9.6:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2024-4030"}, continuesToUpdate: true, }, // Skipping test while troubleshooting https://github.com/fleetdm/fleet/issues/24286 // // "cpe:2.3:a:python:python:3.9.6:*:*:*:*:windows:*:*": { // includedCVEs: []cve{ // {ID: "CVE-2024-4030", resolvedInVersion: "3.9.20"}, // }, // continuesToUpdate: true, // }, // Ensure malformed ipswitch whatsup cpe is successfully matched to CVE // See https://github.com/fleetdm/fleet/issues/32662. "cpe:2.3:a:ipswitch:whatsup:2006:-:professional:premium:*:*:*:*": { includedCVEs: []cve{ {ID: "CVE-2006-2351"}, {ID: "CVE-2006-2352"}, {ID: "CVE-2006-2353"}, {ID: "CVE-2006-2354"}, {ID: "CVE-2006-2355"}, {ID: "CVE-2006-2356"}, {ID: "CVE-2006-2357"}, }, }, // Tests the expandCPEAliases rule for virtualbox on macOS "cpe:2.3:a:oracle:virtualbox:7.0.6:*:*:*:*:macos:*:*": { includedCVEs: []cve{ {ID: "CVE-2023-21989", resolvedInVersion: "7.0.8"}, {ID: "CVE-2024-21141", resolvedInVersion: "7.0.20"}, }, continuesToUpdate: true, }, "cpe:2.3:a:mozilla:firefox:*:*:*:*:*:*:*:*": { excludedCVEs: []string{"CVE-2024-10004"}, continuesToUpdate: true, }, "cpe:2.3:a:okta:verify:9.27.0:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2024-10327"}, continuesToUpdate: true, }, // CVE-2023-48795 false positive and true positive checks (see #26073) "cpe:2.3:a:microsoft:powershell:7.4.3:*:*:*:*:*:*:*": { excludedCVEs: []string{"CVE-2023-48795", "CVE-2025-21171"}, continuesToUpdate: true, }, "cpe:2.3:a:openbsd:openssh:9.5:p1:*:*:*:*:*:*": { includedCVEs: []cve{{ID: "CVE-2023-48795", resolvedInVersion: "9.6"}}, continuesToUpdate: true, }, "cpe:2.3:a:openbsd:openssh:9.6:*:*:*:*:*:*": { excludedCVEs: []string{"CVE-2023-48795"}, continuesToUpdate: true, }, // end of CVE-2023-48795 checks // CVE-2025-21171 handling // https://github.com/fleetdm/fleet/pull/30767 // NVD lists this CVE for version 7.5 and not 7.5.0 "cpe:2.3:a:microsoft:powershell:7.5.0:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2025-21171"}, continuesToUpdate: true, }, "cpe:2.3:a:microsoft:powershell:7.5.0:*:*:*:*:windows:*:*": { excludedCVEs: []string{"CVE-2025-21171"}, includedCVEs: []cve{{ID: "CVE-2025-30399"}}, continuesToUpdate: true, }, "cpe:2.3:a:microsoft:powershell:7.5.2:*:*:*:*:windows:*:*": { excludedCVEs: []string{"CVE-2025-21171", "CVE-2025-30399"}, continuesToUpdate: true, }, "cpe:2.3:a:microsoft:powershell:7.5:*:*:*:*:macos:*:*": { includedCVEs: []cve{{ID: "CVE-2025-21171"}}, continuesToUpdate: true, }, "cpe:2.3:a:microsoft:powershell:7.5:*:*:*:*:windows:*:*": { includedCVEs: []cve{{ID: "CVE-2025-21171"}}, continuesToUpdate: true, }, // end of CVE-2025-21171 checks "cpe:2.3:a:jetbrains:goland:2022.3.99.123.456:*:*:*:*:macos:*:*": { includedCVEs: []cve{{ID: "CVE-2024-37051", resolvedInVersion: "2023.1.6"}}, continuesToUpdate: true, }, "cpe:2.3:a:jetbrains:goland:2024.3:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2024-37051"}, continuesToUpdate: true, }, "cpe:2.3:a:iterm2:iterm2:3.5.1:*:*:*:*:*:*:*": { includedCVEs: []cve{ {ID: "CVE-2024-38395", resolvedInVersion: "3.5.2"}, // NVD is being flakey so may not show this // {ID: "CVE-2024-38396", resolvedInVersion: "3.5.2"}, }, continuesToUpdate: true, }, "cpe:2.3:a:simple_password_store_project:simple_password_store:1.7.0:*:*:*:*:macos:*:*": { includedCVEs: []cve{{ID: "CVE-2018-12356", resolvedInVersion: "1.7.2"}}, }, "cpe:2.3:a:python:python:3.12.0:*:*:*:*:windows:*:*": { excludedCVEs: []string{"CVE-2024-12254"}, continuesToUpdate: true, }, "cpe:2.3:a:python:python:3.13.0:*:*:*:*:windows:*:*": { excludedCVEs: []string{"CVE-2024-12254"}, continuesToUpdate: true, }, "cpe:2.3:a:python:python:3.14.0:alpha1:*:*:*:windows:*:*": { excludedCVEs: []string{"CVE-2024-12254"}, continuesToUpdate: true, }, "cpe:2.3:a:python:python:3.14.0:alpha2:*:*:*:windows:*:*": { excludedCVEs: []string{"CVE-2024-12254"}, continuesToUpdate: true, }, // NOTE: VulnCheck (the sole source — NVD has no CPE config for these CVEs) // currently mislabels several CPython version ranges under product // cpe:2.3:a:libexpat_project:libexpat instead of cpe:2.3:a:python:python. // libexpat's real versions are 2.x, so the 3.x ranges clearly belong to CPython. // These entries are commented out below until VulnCheck corrects the feed — // uncomment them once upstream relabels the CVEs back to python:python. "cpe:2.3:a:python:python:3.12.0:-:*:*:*:macos:*:*": { includedCVEs: []cve{ // { // ID: "CVE-2025-1795", // resolvedInVersion: "3.12.3", // }, { ID: "CVE-2024-7592", resolvedInVersion: "3.12.6", }, // { // ID: "CVE-2024-6923", // resolvedInVersion: "3.12.5", // }, // { // ID: "CVE-2024-0397", // resolvedInVersion: "3.12.3", // }, // { // ID: "CVE-2024-12254", // resolvedInVersion: "3.12.9", // }, { ID: "CVE-2024-9287", resolvedInVersion: "3.12.8", }, // { // ID: "CVE-2025-0938", // resolvedInVersion: "3.12.9", // }, { ID: "CVE-2023-6507", // TODO: fix missing version here (according to vulncheck it was fixed in // 3.12.1, but the generated feed data doesn't have this value) resolvedInVersion: "", }, // { // ID: "CVE-2024-8088", // resolvedInVersion: "3.12.6", // }, // { // ID: "CVE-2024-4032", // resolvedInVersion: "3.12.4", // }, // { // ID: "CVE-2024-3219", // resolvedInVersion: "3.12.5", // }, // { // ID: "CVE-2024-0450", // resolvedInVersion: "3.12.2", // }, // { // ID: "CVE-2023-6597", // resolvedInVersion: "3.12.1", // }, // { // ID: "CVE-2024-3220", // resolvedInVersion: "3.14.0", // }, { ID: "CVE-2024-6232", resolvedInVersion: "3.12.6", }, }, continuesToUpdate: true, }, "cpe:2.3:a:python:python:3.14.0:alpha1:*:*:*:macos:*:*": { includedCVEs: []cve{ // { // ID: "CVE-2024-12254", // resolvedInVersion: "3.14.0a3", // }, { ID: "CVE-2024-9287", resolvedInVersion: "", }, // { // ID: "CVE-2025-0938", // resolvedInVersion: "3.14.0a5", // }, }, continuesToUpdate: true, }, "cpe:2.3:a:python:python:3.14.0:alpha2:*:*:*:macos:*:*": { includedCVEs: []cve{ // { // ID: "CVE-2024-12254", // resolvedInVersion: "3.14.0a3", // }, // { // ID: "CVE-2025-0938", // resolvedInVersion: "3.14.0a5", // }, }, continuesToUpdate: true, }, "cpe:2.3:a:python:python:3.14.0:alpha3:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2024-12254"}, includedCVEs: []cve{ // { // ID: "CVE-2025-0938", // resolvedInVersion: "3.14.0a5", // }, }, continuesToUpdate: true, }, "cpe:2.3:a:google:chrome:138.0.7204.91:*:*:*:*:linux:*:*": { excludedCVEs: []string{"CVE-2025-6554"}, continuesToUpdate: true, }, "cpe:2.3:a:google:chrome:138.0.7204.92:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2025-6554"}, continuesToUpdate: true, }, "cpe:2.3:a:docker:desktop:4.43.2:*:*:*:*:macos:*:*": { includedCVEs: []cve{{ID: "CVE-2026-2664", resolvedInVersion: "4.62.0"}}, continuesToUpdate: true, }, "cpe:2.3:a:docker:desktop:4.39.0:*:*:*:*:windows:*:*": { includedCVEs: []cve{{ID: "CVE-2026-2664", resolvedInVersion: "4.62.0"}}, continuesToUpdate: true, }, // #41586 - Admin By Request false positives on macOS/Linux // These CVEs are Windows-only but NVD data uses target_sw=* so they would match any platform without our fix. "cpe:2.3:a:fasttracksoftware:admin_by_request:5.2:*:*:*:*:macos:*:*": { excludedCVEs: []string{"CVE-2019-17201", "CVE-2019-17202"}, }, "cpe:2.3:a:fasttracksoftware:admin_by_request:5.2:*:*:*:*:windows:*:*": { includedCVEs: []cve{ {ID: "CVE-2019-17201", resolvedInVersion: "6.2.0.0"}, {ID: "CVE-2019-17202", resolvedInVersion: "6.2.0.0"}, }, }, } cveOSTests := []struct { platform string version string osID uint includedCVEs []string excludedCVEs []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", }, }, { platform: "darwin", version: "15.3", osID: 3, // This was resolved in 15.3, so it should be excluded. See https://github.com/fleetdm/fleet/issues/26561. excludedCVEs: []string{"CVE-2025-24176"}, }, { platform: "darwin", version: "13.2", osID: 4, // macOS Ventura 13.2 < 13.3.1 is vulnerable to CVE-2023-28205 includedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "13.3", osID: 5, // macOS Ventura 13.3.0 is vulnerable (< 13.3.1) includedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "13.3.1", osID: 6, // macOS Ventura 13.3.1 includes system-level WebKit patch for CVE-2023-28205 excludedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "13.4", osID: 7, // macOS Ventura 13.4 is patched (>= 13.3.1) excludedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "13.5", osID: 8, // macOS Ventura 13.5 is patched (>= 13.3.1) excludedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "12.7.6", osID: 9, // macOS Monterey 12.7.6 did NOT receive system-level fix (rely on Safari matching) excludedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "12.6.5", osID: 10, // macOS Monterey 12.6.5 did NOT fix CVE-2023-28205 (only fixed CVE-2023-28206) excludedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "11.7.10", osID: 11, // macOS Big Sur 11.7.10 did NOT receive system-level fix (rely on Safari matching) excludedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "11.7.6", osID: 12, // macOS Big Sur 11.7.6 did NOT fix CVE-2023-28205 (only fixed CVE-2023-28206) excludedCVEs: []string{"CVE-2023-28205"}, }, { platform: "darwin", version: "10.15.7", osID: 13, // macOS Catalina (10.x) should NOT match CVE-2023-28205 (pre-Big Sur) excludedCVEs: []string{"CVE-2023-28205"}, }, } t.Run("find_vulns_on_cpes", func(t *testing.T) { t.Parallel() ds := new(mock.Store) softwareIDToCPEs := make(map[uint]string) ds.ListSoftwareCPEsFunc = func(ctx context.Context) ([]fleet.SoftwareCPE, error) { var softwareCPEs []fleet.SoftwareCPE i := uint(0) for cpe := range cveTests { softwareCPEs = append(softwareCPEs, fleet.SoftwareCPE{CPE: cpe, SoftwareID: i}) softwareIDToCPEs[i] = cpe i++ } 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.InsertSoftwareVulnerabilitiesFunc = func(ctx context.Context, vulns []fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) ([]fleet.SoftwareVulnerability, error) { cveLock.Lock() defer cveLock.Unlock() for _, vuln := range vulns { cpe, ok := softwareIDToCPEs[vuln.SoftwareID] if !ok { return nil, fmt.Errorf("software id -> cpe not found: %d", vuln.SoftwareID) } c := cve{ID: vuln.CVE} if vuln.ResolvedInVersion != nil { c.resolvedInVersion = *vuln.ResolvedInVersion } cvesFound[cpe] = append(cvesFound[cpe], c) } return nil, nil } osCVEsFound := make(map[uint][]string) ds.InsertOSVulnerabilitiesFunc = func(ctx context.Context, vulns []fleet.OSVulnerability, src fleet.VulnerabilitySource) (int64, error) { cveLock.Lock() defer cveLock.Unlock() for _, vuln := range vulns { osCVEsFound[vuln.OSID] = append(osCVEsFound[vuln.OSID], vuln.CVE) } return int64(len(vulns)), nil } ds.DeleteOutOfDateVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error { return nil } ds.DeleteOutOfDateOSVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error { return nil } _, err := TranslateCPEToCVE(ctx, ds, tempDir, slog.New(slog.DiscardHandler), false, time.Now().UTC().Add(-time.Hour)) require.NoError(t, err) require.True(t, ds.DeleteOutOfDateVulnerabilitiesFuncInvoked) require.True(t, ds.DeleteOutOfDateOSVulnerabilitiesFuncInvoked) for cpe, tc := range cveTests { if tc.continuesToUpdate { // Given that new vulnerabilities can be found on these // packages/products, we check that at least the // known ones are found. for _, cve := range tc.includedCVEs { require.Contains(t, cvesFound[cpe], cve, fmt.Sprintf("%s does not contain CVE %#v", cpe, cve)) } } else { // Check for exact match of CVEs found. require.ElementsMatch(t, cvesFound[cpe], tc.includedCVEs, cpe) } for _, cve := range tc.excludedCVEs { for _, cveFound := range cvesFound[cpe] { require.NotEqual(t, cve, cveFound.ID, fmt.Sprintf("%s should not contain %s", cpe, cve)) } } } for _, tc := range cveOSTests { for _, cve := range tc.includedCVEs { require.Contains(t, osCVEsFound[tc.osID], cve) } for _, cve := range tc.excludedCVEs { require.NotContains(t, osCVEsFound[tc.osID], cve) } } }) t.Run("recent_vulns", func(t *testing.T) { t.Parallel() ds := new(mock.Store) safeDS := &threadSafeDSMock{Store: ds} softwareCPEs := []fleet.SoftwareCPE{ {CPE: "cpe:2.3:a:google:chrome:*:*:*:*:*:*:*:*", ID: 1, SoftwareID: 1}, {CPE: "cpe:2.3:a:mozilla:firefox:*:*:*:*:*:*:*:*", ID: 2, SoftwareID: 2}, {CPE: "cpe:2.3:a:haxx:curl:*:*:*:*:*:*:*:*", ID: 3, SoftwareID: 3}, } ds.DeleteOutOfDateVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error { return nil } ds.ListSoftwareCPEsFunc = func(ctx context.Context) ([]fleet.SoftwareCPE, error) { return softwareCPEs, nil } // First call: all vulns are new, return whatever was inserted. ds.InsertSoftwareVulnerabilitiesFunc = func(ctx context.Context, vulns []fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) ([]fleet.SoftwareVulnerability, error) { return vulns, nil } ds.ListOperatingSystemsForPlatformFunc = func(ctx context.Context, p string) ([]fleet.OperatingSystem, error) { return nil, nil } ds.DeleteOutOfDateOSVulnerabilitiesFunc = func(ctx context.Context, source fleet.VulnerabilitySource, olderThan time.Time) error { return nil } ds.InsertOSVulnerabilitiesFunc = func(ctx context.Context, vulns []fleet.OSVulnerability, src fleet.VulnerabilitySource) (int64, error) { return 0, nil } recent, err := TranslateCPEToCVE(ctx, safeDS, tempDir, slog.New(slog.DiscardHandler), true, time.Now().Add(-time.Hour)) require.NoError(t, err) byCPE := make(map[uint]int) for _, cpe := range recent { byCPE[cpe.Affected()]++ } // even if it's somewhat far in the past, I've seen the exact numbers // change a bit between runs with different downloads, so allow for a bit // of wiggle room. assert.Greater(t, byCPE[softwareCPEs[0].SoftwareID], 150, "google chrome CVEs") assert.Greater(t, byCPE[softwareCPEs[1].SoftwareID], 280, "mozilla firefox CVEs") assert.Greater(t, byCPE[softwareCPEs[2].SoftwareID], 10, "curl CVEs") // Call it again but now return no new vulns (all already existed in the DB). ds.InsertSoftwareVulnerabilitiesFunc = func(ctx context.Context, vulns []fleet.SoftwareVulnerability, src fleet.VulnerabilitySource) ([]fleet.SoftwareVulnerability, error) { return nil, nil } recent, err = TranslateCPEToCVE(ctx, safeDS, tempDir, slog.New(slog.DiscardHandler), true, time.Now().UTC().Add(-time.Hour)) require.NoError(t, err) // no recent vulnerability should be reported assert.Len(t, recent, 0) }) } func TestSyncsCVEFromURL(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.RequestURI, ".meta") { fmt.Fprint(w, "lastModifiedDate:2021-08-04T11:10:30-04:00\r\n") fmt.Fprint(w, "size:20967174\r\n") fmt.Fprint(w, "zipSize:1453429\r\n") fmt.Fprint(w, "gzSize:1453293\r\n") fmt.Fprint(w, "sha256:10D7338A1E2D8DB344C381793110B67FCA7D729ADA21624EF089EBA78CCE7B53\r\n") } })) defer ts.Close() tempDir := t.TempDir() cveFeedPrefixURL := ts.URL + "/feeds/json/cve/1.1/" err := DownloadCVEFeed(tempDir, cveFeedPrefixURL, false, slog.New(slog.DiscardHandler)) require.Error(t, err) require.Contains(t, err.Error(), fmt.Sprintf("1 synchronisation error:\n\tunexpected size for \"%s/feeds/json/cve/1.1/nvdcve-1.1-2002.json.gz\" (200 OK): want 1453293, have 0", ts.URL), ) } // This test is using real data from the 2022 NVD feed func TestGetMatchingVersionEndExcluding(t *testing.T) { ctx := context.Background() testDict := loadDict(t, "../testdata/nvdcve-1.1-2022.json.gz") tests := []struct { name string cve string meta *wfn.Attributes want string wantErr bool }{ { name: "happy path with version with no Version Start", cve: "CVE-2022-40897", meta: &wfn.Attributes{ Vendor: "python", Product: "setuptools", Version: "64", }, want: "65.5.1", wantErr: false, }, { name: "CVE matches multiple products", cve: "CVE-2022-40956", meta: &wfn.Attributes{ Vendor: "mozilla", Product: "firefox", Version: "93.0.100", }, want: "105.0", wantErr: false, }, { name: "Nodes has nested Children", cve: "CVE-2022-40961", meta: &wfn.Attributes{ Vendor: "mozilla", Product: "firefox", Version: "93.0.100", }, want: "105.0", wantErr: false, }, { name: "Multiple product matches with different version ranges", cve: "CVE-2022-26697", meta: &wfn.Attributes{ Vendor: "apple", Product: "macos", Version: "12.0", }, want: "12.4", wantErr: false, }, { name: "No version end excluding", cve: "CVE-2022-26834", meta: &wfn.Attributes{ Vendor: "cybozu", Product: "remote_service_manager", Version: "3.1.2", }, want: "", wantErr: false, }, { name: "Version exceeds version end excluding", cve: "CVE-2022-4610", meta: &wfn.Attributes{ Vendor: "clickstudios", Product: "passwordstate", Version: "9.5.8.4", TargetSW: "chrome", }, want: "", wantErr: false, }, { name: "Can compare 4th version part", cve: "CVE-2022-45889", meta: &wfn.Attributes{ Vendor: "planetestream", Product: "planet_estream", Version: "6.72.10.06", }, want: "6.72.10.07", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := getMatchingVersionEndExcluding(ctx, tt.cve, tt.meta, testDict, nil) if (err != nil) != tt.wantErr { t.Errorf("getMatchingVersionEndExcluding() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("got %q, want %q", got, tt.want) } }) } } 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) if err != nil { t.Fatal(err) } return dict } func TestExpandCPEAliases(t *testing.T) { firefox := &wfn.Attributes{ Vendor: "mozilla", Product: "firefox", Version: "93.0.100", } chromePlugin := &wfn.Attributes{ Vendor: "google", Product: "plugin foobar", Version: "93.0.100", TargetSW: "chrome", } vsCodeExtension := &wfn.Attributes{ Vendor: "Microsoft", Product: "foo.extension", Version: "2024.2.1", TargetSW: "visual_studio_code", } vsCodeExtensionAlias := *vsCodeExtension vsCodeExtensionAlias.TargetSW = "visual_studio" pythonCodeExtension := &wfn.Attributes{ Vendor: "microsoft", Product: "python_extension", Version: "2024.2.1", TargetSW: "visual_studio_code", } pythonCodeExtensionAlias1 := *pythonCodeExtension pythonCodeExtensionAlias1.TargetSW = "visual_studio" pythonCodeExtensionAlias2 := *pythonCodeExtension pythonCodeExtensionAlias2.Product = "visual_studio_code" pythonCodeExtensionAlias2.TargetSW = "python" pythonCodeExtensionAlias3 := *pythonCodeExtension pythonCodeExtensionAlias3.Product = "python" python3140Alpha2 := &wfn.Attributes{ Vendor: "python", Product: "python", Version: "3.14.0", Update: "alpha2", TargetSW: "windows", } python3140Alpha2Alias1 := *python3140Alpha2 python3140Alpha2Alias1.Version = "3.14.0a2" python3140Alpha2Alias1.Update = "" python3130Beta1 := &wfn.Attributes{ Vendor: "python", Product: "python", Version: "3.13.0", Update: "beta2", TargetSW: "windows", } python3130Beta1Alias := *python3130Beta1 python3130Beta1Alias.Version = "3.13.0b2" python3130Beta1Alias.Update = "" python3130RC1 := &wfn.Attributes{ Vendor: "python", Product: "python", Version: "3.13.0", Update: "rc1", TargetSW: "windows", } python3130RC1Alias := *python3130RC1 python3130RC1Alias.Version = "3.13.0rc1" python3130RC1Alias.Update = "" ipswitchWhatsup := &wfn.Attributes{ Vendor: "ipswitch", Product: "whatsup", Version: "2006", } ipswitchWhatsupAlias := *ipswitchWhatsup ipswitchWhatsupAlias.Product = "whatsup_professional" pgadminMacOS := &wfn.Attributes{ Vendor: "pgadmin", Product: "pgadmin", Version: "9.10", TargetSW: "macos", } pgadminMacOSAliasPgadmin := *pgadminMacOS pgadminMacOSAliasPgadmin.TargetSW = "postgresql" pgadminMacOSAliasPgadmin_4 := *pgadminMacOS pgadminMacOSAliasPgadmin_4.Product = "pgadmin_4" pgadminMacOSAliasPgadmin_4.TargetSW = "postgresql" pgadminMacOSAliasPgadmin4 := *pgadminMacOS pgadminMacOSAliasPgadmin4.Product = "pgadmin4" pgadminMacOSAliasPgadmin4.TargetSW = "postgresql" pgadminWindows := &wfn.Attributes{ Vendor: "pgadmin", Product: "pgadmin_4", Version: "9.10", TargetSW: "windows", } pgadminWindowsAliasPgadmin := *pgadminWindows pgadminWindowsAliasPgadmin.Product = "pgadmin" pgadminWindowsAliasPgadmin.TargetSW = "postgresql" pgadminWindowsAliasPgadmin_4 := *pgadminWindows pgadminWindowsAliasPgadmin_4.TargetSW = "postgresql" pgadminWindowsAliasPgadmin4 := *pgadminWindows pgadminWindowsAliasPgadmin4.Product = "pgadmin4" pgadminWindowsAliasPgadmin4.TargetSW = "postgresql" for _, tc := range []struct { name string cpeItem *wfn.Attributes expectedAliases []*wfn.Attributes }{ { name: "no expansion without target_sw", cpeItem: firefox, expectedAliases: []*wfn.Attributes{firefox}, }, { name: "no expansion with target_sw", cpeItem: chromePlugin, expectedAliases: []*wfn.Attributes{chromePlugin}, }, { name: "visual studio code extension", cpeItem: vsCodeExtension, expectedAliases: []*wfn.Attributes{vsCodeExtension, &vsCodeExtensionAlias}, }, { name: "python visual studio code extension", cpeItem: pythonCodeExtension, expectedAliases: []*wfn.Attributes{pythonCodeExtension, &pythonCodeExtensionAlias1, &pythonCodeExtensionAlias2, &pythonCodeExtensionAlias3}, }, { name: "pre-release python: 3.14.0 alpha2", cpeItem: python3140Alpha2, expectedAliases: []*wfn.Attributes{python3140Alpha2, &python3140Alpha2Alias1}, }, { name: "pre-release python: 3.13.0 beta1", cpeItem: python3130Beta1, expectedAliases: []*wfn.Attributes{python3130Beta1, &python3130Beta1Alias}, }, { name: "pre-release python: 3.13.0 rc1", cpeItem: python3130RC1, expectedAliases: []*wfn.Attributes{python3130RC1, &python3130RC1Alias}, }, { name: "ipswitch whatsup alias", cpeItem: ipswitchWhatsup, expectedAliases: []*wfn.Attributes{ipswitchWhatsup, &ipswitchWhatsupAlias}, }, { name: "pgadmin on macos", cpeItem: pgadminMacOS, expectedAliases: []*wfn.Attributes{ pgadminMacOS, &pgadminMacOSAliasPgadmin, &pgadminMacOSAliasPgadmin_4, &pgadminMacOSAliasPgadmin4, }, }, { name: "pgadmin on windows", cpeItem: pgadminWindows, expectedAliases: []*wfn.Attributes{ pgadminWindows, &pgadminWindowsAliasPgadmin, &pgadminWindowsAliasPgadmin_4, &pgadminWindowsAliasPgadmin4, }, }, } { t.Run(tc.name, func(t *testing.T) { aliases := expandCPEAliases(tc.cpeItem) require.Equal(t, tc.expectedAliases, aliases) }) } }