Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 22 additions & 28 deletions audit/entry_formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions audit/entry_formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions audit/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
36 changes: 36 additions & 0 deletions audit/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
}
}
136 changes: 68 additions & 68 deletions audit/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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"`
}

Expand All @@ -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 {
Expand Down