From cf64d85deb2f559497bd3bf5b9a9c0e7cdb8b770 Mon Sep 17 00:00:00 2001 From: Lucas Manuel Rodriguez Date: Thu, 14 Mar 2024 16:33:12 -0300 Subject: [PATCH] Add visual studio extensions to software inventory (#17501) #17003 - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [X] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [x] Added/updated tests - [X] Manual QA for all new/changed functionality --- changes/17003-ingest-vscode_extensions | 1 + cmd/osquery-perf/agent.go | 250 ++++++++++++---- .../vscode_extensions_vulnerable.software | 5 + server/service/osquery.go | 70 +++++ server/service/osquery_test.go | 278 +++++++++++++++++- server/service/osquery_utils/queries.go | 31 ++ server/service/osquery_utils/queries_test.go | 2 +- server/vulnerabilities/nvd/README.md | 4 +- .../vulnerabilities/nvd/cpe_matching_rule.go | 6 + .../vulnerabilities/nvd/cpe_matching_rules.go | 16 + server/vulnerabilities/nvd/cpe_test.go | 261 +++++++++++++++- .../vulnerabilities/nvd/cpe_translations.json | 210 +++++++++++++ server/vulnerabilities/nvd/cve.go | 144 +++++---- server/vulnerabilities/nvd/cve_test.go | 140 ++++++++- server/vulnerabilities/nvd/sanitize.go | 18 ++ server/vulnerabilities/nvd/sanitize_test.go | 5 + 16 files changed, 1292 insertions(+), 149 deletions(-) create mode 100644 changes/17003-ingest-vscode_extensions create mode 100644 cmd/osquery-perf/vscode_extensions_vulnerable.software diff --git a/changes/17003-ingest-vscode_extensions b/changes/17003-ingest-vscode_extensions new file mode 100644 index 0000000000..a8ffcdf7ef --- /dev/null +++ b/changes/17003-ingest-vscode_extensions @@ -0,0 +1 @@ +* Visual Studio extensions added to Fleet's software inventory. diff --git a/cmd/osquery-perf/agent.go b/cmd/osquery-perf/agent.go index 1e76edd1bb..eea736954a 100644 --- a/cmd/osquery-perf/agent.go +++ b/cmd/osquery-perf/agent.go @@ -38,17 +38,21 @@ var ( //go:embed *.tmpl templatesFS embed.FS - //go:embed *.software + //go:embed macos_vulnerable.software macOSVulnerableSoftwareFS embed.FS + //go:embed vscode_extensions_vulnerable.software + vsCodeExtensionsVulnerableSoftwareFS embed.FS + //go:embed ubuntu_2204-software.json.bz2 ubuntuSoftwareFS embed.FS //go:embed windows_11-software.json.bz2 windowsSoftwareFS embed.FS - macosVulnerableSoftware []fleet.Software - windowsSoftware []map[string]string - ubuntuSoftware []map[string]string + macosVulnerableSoftware []fleet.Software + vsCodeExtensionsVulnerableSoftware []fleet.Software + windowsSoftware []map[string]string + ubuntuSoftware []map[string]string ) func loadMacOSVulnerableSoftware() { @@ -72,6 +76,28 @@ func loadMacOSVulnerableSoftware() { log.Printf("Loaded %d vulnerable macOS software", len(macosVulnerableSoftware)) } +func loadExtraVulnerableSoftware() { + vsCodeExtensionsVulnerableSoftwareData, err := vsCodeExtensionsVulnerableSoftwareFS.ReadFile("vscode_extensions_vulnerable.software") + if err != nil { + log.Fatal("reading vulnerable vscode_extensions software file: ", err) + } + lines := bytes.Split(vsCodeExtensionsVulnerableSoftwareData, []byte("\n")) + for _, line := range lines { + parts := bytes.Split(line, []byte("##")) + if len(parts) < 3 { + log.Println("skipping", string(line)) + continue + } + vsCodeExtensionsVulnerableSoftware = append(vsCodeExtensionsVulnerableSoftware, fleet.Software{ + Vendor: strings.TrimSpace(string(parts[0])), + Name: strings.TrimSpace(string(parts[1])), + Version: strings.TrimSpace(string(parts[2])), + Source: "vscode_extensions", + }) + } + log.Printf("Loaded %d vulnerable vscode_extensions software", len(vsCodeExtensionsVulnerableSoftware)) +} + func loadSoftwareItems(fs embed.FS, path string) []map[string]string { bz2, err := fs.Open(path) if err != nil { @@ -103,6 +129,7 @@ func loadSoftwareItems(fs embed.FS, path string) []map[string]string { func init() { loadMacOSVulnerableSoftware() + loadExtraVulnerableSoftware() windowsSoftware = loadSoftwareItems(windowsSoftwareFS, "windows_11-software.json.bz2") ubuntuSoftware = loadSoftwareItems(ubuntuSoftwareFS, "ubuntu_2204-software.json.bz2") } @@ -333,21 +360,22 @@ func (n *nodeKeyManager) Add(nodekey string) { } type agent struct { - agentIndex int - softwareCount softwareEntityCount - userCount entityCount - policyPassProb float64 - munkiIssueProb float64 - munkiIssueCount int - liveQueryFailProb float64 - liveQueryNoResultsProb float64 - strings map[string]string - serverAddress string - stats *Stats - nodeKeyManager *nodeKeyManager - nodeKey string - templates *template.Template - os string + agentIndex int + softwareCount softwareEntityCount + softwareVSCodeExtensionsCount softwareExtraEntityCount + userCount entityCount + policyPassProb float64 + munkiIssueProb float64 + munkiIssueCount int + liveQueryFailProb float64 + liveQueryNoResultsProb float64 + strings map[string]string + serverAddress string + stats *Stats + nodeKeyManager *nodeKeyManager + nodeKey string + templates *template.Template + os string // deviceAuthToken holds Fleet Desktop device authentication token. // // Non-nil means the agent is identified as orbit osquery, @@ -373,6 +401,9 @@ type agent struct { // single goroutine at a time can execute scripts. scriptExecRunning atomic.Bool + softwareVSCodeExtensionsProb float64 + softwareVSCodeExtensionsFailProb float64 + // // The following are exported to be used by the templates. // @@ -412,13 +443,23 @@ type softwareEntityCount struct { uniqueSoftwareUninstallCount int uniqueSoftwareUninstallProb float64 } +type softwareExtraEntityCount struct { + entityCount + commonSoftwareUninstallCount int + commonSoftwareUninstallProb float64 + uniqueSoftwareUninstallCount int + uniqueSoftwareUninstallProb float64 +} func newAgent( agentIndex int, serverAddress, enrollSecret string, templates *template.Template, configInterval, logInterval, queryInterval, mdmCheckInInterval time.Duration, + softwareQueryFailureProb float64, + softwareVSCodeExtensionsQueryFailureProb float64, softwareCount softwareEntityCount, + softwareVSCodeExtensionsCount softwareExtraEntityCount, userCount entityCount, policyPassProb float64, orbitProb float64, @@ -481,30 +522,34 @@ func newAgent( } return &agent{ - agentIndex: agentIndex, - serverAddress: serverAddress, - softwareCount: softwareCount, - userCount: userCount, - strings: make(map[string]string), - policyPassProb: policyPassProb, - munkiIssueProb: munkiIssueProb, - munkiIssueCount: munkiIssueCount, - liveQueryFailProb: liveQueryFailProb, - liveQueryNoResultsProb: liveQueryNoResultsProb, - templates: templates, - deviceAuthToken: deviceAuthToken, - os: agentOS, + agentIndex: agentIndex, + serverAddress: serverAddress, + softwareCount: softwareCount, + softwareVSCodeExtensionsCount: softwareVSCodeExtensionsCount, + userCount: userCount, + strings: make(map[string]string), + policyPassProb: policyPassProb, + munkiIssueProb: munkiIssueProb, + munkiIssueCount: munkiIssueCount, + liveQueryFailProb: liveQueryFailProb, + liveQueryNoResultsProb: liveQueryNoResultsProb, + templates: templates, + deviceAuthToken: deviceAuthToken, + os: agentOS, + EnrollSecret: enrollSecret, + ConfigInterval: configInterval, + LogInterval: logInterval, + QueryInterval: queryInterval, + MDMCheckInInterval: mdmCheckInInterval, + UUID: hostUUID, + SerialNumber: serialNumber, - EnrollSecret: enrollSecret, - ConfigInterval: configInterval, - LogInterval: logInterval, - QueryInterval: queryInterval, - MDMCheckInInterval: mdmCheckInInterval, - UUID: hostUUID, - SerialNumber: serialNumber, + softwareVSCodeExtensionsProb: softwareQueryFailureProb, + softwareVSCodeExtensionsFailProb: softwareVSCodeExtensionsQueryFailureProb, + + macMDMClient: macMDMClient, + winMDMClient: winMDMClient, - macMDMClient: macMDMClient, - winMDMClient: winMDMClient, disableScriptExec: disableScriptExec, disableFleetDesktop: disableFleetDesktop, loggerTLSMaxLines: loggerTLSMaxLines, @@ -1223,12 +1268,12 @@ func (a *agent) softwareMacOS() []map[string]string { lastOpenedAt = l.Format(time.UnixDate) } commonSoftware[i] = map[string]string{ - "name": fmt.Sprintf("Common_%d", i), + "name": fmt.Sprintf("Common_%d.app", i), "version": "0.0.1", - "bundle_identifier": "com.fleetdm.osquery-perf", - "source": "osquery-perf", + "bundle_identifier": fmt.Sprintf("com.fleetdm.osquery-perf.common_%s_%d", a.CachedString("hostname"), i), + "source": "apps", "last_opened_at": lastOpenedAt, - "installed_path": fmt.Sprintf("/some/path/Common_%d", i), + "installed_path": fmt.Sprintf("/some/path/Common_%d.app", i), } } if a.softwareCount.commonSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareCount.commonSoftwareUninstallProb { @@ -1244,12 +1289,12 @@ func (a *agent) softwareMacOS() []map[string]string { lastOpenedAt = l.Format(time.UnixDate) } uniqueSoftware[i] = map[string]string{ - "name": fmt.Sprintf("Unique_%s_%d", a.CachedString("hostname"), i), + "name": fmt.Sprintf("Unique_%s_%d.app", a.CachedString("hostname"), i), "version": "1.1.1", - "bundle_identifier": "com.fleetdm.osquery-perf", - "source": "osquery-perf", + "bundle_identifier": fmt.Sprintf("com.fleetdm.osquery-perf.unique_%s_%d", a.CachedString("hostname"), i), + "source": "apps", "last_opened_at": lastOpenedAt, - "installed_path": fmt.Sprintf("/some/path/Unique_%s_%d", a.CachedString("hostname"), i), + "installed_path": fmt.Sprintf("/some/path/Unique_%s_%d.app", a.CachedString("hostname"), i), } } if a.softwareCount.uniqueSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareCount.uniqueSoftwareUninstallProb { @@ -1282,6 +1327,52 @@ func (a *agent) softwareMacOS() []map[string]string { return software } +func (a *agent) softwareVSCodeExtensions() []map[string]string { + commonVSCodeExtensionsSoftware := make([]map[string]string, a.softwareVSCodeExtensionsCount.common) + for i := 0; i < len(commonVSCodeExtensionsSoftware); i++ { + commonVSCodeExtensionsSoftware[i] = map[string]string{ + "name": fmt.Sprintf("common.extension_%d", i), + "version": "0.0.1", + "source": "vscode_extensions", + } + } + if a.softwareVSCodeExtensionsCount.commonSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareCount.commonSoftwareUninstallProb { + rand.Shuffle(len(commonVSCodeExtensionsSoftware), func(i, j int) { + commonVSCodeExtensionsSoftware[i], commonVSCodeExtensionsSoftware[j] = commonVSCodeExtensionsSoftware[j], commonVSCodeExtensionsSoftware[i] + }) + commonVSCodeExtensionsSoftware = commonVSCodeExtensionsSoftware[:a.softwareVSCodeExtensionsCount.common-a.softwareVSCodeExtensionsCount.commonSoftwareUninstallCount] + } + uniqueVSCodeExtensionsSoftware := make([]map[string]string, a.softwareVSCodeExtensionsCount.unique) + for i := 0; i < len(uniqueVSCodeExtensionsSoftware); i++ { + uniqueVSCodeExtensionsSoftware[i] = map[string]string{ + "name": fmt.Sprintf("unique.extension_%s_%d", a.CachedString("hostname"), i), + "version": "1.1.1", + "source": "vscode_extensions", + } + } + if a.softwareVSCodeExtensionsCount.uniqueSoftwareUninstallProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsCount.uniqueSoftwareUninstallProb { + rand.Shuffle(len(uniqueVSCodeExtensionsSoftware), func(i, j int) { + uniqueVSCodeExtensionsSoftware[i], uniqueVSCodeExtensionsSoftware[j] = uniqueVSCodeExtensionsSoftware[j], uniqueVSCodeExtensionsSoftware[i] + }) + uniqueVSCodeExtensionsSoftware = uniqueVSCodeExtensionsSoftware[:a.softwareVSCodeExtensionsCount.unique-a.softwareVSCodeExtensionsCount.uniqueSoftwareUninstallCount] + } + var vulnerableVSCodeExtensionsSoftware []map[string]string + for _, vsCodeExtension := range vsCodeExtensionsVulnerableSoftware { + vulnerableVSCodeExtensionsSoftware = append(vulnerableVSCodeExtensionsSoftware, map[string]string{ + "name": vsCodeExtension.Name, + "version": vsCodeExtension.Version, + "vendor": vsCodeExtension.Vendor, + "source": vsCodeExtension.Source, + }) + } + software := append(commonVSCodeExtensionsSoftware, uniqueVSCodeExtensionsSoftware...) + software = append(software, vulnerableVSCodeExtensionsSoftware...) + rand.Shuffle(len(software), func(i, j int) { + software[i], software[j] = software[j], software[i] + }) + return software +} + func (a *agent) DistributedRead() (*distributedReadResponse, error) { request, err := http.NewRequest("POST", a.serverAddress+"/api/osquery/distributed/read", bytes.NewReader([]byte(`{"node_key": "`+a.nodeKey+`"}`))) if err != nil { @@ -1574,6 +1665,7 @@ func (a *agent) processQuery(name, query string) ( ) statusOK := fleet.StatusOK statusNotOK := fleet.OsqueryStatus(1) + results = []map[string]string{} // if a query fails, osquery returns empty results array switch { case strings.HasPrefix(name, liveQueryPrefix): @@ -1624,19 +1716,28 @@ func (a *agent) processQuery(name, query string) ( } return true, results, &ss, nil, nil case name == hostDetailQueryPrefix+"software_macos": - ss := fleet.OsqueryStatus(rand.Intn(2)) + ss := fleet.StatusOK + if a.softwareVSCodeExtensionsProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsProb { + ss = fleet.OsqueryStatus(1) + } if ss == fleet.StatusOK { results = a.softwareMacOS() } return true, results, &ss, nil, nil case name == hostDetailQueryPrefix+"software_windows": - ss := fleet.OsqueryStatus(rand.Intn(2)) + ss := fleet.StatusOK + if a.softwareVSCodeExtensionsProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsProb { + ss = fleet.OsqueryStatus(1) + } if ss == fleet.StatusOK { results = windowsSoftware } return true, results, &ss, nil, nil case name == hostDetailQueryPrefix+"software_linux": - ss := fleet.OsqueryStatus(rand.Intn(2)) + ss := fleet.StatusOK + if a.softwareVSCodeExtensionsProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsProb { + ss = fleet.OsqueryStatus(1) + } if ss == fleet.StatusOK { switch a.os { case "ubuntu": @@ -1644,6 +1745,15 @@ func (a *agent) processQuery(name, query string) ( } } return true, results, &ss, nil, nil + case name == hostDetailQueryPrefix+"software_vscode_extensions": + ss := fleet.StatusOK + if a.softwareVSCodeExtensionsFailProb > 0.0 && rand.Float64() <= a.softwareVSCodeExtensionsFailProb { + ss = fleet.OsqueryStatus(1) + } + if ss == fleet.StatusOK { + results = a.softwareVSCodeExtensions() + } + return true, results, &ss, nil, nil case name == hostDetailQueryPrefix+"disk_space_unix" || name == hostDetailQueryPrefix+"disk_space_windows": ss := fleet.OsqueryStatus(rand.Intn(2)) if ss == fleet.StatusOK { @@ -1889,13 +1999,22 @@ func main() { onlyAlreadyEnrolled = flag.Bool("only_already_enrolled", false, "Only start agents that are already enrolled") nodeKeyFile = flag.String("node_key_file", "", "File with node keys to use") - commonSoftwareCount = flag.Int("common_software_count", 10, "Number of common installed applications reported to fleet") - commonSoftwareUninstallCount = flag.Int("common_software_uninstall_count", 1, "Number of common software to uninstall") - commonSoftwareUninstallProb = flag.Float64("common_software_uninstall_prob", 0.1, "Probability of uninstalling common_software_uninstall_count unique software/s") + softwareQueryFailureProb = flag.Float64("software_query_fail_prob", 0.5, "Probability of the software query failing") + softwareVSCodeExtensionsQueryFailureProb = flag.Float64("software_vscode_extensions_query_fail_prob", 0.5, "Probability of the software vscode_extensions query failing") - uniqueSoftwareCount = flag.Int("unique_software_count", 1, "Number of unique software installed on each host") - uniqueSoftwareUninstallCount = flag.Int("unique_software_uninstall_count", 1, "Number of unique software to uninstall") - uniqueSoftwareUninstallProb = flag.Float64("unique_software_uninstall_prob", 0.1, "Probability of uninstalling unique_software_uninstall_count common software/s") + commonSoftwareCount = flag.Int("common_software_count", 10, "Number of common installed applications reported to fleet") + commonVSCodeExtensionsSoftwareCount = flag.Int("common_vscode_extensions_software_count", 5, "Number of common vscode_extensions installed applications reported to fleet") + commonSoftwareUninstallCount = flag.Int("common_software_uninstall_count", 1, "Number of common software to uninstall") + commonVSCodeExtensionsSoftwareUninstallCount = flag.Int("common_vscode_extensions_software_uninstall_count", 1, "Number of common vscode_extensions software to uninstall") + commonSoftwareUninstallProb = flag.Float64("common_software_uninstall_prob", 0.1, "Probability of uninstalling common_software_uninstall_count unique software/s") + commonVSCodeExtensionsSoftwareUninstallProb = flag.Float64("common_vscode_extensions_software_uninstall_prob", 0.1, "Probability of uninstalling vscode_extensions common_software_uninstall_count unique software/s") + + uniqueSoftwareCount = flag.Int("unique_software_count", 1, "Number of unique software installed on each host") + uniqueVSCodeExtensionsSoftwareCount = flag.Int("unique_vscode_extensions_software_count", 1, "Number of unique vscode_extensions software installed on each host") + uniqueSoftwareUninstallCount = flag.Int("unique_software_uninstall_count", 1, "Number of unique software to uninstall") + uniqueVSCodeExtensionsSoftwareUninstallCount = flag.Int("unique_vscode_extensions_software_uninstall_count", 1, "Number of unique vscode_extensions software to uninstall") + uniqueSoftwareUninstallProb = flag.Float64("unique_software_uninstall_prob", 0.1, "Probability of uninstalling unique_software_uninstall_count common software/s") + uniqueVSCodeExtensionsSoftwareUninstallProb = flag.Float64("unique_vscode_extensions_software_uninstall_prob", 0.1, "Probability of uninstalling unique_vscode_extensions_software_uninstall_count common software/s") vulnerableSoftwareCount = flag.Int("vulnerable_software_count", 10, "Number of vulnerable installed applications reported to fleet") withLastOpenedSoftwareCount = flag.Int("with_last_opened_software_count", 10, "Number of applications that may report a last opened timestamp to fleet") @@ -2016,6 +2135,8 @@ func main() { *logInterval, *queryInterval, *mdmCheckInInterval, + *softwareQueryFailureProb, + *softwareVSCodeExtensionsQueryFailureProb, softwareEntityCount{ entityCount: entityCount{ common: *commonSoftwareCount, @@ -2028,7 +2149,18 @@ func main() { commonSoftwareUninstallProb: *commonSoftwareUninstallProb, uniqueSoftwareUninstallCount: *uniqueSoftwareUninstallCount, uniqueSoftwareUninstallProb: *uniqueSoftwareUninstallProb, - }, entityCount{ + }, + softwareExtraEntityCount{ + entityCount: entityCount{ + common: *commonVSCodeExtensionsSoftwareCount, + unique: *uniqueVSCodeExtensionsSoftwareCount, + }, + commonSoftwareUninstallCount: *commonVSCodeExtensionsSoftwareUninstallCount, + commonSoftwareUninstallProb: *commonVSCodeExtensionsSoftwareUninstallProb, + uniqueSoftwareUninstallCount: *uniqueVSCodeExtensionsSoftwareUninstallCount, + uniqueSoftwareUninstallProb: *uniqueVSCodeExtensionsSoftwareUninstallProb, + }, + entityCount{ common: *commonUserCount, unique: *uniqueUserCount, }, diff --git a/cmd/osquery-perf/vscode_extensions_vulnerable.software b/cmd/osquery-perf/vscode_extensions_vulnerable.software new file mode 100644 index 0000000000..9ea1e4ed63 --- /dev/null +++ b/cmd/osquery-perf/vscode_extensions_vulnerable.software @@ -0,0 +1,5 @@ +Microsoft##ms-vscode-remote.remote-wsl##0.63.10 +GitHub##github.vscode-pull-request-github##0.66.1 +Microsoft##ms-python.python##2020.4.0 +Microsoft##ms-toolsai.jupyter##2023.10.10 +Microsoft##dbaeumer.vscode-eslint##2.0.0 \ No newline at end of file diff --git a/server/service/osquery.go b/server/service/osquery.go index 8342d73f6c..8a77903a88 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -952,6 +952,8 @@ func (svc *Service) SubmitDistributedQueryResults( svc.maybeDebugHost(ctx, host, results, statuses, messages, stats) + preProcessSoftwareResults(host.ID, &results, &statuses, &messages, svc.logger) + var hostWithoutPolicies bool for query, rows := range results { // When receiving this query in the results, we will update the host's @@ -1091,6 +1093,74 @@ func (svc *Service) SubmitDistributedQueryResults( return nil } +// preProcessSoftwareResults will run pre-processing on the responses of the software queries. +// It will move the results from the software extra queries (e.g. software_vscode_extensions) +// into the main software query results (software_{macos|linux|windows}). +// We do this to not grow the main software queries and to ingest +// all software together (one direct ingest function for all software). +func preProcessSoftwareResults( + hostID uint, + results *fleet.OsqueryDistributedQueryResults, + statuses *map[string]fleet.OsqueryStatus, + messages *map[string]string, + logger log.Logger, +) { + vsCodeExtensionsExtraQuery := hostDetailQueryPrefix + "software_vscode_extensions" + preProcessSoftwareExtraResults(vsCodeExtensionsExtraQuery, hostID, results, statuses, messages, logger) +} + +func preProcessSoftwareExtraResults( + softwareExtraQuery string, + hostID uint, + results *fleet.OsqueryDistributedQueryResults, + statuses *map[string]fleet.OsqueryStatus, + messages *map[string]string, + logger log.Logger, +) { + // We always remove the extra query and its results + // in case the main or extra software query failed to execute. + defer delete(*results, softwareExtraQuery) + + status, ok := (*statuses)[softwareExtraQuery] + if !ok { + return // query did not execute, e.g. the table does not exist. + } + failed := status != fleet.StatusOK + if failed { + // extra query executed but with errors, so we return without changing anything. + level.Error(logger).Log( + "query", softwareExtraQuery, + "message", (*messages)[softwareExtraQuery], + "hostID", hostID, + ) + return + } + + // Extract the results of the extra query. + softwareExtraRows, _ := (*results)[softwareExtraQuery] + if len(softwareExtraRows) == 0 { + return + } + + // Append the results of the extra query to the main query. + for _, query := range []string{ + // Only one of these execute in each host. + hostDetailQueryPrefix + "software_macos", + hostDetailQueryPrefix + "software_windows", + hostDetailQueryPrefix + "software_linux", + } { + if _, ok := (*results)[query]; !ok { + continue + } + if status, ok := (*statuses)[query]; ok && status != fleet.StatusOK { + // Do not append results if the main query failed to run. + continue + } + (*results)[query] = append((*results)[query], softwareExtraRows...) + return + } +} + // globalPolicyAutomationsEnabled returns true if any of the global policy automations are enabled. // globalPolicyAutomationsEnabled and teamPolicyAutomationsEnabled are effectively identical. // We could not use Go generics because Go generics does not support accessing common struct fields right now. diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index e4e579212a..6d38c9203c 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -841,8 +841,7 @@ func TestSubmitResultLogsToQueryResultsDoesNotCountNullDataRows(t *testing.T) { assert.True(t, ds.OverwriteQueryResultRowsFuncInvoked) } -type failingLogger struct { -} +type failingLogger struct{} func (n *failingLogger) Write(context.Context, []json.RawMessage) error { return errors.New("some error") @@ -894,7 +893,6 @@ func TestSubmitResultLogsFail(t *testing.T) { err = svc.SubmitResultLogs(ctx, results) require.Error(t, err) assert.Equal(t, http.StatusRequestEntityTooLarge, err.(*osqueryError).Status()) - } func TestGetQueryNameAndTeamIDFromResult(t *testing.T) { @@ -1005,12 +1003,13 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) { assert.Equal(t, len(queries), len(discovery)) // discoveryUsed holds the queries where we know use the distributed discovery feature. discoveryUsed := map[string]struct{}{ - hostDetailQueryPrefix + "google_chrome_profiles": {}, - hostDetailQueryPrefix + "mdm": {}, - hostDetailQueryPrefix + "munki_info": {}, - hostDetailQueryPrefix + "windows_update_history": {}, - hostDetailQueryPrefix + "kubequery_info": {}, - hostDetailQueryPrefix + "orbit_info": {}, + hostDetailQueryPrefix + "google_chrome_profiles": {}, + hostDetailQueryPrefix + "mdm": {}, + hostDetailQueryPrefix + "munki_info": {}, + hostDetailQueryPrefix + "windows_update_history": {}, + hostDetailQueryPrefix + "kubequery_info": {}, + hostDetailQueryPrefix + "orbit_info": {}, + hostDetailQueryPrefix + "software_vscode_extensions": {}, } for name := range queries { require.NotEmpty(t, discovery[name]) @@ -1703,8 +1702,9 @@ func TestDetailQueries(t *testing.T) { // queries) queries, discovery, acc, err := svc.GetDistributedQueries(ctx) require.NoError(t, err) - // +1 for software inventory, +1 for fleet_no_policies_wildcard - if expected := expectedDetailQueriesForPlatform(host.Platform); !assert.Equal(t, len(expected)+1+1, len(queries)) { + // +2 for software inventory (+1 for the main software query +1 software_vscode_extensions) + // +1 for fleet_no_policies_wildcard + if expected := expectedDetailQueriesForPlatform(host.Platform); !assert.Equal(t, len(expected)+2+1, len(queries)) { // this is just to print the diff between the expected and actual query // keys when the count assertion fails, to help debugging - they are not // expected to match. @@ -1968,8 +1968,9 @@ func TestDetailQueries(t *testing.T) { queries, discovery, acc, err = svc.GetDistributedQueries(ctx) require.NoError(t, err) - // +1 software inventory, +1 fleet_no_policies_wildcard query - require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+1+1, len(queries), distQueriesMapKeys(queries)) + // +2 software inventory (+1 main software query and +1 software extra query ) + // +1 fleet_no_policies_wildcard query + require.Equal(t, len(expectedDetailQueriesForPlatform(host.Platform))+2+1, len(queries), distQueriesMapKeys(queries)) verifyDiscovery(t, queries, discovery) assert.Zero(t, acc) } @@ -3506,3 +3507,254 @@ func osqueryMapKeys(m map[string]osquery_utils.DetailQuery) []string { sort.Strings(keys) return keys } + +func TestPreProcessSoftwareResults(t *testing.T) { + foobarApp := map[string]string{ + "name": "Foobar.app", + "version": "1.2.3", + "type": "Application (macOS)", + "bundle_identifier": "com.zoobar.foobar", + "extension_id": "", + "browser": "", + "source": "apps", + "vendor": "", + "last_opened_at": "0", + "installed_path": "/some/path", + } + zoobarApp := map[string]string{ + "name": "Zoobar.app", + "version": "3.2.1", + "type": "Application (macOS)", + "bundle_identifier": "com.acme.zoobar", + "extension_id": "", + "browser": "", + "source": "apps", + "vendor": "", + "last_opened_at": "0", + "installed_path": "/some/other/path", + } + foobarVSCodeExtension := map[string]string{ + "name": "vendor-x.foobar", + "version": "2024.2.1", + "type": "IDE extension (VS Code)", + "bundle_identifier": "", + "extension_id": "", + "browser": "", + "source": "vscode_extensions", + "vendor": "VendorX", + "last_opened_at": "", + "installed_path": "/some/foobar/path", + } + zoobarVSCodeExtension := map[string]string{ + "name": "vendor-x.zoobar", + "version": "2023.2.1", + "type": "IDE extension (VS Code)", + "bundle_identifier": "", + "extension_id": "", + "browser": "", + "source": "vscode_extensions", + "vendor": "VendorX", + "last_opened_at": "", + "installed_path": "/some/zoobar/path", + } + someRow := map[string]string{ + "1": "1", + } + + for _, tc := range []struct { + name string + + resultsIn fleet.OsqueryDistributedQueryResults + statusesIn map[string]fleet.OsqueryStatus + messagesIn map[string]string + + resultsOut fleet.OsqueryDistributedQueryResults + }{ + { + name: "software query works and there are vs code extensions in extra", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK, + hostDetailQueryPrefix + "software_macos": fleet.StatusOK, + hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK, + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + }, + hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{ + foobarVSCodeExtension, + zoobarVSCodeExtension, + }, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + foobarVSCodeExtension, + zoobarVSCodeExtension, + }, + }, + }, + { + name: "software query and extra works and there are no vscode extensions", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK, + hostDetailQueryPrefix + "software_macos": fleet.StatusOK, + hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK, + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + }, + hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{}, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + }, + }, + }, + { + name: "software query works and the software extra status and results are not returned", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK, + hostDetailQueryPrefix + "software_macos": fleet.StatusOK, + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + }, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + }, + }, + }, + { + name: "software doesn't return status or results but the software extra does", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK, + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{ + foobarVSCodeExtension, + zoobarVSCodeExtension, + }, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{}, + }, + { + name: "software query works, but vscode_extensions table doesn't exist", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "software_macos": fleet.StatusOK, + hostDetailQueryPrefix + "software_vscode_extensions": fleet.OsqueryStatus(1), + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + }, + hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{}, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "software_macos": []map[string]string{ + foobarApp, + zoobarApp, + }, + }, + }, + { + name: "software query fails, vscode_extensions table returns results", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "software_macos": fleet.OsqueryStatus(1), + hostDetailQueryPrefix + "software_vscode_extensions": fleet.StatusOK, + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "software_macos": []map[string]string{}, + hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{ + foobarVSCodeExtension, + zoobarVSCodeExtension, + }, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "software_macos": []map[string]string{}, + }, + }, + { + name: "software query fails, software extra query also fails", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "software_macos": fleet.OsqueryStatus(1), + hostDetailQueryPrefix + "software_vscode_extensions": fleet.OsqueryStatus(1), + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "software_macos": []map[string]string{}, + hostDetailQueryPrefix + "software_vscode_extensions": []map[string]string{}, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "software_macos": []map[string]string{}, + }, + }, + { + name: "software inventory turned off", + + statusesIn: map[string]fleet.OsqueryStatus{ + hostDetailQueryPrefix + "other_detail_query": fleet.StatusOK, + }, + resultsIn: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + }, + + resultsOut: fleet.OsqueryDistributedQueryResults{ + hostDetailQueryPrefix + "other_detail_query": []map[string]string{ + someRow, + }, + }, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + preProcessSoftwareResults(1, &tc.resultsIn, &tc.statusesIn, &tc.messagesIn, log.NewNopLogger()) + require.Equal(t, tc.resultsOut, tc.resultsIn) + }) + } +} diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index 878bf0ebf6..27ec3932db 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -722,6 +722,7 @@ SELECT '' AS extension_id, '' AS browser, 'apps' AS source, + '' AS vendor, last_opened_time AS last_opened_at, path AS installed_path FROM apps @@ -734,6 +735,7 @@ SELECT '' AS extension_id, '' AS browser, 'python_packages' AS source, + '' AS vendor, 0 AS last_opened_at, path AS installed_path FROM python_packages @@ -746,6 +748,7 @@ SELECT identifier AS extension_id, browser_type AS browser, 'chrome_extensions' AS source, + '' AS vendor, 0 AS last_opened_at, path AS installed_path FROM cached_users CROSS JOIN chrome_extensions USING (uid) @@ -758,6 +761,7 @@ SELECT identifier AS extension_id, 'firefox' AS browser, 'firefox_addons' AS source, + '' AS vendor, 0 AS last_opened_at, path AS installed_path FROM cached_users CROSS JOIN firefox_addons USING (uid) @@ -770,6 +774,7 @@ SELECT '' AS extension_id, '' AS browser, 'safari_extensions' AS source, + '' AS vendor, 0 AS last_opened_at, path AS installed_path FROM cached_users CROSS JOIN safari_extensions USING (uid) @@ -782,6 +787,7 @@ SELECT '' AS extension_id, '' AS browser, 'homebrew_packages' AS source, + '' AS vendor, 0 AS last_opened_at, path AS installed_path FROM homebrew_packages; @@ -790,6 +796,30 @@ FROM homebrew_packages; DirectIngestFunc: directIngestSoftware, } +// softwareVSCodeExtensions collects VSCode extensions on a separate query for two reasons: +// - vscode_extensions is not available in osquery < 5.11.0. +// - Avoid growing the main `software_{macos|windows|linux}` queries +// (having big queries can cause performance issues or be denylisted). +var softwareVSCodeExtensions = DetailQuery{ + Query: withCachedUsers(`WITH cached_users AS (%s) +SELECT + name, + version, + 'IDE extension (VS Code)' AS type, + '' AS bundle_identifier, + uuid AS extension_id, + '' AS browser, + 'vscode_extensions' AS source, + publisher AS vendor, + '' AS last_opened_at, + path AS installed_path +FROM cached_users CROSS JOIN vscode_extensions USING (uid)`), + Platforms: append(fleet.HostLinuxOSs, "darwin", "windows"), + Discovery: discoveryTable("vscode_extensions"), + // Has no IngestFunc, DirectIngestFunc or DirectTaskIngestFunc because + // the results of this query are appended to the results of the other software queries. +} + var scheduledQueryStats = DetailQuery{ Query: ` SELECT *, @@ -1829,6 +1859,7 @@ func GetDetailQueries( generatedMap["software_linux"] = softwareLinux generatedMap["software_windows"] = softwareWindows generatedMap["software_chrome"] = softwareChrome + generatedMap["software_vscode_extensions"] = softwareVSCodeExtensions } if features != nil && features.EnableHostUsers { diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go index 66121f87ff..022c00aa7d 100644 --- a/server/service/osquery_utils/queries_test.go +++ b/server/service/osquery_utils/queries_test.go @@ -290,7 +290,7 @@ func TestGetDetailQueries(t *testing.T) { sortedKeysCompare(t, queriesWithUsers, qs) queriesWithUsersAndSoftware := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}) - qs = append(baseQueries, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_chrome", "scheduled_query_stats") + qs = append(baseQueries, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions", "software_chrome", "scheduled_query_stats") require.Len(t, queriesWithUsersAndSoftware, len(qs)) sortedKeysCompare(t, queriesWithUsersAndSoftware, qs) diff --git a/server/vulnerabilities/nvd/README.md b/server/vulnerabilities/nvd/README.md index 6f921290e9..8c14c60a18 100644 --- a/server/vulnerabilities/nvd/README.md +++ b/server/vulnerabilities/nvd/README.md @@ -10,7 +10,7 @@ To test these changes locally, you can: 2. host this file on a local web server ```bash - go run ./tools/file-server 8082 ./server/vulnerabilities/nvd + go run ./tools/file-server/main.go 8082 ./server/vulnerabilities/nvd/ ``` 3. (re)launch your local fleet server with one of the following @@ -20,7 +20,6 @@ To test these changes locally, you can: vulnerabilities: cpe_translations_url: "http://localhost:8082/cpe_translations.json" ``` - Environment method ```bash @@ -28,7 +27,6 @@ To test these changes locally, you can: ``` 4. trigger a vulnerabilities scan - ```bash fleetctl trigger --name vulnerabilities ``` diff --git a/server/vulnerabilities/nvd/cpe_matching_rule.go b/server/vulnerabilities/nvd/cpe_matching_rule.go index adbd81e486..222af5e125 100644 --- a/server/vulnerabilities/nvd/cpe_matching_rule.go +++ b/server/vulnerabilities/nvd/cpe_matching_rule.go @@ -41,6 +41,8 @@ type CPEMatchingRule struct { CVEs map[string]struct{} // IgnoreAll will cause all CPEs to not match hence ignoring a CVE. IgnoreAll bool + // IgnoreIf is a function that can determine if a CPE matching rule should be ignored or not. + IgnoreIf func(cpeMeta *wfn.Attributes) bool } // CPEMatches returns true if the provided CPE matches the rule. @@ -53,6 +55,10 @@ func (rule CPEMatchingRule) CPEMatches(cpeMeta *wfn.Attributes) bool { return false } + if rule.IgnoreIf != nil && rule.IgnoreIf(cpeMeta) { + return false + } + ver, err := semver.NewVersion(wfn.StripSlashes(cpeMeta.Version)) if err != nil { return false diff --git a/server/vulnerabilities/nvd/cpe_matching_rules.go b/server/vulnerabilities/nvd/cpe_matching_rules.go index b73ac699c3..76ce97b442 100644 --- a/server/vulnerabilities/nvd/cpe_matching_rules.go +++ b/server/vulnerabilities/nvd/cpe_matching_rules.go @@ -2,6 +2,8 @@ package nvd import ( "fmt" + + "github.com/facebookincubator/nvdtools/wfn" ) type CPEMatchingRules []CPEMatchingRule @@ -181,6 +183,20 @@ func GetKnownNVDBugRules() (CPEMatchingRules, error) { }, }, }, + // These vulnerabilities in the MongoDB client incorrectly match + // the VS Code extension. + CPEMatchingRule{ + CVEs: map[string]struct{}{ + "CVE-2012-6619": {}, + "CVE-2013-1892": {}, + "CVE-2013-2132": {}, + "CVE-2015-1609": {}, + "CVE-2016-6494": {}, + }, + IgnoreIf: func(cpeMeta *wfn.Attributes) bool { + return cpeMeta.TargetSW == "visual_studio_code" + }, + }, } for i, rule := range rules { diff --git a/server/vulnerabilities/nvd/cpe_test.go b/server/vulnerabilities/nvd/cpe_test.go index 9fedaeec3f..8c4298b0d1 100644 --- a/server/vulnerabilities/nvd/cpe_test.go +++ b/server/vulnerabilities/nvd/cpe_test.go @@ -466,8 +466,6 @@ func TestLegacyCPEDB(t *testing.T) { } func TestCPEFromSoftwareIntegration(t *testing.T) { - nettest.Run(t) - testCases := []struct { software fleet.Software cpe string @@ -1334,6 +1332,245 @@ func TestCPEFromSoftwareIntegration(t *testing.T) { }, cpe: "cpe:2.3:a:jetbrains:pycharm:2022.1:*:*:*:*:macos:*:*", }, + { + software: fleet.Software{ + Name: "eamodio.gitlens", + Source: "vscode_extensions", + Version: "14.9.0", + Vendor: "GitKraken", + }, + cpe: "cpe:2.3:a:gitkraken:gitlens:14.9.0:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "ms-python.python", + Source: "vscode_extensions", + Version: "2024.2.1", + Vendor: "Microsoft", + }, + cpe: "cpe:2.3:a:microsoft:python_extension:2024.2.1:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "ms-toolsai.jupyter", + Source: "vscode_extensions", + Version: "2024.2.0", + Vendor: "Microsoft", + }, + cpe: "cpe:2.3:a:microsoft:jupyter:2024.2.0:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "ms-vsliveshare.vsliveshare", + Source: "vscode_extensions", + Version: "1.0.5918", + Vendor: "Microsoft", + }, + cpe: "cpe:2.3:a:microsoft:visual_studio_live_share:1.0.5918:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "dbaeumer.vscode-eslint", + Source: "vscode_extensions", + Version: "2.4.4", + Vendor: "Microsoft", + }, + cpe: "cpe:2.3:a:microsoft:visual_studio_code_eslint_extension:2.4.4:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "vscjava.vscode-maven", + Source: "vscode_extensions", + Version: "0.44.0", + Vendor: "Microsoft", + }, + cpe: "cpe:2.3:a:microsoft:vscode-maven:0.44.0:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "ms-vscode.powershell", + Source: "vscode_extensions", + Version: "2024.0.0", + Vendor: "Microsoft", + }, + cpe: "cpe:2.3:a:microsoft:powershell_extension:2024.0.0:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "ms-vscode-remote.vscode-remote-extensionpack", + Source: "vscode_extensions", + Version: "0.25.0", + Vendor: "Microsoft", + }, + cpe: "cpe:2.3:a:microsoft:remote_development:0.25.0:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "vknabel.vscode-swiftlint", + Source: "vscode_extensions", + Version: "1.8.3", + Vendor: "vknabel", + }, + cpe: "cpe:2.3:a:swiftlint_project:swiftlint:1.8.3:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "vknabel.vscode-swiftformat", + Source: "vscode_extensions", + Version: "1.6.7", + Vendor: "vknabel", + }, + cpe: "cpe:2.3:a:swiftformat_project:swiftformat:1.6.7:*:*:*:*:visual_studio_code:*:*", + }, + { + software: fleet.Software{ + Name: "jbenden.c-cpp-flylint", + Source: "vscode_extensions", + Version: "1.14.0", + Vendor: "Joseph Benden", + }, + cpe: `cpe:2.3:a:c\/c\+\+_advanced_lint_project:c\/c\+\+_advanced_lint:1.14.0:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "stripe.vscode-stripe", + Source: "vscode_extensions", + Version: "2.0.14", + Vendor: "Stripe", + }, + cpe: `cpe:2.3:a:stripe:stripe:2.0.14:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "vscodevim.vim", + Source: "vscode_extensions", + Version: "1.27.2", + Vendor: "vscodevim", + }, + cpe: `cpe:2.3:a:vim_project:vim:1.27.2:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "svelte.svelte-vscode", + Source: "vscode_extensions", + Version: "108.3.1", + Vendor: "Svelte", + }, + cpe: `cpe:2.3:a:svelte:svelte:108.3.1:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "lextudio.restructuredtext", + Source: "vscode_extensions", + Version: "189.3.0", + Vendor: "LeXtudio Inc.", + }, + cpe: `cpe:2.3:a:lextudio:restructuredtext:189.3.0:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "ms-vscode-remote.remote-containers", + Source: "vscode_extensions", + Version: "0.348.0", + Vendor: "Microsoft", + }, + cpe: `cpe:2.3:a:microsoft:remote:0.348.0:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "ms-kubernetes-tools.vscode-kubernetes-tools", + Source: "vscode_extensions", + Version: "0.348.0", + Vendor: "Microsoft", + }, + cpe: `cpe:2.3:a:microsoft:kubernetes_tools:0.348.0:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "ms-dotnettools.vscode-dotnet-sdk", + Source: "vscode_extensions", + Version: "0.8.0", + Vendor: "Microsoft", + }, + cpe: `cpe:2.3:a:microsoft:.net_education_bundle_sdk_install_tool:0.8.0:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "ms-dotnettools.vscode-dotnet-runtime", + Source: "vscode_extensions", + Version: "2.0.2", + Vendor: "Microsoft", + }, + cpe: `cpe:2.3:a:microsoft:.net_install_tool_for_extension_authors:2.0.2:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "ms-vscode-remote.remote-wsl", + Source: "vscode_extensions", + Version: "0.86.0", + Vendor: "Microsoft", + }, + cpe: `cpe:2.3:a:microsoft:windows_subsystem_for_linux:0.86.0:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "mongodb.mongodb-vscode", + Source: "vscode_extensions", + Version: "1.5.0", + Vendor: "MongoDB", + }, + cpe: `cpe:2.3:a:mongodb:mongodb:1.5.0:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "oracle.mysql-shell-for-vs-code", + Source: "vscode_extensions", + Version: "1.14.2", + Vendor: "MongoDB", + }, + cpe: `cpe:2.3:a:oracle:mysql_shell:1.14.2:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "snyk-security.snyk-vulnerability-scanner", + Source: "vscode_extensions", + Version: "2.3.6", + Vendor: "Snyk", + }, + cpe: `cpe:2.3:a:snyk:snyk_security:2.3.6:*:*:*:*:visual_studio_code:*:*`, + }, + { + software: fleet.Software{ + Name: "sourcegraph.cody-ai", + Source: "vscode_extensions", + Version: "1.8.0", + Vendor: "Sourcegraph", + }, + cpe: `cpe:2.3:a:sourcegraph:cody:1.8.0:*:*:*:*:visual_studio_code:*:*`, + }, + // There are vulnerabilities for `cpe:2.3:a:redhat:vscode-xml:` in + // NVD's database but there's no entry for `cpe:2.3:a:redhat:vscode-xml:0.26.1` + // in NVD's CPE database. + /* + { + software: fleet.Software{ + Name: "redhat.vscode-xml", + Source: "vscode_extensions", + Version: "0.26.1", + Vendor: "Red Hat", + }, + cpe: `cpe:2.3:a:redhat:vscode-xml:0.26.1:*:*:*:*:visual_studio_code:*:*`, + }, + */ + { + software: fleet.Software{ + Name: "github.vscode-pull-request-github", + Source: "vscode_extensions", + Version: "0.82.0", + Vendor: "GitHub", + }, + cpe: `cpe:2.3:a:github:pull_requests_and_issues:0.82.0:*:*:*:*:visual_studio_code:*:*`, + }, { software: fleet.Software{ Name: "Google Chrome Helper.app", @@ -1369,18 +1606,22 @@ func TestCPEFromSoftwareIntegration(t *testing.T) { }, } - tempDir := t.TempDir() + // NVD_TEST_CPEDB_PATH can be used to speed up development (sync cpe.sqlite only once). + dbPath := os.Getenv("NVD_TEST_CPEDB_PATH") + if dbPath == "" { + nettest.Run(t) + tempDir := t.TempDir() + err := DownloadCPEDBFromGithub(tempDir, "") + require.NoError(t, err) + dbPath = filepath.Join(tempDir, "cpe.sqlite") + } else { + require.FileExists(t, dbPath) + t.Logf("Using %s as database file", dbPath) + } - err := DownloadCPEDBFromGithub(tempDir, "") - require.NoError(t, err) - - dbPath := filepath.Join(tempDir, "cpe.sqlite") db, err := sqliteDB(dbPath) require.NoError(t, err) - err = DownloadCPETranslationsFromGithub(tempDir, "") - require.NoError(t, err) - cpeTranslationsPath := filepath.Join(".", cpeTranslationsFilename) cpeTranslations, err := loadCPETranslations(cpeTranslationsPath) require.NoError(t, err) diff --git a/server/vulnerabilities/nvd/cpe_translations.json b/server/vulnerabilities/nvd/cpe_translations.json index 2fde92baff..f3eb6b6865 100644 --- a/server/vulnerabilities/nvd/cpe_translations.json +++ b/server/vulnerabilities/nvd/cpe_translations.json @@ -140,6 +140,216 @@ "vendor": ["jetbrains"] } }, + { + "software": { + "name": ["ms-python.python"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["python_extension"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["ms-vsliveshare.vsliveshare"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["visual_studio_live_share"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["dbaeumer.vscode-eslint"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["visual_studio_code_eslint_extension"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["ms-vscode.powershell"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["powershell_extension"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["ms-vscode-remote.vscode-remote-extensionpack"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["remote_development"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["vknabel.vscode-swiftlint"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["swiftlint"], + "vendor": ["swiftlint_project"] + } + }, + { + "software": { + "name": ["vknabel.vscode-swiftformat"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["swiftformat"], + "vendor": ["swiftformat_project"] + } + }, + { + "software": { + "name": ["jbenden.c-cpp-flylint"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["c\\/c\\+\\+_advanced_lint"], + "vendor": ["c\\/c\\+\\+_advanced_lint_project"] + } + }, + { + "software": { + "name": ["stripe.vscode-stripe"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["stripe"], + "vendor": ["stripe"] + } + }, + { + "software": { + "name": ["vscodevim.vim"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["vim"], + "vendor": ["vim_project"] + } + }, + { + "software": { + "name": ["svelte.svelte-vscode"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["svelte"], + "vendor": ["svelte"] + } + }, + { + "software": { + "name": ["ms-vscode-remote.remote-containers"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["remote"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["ms-kubernetes-tools.vscode-kubernetes-tools"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["kubernetes_tools"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["ms-dotnettools.vscode-dotnet-sdk"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": [".net_education_bundle_sdk_install_tool"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["ms-dotnettools.vscode-dotnet-runtime"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": [".net_install_tool_for_extension_authors"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["ms-vscode-remote.remote-wsl"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["windows_subsystem_for_linux"], + "vendor": ["microsoft"] + } + }, + { + "software": { + "name": ["mongodb.mongodb-vscode"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["mongodb"], + "vendor": ["mongodb"] + } + }, + { + "software": { + "name": ["oracle.mysql-shell-for-vs-code"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["mysql_shell"], + "vendor": ["oracle"] + } + }, + { + "software": { + "name": ["snyk-security.snyk-vulnerability-scanner"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["snyk_security"], + "vendor": ["snyk"] + } + }, + { + "software": { + "name": ["sourcegraph.cody-ai"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["cody"], + "vendor": ["sourcegraph"] + } + }, + { + "software": { + "name": ["github.vscode-pull-request-github"], + "source": ["vscode_extensions"] + }, + "filter": { + "product": ["pull_requests_and_issues"], + "vendor": ["github"] + } + }, { "software": { "name": ["Microsoft Edge"], diff --git a/server/vulnerabilities/nvd/cve.go b/server/vulnerabilities/nvd/cve.go index 70fe99254d..eb75e0a98c 100644 --- a/server/vulnerabilities/nvd/cve.go +++ b/server/vulnerabilities/nvd/cve.go @@ -404,66 +404,69 @@ func checkCVEs( // No such vendor in the Vulnerability dictionary continue } - cacheHits := cache.Get([]*wfn.Attributes{CPEItem.GetMeta()}) - for _, matches := range cacheHits { - if len(matches.CPEs) == 0 { - continue - } - if rule, ok := knownNVDBugRules.FindMatch( - matches.CVE.ID(), - ); ok { - if !rule.CPEMatches(CPEItem.GetMeta()) { + cpeItemsWithAliases := expandCPEAliases(CPEItem.GetMeta()) + for _, cpeItem := range cpeItemsWithAliases { + cacheHits := cache.Get([]*wfn.Attributes{cpeItem}) + for _, matches := range cacheHits { + if len(matches.CPEs) == 0 { continue } - } - // For chrome/firefox extensions we only want to match vulnerabilities - // that are reported explicitly for target_sw == "chrome" or target_sw = "firefox". - // - // Why? In many ocassions the NVD dataset reports vulnerabilities in client applications - // 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 CPEItem.GetMeta().TargetSW == "chrome" || CPEItem.GetMeta().TargetSW == "firefox" { - if !matchesExactTargetSW( - CPEItem.GetMeta().TargetSW, - []string{"chrome", "firefox"}, - matches.CVE.Config(), - ) { - continue - } - } - - resolvedVersion, err := getMatchingVersionEndExcluding(ctx, matches.CVE.ID(), CPEItem.GetMeta(), dict, logger) - if err != nil { - level.Debug(logger).Log("err", err) - } - - if _, ok := CPEItem.(softwareCPEWithNVDMeta); ok { - - vuln := fleet.SoftwareVulnerability{ - SoftwareID: CPEItem.GetID(), - CVE: matches.CVE.ID(), - ResolvedInVersion: ptr.String(resolvedVersion), + if rule, ok := knownNVDBugRules.FindMatch( + matches.CVE.ID(), + ); ok { + if !rule.CPEMatches(cpeItem) { + continue + } } - 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), + // For chrome/firefox extensions we only want to match vulnerabilities + // that are reported explicitly for target_sw == "chrome" or target_sw = "firefox". + // + // Why? In many occasions the NVD dataset reports vulnerabilities in client applications + // 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 cpeItem.TargetSW == "chrome" || cpeItem.TargetSW == "firefox" { + if !matchesExactTargetSW( + cpeItem.TargetSW, + []string{"chrome", "firefox"}, + matches.CVE.Config(), + ) { + continue + } } - osMu.Lock() - foundOSVulns = append(foundOSVulns, vuln) - osMu.Unlock() - } + resolvedVersion, err := getMatchingVersionEndExcluding(ctx, matches.CVE.ID(), cpeItem, dict, logger) + if err != nil { + level.Debug(logger).Log("err", err) + } + if _, ok := CPEItem.(softwareCPEWithNVDMeta); ok { + 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(): level.Debug(logger).Log("msg", "quitting") @@ -485,6 +488,45 @@ func checkCVEs( return foundSoftwareVulns, foundOSVulns, nil } +// expandCPEAliases will generate new *wfn.Attributes from the given cpeItem. +// It returns a slice with the given cpeItem plus the generated *wfn.Attributes. +// +// We need this because entries in the CPE database are not consistent. +// E.g. some Visual Studio Code extensions are defined with target_sw=visual_studio_code +// and others are defined with target_sw=visual_studio. +// E.g. The python extension for Visual Studio Code is defined with +// product=python_extension,target_sw=visual_studio_code and with +// product=visual_studio_code,target_sw=python. +func expandCPEAliases(cpeItem *wfn.Attributes) []*wfn.Attributes { + cpeItems := []*wfn.Attributes{cpeItem} + + // Some VSCode extensions are defined with target_sw=visual_studio_code + // and others are defined with target_sw=visual_studio. + for _, cpeItem := range cpeItems { + if cpeItem.TargetSW == "visual_studio_code" { + cpeItem2 := *cpeItem + cpeItem2.TargetSW = "visual_studio" + cpeItems = append(cpeItems, &cpeItem2) + } + } + + // The python extension is defined in two ways in the CPE database: + // cpe:2.3:a:microsoft:python_extension:2024.2.1:*:*:*:*:visual_studio_code:*:* + // cpe:2.3:a:microsoft:visual_studio_code:2024.2.1:*:*:*:*:python:*:* + for _, cpeItem := range cpeItems { + if cpeItem.TargetSW == "visual_studio_code" && + cpeItem.Vendor == "microsoft" && + cpeItem.Product == "python_extension" { + cpeItem2 := *cpeItem + cpeItem2.Product = "visual_studio_code" + cpeItem2.TargetSW = "python" + cpeItems = append(cpeItems, &cpeItem2) + } + } + + return cpeItems +} + // Returns the versionEndExcluding string for the given CVE and host software meta // data, if it exists in the NVD feed. This effectively gives us the version of the // software it needs to upgrade to in order to address the CVE. diff --git a/server/vulnerabilities/nvd/cve_test.go b/server/vulnerabilities/nvd/cve_test.go index 08028a2606..6f0fc21039 100644 --- a/server/vulnerabilities/nvd/cve_test.go +++ b/server/vulnerabilities/nvd/cve_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" "strings" "sync" "testing" @@ -130,20 +131,26 @@ func (d *threadSafeDSMock) InsertSoftwareVulnerability(ctx context.Context, vuln } func TestTranslateCPEToCVE(t *testing.T) { - nettest.Run(t) - - tempDir := t.TempDir() - ctx := context.Background() - // download the CVEs once for all sub-tests, and then disable syncing - err := nettest.RunWithNetRetry(t, func() error { - // We use cveFeedPrefixURL="https://nvd.nist.gov/feeds/json/cve/1.1/" because a full sync - // with the NVD API 2.0 takes a long time (>15m). These feeds will be deprecated - // on December 15th and this test will start failing then. - return DownloadNVDCVEFeed(tempDir, "https://nvd.nist.gov/feeds/json/cve/1.1/", false, log.NewNopLogger()) - }) - require.NoError(t, err) + // 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 == "" { + nettest.Run(t) + // download the CVEs once for all sub-tests, and then disable syncing + tempDir = t.TempDir() + err := nettest.RunWithNetRetry(t, func() error { + // We use cveFeedPrefixURL="https://nvd.nist.gov/feeds/json/cve/1.1/" because a full sync + // with the NVD API 2.0 takes a long time (>15m). These feeds will be deprecated + // TBD during 2024 and this test will start failing then. + // For more information see: https://nvd.nist.gov/general/news/change-timeline. + return DownloadNVDCVEFeed(tempDir, "https://nvd.nist.gov/feeds/json/cve/1.1/", false, log.NewNopLogger()) + }) + require.NoError(t, err) + } else { + require.DirExists(t, tempDir) + t.Logf("Using %s as database path", tempDir) + } cveTests := map[string]struct { cpe string @@ -261,6 +268,48 @@ func TestTranslateCPEToCVE(t *testing.T) { }, 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"}, + }, + 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:visual_studio_code_eslint_extension: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"}, + }, + continuesToUpdate: false, + }, } cveOSTests := []struct { @@ -701,3 +750,70 @@ func loadDict(t *testing.T, path string) cvefeed.Dictionary { } 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" + + 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}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + aliases := expandCPEAliases(tc.cpeItem) + require.Equal(t, tc.expectedAliases, aliases) + }) + } +} diff --git a/server/vulnerabilities/nvd/sanitize.go b/server/vulnerabilities/nvd/sanitize.go index 42476d7043..33dc144f8a 100644 --- a/server/vulnerabilities/nvd/sanitize.go +++ b/server/vulnerabilities/nvd/sanitize.go @@ -145,6 +145,14 @@ func productVariations(s *fleet.Software) []string { r = append(r, re) } + // VSCode extensions have a unique s.Name of the form "." (aka extension ID) + if s.Source == "vscode_extensions" { + parts := strings.SplitN(s.Name, ".", 2) + if len(parts) == 2 && parts[1] != "" { + r = append(r, parts[1]) + } + } + return r } @@ -178,6 +186,14 @@ func vendorVariations(s *fleet.Software) []string { } } + // VSCode extensions have a unique s.Name of the form "." (aka extension ID) + if s.Source == "vscode_extensions" { + parts := strings.SplitN(s.Name, ".", 2) + if len(parts) == 2 && parts[0] != "" { + r = append(r, parts[0]) + } + } + return r } @@ -211,6 +227,8 @@ func targetSW(s *fleet.Software) string { return `node.js` case "programs": return "windows" + case "vscode_extensions": + return "visual_studio_code" } return "*" } diff --git a/server/vulnerabilities/nvd/sanitize_test.go b/server/vulnerabilities/nvd/sanitize_test.go index c0403e6cf0..7a4e2f4ed7 100644 --- a/server/vulnerabilities/nvd/sanitize_test.go +++ b/server/vulnerabilities/nvd/sanitize_test.go @@ -157,6 +157,11 @@ func TestVariations(t *testing.T) { vendorVariations: []string{"apple", "python3"}, productVariations: []string{"python"}, }, + { + software: fleet.Software{Name: "ms-python.python", Version: "3.8.9", BundleIdentifier: "", Source: "vscode_extensions", Vendor: "Microsoft"}, + vendorVariations: []string{"microsoft", "ms-python"}, + productVariations: []string{"python", "ms-python.python"}, + }, } for _, tc := range variationsTestCases {