Skip to content

Commit 17ffe62

Browse files
kubawipeteski22
andauthored
[VAULT-22481] Add audit filtering feature (#24558)
* VAULT-22481: Audit filter node (#24465) * Initial commit on adding filter nodes for audit * tests for audit filter * test: longer filter - more conditions * copywrite headers * Check interface for the right type * Add audit filtering feature (#24554) * Support filter nodes in backend factories and add some tests * More tests and cleanup * Attempt to move control of registration for nodes and pipelines to the audit broker (#24505) * invert control of the pipelines/nodes to the audit broker vs. within each backend * update noop audit test code to implement the pipeliner interface * noop mount path has trailing slash * attempting to make NoopAudit more friendly * NoopAudit uses known salt * Refactor audit.ProcessManual to support filter nodes * HasFiltering * rename the pipeliner * use exported AuditEvent in Filter * Add tests for registering and deregistering backends on the audit broker * Add missing licence header to one file, fix a typo in two tests --------- Co-authored-by: Peter Wilson <peter.wilson@hashicorp.com> * Add changelog file * update bexpr datum to use a strong type * go docs updates * test path * PR review comments * handle scenarios/outcomes from broker.send * don't need to re-check the complete sinks * add extra check to deregister to ensure that re-registering non-filtered device sets sink threshold * Ensure that the multierror is appended before attempting to return it --------- Co-authored-by: Peter Wilson <peter.wilson@hashicorp.com>
1 parent 52c02ae commit 17ffe62

31 files changed

+2648
-399
lines changed

audit/entry_filter.go

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package audit
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/hashicorp/eventlogger"
12+
"github.com/hashicorp/go-bexpr"
13+
"github.com/hashicorp/vault/helper/namespace"
14+
"github.com/hashicorp/vault/internal/observability/event"
15+
)
16+
17+
var _ eventlogger.Node = (*EntryFilter)(nil)
18+
19+
// NewEntryFilter should be used to create an EntryFilter node.
20+
// The filter supplied should be in bexpr format and reference fields from logical.LogInputBexpr.
21+
func NewEntryFilter(filter string) (*EntryFilter, error) {
22+
const op = "audit.NewEntryFilter"
23+
24+
filter = strings.TrimSpace(filter)
25+
if filter == "" {
26+
return nil, fmt.Errorf("%s: cannot create new audit filter with empty filter expression: %w", op, event.ErrInvalidParameter)
27+
}
28+
29+
eval, err := bexpr.CreateEvaluator(filter)
30+
if err != nil {
31+
return nil, fmt.Errorf("%s: cannot create new audit filter: %w", op, err)
32+
}
33+
34+
return &EntryFilter{evaluator: eval}, nil
35+
}
36+
37+
// Reopen is a no-op for the filter node.
38+
func (*EntryFilter) Reopen() error {
39+
return nil
40+
}
41+
42+
// Type describes the type of this node (filter).
43+
func (*EntryFilter) Type() eventlogger.NodeType {
44+
return eventlogger.NodeTypeFilter
45+
}
46+
47+
// Process will attempt to parse the incoming event data and decide whether it
48+
// should be filtered or remain in the pipeline and passed to the next node.
49+
func (f *EntryFilter) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
50+
const op = "audit.(EntryFilter).Process"
51+
52+
select {
53+
case <-ctx.Done():
54+
return nil, ctx.Err()
55+
default:
56+
}
57+
58+
if e == nil {
59+
return nil, fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter)
60+
}
61+
62+
a, ok := e.Payload.(*AuditEvent)
63+
if !ok {
64+
return nil, fmt.Errorf("%s: cannot parse event payload: %w", op, event.ErrInvalidParameter)
65+
}
66+
67+
// If we don't have data to process, then we're done.
68+
if a.Data == nil {
69+
return nil, nil
70+
}
71+
72+
ns, err := namespace.FromContext(ctx)
73+
if err != nil {
74+
return nil, fmt.Errorf("%s: cannot obtain namespace: %w", op, err)
75+
}
76+
77+
datum := a.Data.BexprDatum(ns.Path)
78+
79+
result, err := f.evaluator.Evaluate(datum)
80+
if err != nil {
81+
return nil, fmt.Errorf("%s: unable to evaluate filter: %w", op, err)
82+
}
83+
84+
if result {
85+
// Allow this event to carry on through the pipeline.
86+
return e, nil
87+
}
88+
89+
// End process of this pipeline.
90+
return nil, nil
91+
}

