diff --git a/add.go b/add.go index 35cf1bcb5..9a115a3a5 100644 --- a/add.go +++ b/add.go @@ -81,6 +81,8 @@ func AddResource(fileName string, gossConfig GossConfig, resourceName, key strin res, err = gossConfig.Interfaces.AppendSysResource(key, sys, config) case resource.HTTPResourceName: res, err = gossConfig.HTTPs.AppendSysResource(key, sys, config) + case resource.RegistryResourceName: + res, err = gossConfig.Registries.AppendSysResource(key, sys, config) default: err = fmt.Errorf("undefined resource name: %s", resourceName) } diff --git a/cmd/goss/goss.go b/cmd/goss/goss.go index 0c6a061a2..c0b2378b5 100644 --- a/cmd/goss/goss.go +++ b/cmd/goss/goss.go @@ -401,6 +401,14 @@ func main() { return goss.AddResources(c.GlobalString("gossfile"), resource.InterfaceResourceName, c.Args(), newRuntimeConfigFromCLI(c)) }, }, + { + Name: resource.RegistryResourceKey, + Usage: "add new registry key", + Action: func(c *cli.Context) error { + fatalAlphaIfNeeded(c) + return goss.AddResources(c.GlobalString("gossfile"), resource.RegistryResourceName, c.Args(), newRuntimeConfigFromCLI(c)) + }, + }, }, }, } diff --git a/docs/platforms.md b/docs/platforms.md index 340859d5c..57125ac83 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -95,6 +95,10 @@ This matrix attempts to track parity across platforms. | | mtu | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | **kernel-param** | | {{ fully_supported }} | {{ n_a }} | {{ n_a }} | | | value | {{ fully_supported }} | {{ n_a }} | {{ n_a }} | +| **registry** | | {{ n_a }} | {{ n_a }} | {{community_supported}} | +| | exists | {{ n_a }} | {{ n_a }} | {{community_supported}} | +| | value | {{ n_a }} | {{ n_a }} | {{community_supported}} | +| | type | {{ n_a }} | {{ n_a }} | {{community_supported}} | | **mount** | | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | exists | {{ fully_supported }} | {{ not_implemented }} | {{ not_implemented }} | | | opts | {{ fully_supported }} | {{ not_implemented }} | {{ n_a }} | diff --git a/docs/schema.yaml b/docs/schema.yaml index 661ae527d..965f28bc7 100644 --- a/docs/schema.yaml +++ b/docs/schema.yaml @@ -297,6 +297,36 @@ definitions: value: type: string default: Linux + registryTest: + description: | + Validates the state of a Windows registry key value. + The key format is HIVE\SubKey\Path\ValueName. + When the value name contains backslashes, use "::" as separator: + HIVE\SubKey\Path::ValueName (e.g. HKLM\...\HardenedPaths::\\*\NETLOGON). + Supported hives: HKLM, HKCU, HKCR, HKU, HKCC. + required: + - exists + properties: + title: { "$ref":"#/definitions/title" } + meta: { "$ref":"#/definitions/meta" } + exists: + type: boolean + default: true + value: + anyOf: + - type: string + - type: integer + default: "Windows" + type: + type: string + default: REG_SZ + enum: + - REG_SZ + - REG_EXPAND_SZ + - REG_DWORD + - REG_QWORD + - REG_BINARY + - REG_MULTI_SZ matchingTest: properties: title: { "$ref":"#/definitions/title" } @@ -819,3 +849,9 @@ properties: NOTE: This check is inspecting the contents of local passwd file /etc/passwd, this does not validate remote users (e.g. LDAP). additionalProperties: $ref: "#/definitions/userTest" + + registry: + type: object + description: "Validates the state of a Windows registry key value." + additionalProperties: + $ref: "#/definitions/registryTest" diff --git a/go.mod b/go.mod index b0375c1e9..eb6d34927 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 github.com/urfave/cli v1.22.14 + golang.org/x/sys v0.23.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 ) @@ -54,7 +55,6 @@ require ( golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.23.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/goss_config.go b/goss_config.go index 3f90ba64f..c7f28d1cb 100644 --- a/goss_config.go +++ b/goss_config.go @@ -24,6 +24,7 @@ type GossConfig struct { Interfaces resource.InterfaceMap `json:"interface,omitempty" yaml:"interface,omitempty"` HTTPs resource.HTTPMap `json:"http,omitempty" yaml:"http,omitempty"` Matchings resource.MatchingMap `json:"matching,omitempty" yaml:"matching,omitempty"` + Registries resource.RegistryMap `json:"registry,omitempty" yaml:"registry,omitempty"` } func NewGossConfig() *GossConfig { @@ -44,6 +45,7 @@ func NewGossConfig() *GossConfig { Interfaces: make(resource.InterfaceMap), HTTPs: make(resource.HTTPMap), Matchings: make(resource.MatchingMap), + Registries: make(resource.RegistryMap), } } @@ -109,6 +111,9 @@ func (c *GossConfig) Merge(g2 GossConfig) { for k, v := range g2.Matchings { mergeType(c.Matchings, "matching", k, v) } + for k, v := range g2.Registries { + mergeType(c.Registries, "registry", k, v) + } } func mergeType[V any](m map[string]V, t, k string, v V) { @@ -136,6 +141,7 @@ func (c *GossConfig) Resources() []resource.Resource { c.Mounts, c.Interfaces, c.Matchings, + c.Registries, ) for _, m := range gm { diff --git a/integration-tests/goss/windows/tests/registry.goss.yaml b/integration-tests/goss/windows/tests/registry.goss.yaml new file mode 100644 index 000000000..3e0144df5 --- /dev/null +++ b/integration-tests/goss/windows/tests/registry.goss.yaml @@ -0,0 +1,20 @@ +--- +registry: + HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName: + exists: true + value: + match-regexp: "Windows.*" + type: REG_SZ + + HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentBuild: + exists: true + type: REG_SZ + + HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NonExistentValue12345: + exists: false + + # Test explicit "::" separator for value names containing backslashes. + # HardenedPaths entries use UNC paths as value names (e.g. \\*\NETLOGON). + HKLM\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths::\\*\NETLOGON: + exists: true + skip: true # only present on domain-joined machines with GPO applied diff --git a/resource/registry.go b/resource/registry.go new file mode 100644 index 000000000..1a969eef7 --- /dev/null +++ b/resource/registry.go @@ -0,0 +1,93 @@ +package resource + +import ( + "context" + "fmt" + + "github.com/goss-org/goss/system" + "github.com/goss-org/goss/util" +) + +type Registry struct { + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Meta meta `json:"meta,omitempty" yaml:"meta,omitempty"` + id string `json:"-" yaml:"-"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Exists matcher `json:"exists" yaml:"exists"` + Value matcher `json:"value,omitempty" yaml:"value,omitempty"` + Type matcher `json:"type,omitempty" yaml:"type,omitempty"` + Skip bool `json:"skip,omitempty" yaml:"skip,omitempty"` +} + +const ( + RegistryResourceKey = "registry" + RegistryResourceName = "Registry" +) + +func init() { + registerResource(RegistryResourceKey, &Registry{}) +} + +func (r *Registry) ID() string { + if r.Name != "" && r.Name != r.id { + return fmt.Sprintf("%s: %s", r.id, r.Name) + } + return r.id +} + +func (r *Registry) SetID(id string) { r.id = id } +func (r *Registry) SetSkip() { r.Skip = true } +func (r *Registry) TypeKey() string { return RegistryResourceKey } +func (r *Registry) TypeName() string { return RegistryResourceName } +func (r *Registry) GetTitle() string { return r.Title } +func (r *Registry) GetMeta() meta { return r.Meta } +func (r *Registry) GetName() string { + if r.Name != "" { + return r.Name + } + return r.id +} + +func (r *Registry) Validate(sys *system.System) []TestResult { + ctx := context.WithValue(context.Background(), idKey{}, r.ID()) + skip := r.Skip + sysRegistry := sys.NewRegistry(ctx, r.GetName(), sys, util.Config{}) + + var results []TestResult + results = append(results, ValidateValue(r, "exists", r.Exists, sysRegistry.Exists, skip)) + if shouldSkip(results) { + skip = true + } + if r.Value != nil { + results = append(results, ValidateValue(r, "value", r.Value, sysRegistry.Value, skip)) + } + if r.Type != nil { + results = append(results, ValidateValue(r, "type", r.Type, sysRegistry.Type, skip)) + } + return results +} + +func NewRegistry(sysRegistry system.Registry, config util.Config) (*Registry, error) { + key := sysRegistry.Key() + exists, _ := sysRegistry.Exists() + if !exists { + return &Registry{ + id: key, + Exists: exists, + }, nil + } + value, err := sysRegistry.Value() + if err != nil { + return nil, err + } + regType, err := sysRegistry.Type() + if err != nil { + return nil, err + } + return &Registry{ + id: key, + Exists: exists, + Value: value, + Type: regType, + }, nil +} diff --git a/resource/resource_list.go b/resource/resource_list.go index 61b381596..57f9233f3 100644 --- a/resource/resource_list.go +++ b/resource/resource_list.go @@ -1529,3 +1529,101 @@ func (ret *HTTPMap) UnmarshalYAML(unmarshal func(v interface{}) error) error { *ret = tmp return nil } + +type RegistryMap map[string]*Registry + +func (r RegistryMap) AppendSysResource(sr string, sys *system.System, config util.Config) (*Registry, error) { + ctx := context.WithValue(context.Background(), idKey{}, sr) + sysres := sys.NewRegistry(ctx, sr, sys, config) + res, err := NewRegistry(sysres, config) + if err != nil { + return nil, err + } + if old_res, ok := r[res.ID()]; ok { + res.Title = old_res.Title + res.Meta = old_res.Meta + } + r[res.ID()] = res + return res, nil +} + +func (r RegistryMap) AppendSysResourceIfExists(sr string, sys *system.System) (*Registry, system.Registry, bool, error) { + ctx := context.WithValue(context.Background(), idKey{}, sr) + sysres := sys.NewRegistry(ctx, sr, sys, util.Config{}) + res, err := NewRegistry(sysres, util.Config{}) + if err != nil { + return nil, nil, false, err + } + if e, _ := sysres.Exists(); !e { + return res, sysres, false, nil + } + if old_res, ok := r[res.ID()]; ok { + res.Title = old_res.Title + res.Meta = old_res.Meta + } + r[res.ID()] = res + return res, sysres, true, nil +} + +func (ret *RegistryMap) UnmarshalJSON(data []byte) error { + unmarshal := func(i interface{}) error { + if err := json.Unmarshal(data, i); err != nil { + return err + } + return nil + } + + zero := Registry{} + whitelist, err := util.WhitelistAttrs(zero, util.JSON) + if err != nil { + return err + } + if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { + return err + } + + var tmp map[string]*Registry + if err := unmarshal(&tmp); err != nil { + return err + } + + typ := reflect.TypeOf(zero) + typs := strings.Split(typ.String(), ".")[1] + for id, res := range tmp { + if res == nil { + return fmt.Errorf("Could not parse resource %s:%s", typs, id) + } + res.SetID(id) + } + + *ret = tmp + return nil +} + +func (ret *RegistryMap) UnmarshalYAML(unmarshal func(v interface{}) error) error { + zero := Registry{} + whitelist, err := util.WhitelistAttrs(zero, util.YAML) + if err != nil { + return err + } + if err := util.ValidateSections(unmarshal, zero, whitelist); err != nil { + return err + } + + var tmp map[string]*Registry + if err := unmarshal(&tmp); err != nil { + return err + } + + typ := reflect.TypeOf(zero) + typs := strings.Split(typ.String(), ".")[1] + for id, res := range tmp { + if res == nil { + return fmt.Errorf("Could not parse resource %s:%s", typs, id) + } + res.SetID(id) + } + + *ret = tmp + return nil +} diff --git a/resource/resource_list_genny.go b/resource/resource_list_genny.go index fccdea348..1ff054db3 100644 --- a/resource/resource_list_genny.go +++ b/resource/resource_list_genny.go @@ -15,7 +15,7 @@ import ( "github.com/goss-org/goss/util" ) -//go:generate genny -in=$GOFILE -out=resource_list.go gen "ResourceType=Addr,Command,DNS,File,Gossfile,Group,Package,Port,Process,Service,User,KernelParam,Mount,Interface,HTTP" +//go:generate genny -in=$GOFILE -out=resource_list.go gen "ResourceType=Addr,Command,DNS,File,Gossfile,Group,Package,Port,Process,Service,User,KernelParam,Mount,Interface,HTTP,Registry" //go:generate sed -i -e "/^\\/\\/ +build genny/d" resource_list.go //go:generate sed -i -e "/^\\/\\/go:.*/d" resource_list.go //go:generate sed -i -e "s/aelsabbahy/goss-org/" resource_list.go diff --git a/system/registry.go b/system/registry.go new file mode 100644 index 000000000..46608bcee --- /dev/null +++ b/system/registry.go @@ -0,0 +1,95 @@ +package system + +import ( + "errors" + "strings" +) + +type Registry interface { + Key() string + Exists() (bool, error) + Value() (string, error) + Type() (string, error) +} + +var ErrRegistryUnsupported = errors.New("registry resource is only supported on Windows") + +// registryPathParts holds the parsed components of a registry key path. +type registryPathParts struct { + Hive string + SubKey string + ValueName string +} + +// parseRegistryKey splits a full registry path into hive, subkey, and +// value name. +// +// Two formats are supported: +// +// Standard format: HIVE\subkey\path\ValueName +// The last backslash-separated segment is the value name. A trailing +// backslash indicates the default value. +// +// Explicit format: HIVE\subkey\path::ValueName +// Use "::" to explicitly separate the subkey from the value name. This +// is required when the value name itself contains backslashes (e.g. +// HardenedPaths entries like "\\*\NETLOGON"). +// +// Examples: +// - "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" +// -> Hive="HKLM", SubKey="SOFTWARE\Microsoft\Windows\CurrentVersion", ValueName="Run" +// - "HKLM\SOFTWARE\Microsoft\Windows\" +// -> Hive="HKLM", SubKey="SOFTWARE\Microsoft\Windows", ValueName="" +// - "HKLM\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths::\\*\NETLOGON" +// -> Hive="HKLM", SubKey="SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths", +// ValueName="\\*\NETLOGON" +func parseRegistryKey(key string) (registryPathParts, error) { + if key == "" { + return registryPathParts{}, errors.New("empty registry key") + } + + parts := strings.SplitN(key, `\`, 2) + if len(parts) < 2 { + return registryPathParts{}, errors.New("invalid registry key: missing subkey path") + } + + hive := strings.ToUpper(parts[0]) + switch hive { + case "HKLM", "HKCU", "HKCR", "HKU", "HKCC": + default: + return registryPathParts{}, errors.New("invalid registry hive: " + parts[0]) + } + + rest := parts[1] + if rest == "" { + return registryPathParts{}, errors.New("invalid registry key: empty subkey path") + } + + // Check for explicit "::" separator first. This handles value names + // that contain backslashes (e.g. HardenedPaths UNC entries). + if idx := strings.Index(rest, "::"); idx >= 0 { + return registryPathParts{ + Hive: hive, + SubKey: rest[:idx], + ValueName: rest[idx+2:], + }, nil + } + + // Standard format: split at the last backslash. + // Trailing backslash means default value (empty value name). + lastSep := strings.LastIndex(rest, `\`) + if lastSep < 0 { + // Single segment after hive: treat as value name under root of hive + return registryPathParts{ + Hive: hive, + SubKey: "", + ValueName: rest, + }, nil + } + + return registryPathParts{ + Hive: hive, + SubKey: rest[:lastSep], + ValueName: rest[lastSep+1:], + }, nil +} diff --git a/system/registry_notwindows.go b/system/registry_notwindows.go new file mode 100644 index 000000000..01eb42993 --- /dev/null +++ b/system/registry_notwindows.go @@ -0,0 +1,22 @@ +//go:build !windows + +package system + +import ( + "context" + + "github.com/goss-org/goss/util" +) + +type defRegistry struct { + key string +} + +func NewDefRegistry(_ context.Context, key string, system *System, config util.Config) Registry { + return &defRegistry{key: key} +} + +func (r *defRegistry) Key() string { return r.key } +func (r *defRegistry) Exists() (bool, error) { return false, ErrRegistryUnsupported } +func (r *defRegistry) Value() (string, error) { return "", ErrRegistryUnsupported } +func (r *defRegistry) Type() (string, error) { return "", ErrRegistryUnsupported } diff --git a/system/registry_test.go b/system/registry_test.go new file mode 100644 index 000000000..5feb6f020 --- /dev/null +++ b/system/registry_test.go @@ -0,0 +1,152 @@ +package system + +import ( + "testing" +) + +func TestParseRegistryKey(t *testing.T) { + tests := []struct { + name string + input string + wantHive string + wantSub string + wantValue string + wantErr bool + }{ + { + name: "standard path", + input: `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run`, + wantHive: "HKLM", + wantSub: `SOFTWARE\Microsoft\Windows\CurrentVersion`, + wantValue: "Run", + }, + { + name: "path with spaces", + input: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName`, + wantHive: "HKLM", + wantSub: `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, + wantValue: "ProductName", + }, + { + name: "default value with trailing backslash", + input: `HKLM\SOFTWARE\Microsoft\Windows\`, + wantHive: "HKLM", + wantSub: `SOFTWARE\Microsoft\Windows`, + wantValue: "", + }, + { + name: "HKCU hive", + input: `HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\Hidden`, + wantHive: "HKCU", + wantSub: `Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced`, + wantValue: "Hidden", + }, + { + name: "HKCR hive", + input: `HKCR\.txt\Content Type`, + wantHive: "HKCR", + wantSub: ".txt", + wantValue: "Content Type", + }, + { + name: "HKU hive", + input: `HKU\.DEFAULT\Software\Test`, + wantHive: "HKU", + wantSub: `.DEFAULT\Software`, + wantValue: "Test", + }, + { + name: "HKCC hive", + input: `HKCC\System\CurrentControlSet\Setting`, + wantHive: "HKCC", + wantSub: `System\CurrentControlSet`, + wantValue: "Setting", + }, + { + name: "single segment value under hive", + input: `HKLM\ValueOnly`, + wantHive: "HKLM", + wantSub: "", + wantValue: "ValueOnly", + }, + { + name: "lowercase hive is normalized", + input: `hklm\SOFTWARE\Test`, + wantHive: "HKLM", + wantSub: "SOFTWARE", + wantValue: "Test", + }, + { + name: "empty string", + input: "", + wantErr: true, + }, + { + name: "no backslash", + input: "HKLM", + wantErr: true, + }, + { + name: "invalid hive", + input: `INVALID\SOFTWARE\Test`, + wantErr: true, + }, + { + name: "empty subkey after hive", + input: `HKLM\`, + wantErr: true, + }, + { + name: "explicit separator with backslashes in value name", + input: `HKLM\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths::\\*\NETLOGON`, + wantHive: "HKLM", + wantSub: `SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths`, + wantValue: `\\*\NETLOGON`, + }, + { + name: "explicit separator with SYSVOL UNC path", + input: `HKLM\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths::\\*\SYSVOL`, + wantHive: "HKLM", + wantSub: `SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths`, + wantValue: `\\*\SYSVOL`, + }, + { + name: "explicit separator with simple value name", + input: `HKLM\SOFTWARE\Microsoft\Windows::ProductName`, + wantHive: "HKLM", + wantSub: `SOFTWARE\Microsoft\Windows`, + wantValue: "ProductName", + }, + { + name: "explicit separator with empty value name (default value)", + input: `HKLM\SOFTWARE\Microsoft\Windows::`, + wantHive: "HKLM", + wantSub: `SOFTWARE\Microsoft\Windows`, + wantValue: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseRegistryKey(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.Hive != tt.wantHive { + t.Errorf("Hive = %q, want %q", got.Hive, tt.wantHive) + } + if got.SubKey != tt.wantSub { + t.Errorf("SubKey = %q, want %q", got.SubKey, tt.wantSub) + } + if got.ValueName != tt.wantValue { + t.Errorf("ValueName = %q, want %q", got.ValueName, tt.wantValue) + } + }) + } +} diff --git a/system/registry_windows.go b/system/registry_windows.go new file mode 100644 index 000000000..dea10169e --- /dev/null +++ b/system/registry_windows.go @@ -0,0 +1,171 @@ +//go:build windows + +package system + +import ( + "context" + "encoding/hex" + "fmt" + "strconv" + "strings" + + "golang.org/x/sys/windows/registry" + + "github.com/goss-org/goss/util" +) + +type defRegistryWindows struct { + key string +} + +func NewDefRegistry(_ context.Context, key string, system *System, config util.Config) Registry { + return &defRegistryWindows{key: key} +} + +func (r *defRegistryWindows) Key() string { return r.key } + +func (r *defRegistryWindows) Exists() (bool, error) { + parts, err := parseRegistryKey(r.key) + if err != nil { + return false, err + } + + hive, err := lookupHive(parts.Hive) + if err != nil { + return false, err + } + + k, err := registry.OpenKey(hive, parts.SubKey, registry.QUERY_VALUE) + if err != nil { + return false, nil + } + defer k.Close() + + if parts.ValueName == "" { + // Default value: key exists, that's enough + return true, nil + } + + _, _, err = k.GetValue(parts.ValueName, nil) + if err != nil { + return false, nil + } + return true, nil +} + +func (r *defRegistryWindows) Value() (string, error) { + parts, err := parseRegistryKey(r.key) + if err != nil { + return "", err + } + + hive, err := lookupHive(parts.Hive) + if err != nil { + return "", err + } + + k, err := registry.OpenKey(hive, parts.SubKey, registry.QUERY_VALUE) + if err != nil { + return "", fmt.Errorf("opening registry key: %w", err) + } + defer k.Close() + + _, valType, err := k.GetValue(parts.ValueName, nil) + if err != nil { + return "", fmt.Errorf("reading registry value: %w", err) + } + + return formatValue(valType, k, parts.ValueName) +} + +func (r *defRegistryWindows) Type() (string, error) { + parts, err := parseRegistryKey(r.key) + if err != nil { + return "", err + } + + hive, err := lookupHive(parts.Hive) + if err != nil { + return "", err + } + + k, err := registry.OpenKey(hive, parts.SubKey, registry.QUERY_VALUE) + if err != nil { + return "", fmt.Errorf("opening registry key: %w", err) + } + defer k.Close() + + _, valType, err := k.GetValue(parts.ValueName, nil) + if err != nil { + return "", fmt.Errorf("reading registry value: %w", err) + } + + return typeName(valType), nil +} + +func lookupHive(name string) (registry.Key, error) { + switch name { + case "HKLM": + return registry.LOCAL_MACHINE, nil + case "HKCU": + return registry.CURRENT_USER, nil + case "HKCR": + return registry.CLASSES_ROOT, nil + case "HKU": + return registry.USERS, nil + case "HKCC": + return registry.CURRENT_CONFIG, nil + default: + return 0, fmt.Errorf("unknown registry hive: %s", name) + } +} + +func formatValue(valType uint32, k registry.Key, name string) (string, error) { + switch valType { + case registry.SZ, registry.EXPAND_SZ: + s, _, err := k.GetStringValue(name) + if err != nil { + return "", err + } + return s, nil + case registry.DWORD, registry.QWORD: + v, _, err := k.GetIntegerValue(name) + if err != nil { + return "", err + } + return strconv.FormatUint(v, 10), nil + case registry.BINARY: + b, _, err := k.GetBinaryValue(name) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil + case registry.MULTI_SZ: + ss, _, err := k.GetStringsValue(name) + if err != nil { + return "", err + } + return strings.Join(ss, "\n"), nil + default: + return "", fmt.Errorf("unsupported registry value type: %d", valType) + } +} + +func typeName(t uint32) string { + switch t { + case registry.SZ: + return "REG_SZ" + case registry.EXPAND_SZ: + return "REG_EXPAND_SZ" + case registry.DWORD: + return "REG_DWORD" + case registry.QWORD: + return "REG_QWORD" + case registry.BINARY: + return "REG_BINARY" + case registry.MULTI_SZ: + return "REG_MULTI_SZ" + default: + return fmt.Sprintf("UNKNOWN(%d)", t) + } +} diff --git a/system/system.go b/system/system.go index 6c6083558..5fbdcac95 100644 --- a/system/system.go +++ b/system/system.go @@ -35,6 +35,7 @@ type System struct { NewMount func(context.Context, string, *System, util2.Config) Mount NewInterface func(context.Context, string, *System, util2.Config) Interface NewHTTP func(context.Context, string, *System, util2.Config) HTTP + NewRegistry func(context.Context, string, *System, util2.Config) Registry ports map[string][]GOnetstat.Process portsOnce sync.Once procMap map[string][]ps.Process @@ -73,6 +74,7 @@ func New(packageManager string) *System { NewMount: NewDefMount, NewInterface: NewDefInterface, NewHTTP: NewDefHTTP, + NewRegistry: NewDefRegistry, } sys.detectService()