Skip to content

Commit 60695c9

Browse files
jongioCopilot
andauthored
Security hardening and code quality improvements (#41)
* Security hardening and code quality improvements MQ + Hack dual-model analysis (Opus 4.6 + Codex 5.3) findings and fixes: HIGH: - MCP handlers bypass Key Vault secret resolution (CWE-522) - Secret filtering denylist incomplete - added PAT, SAS, SIGNING, PRIVATE, PASSPHRASE, AUTH (CWE-532) - New() silently ignoring validation errors - now returns error (CWE-754) MEDIUM: - Shell name case sensitivity causing fallback issues (CWE-178) - Dead path traversal check removed (CWE-561) - Deprecated // +build directives replaced with //go:build (CWE-477) Changed New() signature to return error, updated 30+ callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: update to azd-core v0.5.4 and azd-web-core 2.4.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0eb2cba commit 60695c9

17 files changed

Lines changed: 887 additions & 127 deletions

cli/go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.26.0
44

55
require (
66
github.com/azure/azure-dev/cli/azd v0.0.0-20260228002641-8f080b39d69b
7-
github.com/jongio/azd-core v0.5.3
7+
github.com/jongio/azd-core v0.5.4
88
github.com/magefile/mage v1.15.0
99
github.com/mark3labs/mcp-go v0.43.2
1010
github.com/spf13/cobra v1.10.2
@@ -83,12 +83,12 @@ require (
8383
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
8484
go.opentelemetry.io/otel/trace v1.40.0 // indirect
8585
go.uber.org/atomic v1.11.0 // indirect
86-
golang.org/x/crypto v0.47.0 // indirect
86+
golang.org/x/crypto v0.48.0 // indirect
8787
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
88-
golang.org/x/net v0.49.0 // indirect
89-
golang.org/x/sys v0.40.0 // indirect
90-
golang.org/x/term v0.39.0 // indirect
91-
golang.org/x/text v0.33.0 // indirect
88+
golang.org/x/net v0.51.0 // indirect
89+
golang.org/x/sys v0.41.0 // indirect
90+
golang.org/x/term v0.40.0 // indirect
91+
golang.org/x/text v0.34.0 // indirect
9292
golang.org/x/time v0.14.0 // indirect
9393
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
9494
google.golang.org/grpc v1.78.0 // indirect

cli/go.sum

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/
123123
github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I=
124124
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
125125
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
126-
github.com/jongio/azd-core v0.5.3 h1:gCPJs61kUCdRYZZq4Dy4Ncz+S+00rDr9jt5CPpMgBrE=
127-
github.com/jongio/azd-core v0.5.3/go.mod h1:jQCP+px3Pxb3B0fyShfvSVa3KsWT1j2jGXMsPpQezlI=
126+
github.com/jongio/azd-core v0.5.4 h1:pCGn+Q+NJ4pJFsojqf0sn5osTHW8mMRzD3O6SjnlFKc=
127+
github.com/jongio/azd-core v0.5.4/go.mod h1:XeXQJVNJ/+GbBfsYIyDdJ6E3SHCrKlEqnv6eDVqDKQ4=
128128
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
129129
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
130130
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
@@ -240,17 +240,17 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
240240
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
241241
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
242242
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
243-
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
244-
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
243+
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
244+
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
245245
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
246246
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
247247
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
248248
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
249249
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
250250
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
251251
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
252-
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
253-
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
252+
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
253+
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
254254
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
255255
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
256256
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -264,18 +264,18 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
264264
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
265265
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
266266
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
267-
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
268-
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
267+
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
268+
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
269269
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
270270
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
271-
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
272-
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
271+
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
272+
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
273273
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
274274
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
275275
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
276276
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
277-
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
278-
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
277+
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
278+
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
279279
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
280280
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
281281
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

cli/src/cmd/exec/commands/mcp.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
1414
"github.com/jongio/azd-core/azdextutil"
15+
"github.com/jongio/azd-core/keyvault"
1516
"github.com/jongio/azd-core/security"
1617
"github.com/jongio/azd-core/shellutil"
1718
"github.com/jongio/azd-exec/cli/src/internal/version"
@@ -176,7 +177,10 @@ func handleExecScript(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolR
176177

177178
cmdArgs := buildShellArgs(shell, validPath, false, scriptArgs)
178179
cmd := exec.CommandContext(execCtx, cmdArgs[0], cmdArgs[1:]...)
179-
cmd.Env = os.Environ()
180+
181+
// Resolve Key Vault references in environment variables, matching the
182+
// CLI execution path behavior. Continue on error (best-effort).
183+
cmd.Env = prepareEnvironmentForMCP(ctx)
180184

181185
var stdout, stderr bytes.Buffer
182186
cmd.Stdout = &stdout
@@ -214,7 +218,10 @@ func handleExecInline(ctx context.Context, args azdext.ToolArgs) (*mcp.CallToolR
214218

215219
cmdArgs := buildShellArgs(shell, command, true, nil)
216220
cmd := exec.CommandContext(execCtx, cmdArgs[0], cmdArgs[1:]...)
217-
cmd.Env = os.Environ()
221+
222+
// Resolve Key Vault references in environment variables, matching the
223+
// CLI execution path behavior. Continue on error (best-effort).
224+
cmd.Env = prepareEnvironmentForMCP(ctx)
218225

219226
var stdout, stderr bytes.Buffer
220227
cmd.Stdout = &stdout
@@ -283,8 +290,13 @@ func handleGetEnvironment(_ context.Context, _ azdext.ToolArgs) (*mcp.CallToolRe
283290
continue
284291
}
285292

286-
// Exclude known secret-bearing variable names
287-
secretPatterns := []string{"SECRET", "PASSWORD", "KEY", "TOKEN", "CREDENTIAL", "CERTIFICATE", "CONNECTION_STRING", "CONNSTR"}
293+
// Exclude known secret-bearing variable names.
294+
// This denylist covers common Azure, cloud, and application secret patterns.
295+
secretPatterns := []string{
296+
"SECRET", "PASSWORD", "KEY", "TOKEN", "CREDENTIAL", "CERTIFICATE",
297+
"CONNECTION_STRING", "CONNSTR", "PAT", "SAS", "SIGNING",
298+
"PRIVATE", "PASSPHRASE", "AUTH",
299+
}
288300
isSecret := false
289301
upperName := strings.ToUpper(name)
290302
for _, pattern := range secretPatterns {
@@ -306,6 +318,41 @@ func handleGetEnvironment(_ context.Context, _ azdext.ToolArgs) (*mcp.CallToolRe
306318

307319
// --- Helpers ---
308320

321+
// prepareEnvironmentForMCP resolves Key Vault references in environment variables.
322+
// This mirrors the CLI execution path (executor.prepareEnvironment) to ensure
323+
// consistent behavior between CLI and MCP invocations. Operates in best-effort
324+
// mode: if resolution fails, the original environment is returned unchanged.
325+
func prepareEnvironmentForMCP(ctx context.Context) []string {
326+
envVars := os.Environ()
327+
328+
// Quick check: skip resolver setup if no Key Vault references are present.
329+
hasRef := false
330+
for _, envVar := range envVars {
331+
if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 && keyvault.IsKeyVaultReference(parts[1]) {
332+
hasRef = true
333+
break
334+
}
335+
}
336+
if !hasRef {
337+
return envVars
338+
}
339+
340+
resolver, err := keyvault.NewKeyVaultResolver()
341+
if err != nil {
342+
// Cannot create resolver — fall back to raw environment.
343+
fmt.Fprintf(os.Stderr, "Warning: failed to create Key Vault resolver: %v\n", err)
344+
return envVars
345+
}
346+
347+
resolved, _, err := resolver.ResolveEnvironmentVariables(ctx, envVars, keyvault.ResolveEnvironmentOptions{StopOnError: false})
348+
if err != nil {
349+
fmt.Fprintf(os.Stderr, "Warning: Key Vault resolution error: %v\n", err)
350+
return envVars
351+
}
352+
353+
return resolved
354+
}
355+
309356
type execResult struct {
310357
Stdout string `json:"stdout"`
311358
Stderr string `json:"stderr"`

cli/src/cmd/exec/commands/version_integration_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
//go:build integration
2-
// +build integration
32

43
package commands
54

cli/src/cmd/exec/main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type scriptExecutor interface {
3333
ExecuteInline(ctx context.Context, scriptContent string) error
3434
}
3535

36-
var newScriptExecutor = func(config executor.Config) scriptExecutor {
36+
var newScriptExecutor = func(config executor.Config) (scriptExecutor, error) {
3737
return executor.New(config)
3838
}
3939

@@ -75,12 +75,15 @@ Examples:
7575
}
7676

7777
// Create executor
78-
exec := newScriptExecutor(executor.Config{
78+
exec, err := newScriptExecutor(executor.Config{
7979
Shell: shell,
8080
Interactive: interactive,
8181
StopOnKeyVaultError: stopOnKeyVaultError,
8282
Args: scriptArgs,
8383
})
84+
if err != nil {
85+
return fmt.Errorf("invalid configuration: %w", err)
86+
}
8487

8588
// Check if input is a file or inline script
8689
// Try to resolve as file path first

cli/src/cmd/exec/main_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ func TestRunE_DispatchesFileOrInline(t *testing.T) {
9090
defer func() { newScriptExecutor = oldNew }()
9191

9292
fake := &fakeExecutor{}
93-
newScriptExecutor = func(cfg executor.Config) scriptExecutor {
93+
newScriptExecutor = func(cfg executor.Config) (scriptExecutor, error) {
9494
fake.args = append([]string{}, cfg.Args...)
95-
return fake
95+
return fake, nil
9696
}
9797

9898
// Avoid changing env/cwd during Execute.
@@ -150,9 +150,9 @@ func TestRunE_AllowsPassthroughArgsWithoutDoubleDash(t *testing.T) {
150150
defer func() { newScriptExecutor = oldNew }()
151151

152152
fake := &fakeExecutor{}
153-
newScriptExecutor = func(cfg executor.Config) scriptExecutor {
153+
newScriptExecutor = func(cfg executor.Config) (scriptExecutor, error) {
154154
fake.args = append([]string{}, cfg.Args...)
155-
return fake
155+
return fake, nil
156156
}
157157

158158
// Avoid changing env/cwd during Execute.

cli/src/internal/executor/command_builder.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,44 @@ var validShells = map[string]bool{
2525
// - Windows cmd: Use /c for both inline and files
2626
// - Unknown shells: Fall back to -c flag (Unix-like behavior)
2727
//
28+
// Known shell names are normalized to lowercase for the executable binary
29+
// to ensure correct lookup on case-sensitive filesystems.
2830
// Script arguments (e.config.Args) are appended after the script specification.
2931
func (e *Executor) buildCommand(shell, scriptOrPath string, isInline bool) *exec.Cmd {
3032
var cmdArgs []string
3133
skipAppendArgs := false
3234

33-
// Normalize shell name to lowercase for comparison
35+
// Normalize shell name to lowercase for both comparison and executable name.
36+
// This ensures correct binary lookup on case-sensitive filesystems
37+
// (e.g., --shell BASH resolves to "bash", not "BASH").
3438
shellLower := strings.ToLower(shell)
3539

40+
// Use the lowercase name for known shells; keep original for unknown shells
41+
// (custom interpreters like "Python3" should preserve user's casing).
42+
shellBin := shell
43+
if validShells[shellLower] {
44+
shellBin = shellLower
45+
}
46+
3647
switch shellLower {
3748
case shellutil.ShellBash, shellutil.ShellSh, shellutil.ShellZsh:
3849
if isInline {
39-
cmdArgs = []string{shell, "-c", scriptOrPath}
50+
cmdArgs = []string{shellBin, "-c", scriptOrPath}
4051
} else {
41-
cmdArgs = []string{shell, scriptOrPath}
52+
cmdArgs = []string{shellBin, scriptOrPath}
4253
}
4354
case shellutil.ShellPwsh, shellutil.ShellPowerShell:
4455
if isInline {
45-
cmdArgs = []string{shell, "-Command", e.buildPowerShellInlineCommand(scriptOrPath)}
56+
cmdArgs = []string{shellBin, "-Command", e.buildPowerShellInlineCommand(scriptOrPath)}
4657
skipAppendArgs = true
4758
} else {
48-
cmdArgs = []string{shell, "-File", scriptOrPath}
59+
cmdArgs = []string{shellBin, "-File", scriptOrPath}
4960
}
5061
case shellutil.ShellCmd:
51-
cmdArgs = []string{shell, "/c", scriptOrPath}
62+
cmdArgs = []string{shellBin, "/c", scriptOrPath}
5263
default:
53-
// Unknown shell: use Unix-like -c pattern as fallback
64+
// Unknown shell: use Unix-like -c pattern as fallback.
65+
// Preserve original casing for custom interpreters.
5466
if isInline {
5567
cmdArgs = []string{shell, "-c", scriptOrPath}
5668
} else {

cli/src/internal/executor/command_builder_test.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ func TestBuildCommandWithCustomShell(t *testing.T) {
3131

3232
for _, tt := range tests {
3333
t.Run(tt.name, func(t *testing.T) {
34-
exec := New(Config{Shell: tt.shell, Args: tt.args})
34+
exec, err := New(Config{Args: tt.args})
35+
if err != nil {
36+
t.Fatalf("New() error: %v", err)
37+
}
3538
cmd := exec.buildCommand(tt.shell, tt.scriptPath, false)
3639

3740
// Check if command was built
@@ -75,7 +78,10 @@ func TestBuildCommandShellVariations(t *testing.T) {
7578

7679
for _, tt := range tests {
7780
t.Run(tt.shell, func(t *testing.T) {
78-
exec := New(Config{Shell: tt.shell})
81+
exec, err := New(Config{Shell: tt.shell})
82+
if err != nil {
83+
t.Fatalf("New() error: %v", err)
84+
}
7985
cmd := exec.buildCommand(tt.shell, tt.scriptPath, false)
8086

8187
if cmd == nil {
@@ -87,7 +93,10 @@ func TestBuildCommandShellVariations(t *testing.T) {
8793

8894
func TestBuildCommandLookPath(t *testing.T) {
8995
// Test that buildCommand creates a valid exec.Cmd
90-
exec := New(Config{})
96+
exec, err := New(Config{})
97+
if err != nil {
98+
t.Fatalf("New() error: %v", err)
99+
}
91100
cmd := exec.buildCommand("cmd", "test.bat", false)
92101

93102
// On Windows, cmd should be findable
@@ -96,7 +105,7 @@ func TestBuildCommandLookPath(t *testing.T) {
96105
}
97106

98107
// Verify we can look up the command
99-
_, err := execLookPath(cmd.Args[0])
108+
_, err = execLookPath(cmd.Args[0])
100109
if err != nil {
101110
t.Logf("Command %v not found in PATH (may be platform-specific)", cmd.Args[0])
102111
}
@@ -133,15 +142,21 @@ func TestQuotePowerShellArg(t *testing.T) {
133142

134143
func TestBuildPowerShellInlineCommand(t *testing.T) {
135144
t.Run("no args returns script as-is", func(t *testing.T) {
136-
e := New(Config{})
145+
e, err := New(Config{})
146+
if err != nil {
147+
t.Fatalf("New() error: %v", err)
148+
}
137149
got := e.buildPowerShellInlineCommand("Get-Date")
138150
if got != "Get-Date" {
139151
t.Errorf("got %q, want %q", got, "Get-Date")
140152
}
141153
})
142154

143155
t.Run("with args joins and quotes", func(t *testing.T) {
144-
e := New(Config{Args: []string{"arg1", "it's"}})
156+
e, err := New(Config{Args: []string{"arg1", "it's"}})
157+
if err != nil {
158+
t.Fatalf("New() error: %v", err)
159+
}
145160
got := e.buildPowerShellInlineCommand("cmd")
146161
want := "cmd 'arg1' 'it''s'"
147162
if got != want {

0 commit comments

Comments
 (0)