diff --git a/docs/tables/github_organization_dependabot_alert.md b/docs/tables/github_organization_dependabot_alert.md new file mode 100644 index 00000000..38a173c4 --- /dev/null +++ b/docs/tables/github_organization_dependabot_alert.md @@ -0,0 +1,52 @@ +# Table: github_organization_dependabot_alert + +The `github_organization_dependabot_alert` table can be used to query information about dependabot alerts from an organization. You must be an owner or security manager for the organization to successfully query dependabot alerts. + +**You must specify the organization** in the where or join clause (`where organization=`, `join github_organization_depedanbot_alert on organization=`). + +## Examples + +### List dependabot alerts + +```sql +select + organization, + state, + dependency_package_ecosystem, + dependency_package_name +from + github_organization_dependabot_alert +where + organization = 'my_org'; +``` + +### List open dependabot alerts + +```sql +select + organization, + state, + dependency_package_ecosystem, + dependency_package_name +from + github_organization_dependabot_alert +where + organization = 'my_org' + and state = 'open'; +``` + +### List open critical dependabot alerts + +```sql +select + organization, + state, + dependency_package_ecosystem, + dependency_package_name +from + github_organization_dependabot_alert +where + organization = 'my_org' + and state = 'open' + and security_advisory_severity = 'critical'; +``` diff --git a/docs/tables/github_repository_dependabot_alert.md b/docs/tables/github_repository_dependabot_alert.md new file mode 100644 index 00000000..44ee250a --- /dev/null +++ b/docs/tables/github_repository_dependabot_alert.md @@ -0,0 +1,49 @@ +# Table: github_repository_dependabot_alert + +The `github_repository_dependabot_alert` table can be used to query information about dependabot alerts from a repository. + +**You must specify which repository** in the where or join clause using the `repository_full_name` column. + +## Examples + +### List dependabot alerts + +```sql +select + state, + dependency_package_ecosystem, + dependency_package_name +from + github_repository_dependabot_alert +where + repository_full_name = 'turbot/steampipe'; +``` + +### List open dependabot alerts + +```sql +select + state, + dependency_package_ecosystem, + dependency_package_name +from + github_repository_dependabot_alert +where + repository_full_name = 'turbot/steampipe' + and state = 'open'; +``` + +### List open critical dependabot alerts + +```sql +select + state, + dependency_package_ecosystem, + dependency_package_name +from + github_repository_dependabot_alert +where + repository_full_name = 'turbot/steampipe' + and state = 'open' + and security_advisory_severity = 'critical'; +``` diff --git a/github/plugin.go b/github/plugin.go index 65adcb5c..13e32f21 100644 --- a/github/plugin.go +++ b/github/plugin.go @@ -39,10 +39,12 @@ func Plugin(ctx context.Context) *plugin.Plugin { "github_my_team": tableGitHubMyTeam(), "github_organization": tableGitHubOrganization(), "github_organization_member": tableGitHubOrganizationMember(), + "github_organization_dependabot_alert": tableGitHubOrganizationDependabotAlert(), "github_pull_request": tableGitHubPullRequest(), "github_rate_limit": tableGitHubRateLimit(ctx), "github_release": tableGitHubRelease(ctx), "github_repository": tableGitHubRepository(), + "github_repository_dependabot_alert": tableGitHubRepositoryDependabotAlert(), "github_search_code": tableGitHubSearchCode(ctx), "github_search_commit": tableGitHubSearchCommit(ctx), "github_search_issue": tableGitHubSearchIssue(ctx), diff --git a/github/table_github_organization_dependabot_alert.go b/github/table_github_organization_dependabot_alert.go new file mode 100644 index 00000000..470e7439 --- /dev/null +++ b/github/table_github_organization_dependabot_alert.go @@ -0,0 +1,299 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v48/github" + "github.com/turbot/steampipe-plugin-sdk/v4/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v4/plugin" + "github.com/turbot/steampipe-plugin-sdk/v4/plugin/transform" +) + +//// TABLE DEFINITION + +func gitHubDependabotAlertColumns() []*plugin.Column { + return []*plugin.Column{ + { + Name: "alert_number", + Type: proto.ColumnType_INT, + Description: "The security alert number.", + Transform: transform.FromField("Number"), + }, + { + Name: "state", + Type: proto.ColumnType_STRING, + Description: "The state of the Dependabot alert.", + }, + { + Name: "dependency_package_ecosystem", + Type: proto.ColumnType_STRING, + Description: "The package's language or package management ecosystem.", + Transform: transform.FromField("Dependency.Package.Ecosystem"), + }, + { + Name: "dependency_package_name", + Type: proto.ColumnType_STRING, + Description: "The unique package name within its ecosystem.", + Transform: transform.FromField("Dependency.Package.Name"), + }, + { + Name: "dependency_manifest_path", + Type: proto.ColumnType_STRING, + Description: "The unique manifestation path within the ecosystem.", + Transform: transform.FromField("Dependency.ManifestPath"), + }, + { + Name: "dependency_scope", + Type: proto.ColumnType_STRING, + Description: "The execution scope of the vulnerable dependency.", + Transform: transform.FromField("Dependency.Scope"), + }, + { + Name: "security_advisory_ghsa_id", + Type: proto.ColumnType_STRING, + Description: "The unique GitHub Security Advisory ID assigned to the advisory.", + Transform: transform.FromField("SecurityAdvisory.GHSAID"), + }, + { + Name: "security_advisory_cve_id", + Type: proto.ColumnType_STRING, + Description: "The unique CVE ID assigned to the advisory.", + Transform: transform.FromField("SecurityAdvisory.CVEID"), + }, + { + Name: "security_advisory_summary", + Type: proto.ColumnType_STRING, + Description: "A short, plain text summary of the advisory.", + Transform: transform.FromField("SecurityAdvisory.Summary"), + }, + { + Name: "security_advisory_description", + Type: proto.ColumnType_STRING, + Description: "A long-form Markdown-supported description of the advisory.", + Transform: transform.FromField("SecurityAdvisory.Description"), + }, + { + Name: "security_advisory_severity", + Type: proto.ColumnType_STRING, + Description: "The severity of the advisory.", + Transform: transform.FromField("SecurityAdvisory.Severity"), + }, + { + Name: "security_advisory_cvss_score", + Type: proto.ColumnType_DOUBLE, + Description: "The overall CVSS score of the advisory.", + Transform: transform.FromField("SecurityAdvisory.CVSs.Score"), + }, + { + Name: "security_advisory_cvss_vector_string", + Type: proto.ColumnType_STRING, + Description: "The full CVSS vector string for the advisory.", + Transform: transform.FromField("SecurityAdvisory.CVSs.VectorString"), + }, + { + Name: "security_advisory_cwes_cweid", + Type: proto.ColumnType_STRING, + Description: "The unique CWE ID.", + Transform: transform.FromField("SecurityAdvisory.CWEs.CWEID"), + }, + { + Name: "security_advisory_cwes_name", + Type: proto.ColumnType_STRING, + Description: "The short, plain text name of the CWE.", + Transform: transform.FromField("SecurityAdvisory.CWEs.Name"), + }, + { + Name: "security_advisory_published_at", + Type: proto.ColumnType_TIMESTAMP, + Description: "The time that the advisory was published.", + Transform: transform.FromField("SecurityAdvisory.PublishedAt").NullIfZero().Transform(convertTimestamp), + }, + { + Name: "security_advisory_updated_at", + Type: proto.ColumnType_TIMESTAMP, + Description: "The time that the advisory was last modified.", + Transform: transform.FromField("SecurityAdvisory.UpdatedAt").NullIfZero().Transform(convertTimestamp), + }, + { + Name: "security_advisory_withdrawn_at", + Type: proto.ColumnType_TIMESTAMP, + Description: "The time that the advisory was withdrawn.", + Transform: transform.FromField("SecurityAdvisory.WithdrawnAt").NullIfZero().Transform(convertTimestamp), + }, + { + Name: "url", + Type: proto.ColumnType_STRING, + Description: "The REST API URL of the alert resource.", + }, + { + Name: "html_url", + Type: proto.ColumnType_STRING, + Description: "The GitHub URL of the alert resource.", + }, + { + Name: "created_at", + Type: proto.ColumnType_TIMESTAMP, + Description: "The time that the alert was created.", + Transform: transform.FromField("CreatedAt").Transform(convertTimestamp), + }, + { + Name: "updated_at", + Type: proto.ColumnType_TIMESTAMP, + Description: "The time that the alert was last updated.", + Transform: transform.FromField("UpdatedAt").Transform(convertTimestamp), + }, + { + Name: "dismissed_at", + Type: proto.ColumnType_TIMESTAMP, + Description: "The time that the alert was dismissed.", + Transform: transform.FromField("DismissedAt").NullIfZero().Transform(convertTimestamp), + }, + { + Name: "dismissed_reason", + Type: proto.ColumnType_STRING, + Description: "The reason that the alert was dismissed.", + }, + { + Name: "dismissed_comment", + Type: proto.ColumnType_STRING, + Description: "An optional comment associated with the alert's dismissal.", + }, + { + Name: "fixed_at", + Type: proto.ColumnType_TIMESTAMP, + Description: "The time that the alert was no longer detected and was considered fixed.", + Transform: transform.FromField("FixedAt").NullIfZero().Transform(convertTimestamp), + }, + } +} + +func tableGitHubOrganizationDependabotAlert() *plugin.Table { + return &plugin.Table{ + Name: "github_organization_dependabot_alert", + Description: "Dependabot alerts from an organization.", + List: &plugin.ListConfig{ + KeyColumns: []*plugin.KeyColumn{ + { + Name: "organization", + Require: plugin.Required, + }, + { + Name: "state", + Require: plugin.Optional, + }, + { + Name: "security_advisory_severity", + Require: plugin.Optional, + }, + { + Name: "dependency_package_ecosystem", + Require: plugin.Optional, + }, + { + Name: "dependency_package_name", + Require: plugin.Optional, + }, + { + Name: "dependency_scope", + Require: plugin.Optional, + }, + }, + ShouldIgnoreError: isNotFoundError([]string{"404"}), + Hydrate: tableGitHubOrganizationDependabotAlertList, + }, + Columns: append( + gitHubDependabotAlertColumns(), + []*plugin.Column{ + { + Name: "organization", + Type: proto.ColumnType_STRING, + Description: "The login name of the organization.", + Transform: transform.FromQual("organization"), + }, + }..., + ), + } +} + +//// LIST FUNCTION + +func tableGitHubOrganizationDependabotAlertList(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + quals := d.KeyColumnQuals + + org := quals["organization"].GetStringValue() + + opt := &github.ListAlertsOptions{ + ListCursorOptions: github.ListCursorOptions{First: 100}, + } + + if quals["state"] != nil { + state := quals["state"].GetStringValue() + opt.State = &state + } + if quals["security_advisory_severity"] != nil { + severity := quals["security_advisory_severity"].GetStringValue() + opt.Severity = &severity + } + if quals["dependency_package_ecosystem"] != nil { + ecosystem := quals["dependency_package_ecosystem"].GetStringValue() + opt.Ecosystem = &ecosystem + } + if quals["dependency_package_name"] != nil { + package_name := quals["dependency_package_name"].GetStringValue() + opt.Package = &package_name + } + if quals["dependency_scope"] != nil { + scope := quals["dependency_scope"].GetStringValue() + opt.Scope = &scope + } + + type ListPageResponse struct { + alerts []*github.DependabotAlert + resp *github.Response + } + + client := connect(ctx, d) + + limit := d.QueryContext.Limit + if limit != nil { + if *limit < int64(opt.ListCursorOptions.First) { + opt.ListCursorOptions.First = int(*limit) + } + } + + listPage := func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + alerts, resp, err := client.Dependabot.ListOrgAlerts(ctx, org, opt) + return ListPageResponse{ + alerts: alerts, + resp: resp, + }, err + } + for { + listPageResponse, err := retryHydrate(ctx, d, h, listPage) + + if err != nil { + return nil, err + } + + listResponse := listPageResponse.(ListPageResponse) + alerts := listResponse.alerts + resp := listResponse.resp + + for _, i := range alerts { + d.StreamListItem(ctx, i) + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.QueryStatus.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + if resp.After == "" { + break + } + + opt.ListCursorOptions.After = resp.After + } + + return nil, nil +} diff --git a/github/table_github_repository_dependabot_alert.go b/github/table_github_repository_dependabot_alert.go new file mode 100644 index 00000000..5b98384f --- /dev/null +++ b/github/table_github_repository_dependabot_alert.go @@ -0,0 +1,189 @@ +package github + +import ( + "context" + + "github.com/google/go-github/v48/github" + "github.com/turbot/steampipe-plugin-sdk/v4/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v4/plugin" + "github.com/turbot/steampipe-plugin-sdk/v4/plugin/transform" +) + +//// TABLE DEFINITION + +func tableGitHubRepositoryDependabotAlert() *plugin.Table { + return &plugin.Table{ + Name: "github_repository_dependabot_alert", + Description: "Dependabot alerts from a repository.", + List: &plugin.ListConfig{ + KeyColumns: []*plugin.KeyColumn{ + { + Name: "repository_full_name", + Require: plugin.Required, + }, + { + Name: "state", + Require: plugin.Optional, + }, + { + Name: "security_advisory_severity", + Require: plugin.Optional, + }, + { + Name: "dependency_package_ecosystem", + Require: plugin.Optional, + }, + { + Name: "dependency_package_name", + Require: plugin.Optional, + }, + { + Name: "dependency_scope", + Require: plugin.Optional, + }, + }, + ShouldIgnoreError: isNotFoundError([]string{"404"}), + Hydrate: tableGitHubRepositoryDependabotAlertList, + }, + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"repository_full_name", "alert_number"}), + ShouldIgnoreError: isNotFoundError([]string{"404"}), + Hydrate: tableGitHubRepositoryDependabotAlertGet, + }, + Columns: append( + gitHubDependabotAlertColumns(), + []*plugin.Column{ + { + Name: "repository_full_name", + Type: proto.ColumnType_STRING, + Transform: transform.FromQual("repository_full_name"), + Description: "The full name of the repository (login/repo-name).", + }, + }..., + ), + } +} + +//// LIST FUNCTION + +func tableGitHubRepositoryDependabotAlertList(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + quals := d.KeyColumnQuals + + fullName := quals["repository_full_name"].GetStringValue() + owner, repo := parseRepoFullName(fullName) + + opt := &github.ListAlertsOptions{ + ListCursorOptions: github.ListCursorOptions{First: 100}, + } + + if quals["state"] != nil { + state := quals["state"].GetStringValue() + opt.State = &state + } + if quals["security_advisory_severity"] != nil { + severity := quals["security_advisory_severity"].GetStringValue() + opt.Severity = &severity + } + if quals["dependency_package_ecosystem"] != nil { + ecosystem := quals["dependency_package_ecosystem"].GetStringValue() + opt.Ecosystem = &ecosystem + } + if quals["dependency_package_name"] != nil { + package_name := quals["dependency_package_name"].GetStringValue() + opt.Package = &package_name + } + if quals["dependency_scope"] != nil { + scope := quals["dependency_scope"].GetStringValue() + opt.Scope = &scope + } + + type ListPageResponse struct { + alerts []*github.DependabotAlert + resp *github.Response + } + + client := connect(ctx, d) + + limit := d.QueryContext.Limit + if limit != nil { + if *limit < int64(opt.ListCursorOptions.First) { + opt.ListCursorOptions.First = int(*limit) + } + } + + listPage := func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + alerts, resp, err := client.Dependabot.ListRepoAlerts(ctx, owner, repo, opt) + return ListPageResponse{ + alerts: alerts, + resp: resp, + }, err + } + for { + listPageResponse, err := retryHydrate(ctx, d, h, listPage) + + if err != nil { + return nil, err + } + + listResponse := listPageResponse.(ListPageResponse) + alerts := listResponse.alerts + resp := listResponse.resp + + for _, i := range alerts { + d.StreamListItem(ctx, i) + + // Context can be cancelled due to manual cancellation or the limit has been hit + if d.QueryStatus.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + if resp.After == "" { + break + } + + opt.ListCursorOptions.After = resp.After + } + + return nil, nil +} + +//// HYDRATE FUNCTIONS + +func tableGitHubRepositoryDependabotAlertGet(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + var owner, repo string + var alertNumber int + + logger := plugin.Logger(ctx) + quals := d.KeyColumnQuals + + alertNumber = int(d.KeyColumnQuals["alert_number"].GetInt64Value()) + fullName := quals["repository_full_name"].GetStringValue() + owner, repo = parseRepoFullName(fullName) + logger.Trace("tableGitHubDependabotAlertGet", "owner", owner, "repo", repo, "alertNumber", alertNumber) + + client := connect(ctx, d) + + type GetResponse struct { + alert *github.DependabotAlert + resp *github.Response + } + + getDetails := func(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + alert, resp, err := client.Dependabot.GetRepoAlert(ctx, owner, repo, alertNumber) + return GetResponse{ + alert: alert, + resp: resp, + }, err + } + + getResponse, err := retryHydrate(ctx, d, h, getDetails) + if err != nil { + return nil, err + } + + getResp := getResponse.(GetResponse) + alert := getResp.alert + + return alert, nil +} diff --git a/go.mod b/go.mod index 38403666..23ff1a07 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/argonsecurity/pipeline-parser v0.1.16 github.com/ghodss/yaml v1.0.0 - github.com/google/go-github/v48 v48.0.0 + github.com/google/go-github/v48 v48.1.0 github.com/sethvargo/go-retry v0.1.0 github.com/shurcooL/githubv4 v0.0.0-20221021030919-a134b1472cc7 github.com/turbot/go-kit v0.4.0 diff --git a/go.sum b/go.sum index f6553467..aaba8ec6 100644 --- a/go.sum +++ b/go.sum @@ -223,8 +223,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-github/v48 v48.0.0 h1:9H5fWVXFK6ZsRriyPbjtnFAkJnoj0WKFtTYfpCRrTm8= -github.com/google/go-github/v48 v48.0.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y= +github.com/google/go-github/v48 v48.1.0 h1:nqPqq+0oRY2AMR/SRskGrrP4nnewPB7e/m2+kbT/UvM= +github.com/google/go-github/v48 v48.1.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=