Skip to content

Commit 74ba252

Browse files
authored
feat(cli): Implement list rules command (#229)
1 parent 13f15a0 commit 74ba252

3 files changed

Lines changed: 208 additions & 36 deletions

File tree

cmd/fibratus/app/rules/list.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2021-2022 by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package rules
20+
21+
import (
22+
"fmt"
23+
"github.com/enescakir/emoji"
24+
"github.com/jedib0t/go-pretty/v6/table"
25+
"github.com/rabbitstack/fibratus/internal/bootstrap"
26+
"os"
27+
"strings"
28+
)
29+
30+
func listRules() error {
31+
if err := bootstrap.InitConfigAndLogger(cfg); err != nil {
32+
return err
33+
}
34+
if err := cfg.Filters.LoadGroups(); err != nil {
35+
return fmt.Errorf("%v %v", emoji.DisappointedFace, err)
36+
}
37+
groups := cfg.GetRuleGroups()
38+
if len(groups) == 0 {
39+
return fmt.Errorf("%v no rules found in %s", emoji.DisappointedFace, strings.Join(cfg.Filters.Rules.FromPaths, ","))
40+
}
41+
42+
t := table.NewWriter()
43+
t.SetOutputMirror(os.Stdout)
44+
t.SetStyle(table.StyleLight)
45+
46+
// render summary
47+
if summarized {
48+
t.AppendHeader(table.Row{"Tactic", "# Rules"})
49+
t.SetColumnConfigs([]table.ColumnConfig{
50+
{Name: "Tactic", WidthMin: 50, WidthMax: 50},
51+
{Name: "#", WidthMin: 50, WidthMax: 50},
52+
})
53+
tactics := make(map[string]int)
54+
techniques := make(map[string]int)
55+
for _, group := range groups {
56+
tactics[group.Labels["tactic.name"]] += len(group.Rules)
57+
techniques[group.Labels["technique.name"]] += len(group.Rules)
58+
}
59+
tot := 0
60+
for tac, n := range tactics {
61+
t.AppendRow(table.Row{tac, n})
62+
tot += n
63+
}
64+
65+
t.AppendSeparator()
66+
t.AppendRow(table.Row{"TECHNIQUE", "# RULES"})
67+
t.AppendSeparator()
68+
69+
for tec, n := range techniques {
70+
t.AppendRow(table.Row{tec, n})
71+
}
72+
73+
t.AppendFooter(table.Row{"TOTAL", tot})
74+
} else {
75+
// show all rules
76+
t.AppendHeader(table.Row{"#", "Rule", "Technique", "Tactic"})
77+
t.SetColumnConfigs([]table.ColumnConfig{
78+
{Name: "#", WidthMax: 5},
79+
{Name: "Rule"},
80+
{Name: "Technique"},
81+
{Name: "Tactic", WidthMax: 50},
82+
})
83+
84+
n := 0
85+
tactics := make(map[string]int)
86+
techniques := make(map[string]int)
87+
88+
for _, group := range groups {
89+
tac := group.Labels["tactic.name"]
90+
tec := group.Labels["technique.name"]
91+
if _, ok := tactics[tac]; !ok {
92+
tactics[tac] = 1
93+
}
94+
if _, ok := tactics[tec]; !ok {
95+
techniques[tec] = 1
96+
}
97+
for _, rule := range group.Rules {
98+
t.AppendRow(table.Row{n + 1, rule.Name, group.Name, group.Labels["tactic.name"]})
99+
n++
100+
}
101+
}
102+
103+
var (
104+
totTat int
105+
totTec int
106+
)
107+
108+
for _, n := range tactics {
109+
totTat += n
110+
}
111+
for _, n := range techniques {
112+
totTec += n
113+
}
114+
115+
t.AppendFooter(table.Row{"TOTAL", n, totTec, totTat})
116+
}
117+
118+
t.Render()
119+
120+
return nil
121+
}

cmd/fibratus/app/rules/rules.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2021-2022 by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package rules
20+
21+
import (
22+
"fmt"
23+
"github.com/rabbitstack/fibratus/pkg/config"
24+
"github.com/spf13/cobra"
25+
)
26+
27+
var Command = &cobra.Command{
28+
Use: "rules",
29+
Short: "Validate, list, or search detection rules",
30+
}
31+
32+
var validateCmd = &cobra.Command{
33+
Use: "validate",
34+
Short: "Validate rules for structural and syntactic correctness",
35+
RunE: validate,
36+
}
37+
38+
var listCmd = &cobra.Command{
39+
Use: "list",
40+
Short: "List rules",
41+
RunE: list,
42+
}
43+
44+
var cfg = config.NewWithOpts(config.WithValidate(), config.WithList())
45+
46+
var (
47+
summarized bool
48+
)
49+
50+
func init() {
51+
cfg.MustViperize(Command)
52+
53+
Command.AddCommand(validateCmd)
54+
55+
listCmd.PersistentFlags().BoolVarP(&summarized, "summary", "s", false, "Show rules summary by MITRE tactics and techniques")
56+
Command.AddCommand(listCmd)
57+
}
58+
59+
func validate(cmd *cobra.Command, args []string) error {
60+
return validateRules()
61+
}
62+
63+
func list(cmd *cobra.Command, args []string) error {
64+
return listRules()
65+
}
66+
67+
func emo(s string, args ...any) { fmt.Printf(s, args...) }

cmd/fibratus/app/rules/validate.go

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,40 +22,21 @@ import (
2222
"fmt"
2323
"github.com/enescakir/emoji"
2424
"github.com/rabbitstack/fibratus/internal/bootstrap"
25-
"github.com/rabbitstack/fibratus/pkg/config"
2625
"github.com/rabbitstack/fibratus/pkg/filter"
2726
"github.com/rabbitstack/fibratus/pkg/filter/fields"
28-
"github.com/spf13/cobra"
2927
"path/filepath"
28+
"strings"
3029
)
3130

32-
var Command = &cobra.Command{
33-
Use: "rules",
34-
Short: "Validate, list, or search detection rules",
35-
}
36-
37-
var validateCmd = &cobra.Command{
38-
Use: "validate",
39-
Short: "Validate rules for structural and syntactic correctness",
40-
RunE: validate,
41-
}
42-
43-
var cfg = config.NewWithOpts(config.WithValidate())
44-
45-
func init() {
46-
cfg.MustViperize(Command)
47-
Command.AddCommand(validateCmd)
48-
}
49-
50-
func validate(cmd *cobra.Command, args []string) error {
31+
func validateRules() error {
5132
if err := bootstrap.InitConfigAndLogger(cfg); err != nil {
5233
return err
5334
}
5435

5536
isValidExt := func(path string) bool {
5637
return filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".yaml"
5738
}
58-
39+
// load macros and rules
5940
for _, m := range cfg.Filters.Macros.FromPaths {
6041
paths, err := filepath.Glob(m)
6142
if err != nil {
@@ -65,7 +46,7 @@ func validate(cmd *cobra.Command, args []string) error {
6546
if !isValidExt(path) {
6647
continue
6748
}
68-
emo("%v Loading macros from %s\n", emoji.Magnet, path)
49+
emo("%v Loading macros from %s\n", emoji.Hook, path)
6950
}
7051
}
7152
if err := cfg.Filters.LoadMacros(); err != nil {
@@ -87,31 +68,34 @@ func validate(cmd *cobra.Command, args []string) error {
8768
if err := cfg.Filters.LoadGroups(); err != nil {
8869
return fmt.Errorf("%v %v", emoji.DisappointedFace, err)
8970
}
71+
if len(cfg.GetRuleGroups()) == 0 {
72+
return fmt.Errorf("%v no rules found in %s", emoji.DisappointedFace, strings.Join(cfg.Filters.Rules.FromPaths, ","))
73+
}
9074

75+
warnings := make([]string, 0)
76+
// validate rule for every group
9177
for _, group := range cfg.GetRuleGroups() {
9278
for _, rule := range group.Rules {
9379
f := filter.New(rule.Condition, cfg)
9480
err := f.Compile()
9581
if err != nil {
9682
return fmt.Errorf("%v %v", emoji.DisappointedFace, filter.ErrInvalidFilter(rule.Name, group.Name, err))
9783
}
98-
for _, field := range f.GetFields() {
99-
deprecated, d := fields.IsDeprecated(field)
100-
if deprecated {
101-
emo("%v Deprecation: %s rule uses "+
102-
"the [%s] field which was deprecated starting "+
103-
"from version %s. "+
104-
"Please consider migrating to %s field(s) "+
105-
"because [%s] will be removed in future versions\n",
106-
emoji.Warning, rule.Name, field, d.Since, d.Fields, field)
84+
for _, fld := range f.GetFields() {
85+
if isDeprecated, dep := fields.IsDeprecated(fld); isDeprecated {
86+
warnings = append(warnings,
87+
fmt.Sprintf("%s field deprecated in favor of %v in rule %s", fld.String(), dep.Fields, rule.Name))
10788
}
10889
}
10990
}
11091
}
92+
if len(warnings) > 0 {
93+
for _, warn := range warnings {
94+
emo("%v %s\n", emoji.Warning, warn)
95+
}
96+
fmt.Printf("%d warning(s)\n", len(warnings))
97+
}
11198

112-
emo("%v Detection rules OK. Ready to go!", emoji.Rocket)
113-
99+
emo("%v Validation successful. Ready to go!", emoji.Rocket)
114100
return nil
115101
}
116-
117-
func emo(s string, args ...any) { fmt.Printf(s, args...) }

0 commit comments

Comments
 (0)