audit/entry_filter_test.go

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package audit
5+
6+
import (
7+
"context"
8+
"testing"
9+
"time"
10+
11+
"github.com/hashicorp/eventlogger"
12+
"github.com/hashicorp/vault/helper/namespace"
13+
"github.com/hashicorp/vault/internal/observability/event"
14+
"github.com/hashicorp/vault/sdk/logical"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// TestEntryFilter_NewEntryFilter tests that we can create EntryFilter types correctly.
19+
func TestEntryFilter_NewEntryFilter(t *testing.T) {
20+
t.Parallel()
21+
22+
tests := map[string]struct {
23+
Filter string
24+
IsErrorExpected bool
25+
ExpectedErrorMessage string
26+
}{
27+
"empty-filter": {
28+
Filter: "",
29+
IsErrorExpected: true,
30+
ExpectedErrorMessage: "audit.NewEntryFilter: cannot create new audit filter with empty filter expression: invalid parameter",
31+
},
32+
"spacey-filter": {
33+
Filter: " ",
34+
IsErrorExpected: true,
35+
ExpectedErrorMessage: "audit.NewEntryFilter: cannot create new audit filter with empty filter expression: invalid parameter",
36+
},
37+
"bad-filter": {
38+
Filter: "____",
39+
IsErrorExpected: true,
40+
ExpectedErrorMessage: "audit.NewEntryFilter: cannot create new audit filter",
41+
},
42+
"good-filter": {
43+
Filter: "foo == bar",
44+
IsErrorExpected: false,
45+
},
46+
}
47+
48+
for name, tc := range tests {
49+
name := name
50+
tc := tc
51+
t.Run(name, func(t *testing.T) {
52+
t.Parallel()
53+
54+
f, err := NewEntryFilter(tc.Filter)
55+
switch {
56+
case tc.IsErrorExpected:
57+
require.ErrorContains(t, err, tc.ExpectedErrorMessage)
58+
require.Nil(t, f)
59+
default:
60+
require.NoError(t, err)
61+
require.NotNil(t, f)
62+
}
63+
})
64+
}
65+
}
66+
67+
// TestEntryFilter_Reopen ensures we can reopen the filter node.
68+
func TestEntryFilter_Reopen(t *testing.T) {
69+
t.Parallel()
70+
71+
f := &EntryFilter{}
72+
res := f.Reopen()
73+
require.Nil(t, res)
74+
}
75+
76+
// TestEntryFilter_Type ensures we always return the right type for this node.
77+
func TestEntryFilter_Type(t *testing.T) {
78+
t.Parallel()
79+
80+
f := &EntryFilter{}
81+
require.Equal(t, eventlogger.NodeTypeFilter, f.Type())
82+
}
83+
84+
// TestEntryFilter_Process_ContextDone ensures that we stop processing the event
85+
// if the context was cancelled.
86+
func TestEntryFilter_Process_ContextDone(t *testing.T) {
87+
t.Parallel()
88+
89+
ctx, cancel := context.WithCancel(context.Background())
90+
91+
// Explicitly cancel the context
92+
cancel()
93+
94+
l, err := NewEntryFilter("foo == bar")
95+
require.NoError(t, err)
96+
97+
// Fake audit event
98+
a, err := NewEvent(RequestType)
99+
require.NoError(t, err)
100+
101+
// Fake event logger event
102+
e := &eventlogger.Event{
103+
Type: eventlogger.EventType(event.AuditType.String()),
104+
CreatedAt: time.Now(),
105+
Formatted: make(map[string][]byte),
106+
Payload: a,
107+
}
108+
109+
e2, err := l.Process(ctx, e)
110+
111+
require.Error(t, err)
112+
require.ErrorContains(t, err, "context canceled")
113+
114+
// Ensure that the pipeline won't continue.
115+
require.Nil(t, e2)
116+
}
117+
118+
// TestEntryFilter_Process_NilEvent ensures we receive the right error when the
119+
// event we are trying to process is nil.
120+
func TestEntryFilter_Process_NilEvent(t *testing.T) {
121+
t.Parallel()
122+
123+
l, err := NewEntryFilter("foo == bar")
124+
require.NoError(t, err)
125+
e, err := l.Process(context.Background(), nil)
126+
require.Error(t, err)
127+
require.EqualError(t, err, "audit.(EntryFilter).Process: event is nil: invalid parameter")
128+
129+
// Ensure that the pipeline won't continue.
130+
require.Nil(t, e)
131+
}
132+
133+
// TestEntryFilter_Process_BadPayload ensures we receive the correct error when
134+
// attempting to process an event with a payload that cannot be parsed back to
135+
// an audit event.
136+
func TestEntryFilter_Process_BadPayload(t *testing.T) {
137+
t.Parallel()
138+
139+
l, err := NewEntryFilter("foo == bar")
140+
require.NoError(t, err)
141+
142+
e := &eventlogger.Event{
143+
Type: eventlogger.EventType(event.AuditType.String()),
144+
CreatedAt: time.Now(),
145+
Formatted: make(map[string][]byte),
146+
Payload: nil,
147+
}
148+
149+
e2, err := l.Process(context.Background(), e)
150+
require.Error(t, err)
151+
require.EqualError(t, err, "audit.(EntryFilter).Process: cannot parse event payload: invalid parameter")
152+
153+
// Ensure that the pipeline won't continue.
154+
require.Nil(t, e2)
155+
}
156+
157+
// TestEntryFilter_Process_NoAuditDataInPayload ensure we stop processing a pipeline
158+
// when the data in the audit event is nil.
159+
func TestEntryFilter_Process_NoAuditDataInPayload(t *testing.T) {
160+
t.Parallel()
161+
162+
l, err := NewEntryFilter("foo == bar")
163+
require.NoError(t, err)
164+
165+
a, err := NewEvent(RequestType)
166+
require.NoError(t, err)
167+
168+
// Ensure audit data is nil
169+
a.Data = nil
170+
171+
e := &eventlogger.Event{
172+
Type: eventlogger.EventType(event.AuditType.String()),
173+
CreatedAt: time.Now(),
174+
Formatted: make(map[string][]byte),
175+
Payload: a,
176+
}
177+
178+
e2, err := l.Process(context.Background(), e)
179+
180+
// Make sure we get the 'nil, nil' response to stop processing this pipeline.
181+
require.NoError(t, err)
182+
require.Nil(t, e2)
183+
}
184+
185+
// TestEntryFilter_Process_FilterSuccess tests that when a filter matches we
186+
// receive no error and the event is not nil so it continues in the pipeline.
187+
func TestEntryFilter_Process_FilterSuccess(t *testing.T) {
188+
t.Parallel()
189+
190+
l, err := NewEntryFilter("mount_type == juan")
191+
require.NoError(t, err)
192+
193+
a, err := NewEvent(RequestType)
194+
require.NoError(t, err)
195+
196+
a.Data = &logical.LogInput{
197+
Request: &logical.Request{
198+
Operation: logical.CreateOperation,
199+
MountType: "juan",
200+
},
201+
}
202+
203+
e := &eventlogger.Event{
204+
Type: eventlogger.EventType(event.AuditType.String()),
205+
CreatedAt: time.Now(),
206+
Formatted: make(map[string][]byte),
207+
Payload: a,
208+
}
209+
210+
ctx := namespace.ContextWithNamespace(context.Background(), namespace.RootNamespace)
211+
212+
e2, err := l.Process(ctx, e)
213+
214+
require.NoError(t, err)
215+
require.NotNil(t, e2)
216+
}
217+
218+
// TestEntryFilter_Process_FilterFail tests that when a filter fails to match we
219+
// receive no error, but also the event is nil so that the pipeline completes.
220+
func TestEntryFilter_Process_FilterFail(t *testing.T) {
221+
t.Parallel()
222+
223+
l, err := NewEntryFilter("mount_type == john and operation == create and namespace == root")
224+
require.NoError(t, err)
225+
226+
a, err := NewEvent(RequestType)
227+
require.NoError(t, err)
228+
229+
a.Data = &logical.LogInput{
230+
Request: &logical.Request{
231+
Operation: logical.CreateOperation,
232+
MountType: "juan",
233+
},
234+
}
235+
236+
e := &eventlogger.Event{
237+
Type: eventlogger.EventType(event.AuditType.String()),
238+
CreatedAt: time.Now(),
239+
Formatted: make(map[string][]byte),
240+
Payload: a,
241+
}
242+
243+
ctx := namespace.ContextWithNamespace(context.Background(), namespace.RootNamespace)
244+
245+
e2, err := l.Process(ctx, e)
246+
247+
require.NoError(t, err)
248+
require.Nil(t, e2)
249+
}

audit/entry_formatter.go

+5-8
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,13 @@ import (
1111
"strings"
1212
"time"
1313

14-
"github.com/jefferai/jsonx"
15-
16-
"github.com/hashicorp/vault/helper/namespace"
17-
"github.com/hashicorp/vault/sdk/logical"
18-
1914
"github.com/go-jose/go-jose/v3/jwt"
15+
"github.com/hashicorp/eventlogger"
16+
"github.com/hashicorp/vault/helper/namespace"
2017
"github.com/hashicorp/vault/internal/observability/event"
2118
"github.com/hashicorp/vault/sdk/helper/jsonutil"
22-
23-
"github.com/hashicorp/eventlogger"
19+
"github.com/hashicorp/vault/sdk/logical"
20+
"github.com/jefferai/jsonx"
2421
)
2522

2623
var (
@@ -29,7 +26,7 @@ var (
2926
)
3027

3128
// NewEntryFormatter should be used to create an EntryFormatter.
32-
// Accepted options: WithPrefix, WithHeaderFormatter.
29+
// Accepted options: WithHeaderFormatter, WithPrefix.
3330
func NewEntryFormatter(config FormatterConfig, salter Salter, opt ...Option) (*EntryFormatter, error) {
3431
const op = "audit.NewEntryFormatter"
3532

0 commit comments

Comments
 (0)