BED-6446: Add winres tool to generate Windows Resources#169
BED-6446: Add winres tool to generate Windows Resources#169StranDutton merged 6 commits intomainfrom
Conversation
WalkthroughAdds Windows resource generation: new Go CLI to build winres.json and invoke go-winres, workflow steps in Build/Publish to run the CLI on Windows, a new exported Changes
Sequence DiagramsequenceDiagram
participant WF as GitHub Workflow
participant CLI as generate-windows-resources CLI
participant FS as File System
participant Tool as go-winres Tool
WF->>CLI: Run CLI with product version
CLI->>CLI: Parse & validate version
CLI->>CLI: Build winres JSON (icons, RT_VERSION metadata)
CLI->>FS: Create `winres/` directory
CLI->>FS: Write `winres/winres.json`
CLI->>Tool: Execute `go tool go-winres make`
Tool->>FS: Emit Windows resource files
Tool-->>CLI: Return status
CLI->>WF: Exit success/failure
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
cmd/generate-windows-resources/generate-windows-resources.go (2)
67-76:parseProductVersionreads from the globalos.Argsdirectly.This works, but makes the function harder to test in isolation without mutating global state (which the tests do). Consider accepting
[]stringas a parameter for better testability. That said, it's a small build utility so this is purely a style suggestion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/generate-windows-resources/generate-windows-resources.go` around lines 67 - 76, Change parseProductVersion to accept an argument slice (e.g., args []string) instead of reading os.Args directly so it becomes testable; update its signature (parseProductVersion(args []string) (string, error)), replace uses of os.Args within the function with the provided args slice (use args[0] for the program name and args[1] for the version), and update any call sites (main or tests) to pass os.Args or a custom slice when testing.
111-130: Deferredf.Close()error is silently discarded.On buffered/networked filesystems,
Closecan be the call that actually flushes and reports a write error. SinceEncodesucceeds doesn't guarantee the data hit disk. Consider checking the close error explicitly:Suggested fix
func writeWinresConfig(config map[string]interface{}) error { if err := os.MkdirAll(winresDir, 0755); err != nil { return fmt.Errorf("failed to create winres directory: %w", err) } configPath := filepath.Join(winresDir, winresJSONFile) f, err := os.Create(configPath) if err != nil { return fmt.Errorf("failed to create %s: %w", configPath, err) } - defer f.Close() enc := json.NewEncoder(f) enc.SetIndent("", " ") if err := enc.Encode(config); err != nil { + f.Close() return fmt.Errorf("failed to encode JSON: %w", err) } - return nil + return f.Close() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/generate-windows-resources/generate-windows-resources.go` around lines 111 - 130, The deferred file Close in writeWinresConfig currently discards any error from f.Close; capture and handle the Close error so flush/write failures aren't ignored—update writeWinresConfig to check the error returned by f.Close (in the defer or after encoder.Encode) and return or wrap that error (e.g., if enc.Encode succeeded but f.Close returns non-nil, return fmt.Errorf("failed to close %s: %w", configPath, err)). Ensure you reference the same file handle (f) and configPath and propagate the Close error instead of silently dropping it..github/workflows/publish.yml (1)
35-40: Double error suppression:continue-on-error: trueand|| echoare redundant.The
continue-on-error: trueon line 37 already ensures the job won't fail if this step errors. The|| echofallback on line 40 provides a friendlier log message but is technically unnecessary for preventing failure. Not a bug—just noting the overlap. Consider keeping only one mechanism for clarity.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/publish.yml around lines 35 - 40, The step named "Generate Windows Resources" uses both continue-on-error: true and a shell fallback "|| echo 'Failed to generate Windows Resources. Skipping...'", which is redundant; edit that step to remove one mechanism (preferably keep continue-on-error: true and delete the "|| echo ..." suffix in the run block) so error suppression is handled in one place and logs remain clean; look for the step name "Generate Windows Resources" and the run command that invokes go run cmd/generate-windows-resources/generate-windows-resources.go to make the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@go.mod`:
- Line 54: The go.mod currently pulls golang.org/x/image at an old vulnerable
revision; add an explicit module override to bump it to v0.18.0 or later
(recommended v0.36.0) to mitigate CVE-2024-24792 and ensure the indirect
consumer (go-winres) uses the fixed version; update go.mod by adding a
require/replace for golang.org/x/image with the chosen safe version and then run
go mod tidy to update the dependency graph and lockfile.
---
Nitpick comments:
In @.github/workflows/publish.yml:
- Around line 35-40: The step named "Generate Windows Resources" uses both
continue-on-error: true and a shell fallback "|| echo 'Failed to generate
Windows Resources. Skipping...'", which is redundant; edit that step to remove
one mechanism (preferably keep continue-on-error: true and delete the "|| echo
..." suffix in the run block) so error suppression is handled in one place and
logs remain clean; look for the step name "Generate Windows Resources" and the
run command that invokes go run
cmd/generate-windows-resources/generate-windows-resources.go to make the change.
In `@cmd/generate-windows-resources/generate-windows-resources.go`:
- Around line 67-76: Change parseProductVersion to accept an argument slice
(e.g., args []string) instead of reading os.Args directly so it becomes
testable; update its signature (parseProductVersion(args []string) (string,
error)), replace uses of os.Args within the function with the provided args
slice (use args[0] for the program name and args[1] for the version), and update
any call sites (main or tests) to pass os.Args or a custom slice when testing.
- Around line 111-130: The deferred file Close in writeWinresConfig currently
discards any error from f.Close; capture and handle the Close error so
flush/write failures aren't ignored—update writeWinresConfig to check the error
returned by f.Close (in the defer or after encoder.Encode) and return or wrap
that error (e.g., if enc.Encode succeeded but f.Close returns non-nil, return
fmt.Errorf("failed to close %s: %w", configPath, err)). Ensure you reference the
same file handle (f) and configPath and propagate the Close error instead of
silently dropping it.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
go.sumis excluded by!**/*.sumwinres/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (6)
.github/workflows/build.yml.github/workflows/publish.ymlcmd/generate-windows-resources/generate-windows-resources.gocmd/generate-windows-resources/generate-windows-resources_test.goconstants/misc.gogo.mod
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
winres/generate-windows-resources/generate-windows-resources.go (2)
99-99: Inject time source for deterministic metadata generation.Using
time.Now()inline makes output time-dependent and can cause edge-time test flakes (e.g., New Year rollover).⏱️ Proposed refactor
const ( @@ fileVersion = "0.0.0.0" // Windows PE file version; we will update 'productVersion' field instead of this one ) + +var now = time.Now @@ - "LegalCopyright": fmt.Sprintf("Copyright (C) %d %s", time.Now().Year(), constants.Company), + "LegalCopyright": fmt.Sprintf("Copyright (C) %d %s", now().Year(), constants.Company),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@winres/generate-windows-resources/generate-windows-resources.go` at line 99, The code uses time.Now() inline when building the "LegalCopyright" string causing non-deterministic output; refactor the function that constructs the metadata (the function that sets "LegalCopyright" in generate-windows-resources.go) to accept an injected time source or explicit year parameter (e.g., a time.Time or int year) instead of calling time.Now() inside the function, and replace fmt.Sprintf("Copyright (C) %d %s", time.Now().Year(), constants.Company) with a format that uses the injected value so tests can pass a fixed year for deterministic metadata generation.
47-47: Decouple argument parsing fromos.Argsand normalize input.
parseProductVersioncurrently depends on process-global state and accepts whitespace-only values. Passing args explicitly and trimming keeps behavior deterministic and test-friendly.♻️ Proposed refactor
import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" + "strings" "time" @@ func run() error { - productVersion, err := parseProductVersion() + productVersion, err := parseProductVersion(os.Args) if err != nil { return err } @@ -func parseProductVersion() (string, error) { - if len(os.Args) < 2 { - return "", fmt.Errorf("usage: %s <product-version>", filepath.Base(os.Args[0])) +func parseProductVersion(args []string) (string, error) { + if len(args) < 2 { + return "", fmt.Errorf("usage: %s <product-version>", filepath.Base(args[0])) } - version := os.Args[1] + version := strings.TrimSpace(args[1]) if version == "" { return "", fmt.Errorf("product version cannot be empty") } return version, nil }Also applies to: 67-75
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@winres/generate-windows-resources/generate-windows-resources.go` at line 47, Change parseProductVersion to stop reading os.Args directly: give it an explicit input (e.g., func parseProductVersion(raw string) (string, error) or func parseProductVersion(args []string) ...) and trim the received string (strings.TrimSpace) before validation; if the trimmed value is empty return a descriptive error instead of accepting whitespace-only input. Update the call site that does productVersion, err := parseProductVersion() to pass the appropriate argument (the specific CLI token or os.Args element) and apply the same refactor+trim/validation to the other parsing helpers in this file so all argument parsing is deterministic and testable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@winres/generate-windows-resources/generate-windows-resources.go`:
- Around line 111-127: The writeWinresConfig function currently defers f.Close()
without checking its error; update it to capture and handle Close() failures
(after encoding with enc.Encode) by replacing the simple defer f.Close() with a
deferred closure that checks the error returned by f.Close() and wraps/returns
it (or appends it to any existing error from enc.Encode) so the function fails
if the file close/fsync fails; reference the writeWinresConfig function, the
file handle f, configPath and the enc.Encode call when implementing this change.
---
Nitpick comments:
In `@winres/generate-windows-resources/generate-windows-resources.go`:
- Line 99: The code uses time.Now() inline when building the "LegalCopyright"
string causing non-deterministic output; refactor the function that constructs
the metadata (the function that sets "LegalCopyright" in
generate-windows-resources.go) to accept an injected time source or explicit
year parameter (e.g., a time.Time or int year) instead of calling time.Now()
inside the function, and replace fmt.Sprintf("Copyright (C) %d %s",
time.Now().Year(), constants.Company) with a format that uses the injected value
so tests can pass a fixed year for deterministic metadata generation.
- Line 47: Change parseProductVersion to stop reading os.Args directly: give it
an explicit input (e.g., func parseProductVersion(raw string) (string, error) or
func parseProductVersion(args []string) ...) and trim the received string
(strings.TrimSpace) before validation; if the trimmed value is empty return a
descriptive error instead of accepting whitespace-only input. Update the call
site that does productVersion, err := parseProductVersion() to pass the
appropriate argument (the specific CLI token or os.Args element) and apply the
same refactor+trim/validation to the other parsing helpers in this file so all
argument parsing is deterministic and testable.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
.github/workflows/build.yml.github/workflows/publish.ymlwinres/generate-windows-resources/generate-windows-resources.gowinres/generate-windows-resources/generate-windows-resources_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- .github/workflows/build.yml
- .github/workflows/publish.yml
| func writeWinresConfig(config map[string]interface{}) error { | ||
| if err := os.MkdirAll(winresDir, 0755); err != nil { | ||
| return fmt.Errorf("failed to create winres directory: %w", err) | ||
| } | ||
|
|
||
| configPath := filepath.Join(winresDir, winresJSONFile) | ||
| f, err := os.Create(configPath) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create %s: %w", configPath, err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| enc := json.NewEncoder(f) | ||
| enc.SetIndent("", " ") | ||
| if err := enc.Encode(config); err != nil { | ||
| return fmt.Errorf("failed to encode JSON: %w", err) | ||
| } |
There was a problem hiding this comment.
Handle Close() errors when writing winres.json.
Ignoring Close failures can return success even when the file write is incomplete (Line 121).
🛠️ Proposed fix
-func writeWinresConfig(config map[string]interface{}) error {
+func writeWinresConfig(config map[string]interface{}) (err error) {
if err := os.MkdirAll(winresDir, 0755); err != nil {
return fmt.Errorf("failed to create winres directory: %w", err)
}
@@
- f, err := os.Create(configPath)
+ f, err := os.Create(configPath)
if err != nil {
return fmt.Errorf("failed to create %s: %w", configPath, err)
}
- defer f.Close()
+ defer func() {
+ if cerr := f.Close(); err == nil && cerr != nil {
+ err = fmt.Errorf("failed to close %s: %w", configPath, cerr)
+ }
+ }()
@@
if err := enc.Encode(config); err != nil {
return fmt.Errorf("failed to encode JSON: %w", err)
}
return nil
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@winres/generate-windows-resources/generate-windows-resources.go` around lines
111 - 127, The writeWinresConfig function currently defers f.Close() without
checking its error; update it to capture and handle Close() failures (after
encoding with enc.Encode) by replacing the simple defer f.Close() with a
deferred closure that checks the error returned by f.Close() and wraps/returns
it (or appends it to any existing error from enc.Encode) so the function fails
if the file close/fsync fails; reference the writeWinresConfig function, the
file handle f, configPath and the enc.Encode call when implementing this change.
9ddd023 to
a70e4c7
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
winres/generate-windows-resources/generate-windows-resources.go (1)
111-129:⚠️ Potential issue | 🟡 MinorHandle
Close()failures when writingwinres.json.Line 121 defers
f.Close()without checking its error. A close/flush failure can still return success from this function.Proposed fix
-func writeWinresConfig(config map[string]interface{}) error { +func writeWinresConfig(config map[string]interface{}) (err error) { if err := os.MkdirAll(winresDir, 0755); err != nil { return fmt.Errorf("failed to create winres directory: %w", err) } configPath := filepath.Join(winresDir, winresJSONFile) - f, err := os.Create(configPath) + f, err := os.Create(configPath) if err != nil { return fmt.Errorf("failed to create %s: %w", configPath, err) } - defer f.Close() + defer func() { + if cerr := f.Close(); err == nil && cerr != nil { + err = fmt.Errorf("failed to close %s: %w", configPath, cerr) + } + }() enc := json.NewEncoder(f) enc.SetIndent("", " ") if err := enc.Encode(config); err != nil { return fmt.Errorf("failed to encode JSON: %w", err) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@winres/generate-windows-resources/generate-windows-resources.go` around lines 111 - 129, The deferred f.Close() in writeWinresConfig is ignored, so any close/flush errors are lost; update writeWinresConfig to capture and return Close() errors by replacing the simple defer f.Close() with a deferred check that assigns the result of f.Close() to the function's error return (or wraps it) — e.g., use a named error return or a local err variable and in defer do if cerr := f.Close(); cerr != nil { if err == nil { err = fmt.Errorf("failed to close %s: %w", configPath, cerr) } else { err = fmt.Errorf("%v; failed to close %s: %w", err, cerr) } } — keeping the existing JSON encode error handling and file path values (winresDir, winresJSONFile, configPath).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@winres/generate-windows-resources/generate-windows-resources.go`:
- Around line 111-129: The deferred f.Close() in writeWinresConfig is ignored,
so any close/flush errors are lost; update writeWinresConfig to capture and
return Close() errors by replacing the simple defer f.Close() with a deferred
check that assigns the result of f.Close() to the function's error return (or
wraps it) — e.g., use a named error return or a local err variable and in defer
do if cerr := f.Close(); cerr != nil { if err == nil { err = fmt.Errorf("failed
to close %s: %w", configPath, cerr) } else { err = fmt.Errorf("%v; failed to
close %s: %w", err, cerr) } } — keeping the existing JSON encode error handling
and file path values (winresDir, winresJSONFile, configPath).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 174630bd-d3bb-4082-82e9-4773130e61bb
⛔ Files ignored due to path filters (2)
go.sumis excluded by!**/*.sumwinres/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (6)
.github/workflows/build.yml.github/workflows/publish.ymlconstants/misc.gogo.modwinres/generate-windows-resources/generate-windows-resources.gowinres/generate-windows-resources/generate-windows-resources_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
- .github/workflows/build.yml
- constants/misc.go
- go.mod
- winres/generate-windows-resources/generate-windows-resources_test.go
- .github/workflows/publish.yml
Ticket
https://specterops.atlassian.net/browse/BED-6446
Summary
go-winresin order to apply manifest information to the.exefor Windows users.buildandpublishGitHub actions in order to generate these resources just before the application is built and published.go-winres.Go 1.23.0 -> 1.24.0- I reran unit tests and performed a smoke test to make sure everything still works as expected after updating.Testing
act(https://github.com/nektos/act?tab=readme-ov-file)Testing Instructions
Goal: run the script I created to generate the Windows resources, build the image locally, and then check the
.exeto make sure that file properties get populated.Install
go-winresonto the windows machine (not in the AzH project directory)go install http://github.com/tc-hib/go-winres@latestAfter
go-winresis installed, pull my branchBED-6446-include-version-in-exe-propsThen run
go run cmd/generate-windows-resources/generate-windows-resources.go "v0.0.0-rolling+8675".sysofile is generatedBuild AzureHound (on Windows) with this command:
Find the
.exeand inspect the properties to ensure everything looks correct.Demo
All information populated as expected when examining the properties of the .exe ✅


Summary by CodeRabbit
New Features
Chores