Skip to content
Open
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
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,16 @@ poutine analyze_org my-org/project --token "$GL_TOKEN" --scm gitlab --scm-base-u
### Configuration Options

```
--token SCM access token (required for the commands analyze_repo, analyze_org) (env: GH_TOKEN)
--format Output format (default: pretty, json, sarif)
--ignore-forks Ignore forked repositories in the organization(analyze_org)
--scm SCM platform (default: github, gitlab)
--scm-base-url Base URI of the self-hosted SCM instance
--threads Number of threads to use (default: 2)
--config Path to the configuration file (default: .poutine.yml)
--skip Add rules to the skip list for the current run (can be specified multiple times)
--verbose Enable debug logging
--token SCM access token (required for the commands analyze_repo, analyze_org) (env: GH_TOKEN)
--format Output format (default: pretty, json, sarif)
--ignore-forks Ignore forked repositories in the organization(analyze_org)
--scm SCM platform (default: github, gitlab)
--scm-base-url Base URI of the self-hosted SCM instance
--threads Number of threads to use (default: 2)
--config Path to the configuration file (default: .poutine.yml)
--skip Add rules to the skip list for the current run (can be specified multiple times)
--verbose Enable debug logging
--fail-on-violation Exit with a non-zero code (10) when violations are found
```

