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

feat: Create a GitHub data source using the new Project structure #77

Merged
merged 5 commits into from
Aug 4, 2023
Merged
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 api/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,6 @@
}
queryVars["teamsCursor"] = githubv4.NewString(ownerQuery.Organization.Teams.PageInfo.EndCursor)
}
log.Debug().Any("repos", allRepos).Msg("Repositories loaded.")
log.Trace().Any("repos", allRepos).Msg("Repositories loaded.")

Check warning on line 144 in api/github.go

View check run for this annotation

Codecov / codecov/patch

api/github.go#L144

Added line #L144 was not covered by tests
return allRepos
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/underdog-tech/vulnbot
go 1.20

require (
github.com/deckarep/golang-set/v2 v2.3.0
github.com/gookit/color v1.5.3
github.com/rs/zerolog v1.29.1
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9
Expand All @@ -13,6 +14,7 @@ require (
github.com/stretchr/testify v1.8.3
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/oauth2 v0.8.0
golang.org/x/text v0.9.0
)

require (
Expand All @@ -37,7 +39,6 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g=
github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
Expand Down
42 changes: 33 additions & 9 deletions internal/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
"sync"
"time"

"github.com/shurcooL/githubv4"
"github.com/underdog-tech/vulnbot/api"
"github.com/underdog-tech/vulnbot/config"
"github.com/underdog-tech/vulnbot/logger"
"github.com/underdog-tech/vulnbot/reporting"
"golang.org/x/oauth2"

"github.com/shurcooL/githubv4"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)

func Scan(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -41,14 +41,32 @@
log.Error().Err(err).Msg("Failed to load ENV file.")
}

ghTokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: env.GithubToken},
)
ghOrgLogin := env.GithubOrg
slackToken := env.SlackAuthToken
/****
* NOTE: This is working code at the moment, but will remain commented out
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

* until the collating and reporting has been updated to accept the new format.
dataSources := []querying.DataSource{}

httpClient := oauth2.NewClient(context.Background(), ghTokenSource)
ghClient := githubv4.NewClient(httpClient)
if env.GithubToken != "" {
ghds := querying.NewGithubDataSource(userConfig, env)
dataSources = append(dataSources, &ghds)
}

dswg := new(sync.WaitGroup)
projects := querying.NewProjectCollection()
for _, ds := range dataSources {
dswg.Add(1)
go func(currentDS querying.DataSource) {
err := currentDS.CollectFindings(projects, dswg)
if err != nil {
log.Error().Err(err).Type("datasource", currentDS).Msg("Failed to query datasource")
}
}(ds)
}
dswg.Wait()
log.Trace().Any("projects", projects).Msg("Gathered project information.")
*/

slackToken := env.SlackAuthToken

Check warning on line 69 in internal/scan.go

View check run for this annotation

Codecov / codecov/patch

internal/scan.go#L69

Added line #L69 was not covered by tests

reporters := []reporting.Reporter{}

Expand All @@ -64,6 +82,12 @@
reporters = append(reporters, &reporting.ConsoleReporter{Config: userConfig})

reportTime := time.Now().UTC()
ghTokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: env.GithubToken},
)
httpClient := oauth2.NewClient(context.Background(), ghTokenSource)
ghClient := githubv4.NewClient(httpClient)
ghOrgLogin := env.GithubOrg

Check warning on line 90 in internal/scan.go

View check run for this annotation

Codecov / codecov/patch

internal/scan.go#L85-L90

Added lines #L85 - L90 were not covered by tests
ghOrgName, allRepos := api.QueryGithubOrgVulnerabilities(ghOrgLogin, *ghClient)
repositoryOwners := api.QueryGithubOrgRepositoryOwners(ghOrgLogin, *ghClient)
// Count our vulnerabilities
Expand Down
9 changes: 9 additions & 0 deletions querying/datasource.go
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
package querying

import "sync"

type DataSource interface {
CollectFindings(
*ProjectCollection,
*sync.WaitGroup,
) error
}
4 changes: 2 additions & 2 deletions querying/finding.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ type FindingIdentifierType string
type FindingIdentifierMap map[FindingIdentifierType]string

