Skip to content

Commit 29df26b

Browse files
committed
feat(#17): rework cli interface
1 parent 6addde5 commit 29df26b

File tree

13 files changed

+261
-304
lines changed

13 files changed

+261
-304
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,9 @@ Only the **Reporting** and **Scanning** sections of configuration parameters are
107107
In this case you may choose to create a config file such as the following:
108108

109109
```toml
110-
gitlab-groups = ["namespace/group", "namespace/group/cool-repo"]
111-
gitlab-projects = ["namespace/group/cool-repo"]
112-
report-slack-channel = "sheriff-report-test"
113-
report-gitlab-issue = true
110+
url = ["namespace/group", "namespace/group/cool-repo"]
111+
report-to-slack-channel = "sheriff-report-test"
112+
report-to-gitlab-issue = true
114113
```
115114

116115
And if you wish to specify a different file, you can do so with `sheriff patrol --config your-config-file.toml`.

internal/cli/patrol.go

Lines changed: 64 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,17 @@ package cli
33
import (
44
"errors"
55
"fmt"
6-
"regexp"
6+
"net/url"
77
"sheriff/internal/git"
88
"sheriff/internal/gitlab"
99
"sheriff/internal/patrol"
1010
"sheriff/internal/scanner"
1111
"sheriff/internal/slack"
1212

13-
zerolog "github.com/rs/zerolog/log"
1413
"github.com/urfave/cli/v2"
1514
"github.com/urfave/cli/v2/altsrc"
1615
)
1716

18-
// Regexes very loosely defined based on GitLab's reserved names:
19-
// https://docs.gitlab.com/ee/user/reserved_names.html#limitations-on-usernames-project-and-group-names-and-slugs
20-
// In reality the regex should be more restrictive about special characters, for now we're just checking for slashes and non-whitespace characters.
21-
const groupPathRegex = "^\\S+(\\/\\S+)*$" // Matches paths like "group" or "group/subgroup" ...
22-
const projectPathRegex = "^\\S+(\\/\\S+)+$" // Matches paths like "group/project" or "group/subgroup/project" ...
23-
2417
type CommandCategory string
2518

2619
const (
@@ -32,73 +25,59 @@ const (
3225

3326
const configFlag = "config"
3427
const verboseFlag = "verbose"
35-
const testingFlag = "testing"
36-
const groupsFlag = "gitlab-groups"
37-
const projectsFlag = "gitlab-projects"
38-
const reportSlackChannelFlag = "report-slack-channel"
39-
const reportSlackProjectChannelFlag = "report-slack-project-channel"
40-
const reportGitlabFlag = "report-gitlab-issue"
41-
const silentReport = "silent"
42-
const publicSlackChannelFlag = "public-slack-channel"
28+
const urlFlag = "url"
29+
const reportToEmailFlag = "report-to-email"
30+
const reportToIssueFlag = "report-to-issue"
31+
const reportToSlackChannel = "report-to-slack-channel"
32+
const reportEnableProjectReportToFlag = "report-enable-project-report-to"
33+
const silentReportFlag = "silent"
4334
const gitlabTokenFlag = "gitlab-token"
4435
const slackTokenFlag = "slack-token"
4536

4637
var sensitiveFlags = []string{gitlabTokenFlag, slackTokenFlag}
4738

4839
var PatrolFlags = []cli.Flag{
4940
&cli.StringFlag{
50-
Name: configFlag,
51-
Value: "sheriff.toml",
41+
Name: configFlag,
42+
Aliases: []string{"c"},
43+
Value: "sheriff.toml",
5244
},
5345
&cli.BoolFlag{
5446
Name: verboseFlag,
47+
Aliases: []string{"v"},
5548
Usage: "Enable verbose logging",
5649
Category: string(Miscellaneous),
5750
Value: false,
5851
},
5952
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
60-
Name: groupsFlag,
61-
Usage: "Gitlab groups to scan for vulnerabilities (list argument which can be repeated)",
53+
Name: urlFlag,
54+
Usage: "Groups and projects to scan for vulnerabilities (list argument which can be repeated)",
6255
Category: string(Scanning),
63-
Action: validatePaths(groupPathRegex),
6456
}),
6557
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
66-
Name: projectsFlag,
67-
Usage: "Gitlab projects to scan for vulnerabilities (list argument which can be repeated)",
68-
Category: string(Scanning),
69-
Action: validatePaths(projectPathRegex),
70-
}),
71-
altsrc.NewBoolFlag(&cli.BoolFlag{
72-
Name: testingFlag,
73-
Usage: "Enable testing mode. This can enable features that are not safe for production use.",
74-
Category: string(Miscellaneous),
75-
Value: false,
76-
}),
77-
altsrc.NewStringFlag(&cli.StringFlag{
78-
Name: reportSlackChannelFlag,
79-
Usage: "Enable reporting to Slack through messages in the specified channel.",
58+
Name: reportToEmailFlag,
59+
Usage: "Enable reporting to the provided list of emails",
8060
Category: string(Reporting),
8161
}),
8262
altsrc.NewBoolFlag(&cli.BoolFlag{
83-
Name: reportSlackProjectChannelFlag,
84-
Usage: "Enable reporting to Slack through messages in the specified project's channel. Requires a project-level configuration file specifying the channel.",
63+
Name: reportToIssueFlag,
64+
Usage: "Enable or disable reporting to the project's issue on the associated platform (gitlab, github, ...)",
8565
Category: string(Reporting),
8666
}),
87-
altsrc.NewBoolFlag(&cli.BoolFlag{
88-
Name: reportGitlabFlag,
89-
Usage: "Enable reporting to GitLab through issue creation in projects affected by vulnerabilities.",
67+
altsrc.NewStringFlag(&cli.StringFlag{
68+
Name: reportToSlackChannel,
69+
Usage: "Enable reporting to the provided slack channel",
9070
Category: string(Reporting),
91-
Value: false,
9271
}),
9372
altsrc.NewBoolFlag(&cli.BoolFlag{
94-
Name: silentReport,
95-
Usage: "Disable report output to stdout.",
73+
Name: reportEnableProjectReportToFlag,
74+
Usage: "Enable project-level configuration for '--report-to'.",
9675
Category: string(Reporting),
97-
Value: false,
76+
Value: true,
9877
}),
9978
altsrc.NewBoolFlag(&cli.BoolFlag{
100-
Name: publicSlackChannelFlag,
101-
Usage: "Allow the slack report to be posted to a public channel. Note that reports may contain sensitive information which should not be disclosed on a public channel, for this reason this flag will only be enabled when combined with the testing flag.",
79+
Name: silentReportFlag,
80+
Usage: "Disable report output to stdout.",
10281
Category: string(Reporting),
10382
Value: false,
10483
}),
@@ -121,10 +100,10 @@ var PatrolFlags = []cli.Flag{
121100
func PatrolAction(cCtx *cli.Context) error {
122101
verbose := cCtx.Bool(verboseFlag)
123102

124-
var publicChannelsEnabled bool
125-
if cCtx.Bool(testingFlag) {
126-
zerolog.Warn().Msg("Testing mode enabled. This may enable features that are not safe for production use.")
127-
publicChannelsEnabled = cCtx.Bool(publicSlackChannelFlag)
103+
// Parse options
104+
locations, err := parseUrls(cCtx.StringSlice(urlFlag))
105+
if err != nil {
106+
return errors.Join(errors.New("failed to parse `--url` options"), err)
128107
}
129108

130109
// Create services
@@ -133,7 +112,7 @@ func PatrolAction(cCtx *cli.Context) error {
133112
return errors.Join(errors.New("failed to create GitLab service"), err)
134113
}
135114

136-
slackService, err := slack.New(cCtx.String(slackTokenFlag), publicChannelsEnabled, verbose)
115+
slackService, err := slack.New(cCtx.String(slackTokenFlag), verbose)
137116
if err != nil {
138117
return errors.Join(errors.New("failed to create Slack service"), err)
139118
}
@@ -145,13 +124,15 @@ func PatrolAction(cCtx *cli.Context) error {
145124

146125
// Do the patrol
147126
if warn, err := patrolService.Patrol(
148-
cCtx.StringSlice(groupsFlag),
149-
cCtx.StringSlice(projectsFlag),
150-
cCtx.Bool(reportGitlabFlag),
151-
cCtx.String(reportSlackChannelFlag),
152-
cCtx.Bool(reportSlackProjectChannelFlag),
153-
cCtx.Bool(silentReport),
154-
verbose,
127+
patrol.PatrolArgs{
128+
Locations: locations,
129+
ReportToIssue: cCtx.Bool(reportToIssueFlag),
130+
ReportToEmails: cCtx.StringSlice(reportToEmailFlag),
131+
ReportToSlackChannel: cCtx.String(reportToSlackChannel),
132+
EnableProjectReportTo: cCtx.Bool(reportEnableProjectReportToFlag),
133+
SilentReport: cCtx.Bool(silentReportFlag),
134+
Verbose: verbose,
135+
},
155136
); err != nil {
156137
return errors.Join(errors.New("failed to scan"), err)
157138
} else if warn != nil {
@@ -161,20 +142,34 @@ func PatrolAction(cCtx *cli.Context) error {
161142
return nil
162143
}
163144

164-
func validatePaths(regex string) func(*cli.Context, []string) error {
165-
return func(_ *cli.Context, groups []string) (err error) {
166-
rgx, err := regexp.Compile(regex)
167-
if err != nil {
168-
return err
145+
func parseUrls(uris []string) ([]patrol.ProjectLocation, error) {
146+
locations := make([]patrol.ProjectLocation, len(uris))
147+
for i, uri := range uris {
148+
parsed, err := url.Parse(uri)
149+
if err != nil || parsed == nil {
150+
return nil, errors.Join(fmt.Errorf("failed to parse uri"), err)
169151
}
170152

171-
for _, path := range groups {
172-
matched := rgx.Match([]byte(path))
153+
if !parsed.IsAbs() {
154+
return nil, fmt.Errorf("url missing platform scheme %v", uri)
155+
}
173156

174-
if !matched {
175-
return fmt.Errorf("invalid group path: %v", path)
176-
}
157+
if parsed.Scheme == string(patrol.Github) {
158+
return nil, fmt.Errorf("github is currently unsupported, but is on our roadmap :)") // TODO #9
159+
} else if parsed.Scheme != string(patrol.Gitlab) {
160+
return nil, fmt.Errorf("unsupport platform %v", parsed.Scheme)
161+
}
162+
163+
path, err := url.JoinPath(parsed.Host, parsed.Path)
164+
if err != nil {
165+
return nil, fmt.Errorf("failed to join host and path %v", uri)
166+
}
167+
168+
locations[i] = patrol.ProjectLocation{
169+
Type: patrol.PlatformType(parsed.Scheme),
170+
Path: path,
177171
}
178-
return
179172
}
173+
174+
return locations, nil
180175
}

internal/cli/patrol_test.go

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package cli
22

33
import (
44
"flag"
5+
"fmt"
6+
"sheriff/internal/patrol"
57
"testing"
68

79
"github.com/stretchr/testify/assert"
@@ -16,44 +18,31 @@ func TestPatrolActionEmptyRun(t *testing.T) {
1618
assert.Nil(t, err)
1719
}
1820

19-
func TestValidatePathGroupPathRegex(t *testing.T) {
21+
func TestParseUrls(t *testing.T) {
2022
testCases := []struct {
21-
paths []string
22-
want bool
23+
paths []string
24+
wantProjectLocation *patrol.ProjectLocation
25+
wantError bool
2326
}{
24-
{[]string{"group"}, true},
25-
{[]string{"group/subgroup"}, true},
26-
{[]string{"group/subgroup", "not a path"}, false},
27+
{[]string{"gitlab://namespace/project"}, &patrol.ProjectLocation{Type: "gitlab", Path: "namespace/project"}, false},
28+
{[]string{"gitlab://namespace/subgroup/project"}, &patrol.ProjectLocation{Type: "gitlab", Path: "namespace/subgroup/project"}, false},
29+
{[]string{"gitlab://namespace"}, &patrol.ProjectLocation{Type: "gitlab", Path: "namespace"}, false},
30+
{[]string{"github://organization"}, &patrol.ProjectLocation{Type: "github", Path: "organization"}, true},
31+
{[]string{"github://organization/project"}, &patrol.ProjectLocation{Type: "github", Path: "organization/project"}, true},
32+
{[]string{"unknown://namespace/project"}, nil, true},
33+
{[]string{"unknown://not a path"}, nil, true},
34+
{[]string{"not a url"}, nil, true},
2735
}
2836

2937
for _, tc := range testCases {
30-
err := validatePaths(groupPathRegex)(nil, tc.paths)
38+
urls, err := parseUrls(tc.paths)
3139

32-
if tc.want {
33-
assert.Nil(t, err)
34-
} else {
35-
assert.NotNil(t, err)
36-
}
37-
}
38-
}
39-
40-
func TestValidatePathProjectPathRegex(t *testing.T) {
41-
testCases := []struct {
42-
paths []string
43-
want bool
44-
}{
45-
{[]string{"project"}, false}, // top-level projects don't exist
46-
{[]string{"group/project"}, true},
47-
{[]string{"group/project", "not a path"}, false},
48-
}
40+
fmt.Print(urls)
4941

50-
for _, tc := range testCases {
51-
err := validatePaths(projectPathRegex)(nil, tc.paths)
52-
53-
if tc.want {
54-
assert.Nil(t, err, tc.paths)
42+
if tc.wantError {
43+
assert.NotNil(t, err)
5544
} else {
56-
assert.NotNil(t, err, tc.paths)
45+
assert.Equal(t, tc.wantProjectLocation, &(urls[0]))
5746
}
5847
}
5948
}

internal/gitlab/client.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
)
1111

1212
type iclient interface {
13-
ListGroups(opt *gitlab.ListGroupsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Group, *gitlab.Response, error)
14-
ListGroupProjects(groupId int, opt *gitlab.ListGroupProjectsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Project, *gitlab.Response, error)
13+
GetProject(pid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error)
14+
ListGroupProjects(gid interface{}, opt *gitlab.ListGroupProjectsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Project, *gitlab.Response, error)
1515
ListProjectIssues(projectId interface{}, opt *gitlab.ListProjectIssuesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Issue, *gitlab.Response, error)
1616
CreateIssue(projectId interface{}, opt *gitlab.CreateIssueOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Issue, *gitlab.Response, error)
1717
UpdateIssue(projectId interface{}, issueId int, opt *gitlab.UpdateIssueOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Issue, *gitlab.Response, error)
@@ -21,12 +21,12 @@ type client struct {
2121
client *gitlab.Client
2222
}
2323

24-
func (c *client) ListGroups(opt *gitlab.ListGroupsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Group, *gitlab.Response, error) {
25-
return c.client.Groups.ListGroups(opt, options...)
24+
func (c *client) GetProject(gid interface{}, opt *gitlab.GetProjectOptions, options ...gitlab.RequestOptionFunc) (*gitlab.Project, *gitlab.Response, error) {
25+
return c.client.Projects.GetProject(gid, opt, options...)
2626
}
2727

28-
func (c *client) ListGroupProjects(groupId int, opt *gitlab.ListGroupProjectsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Project, *gitlab.Response, error) {
29-
return c.client.Groups.ListGroupProjects(groupId, opt, options...)
28+
func (c *client) ListGroupProjects(gid interface{}, opt *gitlab.ListGroupProjectsOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Project, *gitlab.Response, error) {
29+
return c.client.Groups.ListGroupProjects(gid, opt, options...)
3030
}
3131

3232
func (c *client) ListProjectIssues(projectId interface{}, opt *gitlab.ListProjectIssuesOptions, options ...gitlab.RequestOptionFunc) ([]*gitlab.Issue, *gitlab.Response, error) {

0 commit comments

Comments
 (0)