diff --git a/docker-compose.yaml b/docker-compose.yaml
index 4e4423f0..c58494bb 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -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
diff --git a/pkg/github/client/client.go b/pkg/github/client/client.go
index ae0444f0..fa7993fa 100644
--- a/pkg/github/client/client.go
+++ b/pkg/github/client/client.go
@@ -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)
diff --git a/pkg/github/codescanning.go b/pkg/github/codescanning.go
new file mode 100644
index 00000000..a63abc7d
--- /dev/null
+++ b/pkg/github/codescanning.go
@@ -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
+}
diff --git a/pkg/github/codescanning_handler.go b/pkg/github/codescanning_handler.go
new file mode 100644
index 00000000..be8cfaeb
--- /dev/null
+++ b/pkg/github/codescanning_handler.go
@@ -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
+}
diff --git a/pkg/github/datasource.go b/pkg/github/datasource.go
index ff1b4c98..41e76d10 100644
--- a/pkg/github/datasource.go
+++ b/pkg/github/datasource.go
@@ -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{
@@ -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{
diff --git a/pkg/github/query_handler.go b/pkg/github/query_handler.go
index 333b207e..99a0427f 100644
--- a/pkg/github/query_handler.go
+++ b/pkg/github/query_handler.go
@@ -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
}
diff --git a/pkg/models/client.go b/pkg/models/client.go
index e1d7165e..cffc1a2f 100644
--- a/pkg/models/client.go
+++ b/pkg/models/client.go
@@ -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)
}
diff --git a/pkg/models/codescanning.go b/pkg/models/codescanning.go
new file mode 100644
index 00000000..216f7a8c
--- /dev/null
+++ b/pkg/models/codescanning.go
@@ -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"`
+}
diff --git a/pkg/models/query.go b/pkg/models/query.go
index 09332020..076333e8 100644
--- a/pkg/models/query.go
+++ b/pkg/models/query.go
@@ -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.
@@ -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"`
+}
diff --git a/src/types.ts b/src/types.ts
index 30bb2db6..195adcab 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -47,6 +47,7 @@ export enum GitHubLicenseType {
}
export enum QueryType {
+ Code_Scanning = 'Code_Scanning',
Commits = 'Commits',
Issues = 'Issues',
Contributors = 'Contributors',
diff --git a/src/validation.ts b/src/validation.ts
index 6642f73c..8ede7178 100644
--- a/src/validation.ts
+++ b/src/validation.ts
@@ -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;
diff --git a/src/views/QueryEditor.tsx b/src/views/QueryEditor.tsx
index 1708858b..6ef3c6b4 100644
--- a/src/views/QueryEditor.tsx
+++ b/src/views/QueryEditor.tsx
@@ -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';
@@ -48,6 +49,9 @@ const queryEditors: {
[QueryType.Tags]: {
component: (props: Props, _: (val: any) => void) => ,
},
+ [QueryType.Code_Scanning]: {
+ component: (props: Props, _: (val: any) => void) => ,
+ },
[QueryType.Releases]: {
component: (props: Props, _: (val: any) => void) => ,
},
diff --git a/src/views/QueryEditorCodeScanning.tsx b/src/views/QueryEditorCodeScanning.tsx
new file mode 100644
index 00000000..9e73db3e
--- /dev/null
+++ b/src/views/QueryEditorCodeScanning.tsx
@@ -0,0 +1,4 @@
+import React from 'react';
+
+const QueryEditorCodeScanning = () => <>>;
+export default QueryEditorCodeScanning;