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

refactor(#9): move gitlab logic to specific repository package #46

Merged
merged 1 commit into from
Jan 7, 2025
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
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
Loading