116 lines
2.6 KiB
Go
116 lines
2.6 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
sinkMarshal = json.Marshal
|
|
sinkWrite = func(file *os.File, data []byte) (int, error) { return file.Write(data) }
|
|
)
|
|
|
|
type Sink interface {
|
|
Emit(event *Event) error
|
|
Close() error
|
|
}
|
|
|
|
type NopSink struct{}
|
|
|
|
func (NopSink) Emit(*Event) error { return nil }
|
|
func (NopSink) Close() error { return nil }
|
|
|
|
type FileSink struct {
|
|
writer *DateRotatingWriter
|
|
chain *Chain
|
|
forwarder *HTTPForwarder
|
|
}
|
|
|
|
func NewFileSink(writer *DateRotatingWriter, chain *Chain, forwarder *HTTPForwarder) *FileSink {
|
|
return &FileSink{
|
|
writer: writer,
|
|
chain: chain,
|
|
forwarder: forwarder,
|
|
}
|
|
}
|
|
|
|
func (s *FileSink) Emit(evt *Event) error {
|
|
body, err := marshalWithoutHash(evt)
|
|
if err != nil {
|
|
return fmt.Errorf("audit: marshal event: %w", err)
|
|
}
|
|
|
|
f, release, err := s.writer.beginAppend()
|
|
if err != nil {
|
|
return fmt.Errorf("audit: acquire writer: %w", err)
|
|
}
|
|
|
|
// Derive prev_hash from the file tail, seal, and append — all under the
|
|
// writer's process + inter-process lock so the chain cannot fork.
|
|
prevHash, hash, err := s.chain.SealFromFile(f, body)
|
|
if err != nil {
|
|
release()
|
|
return fmt.Errorf("audit: seal event: %w", err)
|
|
}
|
|
evt.PrevHash = prevHash
|
|
evt.Hash = hash
|
|
|
|
line, err := sinkMarshal(evt)
|
|
if err != nil {
|
|
release()
|
|
return fmt.Errorf("audit: marshal final event: %w", err)
|
|
}
|
|
line = append(line, '\n')
|
|
if _, err := sinkWrite(f, line); err != nil {
|
|
release()
|
|
return fmt.Errorf("audit: write event: %w", err)
|
|
}
|
|
release()
|
|
|
|
if s.forwarder != nil {
|
|
s.forwarder.Forward(*evt)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close flushes in-flight remote forwards (bounded) before closing the writer,
|
|
// so events are not silently dropped when the CLI process exits right after
|
|
// emitting.
|
|
func (s *FileSink) Close() error {
|
|
var forwardErr error
|
|
if s.forwarder != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
forwardErr = s.forwarder.Close(ctx)
|
|
cancel()
|
|
}
|
|
if err := s.writer.Close(); err != nil {
|
|
return err
|
|
}
|
|
return forwardErr
|
|
}
|
|
|
|
func marshalWithoutHash(evt *Event) ([]byte, error) {
|
|
saved := Event{
|
|
Timestamp: evt.Timestamp,
|
|
ExecutionID: evt.ExecutionID,
|
|
AgentID: evt.AgentID,
|
|
Actor: evt.Actor,
|
|
Product: evt.Product,
|
|
Command: evt.Command,
|
|
Endpoint: evt.Endpoint,
|
|
ParamsSummary: evt.ParamsSummary,
|
|
Result: evt.Result,
|
|
ErrCategory: evt.ErrCategory,
|
|
ErrReason: evt.ErrReason,
|
|
DurationMs: evt.DurationMs,
|
|
CLIVersion: evt.CLIVersion,
|
|
OS: evt.OS,
|
|
Arch: evt.Arch,
|
|
}
|
|
return sinkMarshal(saved)
|
|
}
|