diff --git a/actions/setup/js/validate_lockdown_requirements.cjs b/actions/setup/js/validate_lockdown_requirements.cjs index 14f74a99b30..d69d45f0263 100644 --- a/actions/setup/js/validate_lockdown_requirements.cjs +++ b/actions/setup/js/validate_lockdown_requirements.cjs @@ -1,5 +1,7 @@ // @ts-check +const { renderLockdownTokenErrorMessage, renderPublicStrictModeErrorMessage, renderPullRequestTargetErrorMessage } = require("./validate_lockdown_requirements_templates.cjs"); + /** * Validates that lockdown mode requirements are met at runtime. * @@ -22,8 +24,17 @@ * @param {any} core - GitHub Actions core library * @returns {void} */ -const { ERR_VALIDATION } = require("./error_codes.cjs"); function validateLockdownRequirements(core) { + /** + * @param {string} message + * @returns {never} + */ + function failWithError(message) { + core.setOutput("lockdown_check_failed", "true"); + core.setFailed(message); + throw new Error(message); + } + // Check if lockdown mode is explicitly enabled (set to "true" in frontmatter) const lockdownEnabled = process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT === "true"; @@ -46,22 +57,7 @@ function validateLockdownRequirements(core) { core.info(`Custom github-token configured: ${hasCustomToken}`); if (!hasAnyCustomToken) { - const errorMessage = - "Lockdown mode is enabled (lockdown: true) but no custom GitHub token is configured.\\n" + - "\\n" + - "Please configure one of the following as a repository secret:\\n" + - " - GH_AW_GITHUB_TOKEN (recommended)\\n" + - " - GH_AW_GITHUB_MCP_SERVER_TOKEN (alternative)\\n" + - " - Custom github-token in your workflow frontmatter\\n" + - "\\n" + - "See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/auth.mdx\\n" + - "\\n" + - "To set a token:\\n" + - ' gh aw secrets set GH_AW_GITHUB_TOKEN --value "YOUR_FINE_GRAINED_PAT"'; - - core.setOutput("lockdown_check_failed", "true"); - core.setFailed(errorMessage); - throw new Error(errorMessage); + failWithError(renderLockdownTokenErrorMessage()); } core.info("✓ Lockdown mode requirements validated: Custom GitHub token is configured"); @@ -77,20 +73,7 @@ function validateLockdownRequirements(core) { core.info(`Compiled with strict mode: ${isStrict}`); if (isPublic && !isStrict) { - const errorMessage = - "This workflow is running on a public repository but was not compiled with strict mode.\\n" + - "\\n" + - "Public repository workflows must be compiled with strict mode enabled to meet\\n" + - "the security requirements for public exposure.\\n" + - "\\n" + - "To fix this, recompile the workflow with strict mode:\\n" + - " gh aw compile --strict\\n" + - "\\n" + - "See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx"; - - core.setOutput("lockdown_check_failed", "true"); - core.setFailed(errorMessage); - throw new Error(errorMessage); + failWithError(renderPublicStrictModeErrorMessage()); } if (isPublic && isStrict) { @@ -104,20 +87,7 @@ function validateLockdownRequirements(core) { // and potentially exfiltrate secrets or cause unintended side effects. const eventName = process.env.GITHUB_EVENT_NAME; if (isPublic && eventName === "pull_request_target") { - const errorMessage = - "This workflow is triggered by the pull_request_target event on a public repository.\\n" + - "\\n" + - "The pull_request_target event is not allowed on public repositories because it runs\\n" + - "workflows with access to repository secrets even when triggered from a fork, which\\n" + - 'creates a significant security risk (known as a "pwn request").\\n' + - "\\n" + - "To fix this, use the pull_request event instead, or migrate to a private repository.\\n" + - "\\n" + - "See: https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx"; - - core.setOutput("lockdown_check_failed", "true"); - core.setFailed(errorMessage); - throw new Error(errorMessage); + failWithError(renderPullRequestTargetErrorMessage()); } } diff --git a/actions/setup/js/validate_lockdown_requirements.test.cjs b/actions/setup/js/validate_lockdown_requirements.test.cjs index e6e2a323ad1..73522e37cc0 100644 --- a/actions/setup/js/validate_lockdown_requirements.test.cjs +++ b/actions/setup/js/validate_lockdown_requirements.test.cjs @@ -304,4 +304,80 @@ describe("validate_lockdown_requirements", () => { expect(mockCore.setFailed).toHaveBeenCalledWith(expect.stringContaining("not compiled with strict mode")); }); }); + + describe("setOutput side effects", () => { + it("should not set lockdown_check_failed output on a fully successful run", () => { + // All conditions pass: no lockdown, private repo, no special event + process.env.GITHUB_REPOSITORY_VISIBILITY = "private"; + process.env.GITHUB_EVENT_NAME = "push"; + + validateLockdownRequirements(mockCore); + + expect(mockCore.setOutput).not.toHaveBeenCalled(); + expect(mockCore.setFailed).not.toHaveBeenCalled(); + }); + + it("should not set lockdown_check_failed when lockdown is enabled with token and repo is private", () => { + process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT = "true"; + process.env.GH_AW_GITHUB_TOKEN = "ghp_test"; + process.env.GITHUB_REPOSITORY_VISIBILITY = "private"; + + validateLockdownRequirements(mockCore); + + expect(mockCore.setOutput).not.toHaveBeenCalled(); + }); + }); + + describe("multiple tokens", () => { + it("should pass when all three tokens are configured simultaneously", () => { + process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT = "true"; + process.env.GH_AW_GITHUB_TOKEN = "ghp_token1"; + process.env.GH_AW_GITHUB_MCP_SERVER_TOKEN = "ghp_token2"; + process.env.CUSTOM_GITHUB_TOKEN = "ghp_token3"; + + validateLockdownRequirements(mockCore); + + expect(mockCore.setFailed).not.toHaveBeenCalled(); + expect(mockCore.info).toHaveBeenCalledWith("GH_AW_GITHUB_TOKEN configured: true"); + expect(mockCore.info).toHaveBeenCalledWith("GH_AW_GITHUB_MCP_SERVER_TOKEN configured: true"); + expect(mockCore.info).toHaveBeenCalledWith("Custom github-token configured: true"); + expect(mockCore.info).toHaveBeenCalledWith("✓ Lockdown mode requirements validated: Custom GitHub token is configured"); + }); + }); + + describe("error messages", () => { + it("should include token guidance in lockdown error message", () => { + process.env.GITHUB_MCP_LOCKDOWN_EXPLICIT = "true"; + + expect(() => { + validateLockdownRequirements(mockCore); + }).toThrow("no custom GitHub token is configured"); + + const errorMsg = mockCore.setFailed.mock.calls[0][0]; + expect(errorMsg).toContain("GH_AW_GITHUB_TOKEN (recommended)"); + expect(errorMsg).toContain("GH_AW_GITHUB_MCP_SERVER_TOKEN (alternative)"); + }); + + it("should include compile guidance in strict mode error message", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "public"; + process.env.GH_AW_COMPILED_STRICT = "false"; + + expect(() => { + validateLockdownRequirements(mockCore); + }).toThrow("not compiled with strict mode"); + + const errorMsg = mockCore.setFailed.mock.calls[0][0]; + expect(errorMsg).toContain("gh aw compile --strict"); + }); + + it("should include pwn request warning in pull_request_target error message", () => { + process.env.GITHUB_REPOSITORY_VISIBILITY = "public"; + process.env.GH_AW_COMPILED_STRICT = "true"; + process.env.GITHUB_EVENT_NAME = "pull_request_target"; + + expect(() => { + validateLockdownRequirements(mockCore); + }).toThrow("pwn request"); + }); + }); }); diff --git a/actions/setup/js/validate_lockdown_requirements_templates.cjs b/actions/setup/js/validate_lockdown_requirements_templates.cjs new file mode 100644 index 00000000000..ac564c22cec --- /dev/null +++ b/actions/setup/js/validate_lockdown_requirements_templates.cjs @@ -0,0 +1,59 @@ +// @ts-check + +const { renderTemplate } = require("./messages_core.cjs"); + +const LOCKDOWN_TOKEN_ERROR_TEMPLATE = `Lockdown mode is enabled (lockdown: true) but no custom GitHub token is configured. + +Please configure one of the following as a repository secret: + - GH_AW_GITHUB_TOKEN (recommended) + - GH_AW_GITHUB_MCP_SERVER_TOKEN (alternative) + - Custom github-token in your workflow frontmatter + +See: {auth_docs_url} + +To set a token: + gh aw secrets set GH_AW_GITHUB_TOKEN --value "YOUR_FINE_GRAINED_PAT"`; + +const PUBLIC_STRICT_MODE_ERROR_TEMPLATE = `This workflow is running on a public repository but was not compiled with strict mode. + +Public repository workflows must be compiled with strict mode enabled to meet +the security requirements for public exposure. + +To fix this, recompile the workflow with strict mode: + {strict_compile_command} + +See: {security_docs_url}`; + +const PULL_REQUEST_TARGET_ERROR_TEMPLATE = `This workflow is triggered by the pull_request_target event on a public repository. + +The pull_request_target event is not allowed on public repositories because it runs +workflows with access to repository secrets even when triggered from a fork, which +creates a significant security risk (known as a "pwn request"). + +To fix this, use the pull_request event instead, or migrate to a private repository. + +See: {security_docs_url}`; + +const TEMPLATE_CONTEXT = { + auth_docs_url: "https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/auth.mdx", + security_docs_url: "https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/security.mdx", + strict_compile_command: "gh aw compile --strict", +}; + +function renderLockdownTokenErrorMessage() { + return renderTemplate(LOCKDOWN_TOKEN_ERROR_TEMPLATE, TEMPLATE_CONTEXT); +} + +function renderPublicStrictModeErrorMessage() { + return renderTemplate(PUBLIC_STRICT_MODE_ERROR_TEMPLATE, TEMPLATE_CONTEXT); +} + +function renderPullRequestTargetErrorMessage() { + return renderTemplate(PULL_REQUEST_TARGET_ERROR_TEMPLATE, TEMPLATE_CONTEXT); +} + +module.exports = { + renderLockdownTokenErrorMessage, + renderPublicStrictModeErrorMessage, + renderPullRequestTargetErrorMessage, +};