diff --git a/flake.nix b/flake.nix index 28d17a1..e3a3e34 100644 --- a/flake.nix +++ b/flake.nix @@ -43,7 +43,7 @@ "-s -w" ]; - vendorHash = "sha256-LkHC/gFiSfYz9Z4bYMq1QNdapPYp8h1DSMRfFU9f7mw="; + vendorHash = "sha256-40LyEXdJDpWPe9UvqM2siqXdpbae1ba7kN7FtySPpBc="; }; in { diff --git a/go.mod b/go.mod index ee253e7..76817db 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/KimMachineGun/automemlimit v0.7.5 github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 github.com/creasty/defaults v1.8.0 + github.com/fatih/color v1.18.0 github.com/getsentry/sentry-go v0.40.0 github.com/getsentry/sentry-go/slog v0.40.0 github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 @@ -43,6 +44,8 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/leodido/go-syslog/v4 v4.3.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/crc64nvme v1.1.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 23c8742..a9f7bf2 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo= github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= github.com/getsentry/sentry-go/slog v0.40.0 h1:uR2EPL9w6uHw3XB983IAqzqM9mP+fjJpNY9kfob3/Z8= @@ -81,6 +83,11 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-syslog/v4 v4.3.0 h1:bbSpI/41bYK9iSdlYzcwvlxuLOE8yi4VTFmedtnghdA= github.com/leodido/go-syslog/v4 v4.3.0/go.mod h1:eJ8rUfDN5OS6dOkCOBYlg2a+hbAg6pJa99QXXgMrd98= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI= github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs= github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= @@ -150,6 +157,8 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= diff --git a/src/audit.go b/src/audit.go index b170e8a..6dc77ac 100644 --- a/src/audit.go +++ b/src/audit.go @@ -87,6 +87,37 @@ func DecodeAuditRecord(data []byte) (record *AuditRecord, err error) { return } +func (record *AuditRecord) GetAuditID() AuditID { + return AuditID(record.GetId()) +} + +func (record *AuditRecord) DescribePrincipal() string { + var items []string + if record.Principal != nil { + if record.Principal.GetIpAddress() != "" { + items = append(items, record.Principal.GetIpAddress()) + } + if record.Principal.GetCliAdmin() { + items = append(items, "") + } + } + if len(items) > 0 { + return strings.Join(items, ";") + } else { + return "" + } +} + +func (record *AuditRecord) DescribeResource() string { + desc := "" + if record.Domain != nil && record.Project != nil { + desc = fmt.Sprintf("%s/%s", *record.Domain, *record.Project) + } else if record.Domain != nil { + desc = *record.Domain + } + return desc +} + type AuditRecordScope int const ( diff --git a/src/backend.go b/src/backend.go index cf72c96..3dbc759 100644 --- a/src/backend.go +++ b/src/backend.go @@ -52,7 +52,7 @@ type ModifyManifestOptions struct { IfMatch string } -type QueryAuditLogOptions struct { +type SearchAuditLogOptions struct { // Inclusive lower bound on returned audit records, per their Snowflake ID (which may differ // slightly from the embedded timestamp). If zero, audit records are returned since beginning // of time. @@ -63,7 +63,7 @@ type QueryAuditLogOptions struct { Until time.Time } -type QueryAuditLogResult struct { +type SearchAuditLogResult struct { ID AuditID Err error } @@ -130,17 +130,17 @@ type Backend interface { QueryAuditLog(ctx context.Context, id AuditID) (record *AuditRecord, err error) // Retrieve records from the audit log by time range. - SearchAuditLog(ctx context.Context, opts QueryAuditLogOptions) iter.Seq[QueryAuditLogResult] + SearchAuditLog(ctx context.Context, opts SearchAuditLogOptions) iter.Seq2[AuditID, error] } -func CreateBackend(config *StorageConfig) (backend Backend, err error) { +func CreateBackend(ctx context.Context, config *StorageConfig) (backend Backend, err error) { switch config.Type { case "fs": - if backend, err = NewFSBackend(context.Background(), &config.FS); err != nil { + if backend, err = NewFSBackend(ctx, &config.FS); err != nil { err = fmt.Errorf("fs backend: %w", err) } case "s3": - if backend, err = NewS3Backend(context.Background(), &config.S3); err != nil { + if backend, err = NewS3Backend(ctx, &config.S3); err != nil { err = fmt.Errorf("s3 backend: %w", err) } default: diff --git a/src/backend_fs.go b/src/backend_fs.go index 682ef1b..e26d13c 100644 --- a/src/backend_fs.go +++ b/src/backend_fs.go @@ -434,30 +434,28 @@ func (fs *FSBackend) QueryAuditLog(ctx context.Context, id AuditID) (*AuditRecor } func (fs *FSBackend) SearchAuditLog( - ctx context.Context, opts QueryAuditLogOptions, -) iter.Seq[QueryAuditLogResult] { - return func(yield func(QueryAuditLogResult) bool) { + ctx context.Context, opts SearchAuditLogOptions, +) iter.Seq2[AuditID, error] { + return func(yield func(AuditID, error) bool) { iofs.WalkDir(fs.auditRoot.FS(), ".", func(path string, entry iofs.DirEntry, err error) error { if path == "." { - return nil + return nil // skip } - var result QueryAuditLogResult + var id AuditID if err != nil { - result.Err = err - } else if id, err := ParseAuditID(path); err != nil { - result.Err = err + // report error + } else if id, err = ParseAuditID(path); err != nil { + // report error } else if !opts.Since.IsZero() && id.CompareTime(opts.Since) < 0 { - return nil + return nil // skip } else if !opts.Until.IsZero() && id.CompareTime(opts.Until) > 0 { - return nil - } else { - result.ID = id + return nil // skip } - if !yield(result) { - return iofs.SkipAll + if !yield(id, err) { + return iofs.SkipAll // break } else { - return nil + return nil // continue } }) } diff --git a/src/backend_s3.go b/src/backend_s3.go index 76bf205..5a1aca1 100644 --- a/src/backend_s3.go +++ b/src/backend_s3.go @@ -734,9 +734,9 @@ func (s3 *S3Backend) QueryAuditLog(ctx context.Context, id AuditID) (*AuditRecor } func (s3 *S3Backend) SearchAuditLog( - ctx context.Context, opts QueryAuditLogOptions, -) iter.Seq[QueryAuditLogResult] { - return func(yield func(QueryAuditLogResult) bool) { + ctx context.Context, opts SearchAuditLogOptions, +) iter.Seq2[AuditID, error] { + return func(yield func(AuditID, error) bool) { logc.Printf(ctx, "s3: query audit\n") ctx, cancel := context.WithCancel(ctx) @@ -746,15 +746,14 @@ func (s3 *S3Backend) SearchAuditLog( for object := range s3.client.ListObjectsIter(ctx, s3.bucket, minio.ListObjectsOptions{ Prefix: prefix, }) { - var result QueryAuditLogResult + var id AuditID + var err error if object.Err != nil { - result.Err = object.Err - } else if id, err := ParseAuditID(strings.TrimPrefix(object.Key, prefix)); err != nil { - result.Err = err + err = object.Err } else { - result.ID = id + id, err = ParseAuditID(strings.TrimPrefix(object.Key, prefix)) } - if !yield(result) { + if !yield(id, err) { break } } diff --git a/src/main.go b/src/main.go index 71ffa36..c50e751 100644 --- a/src/main.go +++ b/src/main.go @@ -20,8 +20,10 @@ import ( automemlimit "github.com/KimMachineGun/automemlimit/memlimit" "github.com/c2h5oh/datasize" + "github.com/fatih/color" "github.com/kankanreno/go-snowflake" "github.com/prometheus/client_golang/prometheus/promhttp" + "google.golang.org/protobuf/proto" ) var config *Config @@ -175,7 +177,7 @@ func usage() { fmt.Fprintf(os.Stderr, "(admin) "+ "git-pages {-run-migration |-freeze-domain |-unfreeze-domain }\n") fmt.Fprintf(os.Stderr, "(audit) "+ - "git-pages {-audit-read }\n") + "git-pages {-audit-log|-audit-read }\n") fmt.Fprintf(os.Stderr, "(info) "+ "git-pages {-print-config-env-vars|-print-config}\n") fmt.Fprintf(os.Stderr, "(cli) "+ @@ -209,6 +211,8 @@ func Main() { "prevent any site uploads to a given `domain`") unfreezeDomain := flag.String("unfreeze-domain", "", "allow site uploads to a `domain` again after it has been frozen") + auditLog := flag.Bool("audit-log", false, + "display audit log") auditRead := flag.String("audit-read", "", "extract contents of audit record `id` to files '-*'") flag.Parse() @@ -222,6 +226,7 @@ func Main() { *updateSite != "", *freezeDomain != "", *unfreezeDomain != "", + *auditLog, *auditRead != "", } { if selected { @@ -270,7 +275,7 @@ func Main() { switch { case *runMigration != "": - if backend, err = CreateBackend(&config.Storage); err != nil { + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } @@ -279,7 +284,7 @@ func Main() { } case *getBlob != "": - if backend, err = CreateBackend(&config.Storage); err != nil { + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } @@ -290,7 +295,7 @@ func Main() { io.Copy(fileOutputArg(), reader) case *getManifest != "": - if backend, err = CreateBackend(&config.Storage); err != nil { + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } @@ -302,7 +307,7 @@ func Main() { fmt.Fprintln(fileOutputArg(), string(ManifestJSON(manifest))) case *getArchive != "": - if backend, err = CreateBackend(&config.Storage); err != nil { + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } @@ -317,7 +322,10 @@ func Main() { } case *updateSite != "": - if backend, err = CreateBackend(&config.Storage); err != nil { + ctx = WithPrincipal(ctx) + GetPrincipal(ctx).CliAdmin = proto.Bool(true) + + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } @@ -382,6 +390,9 @@ func Main() { } case *freezeDomain != "" || *unfreezeDomain != "": + ctx = WithPrincipal(ctx) + GetPrincipal(ctx).CliAdmin = proto.Bool(true) + var domain string var freeze bool if *freezeDomain != "" { @@ -392,7 +403,7 @@ func Main() { freeze = false } - if backend, err = CreateBackend(&config.Storage); err != nil { + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } @@ -405,8 +416,30 @@ func Main() { logc.Println(ctx, "thawed") } + case *auditLog: + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { + logc.Fatalln(ctx, err) + } + + for id, err := range backend.SearchAuditLog(ctx, SearchAuditLogOptions{}) { + if err != nil { + logc.Fatalln(ctx, err) + } + record, err := backend.QueryAuditLog(ctx, id) + if err != nil { + logc.Fatalln(ctx, err) + } + fmt.Fprintf(color.Output, "%s %s %s %s %s\n", + record.GetAuditID().String(), + color.HiWhiteString(record.GetTimestamp().AsTime().UTC().Format(time.RFC3339)), + color.HiMagentaString(record.DescribePrincipal()), + color.HiGreenString(record.DescribeResource()), + record.GetEvent(), + ) + } + case *auditRead != "": - if backend, err = CreateBackend(&config.Storage); err != nil { + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } @@ -475,7 +508,7 @@ func Main() { caddyListener := listen(ctx, "caddy", config.Server.Caddy) metricsListener := listen(ctx, "metrics", config.Server.Metrics) - if backend, err = CreateBackend(&config.Storage); err != nil { + if backend, err = CreateBackend(ctx, &config.Storage); err != nil { logc.Fatalln(ctx, err) } backend = NewObservedBackend(backend) diff --git a/src/observe.go b/src/observe.go index 09c43aa..14781f7 100644 --- a/src/observe.go +++ b/src/observe.go @@ -457,15 +457,15 @@ func (backend *observedBackend) QueryAuditLog(ctx context.Context, id AuditID) ( } func (backend *observedBackend) SearchAuditLog( - ctx context.Context, opts QueryAuditLogOptions, -) iter.Seq[QueryAuditLogResult] { - return func(yield func(QueryAuditLogResult) bool) { + ctx context.Context, opts SearchAuditLogOptions, +) iter.Seq2[AuditID, error] { + return func(yield func(AuditID, error) bool) { span, ctx := ObserveFunction(ctx, "SearchAuditLog", "audit.search.since", opts.Since, "audit.search.until", opts.Until, ) - for result := range backend.inner.SearchAuditLog(ctx, opts) { - if !yield(result) { + for id, err := range backend.inner.SearchAuditLog(ctx, opts) { + if !yield(id, err) { break } } diff --git a/src/schema.pb.go b/src/schema.pb.go index 825d85f..48158db 100644 --- a/src/schema.pb.go +++ b/src/schema.pb.go @@ -749,6 +749,7 @@ func (x *AuditRecord) GetManifest() *Manifest { type Principal struct { state protoimpl.MessageState `protogen:"open.v1"` IpAddress *string `protobuf:"bytes,1,opt,name=ip_address,json=ipAddress" json:"ip_address,omitempty"` + CliAdmin *bool `protobuf:"varint,2,opt,name=cli_admin,json=cliAdmin" json:"cli_admin,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -790,6 +791,13 @@ func (x *Principal) GetIpAddress() string { return "" } +func (x *Principal) GetCliAdmin() bool { + if x != nil && x.CliAdmin != nil { + return *x.CliAdmin + } + return false +} + var File_schema_proto protoreflect.FileDescriptor const file_schema_proto_rawDesc = "" + @@ -845,10 +853,11 @@ const file_schema_proto_rawDesc = "" + "\x06domain\x18\n" + " \x01(\tR\x06domain\x12\x18\n" + "\aproject\x18\v \x01(\tR\aproject\x12%\n" + - "\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"*\n" + + "\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"G\n" + "\tPrincipal\x12\x1d\n" + "\n" + - "ip_address\x18\x01 \x01(\tR\tipAddress*V\n" + + "ip_address\x18\x01 \x01(\tR\tipAddress\x12\x1b\n" + + "\tcli_admin\x18\x02 \x01(\bR\bcliAdmin*V\n" + "\x04Type\x12\x10\n" + "\fInvalidEntry\x10\x00\x12\r\n" + "\tDirectory\x10\x01\x12\x0e\n" + diff --git a/src/schema.proto b/src/schema.proto index 730ec66..b772396 100644 --- a/src/schema.proto +++ b/src/schema.proto @@ -131,4 +131,5 @@ message AuditRecord { message Principal { string ip_address = 1; + bool cli_admin = 2; }