See [.poutine.sample.yml](.poutine.sample.yml) for an example configuration file.
Expand Down
6 changes: 5 additions & 1 deletion cmd/analyzeLocal.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ Example: poutine analyze_local /path/to/repo`,

analyzer := analyze.NewAnalyzer(localScmClient, localGitClient, formatter, config, opaClient)

_, err = analyzer.AnalyzeLocalRepo(ctx, repoPath)
result, err := analyzer.AnalyzeLocalRepo(ctx, repoPath)
if err != nil {
return fmt.Errorf("failed to analyze repoPath %s: %w", repoPath, err)
}

if failOnViolation && result != nil && len(result.FindingsResults.Findings) > 0 {
return ErrViolationsFound
}

return nil
},
}
Expand Down
10 changes: 9 additions & 1 deletion cmd/analyzeOrg.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@ Note: This command will scan all repositories in the organization except those t

org := args[0]

_, err = analyzer.AnalyzeOrg(ctx, org, &threads)
results, err := analyzer.AnalyzeOrg(ctx, org, &threads)
if err != nil {
return fmt.Errorf("failed to analyze org %s: %w", org, err)
}

if failOnViolation {
for _, pkg := range results {
if pkg != nil && len(pkg.FindingsResults.Findings) > 0 {
return ErrViolationsFound
}
}
}

return nil
},
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/analyzeRepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ Example Scanning a remote Github Repository: poutine analyze_repo org/repo --tok

repo := args[0]

_, err = analyzer.AnalyzeRepo(ctx, repo, ref)
result, err := analyzer.AnalyzeRepo(ctx, repo, ref)
if err != nil {
return fmt.Errorf("failed to analyze repo %s: %w", repo, err)
}

if failOnViolation && result != nil && len(result.FindingsResults.Findings) > 0 {
return ErrViolationsFound
}

return nil
},
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/analyzeRepoStaleBranches.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ Example Scanning a remote Github Repository: poutine analyze_repo_stale_branches
return fmt.Errorf("error compiling regex: %w", err)
}

_, err = analyzer.AnalyzeStaleBranches(ctx, repo, &threads, &expand, reg)
result, err := analyzer.AnalyzeStaleBranches(ctx, repo, &threads, &expand, reg)
if err != nil {
return fmt.Errorf("failed to analyze repo %s: %w", repo, err)
}

if failOnViolation && result != nil && len(result.FindingsResults.Findings) > 0 {
return ErrViolationsFound
}

return nil
},
}
Expand Down
123 changes: 123 additions & 0 deletions cmd/fail_on_violation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"errors"
"testing"

"github.com/boostsecurityio/poutine/models"
"github.com/boostsecurityio/poutine/results"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestErrViolationsFound(t *testing.T) {
assert.EqualError(t, ErrViolationsFound, "poutine: violations found")

Check failure on line 14 in cmd/fail_on_violation_test.go

View workflow job for this annotation

GitHub Actions / lint

require-error: for error assertions use require (testifylint)
assert.True(t, errors.Is(ErrViolationsFound, ErrViolationsFound))

Check failure on line 15 in cmd/fail_on_violation_test.go

View workflow job for this annotation

GitHub Actions / lint

error-is-as: use assert.ErrorIs (testifylint)
}

func TestExitCodeViolations(t *testing.T) {
assert.Equal(t, 10, exitCodeViolations)
}

func TestFailOnViolationFlag(t *testing.T) {
flag := RootCmd.PersistentFlags().Lookup("fail-on-violation")
require.NotNil(t, flag, "--fail-on-violation flag should be registered")
assert.Equal(t, "false", flag.DefValue, "--fail-on-violation should default to false")
}

func TestFailOnViolationLogic(t *testing.T) {
pkgWithFindings := &models.PackageInsights{
FindingsResults: results.FindingsResult{
Findings: []results.Finding{
{RuleId: "injection", Purl: "pkg:github/example/repo"},
},
},
}
pkgNoFindings := &models.PackageInsights{
FindingsResults: results.FindingsResult{
Findings: []results.Finding{},
},
}

t.Run("failOnViolation=false with findings returns no error", func(t *testing.T) {
// Simulate the logic in command handlers
fov := false
pkg := pkgWithFindings
var err error
if fov && pkg != nil && len(pkg.FindingsResults.Findings) > 0 {
err = ErrViolationsFound
}
assert.NoError(t, err)
})

t.Run("failOnViolation=true with findings returns ErrViolationsFound", func(t *testing.T) {
fov := true
pkg := pkgWithFindings
var err error
if fov && pkg != nil && len(pkg.FindingsResults.Findings) > 0 {
err = ErrViolationsFound
}
assert.ErrorIs(t, err, ErrViolationsFound)
})

t.Run("failOnViolation=true with no findings returns no error", func(t *testing.T) {
fov := true
pkg := pkgNoFindings
var err error
if fov && pkg != nil && len(pkg.FindingsResults.Findings) > 0 {
err = ErrViolationsFound
}
assert.NoError(t, err)
})

t.Run("failOnViolation=true with nil result returns no error", func(t *testing.T) {
fov := true
var pkg *models.PackageInsights
var err error
if fov && pkg != nil && len(pkg.FindingsResults.Findings) > 0 {
err = ErrViolationsFound
}
assert.NoError(t, err)
})
}

func TestFailOnViolationOrgLogic(t *testing.T) {
pkgWithFindings := &models.PackageInsights{
FindingsResults: results.FindingsResult{
Findings: []results.Finding{
{RuleId: "injection", Purl: "pkg:github/example/repo"},
},
},
}
pkgNoFindings := &models.PackageInsights{}

t.Run("failOnViolation=true with findings in org returns ErrViolationsFound", func(t *testing.T) {
fov := true
pkgs := []*models.PackageInsights{pkgNoFindings, pkgWithFindings}
var err error
if fov {
for _, pkg := range pkgs {
if pkg != nil && len(pkg.FindingsResults.Findings) > 0 {
err = ErrViolationsFound
break
}
}
}
assert.ErrorIs(t, err, ErrViolationsFound)
})

t.Run("failOnViolation=true with no findings in org returns no error", func(t *testing.T) {
fov := true
pkgs := []*models.PackageInsights{pkgNoFindings, pkgNoFindings}
var err error
if fov {
for _, pkg := range pkgs {
if pkg != nil && len(pkg.FindingsResults.Findings) > 0 {
err = ErrViolationsFound
break
}
}
}
assert.NoError(t, err)
})
}
20 changes: 16 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"embed"
"errors"
"fmt"
"os"
"os/signal"
Expand Down Expand Up @@ -42,18 +43,24 @@ var cfgFile string
var config *models.Config = models.DefaultConfig()
var skipRules []string
var allowedRules []string
var failOnViolation bool

// ErrViolationsFound is returned when violations are detected and --fail-on-violation is set.
var ErrViolationsFound = errors.New("poutine: violations found")

var legacyFlags = []string{"-token", "-format", "-verbose", "-scm", "-scm-base-uri", "-threads"}

const (
exitCodeErr = 1
exitCodeInterrupt = 2
exitCodeErr = 1
exitCodeInterrupt = 2
exitCodeViolations = 10
)

// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "poutine",
Short: "A Supply Chain Vulnerability Scanner for Build Pipelines",
Use: "poutine",
SilenceErrors: true,
Short: "A Supply Chain Vulnerability Scanner for Build Pipelines",
Long: `A Supply Chain Vulnerability Scanner for Build Pipelines
By BoostSecurity.io - https://github.com/boostsecurityio/poutine `,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -95,6 +102,10 @@ func Execute() {

err := RootCmd.ExecuteContext(ctx)
if err != nil {
if errors.Is(err, ErrViolationsFound) {
log.Info().Msg("violations found")
os.Exit(exitCodeViolations)
}
log.Error().Err(err).Msg("")
os.Exit(exitCodeErr)
}
Expand All @@ -120,6 +131,7 @@ func init() {
RootCmd.PersistentFlags().BoolVarP(&config.Quiet, "quiet", "q", false, "Disable progress output")
RootCmd.PersistentFlags().StringSliceVar(&skipRules, "skip", []string{}, "Adds rules to the configured skip list for the current run (optional)")
RootCmd.PersistentFlags().StringSliceVar(&allowedRules, "allowed-rules", []string{}, "Overwrite the configured allowedRules list for the current run (optional)")
RootCmd.PersistentFlags().BoolVar(&failOnViolation, "fail-on-violation", false, "Exit with a non-zero code (10) when violations are found")

_ = viper.BindPFlag("quiet", RootCmd.PersistentFlags().Lookup("quiet"))
}
Expand Down
Loading