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
This commit is contained in:
Lucas Manuel Rodriguez 2024-03-14 16:33:12 -03:00 committed by GitHub
parent 5028722506
commit cf64d85deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1292 additions and 149 deletions

View File

@ -0,0 +1 @@
* Visual Studio extensions added to Fleet's software inventory.

View File

@ -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,
},

View File

@ -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

View File

@ -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.

View File

@ -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)
})
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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
@ -21,14 +21,12 @@ To test these changes locally, you can:
cpe_translations_url: "http://localhost:8082/cpe_translations.json"
```
Environment method
```bash
FLEET_VULNERABILITIES_CPE_TRANSLATIONS_URL="http://localhost:8082/cpe_translations.json" ./build/fleet serve --dev --dev_license --logging_debug
```
4. trigger a vulnerabilities scan
```bash
fleetctl trigger --name vulnerabilities
```

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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"],

View File

@ -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.

View File

@ -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)
})
}
}

View File

@ -145,6 +145,14 @@ func productVariations(s *fleet.Software) []string {
r = append(r, re)
}
// VSCode extensions have a unique s.Name of the form "<vendor>.<extension>" (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 "<vendor>.<extension>" (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 "*"
}

View File

@ -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 {