VAULT-17075: syslog sink node (#21859)

* syslog sink added, options + tests added, tweaks to file sink comments

* defaults for syslog options
This commit is contained in:
Peter Wilson
2023-07-14 18:08:25 +01:00
committed by GitHub
parent 8834e4d16b
commit f351fe471a
4 changed files with 193 additions and 10 deletions

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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.