@@ -33,6 +33,19 @@ class PayloadFormats(models.TextChoices):
3333 THIN = "thin" , "Thin"
3434 FULL = "full" , "Full"
3535
36+ class Kind (models .TextChoices ):
37+ INTEGRATION = "integration" , "Integration"
38+ AUDIT_SINK = "audit_sink" , "Audit sink"
39+
40+ class Transport (models .TextChoices ):
41+ HTTP = "http" , "HTTP"
42+ KAFKA = "kafka" , "Kafka"
43+
44+ class BodyFormat (models .TextChoices ):
45+ CISO_NATIVE = "ciso_native" , "CISO Assistant (HMAC-signed)"
46+ OCSF = "ocsf" , "OCSF"
47+ RAW = "raw" , "Raw LogEntry"
48+
3649 payload_format = models .CharField (
3750 verbose_name = "Payload Format" ,
3851 max_length = 10 ,
@@ -41,6 +54,33 @@ class PayloadFormats(models.TextChoices):
4154 help_text = "The format of the webhook payload sent to this endpoint." ,
4255 )
4356
57+ # An "audit_sink" forwards the audit log (LogEntry stream) to an external
58+ # SIEM; an "integration" is the user-facing model-event webhook. Audit sinks
59+ # are admin/org-managed and hidden from the user webhook list.
60+ kind = models .CharField (
61+ max_length = 20 ,
62+ choices = Kind .choices ,
63+ default = Kind .INTEGRATION ,
64+ )
65+ transport = models .CharField (
66+ max_length = 10 ,
67+ choices = Transport .choices ,
68+ default = Transport .HTTP ,
69+ help_text = "Delivery transport (audit sinks only)." ,
70+ )
71+ body_format = models .CharField (
72+ max_length = 20 ,
73+ choices = BodyFormat .choices ,
74+ default = BodyFormat .OCSF ,
75+ help_text = "Canonical event schema for audit sinks." ,
76+ )
77+ headers = models .JSONField (
78+ default = dict ,
79+ blank = True ,
80+ help_text = "Static headers added to each request, e.g. "
81+ '{"Authorization": "Splunk <token>"}. Used for audit-sink auth.' ,
82+ )
83+
4484 owner = models .ForeignKey (
4585 Actor ,
4686 related_name = "webhook_endpoints" ,
@@ -51,10 +91,24 @@ class PayloadFormats(models.TextChoices):
5191 )
5292
5393 url = models .URLField (
54- max_length = 512 , help_text = "The consumer URL to send webhook events to."
94+ max_length = 512 ,
95+ blank = True ,
96+ default = "" ,
97+ help_text = "Consumer URL (HTTP transport)." ,
5598 )
5699
57- secret = models .CharField (max_length = 100 , help_text = "HMAC signing secret." )
100+ kafka_config = models .JSONField (
101+ default = dict ,
102+ blank = True ,
103+ help_text = "Kafka transport: {bootstrap_servers, topic, config:{...}}." ,
104+ )
105+
106+ secret = models .CharField (
107+ max_length = 100 ,
108+ blank = True ,
109+ default = "" ,
110+ help_text = "HMAC signing secret (integration webhooks only)." ,
111+ )
58112
59113 event_types = models .ManyToManyField (
60114 WebhookEventType ,
@@ -78,6 +132,8 @@ def __str__(self):
78132
79133 def clean (self ):
80134 super ().clean ()
135+ if self .transport != self .Transport .HTTP :
136+ return
81137 if getattr (settings , "WEBHOOK_ALLOW_PRIVATE_IPS" , False ):
82138 return
83139 try :
@@ -91,8 +147,6 @@ def clean(self):
91147 )
92148
93149 def save (self , * args , ** kwargs ):
94- """
95- On save, ensure a secret exists if one wasn't provided.
96- """
97- self .full_clean () # Run validation
150+ """Run full model validation (clean + field checks) before persisting."""
151+ self .full_clean ()
98152 super ().save (* args , ** kwargs )
0 commit comments