From 40a04ca4cee95c15588b1f61ebe6b0914441768c Mon Sep 17 00:00:00 2001 From: ParthaI <47887552+ParthaI@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:26:42 +0530 Subject: [PATCH] Add table github_repository_ruleset Closes #437 (#440) Co-authored-by: Ved misra <47312748+misraved@users.noreply.github.com> --- docs/tables/github_repository_ruleset.md | 285 ++++++++++++++++++++++ github/models/ruleset.go | 149 +++++++++++ github/plugin.go | 1 + github/table_github_repository_ruleset.go | 277 +++++++++++++++++++++ 4 files changed, 712 insertions(+) create mode 100644 docs/tables/github_repository_ruleset.md create mode 100644 github/models/ruleset.go create mode 100644 github/table_github_repository_ruleset.go diff --git a/docs/tables/github_repository_ruleset.md b/docs/tables/github_repository_ruleset.md new file mode 100644 index 0000000..ddc26b5 --- /dev/null +++ b/docs/tables/github_repository_ruleset.md @@ -0,0 +1,285 @@ +--- +title: "Steampipe Table: github_repository_ruleset - Query GitHub Repository Rulesets using SQL" +description: "Allows users to query GitHub Repository Rulesets, providing details about each ruleset within a repository. This information includes ruleset ID, name, enforcement level, bypass actors, and more." +--- + +# Table: github_repository_ruleset - Query GitHub Repository Rulesets using SQL + +GitHub Repository Rulesets is a feature within GitHub that allows organizations to enforce rules and conditions on repositories. These rulesets help manage repository settings, and permissions, and enforce best practices. + +## Table Usage Guide + +The `github_repository_ruleset` table provides insights into the rulesets within GitHub repositories. As a project manager 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 repository policies, manage permissions, and ensure compliance with organizational standards. + +**Important Notes** +- You must specify the `repository_full_name` column in the `where` or `join` clause to query the table. + +## Examples + +### List all rulesets in a repository +Explore all rulesets within a specific repository, including their enforcement levels and creation dates, to understand and manage repository policies. + +```sql+postgres +select + name, + enforcement, + created_at +from + github_repository_ruleset +where + repository_full_name = 'pro-cloud-49/test-rule'; +``` + +```sql+sqlite +select + name, + enforcement, + created_at +from + github_repository_ruleset +where + repository_full_name = 'pro-cloud-49/test-rule'; +``` + +### Get rules from a specific ruleset +Retrieve the detailed rules of a specific ruleset within your repository. This can be useful for reviewing the rules enforced and ensuring they align with your project requirements. + +```sql+postgres +select + name, + r->>'id' as rule_id, + r->>'type' as rule_type, + r->>'parameters' as rule_parameters +from + github_repository_ruleset, + jsonb_array_elements(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' + and name = 'test34'; +``` + +```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_repository_ruleset, + json_each(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' + and name = 'test34'; +``` + +### Get bypass actors for a specific ruleset +Identify the actors who can bypass the ruleset within your repository. This information is crucial for managing exceptions and understanding who has elevated permissions. + +```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_repository_ruleset, + jsonb_array_elements(bypass_actors) as b +where + repository_full_name = 'pro-cloud-49/test-rule' + and name = 'test34'; +``` + +```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_repository_ruleset, + json_each(bypass_actors) as b +where + repository_full_name = 'pro-cloud-49/test-rule' + and name = 'test34'; +``` + +### List rulesets with specific enforcement levels +Identify rulesets within a repository that have specific enforcement levels, helping to understand the compliance and security posture of the repository. + +```sql+postgres +select + name, + enforcement +from + github_repository_ruleset +where + repository_full_name = 'pro-cloud-49/test-rule' + and enforcement = 'strict'; +``` + +```sql+sqlite +select + name, + enforcement +from + github_repository_ruleset +where + repository_full_name = 'pro-cloud-49/test-rule' + and enforcement = 'strict'; +``` + +### List all rulesets created after a specific date +Retrieve all rulesets that were created after a specified date, useful for auditing and tracking recent changes in repository policies. + +```sql+postgres +select + name, + created_at +from + github_repository_ruleset +where + repository_full_name = 'pro-cloud-49/test-rule' + and created_at > '2023-01-01T00:00:00Z'; +``` + +```sql+sqlite +select + name, + created_at +from + github_repository_ruleset +where + repository_full_name = 'pro-cloud-49/test-rule' + and created_at > '2023-01-01T00:00:00Z'; +``` + +### List update parameters +List rules with update parameters, focusing on the `update_allows_fetch_and_merge` setting. + +```sql+postgres +select + id, + name, + r -> 'parameters' ->> 'Type' as type, + r -> 'parameters' -> 'UpdateParameters' ->> 'update_allows_fetch_and_merge' as update_allows_fetch_and_merge +from + github_repository_ruleset, + jsonb_array_elements(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' +and + (r -> 'parameters' ->> 'Type') = 'UpdateParameters'; +``` + +```sql+sqlite +select + id, + name, + json_extract(r.value, '$.parameters.Type') as type, + json_extract(r.value, '$.parameters.UpdateParameters.update_allows_fetch_and_merge') as update_allows_fetch_and_merge +from + github_repository_ruleset, + json_each(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' + and json_extract(r.value, '$.parameters.Type') = 'UpdateParameters'; +``` + +### List workflow parameters +List rules with workflow parameters, focusing on the workflow configurations. + +```sql+postgres +select + id, + name, + r -> 'parameters' ->> 'Type' as type, + r -> 'parameters' -> 'WorkflowsParameters' ->> 'workflows' as workflows +from + github_repository_ruleset, + jsonb_array_elements(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' +and + (r -> 'parameters' ->> 'Type') = 'WorkflowsParameters'; +``` + +```sql+sqlite +select + id, + name, + json_extract(r.value, '$.parameters.Type') as type, + json_extract(r.value, '$.parameters.WorkflowsParameters.workflows') as workflows +from + github_repository_ruleset, + json_each(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' + and json_extract(r.value, '$.parameters.Type') = 'WorkflowsParameters'; +``` + +### List pull request parameters +List rules with pull request parameters, including various settings such as 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_repository_ruleset, + jsonb_array_elements(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' +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_repository_ruleset, + json_each(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule' + 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_repository_ruleset, + jsonb_array_elements(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule'; +``` + +```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_repository_ruleset, + json_each(rules) as r +where + repository_full_name = 'pro-cloud-49/test-rule'; +``` diff --git a/github/models/ruleset.go b/github/models/ruleset.go new file mode 100644 index 0000000..fc0dd23 --- /dev/null +++ b/github/models/ruleset.go @@ -0,0 +1,149 @@ +package models + +type Ruleset struct { + CreatedAt string `json:"created_at"` + DatabaseID int `json:"database_id"` + Enforcement string `json:"enforcement"` + Name string `json:"name"` + ID string `json:"id"` + Rules []Rule `json:"rules"` + BypassActors []BypassActor `json:"bypass_actors"` + Conditions Conditions `json:"conditions"` +} + +type Rule struct { + ID string `json:"id"` + Type string `json:"type"` + Parameters Parameters `json:"parameters"` +} + +type Parameters struct { + Type string `graphql:"__typename"` + PullRequestParameters PullRequestParameters `graphql:"... on PullRequestParameters"` + CodeScanningParameters CodeScanningParameters `graphql:"... on CodeScanningParameters"` + CommitAuthorEmailPatternParameters CommitAuthorEmailPatternParameters `graphql:"... on CommitAuthorEmailPatternParameters"` + CommitMessagePatternParameters CommitMessagePatternParameters `graphql:"... on CommitMessagePatternParameters"` + CommitterEmailPatternParameters CommitterEmailPatternParameters `graphql:"... on CommitterEmailPatternParameters"` + FileExtensionRestrictionParameters FileExtensionRestrictionParameters `graphql:"... on FileExtensionRestrictionParameters"` + FilePathRestrictionParameters FilePathRestrictionParameters `graphql:"... on FilePathRestrictionParameters"` + MaxFilePathLengthParameters MaxFilePathLengthParameters `graphql:"... on MaxFilePathLengthParameters"` + MaxFileSizeParameters MaxFileSizeParameters `graphql:"... on MaxFileSizeParameters"` + RequiredDeploymentsParameters RequiredDeploymentsParameters `graphql:"... on RequiredDeploymentsParameters"` + RequiredStatusChecksParameters RequiredStatusChecksParameters `graphql:"... on RequiredStatusChecksParameters"` + TagNamePatternParameters TagNamePatternParameters `graphql:"... on TagNamePatternParameters"` + UpdateParameters UpdateParameters `graphql:"... on UpdateParameters"` + WorkflowsParameters WorkflowsParameters `graphql:"... on WorkflowsParameters"` +} + +type PullRequestParameters struct { + DismissStaleReviewsOnPush bool `json:"dismiss_stale_reviews_on_push"` + RequireCodeOwnerReview bool `json:"require_code_owner_review"` + RequireLastPushApproval bool `json:"require_last_push_approval"` + RequiredApprovingReviewCount int `json:"required_approving_review_count"` + RequiredReviewThreadResolution bool `json:"required_review_thread_resolution"` +} + +type CodeScanningParameters struct { + CodeScanningTools []CodeScanningTool `json:"code_scanning_tools"` +} + +type CodeScanningTool struct { + AlertsThreshold string `json:"alerts_threshold"` + SecurityAlertsThreshold string `json:"security_alerts_threshold"` + Tool string `json:"tool"` +} + +type CommitAuthorEmailPatternParameters struct { + Name string `json:"name"` + Negate bool `json:"negate"` + Operator string `json:"operator"` + Pattern string `json:"pattern"` +} + +type CommitMessagePatternParameters struct { + Name string `json:"name"` + Negate bool `json:"negate"` + Operator string `json:"operator"` + Pattern string `json:"pattern"` +} + +type CommitterEmailPatternParameters struct { + Name string `json:"name"` + Negate bool `json:"negate"` + Operator string `json:"operator"` + Pattern string `json:"pattern"` +} + +type FileExtensionRestrictionParameters struct { + RestrictedFileExtensions []string `json:"restricted_file_extensions"` +} + +type FilePathRestrictionParameters struct { + RestrictedFilePaths []string `json:"restricted_file_paths"` +} + +type MaxFilePathLengthParameters struct { + MaxFilePathLength int `json:"max_file_path_length"` +} + +type MaxFileSizeParameters struct { + MaxFileSize int `json:"max_file_size"` +} + +type RequiredDeploymentsParameters struct { + RequiredDeploymentEnvironments []string `json:"required_deployment_environments"` +} + +type RequiredStatusChecksParameters struct { + RequiredStatusChecks []StatusCheckConfiguration `json:"required_status_checks"` + StrictRequiredStatusChecksPolicy bool `json:"strict_required_status_checks_policy"` +} + +type StatusCheckConfiguration struct { + Context string `json:"context"` + IntegrationId int `json:"integration_id"` +} + +type TagNamePatternParameters struct { + Name string `json:"name"` + Negate bool `json:"negate"` + Operator string `json:"operator"` + Pattern string `json:"pattern"` +} + +type UpdateParameters struct { + UpdateAllowsFetchAndMerge bool `json:"update_allows_fetch_and_merge"` +} + +type WorkflowsParameters struct { + Workflows []WorkflowFileReference `json:"workflows"` +} + +type WorkflowFileReference struct { + Path string `json:"path"` + Ref string `json:"ref"` + RepositoryId int `json:"repository_id"` + Sha string `json:"sha"` +} +type BypassActor struct { + BypassMode string `json:"bypass_mode"` + DeployKey bool `json:"deploy_key"` + ID string `json:"id"` + RepositoryRoleDatabaseID int `json:"repository_role_database_id"` + RepositoryRoleName string `json:"repository_role_name"` +} + +type Conditions struct { + RefName struct { + Exclude []string `json:"exclude"` + Include []string `json:"include"` + } `json:"ref_name"` + RepositoryID struct { + RepositoryIds []string `json:"repository_ids"` + } `json:"repository_id"` + RepositoryName struct { + Exclude []string `json:"exclude"` + Include []string `json:"include"` + Protected bool `json:"protected"` + } `json:"repository_name"` +} diff --git a/github/plugin.go b/github/plugin.go index 81fbe29..f44e812 100644 --- a/github/plugin.go +++ b/github/plugin.go @@ -62,6 +62,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "github_repository_dependabot_alert": tableGitHubRepositoryDependabotAlert(), "github_repository_deployment": tableGitHubRepositoryDeployment(), "github_repository_environment": tableGitHubRepositoryEnvironment(), + "github_repository_ruleset": tableGitHubRepositoryRuleset(), "github_repository_sbom": tableGitHubRepositorySbom(), "github_repository_vulnerability_alert": tableGitHubRepositoryVulnerabilityAlert(), "github_search_code": tableGitHubSearchCode(), diff --git a/github/table_github_repository_ruleset.go b/github/table_github_repository_ruleset.go new file mode 100644 index 0000000..66ac35b --- /dev/null +++ b/github/table_github_repository_ruleset.go @@ -0,0 +1,277 @@ +package github + +import ( + "context" + "time" + + "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 tableGitHubRepositoryRuleset() *plugin.Table { + return &plugin.Table{ + Name: "github_repository_ruleset", + Description: "Retrieve the rulesets of a specified GitHub repository.", + List: &plugin.ListConfig{ + Hydrate: tableGitHubRepositoryRulesetList, + KeyColumns: []*plugin.KeyColumn{ + {Name: "repository_full_name", Require: plugin.Required}, + }, + }, + Columns: gitHubRulesetColumns(), + } +} + +func gitHubRulesetColumns() []*plugin.Column { + return []*plugin.Column{ + {Name: "repository_full_name", Type: proto.ColumnType_STRING, Transform: transform.FromQual("repository_full_name"), Description: "Full name of the repository that contains the ruleset."}, + {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 tableGitHubRepositoryRulesetList(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) { + var query struct { + RateLimit models.RateLimit + Repository 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:"repository(owner: $owner, name: $name)"` + } + rulesetPageSize := adjustPageSize(100, d.QueryContext.Limit) + rulePageSize := 100 + bypassActorPageSize := 100 + fullName := d.EqualsQuals["repository_full_name"].GetStringValue() + owner, repo := parseRepoFullName(fullName) + + variables := map[string]interface{}{ + "owner": githubv4.String(owner), + "name": githubv4.String(repo), + "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) + + var rulesets []models.Ruleset + for { + err := client.Query(ctx, &query, variables) + plugin.Logger(ctx).Debug(rateLimitLogString("github_repository_ruleset", &query.RateLimit)) + if err != nil { + plugin.Logger(ctx).Error("github_repository_ruleset", "api_error", err) + return nil, err + } + + for _, edge := range query.Repository.Rulesets.Edges { + + // Fetch additional Rules. + var rules []models.Rule + for _, rule := range edge.Node.Rules.Edges { + rules = append(rules, rule.Node) + } + if edge.Node.Rules.PageInfo.HasNextPage { + additionalRules := getAdditionalRules(ctx, d, client, edge.Node.DatabaseID, owner, repo, "") + rules = append(rules, additionalRules...) + } + + // Fetch additional ByPassActors. + var bypassActors []models.BypassActor + for _, actor := range edge.Node.BypassActors.Edges { + bypassActors = append(bypassActors, actor.Node) + } + if edge.Node.BypassActors.PageInfo.HasNextPage { + + additionalBypassActors := getAdditionalBypassActors(ctx, d, client, owner, repo, 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, + } + rulesets = append(rulesets, ruleset) + } + + for _, ruleset := range rulesets { + d.StreamListItem(ctx, ruleset) + + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + if !query.Repository.Rulesets.PageInfo.HasNextPage { + break + } + variables["rulesetCursor"] = githubv4.NewString(query.Repository.Rulesets.PageInfo.EndCursor) + } + + return nil, nil +} + +func getAdditionalRules(ctx context.Context, d *plugin.QueryData, client *githubv4.Client, databaseID int, owner string, repo string, initialCursor githubv4.String) []models.Rule { + + var query struct { + RateLimit models.RateLimit + Repository 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:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "pageSize": githubv4.Int(100), + "cursor": githubv4.NewString(initialCursor), + "databaseID": githubv4.Int(databaseID), + "owner": githubv4.String(owner), + "name": githubv4.String(repo), + } + + var rules []models.Rule + for { + err := client.Query(ctx, &query, variables) + plugin.Logger(ctx).Debug(rateLimitLogString("github_repository_ruleset.getAdditionalRules", &query.RateLimit)) + if err != nil { + plugin.Logger(ctx).Error("github_repository_ruleset.getAdditionalRules", "api_error", err) + return nil + } + + for _, edge := range query.Repository.Ruleset.Rules.Edges { + rules = append(rules, edge.Node) + } + + if !query.Repository.Ruleset.Rules.PageInfo.HasNextPage { + break + } + variables["cursor"] = githubv4.NewString(query.Repository.Ruleset.Rules.PageInfo.EndCursor) + } + + return rules +} + +func getAdditionalBypassActors(ctx context.Context, d *plugin.QueryData, client *githubv4.Client, owner string, repo string, databaseID int, initialCursor githubv4.String) []models.BypassActor { + + var query struct { + RateLimit models.RateLimit + Repository 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:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(owner), + "name": githubv4.String(repo), + "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_repository_ruleset.getAdditionalBypassActors", &query.RateLimit)) + if err != nil { + plugin.Logger(ctx).Error("github_repository_ruleset.getAdditionalBypassActors", "api_error", err) + return nil + } + + for _, edge := range query.Repository.Ruleset.BypassActors.Edges { + bypassActors = append(bypassActors, edge.Node) + } + + if !query.Repository.Ruleset.BypassActors.PageInfo.HasNextPage { + break + } + variables["cursor"] = githubv4.NewString(query.Repository.Ruleset.BypassActors.PageInfo.EndCursor) + } + + return bypassActors +} + +//// TRANSFORM FUNCTION + +// The timestamp value we are receiving has the layout "2024-06-11 13:18:48 +0000 UTC". +// Our generic timestamp function does not support converting this specific layout to the desired format. +// Additionally, it is not feasible to create a generic function that handles all possible timestamp layouts. +// Therefore, we have opted to implement a specific timestamp conversion function for this table only. +func convertRulesetTimestamp(ctx context.Context, d *transform.TransformData) (interface{}, error) { + if d.Value == nil { + return nil, nil + } + t := d.Value.(string) + + // Parse the timestamp into a time.Time object + parsedTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", t) + if err != nil { + plugin.Logger(ctx).Error("Error parsing time:", err) + return nil, err + } + // Format the time.Time object to RFC 3339 format + rfc3339Time := parsedTime.Format(time.RFC3339) + + return rfc3339Time, nil +}