Skip to content

Commit

Permalink
🧹 Use the registry directly to fetch Windows packages
Browse files Browse the repository at this point in the history
Signed-off-by: Preslav <preslav@mondoo.com>
  • Loading branch information
preslavgerchev committed Jun 7, 2024
1 parent 7b1e2dd commit a0b72fb
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 22 deletions.
8 changes: 4 additions & 4 deletions providers/os/registry/registrykey_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ func GetNativeRegistryKeyItems(path string) ([]RegistryKeyItem, error) {
return res, nil
}

func GetNativeRegistryKeyChildren(path string) ([]RegistryKeyChild, error) {
log.Debug().Str("path", path).Msg("search registry key children using native registry api")
key, path, err := parseRegistryKeyPath(path)
func GetNativeRegistryKeyChildren(fullPath string) ([]RegistryKeyChild, error) {
log.Debug().Str("path", fullPath).Msg("search registry key children using native registry api")
key, path, err := parseRegistryKeyPath(fullPath)
if err != nil {
return nil, err
}
Expand All @@ -137,7 +137,7 @@ func GetNativeRegistryKeyChildren(path string) ([]RegistryKeyChild, error) {

for i, entry := range entries {
res[i] = RegistryKeyChild{
Path: path,
Path: fullPath,
Name: entry,
}
}
Expand Down
124 changes: 106 additions & 18 deletions providers/os/resources/packages/windows_packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import (
"encoding/json"
"fmt"
"io"
"runtime"
"time"

"github.com/cockroachdb/errors"
"github.com/rs/zerolog/log"
"go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory"
"go.mondoo.com/cnquery/v11/providers/os/connection/shared"
"go.mondoo.com/cnquery/v11/providers/os/detector/windows"
"go.mondoo.com/cnquery/v11/providers/os/registry"
"go.mondoo.com/cnquery/v11/providers/os/resources/cpe"
"go.mondoo.com/cnquery/v11/providers/os/resources/powershell"
)
Expand Down Expand Up @@ -81,6 +83,16 @@ const (
Defender
)

const installedAppsScript = `
Get-ItemProperty (@(
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKCU:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
) | Where-Object { Test-Path $_ }) |
Select-Object -Property DisplayName,DisplayVersion,Publisher,EstimatedSize,InstallSource,UninstallString | ConvertTo-Json -Compress
`

var (
WINDOWS_QUERY_HOTFIXES = `Get-HotFix | Select-Object -Property Status, Description, HotFixId, Caption, InstalledOn, InstalledBy | ConvertTo-Json`
WINDOWS_QUERY_APPX_PACKAGES = `Get-AppxPackage -AllUsers | Select Name, PackageFullName, Architecture, Version, Publisher | ConvertTo-Json`
Expand Down Expand Up @@ -203,15 +215,91 @@ func (w *WinPkgManager) Format() string {
return "win"
}

const installedAppsScript = `
Get-ItemProperty (@(
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',
'HKCU:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'
) | Where-Object { Test-Path $_ }) |
Select-Object -Property DisplayName,DisplayVersion,Publisher,EstimatedSize,InstallSource,UninstallString | ConvertTo-Json -Compress
`
func (w *WinPkgManager) getLocalInstalledApps() ([]Package, error) {
pkgs := []string{
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"HKLM\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
"HKCU\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
}
packages := []Package{}
for _, r := range pkgs {
// we get all the packages, found under the pkgs paths
children, err := registry.GetNativeRegistryKeyChildren(r)
if err != nil {
continue
}
for _, c := range children {
// for each package the information is contained as items of that registry's key,
// so we request the items under the fully qualified path
items, err := registry.GetNativeRegistryKeyItems(c.Path + "\\" + c.Name)
if err != nil {
log.Debug().Err(err).Str("path", c.Path).Msg("could not read registry key children")
continue
}
p := getPackageFromRegistryKeyItems(items)
if p == nil {
continue
}
packages = append(packages, *p)
}
}
return packages, nil
}

func (w *WinPkgManager) getInstalledApps() ([]Package, error) {
if w.conn.Type() == shared.Type_Local && runtime.GOOS == "windows" {
return w.getLocalInstalledApps()
}

cmd, err := w.conn.RunCommand(powershell.Encode(installedAppsScript))
if err != nil {
return nil, fmt.Errorf("could not read app package list")
}
return ParseWindowsAppPackages(cmd.Stdout)
}

func getPackageFromRegistryKeyItems(children []registry.RegistryKeyItem) *Package {
var uninstallString string
var displayName string
var displayVersion string
var publisher string

for _, i := range children {
switch i.Key {
case "UninstallString":
uninstallString = i.Value.String
case "DisplayName":
displayName = i.Value.String
case "DisplayVersion":
displayVersion = i.Value.String
case "Publisher":
publisher = i.Value.String
}
}

if uninstallString == "" {
return nil
}

pkg := &Package{
Name: displayName,
Version: displayVersion,
Format: "windows/app",
}

if displayName != "" && displayVersion != "" {
cpeWfn, err := cpe.NewPackage2Cpe(publisher, displayName, displayVersion, "", "")
if err != nil {
log.Debug().Err(err).Str("name", displayName).Str("version", displayVersion).Msg("could not create cpe for windows app package")
} else {
pkg.CPE = cpeWfn
}
} else {
log.Debug().Msg("ignored package since information is missing")
}
return pkg
}

// returns installed appx packages as well as hot fixes
func (w *WinPkgManager) List() ([]Package, error) {
Expand All @@ -221,19 +309,20 @@ func (w *WinPkgManager) List() ([]Package, error) {
}

pkgs := []Package{}

cmd, err := w.conn.RunCommand(powershell.Encode(installedAppsScript))
if err != nil {
return nil, fmt.Errorf("could not read app package list")
}
appPkgs, err := ParseWindowsAppPackages(cmd.Stdout)
appPkgs, err := w.getInstalledApps()
if err != nil {
return nil, fmt.Errorf("could not read app package list")
}
pkgs = append(pkgs, appPkgs...)

// only win 10+ are compatible with app x packages
canRunCmd := w.conn.Capabilities().Has(shared.Capability_RunCommand)
if !canRunCmd {
log.Debug().Msg("cannot run command on windows, skipping appx package and hotfixes list")
return pkgs, nil
}

if b.Build > 10240 {
// only win 10+ are compatible with app x packages
cmd, err := w.conn.RunCommand(powershell.Wrap(WINDOWS_QUERY_APPX_PACKAGES))
if err != nil {
return nil, fmt.Errorf("could not read appx package list")
Expand All @@ -246,7 +335,7 @@ func (w *WinPkgManager) List() ([]Package, error) {
}

// hotfixes
cmd, err = w.conn.RunCommand(powershell.Wrap(WINDOWS_QUERY_HOTFIXES))
cmd, err := w.conn.RunCommand(powershell.Wrap(WINDOWS_QUERY_HOTFIXES))
if err != nil {
return nil, errors.Wrap(err, "could not fetch hotfixes")
}
Expand All @@ -257,7 +346,6 @@ func (w *WinPkgManager) List() ([]Package, error) {
hotfixAsPkgs := HotFixesToPackages(hotfixes)

pkgs = append(pkgs, hotfixAsPkgs...)

return pkgs, nil
}

Expand Down
75 changes: 75 additions & 0 deletions providers/os/resources/packages/windows_packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/stretchr/testify/require"
"go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory"
"go.mondoo.com/cnquery/v11/providers/os/connection/mock"
"go.mondoo.com/cnquery/v11/providers/os/registry"
"go.mondoo.com/cnquery/v11/providers/os/resources/cpe"
"go.mondoo.com/cnquery/v11/providers/os/resources/powershell"
)

Expand Down Expand Up @@ -109,3 +111,76 @@ func TestWindowsHotFixParser(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 0, len(hotfixes), "detected the right amount of packages")
}

func TestGetPackageFromRegistryKeyItems(t *testing.T) {
t.Run("get package from registry key items that are empty", func(t *testing.T) {
items := []registry.RegistryKeyItem{}
p := getPackageFromRegistryKeyItems(items)
assert.Nil(t, p)
})
t.Run("get package from registry key items with missing required values", func(t *testing.T) {
items := []registry.RegistryKeyItem{
{
Key: "DisplayName",
Value: registry.RegistryKeyValue{
Kind: registry.SZ,
String: "Microsoft Visual C++ 2015-2019 Redistributable (x86) - 14.28.29913",
},
},
}
p := getPackageFromRegistryKeyItems(items)
assert.Nil(t, p)
})

t.Run("get package from registry key items", func(t *testing.T) {
items := []registry.RegistryKeyItem{
{
Key: "DisplayName",
Value: registry.RegistryKeyValue{
Kind: registry.SZ,
String: "Microsoft Visual C++ 2015-2019 Redistributable (x86) - 14.28.29913",
},
},
{
Key: "UninstallString",
Value: registry.RegistryKeyValue{
Kind: registry.SZ,
String: "UninstallString",
},
},
{
Key: "DisplayVersion",
Value: registry.RegistryKeyValue{
Kind: registry.SZ,
String: "14.28.29913.0",
},
},
{
Key: "Publisher",
Value: registry.RegistryKeyValue{
Kind: registry.SZ,
String: "Microsoft Corporation",
},
},
}
p := getPackageFromRegistryKeyItems(items)
CPE, err := cpe.NewPackage2Cpe(
"Microsoft Corporation",
"Microsoft Visual C++ 2015-2019 Redistributable (x86) - 14.28.29913",
"14.28.29913.0",
"",
"")

assert.Nil(t, err)

expected := &Package{
Name: "Microsoft Visual C++ 2015-2019 Redistributable (x86) - 14.28.29913",
Version: "14.28.29913.0",
Arch: "",
Format: "windows/app",
CPE: CPE,
}
assert.NotNil(t, p)
assert.Equal(t, expected, p)
})
}

0 comments on commit a0b72fb

Please sign in to comment.