From f351fe471aa251698d5ab0c5c1875da148222750 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Fri, 14 Jul 2023 18:08:25 +0100 Subject: [PATCH] VAULT-17075: syslog sink node (#21859) * syslog sink added, options + tests added, tweaks to file sink comments * defaults for syslog options --- .../observability/event/audit_sink_file.go | 11 +-- .../observability/event/audit_sink_syslog.go | 75 ++++++++++++++++++ internal/observability/event/options.go | 39 ++++++++-- internal/observability/event/options_test.go | 78 +++++++++++++++++++ 4 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 internal/observability/event/audit_sink_syslog.go diff --git a/internal/observability/event/audit_sink_file.go b/internal/observability/event/audit_sink_file.go index 250e476d8d..decffce3bc 100644 --- a/internal/observability/event/audit_sink_file.go +++ b/internal/observability/event/audit_sink_file.go @@ -34,6 +34,7 @@ type AuditFileSink struct { } // NewAuditFileSink should be used to create a new AuditFileSink. +// Accepted options: WithFileMode and WithPrefix. func NewAuditFileSink(path string, format auditFormat, opt ...Option) (*AuditFileSink, error) { const op = "event.NewAuditFileSink" @@ -110,12 +111,12 @@ func (f *AuditFileSink) Process(ctx context.Context, e *eventlogger.Event) (*eve return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, f.format) } - buffer := bytes.NewBuffer(formatted) - err := f.log(buffer) + err := f.log(formatted) if err != nil { return nil, fmt.Errorf("%s: error writing file for audit sink: %w", op, err) } + // return nil for the event to indicate the pipeline is complete. return nil, nil } @@ -145,7 +146,7 @@ func (f *AuditFileSink) Reopen() error { return f.open() } -// Type is used to define which type of node AuditFileSink is. +// Type describes the type of this node (sink). func (f *AuditFileSink) Type() eventlogger.NodeType { return eventlogger.NodeTypeSink } @@ -189,13 +190,13 @@ func (f *AuditFileSink) open() error { // log writes the buffer to the file. // It acquires a lock on the file to do this. -func (f *AuditFileSink) log(buf *bytes.Buffer) error { +func (f *AuditFileSink) log(data []byte) error { const op = "event.(AuditFileSink).log" f.fileLock.Lock() defer f.fileLock.Unlock() - reader := bytes.NewReader(buf.Bytes()) + reader := bytes.NewReader(data) var writer io.Writer switch { diff --git a/internal/observability/event/audit_sink_syslog.go b/internal/observability/event/audit_sink_syslog.go new file mode 100644 index 0000000000..61fbfb0873 --- /dev/null +++ b/internal/observability/event/audit_sink_syslog.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package event + +import ( + "context" + "fmt" + + gsyslog "github.com/hashicorp/go-syslog" + + "github.com/hashicorp/eventlogger" +) + +// AuditSyslogSink is a sink node which handles writing audit events to syslog. +type AuditSyslogSink struct { + format auditFormat + logger gsyslog.Syslogger +} + +// NewAuditSyslogSink should be used to create a new AuditSyslogSink. +// Accepted options: WithFacility and WithTag. +func NewAuditSyslogSink(format auditFormat, opt ...Option) (*AuditSyslogSink, error) { + const op = "event.NewAuditSyslogSink" + + opts, err := getOpts(opt...) + if err != nil { + return nil, fmt.Errorf("%s: error applying options: %w", op, err) + } + + logger, err := gsyslog.NewLogger(gsyslog.LOG_INFO, opts.withFacility, opts.withTag) + if err != nil { + return nil, fmt.Errorf("%s: error creating syslogger: %w", op, err) + } + + return &AuditSyslogSink{format: format, logger: logger}, nil +} + +// Process handles writing the event to the syslog. +func (s *AuditSyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { + const op = "event.(AuditSyslogSink).Process" + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + if e == nil { + return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter) + } + + formatted, found := e.Format(s.format.String()) + if !found { + return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.format) + } + + _, err := s.logger.Write(formatted) + if err != nil { + return nil, fmt.Errorf("%s: error writing to syslog: %w", op, err) + } + + // return nil for the event to indicate the pipeline is complete. + return nil, nil +} + +// Reopen is a no-op for a syslog sink. +func (s *AuditSyslogSink) Reopen() error { + return nil +} + +// Type describes the type of this node (sink). +func (s *AuditSyslogSink) Type() eventlogger.NodeType { + return eventlogger.NodeTypeSink +} diff --git a/internal/observability/event/options.go b/internal/observability/event/options.go index 67b3cf4cf8..f43f55af9e 100644 --- a/internal/observability/event/options.go +++ b/internal/observability/event/options.go @@ -25,12 +25,16 @@ type options struct { withFormat auditFormat withFileMode *os.FileMode withPrefix string + withFacility string + withTag string } // getDefaultOptions returns options with their default values. func getDefaultOptions() options { return options{ - withNow: time.Now(), + withNow: time.Now(), + withFacility: "AUTH", + withTag: "vault", } } @@ -143,12 +147,11 @@ func WithFormat(format string) Option { // applied, but it will not return an error in those circumstances. func WithFileMode(mode string) Option { return func(o *options) error { - // Clear up whitespace before attempting to parse + // If supplied file mode is empty, just return early without setting anything. + // We can assume that this option was called by something that didn't + // parse the incoming value, perhaps from a config map etc. mode = strings.TrimSpace(mode) if mode == "" { - // If supplied file mode is empty, just return early without setting anything. - // We can assume that this option was called by something that didn't - // parse the incoming value, perhaps from a config map etc. return nil } @@ -175,3 +178,29 @@ func WithPrefix(prefix string) Option { return nil } } + +// WithFacility provides an option to represent a 'facility' for a syslog sink. +func WithFacility(facility string) Option { + return func(o *options) error { + facility = strings.TrimSpace(facility) + + if facility != "" { + o.withFacility = facility + } + + return nil + } +} + +// WithTag provides an option to represent a 'tag' for a syslog sink. +func WithTag(tag string) Option { + return func(o *options) error { + tag = strings.TrimSpace(tag) + + if tag != "" { + o.withTag = tag + } + + return nil + } +} diff --git a/internal/observability/event/options_test.go b/internal/observability/event/options_test.go index c8026da87f..c0181e3a8d 100644 --- a/internal/observability/event/options_test.go +++ b/internal/observability/event/options_test.go @@ -197,6 +197,82 @@ func TestOptions_WithID(t *testing.T) { } } +// TestOptions_WithFacility exercises WithFacility option to ensure it performs as expected. +func TestOptions_WithFacility(t *testing.T) { + tests := map[string]struct { + Value string + ExpectedValue string + }{ + "empty": { + Value: "", + ExpectedValue: "", + }, + "whitespace": { + Value: " ", + ExpectedValue: "", + }, + "value": { + Value: "juan", + ExpectedValue: "juan", + }, + "spacey-value": { + Value: " juan ", + ExpectedValue: "juan", + }, + } + + for name, tc := range tests { + name := name + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + options := &options{} + applyOption := WithFacility(tc.Value) + err := applyOption(options) + require.NoError(t, err) + require.Equal(t, tc.ExpectedValue, options.withFacility) + }) + } +} + +// TestOptions_WithTag exercises WithTag option to ensure it performs as expected. +func TestOptions_WithTag(t *testing.T) { + tests := map[string]struct { + Value string + ExpectedValue string + }{ + "empty": { + Value: "", + ExpectedValue: "", + }, + "whitespace": { + Value: " ", + ExpectedValue: "", + }, + "value": { + Value: "juan", + ExpectedValue: "juan", + }, + "spacey-value": { + Value: " juan ", + ExpectedValue: "juan", + }, + } + + for name, tc := range tests { + name := name + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + options := &options{} + applyOption := WithTag(tc.Value) + err := applyOption(options) + require.NoError(t, err) + require.Equal(t, tc.ExpectedValue, options.withTag) + }) + } +} + // TestOptions_WithFileMode exercises WithFileMode option to ensure it performs as expected. func TestOptions_WithFileMode(t *testing.T) { tests := map[string]struct { @@ -266,6 +342,8 @@ func TestOptions_Default(t *testing.T) { require.NotNil(t, opts) require.True(t, time.Now().After(opts.withNow)) require.False(t, opts.withNow.IsZero()) + require.Equal(t, "AUTH", opts.withFacility) + require.Equal(t, "vault", opts.withTag) } // TestOptions_Opts exercises getOpts with various Option values.