From 7a2adfb047d8aa5e59e48f43f6d89733591ee735 Mon Sep 17 00:00:00 2001 From: Kamal Chaturvedi Date: Tue, 27 Jan 2026 12:36:40 -0800 Subject: [PATCH 1/6] Added a proto schema to represent SecurityViolationEvent, replacing basic struct. This has been done to allow for management-plane can reference it as a contract with backward/forward compatibility --- api/grpc/events/v1/security_violation.pb.go | 954 ++++++++++++++++++ .../v1/security_violation.pb.validate.go | 620 ++++++++++++ api/grpc/events/v1/security_violation.proto | 147 +++ docs/proto/protos.md | 191 ++++ .../securityviolationsprocessor/model.go | 70 -- 5 files changed, 1912 insertions(+), 70 deletions(-) create mode 100644 api/grpc/events/v1/security_violation.pb.go create mode 100644 api/grpc/events/v1/security_violation.pb.validate.go create mode 100644 api/grpc/events/v1/security_violation.proto delete mode 100644 internal/collector/securityviolationsprocessor/model.go diff --git a/api/grpc/events/v1/security_violation.pb.go b/api/grpc/events/v1/security_violation.pb.go new file mode 100644 index 0000000000..e81fe8be8c --- /dev/null +++ b/api/grpc/events/v1/security_violation.pb.go @@ -0,0 +1,954 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license +// found in the LICENSE file in the root directory of this source tree. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: events/v1/security_violation.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RequestStatus int32 + +const ( + RequestStatus_REQUEST_STATUS_UNKNOWN RequestStatus = 0 + RequestStatus_REQUEST_STATUS_BLOCKED RequestStatus = 1 + RequestStatus_REQUEST_STATUS_ALERTED RequestStatus = 2 + RequestStatus_REQUEST_STATUS_PASSED RequestStatus = 3 +) + +// Enum value maps for RequestStatus. +var ( + RequestStatus_name = map[int32]string{ + 0: "REQUEST_STATUS_UNKNOWN", + 1: "REQUEST_STATUS_BLOCKED", + 2: "REQUEST_STATUS_ALERTED", + 3: "REQUEST_STATUS_PASSED", + } + RequestStatus_value = map[string]int32{ + "REQUEST_STATUS_UNKNOWN": 0, + "REQUEST_STATUS_BLOCKED": 1, + "REQUEST_STATUS_ALERTED": 2, + "REQUEST_STATUS_PASSED": 3, + } +) + +func (x RequestStatus) Enum() *RequestStatus { + p := new(RequestStatus) + *p = x + return p +} + +func (x RequestStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RequestStatus) Descriptor() protoreflect.EnumDescriptor { + return file_events_v1_security_violation_proto_enumTypes[0].Descriptor() +} + +func (RequestStatus) Type() protoreflect.EnumType { + return &file_events_v1_security_violation_proto_enumTypes[0] +} + +func (x RequestStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RequestStatus.Descriptor instead. +func (RequestStatus) EnumDescriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{0} +} + +type RequestOutcome int32 + +const ( + RequestOutcome_REQUEST_OUTCOME_UNKNOWN RequestOutcome = 0 + RequestOutcome_REQUEST_OUTCOME_PASSED RequestOutcome = 1 + RequestOutcome_REQUEST_OUTCOME_REJECTED RequestOutcome = 2 +) + +// Enum value maps for RequestOutcome. +var ( + RequestOutcome_name = map[int32]string{ + 0: "REQUEST_OUTCOME_UNKNOWN", + 1: "REQUEST_OUTCOME_PASSED", + 2: "REQUEST_OUTCOME_REJECTED", + } + RequestOutcome_value = map[string]int32{ + "REQUEST_OUTCOME_UNKNOWN": 0, + "REQUEST_OUTCOME_PASSED": 1, + "REQUEST_OUTCOME_REJECTED": 2, + } +) + +func (x RequestOutcome) Enum() *RequestOutcome { + p := new(RequestOutcome) + *p = x + return p +} + +func (x RequestOutcome) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RequestOutcome) Descriptor() protoreflect.EnumDescriptor { + return file_events_v1_security_violation_proto_enumTypes[1].Descriptor() +} + +func (RequestOutcome) Type() protoreflect.EnumType { + return &file_events_v1_security_violation_proto_enumTypes[1] +} + +func (x RequestOutcome) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RequestOutcome.Descriptor instead. +func (RequestOutcome) EnumDescriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{1} +} + +type RequestOutcomeReason int32 + +const ( + RequestOutcomeReason_SECURITY_WAF_UNKNOWN RequestOutcomeReason = 0 + RequestOutcomeReason_SECURITY_WAF_OK RequestOutcomeReason = 1 + RequestOutcomeReason_SECURITY_WAF_VIOLATION RequestOutcomeReason = 2 + RequestOutcomeReason_SECURITY_WAF_FLAGGED RequestOutcomeReason = 3 + RequestOutcomeReason_SECURITY_WAF_VIOLATION_TRANSPARENT RequestOutcomeReason = 4 +) + +// Enum value maps for RequestOutcomeReason. +var ( + RequestOutcomeReason_name = map[int32]string{ + 0: "SECURITY_WAF_UNKNOWN", + 1: "SECURITY_WAF_OK", + 2: "SECURITY_WAF_VIOLATION", + 3: "SECURITY_WAF_FLAGGED", + 4: "SECURITY_WAF_VIOLATION_TRANSPARENT", + } + RequestOutcomeReason_value = map[string]int32{ + "SECURITY_WAF_UNKNOWN": 0, + "SECURITY_WAF_OK": 1, + "SECURITY_WAF_VIOLATION": 2, + "SECURITY_WAF_FLAGGED": 3, + "SECURITY_WAF_VIOLATION_TRANSPARENT": 4, + } +) + +func (x RequestOutcomeReason) Enum() *RequestOutcomeReason { + p := new(RequestOutcomeReason) + *p = x + return p +} + +func (x RequestOutcomeReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RequestOutcomeReason) Descriptor() protoreflect.EnumDescriptor { + return file_events_v1_security_violation_proto_enumTypes[2].Descriptor() +} + +func (RequestOutcomeReason) Type() protoreflect.EnumType { + return &file_events_v1_security_violation_proto_enumTypes[2] +} + +func (x RequestOutcomeReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RequestOutcomeReason.Descriptor instead. +func (RequestOutcomeReason) EnumDescriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{2} +} + +type Severity int32 + +const ( + Severity_SEVERITY_UNKNOWN Severity = 0 + Severity_SEVERITY_INFORMATIONAL Severity = 1 + Severity_SEVERITY_LOW Severity = 2 + Severity_SEVERITY_MEDIUM Severity = 3 + Severity_SEVERITY_HIGH Severity = 4 + Severity_SEVERITY_CRITICAL Severity = 5 +) + +// Enum value maps for Severity. +var ( + Severity_name = map[int32]string{ + 0: "SEVERITY_UNKNOWN", + 1: "SEVERITY_INFORMATIONAL", + 2: "SEVERITY_LOW", + 3: "SEVERITY_MEDIUM", + 4: "SEVERITY_HIGH", + 5: "SEVERITY_CRITICAL", + } + Severity_value = map[string]int32{ + "SEVERITY_UNKNOWN": 0, + "SEVERITY_INFORMATIONAL": 1, + "SEVERITY_LOW": 2, + "SEVERITY_MEDIUM": 3, + "SEVERITY_HIGH": 4, + "SEVERITY_CRITICAL": 5, + } +) + +func (x Severity) Enum() *Severity { + p := new(Severity) + *p = x + return p +} + +func (x Severity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Severity) Descriptor() protoreflect.EnumDescriptor { + return file_events_v1_security_violation_proto_enumTypes[3].Descriptor() +} + +func (Severity) Type() protoreflect.EnumType { + return &file_events_v1_security_violation_proto_enumTypes[3] +} + +func (x Severity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Severity.Descriptor instead. +func (Severity) EnumDescriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{3} +} + +// SecurityViolationEvent represents the structured NGINX App Protect +// security violation data +type SecurityViolationEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the security policy + PolicyName string `protobuf:"bytes,1,opt,name=policy_name,json=policyName,proto3" json:"policy_name,omitempty"` + // Unique support ID for the violation + SupportId string `protobuf:"bytes,2,opt,name=support_id,json=supportId,proto3" json:"support_id,omitempty"` + // Outcome of the request (e.g., REJECTED, PASSED) + RequestOutcome RequestOutcome `protobuf:"varint,3,opt,name=request_outcome,json=requestOutcome,proto3,enum=events.v1.RequestOutcome" json:"request_outcome,omitempty"` + // Reason for the outcome + RequestOutcomeReason RequestOutcomeReason `protobuf:"varint,4,opt,name=request_outcome_reason,json=requestOutcomeReason,proto3,enum=events.v1.RequestOutcomeReason" json:"request_outcome_reason,omitempty"` + // Reason for blocking exception if applicable + BlockingExceptionReason string `protobuf:"bytes,5,opt,name=blocking_exception_reason,json=blockingExceptionReason,proto3" json:"blocking_exception_reason,omitempty"` + // HTTP method used + Method string `protobuf:"bytes,6,opt,name=method,proto3" json:"method,omitempty"` + // Protocol used (e.g., HTTP/1.1) + Protocol string `protobuf:"bytes,7,opt,name=protocol,proto3" json:"protocol,omitempty"` + // X-Forwarded-For header value + XffHeaderValue string `protobuf:"bytes,8,opt,name=xff_header_value,json=xffHeaderValue,proto3" json:"xff_header_value,omitempty"` + // Request URI + Uri string `protobuf:"bytes,9,opt,name=uri,proto3" json:"uri,omitempty"` + // Full request + Request string `protobuf:"bytes,10,opt,name=request,proto3" json:"request,omitempty"` + // Indicates if the request was truncated + IsTruncated bool `protobuf:"varint,11,opt,name=is_truncated,json=isTruncated,proto3" json:"is_truncated,omitempty"` + // Status of the request + RequestStatus RequestStatus `protobuf:"varint,12,opt,name=request_status,json=requestStatus,proto3,enum=events.v1.RequestStatus" json:"request_status,omitempty"` + // HTTP response code + ResponseCode uint32 `protobuf:"varint,13,opt,name=response_code,json=responseCode,proto3" json:"response_code,omitempty"` + // Server address + ServerAddr string `protobuf:"bytes,14,opt,name=server_addr,json=serverAddr,proto3" json:"server_addr,omitempty"` + // Virtual server name + VsName string `protobuf:"bytes,15,opt,name=vs_name,json=vsName,proto3" json:"vs_name,omitempty"` + // Remote address of the client + RemoteAddr string `protobuf:"bytes,16,opt,name=remote_addr,json=remoteAddr,proto3" json:"remote_addr,omitempty"` + // Destination port + DestinationPort uint32 `protobuf:"varint,17,opt,name=destination_port,json=destinationPort,proto3" json:"destination_port,omitempty"` + // Server port + ServerPort uint32 `protobuf:"varint,18,opt,name=server_port,json=serverPort,proto3" json:"server_port,omitempty"` + // List of violations + Violations string `protobuf:"bytes,19,opt,name=violations,proto3" json:"violations,omitempty"` + // List of sub-violations + SubViolations string `protobuf:"bytes,20,opt,name=sub_violations,json=subViolations,proto3" json:"sub_violations,omitempty"` + // Violation rating + ViolationRating uint32 `protobuf:"varint,21,opt,name=violation_rating,json=violationRating,proto3" json:"violation_rating,omitempty"` + // Signature set names + SigSetNames string `protobuf:"bytes,22,opt,name=sig_set_names,json=sigSetNames,proto3" json:"sig_set_names,omitempty"` + // Signature CVEs + SigCves string `protobuf:"bytes,23,opt,name=sig_cves,json=sigCves,proto3" json:"sig_cves,omitempty"` + // Client class + ClientClass string `protobuf:"bytes,24,opt,name=client_class,json=clientClass,proto3" json:"client_class,omitempty"` + // Client application + ClientApplication string `protobuf:"bytes,25,opt,name=client_application,json=clientApplication,proto3" json:"client_application,omitempty"` + // Client application version + ClientApplicationVersion string `protobuf:"bytes,26,opt,name=client_application_version,json=clientApplicationVersion,proto3" json:"client_application_version,omitempty"` + // Severity of the violation + Severity Severity `protobuf:"varint,27,opt,name=severity,proto3,enum=events.v1.Severity" json:"severity,omitempty"` + // Threat campaign names + ThreatCampaignNames string `protobuf:"bytes,28,opt,name=threat_campaign_names,json=threatCampaignNames,proto3" json:"threat_campaign_names,omitempty"` + // Bot anomalies detected + BotAnomalies string `protobuf:"bytes,29,opt,name=bot_anomalies,json=botAnomalies,proto3" json:"bot_anomalies,omitempty"` + // Bot category + BotCategory string `protobuf:"bytes,30,opt,name=bot_category,json=botCategory,proto3" json:"bot_category,omitempty"` + // Enforced bot anomalies + EnforcedBotAnomalies string `protobuf:"bytes,31,opt,name=enforced_bot_anomalies,json=enforcedBotAnomalies,proto3" json:"enforced_bot_anomalies,omitempty"` + // Bot signature name + BotSignatureName string `protobuf:"bytes,32,opt,name=bot_signature_name,json=botSignatureName,proto3" json:"bot_signature_name,omitempty"` + // System ID + SystemId string `protobuf:"bytes,33,opt,name=system_id,json=systemId,proto3" json:"system_id,omitempty"` + // Display name + DisplayName string `protobuf:"bytes,37,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // Detailed violation data + ViolationsData []*ViolationData `protobuf:"bytes,38,rep,name=violations_data,json=violationsData,proto3" json:"violations_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SecurityViolationEvent) Reset() { + *x = SecurityViolationEvent{} + mi := &file_events_v1_security_violation_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SecurityViolationEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecurityViolationEvent) ProtoMessage() {} + +func (x *SecurityViolationEvent) ProtoReflect() protoreflect.Message { + mi := &file_events_v1_security_violation_proto_msgTypes[0] + 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 SecurityViolationEvent.ProtoReflect.Descriptor instead. +func (*SecurityViolationEvent) Descriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{0} +} + +func (x *SecurityViolationEvent) GetPolicyName() string { + if x != nil { + return x.PolicyName + } + return "" +} + +func (x *SecurityViolationEvent) GetSupportId() string { + if x != nil { + return x.SupportId + } + return "" +} + +func (x *SecurityViolationEvent) GetRequestOutcome() RequestOutcome { + if x != nil { + return x.RequestOutcome + } + return RequestOutcome_REQUEST_OUTCOME_UNKNOWN +} + +func (x *SecurityViolationEvent) GetRequestOutcomeReason() RequestOutcomeReason { + if x != nil { + return x.RequestOutcomeReason + } + return RequestOutcomeReason_SECURITY_WAF_UNKNOWN +} + +func (x *SecurityViolationEvent) GetBlockingExceptionReason() string { + if x != nil { + return x.BlockingExceptionReason + } + return "" +} + +func (x *SecurityViolationEvent) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *SecurityViolationEvent) GetProtocol() string { + if x != nil { + return x.Protocol + } + return "" +} + +func (x *SecurityViolationEvent) GetXffHeaderValue() string { + if x != nil { + return x.XffHeaderValue + } + return "" +} + +func (x *SecurityViolationEvent) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *SecurityViolationEvent) GetRequest() string { + if x != nil { + return x.Request + } + return "" +} + +func (x *SecurityViolationEvent) GetIsTruncated() bool { + if x != nil { + return x.IsTruncated + } + return false +} + +func (x *SecurityViolationEvent) GetRequestStatus() RequestStatus { + if x != nil { + return x.RequestStatus + } + return RequestStatus_REQUEST_STATUS_UNKNOWN +} + +func (x *SecurityViolationEvent) GetResponseCode() uint32 { + if x != nil { + return x.ResponseCode + } + return 0 +} + +func (x *SecurityViolationEvent) GetServerAddr() string { + if x != nil { + return x.ServerAddr + } + return "" +} + +func (x *SecurityViolationEvent) GetVsName() string { + if x != nil { + return x.VsName + } + return "" +} + +func (x *SecurityViolationEvent) GetRemoteAddr() string { + if x != nil { + return x.RemoteAddr + } + return "" +} + +func (x *SecurityViolationEvent) GetDestinationPort() uint32 { + if x != nil { + return x.DestinationPort + } + return 0 +} + +func (x *SecurityViolationEvent) GetServerPort() uint32 { + if x != nil { + return x.ServerPort + } + return 0 +} + +func (x *SecurityViolationEvent) GetViolations() string { + if x != nil { + return x.Violations + } + return "" +} + +func (x *SecurityViolationEvent) GetSubViolations() string { + if x != nil { + return x.SubViolations + } + return "" +} + +func (x *SecurityViolationEvent) GetViolationRating() uint32 { + if x != nil { + return x.ViolationRating + } + return 0 +} + +func (x *SecurityViolationEvent) GetSigSetNames() string { + if x != nil { + return x.SigSetNames + } + return "" +} + +func (x *SecurityViolationEvent) GetSigCves() string { + if x != nil { + return x.SigCves + } + return "" +} + +func (x *SecurityViolationEvent) GetClientClass() string { + if x != nil { + return x.ClientClass + } + return "" +} + +func (x *SecurityViolationEvent) GetClientApplication() string { + if x != nil { + return x.ClientApplication + } + return "" +} + +func (x *SecurityViolationEvent) GetClientApplicationVersion() string { + if x != nil { + return x.ClientApplicationVersion + } + return "" +} + +func (x *SecurityViolationEvent) GetSeverity() Severity { + if x != nil { + return x.Severity + } + return Severity_SEVERITY_UNKNOWN +} + +func (x *SecurityViolationEvent) GetThreatCampaignNames() string { + if x != nil { + return x.ThreatCampaignNames + } + return "" +} + +func (x *SecurityViolationEvent) GetBotAnomalies() string { + if x != nil { + return x.BotAnomalies + } + return "" +} + +func (x *SecurityViolationEvent) GetBotCategory() string { + if x != nil { + return x.BotCategory + } + return "" +} + +func (x *SecurityViolationEvent) GetEnforcedBotAnomalies() string { + if x != nil { + return x.EnforcedBotAnomalies + } + return "" +} + +func (x *SecurityViolationEvent) GetBotSignatureName() string { + if x != nil { + return x.BotSignatureName + } + return "" +} + +func (x *SecurityViolationEvent) GetSystemId() string { + if x != nil { + return x.SystemId + } + return "" +} + +func (x *SecurityViolationEvent) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *SecurityViolationEvent) GetViolationsData() []*ViolationData { + if x != nil { + return x.ViolationsData + } + return nil +} + +// ViolationData represents individual violation details +type ViolationData struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the violation + ViolationDataName string `protobuf:"bytes,1,opt,name=violation_data_name,json=violationDataName,proto3" json:"violation_data_name,omitempty"` + // Context of the violation + ViolationDataContext string `protobuf:"bytes,2,opt,name=violation_data_context,json=violationDataContext,proto3" json:"violation_data_context,omitempty"` + // Context data associated with the violation + ViolationDataContextData *ContextData `protobuf:"bytes,3,opt,name=violation_data_context_data,json=violationDataContextData,proto3" json:"violation_data_context_data,omitempty"` + // Signature data for the violation + ViolationDataSignatures []*SignatureData `protobuf:"bytes,4,rep,name=violation_data_signatures,json=violationDataSignatures,proto3" json:"violation_data_signatures,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ViolationData) Reset() { + *x = ViolationData{} + mi := &file_events_v1_security_violation_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ViolationData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ViolationData) ProtoMessage() {} + +func (x *ViolationData) ProtoReflect() protoreflect.Message { + mi := &file_events_v1_security_violation_proto_msgTypes[1] + 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 ViolationData.ProtoReflect.Descriptor instead. +func (*ViolationData) Descriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{1} +} + +func (x *ViolationData) GetViolationDataName() string { + if x != nil { + return x.ViolationDataName + } + return "" +} + +func (x *ViolationData) GetViolationDataContext() string { + if x != nil { + return x.ViolationDataContext + } + return "" +} + +func (x *ViolationData) GetViolationDataContextData() *ContextData { + if x != nil { + return x.ViolationDataContextData + } + return nil +} + +func (x *ViolationData) GetViolationDataSignatures() []*SignatureData { + if x != nil { + return x.ViolationDataSignatures + } + return nil +} + +// SignatureData represents signature data contained within each violation +type SignatureData struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Signature ID + SigDataId uint32 `protobuf:"varint,1,opt,name=sig_data_id,json=sigDataId,proto3" json:"sig_data_id,omitempty"` + // Blocking mask + SigDataBlockingMask string `protobuf:"bytes,2,opt,name=sig_data_blocking_mask,json=sigDataBlockingMask,proto3" json:"sig_data_blocking_mask,omitempty"` + // Buffer information + SigDataBuffer string `protobuf:"bytes,3,opt,name=sig_data_buffer,json=sigDataBuffer,proto3" json:"sig_data_buffer,omitempty"` + // Offset in the buffer + SigDataOffset uint32 `protobuf:"varint,4,opt,name=sig_data_offset,json=sigDataOffset,proto3" json:"sig_data_offset,omitempty"` + // Length of the signature match + SigDataLength uint32 `protobuf:"varint,5,opt,name=sig_data_length,json=sigDataLength,proto3" json:"sig_data_length,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SignatureData) Reset() { + *x = SignatureData{} + mi := &file_events_v1_security_violation_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SignatureData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignatureData) ProtoMessage() {} + +func (x *SignatureData) ProtoReflect() protoreflect.Message { + mi := &file_events_v1_security_violation_proto_msgTypes[2] + 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 SignatureData.ProtoReflect.Descriptor instead. +func (*SignatureData) Descriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{2} +} + +func (x *SignatureData) GetSigDataId() uint32 { + if x != nil { + return x.SigDataId + } + return 0 +} + +func (x *SignatureData) GetSigDataBlockingMask() string { + if x != nil { + return x.SigDataBlockingMask + } + return "" +} + +func (x *SignatureData) GetSigDataBuffer() string { + if x != nil { + return x.SigDataBuffer + } + return "" +} + +func (x *SignatureData) GetSigDataOffset() uint32 { + if x != nil { + return x.SigDataOffset + } + return 0 +} + +func (x *SignatureData) GetSigDataLength() uint32 { + if x != nil { + return x.SigDataLength + } + return 0 +} + +// ContextData represents the context data of the violation +type ContextData struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the context + ContextDataName string `protobuf:"bytes,1,opt,name=context_data_name,json=contextDataName,proto3" json:"context_data_name,omitempty"` + // Value of the context + ContextDataValue string `protobuf:"bytes,2,opt,name=context_data_value,json=contextDataValue,proto3" json:"context_data_value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ContextData) Reset() { + *x = ContextData{} + mi := &file_events_v1_security_violation_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ContextData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ContextData) ProtoMessage() {} + +func (x *ContextData) ProtoReflect() protoreflect.Message { + mi := &file_events_v1_security_violation_proto_msgTypes[3] + 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 ContextData.ProtoReflect.Descriptor instead. +func (*ContextData) Descriptor() ([]byte, []int) { + return file_events_v1_security_violation_proto_rawDescGZIP(), []int{3} +} + +func (x *ContextData) GetContextDataName() string { + if x != nil { + return x.ContextDataName + } + return "" +} + +func (x *ContextData) GetContextDataValue() string { + if x != nil { + return x.ContextDataValue + } + return "" +} + +var File_events_v1_security_violation_proto protoreflect.FileDescriptor + +const file_events_v1_security_violation_proto_rawDesc = "" + + "\n" + + "\"events/v1/security_violation.proto\x12\tevents.v1\"\xbe\v\n" + + "\x16SecurityViolationEvent\x12\x1f\n" + + "\vpolicy_name\x18\x01 \x01(\tR\n" + + "policyName\x12\x1d\n" + + "\n" + + "support_id\x18\x02 \x01(\tR\tsupportId\x12B\n" + + "\x0frequest_outcome\x18\x03 \x01(\x0e2\x19.events.v1.RequestOutcomeR\x0erequestOutcome\x12U\n" + + "\x16request_outcome_reason\x18\x04 \x01(\x0e2\x1f.events.v1.RequestOutcomeReasonR\x14requestOutcomeReason\x12:\n" + + "\x19blocking_exception_reason\x18\x05 \x01(\tR\x17blockingExceptionReason\x12\x16\n" + + "\x06method\x18\x06 \x01(\tR\x06method\x12\x1a\n" + + "\bprotocol\x18\a \x01(\tR\bprotocol\x12(\n" + + "\x10xff_header_value\x18\b \x01(\tR\x0exffHeaderValue\x12\x10\n" + + "\x03uri\x18\t \x01(\tR\x03uri\x12\x18\n" + + "\arequest\x18\n" + + " \x01(\tR\arequest\x12!\n" + + "\fis_truncated\x18\v \x01(\bR\visTruncated\x12?\n" + + "\x0erequest_status\x18\f \x01(\x0e2\x18.events.v1.RequestStatusR\rrequestStatus\x12#\n" + + "\rresponse_code\x18\r \x01(\rR\fresponseCode\x12\x1f\n" + + "\vserver_addr\x18\x0e \x01(\tR\n" + + "serverAddr\x12\x17\n" + + "\avs_name\x18\x0f \x01(\tR\x06vsName\x12\x1f\n" + + "\vremote_addr\x18\x10 \x01(\tR\n" + + "remoteAddr\x12)\n" + + "\x10destination_port\x18\x11 \x01(\rR\x0fdestinationPort\x12\x1f\n" + + "\vserver_port\x18\x12 \x01(\rR\n" + + "serverPort\x12\x1e\n" + + "\n" + + "violations\x18\x13 \x01(\tR\n" + + "violations\x12%\n" + + "\x0esub_violations\x18\x14 \x01(\tR\rsubViolations\x12)\n" + + "\x10violation_rating\x18\x15 \x01(\rR\x0fviolationRating\x12\"\n" + + "\rsig_set_names\x18\x16 \x01(\tR\vsigSetNames\x12\x19\n" + + "\bsig_cves\x18\x17 \x01(\tR\asigCves\x12!\n" + + "\fclient_class\x18\x18 \x01(\tR\vclientClass\x12-\n" + + "\x12client_application\x18\x19 \x01(\tR\x11clientApplication\x12<\n" + + "\x1aclient_application_version\x18\x1a \x01(\tR\x18clientApplicationVersion\x12/\n" + + "\bseverity\x18\x1b \x01(\x0e2\x13.events.v1.SeverityR\bseverity\x122\n" + + "\x15threat_campaign_names\x18\x1c \x01(\tR\x13threatCampaignNames\x12#\n" + + "\rbot_anomalies\x18\x1d \x01(\tR\fbotAnomalies\x12!\n" + + "\fbot_category\x18\x1e \x01(\tR\vbotCategory\x124\n" + + "\x16enforced_bot_anomalies\x18\x1f \x01(\tR\x14enforcedBotAnomalies\x12,\n" + + "\x12bot_signature_name\x18 \x01(\tR\x10botSignatureName\x12\x1b\n" + + "\tsystem_id\x18! \x01(\tR\bsystemId\x12!\n" + + "\fdisplay_name\x18% \x01(\tR\vdisplayName\x12A\n" + + "\x0fviolations_data\x18& \x03(\v2\x18.events.v1.ViolationDataR\x0eviolationsData\"\xa2\x02\n" + + "\rViolationData\x12.\n" + + "\x13violation_data_name\x18\x01 \x01(\tR\x11violationDataName\x124\n" + + "\x16violation_data_context\x18\x02 \x01(\tR\x14violationDataContext\x12U\n" + + "\x1bviolation_data_context_data\x18\x03 \x01(\v2\x16.events.v1.ContextDataR\x18violationDataContextData\x12T\n" + + "\x19violation_data_signatures\x18\x04 \x03(\v2\x18.events.v1.SignatureDataR\x17violationDataSignatures\"\xdc\x01\n" + + "\rSignatureData\x12\x1e\n" + + "\vsig_data_id\x18\x01 \x01(\rR\tsigDataId\x123\n" + + "\x16sig_data_blocking_mask\x18\x02 \x01(\tR\x13sigDataBlockingMask\x12&\n" + + "\x0fsig_data_buffer\x18\x03 \x01(\tR\rsigDataBuffer\x12&\n" + + "\x0fsig_data_offset\x18\x04 \x01(\rR\rsigDataOffset\x12&\n" + + "\x0fsig_data_length\x18\x05 \x01(\rR\rsigDataLength\"g\n" + + "\vContextData\x12*\n" + + "\x11context_data_name\x18\x01 \x01(\tR\x0fcontextDataName\x12,\n" + + "\x12context_data_value\x18\x02 \x01(\tR\x10contextDataValue*~\n" + + "\rRequestStatus\x12\x1a\n" + + "\x16REQUEST_STATUS_UNKNOWN\x10\x00\x12\x1a\n" + + "\x16REQUEST_STATUS_BLOCKED\x10\x01\x12\x1a\n" + + "\x16REQUEST_STATUS_ALERTED\x10\x02\x12\x19\n" + + "\x15REQUEST_STATUS_PASSED\x10\x03*g\n" + + "\x0eRequestOutcome\x12\x1b\n" + + "\x17REQUEST_OUTCOME_UNKNOWN\x10\x00\x12\x1a\n" + + "\x16REQUEST_OUTCOME_PASSED\x10\x01\x12\x1c\n" + + "\x18REQUEST_OUTCOME_REJECTED\x10\x02*\xa3\x01\n" + + "\x14RequestOutcomeReason\x12\x18\n" + + "\x14SECURITY_WAF_UNKNOWN\x10\x00\x12\x13\n" + + "\x0fSECURITY_WAF_OK\x10\x01\x12\x1a\n" + + "\x16SECURITY_WAF_VIOLATION\x10\x02\x12\x18\n" + + "\x14SECURITY_WAF_FLAGGED\x10\x03\x12&\n" + + "\"SECURITY_WAF_VIOLATION_TRANSPARENT\x10\x04*\x8d\x01\n" + + "\bSeverity\x12\x14\n" + + "\x10SEVERITY_UNKNOWN\x10\x00\x12\x1a\n" + + "\x16SEVERITY_INFORMATIONAL\x10\x01\x12\x10\n" + + "\fSEVERITY_LOW\x10\x02\x12\x13\n" + + "\x0fSEVERITY_MEDIUM\x10\x03\x12\x11\n" + + "\rSEVERITY_HIGH\x10\x04\x12\x15\n" + + "\x11SEVERITY_CRITICAL\x10\x05B\vZ\tevents/v1b\x06proto3" + +var ( + file_events_v1_security_violation_proto_rawDescOnce sync.Once + file_events_v1_security_violation_proto_rawDescData []byte +) + +func file_events_v1_security_violation_proto_rawDescGZIP() []byte { + file_events_v1_security_violation_proto_rawDescOnce.Do(func() { + file_events_v1_security_violation_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_events_v1_security_violation_proto_rawDesc), len(file_events_v1_security_violation_proto_rawDesc))) + }) + return file_events_v1_security_violation_proto_rawDescData +} + +var file_events_v1_security_violation_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_events_v1_security_violation_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_events_v1_security_violation_proto_goTypes = []any{ + (RequestStatus)(0), // 0: events.v1.RequestStatus + (RequestOutcome)(0), // 1: events.v1.RequestOutcome + (RequestOutcomeReason)(0), // 2: events.v1.RequestOutcomeReason + (Severity)(0), // 3: events.v1.Severity + (*SecurityViolationEvent)(nil), // 4: events.v1.SecurityViolationEvent + (*ViolationData)(nil), // 5: events.v1.ViolationData + (*SignatureData)(nil), // 6: events.v1.SignatureData + (*ContextData)(nil), // 7: events.v1.ContextData +} +var file_events_v1_security_violation_proto_depIdxs = []int32{ + 1, // 0: events.v1.SecurityViolationEvent.request_outcome:type_name -> events.v1.RequestOutcome + 2, // 1: events.v1.SecurityViolationEvent.request_outcome_reason:type_name -> events.v1.RequestOutcomeReason + 0, // 2: events.v1.SecurityViolationEvent.request_status:type_name -> events.v1.RequestStatus + 3, // 3: events.v1.SecurityViolationEvent.severity:type_name -> events.v1.Severity + 5, // 4: events.v1.SecurityViolationEvent.violations_data:type_name -> events.v1.ViolationData + 7, // 5: events.v1.ViolationData.violation_data_context_data:type_name -> events.v1.ContextData + 6, // 6: events.v1.ViolationData.violation_data_signatures:type_name -> events.v1.SignatureData + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_events_v1_security_violation_proto_init() } +func file_events_v1_security_violation_proto_init() { + if File_events_v1_security_violation_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_events_v1_security_violation_proto_rawDesc), len(file_events_v1_security_violation_proto_rawDesc)), + NumEnums: 4, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_events_v1_security_violation_proto_goTypes, + DependencyIndexes: file_events_v1_security_violation_proto_depIdxs, + EnumInfos: file_events_v1_security_violation_proto_enumTypes, + MessageInfos: file_events_v1_security_violation_proto_msgTypes, + }.Build() + File_events_v1_security_violation_proto = out.File + file_events_v1_security_violation_proto_goTypes = nil + file_events_v1_security_violation_proto_depIdxs = nil +} diff --git a/api/grpc/events/v1/security_violation.pb.validate.go b/api/grpc/events/v1/security_violation.pb.validate.go new file mode 100644 index 0000000000..09240e7f4b --- /dev/null +++ b/api/grpc/events/v1/security_violation.pb.validate.go @@ -0,0 +1,620 @@ +// Code generated by protoc-gen-validate. DO NOT EDIT. +// source: events/v1/security_violation.proto + +package v1 + +import ( + "bytes" + "errors" + "fmt" + "net" + "net/mail" + "net/url" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "google.golang.org/protobuf/types/known/anypb" +) + +// ensure the imports are used +var ( + _ = bytes.MinRead + _ = errors.New("") + _ = fmt.Print + _ = utf8.UTFMax + _ = (*regexp.Regexp)(nil) + _ = (*strings.Reader)(nil) + _ = net.IPv4len + _ = time.Duration(0) + _ = (*url.URL)(nil) + _ = (*mail.Address)(nil) + _ = anypb.Any{} + _ = sort.Sort +) + +// Validate checks the field values on SecurityViolationEvent with the rules +// defined in the proto definition for this message. If any rules are +// violated, the first error encountered is returned, or nil if there are no violations. +func (m *SecurityViolationEvent) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on SecurityViolationEvent with the rules +// defined in the proto definition for this message. If any rules are +// violated, the result is a list of violation errors wrapped in +// SecurityViolationEventMultiError, or nil if none found. +func (m *SecurityViolationEvent) ValidateAll() error { + return m.validate(true) +} + +func (m *SecurityViolationEvent) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for PolicyName + + // no validation rules for SupportId + + // no validation rules for RequestOutcome + + // no validation rules for RequestOutcomeReason + + // no validation rules for BlockingExceptionReason + + // no validation rules for Method + + // no validation rules for Protocol + + // no validation rules for XffHeaderValue + + // no validation rules for Uri + + // no validation rules for Request + + // no validation rules for IsTruncated + + // no validation rules for RequestStatus + + // no validation rules for ResponseCode + + // no validation rules for ServerAddr + + // no validation rules for VsName + + // no validation rules for RemoteAddr + + // no validation rules for DestinationPort + + // no validation rules for ServerPort + + // no validation rules for Violations + + // no validation rules for SubViolations + + // no validation rules for ViolationRating + + // no validation rules for SigSetNames + + // no validation rules for SigCves + + // no validation rules for ClientClass + + // no validation rules for ClientApplication + + // no validation rules for ClientApplicationVersion + + // no validation rules for Severity + + // no validation rules for ThreatCampaignNames + + // no validation rules for BotAnomalies + + // no validation rules for BotCategory + + // no validation rules for EnforcedBotAnomalies + + // no validation rules for BotSignatureName + + // no validation rules for SystemId + + // no validation rules for DisplayName + + for idx, item := range m.GetViolationsData() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, SecurityViolationEventValidationError{ + field: fmt.Sprintf("ViolationsData[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, SecurityViolationEventValidationError{ + field: fmt.Sprintf("ViolationsData[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return SecurityViolationEventValidationError{ + field: fmt.Sprintf("ViolationsData[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return SecurityViolationEventMultiError(errors) + } + + return nil +} + +// SecurityViolationEventMultiError is an error wrapping multiple validation +// errors returned by SecurityViolationEvent.ValidateAll() if the designated +// constraints aren't met. +type SecurityViolationEventMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m SecurityViolationEventMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m SecurityViolationEventMultiError) AllErrors() []error { return m } + +// SecurityViolationEventValidationError is the validation error returned by +// SecurityViolationEvent.Validate if the designated constraints aren't met. +type SecurityViolationEventValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e SecurityViolationEventValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e SecurityViolationEventValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e SecurityViolationEventValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e SecurityViolationEventValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e SecurityViolationEventValidationError) ErrorName() string { + return "SecurityViolationEventValidationError" +} + +// Error satisfies the builtin error interface +func (e SecurityViolationEventValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sSecurityViolationEvent.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = SecurityViolationEventValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = SecurityViolationEventValidationError{} + +// Validate checks the field values on ViolationData with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *ViolationData) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ViolationData with the rules defined +// in the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in ViolationDataMultiError, or +// nil if none found. +func (m *ViolationData) ValidateAll() error { + return m.validate(true) +} + +func (m *ViolationData) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for ViolationDataName + + // no validation rules for ViolationDataContext + + if all { + switch v := interface{}(m.GetViolationDataContextData()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ViolationDataValidationError{ + field: "ViolationDataContextData", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ViolationDataValidationError{ + field: "ViolationDataContextData", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetViolationDataContextData()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ViolationDataValidationError{ + field: "ViolationDataContextData", + reason: "embedded message failed validation", + cause: err, + } + } + } + + for idx, item := range m.GetViolationDataSignatures() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, ViolationDataValidationError{ + field: fmt.Sprintf("ViolationDataSignatures[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, ViolationDataValidationError{ + field: fmt.Sprintf("ViolationDataSignatures[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return ViolationDataValidationError{ + field: fmt.Sprintf("ViolationDataSignatures[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return ViolationDataMultiError(errors) + } + + return nil +} + +// ViolationDataMultiError is an error wrapping multiple validation errors +// returned by ViolationData.ValidateAll() if the designated constraints +// aren't met. +type ViolationDataMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ViolationDataMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ViolationDataMultiError) AllErrors() []error { return m } + +// ViolationDataValidationError is the validation error returned by +// ViolationData.Validate if the designated constraints aren't met. +type ViolationDataValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ViolationDataValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ViolationDataValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ViolationDataValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ViolationDataValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ViolationDataValidationError) ErrorName() string { return "ViolationDataValidationError" } + +// Error satisfies the builtin error interface +func (e ViolationDataValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sViolationData.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ViolationDataValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ViolationDataValidationError{} + +// Validate checks the field values on SignatureData with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *SignatureData) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on SignatureData with the rules defined +// in the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in SignatureDataMultiError, or +// nil if none found. +func (m *SignatureData) ValidateAll() error { + return m.validate(true) +} + +func (m *SignatureData) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for SigDataId + + // no validation rules for SigDataBlockingMask + + // no validation rules for SigDataBuffer + + // no validation rules for SigDataOffset + + // no validation rules for SigDataLength + + if len(errors) > 0 { + return SignatureDataMultiError(errors) + } + + return nil +} + +// SignatureDataMultiError is an error wrapping multiple validation errors +// returned by SignatureData.ValidateAll() if the designated constraints +// aren't met. +type SignatureDataMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m SignatureDataMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m SignatureDataMultiError) AllErrors() []error { return m } + +// SignatureDataValidationError is the validation error returned by +// SignatureData.Validate if the designated constraints aren't met. +type SignatureDataValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e SignatureDataValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e SignatureDataValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e SignatureDataValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e SignatureDataValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e SignatureDataValidationError) ErrorName() string { return "SignatureDataValidationError" } + +// Error satisfies the builtin error interface +func (e SignatureDataValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sSignatureData.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = SignatureDataValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = SignatureDataValidationError{} + +// Validate checks the field values on ContextData with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *ContextData) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on ContextData with the rules defined in +// the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in ContextDataMultiError, or +// nil if none found. +func (m *ContextData) ValidateAll() error { + return m.validate(true) +} + +func (m *ContextData) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for ContextDataName + + // no validation rules for ContextDataValue + + if len(errors) > 0 { + return ContextDataMultiError(errors) + } + + return nil +} + +// ContextDataMultiError is an error wrapping multiple validation errors +// returned by ContextData.ValidateAll() if the designated constraints aren't met. +type ContextDataMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m ContextDataMultiError) Error() string { + msgs := make([]string, 0, len(m)) + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m ContextDataMultiError) AllErrors() []error { return m } + +// ContextDataValidationError is the validation error returned by +// ContextData.Validate if the designated constraints aren't met. +type ContextDataValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e ContextDataValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e ContextDataValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e ContextDataValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e ContextDataValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e ContextDataValidationError) ErrorName() string { return "ContextDataValidationError" } + +// Error satisfies the builtin error interface +func (e ContextDataValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sContextData.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = ContextDataValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = ContextDataValidationError{} diff --git a/api/grpc/events/v1/security_violation.proto b/api/grpc/events/v1/security_violation.proto new file mode 100644 index 0000000000..e36bad7ab9 --- /dev/null +++ b/api/grpc/events/v1/security_violation.proto @@ -0,0 +1,147 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license +// found in the LICENSE file in the root directory of this source tree. +syntax = "proto3"; +package events.v1; + +option go_package = "events/v1"; + +// SecurityViolationEvent represents the structured NGINX App Protect +// security violation data +message SecurityViolationEvent { + // Name of the security policy + string policy_name = 1; + // Unique support ID for the violation + string support_id = 2; + // Outcome of the request (e.g., REJECTED, PASSED) + RequestOutcome request_outcome = 3; + // Reason for the outcome + RequestOutcomeReason request_outcome_reason = 4; + // Reason for blocking exception if applicable + string blocking_exception_reason = 5; + // HTTP method used + string method = 6; + // Protocol used (e.g., HTTP/1.1) + string protocol = 7; + // X-Forwarded-For header value + string xff_header_value = 8; + // Request URI + string uri = 9; + // Full request + string request = 10; + // Indicates if the request was truncated + bool is_truncated = 11; + // Status of the request + RequestStatus request_status = 12; + // HTTP response code + uint32 response_code = 13; + // Server address + string server_addr = 14; + // Virtual server name + string vs_name = 15; + // Remote address of the client + string remote_addr = 16; + // Destination port + uint32 destination_port = 17; + // Server port + uint32 server_port = 18; + // List of violations + string violations = 19; + // List of sub-violations + string sub_violations = 20; + // Violation rating + uint32 violation_rating = 21; + // Signature set names + string sig_set_names = 22; + // Signature CVEs + string sig_cves = 23; + // Client class + string client_class = 24; + // Client application + string client_application = 25; + // Client application version + string client_application_version = 26; + // Severity of the violation + Severity severity = 27; + // Threat campaign names + string threat_campaign_names = 28; + // Bot anomalies detected + string bot_anomalies = 29; + // Bot category + string bot_category = 30; + // Enforced bot anomalies + string enforced_bot_anomalies = 31; + // Bot signature name + string bot_signature_name = 32; + // System ID + string system_id = 33; + // Display name + string display_name = 37; + // Detailed violation data + repeated ViolationData violations_data = 38; +} + +// ViolationData represents individual violation details +message ViolationData { + // Name of the violation + string violation_data_name = 1; + // Context of the violation + string violation_data_context = 2; + // Context data associated with the violation + ContextData violation_data_context_data = 3; + // Signature data for the violation + repeated SignatureData violation_data_signatures = 4; +} + +// SignatureData represents signature data contained within each violation +message SignatureData { + // Signature ID + uint32 sig_data_id = 1; + // Blocking mask + string sig_data_blocking_mask = 2; + // Buffer information + string sig_data_buffer = 3; + // Offset in the buffer + uint32 sig_data_offset = 4; + // Length of the signature match + uint32 sig_data_length = 5; +} + +// ContextData represents the context data of the violation +message ContextData { + // Name of the context + string context_data_name = 1; + // Value of the context + string context_data_value = 2; +} + +enum RequestStatus { + REQUEST_STATUS_UNKNOWN = 0; + REQUEST_STATUS_BLOCKED = 1; + REQUEST_STATUS_ALERTED = 2; + REQUEST_STATUS_PASSED = 3; +} + +enum RequestOutcome { + REQUEST_OUTCOME_UNKNOWN = 0; + REQUEST_OUTCOME_PASSED = 1; + REQUEST_OUTCOME_REJECTED = 2; +} + +enum RequestOutcomeReason { + SECURITY_WAF_UNKNOWN = 0; + SECURITY_WAF_OK = 1; + SECURITY_WAF_VIOLATION = 2; + SECURITY_WAF_FLAGGED = 3; + SECURITY_WAF_VIOLATION_TRANSPARENT=4; +} + +enum Severity { + SEVERITY_UNKNOWN = 0; + SEVERITY_INFORMATIONAL = 1; + SEVERITY_LOW = 2; + SEVERITY_MEDIUM = 3; + SEVERITY_HIGH = 4; + SEVERITY_CRITICAL = 5; +} diff --git a/docs/proto/protos.md b/docs/proto/protos.md index 8bf916886e..94361150b9 100644 --- a/docs/proto/protos.md +++ b/docs/proto/protos.md @@ -3,6 +3,17 @@ ## Table of Contents +- [events/v1/security_violation.proto](#events_v1_security_violation-proto) + - [ContextData](#events-v1-ContextData) + - [SecurityViolationEvent](#events-v1-SecurityViolationEvent) + - [SignatureData](#events-v1-SignatureData) + - [ViolationData](#events-v1-ViolationData) + + - [RequestOutcome](#events-v1-RequestOutcome) + - [RequestOutcomeReason](#events-v1-RequestOutcomeReason) + - [RequestStatus](#events-v1-RequestStatus) + - [Severity](#events-v1-Severity) + - [mpi/v1/common.proto](#mpi_v1_common-proto) - [AuthSettings](#mpi-v1-AuthSettings) - [CommandResponse](#mpi-v1-CommandResponse) @@ -96,6 +107,186 @@ + +

