Skip to content

Commit

Permalink
feature: Audit log CLI (#194)
Browse files Browse the repository at this point in the history
* feature: Add CLI for viewing audit logs

* WIP

* Decisions TUI works

* Add documentation
  • Loading branch information
charithe committed Jul 12, 2021
1 parent be23ceb commit 1a7d2eb
Show file tree
Hide file tree
Showing 23 changed files with 1,691 additions and 126 deletions.
14 changes: 14 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,28 @@ Third-party dependencies used by the Cerbos project:
Package URL Licence
contrib.go.opencensus.io/exporter/jaeger Unknown Apache-2.0
contrib.go.opencensus.io/exporter/prometheus Unknown Apache-2.0
github.com/alecthomas/chroma https://github.com/alecthomas/chroma/blob/master/COPYING MIT
github.com/antlr/antlr4/runtime/Go/antlr https://github.com/antlr/antlr4/blob/master/runtime/Go/antlr/LICENSE.txt MIT
github.com/beorn7/perks/quantile https://github.com/beorn7/perks/blob/master/quantile/LICENSE MIT
github.com/bluele/gcache https://github.com/bluele/gcache/blob/master/LICENSE MIT
github.com/cespare/xxhash https://github.com/cespare/xxhash/blob/master/LICENSE.txt MIT
github.com/cespare/xxhash/v2 https://github.com/cespare/xxhash/blob/master/v2/LICENSE.txt MIT
github.com/danwakefield/fnmatch https://github.com/danwakefield/fnmatch/blob/master/LICENSE BSD-2-Clause
github.com/DataDog/zstd https://github.com/DataDog/zstd/blob/master/LICENSE BSD-3-Clause
github.com/davecgh/go-spew/spew https://github.com/davecgh/go-spew/blob/master/spew/LICENSE ISC
github.com/dgraph-io/badger/v3 https://github.com/dgraph-io/badger/blob/master/v3/LICENSE Apache-2.0
github.com/dgraph-io/ristretto https://github.com/dgraph-io/ristretto/blob/master/LICENSE Apache-2.0
github.com/dgraph-io/ristretto/z https://github.com/dgraph-io/ristretto/blob/master/z/LICENSE MIT
github.com/dlclark/regexp2 https://github.com/dlclark/regexp2/blob/master/LICENSE MIT
github.com/doug-martin/goqu/v9 https://github.com/doug-martin/goqu/blob/master/v9/LICENSE MIT
github.com/dustin/go-humanize https://github.com/dustin/go-humanize/blob/master/LICENSE MIT
github.com/emirpasic/gods https://github.com/emirpasic/gods/blob/master/LICENSE BSD-2-Clause
github.com/envoyproxy/protoc-gen-validate/validate https://github.com/envoyproxy/protoc-gen-validate/blob/master/validate/LICENSE Apache-2.0
github.com/fatih/color https://github.com/fatih/color/blob/master/LICENSE.md MIT
github.com/felixge/httpsnoop https://github.com/felixge/httpsnoop/blob/master/LICENSE.txt MIT
github.com/fsnotify/fsnotify https://github.com/fsnotify/fsnotify/blob/master/LICENSE BSD-3-Clause
github.com/gdamore/encoding https://github.com/gdamore/encoding/blob/master/LICENSE Apache-2.0
github.com/gdamore/tcell/v2 https://github.com/gdamore/tcell/blob/master/v2/LICENSE Apache-2.0
github.com/ghodss/yaml https://github.com/ghodss/yaml/blob/master/LICENSE MIT
github.com/gobwas/glob https://github.com/gobwas/glob/blob/master/LICENSE MIT
github.com/go-git/gcfg https://github.com/go-git/gcfg/blob/master/LICENSE BSD-3-Clause
Expand All @@ -48,10 +53,16 @@ github.com/grpc-ecosystem/grpc-gateway/v2 https://github
github.com/imdario/mergo https://github.com/imdario/mergo/blob/master/LICENSE BSD-3-Clause
github.com/jbenet/go-context/io https://github.com/jbenet/go-context/blob/master/io/LICENSE MIT
github.com/jmoiron/sqlx https://github.com/jmoiron/sqlx/blob/master/LICENSE MIT
github.com/jwalton/gchalk https://github.com/jwalton/gchalk/blob/master/LICENSE MIT
github.com/jwalton/gchalk/pkg/ansistyles https://github.com/jwalton/gchalk/blob/master/pkg/ansistyles/LICENSE MIT
github.com/jwalton/go-supportscolor https://github.com/jwalton/go-supportscolor/blob/master/LICENSE MIT
github.com/jwalton/go-supportscolor/pkg/hasFlag https://github.com/jwalton/go-supportscolor/blob/master/pkg/hasFlag/LICENSE MIT
github.com/kavu/go_reuseport https://github.com/kavu/go_reuseport/blob/master/LICENSE MIT
github.com/kevinburke/ssh_config https://github.com/kevinburke/ssh_config/blob/master/LICENSE MIT
github.com/lucasb-eyer/go-colorful https://github.com/lucasb-eyer/go-colorful/blob/master/LICENSE MIT
github.com/mattn/go-colorable https://github.com/mattn/go-colorable/blob/master/LICENSE MIT
github.com/mattn/go-isatty https://github.com/mattn/go-isatty/blob/master/LICENSE MIT
github.com/mattn/go-runewidth https://github.com/mattn/go-runewidth/blob/master/LICENSE MIT
github.com/matttproud/golang_protobuf_extensions/pbutil https://github.com/matttproud/golang_protobuf_extensions/blob/master/pbutil/LICENSE Apache-2.0
github.com/mitchellh/go-homedir https://github.com/mitchellh/go-homedir/blob/master/LICENSE MIT
github.com/oklog/ulid/v2 https://github.com/oklog/ulid/blob/master/v2/LICENSE Apache-2.0
Expand All @@ -71,6 +82,8 @@ github.com/prometheus/statsd_exporter/pkg/mapper https://github
github.com/ProtonMail/go-crypto https://github.com/ProtonMail/go-crypto/blob/master/LICENSE BSD-3-Clause
github.com/rcrowley/go-metrics https://github.com/rcrowley/go-metrics/blob/master/LICENSE BSD-2-Clause-FreeBSD
github.com/remyoudompheng/bigfft https://github.com/remyoudompheng/bigfft/blob/master/LICENSE BSD-3-Clause
github.com/rivo/tview https://github.com/rivo/tview/blob/master/LICENSE.txt MIT
github.com/rivo/uniseg https://github.com/rivo/uniseg/blob/master/LICENSE.txt MIT
github.com/rjeczalik/notify https://github.com/rjeczalik/notify/blob/master/LICENSE MIT
github.com/sergi/go-diff/diffmatchpatch https://github.com/sergi/go-diff/blob/master/diffmatchpatch/LICENSE MIT
github.com/spf13/afero https://github.com/spf13/afero/blob/master/LICENSE.txt Apache-2.0
Expand All @@ -92,6 +105,7 @@ golang.org/x/crypto Unknown
golang.org/x/net Unknown BSD-3-Clause
golang.org/x/sync Unknown BSD-3-Clause
golang.org/x/sys Unknown BSD-3-Clause
golang.org/x/term Unknown BSD-3-Clause
golang.org/x/text Unknown BSD-3-Clause
google.golang.org/api/support/bundler Unknown BSD-3-Clause
google.golang.org/genproto Unknown Apache-2.0
Expand Down
247 changes: 247 additions & 0 deletions cmd/ctl/audit/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// Copyright 2021 Zenauth Ltd.

package audit

import (
"bufio"
"context"
"errors"
"fmt"
"io"
"strings"

"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/jwalton/gchalk"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"

requestv1 "github.com/cerbos/cerbos/internal/genpb/request/v1"
responsev1 "github.com/cerbos/cerbos/internal/genpb/response/v1"
svcv1 "github.com/cerbos/cerbos/internal/genpb/svc/v1"
)

const dashLen = 54

var (
auditFlags struct {
kind kindFlag
raw bool
}

auditFilterFlags = NewFilterDef()
newline = []byte("\n")
)

var longDesc = `View audit logs.
Requires audit logging to be enabled on the server. Supports several ways of filtering the data.
tail: View the last N records
between: View records captured between two timestamps. The timestamps must be formatted as ISO-8601
since: View records from X hours/minutes/seconds ago to now. Unit suffixes are: h=hours, m=minutes s=seconds
lookup: View a specific record using the Cerbos Call ID`

var exampleDesc = `
# View the last 10 access logs
cerbos ctl audit --kind=access --tail=10
# View the decision logs from midnight 2021-07-01 to midnight 2021-07-02
cerbos ctl audit --kind=decision --between=2021-07-01T00:00:00Z,2021-07-02T00:00:00Z
# View the decision logs from midnight 2021-07-01 to now
cerbos ctl audit --kind=decision --between=2021-07-01T00:00:00Z
# View the access logs from 3 hours ago to now as newline-delimited JSON
cerbos ctl audit --kind=access --since=3h --raw
# View a specific access log entry by call ID
cerbos ctl audit --kind=access --lookup=01F9Y5MFYTX7Y87A30CTJ2FB0S
`

type clientGenFunc func() (svcv1.CerbosAdminServiceClient, error)

func NewAuditCmd(clientGen clientGenFunc) *cobra.Command {
cmd := &cobra.Command{
Use: "audit",
Short: "View audit logs",
Long: longDesc,
Example: exampleDesc,
PreRunE: checkAuditFlags,
RunE: runAuditCmd(clientGen),
}

cmd.Flags().Var(&auditFlags.kind, "kind", "Kind of log entry ('access' or 'decision')")
cmd.Flags().BoolVar(&auditFlags.raw, "raw", false, "Output results without formatting or colours")
cmd.Flags().AddFlagSet(auditFilterFlags.FlagSet())

return cmd
}

func checkAuditFlags(_ *cobra.Command, _ []string) error {
if err := auditFilterFlags.Validate(); err != nil {
return err
}

if auditFlags.kind.Kind() == requestv1.ListAuditLogEntriesRequest_KIND_UNSPECIFIED {
auditFlags.kind = kindFlag(requestv1.ListAuditLogEntriesRequest_KIND_DECISION)
}

return nil
}

func runAuditCmd(clientGen clientGenFunc) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, _ []string) error {
client, err := clientGen()
if err != nil {
return err
}

req := auditFilterFlags.BuildRequest(auditFlags.kind.Kind())
resp, err := client.ListAuditLogEntries(context.Background(), req)
if err != nil {
return err
}

out := cmd.OutOrStdout()

var writer auditLogWriter
if auditFlags.raw {
writer = newRawAuditLogWriter(out)
} else {
writer = newRichAuditLogWriter(out)
}

defer writer.flush()

for {
entry, err := resp.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}

return err
}

