Skip to content

Commit

Permalink
refactor(elementsinteractive#9): move gitlab logic to specific reposi…
Browse files Browse the repository at this point in the history
…tory package

A first step to adding github is to isolate gitlab
  • Loading branch information
sacha-c committed Jan 7, 2025
1 parent 10df2a4 commit 95fae77
Show file tree
Hide file tree
Showing 18 changed files with 202 additions and 139 deletions.
8 changes: 5 additions & 3 deletions internal/cli/patrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import (
"os/exec"
"sheriff/internal/config"
"sheriff/internal/git"
"sheriff/internal/gitlab"
"sheriff/internal/patrol"
"sheriff/internal/repo"
"sheriff/internal/scanner"
"sheriff/internal/slack"
"strings"

"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
)

Expand Down Expand Up @@ -124,7 +125,7 @@ func PatrolAction(cCtx *cli.Context) error {
slackToken := cCtx.String(slackTokenFlag)

// Create services
gitlabService, err := gitlab.New(gitlabToken)
gitlabService, err := repo.NewGitlabService(gitlabToken)
if err != nil {
return errors.Join(errors.New("failed to create GitLab service"), err)
}
Expand All @@ -149,7 +150,8 @@ func PatrolAction(cCtx *cli.Context) error {
if warn, err := patrolService.Patrol(config); err != nil {
return errors.Join(errors.New("failed to scan"), err)
} else if warn != nil {
return cli.Exit("Patrol was partially successful, some errors occurred. Check the logs for more information.", 1)
log.Err(warn).Msg("Patrol was partially successful, some errors occurred.")
return cli.Exit("Exiting patrol with partial success", 1)
}

return nil
Expand Down
16 changes: 5 additions & 11 deletions internal/config/patrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@ import (
"errors"
"fmt"
"net/url"
"sheriff/internal/repo"

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

type PlatformType string

const (
Gitlab PlatformType = "gitlab"
Github PlatformType = "github"
)

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

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

if parsed.Scheme == string(Github) {
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(Gitlab) {
} else if parsed.Scheme != string(repo.Gitlab) {
return nil, fmt.Errorf("unsupported platform %v", parsed.Scheme)
}

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

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

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

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

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

func TestGetPatrolConfigurationCLIOverridesFile(t *testing.T) {
want := PatrolConfig{
Locations: []ProjectLocation{{Type: Gitlab, Path: "group1"}, {Type: Gitlab, Path: "group2/project1"}},
Locations: []ProjectLocation{{Type: repo.Gitlab, Path: "group1"}, {Type: repo.Gitlab, Path: "group2/project1"}},
ReportToEmails: []string{"email@gmail.com", "other@gmail.com"},
ReportToSlackChannels: []string{"other-slack-channel"},
ReportToIssue: false,
Expand Down
29 changes: 14 additions & 15 deletions internal/patrol/patrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ import (
"os"
"sheriff/internal/config"
"sheriff/internal/git"
"sheriff/internal/gitlab"
"sheriff/internal/publish"
"sheriff/internal/repo"
"sheriff/internal/scanner"
"sheriff/internal/slack"
"sync"

"github.com/elliotchance/pie/v2"
"github.com/rs/zerolog/log"
gogitlab "github.com/xanzy/go-gitlab"
"golang.org/x/exp/slices"
)

Expand All @@ -30,7 +29,7 @@ type securityPatroller interface {

// sheriffService is the implementation of the SecurityPatroller interface.
type sheriffService struct {
gitlabService gitlab.IService
gitlabService repo.IService
slackService slack.IService
gitService git.IService
osvService scanner.VulnScanner[scanner.OsvReport]
Expand All @@ -39,7 +38,7 @@ type sheriffService struct {
// 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 gitlab.IService, slackService slack.IService, gitService git.IService, osvService scanner.VulnScanner[scanner.OsvReport]) securityPatroller {
func New(gitlabService repo.IService, slackService slack.IService, gitService git.IService, osvService scanner.VulnScanner[scanner.OsvReport]) securityPatroller {
return &sheriffService{
gitlabService: gitlabService,
slackService: slackService,
Expand Down Expand Up @@ -109,7 +108,7 @@ func (s *sheriffService) scanAndGetReports(locations []config.ProjectLocation) (
log.Info().Str("path", tempScanDir).Msg("Created temporary directory")

gitlabLocs := pie.Map(
pie.Filter(locations, func(v config.ProjectLocation) bool { return v.Type == config.Gitlab }),
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")
Expand All @@ -127,10 +126,10 @@ func (s *sheriffService) scanAndGetReports(locations []config.ProjectLocation) (
wg.Add(1)
go func(reportsChan chan<- scanner.Report) {
defer wg.Done()
log.Info().Str("project", project.Name).Msg("Scanning project")
log.Info().Str("project", project.Path).Msg("Scanning project")
if report, err := s.scanProject(project); err != nil {
log.Error().Err(err).Str("project", project.Name).Msg("Failed to scan project, skipping.")
err = errors.Join(fmt.Errorf("failed to scan project %v", project.Name), err)
log.Error().Err(err).Str("project", project.Path).Msg("Failed to scan project, skipping.")
err = errors.Join(fmt.Errorf("failed to scan project %v", project.Path), err)
warn = errors.Join(err, warn)
reportsChan <- scanner.Report{Project: project, Error: true}
} else {
Expand All @@ -154,31 +153,31 @@ func (s *sheriffService) scanAndGetReports(locations []config.ProjectLocation) (
}

// scanProject scans a project for vulnerabilities using the osv scanner.
func (s *sheriffService) scanProject(project gogitlab.Project) (report *scanner.Report, err error) {
func (s *sheriffService) scanProject(project repo.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.Name).Str("dir", dir).Msg("Cloning project")
if err = s.gitService.Clone(dir, project.HTTPURLToRepo); err != nil {
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)
}

config := config.GetProjectConfiguration(project.NameWithNamespace, dir)
config := config.GetProjectConfiguration(project.Path, dir)

// Scan the project
log.Info().Str("project", project.Name).Msg("Running osv-scanner")
log.Info().Str("project", project.Path).Msg("Running osv-scanner")
osvReport, err := s.osvService.Scan(dir)
if err != nil {
log.Error().Err(err).Str("project", project.Name).Msg("Failed to run osv-scanner")
log.Error().Err(err).Str("project", project.Path).Msg("Failed to run osv-scanner")
return nil, errors.Join(errors.New("failed to run osv-scanner"), err)
}

r := s.osvService.GenerateReport(project, osvReport)
log.Info().Str("project", project.Name).Msg("Finished scanning with osv-scanner")
log.Info().Str("project", project.Path).Msg("Finished scanning with osv-scanner")

r.ProjectConfig = config

Expand Down
28 changes: 14 additions & 14 deletions internal/patrol/patrol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package patrol

import (
"sheriff/internal/config"
"sheriff/internal/repo"
"sheriff/internal/scanner"
"testing"

"github.com/slack-go/slack"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/xanzy/go-gitlab"
)

func TestNewService(t *testing.T) {
Expand All @@ -19,7 +19,7 @@ func TestNewService(t *testing.T) {

func TestScanNoProjects(t *testing.T) {
mockGitlabService := &mockGitlabService{}
mockGitlabService.On("GetProjectList", []string{"group/to/scan"}).Return([]gitlab.Project{}, nil)
mockGitlabService.On("GetProjectList", []string{"group/to/scan"}).Return([]repo.Project{}, nil)

mockSlackService := &mockSlackService{}

Expand All @@ -32,7 +32,7 @@ func TestScanNoProjects(t *testing.T) {
svc := New(mockGitlabService, mockSlackService, mockGitService, mockOSVService)

warn, err := svc.Patrol(config.PatrolConfig{
Locations: []config.ProjectLocation{{Type: config.Gitlab, Path: "group/to/scan"}},
Locations: []config.ProjectLocation{{Type: repo.Gitlab, Path: "group/to/scan"}},
ReportToEmails: []string{},
ReportToSlackChannels: []string{"channel"},
ReportToIssue: true,
Expand All @@ -49,7 +49,7 @@ func TestScanNoProjects(t *testing.T) {

func TestScanNonVulnerableProject(t *testing.T) {
mockGitlabService := &mockGitlabService{}
mockGitlabService.On("GetProjectList", []string{"group/to/scan"}).Return([]gitlab.Project{{Name: "Hello World", HTTPURLToRepo: "https://gitlab.com/group/to/scan.git"}}, nil)
mockGitlabService.On("GetProjectList", []string{"group/to/scan"}).Return([]repo.Project{{Name: "Hello World", RepoUrl: "https://gitlab.com/group/to/scan.git"}}, nil)
mockGitlabService.On("CloseVulnerabilityIssue", mock.Anything).Return(nil)

mockSlackService := &mockSlackService{}
Expand All @@ -65,7 +65,7 @@ func TestScanNonVulnerableProject(t *testing.T) {
svc := New(mockGitlabService, mockSlackService, mockGitService, mockOSVService)

warn, err := svc.Patrol(config.PatrolConfig{
Locations: []config.ProjectLocation{{Type: config.Gitlab, Path: "group/to/scan"}},
Locations: []config.ProjectLocation{{Type: repo.Gitlab, Path: "group/to/scan"}},
ReportToEmails: []string{},
ReportToSlackChannels: []string{"channel"},
ReportToIssue: true,
Expand All @@ -82,8 +82,8 @@ func TestScanNonVulnerableProject(t *testing.T) {

func TestScanVulnerableProject(t *testing.T) {
mockGitlabService := &mockGitlabService{}
mockGitlabService.On("GetProjectList", []string{"group/to/scan"}).Return([]gitlab.Project{{Name: "Hello World", HTTPURLToRepo: "https://gitlab.com/group/to/scan.git"}}, nil)
mockGitlabService.On("OpenVulnerabilityIssue", mock.Anything, mock.Anything).Return(&gitlab.Issue{}, nil)
mockGitlabService.On("GetProjectList", []string{"group/to/scan"}).Return([]repo.Project{{Name: "Hello World", RepoUrl: "https://gitlab.com/group/to/scan.git"}}, nil)
mockGitlabService.On("OpenVulnerabilityIssue", mock.Anything, mock.Anything).Return(&repo.Issue{}, nil)

mockSlackService := &mockSlackService{}
mockSlackService.On("PostMessage", "channel", mock.Anything).Return("", nil)
Expand All @@ -106,7 +106,7 @@ func TestScanVulnerableProject(t *testing.T) {
svc := New(mockGitlabService, mockSlackService, mockGitService, mockOSVService)

warn, err := svc.Patrol(config.PatrolConfig{
Locations: []config.ProjectLocation{{Type: config.Gitlab, Path: "group/to/scan"}},
Locations: []config.ProjectLocation{{Type: repo.Gitlab, Path: "group/to/scan"}},
ReportToEmails: []string{},
ReportToSlackChannels: []string{"channel"},
ReportToIssue: true,
Expand Down Expand Up @@ -190,19 +190,19 @@ type mockGitlabService struct {
mock.Mock
}

func (c *mockGitlabService) GetProjectList(paths []string) ([]gitlab.Project, error) {
func (c *mockGitlabService) GetProjectList(paths []string) ([]repo.Project, error) {
args := c.Called(paths)
return args.Get(0).([]gitlab.Project), args.Error(1)
return args.Get(0).([]repo.Project), args.Error(1)
}

func (c *mockGitlabService) CloseVulnerabilityIssue(project gitlab.Project) error {
func (c *mockGitlabService) CloseVulnerabilityIssue(project repo.Project) error {
args := c.Called(project)
return args.Error(0)
}

func (c *mockGitlabService) OpenVulnerabilityIssue(project gitlab.Project, report string) (*gitlab.Issue, error) {
func (c *mockGitlabService) OpenVulnerabilityIssue(project repo.Project, report string) (*repo.Issue, error) {
args := c.Called(project, report)
return args.Get(0).(*gitlab.Issue), args.Error(1)
return args.Get(0).(*repo.Issue), args.Error(1)
}

type mockSlackService struct {
Expand Down Expand Up @@ -232,7 +232,7 @@ func (c *mockOSVService) Scan(dir string) (*scanner.OsvReport, error) {
return args.Get(0).(*scanner.OsvReport), args.Error(1)
}

func (c *mockOSVService) GenerateReport(p gitlab.Project, r *scanner.OsvReport) scanner.Report {
func (c *mockOSVService) GenerateReport(p repo.Project, r *scanner.OsvReport) scanner.Report {
args := c.Called(p, r)
return args.Get(0).(scanner.Report)
}
2 changes: 1 addition & 1 deletion internal/publish/to_console.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func formatReportsMessageForConsole(scanReports []scanner.Report) string {
r.WriteString(fmt.Sprintf("Total number of projects scanned: %v\n", len(scanReports)))
for _, report := range scanReports {
r.WriteString(fmt.Sprintln("---------------------------------"))
r.WriteString(fmt.Sprintf("%v\n", report.Project.PathWithNamespace))
r.WriteString(fmt.Sprintf("%v\n", report.Project.Path))
r.WriteString(fmt.Sprintf("\tProject URL: %v\n", report.Project.WebURL))
r.WriteString(fmt.Sprintf("\tNumber of vulnerabilities: %v\n", len(report.Vulnerabilities)))
}
Expand Down
6 changes: 3 additions & 3 deletions internal/publish/to_console_test.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package publish

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

"github.com/stretchr/testify/assert"
gogitlab "github.com/xanzy/go-gitlab"
)

func TestFormatReportMessageForConsole(t *testing.T) {
reports := []scanner.Report{
{
Project: gogitlab.Project{
Project: repo.Project{
Name: "project1",
WebURL: "http://example.com",
},
Expand All @@ -27,7 +27,7 @@ func TestFormatReportMessageForConsole(t *testing.T) {
},
},
{
Project: gogitlab.Project{
Project: repo.Project{
Name: "project2",
WebURL: "http://example2.com",
},
Expand Down
12 changes: 6 additions & 6 deletions internal/publish/to_gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package publish
import (
"errors"
"fmt"
"sheriff/internal/gitlab"
"sheriff/internal/repo"
"sheriff/internal/scanner"
"strconv"
"sync"
Expand All @@ -22,24 +22,24 @@ var now = time.Now

// PublishAsGitlabIssues creates or updates GitLab Issue reports for the given reports
// It will add the Issue URL to the Report if it was created or updated successfully
func PublishAsGitlabIssues(reports []scanner.Report, s gitlab.IService) (warn error) {
func PublishAsGitlabIssues(reports []scanner.Report, s repo.IService) (warn error) {
var wg sync.WaitGroup
for i := 0; i < len(reports); i++ {
wg.Add(1)
go func() {
defer wg.Done()
if reports[i].IsVulnerable {
if issue, err := s.OpenVulnerabilityIssue(reports[i].Project, formatGitlabIssue(reports[i])); err != nil {
log.Error().Err(err).Str("project", reports[i].Project.Name).Msg("Failed to open or update issue")
err = fmt.Errorf("failed to open or update issue for project %v", reports[i].Project.Name)
log.Error().Err(err).Str("project", reports[i].Project.Path).Msg("Failed to open or update issue")
err = fmt.Errorf("failed to open or update issue for project %v", reports[i].Project.Path)
warn = errors.Join(err, warn)
} else {
reports[i].IssueUrl = issue.WebURL
}
} else {
if err := s.CloseVulnerabilityIssue(reports[i].Project); err != nil {
log.Error().Err(err).Str("project", reports[i].Project.Name).Msg("Failed to close issue")
err = fmt.Errorf("failed to close issue for project %v", reports[i].Project.Name)
log.Error().Err(err).Str("project", reports[i].Project.Path).Msg("Failed to close issue")
err = fmt.Errorf("failed to close issue for project %v", reports[i].Project.Path)
warn = errors.Join(err, warn)
}
}
Expand Down
Loading

0 comments on commit 95fae77

Please sign in to comment.