Skip to content

Commit

Permalink
feature/402 dependabot producer
Browse files Browse the repository at this point in the history
add a producer that reads dependabot alerts from github
  • Loading branch information
northdpole committed Oct 7, 2024
1 parent b6e71cc commit 489e48d
Show file tree
Hide file tree
Showing 4 changed files with 286 additions and 0 deletions.
17 changes: 17 additions & 0 deletions components/producers/github-dependabot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Producer: GitHub Code Scanning

<!--lint disable maximum-line-length-->

This producer [queries the GitHub Code Scanning API](https://docs.github.com/en/rest/code-scanning/code-scanning?apiVersion=2022-11-28#list-code-scanning-alerts-for-a-repository) to produce SAST findings.

## Parameters

All parameters are **required**.

| Name | Type | Default | Description |
| ------------------------------------------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `producer-github-code-scanning-repository-owner` | `string` | N/A | The owner of the repository to scan. |
| `producer-github-code-scanning-repository-name` | `string` | N/A | The name of the repository to scan. |
| `producer-github-code-scanning-github-token` | `string` | N/A | The GitHub token to use for scanning. Must have "Code scanning alerts" repository permissions (read) ([More Information](https://docs.github.com/en/rest/code-scanning/code-scanning?apiVersion=2022-11-28#list-code-scanning-alerts-for-a-repository)). |

<!--lint enable maximum-line-length-->
144 changes: 144 additions & 0 deletions components/producers/github-dependabot/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package main

import (
"context"
"flag"
"log"
"log/slog"
"strconv"
"strings"

"github.com/google/go-github/v65/github"
"github.com/package-url/packageurl-go"

v1 "github.com/ocurity/dracon/api/proto/v1"
"github.com/ocurity/dracon/components/producers"
wrapper "github.com/ocurity/dracon/pkg/github"
)

var (
// RepositoryOwner is the owner of the GitHub repository
RepositoryOwner string

// RepositoryName is the name of the GitHub repository
RepositoryName string

// GitHubToken is the GitHub token used to authenticate
GitHubToken string

// Ref is the Ref/branch to get alerts for
Ref string

// Severity, if specified, only code scanning alerts with this severity will be returned. Possible values are: critical, high, medium, low, warning, note, error
Severity string

// Ecosystem is a comma separated list of at least one of composer, go, maven, npm, nuget, pip, pub, rubygems, rust
Ecosystem string
)

func main() {
flag.StringVar(&RepositoryOwner, "repository-owner", "", "The owner of the GitHub repository")
flag.StringVar(&RepositoryName, "repository-name", "", "The name of the GitHub repository")
flag.StringVar(&GitHubToken, "github-token", "", "The GitHub token used to authenticate with the API")
flag.StringVar(&Ref, "reference", "", "The Ref/branch to get alerts for")
flag.StringVar(&Severity, "severity", "", "If specified, only code scanning alerts with this severity will be returned. Possible values are: critical, high, medium, low, warning, note, error")
flag.StringVar(&Ecosystem, "ecosystem", "", "If specified, a comma separated list of at least one of composer, go, maven, npm, nuget, pip, pub, rubygems, rust")
if err := producers.ParseFlags(); err != nil {
log.Fatal(err)
}

alerts, err := listAlertsForRepo(RepositoryOwner, RepositoryName, GitHubToken)
if err != nil {
log.Fatal(err)
}

issues := parseIssues(alerts)

if err := producers.WriteDraconOut(
"github-code-scanning",
issues,
); err != nil {
log.Fatal(err)
}
}

func listAlertsForRepo(owner, repo, token string) ([]*github.DependabotAlert, error) {
apiClient := wrapper.NewClient(token)
open := "open"
opt := &github.ListAlertsOptions{
State: &open,
Severity: &Severity,
Ecosystem: &Ecosystem,

ListOptions: github.ListOptions{
PerPage: 30,
},
}

var allAlerts []*github.DependabotAlert
for {
alerts, resp, err := apiClient.ListRepoDependabotAlerts(context.Background(), owner, repo, opt)
if err != nil {
return nil, err
}

allAlerts = append(allAlerts, alerts...)

if resp.NextPage == 0 {
break
}
opt.ListOptions.Page = resp.NextPage
}

slog.Info("Successfully fetched alerts", "count", len(allAlerts), "repository", owner+"/"+repo)

return allAlerts, nil
}

func parseIssues(alerts []*github.DependabotAlert) []*v1.Issue {
issues := []*v1.Issue{}
for _, alert := range alerts {
ecosystem := *(alert.GetSecurityVulnerability().Package.Ecosystem)
if ecosystem == "pip" {
ecosystem = "pypi"
}
cwe := []int32{}
for _, c := range alert.SecurityAdvisory.CWEs {
numberOnly := strings.ReplaceAll(*c.CWEID, "CWE-", "")
cweNum, err := strconv.Atoi(numberOnly)

if err != nil {
slog.Error("could not extract cwe number from ", slog.String("cweID", *c.CWEID))
continue
}
cwe = append(cwe, int32(cweNum))
}
issue := &v1.Issue{
Target: producers.GetPURLTarget(ecosystem, "", *alert.GetSecurityVulnerability().Package.Name, "", packageurl.Qualifiers{}, ""),
Cve: *alert.GetSecurityAdvisory().CVEID,
Title: *alert.GetSecurityAdvisory().Summary,
Description: *alert.GetSecurityAdvisory().Description,
Severity: parseGitHubSeverity(*alert.GetSecurityAdvisory().Severity),
Cvss: *alert.SecurityAdvisory.GetCVSS().Score,
Cwe: cwe,
}
issues = append(issues, issue)
}

return issues
}

func parseGitHubSeverity(severity string) v1.Severity {
switch severity {
case "low":
return v1.Severity_SEVERITY_LOW
case "medium":
return v1.Severity_SEVERITY_MEDIUM
case "high":
return v1.Severity_SEVERITY_HIGH
case "critical":
return v1.Severity_SEVERITY_CRITICAL
default:
return v1.Severity_SEVERITY_UNSPECIFIED
}
}
85 changes: 85 additions & 0 deletions components/producers/github-dependabot/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"testing"

"github.com/google/go-github/v65/github"
"github.com/stretchr/testify/require"

v1 "github.com/ocurity/dracon/api/proto/v1"
)

func TestParseIssues(t *testing.T) {
require.Fail(t, "unimplemented")

alerts := []*github.DependabotAlert{}

issues := parseIssues(alerts)

expected := []*v1.Issue{
{
Target: "file://spec-main/api-session-spec.ts:917-918",
Type: "1",
Title: "Test description",
Severity: v1.Severity_SEVERITY_LOW,
Cvss: 0,
Confidence: v1.Confidence_CONFIDENCE_UNSPECIFIED,
Description: "Test message",
Source: "https://example.com",
Cwe: []int32{22},
},
}

require.Equal(t, expected, issues)
}

func TestParseGitHubSeverity(t *testing.T) {
testCases := []struct {
name string
severity string
expected v1.Severity
}{
{
name: "low severity",
severity: "low",
expected: v1.Severity_SEVERITY_LOW,
},
{
name: "medium severity",
severity: "medium",
expected: v1.Severity_SEVERITY_MEDIUM,
},
{
name: "high severity",
severity: "high",
expected: v1.Severity_SEVERITY_HIGH,
},
{
name: "critical severity",
severity: "critical",
expected: v1.Severity_SEVERITY_CRITICAL,
},
{
name: "unspecified severity",
severity: "unknown",
expected: v1.Severity_SEVERITY_UNSPECIFIED,
},
{
name: "empty severity",
severity: "",
expected: v1.Severity_SEVERITY_UNSPECIFIED,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
severity := parseGitHubSeverity(tc.severity)
require.Equal(t, tc.expected, severity)
})
}

}

func TestListAlertsForRepo(t *testing.T) {
require.Fail(t, "unimplemented")
}
40 changes: 40 additions & 0 deletions components/producers/github-dependabot/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: producer-github-code-scanning
labels:
v1.dracon.ocurity.com/component: producer
v1.dracon.ocurity.com/test-type: sast
spec:
description: Retrieve a GitHub Code Scanning report from a GitHub repository.
params:
- name: producer-github-code-scanning-repository-owner
description: The owner of the repository to scan.
type: string
- name: producer-github-code-scanning-repository-name
description: The name of the repository to scan.
type: string
- name: producer-github-code-scanning-github-token
description: The GitHub token to use for scanning. Must have "Code scanning alerts" repository permissions (read).
type: string
volumes:
- name: scratch
emptyDir: {}
workspaces:
- name: output
description: The workspace containing the source-code to scan.
steps:
- name: produce-issues
imagePullPolicy: IfNotPresent
image: '{{ default "ghcr.io/ocurity/dracon" .Values.image.registry }}/components/producers/github-code-scanning:{{ .Chart.AppVersion }}'
command: ["/app/components/producers/github-code-scanning/github-code-scanning-parser"]
args:
- "-in=/scratch/out.json"
- "-out=$(workspaces.output.path)/.dracon/producers/github-code-scanning.pb"
- "-github-token=$(params.producer-github-code-scanning-github-token)"
- "-repository-owner=$(params.producer-github-code-scanning-repository-owner)"
- "-repository-name=$(params.producer-github-code-scanning-repository-name)"
volumeMounts:
- mountPath: /scratch
name: scratch

0 comments on commit 489e48d

Please sign in to comment.