diff --git a/conf/config.example.toml b/conf/config.example.toml index fa74d6f..3a716a8 100644 --- a/conf/config.example.toml +++ b/conf/config.example.toml @@ -57,6 +57,7 @@ allowed-custom-headers = ["X-Clacks-Overhead"] [audit] node-id = 0 collect = false +include-ip = false notify-url = "" [observability] diff --git a/src/audit.go b/src/audit.go index 413e0ca..63a8ffe 100644 --- a/src/audit.go +++ b/src/audit.go @@ -28,6 +28,22 @@ var ( }) ) +type principalKey struct{} + +var PrincipalKey = principalKey{} + +func WithPrincipal(ctx context.Context) context.Context { + principal := &Principal{} + return context.WithValue(ctx, PrincipalKey, principal) +} + +func GetPrincipal(ctx context.Context) *Principal { + if principal, ok := ctx.Value(PrincipalKey).(*Principal); ok { + return principal + } + return nil +} + type AuditID int64 func GenerateAuditID() AuditID { @@ -95,6 +111,7 @@ func (audited *auditedBackend) appendNewAuditRecord(ctx context.Context, record id := GenerateAuditID() record.Id = proto.Int64(int64(id)) record.Timestamp = timestamppb.Now() + record.Principal = GetPrincipal(ctx) err = audited.Backend.AppendAuditLog(ctx, id, record) if err != nil { diff --git a/src/config.go b/src/config.go index 8b30ff1..e2cd683 100644 --- a/src/config.go +++ b/src/config.go @@ -152,6 +152,8 @@ type AuditConfig struct { NodeID int `toml:"node-id"` // Whether audit reports should be stored whenever an audit event occurs. Collect bool `toml:"collect"` + // Whether audit reports should include principal's IP address. + IncludeIPs bool `toml:"include-ip"` // Endpoint to notify with a `GET /?` whenever an audit event occurs. NotifyURL *URL `toml:"notify-url"` } diff --git a/src/pages.go b/src/pages.go index 92e9bf3..216f322 100644 --- a/src/pages.go +++ b/src/pages.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "maps" + "net" "net/http" "net/url" "os" @@ -22,6 +23,7 @@ import ( "github.com/pquerna/cachecontrol/cacheobject" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "google.golang.org/protobuf/proto" ) const notFoundPage = "404.html" @@ -783,6 +785,12 @@ func postPage(w http.ResponseWriter, r *http.Request) error { } func ServePages(w http.ResponseWriter, r *http.Request) { + r = r.WithContext(WithPrincipal(r.Context())) + if config.Audit.IncludeIPs { + if ipAddress, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + GetPrincipal(r.Context()).IpAddress = proto.String(ipAddress) + } + } // We want upstream health checks to be done as closely to the normal flow as possible; // any intentional deviation is an opportunity to miss an issue that will affect our // visitors but not our health checks. diff --git a/src/schema.pb.go b/src/schema.pb.go index 093d1e9..825d85f 100644 --- a/src/schema.pb.go +++ b/src/schema.pb.go @@ -657,6 +657,7 @@ type AuditRecord struct { Id *int64 `protobuf:"varint,1,opt,name=id" json:"id,omitempty"` Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp" json:"timestamp,omitempty"` Event *AuditEvent `protobuf:"varint,3,opt,name=event,enum=AuditEvent" json:"event,omitempty"` + Principal *Principal `protobuf:"bytes,4,opt,name=principal" json:"principal,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 @@ -717,6 +718,13 @@ func (x *AuditRecord) GetEvent() AuditEvent { return AuditEvent_InvalidEvent } +func (x *AuditRecord) GetPrincipal() *Principal { + if x != nil { + return x.Principal + } + return nil +} + func (x *AuditRecord) GetDomain() string { if x != nil && x.Domain != nil { return *x.Domain @@ -738,6 +746,50 @@ func (x *AuditRecord) GetManifest() *Manifest { return nil } +type Principal struct { + state protoimpl.MessageState `protogen:"open.v1"` + IpAddress *string `protobuf:"bytes,1,opt,name=ip_address,json=ipAddress" json:"ip_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Principal) Reset() { + *x = Principal{} + mi := &file_schema_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Principal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Principal) ProtoMessage() {} + +func (x *Principal) ProtoReflect() protoreflect.Message { + mi := &file_schema_proto_msgTypes[7] + 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 Principal.ProtoReflect.Descriptor instead. +func (*Principal) Descriptor() ([]byte, []int) { + return file_schema_proto_rawDescGZIP(), []int{7} +} + +func (x *Principal) GetIpAddress() string { + if x != nil && x.IpAddress != nil { + return *x.IpAddress + } + return "" +} + var File_schema_proto protoreflect.FileDescriptor const file_schema_proto_rawDesc = "" + @@ -783,15 +835,20 @@ 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\"\xd3\x01\n" + + "\x05value\x18\x02 \x01(\v2\x06.EntryR\x05value:\x028\x01\"\xfd\x01\n" + "\vAuditRecord\x12\x0e\n" + "\x02id\x18\x01 \x01(\x03R\x02id\x128\n" + "\ttimestamp\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12!\n" + - "\x05event\x18\x03 \x01(\x0e2\v.AuditEventR\x05event\x12\x16\n" + + "\x05event\x18\x03 \x01(\x0e2\v.AuditEventR\x05event\x12(\n" + + "\tprincipal\x18\x04 \x01(\v2\n" + + ".PrincipalR\tprincipal\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" + + "\bmanifest\x18\f \x01(\v2\t.ManifestR\bmanifest\"*\n" + + "\tPrincipal\x12\x1d\n" + + "\n" + + "ip_address\x18\x01 \x01(\tR\tipAddress*V\n" + "\x04Type\x12\x10\n" + "\fInvalidEntry\x10\x00\x12\r\n" + "\tDirectory\x10\x01\x12\x0e\n" + @@ -823,7 +880,7 @@ func file_schema_proto_rawDescGZIP() []byte { } var file_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_schema_proto_goTypes = []any{ (Type)(0), // 0: Type (Transform)(0), // 1: Transform @@ -835,26 +892,28 @@ var file_schema_proto_goTypes = []any{ (*Problem)(nil), // 7: Problem (*Manifest)(nil), // 8: Manifest (*AuditRecord)(nil), // 9: AuditRecord - nil, // 10: Manifest.ContentsEntry - (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*Principal)(nil), // 10: Principal + nil, // 11: Manifest.ContentsEntry + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp } var file_schema_proto_depIdxs = []int32{ 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 + 11, // 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 - 11, // 7: AuditRecord.timestamp:type_name -> google.protobuf.Timestamp + 12, // 7: AuditRecord.timestamp:type_name -> google.protobuf.Timestamp 2, // 8: AuditRecord.event:type_name -> AuditEvent - 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 + 10, // 9: AuditRecord.principal:type_name -> Principal + 8, // 10: AuditRecord.manifest:type_name -> Manifest + 3, // 11: Manifest.ContentsEntry.value:type_name -> Entry + 12, // [12:12] is the sub-list for method output_type + 12, // [12:12] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_schema_proto_init() } @@ -868,7 +927,7 @@ func file_schema_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_schema_proto_rawDesc), len(file_schema_proto_rawDesc)), NumEnums: 3, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/src/schema.proto b/src/schema.proto index 18a9bb7..730ec66 100644 --- a/src/schema.proto +++ b/src/schema.proto @@ -119,6 +119,7 @@ message AuditRecord { int64 id = 1; google.protobuf.Timestamp timestamp = 2; AuditEvent event = 3; + Principal principal = 4; // Affected resource. string domain = 10; @@ -127,3 +128,7 @@ message AuditRecord { // Snapshot of site manifest. Manifest manifest = 12; // only for `*Manifest` events } + +message Principal { + string ip_address = 1; +}