diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ffd530b --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# 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\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\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) + +# Bundle the policies into a tarball for OCI registry +clean: # Cleanup build artifacts + @rm -rf dist/* + +build: clean ## Build the policy bundle + @mkdir -p dist/ + @go build -o dist/plugin main.go \ No newline at end of file diff --git a/main.go b/main.go index b4fe349..ceb98f5 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "slices" + policyManager "github.com/compliance-framework/agent/policy-manager" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" @@ -11,25 +13,28 @@ import ( "github.com/hashicorp/go-hclog" goplugin "github.com/hashicorp/go-plugin" "github.com/mitchellh/mapstructure" - "slices" ) type PluginConfig struct { - Token string `mapstructure:"token"` - Organization *string `mapstructure:"organization"` - User *string `mapstructure:"user"` + Token string `mapstructure:"token"` + Organization *string `mapstructure:"organization"` + User *string `mapstructure:"user"` + SecurityTeamName *string `mapstructure:"security-team-name"` } type DependabotPlugin struct { logger hclog.Logger - data map[string]interface{} - config *PluginConfig + config *PluginConfig githubClient *github.Client } +type DependabotData struct { + Alerts []*github.DependabotAlert + SecurityTeamMembers []*github.User +} + func (l *DependabotPlugin) Configure(req *proto.ConfigureRequest) (*proto.ConfigureResponse, error) { - //l.config = req.GetConfig() config := &PluginConfig{} mapstructure.Decode(req.GetConfig(), config) l.config = config @@ -42,7 +47,20 @@ func (l *DependabotPlugin) Configure(req *proto.ConfigureRequest) (*proto.Config func (l *DependabotPlugin) Eval(req *proto.EvalRequest, apiHelper runner.ApiHelper) (*proto.EvalResponse, error) { ctx := context.TODO() repochan, errchan := l.FetchRepositories(ctx) + + var securityTeamMembers []*github.User + if l.config.SecurityTeamName != nil && *l.config.SecurityTeamName != "" { + var err error + securityTeamMembers, err = l.FetchSecurityTeamMembers(ctx) + if err != nil { + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } + } + done := false + for !done { select { case err, ok := <-errchan: @@ -66,7 +84,14 @@ func (l *DependabotPlugin) Eval(req *proto.EvalRequest, apiHelper runner.ApiHelp }, err } - evidences, err := l.EvaluatePolicies(ctx, repo, alerts, req) + data := &DependabotData{ + Alerts: alerts, + } + if securityTeamMembers != nil { + data.SecurityTeamMembers = securityTeamMembers + } + + evidences, err := l.EvaluatePolicies(ctx, repo, data, req) if err != nil { return &proto.EvalResponse{ Status: proto.ExecutionStatus_FAILURE, @@ -87,6 +112,14 @@ func (l *DependabotPlugin) Eval(req *proto.EvalRequest, apiHelper runner.ApiHelp }, nil } +func (l *DependabotPlugin) FetchSecurityTeamMembers(ctx context.Context) ([]*github.User, error) { + members, _, err := l.githubClient.Teams.ListTeamMembersBySlug(ctx, *l.config.Organization, *l.config.SecurityTeamName, nil) + if err != nil { + return nil, err + } + return members, nil +} + func (l *DependabotPlugin) FetchRepositoryDependabotAlerts(ctx context.Context, repo *github.Repository) ([]*github.DependabotAlert, error) { alerts, _, err := l.githubClient.Dependabot.ListRepoAlerts(ctx, repo.GetOwner().GetLogin(), repo.GetName(), &github.ListAlertsOptions{ ListOptions: github.ListOptions{ @@ -140,7 +173,7 @@ func (l *DependabotPlugin) FetchRepositories(ctx context.Context) (<-chan *githu return repositories, errs } -func (l *DependabotPlugin) EvaluatePolicies(ctx context.Context, repo *github.Repository, alerts []*github.DependabotAlert, req *proto.EvalRequest) ([]*proto.Evidence, error) { +func (l *DependabotPlugin) EvaluatePolicies(ctx context.Context, repo *github.Repository, data *DependabotData, req *proto.EvalRequest) ([]*proto.Evidence, error) { var accumulatedErrors error activities := make([]*proto.Activity, 0) @@ -260,7 +293,7 @@ func (l *DependabotPlugin) EvaluatePolicies(ctx context.Context, repo *github.Re actors, activities, ) - evidence, err := processor.GenerateResults(ctx, policyPath, alerts) + evidence, err := processor.GenerateResults(ctx, policyPath, data) evidences = slices.Concat(evidences, evidence) if err != nil { accumulatedErrors = errors.Join(accumulatedErrors, err) diff --git a/main_integration_test.go b/main_integration_test.go index f2803c7..b21635b 100644 --- a/main_integration_test.go +++ b/main_integration_test.go @@ -4,15 +4,15 @@ package main import ( "context" + "os" + "testing" + policy_manager "github.com/compliance-framework/agent/policy-manager" "github.com/google/go-github/v71/github" "github.com/hashicorp/go-hclog" - "os" - "testing" ) func TestDependabotPlugin_Integration_FetchRepositories(t *testing.T) { - ctx := context.Background() plugin := DependabotPlugin{ @@ -76,3 +76,23 @@ func TestDependabotPlugin_Integration_FetchRepositoryDependabotAlerts(t *testing t.Log("Successfully collected alerts", len(alerts)) } + +func TestDependabotPlugin_Integration_FetchTeamMembers(t *testing.T) { + ctx := context.Background() + + plugin := DependabotPlugin{ + logger: hclog.NewNullLogger(), + config: &PluginConfig{ + Token: os.Getenv("GITHUB_TOKEN"), + Organization: policy_manager.Pointer("compliance-framework"), + SecurityTeamName: policy_manager.Pointer("security"), + }, + githubClient: github.NewClient(nil).WithAuthToken(os.Getenv("GITHUB_TOKEN")), + } + + members, err := plugin.FetchSecurityTeamMembers(ctx) + if err != nil { + t.Error(err) + } + t.Log("Successfully collected security team members", members) +}