diff --git a/.github/workflows/archie.lock.yml b/.github/workflows/archie.lock.yml index 3292ccf3621..1affa0fa12d 100644 --- a/.github/workflows/archie.lock.yml +++ b/.github/workflows/archie.lock.yml @@ -5243,7 +5243,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/brave.lock.yml b/.github/workflows/brave.lock.yml index 1ba073d8bc8..cc22ceb0e6b 100644 --- a/.github/workflows/brave.lock.yml +++ b/.github/workflows/brave.lock.yml @@ -5084,7 +5084,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 20bba9a5f28..417384a76e1 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -5226,7 +5226,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 6d3c91f7717..95e54b43df0 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -4923,8 +4923,82 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'failure' }} runs-on: ubuntu-slim outputs: - activated: ${{ steps.check_stop_time.outputs.stop_time_ok == 'true' }} + activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_stop_time.outputs.stop_time_ok == 'true') }} steps: + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: admin,maintainer,write + with: + script: | + async function main() { + const { eventName } = context; + const actor = context.actor; + const { owner, repo } = context.repo; + const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; + const requiredPermissions = requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : []; + if (eventName === "workflow_dispatch") { + const hasWriteRole = requiredPermissions.includes("write"); + if (hasWriteRole) { + core.info(`✅ Event ${eventName} does not require validation (write role allowed)`); + core.setOutput("is_team_member", "true"); + core.setOutput("result", "safe_event"); + return; + } + core.info(`Event ${eventName} requires validation (write role not allowed)`); + } + const safeEvents = ["schedule"]; + if (safeEvents.includes(eventName)) { + core.info(`✅ Event ${eventName} does not require validation`); + core.setOutput("is_team_member", "true"); + core.setOutput("result", "safe_event"); + return; + } + if (!requiredPermissions || requiredPermissions.length === 0) { + core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator."); + core.setOutput("is_team_member", "false"); + core.setOutput("result", "config_error"); + core.setOutput("error_message", "Configuration error: Required permissions not specified"); + return; + } + try { + core.info(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`); + core.info(`Required permissions: ${requiredPermissions.join(", ")}`); + const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: owner, + repo: repo, + username: actor, + }); + const permission = repoPermission.data.permission; + core.info(`Repository permission level: ${permission}`); + for (const requiredPerm of requiredPermissions) { + if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) { + core.info(`✅ User has ${permission} access to repository`); + core.setOutput("is_team_member", "true"); + core.setOutput("result", "authorized"); + core.setOutput("user_permission", permission); + return; + } + } + core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`); + core.setOutput("is_team_member", "false"); + core.setOutput("result", "insufficient_permissions"); + core.setOutput("user_permission", permission); + core.setOutput( + "error_message", + `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` + ); + } catch (repoError) { + const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); + core.warning(`Repository permission check failed: ${errorMessage}`); + core.setOutput("is_team_member", "false"); + core.setOutput("result", "api_error"); + core.setOutput("error_message", `Repository permission check failed: ${errorMessage}`); + return; + } + } + await main(); - name: Check stop-time limit id: check_stop_time uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 diff --git a/.github/workflows/craft.lock.yml b/.github/workflows/craft.lock.yml index ccbd2c04af3..1d2414aef72 100644 --- a/.github/workflows/craft.lock.yml +++ b/.github/workflows/craft.lock.yml @@ -5368,7 +5368,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index 91287c5694f..e6523497e90 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -13,6 +13,8 @@ # agent["agent"] # detection["detection"] # missing_tool["missing_tool"] +# pre_activation["pre_activation"] +# pre_activation --> activation # agent --> add_comment # detection --> add_comment # activation --> agent @@ -55,9 +57,10 @@ run-name: "Dev Hawk" jobs: activation: + needs: pre_activation if: > - (github.event.workflow_run.event == 'workflow_dispatch') && ((github.event_name != 'workflow_run') || - (github.event.workflow_run.repository.id == github.repository_id)) + ((needs.pre_activation.outputs.activated == 'true') && (github.event.workflow_run.event == 'workflow_dispatch')) && + ((github.event_name != 'workflow_run') || (github.event.workflow_run.repository.id == github.repository_id)) runs-on: ubuntu-slim permissions: contents: read @@ -4449,3 +4452,84 @@ jobs: core.setFailed(`Error processing missing-tool reports: ${error}`); }); + pre_activation: + if: ${{ github.event.workflow_run.event == 'workflow_dispatch' }} + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + steps: + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: admin,maintainer,write + with: + script: | + async function main() { + const { eventName } = context; + const actor = context.actor; + const { owner, repo } = context.repo; + const requiredPermissionsEnv = process.env.GH_AW_REQUIRED_ROLES; + const requiredPermissions = requiredPermissionsEnv ? requiredPermissionsEnv.split(",").filter(p => p.trim() !== "") : []; + if (eventName === "workflow_dispatch") { + const hasWriteRole = requiredPermissions.includes("write"); + if (hasWriteRole) { + core.info(`✅ Event ${eventName} does not require validation (write role allowed)`); + core.setOutput("is_team_member", "true"); + core.setOutput("result", "safe_event"); + return; + } + core.info(`Event ${eventName} requires validation (write role not allowed)`); + } + const safeEvents = ["schedule"]; + if (safeEvents.includes(eventName)) { + core.info(`✅ Event ${eventName} does not require validation`); + core.setOutput("is_team_member", "true"); + core.setOutput("result", "safe_event"); + return; + } + if (!requiredPermissions || requiredPermissions.length === 0) { + core.warning("❌ Configuration error: Required permissions not specified. Contact repository administrator."); + core.setOutput("is_team_member", "false"); + core.setOutput("result", "config_error"); + core.setOutput("error_message", "Configuration error: Required permissions not specified"); + return; + } + try { + core.info(`Checking if user '${actor}' has required permissions for ${owner}/${repo}`); + core.info(`Required permissions: ${requiredPermissions.join(", ")}`); + const repoPermission = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: owner, + repo: repo, + username: actor, + }); + const permission = repoPermission.data.permission; + core.info(`Repository permission level: ${permission}`); + for (const requiredPerm of requiredPermissions) { + if (permission === requiredPerm || (requiredPerm === "maintainer" && permission === "maintain")) { + core.info(`✅ User has ${permission} access to repository`); + core.setOutput("is_team_member", "true"); + core.setOutput("result", "authorized"); + core.setOutput("user_permission", permission); + return; + } + } + core.warning(`User permission '${permission}' does not meet requirements: ${requiredPermissions.join(", ")}`); + core.setOutput("is_team_member", "false"); + core.setOutput("result", "insufficient_permissions"); + core.setOutput("user_permission", permission); + core.setOutput( + "error_message", + `Access denied: User '${actor}' is not authorized. Required permissions: ${requiredPermissions.join(", ")}` + ); + } catch (repoError) { + const errorMessage = repoError instanceof Error ? repoError.message : String(repoError); + core.warning(`Repository permission check failed: ${errorMessage}`); + core.setOutput("is_team_member", "false"); + core.setOutput("result", "api_error"); + core.setOutput("error_message", `Repository permission check failed: ${errorMessage}`); + return; + } + } + await main(); + diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index e9bcfa96a5d..2be24877496 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -4177,7 +4177,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/grumpy-reviewer.lock.yml b/.github/workflows/grumpy-reviewer.lock.yml index d9f1f22d4b7..ccfc067f1de 100644 --- a/.github/workflows/grumpy-reviewer.lock.yml +++ b/.github/workflows/grumpy-reviewer.lock.yml @@ -5472,7 +5472,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/issue-classifier.lock.yml b/.github/workflows/issue-classifier.lock.yml index a8c456287a6..2da9735e51e 100644 --- a/.github/workflows/issue-classifier.lock.yml +++ b/.github/workflows/issue-classifier.lock.yml @@ -3583,7 +3583,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/mergefest.lock.yml b/.github/workflows/mergefest.lock.yml index c77894aeab7..6da371c8e2c 100644 --- a/.github/workflows/mergefest.lock.yml +++ b/.github/workflows/mergefest.lock.yml @@ -4793,7 +4793,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/pdf-summary.lock.yml b/.github/workflows/pdf-summary.lock.yml index e52143076d4..d88fe0b887c 100644 --- a/.github/workflows/pdf-summary.lock.yml +++ b/.github/workflows/pdf-summary.lock.yml @@ -5193,7 +5193,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index 72bf7be6ed3..61761264a4b 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -5021,7 +5021,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index 02084379813..698f5c8c019 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -6929,7 +6929,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index 8a54059c1dd..baf8a1a2e22 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -5856,7 +5856,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index cc9ff98fe76..13cd95dcc67 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -6302,7 +6302,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/scout.lock.yml b/.github/workflows/scout.lock.yml index 2c799b061af..4383e4c1801 100644 --- a/.github/workflows/scout.lock.yml +++ b/.github/workflows/scout.lock.yml @@ -5233,7 +5233,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index de720ce3b95..4c06821ea75 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -4148,7 +4148,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 4291fe6f136..4b4ada8c57d 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -3667,7 +3667,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index f2d620b902c..9bd56bb4544 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -4711,7 +4711,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml index e4dcd6a9f09..1b46c33228c 100644 --- a/.github/workflows/smoke-detector.lock.yml +++ b/.github/workflows/smoke-detector.lock.yml @@ -5218,7 +5218,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/test-claude-oauth-workflow.lock.yml b/.github/workflows/test-claude-oauth-workflow.lock.yml index d756d34bdba..11f2e58af68 100644 --- a/.github/workflows/test-claude-oauth-workflow.lock.yml +++ b/.github/workflows/test-claude-oauth-workflow.lock.yml @@ -1550,7 +1550,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/test-manual-approval.lock.yml b/.github/workflows/test-manual-approval.lock.yml index 89f6365403f..57756c383fb 100644 --- a/.github/workflows/test-manual-approval.lock.yml +++ b/.github/workflows/test-manual-approval.lock.yml @@ -1828,7 +1828,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/test-secret-masking.lock.yml b/.github/workflows/test-secret-masking.lock.yml index 8eb988f00b6..d7eeb0d3a16 100644 --- a/.github/workflows/test-secret-masking.lock.yml +++ b/.github/workflows/test-secret-masking.lock.yml @@ -1837,7 +1837,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index dc3a2491c31..9a74ba2b04b 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -5236,7 +5236,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 00249a4ec0d..ef5ae06b8a1 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -5724,7 +5724,7 @@ jobs: } core.info(`Event ${eventName} requires validation (write role not allowed)`); } - const safeEvents = ["workflow_run", "schedule"]; + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index fe6564473cf..4331b903d2c 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -74,7 +74,11 @@ const DefaultActivationJobRunnerImage = "ubuntu-slim" var DefaultAllowedDomains = []string{"localhost", "localhost:*", "127.0.0.1", "127.0.0.1:*"} // SafeWorkflowEvents defines events that are considered safe and don't require permission checks -var SafeWorkflowEvents = []string{"workflow_dispatch", "workflow_run", "schedule"} +// workflow_run is intentionally excluded because it has HIGH security risks: +// - Privilege escalation (inherits permissions from triggering workflow) +// - Branch protection bypass (can execute on protected branches via unprotected branches) +// - Secret exposure (secrets available even when triggered by untrusted code) +var SafeWorkflowEvents = []string{"workflow_dispatch", "schedule"} // AllowedExpressions contains the GitHub Actions expressions that can be used in workflow markdown content // see https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context diff --git a/pkg/constants/constants_test.go b/pkg/constants/constants_test.go index f7adc6c88da..9600bc71b5e 100644 --- a/pkg/constants/constants_test.go +++ b/pkg/constants/constants_test.go @@ -36,7 +36,8 @@ func TestSafeWorkflowEvents(t *testing.T) { t.Error("SafeWorkflowEvents should not be empty") } - expectedEvents := []string{"workflow_dispatch", "workflow_run", "schedule"} + // workflow_run is intentionally excluded due to HIGH security risks + expectedEvents := []string{"workflow_dispatch", "schedule"} if len(SafeWorkflowEvents) != len(expectedEvents) { t.Errorf("SafeWorkflowEvents length = %d, want %d", len(SafeWorkflowEvents), len(expectedEvents)) } diff --git a/pkg/workflow/js/check_membership.cjs b/pkg/workflow/js/check_membership.cjs index e48e80c4ec4..580cb645bfc 100644 --- a/pkg/workflow/js/check_membership.cjs +++ b/pkg/workflow/js/check_membership.cjs @@ -23,7 +23,11 @@ async function main() { } // skip check for other safe events - const safeEvents = ["workflow_run", "schedule"]; + // workflow_run is intentionally excluded due to HIGH security risks: + // - Privilege escalation (inherits permissions from triggering workflow) + // - Branch protection bypass (can execute on protected branches) + // - Secret exposure (secrets available from untrusted code) + const safeEvents = ["schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); core.setOutput("is_team_member", "true"); diff --git a/pkg/workflow/js/check_membership.test.cjs b/pkg/workflow/js/check_membership.test.cjs index ec3ae7b2260..a2effc46456 100644 --- a/pkg/workflow/js/check_membership.test.cjs +++ b/pkg/workflow/js/check_membership.test.cjs @@ -65,14 +65,10 @@ describe("check_membership.cjs", () => { }; describe("safe events", () => { - it("should skip check for workflow_run events", async () => { - mockContext.eventName = "workflow_run"; - await runScript(); - - expect(mockCore.info).toHaveBeenCalledWith("✅ Event workflow_run does not require validation"); - expect(mockCore.setOutput).toHaveBeenCalledWith("is_team_member", "true"); - expect(mockCore.setOutput).toHaveBeenCalledWith("result", "safe_event"); - }); + // workflow_run is no longer a safe event due to HIGH security risks: + // - Privilege escalation (inherits permissions from triggering workflow) + // - Branch protection bypass (can execute on protected branches) + // - Secret exposure (secrets available from untrusted code) it("should skip check for schedule events", async () => { mockContext.eventName = "schedule"; diff --git a/pkg/workflow/js/check_permissions.cjs b/pkg/workflow/js/check_permissions.cjs index c790794822b..12d841824a6 100644 --- a/pkg/workflow/js/check_permissions.cjs +++ b/pkg/workflow/js/check_permissions.cjs @@ -5,7 +5,11 @@ async function main() { const { eventName } = context; // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; + // workflow_run is intentionally excluded due to HIGH security risks: + // - Privilege escalation (inherits permissions from triggering workflow) + // - Branch protection bypass (can execute on protected branches) + // - Secret exposure (secrets available from untrusted code) + const safeEvents = ["workflow_dispatch", "schedule"]; if (safeEvents.includes(eventName)) { core.info(`✅ Event ${eventName} does not require validation`); return; diff --git a/pkg/workflow/js/check_permissions.test.cjs b/pkg/workflow/js/check_permissions.test.cjs index a090703ba6a..4406bebf7e9 100644 --- a/pkg/workflow/js/check_permissions.test.cjs +++ b/pkg/workflow/js/check_permissions.test.cjs @@ -303,16 +303,10 @@ describe("check_permissions.cjs", () => { expect(mockCore.warning).not.toHaveBeenCalled(); }); - it("should skip validation for workflow_run events", async () => { - process.env.GH_AW_REQUIRED_ROLES = "admin"; - global.context.eventName = "workflow_run"; - - // Execute the script - await eval(`(async () => { ${checkPermissionsScript} })()`); - - expect(mockCore.info).toHaveBeenCalledWith("✅ Event workflow_run does not require validation"); - expect(mockGithub.rest.repos.getCollaboratorPermissionLevel).not.toHaveBeenCalled(); - }); + // workflow_run is no longer a safe event due to HIGH security risks: + // - Privilege escalation (inherits permissions from triggering workflow) + // - Branch protection bypass (can execute on protected branches) + // - Secret exposure (secrets available from untrusted code) it("should skip validation for schedule events", async () => { process.env.GH_AW_REQUIRED_ROLES = "admin"; diff --git a/pkg/workflow/js_comments_test.go b/pkg/workflow/js_comments_test.go index c56c3995eca..a0db2734d02 100644 --- a/pkg/workflow/js_comments_test.go +++ b/pkg/workflow/js_comments_test.go @@ -690,7 +690,7 @@ async function main() { const { eventName } = context; // skip check for safe events - const safeEvents = ["workflow_dispatch", "workflow_run", "schedule"]; + const safeEvents = ["workflow_dispatch", "schedule"]; if (safeEvents.includes(eventName)) { core.info('Event does not require validation'); return; diff --git a/pkg/workflow/permission_restriction_test.go b/pkg/workflow/permission_restriction_test.go index f38c5f0d23c..f3c828440f3 100644 --- a/pkg/workflow/permission_restriction_test.go +++ b/pkg/workflow/permission_restriction_test.go @@ -147,7 +147,7 @@ Test workflow content.`, expectedPermissions: []string{"admin", "maintainer", "write"}, }, { - name: "workflow with workflow_run only should NOT include permission check (safe event)", + name: "workflow with workflow_run should INCLUDE permission check (unsafe event)", frontmatter: `--- on: workflow_run: @@ -161,7 +161,7 @@ tools: # Workflow Run Trigger Test workflow content.`, filename: "workflow-run-workflow.md", - expectPermissionCheck: false, + expectPermissionCheck: true, expectedPermissions: []string{"admin", "maintainer", "write"}, }, {