diff --git a/docs/src/content/docs/reference/github-tools.md b/docs/src/content/docs/reference/github-tools.md index cb8816e6ea1..2e8c19ab0c7 100644 --- a/docs/src/content/docs/reference/github-tools.md +++ b/docs/src/content/docs/reference/github-tools.md @@ -40,7 +40,7 @@ Some key toolsets are: - `labels` (labels management) :::note -`toolsets: [all]` does **not** include the `dependabot` toolset. Because `dependabot` requires the `vulnerability-alerts` GitHub App-only permission (not grantable via `GITHUB_TOKEN`), it must be opted into explicitly: +`toolsets: [all]` does **not** include the `dependabot` toolset. The `dependabot` toolset must be opted into explicitly: ```yaml wrap tools: @@ -167,7 +167,15 @@ gh aw secrets set GH_AW_GITHUB_MCP_SERVER_TOKEN --value "" ### Using the `dependabot` toolset -The `dependabot` toolset can only be used if authenticating with a PAT or GitHub App and also requires the `vulnerability-alerts` GitHub App permission. If you are using a GitHub App (rather than a PAT), add `vulnerability-alerts: read` to your workflow's `permissions:` field and ensure the GitHub App is configured with this permission. See [GitHub App-Only Permissions](/gh-aw/reference/permissions/#github-app-only-permissions). +The `dependabot` toolset requires the `vulnerability-alerts: read` and `security-events: read` permissions. These are now supported natively by `GITHUB_TOKEN`. Add them to your workflow's `permissions:` field: + +```yaml +permissions: + vulnerability-alerts: read + security-events: read +``` + +Alternatively, you can authenticate with a PAT or GitHub App. If using a GitHub App, add `vulnerability-alerts: read` to your workflow's `permissions:` field and ensure the GitHub App is configured with this permission. ## Related Documentation diff --git a/docs/src/content/docs/reference/permissions.md b/docs/src/content/docs/reference/permissions.md index 74df8ff59f9..73a737158b2 100644 --- a/docs/src/content/docs/reference/permissions.md +++ b/docs/src/content/docs/reference/permissions.md @@ -44,7 +44,7 @@ See [GitHub's permissions reference](https://docs.github.com/en/actions/using-jo Certain permission scopes require [additional authentication](/gh-aw/reference/github-tools/#additional-authentication-for-github-tools). These include: -**Repository-level:** `administration`, `environments`, `git-signing`, `vulnerability-alerts`, `workflows`, `repository-hooks`, `single-file`, `codespaces`, `repository-custom-properties` +**Repository-level:** `administration`, `environments`, `git-signing`, `workflows`, `repository-hooks`, `single-file`, `codespaces`, `repository-custom-properties` **Organization-level:** `organization-projects`, `members`, `organization-administration`, `team-discussions`, `organization-hooks`, `organization-members`, `organization-packages`, `organization-self-hosted-runners`, `organization-custom-org-roles`, `organization-custom-properties`, `organization-custom-repository-roles`, `organization-announcement-banners`, `organization-events`, `organization-plan`, `organization-user-blocking`, `organization-personal-access-token-requests`, `organization-personal-access-tokens`, `organization-copilot`, `organization-codespaces` diff --git a/pkg/cli/compile_permissions_integration_test.go b/pkg/cli/compile_permissions_integration_test.go index 9d9cf5a186d..21cf331f3bb 100644 --- a/pkg/cli/compile_permissions_integration_test.go +++ b/pkg/cli/compile_permissions_integration_test.go @@ -14,15 +14,15 @@ import ( "github.com/stretchr/testify/require" ) -// TestCompileVulnerabilityAlertsPermissionFiltered compiles the canonical +// TestCompileVulnerabilityAlertsPermissionIncluded compiles the canonical // test-vulnerability-alerts-permission.md workflow file and verifies that -// the GitHub App-only `vulnerability-alerts` scope does NOT appear in any -// job-level permissions block of the compiled lock file. +// the `vulnerability-alerts` scope appears correctly in the compiled lock file. // -// GitHub Actions rejects workflows that declare App-only permissions at the -// job level (e.g. vulnerability-alerts, members, administration). These scopes -// must only appear as `permission-*` inputs to actions/create-github-app-token. -func TestCompileVulnerabilityAlertsPermissionFiltered(t *testing.T) { +// Since vulnerability-alerts is now a native GITHUB_TOKEN permission scope, it +// SHOULD appear in job-level permissions blocks. It is also forwarded as a +// `permission-vulnerability-alerts` input to actions/create-github-app-token +// when a GitHub App is configured. +func TestCompileVulnerabilityAlertsPermissionIncluded(t *testing.T) { setup := setupIntegrationTest(t) defer setup.cleanup() @@ -51,8 +51,8 @@ func TestCompileVulnerabilityAlertsPermissionFiltered(t *testing.T) { assert.Contains(t, lockContentStr, "id: github-mcp-app-token", "GitHub App token minting step should be generated") - // Critically: vulnerability-alerts must NOT appear inside any job-level permissions block. - // Parse the YAML and walk every job's permissions map to check. + // vulnerability-alerts is now a valid GITHUB_TOKEN scope and SHOULD appear in + // job-level permissions blocks. var workflow map[string]any require.NoError(t, goyaml.Unmarshal(lockContent, &workflow), "Lock file should be valid YAML") @@ -60,7 +60,8 @@ func TestCompileVulnerabilityAlertsPermissionFiltered(t *testing.T) { jobs, ok := workflow["jobs"].(map[string]any) require.True(t, ok, "Lock file should have a jobs section") - for jobName, jobConfig := range jobs { + foundVulnAlerts := false + for _, jobConfig := range jobs { jobMap, ok := jobConfig.(map[string]any) if !ok { continue @@ -71,22 +72,21 @@ func TestCompileVulnerabilityAlertsPermissionFiltered(t *testing.T) { } permsMap, ok := perms.(map[string]any) if !ok { - // Shorthand permissions (read-all / write-all / none) — nothing to check continue } - assert.NotContains(t, permsMap, "vulnerability-alerts", - "Job %q must not have vulnerability-alerts in job-level permissions block "+ - "(it is a GitHub App-only scope and not a valid GitHub Actions permission)", jobName) + if _, found := permsMap["vulnerability-alerts"]; found { + foundVulnAlerts = true + } } + assert.True(t, foundVulnAlerts, + "vulnerability-alerts should appear in at least one job-level permissions block (it is a GITHUB_TOKEN scope)") - // Extra belt-and-suspenders: the string "vulnerability-alerts: read" must not appear - // anywhere other than inside the App token step inputs. - // We verify by counting occurrences: exactly one occurrence for the App token step. + // vulnerability-alerts: read should appear in both job-level permissions + // and in the App token step inputs (permission-vulnerability-alerts: read). occurrences := strings.Count(lockContentStr, "vulnerability-alerts: read") - // The permission-vulnerability-alerts: read line contains "vulnerability-alerts: read" - // as a substring, so we count that and only that occurrence. appTokenOccurrences := strings.Count(lockContentStr, "permission-vulnerability-alerts: read") - assert.Equal(t, appTokenOccurrences, occurrences, - "vulnerability-alerts: read should appear only inside the App token step inputs, not elsewhere in the lock file\nLock file:\n%s", - lockContentStr) + assert.GreaterOrEqual(t, occurrences, appTokenOccurrences, + "vulnerability-alerts: read should appear at least as often as permission-vulnerability-alerts: read") + assert.Greater(t, occurrences, 0, + "vulnerability-alerts: read should appear at least once in the lock file") } diff --git a/pkg/cli/workflows/test-vulnerability-alerts-permission.md b/pkg/cli/workflows/test-vulnerability-alerts-permission.md index 0bb221efb3c..994950b3f25 100644 --- a/pkg/cli/workflows/test-vulnerability-alerts-permission.md +++ b/pkg/cli/workflows/test-vulnerability-alerts-permission.md @@ -20,6 +20,6 @@ tools: # Vulnerability Alerts with GitHub App Token This workflow uses the Dependabot toolset with a GitHub App token. -The `vulnerability-alerts: read` permission is a GitHub App-only scope and must NOT -appear in the compiled job-level `permissions:` block. It should only appear as a -`permission-vulnerability-alerts: read` input to the `create-github-app-token` step. +The `vulnerability-alerts: read` permission is now a native GITHUB_TOKEN scope +and will appear in the compiled job-level `permissions:` block. It is also forwarded +as a `permission-vulnerability-alerts: read` input to the `create-github-app-token` step. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 61f1e0e8d8a..f5e55bc61c2 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -10380,7 +10380,7 @@ "vulnerability-alerts": { "type": "string", "enum": ["read", "write", "none"], - "description": "Permission level for Dependabot vulnerability alerts (read/write/none). GitHub App-only permission: required to access Dependabot alerts via the GitHub MCP server. The GITHUB_TOKEN does not have this permission \u2014 a GitHub App must be configured." + "description": "Permission level for Dependabot vulnerability alerts (read/write/none). Allows workflows to access the Dependabot alerts API via GITHUB_TOKEN instead of requiring a PAT or GitHub App." }, "all": { "type": "string", @@ -10536,8 +10536,8 @@ }, "vulnerability-alerts": { "type": "string", - "enum": ["read", "none", "write"], - "description": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler). GitHub App-only permission." + "enum": ["read", "none"], + "description": "Permission level for Dependabot vulnerability alerts (read/none; \"write\" is rejected by the compiler). Also available as a GITHUB_TOKEN scope. When used with a GitHub App, forwarded as permission-vulnerability-alerts input." }, "workflows": { "type": "string", diff --git a/pkg/workflow/compiler_main_job.go b/pkg/workflow/compiler_main_job.go index cd0a0b439b7..99e6b93c14d 100644 --- a/pkg/workflow/compiler_main_job.go +++ b/pkg/workflow/compiler_main_job.go @@ -247,7 +247,7 @@ func (c *Compiler) buildMainJob(data *WorkflowData, activationJobCreated bool) ( // In dev/script mode, automatically add contents: read if the actions folder checkout is needed // In release mode, use the permissions as specified by the user (no automatic augmentation) // - // GitHub App-only permissions (e.g., vulnerability-alerts) must be filtered out before + // GitHub App-only permissions (e.g., members, administration) must be filtered out before // rendering to the job-level permissions block. These scopes are not valid GitHub Actions // workflow permissions and cause a parse error when queued. They are handled separately // when minting GitHub App installation access tokens (as permission-* inputs). diff --git a/pkg/workflow/dangerous_permissions_validation_test.go b/pkg/workflow/dangerous_permissions_validation_test.go index d04bae38773..80e2a450678 100644 --- a/pkg/workflow/dangerous_permissions_validation_test.go +++ b/pkg/workflow/dangerous_permissions_validation_test.go @@ -153,7 +153,7 @@ func TestFindWritePermissions(t *testing.T) { { name: "write-all shorthand", permissions: NewPermissionsWriteAll(), - expectedWriteCount: 14, // All GitHub Actions permission scopes except id-token and metadata (which are excluded) + expectedWriteCount: 15, // All GitHub Actions permission scopes except id-token and metadata (which are excluded) expectedScopes: nil, // Don't check specific scopes for shorthand }, { diff --git a/pkg/workflow/frontmatter_parsing.go b/pkg/workflow/frontmatter_parsing.go index 7de6897d95d..d9560792655 100644 --- a/pkg/workflow/frontmatter_parsing.go +++ b/pkg/workflow/frontmatter_parsing.go @@ -204,6 +204,8 @@ func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, err config.SecurityEvents = levelStr case "statuses": config.Statuses = levelStr + case "vulnerability-alerts": + config.VulnerabilityAlerts = levelStr case "organization-projects": config.OrganizationProjects = levelStr // GitHub App-only permission scopes @@ -213,8 +215,6 @@ func parsePermissionsConfig(permissions map[string]any) (*PermissionsConfig, err config.Environments = levelStr case "git-signing": config.GitSigning = levelStr - case "vulnerability-alerts": - config.VulnerabilityAlerts = levelStr case "workflows": config.Workflows = levelStr case "repository-hooks": diff --git a/pkg/workflow/frontmatter_serialization.go b/pkg/workflow/frontmatter_serialization.go index bb90c989357..1396d9af3a7 100644 --- a/pkg/workflow/frontmatter_serialization.go +++ b/pkg/workflow/frontmatter_serialization.go @@ -336,6 +336,9 @@ func permissionsConfigToMap(config *PermissionsConfig) map[string]any { if config.Statuses != "" { result["statuses"] = config.Statuses } + if config.VulnerabilityAlerts != "" { + result["vulnerability-alerts"] = config.VulnerabilityAlerts + } if config.OrganizationProjects != "" { result["organization-projects"] = config.OrganizationProjects } @@ -350,9 +353,6 @@ func permissionsConfigToMap(config *PermissionsConfig) map[string]any { if config.GitSigning != "" { result["git-signing"] = config.GitSigning } - if config.VulnerabilityAlerts != "" { - result["vulnerability-alerts"] = config.VulnerabilityAlerts - } if config.Workflows != "" { result["workflows"] = config.Workflows } diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go index 399c15fda3a..5ca20333ac7 100644 --- a/pkg/workflow/frontmatter_types.go +++ b/pkg/workflow/frontmatter_types.go @@ -35,19 +35,20 @@ type RuntimesConfig struct { // These scopes can be declared in the workflow's top-level permissions block and are enforced // natively by GitHub Actions. type GitHubActionsPermissionsConfig struct { - Actions string `json:"actions,omitempty"` - Checks string `json:"checks,omitempty"` - Contents string `json:"contents,omitempty"` - Deployments string `json:"deployments,omitempty"` - IDToken string `json:"id-token,omitempty"` - Issues string `json:"issues,omitempty"` - Discussions string `json:"discussions,omitempty"` - Packages string `json:"packages,omitempty"` - Pages string `json:"pages,omitempty"` - PullRequests string `json:"pull-requests,omitempty"` - RepositoryProjects string `json:"repository-projects,omitempty"` - SecurityEvents string `json:"security-events,omitempty"` - Statuses string `json:"statuses,omitempty"` + Actions string `json:"actions,omitempty"` + Checks string `json:"checks,omitempty"` + Contents string `json:"contents,omitempty"` + Deployments string `json:"deployments,omitempty"` + IDToken string `json:"id-token,omitempty"` + Issues string `json:"issues,omitempty"` + Discussions string `json:"discussions,omitempty"` + Packages string `json:"packages,omitempty"` + Pages string `json:"pages,omitempty"` + PullRequests string `json:"pull-requests,omitempty"` + RepositoryProjects string `json:"repository-projects,omitempty"` + SecurityEvents string `json:"security-events,omitempty"` + Statuses string `json:"statuses,omitempty"` + VulnerabilityAlerts string `json:"vulnerability-alerts,omitempty"` } // GitHubAppPermissionsConfig holds permission scopes that are exclusive to GitHub App @@ -78,7 +79,6 @@ type GitHubAppPermissionsConfig struct { Administration string `json:"administration,omitempty"` Environments string `json:"environments,omitempty"` GitSigning string `json:"git-signing,omitempty"` - VulnerabilityAlerts string `json:"vulnerability-alerts,omitempty"` Workflows string `json:"workflows,omitempty"` RepositoryHooks string `json:"repository-hooks,omitempty"` SingleFile string `json:"single-file,omitempty"` diff --git a/pkg/workflow/github_app_permissions_validation_test.go b/pkg/workflow/github_app_permissions_validation_test.go index 8aab7df8bc8..c6dbb05f0c0 100644 --- a/pkg/workflow/github_app_permissions_validation_test.go +++ b/pkg/workflow/github_app_permissions_validation_test.go @@ -85,10 +85,9 @@ func TestValidateGitHubAppOnlyPermissions(t *testing.T) { errorContains: "workflows", }, { - name: "vulnerability-alerts permission without github-app - should error", - permissions: "permissions:\n vulnerability-alerts: read", - shouldError: true, - errorContains: "vulnerability-alerts", + name: "vulnerability-alerts permission without github-app - should pass (GITHUB_TOKEN scope)", + permissions: "permissions:\n vulnerability-alerts: read", + shouldError: false, }, { name: "mixed Actions and App-only permissions with github-app - should pass", @@ -228,6 +227,7 @@ func TestIsGitHubAppOnlyScope(t *testing.T) { {PermissionSecurityEvents, false}, {PermissionStatuses, false}, {PermissionDiscussions, false}, + {PermissionVulnerabilityAlerts, false}, // organization-projects is a GitHub App-only scope (not in GitHub Actions GITHUB_TOKEN) {PermissionOrganizationProj, true}, // GitHub App-only scopes - should return true @@ -237,7 +237,6 @@ func TestIsGitHubAppOnlyScope(t *testing.T) { {PermissionEnvironments, true}, {PermissionGitSigning, true}, {PermissionTeamDiscussions, true}, - {PermissionVulnerabilityAlerts, true}, {PermissionWorkflows, true}, {PermissionRepositoryHooks, true}, {PermissionOrganizationHooks, true}, @@ -272,7 +271,6 @@ func TestGetAllGitHubAppOnlyScopes(t *testing.T) { PermissionOrganizationAdministration, PermissionEnvironments, PermissionWorkflows, - PermissionVulnerabilityAlerts, PermissionOrganizationPackages, } diff --git a/pkg/workflow/github_mcp_app_token_test.go b/pkg/workflow/github_mcp_app_token_test.go index be4c9cd7e14..e2db85d21bf 100644 --- a/pkg/workflow/github_mcp_app_token_test.go +++ b/pkg/workflow/github_mcp_app_token_test.go @@ -367,14 +367,14 @@ Test that permission-vulnerability-alerts is emitted in the App token minting st assert.Contains(t, lockContent, "permission-security-events: read", "Should also include security-events read permission in App token") // Verify the token minting step is present assert.Contains(t, lockContent, "id: github-mcp-app-token", "GitHub App token step should be generated") - // Verify that vulnerability-alerts does NOT appear in any job-level permissions block. - // It is a GitHub App-only permission and not a valid GitHub Actions workflow permission; - // GitHub Actions rejects workflows that declare it at the job level. + // Verify that vulnerability-alerts DOES appear in job-level permissions block. + // It is now a valid GITHUB_TOKEN permission scope. var workflow map[string]any require.NoError(t, goyaml.Unmarshal(content, &workflow), "Lock file should be valid YAML") jobs, ok := workflow["jobs"].(map[string]any) require.True(t, ok, "Should have jobs section") - for jobName, jobConfig := range jobs { + foundVulnAlerts := false + for _, jobConfig := range jobs { jobMap, ok := jobConfig.(map[string]any) if !ok { continue @@ -388,9 +388,10 @@ Test that permission-vulnerability-alerts is emitted in the App token minting st continue } if _, found := permsMap["vulnerability-alerts"]; found { - t.Errorf("Job %q should not have vulnerability-alerts in job-level permissions block (it is a GitHub App-only permission)", jobName) + foundVulnAlerts = true } } + assert.True(t, foundVulnAlerts, "vulnerability-alerts should appear in at least one job-level permissions block (it is a GITHUB_TOKEN scope)") } // TestGitHubMCPAppTokenWithExtraPermissions tests that extra permissions under @@ -450,7 +451,7 @@ Test extra org-level permissions in GitHub App token. } // TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel tests that extra permissions -// under tools.github.github-app.permissions can suppress a GitHub App-only scope +// under tools.github.github-app.permissions can suppress a scope // that was set at job level by overriding it with 'none' (nested wins). func TestGitHubMCPAppTokenExtraPermissionsOverrideJobLevel(t *testing.T) { compiler := NewCompiler(WithVersion("1.0.0")) @@ -474,7 +475,7 @@ tools: # Test Workflow -Test that nested permissions override job-level GitHub App-only scopes (nested wins). +Test that nested permissions override job-level scopes (nested wins). ` tmpDir := t.TempDir() diff --git a/pkg/workflow/github_toolsets.go b/pkg/workflow/github_toolsets.go index cc916b4feff..a2284d02efa 100644 --- a/pkg/workflow/github_toolsets.go +++ b/pkg/workflow/github_toolsets.go @@ -19,8 +19,7 @@ var DefaultGitHubToolsets = []string{"context", "repos", "issues", "pull_request var ActionFriendlyGitHubToolsets = []string{"context", "repos", "issues", "pull_requests"} // GitHubToolsetsExcludedFromAll defines toolsets that are NOT included when "all" is specified. -// These toolsets require GitHub App-only permissions (e.g., vulnerability-alerts) that -// cannot be granted via GITHUB_TOKEN, so they must be opted-in to explicitly. +// These toolsets are opt-in only to avoid granting unnecessary permissions by default. var GitHubToolsetsExcludedFromAll = []string{"dependabot"} // ParseGitHubToolsets parses the toolsets string and expands "default" and "all" diff --git a/pkg/workflow/permissions.go b/pkg/workflow/permissions.go index 8b466080f01..8b5626ad1cc 100644 --- a/pkg/workflow/permissions.go +++ b/pkg/workflow/permissions.go @@ -46,6 +46,8 @@ func convertStringToPermissionScope(key string) PermissionScope { return PermissionStatuses case "copilot-requests": return PermissionCopilotRequests + case "vulnerability-alerts": + return PermissionVulnerabilityAlerts // GitHub App-only permission scopes (not supported by GITHUB_TOKEN, require a GitHub App) // organization-projects is included here because it is a GitHub App-only scope // (it is excluded from GetAllPermissionScopes() and skipped in YAML rendering). @@ -63,8 +65,6 @@ func convertStringToPermissionScope(key string) PermissionScope { return PermissionGitSigning case "team-discussions": return PermissionTeamDiscussions - case "vulnerability-alerts": - return PermissionVulnerabilityAlerts case "workflows": return PermissionWorkflows case "repository-hooks": @@ -133,24 +133,31 @@ const ( type PermissionScope string const ( - // GitHub Actions permission scopes (supported by GITHUB_TOKEN) - PermissionActions PermissionScope = "actions" - PermissionAttestations PermissionScope = "attestations" - PermissionChecks PermissionScope = "checks" - PermissionContents PermissionScope = "contents" - PermissionDeployments PermissionScope = "deployments" - PermissionDiscussions PermissionScope = "discussions" - PermissionIdToken PermissionScope = "id-token" - PermissionIssues PermissionScope = "issues" - PermissionMetadata PermissionScope = "metadata" - PermissionModels PermissionScope = "models" - PermissionPackages PermissionScope = "packages" - PermissionPages PermissionScope = "pages" - PermissionPullRequests PermissionScope = "pull-requests" - PermissionRepositoryProj PermissionScope = "repository-projects" + // GitHub Actions permission scopes (supported by GITHUB_TOKEN), except + // organization-projects which is declared here for historical grouping but + // treated as GitHub App-only by GetAllGitHubAppOnlyScopes/IsGitHubAppOnlyScope. + PermissionActions PermissionScope = "actions" + PermissionAttestations PermissionScope = "attestations" + PermissionChecks PermissionScope = "checks" + PermissionContents PermissionScope = "contents" + PermissionDeployments PermissionScope = "deployments" + PermissionDiscussions PermissionScope = "discussions" + PermissionIdToken PermissionScope = "id-token" + PermissionIssues PermissionScope = "issues" + PermissionMetadata PermissionScope = "metadata" + PermissionModels PermissionScope = "models" + PermissionPackages PermissionScope = "packages" + PermissionPages PermissionScope = "pages" + PermissionPullRequests PermissionScope = "pull-requests" + PermissionRepositoryProj PermissionScope = "repository-projects" + PermissionSecurityEvents PermissionScope = "security-events" + PermissionStatuses PermissionScope = "statuses" + PermissionVulnerabilityAlerts PermissionScope = "vulnerability-alerts" + + // PermissionOrganizationProj is declared here for constant grouping but is treated as + // GitHub App-only at runtime (excluded from GetAllPermissionScopes(), included in + // GetAllGitHubAppOnlyScopes() and IsGitHubAppOnlyScope). PermissionOrganizationProj PermissionScope = "organization-projects" - PermissionSecurityEvents PermissionScope = "security-events" - PermissionStatuses PermissionScope = "statuses" // PermissionCopilotRequests is a GitHub Actions permission scope used with the copilot-requests feature. // It enables use of the GitHub Actions token as the Copilot authentication token. PermissionCopilotRequests PermissionScope = "copilot-requests" @@ -164,7 +171,6 @@ const ( PermissionAdministration PermissionScope = "administration" PermissionEnvironments PermissionScope = "environments" PermissionGitSigning PermissionScope = "git-signing" - PermissionVulnerabilityAlerts PermissionScope = "vulnerability-alerts" PermissionWorkflows PermissionScope = "workflows" PermissionRepositoryHooks PermissionScope = "repository-hooks" PermissionSingleFile PermissionScope = "single-file" @@ -218,6 +224,7 @@ func GetAllPermissionScopes() []PermissionScope { PermissionRepositoryProj, PermissionSecurityEvents, PermissionStatuses, + PermissionVulnerabilityAlerts, } } @@ -230,7 +237,6 @@ func GetAllGitHubAppOnlyScopes() []PermissionScope { PermissionAdministration, PermissionEnvironments, PermissionGitSigning, - PermissionVulnerabilityAlerts, PermissionWorkflows, PermissionRepositoryHooks, PermissionSingleFile, diff --git a/pkg/workflow/permissions_operations.go b/pkg/workflow/permissions_operations.go index c0fbb185420..198f0ed858f 100644 --- a/pkg/workflow/permissions_operations.go +++ b/pkg/workflow/permissions_operations.go @@ -21,7 +21,7 @@ func SortPermissionScopes(s []PermissionScope) { // filterJobLevelPermissions takes a raw permissions YAML string (as stored in WorkflowData.Permissions) // and returns a version suitable for use in a GitHub Actions job-level permissions block. // -// GitHub App-only permission scopes (e.g., vulnerability-alerts, members, administration) are not +// GitHub App-only permission scopes (e.g., members, administration) are not // valid GitHub Actions workflow permissions and cause a parse error when GitHub Actions tries to // queue the workflow. Those scopes must only appear as permission-* inputs when minting GitHub App // installation access tokens via actions/create-github-app-token, not in the job-level block. diff --git a/pkg/workflow/permissions_operations_test.go b/pkg/workflow/permissions_operations_test.go index 7232eba8338..43021b08607 100644 --- a/pkg/workflow/permissions_operations_test.go +++ b/pkg/workflow/permissions_operations_test.go @@ -423,21 +423,22 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionContents: PermissionWrite}), merge: NewPermissionsReadAll(), want: map[PermissionScope]PermissionLevel{ - PermissionContents: PermissionWrite, // preserved - PermissionActions: PermissionRead, // added - PermissionAttestations: PermissionRead, - PermissionChecks: PermissionRead, - PermissionDeployments: PermissionRead, - PermissionDiscussions: PermissionRead, - PermissionIssues: PermissionRead, - PermissionMetadata: PermissionRead, - PermissionPackages: PermissionRead, - PermissionPages: PermissionRead, - PermissionPullRequests: PermissionRead, - PermissionRepositoryProj: PermissionRead, - PermissionSecurityEvents: PermissionRead, - PermissionStatuses: PermissionRead, - PermissionModels: PermissionRead, + PermissionContents: PermissionWrite, // preserved + PermissionActions: PermissionRead, // added + PermissionAttestations: PermissionRead, + PermissionChecks: PermissionRead, + PermissionDeployments: PermissionRead, + PermissionDiscussions: PermissionRead, + PermissionIssues: PermissionRead, + PermissionMetadata: PermissionRead, + PermissionPackages: PermissionRead, + PermissionPages: PermissionRead, + PermissionPullRequests: PermissionRead, + PermissionRepositoryProj: PermissionRead, + PermissionSecurityEvents: PermissionRead, + PermissionStatuses: PermissionRead, + PermissionModels: PermissionRead, + PermissionVulnerabilityAlerts: PermissionRead, // Note: id-token is NOT included because it doesn't support read level // Note: organization-projects is NOT included because it's a GitHub App-only scope }, @@ -447,22 +448,23 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionContents: PermissionRead}), merge: NewPermissionsWriteAll(), want: map[PermissionScope]PermissionLevel{ - PermissionContents: PermissionRead, // preserved (not overwritten) - PermissionActions: PermissionWrite, - PermissionAttestations: PermissionWrite, - PermissionChecks: PermissionWrite, - PermissionDeployments: PermissionWrite, - PermissionDiscussions: PermissionWrite, - PermissionIdToken: PermissionWrite, // id-token supports write - PermissionIssues: PermissionWrite, - PermissionMetadata: PermissionWrite, - PermissionPackages: PermissionWrite, - PermissionPages: PermissionWrite, - PermissionPullRequests: PermissionWrite, - PermissionRepositoryProj: PermissionWrite, - PermissionSecurityEvents: PermissionWrite, - PermissionStatuses: PermissionWrite, - PermissionModels: PermissionWrite, + PermissionContents: PermissionRead, // preserved (not overwritten) + PermissionActions: PermissionWrite, + PermissionAttestations: PermissionWrite, + PermissionChecks: PermissionWrite, + PermissionDeployments: PermissionWrite, + PermissionDiscussions: PermissionWrite, + PermissionIdToken: PermissionWrite, // id-token supports write + PermissionIssues: PermissionWrite, + PermissionMetadata: PermissionWrite, + PermissionPackages: PermissionWrite, + PermissionPages: PermissionWrite, + PermissionPullRequests: PermissionWrite, + PermissionRepositoryProj: PermissionWrite, + PermissionSecurityEvents: PermissionWrite, + PermissionStatuses: PermissionWrite, + PermissionModels: PermissionWrite, + PermissionVulnerabilityAlerts: PermissionWrite, // Note: organization-projects is NOT included because it's a GitHub App-only scope }, }, @@ -471,21 +473,22 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionContents: PermissionWrite}), merge: NewPermissionsReadAll(), want: map[PermissionScope]PermissionLevel{ - PermissionContents: PermissionWrite, - PermissionActions: PermissionRead, - PermissionAttestations: PermissionRead, - PermissionChecks: PermissionRead, - PermissionDeployments: PermissionRead, - PermissionDiscussions: PermissionRead, - PermissionIssues: PermissionRead, - PermissionMetadata: PermissionRead, - PermissionPackages: PermissionRead, - PermissionPages: PermissionRead, - PermissionPullRequests: PermissionRead, - PermissionRepositoryProj: PermissionRead, - PermissionSecurityEvents: PermissionRead, - PermissionStatuses: PermissionRead, - PermissionModels: PermissionRead, + PermissionContents: PermissionWrite, + PermissionActions: PermissionRead, + PermissionAttestations: PermissionRead, + PermissionChecks: PermissionRead, + PermissionDeployments: PermissionRead, + PermissionDiscussions: PermissionRead, + PermissionIssues: PermissionRead, + PermissionMetadata: PermissionRead, + PermissionPackages: PermissionRead, + PermissionPages: PermissionRead, + PermissionPullRequests: PermissionRead, + PermissionRepositoryProj: PermissionRead, + PermissionSecurityEvents: PermissionRead, + PermissionStatuses: PermissionRead, + PermissionModels: PermissionRead, + PermissionVulnerabilityAlerts: PermissionRead, // Note: id-token is NOT included because it doesn't support read level // Note: organization-projects is NOT included because it's a GitHub App-only scope }, @@ -495,22 +498,23 @@ func TestPermissionsMerge(t *testing.T) { base: NewPermissionsFromMap(map[PermissionScope]PermissionLevel{PermissionIssues: PermissionRead}), merge: NewPermissionsWriteAll(), want: map[PermissionScope]PermissionLevel{ - PermissionIssues: PermissionRead, - PermissionActions: PermissionWrite, - PermissionAttestations: PermissionWrite, - PermissionChecks: PermissionWrite, - PermissionContents: PermissionWrite, - PermissionDeployments: PermissionWrite, - PermissionDiscussions: PermissionWrite, - PermissionIdToken: PermissionWrite, // id-token supports write - PermissionMetadata: PermissionWrite, - PermissionPackages: PermissionWrite, - PermissionPages: PermissionWrite, - PermissionPullRequests: PermissionWrite, - PermissionRepositoryProj: PermissionWrite, - PermissionSecurityEvents: PermissionWrite, - PermissionStatuses: PermissionWrite, - PermissionModels: PermissionWrite, + PermissionIssues: PermissionRead, + PermissionActions: PermissionWrite, + PermissionAttestations: PermissionWrite, + PermissionChecks: PermissionWrite, + PermissionContents: PermissionWrite, + PermissionDeployments: PermissionWrite, + PermissionDiscussions: PermissionWrite, + PermissionIdToken: PermissionWrite, // id-token supports write + PermissionMetadata: PermissionWrite, + PermissionPackages: PermissionWrite, + PermissionPages: PermissionWrite, + PermissionPullRequests: PermissionWrite, + PermissionRepositoryProj: PermissionWrite, + PermissionSecurityEvents: PermissionWrite, + PermissionStatuses: PermissionWrite, + PermissionModels: PermissionWrite, + PermissionVulnerabilityAlerts: PermissionWrite, }, }, { @@ -702,32 +706,34 @@ func TestFilterJobLevelPermissions(t *testing.T) { excludes: []string{}, }, { - name: "vulnerability-alerts is filtered out", + name: "vulnerability-alerts is preserved (GITHUB_TOKEN scope)", input: "permissions:\n contents: read\n pull-requests: read\n security-events: read\n vulnerability-alerts: read", contains: []string{ "permissions:", " contents: read", " pull-requests: read", " security-events: read", + " vulnerability-alerts: read", }, - excludes: []string{"vulnerability-alerts"}, + excludes: []string{}, }, { - name: "multiple GitHub App-only scopes are filtered out", + name: "multiple GitHub App-only scopes are filtered out but vulnerability-alerts is preserved", input: "permissions:\n contents: read\n issues: write\n administration: read\n members: read\n vulnerability-alerts: read", contains: []string{ "permissions:", " contents: read", " issues: write", + " vulnerability-alerts: read", }, - excludes: []string{"administration", "members", "vulnerability-alerts"}, + excludes: []string{"administration", "members"}, }, { name: "only GitHub App-only scopes returns empty string", - input: "permissions:\n vulnerability-alerts: read\n members: read", + input: "permissions:\n members: read", expectEmpty: true, contains: []string{}, - excludes: []string{"vulnerability-alerts", "members"}, + excludes: []string{"members"}, }, { name: "shorthand read-all is preserved unchanged", diff --git a/pkg/workflow/permissions_validator_test.go b/pkg/workflow/permissions_validator_test.go index a4da6b7b03e..9d15393cc80 100644 --- a/pkg/workflow/permissions_validator_test.go +++ b/pkg/workflow/permissions_validator_test.go @@ -89,11 +89,12 @@ func TestCollectRequiredPermissions(t *testing.T) { }, }, { - name: "Dependabot toolset requires only security-events (vulnerability-alerts is GitHub App-only)", + name: "Dependabot toolset requires security-events and vulnerability-alerts", toolsets: []string{"dependabot"}, readOnly: false, expected: map[PermissionScope]PermissionLevel{ - PermissionSecurityEvents: PermissionRead, + PermissionSecurityEvents: PermissionRead, + PermissionVulnerabilityAlerts: PermissionRead, }, }, { diff --git a/pkg/workflow/safe_outputs_app_config.go b/pkg/workflow/safe_outputs_app_config.go index df67ab6a9ce..daa8f1d2a1c 100644 --- a/pkg/workflow/safe_outputs_app_config.go +++ b/pkg/workflow/safe_outputs_app_config.go @@ -266,6 +266,9 @@ func convertPermissionsToAppTokenFields(permissions *Permissions) map[string]str if level, ok := permissions.Get(PermissionStatuses); ok { fields["permission-statuses"] = string(level) } + if level, ok := permissions.Get(PermissionVulnerabilityAlerts); ok { + fields["permission-vulnerability-alerts"] = string(level) + } // "permission-discussions" is a declared input in actions/create-github-app-token v3+. // Crucially, when ANY permission-* input is specified the action scopes the token to ONLY those // permissions (returning undefined → inherit-all only when zero permission-* inputs are present). @@ -288,9 +291,6 @@ func convertPermissionsToAppTokenFields(permissions *Permissions) map[string]str if level, ok := permissions.GetExplicit(PermissionGitSigning); ok { fields["permission-git-signing"] = string(level) } - if level, ok := permissions.GetExplicit(PermissionVulnerabilityAlerts); ok { - fields["permission-vulnerability-alerts"] = string(level) - } if level, ok := permissions.GetExplicit(PermissionWorkflows); ok { fields["permission-workflows"] = string(level) }