From 94a2882e5113c93f3b363571d850ac8cef50e663 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:09:42 +0000 Subject: [PATCH 1/3] Initial plan From 521652753dd699536b71576296dd39c8bcfbc331 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Fri, 28 Nov 2025 00:29:02 +0000 Subject: [PATCH 2/3] Add support for custom AWF installation path in firewall configuration Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> # Conflicts: # docs/src/content/docs/reference/frontmatter-full.md # docs/src/content/docs/reference/network.md # pkg/workflow/copilot_engine.go --- .../docs/reference/frontmatter-full.md | 5 + docs/src/content/docs/reference/network.md | 33 +++ pkg/parser/schemas/main_workflow_schema.json | 4 + pkg/workflow/copilot_engine.go | 75 +++++- pkg/workflow/firewall.go | 1 + .../firewall_custom_path_integration_test.go | 245 ++++++++++++++++++ pkg/workflow/firewall_custom_path_test.go | 210 +++++++++++++++ pkg/workflow/frontmatter_extraction.go | 7 + 8 files changed, 570 insertions(+), 10 deletions(-) create mode 100644 pkg/workflow/firewall_custom_path_integration_test.go create mode 100644 pkg/workflow/firewall_custom_path_test.go diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 725ae1c861..f8077e32cd 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -885,6 +885,11 @@ network: # (optional) log-level: "debug" + # Custom path to AWF binary. When specified, skips downloading AWF from GitHub + # releases. Supports absolute paths or paths relative to GITHUB_WORKSPACE. + # (optional) + path: "example-value" + # Sandbox runtime configuration for AI engines. Controls the execution sandbox # (AWF or Sandbox Runtime). Only supported for Copilot engine. # (optional) diff --git a/docs/src/content/docs/reference/network.md b/docs/src/content/docs/reference/network.md index c0534aabfb..815afd81b0 100644 --- a/docs/src/content/docs/reference/network.md +++ b/docs/src/content/docs/reference/network.md @@ -141,6 +141,39 @@ When the firewall is disabled with specific `allowed` domains: This configuration is useful during development or when the firewall is incompatible with your workflow requirements. For production workflows, enabling the firewall is recommended for better network security. +### Custom AWF Binary Path + +Specify a custom AWF binary instead of downloading from GitHub releases: + +```yaml wrap +network: + firewall: + path: /usr/local/bin/awf-custom # Absolute path + allowed: + - defaults +``` + +Relative paths are resolved relative to the repository root: + +```yaml wrap +network: + firewall: + path: bin/awf # Resolves to ${GITHUB_WORKSPACE}/bin/awf + allowed: + - defaults +``` + +When `path` is specified: +- AWF is not downloaded from GitHub releases +- The specified binary must exist and be executable +- Path is validated before workflow execution +- The `version` field is ignored (if also specified) + +**Use cases:** +- Pre-installed AWF on self-hosted runners +- Custom AWF builds with patches or modifications +- Repository-specific AWF versions + ## Best Practices Follow the principle of least privilege by only allowing access to domains and ecosystems actually needed. Prefer ecosystem identifiers over listing individual domains. When adding custom domains, use the base domain (e.g., `trusted.com`) which automatically includes all subdomains—do not use wildcard syntax like `*.trusted.com`. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a3dccfbb7a..1c6963827b 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -1684,6 +1684,10 @@ "type": "string", "description": "AWF log level (default: info). Valid values: debug, info, warn, error", "enum": ["debug", "info", "warn", "error"] + }, + "path": { + "type": "string", + "description": "Custom path to AWF binary. When specified, skips downloading AWF from GitHub releases. Supports absolute paths or paths relative to GITHUB_WORKSPACE." } }, "additionalProperties": false diff --git a/pkg/workflow/copilot_engine.go b/pkg/workflow/copilot_engine.go index 78a3ccadc8..a56a1acacf 100644 --- a/pkg/workflow/copilot_engine.go +++ b/pkg/workflow/copilot_engine.go @@ -108,16 +108,23 @@ func (e *CopilotEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHu srtInstall := generateSRTInstallationStep() steps = append(steps, srtInstall) } else if isFirewallEnabled(workflowData) { - // Install AWF after Node.js setup but before Copilot CLI installation + // Add AWF installation or validation steps only if firewall is enabled firewallConfig := getFirewallConfig(workflowData) - var awfVersion string - if firewallConfig != nil { - awfVersion = firewallConfig.Version - } - // Install AWF binary - awfInstall := generateAWFInstallationStep(awfVersion) - steps = append(steps, awfInstall) + if firewallConfig == nil || firewallConfig.Path == "" { + // Default: Download and install AWF from GitHub releases + var awfVersion string + if firewallConfig != nil { + awfVersion = firewallConfig.Version + } + + awfInstall := generateAWFInstallationStep(awfVersion) + steps = append(steps, awfInstall) + } else { + // Custom path: Validate the binary exists and is executable + validationStep := generateAWFPathValidationStep(firewallConfig.Path) + steps = append(steps, validationStep) + } } // Add Copilot CLI installation step after sandbox installation @@ -321,11 +328,14 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st awfArgs = append(awfArgs, firewallConfig.Args...) } + // Get AWF binary path (custom or default) + awfBinary := getAWFBinaryPath(firewallConfig) + // Build the full AWF command with proper argument separation // AWF v0.2.0 uses -- to separate AWF args from the actual command // The command arguments should be passed as individual shell arguments, not as a single string command = fmt.Sprintf(`set -o pipefail -sudo -E awf %s \ +sudo -E %s %s \ -- %s \ 2>&1 | tee %s @@ -337,7 +347,7 @@ if [ -n "$AGENT_LOGS_DIR" ] && [ -d "$AGENT_LOGS_DIR" ]; then sudo mkdir -p %s sudo mv "$AGENT_LOGS_DIR"/* %s || true sudo rmdir "$AGENT_LOGS_DIR" || true -fi`, shellJoinArgs(awfArgs), copilotCommand, shellEscapeArg(logFile), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder)) +fi`, shellEscapeArg(awfBinary), shellJoinArgs(awfArgs), copilotCommand, shellEscapeArg(logFile), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder)) } else { // Run copilot command without AWF wrapper command = fmt.Sprintf(`set -o pipefail @@ -940,6 +950,50 @@ func generateAWFInstallationStep(version string) GitHubActionStep { return GitHubActionStep(stepLines) } +// resolveAWFPath handles path resolution for absolute and relative paths +func resolveAWFPath(customPath string) string { + if customPath == "" { + return "/usr/local/bin/awf" + } + + if strings.HasPrefix(customPath, "/") { + return customPath // Absolute path + } + + // Relative path - resolve against GITHUB_WORKSPACE + return fmt.Sprintf("${GITHUB_WORKSPACE}/%s", customPath) +} + +// getAWFBinaryPath returns appropriate AWF binary path for execution +func getAWFBinaryPath(firewallConfig *FirewallConfig) string { + if firewallConfig != nil && firewallConfig.Path != "" { + return resolveAWFPath(firewallConfig.Path) + } + return "awf" // Default (in PATH from installation step) +} + +// generateAWFPathValidationStep creates a validation step to verify custom AWF binary +func generateAWFPathValidationStep(customPath string) GitHubActionStep { + resolvedPath := resolveAWFPath(customPath) + + stepLines := []string{ + " - name: Validate custom AWF binary", + " run: |", + fmt.Sprintf(" echo \"Validating custom AWF binary at: %s\"", resolvedPath), + fmt.Sprintf(" if [ ! -f %s ]; then", shellEscapeArg(resolvedPath)), + fmt.Sprintf(" echo \"Error: AWF binary not found at %s\"", resolvedPath), + " exit 1", + " fi", + fmt.Sprintf(" if [ ! -x %s ]; then", shellEscapeArg(resolvedPath)), + fmt.Sprintf(" echo \"Error: AWF binary at %s is not executable\"", resolvedPath), + " exit 1", + " fi", + fmt.Sprintf(" %s --version", shellEscapeArg(resolvedPath)), + } + + return GitHubActionStep(stepLines) +} + // generateSRTSystemDepsStep creates a GitHub Actions step to install SRT system dependencies func generateSRTSystemDepsStep() GitHubActionStep { stepLines := []string{ @@ -1112,6 +1166,7 @@ fi`, escapedConfigJSON, escapedCopilotCommand, shellEscapeArg(logFile), shellEsc return script } + // generateSquidLogsCollectionStep creates a GitHub Actions step to collect Squid logs from AWF func generateSquidLogsCollectionStep(workflowName string) GitHubActionStep { sanitizedName := strings.ToLower(SanitizeWorkflowName(workflowName)) diff --git a/pkg/workflow/firewall.go b/pkg/workflow/firewall.go index 0938d0664c..fac02f5cc4 100644 --- a/pkg/workflow/firewall.go +++ b/pkg/workflow/firewall.go @@ -13,6 +13,7 @@ type FirewallConfig struct { Args []string `yaml:"args,omitempty"` // Additional arguments to pass to AWF LogLevel string `yaml:"log_level,omitempty"` // AWF log level (default: "info") CleanupScript string `yaml:"cleanup_script,omitempty"` // Cleanup script path (default: "./scripts/ci/cleanup.sh") + Path string `yaml:"path,omitempty"` // Custom AWF binary path (skips download when specified) } // isFirewallEnabled checks if AWF firewall is enabled for the workflow diff --git a/pkg/workflow/firewall_custom_path_integration_test.go b/pkg/workflow/firewall_custom_path_integration_test.go new file mode 100644 index 0000000000..b831c96828 --- /dev/null +++ b/pkg/workflow/firewall_custom_path_integration_test.go @@ -0,0 +1,245 @@ +package workflow + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestFirewallCustomPathCompilation(t *testing.T) { + tests := []struct { + name string + markdown string + wantContains []string + dontWantContain []string + }{ + { + name: "custom absolute path skips installation", + markdown: `--- +on: push +permissions: + contents: read +engine: copilot +network: + firewall: + path: /usr/local/bin/awf-custom + allowed: + - defaults +--- + +# Test workflow with custom absolute AWF path +`, + wantContains: []string{ + "Validate custom AWF binary", + "/usr/local/bin/awf-custom", + "--version", + "if [ ! -f", + "if [ ! -x", + }, + dontWantContain: []string{ + "Install awf binary", + "curl -L https://github.com/githubnext/gh-aw-firewall", + }, + }, + { + name: "custom relative path resolved to workspace", + markdown: `--- +on: push +permissions: + contents: read +engine: copilot +network: + firewall: + path: bin/awf + allowed: + - defaults +--- + +# Test workflow with relative AWF path +`, + wantContains: []string{ + "Validate custom AWF binary", + "${GITHUB_WORKSPACE}/bin/awf", + }, + dontWantContain: []string{ + "Install awf binary", + }, + }, + { + name: "no path triggers default installation", + markdown: `--- +on: push +permissions: + contents: read +engine: copilot +network: + firewall: true + allowed: + - defaults +--- + +# Test workflow with default AWF installation +`, + wantContains: []string{ + "Install awf binary", + "curl -L https://github.com/githubnext/gh-aw-firewall", + }, + dontWantContain: []string{ + "Validate custom AWF binary", + }, + }, + { + name: "path with version ignores version", + markdown: `--- +on: push +permissions: + contents: read +engine: copilot +network: + firewall: + path: /custom/awf + version: v999.0.0 + allowed: + - defaults +--- + +# Test workflow where path takes precedence over version +`, + wantContains: []string{ + "Validate custom AWF binary", + "/custom/awf", + }, + dontWantContain: []string{ + "Install awf binary", + "v999.0.0", + }, + }, + { + name: "custom path used in execution command", + markdown: `--- +on: push +permissions: + contents: read +engine: copilot +network: + firewall: + path: /opt/awf + allowed: + - defaults +--- + +# Test that custom path is used in execution +`, + wantContains: []string{ + "sudo -E /opt/awf", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temp directory for test + tmpDir, err := os.MkdirTemp("", "awf-path-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Write the test workflow file + workflowFile := filepath.Join(tmpDir, "test-workflow.md") + if err := os.WriteFile(workflowFile, []byte(tt.markdown), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowFile); err != nil { + t.Fatalf("CompileWorkflow() error: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(workflowFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + result := string(lockContent) + + for _, want := range tt.wantContains { + if !strings.Contains(result, want) { + t.Errorf("Compiled workflow missing expected content: %q\n\nGot:\n%s", want, result) + } + } + + for _, dontWant := range tt.dontWantContain { + if strings.Contains(result, dontWant) { + t.Errorf("Compiled workflow contains unexpected content: %q", dontWant) + } + } + }) + } +} + +func TestFirewallCustomPathBackwardCompatibility(t *testing.T) { + // Ensure existing workflows without path field continue to work + markdown := `--- +on: push +permissions: + contents: read +engine: copilot +network: + firewall: + version: v1.2.3 + log-level: debug + allowed: + - defaults +--- + +# Existing workflow without custom path +` + + // Create temp directory for test + tmpDir, err := os.MkdirTemp("", "awf-compat-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + // Write the test workflow file + workflowFile := filepath.Join(tmpDir, "backward-compat.md") + if err := os.WriteFile(workflowFile, []byte(markdown), 0644); err != nil { + t.Fatal(err) + } + + // Compile the workflow + compiler := NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowFile); err != nil { + t.Fatalf("CompileWorkflow() error: %v", err) + } + + // Read the generated lock file + lockFile := strings.Replace(workflowFile, ".md", ".lock.yml", 1) + lockContent, err := os.ReadFile(lockFile) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + result := string(lockContent) + + // Should use default installation + if !strings.Contains(result, "Install awf binary") { + t.Error("Expected default AWF installation for workflow without path field") + } + + // Should include version + if !strings.Contains(result, "v1.2.3") { + t.Error("Expected specified version in installation step") + } + + // Should not have validation step + if strings.Contains(result, "Validate custom AWF binary") { + t.Error("Should not have validation step for workflow without custom path") + } +} diff --git a/pkg/workflow/firewall_custom_path_test.go b/pkg/workflow/firewall_custom_path_test.go new file mode 100644 index 0000000000..5c647ce185 --- /dev/null +++ b/pkg/workflow/firewall_custom_path_test.go @@ -0,0 +1,210 @@ +package workflow + +import ( + "strings" + "testing" +) + +func TestResolveAWFPath(t *testing.T) { + tests := []struct { + name string + customPath string + expectedPath string + }{ + { + name: "empty path returns default", + customPath: "", + expectedPath: "/usr/local/bin/awf", + }, + { + name: "absolute path returned as-is", + customPath: "/usr/local/bin/awf-custom", + expectedPath: "/usr/local/bin/awf-custom", + }, + { + name: "absolute path with nested directory", + customPath: "/opt/tools/bin/awf", + expectedPath: "/opt/tools/bin/awf", + }, + { + name: "relative path resolved to GITHUB_WORKSPACE", + customPath: "bin/awf", + expectedPath: "${GITHUB_WORKSPACE}/bin/awf", + }, + { + name: "relative path without subdirectory", + customPath: "awf", + expectedPath: "${GITHUB_WORKSPACE}/awf", + }, + { + name: "relative path with nested directories", + customPath: "tools/bin/awf", + expectedPath: "${GITHUB_WORKSPACE}/tools/bin/awf", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := resolveAWFPath(tt.customPath) + if result != tt.expectedPath { + t.Errorf("resolveAWFPath(%q) = %q, want %q", tt.customPath, result, tt.expectedPath) + } + }) + } +} + +func TestGetAWFBinaryPath(t *testing.T) { + tests := []struct { + name string + firewallConfig *FirewallConfig + expectedPath string + }{ + { + name: "nil config returns default", + firewallConfig: nil, + expectedPath: "awf", + }, + { + name: "config without path returns default", + firewallConfig: &FirewallConfig{Enabled: true}, + expectedPath: "awf", + }, + { + name: "config with empty path returns default", + firewallConfig: &FirewallConfig{Enabled: true, Path: ""}, + expectedPath: "awf", + }, + { + name: "config with absolute path", + firewallConfig: &FirewallConfig{Enabled: true, Path: "/custom/path/awf"}, + expectedPath: "/custom/path/awf", + }, + { + name: "config with relative path", + firewallConfig: &FirewallConfig{Enabled: true, Path: "bin/awf"}, + expectedPath: "${GITHUB_WORKSPACE}/bin/awf", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getAWFBinaryPath(tt.firewallConfig) + if result != tt.expectedPath { + t.Errorf("getAWFBinaryPath() = %q, want %q", result, tt.expectedPath) + } + }) + } +} + +func TestGenerateAWFPathValidationStep(t *testing.T) { + tests := []struct { + name string + customPath string + expectedChecks []string + }{ + { + name: "absolute path validation", + customPath: "/usr/local/bin/awf-custom", + expectedChecks: []string{ + "Validate custom AWF binary", + "/usr/local/bin/awf-custom", + "if [ ! -f", + "if [ ! -x", + "--version", + }, + }, + { + name: "relative path validation", + customPath: "bin/awf", + expectedChecks: []string{ + "Validate custom AWF binary", + "${GITHUB_WORKSPACE}/bin/awf", + "if [ ! -f", + "if [ ! -x", + "--version", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + step := generateAWFPathValidationStep(tt.customPath) + stepContent := strings.Join(step, "\n") + + for _, check := range tt.expectedChecks { + if !strings.Contains(stepContent, check) { + t.Errorf("generateAWFPathValidationStep(%q) missing expected content: %q\nGot:\n%s", tt.customPath, check, stepContent) + } + } + }) + } +} + +func TestFirewallConfigPathExtraction(t *testing.T) { + compiler := &Compiler{} + + tests := []struct { + name string + firewall any + expectedPath string + }{ + { + name: "path from object config", + firewall: map[string]any{ + "path": "/custom/awf", + }, + expectedPath: "/custom/awf", + }, + { + name: "path with other config options", + firewall: map[string]any{ + "path": "bin/awf", + "log-level": "debug", + }, + expectedPath: "bin/awf", + }, + { + name: "no path in config", + firewall: map[string]any{ + "version": "v1.0.0", + }, + expectedPath: "", + }, + { + name: "boolean config has no path", + firewall: true, + expectedPath: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := compiler.extractFirewallConfig(tt.firewall) + if config == nil { + if tt.expectedPath != "" { + t.Errorf("extractFirewallConfig() returned nil, expected path %q", tt.expectedPath) + } + return + } + + if config.Path != tt.expectedPath { + t.Errorf("extractFirewallConfig().Path = %q, want %q", config.Path, tt.expectedPath) + } + }) + } +} + +func TestVersionIgnoredWhenPathSet(t *testing.T) { + // When both path and version are specified, path takes precedence + // and version is effectively ignored (no download happens) + config := &FirewallConfig{ + Enabled: true, + Path: "/custom/awf", + Version: "v1.0.0", // Should be ignored + } + + path := getAWFBinaryPath(config) + if path != "/custom/awf" { + t.Errorf("getAWFBinaryPath() should use custom path, got %q", path) + } +} diff --git a/pkg/workflow/frontmatter_extraction.go b/pkg/workflow/frontmatter_extraction.go index 9c2b3c533a..9d8b56ba0b 100644 --- a/pkg/workflow/frontmatter_extraction.go +++ b/pkg/workflow/frontmatter_extraction.go @@ -689,6 +689,13 @@ func (c *Compiler) extractFirewallConfig(firewall any) *FirewallConfig { } } + // Extract path if present + if path, hasPath := firewallObj["path"]; hasPath { + if pathStr, ok := path.(string); ok { + config.Path = pathStr + } + } + return config } From 680093a3f25734843f269b8012b59b611e214230 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:37:55 +0000 Subject: [PATCH 3/3] Address code review: improve security of path handling Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com> --- pkg/workflow/copilot_engine.go | 25 +++++++++++++++-------- pkg/workflow/firewall_custom_path_test.go | 10 +++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/pkg/workflow/copilot_engine.go b/pkg/workflow/copilot_engine.go index a56a1acacf..98642e292b 100644 --- a/pkg/workflow/copilot_engine.go +++ b/pkg/workflow/copilot_engine.go @@ -2,6 +2,7 @@ package workflow import ( "fmt" + "path/filepath" "sort" "strings" @@ -951,17 +952,22 @@ func generateAWFInstallationStep(version string) GitHubActionStep { } // resolveAWFPath handles path resolution for absolute and relative paths +// It returns the path to use for the AWF binary, resolving relative paths against GITHUB_WORKSPACE func resolveAWFPath(customPath string) string { if customPath == "" { return "/usr/local/bin/awf" } - if strings.HasPrefix(customPath, "/") { - return customPath // Absolute path + // Clean the path to normalize separators and remove redundant elements + // Note: filepath.Clean is used for normalization only, the path is not resolved against the filesystem + cleanPath := filepath.Clean(customPath) + + if strings.HasPrefix(cleanPath, "/") { + return cleanPath // Absolute path } // Relative path - resolve against GITHUB_WORKSPACE - return fmt.Sprintf("${GITHUB_WORKSPACE}/%s", customPath) + return fmt.Sprintf("${GITHUB_WORKSPACE}/%s", cleanPath) } // getAWFBinaryPath returns appropriate AWF binary path for execution @@ -975,20 +981,21 @@ func getAWFBinaryPath(firewallConfig *FirewallConfig) string { // generateAWFPathValidationStep creates a validation step to verify custom AWF binary func generateAWFPathValidationStep(customPath string) GitHubActionStep { resolvedPath := resolveAWFPath(customPath) + escapedPath := shellEscapeArg(resolvedPath) stepLines := []string{ " - name: Validate custom AWF binary", " run: |", - fmt.Sprintf(" echo \"Validating custom AWF binary at: %s\"", resolvedPath), - fmt.Sprintf(" if [ ! -f %s ]; then", shellEscapeArg(resolvedPath)), - fmt.Sprintf(" echo \"Error: AWF binary not found at %s\"", resolvedPath), + fmt.Sprintf(" echo \"Validating custom AWF binary at: %s\"", escapedPath), + fmt.Sprintf(" if [ ! -f %s ]; then", escapedPath), + fmt.Sprintf(" echo \"Error: AWF binary not found at %s\"", escapedPath), " exit 1", " fi", - fmt.Sprintf(" if [ ! -x %s ]; then", shellEscapeArg(resolvedPath)), - fmt.Sprintf(" echo \"Error: AWF binary at %s is not executable\"", resolvedPath), + fmt.Sprintf(" if [ ! -x %s ]; then", escapedPath), + fmt.Sprintf(" echo \"Error: AWF binary at %s is not executable\"", escapedPath), " exit 1", " fi", - fmt.Sprintf(" %s --version", shellEscapeArg(resolvedPath)), + fmt.Sprintf(" %s --version", escapedPath), } return GitHubActionStep(stepLines) diff --git a/pkg/workflow/firewall_custom_path_test.go b/pkg/workflow/firewall_custom_path_test.go index 5c647ce185..94fde62cfa 100644 --- a/pkg/workflow/firewall_custom_path_test.go +++ b/pkg/workflow/firewall_custom_path_test.go @@ -41,6 +41,16 @@ func TestResolveAWFPath(t *testing.T) { customPath: "tools/bin/awf", expectedPath: "${GITHUB_WORKSPACE}/tools/bin/awf", }, + { + name: "path with redundant separators cleaned", + customPath: "bin//awf", + expectedPath: "${GITHUB_WORKSPACE}/bin/awf", + }, + { + name: "path with dot components cleaned", + customPath: "./bin/awf", + expectedPath: "${GITHUB_WORKSPACE}/bin/awf", + }, } for _, tt := range tests {