From 2bba481413c85b5685d0b8e0c401b8ce362c8bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nedim=20=C5=A0abi=C4=87=C2=B2?= Date: Fri, 15 Dec 2023 13:09:59 +0100 Subject: [PATCH] feat(cli): Rules validation command (#215) --- .github/workflows/pr.yml | 4 + .github/workflows/rules.yml | 21 ++++++ cmd/fibratus/app/root.go | 2 + cmd/fibratus/app/rules/validate.go | 117 +++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + pkg/config/config_windows.go | 22 ++++-- 7 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/rules.yml create mode 100644 cmd/fibratus/app/rules/validate.go diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a7df99ebc..51f575c27 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -74,6 +74,10 @@ jobs: ./make.bat env: TAGS: kcap,filament,yara,yara_static + - uses: actions/upload-artifact@v2 + with: + name: "fibratus-amd64.exe" + path: "./cmd/fibratus/fibratus.exe" test: runs-on: windows-latest diff --git a/.github/workflows/rules.yml b/.github/workflows/rules.yml new file mode 100644 index 000000000..e7c3d1b7a --- /dev/null +++ b/.github/workflows/rules.yml @@ -0,0 +1,21 @@ +name: rules + +on: + workflow_run: + workflows: ["master", "pr"] + types: + - completed + +jobs: + validate: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: actions/download-artifact@v3 + with: + name: fibratus-amd64.exe + path: . + - name: Validate rules + run: | + ./fibratus-amd64.exe rules validate --filters.rules.from-paths=rules\* --filters.macros.from-paths=rules\macros\* diff --git a/cmd/fibratus/app/root.go b/cmd/fibratus/app/root.go index 8b48c8bd3..afdbc4350 100644 --- a/cmd/fibratus/app/root.go +++ b/cmd/fibratus/app/root.go @@ -24,6 +24,7 @@ import ( "github.com/rabbitstack/fibratus/cmd/fibratus/app/config" "github.com/rabbitstack/fibratus/cmd/fibratus/app/list" "github.com/rabbitstack/fibratus/cmd/fibratus/app/replay" + "github.com/rabbitstack/fibratus/cmd/fibratus/app/rules" "github.com/rabbitstack/fibratus/cmd/fibratus/app/service" "github.com/rabbitstack/fibratus/cmd/fibratus/app/stats" "github.com/spf13/cobra" @@ -69,6 +70,7 @@ func init() { RootCmd.AddCommand(stats.Command) RootCmd.AddCommand(config.Command) RootCmd.AddCommand(list.Command) + RootCmd.AddCommand(rules.Command) RootCmd.AddCommand(runCmd) RootCmd.AddCommand(docsCmd) RootCmd.AddCommand(versionCmd) diff --git a/cmd/fibratus/app/rules/validate.go b/cmd/fibratus/app/rules/validate.go new file mode 100644 index 000000000..81f4624d5 --- /dev/null +++ b/cmd/fibratus/app/rules/validate.go @@ -0,0 +1,117 @@ +/* + * 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/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" +) + +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 { + if err := bootstrap.InitConfigAndLogger(cfg); err != nil { + return err + } + + isValidExt := func(path string) bool { + return filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".yaml" + } + + for _, m := range cfg.Filters.Macros.FromPaths { + paths, err := filepath.Glob(m) + if err != nil { + return err + } + for _, path := range paths { + if !isValidExt(path) { + continue + } + emo("%v Loading macros from %s\n", emoji.Magnet, path) + } + } + if err := cfg.Filters.LoadMacros(); err != nil { + return fmt.Errorf("%v %v", emoji.DisappointedFace, err) + } + + for _, r := range cfg.Filters.Rules.FromPaths { + paths, err := filepath.Glob(r) + if err != nil { + return err + } + for _, path := range paths { + if !isValidExt(path) { + continue + } + emo("%v Loading rules from %s\n", emoji.Package, path) + } + } + if err := cfg.Filters.LoadGroups(); err != nil { + return fmt.Errorf("%v %v", emoji.DisappointedFace, err) + } + + for _, group := range cfg.GetRuleGroups() { + for _, rule := range group.Rules { + f := filter.New(rule.Condition, cfg) + err := f.Compile() + 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) + } + } + } + } + + emo("%v Detection rules OK. Ready to go!", emoji.Rocket) + + return nil +} + +func emo(s string, args ...any) { fmt.Printf(s, args...) } diff --git a/go.mod b/go.mod index 3b03988ac..f899762ff 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( ) require ( + github.com/enescakir/emoji v1.0.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 2bff74d4e..d44fdabd6 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= +github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 0311caf92..0d2109141 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -115,11 +115,12 @@ type Config struct { // Options determines which config flags are toggled depending on the command type. type Options struct { - capture bool - replay bool - run bool - list bool - stats bool + capture bool + replay bool + run bool + list bool + stats bool + validate bool } // Option is the type alias for the config option. @@ -160,6 +161,13 @@ func WithStats() Option { } } +// WithValidate determines the validate command is executed. +func WithValidate() Option { + return func(o *Options) { + o.validate = true + } +} + // NewWithOpts builds a new configuration store from a variety of sources such as configuration files, // environment variables or command line flags. func NewWithOpts(options ...Option) *Config { @@ -321,7 +329,7 @@ func (c *Config) File() string { return c.viper.GetString(configFile) } func (c *Config) addFlags() { c.flags.String(configFile, filepath.Join(os.Getenv("PROGRAMFILES"), "fibratus", "config", "fibratus.yml"), "Indicates the location of the configuration file") - if c.opts.run || c.opts.replay { + if c.opts.run || c.opts.replay || c.opts.validate { c.flags.StringP(filamentName, "f", "", "Specifies the filament to execute") // initialize default rules paths @@ -342,7 +350,7 @@ func (c *Config) addFlags() { if c.opts.replay { c.flags.StringP(kcapFile, "k", "", "The path of the input kcap file") } - if c.opts.run || c.opts.replay || c.opts.list { + if c.opts.run || c.opts.replay || c.opts.list || c.opts.validate { c.flags.String(filamentPath, filepath.Join(os.Getenv("PROGRAMFILES"), "fibratus", "filaments"), "Denotes the directory where filaments are located") } if c.opts.run || c.opts.replay || c.opts.capture || c.opts.stats {