Skip to content

feat: add registry resource type for Windows - #1053

Open
Blankf wants to merge 2 commits into
goss-org:masterfrom
Blankf:feat/registry-resource
Open

feat: add registry resource type for Windows#1053
Blankf wants to merge 2 commits into
goss-org:masterfrom
Blankf:feat/registry-resource

Conversation

@Blankf

@Blankf Blankf commented Apr 15, 2026

Copy link
Copy Markdown

Depends on: #1055 (fix: bytes.Reader issue in HavePatternsMatcher)

Summary

Adds a new registry resource type that validates Windows registry keys natively using the golang.org/x/sys/windows/registry API. This replaces the need to shell out to PowerShell for registry checks.

Closes #616

Motivation

The ansible-lockdown Windows CIS audit repos (Windows 2016/2019/2025) currently rely entirely on the command resource shelling out to PowerShell for ~300+ registry checks per benchmark. This is slow (~180ms per check due to PowerShell process spawn) and verbose.

With the native registry resource, the same checks run in milliseconds via direct Windows API calls.

Benchmark results (250 identical checks):

Method Time
Native registry resource 0.020s
command + PowerShell 43.784s

Gossfile syntax

registry:
  HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName:
    exists: true
    value: "Windows Server 2025 Datacenter"
    type: REG_SZ

For value names containing backslashes (e.g. HardenedPaths UNC entries), use :: as an explicit separator:

registry:
  HKLM\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths::\\*\NETLOGON:
    exists: true
    value: "RequireMutualAuthentication=1, RequireIntegrity=1"

Details

  • Supported hives: HKLM, HKCU, HKCR, HKU, HKCC
  • Supported types: REG_SZ, REG_EXPAND_SZ, REG_DWORD, REG_QWORD, REG_BINARY, REG_MULTI_SZ
  • Opens keys with QUERY_VALUE access (read-only, least privilege)
  • On non-Windows platforms, returns an error ("registry resource is only supported on Windows"), following the NullPackage pattern
  • Cross-compilation from Linux continues to work

Files changed

New files:

  • system/registry.go -- Registry interface + key path parser
  • system/registry_test.go -- 17 unit tests for the parser
  • system/registry_notwindows.go -- Non-Windows stub
  • system/registry_windows.go -- Windows implementation
  • resource/registry.go -- Resource type definition
  • integration-tests/goss/windows/tests/registry.goss.yaml -- Integration tests
  • docs/windows-parity-progress.md -- Progress tracker

Modified files:

  • system/system.go -- Added NewRegistry factory
  • resource/resource_list_genny.go + resource/resource_list.go -- Added RegistryMap
  • goss_config.go -- Wired into config
  • add.go -- Added to AddResource switch
  • cmd/goss/goss.go -- Added CLI subcommand
  • docs/schema.yaml -- Added schema definition
  • docs/platforms.md -- Updated platform matrix

Test plan

  • go vet ./... passes
  • go test ./... passes (17 new parser tests + all existing)
  • GOOS=windows GOARCH=amd64 go build cross-compiles successfully
  • gosec ./system/ ./resource/ -- zero findings in new code
  • Tested on Windows Server 2025 -- registry checks work correctly
  • Tested on Windows Server 2025 -- existing command resource still works
  • Benchmarked: 250 registry checks in 0.020s vs 43.784s via PowerShell

@Blankf
Blankf requested a review from aelsabbahy as a code owner April 15, 2026 13:33
@Blankf
Blankf force-pushed the feat/registry-resource branch from 7ac5f7b to eb7a099 Compare April 15, 2026 13:36
@Blankf
Blankf marked this pull request as draft April 20, 2026 05:24
@Blankf
Blankf marked this pull request as ready for review April 20, 2026 10:57
Comment thread cmd/goss/goss.go Outdated
return goss.AddResources(c.GlobalString("gossfile"), resource.InterfaceResourceName, c.Args(), newRuntimeConfigFromCLI(c))
},
},
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just an FYI: #1060 will break this if/when it's merged. The fix is easy though: it's just an indentation change and Action: func(c *cli.Context) error { needs to become Action: func(_ context.Context, c *cli.Context) error {.

@kgaughan

Copy link
Copy Markdown
Collaborator

It was merged just a few hours ago, so you'll need to rebase or merge from master to resolve the issues. It should be a relatively straightforward merge conflict to resolve.

Add a new registry resource type that validates Windows registry keys
natively using the golang.org/x/sys/windows/registry API. This replaces
the need to shell out to PowerShell for registry checks, providing
significant performance improvements (0.02s vs 44s for 250 checks).

Gossfile syntax:

  registry:
    HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName:
      exists: true
      value: "Windows Server 2025 Datacenter"
      type: REG_SZ

