Skip to content
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ go.work.sum
tmp_scans/

# Sheriff configuration file, untracked for use in local development
sheriff.toml
/sheriff.toml
116 changes: 112 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ Sheriff is a tool to scan repositories and generate security reports.
- [CLI flags](#cli-flags)
- [Environment variables](#environment-variables)
- [Configuration file](#configuration-file)
- [Configuration options](#configuration-options)
- [Miscellaneous](#miscellaneous)
- [config](#config)
- [verbose](#verbose)
- [Scanning](#scanning)
- [targets](#targets)
- [Reporting](#reporting)
- [report to issue](#report-to-issue)
- [report to email (TODO #12)](#report-to-email-todo-12)
- [report to slack channels](#report-to-slack-channels)
- [enable project report to](#enable-project-report-to)
- [silent](#silent)
- [Tokens](#tokens)
- [gitlab token](#gitlab-token)
- [slack token](#slack-token)
- [Supported platforms](#supported-platforms)
- [Source code hosting services](#source-code-hosting-services)
- [Messaging services](#messaging-services)
Expand All @@ -28,7 +43,7 @@ Sheriff is a tool to scan repositories and generate security reports.
## Quick Usage

```sh
sheriff patrol --url gitlab://your-namespace-or-group --report-to-issue
sheriff patrol --target gitlab://your-namespace-or-group --report-to-issue
```

## How it works
Expand Down Expand Up @@ -104,16 +119,109 @@ Only the **Reporting** and **Scanning** sections of configuration parameters are
In this case you may choose to create a config file such as the following:

```toml
url = ["namespace/group", "namespace/group/cool-repo"]
report-to-slack-channel = "sheriff-report-test"
report-to-gitlab-issue = true
targets = ["namespace/group", "namespace/group/cool-repo"]
[report.to]
slack-channel = "sheriff-report-test"
issue = true
```

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

> [!NOTE]
> When using several types of configurations at once there is an order of preference: **cli flags** > **env vars** > **config file**

### Configuration options

#### Miscellaneous

##### config

| CLI options | File config |
|---|---|
| `--config` | - |

Sets the path of your sheriff configuration file

##### verbose

| CLI options | File config |
|---|---|
| `--verbose`/`-v` | - |

Sets the log level to verbose

#### Scanning

##### targets

| CLI options | File config |
|---|---|
| (repeatable) `--target` | `targets` |

Sets the list of groups and projects to be scanned.
The expected format of a target is `platform://path/to/your/group-or-project`

For example:
`--target gitlab://namespace/group --target github://organization/project`

#### Reporting

##### report to issue

| CLI options | File config |
|---|---|
| `--report-to-issue` | <code>[report.to]<br>issue</code> |

Enables reporting to an issue on the project's platform

##### report to email (TODO #12)

| CLI options | File config |
|---|---|
| (repeatable) `--report-to-email` | <code>[report.to]<br>emails</code> |

Sets the list of email to which a full scan report should be sent

##### report to slack channels

| CLI options | File config |
|---|---|
| (repeatable) `--report-to-slack-channels` | <code>[report.to]<br>slack-channels</code> |

##### enable project report to

| CLI options | File config |
|---|---|
| `--report-to-enable-project-report-to` | <code>[report.to]<br>enable-project-report-to</code> |

Enable project-level configuration `report-to` to allow projects to control where their individual reports are sent

##### silent

| CLI options | File config |
|---|---|
| `--report-silent` | <code>[report]<br>silent</code> |

Disable printing the report in the bash output

#### Tokens

##### gitlab token

| ENV VAR |
|---|
| `$GITLAB_TOKEN` |

Sets the token to be used when fetching projects from gitlab

##### slack token

| ENV VAR |
|---|
| `$SLACK_TOKEN` |

Sets the token to be used when reporting the security report on slack

## Supported platforms

### Source code hosting services
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ This file is formatted in TOML and can contain any of the flags that can be set
`,
Flags: PatrolFlags,
Action: PatrolAction,
Before: CombineBeforeFuncs(ConfigureLogs, GetConfigFileLoader(PatrolFlags, configFlag), LogArguments),
Before: ConfigureLogs,
},
},
}
Expand Down
53 changes: 29 additions & 24 deletions internal/cli/patrol.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"strings"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)

type CommandCategory string
Expand All @@ -27,7 +26,7 @@ const (

const configFlag = "config"
const verboseFlag = "verbose"
const urlFlag = "url"
const targetFlag = "target"
const reportToEmailFlag = "report-to-email"
const reportToIssueFlag = "report-to-issue"
const reportToSlackChannel = "report-to-slack-channel"
Expand All @@ -36,7 +35,6 @@ const silentReportFlag = "silent"
const gitlabTokenFlag = "gitlab-token"
const slackTokenFlag = "slack-token"

var sensitiveFlags = []string{gitlabTokenFlag, slackTokenFlag}
var necessaryScanners = []string{scanner.OsvCommandName}

var PatrolFlags = []cli.Flag{
Expand All @@ -52,38 +50,38 @@ var PatrolFlags = []cli.Flag{
Category: string(Miscellaneous),
Value: false,
},
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: urlFlag,
&cli.StringSliceFlag{
Name: targetFlag,
Usage: "Groups and projects to scan for vulnerabilities (list argument which can be repeated)",
Category: string(Scanning),
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
},
&cli.StringSliceFlag{
Name: reportToEmailFlag,
Usage: "Enable reporting to the provided list of emails",
Category: string(Reporting),
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
},
&cli.BoolFlag{
Name: reportToIssueFlag,
Usage: "Enable or disable reporting to the project's issue on the associated platform (gitlab, github, ...)",
Category: string(Reporting),
}),
altsrc.NewStringFlag(&cli.StringFlag{
},
&cli.StringSliceFlag{
Name: reportToSlackChannel,
Usage: "Enable reporting to the provided slack channel",
Usage: "Enable reporting to the provided slack channels",
Category: string(Reporting),
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
},
&cli.BoolFlag{
Name: reportEnableProjectReportToFlag,
Usage: "Enable project-level configuration for '--report-to-*'.",
Category: string(Reporting),
Value: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
},
&cli.BoolFlag{
Name: silentReportFlag,
Usage: "Disable report output to stdout.",
Category: string(Reporting),
Value: false,
}),
},
// Secret tokens
&cli.StringFlag{
Name: gitlabTokenFlag,
Expand All @@ -102,13 +100,20 @@ var PatrolFlags = []cli.Flag{

func PatrolAction(cCtx *cli.Context) error {
config, err := config.GetPatrolConfiguration(config.PatrolCLIOpts{
Urls: cCtx.StringSlice(urlFlag),
ReportToIssue: cCtx.Bool(reportToIssueFlag),
ReportToEmails: cCtx.StringSlice(reportToEmailFlag),
ReportToSlackChannel: cCtx.String(reportToSlackChannel),
EnableProjectReportTo: cCtx.Bool(reportEnableProjectReportToFlag),
SilentReport: cCtx.Bool(silentReportFlag),
Verbose: cCtx.Bool(verboseFlag),
PatrolCommonOpts: config.PatrolCommonOpts{
Targets: getStringSliceIfSet(cCtx, targetFlag),
Report: config.PatrolReportOpts{
To: config.PatrolReportToOpts{
Issue: getBoolIfSet(cCtx, reportToIssueFlag),
Emails: getStringSliceIfSet(cCtx, reportToEmailFlag),
SlackChannels: getStringSliceIfSet(cCtx, reportToSlackChannel),
EnableProjectReportTo: getBoolIfSet(cCtx, reportEnableProjectReportToFlag),
},
SilentReport: getBoolIfSet(cCtx, silentReportFlag),
},
},
Config: cCtx.String(configFlag),
Verbose: cCtx.Bool(verboseFlag),
})
if err != nil {
return errors.Join(errors.New("failed to get patrol configuration"), err)
Expand Down
68 changes: 18 additions & 50 deletions internal/cli/utils.go
Original file line number Diff line number Diff line change
@@ -1,74 +1,42 @@
package cli

import (
"errors"
"fmt"
"os"
"sheriff/internal/log"
"slices"

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

func CombineBeforeFuncs(beforeFuncs ...cli.BeforeFunc) cli.BeforeFunc {
return func(cCtx *cli.Context) error {
for _, beforeFunc := range beforeFuncs {
if err := beforeFunc(cCtx); err != nil {
return err
}
}
return nil
}
}

func ConfigureLogs(cCtx *cli.Context) error {
log.ConfigureLogs(cCtx.Bool(verboseFlag))
zerolog.Info().Msg("Logging configured")
return nil
}

func GetConfigFileLoader(flags []cli.Flag, fileNameFlag string) cli.BeforeFunc {
return func(cCtx *cli.Context) error {
fileName := cCtx.String(fileNameFlag)
if _, err := os.Stat(fileName); err == nil {
// Config file exists
zerolog.Info().Str("file", fileName).Msg("Loading configuration file")
return altsrc.InitInputSourceWithContext(flags, func(cCtx *cli.Context) (altsrc.InputSourceContext, error) {
return altsrc.NewTomlSourceFromFile(fileName)
})(cCtx)
} else if errors.Is(err, os.ErrNotExist) {
if cCtx.IsSet(fileNameFlag) {
// Config file was explicitly set but does not exist
return fmt.Errorf("config file %v does not exist", fileName)
}
zerolog.Info().Str("file", fileName).Msg("No configuration file found")
return nil // No config file, do nothing
} else {
// Error stating config file
return errors.Join(fmt.Errorf("failed to stat config file %s", fileName), err)
}
func getStringSliceIfSet(cCtx *cli.Context, flagName string) *[]string {
if cCtx.IsSet(flagName) {
v := cCtx.StringSlice(flagName)
return &v
}
}

func LogArguments(cCtx *cli.Context) error {
flags := cCtx.FlagNames()
return nil
}

flagList := make([]string, 0, len(flags))
func getStringIfSet(cCtx *cli.Context, flagName string) *string {
if cCtx.IsSet(flagName) {
v := cCtx.String(flagName)
print(v)
return &v
}

for _, flag := range flags {
val := cCtx.Value(flag)
if slices.Contains(sensitiveFlags, flag) {
val = "REDACTED"
} else if sliceVal, ok := val.(cli.StringSlice); ok {
val = sliceVal.String()
}
return nil
}

flagList = append(flagList, fmt.Sprintf("%s=%v", flag, val))
func getBoolIfSet(cCtx *cli.Context, flagName string) *bool {
if cCtx.IsSet(flagName) {
v := cCtx.Bool(flagName)
return &v
}

zerolog.Info().Strs("arguments", flagList).Msg("Running with configuration")

return nil
}
Loading
Loading