if err := writer.write(entry); err != nil {
return err
}
}
}
}

type auditLogWriter interface {
write(*responsev1.ListAuditLogEntriesResponse) error
flush()
}

func newRawAuditLogWriter(out io.Writer) *rawAuditLogWriter {
return &rawAuditLogWriter{out: out}
}

type rawAuditLogWriter struct {
out io.Writer
}

func (r *rawAuditLogWriter) write(entry *responsev1.ListAuditLogEntriesResponse) error {
e := extractEntry(entry)
if e == nil {
return nil
}

outBytes, err := protojson.Marshal(e)
if err != nil {
return err
}

if _, err := r.out.Write(outBytes); err != nil {
return err
}

_, err = r.out.Write(newline)
return err
}

func (r *rawAuditLogWriter) flush() {}

type richAuditLogWriter struct {
out *bufio.Writer
lexer chroma.Lexer
formatter chroma.Formatter
rowStyle func(...string) string
}

func newRichAuditLogWriter(out io.Writer) *richAuditLogWriter {
lexer := lexers.Get("json")
if lexer == nil {
lexer = lexers.Fallback
}

var formatter chroma.Formatter
switch gchalk.GetLevel() {
case gchalk.LevelAnsi256:
formatter = formatters.TTY256
case gchalk.LevelAnsi16m:
formatter = formatters.TTY16m
default:
formatter = formatters.TTY
}

return &richAuditLogWriter{
out: bufio.NewWriter(out),
lexer: chroma.Coalesce(lexer),
formatter: formatter,
rowStyle: gchalk.WithHex("#eeeeee").WithBgHex("#005fff").Bold,
}
}