For value names containing backslashes (e.g. HardenedPaths UNC entries),
use "::" as an explicit separator:

  registry:
    HKLM\...\HardenedPaths::\\*\NETLOGON:
      exists: true

Supported hives: HKLM, HKCU, HKCR, HKU, HKCC.
Supported types: REG_SZ, REG_EXPAND_SZ, REG_DWORD, REG_QWORD,
REG_BINARY, REG_MULTI_SZ.

On non-Windows platforms, the resource returns an error indicating
it is only supported on Windows, following the NullPackage pattern.
@Blankf
Blankf force-pushed the feat/registry-resource branch from eb7a099 to 0cc113f Compare July 7, 2026 12:25
@Blankf

Blankf commented Jul 7, 2026

Copy link
Copy Markdown
Author

Rebased on top of current master (906c395). Scope of this update is limited to what's needed to resolve the conflicts from #1060 and get the branch buildable again:

  • cmd/goss/goss.go — the new registry add subcommand now uses the v3 API:

    Action: func(ctx context.Context, c *cli.Command) error {
        fatalAlphaIfNeeded(c)
        return goss.AddResources(c.String("gossfile"), resource.RegistryResourceName, c.Args().Slice(), newRuntimeConfigFromCLI(c))
    },

    Note: @kgaughan's hint suggested *cli.Context, but the actual v3 rename in Switch to urfave/cli/v3 #1060 is to *cli.Command, so that's what I used (matches every other subcommand in the file).

    Also fixed a placement bug the auto-merge introduced: the block ended up outside the add command's Commands slice, which wouldn't compile. It's now back inside the slice next to interface.

  • go.mod — kept upstream's Go 1.25.11 and bumped deps; the only PR-owned change is promoting golang.org/x/sys from indirect to direct require. go mod tidy clean, no go.sum changes needed.

  • goss_config.go — one gofmt-driven change: the Registries struct tag alignment (single-space fixed to match the column).

  • system/registry.go — one gofmt-driven change: doc-comment indentation reflow (Go 1.25 stricter comment formatter).

Verified locally in golang:1.25:

  • go vet ./... clean
  • go test ./... all packages pass
  • go build ./... (linux/amd64) clean
  • GOOS=windows GOARCH=amd64 go build ./... clean
  • gofmt -l . clean

Diff shape unchanged: 15 files, +711/-2 (was +726/-3; the delta is entirely from the tighter go.mod hunk).

The dependency note on #1055 stays as-is — this PR doesn't touch any file that #1055 touches, so the two rebase independently, but #1055 is still recommended to land first per the original description.

Comment thread resource/resource_list.go
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This has put something on my to-do list: we should really be passing the context down from main(). I'll look into doing that later.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

#1090 is there as a reminder for me to fix up context handling.

@kgaughan kgaughan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good! I've some suggestions regarding some tweaks to the error handling: I want to be able to turn on the err113 linter at some point.

Comment thread system/registry.go
// ValueName="\\*\NETLOGON"
func parseRegistryKey(key string) (registryPathParts, error) {
if key == "" {
return registryPathParts{}, errors.New("empty registry key")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you lift this error out so it's defined once? Something like this:

var errInvalidRegistryKey = errors.New("invalid registry key")

// ...

	if key == "" {
		return registryPathParts{}, fmt.Errorf("%w: empty key", errInvalidRegistryKey)

Comment thread system/registry.go

parts := strings.SplitN(key, `\`, 2)
if len(parts) < 2 {
return registryPathParts{}, errors.New("invalid registry key: missing subkey path")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ditto: could you lift this error out so it's defined once? Going off the above, this would become:

Suggested change
return registryPathParts{}, errors.New("invalid registry key: missing subkey path")
return registryPathParts{}, fmt.Errorf("%w: missing subkey path", errInvalidRegistryKey)

Comment thread system/registry.go
switch hive {
case "HKLM", "HKCU", "HKCR", "HKU", "HKCC":
default:
return registryPathParts{}, errors.New("invalid registry hive: " + parts[0])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same with this, though I'd suggest something like this:

var errInvalidRegistryHive = errors.New("invalid registry hive")

// ...
	default:
		return registryPathParts{}, fmt.Errorf("%w: %s", errInvalidRegistryHive, parts[0])

Comment thread system/registry.go

rest := parts[1]
if rest == "" {
return registryPathParts{}, errors.New("invalid registry key: empty subkey path")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same again:

Suggested change
return registryPathParts{}, errors.New("invalid registry key: empty subkey path")
return registryPathParts{}, fmt.Errorf("%w: empty subkey path", errInvalidRegistryKey)

Comment thread resource/resource_list.go
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

#1090 is there as a reminder for me to fix up context handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Goss should be able to assert against Windows Registry state

2 participants