Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GitHub Security Code Scanning section #377

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
context: ./.config
args:
grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise}
grafana_version: ${GRAFANA_VERSION:-10.4.2}
grafana_version: ${GRAFANA_VERSION:-latest}
development: ${DEVELOPMENT:-true}
ports:
- 3000:3000/tcp
Expand Down
9 changes: 9 additions & 0 deletions pkg/github/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ func (client *Client) ListWorkflows(ctx context.Context, owner, repo string, opt
return wf, resp, err
}

// / ListAlertsForRepo sends a request to the GitHub rest API to list the code scanning alerts in a specific repository.
func (client *Client) ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error) {
alerts, resp, err := client.restClient.CodeScanning.ListAlertsForRepo(ctx, owner, repo, opts)
if err != nil {
return nil, nil, addErrorSourceToError(err, resp)
}
return alerts, resp, err
}

// GetWorkflowUsage returns the workflow usage for a specific workflow.
func (client *Client) GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (models.WorkflowUsage, error) {
actors := make(map[string]struct{}, 0)
Expand Down
163 changes: 163 additions & 0 deletions pkg/github/codescanning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package github

import (
"context"
"strings"
"time"

googlegithub "github.com/google/go-github/v53/github"
"github.com/grafana/github-datasource/pkg/models"
"github.com/grafana/grafana-plugin-sdk-go/data"
)

type CodeScanningWrapper []*googlegithub.Alert

func (alerts CodeScanningWrapper) Frames() data.Frames {
frames := data.NewFrame("code_scanning_alerts",
data.NewField("number", nil, []*int64{}),
data.NewField("created_at", nil, []time.Time{}),
data.NewField("updated_at", nil, []time.Time{}),
data.NewField("dismissed_at", nil, []*time.Time{}),
data.NewField("url", nil, []string{}),
data.NewField("state", nil, []string{}),
data.NewField("dismissed_by", nil, []string{}),
data.NewField("dismissed_reason", nil, []string{}),
data.NewField("dismissed_comment", nil, []string{}),
data.NewField("rule_id", nil, []string{}),
data.NewField("rule_severity", nil, []string{}),
data.NewField("rule_security_severity_level", nil, []string{}),
data.NewField("rule_description", nil, []string{}),
data.NewField("rule_full_description", nil, []string{}),
data.NewField("rule_tags", nil, []string{}),
data.NewField("rule_help", nil, []string{}),
data.NewField("tool_name", nil, []string{}),
data.NewField("tool_version", nil, []string{}),
data.NewField("tool_guid", nil, []string{}),
)

for _, alert := range alerts {
frames.AppendRow(
func() *int64 {
num := int64(alert.GetNumber())
return &num
}(),
func() time.Time {
if !alert.GetCreatedAt().Time.IsZero() {
return alert.GetCreatedAt().Time
}
return time.Time{}
}(),
func() time.Time {
if !alert.GetUpdatedAt().Time.IsZero() {
return alert.GetUpdatedAt().Time
}
return time.Time{}
}(),
func() *time.Time {
if !alert.GetDismissedAt().Time.IsZero() {
t := alert.GetDismissedAt().Time
return &t
}
return nil
}(),
func() string {
str := alert.GetHTMLURL()
return str
}(),
func() string {
str := alert.GetState()
return str
}(),
func() string {
if alert.GetDismissedBy() != nil {
str := alert.GetDismissedBy().GetLogin()
return str
}
return ""
}(),
func() string {
str := alert.GetDismissedReason()
return str
}(),
func() string {
str := alert.GetDismissedComment()
return str
}(),
func() string {
if alert.GetRule() != nil {
return *alert.GetRule().ID
}
return ""
}(),
func() string {
if alert.GetRule() != nil {
return *alert.GetRule().Severity
}
return ""
}(),
func() string {
if alert.GetRule() != nil {
return *alert.GetRule().SecuritySeverityLevel
}
return ""
}(),
func() string {
if alert.GetRule() != nil && alert.GetRule().Description != nil {
return *alert.GetRule().Description
}
return ""
}(),
func() string {
if alert.GetRule() != nil && alert.GetRule().FullDescription != nil {
return *alert.GetRule().FullDescription
}
return ""
}(),
func() string {
if alert.GetRule() != nil {
str := strings.Join(alert.GetRule().Tags, ", ")
return str
}
return ""
}(),
func() string {
if alert.GetRule() != nil {
return *alert.GetRule().Help
}
return ""
}(),
func() string {
if alert.GetTool() != nil && alert.GetTool().Name != nil {
return *alert.GetTool().Name
}
return ""
}(),
func() string {
if alert.GetTool() != nil && alert.GetTool().Version != nil {
return *alert.GetTool().Version
}
return ""
}(),
func() string {
if alert.GetTool() != nil && alert.GetTool().GUID != nil {
return *alert.GetTool().GUID
}
return ""
}(),
)
}

return data.Frames{frames}
}

// Function to get a list of alerts for a repository
// GET /repos/{owner}/{repo}/code-scanning/alerts
// https://docs.github.com/en/rest/reference/code-scanning#get-a-list-of-code-scanning-alerts-for-a-repository
func GetCodeScanningAlerts(context context.Context, owner, repo string, c models.Client) (CodeScanningWrapper, error) {
alerts, _, err := c.ListAlertsForRepo(context, owner, repo, &googlegithub.AlertListOptions{ListOptions: googlegithub.ListOptions{Page: 1, PerPage: 100}})
if err != nil {
return nil, err
}

return CodeScanningWrapper(alerts), nil
}
24 changes: 24 additions & 0 deletions pkg/github/codescanning_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package github

import (
"context"

"github.com/grafana/github-datasource/pkg/dfutil"
"github.com/grafana/github-datasource/pkg/models"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)

func (s *QueryHandler) handleCodeScanningRequests(ctx context.Context, q backend.DataQuery) backend.DataResponse {
query := &models.CodeScanningQuery{}
if err := UnmarshalQuery(q.JSON, query); err != nil {
return *err
}
return dfutil.FrameResponseWithError(s.Datasource.HandleCodeScanningQuery(ctx, query, q))
}

// handleCodeScanning handles the plugin query for github code scanning
func (s *QueryHandler) HandleCodeScanning(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return &backend.QueryDataResponse{
Responses: processQueries(ctx, req, s.handleCodeScanningRequests),
}, nil
}
16 changes: 16 additions & 0 deletions pkg/github/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ func (d *Datasource) HandleVulnerabilitiesQuery(ctx context.Context, query *mode
return GetAllVulnerabilities(ctx, d.client, opt)
}

// ListAlertsForRepo is the query handler for listing GitHub Security Alerts
func (d *Datasource) ListAlertsForRepo(ctx context.Context, query *models.CodeScanningQuery, req backend.DataQuery) (dfutil.Framer, error) {
opt := models.ListCodeScanningOptions{
Repository: query.Repository,
Owner: query.Owner,
}

return d.HandleCodeScanningQuery(ctx, &models.CodeScanningQuery{Query: query.Query, Options: opt}, req)
}

// HandleProjectsQuery is the query handler for listing GitHub Projects
func (d *Datasource) HandleProjectsQuery(ctx context.Context, query *models.ProjectsQuery, req backend.DataQuery) (dfutil.Framer, error) {
opt := models.ProjectOptions{
Expand Down Expand Up @@ -179,6 +189,12 @@ func (d *Datasource) HandleWorkflowUsageQuery(ctx context.Context, query *models
return GetWorkflowUsage(ctx, d.client, opt, req.TimeRange)
}

// HandleCodeScanningQuery is the query handler for listing code scanning alerts of a GitHub repository
func (d *Datasource) HandleCodeScanningQuery(ctx context.Context, query *models.CodeScanningQuery, req backend.DataQuery) (dfutil.Framer, error) {

return GetCodeScanningAlerts(ctx, query.Owner, query.Repository, d.client)
}

// CheckHealth is the health check for GitHub
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
_, err := GetAllRepositories(ctx, d.client, models.ListRepositoriesOptions{
Expand Down
1 change: 1 addition & 0 deletions pkg/github/query_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func GetQueryHandlers(s *QueryHandler) *datasource.QueryTypeMux {
mux.HandleFunc(models.QueryTypeStargazers, s.HandleStargazers)
mux.HandleFunc(models.QueryTypeWorkflows, s.HandleWorkflows)
mux.HandleFunc(models.QueryTypeWorkflowUsage, s.HandleWorkflowUsage)
mux.HandleFunc(models.QueryTypeCodeScanning, s.HandleCodeScanning)

return mux
}
2 changes: 2 additions & 0 deletions pkg/models/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ type Client interface {
Query(ctx context.Context, q interface{}, variables map[string]interface{}) error
ListWorkflows(ctx context.Context, owner, repo string, opts *googlegithub.ListOptions) (*googlegithub.Workflows, *googlegithub.Response, error)
GetWorkflowUsage(ctx context.Context, owner, repo, workflow string, timeRange backend.TimeRange) (WorkflowUsage, error)
// interface for getting code security alerts
ListAlertsForRepo(ctx context.Context, owner, repo string, opts *googlegithub.AlertListOptions) ([]*googlegithub.Alert, *googlegithub.Response, error)
}
12 changes: 12 additions & 0 deletions pkg/models/codescanning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package models

type ListCodeScanningOptions struct {
// Owner is the owner of the repository (ex: grafana)
Owner string `json:"owner"`

// Repository is the name of the repository being queried (ex: grafana)
Repository string `json:"repository"`

// The field used to check if an entry is in the requested range.
TimeField uint32 `json:"timeField"`
}
8 changes: 8 additions & 0 deletions pkg/models/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const (
QueryTypeWorkflows = "Workflows"
// QueryTypeWorkflowUsage is used when querying a specific workflow usage
QueryTypeWorkflowUsage = "Workflow_Usage"
// QueryTypeCodeScanning is used when querying code scanning alerts for a repository
QueryTypeCodeScanning = "Code_Scanning"
)

// Query refers to the structure of a query built using the QueryEditor.
Expand Down Expand Up @@ -129,3 +131,9 @@ type WorkflowUsageQuery struct {
Query
Options WorkflowUsageOptions `json:"options"`
}

// CodeScanningQuery is used when querying code scanning alerts for a repository
type CodeScanningQuery struct {
Query
Options ListCodeScanningOptions `json:"options"`
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
}

export enum QueryType {
Code_Scanning = 'Code_Scanning',
Commits = 'Commits',
Issues = 'Issues',
Contributors = 'Contributors',
Expand Down Expand Up @@ -154,7 +155,7 @@
USER = 1,
}

export interface GitHubQuery extends Indexable, DataQuery, RepositoryOptions {

Check warning on line 158 in src/types.ts

View workflow job for this annotation

GitHub Actions / Build, lint and unit tests

'DataQuery' is deprecated. use the type from
options?:
| PullRequestsOptions
| ReleasesOptions
Expand Down
3 changes: 2 additions & 1 deletion src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const isValid = (query: GitHubQuery): boolean => {
query.queryType === QueryType.Labels ||
query.queryType === QueryType.Milestones ||
query.queryType === QueryType.Vulnerabilities ||
query.queryType === QueryType.Stargazers
query.queryType === QueryType.Stargazers ||
query.queryType === QueryType.Code_Scanning
) {
if (isEmpty(query.owner) || isEmpty(query.repository)) {
return false;
Expand Down
4 changes: 4 additions & 0 deletions src/views/QueryEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import QueryEditorIssues from './QueryEditorIssues';
import QueryEditorMilestones from './QueryEditorMilestones';
import QueryEditorPullRequests from './QueryEditorPullRequests';
import QueryEditorTags from './QueryEditorTags';
import QueryEditorCodeScanning from './QueryEditorCodeScanning';
import QueryEditorContributors from './QueryEditorContributors';
import QueryEditorLabels from './QueryEditorLabels';
import QueryEditorPackages from './QueryEditorPackages';
Expand Down Expand Up @@ -48,6 +49,9 @@ const queryEditors: {
[QueryType.Tags]: {
component: (props: Props, _: (val: any) => void) => <QueryEditorTags {...(props.query.options || {})} />,
},
[QueryType.Code_Scanning]: {
component: (props: Props, _: (val: any) => void) => <QueryEditorCodeScanning {...(props.query.options || {})} />,
},
[QueryType.Releases]: {
component: (props: Props, _: (val: any) => void) => <QueryEditorReleases {...(props.query.options || {})} />,
},
Expand Down
4 changes: 4 additions & 0 deletions src/views/QueryEditorCodeScanning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import React from 'react';

const QueryEditorCodeScanning = () => <></>;
export default QueryEditorCodeScanning;
Loading