Add Windows Program Files scan for software without registry entries (#42992)

This commit is contained in:
Tim Lee 2026-04-11 13:42:50 -06:00 committed by GitHub
parent 577fe75c54
commit 1f45f5383a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 446 additions and 1 deletions

View file

@ -2911,6 +2911,67 @@ func (a *agent) processQuery(name, query string, cachedResults *cachedResults) (
results = append(results, value.(map[string]string))
return true
})
cachedResults.software = results
}
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"software_windows_program_files_scan":
// Given queries run in lexicographic order, software_windows already ran and
// cachedResults.software should have its results.
ss := fleet.StatusOK
if a.softwareQueryFailureProb > 0.0 && rand.Float64() <= a.softwareQueryFailureProb {
ss = fleet.OsqueryStatus(1)
}
if ss == fleet.StatusOK {
// Generate file scan results: some duplicate entries (matching programs installed_paths)
// and some unique entries (new software not in programs).
if len(cachedResults.software) > 0 {
// Pick up to 3 programs entries to create duplicate file scan results
dupeCount := 0
for _, s := range cachedResults.software {
if dupeCount >= 3 {
break
}
if s["source"] != "programs" {
continue
}
installedPath := s["installed_path"]
if installedPath == "" {
continue
}
results = append(results, map[string]string{
"path": installedPath + `\` + s["name"] + ".exe",
"filename": s["name"] + ".exe",
"file_version": s["version"],
"product_version": s["version"],
"size": fmt.Sprintf("%d", rand.Intn(50000000)), //nolint:gosec // load testing
})
dupeCount++
}
}
// Add unique entries that won't be deduplicated
results = append(results,
map[string]string{
"path": `C:\Program Files\Windows Defender\MsMpEng.exe`,
"filename": "MsMpEng.exe",
"file_version": "4.18.25030.2",
"product_version": "4.18.25030.2",
"size": "12345678",
},
map[string]string{
"path": `C:\Program Files\Adobe\DNG Converter\DNGConverter.exe`,
"filename": "DNGConverter.exe",
"file_version": "16.1.0.0",
"product_version": "16.1",
"size": "98765432",
},
map[string]string{
"path": `C:\Program Files\Custom App\Subfolder\customapp.exe`,
"filename": "customapp.exe",
"file_version": "2.5.0.0",
"product_version": "2.5.0",
"size": "5432100",
},
)
}
return true, results, &ss, nil, nil
case name == hostDetailQueryPrefix+"software_linux":

View file

@ -1243,6 +1243,28 @@ SELECT
GROUP BY executable_path
```
## software_windows_program_files_scan
- Description: A software override query[^1] to detect Windows software installed to Program Files without registry entries.
- Platforms: windows
- Query:
```sql
SELECT
path,
filename,
file_version,
product_version,
size
FROM file
WHERE (
path LIKE 'C:\Program Files\%\%.exe'
OR path LIKE 'C:\Program Files\%\%\%.exe'
)
AND path NOT LIKE 'C:\Program Files\WindowsApps\%'
```
## system_info
- Platforms: all

View file

@ -1685,6 +1685,146 @@ var SoftwareOverrideQueries = map[string]DetailQuery{
Discovery: discoveryTable("rpm_package_files"),
SoftwareProcessResults: processPackageLastOpenedAt("rpm_packages"),
},
// windows_program_files_scan detects Windows software installed to C:\Program Files that lacks
// registry Uninstall entries (invisible to the osquery programs table). Scans at depth 0
// (Vendor\app.exe) and depth 1 (Vendor\Subfolder\app.exe), excluding WindowsApps (already
// covered by the programs table). Deduplication against programs entries is handled server-side
// via SoftwareProcessResults. Also enables detection of Windows Defender (MsMpEng.exe) which
// does not create registry entries (#42878).
"windows_program_files_scan": {
Description: "A software override query[^1] to detect Windows software installed to Program Files without registry entries.",
Platforms: []string{"windows"},
Query: `SELECT
path,
filename,
file_version,
product_version,
size
FROM file
WHERE (
path LIKE 'C:\Program Files\%\%.exe'
OR path LIKE 'C:\Program Files\%\%\%.exe'
)
AND path NOT LIKE 'C:\Program Files\WindowsApps\%'`,
SoftwareProcessResults: processProgramFilesScan,
},
}
// processProgramFilesScan deduplicates file scan results against existing programs entries,
// then appends new (non-duplicate) entries to the main software results.
//
// Dedup uses a directory prefix check rather than direct path equality because
// programs.install_location and the file scan path are not directly comparable:
// - install_location is a root directory (e.g. ...\GoLand 2025.3.3), not the exe path
// - The exe may be nested deeper (e.g. ...\GoLand 2025.3.3\bin\goland64.exe)
// - install_location may or may not have a trailing backslash
//
// By normalizing both sides (lowercase, trailing backslash) and checking whether the
// exe's parent directory starts with a known install_location, we correctly match
// executables at any depth beneath a program's install root.
func processProgramFilesScan(mainSoftwareResults, fileScanResults []map[string]string) []map[string]string {
if len(fileScanResults) == 0 {
return mainSoftwareResults
}
// Build a set of normalized installed paths from existing programs entries.
// normalizeWindowsDir ensures consistent trailing backslash and lowercase.
//
// We skip over-broad locations (drive roots, Windows\System32) because some
// programs report install_location as e.g. "C:\" which would prefix-match
// every file scan result. See the analogous skipInstallPaths filter in the
// windows_last_opened_at handler.
skipInstallPaths := map[string]struct{}{
`\`: {},
`\windows\`: {},
`\windows\system32\`: {},
`\program files\`: {},
`\program files (x86)\`: {},
}
knownPaths := make(map[string]struct{})
for _, row := range mainSoftwareResults {
if row["source"] != "programs" {
continue
}
p := normalizeWindowsDir(row["installed_path"])
if p == "" {
continue
}
// Strip the drive letter (e.g. "c:") to get a volume-relative path for skip checking.
if vol, rel, ok := strings.Cut(p, ":"); !ok || len(vol) != 1 || len(rel) == 0 {
continue
} else if _, skip := skipInstallPaths[rel]; skip {
continue
}
knownPaths[p] = struct{}{}
}
for _, row := range fileScanResults {
exePath := row["path"]
if exePath == "" {
continue
}
// Extract and normalize the directory containing the exe.
exeDir := normalizeWindowsDir(windowsDirname(exePath))
// An exe is a duplicate if its directory falls under any known install_location.
// For example, install_location "c:\program files\foo\" matches exe dir
// "c:\program files\foo\bin\" because the exe lives inside that install root.
duplicate := false
for knownPath := range knownPaths {
if strings.HasPrefix(exeDir, knownPath) {
duplicate = true
break
}
}
if duplicate {
continue
}
version := row["product_version"]
if version == "" {
version = row["file_version"]
}
mainSoftwareResults = append(mainSoftwareResults, map[string]string{
"name": row["filename"],
"version": version,
"source": "programs",
"vendor": "",
"installed_path": windowsDirname(exePath),
"extension_id": "",
"extension_for": "",
"upgrade_code": "",
"release": "",
"arch": "",
"bundle_identifier": "",
"last_opened_at": "",
})
}
return mainSoftwareResults
}
// windowsDirname returns the directory portion of a Windows path (everything before the last backslash).
func windowsDirname(path string) string {
if idx := strings.LastIndex(path, `\`); idx >= 0 {
return path[:idx]
}
return path
}
// normalizeWindowsDir lowercases a Windows directory path and ensures it ends with a backslash.
func normalizeWindowsDir(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
path = strings.ToLower(path)
if !strings.HasSuffix(path, `\`) {
path += `\`
}
return path
}
// processPackageLastOpenedAt is a shared function that processes package last_opened_at information
@ -2338,6 +2478,18 @@ var (
s.Release = "" // Clear release to avoid issues with vulnerability matching
},
},
{
// Windows Defender's service executable (MsMpEng.exe) is installed under
// C:\Program Files\Windows Defender without registry Uninstall entries.
// Map it to a user-friendly name for software inventory. (#42878)
matches: func(s *fleet.Software) bool {
return s.Source == "programs" &&
strings.EqualFold(s.Name, "MsMpEng.exe")
},
mutate: func(s *fleet.Software, logger *slog.Logger) {
s.Name = "Windows Defender"
},
},
}
)

View file

@ -224,6 +224,30 @@ func TestSoftwareIngestionMutations(t *testing.T) {
MutateSoftwareOnIngestion(t.Context(), rpmPackage, slog.New(slog.DiscardHandler))
assert.Equal(t, "3.0.7", rpmPackage.Version)
assert.Equal(t, "24.el9", rpmPackage.Release)
// Test Windows Defender sanitizer - MsMpEng.exe → Windows Defender (#18494)
winDefender := &fleet.Software{
Name: "MsMpEng.exe",
Source: "programs",
}
MutateSoftwareOnIngestion(t.Context(), winDefender, slog.New(slog.DiscardHandler))
assert.Equal(t, "Windows Defender", winDefender.Name)
// Test Windows Defender case-insensitive match
winDefenderLower := &fleet.Software{
Name: "msmpeng.exe",
Source: "programs",
}
MutateSoftwareOnIngestion(t.Context(), winDefenderLower, slog.New(slog.DiscardHandler))
assert.Equal(t, "Windows Defender", winDefenderLower.Name)
// Test Windows Defender with wrong source is not mutated
winDefenderWrongSource := &fleet.Software{
Name: "MsMpEng.exe",
Source: "apps",
}
MutateSoftwareOnIngestion(t.Context(), winDefenderWrongSource, slog.New(slog.DiscardHandler))
assert.Equal(t, "MsMpEng.exe", winDefenderWrongSource.Name)
}
func TestDetailQueryNetworkInterfaces(t *testing.T) {
@ -507,7 +531,7 @@ func TestGetDetailQueries(t *testing.T) {
queriesWithUsersAndSoftware := GetDetailQueries(t.Context(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true, EnableSoftwareInventory: true}, Integrations{}, nil)
qs = baseQueries
qs = append(qs, "users", "users_chrome", "software_macos", "software_linux", "software_windows", "software_vscode_extensions", "software_jetbrains_plugins", "software_linux_fleetd_pacman",
"software_chrome", "software_python_packages", "software_python_packages_with_users_dir", "scheduled_query_stats", "software_macos_firefox", "software_macos_codesign", "software_macos_executable_sha256", "software_windows_last_opened_at", "software_deb_last_opened_at", "software_rpm_last_opened_at", "software_windows_acrobat_dc", "software_go_binaries")
"software_chrome", "software_python_packages", "software_python_packages_with_users_dir", "scheduled_query_stats", "software_macos_firefox", "software_macos_codesign", "software_macos_executable_sha256", "software_windows_last_opened_at", "software_deb_last_opened_at", "software_rpm_last_opened_at", "software_windows_acrobat_dc", "software_go_binaries", "software_windows_program_files_scan")
require.Len(t, queriesWithUsersAndSoftware, len(qs))
sortedKeysCompare(t, queriesWithUsersAndSoftware, qs)
@ -3360,6 +3384,192 @@ func TestWindowsAcrobatDC(t *testing.T) {
}
}
func TestWindowsProgramFilesScan(t *testing.T) {
processFunc := SoftwareOverrideQueries["windows_program_files_scan"].SoftwareProcessResults
testCases := []struct {
name string
mainResults []map[string]string
fileScanResults []map[string]string
expected []map[string]string
}{
{
name: "no file scan results returns main unchanged",
mainResults: []map[string]string{
{"name": "Git", "source": "programs", "installed_path": `C:\Program Files\Git\`},
},
fileScanResults: nil,
expected: []map[string]string{
{"name": "Git", "source": "programs", "installed_path": `C:\Program Files\Git\`},
},
},
{
name: "all duplicates filtered out",
mainResults: []map[string]string{
{"name": "Git", "source": "programs", "installed_path": `C:\Program Files\Git\`},
{"name": "CMake", "source": "programs", "installed_path": `C:\Program Files\CMake\`},
},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\Git\cmd\git.exe`, "filename": "git.exe", "product_version": "2.43.0", "file_version": "2.43.0"},
{"path": `C:\Program Files\CMake\bin\cmake.exe`, "filename": "cmake.exe", "product_version": "3.28.1", "file_version": "3.28.1"},
},
expected: []map[string]string{
{"name": "Git", "source": "programs", "installed_path": `C:\Program Files\Git\`},
{"name": "CMake", "source": "programs", "installed_path": `C:\Program Files\CMake\`},
},
},
{
name: "new entries appended with correct columns",
mainResults: []map[string]string{
{"name": "Git", "source": "programs", "installed_path": `C:\Program Files\Git\`},
},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\Windows Defender\MsMpEng.exe`, "filename": "MsMpEng.exe", "product_version": "4.18.25030.2", "file_version": "4.18.25030.2"},
{"path": `C:\Program Files\Adobe\DNG Converter\DNGConverter.exe`, "filename": "DNGConverter.exe", "product_version": "16.1", "file_version": "16.1.0.0"},
},
expected: []map[string]string{
{"name": "Git", "source": "programs", "installed_path": `C:\Program Files\Git\`},
{
"name": "MsMpEng.exe", "version": "4.18.25030.2", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\Windows Defender`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
{
"name": "DNGConverter.exe", "version": "16.1", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\Adobe\DNG Converter`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
},
},
{
name: "version falls back to file_version when product_version is empty",
mainResults: []map[string]string{},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\SomeApp\app.exe`, "filename": "app.exe", "product_version": "", "file_version": "1.0.0.0"},
},
expected: []map[string]string{
{
"name": "app.exe", "version": "1.0.0.0", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\SomeApp`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
},
},
{
name: "case-insensitive path matching deduplicates",
mainResults: []map[string]string{
{"name": "Adobe Acrobat", "source": "programs", "installed_path": `C:\Program Files\Adobe`},
},
fileScanResults: []map[string]string{
{"path": `c:\program files\Adobe\subfolder\tool.exe`, "filename": "tool.exe", "product_version": "1.0", "file_version": "1.0"},
},
expected: []map[string]string{
{"name": "Adobe Acrobat", "source": "programs", "installed_path": `C:\Program Files\Adobe`},
},
},
{
name: "exe in parent directory of known install path is not a duplicate",
mainResults: []map[string]string{
{"name": "GoLand", "source": "programs", "installed_path": `C:\Program Files\JetBrains\GoLand 2025.3.3`},
},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\JetBrains\updater.exe`, "filename": "updater.exe", "product_version": "1.0", "file_version": "1.0"},
},
expected: []map[string]string{
{"name": "GoLand", "source": "programs", "installed_path": `C:\Program Files\JetBrains\GoLand 2025.3.3`},
{
"name": "updater.exe", "version": "1.0", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\JetBrains`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
},
},
{
name: "drive root installed_path does not suppress file scan results",
mainResults: []map[string]string{
{"name": "SomeTool", "source": "programs", "installed_path": `C:\`},
},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\NewApp\app.exe`, "filename": "app.exe", "product_version": "1.0", "file_version": "1.0"},
},
expected: []map[string]string{
{"name": "SomeTool", "source": "programs", "installed_path": `C:\`},
{
"name": "app.exe", "version": "1.0", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\NewApp`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
},
},
{
name: "system32 installed_path does not suppress file scan results",
mainResults: []map[string]string{
{"name": "SysTool", "source": "programs", "installed_path": `C:\Windows\System32`},
},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\AnotherApp\tool.exe`, "filename": "tool.exe", "product_version": "3.0", "file_version": "3.0"},
},
expected: []map[string]string{
{"name": "SysTool", "source": "programs", "installed_path": `C:\Windows\System32`},
{
"name": "tool.exe", "version": "3.0", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\AnotherApp`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
},
},
{
name: "Program Files root installed_path does not suppress file scan results",
mainResults: []map[string]string{
{"name": "BadEntry", "source": "programs", "installed_path": `C:\Program Files`},
},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\SomeVendor\app.exe`, "filename": "app.exe", "product_version": "2.0", "file_version": "2.0"},
},
expected: []map[string]string{
{"name": "BadEntry", "source": "programs", "installed_path": `C:\Program Files`},
{
"name": "app.exe", "version": "2.0", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\SomeVendor`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
},
},
{
name: "non-programs entries in main do not affect dedup",
mainResults: []map[string]string{
{"name": "1Password", "source": "chrome_extensions", "installed_path": `C:\Program Files\SomeApp`},
},
fileScanResults: []map[string]string{
{"path": `C:\Program Files\SomeApp\app.exe`, "filename": "app.exe", "product_version": "2.0", "file_version": "2.0"},
},
expected: []map[string]string{
{"name": "1Password", "source": "chrome_extensions", "installed_path": `C:\Program Files\SomeApp`},
{
"name": "app.exe", "version": "2.0", "source": "programs",
"vendor": "", "installed_path": `C:\Program Files\SomeApp`,
"extension_id": "", "extension_for": "", "upgrade_code": "",
"release": "", "arch": "", "bundle_identifier": "", "last_opened_at": "",
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := processFunc(tc.mainResults, tc.fileScanResults)
require.Equal(t, tc.expected, result)
})
}
}
func TestTPMPinSetVerifyIngest(t *testing.T) {
tests := []struct {
name string