Skip to content

Commit 96441ef

Browse files
committed
feat: warning errors to bubble up as cli exit code
1 parent 2016cf9 commit 96441ef

File tree

7 files changed

+114
-57
lines changed

7 files changed

+114
-57
lines changed

internal/cli/patrol.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func PatrolAction(cCtx *cli.Context) error {
144144
patrolService := patrol.New(gitlabService, slackService, gitService, osvService)
145145

146146
// Run the scan
147-
if err := patrolService.Patrol(
147+
if warn, err := patrolService.Patrol(
148148
cCtx.StringSlice(groupsFlag),
149149
cCtx.StringSlice(projectsFlag),
150150
cCtx.Bool(reportGitlabFlag),
@@ -154,6 +154,8 @@ func PatrolAction(cCtx *cli.Context) error {
154154
verbose,
155155
); err != nil {
156156
return errors.Join(errors.New("failed to scan"), err)
157+
} else if warn != nil {
158+
return cli.Exit("Scan was partially successful, some errors occurred. Check the logs for more information.", 1)
157159
}
158160

159161
return nil

internal/gitlab/gitlab.go

Lines changed: 61 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const VulnerabilityIssueTitle = "Sheriff - 🚨 Vulnerability report"
1515

1616
// IService is the interface of the GitLab service as needed by sheriff
1717
type IService interface {
18-
GetProjectList(groupPaths []string, projectPaths []string) ([]gitlab.Project, error)
18+
GetProjectList(groupPaths []string, projectPaths []string) (projects []gitlab.Project, warn error)
1919
CloseVulnerabilityIssue(project gitlab.Project) error
2020
OpenVulnerabilityIssue(project gitlab.Project, report string) (*gitlab.Issue, error)
2121
}
@@ -36,28 +36,28 @@ func New(gitlabToken string) (IService, error) {
3636
return &s, nil
3737
}
3838

39-
func (s *service) GetProjectList(groupPaths []string, projectPaths []string) (projects []gitlab.Project, err error) {
40-
projects, err = s.gatherProjects(projectPaths)
41-
if err != nil {
42-
log.Error().Err(err).Msg("Failed to fetch projects")
43-
err = nil
39+
func (s *service) GetProjectList(groupPaths []string, projectPaths []string) (projects []gitlab.Project, warn error) {
40+
projects, pwarn := s.gatherProjects(projectPaths)
41+
if pwarn != nil {
42+
pwarn = errors.Join(errors.New("errors occured when gathering projects"), pwarn)
43+
warn = errors.Join(pwarn, warn)
4444
}
4545

46-
groupsProjects, err := s.gatherGroupsProjects(groupPaths)
47-
if err != nil {
48-
log.Error().Err(err).Msg("Failed to fetch projects from groups")
49-
err = nil
50-
} else {
51-
projects = append(projects, groupsProjects...)
46+
groupsProjects, gpwarn := s.gatherGroupsProjects(groupPaths)
47+
if gpwarn != nil {
48+
gpwarn = errors.Join(errors.New("errors occured when gathering groups projects"), gpwarn)
49+
warn = errors.Join(gpwarn, warn)
5250
}
5351

52+
projects = append(projects, groupsProjects...)
53+
5454
// Filter unique projects -- there may be duplicates between groups, other groups and projects
5555
projects = filterUniqueProjects(projects)
5656

5757
projectsNamespaces := pie.Map(projects, func(p gitlab.Project) string { return p.PathWithNamespace })
5858
log.Info().Strs("projects", projectsNamespaces).Msg("Projects to scan")
5959

60-
return
60+
return projects, warn
6161
}
6262

6363
// CloseVulnerabilityIssue closes the vulnerability issue for the given project
@@ -160,11 +160,10 @@ func (s *service) getProject(path string) (*gitlab.Project, error) {
160160
return nil, errors.Join(fmt.Errorf("failed to fetch group %v", groupPath), err)
161161
}
162162

163-
projects, err := s.listGroupProjects(group.ID)
164-
if err != nil {
163+
projects, _, lgerr := s.listGroupProjects(group.ID)
164+
if lgerr != nil {
165165
return nil, errors.Join(fmt.Errorf("failed to fetch list of projects like %v", path), err)
166166
}
167-
168167
for _, project := range projects {
169168
if project.PathWithNamespace == path {
170169
return &project, nil
@@ -174,35 +173,41 @@ func (s *service) getProject(path string) (*gitlab.Project, error) {
174173
return nil, fmt.Errorf("project %v not found", path)
175174
}
176175

177-
func (s *service) gatherGroupsProjects(groupPaths []string) (projects []gitlab.Project, err error) {
176+
func (s *service) gatherGroupsProjects(groupPaths []string) (projects []gitlab.Project, warn error) {
178177
for _, groupPath := range groupPaths {
179-
group, err := s.getGroup(groupPath)
180-
if err != nil {
181-
log.Error().Err(err).Str("group", groupPath).Msg("Failed to fetch group")
182-
err = nil
178+
group, gerr := s.getGroup(groupPath)
179+
if gerr != nil {
180+
log.Error().Err(gerr).Str("group", groupPath).Msg("Failed to fetch group")
181+
gerr = errors.Join(fmt.Errorf("failed to fetch group %v", groupPath), gerr)
182+
warn = errors.Join(gerr, warn)
183183
continue
184184
}
185185

186-
groupProjects, err := s.listGroupProjects(group.ID)
187-
if err != nil {
188-
log.Error().Err(err).Str("group", groupPath).Msg("Failed to fetch projects of group")
189-
err = nil
190-
continue
186+
if groupProjects, gpwarn, gperr := s.listGroupProjects(group.ID); gperr != nil {
187+
log.Error().Err(gpwarn).Str("group", groupPath).Msg("Failed to fetch projects of group")
188+
gperr = errors.Join(fmt.Errorf("failed to fetch projects of group %v", groupPath), gperr)
189+
warn = errors.Join(gperr, warn)
190+
} else if gpwarn != nil {
191+
gpwarn = errors.Join(fmt.Errorf("failed to fetch projects of group %v", groupPath), gpwarn)
192+
warn = errors.Join(gpwarn, warn)
193+
194+
projects = append(projects, groupProjects...)
195+
} else {
196+
projects = append(projects, groupProjects...)
191197
}
192-
193-
projects = append(projects, groupProjects...)
194198
}
195199

196200
return
197201
}
198202

199-
func (s *service) gatherProjects(projectPaths []string) (projects []gitlab.Project, err error) {
203+
func (s *service) gatherProjects(projectPaths []string) (projects []gitlab.Project, warn error) {
200204
for _, projectPath := range projectPaths {
201205
log.Info().Str("project", projectPath).Msg("Getting project")
202206
p, err := s.getProject(projectPath)
203207
if err != nil {
204208
log.Error().Err(err).Str("project", projectPath).Msg("Failed to fetch project")
205-
err = nil
209+
err = errors.Join(fmt.Errorf("failed to fetch project %v", projectPath), err)
210+
warn = errors.Join(err, warn)
206211
continue
207212
}
208213

@@ -230,7 +235,7 @@ func (s *service) getVulnerabilityIssue(project gitlab.Project) (issue *gitlab.I
230235
}
231236

232237
// listGroupProjects returns the list of projects for the given group ID
233-
func (s *service) listGroupProjects(groupID int) (projects []gitlab.Project, err error) {
238+
func (s *service) listGroupProjects(groupID int) (projects []gitlab.Project, warn error, err error) {
234239
projectPtrs, response, err := s.client.ListGroupProjects(groupID,
235240
&gitlab.ListGroupProjectsOptions{
236241
Archived: gitlab.Ptr(false),
@@ -242,18 +247,19 @@ func (s *service) listGroupProjects(groupID int) (projects []gitlab.Project, err
242247
},
243248
})
244249
if err != nil {
245-
return projects, errors.Join(errors.New("failed to fetch list of projects"), err)
250+
return nil, nil, errors.Join(errors.New("failed to fetch list of projects"), err)
246251
}
247252

248253
projects, errCount := dereferenceProjectsPointers(projectPtrs)
249254
if errCount > 0 {
250-
log.Error().Int("groupID", groupID).Int("count", errCount).Msg("Found nil projects, skipping them.")
255+
log.Warn().Int("groupID", groupID).Int("count", errCount).Msg("Found nil projects, skipping them.")
251256
}
252257

253258
if response.TotalPages > 1 {
254-
nextProjects, err := s.listGroupNextProjects(groupID, response.TotalPages)
255-
if err != nil {
256-
return nil, err
259+
nextProjects, lgwarn := s.listGroupNextProjects(groupID, response.TotalPages)
260+
if lgwarn != nil {
261+
lgwarn = errors.Join(errors.New("errors occured when fetching next pages"), lgwarn)
262+
warn = errors.Join(lgwarn, warn)
257263
}
258264

259265
projects = append(projects, nextProjects...)
@@ -262,10 +268,20 @@ func (s *service) listGroupProjects(groupID int) (projects []gitlab.Project, err
262268
return
263269
}
264270

271+
func ToChan[T any](s []T) <-chan T {
272+
ch := make(chan T, len(s))
273+
for _, e := range s {
274+
ch <- e
275+
}
276+
close(ch)
277+
return ch
278+
}
279+
265280
// listGroupNextProjects returns the list of projects for the given group ID from the next pages
266-
func (s *service) listGroupNextProjects(groupID int, totalPages int) (projects []gitlab.Project, err error) {
281+
func (s *service) listGroupNextProjects(groupID int, totalPages int) (projects []gitlab.Project, warn error) {
267282
var wg sync.WaitGroup
268283
nextProjectsChan := make(chan []gitlab.Project, totalPages)
284+
warnChan := make(chan error, totalPages)
269285
for p := 2; p <= totalPages; p++ {
270286
wg.Add(1)
271287

@@ -284,24 +300,31 @@ func (s *service) listGroupNextProjects(groupID int, totalPages int) (projects [
284300
})
285301
if err != nil {
286302
log.Error().Err(err).Int("groupID", groupID).Int("page", p).Msg("Failed to fetch projects of next page, these projects will be missing.")
303+
warnChan <- err
287304
}
288305

289306
projects, errCount := dereferenceProjectsPointers(projectPtrs)
290307
if errCount > 0 {
291-
log.Error().Int("groupID", groupID).Int("page", p).Int("count", errCount).Msg("Found nil projects, skipping them.")
308+
log.Warn().Int("groupID", groupID).Int("page", p).Int("count", errCount).Msg("Found nil projects, skipping them.")
292309
}
293310

294311
nextProjectsChan <- projects
295312
}(nextProjectsChan)
296313
}
297314
wg.Wait()
298315
close(nextProjectsChan)
316+
close(warnChan)
299317

300318
// Collect projects
301319
for nextProjects := range nextProjectsChan {
302320
projects = append(projects, nextProjects...)
303321
}
304322

323+
// Collect warnings
324+
for w := range warnChan {
325+
warn = errors.Join(w, warn)
326+
}
327+
305328
return
306329
}
307330

internal/patrol/patrol.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const projectConfigFileName = "sheriff.toml"
2424
// securityPatroller is the interface of the main security scanner service of this tool.
2525
type securityPatroller interface {
2626
// Scans the given Gitlab groups and projects, creates and publishes the necessary reports
27-
Patrol(grouiPaths []string, projectPaths []string, gitlabIssue bool, slackChannel string, reportProjectSlack bool, silentReport bool, verbose bool) error
27+
Patrol(grouiPaths []string, projectPaths []string, gitlabIssue bool, slackChannel string, reportProjectSlack bool, silentReport bool, verbose bool) (warn error, err error)
2828
}
2929

3030
// sheriffService is the implementation of the SecurityPatroller interface.
@@ -48,20 +48,28 @@ func New(gitlabService gitlab.IService, slackService slack.IService, gitService
4848
}
4949

5050
// Patrol scans the given Gitlab groups and projects, creates and publishes the necessary reports.
51-
func (s *sheriffService) Patrol(groupPaths []string, projectPaths []string, gitlabIssue bool, slackChannel string, reportProjectSlack bool, silentReport bool, verbose bool) error {
52-
scanReports, err := s.scanAndGetReports(groupPaths, projectPaths)
51+
func (s *sheriffService) Patrol(groupPaths []string, projectPaths []string, gitlabIssue bool, slackChannel string, reportProjectSlack bool, silentReport bool, verbose bool) (warn error, err error) {
52+
scanReports, swarn, err := s.scanAndGetReports(groupPaths, projectPaths)
5353
if err != nil {
54-
return errors.Join(errors.New("failed to scan projects"), err)
54+
return nil, errors.Join(errors.New("failed to scan projects"), err)
55+
}
56+
if swarn != nil {
57+
swarn = errors.Join(errors.New("errors occured when scanning projects"), swarn)
58+
warn = errors.Join(swarn, warn)
5559
}
5660

5761
if len(scanReports) == 0 {
5862
log.Warn().Msg("No reports found. Check if projects and group paths are correct, and check the logs for any earlier errors.")
59-
return nil
63+
return swarn, nil
6064
}
6165

6266
if gitlabIssue {
6367
log.Info().Msg("Creating issue in affected projects")
64-
publish.PublishAsGitlabIssues(scanReports, s.gitlabService)
68+
if gwarn := publish.PublishAsGitlabIssues(scanReports, s.gitlabService); gwarn != nil {
69+
gwarn = errors.Join(errors.New("errors occured when creating issues"), gwarn)
70+
warn = errors.Join(gwarn, warn)
71+
}
72+
6573
}
6674

6775
if s.slackService != nil {
@@ -70,33 +78,40 @@ func (s *sheriffService) Patrol(groupPaths []string, projectPaths []string, gitl
7078

7179
if err := publish.PublishAsGeneralSlackMessage(slackChannel, scanReports, groupPaths, projectPaths, s.slackService); err != nil {
7280
log.Error().Err(err).Msg("Failed to post slack report")
81+
err = errors.Join(errors.New("failed to post slack report"), err)
82+
warn = errors.Join(err, warn)
7383
}
7484
}
7585

7686
if reportProjectSlack {
7787
log.Info().Msg("Posting report to project slack channel")
78-
publish.PublishAsSpecificChannelSlackMessage(scanReports, s.slackService)
88+
if swarn := publish.PublishAsSpecificChannelSlackMessage(scanReports, s.slackService); swarn != nil {
89+
swarn = errors.Join(errors.New("errors occured when posting to project slack channel"), swarn)
90+
warn = errors.Join(swarn, warn)
91+
}
92+
7993
}
8094
}
8195

8296
publish.PublishToConsole(scanReports, silentReport)
8397

84-
return nil
98+
return warn, nil
8599
}
86100

87-
func (s *sheriffService) scanAndGetReports(groupPaths []string, projectPaths []string) (reports []scanner.Report, err error) {
101+
func (s *sheriffService) scanAndGetReports(groupPaths []string, projectPaths []string) (reports []scanner.Report, warn error, err error) {
88102
// Create a temporary directory to store the scans
89103
err = os.MkdirAll(tempScanDir, os.ModePerm)
90104
if err != nil {
91-
return nil, errors.New("could not create temporary directory")
105+
return nil, nil, errors.New("could not create temporary directory")
92106
}
93107
defer os.RemoveAll(tempScanDir)
94108
log.Info().Str("path", tempScanDir).Msg("Created temporary directory")
95109
log.Info().Strs("groups", groupPaths).Strs("projects", projectPaths).Msg("Getting the list of projects to scan")
96110

97-
projects, err := s.gitlabService.GetProjectList(groupPaths, projectPaths)
98-
if err != nil {
99-
return nil, errors.Join(errors.New("could not get project list"), err)
111+
projects, pwarn := s.gitlabService.GetProjectList(groupPaths, projectPaths)
112+
if pwarn != nil {
113+
pwarn = errors.Join(errors.New("errors occured when getting project list"), pwarn)
114+
warn = errors.Join(pwarn, warn)
100115
}
101116

102117
// Scan all projects in parallel
@@ -109,6 +124,8 @@ func (s *sheriffService) scanAndGetReports(groupPaths []string, projectPaths []s
109124
log.Info().Str("project", project.Name).Msg("Scanning project")
110125
if report, err := s.scanProject(project); err != nil {
111126
log.Error().Err(err).Str("project", project.Name).Msg("Failed to scan project, skipping.")
127+
err = errors.Join(fmt.Errorf("failed to scan project %v", project.Name), err)
128+
warn = errors.Join(err, warn)
112129
reportsChan <- scanner.Report{Project: project, Error: true}
113130
} else {
114131
reportsChan <- *report

internal/patrol/patrol_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ func TestScanNoProjects(t *testing.T) {
3030

3131
svc := New(mockGitlabService, mockSlackService, mockGitService, mockOSVService)
3232

33-
err := svc.Patrol([]string{"group/to/scan"}, []string{}, true, "channel", true, false, false)
33+
warn, err := svc.Patrol([]string{"group/to/scan"}, []string{}, true, "channel", true, false, false)
3434

3535
assert.Nil(t, err)
36+
assert.Nil(t, warn)
3637
mockGitlabService.AssertExpectations(t)
3738
mockSlackService.AssertExpectations(t)
3839
}
@@ -54,9 +55,10 @@ func TestScanNonVulnerableProject(t *testing.T) {
5455

5556
svc := New(mockGitlabService, mockSlackService, mockGitService, mockOSVService)
5657

57-
err := svc.Patrol([]string{"group/to/scan"}, []string{}, true, "channel", true, false, false)
58+
warn, err := svc.Patrol([]string{"group/to/scan"}, []string{}, true, "channel", true, false, false)
5859

5960
assert.Nil(t, err)
61+
assert.Nil(t, warn)
6062
mockGitlabService.AssertExpectations(t)
6163
mockSlackService.AssertExpectations(t)
6264
}
@@ -86,9 +88,10 @@ func TestScanVulnerableProject(t *testing.T) {
8688

8789
svc := New(mockGitlabService, mockSlackService, mockGitService, mockOSVService)
8890

89-
err := svc.Patrol([]string{"group/to/scan"}, []string{}, true, "channel", true, false, false)
91+
warn, err := svc.Patrol([]string{"group/to/scan"}, []string{}, true, "channel", true, false, false)
9092

9193
assert.Nil(t, err)
94+
assert.Nil(t, warn)
9295
mockGitlabService.AssertExpectations(t)
9396
mockSlackService.AssertExpectations(t)
9497
}

0 commit comments

Comments
 (0)