Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⭐️ Add support for static analysis of Windows systems #4203

Merged
merged 3 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions providers/os/connection/fs/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package fs

import (
"errors"
"path/filepath"

"github.com/rs/zerolog/log"
"github.com/spf13/afero"
Expand Down Expand Up @@ -85,6 +86,7 @@ func (c *FileSystemConnection) FileInfo(path string) (shared.FileInfoDetails, er
Size: stat.Size(),
Uid: uid,
Gid: gid,
Path: filepath.Join(c.MountedDir, path),
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions providers/os/connection/shared/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ type FileInfoDetails struct {
Mode FileModeDetails
Uid int64
Gid int64
Path string
}

type FileModeDetails struct {
Expand Down
94 changes: 71 additions & 23 deletions providers/os/detector/detector_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory"
"go.mondoo.com/cnquery/v11/providers/os/connection/shared"
win "go.mondoo.com/cnquery/v11/providers/os/detector/windows"
"go.mondoo.com/cnquery/v11/providers/os/registry"
)

const (
Expand Down Expand Up @@ -703,36 +704,83 @@ var windows = &PlatformResolver{
Name: "windows",
IsFamily: false,
Detect: func(r *PlatformResolver, pf *inventory.Platform, conn shared.Connection) (bool, error) {
data, err := win.GetWmiInformation(conn)
if err != nil {
log.Debug().Err(err).Msg("could not gather wmi information")
return false, nil
}
if conn.Capabilities().Has(shared.Capability_RunCommand) {
data, err := win.GetWmiInformation(conn)
preslavgerchev marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Debug().Err(err).Msg("could not gather wmi information")
}

pf.Name = "windows"
pf.Title = data.Caption
pf.Name = "windows"
pf.Title = data.Caption

// instead of using windows major.minor.build.ubr we just use build.ubr since
// major and minor can be derived from the build version
pf.Version = data.BuildNumber
// instead of using windows major.minor.build.ubr we just use build.ubr since
// major and minor can be derived from the build version
pf.Version = data.BuildNumber

// FIXME: we need to ask wmic cpu get architecture
pf.Arch = data.OSArchitecture
// FIXME: we need to ask wmic cpu get architecture
pf.Arch = data.OSArchitecture

if pf.Labels == nil {
pf.Labels = map[string]string{}
}
pf.Labels["windows.mondoo.com/product-type"] = data.ProductType
if pf.Labels == nil {
pf.Labels = map[string]string{}
}
pf.Labels["windows.mondoo.com/product-type"] = data.ProductType

// optional: try to get the ubr number (win 10 + 2019)
current, err := win.GetWindowsOSBuild(conn)
if err == nil && current.UBR > 0 {
pf.Build = strconv.Itoa(current.UBR)
} else {
log.Debug().Err(err).Msg("could not parse windows current version")
}

// optional: try to get the ubr number (win 10 + 2019)
current, err := win.GetWindowsOSBuild(conn)
if err == nil && current.UBR > 0 {
pf.Build = strconv.Itoa(current.UBR)
} else {
log.Debug().Err(err).Msg("could not parse windows current version")
return true, nil
}

return true, nil
if conn.Capabilities().Has(shared.Capability_FileSearch) {
rh := registry.NewRegistryHandler()
defer func() {
err := rh.UnloadSubkeys()
if err != nil {
log.Debug().Err(err).Msg("could not unload registry subkeys")
}
}()
fi, err := conn.FileInfo(registry.SoftwareRegPath)
if err != nil {
log.Debug().Err(err).Msg("could not find SOFTWARE registry key file")
return false, nil
}
err = rh.LoadSubkey(registry.Software, fi.Path)
if err != nil {
log.Debug().Err(err).Msg("could not load SOFTWARE registry key file")
return false, nil
}

pf.Name = "windows"
productName, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion", "ProductName")
if err == nil {
pf.Title = productName.Value.String
}

ubr, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion", "UBR")
if err == nil && ubr.Value.String != "" {
log.Debug().Str("ubr", ubr.Value.String).Msg("found ubr")
pf.Build = ubr.Value.String
}
// we try both CurrentBuild and CurrentBuildNumber for the version number
currentBuild, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion", "CurrentBuild")
if err == nil && currentBuild.Value.String != "" {
log.Debug().Str("currentBuild", currentBuild.Value.String).Msg("found currentBuild")
pf.Version = currentBuild.Value.String
} else {
currentBuildNumber, err := rh.GetRegistryItemValue(registry.Software, "Microsoft\\Windows NT\\CurrentVersion", "CurrentBuildNumber")
if err == nil && currentBuildNumber.Value.String != "" {
log.Debug().Str("currentBuildNumber", currentBuildNumber.Value.String).Msg("found currentBuildNumber")
pf.Version = currentBuildNumber.Value.String
}
}
return true, nil
}
return false, nil
},
}

Expand Down
33 changes: 33 additions & 0 deletions providers/os/id/hostname/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/spf13/afero"
"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/registry"
)

// Hostname returns the hostname of the system.
Expand Down Expand Up @@ -65,5 +66,37 @@ func Hostname(conn shared.Connection, pf *inventory.Platform) (string, bool) {
}
}

// Fallback for windows systems to using registry for static analysis
if pf.IsFamily(inventory.FAMILY_WINDOWS) && conn.Capabilities().Has(shared.Capability_FileSearch) {
fi, err := conn.FileInfo(registry.SystemRegPath)
if err != nil {
log.Debug().Err(err).Msg("could not find SYSTEM registry file, cannot perform hostname lookup")
return "", false
}

rh := registry.NewRegistryHandler()
defer func() {
err := rh.UnloadSubkeys()
if err != nil {
log.Debug().Err(err).Msg("could not unload registry subkeys")
}
}()
err = rh.LoadSubkey(registry.System, fi.Path)
if err != nil {
log.Debug().Err(err).Msg("could not load SYSTEM registry key file")
return "", false
}
key, err := rh.GetRegistryItemValue(registry.System, "ControlSet001\\Control\\ComputerName\\ComputerName", "ComputerName")
if err == nil {
return key.Value.String, true
} else {
// we also can try ControlSet002 as a fallback
key, err := rh.GetRegistryItemValue(registry.System, "ControlSet002\\Control\\ComputerName\\ComputerName", "ComputerName")
if err == nil {
return key.Value.String, true
}
}
preslavgerchev marked this conversation as resolved.
Show resolved Hide resolved
}

return "", false
}
28 changes: 28 additions & 0 deletions providers/os/registry/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package registry

