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(#9): add github project scanning support #47

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-github/v68 v68.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s=
github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
Expand Down
19 changes: 13 additions & 6 deletions internal/cli/patrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import (
"fmt"
"os/exec"
"sheriff/internal/config"
"sheriff/internal/git"
"sheriff/internal/patrol"
"sheriff/internal/repo"
"sheriff/internal/repository/provider"
"sheriff/internal/scanner"
"sheriff/internal/slack"
"strings"
Expand All @@ -34,6 +33,7 @@ const reportToSlackChannel = "report-to-slack-channel"
const reportEnableProjectReportToFlag = "report-enable-project-report-to"
const silentReportFlag = "silent"
const gitlabTokenFlag = "gitlab-token"
const githubTokenFlag = "github-token"
const slackTokenFlag = "slack-token"

var necessaryScanners = []string{scanner.OsvCommandName}
Expand Down Expand Up @@ -91,6 +91,13 @@ var PatrolFlags = []cli.Flag{
EnvVars: []string{"GITLAB_TOKEN"},
Category: string(Tokens),
},
&cli.StringFlag{
Name: githubTokenFlag,
Usage: "Token to access the Github API.",
Required: true,
EnvVars: []string{"GITHUB_TOKEN"},
Category: string(Tokens),
},
&cli.StringFlag{
Name: slackTokenFlag,
Usage: "Token to access the Slack API.",
Expand Down Expand Up @@ -122,23 +129,23 @@ func PatrolAction(cCtx *cli.Context) error {

// Get tokens
gitlabToken := cCtx.String(gitlabTokenFlag)
githubToken := cCtx.String(githubTokenFlag)
slackToken := cCtx.String(slackTokenFlag)

// Create services
gitlabService, err := repo.NewGitlabService(gitlabToken)
repositoryService, err := provider.NewProvider(gitlabToken, githubToken)
if err != nil {
return errors.Join(errors.New("failed to create GitLab service"), err)
return errors.Join(errors.New("failed to create repository service"), err)
}

slackService, err := slack.New(slackToken, config.Verbose)
if err != nil {
return errors.Join(errors.New("failed to create Slack service"), err)
}

gitService := git.New(gitlabToken)
osvService := scanner.NewOsvScanner()

patrolService := patrol.New(gitlabService, slackService, gitService, osvService)
patrolService := patrol.New(repositoryService, slackService, osvService)

// Check whether the necessary scanners are available
missingScanners := getMissingScanners(necessaryScanners)
Expand Down
10 changes: 4 additions & 6 deletions internal/config/patrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import (
"errors"
"fmt"
"net/url"
"sheriff/internal/repo"
"sheriff/internal/repository"

zerolog "github.com/rs/zerolog/log"
)

type ProjectLocation struct {
Type repo.PlatformType
Type repository.RepositoryType
Path string
}

Expand Down Expand Up @@ -124,9 +124,7 @@ func parseTargets(targets []string) ([]ProjectLocation, error) {
return nil, fmt.Errorf("target missing platform scheme %v", t)
}

if parsed.Scheme == string(repo.Github) {
return nil, fmt.Errorf("github is currently unsupported, but is on our roadmap 😃") // TODO #9
} else if parsed.Scheme != string(repo.Gitlab) {
if parsed.Scheme != string(repository.Gitlab) && parsed.Scheme != string(repository.Github) {
return nil, fmt.Errorf("unsupported platform %v", parsed.Scheme)
}

Expand All @@ -136,7 +134,7 @@ func parseTargets(targets []string) ([]ProjectLocation, error) {
}

locations[i] = ProjectLocation{
Type: repo.PlatformType(parsed.Scheme),
Type: repository.RepositoryType(parsed.Scheme),
Path: path,
}
}
Expand Down
10 changes: 5 additions & 5 deletions internal/config/patrol_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package config

import (
"sheriff/internal/repo"
"sheriff/internal/repository"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetPatrolConfiguration(t *testing.T) {
want := PatrolConfig{
Locations: []ProjectLocation{{Type: repo.Gitlab, Path: "group1"}, {Type: repo.Gitlab, Path: "group2/project1"}},
Locations: []ProjectLocation{{Type: repository.Gitlab, Path: "group1"}, {Type: repository.Gitlab, Path: "group2/project1"}},
ReportToEmails: []string{"[email protected]"},
ReportToSlackChannels: []string{"report-slack-channel"},
ReportToIssue: true,
Expand All @@ -29,7 +29,7 @@ func TestGetPatrolConfiguration(t *testing.T) {

func TestGetPatrolConfigurationCLIOverridesFile(t *testing.T) {
want := PatrolConfig{
Locations: []ProjectLocation{{Type: repo.Gitlab, Path: "group1"}, {Type: repo.Gitlab, Path: "group2/project1"}},
Locations: []ProjectLocation{{Type: repository.Gitlab, Path: "group1"}, {Type: repository.Gitlab, Path: "group2/project1"}},
ReportToEmails: []string{"[email protected]", "[email protected]"},
ReportToSlackChannels: []string{"other-slack-channel"},
ReportToIssue: false,
Expand Down Expand Up @@ -87,8 +87,8 @@ func TestParseUrls(t *testing.T) {
{[]string{"gitlab://namespace/project"}, &ProjectLocation{Type: "gitlab", Path: "namespace/project"}, false},
{[]string{"gitlab://namespace/subgroup/project"}, &ProjectLocation{Type: "gitlab", Path: "namespace/subgroup/project"}, false},
{[]string{"gitlab://namespace"}, &ProjectLocation{Type: "gitlab", Path: "namespace"}, false},
{[]string{"github://organization"}, &ProjectLocation{Type: "github", Path: "organization"}, true},
{[]string{"github://organization/project"}, &ProjectLocation{Type: "github", Path: "organization/project"}, true},
{[]string{"github://organization"}, &ProjectLocation{Type: "github", Path: "organization"}, false},
{[]string{"github://organization/project"}, &ProjectLocation{Type: "github", Path: "organization/project"}, false},
{[]string{"unknown://namespace/project"}, nil, true},
{[]string{"unknown://not a path"}, nil, true},
{[]string{"not a target"}, nil, true},
Expand Down
19 changes: 0 additions & 19 deletions internal/git/client.go

This file was deleted.

35 changes: 0 additions & 35 deletions internal/git/git.go

This file was deleted.

48 changes: 0 additions & 48 deletions internal/git/git_test.go

This file was deleted.

71 changes: 48 additions & 23 deletions internal/patrol/patrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"fmt"
"os"
"sheriff/internal/config"
"sheriff/internal/git"
"sheriff/internal/publish"
"sheriff/internal/repo"
"sheriff/internal/repository"
"sheriff/internal/repository/provider"
"sheriff/internal/scanner"
"sheriff/internal/slack"
"sync"
Expand All @@ -29,21 +29,19 @@ type securityPatroller interface {

// sheriffService is the implementation of the SecurityPatroller interface.
type sheriffService struct {
gitlabService repo.IService
slackService slack.IService
gitService git.IService
osvService scanner.VulnScanner[scanner.OsvReport]
repoService provider.IProvider
slackService slack.IService
osvService scanner.VulnScanner[scanner.OsvReport]
}

// New creates a new securityPatroller service.
// It contains the main "loop" logic of this tool.
// A "patrol" is defined as scanning GitLab groups for vulnerabilities and publishing reports where needed.
func New(gitlabService repo.IService, slackService slack.IService, gitService git.IService, osvService scanner.VulnScanner[scanner.OsvReport]) securityPatroller {
func New(repoService provider.IProvider, slackService slack.IService, osvService scanner.VulnScanner[scanner.OsvReport]) securityPatroller {
return &sheriffService{
gitlabService: gitlabService,
slackService: slackService,
gitService: gitService,
osvService: osvService,
repoService: repoService,
slackService: slackService,
osvService: osvService,
}
}

Expand All @@ -65,7 +63,7 @@ func (s *sheriffService) Patrol(args config.PatrolConfig) (warn error, err error

if args.ReportToIssue {
log.Info().Msg("Creating issue in affected projects")
if gwarn := publish.PublishAsGitlabIssues(scanReports, s.gitlabService); gwarn != nil {
if gwarn := publish.PublishAsIssues(scanReports, s.repoService); gwarn != nil {
gwarn = errors.Join(errors.New("errors occured when creating issues"), gwarn)
warn = errors.Join(gwarn, warn)
}
Expand Down Expand Up @@ -107,13 +105,7 @@ func (s *sheriffService) scanAndGetReports(locations []config.ProjectLocation) (
defer os.RemoveAll(tempScanDir)
log.Info().Str("path", tempScanDir).Msg("Created temporary directory")

gitlabLocs := pie.Map(
pie.Filter(locations, func(v config.ProjectLocation) bool { return v.Type == repo.Gitlab }),
func(v config.ProjectLocation) string { return v.Path },
)
log.Info().Strs("locations", gitlabLocs).Msg("Getting the list of projects to scan")

projects, pwarn := s.gitlabService.GetProjectList(gitlabLocs)
projects, pwarn := s.getProjectList(locations)
if pwarn != nil {
pwarn = errors.Join(errors.New("errors occured when getting project list"), pwarn)
warn = errors.Join(pwarn, warn)
Expand Down Expand Up @@ -152,18 +144,51 @@ func (s *sheriffService) scanAndGetReports(locations []config.ProjectLocation) (
return
}

func (s *sheriffService) getProjectList(locs []config.ProjectLocation) (projects []repository.Project, warn error) {
gitlabLocs := pie.Map(
pie.Filter(locs, func(loc config.ProjectLocation) bool { return loc.Type == repository.Gitlab }),
func(loc config.ProjectLocation) string { return loc.Path },
)
githubLocs := pie.Map(
pie.Filter(locs, func(loc config.ProjectLocation) bool { return loc.Type == repository.Github }),
func(loc config.ProjectLocation) string { return loc.Path },
)

if len(gitlabLocs) > 0 {
log.Info().Strs("locations", gitlabLocs).Msg("Getting the list of projects from gitlab to scan")
gitlabProjects, err := s.repoService.Provide(repository.Gitlab).GetProjectList(gitlabLocs)
if err != nil {
warn = errors.Join(errors.New("non-critical errors encountered when scanning for gitlab projects"), err)
}

projects = append(projects, gitlabProjects...)
}

if len(githubLocs) > 0 {
log.Info().Strs("locations", githubLocs).Msg("Getting the list of projects from github to scan")
githubProjects, err := s.repoService.Provide(repository.Github).GetProjectList(githubLocs)
if err != nil {
warn = errors.Join(errors.New("non-critical errors encountered when scanning for github projects"), err)
}

projects = append(projects, githubProjects...)
}

return
}

// scanProject scans a project for vulnerabilities using the osv scanner.
func (s *sheriffService) scanProject(project repo.Project) (report *scanner.Report, err error) {
func (s *sheriffService) scanProject(project repository.Project) (report *scanner.Report, err error) {
dir, err := os.MkdirTemp(tempScanDir, fmt.Sprintf("%v-", project.Name))
if err != nil {
return nil, errors.Join(errors.New("failed to create project temporary directory"), err)
}
defer os.RemoveAll(dir)

// Clone the project
log.Info().Str("project", project.Path).Str("dir", dir).Msg("Cloning project")
if err = s.gitService.Clone(dir, project.RepoUrl); err != nil {
return nil, errors.Join(errors.New("failed to clone project"), err)
log.Info().Str("project", project.Path).Str("dir", dir).Str("url", project.RepoUrl).Msg("Cloning project")
if err := s.repoService.Provide(project.Repository).Clone(project.RepoUrl, dir); err != nil {
return nil, errors.Join(fmt.Errorf("failed to clone project %v", project.Path), err)
}

config := config.GetProjectConfiguration(project.Path, dir)
Expand Down
Loading
Loading