From 4f2ea19074dcfec126ad07a1d831ecdc876695b4 Mon Sep 17 00:00:00 2001 From: Kanthi Hegde Date: Tue, 30 Jun 2026 18:41:07 +0530 Subject: [PATCH 1/2] Add github_organization_credential_authorization table --- ...b_organization_credential_authorization.md | 209 ++++++++++++++++++ github/plugin.go | 137 ++++++------ ...b_organization_credential_authorization.go | 86 +++++++ 3 files changed, 364 insertions(+), 68 deletions(-) create mode 100644 docs/tables/github_organization_credential_authorization.md create mode 100644 github/table_github_organization_credential_authorization.go diff --git a/docs/tables/github_organization_credential_authorization.md b/docs/tables/github_organization_credential_authorization.md new file mode 100644 index 0000000..2767784 --- /dev/null +++ b/docs/tables/github_organization_credential_authorization.md @@ -0,0 +1,209 @@ +--- +title: "Steampipe Table: github_organization_credential_authorization - Query GitHub Organization Credential Authorizations using SQL" +description: "Allows users to query classic personal access tokens (and SSH keys) that members have authorized to access an organization's resources through SAML single sign-on (SSO)." +folder: "Organization" +--- + +# Table: github_organization_credential_authorization - Query GitHub Organization Credential Authorizations using SQL + +GitHub organizations that enforce SAML single sign-on (SSO) require members to authorize their classic personal access tokens (PATs) and SSH keys before those credentials can access organization resources. A credential authorization records the token (or key) that a member has approved for use against the organization, including who it belongs to, its scopes, when it was authorized, when it was last used, and when it expires. + +## Table Usage Guide + +The `github_organization_credential_authorization` table provides insights into the classic personal access tokens authorized against a GitHub organization. As a security or compliance engineer, use this table to inventory the classic PATs that can reach your organization's resources, review the scopes they have been granted, and identify tokens that are stale, never used, or approaching expiration. The same endpoint also returns authorized SSH keys, which can be distinguished using the `credential_type` column. + +To query this table, the credential must be created under an organization owner. Classic personal access tokens require the `admin:org` scope (specifically `read:org`). + +**Important Notes** +- You must specify the `organization` column in the `where` or `join` clause to query the table. +- This table is only available for organizations on **GitHub Enterprise Cloud** that have **SAML single sign-on (SSO)** enabled. Organizations without SAML SSO return no rows. +- The underlying [SAML SSO authorizations API](https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/orgs#list-saml-sso-authorizations-for-an-organization) returns both personal access tokens and SSH keys. Filter on `credential_type = 'personal access token'` to return only classic PATs. + +## Examples + +### List all credential authorizations for an organization +Inventory every classic personal access token and SSH key that has been authorized to access your organization's resources. + +```sql+postgres +select + login, + credential_id, + credential_type, + scopes, + credential_authorized_at, + credential_accessed_at, + authorized_credential_expires_at +from + github_organization_credential_authorization +where + organization = 'my_org'; +``` + +```sql+sqlite +select + login, + credential_id, + credential_type, + scopes, + credential_authorized_at, + credential_accessed_at, + authorized_credential_expires_at +from + github_organization_credential_authorization +where + organization = 'my_org'; +``` + +### List only classic personal access tokens +Focus on classic PATs (excluding SSH keys) to review the tokens that can access your organization. + +```sql+postgres +select + login, + credential_id, + token_last_eight, + scopes, + authorized_credential_expires_at +from + github_organization_credential_authorization +where + organization = 'my_org' + and credential_type = 'personal access token'; +``` + +```sql+sqlite +select + login, + credential_id, + token_last_eight, + scopes, + authorized_credential_expires_at +from + github_organization_credential_authorization +where + organization = 'my_org' + and credential_type = 'personal access token'; +``` + +### Find tokens that have expired or expire within 30 days +Identify personal access tokens that need to be rotated or removed because they are expired or about to expire. + +```sql+postgres +select + login, + credential_id, + scopes, + authorized_credential_expires_at +from + github_organization_credential_authorization +where + organization = 'my_org' + and authorized_credential_expires_at is not null + and authorized_credential_expires_at < now() + interval '30 days' +order by + authorized_credential_expires_at; +``` + +```sql+sqlite +select + login, + credential_id, + scopes, + authorized_credential_expires_at +from + github_organization_credential_authorization +where + organization = 'my_org' + and authorized_credential_expires_at is not null + and authorized_credential_expires_at < datetime('now', '+30 days') +order by + authorized_credential_expires_at; +``` + +### Find tokens that have never been used +Spot credentials that were authorized but never accessed, which are good candidates for revocation. + +```sql+postgres +select + login, + credential_id, + credential_type, + credential_authorized_at +from + github_organization_credential_authorization +where + organization = 'my_org' + and credential_accessed_at is null; +``` + +```sql+sqlite +select + login, + credential_id, + credential_type, + credential_authorized_at +from + github_organization_credential_authorization +where + organization = 'my_org' + and credential_accessed_at is null; +``` + +### Find tokens with highly privileged scopes +Surface classic PATs that have been granted broad access such as full `repo` or organization administration scopes. + +```sql+postgres +select + login, + credential_id, + scopes +from + github_organization_credential_authorization +where + organization = 'my_org' + and scopes ?| array['repo', 'admin:org', 'delete_repo']; +``` + +```sql+sqlite +select + c.login, + c.credential_id, + c.scopes +from + github_organization_credential_authorization as c, + json_each(c.scopes) as s +where + c.organization = 'my_org' + and s.value in ('repo', 'admin:org', 'delete_repo'); +``` + +### Count authorized tokens per member +Understand which members have the most credentials authorized against the organization. + +```sql+postgres +select + login, + count(*) as authorized_credentials +from + github_organization_credential_authorization +where + organization = 'my_org' +group by + login +order by + authorized_credentials desc; +``` + +```sql+sqlite +select + login, + count(*) as authorized_credentials +from + github_organization_credential_authorization +where + organization = 'my_org' +group by + login +order by + authorized_credentials desc; +``` diff --git a/github/plugin.go b/github/plugin.go index 7915a0d..309f6c6 100644 --- a/github/plugin.go +++ b/github/plugin.go @@ -23,74 +23,75 @@ func Plugin(ctx context.Context) *plugin.Plugin { DefaultTransform: transform.FromGo(), DefaultRetryConfig: retryConfig(), TableMap: map[string]*plugin.Table{ - "github_actions_artifact": tableGitHubActionsArtifact(), - "github_actions_environment_variable": tableGitHubActionsEnvironmentVariable(), - "github_actions_organization_variable": tableGitHubActionsOrganizationVariable(), - "github_actions_repository_runner": tableGitHubActionsRepositoryRunner(), - "github_actions_repository_secret": tableGitHubActionsRepositorySecret(), - "github_actions_repository_variable": tableGitHubActionsRepositoryVariable(), - "github_actions_repository_workflow_run": tableGitHubActionsRepositoryWorkflowRun(), - "github_audit_log": tableGitHubAuditLog(), - "github_blob": tableGitHubBlob(), - "github_branch": tableGitHubBranch(), - "github_branch_protection": tableGitHubBranchProtection(), - "github_code_owner": tableGitHubCodeOwner(), - "github_commit": tableGitHubCommit(), - "github_community_profile": tableGitHubCommunityProfile(), - "github_gist": tableGitHubGist(), - "github_gitignore": tableGitHubGitignore(), - "github_issue": tableGitHubIssue(), - "github_issue_comment": tableGitHubIssueComment(), - "github_license": tableGitHubLicense(), - "github_my_gist": tableGitHubMyGist(), - "github_my_issue": tableGitHubMyIssue(), - "github_my_organization": tableGitHubMyOrganization(), - "github_my_repository": tableGitHubMyRepository(), - "github_my_star": tableGitHubMyStar(), - "github_my_team": tableGitHubMyTeam(), - "github_organization": tableGitHubOrganization(), - "github_organization_dependabot_alert": tableGitHubOrganizationDependabotAlert(), - "github_organization_external_identity": tableGitHubOrganizationExternalIdentity(), - "github_organization_member": tableGitHubOrganizationMember(), - "github_organization_collaborator": tableGitHubOrganizationCollaborator(), - "github_package": tableGitHubPackage(), - "github_package_version": tableGitHubPackageVersion(), - "github_pull_request": tableGitHubPullRequest(), - "github_pull_request_comment": tableGitHubPullRequestComment(), - "github_pull_request_review": tableGitHubPullRequestReview(), - "github_rate_limit": tableGitHubRateLimit(), - "github_rate_limit_graphql": tableGitHubRateLimitGraphQL(), - "github_release": tableGitHubRelease(), - "github_repository": tableGitHubRepository(), - "github_repository_collaborator": tableGitHubRepositoryCollaborator(), - "github_repository_content": tableGitHubRepositoryContent(), - "github_repository_dependabot_alert": tableGitHubRepositoryDependabotAlert(), - "github_repository_deployment": tableGitHubRepositoryDeployment(), - "github_repository_discussion": tableGitHubRepositoryDiscussion(), - "github_repository_environment": tableGitHubRepositoryEnvironment(), - "github_repository_ruleset": tableGitHubRepositoryRuleset(), - "github_repository_sbom": tableGitHubRepositorySbom(), - "github_repository_vulnerability_alert": tableGitHubRepositoryVulnerabilityAlert(), - "github_search_code": tableGitHubSearchCode(), - "github_search_commit": tableGitHubSearchCommit(), - "github_search_issue": tableGitHubSearchIssue(), - "github_search_label": tableGitHubSearchLabel(), - "github_search_pull_request": tableGitHubSearchPullRequest(), - "github_search_repository": tableGitHubSearchRepository(), - "github_search_topic": tableGitHubSearchTopic(), - "github_search_user": tableGitHubSearchUser(), - "github_stargazer": tableGitHubStargazer(), - "github_tag": tableGitHubTag(), - "github_team": tableGitHubTeam(), - "github_team_member": tableGitHubTeamMember(), - "github_team_repository": tableGitHubTeamRepository(), - "github_traffic_clone_daily": tableGitHubTrafficCloneDaily(), - "github_traffic_clone_weekly": tableGitHubTrafficCloneWeekly(), - "github_traffic_view_daily": tableGitHubTrafficViewDaily(), - "github_traffic_view_weekly": tableGitHubTrafficViewWeekly(), - "github_tree": tableGitHubTree(), - "github_user": tableGitHubUser(), - "github_workflow": tableGitHubWorkflow(), + "github_actions_artifact": tableGitHubActionsArtifact(), + "github_actions_environment_variable": tableGitHubActionsEnvironmentVariable(), + "github_actions_organization_variable": tableGitHubActionsOrganizationVariable(), + "github_actions_repository_runner": tableGitHubActionsRepositoryRunner(), + "github_actions_repository_secret": tableGitHubActionsRepositorySecret(), + "github_actions_repository_variable": tableGitHubActionsRepositoryVariable(), + "github_actions_repository_workflow_run": tableGitHubActionsRepositoryWorkflowRun(), + "github_audit_log": tableGitHubAuditLog(), + "github_blob": tableGitHubBlob(), + "github_branch": tableGitHubBranch(), + "github_branch_protection": tableGitHubBranchProtection(), + "github_code_owner": tableGitHubCodeOwner(), + "github_commit": tableGitHubCommit(), + "github_community_profile": tableGitHubCommunityProfile(), + "github_gist": tableGitHubGist(), + "github_gitignore": tableGitHubGitignore(), + "github_issue": tableGitHubIssue(), + "github_issue_comment": tableGitHubIssueComment(), + "github_license": tableGitHubLicense(), + "github_my_gist": tableGitHubMyGist(), + "github_my_issue": tableGitHubMyIssue(), + "github_my_organization": tableGitHubMyOrganization(), + "github_my_repository": tableGitHubMyRepository(), + "github_my_star": tableGitHubMyStar(), + "github_my_team": tableGitHubMyTeam(), + "github_organization": tableGitHubOrganization(), + "github_organization_credential_authorization": tableGitHubOrganizationCredentialAuthorization(), + "github_organization_dependabot_alert": tableGitHubOrganizationDependabotAlert(), + "github_organization_external_identity": tableGitHubOrganizationExternalIdentity(), + "github_organization_member": tableGitHubOrganizationMember(), + "github_organization_collaborator": tableGitHubOrganizationCollaborator(), + "github_package": tableGitHubPackage(), + "github_package_version": tableGitHubPackageVersion(), + "github_pull_request": tableGitHubPullRequest(), + "github_pull_request_comment": tableGitHubPullRequestComment(), + "github_pull_request_review": tableGitHubPullRequestReview(), + "github_rate_limit": tableGitHubRateLimit(), + "github_rate_limit_graphql": tableGitHubRateLimitGraphQL(), + "github_release": tableGitHubRelease(), + "github_repository": tableGitHubRepository(), + "github_repository_collaborator": tableGitHubRepositoryCollaborator(), + "github_repository_content": tableGitHubRepositoryContent(), + "github_repository_dependabot_alert": tableGitHubRepositoryDependabotAlert(), + "github_repository_deployment": tableGitHubRepositoryDeployment(), + "github_repository_discussion": tableGitHubRepositoryDiscussion(), + "github_repository_environment": tableGitHubRepositoryEnvironment(), + "github_repository_ruleset": tableGitHubRepositoryRuleset(), + "github_repository_sbom": tableGitHubRepositorySbom(), + "github_repository_vulnerability_alert": tableGitHubRepositoryVulnerabilityAlert(), + "github_search_code": tableGitHubSearchCode(), + "github_search_commit": tableGitHubSearchCommit(), + "github_search_issue": tableGitHubSearchIssue(), + "github_search_label": tableGitHubSearchLabel(), + "github_search_pull_request": tableGitHubSearchPullRequest(), + "github_search_repository": tableGitHubSearchRepository(), + "github_search_topic": tableGitHubSearchTopic(), + "github_search_user": tableGitHubSearchUser(), + "github_stargazer": tableGitHubStargazer(), + "github_tag": tableGitHubTag(), + "github_team": tableGitHubTeam(), + "github_team_member": tableGitHubTeamMember(), + "github_team_repository": tableGitHubTeamRepository(), + "github_traffic_clone_daily": tableGitHubTrafficCloneDaily(), + "github_traffic_clone_weekly": tableGitHubTrafficCloneWeekly(), + "github_traffic_view_daily": tableGitHubTrafficViewDaily(), + "github_traffic_view_weekly": tableGitHubTrafficViewWeekly(), + "github_tree": tableGitHubTree(), + "github_user": tableGitHubUser(), + "github_workflow": tableGitHubWorkflow(), }, } return p diff --git a/github/table_github_organization_credential_authorization.go b/github/table_github_organization_credential_authorization.go new file mode 100644 index 0000000..a6e43cf --- /dev/null +++ b/github/table_github_organization_credential_authorization.go @@ -0,0 +1,86 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v55/github" + "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 tableGitHubOrganizationCredentialAuthorization() *plugin.Table { + return &plugin.Table{ + Name: "github_organization_credential_authorization", + Description: "Classic personal access tokens (and SSH keys) that members have authorized to access an organization's resources through SAML single sign-on (SSO).", + List: &plugin.ListConfig{ + KeyColumns: []*plugin.KeyColumn{ + {Name: "organization", Require: plugin.Required}, + }, + ShouldIgnoreError: isNotFoundError([]string{"404", "403"}), + Hydrate: tableGitHubOrganizationCredentialAuthorizationList, + }, + Columns: commonColumns([]*plugin.Column{ + {Name: "organization", Type: proto.ColumnType_STRING, Transform: transform.FromQual("organization"), Description: "The login name of the organization the credential is authorized against."}, + {Name: "login", Type: proto.ColumnType_STRING, Description: "The login of the user that owns the underlying credential."}, + {Name: "credential_id", Type: proto.ColumnType_INT, Description: "The unique identifier for the credential."}, + {Name: "credential_type", Type: proto.ColumnType_STRING, Description: "A human-readable description of the credential type, for example 'personal access token' or 'SSH key'."}, + {Name: "token_last_eight", Type: proto.ColumnType_STRING, Description: "The last eight characters of the credential. Only included when the credential is a personal access token."}, + {Name: "scopes", Type: proto.ColumnType_JSON, Description: "The list of OAuth scopes the credential has been granted."}, + {Name: "credential_authorized_at", Type: proto.ColumnType_TIMESTAMP, Transform: transform.FromField("CredentialAuthorizedAt").NullIfZero().Transform(convertTimestamp), Description: "The time when the credential was authorized for use."}, + {Name: "credential_accessed_at", Type: proto.ColumnType_TIMESTAMP, Transform: transform.FromField("CredentialAccessedAt").NullIfZero().Transform(convertTimestamp), Description: "The time when the credential was last accessed. May be null if it was never used."}, + {Name: "authorized_credential_id", Type: proto.ColumnType_INT, Description: "The unique identifier for the underlying authorized credential."}, + {Name: "authorized_credential_note", Type: proto.ColumnType_STRING, Description: "The note attached to the token. Only included when the credential is a personal access token."}, + {Name: "authorized_credential_expires_at", Type: proto.ColumnType_TIMESTAMP, Transform: transform.FromField("AuthorizedCredentialExpiresAt").NullIfZero().Transform(convertTimestamp), Description: "The time when the token expires. Only included when the credential is a personal access token."}, + {Name: "authorized_credential_title", Type: proto.ColumnType_STRING, Description: "The title given to the SSH key. Only included when the credential is an SSH key."}, + {Name: "fingerprint", Type: proto.ColumnType_STRING, Description: "The unique string used to distinguish the credential. Only included when the credential is an SSH key."}, + }), + } +} + +func tableGitHubOrganizationCredentialAuthorizationList(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + client := connect(ctx, d) + + org := d.EqualsQuals["organization"].GetStringValue() + + // Empty check + if org == "" { + return nil, nil + } + + opts := &github.ListOptions{PerPage: 100} + + limit := d.QueryContext.Limit + if limit != nil { + if *limit < int64(opts.PerPage) { + opts.PerPage = int(*limit) + } + } + + for { + creds, resp, err := client.Organizations.ListCredentialAuthorizations(ctx, org, opts) + if err != nil { + plugin.Logger(ctx).Error("github_organization_credential_authorization", "api_error", err) + return nil, err + } + + for _, c := range creds { + if c != nil { + d.StreamListItem(ctx, c) + } + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + if resp.NextPage == 0 { + break + } + + opts.Page = resp.NextPage + } + + return nil, nil +} From e36fd21ffe1fda566b4a2571d7008c09ca0bbb8c Mon Sep 17 00:00:00 2001 From: Kanthi Hegde Date: Wed, 1 Jul 2026 11:34:51 +0530 Subject: [PATCH 2/2] Return error when org is empty --- github/table_github_organization_credential_authorization.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/github/table_github_organization_credential_authorization.go b/github/table_github_organization_credential_authorization.go index a6e43cf..78d2fac 100644 --- a/github/table_github_organization_credential_authorization.go +++ b/github/table_github_organization_credential_authorization.go @@ -2,6 +2,7 @@ package github import ( "context" + "fmt" "github.com/google/go-github/v55/github" "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" @@ -45,7 +46,7 @@ func tableGitHubOrganizationCredentialAuthorizationList(ctx context.Context, d * // Empty check if org == "" { - return nil, nil + return nil, fmt.Errorf("'organization' qual is required for the github_organization_credential_authorization table") } opts := &github.ListOptions{PerPage: 100}