diff --git a/cmd/fibratus/app/rules/list.go b/cmd/fibratus/app/rules/list.go new file mode 100644 index 000000000..b0a26483c --- /dev/null +++ b/cmd/fibratus/app/rules/list.go @@ -0,0 +1,121 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rules + +import ( + "fmt" + "github.com/enescakir/emoji" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/rabbitstack/fibratus/internal/bootstrap" + "os" + "strings" +) + +func listRules() error { + if err := bootstrap.InitConfigAndLogger(cfg); err != nil { + return err + } + if err := cfg.Filters.LoadGroups(); err != nil { + return fmt.Errorf("%v %v", emoji.DisappointedFace, err) + } + groups := cfg.GetRuleGroups() + if len(groups) == 0 { + return fmt.Errorf("%v no rules found in %s", emoji.DisappointedFace, strings.Join(cfg.Filters.Rules.FromPaths, ",")) + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + + // render summary + if summarized { + t.AppendHeader(table.Row{"Tactic", "# Rules"}) + t.SetColumnConfigs([]table.ColumnConfig{ + {Name: "Tactic", WidthMin: 50, WidthMax: 50}, + {Name: "#", WidthMin: 50, WidthMax: 50}, + }) + tactics := make(map[string]int) + techniques := make(map[string]int) + for _, group := range groups { + tactics[group.Labels["tactic.name"]] += len(group.Rules) + techniques[group.Labels["technique.name"]] += len(group.Rules) + } + tot := 0 + for tac, n := range tactics { + t.AppendRow(table.Row{tac, n}) + tot += n + } + + t.AppendSeparator() + t.AppendRow(table.Row{"TECHNIQUE", "# RULES"}) + t.AppendSeparator() + + for tec, n := range techniques { + t.AppendRow(table.Row{tec, n}) + } + + t.AppendFooter(table.Row{"TOTAL", tot}) + } else { + // show all rules + t.AppendHeader(table.Row{"#", "Rule", "Technique", "Tactic"}) + t.SetColumnConfigs([]table.ColumnConfig{ + {Name: "#", WidthMax: 5}, + {Name: "Rule"}, + {Name: "Technique"}, + {Name: "Tactic", WidthMax: 50}, + }) + + n := 0 + tactics := make(map[string]int) + techniques := make(map[string]int) + + for _, group := range groups { + tac := group.Labels["tactic.name"] + tec := group.Labels["technique.name"] + if _, ok := tactics[tac]; !ok { + tactics[tac] = 1 + } + if _, ok := tactics[tec]; !ok { + techniques[tec] = 1 + } + for _, rule := range group.Rules { + t.AppendRow(table.Row{n + 1, rule.Name, group.Name, group.Labels["tactic.name"]}) + n++ + } + } + + var ( + totTat int + totTec int + ) + + for _, n := range tactics { + totTat += n + } + for _, n := range techniques { + totTec += n + } + + t.AppendFooter(table.Row{"TOTAL", n, totTec, totTat}) + } + + t.Render() + + return nil +} diff --git a/cmd/fibratus/app/rules/rules.go b/cmd/fibratus/app/rules/rules.go new file mode 100644 index 000000000..49d1086f6 --- /dev/null +++ b/cmd/fibratus/app/rules/rules.go @@ -0,0 +1,67 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rules + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/spf13/cobra" +) + +var Command = &cobra.Command{ + Use: "rules", + Short: "Validate, list, or search detection rules", +} + +var validateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate rules for structural and syntactic correctness", + RunE: validate, +} + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List rules", + RunE: list, +} + +var cfg = config.NewWithOpts(config.WithValidate(), config.WithList()) + +var ( + summarized bool +) + +func init() { + cfg.MustViperize(Command) + + Command.AddCommand(validateCmd) + + listCmd.PersistentFlags().BoolVarP(&summarized, "summary", "s", false, "Show rules summary by MITRE tactics and techniques") + Command.AddCommand(listCmd) +} + +func validate(cmd *cobra.Command, args []string) error { + return validateRules() +} + +func list(cmd *cobra.Command, args []string) error { + return listRules() +} + +func emo(s string, args ...any) { fmt.Printf(s, args...) } diff --git a/cmd/fibratus/app/rules/validate.go b/cmd/fibratus/app/rules/validate.go index 81f4624d5..7492010a5 100644 --- a/cmd/fibratus/app/rules/validate.go +++ b/cmd/fibratus/app/rules/validate.go @@ -22,32 +22,13 @@ import ( "fmt" "github.com/enescakir/emoji" "github.com/rabbitstack/fibratus/internal/bootstrap" - "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/filter/fields" - "github.com/spf13/cobra" "path/filepath" + "strings" ) -var Command = &cobra.Command{ - Use: "rules", - Short: "Validate, list, or search detection rules", -} - -var validateCmd = &cobra.Command{ - Use: "validate", - Short: "Validate rules for structural and syntactic correctness", - RunE: validate, -} - -var cfg = config.NewWithOpts(config.WithValidate()) - -func init() { - cfg.MustViperize(Command) - Command.AddCommand(validateCmd) -} - -func validate(cmd *cobra.Command, args []string) error { +func validateRules() error { if err := bootstrap.InitConfigAndLogger(cfg); err != nil { return err } @@ -55,7 +36,7 @@ func validate(cmd *cobra.Command, args []string) error { isValidExt := func(path string) bool { return filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".yaml" } - + // load macros and rules for _, m := range cfg.Filters.Macros.FromPaths { paths, err := filepath.Glob(m) if err != nil { @@ -65,7 +46,7 @@ func validate(cmd *cobra.Command, args []string) error { if !isValidExt(path) { continue } - emo("%v Loading macros from %s\n", emoji.Magnet, path) + emo("%v Loading macros from %s\n", emoji.Hook, path) } } if err := cfg.Filters.LoadMacros(); err != nil { @@ -87,7 +68,12 @@ func validate(cmd *cobra.Command, args []string) error { if err := cfg.Filters.LoadGroups(); err != nil { return fmt.Errorf("%v %v", emoji.DisappointedFace, err) } + if len(cfg.GetRuleGroups()) == 0 { + return fmt.Errorf("%v no rules found in %s", emoji.DisappointedFace, strings.Join(cfg.Filters.Rules.FromPaths, ",")) + } + warnings := make([]string, 0) + // validate rule for every group for _, group := range cfg.GetRuleGroups() { for _, rule := range group.Rules { f := filter.New(rule.Condition, cfg) @@ -95,23 +81,21 @@ func validate(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("%v %v", emoji.DisappointedFace, filter.ErrInvalidFilter(rule.Name, group.Name, err)) } - for _, field := range f.GetFields() { - deprecated, d := fields.IsDeprecated(field) - if deprecated { - emo("%v Deprecation: %s rule uses "+ - "the [%s] field which was deprecated starting "+ - "from version %s. "+ - "Please consider migrating to %s field(s) "+ - "because [%s] will be removed in future versions\n", - emoji.Warning, rule.Name, field, d.Since, d.Fields, field) + for _, fld := range f.GetFields() { + if isDeprecated, dep := fields.IsDeprecated(fld); isDeprecated { + warnings = append(warnings, + fmt.Sprintf("%s field deprecated in favor of %v in rule %s", fld.String(), dep.Fields, rule.Name)) } } } } + if len(warnings) > 0 { + for _, warn := range warnings { + emo("%v %s\n", emoji.Warning, warn) + } + fmt.Printf("%d warning(s)\n", len(warnings)) + } - emo("%v Detection rules OK. Ready to go!", emoji.Rocket) - + emo("%v Validation successful. Ready to go!", emoji.Rocket) return nil } - -func emo(s string, args ...any) { fmt.Printf(s, args...) }