const (
FindingIdentifierCVE FindingIdentifierType = "cve"
FindingIdentifierGHSA FindingIdentifierType = "ghsa"
FindingIdentifierCVE FindingIdentifierType = "CVE"
FindingIdentifierGHSA FindingIdentifierType = "GHSA"
)

type Finding struct {
Expand Down
229 changes: 229 additions & 0 deletions querying/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package querying

import (
"context"
"sync"

"github.com/shurcooL/githubv4"
"github.com/underdog-tech/vulnbot/config"
"github.com/underdog-tech/vulnbot/logger"
"golang.org/x/oauth2"
)

type githubClient interface {
Query(context.Context, interface{}, map[string]interface{}) error
}

// GithubDataSource is used to pull Dependabot alerts for an individual organization.
type GithubDataSource struct {
GhClient githubClient
orgName string
conf config.Config
ctx context.Context
}

func NewGithubDataSource(conf config.Config, env config.Env) GithubDataSource {
ghTokenSource := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: env.GithubToken},
)
httpClient := oauth2.NewClient(context.Background(), ghTokenSource)
ghClient := githubv4.NewClient(httpClient)

return GithubDataSource{
GhClient: ghClient,
orgName: env.GithubOrg,
conf: conf,
ctx: context.Background(),
}
}

type orgRepo struct {
Name string
Url string
VulnerabilityAlerts struct {
TotalCount int
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
Nodes []struct {
SecurityAdvisory struct {
Description string
Identifiers []struct {
Type string
Value string
}
}
SecurityVulnerability struct {
Severity string
Package struct {
Ecosystem string
Name string
}
}
}
} `graphql:"vulnerabilityAlerts(states: OPEN, first: 100, after: $alertCursor)"`
}

type orgVulnerabilityQuery struct {
Organization struct {
Name string
Login string
Repositories struct {
TotalCount int
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
Nodes []orgRepo
} `graphql:"repositories(orderBy: {field: NAME, direction: ASC}, isFork: false, isArchived: false, first: 100, after: $repoCursor)"`
} `graphql:"organization(login: $login)"`
}

// Ref: https://docs.github.com/en/graphql/reference/enums#securityadvisoryecosystem
var githubEcosystems = map[string]FindingEcosystemType{
"ACTIONS": FindingEcosystemGHA,
"COMPOSER": FindingEcosystemPHP,
"ERLANG": FindingEcosystemErlang,
"GO": FindingEcosystemGo,
"MAVEN": FindingEcosystemJava,
"NPM": FindingEcosystemJS,
"NUGET": FindingEcosystemCSharp,
"PIP": FindingEcosystemPython,
"PUB": FindingEcosystemDart,
"RUBYGEMS": FindingEcosystemRuby,
"RUST": FindingEcosystemRust,
"SWIFT": FindingEcosystemSwift,
}

func (gh *GithubDataSource) CollectFindings(projects *ProjectCollection, wg *sync.WaitGroup) error {
var alertQuery orgVulnerabilityQuery
log := logger.Get()
defer wg.Done()

queryVars := map[string]interface{}{
"login": githubv4.String(gh.orgName),
"repoCursor": (*githubv4.String)(nil), // We pass nil/null to get the first page
"alertCursor": (*githubv4.String)(nil),
}

for {
log.Info().Any("repoCursor", queryVars["repoCursor"]).Msg("Querying GitHub API for repositories with vulnerabilities.")
err := gh.GhClient.Query(gh.ctx, &alertQuery, queryVars)
if err != nil {
log.Error().Err(err).Msg("GitHub repository query failed!")
return err
}

Check warning on line 116 in querying/github.go

View check run for this annotation

Codecov / codecov/patch

querying/github.go#L114-L116

Added lines #L114 - L116 were not covered by tests
for _, repo := range alertQuery.Organization.Repositories.Nodes {
err := gh.processRepoFindings(projects, repo, queryVars["repoCursor"].(*githubv4.String))
if err != nil {
log.Warn().Err(err).Str("repository", repo.Name).Msg("Failed to process findings for repository.")
}

Check warning on line 121 in querying/github.go

View check run for this annotation

Codecov / codecov/patch

querying/github.go#L120-L121

Added lines #L120 - L121 were not covered by tests
}

if !alertQuery.Organization.Repositories.PageInfo.HasNextPage {
break
}
queryVars["repoCursor"] = githubv4.NewString(alertQuery.Organization.Repositories.PageInfo.EndCursor)

Check warning on line 127 in querying/github.go

View check run for this annotation

Codecov / codecov/patch

querying/github.go#L127

Added line #L127 was not covered by tests
}
gh.gatherRepoOwners(projects)
return nil
}

