From 1a119f792b1eacae390f7281011140f3027c90ba Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 24 Apr 2024 10:31:35 +0100 Subject: [PATCH 1/2] CE changes to support exclusion in audit --- audit/backend.go | 2 ++ audit/backend_test.go | 18 +++++++++++++++ audit/entry_formatter.go | 11 +++++++++ audit/entry_formatter_ce.go | 18 +++++++++++++++ audit/entry_formatter_ce_test.go | 37 ++++++++++++++++++++++++++++++ audit/entry_formatter_config.go | 8 +++++++ audit/entry_formatter_config_ce.go | 16 +++++++++++++ 7 files changed, 110 insertions(+) create mode 100644 audit/entry_formatter_ce.go create mode 100644 audit/entry_formatter_ce_test.go create mode 100644 audit/entry_formatter_config_ce.go diff --git a/audit/backend.go b/audit/backend.go index 49d46c8f00394..4bce4b6edb7d0 100644 --- a/audit/backend.go +++ b/audit/backend.go @@ -19,6 +19,7 @@ import ( const ( optionElideListResponses = "elide_list_responses" + optionExclude = "exclude" optionFallback = "fallback" optionFilter = "filter" optionFormat = "format" @@ -253,6 +254,7 @@ func HasInvalidOptions(options map[string]string) bool { // are only for use in the Enterprise version of Vault. func hasEnterpriseAuditOptions(options map[string]string) bool { enterpriseAuditOptions := []string{ + optionExclude, optionFallback, optionFilter, } diff --git a/audit/backend_test.go b/audit/backend_test.go index f2a2da363033f..b0d4809643b4d 100644 --- a/audit/backend_test.go +++ b/audit/backend_test.go @@ -188,6 +188,15 @@ func TestBackend_hasEnterpriseAuditOptions(t *testing.T) { }, expected: true, }, + "ent-opt-exclude": { + input: map[string]string{ + "exclude": `{ + "condition": "\"/request/mount_type\" == transit", + "fields": [ "/request/data", "/response/data" ] + }`, + }, + expected: true, + }, } for name, tc := range tests { @@ -241,6 +250,15 @@ func TestBackend_hasInvalidAuditOptions(t *testing.T) { }, expected: !constants.IsEnterprise, }, + "ent-opt-exclude": { + input: map[string]string{ + "exclude": `{ + "condition": "\"/request/mount_type\" == transit", + "fields": [ "/request/data", "/response/data" ] + }`, + }, + expected: !constants.IsEnterprise, + }, } for name, tc := range tests { diff --git a/audit/entry_formatter.go b/audit/entry_formatter.go index 74c0c4035eff7..0072b704dbda2 100644 --- a/audit/entry_formatter.go +++ b/audit/entry_formatter.go @@ -162,6 +162,17 @@ func (f *entryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * return nil, fmt.Errorf("unable to parse %s from audit event: %w", a.Subtype, err) } + // If this pipeline has been configured with (Enterprise-only) exclusions then + // attempt to exclude the fields from the audit entry. + if f.shouldExclude() { + m, err := f.excludeFields(entry) + if err != nil { + return nil, fmt.Errorf("unable to exclude %s audit data from %q: %w", a.Subtype, f.name, err) + } + + entry = m + } + result, err := jsonutil.EncodeJSON(entry) if err != nil { return nil, fmt.Errorf("unable to format %s: %w", a.Subtype, err) diff --git a/audit/entry_formatter_ce.go b/audit/entry_formatter_ce.go new file mode 100644 index 0000000000000..7c3f33212e69c --- /dev/null +++ b/audit/entry_formatter_ce.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package audit + +import ( + "errors" +) + +func (f *entryFormatter) shouldExclude() bool { + return false +} + +func (f *entryFormatter) excludeFields(entry any) (map[string]any, error) { + return nil, errors.New("enterprise-only feature: audit exclusion") +} diff --git a/audit/entry_formatter_ce_test.go b/audit/entry_formatter_ce_test.go new file mode 100644 index 0000000000000..ffe181f4b882b --- /dev/null +++ b/audit/entry_formatter_ce_test.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package audit + +import ( + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" +) + +// TestEntryFormatter_excludeFields tests that we can exclude data based on the +// pre-configured conditions/fields of the EntryFormatter. It covers some scenarios +// where we expect errors due to invalid input, which is unlikely to happen in reality. +func TestEntryFormatter_excludeFields(t *testing.T) { + // Create the formatter node. + cfg, err := newFormatterConfig(&testHeaderFormatter{}, nil) + require.NoError(t, err) + ss := newStaticSalt(t) + + // We intentionally create the EntryFormatter manually, as we wouldn't be + // able to set exclusions via NewEntryFormatter WithExclusions option. + formatter := &entryFormatter{ + config: cfg, + salter: ss, + logger: hclog.NewNullLogger(), + name: "juan", + } + + res, err := formatter.excludeFields(nil) + require.Error(t, err) + require.EqualError(t, err, "enterprise-only feature: audit exclusion") + require.Nil(t, res) +} diff --git a/audit/entry_formatter_config.go b/audit/entry_formatter_config.go index 2caefb02e3463..3846c6c5992f1 100644 --- a/audit/entry_formatter_config.go +++ b/audit/entry_formatter_config.go @@ -12,6 +12,8 @@ import ( // formatterConfig is used to provide basic configuration to a formatter. // Use newFormatterConfig to initialize the formatterConfig struct. type formatterConfig struct { + formatterConfigEnt + raw bool hmacAccessor bool @@ -101,7 +103,13 @@ func newFormatterConfig(headerFormatter HeaderFormatter, config map[string]strin return formatterConfig{}, err } + fmtCfgEnt, err := newFormatterConfigEnt(config) + if err != nil { + return formatterConfig{}, err + } + return formatterConfig{ + formatterConfigEnt: fmtCfgEnt, headerFormatter: headerFormatter, elideListResponses: opts.withElision, hmacAccessor: opts.withHMACAccessor, diff --git a/audit/entry_formatter_config_ce.go b/audit/entry_formatter_config_ce.go new file mode 100644 index 0000000000000..43b7307f4d713 --- /dev/null +++ b/audit/entry_formatter_config_ce.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package audit + +// formatterConfigEnt provides extensions to a formatterConfig which behave differently +// for Enterprise and community edition. +// NOTE: Use newFormatterConfigEnt to initialize the formatterConfigEnt struct. +type formatterConfigEnt struct{} + +// newFormatterConfigEnt should be used to create formatterConfigEnt. +func newFormatterConfigEnt(config map[string]string) (formatterConfigEnt, error) { + return formatterConfigEnt{}, nil +} From 7c1524b78ac52ca1d0d527a73ca0174ab9ea7a92 Mon Sep 17 00:00:00 2001 From: Kuba Wieczorek Date: Tue, 4 Jun 2024 16:57:46 +0100 Subject: [PATCH 2/2] Add an external test for audit exclusion --- .../audit/audit_exclusion_test.go | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 vault/external_tests/audit/audit_exclusion_test.go diff --git a/vault/external_tests/audit/audit_exclusion_test.go b/vault/external_tests/audit/audit_exclusion_test.go new file mode 100644 index 0000000000000..a76268f867e9b --- /dev/null +++ b/vault/external_tests/audit/audit_exclusion_test.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package audit + +import ( + "testing" + + "github.com/hashicorp/vault/helper/constants" + "github.com/hashicorp/vault/helper/testhelpers/minimal" + "github.com/stretchr/testify/require" +) + +// TestAudit_Exclusion_ByVaultVersion ensures that the audit device 'exclude' +// option is only supported in the enterprise edition of the product. +func TestAudit_Exclusion_ByVaultVersion(t *testing.T) { + t.Parallel() + + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + + // Attempt to create an audit device with exclusion enabled. + mountPointFilterDevicePath := "mountpoint" + mountPointFilterDeviceData := map[string]any{ + "type": "file", + "description": "", + "local": false, + "options": map[string]any{ + "file_path": "discard", + "exclude": "[ { \"fields\": [ \"/response/data\" ] } ]", + }, + } + + _, err := client.Logical().Write("sys/audit/"+mountPointFilterDevicePath, mountPointFilterDeviceData) + if constants.IsEnterprise { + require.NoError(t, err) + } else { + require.Error(t, err) + require.ErrorContains(t, err, "enterprise-only options supplied") + } + + devices, err := client.Sys().ListAudit() + require.NoError(t, err) + if constants.IsEnterprise { + require.Len(t, devices, 1) + } else { + // Ensure the device has not been created. + require.Len(t, devices, 0) + } +}