// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package event import ( "fmt" "time" "github.com/hashicorp/eventlogger" ) // auditVersion defines the version of audit events const auditVersion = "v0.1" // auditEventType defines the type of audit event type auditEventType string const ( ApiRequest auditEventType = "APIRequest" // ApiRequest defines an API request audit event type ) // audit defines the data of audit events type audit struct { Id string `json:"id"` // std audit/boundary field Version string `json:"version"` // std audit/boundary field Type string `json:"type"` // std audit field Timestamp time.Time `json:"timestamp"` // std audit field RequestInfo *RequestInfo `json:"request_info,omitempty"` // boundary field Auth *Auth `json:"auth,omitempty"` // std audit field Request *Request `json:"request,omitempty"` // std audit field Response *Response `json:"response,omitempty"` // std audit field Flush bool `json:"-"` CorrelationId string `json:"correlation_id,omitempty"` } func newAudit(fromOperation Op, opt ...Option) (*audit, error) { const op = "event.newAudit" if fromOperation == "" { return nil, fmt.Errorf("%s: missing from operation: %w", op, ErrInvalidParameter) } opts := getOpts(opt...) if opts.withId == "" { var err error opts.withId, err = NewId(string(AuditType)) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } } var dtm time.Time switch opts.withNow.IsZero() { case false: dtm = opts.withNow default: dtm = time.Now() } a := &audit{ Id: opts.withId, Version: auditVersion, Type: string(ApiRequest), Timestamp: dtm, RequestInfo: opts.withRequestInfo, Auth: opts.withAuth, Request: opts.withRequest, Response: opts.withResponse, Flush: opts.withFlush, CorrelationId: opts.withCorrelationId, } if err := a.validate(); err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return a, nil } // EventType is required for all event types by the eventlogger broker func (a *audit) EventType() string { return string(AuditType) } func (a *audit) validate() error { const op = "event.(audit).validate" if a.Id == "" { return fmt.Errorf("%s: missing id: %w", op, ErrInvalidParameter) } return nil } // GetID is part of the eventlogger.Gateable interface and returns the audit // event's id. func (a *audit) GetID() string { return a.Id } // FlushEvent is part of the eventlogger.Gateable interface and returns the // value of the audit event's flush field func (a *audit) FlushEvent() bool { return a.Flush } // ComposedFrom is part of the eventlogger.Gatable interface. It's important to // remember that the receiver will always be nil when this is called by the eventlogger.GatedFilter func (a *audit) ComposeFrom(events []*eventlogger.Event) (eventlogger.EventType, any, error) { const op = "event.(audit).ComposedFrom" if len(events) == 0 { return "", nil, fmt.Errorf("%s: missing events: %w", op, ErrInvalidParameter) } var validId string payload := audit{} for i, v := range events { gated, ok := v.Payload.(*audit) if !ok { return "", nil, fmt.Errorf("%s: event %d is not an audit payload: %w", op, i, ErrInvalidParameter) } if gated.Id == "" { // can't really happen since it has to have an id to be gated, but // I'll add this check in the name of completeness return "", nil, fmt.Errorf("%s: event %d: id is required: %w", op, i, ErrInvalidParameter) } if validId == "" { validId = gated.Id } if gated.Id != validId { return "", nil, fmt.Errorf("%s: event %d has an invalid id: %s != %s: %w", op, i, gated.Id, validId, ErrInvalidParameter) } if gated.Version != auditVersion { return "", nil, fmt.Errorf("%s: event %d has an invalid version: %s != %s: %w", op, i, gated.Version, auditVersion, ErrInvalidParameter) } if gated.Type != string(ApiRequest) { return "", nil, fmt.Errorf("%s: event %d has an invalid type: %s != %s: %w", op, i, gated.Type, string(AuditType), ErrInvalidParameter) } if gated.RequestInfo != nil { payload.RequestInfo = gated.RequestInfo } if gated.Auth != nil { payload.Auth = gated.Auth } if gated.Request != nil { if payload.Request == nil { payload.Request = &Request{} } if gated.Request.Endpoint != "" { payload.Request.Endpoint = gated.Request.Endpoint } if gated.Request.Operation != "" { payload.Request.Operation = gated.Request.Operation } if gated.Request.Details != nil { payload.Request.Details = gated.Request.Details } if gated.Request.DetailsUpstreamMessage != nil { payload.Request.DetailsUpstreamMessage = gated.Request.DetailsUpstreamMessage } } if gated.Response != nil { if payload.Response == nil { payload.Response = &Response{} } if gated.Response.StatusCode != 0 { payload.Response.StatusCode = gated.Response.StatusCode } if gated.Response.Details != nil { payload.Response.Details = gated.Response.Details } if gated.Response.DetailsUpstreamMessage != nil { payload.Response.DetailsUpstreamMessage = gated.Response.DetailsUpstreamMessage } } if !gated.Timestamp.IsZero() { payload.Timestamp = gated.Timestamp } if gated.CorrelationId != "" { payload.CorrelationId = gated.CorrelationId } } payload.Id = validId payload.Version = auditVersion payload.Type = string(ApiRequest) return eventlogger.EventType(a.EventType()), payload, nil }