const (
// According to https://learn.microsoft.com/en-gb/windows/win32/sysinfo/structure-of-the-registry
// we have the following registry keys
Software = "SOFTWARE"
System = "SYSTEM"
Security = "SECURITY"
Default = "DEFAULT"
Sam = "SAM"

SoftwareRegPath = "Windows\\System32\\config\\SOFTWARE"
SystemRegPath = "Windows\\System32\\config\\SYSTEM"
SecurityRegPath = "Windows\\System32\\config\\SECURITY"
DefaultRegPath = "Windows\\System32\\config\\DEFAULT"
SamRegPath = "Windows\\System32\\config\\SAM"
)

var KnownRegistryFiles = map[string]string{
Software: SoftwareRegPath,
System: SystemRegPath,
Security: SecurityRegPath,
Default: DefaultRegPath,
Sam: SamRegPath,
}
103 changes: 103 additions & 0 deletions providers/os/registry/registryhandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package registry

import (
"errors"
"fmt"
"runtime"
"sync"
)

// RegistryHandler allows for loading and unloading of registry keys from files. It also allows for looking up registry keys and values.
type RegistryHandler struct {
// a map of currently loaded registries.
// the id is the registry's id (e.g. "SOFTWARE") and the value is the path to the loaded key (e.g. "TmpReg_SOFTWARE")
registries map[string]string
lock sync.Mutex
}

const (
subkeyPrefix = "TMPREG"
)

func NewRegistryHandler() *RegistryHandler {
return &RegistryHandler{
registries: make(map[string]string),
}
}

// Loads the given registry file into the registry handler under a subkey, generated by buildSubKeyPath.
// The subkey file is indicated by the filepath parameter.
// Only known registry files (see KnownRegistryFiles) can be loaded.
func (r *RegistryHandler) LoadSubkey(registryId, filepath string) error {
if runtime.GOOS != "windows" {
Copy link
Contributor

@afiune afiune Jun 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit; why not define this function also inside the files registryhandler_unix.go and registryhandler_windows.go instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also has some additional logic besides making the syscall so I figured it best belongs to the struct which handles the syscalls too (and storing which registries are loaded)

return errors.New("loading of registry subkeys is only supported on windows")
}

r.lock.Lock()
defer r.lock.Unlock()
// sanity check, make sure we only try and load registry files we know of
if _, ok := KnownRegistryFiles[registryId]; !ok {
return errors.New("invalid registry id")
}
if _, ok := r.registries[registryId]; ok {
return nil
}
// format the registry path
key := buildSubKeyPath(registryId)
err := LoadRegistrySubkey(key, filepath)
if err != nil {
return err
}
r.registries[registryId] = key
return nil
}

