-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
8fba105
feat: Add the beginning of the GitHub data source
tarkatronic b7db6f6
fix: Tune the repo info log down to trace
tarkatronic 456e05c
feat: Build out the GithubDataSource to populate a ProjectCollection
tarkatronic af55100
test: Add an initial test for collecting github findings
tarkatronic 8ed98b9
fix: Fix linting errors in the test
tarkatronic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
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.") | ||
} | ||
} | ||
|
||
if !alertQuery.Organization.Repositories.PageInfo.HasNextPage { | ||
break | ||
} | ||
queryVars["repoCursor"] = githubv4.NewString(alertQuery.Organization.Repositories.PageInfo.EndCursor) | ||
} | ||
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.") | ||
} | ||
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 | ||
} | ||
} | ||
} | ||
if !ownerQuery.Organization.Teams.PageInfo.HasNextPage { | ||
break | ||
} | ||
queryVars["teamCursor"] = githubv4.NewString(ownerQuery.Organization.Teams.PageInfo.EndCursor) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