From 12e708e645859b9304d0ab071c14d2229a8a15ed Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Mon, 16 Oct 2023 09:27:05 +1030 Subject: [PATCH] auditbeat/module/auditd: add ignore_errors config option Setting ignore_errors to true allows incompletely valid rule sets to be used in a configuration. This is equivalent to the -i flag of auditctl. --- CHANGELOG.next.asciidoc | 1 + auditbeat/docs/modules/auditd.asciidoc | 3 + auditbeat/module/auditd/_meta/docs.asciidoc | 3 + auditbeat/module/auditd/config.go | 35 +- auditbeat/module/auditd/config_test.go | 349 +++++++++++--------- 5 files changed, 221 insertions(+), 170 deletions(-) diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index c4ec867eaf3..171abfa0bf7 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -166,6 +166,7 @@ is collected by it. *Auditbeat* +- Add `ignore_errors` option to audit module. {issue}15768[15768] {pull}36851[36851] *Filebeat* diff --git a/auditbeat/docs/modules/auditd.asciidoc b/auditbeat/docs/modules/auditd.asciidoc index a0d2693487e..9204e243f64 100644 --- a/auditbeat/docs/modules/auditd.asciidoc +++ b/auditbeat/docs/modules/auditd.asciidoc @@ -212,6 +212,9 @@ loaded after the rules declared in `audit_rules` are loaded. Wildcards are supported and will expand in lexicographical order. The format is the same as that of the `audit_rules` field. +*`ignore_errors`*:: This setting allows errors during rule loading and parsing +to be ignored, but logged as warnings. + *`backpressure_strategy`*:: Specifies the strategy that {beatname_uc} uses to prevent backpressure from propagating to the kernel and impacting audited processes. diff --git a/auditbeat/module/auditd/_meta/docs.asciidoc b/auditbeat/module/auditd/_meta/docs.asciidoc index 587a40dd982..b1dd7d87c63 100644 --- a/auditbeat/module/auditd/_meta/docs.asciidoc +++ b/auditbeat/module/auditd/_meta/docs.asciidoc @@ -205,6 +205,9 @@ loaded after the rules declared in `audit_rules` are loaded. Wildcards are supported and will expand in lexicographical order. The format is the same as that of the `audit_rules` field. +*`ignore_errors`*:: This setting allows errors during rule loading and parsing +to be ignored, but logged as warnings. + *`backpressure_strategy`*:: Specifies the strategy that {beatname_uc} uses to prevent backpressure from propagating to the kernel and impacting audited processes. diff --git a/auditbeat/module/auditd/config.go b/auditbeat/module/auditd/config.go index 9729e0e4451..6762a3924a9 100644 --- a/auditbeat/module/auditd/config.go +++ b/auditbeat/module/auditd/config.go @@ -32,6 +32,7 @@ import ( "github.com/joeshaw/multierror" + "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/go-libaudit/v2/rule" "github.com/elastic/go-libaudit/v2/rule/flags" ) @@ -48,6 +49,7 @@ type Config struct { RuleFiles []string `config:"audit_rule_files"` // List of rule files. SocketType string `config:"socket_type"` // Socket type to use with the kernel (unicast or multicast). Immutable bool `config:"immutable"` // Sets kernel audit config immutable. + IgnoreErrors bool `config:"ignore_errors"` // Ignore errors when reading and parsing rules, equivalent to auditctl -i. // Tuning options (advanced, use with care) ReassemblerMaxInFlight uint32 `config:"reassembler.max_in_flight"` @@ -122,11 +124,19 @@ func (c Config) rules() []auditRule { } func (c *Config) loadRules() error { + var log *logp.Logger + if c.IgnoreErrors { + log = logp.NewLogger(moduleName) + } + var paths []string for _, pattern := range c.RuleFiles { absPattern, err := filepath.Abs(pattern) if err != nil { - return fmt.Errorf("unable to get the absolute path for %s: %w", pattern, err) + if log == nil { + return fmt.Errorf("unable to get the absolute path for %s: %w", pattern, err) + } + log.Warnf("unable to get the absolute path for %s: %v", pattern, err) } files, err := filepath.Glob(absPattern) if err != nil { @@ -138,7 +148,7 @@ func (c *Config) loadRules() error { knownRules := ruleSet{} - rules, err := readRules(bytes.NewBufferString(c.RulesBlob), "(audit_rules at auditbeat.yml)", knownRules) + rules, err := readRules(bytes.NewBufferString(c.RulesBlob), "(audit_rules at auditbeat.yml)", knownRules, log) if err != nil { return err } @@ -147,9 +157,13 @@ func (c *Config) loadRules() error { for _, filename := range paths { fHandle, err := os.Open(filename) if err != nil { - return fmt.Errorf("unable to open rule file '%s': %w", filename, err) + if log == nil { + return fmt.Errorf("unable to open rule file '%s': %w", filename, err) + } + log.Warnf("unable to open rule file '%s': %v", filename, err) + continue } - rules, err = readRules(fHandle, filename, knownRules) + rules, err = readRules(fHandle, filename, knownRules, log) if err != nil { return err } @@ -172,7 +186,11 @@ func (c Config) failureMode() (uint32, error) { } } -func readRules(reader io.Reader, source string, knownRules ruleSet) (rules []auditRule, err error) { +// readRules reads the audit rules from reader, adding them to knownRules. If +// log is nil, errors will result in an empty rules set being returned. Otherwise +// errors will be logged as warnings and any successfully parsed rules will be +// returned. +func readRules(reader io.Reader, source string, knownRules ruleSet, log *logp.Logger) (rules []auditRule, err error) { var errs multierror.Errors s := bufio.NewScanner(reader) @@ -209,8 +227,11 @@ func readRules(reader io.Reader, source string, knownRules ruleSet) (rules []aud rules = append(rules, rule) } - if len(errs) > 0 { - return nil, fmt.Errorf("failed loading rules: %w", errs.Err()) + if len(errs) != 0 { + if log == nil { + return nil, fmt.Errorf("failed loading rules: %w", errs.Err()) + } + log.Warnf("errors loading rules: %v", errs.Err()) } return rules, nil } diff --git a/auditbeat/module/auditd/config_test.go b/auditbeat/module/auditd/config_test.go index 13e5fda19d0..81da2d9b85b 100644 --- a/auditbeat/module/auditd/config_test.go +++ b/auditbeat/module/auditd/config_test.go @@ -30,206 +30,229 @@ import ( "github.com/stretchr/testify/assert" conf "github.com/elastic/elastic-agent-libs/config" + "github.com/elastic/elastic-agent-libs/logp" ) -func TestConfigValidate(t *testing.T) { - data := ` +func TestConfig(t *testing.T) { + logp.TestingSetup() + + t.Run("Validate", func(t *testing.T) { + data := ` audit_rules: | # Comments and empty lines are ignored. -w /etc/passwd -p wa -k auth -a always,exit -S execve -k exec` - config, err := parseConfig(t, data) - if err != nil { - t.Fatal(err) - } - rules := config.rules() + config, err := parseConfig(t, data) + if err != nil { + t.Fatal(err) + } + rules := config.rules() - assert.EqualValues(t, []string{ - "-w /etc/passwd -p wa -k auth", - "-a always,exit -S execve -k exec", - }, commands(rules)) -} + assert.EqualValues(t, []string{ + "-w /etc/passwd -p wa -k auth", + "-a always,exit -S execve -k exec", + }, commands(rules)) + }) -func TestConfigValidateWithError(t *testing.T) { - data := ` + t.Run("ValidateWithError", func(t *testing.T) { + data := ` audit_rules: | -x bad -F flag -a always,exit -w /etc/passwd -a always,exit -S fake -k exec` - _, err := parseConfig(t, data) - if err == nil { - t.Fatal("expected error") - } - t.Log(err) -} + _, err := parseConfig(t, data) + if err == nil { + t.Fatal("expected error") + } + t.Log(err) + }) -func TestConfigValidateWithDuplicates(t *testing.T) { - data := ` + t.Run("ValidateWithErrorIgnored", func(t *testing.T) { + data := ` +ignore_errors: true audit_rules: | - -w /etc/passwd -p rwxa -k auth + -x bad -F flag + -a always,exit -w /etc/passwd + -a always,exit -S fake -k exec -w /etc/passwd -k auth` - _, err := parseConfig(t, data) - if err == nil { - t.Fatal("expected error") - } - t.Log(err) -} + cfg, err := parseConfig(t, data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(cfg.auditRules) != 1 { + t.Fatalf("unexpected number of rules from parseConfig: got %d, want %d", len(cfg.auditRules), 1) + } + }) -func TestConfigValidateFailureMode(t *testing.T) { - config := defaultConfig - config.FailureMode = "boom" - err := config.Validate() - assert.Error(t, err) - t.Log(err) -} + t.Run("ValidateWithDuplicates", func(t *testing.T) { + data := ` +audit_rules: | + -w /etc/passwd -p rwxa -k auth + -w /etc/passwd -k auth` -func TestConfigValidateConnectionType(t *testing.T) { - config := defaultConfig - config.SocketType = "Satellite" - err := config.Validate() - assert.Error(t, err) - t.Log(err) -} + _, err := parseConfig(t, data) + if err == nil { + t.Fatal("expected error") + } + t.Log(err) + }) -func TestConfigValidateImmutable(t *testing.T) { - tcs := []struct { - name string - socketType string - mustFail bool - }{ - { - name: "Must pass for default", - socketType: "", - mustFail: false, - }, - { - name: "Must pass for unicast", - socketType: "unicast", - mustFail: false, - }, - { - name: "Must fail for multicast", - socketType: "multicast", - mustFail: true, - }, - } + t.Run("ValidateFailureMode", func(t *testing.T) { + config := defaultConfig + config.FailureMode = "boom" + err := config.Validate() + assert.Error(t, err) + t.Log(err) + }) - for _, tc := range tcs { - tc := tc - t.Run(tc.name, func(t *testing.T) { - config := defaultConfig - config.SocketType = tc.socketType - config.Immutable = true - err := config.Validate() - if tc.mustFail { - assert.Error(t, err) - t.Log(err) - } else { - assert.NoError(t, err) - } - }) - } -} + t.Run("ValidateConnectionType", func(t *testing.T) { + config := defaultConfig + config.SocketType = "Satellite" + err := config.Validate() + assert.Error(t, err) + t.Log(err) + }) -func TestConfigRuleOrdering(t *testing.T) { - const fileMode = 0o644 - config := defaultConfig - config.RulesBlob = strings.Join([]string{ - makeRuleFlags(0, 0), - makeRuleFlags(0, 1), - makeRuleFlags(0, 2), - }, "\n") + t.Run("ValidateImmutable", func(t *testing.T) { + tcs := []struct { + name string + socketType string + mustFail bool + }{ + { + name: "Must pass for default", + socketType: "", + mustFail: false, + }, + { + name: "Must pass for unicast", + socketType: "unicast", + mustFail: false, + }, + { + name: "Must fail for multicast", + socketType: "multicast", + mustFail: true, + }, + } - dir1, err := ioutil.TempDir("", "rules1") - if err != nil { - t.Fatal(err) - } + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + config := defaultConfig + config.SocketType = tc.socketType + config.Immutable = true + err := config.Validate() + if tc.mustFail { + assert.Error(t, err) + t.Log(err) + } else { + assert.NoError(t, err) + } + }) + } + }) - for _, file := range []struct { - order int - name string - }{ - {0, "00_first.conf"}, - {5, "99_last.conf"}, - {2, "03_auth.conf"}, - {4, "20_exec.conf"}, - {3, "10_network_access.conf"}, - {1, "01_32bit_abi.conf"}, - } { - path := filepath.Join(dir1, file.name) - content := []byte(strings.Join([]string{ - makeRuleFlags(1+file.order, 0), - makeRuleFlags(1+file.order, 1), - makeRuleFlags(1+file.order, 2), - makeRuleFlags(1+file.order, 3), - }, "\n")) - if err = ioutil.WriteFile(path, content, fileMode); err != nil { + t.Run("RuleOrdering", func(t *testing.T) { + const fileMode = 0o644 + config := defaultConfig + config.RulesBlob = strings.Join([]string{ + makeRuleFlags(0, 0), + makeRuleFlags(0, 1), + makeRuleFlags(0, 2), + }, "\n") + + dir1, err := ioutil.TempDir("", "rules1") + if err != nil { t.Fatal(err) } - } - dir2, err := ioutil.TempDir("", "rules0") - if err != nil { - t.Fatal(err) - } + for _, file := range []struct { + order int + name string + }{ + {0, "00_first.conf"}, + {5, "99_last.conf"}, + {2, "03_auth.conf"}, + {4, "20_exec.conf"}, + {3, "10_network_access.conf"}, + {1, "01_32bit_abi.conf"}, + } { + path := filepath.Join(dir1, file.name) + content := []byte(strings.Join([]string{ + makeRuleFlags(1+file.order, 0), + makeRuleFlags(1+file.order, 1), + makeRuleFlags(1+file.order, 2), + makeRuleFlags(1+file.order, 3), + }, "\n")) + if err = ioutil.WriteFile(path, content, fileMode); err != nil { + t.Fatal(err) + } + } - for _, file := range []struct { - order int - name string - }{ - {3, "99_tail.conf"}, - {0, "00_head.conf"}, - {2, "50_mid.conf"}, - {1, "13.conf"}, - } { - path := filepath.Join(dir2, file.name) - content := []byte(strings.Join([]string{ - makeRuleFlags(10+file.order, 0), - makeRuleFlags(10+file.order, 1), - makeRuleFlags(10+file.order, 2), - makeRuleFlags(10+file.order, 3), - }, "\n")) - if err = ioutil.WriteFile(path, content, fileMode); err != nil { + dir2, err := ioutil.TempDir("", "rules0") + if err != nil { t.Fatal(err) } - } - config.RuleFiles = []string{ - fmt.Sprintf("%s/*.conf", dir1), - fmt.Sprintf("%s/*.conf", dir2), - } - - if err = config.Validate(); err != nil { - t.Fatal(err) - } + for _, file := range []struct { + order int + name string + }{ + {3, "99_tail.conf"}, + {0, "00_head.conf"}, + {2, "50_mid.conf"}, + {1, "13.conf"}, + } { + path := filepath.Join(dir2, file.name) + content := []byte(strings.Join([]string{ + makeRuleFlags(10+file.order, 0), + makeRuleFlags(10+file.order, 1), + makeRuleFlags(10+file.order, 2), + makeRuleFlags(10+file.order, 3), + }, "\n")) + if err = ioutil.WriteFile(path, content, fileMode); err != nil { + t.Fatal(err) + } + } - rules := config.rules() - fileNo, ruleNo := 0, 0 - for _, rule := range rules { - parts := strings.Split(rule.flags, " ") - assert.Len(t, parts, 6, rule.flags) - fields := strings.Split(parts[5], ":") - assert.Len(t, fields, 3, rule.flags) - fileID, err := strconv.Atoi(fields[1]) - if err != nil { - t.Fatal(err, rule.flags) + config.RuleFiles = []string{ + fmt.Sprintf("%s/*.conf", dir1), + fmt.Sprintf("%s/*.conf", dir2), } - ruleID, err := strconv.Atoi(fields[2]) - if err != nil { - t.Fatal(err, rule.flags) + + if err = config.Validate(); err != nil { + t.Fatal(err) } - if fileID > fileNo { - fileNo = fileID - ruleNo = 0 + + rules := config.rules() + fileNo, ruleNo := 0, 0 + for _, rule := range rules { + parts := strings.Split(rule.flags, " ") + assert.Len(t, parts, 6, rule.flags) + fields := strings.Split(parts[5], ":") + assert.Len(t, fields, 3, rule.flags) + fileID, err := strconv.Atoi(fields[1]) + if err != nil { + t.Fatal(err, rule.flags) + } + ruleID, err := strconv.Atoi(fields[2]) + if err != nil { + t.Fatal(err, rule.flags) + } + if fileID > fileNo { + fileNo = fileID + ruleNo = 0 + } + assert.Equal(t, fileNo, fileID, rule.flags) + assert.Equal(t, ruleNo, ruleID, rule.flags) + ruleNo++ } - assert.Equal(t, fileNo, fileID, rule.flags) - assert.Equal(t, ruleNo, ruleID, rule.flags) - ruleNo++ - } + }) } func makeRuleFlags(fileID, ruleID int) string {