Top

+ +## events/v1/security_violation.proto +Copyright (c) F5, Inc. + +This source code is licensed under the Apache License, Version 2.0 license +found in the LICENSE file in the root directory of this source tree. + + + + +### ContextData +ContextData represents the context data of the violation + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| context_data_name | [string](#string) | | Name of the context | +| context_data_value | [string](#string) | | Value of the context | + + + + + + + + +### SecurityViolationEvent +SecurityViolationEvent represents the structured NGINX App Protect +security violation data + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| policy_name | [string](#string) | | Name of the security policy | +| support_id | [string](#string) | | Unique support ID for the violation | +| request_outcome | [RequestOutcome](#events-v1-RequestOutcome) | | Outcome of the request (e.g., REJECTED, PASSED) | +| request_outcome_reason | [RequestOutcomeReason](#events-v1-RequestOutcomeReason) | | Reason for the outcome | +| blocking_exception_reason | [string](#string) | | Reason for blocking exception if applicable | +| method | [string](#string) | | HTTP method used | +| protocol | [string](#string) | | Protocol used (e.g., HTTP/1.1) | +| xff_header_value | [string](#string) | | X-Forwarded-For header value | +| uri | [string](#string) | | Request URI | +| request | [string](#string) | | Full request | +| is_truncated | [bool](#bool) | | Indicates if the request was truncated | +| request_status | [RequestStatus](#events-v1-RequestStatus) | | Status of the request | +| response_code | [uint32](#uint32) | | HTTP response code | +| server_addr | [string](#string) | | Server address | +| vs_name | [string](#string) | | Virtual server name | +| remote_addr | [string](#string) | | Remote address of the client | +| destination_port | [uint32](#uint32) | | Destination port | +| server_port | [uint32](#uint32) | | Server port | +| violations | [string](#string) | | List of violations | +| sub_violations | [string](#string) | | List of sub-violations | +| violation_rating | [uint32](#uint32) | | Violation rating | +| sig_set_names | [string](#string) | | Signature set names | +| sig_cves | [string](#string) | | Signature CVEs | +| client_class | [string](#string) | | Client class | +| client_application | [string](#string) | | Client application | +| client_application_version | [string](#string) | | Client application version | +| severity | [Severity](#events-v1-Severity) | | Severity of the violation | +| threat_campaign_names | [string](#string) | | Threat campaign names | +| bot_anomalies | [string](#string) | | Bot anomalies detected | +| bot_category | [string](#string) | | Bot category | +| enforced_bot_anomalies | [string](#string) | | Enforced bot anomalies | +| bot_signature_name | [string](#string) | | Bot signature name | +| system_id | [string](#string) | | System ID | +| display_name | [string](#string) | | Display name | +| violations_data | [ViolationData](#events-v1-ViolationData) | repeated | Detailed violation data | + + + + + + + + +### SignatureData +SignatureData represents signature data contained within each violation + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| sig_data_id | [uint32](#uint32) | | Signature ID | +| sig_data_blocking_mask | [string](#string) | | Blocking mask | +| sig_data_buffer | [string](#string) | | Buffer information | +| sig_data_offset | [uint32](#uint32) | | Offset in the buffer | +| sig_data_length | [uint32](#uint32) | | Length of the signature match | + + + + + + + + +### ViolationData +ViolationData represents individual violation details + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| violation_data_name | [string](#string) | | Name of the violation | +| violation_data_context | [string](#string) | | Context of the violation | +| violation_data_context_data | [ContextData](#events-v1-ContextData) | | Context data associated with the violation | +| violation_data_signatures | [SignatureData](#events-v1-SignatureData) | repeated | Signature data for the violation | + + + + + + + + + + +### RequestOutcome + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| REQUEST_OUTCOME_UNKNOWN | 0 | | +| REQUEST_OUTCOME_PASSED | 1 | | +| REQUEST_OUTCOME_REJECTED | 2 | | + + + + + +### RequestOutcomeReason + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| SECURITY_WAF_UNKNOWN | 0 | | +| SECURITY_WAF_OK | 1 | | +| SECURITY_WAF_VIOLATION | 2 | | +| SECURITY_WAF_FLAGGED | 3 | | +| SECURITY_WAF_VIOLATION_TRANSPARENT | 4 | | + + + + + +### RequestStatus + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| REQUEST_STATUS_UNKNOWN | 0 | | +| REQUEST_STATUS_BLOCKED | 1 | | +| REQUEST_STATUS_ALERTED | 2 | | +| REQUEST_STATUS_PASSED | 3 | | + + + + + +### Severity + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| SEVERITY_UNKNOWN | 0 | | +| SEVERITY_INFORMATIONAL | 1 | | +| SEVERITY_LOW | 2 | | +| SEVERITY_MEDIUM | 3 | | +| SEVERITY_HIGH | 4 | | +| SEVERITY_CRITICAL | 5 | | + + + + + + + + + +