func (gh *GithubDataSource) processRepoFindings(projects *ProjectCollection, repo orgRepo, repoCursor *githubv4.String) error {
log := logger.Get()
project := projects.GetProject(repo.Name)
project.Links["GitHub"] = repo.Url
log.Debug().Str("project", project.Name).Msg("Processing findings for project.")
// TODO: Handle pagination of vulnerabilityAlerts
for _, vuln := range repo.VulnerabilityAlerts.Nodes {
identifiers := FindingIdentifierMap{}
for _, id := range vuln.SecurityAdvisory.Identifiers {
identifiers[FindingIdentifierType(id.Type)] = id.Value
}
log.Debug().Any("identifiers", identifiers).Msg("Processing finding.")
// Utilizing a lambda to account for locks/deferrals
func() {
finding := project.GetFinding(identifiers)
finding.mu.Lock()
defer finding.mu.Unlock()

if finding.Description == "" {
finding.Description = vuln.SecurityAdvisory.Description
}
if finding.Ecosystem == "" {
finding.Ecosystem = githubEcosystems[vuln.SecurityVulnerability.Package.Ecosystem]
}
if finding.PackageName == "" {
finding.PackageName = vuln.SecurityVulnerability.Package.Name
}
}()
}
return nil
}

type orgTeam struct {
Name string
Slug string
Repositories struct {
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
Edges []struct {
Permission string
Node struct {
Name string
}
}
} `graphql:"repositories(orderBy: {field: NAME, direction: ASC}, first: 100, after: $repoCursor)"`
}

type orgRepoOwnerQuery struct {
Organization struct {
Teams struct {
TotalCount int
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
Nodes []orgTeam
} `graphql:"teams(orderBy: {field: NAME, direction: ASC}, first: 100, after: $teamCursor)"`
} `graphql:"organization(login: $login)"`
}

func (gh *GithubDataSource) gatherRepoOwners(projects *ProjectCollection) {
var ownerQuery orgRepoOwnerQuery
log := logger.Get()

queryVars := map[string]interface{}{
"login": githubv4.String(gh.orgName),
"repoCursor": (*githubv4.String)(nil), // We pass nil/null to get the first page
"teamCursor": (*githubv4.String)(nil),
}

for {
log.Info().Msg("Querying GitHub API for repository ownership information.")
err := gh.GhClient.Query(gh.ctx, &ownerQuery, queryVars)
if err != nil {
log.Fatal().Err(err).Msg("Failed to query GitHub for repository ownership.")
}

Check warning on line 210 in querying/github.go

View check run for this annotation

Codecov / codecov/patch

querying/github.go#L209-L210

Added lines #L209 - L210 were not covered by tests
for _, team := range ownerQuery.Organization.Teams.Nodes {
teamConfig, _ := config.GetTeamConfigBySlug(team.Slug, gh.conf.Team)
// TODO: Handle pagination of repositories owned by a team
for _, repo := range team.Repositories.Edges {
switch repo.Permission {
case "ADMIN", "MAINTAIN":
project := projects.GetProject(repo.Node.Name)
project.Owners.Add(teamConfig)
default:
continue

Check warning on line 220 in querying/github.go

View check run for this annotation

Codecov / codecov/patch

querying/github.go#L212-L220

Added lines #L212 - L220 were not covered by tests
}
}
}
if !ownerQuery.Organization.Teams.PageInfo.HasNextPage {
break
}
queryVars["teamCursor"] = githubv4.NewString(ownerQuery.Organization.Teams.PageInfo.EndCursor)

Check warning on line 227 in querying/github.go

View check run for this annotation

Codecov / codecov/patch

querying/github.go#L227

Added line #L227 was not covered by tests
}
}
Loading