Skip to content

Commit

Permalink
feat(elementsinteractive#9): add github project scanning support
Browse files Browse the repository at this point in the history
  • Loading branch information
sacha-c committed Jan 9, 2025
1 parent b045a01 commit b65d15d
Show file tree
Hide file tree
Showing 27 changed files with 699 additions and 310 deletions.
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{"some-email@gmail.com"},
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@gmail.com", "other@gmail.com"},
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

0 comments on commit b65d15d

Please sign in to comment.