Top

diff --git a/internal/collector/securityviolationsprocessor/model.go b/internal/collector/securityviolationsprocessor/model.go deleted file mode 100644 index 3ce92c8b8f..0000000000 --- a/internal/collector/securityviolationsprocessor/model.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) F5, Inc. -// -// This source code is licensed under the Apache License, Version 2.0 license found in the -// LICENSE file in the root directory of this source tree. - -package securityviolationsprocessor - -// SecurityViolationEvent represents the structured NGINX App Protect security violation data -type SecurityViolationEvent struct { - PolicyName string `json:"policy_name"` - SupportID string `json:"support_id"` - Outcome string `json:"outcome"` - OutcomeReason string `json:"outcome_reason"` - BlockingExceptionReason string `json:"blocking_exception_reason"` - Method string `json:"method"` - Protocol string `json:"protocol"` - XForwardedForHeaderValue string `json:"xff_header_value"` - URI string `json:"uri"` - Request string `json:"request"` - IsTruncated string `json:"is_truncated"` - RequestStatus string `json:"request_status"` - ResponseCode string `json:"response_code"` - ServerAddr string `json:"server_addr"` - VSName string `json:"vs_name"` - RemoteAddr string `json:"remote_addr"` - RemotePort string `json:"destination_port"` - ServerPort string `json:"server_port"` - Violations string `json:"violations"` - SubViolations string `json:"sub_violations"` - ViolationRating string `json:"violation_rating"` - SigSetNames string `json:"sig_set_names"` - SigCVEs string `json:"sig_cves"` - ClientClass string `json:"client_class"` - ClientApplication string `json:"client_application"` - ClientApplicationVersion string `json:"client_application_version"` - Severity string `json:"severity"` - ThreatCampaignNames string `json:"threat_campaign_names"` - BotAnomalies string `json:"bot_anomalies"` - BotCategory string `json:"bot_category"` - EnforcedBotAnomalies string `json:"enforced_bot_anomalies"` - BotSignatureName string `json:"bot_signature_name"` - SystemID string `json:"system_id"` - InstanceTags string `json:"instance_tags"` - InstanceGroup string `json:"instance_group"` - ParentHostname string `json:"parent_hostname"` - DisplayName string `json:"display_name"` - ViolationsData []ViolationData `json:"violations_data"` -} - -type ViolationData struct { - Name string `json:"violation_data_name"` - Context string `json:"violation_data_context"` - ContextData ContextData `json:"violation_data_context_data"` - Signatures []SignatureData `json:"violation_data_signatures"` -} - -// SignatureData represents signature data contained within each violation -type SignatureData struct { - ID string `json:"sig_data_id"` - BlockingMask string `json:"sig_data_blocking_mask"` - Buffer string `json:"sig_data_buffer"` - Offset string `json:"sig_data_offset"` - Length string `json:"sig_data_length"` -} - -// ContextData represents the context data of the violation -type ContextData struct { - Name string `json:"context_data_name"` - Value string `json:"context_data_value"` -} From b8f1bdd6e6459a2234786f53ed017e2d2c9b29ba Mon Sep 17 00:00:00 2001 From: Kamal Chaturvedi Date: Tue, 27 Jan 2026 12:38:06 -0800 Subject: [PATCH 2/6] Added XML parsing to extract deeper context from violations, bringing the logic in line with agent v2 --- .../securityviolationsprocessor/README.md | 52 +- .../securityviolationsprocessor/csv_parser.go | 220 ++++++++ .../securityviolationsprocessor/processor.go | 290 +---------- .../processor_bench_test.go | 8 +- .../processor_test.go | 116 ++--- .../violations_parser.go | 483 ++++++++++++++++++ .../xml_structs.go | 134 +++++ 7 files changed, 961 insertions(+), 342 deletions(-) create mode 100644 internal/collector/securityviolationsprocessor/csv_parser.go create mode 100644 internal/collector/securityviolationsprocessor/violations_parser.go create mode 100644 internal/collector/securityviolationsprocessor/xml_structs.go diff --git a/internal/collector/securityviolationsprocessor/README.md b/internal/collector/securityviolationsprocessor/README.md index 77a4d05801..8be5d67d33 100644 --- a/internal/collector/securityviolationsprocessor/README.md +++ b/internal/collector/securityviolationsprocessor/README.md @@ -1,5 +1,51 @@ -# SecurityViolations Processor +# Security Violations Processor -Internal component of the NGINX Agent that processes security violation syslog messages. Parses RFC3164 formatted syslog entries from log records and extracts structured attributes. Successfully parsed messages have their body replaced with the clean message content. +OpenTelemetry Collector processor that transforms NGINX App Protect security violation syslog messages into structured protobuf events. -Part of the NGINX Agent's log collection pipeline. \ No newline at end of file +## What It Does + +Processes NGINX App Protect WAF syslog messages and transforms them into `SecurityViolationEvent` protobuf messages: + +1. Parses RFC3164 syslog messages (best-effort mode) +2. Extracts CSV formatted data from NAP `secops_dashboard` log profile +3. Parses XML violation details with context extraction (parameter, header, cookie, uri, request) +4. Extracts attack signature details +5. Outputs structured protobuf events for downstream consumption + +## Implementation + +| File | Purpose | +|------|---------| +| [`processor.go`](processor.go) | Main processor implementation, RFC3164 parsing, orchestration | +| [`csv_parser.go`](csv_parser.go) | CSV parsing and field mapping | +| [`violations_parser.go`](violations_parser.go) | XML parsing, context extraction, signature parsing | +| [`xml_structs.go`](xml_structs.go) | XML structure definitions (BADMSG, violation contexts) | +| [`helpers.go`](helpers.go) | Utility functions | + +See individual files for implementation details. Protobuf schema defined in [`api/grpc/events/v1/security_violation.proto`](../../../api/grpc/events/v1/security_violation.proto). + +## Requirements + +- **Input**: NAP syslog messages with `secops_dashboard` log profile (33 CSV fields) +- **Output**: `SecurityViolationEvent` protobuf messages + +## Testing + +```bash +# Run all tests +go test ./internal/collector/securityviolationsprocessor -v + +# Check coverage +go test ./internal/collector/securityviolationsprocessor -coverprofile=coverage.out +go tool cover -html=coverage.out +``` + +Test coverage: CSV parsing, XML parsing (5 violation contexts), encoding edge cases, error handling. + +## Error Handling + +Implements graceful degradation: +- Malformed XML: Logs warning, continues processing +- Base64 decode errors: Falls back to raw data +- Missing fields: Uses empty strings +- Context inference: Derives from violation names when not explicit diff --git a/internal/collector/securityviolationsprocessor/csv_parser.go b/internal/collector/securityviolationsprocessor/csv_parser.go new file mode 100644 index 0000000000..ec3c443278 --- /dev/null +++ b/internal/collector/securityviolationsprocessor/csv_parser.go @@ -0,0 +1,220 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package securityviolationsprocessor + +import ( + "encoding/csv" + "strconv" + "strings" + + events "github.com/nginx/agent/v3/api/grpc/events/v1" +) + +// Mapping of CSV field positions to their corresponding keys +var fieldOrder = []string{ + "blocking_exception_reason", + "dest_port", + "ip_client", + "is_truncated_bool", + "method", + "policy_name", + "protocol", + "request_status", + "response_code", + "severity", + "sig_cves", + "sig_set_names", + "src_port", + "sub_violations", + "support_id", + "threat_campaign_names", + "violation_rating", + "vs_name", + "x_forwarded_for_header_value", + "outcome", + "outcome_reason", + "violations", + "violation_details", + "bot_signature_name", + "bot_category", + "bot_anomalies", + "enforced_bot_anomalies", + "client_class", + "client_application", + "client_application_version", + "transport_protocol", + "uri", + "request", +} + +// parseCSVLog parses comma-separated syslog messages where fields are in a +// order : blocking_exception_reason,dest_port,ip_client,is_truncated_bool,method,policy_name,protocol,request_status,response_code,severity,sig_cves,sig_set_names,src_port,sub_violations,support_id,threat_campaign_names,violation_rating,vs_name,x_forwarded_for_header_value,outcome,outcome_reason,violations,violation_details,bot_signature_name,bot_category,bot_anomalies,enforced_bot_anomalies,client_class,client_application,client_application_version,transport_protocol,uri,request (secops_dashboard-log profile format). +// versions when key-value logging isn't enabled. +// +//nolint:lll //long test string kept for log profile readability +func parseCSVLog(message string) map[string]string { + fieldValueMap := make(map[string]string, 33) + + // Remove the "ASM:" prefix if present so we only process the values + message = strings.TrimPrefix(message, "ASM:") + + reader := csv.NewReader(strings.NewReader(message)) + reader.LazyQuotes = true + fields, err := reader.Read() + if err != nil { + // fallback: return empty map if parsing fails + return fieldValueMap + } + + for i, field := range fields { + if i >= len(fieldOrder) { + break + } + fieldValueMap[fieldOrder[i]] = strings.TrimSpace(field) + } + + // combine multiple values separated by '::' + if combined, ok := fieldValueMap["sig_cves"]; ok { + parts := strings.SplitN(combined, "::", maxSplitParts) + fieldValueMap["sig_ids"] = parts[0] + if len(parts) > 1 { + fieldValueMap["sig_names"] = parts[1] + } + } + + if combined, ok := fieldValueMap["sig_set_names"]; ok { + parts := strings.SplitN(combined, "::", maxSplitParts) + fieldValueMap["sig_set_names"] = parts[0] + if len(parts) > 1 { + fieldValueMap["sig_cves"] = parts[1] + } + } + + return fieldValueMap +} + +// parseOutcome converts string outcome to RequestOutcome enum +func parseOutcome(outcome string) events.RequestOutcome { + switch strings.ToLower(strings.TrimSpace(outcome)) { + case "passed": + return events.RequestOutcome_REQUEST_OUTCOME_PASSED + case "rejected": + return events.RequestOutcome_REQUEST_OUTCOME_REJECTED + default: + return events.RequestOutcome_REQUEST_OUTCOME_UNKNOWN + } +} + +// parseIsTruncated converts string to boolean +func parseIsTruncated(value string) bool { + switch strings.ToLower(strings.TrimSpace(value)) { + case "true": + return true + default: + return false + } +} + +// parseSeverity converts string severity to Severity enum +func parseSeverity(severity string) events.Severity { + switch strings.ToLower(strings.TrimSpace(severity)) { + case "informational": + return events.Severity_SEVERITY_INFORMATIONAL + case "low": + return events.Severity_SEVERITY_LOW + case "medium": + return events.Severity_SEVERITY_MEDIUM + case "high": + return events.Severity_SEVERITY_HIGH + case "critical": + return events.Severity_SEVERITY_CRITICAL + default: + return events.Severity_SEVERITY_UNKNOWN + } +} + +// parseRequestOutcomeReason converts string outcome reason to RequestOutcomeReason enum +func parseRequestOutcomeReason(reason string) events.RequestOutcomeReason { + switch strings.ToUpper(strings.TrimSpace(reason)) { + case "SECURITY_WAF_OK": + return events.RequestOutcomeReason_SECURITY_WAF_OK + case "SECURITY_WAF_VIOLATION": + return events.RequestOutcomeReason_SECURITY_WAF_VIOLATION + case "SECURITY_WAF_FLAGGED": + return events.RequestOutcomeReason_SECURITY_WAF_FLAGGED + case "SECURITY_WAF_VIOLATION_TRANSPARENT": + return events.RequestOutcomeReason_SECURITY_WAF_VIOLATION_TRANSPARENT + default: + return events.RequestOutcomeReason_SECURITY_WAF_UNKNOWN + } +} + +// parseRequestStatus converts string request status to RequestStatus enum +func parseRequestStatus(status string) events.RequestStatus { + switch strings.ToLower(strings.TrimSpace(status)) { + case "blocked": + return events.RequestStatus_REQUEST_STATUS_BLOCKED + case "alerted": + return events.RequestStatus_REQUEST_STATUS_ALERTED + case "passed": + return events.RequestStatus_REQUEST_STATUS_PASSED + default: + return events.RequestStatus_REQUEST_STATUS_UNKNOWN + } +} + +// parseUint32 converts string to uint32 +func parseUint32(value string) uint32 { + if val, err := strconv.ParseUint(strings.TrimSpace(value), 10, 32); err == nil { + return uint32(val) + } + return 0 +} + +func mapKVToSecurityViolationEvent(log *events.SecurityViolationEvent, + kvMap map[string]string, +) { + log.PolicyName = kvMap["policy_name"] + log.SupportId = kvMap["support_id"] + log.RequestOutcome = parseOutcome(kvMap["outcome"]) + log.RequestOutcomeReason = parseRequestOutcomeReason(kvMap["outcome_reason"]) + log.BlockingExceptionReason = kvMap["blocking_exception_reason"] + log.Method = kvMap["method"] + log.Protocol = kvMap["protocol"] + log.XffHeaderValue = kvMap["x_forwarded_for_header_value"] + log.Uri = kvMap["uri"] + log.Request = kvMap["request"] + log.IsTruncated = parseIsTruncated(kvMap["is_truncated_bool"]) + log.RequestStatus = parseRequestStatus(kvMap["request_status"]) + log.ResponseCode = parseUint32(kvMap["response_code"]) + log.ServerAddr = kvMap["server_addr"] + log.VsName = kvMap["vs_name"] + log.RemoteAddr = kvMap["ip_client"] + log.DestinationPort = parseUint32(kvMap["dest_port"]) + log.ServerPort = parseUint32(kvMap["src_port"]) + log.Violations = kvMap["violations"] + log.SubViolations = kvMap["sub_violations"] + log.ViolationRating = parseUint32(kvMap["violation_rating"]) + log.SigSetNames = kvMap["sig_set_names"] + log.SigCves = kvMap["sig_cves"] + log.ClientClass = kvMap["client_class"] + log.ClientApplication = kvMap["client_application"] + log.ClientApplicationVersion = kvMap["client_application_version"] + log.Severity = parseSeverity(kvMap["severity"]) + log.ThreatCampaignNames = kvMap["threat_campaign_names"] + log.BotAnomalies = kvMap["bot_anomalies"] + log.BotCategory = kvMap["bot_category"] + log.EnforcedBotAnomalies = kvMap["enforced_bot_anomalies"] + log.BotSignatureName = kvMap["bot_signature_name"] + log.DisplayName = kvMap["display_name"] + + if log.GetRemoteAddr() == "" { + log.RemoteAddr = kvMap["remote_addr"] + } + if log.GetDestinationPort() == 0 { + log.DestinationPort = parseUint32(kvMap["remote_port"]) + } +} diff --git a/internal/collector/securityviolationsprocessor/processor.go b/internal/collector/securityviolationsprocessor/processor.go index a4dbdaedfa..9c14a3dd06 100644 --- a/internal/collector/securityviolationsprocessor/processor.go +++ b/internal/collector/securityviolationsprocessor/processor.go @@ -7,7 +7,6 @@ package securityviolationsprocessor import ( "context" - "encoding/json" "errors" "fmt" "net" @@ -17,6 +16,7 @@ import ( syslog "github.com/leodido/go-syslog/v4" "github.com/leodido/go-syslog/v4/rfc3164" + events "github.com/nginx/agent/v3/api/grpc/events/v1" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/pdata/pcommon" @@ -24,13 +24,15 @@ import ( "go.opentelemetry.io/collector/processor" "go.uber.org/multierr" "go.uber.org/zap" + "google.golang.org/protobuf/proto" ) const ( - notAvailable = "N/A" maxSplitParts = 2 ) +var ipRegex = regexp.MustCompile(`^ip-([0-9-]+)`) + // securityViolationsProcessor parses syslog-formatted log records and annotates // them with structured SecurityEvent attributes. type securityViolationsProcessor struct { @@ -158,31 +160,32 @@ func (p *securityViolationsProcessor) processAppProtectMessage(lr plog.LogRecord ) error { appProtectLog := p.parseAppProtectLog(message, hostname) - jsonData, marshalErr := json.Marshal(appProtectLog) + protoData, marshalErr := proto.Marshal(appProtectLog) if marshalErr != nil { return marshalErr } - - lr.Body().SetStr(string(jsonData)) + lr.Body().SetEmptyBytes().FromRaw(protoData) attrs := lr.Attributes() - attrs.PutStr("app_protect.policy_name", appProtectLog.PolicyName) - attrs.PutStr("app_protect.support_id", appProtectLog.SupportID) - attrs.PutStr("app_protect.outcome", appProtectLog.Outcome) - attrs.PutStr("app_protect.remote_addr", appProtectLog.RemoteAddr) + attrs.PutStr("app_protect.policy_name", appProtectLog.GetPolicyName()) + attrs.PutStr("app_protect.support_id", appProtectLog.GetSupportId()) + attrs.PutStr("app_protect.outcome", appProtectLog.GetRequestOutcome().String()) + attrs.PutStr("app_protect.remote_addr", appProtectLog.GetRemoteAddr()) return nil } -func (p *securityViolationsProcessor) parseAppProtectLog(message string, hostname *string) *SecurityViolationEvent { - log := &SecurityViolationEvent{} +func (p *securityViolationsProcessor) parseAppProtectLog( + message string, hostname *string, +) *events.SecurityViolationEvent { + log := &events.SecurityViolationEvent{} - p.assignHostnames(log, hostname) + assignHostnames(log, hostname) - kvMap := p.parseCSVLog(message) + kvMap := parseCSVLog(message) - p.mapKVToSecurityViolationEvent(log, kvMap) + mapKVToSecurityViolationEvent(log, kvMap) - if log.ServerAddr == "" && hostname != nil { + if log.GetServerAddr() == "" && hostname != nil { if ip := extractIPFromHostname(*hostname); ip != "" { log.ServerAddr = ip } @@ -194,274 +197,25 @@ func (p *securityViolationsProcessor) parseAppProtectLog(message string, hostnam return log } -func (p *securityViolationsProcessor) assignHostnames(log *SecurityViolationEvent, hostname *string) { +func assignHostnames(log *events.SecurityViolationEvent, hostname *string) { if hostname == nil { return } - log.SystemID = *hostname - log.ParentHostname = *hostname + log.SystemId = *hostname - if log.ServerAddr == "" { + if log.GetServerAddr() == "" { if ip := extractIPFromHostname(*hostname); ip != "" { log.ServerAddr = ip } } } -// parseCSVLog parses comma-separated syslog messages where fields are in a -// order : blocking_exception_reason,dest_port,ip_client,is_truncated_bool,method,policy_name,protocol,request_status,response_code,severity,sig_cves,sig_set_names,src_port,sub_violations,support_id,threat_campaign_names,violation_rating,vs_name,x_forwarded_for_header_value,outcome,outcome_reason,violations,violation_details,bot_signature_name,bot_category,bot_anomalies,enforced_bot_anomalies,client_class,client_application,client_application_version,transport_protocol,uri,request (secops_dashboard-log profile format). -// versions when key-value logging isn't enabled. -// -//nolint:lll //long test string kept for log profile readability -func (p *securityViolationsProcessor) parseCSVLog(message string) map[string]string { - fieldValueMap := make(map[string]string) - - // Remove the "ASM:" prefix if present so we only process the values - if idx := strings.Index(message, ":"); idx >= 0 { - message = message[idx+1:] - } - - fields := strings.Split(message, ",") - - // Mapping of CSV field positions to their corresponding keys - fieldOrder := []string{ - "blocking_exception_reason", - "dest_port", - "ip_client", - "is_truncated_bool", - "method", - "policy_name", - "protocol", - "request_status", - "response_code", - "severity", - "sig_cves", - "sig_set_names", - "src_port", - "sub_violations", - "support_id", - "threat_campaign_names", - "violation_rating", - "vs_name", - "x_forwarded_for_header_value", - "outcome", - "outcome_reason", - "violations", - "violation_details", - "bot_signature_name", - "bot_category", - "bot_anomalies", - "enforced_bot_anomalies", - "client_class", - "client_application", - "client_application_version", - "transport_protocol", - "uri", - "request", - } - - for i, field := range fields { - if i >= len(fieldOrder) { - break - } - fieldValueMap[fieldOrder[i]] = strings.TrimSpace(field) - } - - // combine multiple values separated by '::' - if combined, ok := fieldValueMap["sig_cves"]; ok { - parts := strings.SplitN(combined, "::", maxSplitParts) - fieldValueMap["sig_ids"] = parts[0] - if len(parts) > 1 { - fieldValueMap["sig_names"] = parts[1] - } - } - - if combined, ok := fieldValueMap["sig_set_names"]; ok { - parts := strings.SplitN(combined, "::", maxSplitParts) - fieldValueMap["sig_set_names"] = parts[0] - if len(parts) > 1 { - fieldValueMap["sig_cves"] = parts[1] - } - } - - return fieldValueMap -} - -func (p *securityViolationsProcessor) mapKVToSecurityViolationEvent(log *SecurityViolationEvent, - kvMap map[string]string, -) { - log.PolicyName = kvMap["policy_name"] - log.SupportID = kvMap["support_id"] - log.Outcome = kvMap["outcome"] - log.OutcomeReason = kvMap["outcome_reason"] - log.BlockingExceptionReason = kvMap["blocking_exception_reason"] - log.Method = kvMap["method"] - log.Protocol = kvMap["protocol"] - log.XForwardedForHeaderValue = kvMap["x_forwarded_for_header_value"] - log.URI = kvMap["uri"] - log.Request = kvMap["request"] - log.IsTruncated = kvMap["is_truncated_bool"] - log.RequestStatus = kvMap["request_status"] - log.ResponseCode = kvMap["response_code"] - log.ServerAddr = kvMap["server_addr"] - log.VSName = kvMap["vs_name"] - log.RemoteAddr = kvMap["ip_client"] - log.RemotePort = kvMap["dest_port"] - log.ServerPort = kvMap["src_port"] - log.Violations = kvMap["violations"] - log.SubViolations = kvMap["sub_violations"] - log.ViolationRating = kvMap["violation_rating"] - log.SigSetNames = kvMap["sig_set_names"] - log.SigCVEs = kvMap["sig_cves"] - log.ClientClass = kvMap["client_class"] - log.ClientApplication = kvMap["client_application"] - log.ClientApplicationVersion = kvMap["client_application_version"] - log.Severity = kvMap["severity"] - log.ThreatCampaignNames = kvMap["threat_campaign_names"] - log.BotAnomalies = kvMap["bot_anomalies"] - log.BotCategory = kvMap["bot_category"] - log.EnforcedBotAnomalies = kvMap["enforced_bot_anomalies"] - log.BotSignatureName = kvMap["bot_signature_name"] - log.InstanceTags = kvMap["instance_tags"] - log.InstanceGroup = kvMap["instance_group"] - log.DisplayName = kvMap["display_name"] - - if log.RemoteAddr == "" { - log.RemoteAddr = kvMap["remote_addr"] - } - if log.RemotePort == "" { - log.RemotePort = kvMap["remote_port"] - } -} - -// parseViolationsData extracts violation data from the syslog key-value map -func (p *securityViolationsProcessor) parseViolationsData(kvMap map[string]string) []ViolationData { - var violationsData []ViolationData - - // Extract violation name from violation_details XML - this is the only source - violationName := "" - if violationDetails := kvMap["violation_details"]; violationDetails != "" { - violNameRegex := regexp.MustCompile(`([^<]+)`) - if matches := violNameRegex.FindStringSubmatch(violationDetails); len(matches) > 1 { - violationName = matches[1] - } - } - - // Create violation data if we have violation information - if violationName != "" || kvMap["violations"] != "" { - signatures := p.extractSignatureData(kvMap) - if signatures == nil { - signatures = []SignatureData{} - } - - violationData := ViolationData{ - Name: violationName, - Context: p.extractViolationContext(kvMap), - ContextData: p.extractContextData(kvMap), - Signatures: signatures, - } - violationsData = append(violationsData, violationData) - } - - return violationsData -} - -// extractViolationContext extracts the violation context from syslog data -func (p *securityViolationsProcessor) extractViolationContext(kvMap map[string]string) string { - if uri := kvMap["uri"]; uri != "" { - return uri - } - if method := kvMap["method"]; method != "" { - return method - } - - return "" -} - -// extractContextData extracts context data from syslog -func (p *securityViolationsProcessor) extractContextData(kvMap map[string]string) ContextData { - contextData := ContextData{} - - if paramName := kvMap["parameter_name"]; paramName != "" { - contextData.Name = paramName - contextData.Value = kvMap["parameter_value"] - } else if uri := kvMap["uri"]; uri != "" { - // Use URI as context if no specific parameter data - contextData.Name = "uri" - contextData.Value = uri - } else if request := kvMap["request"]; request != "" { - // Use request as context if no URI - contextData.Name = "request" - contextData.Value = request - } - - return contextData -} - -// extractSignatureData extracts signature data from syslog -func (p *securityViolationsProcessor) extractSignatureData(kvMap map[string]string) []SignatureData { - sigIDs := kvMap["sig_ids"] - sigNames := kvMap["sig_names"] - blockingMask := kvMap["blocking_mask"] - sigOffset := kvMap["sig_offset"] - sigLength := kvMap["sig_length"] - - if sigIDs == "" || sigIDs == notAvailable { - return []SignatureData{} - } - - ids := splitAndTrim(sigIDs) - names := splitAndTrim(sigNames) - - return buildSignatures(ids, names, blockingMask, sigOffset, sigLength) -} - -func splitAndTrim(value string) []string { - if strings.TrimSpace(value) == "" || value == notAvailable { - return nil - } - - parts := strings.Split(value, ",") - - var trimmedParts []string - for _, part := range parts { - trimmed := strings.TrimSpace(part) - if trimmed != "" { - trimmedParts = append(trimmedParts, trimmed) - } - } - - return trimmedParts -} - -func buildSignatures(ids, names []string, mask, offset, length string) []SignatureData { - signatures := make([]SignatureData, 0, len(ids)) - for i, id := range ids { - if id == "" || id == notAvailable { - continue - } - signature := SignatureData{ - ID: id, - BlockingMask: mask, - Offset: offset, - Length: length, - } - if i < len(names) { - signature.Buffer = names[i] - } - signatures = append(signatures, signature) - } - - return signatures -} - func extractIPFromHostname(hostname string) string { if ip := net.ParseIP(hostname); ip != nil { return ip.String() } - re := regexp.MustCompile(`^ip-([0-9-]+)`) - if matches := re.FindStringSubmatch(hostname); len(matches) > 1 { + if matches := ipRegex.FindStringSubmatch(hostname); len(matches) > 1 { candidate := strings.ReplaceAll(matches[1], "-", ".") if net.ParseIP(candidate) != nil { return candidate diff --git a/internal/collector/securityviolationsprocessor/processor_bench_test.go b/internal/collector/securityviolationsprocessor/processor_bench_test.go index fc64609530..43b4035133 100644 --- a/internal/collector/securityviolationsprocessor/processor_bench_test.go +++ b/internal/collector/securityviolationsprocessor/processor_bench_test.go @@ -44,6 +44,8 @@ func BenchmarkSecurityViolationsProcessor(b *testing.B) { message string numRecords int }{ + {name: "AppProtect_1", message: sampleAppProtectSyslog, numRecords: 1}, + {name: "AppProtect_10", message: sampleAppProtectSyslog, numRecords: 10}, {name: "AppProtect_100", message: sampleAppProtectSyslog, numRecords: 100}, {name: "AppProtect_1000", message: sampleAppProtectSyslog, numRecords: 1000}, } @@ -56,7 +58,9 @@ func BenchmarkSecurityViolationsProcessor(b *testing.B) { b.ReportAllocs() b.ResetTimer() for range b.N { - _ = p.ConsumeLogs(context.Background(), logs) + logsCopy := plog.NewLogs() + logs.CopyTo(logsCopy) + _ = p.ConsumeLogs(context.Background(), logsCopy) } }) } @@ -64,7 +68,7 @@ func BenchmarkSecurityViolationsProcessor(b *testing.B) { func BenchmarkSecurityViolationsProcessor_Concurrent(b *testing.B) { p := newBenchmarkProcessor() - logs := generateSecurityViolationLogs(200, sampleAppProtectSyslog) + logs := generateSecurityViolationLogs(1000, sampleAppProtectSyslog) b.ReportAllocs() b.ResetTimer() diff --git a/internal/collector/securityviolationsprocessor/processor_test.go b/internal/collector/securityviolationsprocessor/processor_test.go index 13802fb2f6..9f3baaa391 100644 --- a/internal/collector/securityviolationsprocessor/processor_test.go +++ b/internal/collector/securityviolationsprocessor/processor_test.go @@ -7,16 +7,17 @@ package securityviolationsprocessor import ( "context" - "encoding/json" "testing" "github.com/leodido/go-syslog/v4/rfc3164" + events "github.com/nginx/agent/v3/api/grpc/events/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/consumer/consumertest" "go.opentelemetry.io/collector/pdata/plog" "go.opentelemetry.io/collector/processor/processortest" "go.uber.org/zap" + "google.golang.org/protobuf/proto" ) //nolint:lll,revive // long test string kept for readability @@ -35,7 +36,7 @@ func TestSecurityViolationsProcessor(t *testing.T) { expectAttrs: map[string]string{ "app_protect.policy_name": "nms_app_protect_default_policy", "app_protect.support_id": "5377540117854870581", - "app_protect.outcome": "REJECTED", + "app_protect.outcome": "REQUEST_OUTCOME_REJECTED", "app_protect.remote_addr": "127.0.0.1", }, expectRecords: 1, @@ -106,60 +107,53 @@ func TestSecurityViolationsProcessor(t *testing.T) { } if tc.name == "Test 1: CSV NGINX App Protect syslog message" { - processedBody := lrOut.Body().Str() + processedBody := lrOut.Body().Bytes().AsRaw() - var actualEvent SecurityViolationEvent - jsonErr := json.Unmarshal([]byte(processedBody), &actualEvent) + var actualEvent events.SecurityViolationEvent + jsonErr := proto.Unmarshal(processedBody, &actualEvent) require.NoError(t, jsonErr, "Failed to unmarshal processed log body as SecurityViolationEvent") - assert.Equal(t, "nms_app_protect_default_policy", actualEvent.PolicyName) - assert.Equal(t, "5377540117854870581", actualEvent.SupportID) - assert.Equal(t, "REJECTED", actualEvent.Outcome) - assert.Equal(t, "SECURITY_WAF_VIOLATION", actualEvent.OutcomeReason) - assert.Equal(t, "GET", actualEvent.Method) - assert.Equal(t, "HTTP", actualEvent.Protocol) - assert.Equal(t, "N/A", actualEvent.XForwardedForHeaderValue) - assert.Equal(t, "/<>