main
py 480 lines 21.7 KB
Raw
1 from datetime import datetime
2 from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 from sqlalchemy import PrimaryKeyConstraint
7 from sqlmodel import JSON
8 from sqlmodel import Column
9 from sqlmodel import Field
10 from sqlmodel import Relationship
11 from sqlmodel import SQLModel
12 from sqlmodel import Text
13
14
15 class IoC(SQLModel, table=True):
16 __tablename__ = "incident_management_ioc"
17 id: Optional[int] = Field(default=None, primary_key=True)
18 value: str = Field(nullable=False)
19 type: str = Field(max_length=50, nullable=False) # e.g., IP address, domain, URL, etc.
20 description: Optional[str] = Field(sa_column=Column(Text, nullable=True))
21
22 alerts: List["AlertToIoC"] = Relationship(back_populates="ioc")
23
24
25 class AlertToIoC(SQLModel, table=True):
26 __tablename__ = "incident_management_alert_to_ioc"
27 alert_id: int = Field(foreign_key="incident_management_alert.id", primary_key=True)
28 ioc_id: int = Field(foreign_key="incident_management_ioc.id", primary_key=True)
29
30 alert: "Alert" = Relationship(back_populates="iocs")
31 ioc: "IoC" = Relationship(back_populates="alerts")
32
33
34 class Alert(SQLModel, table=True):
35 __tablename__ = "incident_management_alert"
36 id: Optional[int] = Field(default=None, primary_key=True)
37 alert_name: str = Field(sa_column=Column(Text, nullable=False))
38 alert_description: str = Field(sa_column=Column(Text, nullable=False))
39 status: str = Field(max_length=50, nullable=False)
40 alert_creation_time: datetime = Field(default_factory=datetime.utcnow)
41 customer_code: str = Field(max_length=50, nullable=False)
42 time_closed: Optional[datetime] = Field(default=None)
43 source: str = Field(max_length=50, nullable=False)
44 assigned_to: Optional[str] = Field(max_length=50, nullable=True)
45 escalated: bool = Field(default=False, nullable=False)
46
47 comments: List["Comment"] = Relationship(back_populates="alert")
48 assets: List["Asset"] = Relationship(back_populates="alert")
49 cases: List["CaseAlertLink"] = Relationship(back_populates="alert")
50 tags: List["AlertToTag"] = Relationship(back_populates="alert")
51 iocs: List["AlertToIoC"] = Relationship(back_populates="alert")
52
53
54 class AlertTag(SQLModel, table=True):
55 __tablename__ = "incident_management_alerttag"
56 id: Optional[int] = Field(default=None, primary_key=True)
57 tag: str = Field(max_length=50, nullable=False)
58
59 alerts: List["AlertToTag"] = Relationship(back_populates="tag")
60
61
62 class AlertToTag(SQLModel, table=True):
63 __tablename__ = "incident_management_alert_to_tag"
64 alert_id: int = Field(foreign_key="incident_management_alert.id", primary_key=True)
65 tag_id: int = Field(foreign_key="incident_management_alerttag.id", primary_key=True)
66
67 alert: Alert = Relationship(back_populates="tags")
68 tag: AlertTag = Relationship(back_populates="alerts")
69
70
71 class Comment(SQLModel, table=True):
72 __tablename__ = "incident_management_comment"
73 id: Optional[int] = Field(default=None, primary_key=True)
74 alert_id: int = Field(default=None, foreign_key="incident_management_alert.id")
75 comment: str = Field(sa_column=Text)
76 user_name: str = Field(max_length=50, nullable=False)
77 created_at: datetime = Field(default_factory=datetime.utcnow)
78
79 alert: Alert = Relationship(back_populates="comments")
80
81
82 class AlertContext(SQLModel, table=True):
83 __tablename__ = "incident_management_alertcontext"
84 id: Optional[int] = Field(default=None, primary_key=True)
85 source: str = Field(max_length=50, nullable=False)
86 context: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True))
87
88 assets: List["Asset"] = Relationship(back_populates="alert_context")
89
90
91 class Asset(SQLModel, table=True):
92 __tablename__ = "incident_management_asset"
93 id: Optional[int] = Field(default=None, primary_key=True)
94 alert_linked: int = Field(default=None, foreign_key="incident_management_alert.id")
95 asset_name: str = Field(max_length=255, nullable=False)
96 alert_context_id: int = Field(foreign_key="incident_management_alertcontext.id")
97 agent_id: Optional[str] = Field(default=None, max_length=50)
98 velociraptor_id: Optional[str] = Field(default=None, max_length=150)
99 customer_code: str = Field(max_length=50, nullable=False)
100 index_name: str = Field(max_length=255, nullable=False)
101 index_id: str = Field(max_length=255, nullable=False)
102
103 alert: Alert = Relationship(back_populates="assets")
104 alert_context: AlertContext = Relationship(back_populates="assets")
105
106
107 class FieldName(SQLModel, table=True):
108 __tablename__ = "incident_management_fieldname"
109 id: Optional[int] = Field(default=None, primary_key=True)
110 source: str = Field(max_length=50, nullable=False)
111 field_name: str = Field(max_length=100, nullable=False)
112
113
114 class AssetFieldName(SQLModel, table=True):
115 __tablename__ = "incident_management_assetfieldname"
116 id: Optional[int] = Field(default=None, primary_key=True)
117 source: str = Field(max_length=50, nullable=False)
118 field_name: str = Field(max_length=100, nullable=False)
119
120
121 class TimestampFieldName(SQLModel, table=True):
122 __tablename__ = "incident_management_timestampfieldname"
123 id: Optional[int] = Field(default=None, primary_key=True)
124 source: str = Field(max_length=50, nullable=False)
125 field_name: str = Field(max_length=100, nullable=False)
126
127
128 class AlertTitleFieldName(SQLModel, table=True):
129 __tablename__ = "incident_management_alerttitlefieldname"
130 id: Optional[int] = Field(default=None, primary_key=True)
131 source: str = Field(max_length=50, nullable=False)
132 field_name: str = Field(max_length=100, nullable=False)
133
134
135 class IoCFieldName(SQLModel, table=True):
136 __tablename__ = "incident_management_iocfieldname"
137 id: Optional[int] = Field(default=None, primary_key=True)
138 source: str = Field(max_length=50, nullable=False)
139 field_name: str = Field(max_length=100, nullable=False)
140
141
142 class CustomerCodeFieldName(SQLModel, table=True):
143 __tablename__ = "incident_management_customercodefieldname"
144 id: Optional[int] = Field(default=None, primary_key=True)
145 source: str = Field(max_length=50, nullable=False)
146 field_name: str = Field(max_length=100, nullable=False)
147
148
149 class CaseComment(SQLModel, table=True):
150 __tablename__ = "incident_management_case_comment"
151 id: Optional[int] = Field(default=None, primary_key=True)
152 case_id: int = Field(default=None, foreign_key="incident_management_case.id")
153 comment: str = Field(sa_column=Text)
154 user_name: str = Field(max_length=50, nullable=False)
155 created_at: datetime = Field(default_factory=datetime.utcnow)
156
157 case: "Case" = Relationship(back_populates="comments")
158
159
160 class Case(SQLModel, table=True):
161 __tablename__ = "incident_management_case"
162 id: Optional[int] = Field(default=None, primary_key=True)
163 case_name: str = Field(max_length=10000, nullable=False)
164 case_description: str = Field(sa_column=Text)
165 case_creation_time: datetime = Field(default_factory=datetime.utcnow)
166 case_status: str = Field(max_length=50, nullable=False)
167 case_closed_time: Optional[datetime] = Field(default=None, nullable=True)
168 assigned_to: Optional[str] = Field(max_length=50, nullable=True)
169 customer_code: Optional[str] = Field(max_length=50, nullable=True)
170 notification_invoked_number: Optional[int] = Field(default=0, nullable=True)
171 escalated: bool = Field(default=False, nullable=False)
172
173 alerts: List["CaseAlertLink"] = Relationship(back_populates="case")
174 data_store: List["CaseDataStore"] = Relationship(back_populates="case")
175 comments: List["CaseComment"] = Relationship(back_populates="case")
176
177
178 class CaseAlertLink(SQLModel, table=True):
179 __tablename__ = "incident_management_casealertlink"
180 case_id: Optional[int] = Field(default=None, foreign_key="incident_management_case.id")
181 alert_id: Optional[int] = Field(default=None, foreign_key="incident_management_alert.id")
182
183 case: Case = Relationship(back_populates="alerts")
184 alert: Alert = Relationship(back_populates="cases")
185
186 __table_args__ = (PrimaryKeyConstraint("case_id", "alert_id"),)
187
188
189 class Notification(SQLModel, table=True):
190 __tablename__ = "incident_management_notification"
191 id: Optional[int] = Field(default=None, primary_key=True)
192 customer_code: str = Field(max_length=50, nullable=False)
193 shuffle_workflow_id: str = Field(max_length=1000, nullable=False)
194 enabled: bool = Field(default=True)
195
196
197 class AIAnalystTriggerEnabled(SQLModel, table=True):
198 __tablename__ = "incident_management_ai_analyst_trigger_enabled"
199 id: Optional[int] = Field(default=None, primary_key=True)
200 customer_code: str = Field(max_length=50, nullable=False, unique=True)
201 enabled: bool = Field(default=True)
202
203
204 class CaseDataStore(SQLModel, table=True):
205 __tablename__ = "incident_management_case_datastore"
206 id: Optional[int] = Field(default=None, primary_key=True)
207
208 case_id: int = Field(foreign_key="incident_management_case.id", nullable=False)
209 bucket_name: str = Field(max_length=255, nullable=False) # Name of the MinIO bucket
210 object_key: str = Field(max_length=1024, nullable=False) # Path/key of the file in MinIO
211 file_name: str = Field(max_length=255, nullable=False) # Original file name uploaded by the user
212 content_type: Optional[str] = Field(max_length=100, nullable=True) # MIME type of the file
213 file_size: Optional[int] = Field(nullable=True) # File size in bytes
214 upload_time: datetime = Field(default_factory=datetime.utcnow) # Time of upload
215 file_hash: str = Field(max_length=128, nullable=False) # Hash of the file (e.g., SHA-256)
216
217 case: "Case" = Relationship(back_populates="data_store")
218
219
220 class CaseReportTemplateDataStore(SQLModel, table=True):
221 __tablename__ = "incident_management_case_report_template_datastore"
222 id: Optional[int] = Field(default=None, primary_key=True)
223
224 report_template_name: str = Field(max_length=255, nullable=False)
225 bucket_name: str = Field(max_length=255, nullable=False) # Name of the MinIO bucket
226 object_key: str = Field(max_length=1024, nullable=False) # Path/key of the file in MinIO
227 file_name: str = Field(max_length=255, nullable=False) # Original file name uploaded by the user
228 content_type: Optional[str] = Field(max_length=100, nullable=True) # MIME type of the file
229 file_size: Optional[int] = Field(nullable=True) # File size in bytes
230 upload_time: datetime = Field(default_factory=datetime.utcnow) # Time of upload
231 file_hash: str = Field(max_length=128, nullable=False) # Hash of the file (e.g., SHA-256)
232
233
234 class VeloSigmaExclusion(SQLModel, table=True):
235 """Exclusion rules for Velociraptor Sigma alerts."""
236
237 __tablename__ = "incident_management_velo_sigma_exclusion"
238
239 id: Optional[int] = Field(default=None, primary_key=True)
240 name: str = Field(max_length=255, nullable=False, description="Friendly name for this exclusion rule")
241 description: Optional[str] = Field(sa_column=Column(Text, nullable=True), description="Description of why this exclusion exists")
242
243 # Core matching criteria
244 channel: Optional[str] = Field(max_length=255, nullable=True, description="Windows event channel to match (exact match)")
245 title: Optional[str] = Field(max_length=255, nullable=True, description="Sigma rule title to match (exact match)")
246
247 # Field matching data - stored as JSON to allow flexible field matching
248 field_matches: Optional[Dict] = Field(
249 sa_column=Column(JSON, nullable=True),
250 description="JSON of field names and values to match in the event data",
251 )
252
253 # Metadata
254 customer_code: Optional[str] = Field(
255 max_length=50,
256 nullable=True,
257 description="Customer code this exclusion applies to (null means all customers)",
258 )
259 created_by: str = Field(max_length=100, nullable=False, description="User who created this exclusion")
260 created_at: datetime = Field(default_factory=datetime.utcnow, description="When this exclusion was created")
261 last_matched_at: Optional[datetime] = Field(nullable=True, description="When this exclusion last matched an alert")
262 match_count: int = Field(default=0, description="How many times this exclusion has matched")
263 enabled: bool = Field(default=True, description="Whether this exclusion is active")
264
265
266 class ThresholdAlertMetadata(SQLModel, table=True):
267 """Metadata for threshold alerts to enable event resolution and timeline retrieval."""
268
269 __tablename__ = "incident_management_threshold_alert_metadata"
270
271 id: Optional[int] = Field(default=None, primary_key=True)
272 alert_id: int = Field(foreign_key="incident_management_alert.id", nullable=False, unique=True)
273 event_definition_id: str = Field(max_length=255, nullable=False, description="Graylog event definition ID")
274 replay_query: str = Field(sa_column=Column(Text, nullable=False), description="Lucene query from Graylog replay_info")
275 timerange_start: datetime = Field(nullable=False, description="Start of the threshold evaluation window")
276 timerange_end: datetime = Field(nullable=False, description="End of the threshold evaluation window")
277 group_by_fields: Optional[Dict] = Field(
278 sa_column=Column(JSON, nullable=True),
279 description="Group-by field key/value pairs from the threshold event",
280 )
281 source_streams: Optional[List] = Field(sa_column=Column(JSON, nullable=True), description="Graylog source stream IDs")
282 source: str = Field(max_length=50, nullable=False, description="SOURCE field value (e.g. wazuh)")
283 resolved_index_name: str = Field(max_length=255, nullable=False, description="OpenSearch index of the resolved event")
284 resolved_index_id: str = Field(max_length=255, nullable=False, description="OpenSearch document ID of the resolved event")
285
286 alert: Alert = Relationship()
287
288
289 class CaseTemplate(SQLModel, table=True):
290 """
291 Reusable investigation playbook applied to a Case at creation time.
292
293 Templates are scoped via ``customer_code`` (NULL = global) and ``source``
294 (NULL = any alert source). Selection priority on case creation is
295 customer+source > customer > source > is_default. Templates carry a
296 set of ``CaseTemplateTask`` rows that are snapshot-copied into
297 ``CaseTask`` rows on the target case.
298 """
299
300 __tablename__ = "incident_management_case_template"
301
302 id: Optional[int] = Field(default=None, primary_key=True)
303 name: str = Field(max_length=255, nullable=False, description="Friendly template name")
304 description: Optional[str] = Field(sa_column=Column(Text, nullable=True), description="What this template is for")
305 customer_code: Optional[str] = Field(
306 max_length=50,
307 nullable=True,
308 description="Customer this template applies to. NULL = global / any customer.",
309 )
310 source: Optional[str] = Field(
311 max_length=50,
312 nullable=True,
313 description="Alert source this template applies to (e.g., wazuh, velociraptor). NULL = any source.",
314 )
315 is_default: bool = Field(
316 default=False,
317 nullable=False,
318 description="Default template for its (customer_code, source) scope. Used as the final fallback in selection.",
319 )
320 match_field: Optional[str] = Field(
321 default=None,
322 max_length=255,
323 nullable=True,
324 description=(
325 "Optional conditional auto-apply: name of a flat top-level field on the originating Wazuh "
326 "document (e.g., 'data_win_system_eventID'). When both match_field and match_value are set, "
327 "auto-apply fetches the raw event via the asset's (index_name, index_id) and applies this "
328 "template only when document[match_field] == match_value. Both null = unconditional template "
329 "(legacy customer/source tier picker)."
330 ),
331 )
332 match_value: Optional[str] = Field(
333 default=None,
334 sa_column=Column(Text, nullable=True),
335 description=(
336 "Optional conditional auto-apply: the string value compared (equality) against the raw "
337 "document field. Stored as text since Wazuh field values are heterogeneous; numeric fields "
338 "like eventID arrive as quoted strings already ('1' not 1)."
339 ),
340 )
341 created_by: str = Field(max_length=100, nullable=False, description="User who created this template")
342 created_at: datetime = Field(default_factory=datetime.utcnow)
343 updated_at: datetime = Field(default_factory=datetime.utcnow)
344
345 tasks: List["CaseTemplateTask"] = Relationship(back_populates="template")
346
347
348 class CaseTemplateTask(SQLModel, table=True):
349 """A predefined task on a CaseTemplate. Definition only — instances live in CaseTask."""
350
351 __tablename__ = "incident_management_case_template_task"
352
353 id: Optional[int] = Field(default=None, primary_key=True)
354 template_id: int = Field(foreign_key="incident_management_case_template.id", nullable=False)
355 title: str = Field(max_length=500, nullable=False)
356 description: Optional[str] = Field(sa_column=Column(Text, nullable=True))
357 guidelines: Optional[str] = Field(
358 sa_column=Column(Text, nullable=True),
359 description="Best practices / steps the analyst should follow when executing this task",
360 )
361 mandatory: bool = Field(
362 default=False,
363 nullable=False,
364 description="If true, NOT_NECESSARY status is rejected and closing the case with this task incomplete triggers a soft warning.",
365 )
366 order_index: int = Field(default=0, nullable=False, description="Display order; lower = first")
367
368 template: "CaseTemplate" = Relationship(back_populates="tasks")
369
370
371 class CaseTask(SQLModel, table=True):
372 """
373 Instance of a task attached to a real Case.
374
375 Rows are snapshots created by copying CaseTemplateTask fields when a
376 template is applied. ``template_task_id`` is an informational soft link
377 only — editing the source template does NOT mutate existing CaseTask rows.
378 Custom tasks added by analysts during investigation have ``template_task_id``
379 set to NULL.
380 """
381
382 __tablename__ = "incident_management_case_task"
383
384 id: Optional[int] = Field(default=None, primary_key=True)
385 case_id: int = Field(foreign_key="incident_management_case.id", nullable=False)
386 alert_id: Optional[int] = Field(
387 default=None,
388 foreign_key="incident_management_alert.id",
389 nullable=True,
390 index=True,
391 description=(
392 "Originating alert this task batch was materialized for. NULL = case-wide / general task. "
393 "Set to NULL ('orphaned') when the alert is unlinked from the case so task history survives."
394 ),
395 )
396 template_task_id: Optional[int] = Field(
397 default=None,
398 foreign_key="incident_management_case_template_task.id",
399 nullable=True,
400 description="Soft link back to the source template task. NULL for custom-added tasks.",
401 )
402
403 # Snapshot of template task definition at the time of application.
404 title: str = Field(max_length=500, nullable=False)
405 description: Optional[str] = Field(sa_column=Column(Text, nullable=True))
406 guidelines: Optional[str] = Field(sa_column=Column(Text, nullable=True))
407 mandatory: bool = Field(default=False, nullable=False)
408 order_index: int = Field(default=0, nullable=False)
409
410 # Lifecycle.
411 status: str = Field(
412 default="TODO",
413 max_length=50,
414 nullable=False,
415 description="One of TODO, DONE, NOT_NECESSARY (NOT_NECESSARY only valid when mandatory=False).",
416 )
417 evidence_comment: Optional[str] = Field(
418 sa_column=Column(Text, nullable=True),
419 description="Free-form notes / evidence (logs, command output) attached when status changes.",
420 )
421 completed_by: Optional[str] = Field(max_length=100, nullable=True)
422 completed_at: Optional[datetime] = Field(default=None, nullable=True)
423
424 created_by: str = Field(max_length=100, nullable=False)
425 created_at: datetime = Field(default_factory=datetime.utcnow)
426 updated_at: datetime = Field(default_factory=datetime.utcnow)
427
428
429 class CaseEvent(SQLModel, table=True):
430 """
431 Append-only audit log of mutations against a Case.
432
433 Every case-level mutation (status change, alert link/unlink, assignment,
434 template application, task add/status change/comment) emits one row.
435 Used to power the case timeline view.
436 """
437
438 __tablename__ = "incident_management_case_event"
439
440 id: Optional[int] = Field(default=None, primary_key=True)
441 case_id: int = Field(foreign_key="incident_management_case.id", nullable=False, index=True)
442 event_type: str = Field(
443 max_length=64,
444 nullable=False,
445 index=True,
446 description=(
447 "One of: case_created, case_status_changed, case_assigned, case_escalated, "
448 "alert_linked, alert_unlinked, comment_added, template_applied, "
449 "task_added, task_status_changed, task_commented"
450 ),
451 )
452 actor: str = Field(max_length=100, nullable=False, description="user_name that performed the action")
453 timestamp: datetime = Field(default_factory=datetime.utcnow, index=True)
454 payload: Optional[Dict] = Field(
455 sa_column=Column(JSON, nullable=True),
456 description="Event-type-specific JSON payload (e.g., from_status/to_status, alert_id, task_id).",
457 )
458
459
460 class TagAccessSettings(SQLModel, table=True):
461 """Global settings for tag-based access control."""
462
463 __tablename__ = "incident_management_tag_access_settings"
464 id: Optional[int] = Field(default=None, primary_key=True)
465
466 # Whether tag-based RBAC is enabled (False = current behavior, no filtering)
467 enabled: bool = Field(default=False)
468
469 # How to handle untagged alerts: "admin_only", "visible_to_all", "default_tag"
470 untagged_alert_behavior: str = Field(default="visible_to_all", max_length=50)
471
472 # If untagged_alert_behavior is "default_tag", which tag to use
473 default_tag_id: Optional[int] = Field(
474 foreign_key="incident_management_alerttag.id",
475 nullable=True,
476 )
477
478 # Last modified
479 updated_at: datetime = Field(default_factory=datetime.utcnow)
480 updated_by: Optional[str] = Field(max_length=100, nullable=True)