Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI catalog characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php

# Check if OPA CLI is installed
OPA := $(shell command -v opa 2> /dev/null)
ifeq ($(OPA),)
$(error "opa CLI not found. Please install it: https://www.openpolicyagent.org/docs/latest/cli/")
endif

##@ Help
help: ## Display this concise help, ie only the porcelain target
@awk 'BEGIN {FS = ":.*##"; printf "\033[1mUsage\033[0m\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-30s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

help-all: ## Display all help items, ie including plumbing targets
@awk 'BEGIN {FS = ":.*#"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?#/ { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^#@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Policies
test: ## Test policy files
@opa test examples

validate: ## Validate policy files
@opa check examples

clean: # Cleanup build artifacts
@rm -rf dist/*

# Bundle the policies into a tarball for OCI registry
build: clean ## Build the policy bundle
@mkdir -p dist/
@opa build -b examples -o dist/bundle.tar.gz
25 changes: 8 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Compliance Framework Plugin For Github

This is the individual plugin for polling github settings for organizations and repositories to test for configuration flags that are going to fail compliance checks.
This is the individual plugin for polling github settings for organizations and repositories to test for configuration flags that are going to fail compliance checks.

For the moment, it is solely limited to authenticated Github organizations with a Github PAT, but in the future it should query security plans & repositories for specific settings
For the moment, it is solely limited to authenticated Github organizations with a Github PAT, but in the future it should query security plans & repositories for specific settings

## Prerequisites

* GoReleaser https://goreleaser.com/install/
* Github Fine Grain Personal Access Token with the following scopes:
* `read:org` for the organization to be queried. Note - you *might* need to be an administrator of the GH Org to work correctly

- GoReleaser https://goreleaser.com/install/
- Github Fine Grain Personal Access Token with the following scopes:
- `read:org` for the organization to be queried. Note - you _might_ need to be an administrator of the GH Org to work correctly
- `read:members` to be able to read teams

## Building

Expand Down Expand Up @@ -40,13 +40,12 @@ export CCF_PLUGINS_GITHUB_CONFIG_TOKEN="github_pat_1234..."
```

```yaml
...
---
plugins:
github:
config:
token: "" # Will be read from the CCF_PLUGINS_GITHUB_CONFIG_TOKEN environment variable
organization: test-org # The name of the organization
...
organization: test-org # The name of the organization
```

## Releasing
Expand All @@ -62,11 +61,3 @@ You can find the OCI implementations in the GitHub Packages page.
```shell
concom agent --plugin=https://github.com/compliance-framework/plugin-template/releases/tag/0.0.1
```

## Todo

- [X] Pull Organization settings as an authenticated user
- [ ] Pull repository information for the listed Organization
- [ ] Populate Security Plans and map them to the repositories to ensure that settings are enabled
- [ ] Sensible defaults for the configuration
- [ ] Better error handling for sending issues back to the agent
2 changes: 1 addition & 1 deletion examples/policies/gh_org_mfa_enabled.rego
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ violation[{
"description": "Two factor authentication should be enabled and enforced for all users within the Github Organization to make it harder for malicious actors to gain access to the organizations settings and repositories & settings",
"remarks": "More information from Github can be found here: https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-two-factor-authentication-for-your-organization/requiring-two-factor-authentication-in-your-organization"
}] if {
input.organization.two_factor_requirement_enabled == false
input.settings.two_factor_requirement_enabled == false
}
36 changes: 34 additions & 2 deletions internal/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package internal

import (
"context"

policy_manager "github.com/compliance-framework/agent/policy-manager"

"github.com/compliance-framework/agent/runner/proto"
"github.com/google/go-github/v71/github"
"github.com/hashicorp/go-hclog"
)

type GithubData struct {
Settings *github.Organization `json:"settings"`
Teams []*github.Team `json:"teams"`
}

type DataFetcher struct {
logger hclog.Logger
client *github.Client
Expand All @@ -21,7 +27,7 @@ func NewDataFetcher(logger hclog.Logger, client *github.Client) *DataFetcher {
}
}

func (df DataFetcher) FetchData(ctx context.Context, organization string) (*github.Organization, []*proto.Step, error) {
func (df DataFetcher) FetchData(ctx context.Context, organization string) (*GithubData, []*proto.Step, error) {
steps := make([]*proto.Step, 0)

steps = append(steps, &proto.Step{
Expand All @@ -35,11 +41,37 @@ func (df DataFetcher) FetchData(ctx context.Context, organization string) (*gith
Remarks: policy_manager.Pointer("More information about data being sent back can be found here: https://docs.github.com/en/rest/orgs/orgs?apiVersion=2022-11-28#get-an-organization"),
})

steps = append(steps, &proto.Step{
Title: "Get Teams",
Description: "Using the client's native APIs, Get all the information from the teams endpoint",
Remarks: policy_manager.Pointer("More information about data being sent back can be found here: https://docs.github.com/en/rest/teams/teams?apiVersion=2022-11-28#list-teams"),
})

org, _, err := df.client.Organizations.Get(ctx, organization)
if err != nil {
df.logger.Error("Error getting organization information", "org", organization, "error", err)
return nil, nil, err
}

return org, steps, nil
var allTeams []*github.Team
paginationOpt := &github.ListOptions{PerPage: 100}

for {
teams, resp, err := df.client.Teams.ListTeams(ctx, organization, paginationOpt)
if err != nil {
df.logger.Error("Error getting teams information", "org", organization, "error", err)
return nil, nil, err
}

allTeams = append(allTeams, teams...)
if resp.NextPage == 0 {
break
}
paginationOpt.Page = resp.NextPage
}

return &GithubData{
Settings: org,
Teams: allTeams,
}, steps, nil
}
19 changes: 9 additions & 10 deletions internal/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"github.com/google/go-github/v71/github"
"slices"

policyManager "github.com/compliance-framework/agent/policy-manager"
Expand Down Expand Up @@ -34,7 +33,7 @@ func (pe *PolicyEvaluator) GetEvidences() []*proto.Evidence {

// Eval is used to run policies against the data you've collected. You could also consider an
// `EvalAndSend` by passing in the `apiHelper` that sends the observations directly to the API.
func (pe *PolicyEvaluator) Eval(organization *github.Organization, policyPaths []string) (proto.ExecutionStatus, error) {
func (pe *PolicyEvaluator) Eval(data *GithubData, policyPaths []string) (proto.ExecutionStatus, error) {
var accumulatedErrors error

evalStatus := proto.ExecutionStatus_SUCCESS
Expand Down Expand Up @@ -97,22 +96,22 @@ func (pe *PolicyEvaluator) Eval(organization *github.Organization, policyPaths [

inventory := []*proto.InventoryItem{
{
Identifier: fmt.Sprintf("github-organization/%s", organization.GetLogin()),
Identifier: fmt.Sprintf("github-organization/%s", data.Settings.GetLogin()),
Type: "github-organization",
Title: fmt.Sprintf("Github Organization [%s]", organization.GetName()),
Title: fmt.Sprintf("Github Organization [%s]", data.Settings.GetName()),
Props: []*proto.Property{
{
Name: "name",
Value: organization.GetName(),
Value: data.Settings.GetName(),
},
{
Name: "path",
Value: organization.GetLogin(),
Value: data.Settings.GetLogin(),
},
},
Links: []*proto.Link{
{
Href: organization.GetURL(),
Href: data.Settings.GetURL(),
Text: policyManager.Pointer("Organization URL"),
},
},
Expand All @@ -130,7 +129,7 @@ func (pe *PolicyEvaluator) Eval(organization *github.Organization, policyPaths [
subjects := []*proto.Subject{
{
Type: proto.SubjectType_SUBJECT_TYPE_INVENTORY_ITEM,
Identifier: fmt.Sprintf("github-organization/%s", organization.GetLogin()),
Identifier: fmt.Sprintf("github-organization/%s", data.Settings.GetLogin()),
},
{
Type: proto.SubjectType_SUBJECT_TYPE_COMPONENT,
Expand All @@ -155,15 +154,15 @@ func (pe *PolicyEvaluator) Eval(organization *github.Organization, policyPaths [
map[string]string{
"provider": "github",
"type": "organization",
"organization": organization.GetLogin(),
"organization": data.Settings.GetLogin(),
},
subjects,
components,
inventory,
actors,
activities,
)
evidence, err := processor.GenerateResults(pe.ctx, policyPath, organization)
evidence, err := processor.GenerateResults(pe.ctx, policyPath, data)
evidences = slices.Concat(evidences, evidence)
if err != nil {
accumulatedErrors = errors.Join(accumulatedErrors, err)
Expand Down
128 changes: 65 additions & 63 deletions internal/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,85 +6,87 @@ import (
"testing"

"github.com/compliance-framework/agent/runner/proto"
"github.com/google/go-github/v71/github"
"github.com/hashicorp/go-hclog"
)

var test_org_data = `
var test_response = `
{
"login": "test-org",
"id": 1234567,
"node_id": "O_abcdefg",
"url": "https://api.github.com/orgs/test-org",
"repos_url": "https://api.github.com/orgs/test-org/repos",
"events_url": "https://api.github.com/orgs/test-org/events",
"hooks_url": "https://api.github.com/orgs/test-org/hooks",
"issues_url": "https://api.github.com/orgs/test-org/issues",
"members_url": "https://api.github.com/orgs/test-org/members{/member}",
"public_members_url": "https://api.github.com/orgs/test-org/public_members{/member}",
"avatar_url": "https://avatars.githubusercontent.com/u/1234567?v=4",
"description": null,
"is_verified": false,
"has_organization_projects": true,
"has_repository_projects": true,
"public_repos": 0,
"public_gists": 0,
"followers": 0,
"following": 0,
"html_url": "https://github.com/test-org",
"created_at": "2025-04-09T15:36:21Z",
"updated_at": "2025-04-09T15:38:25Z",
"archived_at": null,
"type": "Organization",
"total_private_repos": 0,
"owned_private_repos": 0,
"private_gists": 0,
"disk_usage": 0,
"collaborators": 0,
"billing_email": "test@example.com",
"default_repository_permission": "read",
"members_can_create_repositories": true,
"two_factor_requirement_enabled": false,
"members_allowed_repository_creation_type": "all",
"members_can_create_public_repositories": true,
"members_can_create_private_repositories": true,
"members_can_create_internal_repositories": false,
"members_can_create_pages": true,
"members_can_fork_private_repositories": false,
"web_commit_signoff_required": false,
"deploy_keys_enabled_for_repositories": false,
"members_can_create_public_pages": true,
"members_can_create_private_pages": true,
"plan": {
"name": "free",
"space": 976562499,
"private_repos": 10000,
"filled_seats": 2,
"seats": 1
"settings": {
"login": "test-org",
"id": 1234567,
"node_id": "O_abcdefg",
"url": "https://api.github.com/orgs/test-org",
"repos_url": "https://api.github.com/orgs/test-org/repos",
"events_url": "https://api.github.com/orgs/test-org/events",
"hooks_url": "https://api.github.com/orgs/test-org/hooks",
"issues_url": "https://api.github.com/orgs/test-org/issues",
"members_url": "https://api.github.com/orgs/test-org/members{/member}",
"public_members_url": "https://api.github.com/orgs/test-org/public_members{/member}",
"avatar_url": "https://avatars.githubusercontent.com/u/1234567?v=4",
"description": null,
"is_verified": false,
"has_organization_projects": true,
"has_repository_projects": true,
"public_repos": 0,
"public_gists": 0,
"followers": 0,
"following": 0,
"html_url": "https://github.com/test-org",
"created_at": "2025-04-09T15:36:21Z",
"updated_at": "2025-04-09T15:38:25Z",
"archived_at": null,
"type": "Organization",
"total_private_repos": 0,
"owned_private_repos": 0,
"private_gists": 0,
"disk_usage": 0,
"collaborators": 0,
"billing_email": "test@example.com",
"default_repository_permission": "read",
"members_can_create_repositories": true,
"two_factor_requirement_enabled": false,
"members_allowed_repository_creation_type": "all",
"members_can_create_public_repositories": true,
"members_can_create_private_repositories": true,
"members_can_create_internal_repositories": false,
"members_can_create_pages": true,
"members_can_fork_private_repositories": false,
"web_commit_signoff_required": false,
"deploy_keys_enabled_for_repositories": false,
"members_can_create_public_pages": true,
"members_can_create_private_pages": true,
"plan": {
"name": "free",
"space": 976562499,
"private_repos": 10000,
"filled_seats": 2,
"seats": 1
},
"advanced_security_enabled_for_new_repositories": false,
"dependabot_alerts_enabled_for_new_repositories": false,
"dependabot_security_updates_enabled_for_new_repositories": false,
"dependency_graph_enabled_for_new_repositories": false,
"secret_scanning_enabled_for_new_repositories": false,
"secret_scanning_push_protection_enabled_for_new_repositories": false,
"secret_scanning_push_protection_custom_link_enabled": false,
"secret_scanning_push_protection_custom_link": null,
"secret_scanning_validity_checks_enabled": false
},
"advanced_security_enabled_for_new_repositories": false,
"dependabot_alerts_enabled_for_new_repositories": false,
"dependabot_security_updates_enabled_for_new_repositories": false,
"dependency_graph_enabled_for_new_repositories": false,
"secret_scanning_enabled_for_new_repositories": false,
"secret_scanning_push_protection_enabled_for_new_repositories": false,
"secret_scanning_push_protection_custom_link_enabled": false,
"secret_scanning_push_protection_custom_link": null,
"secret_scanning_validity_checks_enabled": false
"teams": []
}
`

func TestGithubOrg_EvaluatePolicies(t *testing.T) {
logger := hclog.NewNullLogger()
steps := make([]*proto.Activity, 0)

organization := &github.Organization{}
_ = json.Unmarshal([]byte(test_org_data), organization)
settings := &GithubData{}
_ = json.Unmarshal([]byte(test_response), settings)

ctx := context.TODO()

evaluator := NewPolicyEvaluator(ctx, logger, steps)
status, err := evaluator.Eval(organization, []string{"../examples/policies"})
status, err := evaluator.Eval(settings, []string{"../examples/policies"})

if status != proto.ExecutionStatus_SUCCESS {
t.Fail()
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"

"github.com/compliance-framework/agent/runner"
"github.com/compliance-framework/agent/runner/proto"
"github.com/compliance-framework/plugin-github-settings/internal"
Expand Down