diff --git a/docs/tables/github_organization_ruleset.md b/docs/tables/github_organization_ruleset.md new file mode 100644 index 0000000..24ffea1 --- /dev/null +++ b/docs/tables/github_organization_ruleset.md @@ -0,0 +1,225 @@ +--- +title: "Steampipe Table: github_organization_ruleset - Query GitHub Organization Rulesets using SQL" +description: "Allows users to query GitHub Organization Rulesets, providing details about each ruleset within an organization. This information includes ruleset ID, name, enforcement level, bypass actors, and more." +folder: "Organization" +--- + +# Table: github_organization_ruleset - Query GitHub Organization Rulesets using SQL + +GitHub Organization Rulesets is a feature within GitHub that allows organizations to enforce rules and conditions across repositories. These rulesets help manage repository settings, permissions, and enforce best practices at the organization level. + +## Table Usage Guide + +The `github_organization_ruleset` table provides insights into the rulesets within a GitHub organization. As a security engineer or team lead, you can explore ruleset-specific details through this table, including ruleset ID, name, enforcement level, bypass actors, and conditions. Utilize it to enforce organization-wide policies, manage permissions, and ensure compliance with organizational standards. + +To query this table using a [fine-grained access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token), the following permissions are required: + - Organization permissions: + - Members (Read-only): Required to access organization rulesets. + +**Important Notes** +- You must specify the `organization` column in the `where` or `join` clause to query the table. + +## Examples + +### List all rulesets in an organization +Explore all rulesets within a specific organization, including their enforcement levels and creation dates, to understand and manage organization-wide policies. + +```sql+postgres +select + name, + enforcement, + created_at +from + github_organization_ruleset +where + organization = 'my-org'; +``` + +```sql+sqlite +select + name, + enforcement, + created_at +from + github_organization_ruleset +where + organization = 'my-org'; +``` + +### Get rules from a specific ruleset +Retrieve the detailed rules of a specific ruleset within your organization. + +```sql+postgres +select + name, + r->>'id' as rule_id, + r->>'type' as rule_type, + r->>'parameters' as rule_parameters +from + github_organization_ruleset, + jsonb_array_elements(rules) as r +where + organization = 'my-org' + and name = 'my-ruleset'; +``` + +```sql+sqlite +select + name, + json_extract(r.value, '$.id') as rule_id, + json_extract(r.value, '$.type') as rule_type, + json_extract(r.value, '$.parameters') as rule_parameters +from + github_organization_ruleset, + json_each(rules) as r +where + organization = 'my-org' + and name = 'my-ruleset'; +``` + +### Get bypass actors for a specific ruleset +Identify the actors who can bypass the ruleset within your organization. + +```sql+postgres +select + name, + b->>'id' as bypass_actor_id, + b->>'deploy_key' as deploy_key, + b->>'bypass_mode' as bypass_mode, + b->>'repository_role_name' as repository_role_name, + b->>'repository_role_database_id' as repository_role_database_id +from + github_organization_ruleset, + jsonb_array_elements(bypass_actors) as b +where + organization = 'my-org' + and name = 'my-ruleset'; +``` + +```sql+sqlite +select + name, + json_extract(b.value, '$.id') as bypass_actor_id, + json_extract(b.value, '$.deploy_key') as deploy_key, + json_extract(b.value, '$.bypass_mode') as bypass_mode, + json_extract(b.value, '$.repository_role_name') as repository_role_name, + json_extract(b.value, '$.repository_role_database_id') as repository_role_database_id +from + github_organization_ruleset, + json_each(bypass_actors) as b +where + organization = 'my-org' + and name = 'my-ruleset'; +``` + +### List rulesets with specific enforcement levels +Identify rulesets within an organization that have specific enforcement levels. + +```sql+postgres +select + name, + enforcement +from + github_organization_ruleset +where + organization = 'my-org' + and enforcement = 'active'; +``` + +```sql+sqlite +select + name, + enforcement +from + github_organization_ruleset +where + organization = 'my-org' + and enforcement = 'active'; +``` + +### List all rulesets created after a specific date +Retrieve all rulesets created after a specified date, useful for auditing recent policy changes. + +```sql+postgres +select + name, + created_at +from + github_organization_ruleset +where + organization = 'my-org' + and created_at > '2023-01-01T00:00:00Z'; +``` + +```sql+sqlite +select + name, + created_at +from + github_organization_ruleset +where + organization = 'my-org' + and created_at > '2023-01-01T00:00:00Z'; +``` + +### List pull request parameters +List rules with pull request parameters, including code owner review requirements. + +```sql+postgres +select + id, + name, + r -> 'parameters' ->> 'Type' as type, + r -> 'parameters' -> 'PullRequestParameters' ->> 'require_code_owner_review' as require_code_owner_review, + r -> 'parameters' -> 'PullRequestParameters' ->> 'required_approving_review_count' as required_approving_review_count +from + github_organization_ruleset, + jsonb_array_elements(rules) as r +where + organization = 'my-org' + and (r -> 'parameters' ->> 'Type') = 'PullRequestParameters'; +``` + +```sql+sqlite +select + id, + name, + json_extract(r.value, '$.parameters.Type') as type, + json_extract(r.value, '$.parameters.PullRequestParameters.require_code_owner_review') as require_code_owner_review, + json_extract(r.value, '$.parameters.PullRequestParameters.required_approving_review_count') as required_approving_review_count +from + github_organization_ruleset, + json_each(rules) as r +where + organization = 'my-org' + and json_extract(r.value, '$.parameters.Type') = 'PullRequestParameters'; +``` + +### List required status check parameters +List rules with required status check parameters. + +```sql+postgres +select + id, + name, + r -> 'parameters' ->> 'Type' as type, + r -> 'parameters' -> 'RequiredStatusChecksParameters' ->> 'required_status_checks' as required_status_checks +from + github_organization_ruleset, + jsonb_array_elements(rules) as r +where + organization = 'my-org'; +``` + +```sql+sqlite +select + id, + name, + json_extract(r.value, '$.parameters.Type') as type, + json_extract(r.value, '$.parameters.RequiredStatusChecksParameters.required_status_checks') as required_status_checks +from + github_organization_ruleset, + json_each(rules) as r +where + organization = 'my-org'; +``` diff --git a/github/plugin.go b/github/plugin.go index 7915a0d..1a9c695 100644 --- a/github/plugin.go +++ b/github/plugin.go @@ -53,6 +53,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "github_organization_external_identity": tableGitHubOrganizationExternalIdentity(), "github_organization_member": tableGitHubOrganizationMember(), "github_organization_collaborator": tableGitHubOrganizationCollaborator(), + "github_organization_ruleset": tableGitHubOrganizationRuleset(), "github_package": tableGitHubPackage(), "github_package_version": tableGitHubPackageVersion(), "github_pull_request": tableGitHubPullRequest(), diff --git a/github/table_github_organization_ruleset.go b/github/table_github_organization_ruleset.go new file mode 100644 index 0000000..c2b3f29 --- /dev/null +++ b/github/table_github_organization_ruleset.go @@ -0,0 +1,239 @@ +package github + +import ( + "context" + + "github.com/shurcooL/githubv4" + "github.com/turbot/steampipe-plugin-github/github/models" + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" +) + +func tableGitHubOrganizationRuleset() *plugin.Table { + return &plugin.Table{ + Name: "github_organization_ruleset", + Description: "Retrieve the rulesets of a specified GitHub organization.", + List: &plugin.ListConfig{ + Hydrate: tableGitHubOrganizationRulesetList, + KeyColumns: []*plugin.KeyColumn{ + {Name: "organization", Require: plugin.Required}, + }, + }, + Columns: gitHubOrganizationRulesetColumns(), + } +} + +func gitHubOrganizationRulesetColumns() []*plugin.Column { + return []*plugin.Column{ + {Name: "organization", Type: proto.ColumnType_STRING, Transform: transform.FromQual("organization"), Description: "The organization login name."}, + {Name: "name", Type: proto.ColumnType_STRING, Description: "The name of the ruleset."}, + {Name: "id", Type: proto.ColumnType_STRING, Description: "The ID of the ruleset."}, + {Name: "created_at", Type: proto.ColumnType_TIMESTAMP, Transform: transform.FromField("CreatedAt").Transform(convertRulesetTimestamp), Description: "The date and time when the ruleset was created."}, + {Name: "database_id", Type: proto.ColumnType_INT, Description: "The database ID of the ruleset."}, + {Name: "enforcement", Type: proto.ColumnType_STRING, Description: "The enforcement level of the ruleset."}, + {Name: "rules", Type: proto.ColumnType_JSON, Description: "The list of rules in the ruleset."}, + {Name: "bypass_actors", Type: proto.ColumnType_JSON, Description: "The list of actors who can bypass the ruleset."}, + {Name: "conditions", Type: proto.ColumnType_JSON, Description: "The conditions under which the ruleset applies."}, + } +} + +func tableGitHubOrganizationRulesetList(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + var query struct { + RateLimit models.RateLimit + Organization struct { + Rulesets struct { + PageInfo struct { + HasNextPage bool + EndCursor githubv4.String + } + Edges []struct { + Node struct { + CreatedAt githubv4.DateTime + DatabaseID int + Enforcement string + Name string + ID string + Rules struct { + PageInfo models.PageInfo + Edges []struct { + Node models.Rule + } + } `graphql:"rules(first: $rulePageSize, after: $ruleCursor)"` + BypassActors struct { + PageInfo models.PageInfo + Edges []struct { + Node models.BypassActor + } + } `graphql:"bypassActors(first: $bypassActorPageSize, after: $bypassActorCursor)"` + Conditions models.Conditions + } + } + } `graphql:"rulesets(first: $rulesetPageSize, after: $rulesetCursor)"` + } `graphql:"organization(login: $org)"` + } + + rulesetPageSize := adjustPageSize(100, d.QueryContext.Limit) + rulePageSize := 100 + bypassActorPageSize := 100 + org := d.EqualsQuals["organization"].GetStringValue() + + variables := map[string]interface{}{ + "org": githubv4.String(org), + "rulesetPageSize": githubv4.Int(rulesetPageSize), + "rulesetCursor": (*githubv4.String)(nil), + "rulePageSize": githubv4.Int(rulePageSize), + "ruleCursor": (*githubv4.String)(nil), + "bypassActorPageSize": githubv4.Int(bypassActorPageSize), + "bypassActorCursor": (*githubv4.String)(nil), + } + + client := connectV4(ctx, d) + + for { + err := client.Query(ctx, &query, variables) + plugin.Logger(ctx).Debug(rateLimitLogString("github_organization_ruleset", &query.RateLimit)) + if err != nil { + plugin.Logger(ctx).Error("github_organization_ruleset", "api_error", err) + return nil, err + } + + for _, edge := range query.Organization.Rulesets.Edges { + var rules []models.Rule + for _, rule := range edge.Node.Rules.Edges { + rules = append(rules, rule.Node) + } + if edge.Node.Rules.PageInfo.HasNextPage { + additionalRules := getAdditionalOrgRules(ctx, d, client, edge.Node.DatabaseID, org, "") + rules = append(rules, additionalRules...) + } + + var bypassActors []models.BypassActor + for _, actor := range edge.Node.BypassActors.Edges { + bypassActors = append(bypassActors, actor.Node) + } + if edge.Node.BypassActors.PageInfo.HasNextPage { + additionalBypassActors := getAdditionalOrgBypassActors(ctx, d, client, org, edge.Node.DatabaseID, "") + bypassActors = append(bypassActors, additionalBypassActors...) + } + + ruleset := models.Ruleset{ + CreatedAt: edge.Node.CreatedAt.String(), + DatabaseID: edge.Node.DatabaseID, + Enforcement: edge.Node.Enforcement, + Name: edge.Node.Name, + ID: edge.Node.ID, + Rules: rules, + BypassActors: bypassActors, + Conditions: edge.Node.Conditions, + } + + d.StreamListItem(ctx, ruleset) + + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + if !query.Organization.Rulesets.PageInfo.HasNextPage { + break + } + variables["rulesetCursor"] = githubv4.NewString(query.Organization.Rulesets.PageInfo.EndCursor) + } + + return nil, nil +} + +func getAdditionalOrgRules(ctx context.Context, d *plugin.QueryData, client *githubv4.Client, databaseID int, org string, initialCursor githubv4.String) []models.Rule { + var query struct { + RateLimit models.RateLimit + Organization struct { + Ruleset struct { + Rules struct { + PageInfo struct { + HasNextPage bool + EndCursor githubv4.String + } + Edges []struct { + Node models.Rule + } + } `graphql:"rules(first: $pageSize, after: $cursor)"` + } `graphql:"ruleset(databaseId: $databaseID)"` + } `graphql:"organization(login: $org)"` + } + + variables := map[string]interface{}{ + "org": githubv4.String(org), + "pageSize": githubv4.Int(100), + "cursor": githubv4.NewString(initialCursor), + "databaseID": githubv4.Int(databaseID), + } + + var rules []models.Rule + for { + err := client.Query(ctx, &query, variables) + plugin.Logger(ctx).Debug(rateLimitLogString("github_organization_ruleset.getAdditionalOrgRules", &query.RateLimit)) + if err != nil { + plugin.Logger(ctx).Error("github_organization_ruleset.getAdditionalOrgRules", "api_error", err) + return nil + } + + for _, edge := range query.Organization.Ruleset.Rules.Edges { + rules = append(rules, edge.Node) + } + + if !query.Organization.Ruleset.Rules.PageInfo.HasNextPage { + break + } + variables["cursor"] = githubv4.NewString(query.Organization.Ruleset.Rules.PageInfo.EndCursor) + } + + return rules +} + +func getAdditionalOrgBypassActors(ctx context.Context, d *plugin.QueryData, client *githubv4.Client, org string, databaseID int, initialCursor githubv4.String) []models.BypassActor { + var query struct { + RateLimit models.RateLimit + Organization struct { + Ruleset struct { + BypassActors struct { + PageInfo struct { + HasNextPage bool + EndCursor githubv4.String + } + Edges []struct { + Node models.BypassActor + } + } `graphql:"bypassActors(first: $pageSize, after: $cursor)"` + } `graphql:"ruleset(databaseId: $databaseID)"` + } `graphql:"organization(login: $org)"` + } + + variables := map[string]interface{}{ + "org": githubv4.String(org), + "pageSize": githubv4.Int(100), + "cursor": githubv4.NewString(initialCursor), + "databaseID": githubv4.Int(databaseID), + } + + var bypassActors []models.BypassActor + for { + err := client.Query(ctx, &query, variables) + plugin.Logger(ctx).Debug(rateLimitLogString("github_organization_ruleset.getAdditionalOrgBypassActors", &query.RateLimit)) + if err != nil { + plugin.Logger(ctx).Error("github_organization_ruleset.getAdditionalOrgBypassActors", "api_error", err) + return nil + } + + for _, edge := range query.Organization.Ruleset.BypassActors.Edges { + bypassActors = append(bypassActors, edge.Node) + } + + if !query.Organization.Ruleset.BypassActors.PageInfo.HasNextPage { + break + } + variables["cursor"] = githubv4.NewString(query.Organization.Ruleset.BypassActors.PageInfo.EndCursor) + } + + return bypassActors +}