diff --git a/providers/os/resources/os.lr b/providers/os/resources/os.lr index e74563b270..c9c272fb77 100644 --- a/providers/os/resources/os.lr +++ b/providers/os/resources/os.lr @@ -963,9 +963,12 @@ registrykey @defaults("path") { init(path string) // Registry key path path string + // Indicates if the property exists exists() bool - // Registry key properties + // deprecated: Registry key properties, use `items` instead properties() map[string]string + // Registry key items + items() []registrykey.property // Registry key children children() []string } @@ -973,10 +976,18 @@ registrykey @defaults("path") { // Windows registry key property registrykey.property @defaults("path name") { init(path string, name string) + // Registry key path path string + // Registry key name name string + // Indicates if the property exists exists() bool - value(exists) string + // deprecated: Registry key property value converted to string, use `data` instead + value() string + // Registry key type + type() string + // Registry key data + data() dict } // Container Image diff --git a/providers/os/resources/os.lr.go b/providers/os/resources/os.lr.go index c8c4fedecb..4168f644ad 100644 --- a/providers/os/resources/os.lr.go +++ b/providers/os/resources/os.lr.go @@ -315,7 +315,7 @@ func init() { Create: createRegistrykey, }, "registrykey.property": { - // to override args, implement: initRegistrykeyProperty(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Init: initRegistrykeyProperty, Create: createRegistrykeyProperty, }, "container.image": { @@ -1465,6 +1465,9 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "registrykey.properties": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlRegistrykey).GetProperties()).ToDataRes(types.Map(types.String, types.String)) }, + "registrykey.items": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlRegistrykey).GetItems()).ToDataRes(types.Array(types.Resource("registrykey.property"))) + }, "registrykey.children": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlRegistrykey).GetChildren()).ToDataRes(types.Array(types.String)) }, @@ -1480,6 +1483,12 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "registrykey.property.value": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlRegistrykeyProperty).GetValue()).ToDataRes(types.String) }, + "registrykey.property.type": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlRegistrykeyProperty).GetType()).ToDataRes(types.String) + }, + "registrykey.property.data": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlRegistrykeyProperty).GetData()).ToDataRes(types.Dict) + }, "container.image.reference": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlContainerImage).GetReference()).ToDataRes(types.String) }, @@ -3519,6 +3528,10 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlRegistrykey).Properties, ok = plugin.RawToTValue[map[string]interface{}](v.Value, v.Error) return }, + "registrykey.items": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlRegistrykey).Items, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, "registrykey.children": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlRegistrykey).Children, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) return @@ -3543,6 +3556,14 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlRegistrykeyProperty).Value, ok = plugin.RawToTValue[string](v.Value, v.Error) return }, + "registrykey.property.type": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlRegistrykeyProperty).Type, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "registrykey.property.data": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlRegistrykeyProperty).Data, ok = plugin.RawToTValue[interface{}](v.Value, v.Error) + return + }, "container.image.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlContainerImage).__id, ok = v.Value.(string) return @@ -10016,6 +10037,7 @@ type mqlRegistrykey struct { Path plugin.TValue[string] Exists plugin.TValue[bool] Properties plugin.TValue[map[string]interface{}] + Items plugin.TValue[[]interface{}] Children plugin.TValue[[]interface{}] } @@ -10072,6 +10094,22 @@ func (c *mqlRegistrykey) GetProperties() *plugin.TValue[map[string]interface{}] }) } +func (c *mqlRegistrykey) GetItems() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.Items, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("registrykey", c.__id, "items") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + return c.items() + }) +} + func (c *mqlRegistrykey) GetChildren() *plugin.TValue[[]interface{}] { return plugin.GetOrCompute[[]interface{}](&c.Children, func() ([]interface{}, error) { return c.children() @@ -10082,11 +10120,13 @@ func (c *mqlRegistrykey) GetChildren() *plugin.TValue[[]interface{}] { type mqlRegistrykeyProperty struct { MqlRuntime *plugin.Runtime __id string - mqlRegistrykeyPropertyInternal + // optional: if you define mqlRegistrykeyPropertyInternal it will be used here Path plugin.TValue[string] Name plugin.TValue[string] Exists plugin.TValue[bool] Value plugin.TValue[string] + Type plugin.TValue[string] + Data plugin.TValue[interface{}] } // createRegistrykeyProperty creates a new instance of this resource @@ -10142,12 +10182,19 @@ func (c *mqlRegistrykeyProperty) GetExists() *plugin.TValue[bool] { func (c *mqlRegistrykeyProperty) GetValue() *plugin.TValue[string] { return plugin.GetOrCompute[string](&c.Value, func() (string, error) { - vargExists := c.GetExists() - if vargExists.Error != nil { - return "", vargExists.Error - } + return c.value() + }) +} + +func (c *mqlRegistrykeyProperty) GetType() *plugin.TValue[string] { + return plugin.GetOrCompute[string](&c.Type, func() (string, error) { + return c.compute_type() + }) +} - return c.value(vargExists.Data) +func (c *mqlRegistrykeyProperty) GetData() *plugin.TValue[interface{}] { + return plugin.GetOrCompute[interface{}](&c.Data, func() (interface{}, error) { + return c.data() }) } diff --git a/providers/os/resources/registrykey.go b/providers/os/resources/registrykey.go index fa602d643a..3975563e35 100644 --- a/providers/os/resources/registrykey.go +++ b/providers/os/resources/registrykey.go @@ -1,17 +1,16 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - package resources import ( "errors" + "runtime" "strings" - "github.com/rs/zerolog/log" "go.mondoo.com/cnquery/llx" "go.mondoo.com/cnquery/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/providers/os/resources/powershell" "go.mondoo.com/cnquery/providers/os/resources/windows" + "go.mondoo.com/ranger-rpc/codes" + "go.mondoo.com/ranger-rpc/status" ) func (k *mqlRegistrykey) id() (string, error) { @@ -19,12 +18,26 @@ func (k *mqlRegistrykey) id() (string, error) { } func (k *mqlRegistrykey) exists() (bool, error) { + // if we are running locally on windows, we can use native api + if runtime.GOOS == "windows" { + items, err := windows.GetNativeRegistryKeyItems(k.Path.Data) + if err == nil && len(items) > 0 { + return true, nil + } + std, ok := status.FromError(err) + if ok && std.Code() == codes.NotFound { + return false, nil + } + if err != nil { + return false, err + } + } + script := powershell.Encode(windows.GetRegistryKeyItemScript(k.Path.Data)) o, err := CreateResource(k.MqlRuntime, "command", map[string]*llx.RawData{ "command": llx.StringData(script), }) if err != nil { - log.Error().Err(err).Msg("could not create resource") return false, err } cmd := o.(*mqlCommand) @@ -46,8 +59,14 @@ func (k *mqlRegistrykey) exists() (bool, error) { return true, nil } -func (k *mqlRegistrykey) properties() (map[string]interface{}, error) { - res := map[string]interface{}{} +// GetEntries returns a list of registry key property resources +func (k *mqlRegistrykey) getEntries() ([]windows.RegistryKeyItem, error) { + // if we are running locally on windows, we can use native api + if runtime.GOOS == "windows" { + return windows.GetNativeRegistryKeyItems(k.Path.Data) + } + + // parse the output of the powershell script script := powershell.Encode(windows.GetRegistryKeyItemScript(k.Path.Data)) o, err := CreateResource(k.MqlRuntime, "command", map[string]*llx.RawData{ "command": llx.StringData(script), @@ -56,7 +75,6 @@ func (k *mqlRegistrykey) properties() (map[string]interface{}, error) { return nil, err } cmd := o.(*mqlCommand) - exit := cmd.GetExitcode() if exit.Error != nil { return nil, exit.Error @@ -65,42 +83,96 @@ func (k *mqlRegistrykey) properties() (map[string]interface{}, error) { return nil, errors.New("could not retrieve registry key") } - entries, err := windows.ParseRegistryKeyItems(strings.NewReader(cmd.GetStdout().Data)) + stdout := cmd.GetStdout() + if stdout.Error != nil { + return nil, stdout.Error + } + + return windows.ParsePowershellRegistryKeyItems(strings.NewReader(stdout.Data)) +} + +// Deprecated: properties returns the properties of a registry key +// This function is deprecated and will be removed in a future release +func (k *mqlRegistrykey) properties() (map[string]interface{}, error) { + res := map[string]interface{}{} + + entries, err := k.getEntries() if err != nil { return nil, err } for i := range entries { rkey := entries[i] - res[rkey.Key] = rkey.GetValue() + res[rkey.Key] = rkey.String() } return res, nil } -func (k *mqlRegistrykey) children() ([]interface{}, error) { - res := []interface{}{} - - script := powershell.Encode(windows.GetRegistryKeyChildItemsScript(k.Path.Data)) - o, err := CreateResource(k.MqlRuntime, "command", map[string]*llx.RawData{ - "command": llx.StringData(script), - }) +// items returns a list of registry key property resources +func (k *mqlRegistrykey) items() ([]interface{}, error) { + entries, err := k.getEntries() if err != nil { - return res, err + return nil, err } - cmd := o.(*mqlCommand) - exit := cmd.GetExitcode() - if exit.Error != nil { - return nil, exit.Error - } - if exit.Data != 0 { - return nil, errors.New("could not retrieve registry key") + // create MQL mount entry resources for each mount + items := make([]interface{}, len(entries)) + for i, entry := range entries { + o, err := CreateResource(k.MqlRuntime, "registrykey.property", map[string]*llx.RawData{ + "path": llx.StringData(k.Path.Data), + "name": llx.StringData(entry.Key), + "value": llx.StringData(entry.String()), + "type": llx.StringData(entry.Kind()), + "data": llx.DictData(entry.GetRawValue()), + "exists": llx.BoolData(true), + }) + if err != nil { + return nil, err + } + + items[i] = o.(*mqlRegistrykeyProperty) } - children, err := windows.ParseRegistryKeyChildren(strings.NewReader(cmd.GetStdout().Data)) - if err != nil { - return nil, err + return items, nil +} + +func (k *mqlRegistrykey) children() ([]interface{}, error) { + res := []interface{}{} + + var children []windows.RegistryKeyChild + if runtime.GOOS == "windows" { + var err error + children, err = windows.GetNativeRegistryKeyChildren(k.Path.Data) + if err != nil { + return nil, err + } + } else { + // parse powershell script + script := powershell.Encode(windows.GetRegistryKeyChildItemsScript(k.Path.Data)) + o, err := CreateResource(k.MqlRuntime, "command", map[string]*llx.RawData{ + "command": llx.StringData(script), + }) + if err != nil { + return res, err + } + cmd := o.(*mqlCommand) + exitcode := cmd.GetExitcode() + if exitcode.Error != nil { + return nil, exitcode.Error + } + if exitcode.Data != 0 { + return nil, errors.New("could not retrieve registry key") + } + + stdout := cmd.GetStdout() + if stdout.Error != nil { + return res, stdout.Error + } + children, err = windows.ParsePowershellRegistryKeyChildren(strings.NewReader(stdout.Data)) + if err != nil { + return nil, err + } } for i := range children { @@ -111,70 +183,85 @@ func (k *mqlRegistrykey) children() ([]interface{}, error) { return res, nil } -type mqlRegistrykeyPropertyInternal struct { - key plugin.TValue[*mqlRegistrykey] -} - func (p *mqlRegistrykeyProperty) id() (string, error) { return p.Path.Data + " - " + p.Name.Data, nil } -func (p *mqlRegistrykeyProperty) lookupKey() (*mqlRegistrykey, error) { - if p.key.State == plugin.StateIsSet { - return p.key.Data, p.key.Error +func initRegistrykeyProperty(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { + path := args["path"] + if path == nil { + return args, nil, nil } - // create resource here, but do not use it yet - obj, err := CreateResource(p.MqlRuntime, "registrykey", map[string]*llx.RawData{ - "path": llx.StringData(p.Path.Data), - }) - if err != nil { - p.key = plugin.TValue[*mqlRegistrykey]{Error: err, State: plugin.StateIsSet} - return p.key.Data, p.key.Error + name := args["name"] + if name == nil { + return args, nil, nil } - registryKey := obj.(*mqlRegistrykey) - p.key = plugin.TValue[*mqlRegistrykey]{Data: registryKey, State: plugin.StateIsSet} - return p.key.Data, p.key.Error -} + // if the data is set, we do not need to fetch the data first + dataRaw := args["data"] + if dataRaw != nil { + return args, nil, nil + } -func (p *mqlRegistrykeyProperty) exists() (bool, error) { - key, err := p.lookupKey() + // create resource here, but do not use it yet + obj, err := CreateResource(runtime, "registrykey", map[string]*llx.RawData{ + "path": path, + }) if err != nil { - return false, err + return nil, nil, err } + key := obj.(*mqlRegistrykey) exists := key.GetExists() - if exists.Error != nil { - return false, exists.Error + if err != nil { + return nil, nil, err } - return exists.Data, nil -} + // set default values + args["exists"] = llx.BoolFalse + // NOTE: we do not set a value here so that MQL throws an error when a user try to gather the data for a + // non-existing key -func (p *mqlRegistrykeyProperty) value(exists bool) (string, error) { - if !exists { - return "", nil - } + // path exists + if exists.Data { + items := key.GetItems() + if items.Error != nil { + return nil, nil, items.Error + } - key, err := p.lookupKey() - if err != nil { - return "", err - } + for i := range items.Data { + property := items.Data[i].(*mqlRegistrykeyProperty) + iname := property.GetName() + if iname.Error != nil { + return nil, nil, iname.Error + } - props := key.GetProperties() - if props.Error != nil { - return "", props.Error - } - - // search for property - found := "" - for k := range props.Data { - if strings.EqualFold(k, p.Name.Data) { - found = props.Data[k].(string) - break + // property exists, return it + if strings.EqualFold(iname.Data, name.Value.(string)) { + return nil, property, nil + } } } + return args, nil, nil +} + +func (p *mqlRegistrykeyProperty) exists() (bool, error) { + // NOTE: will not be called since it will always be set in init + return false, errors.New("could not determine if the property exists") +} + +func (p *mqlRegistrykeyProperty) compute_type() (string, error) { + // NOTE: if we reach here the value has not been set in init, therefore we return an error + return "", errors.New("requested property does not exist") +} + +func (p *mqlRegistrykeyProperty) data() (interface{}, error) { + // NOTE: if we reach here the value has not been set in init, therefore we return an error + return "", errors.New("requested property does not exist") +} - return found, nil +func (p *mqlRegistrykeyProperty) value() (string, error) { + // NOTE: if we reach here the value has not been set in init, therefore we return an error + return "", errors.New("requested property does not exist") } diff --git a/providers/os/resources/windows/registrykey.go b/providers/os/resources/windows/registrykey.go index b43ad52498..f1ad00dbc5 100644 --- a/providers/os/resources/windows/registrykey.go +++ b/providers/os/resources/windows/registrykey.go @@ -1,68 +1,8 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - package windows -import ( - "encoding/binary" - "encoding/json" - "fmt" - "io" - "math" - "strconv" - - "github.com/rs/zerolog/log" -) - -const getRegistryKeyItemScript = ` -$path = '%s' -$reg = Get-Item ('Registry::' + $path) -if ($reg -eq $null) { - Write-Error "Could not find registry key" - exit 1 -} -$properties = @() -$reg.Property | ForEach-Object { - $fetchKeyValue = $_ - if ("(default)".Equals($_)) { $fetchKeyValue = '' } - $entry = New-Object psobject -Property @{ - "key" = $_ - "value" = New-Object psobject -Property @{ - "data" = $(Get-ItemProperty ('Registry::' + $path)).$_; - "kind" = $reg.GetValueKind($fetchKeyValue); - } - } - $properties += $entry -} -ConvertTo-Json -Compress $properties -` - -func GetRegistryKeyItemScript(path string) string { - return fmt.Sprintf(getRegistryKeyItemScript, path) -} - -const getRegistryKeyChildItemsScript = ` -$path = '%s' -$children = Get-ChildItem -Path ('Registry::' + $path) -rec -ea SilentlyContinue - -$properties = @() -$children | ForEach-Object { - $entry = New-Object psobject -Property @{ - "name" = $_.PSChildName - "path" = $_.Name - "properties" = $_.Property - "children" = $_.SubKeyCount - } - $properties += $entry -} -ConvertTo-Json -compress $properties -` - -func GetRegistryKeyChildItemsScript(path string) string { - return fmt.Sprintf(getRegistryKeyChildItemsScript, path) -} +import "go.mondoo.com/cnquery/providers-sdk/v1/util/convert" -// derrived from "golang.org/x/sys/windows/registry" +// derived from "golang.org/x/sys/windows/registry" // see https://github.com/golang/sys/blob/master/windows/registry/value.go#L17-L31 const ( NONE = 0 @@ -84,106 +24,81 @@ type RegistryKeyItem struct { Value RegistryKeyValue } -type RegistryKeyValue struct { - Kind int - Binary []byte - Number int64 - String string -} - -type keyKindRaw struct { - Kind int - Data interface{} -} - -func (k *RegistryKeyValue) UnmarshalJSON(b []byte) error { - var raw keyKindRaw - - // try to unmarshal the type - err := json.Unmarshal(b, &raw) - if err != nil { - return err - } - k.Kind = raw.Kind - - if raw.Data == nil { - return nil +func (k RegistryKeyItem) Kind() string { + switch k.Value.Kind { + case NONE: + return "bone" + case SZ: + return "string" + case EXPAND_SZ: + return "expandstring" + case BINARY: + return "binary" + case DWORD: + return "dword" + case DWORD_BIG_ENDIAN: + return "dword" + case LINK: + return "link" + case MULTI_SZ: + return "multistring" + case RESOURCE_LIST: + return "" + case FULL_RESOURCE_DESCRIPTOR: + return "" + case RESOURCE_REQUIREMENTS_LIST: + return "" + case QWORD: + return "qword" } + return "" +} - // see https://docs.microsoft.com/en-us/powershell/scripting/samples/working-with-registry-entries?view=powershell-7 - switch raw.Kind { +func (k RegistryKeyItem) GetRawValue() interface{} { + switch k.Value.Kind { case NONE: - // ignore - case SZ: // Any string value - k.String = raw.Data.(string) - case EXPAND_SZ: // A string that can contain environment variables that are dynamically expanded - k.String = raw.Data.(string) - case BINARY: // Binary data - k.Binary = []byte(raw.Data.(string)) - case DWORD: // A number that is a valid UInt32 - data := raw.Data.(float64) - number := int64(data) - // string fallback - k.Number = number - k.String = strconv.FormatInt(number, 10) + return nil + case SZ: + return k.Value.String + case EXPAND_SZ: + return k.Value.String + case BINARY: + return k.Value.Binary + case DWORD: + return k.Value.Number case DWORD_BIG_ENDIAN: - log.Warn().Msg("DWORD_BIG_ENDIAN for registry key is not supported") + return nil case LINK: - log.Warn().Msg("LINK for registry key is not supported") - case MULTI_SZ: // A multiline string - k.String = raw.Data.(string) + return nil + case MULTI_SZ: + return convert.SliceAnyToInterface(k.Value.MultiString) case RESOURCE_LIST: - log.Warn().Msg("RESOURCE_LIST for registry key is not supported") + return nil case FULL_RESOURCE_DESCRIPTOR: - log.Warn().Msg("FULL_RESOURCE_DESCRIPTOR for registry key is not supported") + return nil case RESOURCE_REQUIREMENTS_LIST: - log.Warn().Msg("RESOURCE_REQUIREMENTS_LIST for registry key is not supported") - case QWORD: // 8 bytes of binary data - f := raw.Data.(float64) - buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf[:], math.Float64bits(f)) - k.Binary = buf + return nil + case QWORD: + return k.Value.Number } return nil } -func (k RegistryKeyItem) GetValue() string { - return k.Value.String +// String returns a string representation of the registry key value +func (k RegistryKeyItem) String() string { + return k.Value.String // conversion to string is handled in UnmarshalJSON +} + +type RegistryKeyValue struct { + Kind int + Binary []byte + Number int64 + String string + MultiString []string } type RegistryKeyChild struct { Name string Path string Properties []string - Children int -} - -func ParseRegistryKeyItems(r io.Reader) ([]RegistryKeyItem, error) { - data, err := io.ReadAll(r) - if err != nil { - return nil, err - } - - var items []RegistryKeyItem - err = json.Unmarshal(data, &items) - if err != nil { - return nil, err - } - - return items, nil -} - -func ParseRegistryKeyChildren(r io.Reader) ([]RegistryKeyChild, error) { - data, err := io.ReadAll(r) - if err != nil { - return nil, err - } - - var children []RegistryKeyChild - err = json.Unmarshal(data, &children) - if err != nil { - return nil, err - } - - return children, nil } diff --git a/providers/os/resources/windows/registrykey_powershell.go b/providers/os/resources/windows/registrykey_powershell.go new file mode 100644 index 0000000000..e9c6baa7e1 --- /dev/null +++ b/providers/os/resources/windows/registrykey_powershell.go @@ -0,0 +1,196 @@ +package windows + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "io" + "math" + "strconv" + "strings" + + "github.com/rs/zerolog/log" +) + +// RegistryKeyItem represents a registry key item and its properties +const getRegistryKeyItemScript = ` +$path = '%s' +$reg = Get-Item ('Registry::' + $path) +if ($reg -eq $null) { + Write-Error "Could not find registry key" + exit 1 +} +$properties = @() +$reg.Property | ForEach-Object { + $fetchKeyValue = $_ + if ("(default)".Equals($_)) { $fetchKeyValue = '' } + $data = $(Get-ItemProperty ('Registry::' + $path)).$_; + $kind = $reg.GetValueKind($fetchKeyValue); + if ($kind -eq 7) { + $data = $(Get-ItemProperty ('Registry::' + $path)) | Select-Object -ExpandProperty $_ + } + $entry = New-Object psobject -Property @{ + "key" = $_ + "value" = New-Object psobject -Property @{ + "data" = $data; + "kind" = $kind; + } + } + $properties += $entry +} +ConvertTo-Json -Depth 3 -Compress $properties +` + +func GetRegistryKeyItemScript(path string) string { + return fmt.Sprintf(getRegistryKeyItemScript, path) +} + +// getRegistryKeyChildItemsScript represents a registry key item and its children +const getRegistryKeyChildItemsScript = ` +$path = '%s' +$children = Get-ChildItem -Path ('Registry::' + $path) -rec -ea SilentlyContinue + +$properties = @() +$children | ForEach-Object { + $entry = New-Object psobject -Property @{ + "name" = $_.PSChildName + "path" = $_.Name + "properties" = $_.Property + "children" = $_.SubKeyCount + } + $properties += $entry +} +ConvertTo-Json -compress $properties +` + +func GetRegistryKeyChildItemsScript(path string) string { + return fmt.Sprintf(getRegistryKeyChildItemsScript, path) +} + +type keyKindRaw struct { + Kind int + Data interface{} +} + +func (k *RegistryKeyValue) UnmarshalJSON(b []byte) error { + var raw keyKindRaw + + // try to unmarshal the type + err := json.Unmarshal(b, &raw) + if err != nil { + return err + } + k.Kind = raw.Kind + + if raw.Data == nil { + return nil + } + + // see https://docs.microsoft.com/en-us/powershell/scripting/samples/working-with-registry-entries?view=powershell-7 + switch raw.Kind { + case NONE: + // ignore + case SZ: // Any string value + value, ok := raw.Data.(string) + if !ok { + return fmt.Errorf("registry key value is not a string: %v", raw.Data) + } + k.String = value + case EXPAND_SZ: // A string that can contain environment variables that are dynamically expanded + value, ok := raw.Data.(string) + if !ok { + return fmt.Errorf("registry key value is not a string: %v", raw.Data) + } + k.String = value + case BINARY: // Binary data + rawData, ok := raw.Data.([]interface{}) + if !ok { + return fmt.Errorf("registry key value is not a byte array: %v", raw.Data) + } + data := make([]byte, len(rawData)) + for i, v := range rawData { + val, ok := v.(float64) + if !ok { + return fmt.Errorf("registry key value is not a byte array: %v", raw.Data) + } + data[i] = byte(val) + } + k.Binary = data + case DWORD: // A number that is a valid UInt32 + data, ok := raw.Data.(float64) + if !ok { + return fmt.Errorf("registry key value is not a number: %v", raw.Data) + } + number := int64(data) + // string fallback + k.Number = number + k.String = strconv.FormatInt(number, 10) + case DWORD_BIG_ENDIAN: + log.Warn().Msg("DWORD_BIG_ENDIAN for registry key is not supported") + case LINK: + log.Warn().Msg("LINK for registry key is not supported") + case MULTI_SZ: // A multiline string + switch value := raw.Data.(type) { + case string: + k.String = value + if value != "" { + k.MultiString = []string{value} + } + case []interface{}: + if len(value) > 0 { + var multiString []string + for _, v := range value { + multiString = append(multiString, v.(string)) + } + // NOTE: this is to be consistent with the output before we moved to multi-datatype support for registry keys + k.String = strings.Join(multiString, " ") + k.MultiString = multiString + } + } + case RESOURCE_LIST: + log.Warn().Msg("RESOURCE_LIST for registry key is not supported") + case FULL_RESOURCE_DESCRIPTOR: + log.Warn().Msg("FULL_RESOURCE_DESCRIPTOR for registry key is not supported") + case RESOURCE_REQUIREMENTS_LIST: + log.Warn().Msg("RESOURCE_REQUIREMENTS_LIST for registry key is not supported") + case QWORD: // 8 bytes of binary data + f, ok := raw.Data.(float64) + if !ok { + return fmt.Errorf("registry key value is not a number: %v", raw.Data) + } + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf[:], math.Float64bits(f)) + k.Binary = buf + } + return nil +} + +func ParsePowershellRegistryKeyItems(r io.Reader) ([]RegistryKeyItem, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + var items []RegistryKeyItem + err = json.Unmarshal(data, &items) + if err != nil { + return nil, err + } + + return items, nil +} + +func ParsePowershellRegistryKeyChildren(r io.Reader) ([]RegistryKeyChild, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + var children []RegistryKeyChild + err = json.Unmarshal(data, &children) + if err != nil { + return nil, err + } + + return children, nil +} diff --git a/providers/os/resources/windows/registrykey_powershell_test.go b/providers/os/resources/windows/registrykey_powershell_test.go new file mode 100644 index 0000000000..99d921a97d --- /dev/null +++ b/providers/os/resources/windows/registrykey_powershell_test.go @@ -0,0 +1,48 @@ +package windows + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWindowsRegistryKeyItemParser(t *testing.T) { + r, err := os.Open("./testdata/registrykey.json") + require.NoError(t, err) + + items, err := ParsePowershellRegistryKeyItems(r) + assert.Nil(t, err) + assert.Equal(t, 10, len(items)) + assert.Equal(t, "ConsentPromptBehaviorAdmin", items[0].Key) + assert.Equal(t, 4, items[0].Value.Kind) + assert.Equal(t, int64(5), items[0].Value.Number) + assert.Equal(t, int64(5), items[0].GetRawValue()) + assert.Equal(t, "5", items[0].String()) +} + +func TestWindowsRegistryKeyChildParser(t *testing.T) { + r, err := os.Open("./testdata/registrykey-children.json") + require.NoError(t, err) + + items, err := ParsePowershellRegistryKeyChildren(r) + assert.Nil(t, err) + assert.Equal(t, 5, len(items)) +} + +func TestWindowsRegistryKeyMultiStringParser(t *testing.T) { + r, err := os.Open("./testdata/registrykey_multistring.json") + require.NoError(t, err) + + items, err := ParsePowershellRegistryKeyItems(r) + assert.Nil(t, err) + assert.Equal(t, 1, len(items)) + assert.Equal(t, "Machine", items[0].Key) + assert.Equal(t, 7, items[0].Value.Kind) + assert.Equal(t, []interface{}{ + "Software\\Microsoft\\Windows NT\\CurrentVersion\\Print", + "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows", + "System\\CurrentControlSet\\Control\\Print\\Printers", + }, items[0].GetRawValue()) +} diff --git a/providers/os/resources/windows/registrykey_test.go b/providers/os/resources/windows/registrykey_test.go deleted file mode 100644 index c26748c7cf..0000000000 --- a/providers/os/resources/windows/registrykey_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package windows - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestWindowsRegistryKeyItemParser(t *testing.T) { - r, err := os.Open("./testdata/registrykey.json") - require.NoError(t, err) - - items, err := ParseRegistryKeyItems(r) - assert.Nil(t, err) - assert.Equal(t, 10, len(items)) - assert.Equal(t, "ConsentPromptBehaviorAdmin", items[0].Key) - assert.Equal(t, 4, items[0].Value.Kind) - assert.Equal(t, int64(5), items[0].Value.Number) - assert.Equal(t, "5", items[0].GetValue()) -} - -func TestWindowsRegistryKeyChildParser(t *testing.T) { - r, err := os.Open("./testdata/registrykey-children.json") - require.NoError(t, err) - - items, err := ParseRegistryKeyChildren(r) - assert.Nil(t, err) - assert.Equal(t, 5, len(items)) -} diff --git a/providers/os/resources/windows/registrykey_unix.go b/providers/os/resources/windows/registrykey_unix.go new file mode 100644 index 0000000000..8a494e0017 --- /dev/null +++ b/providers/os/resources/windows/registrykey_unix.go @@ -0,0 +1,15 @@ +//go:build !windows +// +build !windows + +package windows + +import "errors" + +// non-windows stubs +func GetNativeRegistryKeyItems(path string) ([]RegistryKeyItem, error) { + return nil, errors.New("native registry key items not supported on non-windows platforms") +} + +func GetNativeRegistryKeyChildren(path string) ([]RegistryKeyChild, error) { + return nil, errors.New("native registry key children not supported on non-windows platforms") +} diff --git a/providers/os/resources/windows/registrykey_windows.go b/providers/os/resources/windows/registrykey_windows.go new file mode 100644 index 0000000000..ce9d93dbd4 --- /dev/null +++ b/providers/os/resources/windows/registrykey_windows.go @@ -0,0 +1,142 @@ +//go:build windows +// +build windows + +package windows + +import ( + "errors" + "strconv" + "strings" + + "github.com/rs/zerolog/log" + "go.mondoo.com/ranger-rpc/codes" + "go.mondoo.com/ranger-rpc/status" + "golang.org/x/sys/windows/registry" +) + +// parseRegistryKeyPath parses a registry key path into the hive and the path +// https://learn.microsoft.com/en-us/windows/win32/sysinfo/registry-hives +func parseRegistryKeyPath(path string) (registry.Key, string, error) { + if strings.HasPrefix(path, "HKEY_LOCAL_MACHINE") { + return registry.LOCAL_MACHINE, strings.TrimPrefix(path, "HKEY_LOCAL_MACHINE\\"), nil + } + if strings.HasPrefix(path, "HKLM") { + return registry.LOCAL_MACHINE, strings.TrimPrefix(path, "HKLM\\"), nil + } + + if strings.HasPrefix(path, "HKEY_CURRENT_USER") { + return registry.CURRENT_USER, strings.TrimPrefix(path, "HKEY_CURRENT_USER\\"), nil + } + + if strings.HasPrefix(path, "HKCU") { + return registry.CURRENT_USER, strings.TrimPrefix(path, "HKCU\\"), nil + } + + if strings.HasPrefix(path, "HKEY_USERS") { + return registry.USERS, strings.TrimPrefix(path, "HKEY_USERS\\"), nil + } + + return registry.LOCAL_MACHINE, "", errors.New("invalid registry key hive: " + path) +} + +func GetNativeRegistryKeyItems(path string) ([]RegistryKeyItem, error) { + log.Debug().Str("path", path).Msg("search registry key values using native registry api") + key, path, err := parseRegistryKeyPath(path) + if err != nil { + return nil, err + } + regKey, err := registry.OpenKey(key, path, registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) + if err != nil && registry.ErrNotExist == err { + return nil, status.Error(codes.NotFound, "registry key not found: "+path) + } else if err != nil { + return nil, err + } + defer regKey.Close() + + res := []RegistryKeyItem{} + values, err := regKey.ReadValueNames(0) + if err != nil { + return nil, err + } + for _, value := range values { + stringValue, valtype, err := regKey.GetStringValue(value) + if err != registry.ErrUnexpectedType && err != nil { + return nil, err + } + + regValue := RegistryKeyValue{ + Kind: int(valtype), + String: stringValue, + } + + switch valtype { + case registry.SZ, registry.EXPAND_SZ: + // covered by GetStringValue, nothing to do + case registry.BINARY: + binaryValue, _, err := regKey.GetBinaryValue(value) + if err != nil { + return nil, err + } + regValue.Binary = binaryValue + case registry.DWORD: + fallthrough + case registry.QWORD: + intVal, _, err := regKey.GetIntegerValue(value) + if err != nil { + return nil, err + } + regValue.Number = int64(intVal) + regValue.String = strconv.FormatInt(int64(intVal), 10) + case registry.MULTI_SZ: + entries, _, err := regKey.GetStringsValue(value) + if err != nil { + return nil, err + } + regValue.MultiString = entries + if len(entries) > 0 { + // NOTE: this is to be consistent with the output before we moved to multi-datatype support for registry keys + regValue.String = strings.Join(entries, " ") + } + case registry.DWORD_BIG_ENDIAN, registry.LINK, registry.RESOURCE_LIST, registry.FULL_RESOURCE_DESCRIPTOR, registry.RESOURCE_REQUIREMENTS_LIST: + // not supported by golang.org/x/sys/windows/registry + } + res = append(res, RegistryKeyItem{ + Key: value, + Value: regValue, + }) + } + 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) + if err != nil { + return nil, err + } + + regKey, err := registry.OpenKey(key, path, registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) + if err != nil && registry.ErrNotExist == err { + return nil, status.Error(codes.NotFound, "registry key not found: "+path) + } else if err != nil { + return nil, err + } + defer regKey.Close() + + // reads all child keys + entries, err := regKey.ReadSubKeyNames(0) + if err != nil { + return nil, err + } + + res := make([]RegistryKeyChild, len(entries)) + + for i, entry := range entries { + res[i] = RegistryKeyChild{ + Path: path, + Name: entry, + } + } + + return res, nil +} diff --git a/providers/os/resources/windows/testdata/registrykey_multistring.json b/providers/os/resources/windows/testdata/registrykey_multistring.json new file mode 100644 index 0000000000..7a72de9e00 --- /dev/null +++ b/providers/os/resources/windows/testdata/registrykey_multistring.json @@ -0,0 +1,13 @@ +[ + { + "key": "Machine", + "value": { + "kind": 7, + "data": [ + "Software\\Microsoft\\Windows NT\\CurrentVersion\\Print", + "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows", + "System\\CurrentControlSet\\Control\\Print\\Printers" + ] + } + } +] \ No newline at end of file