From 780245334faa0ec3b4c9131eaef93806880faae7 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 12 Mar 2024 08:53:09 +0000 Subject: [PATCH] refactored audit package --- audit/entry_formatter.go | 50 ++++++------- audit/entry_formatter_test.go | 8 +- audit/event.go | 12 +++ audit/event_test.go | 36 +++++++++ audit/types.go | 136 +++++++++++++++++----------------- 5 files changed, 142 insertions(+), 100 deletions(-) diff --git a/audit/entry_formatter.go b/audit/entry_formatter.go index 96ec7762baf..e4838d6ccd0 100644 --- a/audit/entry_formatter.go +++ b/audit/entry_formatter.go @@ -93,12 +93,16 @@ func (*EntryFormatter) Type() eventlogger.NodeType { func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ *eventlogger.Event, retErr error) { const op = "audit.(EntryFormatter).Process" + // Return early if the context was cancelled, eventlogger will not carry on + // asking nodes to process, so any sink node in the pipeline won't be called. select { case <-ctx.Done(): return nil, ctx.Err() default: } + // Perform validation on the event, then retrieve the underlying AuditEvent + // and LogInput (from the AuditEvent Data). if e == nil { return nil, fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter) } @@ -135,18 +139,14 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * return nil, fmt.Errorf("%s: unable to copy audit event data: %w", op, err) } - var headers map[string][]string - if data.Request != nil && data.Request.Headers != nil { - headers = data.Request.Headers - } - - if f.headerFormatter != nil { - adjustedHeaders, err := f.headerFormatter.ApplyConfig(ctx, headers, f.salter) + // Ensure that any headers in the request, are formatted as required, and are + // only present if they have been configured to appear in the audit log. + // e.g. via: /sys/config/auditing/request-headers/:name + if f.headerFormatter != nil && data.Request != nil && data.Request.Headers != nil { + data.Request.Headers, err = f.headerFormatter.ApplyConfig(ctx, data.Request.Headers, f.salter) if err != nil { return nil, fmt.Errorf("%s: unable to transform headers for auditing: %w", op, err) } - - data.Request.Headers = adjustedHeaders } // If the request contains a Server-Side Consistency Token (SSCT), and we @@ -156,32 +156,26 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * data.Auth.ClientToken = data.Request.InboundSSCToken } - var result []byte + // Using 'any' as we have two different types that we can get back from either + // FormatRequest or FormatResponse, but the JSON encoder doesn't care about types. + var entry any switch a.Subtype { case RequestType: - entry, err := f.FormatRequest(ctx, data) - if err != nil { - return nil, fmt.Errorf("%s: unable to parse request from audit event: %w", op, err) - } - - result, err = jsonutil.EncodeJSON(entry) - if err != nil { - return nil, fmt.Errorf("%s: unable to format request: %w", op, err) - } + entry, err = f.FormatRequest(ctx, data) case ResponseType: - entry, err := f.FormatResponse(ctx, data) - if err != nil { - return nil, fmt.Errorf("%s: unable to parse response from audit event: %w", op, err) - } - - result, err = jsonutil.EncodeJSON(entry) - if err != nil { - return nil, fmt.Errorf("%s: unable to format response: %w", op, err) - } + entry, err = f.FormatResponse(ctx, data) default: return nil, fmt.Errorf("%s: unknown audit event subtype: %q", op, a.Subtype) } + if err != nil { + return nil, fmt.Errorf("%s: unable to parse %s from audit event: %w", op, a.Subtype.String(), err) + } + + result, err := jsonutil.EncodeJSON(entry) + if err != nil { + return nil, fmt.Errorf("%s: unable to format %s: %w", op, a.Subtype.String(), err) + } if f.config.RequiredFormat == JSONxFormat { var err error diff --git a/audit/entry_formatter_test.go b/audit/entry_formatter_test.go index 9b5dc1ccacb..4dacdf5220d 100644 --- a/audit/entry_formatter_test.go +++ b/audit/entry_formatter_test.go @@ -231,14 +231,14 @@ func TestEntryFormatter_Process(t *testing.T) { }{ "json-request-no-data": { IsErrorExpected: true, - ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditRequest) with no data: invalid parameter", + ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (request) with no data: invalid parameter", Subtype: RequestType, RequiredFormat: JSONFormat, Data: nil, }, "json-response-no-data": { IsErrorExpected: true, - ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditResponse) with no data: invalid parameter", + ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (response) with no data: invalid parameter", Subtype: ResponseType, RequiredFormat: JSONFormat, Data: nil, @@ -287,14 +287,14 @@ func TestEntryFormatter_Process(t *testing.T) { }, "jsonx-request-no-data": { IsErrorExpected: true, - ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditRequest) with no data: invalid parameter", + ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (request) with no data: invalid parameter", Subtype: RequestType, RequiredFormat: JSONxFormat, Data: nil, }, "jsonx-response-no-data": { IsErrorExpected: true, - ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditResponse) with no data: invalid parameter", + ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (response) with no data: invalid parameter", Subtype: ResponseType, RequiredFormat: JSONxFormat, Data: nil, diff --git a/audit/event.go b/audit/event.go index 8802ea4f5cd..437297b85fb 100644 --- a/audit/event.go +++ b/audit/event.go @@ -140,5 +140,17 @@ func (t subtype) MetricTag() string { return "log_response" } + return t.String() +} + +// String returns the subtype as a human-readable string. +func (t subtype) String() string { + switch t { + case RequestType: + return "request" + case ResponseType: + return "response" + } + return string(t) } diff --git a/audit/event_test.go b/audit/event_test.go index 66bdf424c7f..8c0d9ad5194 100644 --- a/audit/event_test.go +++ b/audit/event_test.go @@ -332,3 +332,39 @@ func TestAuditEvent_Subtype_MetricTag(t *testing.T) { }) } } + +// TestAuditEvent_Subtype_String is used to ensure that we get the string value +// we expect for a subtype when it is used with the Stringer interface. +// e.g. an AuditRequest subtype is 'request' +func TestAuditEvent_Subtype_String(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + input string + expectedOutput string + }{ + "request": { + input: "AuditRequest", + expectedOutput: "request", + }, + "response": { + input: "AuditResponse", + expectedOutput: "response", + }, + "non-validated": { + input: "juan", + expectedOutput: "juan", + }, + } + + for name, tc := range tests { + name := name + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + st := subtype(tc.input) + require.Equal(t, tc.expectedOutput, st.String()) + }) + } +} diff --git a/audit/types.go b/audit/types.go index 5c64fe38618..f90d765bd95 100644 --- a/audit/types.go +++ b/audit/types.go @@ -12,6 +12,36 @@ import ( "github.com/hashicorp/vault/sdk/logical" ) +// Backend interface must be implemented for an audit +// mechanism to be made available. Audit backends can be enabled to +// sink information to different backends such as logs, file, databases, +// or other external services. +type Backend interface { + // Salter interface must be implemented by anything implementing Backend. + Salter + + // The PipelineReader interface allows backends to surface information about their + // nodes for node and pipeline registration. + event.PipelineReader + + // IsFallback can be used to determine if this audit backend device is intended to + // be used as a fallback to catch all events that are not written when only using + // filtered pipelines. + IsFallback() bool + + // LogTestMessage is used to check an audit backend before adding it + // permanently. It should attempt to synchronously log the given test + // message, WITHOUT using the normal Salt (which would require a storage + // operation on creation, which is currently disallowed.) + LogTestMessage(context.Context, *logical.LogInput) error + + // Reload is called on SIGHUP for supporting backends. + Reload(context.Context) error + + // Invalidate is called for path invalidation + Invalidate(context.Context) +} + // Salter is an interface that provides a way to obtain a Salt for hashing. type Salter interface { // Salt returns a non-nil salt or an error. @@ -73,86 +103,86 @@ type FormatterConfig struct { // RequestEntry is the structure of a request audit log entry. type RequestEntry struct { - Time string `json:"time,omitempty"` - Type string `json:"type,omitempty"` Auth *Auth `json:"auth,omitempty"` - Request *Request `json:"request,omitempty"` Error string `json:"error,omitempty"` ForwardedFrom string `json:"forwarded_from,omitempty"` // Populated in Enterprise when a request is forwarded + Request *Request `json:"request,omitempty"` + Time string `json:"time,omitempty"` + Type string `json:"type,omitempty"` } // ResponseEntry is the structure of a response audit log entry. type ResponseEntry struct { + Auth *Auth `json:"auth,omitempty"` + Error string `json:"error,omitempty"` + Forwarded bool `json:"forwarded,omitempty"` Time string `json:"time,omitempty"` Type string `json:"type,omitempty"` - Auth *Auth `json:"auth,omitempty"` Request *Request `json:"request,omitempty"` Response *Response `json:"response,omitempty"` - Error string `json:"error,omitempty"` - Forwarded bool `json:"forwarded,omitempty"` } type Request struct { - ID string `json:"id,omitempty"` + ClientCertificateSerialNumber string `json:"client_certificate_serial_number,omitempty"` ClientID string `json:"client_id,omitempty"` - ReplicationCluster string `json:"replication_cluster,omitempty"` - Operation logical.Operation `json:"operation,omitempty"` + ClientToken string `json:"client_token,omitempty"` + ClientTokenAccessor string `json:"client_token_accessor,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + ID string `json:"id,omitempty"` + Headers map[string][]string `json:"headers,omitempty"` + MountAccessor string `json:"mount_accessor,omitempty"` + MountClass string `json:"mount_class,omitempty"` MountPoint string `json:"mount_point,omitempty"` MountType string `json:"mount_type,omitempty"` - MountAccessor string `json:"mount_accessor,omitempty"` MountRunningVersion string `json:"mount_running_version,omitempty"` MountRunningSha256 string `json:"mount_running_sha256,omitempty"` - MountClass string `json:"mount_class,omitempty"` MountIsExternalPlugin bool `json:"mount_is_external_plugin,omitempty"` - ClientToken string `json:"client_token,omitempty"` - ClientTokenAccessor string `json:"client_token_accessor,omitempty"` Namespace *Namespace `json:"namespace,omitempty"` + Operation logical.Operation `json:"operation,omitempty"` Path string `json:"path,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` PolicyOverride bool `json:"policy_override,omitempty"` RemoteAddr string `json:"remote_address,omitempty"` RemotePort int `json:"remote_port,omitempty"` - WrapTTL int `json:"wrap_ttl,omitempty"` - Headers map[string][]string `json:"headers,omitempty"` - ClientCertificateSerialNumber string `json:"client_certificate_serial_number,omitempty"` + ReplicationCluster string `json:"replication_cluster,omitempty"` RequestURI string `json:"request_uri,omitempty"` + WrapTTL int `json:"wrap_ttl,omitempty"` } type Response struct { Auth *Auth `json:"auth,omitempty"` - MountPoint string `json:"mount_point,omitempty"` - MountType string `json:"mount_type,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + Headers map[string][]string `json:"headers,omitempty"` MountAccessor string `json:"mount_accessor,omitempty"` - MountRunningVersion string `json:"mount_running_plugin_version,omitempty"` - MountRunningSha256 string `json:"mount_running_sha256,omitempty"` MountClass string `json:"mount_class,omitempty"` MountIsExternalPlugin bool `json:"mount_is_external_plugin,omitempty"` - Secret *Secret `json:"secret,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` - Warnings []string `json:"warnings,omitempty"` + MountPoint string `json:"mount_point,omitempty"` + MountRunningSha256 string `json:"mount_running_sha256,omitempty"` + MountRunningVersion string `json:"mount_running_plugin_version,omitempty"` + MountType string `json:"mount_type,omitempty"` Redirect string `json:"redirect,omitempty"` + Secret *Secret `json:"secret,omitempty"` WrapInfo *ResponseWrapInfo `json:"wrap_info,omitempty"` - Headers map[string][]string `json:"headers,omitempty"` + Warnings []string `json:"warnings,omitempty"` } type Auth struct { - ClientToken string `json:"client_token,omitempty"` Accessor string `json:"accessor,omitempty"` + ClientToken string `json:"client_token,omitempty"` DisplayName string `json:"display_name,omitempty"` - Policies []string `json:"policies,omitempty"` - TokenPolicies []string `json:"token_policies,omitempty"` - IdentityPolicies []string `json:"identity_policies,omitempty"` + EntityCreated bool `json:"entity_created,omitempty"` + EntityID string `json:"entity_id,omitempty"` ExternalNamespacePolicies map[string][]string `json:"external_namespace_policies,omitempty"` - NoDefaultPolicy bool `json:"no_default_policy,omitempty"` - PolicyResults *PolicyResults `json:"policy_results,omitempty"` + IdentityPolicies []string `json:"identity_policies,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` + NoDefaultPolicy bool `json:"no_default_policy,omitempty"` NumUses int `json:"num_uses,omitempty"` + Policies []string `json:"policies,omitempty"` + PolicyResults *PolicyResults `json:"policy_results,omitempty"` RemainingUses int `json:"remaining_uses,omitempty"` - EntityID string `json:"entity_id,omitempty"` - EntityCreated bool `json:"entity_created,omitempty"` - TokenType string `json:"token_type,omitempty"` - TokenTTL int64 `json:"token_ttl,omitempty"` + TokenPolicies []string `json:"token_policies,omitempty"` TokenIssueTime string `json:"token_issue_time,omitempty"` + TokenTTL int64 `json:"token_ttl,omitempty"` + TokenType string `json:"token_type,omitempty"` } type PolicyResults struct { @@ -172,11 +202,11 @@ type Secret struct { } type ResponseWrapInfo struct { - TTL int `json:"ttl,omitempty"` - Token string `json:"token,omitempty"` Accessor string `json:"accessor,omitempty"` - CreationTime string `json:"creation_time,omitempty"` CreationPath string `json:"creation_path,omitempty"` + CreationTime string `json:"creation_time,omitempty"` + Token string `json:"token,omitempty"` + TTL int `json:"ttl,omitempty"` WrappedAccessor string `json:"wrapped_accessor,omitempty"` } @@ -188,36 +218,6 @@ type Namespace struct { // nonPersistentSalt is used for obtaining a salt that is not persisted. type nonPersistentSalt struct{} -// Backend interface must be implemented for an audit -// mechanism to be made available. Audit backends can be enabled to -// sink information to different backends such as logs, file, databases, -// or other external services. -type Backend interface { - // Salter interface must be implemented by anything implementing Backend. - Salter - - // The PipelineReader interface allows backends to surface information about their - // nodes for node and pipeline registration. - event.PipelineReader - - // IsFallback can be used to determine if this audit backend device is intended to - // be used as a fallback to catch all events that are not written when only using - // filtered pipelines. - IsFallback() bool - - // LogTestMessage is used to check an audit backend before adding it - // permanently. It should attempt to synchronously log the given test - // message, WITHOUT using the normal Salt (which would require a storage - // operation on creation, which is currently disallowed.) - LogTestMessage(context.Context, *logical.LogInput) error - - // Reload is called on SIGHUP for supporting backends. - Reload(context.Context) error - - // Invalidate is called for path invalidation - Invalidate(context.Context) -} - // BackendConfig contains configuration parameters used in the factory func to // instantiate audit backends type BackendConfig struct {