From fd8e17c41dfd28c738ee85d0a18aaade0c590ba7 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 08:45:39 -0300 Subject: [PATCH 1/6] feat: add more checks to github repositories Signed-off-by: Gustavo Carvalho --- main.go | 83 +++++++++++- repository_controls.go | 285 +++++++++++++++++++++++++++++++++++++++++ types.go | 54 ++++++++ 3 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 repository_controls.go diff --git a/main.go b/main.go index ccdb62a..283f258 100644 --- a/main.go +++ b/main.go @@ -113,6 +113,11 @@ type SaturatedRepository struct { CodeOwners *github.RepositoryContent `json:"code_owners"` OrgTeams []*OrgTeam `json:"org_teams"` Deployments []*DeploymentWithStatuses `json:"deployments"` + FailedDeployments []*DeploymentWithStatuses `json:"failed_deployments"` + Collaborators []*RepositoryCollaborator `json:"collaborators"` + RepositoryTeams []*RepositoryTeam `json:"repository_teams"` + Environments []*RepositoryEnvironment `json:"environments"` + EffectiveBranchRules map[string]*BranchRuleEvidence `json:"effective_branch_rules"` } type GithubReposPlugin struct { @@ -309,6 +314,41 @@ func (l *GithubReposPlugin) Eval(req *proto.EvalRequest, apiHelper runner.ApiHel Status: proto.ExecutionStatus_FAILURE, }, err } + failedDeployments, err := l.FetchFailedDeploymentsWithStatuses(ctx, repo) + if err != nil { + l.Logger.Error("error gathering failed deployments", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } + collaborators, err := l.GatherRepositoryCollaborators(ctx, repo) + if err != nil { + l.Logger.Error("error gathering repository collaborators", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } + repositoryTeams, err := l.GatherRepositoryTeams(ctx, repo, orgTeams) + if err != nil { + l.Logger.Error("error gathering repository teams", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } + environments, err := l.GatherRepositoryEnvironments(ctx, repo) + if err != nil { + l.Logger.Error("error gathering repository environments", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } + effectiveBranchRules, err := l.GatherEffectiveBranchRules(ctx, repo, branchNames) + if err != nil { + l.Logger.Error("error gathering effective branch rules", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } data := &SaturatedRepository{ Settings: repo, Workflows: workflows, @@ -322,6 +362,11 @@ func (l *GithubReposPlugin) Eval(req *proto.EvalRequest, apiHelper runner.ApiHel CodeOwners: codeOwners, OrgTeams: orgTeams, Deployments: deployments, + FailedDeployments: failedDeployments, + Collaborators: collaborators, + RepositoryTeams: repositoryTeams, + Environments: environments, + EffectiveBranchRules: effectiveBranchRules, } // Uncomment to check the data that is being passed through from // the client, as data formats are often slightly different than @@ -450,6 +495,26 @@ func (l *GithubReposPlugin) GatherWorkflowRuns(ctx context.Context, repo *github } func (l *GithubReposPlugin) FetchDeploymentsWithStatuses(ctx context.Context, repo *github.Repository) ([]*DeploymentWithStatuses, error) { + return l.fetchDeploymentsWithStatuses(ctx, repo, false) +} + +func (l *GithubReposPlugin) FetchFailedDeploymentsWithStatuses(ctx context.Context, repo *github.Repository) ([]*DeploymentWithStatuses, error) { + deployments, err := l.fetchDeploymentsWithStatuses(ctx, repo, true) + if err != nil { + return nil, err + } + + var failed []*DeploymentWithStatuses + for _, deployment := range deployments { + if deploymentHasFailed(deployment) { + failed = append(failed, deployment) + } + } + + return failed, nil +} + +func (l *GithubReposPlugin) fetchDeploymentsWithStatuses(ctx context.Context, repo *github.Repository, includeSkipped bool) ([]*DeploymentWithStatuses, error) { owner := repo.GetOwner().GetLogin() name := repo.GetName() @@ -486,7 +551,7 @@ func (l *GithubReposPlugin) FetchDeploymentsWithStatuses(ctx context.Context, re } // Check if deployment should be filtered based on status - if l.shouldSkipDeployment(deployment, statuses) { + if !includeSkipped && l.shouldSkipDeployment(deployment, statuses) { continue } @@ -506,6 +571,22 @@ func (l *GithubReposPlugin) FetchDeploymentsWithStatuses(ctx context.Context, re return deploymentsWithStatuses, nil } +func deploymentHasFailed(deployment *DeploymentWithStatuses) bool { + if deployment == nil { + return false + } + for _, status := range deployment.Statuses { + if status == nil { + continue + } + state := status.GetState() + if state == "failure" || state == "error" { + return true + } + } + return false +} + // shouldSkipDeployment determines if a deployment should be filtered out based on configuration func (l *GithubReposPlugin) shouldSkipDeployment(deployment *github.Deployment, statuses []*github.DeploymentStatus) bool { if len(statuses) == 0 { diff --git a/repository_controls.go b/repository_controls.go new file mode 100644 index 0000000..bd29118 --- /dev/null +++ b/repository_controls.go @@ -0,0 +1,285 @@ +package main + +import ( + "context" + + "github.com/google/go-github/v71/github" +) + +func (l *GithubReposPlugin) GatherRepositoryCollaborators(ctx context.Context, repo *github.Repository) ([]*RepositoryCollaborator, error) { + owner := repo.GetOwner().GetLogin() + name := repo.GetName() + opts := &github.ListCollaboratorsOptions{ + Affiliation: "direct", + ListOptions: github.ListOptions{PerPage: 100, Page: 1}, + } + + var collaborators []*RepositoryCollaborator + for { + users, resp, err := l.githubClient.Repositories.ListCollaborators(ctx, owner, name, opts) + if err != nil { + if isPermissionError(err) { + l.Logger.Debug("Repository collaborators fetch skipped due to permissions", "repo", repo.GetFullName(), "error", err) + return nil, nil + } + return nil, err + } + + for _, user := range users { + if user == nil { + continue + } + collaborators = append(collaborators, &RepositoryCollaborator{ + Login: user.GetLogin(), + RoleName: user.GetRoleName(), + Permissions: copyPermissions(user.GetPermissions()), + }) + } + + if resp == nil || resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + + return collaborators, nil +} + +func (l *GithubReposPlugin) GatherRepositoryTeams(ctx context.Context, repo *github.Repository, orgTeams []*OrgTeam) ([]*RepositoryTeam, error) { + owner := repo.GetOwner().GetLogin() + name := repo.GetName() + opts := &github.ListOptions{PerPage: 100, Page: 1} + + var teams []*RepositoryTeam + for { + ghTeams, resp, err := l.githubClient.Repositories.ListTeams(ctx, owner, name, opts) + if err != nil { + if isPermissionError(err) { + l.Logger.Debug("Repository teams fetch skipped due to permissions", "repo", repo.GetFullName(), "error", err) + return nil, nil + } + return nil, err + } + + for _, team := range ghTeams { + if team == nil { + continue + } + teams = append(teams, &RepositoryTeam{ + ID: team.GetID(), + Name: team.GetName(), + Slug: team.GetSlug(), + Permission: team.GetPermission(), + Permissions: copyPermissions(team.GetPermissions()), + Members: membersForOrgTeam(orgTeams, team.GetSlug()), + }) + } + + if resp == nil || resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + + return teams, nil +} + +func (l *GithubReposPlugin) GatherRepositoryEnvironments(ctx context.Context, repo *github.Repository) ([]*RepositoryEnvironment, error) { + owner := repo.GetOwner().GetLogin() + name := repo.GetName() + opts := &github.EnvironmentListOptions{ListOptions: github.ListOptions{PerPage: 100, Page: 1}} + + var environments []*RepositoryEnvironment + for { + envResp, resp, err := l.githubClient.Repositories.ListEnvironments(ctx, owner, name, opts) + if err != nil { + if isPermissionError(err) { + l.Logger.Debug("Repository environments fetch skipped due to permissions", "repo", repo.GetFullName(), "error", err) + return nil, nil + } + return nil, err + } + + if envResp != nil { + for _, env := range envResp.Environments { + if env == nil { + continue + } + detail, _, err := l.githubClient.Repositories.GetEnvironment(ctx, owner, name, env.GetName()) + if err != nil { + l.Logger.Trace("Repository environment detail fetch failed", "repo", repo.GetFullName(), "environment", env.GetName(), "error", err) + detail = env + } + environments = append(environments, repositoryEnvironmentFromGitHub(detail)) + } + } + + if resp == nil || resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + + return environments, nil +} + +func (l *GithubReposPlugin) GatherEffectiveBranchRules(ctx context.Context, repo *github.Repository, branches []string) (map[string]*BranchRuleEvidence, error) { + owner := repo.GetOwner().GetLogin() + name := repo.GetName() + targets := map[string]struct{}{} + for _, branch := range branches { + if branch != "" { + targets[branch] = struct{}{} + } + } + if def := repo.GetDefaultBranch(); def != "" { + targets[def] = struct{}{} + } + + evidence := make(map[string]*BranchRuleEvidence, len(targets)) + for branch := range targets { + rules, _, err := l.githubClient.Repositories.GetRulesForBranch(ctx, owner, name, branch) + if err != nil { + l.Logger.Trace("Effective branch rules fetch failed", "repo", repo.GetFullName(), "branch", branch, "error", err) + evidence[branch] = &BranchRuleEvidence{} + continue + } + evidence[branch] = branchRuleEvidenceFromGitHub(rules) + } + + return evidence, nil +} + +func branchRuleEvidenceFromGitHub(rules *github.BranchRules) *BranchRuleEvidence { + evidence := &BranchRuleEvidence{} + if rules == nil { + return evidence + } + + evidence.RequiredSignatures = len(rules.RequiredSignatures) > 0 + for _, rule := range rules.RequiredDeployments { + if rule == nil { + continue + } + evidence.RequiredDeployments = append(evidence.RequiredDeployments, rule.Parameters.RequiredDeploymentEnvironments...) + } + for _, rule := range rules.CodeScanning { + if rule == nil { + continue + } + for _, tool := range rule.Parameters.CodeScanningTools { + if tool == nil { + continue + } + evidence.CodeScanningTools = append(evidence.CodeScanningTools, tool.Tool) + } + } + + return evidence +} + +func repositoryEnvironmentFromGitHub(env *github.Environment) *RepositoryEnvironment { + if env == nil { + return nil + } + + out := &RepositoryEnvironment{ + ID: env.GetID(), + Name: env.GetName(), + URL: env.GetURL(), + HTMLURL: env.GetHTMLURL(), + WaitTimer: env.GetWaitTimer(), + CanAdminsBypass: env.GetCanAdminsBypass(), + Reviewers: reviewersFromEnvReviewers(env.Reviewers), + } + if env.DeploymentBranchPolicy != nil { + out.DeploymentBranchPolicy = &EnvironmentBranchPolicy{ + ProtectedBranches: env.DeploymentBranchPolicy.GetProtectedBranches(), + CustomBranchPolicies: env.DeploymentBranchPolicy.GetCustomBranchPolicies(), + } + } + for _, rule := range env.ProtectionRules { + out.ProtectionRules = append(out.ProtectionRules, protectionRuleFromGitHub(rule)) + } + + return out +} + +func protectionRuleFromGitHub(rule *github.ProtectionRule) *EnvironmentProtectionRule { + if rule == nil { + return nil + } + + out := &EnvironmentProtectionRule{ + ID: rule.GetID(), + Type: rule.GetType(), + WaitTimer: rule.GetWaitTimer(), + PreventSelfReview: rule.GetPreventSelfReview(), + } + for _, reviewer := range rule.Reviewers { + out.Reviewers = append(out.Reviewers, reviewerFromRequiredReviewer(reviewer)) + } + + return out +} + +func reviewersFromEnvReviewers(reviewers []*github.EnvReviewers) []*EnvironmentReviewer { + out := make([]*EnvironmentReviewer, 0, len(reviewers)) + for _, reviewer := range reviewers { + if reviewer == nil { + continue + } + out = append(out, &EnvironmentReviewer{ + Type: reviewer.GetType(), + ID: reviewer.GetID(), + }) + } + return out +} + +func reviewerFromRequiredReviewer(reviewer *github.RequiredReviewer) *EnvironmentReviewer { + if reviewer == nil { + return nil + } + + out := &EnvironmentReviewer{Type: reviewer.GetType()} + switch r := reviewer.Reviewer.(type) { + case *github.User: + out.ID = r.GetID() + out.Login = r.GetLogin() + out.Name = r.GetName() + case github.User: + out.ID = r.GetID() + out.Login = r.GetLogin() + out.Name = r.GetName() + case *github.Team: + out.ID = r.GetID() + out.Slug = r.GetSlug() + out.Name = r.GetName() + case github.Team: + out.ID = r.GetID() + out.Slug = r.GetSlug() + out.Name = r.GetName() + } + return out +} + +func membersForOrgTeam(orgTeams []*OrgTeam, slug string) []string { + for _, team := range orgTeams { + if team != nil && team.Slug == slug { + return append([]string{}, team.Members...) + } + } + return nil +} + +func copyPermissions(in map[string]bool) map[string]bool { + if len(in) == 0 { + return nil + } + out := make(map[string]bool, len(in)) + for key, value := range in { + out[key] = value + } + return out +} diff --git a/types.go b/types.go index 8c1388c..b2d367b 100644 --- a/types.go +++ b/types.go @@ -49,3 +49,57 @@ type OrgTeam struct { Slug string `json:"slug"` Members []string `json:"members"` } + +type RepositoryCollaborator struct { + Login string `json:"login"` + RoleName string `json:"role_name"` + Permissions map[string]bool `json:"permissions"` +} + +type RepositoryTeam struct { + ID int64 `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Permission string `json:"permission"` + Permissions map[string]bool `json:"permissions"` + Members []string `json:"members"` +} + +type EnvironmentReviewer struct { + Type string `json:"type"` + ID int64 `json:"id,omitempty"` + Login string `json:"login,omitempty"` + Slug string `json:"slug,omitempty"` + Name string `json:"name,omitempty"` +} + +type EnvironmentProtectionRule struct { + ID int64 `json:"id"` + Type string `json:"type"` + WaitTimer int `json:"wait_timer,omitempty"` + PreventSelfReview bool `json:"prevent_self_review"` + Reviewers []*EnvironmentReviewer `json:"reviewers,omitempty"` +} + +type EnvironmentBranchPolicy struct { + ProtectedBranches bool `json:"protected_branches"` + CustomBranchPolicies bool `json:"custom_branch_policies"` +} + +type RepositoryEnvironment struct { + ID int64 `json:"id"` + Name string `json:"name"` + URL string `json:"url,omitempty"` + HTMLURL string `json:"html_url,omitempty"` + WaitTimer int `json:"wait_timer,omitempty"` + CanAdminsBypass bool `json:"can_admins_bypass"` + Reviewers []*EnvironmentReviewer `json:"reviewers,omitempty"` + ProtectionRules []*EnvironmentProtectionRule `json:"protection_rules,omitempty"` + DeploymentBranchPolicy *EnvironmentBranchPolicy `json:"deployment_branch_policy,omitempty"` +} + +type BranchRuleEvidence struct { + RequiredSignatures bool `json:"required_signatures"` + RequiredDeployments []string `json:"required_deployments,omitempty"` + CodeScanningTools []string `json:"code_scanning_tools,omitempty"` +} From 89a6ec9eeb704598cf85186297a6c0dee1e2701e Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 09:01:57 -0300 Subject: [PATCH 2/6] fix: copilot issues Signed-off-by: Gustavo Carvalho --- repository_controls.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/repository_controls.go b/repository_controls.go index bd29118..d844edb 100644 --- a/repository_controls.go +++ b/repository_controls.go @@ -140,6 +140,10 @@ func (l *GithubReposPlugin) GatherEffectiveBranchRules(ctx context.Context, repo for branch := range targets { rules, _, err := l.githubClient.Repositories.GetRulesForBranch(ctx, owner, name, branch) if err != nil { + if isPermissionError(err) { + l.Logger.Debug("Effective branch rules fetch skipped due to permissions", "repo", repo.GetFullName(), "branch", branch, "error", err) + return nil, nil + } l.Logger.Trace("Effective branch rules fetch failed", "repo", repo.GetFullName(), "branch", branch, "error", err) evidence[branch] = &BranchRuleEvidence{} continue From e18daab1a7236a9acddbb80f90cb0443a9df1490 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 09:14:59 -0300 Subject: [PATCH 3/6] fix: copilot issues Signed-off-by: Gustavo Carvalho --- main.go | 45 +++++++++++++++++----------- repository_controls.go | 67 +++++++++++++++++++++++++++++++++--------- 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/main.go b/main.go index 283f258..233cc1f 100644 --- a/main.go +++ b/main.go @@ -307,20 +307,15 @@ func (l *GithubReposPlugin) Eval(req *proto.EvalRequest, apiHelper runner.ApiHel Status: proto.ExecutionStatus_FAILURE, }, err } - deployments, err := l.FetchDeploymentsWithStatuses(ctx, repo) + allDeployments, err := l.fetchDeploymentsWithStatuses(ctx, repo) if err != nil { l.Logger.Error("error gathering deployments", "error", err) return &proto.EvalResponse{ Status: proto.ExecutionStatus_FAILURE, }, err } - failedDeployments, err := l.FetchFailedDeploymentsWithStatuses(ctx, repo) - if err != nil { - l.Logger.Error("error gathering failed deployments", "error", err) - return &proto.EvalResponse{ - Status: proto.ExecutionStatus_FAILURE, - }, err - } + deployments := l.filterDeployments(allDeployments) + failedDeployments := deploymentsWithFailures(allDeployments) collaborators, err := l.GatherRepositoryCollaborators(ctx, repo) if err != nil { l.Logger.Error("error gathering repository collaborators", "error", err) @@ -495,15 +490,22 @@ func (l *GithubReposPlugin) GatherWorkflowRuns(ctx context.Context, repo *github } func (l *GithubReposPlugin) FetchDeploymentsWithStatuses(ctx context.Context, repo *github.Repository) ([]*DeploymentWithStatuses, error) { - return l.fetchDeploymentsWithStatuses(ctx, repo, false) + deployments, err := l.fetchDeploymentsWithStatuses(ctx, repo) + if err != nil { + return nil, err + } + return l.filterDeployments(deployments), nil } func (l *GithubReposPlugin) FetchFailedDeploymentsWithStatuses(ctx context.Context, repo *github.Repository) ([]*DeploymentWithStatuses, error) { - deployments, err := l.fetchDeploymentsWithStatuses(ctx, repo, true) + deployments, err := l.fetchDeploymentsWithStatuses(ctx, repo) if err != nil { return nil, err } + return deploymentsWithFailures(deployments), nil +} +func deploymentsWithFailures(deployments []*DeploymentWithStatuses) []*DeploymentWithStatuses { var failed []*DeploymentWithStatuses for _, deployment := range deployments { if deploymentHasFailed(deployment) { @@ -511,10 +513,24 @@ func (l *GithubReposPlugin) FetchFailedDeploymentsWithStatuses(ctx context.Conte } } - return failed, nil + return failed +} + +func (l *GithubReposPlugin) filterDeployments(deployments []*DeploymentWithStatuses) []*DeploymentWithStatuses { + var filtered []*DeploymentWithStatuses + for _, deployment := range deployments { + if deployment == nil || deployment.Deployment == nil { + continue + } + if l.shouldSkipDeployment(deployment.Deployment, deployment.Statuses) { + continue + } + filtered = append(filtered, deployment) + } + return filtered } -func (l *GithubReposPlugin) fetchDeploymentsWithStatuses(ctx context.Context, repo *github.Repository, includeSkipped bool) ([]*DeploymentWithStatuses, error) { +func (l *GithubReposPlugin) fetchDeploymentsWithStatuses(ctx context.Context, repo *github.Repository) ([]*DeploymentWithStatuses, error) { owner := repo.GetOwner().GetLogin() name := repo.GetName() @@ -550,11 +566,6 @@ func (l *GithubReposPlugin) fetchDeploymentsWithStatuses(ctx context.Context, re continue } - // Check if deployment should be filtered based on status - if !includeSkipped && l.shouldSkipDeployment(deployment, statuses) { - continue - } - deploymentsWithStatuses = append(deploymentsWithStatuses, &DeploymentWithStatuses{ Deployment: deployment, Statuses: statuses, diff --git a/repository_controls.go b/repository_controls.go index d844edb..ae6f47f 100644 --- a/repository_controls.go +++ b/repository_controls.go @@ -2,10 +2,13 @@ package main import ( "context" + "sync" "github.com/google/go-github/v71/github" ) +const maxEnvironmentDetailConcurrency = 5 + func (l *GithubReposPlugin) GatherRepositoryCollaborators(ctx context.Context, repo *github.Repository) ([]*RepositoryCollaborator, error) { owner := repo.GetOwner().GetLogin() name := repo.GetName() @@ -101,17 +104,7 @@ func (l *GithubReposPlugin) GatherRepositoryEnvironments(ctx context.Context, re } if envResp != nil { - for _, env := range envResp.Environments { - if env == nil { - continue - } - detail, _, err := l.githubClient.Repositories.GetEnvironment(ctx, owner, name, env.GetName()) - if err != nil { - l.Logger.Trace("Repository environment detail fetch failed", "repo", repo.GetFullName(), "environment", env.GetName(), "error", err) - detail = env - } - environments = append(environments, repositoryEnvironmentFromGitHub(detail)) - } + environments = append(environments, l.repositoryEnvironmentsFromGitHub(ctx, repo, envResp.Environments)...) } if resp == nil || resp.NextPage == 0 { @@ -145,7 +138,6 @@ func (l *GithubReposPlugin) GatherEffectiveBranchRules(ctx context.Context, repo return nil, nil } l.Logger.Trace("Effective branch rules fetch failed", "repo", repo.GetFullName(), "branch", branch, "error", err) - evidence[branch] = &BranchRuleEvidence{} continue } evidence[branch] = branchRuleEvidenceFromGitHub(rules) @@ -154,6 +146,45 @@ func (l *GithubReposPlugin) GatherEffectiveBranchRules(ctx context.Context, repo return evidence, nil } +func (l *GithubReposPlugin) repositoryEnvironmentsFromGitHub(ctx context.Context, repo *github.Repository, environments []*github.Environment) []*RepositoryEnvironment { + owner := repo.GetOwner().GetLogin() + name := repo.GetName() + details := make([]*github.Environment, len(environments)) + + var wg sync.WaitGroup + limiter := make(chan struct{}, maxEnvironmentDetailConcurrency) + for i, env := range environments { + if env == nil { + continue + } + wg.Add(1) + go func(i int, env *github.Environment) { + defer wg.Done() + + limiter <- struct{}{} + defer func() { <-limiter }() + + detail, _, err := l.githubClient.Repositories.GetEnvironment(ctx, owner, name, env.GetName()) + if err != nil { + l.Logger.Trace("Repository environment detail fetch failed", "repo", repo.GetFullName(), "environment", env.GetName(), "error", err) + detail = env + } + details[i] = detail + }(i, env) + } + wg.Wait() + + out := make([]*RepositoryEnvironment, 0, len(details)) + for _, detail := range details { + env := repositoryEnvironmentFromGitHub(detail) + if env == nil { + continue + } + out = append(out, env) + } + return out +} + func branchRuleEvidenceFromGitHub(rules *github.BranchRules) *BranchRuleEvidence { evidence := &BranchRuleEvidence{} if rules == nil { @@ -203,7 +234,11 @@ func repositoryEnvironmentFromGitHub(env *github.Environment) *RepositoryEnviron } } for _, rule := range env.ProtectionRules { - out.ProtectionRules = append(out.ProtectionRules, protectionRuleFromGitHub(rule)) + protectionRule := protectionRuleFromGitHub(rule) + if protectionRule == nil { + continue + } + out.ProtectionRules = append(out.ProtectionRules, protectionRule) } return out @@ -221,7 +256,11 @@ func protectionRuleFromGitHub(rule *github.ProtectionRule) *EnvironmentProtectio PreventSelfReview: rule.GetPreventSelfReview(), } for _, reviewer := range rule.Reviewers { - out.Reviewers = append(out.Reviewers, reviewerFromRequiredReviewer(reviewer)) + requiredReviewer := reviewerFromRequiredReviewer(reviewer) + if requiredReviewer == nil { + continue + } + out.Reviewers = append(out.Reviewers, requiredReviewer) } return out From 64c6b7c12a6973ddb4754fb300c2b44dd5f9cc7e Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 09:27:18 -0300 Subject: [PATCH 4/6] fix: copilot issues Signed-off-by: Gustavo Carvalho --- repository_controls.go | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/repository_controls.go b/repository_controls.go index ae6f47f..0fc38f5 100644 --- a/repository_controls.go +++ b/repository_controls.go @@ -152,26 +152,35 @@ func (l *GithubReposPlugin) repositoryEnvironmentsFromGitHub(ctx context.Context details := make([]*github.Environment, len(environments)) var wg sync.WaitGroup - limiter := make(chan struct{}, maxEnvironmentDetailConcurrency) + jobs := make(chan int) + workers := min(maxEnvironmentDetailConcurrency, len(environments)) + for range workers { + wg.Add(1) + go func() { + defer wg.Done() + for i := range jobs { + env := environments[i] + if env == nil { + continue + } + + detail, _, err := l.githubClient.Repositories.GetEnvironment(ctx, owner, name, env.GetName()) + if err != nil { + l.Logger.Trace("Repository environment detail fetch failed", "repo", repo.GetFullName(), "environment", env.GetName(), "error", err) + detail = env + } + details[i] = detail + } + }() + } + for i, env := range environments { if env == nil { continue } - wg.Add(1) - go func(i int, env *github.Environment) { - defer wg.Done() - - limiter <- struct{}{} - defer func() { <-limiter }() - - detail, _, err := l.githubClient.Repositories.GetEnvironment(ctx, owner, name, env.GetName()) - if err != nil { - l.Logger.Trace("Repository environment detail fetch failed", "repo", repo.GetFullName(), "environment", env.GetName(), "error", err) - detail = env - } - details[i] = detail - }(i, env) + jobs <- i } + close(jobs) wg.Wait() out := make([]*RepositoryEnvironment, 0, len(details)) From eb6e609157985e5feb979b04bb4a1a547e5eeab4 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Wed, 6 May 2026 09:36:49 -0300 Subject: [PATCH 5/6] fix: copilot issues Signed-off-by: Gustavo Carvalho --- main.go | 11 ++++++++--- repository_controls.go | 24 +++++++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 233cc1f..eeddaa7 100644 --- a/main.go +++ b/main.go @@ -921,15 +921,20 @@ func (l *GithubReposPlugin) EvaluatePolicies(ctx context.Context, data *Saturate // isPermissionError returns true if the error from the GitHub client indicates // a permissions or visibility issue (e.g., 401/403/404). func isPermissionError(err error) bool { + return isHTTPStatusError(err, 401, 403, 404) +} + +func isHTTPStatusError(err error, statusCodes ...int) bool { if err == nil { return false } var ger *github.ErrorResponse if errors.As(err, &ger) { if ger.Response != nil { - switch ger.Response.StatusCode { - case 401, 403, 404: - return true + for _, statusCode := range statusCodes { + if ger.Response.StatusCode == statusCode { + return true + } } } } diff --git a/repository_controls.go b/repository_controls.go index 0fc38f5..678791a 100644 --- a/repository_controls.go +++ b/repository_controls.go @@ -52,6 +52,7 @@ func (l *GithubReposPlugin) GatherRepositoryTeams(ctx context.Context, repo *git owner := repo.GetOwner().GetLogin() name := repo.GetName() opts := &github.ListOptions{PerPage: 100, Page: 1} + orgTeamMembers := orgTeamMembersBySlug(orgTeams) var teams []*RepositoryTeam for { @@ -74,7 +75,7 @@ func (l *GithubReposPlugin) GatherRepositoryTeams(ctx context.Context, repo *git Slug: team.GetSlug(), Permission: team.GetPermission(), Permissions: copyPermissions(team.GetPermissions()), - Members: membersForOrgTeam(orgTeams, team.GetSlug()), + Members: membersForOrgTeam(orgTeamMembers, team.GetSlug()), }) } @@ -133,7 +134,11 @@ func (l *GithubReposPlugin) GatherEffectiveBranchRules(ctx context.Context, repo for branch := range targets { rules, _, err := l.githubClient.Repositories.GetRulesForBranch(ctx, owner, name, branch) if err != nil { - if isPermissionError(err) { + if isHTTPStatusError(err, 404) { + l.Logger.Debug("Effective branch rules fetch skipped for missing branch", "repo", repo.GetFullName(), "branch", branch, "error", err) + continue + } + if isHTTPStatusError(err, 401, 403) { l.Logger.Debug("Effective branch rules fetch skipped due to permissions", "repo", repo.GetFullName(), "branch", branch, "error", err) return nil, nil } @@ -316,11 +321,20 @@ func reviewerFromRequiredReviewer(reviewer *github.RequiredReviewer) *Environmen return out } -func membersForOrgTeam(orgTeams []*OrgTeam, slug string) []string { +func orgTeamMembersBySlug(orgTeams []*OrgTeam) map[string][]string { + members := make(map[string][]string, len(orgTeams)) for _, team := range orgTeams { - if team != nil && team.Slug == slug { - return append([]string{}, team.Members...) + if team == nil || team.Slug == "" { + continue } + members[team.Slug] = append([]string{}, team.Members...) + } + return members +} + +func membersForOrgTeam(orgTeamMembers map[string][]string, slug string) []string { + if members, ok := orgTeamMembers[slug]; ok { + return append([]string{}, members...) } return nil } From 93df3e07e63d68abd5ea8b75538ef9febd745451 Mon Sep 17 00:00:00 2001 From: Gustavo Carvalho Date: Tue, 12 May 2026 05:52:51 -0300 Subject: [PATCH 6/6] chore: bump agent version Signed-off-by: Gustavo Carvalho --- go.mod | 13 ++++++------- go.sum | 40 ++++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 858617a..63cbe2b 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/compliance-framework/plugin-github-repositories -go 1.25.8 +go 1.26.1 require ( - github.com/compliance-framework/agent v0.3.1 + github.com/compliance-framework/agent v0.6.2 github.com/google/go-github/v71 v71.0.0 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 @@ -15,7 +15,7 @@ require ( require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/compliance-framework/api v0.13.0 // indirect + github.com/compliance-framework/api v0.16.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/defenseunicorns/go-oscal v0.7.0 // indirect @@ -55,12 +55,11 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.51.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go.sum b/go.sum index 89a3143..c4f536f 100644 --- a/go.sum +++ b/go.sum @@ -56,10 +56,10 @@ github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqy github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/compliance-framework/agent v0.3.1 h1:RikYgITNcu5Wc8i4sTzTzfZvbon2/r8Hot6ZcGZ+1UA= -github.com/compliance-framework/agent v0.3.1/go.mod h1:S0x4qpbUdlVZD6NlyGlrsSLUODdtB3M1rOHHcXQdadU= -github.com/compliance-framework/api v0.13.0 h1:pW0JS4e9ZwRIwSZM32ObjdCBIxuxL+TL4nHAcopqMO0= -github.com/compliance-framework/api v0.13.0/go.mod h1:CMHwcOOCcVRf1u/n3BeqbrP09WWCuwnFAlD7dQfIWCA= +github.com/compliance-framework/agent v0.6.2 h1:4Ha3kTDpoAXDsGOnczeVXdf56dl7h2XNxIfawWJc+LI= +github.com/compliance-framework/agent v0.6.2/go.mod h1:k6sNhVQXviFHbz/Fe/jOkfBZ+AFLnRPIuOH2aaaCTNo= +github.com/compliance-framework/api v0.16.0 h1:0HO5a5N80ktJLeLD5GVeTk7cK7PO9Xj5WN4SR+KGBH0= +github.com/compliance-framework/api v0.16.0/go.mod h1:BupcN8mQFgB0/2+YShU/r4BUYoGwzSjbz2esdOUaX/4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -113,8 +113,8 @@ github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -170,6 +170,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= @@ -180,8 +182,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= @@ -310,6 +312,8 @@ github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZV github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/slack-go/slack v0.20.0 h1:gbDdbee8+Z2o+DWx05Spq3GzbrLLleiRwHUKs+hZLSU= +github.com/slack-go/slack v0.20.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -389,12 +393,12 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= @@ -407,12 +411,12 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=