func (r *richAuditLogWriter) write(entry *responsev1.ListAuditLogEntriesResponse) error {
switch e := entry.Entry.(type) {
case *responsev1.ListAuditLogEntriesResponse_AccessLogEntry:
r.header(fmt.Sprintf("%s %s", e.AccessLogEntry.CallId, strings.Repeat("┈", dashLen)))
return r.formattedJSON(e.AccessLogEntry)
case *responsev1.ListAuditLogEntriesResponse_DecisionLogEntry:
r.header(fmt.Sprintf("%s %s", e.DecisionLogEntry.CallId, strings.Repeat("┈", dashLen)))
return r.formattedJSON(e.DecisionLogEntry)
default:
return nil
}
}

func (r *richAuditLogWriter) header(h string) {
_, _ = r.out.WriteString("\n\n")
_, _ = r.out.WriteString(r.rowStyle(h))
_, _ = r.out.WriteString("\n")
}

func (r *richAuditLogWriter) formattedJSON(msg proto.Message) error {
iterator, err := r.lexer.Tokenise(nil, protojson.Format(msg))
if err != nil {
return err
}

if err := r.formatter.Format(r.out, styles.SolarizedDark256, iterator); err != nil {
return err
}

_, err = r.out.Write(newline)
return err
}

func (r *richAuditLogWriter) flush() {
_ = r.out.Flush()
}

func extractEntry(entry *responsev1.ListAuditLogEntriesResponse) proto.Message {
switch e := entry.Entry.(type) {
case *responsev1.ListAuditLogEntriesResponse_AccessLogEntry:
return e.AccessLogEntry
case *responsev1.ListAuditLogEntriesResponse_DecisionLogEntry:
return e.DecisionLogEntry
default:
return nil
}
}

0 comments on commit 1a7d2eb

Please sign in to comment.