diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index baf3dd3e7ec..e833f99d0b1 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -178,6 +178,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_linux.go b/auditbeat/module/auditd/config.go similarity index 83% rename from auditbeat/module/auditd/config_linux.go rename to auditbeat/module/auditd/config.go index 4a03584c3b7..6762a3924a9 100644 --- a/auditbeat/module/auditd/config_linux.go +++ b/auditbeat/module/auditd/config.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +//go:build unix + package auditd import ( @@ -30,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" ) @@ -46,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"` @@ -120,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 { @@ -136,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 } @@ -145,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 } @@ -170,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) @@ -207,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_linux_test.go b/auditbeat/module/auditd/config_linux_test.go deleted file mode 100644 index d48eea8c6c8..00000000000 --- a/auditbeat/module/auditd/config_linux_test.go +++ /dev/null @@ -1,254 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you 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 auditd - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "strconv" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - - conf "github.com/elastic/elastic-agent-libs/config" -) - -func TestConfigValidate(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() - - 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 := ` -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) -} - -func TestConfigValidateWithDuplicates(t *testing.T) { - data := ` -audit_rules: | - -w /etc/passwd -p rwxa -k auth - -w /etc/passwd -k auth` - - _, err := parseConfig(t, data) - if err == nil { - t.Fatal("expected error") - } - t.Log(err) -} - -func TestConfigValidateFailureMode(t *testing.T) { - config := defaultConfig - config.FailureMode = "boom" - err := config.Validate() - assert.Error(t, err) - t.Log(err) -} - -func TestConfigValidateConnectionType(t *testing.T) { - config := defaultConfig - config.SocketType = "Satellite" - err := config.Validate() - assert.Error(t, err) - 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, - }, - } - - 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) - } - }) - } -} - -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") - - dir1, err := ioutil.TempDir("", "rules1") - 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) - } - } - - dir2, err := ioutil.TempDir("", "rules0") - if 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) - } - } - - config.RuleFiles = []string{ - fmt.Sprintf("%s/*.conf", dir1), - fmt.Sprintf("%s/*.conf", dir2), - } - - if err = config.Validate(); 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) - } - 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++ - } -} - -func makeRuleFlags(fileID, ruleID int) string { - return fmt.Sprintf("-w /path/%d/%d -p rwxa -k rule:%d:%d", fileID, ruleID, fileID, ruleID) -} - -func parseConfig(t testing.TB, yaml string) (Config, error) { - c, err := conf.NewConfigWithYAML([]byte(yaml), "") - if err != nil { - t.Fatal(err) - } - - config := defaultConfig - err = c.Unpack(&config) - return config, err -} - -func commands(rules []auditRule) []string { - var cmds []string - for _, r := range rules { - cmds = append(cmds, r.flags) - } - return cmds -} diff --git a/auditbeat/module/auditd/config_test.go b/auditbeat/module/auditd/config_test.go new file mode 100644 index 00000000000..81da2d9b85b --- /dev/null +++ b/auditbeat/module/auditd/config_test.go @@ -0,0 +1,279 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +//go:build unix + +package auditd + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + conf "github.com/elastic/elastic-agent-libs/config" + "github.com/elastic/elastic-agent-libs/logp" +) + +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() + + assert.EqualValues(t, []string{ + "-w /etc/passwd -p wa -k auth", + "-a always,exit -S execve -k exec", + }, commands(rules)) + }) + + 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) + }) + + t.Run("ValidateWithErrorIgnored", func(t *testing.T) { + data := ` +ignore_errors: true +audit_rules: | + -x bad -F flag + -a always,exit -w /etc/passwd + -a always,exit -S fake -k exec + -w /etc/passwd -k auth` + + 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) + } + }) + + t.Run("ValidateWithDuplicates", func(t *testing.T) { + data := ` +audit_rules: | + -w /etc/passwd -p rwxa -k auth + -w /etc/passwd -k auth` + + _, err := parseConfig(t, data) + if err == nil { + t.Fatal("expected error") + } + t.Log(err) + }) + + t.Run("ValidateFailureMode", func(t *testing.T) { + config := defaultConfig + config.FailureMode = "boom" + err := config.Validate() + assert.Error(t, err) + t.Log(err) + }) + + t.Run("ValidateConnectionType", func(t *testing.T) { + config := defaultConfig + config.SocketType = "Satellite" + err := config.Validate() + assert.Error(t, err) + t.Log(err) + }) + + 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, + }, + } + + 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("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) + } + + 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) + } + } + + dir2, err := ioutil.TempDir("", "rules0") + if 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) + } + } + + config.RuleFiles = []string{ + fmt.Sprintf("%s/*.conf", dir1), + fmt.Sprintf("%s/*.conf", dir2), + } + + if err = config.Validate(); 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) + } + 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++ + } + }) +} + +func makeRuleFlags(fileID, ruleID int) string { + return fmt.Sprintf("-w /path/%d/%d -p rwxa -k rule:%d:%d", fileID, ruleID, fileID, ruleID) +} + +func parseConfig(t testing.TB, yaml string) (Config, error) { + c, err := conf.NewConfigWithYAML([]byte(yaml), "") + if err != nil { + t.Fatal(err) + } + + config := defaultConfig + err = c.Unpack(&config) + return config, err +} + +func commands(rules []auditRule) []string { + var cmds []string + for _, r := range rules { + cmds = append(cmds, r.flags) + } + return cmds +}