diff --git a/cmd/root.go b/cmd/root.go index db01fc3d..3db49a71 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ THE SOFTWARE. package cmd import ( + "errors" "fmt" "os" "runtime" @@ -44,12 +45,13 @@ import ( var ( // flags - exitOneOnFailure bool - cfgFile string - debug bool - stdin bool - outputName string - noIgnore bool + exitOneOnFailure bool + cfgFile string + debug bool + stdin bool + outputName string + noIgnore bool + disableDefaultRules bool // Version is populated by goreleaser during build // Version... @@ -73,6 +75,8 @@ Provide a list file globs for files you'd like to check.`, RunE: rootRunE, } +var ErrNoRulesEnabled = errors.New("no rules enabled: either configure rules in your config file or remove the `--disable-default-rules` flag") + func rootRunE(cmd *cobra.Command, args []string) error { setDebugLogLevel() runtime.GOMAXPROCS(runtime.NumCPU()) @@ -86,11 +90,15 @@ func rootRunE(cmd *cobra.Command, args []string) error { Msg("woke completed") }() - cfg, err := config.NewConfig(viper.ConfigFileUsed()) + cfg, err := config.NewConfig(viper.ConfigFileUsed(), disableDefaultRules) if err != nil { return err } + if len(cfg.Rules) == 0 { + return ErrNoRulesEnabled + } + var ignorer *ignore.Ignore if !noIgnore { ignorer = ignore.NewIgnore(cfg.IgnoreFiles) @@ -136,6 +144,7 @@ func init() { rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging") rootCmd.PersistentFlags().BoolVar(&noIgnore, "no-ignore", false, "Ignored files in .gitignore, .ignore, .wokeignore, .git/info/exclude, and inline ignores are processed") rootCmd.PersistentFlags().StringVarP(&outputName, "output", "o", printer.OutFormatText, fmt.Sprintf("Output type [%s]", printer.OutFormatsString)) + rootCmd.PersistentFlags().BoolVar(&disableDefaultRules, "disable-default-rules", false, "Disable the default ruleset") } // GetRootCmd returns the rootCmd, which should only be used by the docs generator in cmd/docs/main.go diff --git a/cmd/root_test.go b/cmd/root_test.go index 03c1e16e..d194978b 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -31,21 +31,43 @@ func BenchmarkRootRunE(b *testing.B) { } } -func TestInitConfig(t *testing.T) { +func setTestConfigFile(t *testing.T, filename string) { + origConfigFile := viper.ConfigFileUsed() t.Cleanup(func() { - cfgFile = "" - debug = false - }) - debug = true - t.Run("good config", func(t *testing.T) { - cfgFile = "../testdata/good.yml" - initConfig() + viper.SetConfigFile(origConfigFile) }) + viper.SetConfigFile(filename) +} - t.Run("no config", func(t *testing.T) { - cfgFile = "" - initConfig() - }) +func TestInitConfig(t *testing.T) { + tests := []struct { + desc string + cfgFile string + }{ + { + desc: "good config", + cfgFile: "../testdata/good.yml", + }, + { + desc: "no config", + cfgFile: "", + }, + { + desc: "invalid config", + cfgFile: "../testdata/invalid.yml", + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + t.Cleanup(func() { + cfgFile = "" + initConfig() + }) + cfgFile = tt.cfgFile + initConfig() + assert.Equal(t, tt.cfgFile, viper.ConfigFileUsed()) + }) + } } func TestParseArgs(t *testing.T) { @@ -62,13 +84,9 @@ func TestParseArgs(t *testing.T) { func TestRunE(t *testing.T) { origStdout := output.Stdout - origConfigFile := viper.ConfigFileUsed() t.Cleanup(func() { - exitOneOnFailure = false - noIgnore = false // Reset back to original output.Stdout = origStdout - viper.SetConfigName(origConfigFile) }) t.Run("no findings found", func(t *testing.T) { @@ -86,8 +104,7 @@ func TestRunE(t *testing.T) { t.Run("no findings found with custom message", func(t *testing.T) { buf := new(bytes.Buffer) output.Stdout = buf - - viper.SetConfigFile("../testdata/.woke-custom-exit-success.yaml") + setTestConfigFile(t, "../testdata/.woke-custom-exit-success.yaml") err := rootRunE(new(cobra.Command), []string{"../testdata/good.yml"}) assert.NoError(t, err) @@ -98,9 +115,38 @@ func TestRunE(t *testing.T) { t.Run("findings w error", func(t *testing.T) { exitOneOnFailure = true - + t.Cleanup(func() { + exitOneOnFailure = false + }) err := rootRunE(new(cobra.Command), []string{"../testdata"}) assert.Error(t, err) assert.Regexp(t, regexp.MustCompile(`^files with findings: \d`), err.Error()) }) + + t.Run("no rules enabled", func(t *testing.T) { + disableDefaultRules = true + t.Cleanup(func() { + disableDefaultRules = false + }) + + err := rootRunE(new(cobra.Command), []string{"../testdata"}) + assert.Error(t, err) + assert.ErrorIs(t, err, ErrNoRulesEnabled) + }) + + t.Run("invalid printer", func(t *testing.T) { + outputName = "foo" + t.Cleanup(func() { + outputName = "text" + }) + err := rootRunE(new(cobra.Command), []string{"../testdata"}) + assert.Error(t, err) + assert.Equal(t, "foo is not a valid printer type", err.Error()) + }) + + t.Run("invalid config", func(t *testing.T) { + setTestConfigFile(t, "../testdata/invalid.yaml") + err := rootRunE(new(cobra.Command), []string{"../testdata"}) + assert.Error(t, err) + }) } diff --git a/docs/rules.md b/docs/rules.md index 91d348b7..42f3cfdd 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -79,6 +79,18 @@ rules: - name: whitelist ``` +### Disable all Default Rules + +There may be a case where you want full control over the rules you want to run with woke. + +You can either disable each default rule via the instructions above. + +Or you can run woke with `--disable-default-rules` to completely disable all default rules. + +!!! note + `woke` will fail to run if you use `--disable-default-rules` without providing your own rules + because that would mean running `woke` without any rules, which is pointless. + ## Excluding Categories of Rules You can also specify any number of rule categories to be excluded, or filtered out, from within your `woke` configuration. If any rules in a configuration file have matching categories, they will be excluded and will not be run against the target files. diff --git a/pkg/config/config.go b/pkg/config/config.go index 0363a8e7..cf973bd7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,7 +26,7 @@ type Config struct { } // NewConfig returns a new Config -func NewConfig(filename string) (*Config, error) { +func NewConfig(filename string, disableDefaultRules bool) (*Config, error) { var c Config if len(filename) > 0 { var err error @@ -50,7 +50,7 @@ func NewConfig(filename string) (*Config, error) { log.Debug().Msg("no config file loaded, using only default rules") } - c.ConfigureRules() + c.ConfigureRules(disableDefaultRules) logRuleset("all enabled", c.Rules) return &c, nil @@ -78,13 +78,16 @@ func (c *Config) inExistingRules(r *rule.Rule) bool { // Configure RegExps for all rules // Configure IncludeNote for all rules // Filter out any rules that fall under ExcludeCategories -func (c *Config) ConfigureRules() { - for _, r := range rule.DefaultRules { - if !c.inExistingRules(r) { - c.Rules = append(c.Rules, r) +func (c *Config) ConfigureRules(disableDefaultRules bool) { + if disableDefaultRules { + log.Debug().Msg("disabling default rules") + } else { + for _, r := range rule.DefaultRules { + if !c.inExistingRules(r) { + c.Rules = append(c.Rules, r) + } } } - logRuleset("default", rule.DefaultRules) var excludeIndices []int diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 8d5d6415..4e3b1e72 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -35,7 +35,7 @@ func TestNewConfig(t *testing.T) { defaultRules[i] = fmt.Sprintf("%q", rule.DefaultRules[i].Name) } - c, err := NewConfig("testdata/good.yaml") + c, err := NewConfig("testdata/good.yaml", false) assert.NoError(t, err) enabledRules := make([]string, len(c.Rules)) for i := range c.Rules { @@ -54,7 +54,7 @@ func TestNewConfig(t *testing.T) { }) t.Run("config-good", func(t *testing.T) { - c, err := NewConfig("testdata/good.yaml") + c, err := NewConfig("testdata/good.yaml", false) assert.NoError(t, err) expectedRules := []*rule.Rule{} @@ -81,7 +81,7 @@ func TestNewConfig(t *testing.T) { Rules: expectedRules, IgnoreFiles: []string{"README.md", "pkg/rule/default.go", "testdata/good.yaml"}, } - expected.ConfigureRules() + expected.ConfigureRules(false) assert.EqualValues(t, expected.Rules, c.Rules) @@ -91,7 +91,7 @@ func TestNewConfig(t *testing.T) { t.Run("config-empty-missing", func(t *testing.T) { // Test when no config file is provided - c, err := NewConfig("") + c, err := NewConfig("", false) assert.NoError(t, err) expectedEmpty := &Config{ @@ -103,14 +103,14 @@ func TestNewConfig(t *testing.T) { t.Run("config-missing", func(t *testing.T) { // Test when no config file is provided - c, err := NewConfig("testdata/missing.yaml") + c, err := NewConfig("testdata/missing.yaml", false) assert.Error(t, err) assert.Nil(t, c) }) t.Run("config-empty-success-message", func(t *testing.T) { // Test when no config file is provided - c, err := NewConfig("testdata/empty-success-message.yaml") + c, err := NewConfig("testdata/empty-success-message.yaml", false) assert.NoError(t, err) // check default config message @@ -119,7 +119,7 @@ func TestNewConfig(t *testing.T) { t.Run("config-custom-success-message", func(t *testing.T) { // Test when no config file is provided - c, err := NewConfig("testdata/custom-success-message.yaml") + c, err := NewConfig("testdata/custom-success-message.yaml", false) assert.NoError(t, err) // check default config message @@ -128,7 +128,7 @@ func TestNewConfig(t *testing.T) { t.Run("config-add-note-messaage", func(t *testing.T) { // Test when it is configured to add a note to the output message - c, err := NewConfig("testdata/add-note-message.yaml") + c, err := NewConfig("testdata/add-note-message.yaml", false) assert.NoError(t, err) // check global IncludeNote @@ -141,9 +141,9 @@ func TestNewConfig(t *testing.T) { assert.Equal(t, false, *c.Rules[0].Options.IncludeNote) }) - t.Run("config-dont-add-note-messaage", func(t *testing.T) { + t.Run("config-dont-add-note-message", func(t *testing.T) { // Test when it is nott configured to add a note to the output message - c, err := NewConfig("testdata/dont-add-note-message.yaml") + c, err := NewConfig("testdata/dont-add-note-message.yaml", false) assert.NoError(t, err) // check global IncludeNote @@ -156,9 +156,19 @@ func TestNewConfig(t *testing.T) { assert.Equal(t, true, *c.Rules[0].Options.IncludeNote) }) + t.Run("disable-default-rules", func(t *testing.T) { + c, err := NewConfig("testdata/good.yaml", true) + assert.NoError(t, err) + assert.Len(t, c.Rules, 3) + + c, err = NewConfig("testdata/good.yaml", false) + assert.NoError(t, err) + assert.Len(t, c.Rules, len(rule.DefaultRules)+2) + }) + t.Run("config-exclude-a-category", func(t *testing.T) { // Test when configured to exclude a single category - c, err := NewConfig("testdata/exclude-single-category.yaml") + c, err := NewConfig("testdata/exclude-single-category.yaml", false) assert.NoError(t, err) expectedRules := []*rule.Rule{} @@ -180,7 +190,7 @@ func TestNewConfig(t *testing.T) { Rules: expectedRules, ExcludeCategories: []string{"cat2"}, } - expected.ConfigureRules() + expected.ConfigureRules(false) assert.EqualValues(t, expected.Rules, c.Rules) assert.Equal(t, "No findings found.", c.GetSuccessExitMessage()) @@ -188,7 +198,7 @@ func TestNewConfig(t *testing.T) { t.Run("config-exclude-multiple-categories", func(t *testing.T) { // Test when configured to exclude multiple categories - c, err := NewConfig("testdata/exclude-multiple-categories.yaml") + c, err := NewConfig("testdata/exclude-multiple-categories.yaml", false) assert.NoError(t, err) expectedRules := []*rule.Rule{} @@ -203,19 +213,19 @@ func TestNewConfig(t *testing.T) { Rules: expectedRules, ExcludeCategories: []string{"cat1", "cat2"}, } - expected.ConfigureRules() + expected.ConfigureRules(false) assert.EqualValues(t, expected.Rules, c.Rules) assert.Equal(t, "No findings found.", c.GetSuccessExitMessage()) }) t.Run("load-config-with-bad-url", func(t *testing.T) { - _, err := NewConfig("https://raw.githubusercontent.com/get-woke/woke/main/example") + _, err := NewConfig("https://raw.githubusercontent.com/get-woke/woke/main/example", false) assert.Error(t, err) }) t.Run("load-config-with-url", func(t *testing.T) { - c, err := NewConfig("https://raw.githubusercontent.com/get-woke/woke/main/example.yaml") + c, err := NewConfig("https://raw.githubusercontent.com/get-woke/woke/main/example.yaml", false) assert.NoError(t, err) assert.NotNil(t, c) }) diff --git a/testdata/invalid.yaml b/testdata/invalid.yaml new file mode 100644 index 00000000..9f184f32 --- /dev/null +++ b/testdata/invalid.yaml @@ -0,0 +1,14 @@ +- name: rule1 + terms: + - rule1 + alternatives: + - alt-rule1 + severity: warning + options: + include_note: false +- name: rule2 + terms: + - rule2 + alternatives: + - alt-rule2 + severity: warning