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`.
This commit is contained in:
Catherine
2025-12-03 04:10:57 +00:00
parent dcf70dfdda
commit e226f51dd4
14 changed files with 418 additions and 61 deletions

View File

@@ -54,5 +54,9 @@ forbidden-domains = []
# allowed-repository-url-prefixes = <nil>
allowed-custom-headers = ["X-Clacks-Overhead"]
[audit]
node-id = 0
collect = false
[observability]
slow-response-threshold = "500ms"

View File

@@ -43,7 +43,7 @@
"-s -w"
];
vendorHash = "sha256-oFKS3ciZyuzzMYg7g3idbssHfDdNYXzNjAXB6XDzMJg=";
vendorHash = "sha256-opS3f4GDczDRp7mrBzvQtK13Qi4snanX4I64FHTh7Pw=";
};
in
{

1
go.mod
View File

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

2
go.sum
View File

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

112
src/audit.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -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"`

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, Entry> 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
}