mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add Windows Program Files scan for software without registry entries (#42992)
This commit is contained in:
parent
577fe75c54
commit
1f45f5383a
4 changed files with 446 additions and 1 deletions
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue