Skip to content

Commit

Permalink
feat: policy GitHub actions default permissions (#46)
Browse files Browse the repository at this point in the history
* added actions policies for organization

* added repository github actions settings policies

* updated docs
  • Loading branch information
noamd-legit committed Nov 21, 2022
1 parent 6dcb9b0 commit aefbe88
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 16 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-github/v38 v38.1.0 // indirect
github.com/google/go-github/v41 v41.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-github/v38 v38.1.0 h1:C6h1FkaITcBFK7gAmq4eFzt6gbhEhk7L5z6R3Uva+po=
github.com/google/go-github/v38 v38.1.0/go.mod h1:cStvrz/7nFr0FoENgG6GLbp53WaelXucT+BBz/3VKx4=
github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
Expand Down
30 changes: 28 additions & 2 deletions internal/clients/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"github.com/Legit-Labs/legitify/internal/clients/github/types"
"log"
"net/http"
"regexp"
Expand All @@ -13,7 +14,6 @@ import (
githubcollected "github.com/Legit-Labs/legitify/internal/collected/github"
"github.com/Legit-Labs/legitify/internal/common/permissions"

"github.com/google/go-github/v44/github"
gh "github.com/google/go-github/v44/github"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
Expand All @@ -26,6 +26,8 @@ type Client interface {
Scopes() permissions.TokenScopes
Orgs() []string
IsGithubCloud() bool
GetActionsTokenPermissionsForOrganization(organization string) (*types.TokenPermissions, error)
GetActionsTokenPermissionsForRepository(organization string, repository string) (*types.TokenPermissions, error)
}

const experimentalApiAcceptHeader = "application/vnd.github.hawkgirl-preview+json"
Expand Down Expand Up @@ -290,7 +292,7 @@ func (c *client) collectTokenScopes() (permissions.TokenScopes, error) {

func (c *client) collectOrgsList() ([]string, error) {
var orgNames []string
err := PaginateResults(func(opts *github.ListOptions) (*github.Response, error) {
err := PaginateResults(func(opts *gh.ListOptions) (*gh.Response, error) {
orgs, resp, err := c.Client().Organizations.List(c.context, "", opts)

if err != nil {
Expand Down Expand Up @@ -334,6 +336,30 @@ func (c *client) collectSpecificOrganizations() ([]githubcollected.ExtendedOrg,
return res, nil
}

func (c *client) GetActionsTokenPermissionsForOrganization(organization string) (*types.TokenPermissions, error) {
u := fmt.Sprintf("orgs/%s/actions/permissions/workflow", organization)
return c.GetActionsTokenPermissions(u)
}

func (c *client) GetActionsTokenPermissionsForRepository(organization string, repository string) (*types.TokenPermissions, error) {
u := fmt.Sprintf("repos/%s/%s/actions/permissions/workflow", organization, repository)
return c.GetActionsTokenPermissions(u)
}

func (c *client) GetActionsTokenPermissions(url string) (*types.TokenPermissions, error) {
req, err := c.client.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}

p := types.TokenPermissions{}
_, err = c.client.Do(c.context, req, &p)
if err != nil {
return nil, err
}
return &p, nil
}

type samlError struct {
organization string
}
Expand Down
6 changes: 6 additions & 0 deletions internal/clients/github/types/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package types

type TokenPermissions struct {
DefaultWorkflowPermissions *string `json:"default_workflow_permissions,omitempty"`
CanApprovePullRequestReviews *bool `json:"can_approve_pull_request_reviews,omitempty"`
}
2 changes: 2 additions & 0 deletions internal/collected/github/organization_actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package githubcollected

import (
"fmt"
"github.com/Legit-Labs/legitify/internal/clients/github/types"

"github.com/google/go-github/v44/github"
)

type OrganizationActions struct {
Organization ExtendedOrg `json:"organization"`
ActionsPermissions *github.ActionsPermissions `json:"actions_permissions"`
TokenPermissions *types.TokenPermissions `json:"token_permissions"`
}

func (o OrganizationActions) ViolationEntityType() string {
Expand Down
14 changes: 8 additions & 6 deletions internal/collected/github/repository.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package githubcollected

import (
"github.com/Legit-Labs/legitify/internal/clients/github/types"
"github.com/Legit-Labs/legitify/internal/common/namespace"
"github.com/Legit-Labs/legitify/internal/scorecard"
"github.com/google/go-github/v44/github"
Expand Down Expand Up @@ -63,12 +64,13 @@ type GitHubQLBranch struct {
}

type Repository struct {
Repository *GitHubQLRepository `json:"repository"`
VulnerabilityAlertsEnabled *bool `json:"vulnerability_alerts_enabled"`
NoBranchProtectionPermission bool `json:"no_branch_protection_permission"`
Scorecard *scorecard.Result `json:"scorecard,omitempty"`
Hooks []*github.Hook `json:"hooks"`
Collaborators []*github.User `json:"collaborators"`
Repository *GitHubQLRepository `json:"repository"`
VulnerabilityAlertsEnabled *bool `json:"vulnerability_alerts_enabled"`
NoBranchProtectionPermission bool `json:"no_branch_protection_permission"`
Scorecard *scorecard.Result `json:"scorecard,omitempty"`
Hooks []*github.Hook `json:"hooks"`
Collaborators []*github.User `json:"collaborators"`
ActionsTokenPermissions *types.TokenPermissions `json:"actions_token_permissions"`
}

func (r Repository) ViolationEntityType() string {
Expand Down
10 changes: 5 additions & 5 deletions internal/collectors/actions_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

const (
orgActionPermEffect = "Cannot read organization actions"
orgActionPermEffect = "Cannot read organization actions settings"
)

type actionCollector struct {
Expand Down Expand Up @@ -56,12 +56,11 @@ func (c *actionCollector) Collect() subCollectorChannels {
return
}

c.totalCollectionChange(len(orgs))

for _, org := range orgs {
actionsData, _, err := c.client.Client().Organizations.GetActionsPermissions(c.context, org.Name())
actionsPermissions, err1 := c.client.GetActionsTokenPermissionsForOrganization(org.Name())
actionsData, _, err2 := c.client.Client().Organizations.GetActionsPermissions(c.context, org.Name())

if err != nil {
if err1 != nil || err2 != nil {
entityName := fmt.Sprintf("%s/%s", namespace.Organization, org.Name())
perm := newMissingPermission(permissions.OrgAdmin, entityName, orgActionPermEffect, namespace.Organization)
c.issueMissingPermissions(perm)
Expand All @@ -73,6 +72,7 @@ func (c *actionCollector) Collect() subCollectorChannels {
ghcollected.OrganizationActions{
Organization: org,
ActionsPermissions: actionsData,
TokenPermissions: actionsPermissions,
},
org.CanonicalLink(),
[]permissions.Role{org.Role})
Expand Down
17 changes: 17 additions & 0 deletions internal/collectors/repository_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ func (rc *repositoryCollector) collectExtraData(login string,
log.Printf("error getting repository collaborators for %s: %s", fullRepoName(login, repo.Repository.Name), err)
}

repo, err = rc.getActionsSettings(repo, login)
if err != nil {
log.Printf("error getting repository actions settings for %s: %s", fullRepoName(login, repo.Repository.Name), err)
}

if context.IsBranchProtectionSupported() {
repo, err = rc.fixBranchProtectionInfo(repo, login)
if err != nil {
Expand All @@ -298,6 +303,18 @@ func (rc *repositoryCollector) collectExtraData(login string,
return repo
}

func (rc *repositoryCollector) getActionsSettings(repo ghcollected.Repository, org string) (ghcollected.Repository, error) {
settings, err := rc.Client.GetActionsTokenPermissionsForRepository(org, repo.Name())
if err != nil {
perm := newMissingPermission(permissions.RepoAdmin, fullRepoName(org, repo.Repository.Name),
"Cannot read repository actions settings", namespace.Repository)
rc.issueMissingPermissions(perm)
return repo, err
}
repo.ActionsTokenPermissions = settings
return repo, nil
}

func (rc *repositoryCollector) getRepositoryHooks(repo ghcollected.Repository, org string) (ghcollected.Repository, error) {
var result []*github.Hook

Expand Down
42 changes: 42 additions & 0 deletions policies/github/actions.rego
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,46 @@ all_repositories_can_run_github_actions {
default all_github_actions_are_allowed = false
all_github_actions_are_allowed {
input.actions_permissions.allowed_actions == "all"
}

# METADATA
# scope: rule
# title: Default workflow token permission is not read only
# description: Your default GitHub Action workflow token permission is set to read-write. When creating workflow tokens, it is highly recommended to follow the Principle of Least Privilege and force workflow authors to specify explicitly which permissions they need.
# custom:
# requiredEnrichers: [organizationId]
# remediationSteps:
# - 1. Make sure you have admin permissions
# - 2. Go to the org's settings page
# - 3. Enter "Actions - General" tab
# - 4. Under 'Workflow permissions'
# - 5. Select 'Read repository contents permission'
# - 6. Click 'Save'
# severity: MEDIUM
# requiredScopes: [admin:org]
# threat: In case of token compromise (due to a vulnerability or malicious third-party GitHub actions), an attacker can use this token to sabotage various assets in your CI/CD pipeline, such as packages, pull-requests, deployments, and more.
default token_default_permissions_is_read_write = false
token_default_permissions_is_read_write {
input.token_permissions.default_workflow_permissions != "read"
}

# METADATA
# scope: rule
# title: Workflows Are Allowed To Approve Pull Requests
# description: Your default GitHub Actions configuration allows for workflows to approve pull requests. This could allow users to bypass code-review restrictions.
# custom:
# requiredEnrichers: [organizationId]
# remediationSteps:
# - 1. Make sure you have admin permissions
# - 2. Go to the org's settings page
# - 3. Enter "Actions - General" tab
# - 4. Under 'Workflow permissions'
# - 5. Uncheck 'Allow GitHub actions to create and approve pull requests.
# - 6. Click 'Save'
# severity: HIGH
# requiredScopes: [admin:org]
# threat: Attackers can exploit this misconfiguration to bypass code-review restrictions by creating a workflow that approves their own pull request and then merging the pull request without anyone noticing, introducing malicious code that would go straight ahead to production.
default actions_can_approve_pull_requests = false
actions_can_approve_pull_requests {
input.token_permissions.can_approve_pull_request_reviews
}
44 changes: 43 additions & 1 deletion policies/github/repository.rego
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,46 @@ default scorecard_score_too_low = false
scorecard_score_too_low {
not is_null(input.scorecard)
input.scorecard.score < 7.0
}
}

# METADATA
# scope: rule
# title: Default workflow token permission is not read only
# description: Your default GitHub Action workflow token permission is set to read-write. When creating workflow tokens, it is highly recommended to follow the Principle of Least Privilege and force workflow authors to specify explicitly which permissions they need.
# custom:
# requiredEnrichers: [organizationId]
# remediationSteps:
# - 1. Make sure you have admin permissions
# - 2. Go to the org's settings page
# - 3. Enter "Actions - General" tab
# - 4. Under 'Workflow permissions'
# - 5. Select 'Read repository contents permission'
# - 6. Click 'Save'
# severity: MEDIUM
# requiredScopes: [admin:org]
# threat: In case of token compromise (due to a vulnerability or malicious third-party GitHub actions), an attacker can use this token to sabotage various assets in your CI/CD pipeline, such as packages, pull-requests, deployments, and more.
default token_default_permissions_is_read_write = false
token_default_permissions_is_read_write {
input.actions_token_permissions.default_workflow_permissions != "read"
}

# METADATA
# scope: rule
# title: Workflows Are Allowed To Approve Pull Requests
# description: Your default GitHub Actions configuration allows for workflows to approve pull requests. This could allow users to bypass code-review restrictions.
# custom:
# requiredEnrichers: [organizationId]
# remediationSteps:
# - 1. Make sure you have admin permissions
# - 2. Go to the org's settings page
# - 3. Enter "Actions - General" tab
# - 4. Under 'Workflow permissions'
# - 5. Uncheck 'Allow GitHub actions to create and approve pull requests.
# - 6. Click 'Save'
# severity: HIGH
# requiredScopes: [admin:org]
# threat: Attackers can exploit this misconfiguration to bypass code-review restrictions by creating a workflow that approves their own pull request and then merging the pull request without anyone noticing, introducing malicious code that would go straight ahead to production.
default actions_can_approve_pull_requests = false
actions_can_approve_pull_requests {
input.actions_token_permissions.can_approve_pull_request_reviews
}
47 changes: 45 additions & 2 deletions test/actions_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package test

import (
"github.com/Legit-Labs/legitify/internal/clients/github/types"
"testing"

githubcollected "github.com/Legit-Labs/legitify/internal/collected/github"
Expand All @@ -9,8 +10,10 @@ import (
)

type organizationActionsMockConfiguration struct {
allowedActions *string
enabledRepositories *string
allowedActions *string
enabledRepositories *string
tokenDefaultPermission string
workflowsCanApprovePRs bool
}

func newOrganizationActionsMock(config organizationActionsMockConfiguration) githubcollected.OrganizationActions {
Expand All @@ -20,6 +23,10 @@ func newOrganizationActionsMock(config organizationActionsMockConfiguration) git
EnabledRepositories: config.enabledRepositories,
AllowedActions: config.allowedActions,
},
TokenPermissions: &types.TokenPermissions{
DefaultWorkflowPermissions: &config.tokenDefaultPermission,
CanApprovePullRequestReviews: &config.workflowsCanApprovePRs,
},
}
}

Expand Down Expand Up @@ -64,6 +71,42 @@ func TestActions(t *testing.T) {
enabledRepositories: &selected,
},
},
{
name: "actions can approve pull requests",
policyName: "actions_can_approve_pull_requests",
shouldBeViolated: true,
args: organizationActionsMockConfiguration{
enabledRepositories: &selected,
workflowsCanApprovePRs: true,
},
},
{
name: "actions can not approve pull requests",
policyName: "actions_can_approve_pull_requests",
shouldBeViolated: false,
args: organizationActionsMockConfiguration{
enabledRepositories: &selected,
workflowsCanApprovePRs: false,
},
},
{
name: "workflow token default permissions is not set to read only",
policyName: "token_default_permissions_is_read_write",
shouldBeViolated: true,
args: organizationActionsMockConfiguration{
enabledRepositories: &selected,
tokenDefaultPermission: "write",
},
},
{
name: "workflow token default permissions is set to read only",
policyName: "token_default_permissions_is_read_write",
shouldBeViolated: false,
args: organizationActionsMockConfiguration{
enabledRepositories: &selected,
tokenDefaultPermission: "read",
},
},
}

for _, test := range tests {
Expand Down

0 comments on commit aefbe88

Please sign in to comment.