// we use the name of the registry file as an id, e.g. "SOFTWARE"
// we combine this with the prefix to get a subkey like "TmpReg_SOFTWARE"
func buildSubKeyPath(registryId string) string {
return fmt.Sprintf("%s_%s", subkeyPrefix, registryId)
}

// Unloads all the subkeys, that were loaded by the handler.
func (r *RegistryHandler) UnloadSubkeys() error {
r.lock.Lock()
defer r.lock.Unlock()
for id, regPath := range r.registries {
err := UnloadRegistrySubkey(regPath)
if err != nil {
return err
}
delete(r.registries, id)
}
return nil
}

// Gets a fully qualified registry path for a given registry id, including the root (HKLM).
func (r *RegistryHandler) getRegistryPath(id string) (string, error) {
if path, ok := r.registries[id]; ok {
// we always point the syscalls to HKEY_LOCAL_MACHINE so we can safely prefix it here
return fmt.Sprintf("HKLM\\%s", path), nil
}
return "", fmt.Errorf("registry %s not loaded", id)
}

// Gets a fully qualified registry key path for a given registry id and key.
// E.g. if the registry id is "SOFTWARE" and the key is "Microsoft", the result will be "HKLM\TmpReg_SOFTWARE\Microsoft".
func (r *RegistryHandler) getRegistryKeyPath(id string, key string) (string, error) {
root, err := r.getRegistryPath(id)
if err != nil {
return "", err
}
return fmt.Sprintf("%s\\%s", root, key), nil
}

func (r *RegistryHandler) GetRegistryItemValue(registryId string, path, key string) (RegistryKeyItem, error) {
regPath, err := r.getRegistryKeyPath(registryId, path)
if err != nil {
return RegistryKeyItem{}, err
}
return GetNativeRegistryKeyItem(regPath, key)
}
44 changes: 44 additions & 0 deletions providers/os/registry/registryhandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package registry

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestBuildSubKey(t *testing.T) {
require.Equal(t, "TMPREG_T", buildSubKeyPath("T"))
}

func TestGetRegistryPath(t *testing.T) {
t.Run("get registry path for an registry that has not been loaded yet", func(t *testing.T) {
rh := NewRegistryHandler()
_, err := rh.getRegistryPath("TMPREG_T")
require.Error(t, err)
})
t.Run("get registry path for an registry that has been loaded", func(t *testing.T) {
rh := NewRegistryHandler()
rh.registries["SOFTWARE"] = "TMPREG_SOFTWARE"
path, err := rh.getRegistryPath("SOFTWARE")
require.NoError(t, err)
require.Equal(t, "HKLM\\TMPREG_SOFTWARE", path)
})
}

func TestGetRegistryKeyPath(t *testing.T) {
t.Run("get registry key path for an registry that has not been loaded yet", func(t *testing.T) {
rh := NewRegistryHandler()
_, err := rh.getRegistryKeyPath("TMPREG_T", "Microsoft\\Windows")
require.Error(t, err)
})
t.Run("get registry key path for an registry that has been loaded", func(t *testing.T) {
rh := NewRegistryHandler()
rh.registries["SOFTWARE"] = "TMPREG_SOFTWARE"
path, err := rh.getRegistryKeyPath("SOFTWARE", "Microsoft\\Windows")
require.NoError(t, err)
require.Equal(t, "HKLM\\TMPREG_SOFTWARE\\Microsoft\\Windows", path)
})
}
19 changes: 19 additions & 0 deletions providers/os/registry/registryhandler_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

//go:build !windows
// +build !windows

package registry

import (
"errors"
)

func LoadRegistrySubkey(key, path string) error {
return errors.New("LoadRegistrySubkey is not supported on non-windows platforms")
}

func UnloadRegistrySubkey(key string) error {
return errors.New("UnloadRegistrySubkey is not supported on non-windows platforms")
}
Loading
Loading