From e226f51dd484f5815250af5e986c0aa5bd495b0b Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 3 Dec 2025 04:10:57 +0000 Subject: [PATCH] Implement auditing of important site lifecycle actions. The list of audit events is: - `CommitManifest` - `DeleteManifest` - `FreezeDomain` - `UnfreezeDomain` Currently these are the main abuse/moderation-relevant actions. If collection is enabled, these events will be logged to `audit/...` storage hierarchy; a way to examine audit logs will be added in the future. The auditing interposer backend is enabled with feature `audit`. --- conf/config.example.toml | 4 + flake.nix | 2 +- go.mod | 1 + go.sum | 2 + src/audit.go | 112 ++++++++++++++++++ src/backend.go | 6 +- src/backend_fs.go | 19 ++- src/backend_s3.go | 18 +++ src/config.go | 8 ++ src/manifest.go | 14 +-- src/observe.go | 7 ++ src/pages.go | 4 +- src/schema.pb.go | 244 ++++++++++++++++++++++++++++++++------- src/schema.proto | 38 +++++- 14 files changed, 418 insertions(+), 61 deletions(-) create mode 100644 src/audit.go diff --git a/conf/config.example.toml b/conf/config.example.toml index 550ce0d..fb03a9c 100644 --- a/conf/config.example.toml +++ b/conf/config.example.toml @@ -54,5 +54,9 @@ forbidden-domains = [] # allowed-repository-url-prefixes = allowed-custom-headers = ["X-Clacks-Overhead"] +[audit] +node-id = 0 +collect = false + [observability] slow-response-threshold = "500ms" diff --git a/flake.nix b/flake.nix index 3c865e6..12a58b7 100644 --- a/flake.nix +++ b/flake.nix @@ -43,7 +43,7 @@ "-s -w" ]; - vendorHash = "sha256-oFKS3ciZyuzzMYg7g3idbssHfDdNYXzNjAXB6XDzMJg="; + vendorHash = "sha256-opS3f4GDczDRp7mrBzvQtK13Qi4snanX4I64FHTh7Pw="; }; in { diff --git a/go.mod b/go.mod index b3fdb9b..11c426f 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/getsentry/sentry-go/slog v0.40.0 github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 + github.com/influxdata/influxdb v1.12.2 github.com/klauspost/compress v1.18.1 github.com/maypok86/otter/v2 v2.2.1 github.com/minio/minio-go/v7 v7.0.97 diff --git a/go.sum b/go.sum index 427cde6..bbe1b95 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/influxdata/influxdb v1.12.2 h1:Y0ZBu47gYVbDCRPMFOrlRRZ3grdqPGIJxerFysVSq+g= +github.com/influxdata/influxdb v1.12.2/go.mod h1:EwqFMB6GKV0Huug82Msa5f8QfXhqETUmC4L9A0QZJQM= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= diff --git a/src/audit.go b/src/audit.go new file mode 100644 index 0000000..608d0f9 --- /dev/null +++ b/src/audit.go @@ -0,0 +1,112 @@ +package git_pages + +import ( + "context" + "fmt" + "strings" + + "github.com/influxdata/influxdb/pkg/snowflake" + "google.golang.org/protobuf/proto" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +func EncodeAuditRecord(auditRecord *AuditRecord) (data []byte) { + data, err := proto.MarshalOptions{Deterministic: true}.Marshal(auditRecord) + if err != nil { + panic(err) + } + return +} + +func DecodeAuditRecord(data []byte) (auditRecord *AuditRecord, err error) { + auditRecord = &AuditRecord{} + err = proto.Unmarshal(data, auditRecord) + return +} + +type auditedBackend struct { + Backend + ids *snowflake.Generator +} + +var _ Backend = (*auditedBackend)(nil) + +func NewAuditedBackend(backend Backend) Backend { + if config.Feature("audit") { + ids := snowflake.New(config.Audit.NodeID) + return &auditedBackend{backend, ids} + } else { + return backend + } +} + +// This function does not retry appending audit records; as such, if it returns an error, +// this error must interrupt whatever operation it was auditing. A corollary is that it is +// possible that appending an audit record succeeds but the audited operation fails. +// This is considered fine since the purpose of auditing is to record end user intent, not +// to be a 100% accurate reflection of performed actions. When in doubt, the audit records +// should be examined together with the application logs. +func (audited *auditedBackend) appendNewAuditRecord(ctx context.Context, record *AuditRecord) (err error) { + record.Timestamp = timestamppb.Now() + + if config.Audit.Collect { + id := fmt.Sprintf("%016x", audited.ids.Next()) + err = audited.Backend.AppendAuditRecord(ctx, id, record) + if err != nil { + err = fmt.Errorf("audit: %w", err) + } else { + var subject string + if record.Project == nil { + subject = *record.Domain + } else { + subject = fmt.Sprintf("%s/%s", *record.Domain, *record.Project) + } + logc.Printf(ctx, "audit %s ok: %s %s\n", subject, record.Event.String(), id) + } + } + return +} + +func (audited *auditedBackend) CommitManifest(ctx context.Context, name string, manifest *Manifest) (err error) { + domain, project, ok := strings.Cut(name, "/") + if !ok { + panic("malformed manifest name") + } + audited.appendNewAuditRecord(ctx, &AuditRecord{ + Event: AuditEvent_CommitManifest.Enum(), + Domain: proto.String(domain), + Project: proto.String(project), + Manifest: manifest, + }) + + return audited.Backend.CommitManifest(ctx, name, manifest) +} + +func (audited *auditedBackend) DeleteManifest(ctx context.Context, name string) (err error) { + domain, project, ok := strings.Cut(name, "/") + if !ok { + panic("malformed manifest name") + } + audited.appendNewAuditRecord(ctx, &AuditRecord{ + Event: AuditEvent_DeleteManifest.Enum(), + Domain: proto.String(domain), + Project: proto.String(project), + }) + + return audited.Backend.DeleteManifest(ctx, name) +} + +func (audited *auditedBackend) FreezeDomain(ctx context.Context, domain string, freeze bool) (err error) { + var event AuditEvent + if freeze { + event = AuditEvent_FreezeDomain + } else { + event = AuditEvent_UnfreezeDomain + } + audited.appendNewAuditRecord(ctx, &AuditRecord{ + Event: event.Enum(), + Domain: proto.String(domain), + }) + + return audited.Backend.FreezeDomain(ctx, domain, freeze) +} diff --git a/src/backend.go b/src/backend.go index c838d38..d7cfdf9 100644 --- a/src/backend.go +++ b/src/backend.go @@ -75,12 +75,15 @@ type Backend interface { // Check whether a domain has any deployments. CheckDomain(ctx context.Context, domain string) (found bool, err error) - // Creates a domain. This allows us to start serving content for the domain. + // Create a domain. This allows us to start serving content for the domain. CreateDomain(ctx context.Context, domain string) error // Freeze or thaw a domain. This allows a site to be administratively locked, e.g. if it // is discovered serving abusive content. FreezeDomain(ctx context.Context, domain string, freeze bool) error + + // Append an audit record to the log. + AppendAuditRecord(ctx context.Context, id string, record *AuditRecord) error } func CreateBackend(config *StorageConfig) (backend Backend, err error) { @@ -96,5 +99,6 @@ func CreateBackend(config *StorageConfig) (backend Backend, err error) { default: err = fmt.Errorf("unknown backend: %s", config.Type) } + backend = NewAuditedBackend(backend) return } diff --git a/src/backend_fs.go b/src/backend_fs.go index da5740b..5f593b5 100644 --- a/src/backend_fs.go +++ b/src/backend_fs.go @@ -14,8 +14,9 @@ import ( ) type FSBackend struct { - blobRoot *os.Root - siteRoot *os.Root + blobRoot *os.Root + siteRoot *os.Root + auditRoot *os.Root } var _ Backend = (*FSBackend)(nil) @@ -63,7 +64,11 @@ func NewFSBackend(config *FSConfig) (*FSBackend, error) { if err != nil { return nil, fmt.Errorf("site: %w", err) } - return &FSBackend{blobRoot, siteRoot}, nil + auditRoot, err := maybeCreateOpenRoot(config.Root, "audit") + if err != nil { + return nil, fmt.Errorf("audit: %w", err) + } + return &FSBackend{blobRoot, siteRoot, auditRoot}, nil } func (fs *FSBackend) Backend() Backend { @@ -287,3 +292,11 @@ func (fs *FSBackend) FreezeDomain(ctx context.Context, domain string, freeze boo } } } + +func (fs *FSBackend) AppendAuditRecord(ctx context.Context, id string, record *AuditRecord) error { + if _, err := fs.auditRoot.Stat(id); err == nil { + panic(fmt.Errorf("audit ID collision: %s", id)) + } + + return fs.auditRoot.WriteFile(id, EncodeAuditRecord(record), 0o644) +} diff --git a/src/backend_s3.go b/src/backend_s3.go index a88d9f6..06d939e 100644 --- a/src/backend_s3.go +++ b/src/backend_s3.go @@ -630,3 +630,21 @@ func (s3 *S3Backend) FreezeDomain(ctx context.Context, domain string, freeze boo } } } + +func auditObjectName(id string) string { + return fmt.Sprintf("audit/%s", id) +} + +func (s3 *S3Backend) AppendAuditRecord(ctx context.Context, id string, record *AuditRecord) error { + name := auditObjectName(id) + data := EncodeAuditRecord(record) + + options := minio.PutObjectOptions{} + options.SetMatchETagExcept("*") // may or may not be supported + _, err := s3.client.PutObject(ctx, s3.bucket, name, + bytes.NewReader(data), int64(len(data)), options) + if errResp := minio.ToErrorResponse(err); errResp.StatusCode == 412 { + panic(fmt.Errorf("audit ID collision: %s", name)) + } + return err +} diff --git a/src/config.go b/src/config.go index b4db8da..80a1845 100644 --- a/src/config.go +++ b/src/config.go @@ -44,6 +44,7 @@ type Config struct { Fallback FallbackConfig `toml:"fallback"` Storage StorageConfig `toml:"storage"` Limits LimitsConfig `toml:"limits"` + Audit AuditConfig `toml:"audit"` Observability ObservabilityConfig `toml:"observability"` } @@ -121,6 +122,13 @@ type LimitsConfig struct { AllowedCustomHeaders []string `toml:"allowed-custom-headers" default:"[\"X-Clacks-Overhead\"]"` } +type AuditConfig struct { + // Globally unique node identifier (0 to 1023 inclusive). + NodeID int `toml:"node-id"` + // Whether audit reports should be stored whenever an audit event occurs. + Collect bool `toml:"collect"` +} + type ObservabilityConfig struct { // Minimum duration for an HTTP request transaction to be unconditionally sampled. SlowResponseThreshold Duration `toml:"slow-response-threshold" default:"500ms"` diff --git a/src/manifest.go b/src/manifest.go index 3aae654..c16b8c2 100644 --- a/src/manifest.go +++ b/src/manifest.go @@ -68,18 +68,18 @@ func CompareManifest(left *Manifest, right *Manifest) bool { return true } -func EncodeManifest(manifest *Manifest) []byte { - result, err := proto.MarshalOptions{Deterministic: true}.Marshal(manifest) +func EncodeManifest(manifest *Manifest) (data []byte) { + data, err := proto.MarshalOptions{Deterministic: true}.Marshal(manifest) if err != nil { panic(err) } - return result + return } -func DecodeManifest(data []byte) (*Manifest, error) { - manifest := Manifest{} - err := proto.Unmarshal(data, &manifest) - return &manifest, err +func DecodeManifest(data []byte) (manifest *Manifest, err error) { + manifest = &Manifest{} + err = proto.Unmarshal(data, manifest) + return } func AddProblem(manifest *Manifest, path, format string, args ...any) error { diff --git a/src/observe.go b/src/observe.go index ed048b1..30127fb 100644 --- a/src/observe.go +++ b/src/observe.go @@ -436,3 +436,10 @@ func (backend *observedBackend) FreezeDomain(ctx context.Context, domain string, span.Finish() return } + +func (backend *observedBackend) AppendAuditRecord(ctx context.Context, id string, record *AuditRecord) (err error) { + span, ctx := ObserveFunction(ctx, "AppendAudit", "audit.id", id) + err = backend.inner.AppendAuditRecord(ctx, id, record) + span.Finish() + return +} diff --git a/src/pages.go b/src/pages.go index 04a2762..0b95955 100644 --- a/src/pages.go +++ b/src/pages.go @@ -241,7 +241,7 @@ func getPage(w http.ResponseWriter, r *http.Request) error { entry = manifest.Contents[entryPath] if !appliedRedirect { redirectKind := RedirectAny - if entry != nil && entry.GetType() != Type_Invalid { + if entry != nil && entry.GetType() != Type_InvalidEntry { redirectKind = RedirectForce } originalURL := (&url.URL{Host: r.Host}).ResolveReference(r.URL) @@ -258,7 +258,7 @@ func getPage(w http.ResponseWriter, r *http.Request) error { continue } } - if entry == nil || entry.GetType() == Type_Invalid { + if entry == nil || entry.GetType() == Type_InvalidEntry { status = 404 if entryPath != notFoundPage { entryPath = notFoundPage diff --git a/src/schema.pb.go b/src/schema.pb.go index b9b98d3..cfe3930 100644 --- a/src/schema.pb.go +++ b/src/schema.pb.go @@ -9,6 +9,7 @@ package git_pages import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -25,7 +26,7 @@ type Type int32 const ( // Invalid entry. - Type_Invalid Type = 0 + Type_InvalidEntry Type = 0 // Directory. Type_Directory Type = 1 // Inline file. `Blob.Data` contains file contents. @@ -39,14 +40,14 @@ const ( // Enum value maps for Type. var ( Type_name = map[int32]string{ - 0: "Invalid", + 0: "InvalidEntry", 1: "Directory", 2: "InlineFile", 3: "ExternalFile", 4: "Symlink", } Type_value = map[string]int32{ - "Invalid": 0, + "InvalidEntry": 0, "Directory": 1, "InlineFile": 2, "ExternalFile": 3, @@ -130,6 +131,66 @@ func (Transform) EnumDescriptor() ([]byte, []int) { return file_schema_proto_rawDescGZIP(), []int{1} } +type AuditEvent int32 + +const ( + // Invalid event. + AuditEvent_InvalidEvent AuditEvent = 0 + // A manifest was committed (a site was created or updated). + AuditEvent_CommitManifest AuditEvent = 1 + // A manifest was deleted (a site was deleted). + AuditEvent_DeleteManifest AuditEvent = 2 + // A domain was frozen. + AuditEvent_FreezeDomain AuditEvent = 3 + // A domain was thawed. + AuditEvent_UnfreezeDomain AuditEvent = 4 +) + +// Enum value maps for AuditEvent. +var ( + AuditEvent_name = map[int32]string{ + 0: "InvalidEvent", + 1: "CommitManifest", + 2: "DeleteManifest", + 3: "FreezeDomain", + 4: "UnfreezeDomain", + } + AuditEvent_value = map[string]int32{ + "InvalidEvent": 0, + "CommitManifest": 1, + "DeleteManifest": 2, + "FreezeDomain": 3, + "UnfreezeDomain": 4, + } +) + +func (x AuditEvent) Enum() *AuditEvent { + p := new(AuditEvent) + *p = x + return p +} + +func (x AuditEvent) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AuditEvent) Descriptor() protoreflect.EnumDescriptor { + return file_schema_proto_enumTypes[2].Descriptor() +} + +func (AuditEvent) Type() protoreflect.EnumType { + return &file_schema_proto_enumTypes[2] +} + +func (x AuditEvent) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AuditEvent.Descriptor instead. +func (AuditEvent) EnumDescriptor() ([]byte, []int) { + return file_schema_proto_rawDescGZIP(), []int{2} +} + type Entry struct { state protoimpl.MessageState `protogen:"open.v1"` Type *Type `protobuf:"varint,1,opt,name=type,enum=Type" json:"type,omitempty"` @@ -198,7 +259,7 @@ func (x *Entry) GetType() Type { if x != nil && x.Type != nil { return *x.Type } - return Type_Invalid + return Type_InvalidEntry } func (x *Entry) GetOriginalSize() int64 { @@ -472,19 +533,19 @@ func (x *Problem) GetCause() string { type Manifest struct { state protoimpl.MessageState `protogen:"open.v1"` - // Source metadata + // Source metadata. RepoUrl *string `protobuf:"bytes,1,opt,name=repo_url,json=repoUrl" json:"repo_url,omitempty"` Branch *string `protobuf:"bytes,2,opt,name=branch" json:"branch,omitempty"` Commit *string `protobuf:"bytes,3,opt,name=commit" json:"commit,omitempty"` - // Contents + // Site contents. Contents map[string]*Entry `protobuf:"bytes,4,rep,name=contents" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - OriginalSize *int64 `protobuf:"varint,10,opt,name=original_size,json=originalSize" json:"original_size,omitempty"` // total size of entries before compression - CompressedSize *int64 `protobuf:"varint,5,opt,name=compressed_size,json=compressedSize" json:"compressed_size,omitempty"` // simple sum of each `entry.size` - StoredSize *int64 `protobuf:"varint,8,opt,name=stored_size,json=storedSize" json:"stored_size,omitempty"` // total size of (deduplicated) external objects - // Netlify-style `_redirects` and `_headers` + OriginalSize *int64 `protobuf:"varint,10,opt,name=original_size,json=originalSize" json:"original_size,omitempty"` // sum of each `entry.original_size` + CompressedSize *int64 `protobuf:"varint,5,opt,name=compressed_size,json=compressedSize" json:"compressed_size,omitempty"` // sum of each `entry.compressed_size` + StoredSize *int64 `protobuf:"varint,8,opt,name=stored_size,json=storedSize" json:"stored_size,omitempty"` // sum of deduplicated `entry.compressed_size` for external files only + // Netlify-style `_redirects` and `_headers` rules. Redirects []*RedirectRule `protobuf:"bytes,6,rep,name=redirects" json:"redirects,omitempty"` Headers []*HeaderRule `protobuf:"bytes,9,rep,name=headers" json:"headers,omitempty"` - // Diagnostics for non-fatal errors + // Diagnostics for non-fatal errors. Problems []*Problem `protobuf:"bytes,7,rep,name=problems" json:"problems,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -590,11 +651,90 @@ func (x *Manifest) GetProblems() []*Problem { return nil } +type AuditRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Audit event metadata. + Event *AuditEvent `protobuf:"varint,1,opt,name=event,enum=AuditEvent" json:"event,omitempty"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp" json:"timestamp,omitempty"` + // Affected resource. + Domain *string `protobuf:"bytes,10,opt,name=domain" json:"domain,omitempty"` + Project *string `protobuf:"bytes,11,opt,name=project" json:"project,omitempty"` // only for `*Manifest` events + // Snapshot of site manifest. + Manifest *Manifest `protobuf:"bytes,12,opt,name=manifest" json:"manifest,omitempty"` // only for `*Manifest` events + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuditRecord) Reset() { + *x = AuditRecord{} + mi := &file_schema_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuditRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuditRecord) ProtoMessage() {} + +func (x *AuditRecord) ProtoReflect() protoreflect.Message { + mi := &file_schema_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuditRecord.ProtoReflect.Descriptor instead. +func (*AuditRecord) Descriptor() ([]byte, []int) { + return file_schema_proto_rawDescGZIP(), []int{6} +} + +func (x *AuditRecord) GetEvent() AuditEvent { + if x != nil && x.Event != nil { + return *x.Event + } + return AuditEvent_InvalidEvent +} + +func (x *AuditRecord) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *AuditRecord) GetDomain() string { + if x != nil && x.Domain != nil { + return *x.Domain + } + return "" +} + +func (x *AuditRecord) GetProject() string { + if x != nil && x.Project != nil { + return *x.Project + } + return "" +} + +func (x *AuditRecord) GetManifest() *Manifest { + if x != nil { + return x.Manifest + } + return nil +} + var File_schema_proto protoreflect.FileDescriptor const file_schema_proto_rawDesc = "" + "\n" + - "\fschema.proto\"\xec\x01\n" + + "\fschema.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xec\x01\n" + "\x05Entry\x12\x19\n" + "\x04type\x18\x01 \x01(\x0e2\x05.TypeR\x04type\x12#\n" + "\roriginal_size\x18\a \x01(\x03R\foriginalSize\x12'\n" + @@ -635,9 +775,16 @@ const file_schema_proto_rawDesc = "" + "\bproblems\x18\a \x03(\v2\b.ProblemR\bproblems\x1aC\n" + "\rContentsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x1c\n" + - "\x05value\x18\x02 \x01(\v2\x06.EntryR\x05value:\x028\x01*Q\n" + - "\x04Type\x12\v\n" + - "\aInvalid\x10\x00\x12\r\n" + + "\x05value\x18\x02 \x01(\v2\x06.EntryR\x05value:\x028\x01\"\xc3\x01\n" + + "\vAuditRecord\x12!\n" + + "\x05event\x18\x01 \x01(\x0e2\v.AuditEventR\x05event\x128\n" + + "\ttimestamp\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x16\n" + + "\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*V\n" + + "\x04Type\x12\x10\n" + + "\fInvalidEntry\x10\x00\x12\r\n" + "\tDirectory\x10\x01\x12\x0e\n" + "\n" + "InlineFile\x10\x02\x12\x10\n" + @@ -645,7 +792,14 @@ const file_schema_proto_rawDesc = "" + "\aSymlink\x10\x04*#\n" + "\tTransform\x12\f\n" + "\bIdentity\x10\x00\x12\b\n" + - "\x04Zstd\x10\x01B,Z*codeberg.org/git-pages/git-pages/git_pagesb\beditionsp\xe8\a" + "\x04Zstd\x10\x01*l\n" + + "\n" + + "AuditEvent\x12\x10\n" + + "\fInvalidEvent\x10\x00\x12\x12\n" + + "\x0eCommitManifest\x10\x01\x12\x12\n" + + "\x0eDeleteManifest\x10\x02\x12\x10\n" + + "\fFreezeDomain\x10\x03\x12\x12\n" + + "\x0eUnfreezeDomain\x10\x04B,Z*codeberg.org/git-pages/git-pages/git_pagesb\beditionsp\xe8\a" var ( file_schema_proto_rawDescOnce sync.Once @@ -659,33 +813,39 @@ func file_schema_proto_rawDescGZIP() []byte { return file_schema_proto_rawDescData } -var file_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_schema_proto_goTypes = []any{ - (Type)(0), // 0: Type - (Transform)(0), // 1: Transform - (*Entry)(nil), // 2: Entry - (*RedirectRule)(nil), // 3: RedirectRule - (*Header)(nil), // 4: Header - (*HeaderRule)(nil), // 5: HeaderRule - (*Problem)(nil), // 6: Problem - (*Manifest)(nil), // 7: Manifest - nil, // 8: Manifest.ContentsEntry + (Type)(0), // 0: Type + (Transform)(0), // 1: Transform + (AuditEvent)(0), // 2: AuditEvent + (*Entry)(nil), // 3: Entry + (*RedirectRule)(nil), // 4: RedirectRule + (*Header)(nil), // 5: Header + (*HeaderRule)(nil), // 6: HeaderRule + (*Problem)(nil), // 7: Problem + (*Manifest)(nil), // 8: Manifest + (*AuditRecord)(nil), // 9: AuditRecord + nil, // 10: Manifest.ContentsEntry + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp } var file_schema_proto_depIdxs = []int32{ - 0, // 0: Entry.type:type_name -> Type - 1, // 1: Entry.transform:type_name -> Transform - 4, // 2: HeaderRule.header_map:type_name -> Header - 8, // 3: Manifest.contents:type_name -> Manifest.ContentsEntry - 3, // 4: Manifest.redirects:type_name -> RedirectRule - 5, // 5: Manifest.headers:type_name -> HeaderRule - 6, // 6: Manifest.problems:type_name -> Problem - 2, // 7: Manifest.ContentsEntry.value:type_name -> Entry - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 0, // 0: Entry.type:type_name -> Type + 1, // 1: Entry.transform:type_name -> Transform + 5, // 2: HeaderRule.header_map:type_name -> Header + 10, // 3: Manifest.contents:type_name -> Manifest.ContentsEntry + 4, // 4: Manifest.redirects:type_name -> RedirectRule + 6, // 5: Manifest.headers:type_name -> HeaderRule + 7, // 6: Manifest.problems:type_name -> Problem + 2, // 7: AuditRecord.event:type_name -> AuditEvent + 11, // 8: AuditRecord.timestamp:type_name -> google.protobuf.Timestamp + 8, // 9: AuditRecord.manifest:type_name -> Manifest + 3, // 10: Manifest.ContentsEntry.value:type_name -> Entry + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_schema_proto_init() } @@ -698,8 +858,8 @@ func file_schema_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_schema_proto_rawDesc), len(file_schema_proto_rawDesc)), - NumEnums: 2, - NumMessages: 7, + NumEnums: 3, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/src/schema.proto b/src/schema.proto index 6a8d006..4ed4720 100644 --- a/src/schema.proto +++ b/src/schema.proto @@ -2,9 +2,11 @@ edition = "2023"; option go_package = "codeberg.org/git-pages/git-pages/git_pages"; +import "google/protobuf/timestamp.proto"; + enum Type { // Invalid entry. - Invalid = 0; + InvalidEntry = 0; // Directory. Directory = 1; // Inline file. `Blob.Data` contains file contents. @@ -80,21 +82,47 @@ message Problem { } message Manifest { - // Source metadata + // Source metadata. string repo_url = 1; string branch = 2; string commit = 3; - // Contents + // Site contents. map contents = 4; int64 original_size = 10; // sum of each `entry.original_size` int64 compressed_size = 5; // sum of each `entry.compressed_size` int64 stored_size = 8; // sum of deduplicated `entry.compressed_size` for external files only - // Netlify-style `_redirects` and `_headers` + // Netlify-style `_redirects` and `_headers` rules. repeated RedirectRule redirects = 6; repeated HeaderRule headers = 9; - // Diagnostics for non-fatal errors + // Diagnostics for non-fatal errors. repeated Problem problems = 7; } + +enum AuditEvent { + // Invalid event. + InvalidEvent = 0; + // A manifest was committed (a site was created or updated). + CommitManifest = 1; + // A manifest was deleted (a site was deleted). + DeleteManifest = 2; + // A domain was frozen. + FreezeDomain = 3; + // A domain was thawed. + UnfreezeDomain = 4; +} + +message AuditRecord { + // Audit event metadata. + AuditEvent event = 1; + google.protobuf.Timestamp timestamp = 2; + + // Affected resource. + string domain = 10; + string project = 11; // only for `*Manifest` events + + // Snapshot of site manifest. + Manifest manifest = 12; // only for `*Manifest` events +}