@cryptotaxi247 / CoPilot / commits / f5eef97b

chore: pydantic 1 → 2 + sqlmodel + SQLAlchemy 1.4 → 2.0 (atomic) (#849)

Lifts the protective <2 pins added in #844 for pydantic, SQLAlchemy, and sqlmodel. The three are coupled — sqlmodel<0.0.10 requires pydantic<2 + SQLAlchemy<2, and sqlmodel>=0.0.10 requires both at v2 — so they have to be bumped atomically. Versions: pydantic[email] 1.10.26 → 2.13.4 pydantic-core (added) → 2.46.4 SQLAlchemy 1.4.54 → 2.0.49 sqlmodel 0.0.9 → 0.0.38 How the migration was structured: 1. bump-pydantic codemod (v0.8.0) ran against backend/, refactored 92 files mechanically: - 50 @validator → @field_validator (+ @classmethod) - 21 @root_validator(pre=*) → @model_validator(mode='before') - 111 class Config: → model_config = ConfigDict(...) - All Config option renames: orm_mode → from_attributes, schema_extra → json_schema_extra, allow_population_by_field_name → populate_by_name, etc. 2. Hand-fixed cases the codemod can't safely auto-migrate: - 3x @root_validator (no pre=) — pydantic 2 requires skip_on_failure=True or migration to @model_validator(mode='after'). Migrated to model_validator with self-style signatures: app/connectors/shuffle/schema/integrations.py (1) app/integrations/scoutsuite/schema/scoutsuite.py (2) - 1x duplicate `from_attributes=True` codemod artifact: app/connectors/schema.py:36 - 1x empty `class Config: pass` block (no-op in v2): app/db/universal_models.py removed - 2x pre-existing typo `URI = str = Field(...)` (should be `URI: str = Field(...)`); pydantic 2's stricter annotation rules surfaced this as `Field 'URI' requires a type annotation`: app/integrations/mimecast/schema/mimecast.py app/integrations/modules/schema/mimecast.py 3. SQLAlchemy 2 / sqlmodel 0.0.38 incompatibilities: - 33 fields used `Field(sa_column=Type, nullable=...)` or `Field(sa_column=Column(Type), nullable=...)`, which sqlmodel 0.0.10+ rejects with "Passing nullable is not supported when also passing a sa_column". Moved nullable into the Column() constructor in 4 files (paren-aware fixer): backend/app/db/universal_models.py (8 sites) backend/app/incidents/models.py (16 sites) backend/app/integrations/github_audit/model.py (7 sites) backend/app/connectors/wazuh_indexer/models/sigma.py (2 sites) - sigma.py also needed Column added to its sqlmodel imports - 4x `conn.execute("commit")` no longer accepts a raw string in SQLAlchemy 2 — wrapped with `text()`: backend/app/db/db_setup.py Verified: - docker build of backend/Dockerfile succeeds - backend boots cleanly: alembic upgrade head, all seed funcs, "Application startup complete" + "Scheduler started." in logs - all ~50 routers import without ModuleNotFoundError - smoke tests pass: Password.generate() bcrypt round-trip UserInput @field_validator (codemod-migrated) Pydantic 2 .model_dump() SQLAlchemy 2 select() statement SQLModel table creation ExecuteWorkflowRequest @model_validator (hand-migrated, raises HTTPException on missing customer_code) AWSScoutSuiteReportRequest @model_validator (hand-migrated) - existing MySQL test data preserved across the rebuild - OpenAPI shape: 540 paths unchanged; 902→901 schemas (only cosmetic — pydantic 2 collapses duplicate-name classes from `app__connectors__sublime__schema__alerts__Data` → `Data-Input`/`Data-Output`) Out of scope (compat-shimmed, work via deprecation aliases — clean up in a follow-up): - 10 @validator decorators using always=, each_item=, allow_reuse=, or values= parameter. Pydantic 2 treats @validator as deprecated but functional. Migrating to @model_validator(mode='after') with `self` access is straightforward but requires per-site judgment. - 261 .dict() calls — work as deprecated aliases for .model_dump(). - 1 update_forward_refs() call → model_rebuild() rename. - 2 SQLAlchemy 2 SAWarnings about overlapping FKs in NotificationDispatchLog/CustomerNotificationRoute — relationships deliberately use one-way back_populates to avoid AsyncSession MissingGreenlet issues; adding `overlaps="..."` would silence the warning without changing behavior. - 2 FastAPI on_event deprecation warnings (move to lifespan handlers). Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

taylorcopilot committed May 7, 2026 at 19:38 UTC f5eef97bfc3c6dcc5853e43de745e07cbd3714a1
98 files changed +1306 -1550
backend/app/active_response/schema/active_response.py
+24 -27
@@ -1,14 +1,12 @@
1 from enum import Enum
2 -from typing import Any
2 +from typing import Literal, Any
3 from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 from fastapi import HTTPException
8 -from pydantic import BaseModel
8 +from pydantic import field_validator, model_validator, ConfigDict, BaseModel
9 from pydantic import Field
10 -from pydantic import root_validator
11 -from pydantic import validator
10
11
12 class ActiveResponsesSupported(Enum):
@@ -31,9 +29,9 @@ class ActiveResponseDetails(BaseModel):
29 name: str
30 description: str
31 markdown_content: str
34 -
35 - class Config:
36 - json_encoders = {str: lambda v: v.encode("utf-8", "ignore").decode("utf-8")}
32 + # TODO[pydantic]: The following keys were removed: `json_encoders`.
33 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.
34 + model_config = ConfigDict(json_encoders={str: lambda v: v.encode("utf-8", "ignore").decode("utf-8")})
35
36
37 class ActiveResponseDetailsResponse(BaseModel):
@@ -50,8 +48,7 @@ class AlertAction(str, Enum):
48
49
50 class BaseModelWithEnum(BaseModel):
53 - class Config:
54 - use_enum_values = True
51 + model_config = ConfigDict(use_enum_values=True)
52
53
54 class WindowsFirewallAlert(BaseModelWithEnum):
@@ -65,7 +62,7 @@ class LinuxFirewallAlert(BaseModelWithEnum):
62
63
64 class SysmonConfigReloadAlert(BaseModelWithEnum):
68 - action: AlertAction = Field(default=AlertAction.sysmon_config_reload, const=True)
65 + action: Literal[AlertAction.sysmon_config_reload] = AlertAction.sysmon_config_reload
66
67
68 class ActiveResponseCommand(str, Enum):
@@ -91,9 +88,10 @@ class ActiveResponseCommand(str, Enum):
88
89 class ParamsModel(BaseModel):
90 wait_for_complete: bool
94 - agents_list: Optional[List[str]]
91 + agents_list: Optional[List[str]] = None
92
96 - @validator("agents_list", pre=True)
93 + @field_validator("agents_list", mode="before")
94 + @classmethod
95 def check_agents_list(cls, v):
96 if v == ["*"]:
97 return []
@@ -101,14 +99,15 @@ class ParamsModel(BaseModel):
99
100
101 class InvokeActiveResponseRequest(BaseModel):
104 - endpoint: str = Field("/active-response", const=True)
102 + endpoint: Literal["/active-response"] = "/active-response"
103 arguments: list[str] = Field(default_factory=list)
104 command: ActiveResponseCommand
107 - custom: bool = Field(True, const=True)
105 + custom: Literal[True] = True
106 alert: Dict[str, Any]
107 params: ParamsModel
108
111 - @root_validator(pre=True)
109 + @model_validator(mode="before")
110 + @classmethod
111 def create_alert(cls, values):
112 command = values.get("command")
113 alert = values.get("alert")
@@ -122,18 +121,16 @@ class InvokeActiveResponseRequest(BaseModel):
121 raise HTTPException(status_code=400, detail="Invalid command for alert")
122
123 return values
125 -
126 - class Config:
127 - schema_extra = {
128 - "example": {
129 - "endpoint": "/active-response",
130 - "arguments": [],
131 - "command": "windows_firewall",
132 - "custom": True,
133 - "alert": {"action": "block", "ip": "1.1.1.1"},
134 - "params": {"wait_for_complete": True, "agents_list": ["032"]},
135 - },
136 - }
124 + model_config = ConfigDict(json_schema_extra={
125 + "example": {
126 + "endpoint": "/active-response",
127 + "arguments": [],
128 + "command": "windows_firewall",
129 + "custom": True,
130 + "alert": {"action": "block", "ip": "1.1.1.1"},
131 + "params": {"wait_for_complete": True, "agents_list": ["032"]},
132 + },
133 + })
134
135
136 class InvokeActiveResponseResponse(BaseModel):
backend/app/active_response/schema/graylog.py
+79 -89
@@ -4,7 +4,7 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
7 +from pydantic import ConfigDict, BaseModel
8 from pydantic import Field
9
10
@@ -23,10 +23,7 @@ class GraylogEventFields(BaseModel):
23 VALUE: str
24 # Allow additional fields
25 additional_fields: Dict[str, Any] = Field(default_factory=dict, alias="__extra__")
26 -
27 - class Config:
28 - extra = "allow" # Allow extra fields
29 - populate_by_name = True # Process alias fields
26 + model_config = ConfigDict(extra="allow", populate_by_name=True)
27
28
29 class GraylogThresholdEventFields(BaseModel):
@@ -36,10 +33,7 @@ class GraylogThresholdEventFields(BaseModel):
33 ASSET_NAME: Optional[str] = None
34 # Allow additional fields
35 additional_fields: Dict[str, Any] = Field(default_factory=dict, alias="__extra__")
39 -
40 - class Config:
41 - extra = "allow"
42 - populate_by_name = True
36 + model_config = ConfigDict(extra="allow", populate_by_name=True)
37
38
39 class GraylogEvent(BaseModel):
@@ -75,48 +69,46 @@ class GraylogEventNotification(BaseModel):
69 job_trigger_id: str
70 event: GraylogEvent
71 backlog: List[Any] = Field(default_factory=list)
78 -
79 - class Config:
80 - schema_extra = {
81 - "example": {
82 - "event_definition_id": "67c78b93cf26aa2045bdc2ea",
72 + model_config = ConfigDict(json_schema_extra={
73 + "example": {
74 + "event_definition_id": "67c78b93cf26aa2045bdc2ea",
75 + "event_definition_type": "aggregation-v1",
76 + "event_definition_title": "ACTIVE RESPONSE WEBSERVER THREAT INTEL",
77 + "event_definition_description": "",
78 + "job_definition_id": "67c78befcf26aa2045bdc4e9",
79 + "job_trigger_id": "67c78c0bcf26aa2045bdc5b5",
80 + "event": {
81 + "id": "01JNHQP35THD48VH8601F6EMHE",
82 "event_definition_type": "aggregation-v1",
84 - "event_definition_title": "ACTIVE RESPONSE WEBSERVER THREAT INTEL",
85 - "event_definition_description": "",
86 - "job_definition_id": "67c78befcf26aa2045bdc4e9",
87 - "job_trigger_id": "67c78c0bcf26aa2045bdc5b5",
88 - "event": {
89 - "id": "01JNHQP35THD48VH8601F6EMHE",
90 - "event_definition_type": "aggregation-v1",
91 - "event_definition_id": "67c78b93cf26aa2045bdc2ea",
92 - "origin_context": "urn:graylog:message:es:graylog-01_3:766b70b4-f94f-11ef-be9a-005056b6f13d",
93 - "timestamp": "2025-03-04T23:21:55.840Z",
94 - "timestamp_processing": "2025-03-04T23:26:03.450Z",
95 - "timerange_start": None,
96 - "timerange_end": None,
97 - "streams": [],
98 - "source_streams": ["679945aa7c4dd06afcef5feb", "679945a47c4dd06afcef5ef3"],
99 - "message": "ACTIVE RESPONSE WEBSERVER THREAT INTEL",
100 - "source": "soc-grlog01",
101 - "key_tuple": [],
102 - "key": "",
103 - "priority": 2,
104 - "scores": {},
105 - "associated_assets": [],
106 - "alert": True,
107 - "fields": {"COMMAND": "domain_sinkhole", "AGENT_ID": "032", "ACTION": "sinkhole", "VALUE": "example.com"},
108 - "group_by_fields": {},
109 - "replay_info": {
110 - "timerange_start": "2025-03-04T23:21:03.360Z",
111 - "timerange_end": "2025-03-04T23:26:03.360Z",
112 - "query": "_exists_:threat_intel_score",
113 - "streams": ["679945aa7c4dd06afcef5feb", "679945a47c4dd06afcef5ef3"],
114 - "filters": [],
115 - },
83 + "event_definition_id": "67c78b93cf26aa2045bdc2ea",
84 + "origin_context": "urn:graylog:message:es:graylog-01_3:766b70b4-f94f-11ef-be9a-005056b6f13d",
85 + "timestamp": "2025-03-04T23:21:55.840Z",
86 + "timestamp_processing": "2025-03-04T23:26:03.450Z",
87 + "timerange_start": None,
88 + "timerange_end": None,
89 + "streams": [],
90 + "source_streams": ["679945aa7c4dd06afcef5feb", "679945a47c4dd06afcef5ef3"],
91 + "message": "ACTIVE RESPONSE WEBSERVER THREAT INTEL",
92 + "source": "soc-grlog01",
93 + "key_tuple": [],
94 + "key": "",
95 + "priority": 2,
96 + "scores": {},
97 + "associated_assets": [],
98 + "alert": True,
99 + "fields": {"COMMAND": "domain_sinkhole", "AGENT_ID": "032", "ACTION": "sinkhole", "VALUE": "example.com"},
100 + "group_by_fields": {},
101 + "replay_info": {
102 + "timerange_start": "2025-03-04T23:21:03.360Z",
103 + "timerange_end": "2025-03-04T23:26:03.360Z",
104 + "query": "_exists_:threat_intel_score",
105 + "streams": ["679945aa7c4dd06afcef5feb", "679945a47c4dd06afcef5ef3"],
106 + "filters": [],
107 },
117 - "backlog": [],
108 },
119 - }
109 + "backlog": [],
110 + },
111 + })
112
113
114 class GraylogThresholdEvent(BaseModel):
@@ -152,49 +144,47 @@ class GraylogThresholdEventNotification(BaseModel):
144 job_trigger_id: str
145 event: GraylogThresholdEvent
146 backlog: List[Any] = Field(default_factory=list)
155 -
156 - class Config:
157 - schema_extra = {
158 - "example": {
159 - "event_definition_id": "67b6687184088513bdc6cd1b",
147 + model_config = ConfigDict(json_schema_extra={
148 + "example": {
149 + "event_definition_id": "67b6687184088513bdc6cd1b",
150 + "event_definition_type": "aggregation-v1",
151 + "event_definition_title": "DELL SWITCHES - MULTIPLE AUTH FAILURES",
152 + "event_definition_description": "DELL SWITCHES - MULTIPLE AUTH FAILURES",
153 + "job_definition_id": "67dde9bc84088513bde5ce29",
154 + "job_trigger_id": "67ddeabf84088513bde5d283",
155 + "event": {
156 + "id": "01JPXDSZ8AECWZ88HR9JQPYHJP",
157 "event_definition_type": "aggregation-v1",
161 - "event_definition_title": "DELL SWITCHES - MULTIPLE AUTH FAILURES",
162 - "event_definition_description": "DELL SWITCHES - MULTIPLE AUTH FAILURES",
163 - "job_definition_id": "67dde9bc84088513bde5ce29",
164 - "job_trigger_id": "67ddeabf84088513bde5d283",
165 - "event": {
166 - "id": "01JPXDSZ8AECWZ88HR9JQPYHJP",
167 - "event_definition_type": "aggregation-v1",
168 - "event_definition_id": "67b6687184088513bdc6cd1b",
169 - "origin_context": None,
170 - "timestamp": "2025-03-21T22:39:54.219Z",
171 - "timestamp_processing": "2025-03-21T22:39:59.754Z",
158 + "event_definition_id": "67b6687184088513bdc6cd1b",
159 + "origin_context": None,
160 + "timestamp": "2025-03-21T22:39:54.219Z",
161 + "timestamp_processing": "2025-03-21T22:39:59.754Z",
162 + "timerange_start": "2024-08-25T14:39:54.219Z",
163 + "timerange_end": "2025-03-21T22:39:54.219Z",
164 + "streams": [],
165 + "source_streams": ["67abcb0a84088513bdc09e32"],
166 + "message": "DELL SWITCHES - MULTIPLE AUTH FAILURES: 10.0.64.233 - count()=14.0",
167 + "source": "soc-grlog02",
168 + "key_tuple": [],
169 + "key": "",
170 + "priority": 2,
171 + "scores": {},
172 + "associated_assets": [],
173 + "alert": True,
174 + "fields": {
175 + "CUSTOMER_CODE": "6bdd96a0-06a5-11f0-a499-005056b6c109",
176 + "SOURCE": "DELLSWITCH",
177 + "ALERT_DESCRIPTION": "THIS IS A TEST",
178 + },
179 + "group_by_fields": {"source": "10.0.64.233"},
180 + "replay_info": {
181 "timerange_start": "2024-08-25T14:39:54.219Z",
182 "timerange_end": "2025-03-21T22:39:54.219Z",
174 - "streams": [],
175 - "source_streams": ["67abcb0a84088513bdc09e32"],
176 - "message": "DELL SWITCHES - MULTIPLE AUTH FAILURES: 10.0.64.233 - count()=14.0",
177 - "source": "soc-grlog02",
178 - "key_tuple": [],
179 - "key": "",
180 - "priority": 2,
181 - "scores": {},
182 - "associated_assets": [],
183 - "alert": True,
184 - "fields": {
185 - "CUSTOMER_CODE": "6bdd96a0-06a5-11f0-a499-005056b6c109",
186 - "SOURCE": "DELLSWITCH",
187 - "ALERT_DESCRIPTION": "THIS IS A TEST",
188 - },
189 - "group_by_fields": {"source": "10.0.64.233"},
190 - "replay_info": {
191 - "timerange_start": "2024-08-25T14:39:54.219Z",
192 - "timerange_end": "2025-03-21T22:39:54.219Z",
193 - "query": '"An invalid user tried to login"',
194 - "streams": ["67abcb0a84088513bdc09e32"],
195 - "filters": [],
196 - },
183 + "query": '"An invalid user tried to login"',
184 + "streams": ["67abcb0a84088513bdc09e32"],
185 + "filters": [],
186 },
198 - "backlog": [],
187 },
200 - }
188 + "backlog": [],
189 + },
190 + })
backend/app/agents/sca/schema/sca.py
+2 -4
@@ -4,7 +4,7 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
7 +from pydantic import ConfigDict, BaseModel
8 from pydantic import Field
9
10
@@ -26,9 +26,7 @@ class AgentScaOverviewItem(BaseModel):
26 end_scan: str
27 references: Optional[str] = None
28 hash_file: Optional[str] = None
29 -
30 - class Config:
31 - allow_population_by_field_name = True
29 + model_config = ConfigDict(populate_by_name=True)
30
31
32 class ScaOverviewResponse(BaseModel):
backend/app/agents/velociraptor/schema/agents.py
+2 -4
@@ -2,7 +2,7 @@ from datetime import datetime
2 from typing import List
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import ConfigDict, BaseModel
6 from pydantic import Field
7
8
@@ -16,9 +16,7 @@ class VelociraptorAgent(BaseModel):
16 def client_last_seen_as_datetime(self):
17 dt = datetime.strptime(self.client_last_seen, "%Y-%m-%dT%H:%M:%S%z")
18 return dt.replace(tzinfo=None)
19 -
20 - class Config:
21 - allow_population_by_field_name = True
19 + model_config = ConfigDict(populate_by_name=True)
20
21
22 class VelociraptorAgentInformation(BaseModel):
backend/app/agents/vulnerabilities/schema/vulnerabilities.py
+2 -4
@@ -4,7 +4,7 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
7 +from pydantic import ConfigDict, BaseModel
8 from pydantic import Field
9
10
@@ -21,9 +21,7 @@ class WazuhVulnerabilityData(BaseModel):
21 package_name: Optional[str] = None
22 package_version: Optional[str] = None
23 package_architecture: Optional[str] = None
24 -
25 - class Config:
26 - allow_population_by_field_name = True
24 + model_config = ConfigDict(populate_by_name=True)
25
26
27 class AgentVulnerabilityOut(BaseModel):
backend/app/agents/wazuh/schema/agents.py
+18 -22
@@ -3,7 +3,7 @@ from enum import Enum
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import ConfigDict, BaseModel
7 from pydantic import Field
8
9
@@ -29,37 +29,33 @@ class WazuhAgent(BaseModel):
29 def agent_last_seen_as_datetime(self):
30 dt = datetime.strptime(self.agent_last_seen, "%Y-%m-%dT%H:%M:%S%z")
31 return dt.replace(tzinfo=None)
32 -
33 - class Config:
34 - allow_population_by_field_name = True
32 + model_config = ConfigDict(populate_by_name=True)
33
34
35 class WazuhAgentsList(BaseModel):
36 agents: List[WazuhAgent]
37 success: bool
38 message: str
41 -
42 - class Config:
43 - allow_population_by_field_name = True
39 + model_config = ConfigDict(populate_by_name=True)
40
41
42 class WazuhAgentVulnerabilities(BaseModel):
47 - severity: Optional[str]
48 - version: Optional[str]
49 - type: Optional[str]
50 - name: Optional[str]
51 - external_references: Optional[List[str]]
52 - detection_time: Optional[str]
53 - cvss3_score: Optional[float]
54 - published: Optional[str]
55 - architecture: Optional[str]
56 - cve: Optional[str]
57 - status: Optional[str]
58 - title: Optional[str]
43 + severity: Optional[str] = None
44 + version: Optional[str] = None
45 + type: Optional[str] = None
46 + name: Optional[str] = None
47 + external_references: Optional[List[str]] = None
48 + detection_time: Optional[str] = None
49 + cvss3_score: Optional[float] = None
50 + published: Optional[str] = None
51 + architecture: Optional[str] = None
52 + cve: Optional[str] = None
53 + status: Optional[str] = None
54 + title: Optional[str] = None
55
56
57 class WazuhAgentVulnerabilitiesResponse(BaseModel):
62 - vulnerabilities: Optional[List[WazuhAgentVulnerabilities]]
58 + vulnerabilities: Optional[List[WazuhAgentVulnerabilities]] = None
59 success: bool
60 message: str
61
@@ -80,7 +76,7 @@ class WazuhAgentScaResults(BaseModel):
76
77
78 class WazuhAgentScaResponse(BaseModel):
83 - sca: Optional[List[WazuhAgentScaResults]]
79 + sca: Optional[List[WazuhAgentScaResults]] = None
80 success: bool
81 message: str
82
@@ -120,6 +116,6 @@ class WazuhAgentScaPolicyResults(BaseModel):
116
117
118 class WazuhAgentScaPolicyResultsResponse(BaseModel):
123 - sca_policy_results: Optional[List[WazuhAgentScaPolicyResults]]
119 + sca_policy_results: Optional[List[WazuhAgentScaPolicyResults]] = None
120 success: bool
121 message: str
backend/app/agents/wazuh/syscollector/schema/packages.py
+3 -8
@@ -3,7 +3,7 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import ConfigDict, BaseModel
7 from pydantic import Field
8
9
@@ -21,9 +21,7 @@ class PackageItem(BaseModel):
21 vendor: Optional[str] = None
22 version: Optional[str] = None
23 agent_id: Optional[str] = None
24 -
25 - class Config:
26 - extra = "allow"
24 + model_config = ConfigDict(extra="allow")
25
26
27 class AgentPackagesResponse(BaseModel):
@@ -65,10 +63,7 @@ class IndexerPackageItem(BaseModel):
63 id: Optional[str] = Field(None, alias="_id")
64 agent: Optional[IndexerPackageAgent] = None
65 package: Optional[IndexerPackageDetail] = None
68 -
69 - class Config:
70 - populate_by_name = True
71 - extra = "allow"
66 + model_config = ConfigDict(populate_by_name=True, extra="allow")
67
68
69 class IndexerPackagesResponse(BaseModel):
backend/app/ai_analyst/schema/ai_analyst.py
+34 -30
@@ -4,9 +4,8 @@ from enum import Enum
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
7 +from pydantic import field_validator, BaseModel
8 from pydantic import Field
9 -from pydantic import validator
9
10 # --- Enums ---
11
@@ -107,7 +106,8 @@ class SubmitReportRequest(BaseModel):
106 report_markdown: Optional[str] = Field(None, description="Full investigation report in Markdown")
107 recommended_actions: Optional[str] = Field(None, description="Recommended response actions")
108
110 - @validator("summary", "report_markdown", "recommended_actions", pre=True)
109 + @field_validator("summary", "report_markdown", "recommended_actions", mode="before")
110 + @classmethod
111 def strip_control_characters(cls, v):
112 """Strip control characters that break JSON serialization.
113 Preserves newline (0x0a), carriage return (0x0d), and tab (0x09).
@@ -140,13 +140,13 @@ class JobResponse(BaseModel):
140 alert_id: int
141 customer_code: str
142 status: str
143 - alert_type: Optional[str]
143 + alert_type: Optional[str] = None
144 triggered_by: str
145 - template_used: Optional[str]
145 + template_used: Optional[str] = None
146 created_at: datetime
147 - started_at: Optional[datetime]
148 - completed_at: Optional[datetime]
149 - error_message: Optional[str]
147 + started_at: Optional[datetime] = None
148 + completed_at: Optional[datetime] = None
149 + error_message: Optional[str] = None
150
151
152 class ReportResponse(BaseModel):
@@ -154,10 +154,10 @@ class ReportResponse(BaseModel):
154 job_id: str
155 alert_id: int
156 customer_code: str
157 - severity_assessment: Optional[str]
158 - summary: Optional[str]
159 - report_markdown: Optional[str]
160 - recommended_actions: Optional[str]
157 + severity_assessment: Optional[str] = None
158 + summary: Optional[str] = None
159 + report_markdown: Optional[str] = None
160 + recommended_actions: Optional[str] = None
161 created_at: datetime
162
163
@@ -169,8 +169,8 @@ class IocResponse(BaseModel):
169 ioc_value: str
170 ioc_type: str
171 vt_verdict: str
172 - vt_score: Optional[str]
173 - details: Optional[str]
172 + vt_score: Optional[str] = None
173 + details: Optional[str] = None
174 created_at: datetime
175
176
@@ -223,7 +223,7 @@ class AlertWithReportResponse(BaseModel):
223 customer_code: str
224 status: str
225 source: str
226 - assigned_to: Optional[str]
226 + assigned_to: Optional[str] = None
227 alert_creation_time: datetime
228 report: ReportResponse
229
@@ -250,7 +250,8 @@ class IocVerdictCorrection(BaseModel):
250 verdict_correct: bool = Field(..., description="True if the original VT verdict was correct")
251 note: Optional[str] = Field(None, max_length=2000, description="Optional reviewer note")
252
253 - @validator("note", pre=True)
253 + @field_validator("note", mode="before")
254 + @classmethod
255 def strip_control_characters_note(cls, v):
256 if v is None:
257 return v
@@ -268,7 +269,8 @@ class SubmitReviewRequest(BaseModel):
269 suggested_edits: Optional[str] = Field(None, description="Free-text suggested prompt / template edits")
270 ioc_reviews: List[IocVerdictCorrection] = Field(default_factory=list, description="Per-IOC verdict corrections")
271
271 - @validator("missing_steps", "suggested_edits", pre=True)
272 + @field_validator("missing_steps", "suggested_edits", mode="before")
273 + @classmethod
274 def strip_control_characters(cls, v):
275 if v is None:
276 return v
@@ -280,7 +282,7 @@ class IocReviewResponse(BaseModel):
282 review_id: int
283 ioc_id: int
284 verdict_correct: bool
283 - note: Optional[str]
285 + note: Optional[str] = None
286 created_at: datetime
287
288
@@ -290,14 +292,14 @@ class ReviewResponse(BaseModel):
292 alert_id: int
293 customer_code: str
294 reviewer_user_id: int
293 - overall_verdict: Optional[str]
294 - template_choice: Optional[str]
295 - template_used: Optional[str]
296 - rating_instructions: Optional[int]
297 - rating_artifacts: Optional[int]
298 - rating_severity: Optional[int]
299 - missing_steps: Optional[str]
300 - suggested_edits: Optional[str]
295 + overall_verdict: Optional[str] = None
296 + template_choice: Optional[str] = None
297 + template_used: Optional[str] = None
298 + rating_instructions: Optional[int] = None
299 + rating_artifacts: Optional[int] = None
300 + rating_severity: Optional[int] = None
301 + missing_steps: Optional[str] = None
302 + suggested_edits: Optional[str] = None
303 created_at: datetime
304 updated_at: Optional[datetime] = None
305 ioc_reviews: List[IocReviewResponse] = Field(default_factory=list)
@@ -331,7 +333,8 @@ class QueuePalaceLessonRequest(BaseModel):
333 durability: Durability = Field(default=Durability.DURABLE, description="one_off = single-session hint, durable = persistent knowledge")
334 review_id: Optional[int] = Field(None, description="Optional review.id this lesson was born from")
335
334 - @validator("lesson_text", pre=True)
336 + @field_validator("lesson_text", mode="before")
337 + @classmethod
338 def strip_control_characters_lesson(cls, v):
339 if v is None:
340 return v
@@ -340,13 +343,13 @@ class QueuePalaceLessonRequest(BaseModel):
343
344 class PalaceLessonResponse(BaseModel):
345 id: int
343 - review_id: Optional[int]
346 + review_id: Optional[int] = None
347 customer_code: str
348 lesson_type: str
349 lesson_text: str
350 durability: str
351 status: str
349 - ingested_at: Optional[datetime]
352 + ingested_at: Optional[datetime] = None
353 created_at: datetime
354
355
@@ -365,7 +368,8 @@ class ReplayRequest(BaseModel):
368 customer_code: str = Field(..., max_length=64, description="Customer code for the alert")
369 sender: str = Field(default="copilot-replay", max_length=64, description="Sender identifier for audit")
370
368 - @validator("template_override")
371 + @field_validator("template_override")
372 + @classmethod
373 def validate_template_filename(cls, v):
374 if not re.match(r"^[a-zA-Z0-9._-]+\.txt$", v):
375 raise ValueError("template_override must be a filename matching ^[a-zA-Z0-9._-]+\\.txt$")
backend/app/auth/models/users.py
+7 -5
@@ -7,9 +7,8 @@ from typing import List
7 from typing import Optional
8
9 import bcrypt
10 -from pydantic import BaseModel
10 +from pydantic import field_validator, BaseModel
11 from pydantic import EmailStr
12 -from pydantic import validator
12 from sqlmodel import Field
13 from sqlmodel import Relationship
14 from sqlmodel import SQLModel
@@ -97,7 +96,8 @@ class UserInput(SQLModel):
96 foreign_key="role.id",
97 )
98
100 - @validator("role_id")
99 + @field_validator("role_id")
100 + @classmethod
101 def check_role_id(cls, value):
102 if value not in [e.value for e in RoleEnum]:
103 raise ValueError("Invalid role ID")
@@ -119,7 +119,8 @@ class Password(BaseModel):
119 hashed: str # Holds the hashed password
120 plain: str # Holds the plain password
121
122 - @validator("length")
122 + @field_validator("length")
123 + @classmethod
124 def validate_length(cls, value):
125 if value < 8 or value > 128:
126 raise ValueError("Password length must be between 8 and 128 characters.")
@@ -176,7 +177,8 @@ class PasswordResetToken(BaseModel):
177 reset_token: str
178 new_password: str
179
179 - @validator("new_password")
180 + @field_validator("new_password")
181 + @classmethod
182 def validate_password(cls, password):
183 if len(password) < 8 or len(password) > 256:
184 raise ValueError("Password length must be between 8 and 256 characters.")
backend/app/connectors/cortex/schema/analyzers.py
+2
@@ -35,6 +35,8 @@ class RunAnalyzerBody(BaseModel):
35 description="Data type determined after validation",
36 )
37
38 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
39 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
40 @validator("analyzer_data", pre=True, always=True)
41 def validate_and_set_data_type(cls, value: str, values: dict) -> str:
42 is_valid, data_type = cls.is_valid_datatype(value)
backend/app/connectors/grafana/schema/dashboards.py
+2
@@ -155,6 +155,8 @@ class DashboardProvisionRequest(BaseModel):
155 description="URL of the Grafana instance for the links within the dashboards.",
156 )
157
158 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
159 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
160 @validator("dashboards", each_item=True)
161 def check_dashboard_exists(cls, e):
162 valid_dashboards = {
backend/app/connectors/grafana/schema/reporting.py
+9 -9
@@ -2,9 +2,8 @@ from typing import List
2 from typing import Optional
3
4 from fastapi import HTTPException
5 -from pydantic import BaseModel
5 +from pydantic import field_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import validator
7
8
9 class GrafanaOrganizations(BaseModel):
@@ -53,7 +52,7 @@ class Annotation(BaseModel):
52
53 class Threshold(BaseModel):
54 color: str
56 - value: Optional[float]
55 + value: Optional[float] = None
56
57
58 class FieldConfigDefaults(BaseModel):
@@ -164,7 +163,8 @@ class TimeRange(BaseModel):
163 value: int
164 unit: str
165
167 - @validator("unit")
166 + @field_validator("unit")
167 + @classmethod
168 def validate_unit(cls, unit):
169 valid_units = ["m", "h", "d"]
170 if unit not in valid_units:
@@ -211,19 +211,19 @@ class RequestRow(BaseModel):
211
212
213 class GenerateReportRequest(BaseModel):
214 - company_name: str = Field(..., description="Company name", example="SOC Fortress")
215 - timerange_text: str = Field(..., description="Time range text", example="Last 7 days")
214 + company_name: str = Field(..., description="Company name", examples=["SOC Fortress"])
215 + timerange_text: str = Field(..., description="Time range text", examples=["Last 7 days"])
216 logo_base64: str = Field(
217 ...,
218 description="Base64 encoded logo",
219 - example="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABjklEQVRIS+2Vv0oDQRDG",
219 + examples=["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABjklEQVRIS+2Vv0oDQRDG"],
220 )
221 timerange: str = Field(..., description="Time range for the report")
222 # rows: List[RequestRow] = Field(..., description="Rows in the report")
223 rows: List[RequestRow] = Field(
224 ...,
225 description="Rows in the report",
226 - example=[
226 + examples=[[
227 {
228 "id": 1710437961108,
229 "panels": [
@@ -266,7 +266,7 @@ class GenerateReportRequest(BaseModel):
266 },
267 ],
268 },
269 - ],
269 + ]],
270 )
271
272
backend/app/connectors/graylog/schema/collector.py
+23 -23
@@ -62,52 +62,52 @@ class GraylogIndicesResponse(BaseModel):
62 class ConfiguredInputAttributes(BaseModel):
63 recv_buffer_size: int
64 tcp_keepalive: Optional[bool] = Field(None, description="TCP keepalive")
65 - use_null_delimiter: Optional[bool]
65 + use_null_delimiter: Optional[bool] = None
66 number_worker_threads: int
67 - tls_client_auth_cert_file: Optional[str]
68 - force_rdns: Optional[bool]
67 + tls_client_auth_cert_file: Optional[str] = None
68 + force_rdns: Optional[bool] = None
69 bind_address: str
70 - tls_cert_file: Optional[str]
71 - store_full_message: Optional[bool]
72 - expand_structured_data: Optional[bool]
70 + tls_cert_file: Optional[str] = None
71 + store_full_message: Optional[bool] = None
72 + expand_structured_data: Optional[bool] = None
73 port: int
74 - tls_key_file: Optional[str]
74 + tls_key_file: Optional[str] = None
75 tls_enable: Optional[bool] = Field(None, description="TLS is enabled")
76 - tls_key_password: Optional[str]
77 - max_message_size: Optional[int]
76 + tls_key_password: Optional[str] = None
77 + max_message_size: Optional[int] = None
78 tls_client_auth: Optional[str] = Field(None, description="TLS client authentication")
79 - override_source: Optional[str]
80 - charset_name: Optional[str]
81 - allow_override_date: Optional[bool]
79 + override_source: Optional[str] = None
80 + charset_name: Optional[str] = None
81 + allow_override_date: Optional[bool] = None
82
83
84 class ConfiguredInput(BaseModel):
85 title: str
86 global_field: bool = Field(alias="global")
87 name: str
88 - content_pack: Optional[str]
88 + content_pack: Optional[str] = None
89 created_at: str
90 type: str
91 creator_user_id: str
92 attributes: ConfiguredInputAttributes
93 static_fields: Dict[str, str]
94 - node: Optional[str]
94 + node: Optional[str] = None
95 id: str
96
97
98 class MessageInputAttributes(BaseModel):
99 recv_buffer_size: int
100 tcp_keepalive: Optional[bool] = Field(None, description="TCP keepalive")
101 - use_null_delimiter: Optional[bool]
101 + use_null_delimiter: Optional[bool] = None
102 number_worker_threads: int
103 - tls_client_auth_cert_file: Optional[str]
103 + tls_client_auth_cert_file: Optional[str] = None
104 bind_address: str
105 - tls_cert_file: Optional[str]
105 + tls_cert_file: Optional[str] = None
106 port: int
107 - tls_key_file: Optional[str]
107 + tls_key_file: Optional[str] = None
108 tls_enable: Optional[bool] = Field(None, description="TLS is enabled")
109 - tls_key_password: Optional[str]
110 - max_message_size: Optional[int]
109 + tls_key_password: Optional[str] = None
110 + max_message_size: Optional[int] = None
111 tls_client_auth: Optional[str] = Field(None, description="TLS client authentication")
112
113
@@ -115,13 +115,13 @@ class MessageInput(BaseModel):
115 title: str
116 global_field: bool = Field(alias="global")
117 name: str
118 - content_pack: Optional[str]
118 + content_pack: Optional[str] = None
119 created_at: str
120 type: str
121 creator_user_id: str
122 attributes: MessageInputAttributes
123 static_fields: Dict[str, str]
124 - node: Optional[str]
124 + node: Optional[str] = None
125 id: str
126
127
@@ -129,7 +129,7 @@ class RunningInput(BaseModel):
129 id: str
130 state: str
131 started_at: str
132 - detailed_message: Optional[str]
132 + detailed_message: Optional[str] = None
133 message_input: MessageInput
134
135
backend/app/connectors/graylog/schema/content_packs.py
+9 -27
@@ -3,8 +3,7 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
7 -from pydantic import Extra
6 +from pydantic import ConfigDict, BaseModel
7 from pydantic import Field
8
9
@@ -16,10 +15,7 @@ class Configuration(BaseModel):
15 http_write_timeout: Optional[int] = Field(None, alias="@value")
16 indicator: Optional[str] = Field(None, alias="@value")
17 type: Optional[str] = Field(None, alias="@value")
19 - # Add other fields as needed
20 -
21 - class Config:
22 - extra = Extra.allow # This line allows for additional fields that are not defined in the model.
18 + model_config = ConfigDict(extra="allow")
19
20
21 class Data(BaseModel):
@@ -27,27 +23,19 @@ class Data(BaseModel):
23 description: Optional[str] = Field(None, alias="@value")
24 name: Optional[str] = Field(None, alias="@value")
25 title: Optional[str] = Field(None, alias="@value")
30 - # Define other fields as per your JSON structure
31 -
32 - class Config:
33 - extra = Extra.allow # This line allows for additional fields that are not defined in the model.
26 + model_config = ConfigDict(extra="allow")
27
28
29 class Type(BaseModel):
30 name: str
31 version: str
39 -
40 - class Config:
41 - extra = Extra.allow # This line allows for additional fields that are not defined in the model.
32 + model_config = ConfigDict(extra="allow")
33
34
35 class Constraint(BaseModel):
36 type: str
37 version: str
47 - # Additional fields based on constraints data
48 -
49 - class Config:
50 - extra = Extra.allow # This line allows for additional fields that are not defined in the model.
38 + model_config = ConfigDict(extra="allow")
39
40
41 class Entity(BaseModel):
@@ -56,9 +44,7 @@ class Entity(BaseModel):
44 v: str
45 data: Data
46 constraints: List[Constraint]
59 -
60 - class Config:
61 - extra = Extra.allow # This line allows for additional fields that are not defined in the model.
47 + model_config = ConfigDict(extra="allow")
48
49
50 class ContentPack(BaseModel):
@@ -74,15 +60,11 @@ class ContentPack(BaseModel):
60 server_version: str
61 parameters: List
62 entities: List[Entity]
77 -
78 - class Config:
79 - extra = Extra.allow # This line allows for additional fields that are not defined in the model.
63 + model_config = ConfigDict(extra="allow")
64
65
66 class ContentPackList(BaseModel):
67 total: int
68 content_packs: List[ContentPack]
85 - content_packs_metadata: Optional[Dict[str, Any]]
86 -
87 - class Config:
88 - extra = Extra.allow # This line allows for additional fields that are not defined in the model.
69 + content_packs_metadata: Optional[Dict[str, Any]] = None
70 + model_config = ConfigDict(extra="allow")
backend/app/connectors/graylog/schema/events.py
+4 -4
@@ -70,7 +70,7 @@ class EventDefinition(BaseModel):
70 id: str
71 key_spec: List[str]
72 notification_settings: NotificationSettings
73 - notifications: Optional[List[Dict[str, Union[str, None]]]]
73 + notifications: Optional[List[Dict[str, Union[str, None]]]] = None
74 priority: int
75 storage: List[Storage]
76 title: str
@@ -120,7 +120,7 @@ class Event(BaseModel):
120 fields: Dict[str, str]
121 group_by_fields: Dict[str, str]
122 id: str
123 - key: Optional[str]
123 + key: Optional[str] = None
124 key_tuple: List[str]
125 message: str
126 origin_context: str
@@ -128,8 +128,8 @@ class Event(BaseModel):
128 source: str
129 source_streams: List[str]
130 streams: List[str]
131 - timerange_end: Optional[str]
132 - timerange_start: Optional[str]
131 + timerange_end: Optional[str] = None
132 + timerange_start: Optional[str] = None
133 timestamp: str
134 timestamp_processing: str
135
backend/app/connectors/graylog/schema/monitoring.py
+9 -9
@@ -32,25 +32,25 @@ class GraylogThroughputMetrics(BaseModel):
32
33 class GraylogThroughputMetricsCollection(BaseModel):
34 graylog2_buffers_input_usage: Optional[str] = Field(
35 - alias="org.graylog2.buffers.input.usage",
35 + None, alias="org.graylog2.buffers.input.usage",
36 )
37 graylog2_buffers_output_usage: Optional[str] = Field(
38 - alias="org.graylog2.buffers.output.usage",
38 + None, alias="org.graylog2.buffers.output.usage",
39 )
40 graylog2_buffers_process_usage: Optional[str] = Field(
41 - alias="org.graylog2.buffers.process.usage",
41 + None, alias="org.graylog2.buffers.process.usage",
42 )
43 graylog2_throughput_input_1_sec_rate: Optional[str] = Field(
44 - alias="org.graylog2.throughput.input.1-sec-rate",
44 + None, alias="org.graylog2.throughput.input.1-sec-rate",
45 )
46 graylog2_throughput_output_1_sec_rate: Optional[str] = Field(
47 - alias="org.graylog2.throughput.output.1-sec-rate",
47 + None, alias="org.graylog2.throughput.output.1-sec-rate",
48 )
49 graylog2_throughput_output: Optional[str] = Field(
50 - alias="org.graylog2.throughput.output",
50 + None, alias="org.graylog2.throughput.output",
51 )
52 graylog2_throughput_input: Optional[str] = Field(
53 - alias="org.graylog2.throughput.input",
53 + None, alias="org.graylog2.throughput.input",
54 )
55
56
@@ -91,7 +91,7 @@ class GraylogEventNotificationsNotification(BaseModel):
91 title: str
92 description: str
93 # config: GraylogEventNotificationsConfig
94 - config: Optional[Dict[str, Any]]
94 + config: Optional[Dict[str, Any]] = None
95
96
97 class GraylogEventNotifications(BaseModel):
@@ -105,6 +105,6 @@ class GraylogEventNotifications(BaseModel):
105
106
107 class GraylogEventNotificationsResponse(BaseModel):
108 - event_notifications: Optional[GraylogEventNotifications]
108 + event_notifications: Optional[GraylogEventNotifications] = None
109 message: str
110 success: bool
backend/app/connectors/graylog/schema/pipelines.py
+2 -2
@@ -17,7 +17,7 @@ class StageWithRuleID(Stage):
17 class Pipeline(BaseModel):
18 created_at: str
19 description: Optional[str] = None # Make description optional
20 - errors: Optional[None]
20 + errors: Optional[None] = None
21 id: str
22 modified_at: Optional[str] = None # Make modified_at optional
23 source: str
@@ -34,7 +34,7 @@ class GraylogPipelinesResponse(BaseModel):
34 class PipelineRule(BaseModel):
35 created_at: str
36 description: Optional[str] = None # Make description optional
37 - errors: Optional[None]
37 + errors: Optional[None] = None
38 id: str
39 modified_at: Optional[str] = None # Make modified_at optional
40 source: str
backend/app/connectors/graylog/schema/streams.py
+2 -2
@@ -6,7 +6,7 @@ from pydantic import Field
6
7
8 class Rule(BaseModel):
9 - description: Optional[str]
9 + description: Optional[str] = None
10 field: str
11 id: str
12 inverted: bool
@@ -16,7 +16,7 @@ class Rule(BaseModel):
16
17
18 class Stream(BaseModel):
19 - content_pack: Optional[str]
19 + content_pack: Optional[str] = None
20 created_at: str
21 creator_user_id: str
22 description: Optional[str] = Field("No description provided")
backend/app/connectors/influxdb/schema/alerts.py
+30 -38
@@ -2,7 +2,7 @@ from datetime import datetime
2 from enum import Enum
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import ConfigDict, BaseModel
6 from pydantic import Field
7
8
@@ -50,9 +50,7 @@ class InfluxDBAlert(BaseModel):
50 message: str
51 status: Optional[str] = None # active or cleared
52 check_id: Optional[str] = None # For internal filtering
53 -
54 - class Config:
55 - from_attributes = True
53 + model_config = ConfigDict(from_attributes=True)
54
55
56 class InfluxDBAlertResponse(BaseModel):
@@ -67,29 +65,26 @@ class InfluxDBAlertResponse(BaseModel):
65 filtered_count: int
66 active_alerts_count: int = 0
67 cleared_alerts_count: int = 0
70 -
71 - class Config:
72 - from_attributes = True
73 - json_schema_extra = {
74 - "example": {
75 - "success": True,
76 - "message": "Successfully retrieved alerts",
77 - "alerts": [
78 - {
79 - "time": "2025-12-01T10:30:00Z",
80 - "check_name": "CPU CHECK",
81 - "sensor_type": "CPU",
82 - "severity": "warning",
83 - "message": "CPU usage high",
84 - "status": "active",
85 - },
86 - ],
87 - "total_count": 150,
88 - "filtered_count": 25,
89 - "active_alerts_count": 5,
90 - "cleared_alerts_count": 20,
91 - },
92 - }
68 + model_config = ConfigDict(from_attributes=True, json_schema_extra={
69 + "example": {
70 + "success": True,
71 + "message": "Successfully retrieved alerts",
72 + "alerts": [
73 + {
74 + "time": "2025-12-01T10:30:00Z",
75 + "check_name": "CPU CHECK",
76 + "sensor_type": "CPU",
77 + "severity": "warning",
78 + "message": "CPU usage high",
79 + "status": "active",
80 + },
81 + ],
82 + "total_count": 150,
83 + "filtered_count": 25,
84 + "active_alerts_count": 5,
85 + "cleared_alerts_count": 20,
86 + },
87 + })
88
89
90 class InfluxDBAlertsResponse(BaseModel):
@@ -107,14 +102,11 @@ class InfluxDBCheckNamesResponse(BaseModel):
102 message: str
103 check_names: list[str]
104 total_count: int
110 -
111 - class Config:
112 - from_attributes = True
113 - json_schema_extra = {
114 - "example": {
115 - "success": True,
116 - "message": "Successfully retrieved check names",
117 - "check_names": ["CPU CHECK", "Host Offline", "Memory Usage", "Disk Space"],
118 - "total_count": 4,
119 - },
120 - }
105 + model_config = ConfigDict(from_attributes=True, json_schema_extra={
106 + "example": {
107 + "success": True,
108 + "message": "Successfully retrieved check names",
109 + "check_names": ["CPU CHECK", "Host Offline", "Memory Usage", "Disk Space"],
110 + "total_count": 4,
111 + },
112 + })
backend/app/connectors/schema.py
+16 -21
@@ -2,30 +2,28 @@ from datetime import datetime
2 from typing import List
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import ConfigDict, BaseModel
6
7
8 class ConnectorHistoryResponse(BaseModel):
9 - id: Optional[int]
9 + id: Optional[int] = None
10 connector_id: int
11 change_timestamp: datetime
12 change_description: str
13 -
14 - class Config:
15 - orm_mode = True
13 + model_config = ConfigDict(from_attributes=True)
14
15
16 class ConnectorResponse(BaseModel):
19 - id: Optional[int]
17 + id: Optional[int] = None
18 connector_name: str
19 connector_type: str
20 connector_url: str
21 connector_last_updated: datetime
24 - connector_username: Optional[str]
25 - connector_password: Optional[str]
26 - connector_api_key: Optional[str]
27 - connector_description: Optional[str]
28 - connector_supports: Optional[str]
22 + connector_username: Optional[str] = None
23 + connector_password: Optional[str] = None
24 + connector_api_key: Optional[str] = None
25 + connector_description: Optional[str] = None
26 + connector_supports: Optional[str] = None
27 connector_configured: bool
28 connector_verified: bool
29 connector_accepts_host_only: bool
@@ -33,12 +31,9 @@ class ConnectorResponse(BaseModel):
31 connector_accepts_username_password: bool
32 connector_accepts_file: bool
33 connector_accepts_extra_data: bool
36 - connector_extra_data: Optional[str]
37 - history_logs: Optional[List[ConnectorHistoryResponse]]
38 -
39 - class Config:
40 - orm_mode = True
41 - from_attributes = True
34 + connector_extra_data: Optional[str] = None
35 + history_logs: Optional[List[ConnectorHistoryResponse]] = None
36 + model_config = ConfigDict(from_attributes=True)
37
38
39 class ConnectorsListResponse(BaseModel):
@@ -60,7 +55,7 @@ class VerifyConnectorResponse(BaseModel):
55
56 class UpdateConnector(BaseModel):
57 connector_url: str
63 - connector_username: Optional[str]
64 - connector_password: Optional[str]
65 - connector_api_key: Optional[str]
66 - connector_extra_data: Optional[str]
58 + connector_username: Optional[str] = None
59 + connector_password: Optional[str] = None
60 + connector_api_key: Optional[str] = None
61 + connector_extra_data: Optional[str] = None
backend/app/connectors/shuffle/schema/integrations.py
+14 -14
@@ -6,41 +6,41 @@ from typing import Optional
6 from fastapi import HTTPException
7 from pydantic import BaseModel
8 from pydantic import Field
9 -from pydantic import root_validator
9 +from pydantic import model_validator
10
11
12 class IntegrationRequest(BaseModel):
13 - app_name: str = Field(..., description="The name of the application", example="PagerDuty")
14 - category: str = Field(..., description="The category of the application", example="cases")
15 - label: str = Field(..., description="The label of the application", example="create_ticket")
13 + app_name: str = Field(..., description="The name of the application", examples=["PagerDuty"])
14 + category: str = Field(..., description="The category of the application", examples=["cases"])
15 + label: str = Field(..., description="The label of the application", examples=["create_ticket"])
16 fields: Optional[List[Dict[str, Any]]] = Field(
17 [],
18 description="The fields of the application",
19 - example=[
19 + examples=[[
20 {"key": "title", "value": "This is the title"},
21 {"key": "description", "value": "This is the description"},
22 {"key": "source", "value": "Shuffle"},
23 - ],
23 + ]],
24 )
25 skip_workflow: Optional[bool] = Field(
26 False,
27 description="Skip the workflow",
28 - example=True,
28 + examples=[True],
29 )
30
31
32 class ExecuteWorkflowRequest(BaseModel):
33 - workflow_id: str = Field(..., description="The ID of the workflow", example="workflow_id")
33 + workflow_id: str = Field(..., description="The ID of the workflow", examples=["workflow_id"])
34 execution_arguments: Optional[Dict[str, Any]] = Field(
35 {},
36 description="The execution arguments",
37 - example={"key": "value"},
37 + examples=[{"key": "value"}],
38 )
39 - start: str = Field("", description="The start of the workflow", example="start")
39 + start: str = Field("", description="The start of the workflow", examples=["start"])
40
41 - @root_validator
42 - def check_customer_code(cls, values):
43 - execution_arguments = values.get("execution_arguments", {})
41 + @model_validator(mode="after")
42 + def check_customer_code(self):
43 + execution_arguments = self.execution_arguments or {}
44 if "customer_code" not in execution_arguments or not execution_arguments["customer_code"]:
45 raise HTTPException(status_code=400, detail="customer_code is required")
46 - return values
46 + return self
backend/app/connectors/shuffle/schema/organizations.py
+2
@@ -103,6 +103,8 @@ class DetailedOrganization(BaseModel):
103 region_url: str = ""
104 tutorials: List[Any] = []
105
106 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
107 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
108 @validator("manager_orgs", pre=True, always=True)
109 def validate_manager_orgs(cls, v):
110 """
backend/app/connectors/shuffle/schema/singul.py
+1 -1
@@ -3,4 +3,4 @@ from pydantic import Field
3
4
5 class SingulRequest(BaseModel):
6 - app: str = Field(..., description="The name of the application", example="outlook_office365")
6 + app: str = Field(..., description="The name of the application", examples=["outlook_office365"])
backend/app/connectors/sublime/schema/alerts.py
+7 -19
@@ -2,7 +2,7 @@ import datetime
2 from typing import List
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import ConfigDict, BaseModel
6 from pydantic import Field
7
8
@@ -82,9 +82,7 @@ class FlaggedRuleSchema(BaseModel):
82 description="Severity level of the flagged rule",
83 )
84 tags: str
85 -
86 - class Config:
87 - orm_mode = True
85 + model_config = ConfigDict(from_attributes=True)
86
87
88 class MailboxSchema(BaseModel):
@@ -93,34 +91,26 @@ class MailboxSchema(BaseModel):
91 description="External identifier for the mailbox",
92 )
93 mailbox_id: str
96 -
97 - class Config:
98 - orm_mode = True
94 + model_config = ConfigDict(from_attributes=True)
95
96
97 class TriggeredActionSchema(BaseModel):
98 action_id: str
99 name: str
100 type: str
105 -
106 - class Config:
107 - orm_mode = True
101 + model_config = ConfigDict(from_attributes=True)
102
103
104 class SenderSchema(BaseModel):
105 email: str
106 name: str
113 -
114 - class Config:
115 - orm_mode = True
107 + model_config = ConfigDict(from_attributes=True)
108
109
110 class RecipientSchema(BaseModel):
111 email: str
112 name: str
121 -
122 - class Config:
123 - orm_mode = True
113 + model_config = ConfigDict(from_attributes=True)
114
115
116 class SublimeAlertsSchema(BaseModel):
@@ -138,9 +128,7 @@ class SublimeAlertsSchema(BaseModel):
128 triggered_actions: List[TriggeredActionSchema]
129 sender: List[SenderSchema]
130 recipients: List[RecipientSchema]
141 -
142 - class Config:
143 - orm_mode = True
131 + model_config = ConfigDict(from_attributes=True)
132
133
134 class SublimeAlertsResponse(BaseModel):
backend/app/connectors/velociraptor/schema/artifacts.py
+30 -34
@@ -6,9 +6,8 @@ from typing import Optional
6 from typing import Union
7
8 from fastapi import HTTPException
9 -from pydantic import BaseModel
9 +from pydantic import field_validator, ConfigDict, BaseModel
10 from pydantic import Field
11 -from pydantic import validator
11
12
13 class ArtifactParameter(BaseModel):
@@ -34,7 +33,7 @@ class Artifacts(BaseModel):
33 class ArtifactsResponse(BaseModel):
34 message: str = Field(...)
35 # make artifacts optional
37 - artifacts: Optional[List[Artifacts]]
36 + artifacts: Optional[List[Artifacts]] = None
37 success: str = Field(...)
38
39
@@ -47,21 +46,19 @@ class ArtifactParametersResponse(BaseModel):
46 parameter_prefix: str = Field(..., description="The prefix used for filtering")
47 matching_parameters: List[ArtifactParameter] = Field(default_factory=list, description="List of parameters that match the prefix")
48 total_matches: int = Field(..., description="Total number of matching parameters")
50 -
51 - class Config:
52 - schema_extra = {
53 - "example": {
54 - "success": True,
55 - "message": "Found 2 parameters matching prefix 'T1552.001'",
56 - "artifact_name": "Windows.AttackSimulation.AtomicRedTeam",
57 - "parameter_prefix": "T1552.001",
58 - "matching_parameters": [
59 - {"name": "T1552.001 - 3", "description": "Credentials In Files - Extracting passwords with findstr", "type": "bool"},
60 - {"name": "T1552.001 - 4", "description": "Credentials In Files - Access unattend.xml", "type": "bool"},
61 - ],
62 - "total_matches": 2,
63 - },
64 - }
49 + model_config = ConfigDict(json_schema_extra={
50 + "example": {
51 + "success": True,
52 + "message": "Found 2 parameters matching prefix 'T1552.001'",
53 + "artifact_name": "Windows.AttackSimulation.AtomicRedTeam",
54 + "parameter_prefix": "T1552.001",
55 + "matching_parameters": [
56 + {"name": "T1552.001 - 3", "description": "Credentials In Files - Extracting passwords with findstr", "type": "bool"},
57 + {"name": "T1552.001 - 4", "description": "Credentials In Files - Access unattend.xml", "type": "bool"},
58 + ],
59 + "total_matches": 2,
60 + },
61 + })
62
63
64 class OSPrefixEnum(Enum):
@@ -71,7 +68,7 @@ class OSPrefixEnum(Enum):
68
69
70 class OSPrefixModel(BaseModel):
74 - os_name: Optional[str]
71 + os_name: Optional[str] = None
72 os_prefix_mapping: Dict[str, str] = {
73 "windows": "Windows",
74 "linux": "Linux",
@@ -142,18 +139,16 @@ class CollectArtifactBody(BaseBody):
139 False,
140 description="If true, only store the collected data in the datastore without sending it back immediately",
141 )
145 -
146 - class Config:
147 - schema_extra = {
148 - "example": {
149 - "hostname": "WIN-HFOU106TD7K",
150 - "velociraptor_id": "C.475df76785008b04",
151 - "velociraptor_org": "root",
152 - "artifact_name": "Windows.AttackSimulation.AtomicRedTeam",
153 - "parameters": {"env": [{"key": "InstallART", "value": "N"}, {"key": "T1552.001 - 3", "value": "Y"}]},
154 - "data_store_only": False,
155 - },
156 - }
142 + model_config = ConfigDict(json_schema_extra={
143 + "example": {
144 + "hostname": "WIN-HFOU106TD7K",
145 + "velociraptor_id": "C.475df76785008b04",
146 + "velociraptor_org": "root",
147 + "artifact_name": "Windows.AttackSimulation.AtomicRedTeam",
148 + "parameters": {"env": [{"key": "InstallART", "value": "N"}, {"key": "T1552.001 - 3", "value": "Y"}]},
149 + "data_store_only": False,
150 + },
151 + })
152
153
154 class InvokeCopilotActionBody(BaseModel):
@@ -176,7 +171,8 @@ class CollectFileBody(BaseBody):
171 file: str = Field("Glob\nUsers\\Administrator\\Documents\\*\n", description="File to collect")
172 root_disk: Optional[str] = Field("C:", description="Root disk to collect from")
173
179 - @validator("artifact_name")
174 + @field_validator("artifact_name")
175 + @classmethod
176 def validate_artifact_name(cls, value):
177 if value != "Generic.Collectors.File":
178 raise HTTPException(status_code=400, detail="Invalid artifact name. Name should be 'Generic.Collectors.File'")
@@ -315,13 +311,13 @@ class OS(str, Enum):
311
312 class ArtifactReccomendationAIRequest(BaseModel):
313 os: OS = Field(..., description="Operating system of the client")
318 - prompt: dict = Field(..., example=payload)
314 + prompt: dict = Field(..., examples=[payload])
315
316
317 class ArtifactReccomendationRequest(BaseModel):
318 artifacts: List[Artifacts] = Field(..., description="List of artifacts to be recommended")
319 os: str = Field(..., description="Operating system of the client")
324 - prompt: dict = Field(..., example=payload)
320 + prompt: dict = Field(..., examples=[payload])
321
322
323 class VelociraptorArtifactRecommendation(BaseModel):
backend/app/connectors/velociraptor/schema/flows.py
+6 -5
@@ -3,15 +3,14 @@ from typing import Optional
3
4 from fastapi import HTTPException
5 from loguru import logger
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class FlowSpecParameter(BaseModel):
11 key: str
12 value: str
14 - comment: Optional[str]
13 + comment: Optional[str] = None
14
15
16 class FlowSpec(BaseModel):
@@ -45,7 +44,8 @@ class FlowRequest(BaseModel):
44 compiled_collector_args: List[str]
45 ops_per_second: int
46
48 - @root_validator(pre=True)
47 + @model_validator(mode="before")
48 + @classmethod
49 def validate_specs(cls, values):
50 if "specs" in values and values["specs"] is not None:
51 validated_specs = []
@@ -116,7 +116,8 @@ class RetrieveFlowRequest(BaseModel):
116 client_id: str
117 session_id: str
118
119 - @root_validator(pre=True)
119 + @model_validator(mode="before")
120 + @classmethod
121 def validate_session_id(cls, values):
122 if "session_id" in values and values["session_id"] == "":
123 raise HTTPException(
backend/app/connectors/wazuh_indexer/models/sigma.py
+3 -2
@@ -1,6 +1,7 @@
1 from datetime import datetime
2 from typing import Optional
3
4 +from sqlmodel import Column
5 from sqlmodel import Field
6 from sqlmodel import SQLModel
7 from sqlmodel import Text
@@ -9,8 +10,8 @@ from sqlmodel import Text
10 class SigmaQuery(SQLModel, table=True):
11 __tablename__ = "sigma_queries"
12 id: Optional[int] = Field(default=None, primary_key=True)
12 - rule_name: str = Field(sa_column=Text, nullable=False)
13 - rule_query: str = Field(sa_column=Text, nullable=False)
13 + rule_name: str = Field(sa_column=Column(Text, nullable=False))
14 + rule_query: str = Field(sa_column=Column(Text, nullable=False))
15 active: bool = Field(default=False)
16 time_interval: str = Field(default="5m", nullable=False)
17 last_updated: datetime = Field(default_factory=datetime.utcnow)
backend/app/connectors/wazuh_indexer/schema/alerts.py
+5 -4
@@ -4,9 +4,8 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
7 +from pydantic import field_validator, BaseModel
8 from pydantic import Field
9 -from pydantic import validator
9
10
11 class Alert(BaseModel):
@@ -31,7 +30,8 @@ class AlertsSearchBody(BaseModel):
30 description="The timestamp field to search alerts in.",
31 )
32
34 - @validator("timerange")
33 + @field_validator("timerange")
34 + @classmethod
35 def validate_timerange(cls, value):
36 if value[-1] not in ("h", "d", "w"):
37 raise ValueError(
@@ -127,7 +127,8 @@ class GraylogAlertsSearchBody(BaseModel):
127 description="The index prefix to search alerts in.",
128 )
129
130 - @validator("timerange")
130 + @field_validator("timerange")
131 + @classmethod
132 def validate_timerange(cls, value):
133 if value[-1] not in ("h", "d", "w"):
134 raise ValueError(
backend/app/connectors/wazuh_indexer/schema/monitoring.py
+5 -5
@@ -27,7 +27,7 @@ class ClusterHealth(BaseModel):
27
28
29 class ClusterHealthResponse(BaseModel):
30 - cluster_health: Optional[ClusterHealth]
30 + cluster_health: Optional[ClusterHealth] = None
31 message: str
32 success: bool
33
@@ -41,7 +41,7 @@ class NodeAllocation(BaseModel):
41
42
43 class NodeAllocationResponse(BaseModel):
44 - node_allocation: Optional[List[NodeAllocation]]
44 + node_allocation: Optional[List[NodeAllocation]] = None
45 message: str
46 success: bool
47
@@ -61,7 +61,7 @@ class IndicesStats(BaseModel):
61
62
63 class IndicesStatsResponse(BaseModel):
64 - indices_stats: Optional[List[IndicesStats]]
64 + indices_stats: Optional[List[IndicesStats]] = None
65 message: str
66 success: bool
67
@@ -75,7 +75,7 @@ class Shards(BaseModel):
75
76
77 class ShardsResponse(BaseModel):
78 - shards: Optional[List[Shards]]
78 + shards: Optional[List[Shards]] = None
79 message: str
80 success: bool
81
@@ -89,6 +89,6 @@ class CustomerIndicesSize(BaseModel):
89
90
91 class CustomerIndicesSizeResponse(BaseModel):
92 - customer_sizes: Optional[List[CustomerIndicesSize]]
92 + customer_sizes: Optional[List[CustomerIndicesSize]] = None
93 message: str
94 success: bool
backend/app/connectors/wazuh_indexer/schema/snapshot_and_restore.py
+3 -3
@@ -132,12 +132,12 @@ class RestoreSnapshotRequest(BaseModel):
132 rename_pattern: Optional[str] = Field(
133 None,
134 description="Pattern to match indices to rename",
135 - example="wazuh_(.+)",
135 + examples=["wazuh_(.+)"],
136 )
137 rename_replacement: Optional[str] = Field(
138 None,
139 description="Replacement string for renamed indices",
140 - example="restored_wazuh_$1",
140 + examples=["restored_wazuh_$1"],
141 )
142 include_aliases: Optional[bool] = Field(
143 True,
@@ -246,7 +246,7 @@ class SnapshotScheduleCreate(BaseModel):
246 index_pattern: str = Field(
247 ...,
248 description="Index pattern to snapshot (e.g., wazuh_customer_*)",
249 - example="wazuh_customer_*",
249 + examples=["wazuh_customer_*"],
250 )
251 repository: str = Field(..., description="Repository to store snapshots")
252 enabled: Optional[bool] = Field(True, description="Whether this schedule is active")
backend/app/connectors/wazuh_manager/schema/groups.py
+8 -14
@@ -2,7 +2,7 @@ from typing import List
2 from typing import Optional
3 from typing import Union
4
5 -from pydantic import BaseModel
5 +from pydantic import ConfigDict, BaseModel
6 from pydantic import Field
7
8
@@ -13,9 +13,7 @@ class WazuhGroup(BaseModel):
13 count: int = Field(..., description="Number of agents belonging to the group")
14 mergedSum: str = Field(..., description="Checksum of merged configuration files")
15 configSum: str = Field(..., description="Checksum of configuration files")
16 -
17 - class Config:
18 - extra = "ignore" # Ignore extra fields from API
16 + model_config = ConfigDict(extra="ignore")
17
18
19 class WazuhGroupsResponse(BaseModel):
@@ -44,9 +42,7 @@ class WazuhGroupFile(BaseModel):
42
43 filename: str = Field(..., description="Name of the file")
44 hash: str = Field(..., description="Hash/checksum of the file")
47 -
48 - class Config:
49 - extra = "ignore" # Ignore extra fields from API
45 + model_config = ConfigDict(extra="ignore")
46
47
48 class WazuhGroupFilesResponse(BaseModel):
@@ -63,11 +59,9 @@ class WazuhGroupConfigurationUpdateRequest(BaseModel):
59 """Request model for updating group configuration."""
60
61 configuration: str = Field(..., description="Full valid XML configuration content")
66 -
67 - class Config:
68 - schema_extra = {
69 - "example": {
70 - "configuration": """<agent_config>
62 + model_config = ConfigDict(json_schema_extra={
63 + "example": {
64 + "configuration": """<agent_config>
65 <labels>
66 <label key="customer">example</label>
67 </labels>
@@ -77,8 +71,8 @@ class WazuhGroupConfigurationUpdateRequest(BaseModel):
71 <events_per_second>1000</events_per_second>
72 </client_buffer>
73 </agent_config>""",
80 - },
81 - }
74 + },
75 + })
76
77
78 class WazuhGroupConfigurationUpdateResponse(BaseModel):
backend/app/connectors/wazuh_manager/schema/mitre.py
+6 -26
@@ -3,7 +3,7 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import ConfigDict, BaseModel
7 from pydantic import Field
8
9
@@ -98,11 +98,7 @@ class MitreTechniqueItem(BaseModel):
98 platforms: List[str] = []
99 data_sources: List[str] = []
100 is_subtechnique: bool = False
101 -
102 - class Config:
103 - """Configuration for the model."""
104 -
105 - extra = "ignore" # Ignore extra fields from the API
101 + model_config = ConfigDict(extra="ignore")
102
103
104 class WazuhMitreTechniquesResponse(BaseModel):
@@ -208,11 +204,7 @@ class MitreSoftwareItem(BaseModel):
204 platforms: Optional[List[str]] = None
205 aliases: Optional[List[str]] = None
206 type: Optional[str] = None # For distinguishing between malware, tool, etc.
211 -
212 - class Config:
213 - """Configuration for the model."""
214 -
215 - extra = "ignore" # Ignore extra fields from the API
207 + model_config = ConfigDict(extra="ignore")
208
209
210 class WazuhMitreSoftwareResponse(BaseModel):
@@ -231,11 +223,7 @@ class MitreReferenceItem(BaseModel):
223 source: str
224 id: Optional[str] = None # ID of the related technique, tactic, or software
225 type: Optional[str] = None # Type of the item the reference belongs to (technique, tactic, etc.)
234 -
235 - class Config:
236 - """Configuration for the model."""
237 -
238 - extra = "ignore" # Ignore extra fields from the API
226 + model_config = ConfigDict(extra="ignore")
227
228
229 class WazuhMitreReferencesResponse(BaseModel):
@@ -262,11 +250,7 @@ class MitreMitigationItem(BaseModel):
250 url: str
251 source: str
252 external_id: str
265 -
266 - class Config:
267 - """Configuration for the model."""
268 -
269 - extra = "ignore" # Ignore extra fields from the API
253 + model_config = ConfigDict(extra="ignore")
254
255
256 class WazuhMitreMitigationsResponse(BaseModel):
@@ -298,11 +282,7 @@ class MitreGroupItem(BaseModel):
282 # Additional fields that might be present
283 aliases: Optional[List[str]] = None
284 country: Optional[str] = None
301 -
302 - class Config:
303 - """Configuration for the model."""
304 -
305 - extra = "ignore" # Ignore extra fields from the API
285 + model_config = ConfigDict(extra="ignore")
286
287
288 class WazuhMitreGroupsResponse(BaseModel):
backend/app/connectors/wazuh_manager/schema/rules.py
+11 -15
@@ -3,9 +3,8 @@ from typing import Optional
3 from typing import Union
4
5 from fastapi import HTTPException
6 -from pydantic import BaseModel
6 +from pydantic import field_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import validator
8
9
10 class RuleDisable(BaseModel):
@@ -15,7 +14,7 @@ class RuleDisable(BaseModel):
14
15
16 class RuleDisableResponse(BaseModel):
18 - previous_level: Optional[str]
17 + previous_level: Optional[str] = None
18 message: str
19 success: bool
20
@@ -26,7 +25,7 @@ class RuleEnable(BaseModel):
25
26
27 class RuleEnableResponse(BaseModel):
29 - new_level: Optional[str]
28 + new_level: Optional[str] = None
29 message: str
30 success: bool
31
@@ -65,10 +64,7 @@ class WazuhRule(BaseModel):
64 tsc: List[str] = []
65 mitre: List[str] = []
66 details: Optional[dict] = None
68 -
69 - class Config:
70 - allow_population_by_field_name = True
71 - extra = "ignore" # Ignore extra fields from API
67 + model_config = ConfigDict(populate_by_name=True, extra="ignore")
68
69
70 class WazuhRulesResponse(BaseModel):
@@ -86,9 +82,7 @@ class WazuhRuleFile(BaseModel):
82 filename: str = Field(..., description="Rule file name")
83 relative_dirname: str = Field(..., description="Relative directory path")
84 status: str = Field(..., description="File status (enabled/disabled)")
89 -
90 - class Config:
91 - extra = "ignore" # Ignore extra fields from API
85 + model_config = ConfigDict(extra="ignore")
86
87
88 class WazuhRuleFilesResponse(BaseModel):
@@ -179,10 +173,11 @@ payload = {
173
174
175 class RuleExcludeRequest(BaseModel):
182 - integration: str = Field(..., example="wazuh-rule-exclusion")
183 - prompt: dict = Field(..., example=payload)
176 + integration: str = Field(..., examples=["wazuh-rule-exclusion"])
177 + prompt: dict = Field(..., examples=[payload])
178
185 - @validator("integration")
179 + @field_validator("integration")
180 + @classmethod
181 def check_integration(cls, v):
182 if v != "wazuh-rule-exclusion":
183 raise HTTPException(
@@ -191,7 +186,8 @@ class RuleExcludeRequest(BaseModel):
186 )
187 return v
188
194 - @validator("prompt")
189 + @field_validator("prompt")
190 + @classmethod
191 def check_rule_group(cls, v):
192 if "rule_group3" in v:
193 if "rule_group1" not in v and "rule_group3" not in v:
backend/app/customer_portal/schema/settings.py
+7 -10
@@ -3,9 +3,8 @@ import re
3 from typing import Optional
4
5 from fastapi import HTTPException
6 -from pydantic import BaseModel
6 +from pydantic import field_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import validator
8
9
10 class UpdatePortalSettingsRequest(BaseModel):
@@ -13,7 +12,8 @@ class UpdatePortalSettingsRequest(BaseModel):
12 logo_base64: Optional[str] = Field(None, description="Base64 encoded logo image. Set to null to restore default.")
13 logo_mime_type: Optional[str] = Field(None, max_length=50, description="MIME type of the logo. Set to null to restore default.")
14
16 - @validator("logo_base64")
15 + @field_validator("logo_base64")
16 + @classmethod
17 def validate_base64(cls, v):
18 if v is None:
19 return v
@@ -42,7 +42,8 @@ class UpdatePortalSettingsRequest(BaseModel):
42
43 return v
44
45 - @validator("logo_mime_type")
45 + @field_validator("logo_mime_type")
46 + @classmethod
47 def validate_mime_type(cls, v):
48 if v is None:
49 return v
@@ -51,9 +52,7 @@ class UpdatePortalSettingsRequest(BaseModel):
52 if v not in allowed_types:
53 raise HTTPException(status_code=400, detail=f"Invalid MIME type. Allowed types are: {', '.join(allowed_types)}")
54 return v
54 -
55 - class Config:
56 - json_schema_extra = {"example": {"title": "My Custom Portal", "logo_base64": "iVBORw0KGgoAAAANS...", "logo_mime_type": "image/png"}}
55 + model_config = ConfigDict(json_schema_extra={"example": {"title": "My Custom Portal", "logo_base64": "iVBORw0KGgoAAAANS...", "logo_mime_type": "image/png"}})
56
57
58 class PortalSettingsData(BaseModel):
@@ -62,9 +61,7 @@ class PortalSettingsData(BaseModel):
61 logo_base64: Optional[str] = None
62 logo_mime_type: Optional[str] = None
63 updated_at: str
65 -
66 - class Config:
67 - from_attributes = True
64 + model_config = ConfigDict(from_attributes=True)
65
66
67 class PortalSettingsResponse(BaseModel):
backend/app/customer_provisioning/schema/decommission.py
+6 -6
@@ -7,27 +7,27 @@ from pydantic import Field
7 class DecommissionedData(BaseModel):
8 agents_deleted: List[str] = Field(
9 ...,
10 - example=["agent1", "agent2"],
10 + examples=[["agent1", "agent2"]],
11 description="List of agents deleted",
12 )
13 groups_deleted: List[str] = Field(
14 ...,
15 - example=["group1", "group2"],
15 + examples=[["group1", "group2"]],
16 description="List of groups deleted",
17 )
18 - stream_deleted: str = Field(..., example="stream1", description="Stream deleted")
19 - index_deleted: str = Field(..., example="index1", description="Index deleted")
18 + stream_deleted: str = Field(..., examples=["stream1"], description="Stream deleted")
19 + index_deleted: str = Field(..., examples=["index1"], description="Index deleted")
20
21
22 class DecommissionCustomerResponse(BaseModel):
23 message: str = Field(
24 ...,
25 - example="Customer decommissioned successfully",
25 + examples=["Customer decommissioned successfully"],
26 description="Message indicating the customer was decommissioned successfully",
27 )
28 success: bool = Field(
29 ...,
30 - example=True,
30 + examples=[True],
31 description="Whether the customer was decommissioned successfully or not",
32 )
33 decomissioned_data: DecommissionedData = Field(
backend/app/customer_provisioning/schema/default.py
+2 -2
@@ -9,12 +9,12 @@ from app.customer_provisioning.models.default_settings import (
9 class CustomerProvisioningDefaultSettingsResponse(BaseModel):
10 message: str = Field(
11 ...,
12 - example="Customer Provisioning Default Settings retrieved successfully",
12 + examples=["Customer Provisioning Default Settings retrieved successfully"],
13 description="Message indicating the customer provisioning default settings were retrieved successfully",
14 )
15 success: bool = Field(
16 ...,
17 - example=True,
17 + examples=[True],
18 description="Whether the customer provisioning default settings were retrieved successfully or not",
19 )
20 customer_provisioning_default_settings: CustomerProvisioningDefaultSettings = Field(
backend/app/customer_provisioning/schema/graylog.py
+67 -73
@@ -1,7 +1,7 @@
1 from typing import List
2 from typing import Optional
3
4 -from pydantic import BaseModel
4 +from pydantic import ConfigDict, BaseModel
5 from pydantic import Field
6
7
@@ -32,33 +32,31 @@ class TimeBasedIndexSet(BaseModel):
32 index_optimization_disabled: bool
33 writable: bool
34 field_type_refresh_interval: int
35 -
36 - class Config:
37 - schema_extra = {
38 - "example": {
39 - "title": "Wazuh - Example Company",
40 - "description": "Wazuh - Example Company",
41 - "index_prefix": "wazuh-examplecode",
42 - "rotation_strategy_class": "org.graylog2.indexer.rotation.strategies.SizeBasedRotationStrategy",
43 - "rotation_strategy": {
44 - "type": "org.graylog2.indexer.rotation.strategies.SizeBasedRotationStrategyConfig",
45 - "max_size": 2684354560,
46 - },
47 - "retention_strategy_class": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy",
48 - "retention_strategy": {
49 - "type": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig",
50 - "max_number_of_indices": 20,
51 - },
52 - "creation_date": "2021-01-01T00:00:00.000Z",
53 - "index_analyzer": "standard",
54 - "shards": 1,
55 - "replicas": 0,
56 - "index_optimization_max_num_segments": 1,
57 - "index_optimization_disabled": False,
58 - "writable": True,
59 - "field_type_refresh_interval": 5000,
35 + model_config = ConfigDict(json_schema_extra={
36 + "example": {
37 + "title": "Wazuh - Example Company",
38 + "description": "Wazuh - Example Company",
39 + "index_prefix": "wazuh-examplecode",
40 + "rotation_strategy_class": "org.graylog2.indexer.rotation.strategies.SizeBasedRotationStrategy",
41 + "rotation_strategy": {
42 + "type": "org.graylog2.indexer.rotation.strategies.SizeBasedRotationStrategyConfig",
43 + "max_size": 2684354560,
44 },
61 - }
45 + "retention_strategy_class": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy",
46 + "retention_strategy": {
47 + "type": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig",
48 + "max_number_of_indices": 20,
49 + },
50 + "creation_date": "2021-01-01T00:00:00.000Z",
51 + "index_analyzer": "standard",
52 + "shards": 1,
53 + "replicas": 0,
54 + "index_optimization_max_num_segments": 1,
55 + "index_optimization_disabled": False,
56 + "writable": True,
57 + "field_type_refresh_interval": 5000,
58 + },
59 + })
60
61
62 class RotationStrategyConfig(BaseModel):
@@ -126,26 +124,24 @@ class WazuhEventStream(BaseModel):
124 None,
125 description="Associated content pack, if any",
126 )
129 -
130 - class Config:
131 - schema_extra = {
132 - "example": {
133 - "title": "WAZUH EVENTS CUSTOMERS - Example Company",
134 - "description": "WAZUH EVENTS CUSTOMERS - Example Company",
135 - "index_set_id": "12345",
136 - "rules": [
137 - {
138 - "field": "agent_labels_customer",
139 - "type": 1,
140 - "inverted": False,
141 - "value": "ExampleCode",
142 - },
143 - ],
144 - "matching_type": "AND",
145 - "remove_matches_from_default_stream": True,
146 - "content_pack": None,
147 - },
148 - }
127 + model_config = ConfigDict(json_schema_extra={
128 + "example": {
129 + "title": "WAZUH EVENTS CUSTOMERS - Example Company",
130 + "description": "WAZUH EVENTS CUSTOMERS - Example Company",
131 + "index_set_id": "12345",
132 + "rules": [
133 + {
134 + "field": "agent_labels_customer",
135 + "type": 1,
136 + "inverted": False,
137 + "value": "ExampleCode",
138 + },
139 + ],
140 + "matching_type": "AND",
141 + "remove_matches_from_default_stream": True,
142 + "content_pack": None,
143 + },
144 + })
145
146
147 class Office365EventStream(BaseModel):
@@ -162,32 +158,30 @@ class Office365EventStream(BaseModel):
158 None,
159 description="Associated content pack, if any",
160 )
165 -
166 - class Config:
167 - schema_extra = {
168 - "example": {
169 - "title": "Office365 EVENTS - Example Company",
170 - "description": "Office365 EVENTS - Example Company",
171 - "index_set_id": "12345",
172 - "rules": [
173 - {
174 - "field": "agent_labels_customer",
175 - "type": 1,
176 - "inverted": False,
177 - "value": "ExampleCode",
178 - },
179 - {
180 - "field": "agent_labels_integration",
181 - "type": 1,
182 - "inverted": False,
183 - "value": "Office365",
184 - },
185 - ],
186 - "matching_type": "AND",
187 - "remove_matches_from_default_stream": True,
188 - "content_pack": None,
189 - },
190 - }
161 + model_config = ConfigDict(json_schema_extra={
162 + "example": {
163 + "title": "Office365 EVENTS - Example Company",
164 + "description": "Office365 EVENTS - Example Company",
165 + "index_set_id": "12345",
166 + "rules": [
167 + {
168 + "field": "agent_labels_customer",
169 + "type": 1,
170 + "inverted": False,
171 + "value": "ExampleCode",
172 + },
173 + {
174 + "field": "agent_labels_integration",
175 + "type": 1,
176 + "inverted": False,
177 + "value": "Office365",
178 + },
179 + ],
180 + "matching_type": "AND",
181 + "remove_matches_from_default_stream": True,
182 + "content_pack": None,
183 + },
184 + })
185
186
187 class StreamData(BaseModel):
backend/app/customer_provisioning/schema/provision.py
+22 -22
@@ -3,9 +3,8 @@ from enum import Enum
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import field_validator, BaseModel
7 from pydantic import Field
8 -from pydantic import validator
8
9 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
10 from app.db.universal_models import CustomersMeta
@@ -19,52 +18,52 @@ class CustomerSubsctipion(Enum):
18 class ProvisionNewCustomer(BaseModel):
19 customer_name: str = Field(
20 ...,
22 - example="SOC Fortress",
21 + examples=["SOC Fortress"],
22 description="Name of the customer",
23 )
24 customer_code: str = Field(
25 ...,
27 - example="SOCF",
26 + examples=["SOCF"],
27 description="Code of the customer. Referenced in Wazuh Agent Label, Graylog Stream, etc.",
28 )
29 customer_index_name: str = Field(
30 ...,
32 - example="socf",
31 + examples=["socf"],
32 description="Index prefix for the customer's Graylog instance",
33 )
34 customer_grafana_org_name: str = Field(
35 ...,
37 - example="SOCFortress",
36 + examples=["SOCFortress"],
37 description="Name of the customer's Grafana organization",
38 )
39 hot_data_retention: int = Field(
40 ...,
42 - example=30,
41 + examples=[30],
42 description="Number of days to retain hot data",
43 )
44 index_replicas: int = Field(
45 ...,
47 - example=1,
46 + examples=[1],
47 description="Number of replicas for the customer's Graylog instance",
48 )
49 index_shards: int = Field(
50 ...,
52 - example=1,
51 + examples=[1],
52 description="Number of shards for the customer's Graylog instance",
53 )
54 customer_subscription: List[CustomerSubsctipion] = Field(
55 ...,
57 - example=["Wazuh"],
56 + examples=[["Wazuh"]],
57 description="List of subscriptions for the customer",
58 )
59 dashboards_to_include: DashboardProvisionRequest = Field(
60 ...,
61 description="Dashboards to include in the customer's Grafana instance",
63 - example={
62 + examples=[{
63 "dashboards": [
64 "WAZUH_SUMMARY",
65 ],
67 - },
66 + }],
67 )
68 wazuh_auth_password: Optional[str] = Field("n/a", description="Password for the Wazuh API user")
69 wazuh_registration_port: Optional[str] = Field(
@@ -107,7 +106,8 @@ class ProvisionNewCustomer(BaseModel):
106 description="Whether deployment of Portainer is occurring",
107 )
108
110 - @validator("customer_index_name")
109 + @field_validator("customer_index_name")
110 + @classmethod
111 def validate_customer_index_name(cls, v):
112 pattern = r"^[a-z0-9][a-z0-9_+-]*$"
113 if not re.match(pattern, v):
@@ -185,32 +185,32 @@ class CustomersMetaResponse(BaseModel):
185 class ProvisionHaProxyRequest(BaseModel):
186 customer_name: str = Field(
187 ...,
188 - example="SOCFortress",
188 + examples=["SOCFortress"],
189 description="The name of the customer",
190 )
191 wazuh_registration_port: str = Field(
192 ...,
193 - example="1515",
193 + examples=["1515"],
194 description="The port for the Wazuh registration service",
195 )
196 wazuh_logs_port: str = Field(
197 ...,
198 - example="1514",
198 + examples=["1514"],
199 description="The port for the Wazuh logs service",
200 )
201 wazuh_worker_hostname: Optional[str] = Field(
202 None,
203 - example="worker1",
203 + examples=["worker1"],
204 description="The hostname of the Wazuh worker",
205 )
206 portainer_deployment: Optional[bool] = Field(
207 None,
208 - example=True,
208 + examples=[True],
209 description="Whether deployment of Portainer is occurring",
210 )
211 swarm_nodes: Optional[List[str]] = Field(
212 None,
213 - example=["127.0.0.1"],
213 + examples=[["127.0.0.1"]],
214 description="The IP addresses of the swarm nodes",
215 )
216
@@ -218,13 +218,13 @@ class ProvisionHaProxyRequest(BaseModel):
218 class ProvisionDashboardRequest(BaseModel):
219 customer_name: str = Field(
220 ...,
221 - example="SOCFortress",
221 + examples=["SOCFortress"],
222 description="The name of the customer",
223 )
224 dashboards_to_include: DashboardProvisionRequest = Field(
225 ...,
226 description="Dashboards to include in the customer's Grafana instance",
227 - example={
227 + examples=[{
228 "dashboards": [
229 "WAZUH_SUMMARY",
230 "EDR_WINDOWS_EVENT_LOGS",
@@ -250,7 +250,7 @@ class ProvisionDashboardRequest(BaseModel):
250 "organizationId": 1,
251 "folderId": 1,
252 "datasourceUid": "wazuh",
253 - },
253 + }],
254 )
255 grafana_org_id: int = Field(
256 ...,
backend/app/customer_provisioning/schema/wazuh_worker.py
+21 -21
@@ -8,72 +8,72 @@ from pydantic import Field
8 class ProvisionWorkerRequest(BaseModel):
9 customer_name: str = Field(
10 ...,
11 - example="SOCFortress",
11 + examples=["SOCFortress"],
12 description="The name of the customer",
13 )
14 customer_code: str = Field(
15 ...,
16 - example="socfortress",
16 + examples=["socfortress"],
17 description="The code of the customer",
18 )
19 wazuh_auth_password: str = Field(
20 ...,
21 - example="password",
21 + examples=["password"],
22 description="The password for the Wazuh API user",
23 )
24 wazuh_registration_port: str = Field(
25 ...,
26 - example="1515",
26 + examples=["1515"],
27 description="The port for the Wazuh registration service",
28 )
29 wazuh_logs_port: str = Field(
30 ...,
31 - example="1514",
31 + examples=["1514"],
32 description="The port for the Wazuh logs service",
33 )
34 wazuh_api_port: str = Field(
35 ...,
36 - example="55001",
36 + examples=["55001"],
37 description="The port for the Wazuh API service",
38 )
39 wazuh_cluster_name: str = Field(
40 ...,
41 - example="SOCFortress",
41 + examples=["SOCFortress"],
42 description="The name of the Wazuh cluster",
43 )
44 wazuh_cluster_key: str = Field(
45 ...,
46 - example="password",
46 + examples=["password"],
47 description="The password for the Wazuh cluster",
48 )
49 wazuh_master_ip: str = Field(
50 ...,
51 - example="1.1.1.1",
51 + examples=["1.1.1.1"],
52 description="The IP address of the Wazuh master",
53 )
54 wazuh_worker_hostname: Optional[str] = Field(
55 None,
56 - example="worker1",
56 + examples=["worker1"],
57 description="The hostname of the Wazuh worker",
58 )
59 portainer_deployment: Optional[bool] = Field(
60 None,
61 - example=True,
61 + examples=[True],
62 description="Whether deployment of Portainer is occurring",
63 )
64 swarm_nodes: Optional[List[str]] = Field(
65 None,
66 - example=["127.0.0.1"],
66 + examples=[["127.0.0.1"]],
67 description="The IP addresses of the swarm nodes",
68 )
69 wazuh_manager_version: Optional[str] = Field(
70 None,
71 - example="4.10.1",
71 + examples=["4.10.1"],
72 description="The version of the Wazuh manager",
73 )
74 node_id: Optional[str] = Field(
75 "1",
76 - example="1",
76 + examples=["1"],
77 description="The ID of the node in the swarm",
78 )
79
@@ -81,12 +81,12 @@ class ProvisionWorkerRequest(BaseModel):
81 class ProvisionWorkerResponse(BaseModel):
82 success: bool = Field(
83 ...,
84 - example=True,
84 + examples=[True],
85 description="Whether the worker was provisioned successfully",
86 )
87 message: str = Field(
88 ...,
89 - example="Worker provisioned successfully",
89 + examples=["Worker provisioned successfully"],
90 description="The message returned by the API",
91 )
92
@@ -94,17 +94,17 @@ class ProvisionWorkerResponse(BaseModel):
94 class DecommissionWorkerRequest(BaseModel):
95 customer_name: str = Field(
96 ...,
97 - example="SOCFortress",
97 + examples=["SOCFortress"],
98 description="The name of the customer",
99 )
100 customer_code: str = Field(
101 ...,
102 - example="socfortress",
102 + examples=["socfortress"],
103 description="The code of the customer",
104 )
105 portainer_deployment: Optional[bool] = Field(
106 None,
107 - example=True,
107 + examples=[True],
108 description="Whether deployment of Portainer is occurring",
109 )
110
@@ -112,11 +112,11 @@ class DecommissionWorkerRequest(BaseModel):
112 class DecommissionWorkerResponse(BaseModel):
113 success: bool = Field(
114 ...,
115 - example=True,
115 + examples=[True],
116 description="Whether the worker was decommissioned successfully",
117 )
118 message: str = Field(
119 ...,
120 - example="Worker decommissioned successfully",
120 + examples=["Worker decommissioned successfully"],
121 description="The message returned by the API",
122 )
backend/app/customers/schema/customers.py
+55 -70
@@ -2,7 +2,7 @@ from datetime import datetime
2 from typing import List
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import ConfigDict, BaseModel
6 from pydantic import Field
7
8
@@ -26,31 +26,28 @@ class CustomerRequestBody(BaseModel):
26 customer_type: Optional[str] = Field(None, description="Type of the customer")
27 logo_file: Optional[str] = Field(None, description="Logo file for the customer")
28 is_provisioned: Optional[bool] = Field(None, description="Whether the customer has been provisioned")
29 -
30 - class Config:
31 - orm_mode = True
32 - schema_extra = {
33 - "example": {
34 - "customer_code": "CUST123",
35 - "customer_name": "Sample Customer",
36 - "contact_last_name": "Doe",
37 - "contact_first_name": "John",
38 - "phone": "123-456-7890",
39 - "address_line1": "123 Main St",
40 - "address_line2": "Apt 4",
41 - "city": "Anytown",
42 - "state": "CA",
43 - "postal_code": "12345",
44 - "country": "USA",
45 - "customer_type": "Enterprise",
46 - "logo_file": "logo.png",
47 - "is_provisioned": True,
48 - },
49 - }
29 + model_config = ConfigDict(from_attributes=True, json_schema_extra={
30 + "example": {
31 + "customer_code": "CUST123",
32 + "customer_name": "Sample Customer",
33 + "contact_last_name": "Doe",
34 + "contact_first_name": "John",
35 + "phone": "123-456-7890",
36 + "address_line1": "123 Main St",
37 + "address_line2": "Apt 4",
38 + "city": "Anytown",
39 + "state": "CA",
40 + "postal_code": "12345",
41 + "country": "USA",
42 + "customer_type": "Enterprise",
43 + "logo_file": "logo.png",
44 + "is_provisioned": True,
45 + },
46 + })
47
48
49 class CustomerResponse(BaseModel):
53 - customer: Optional[CustomerRequestBody]
50 + customer: Optional[CustomerRequestBody] = None
51 success: bool
52 message: str
53
@@ -103,55 +100,50 @@ class CustomerMetaRequestBody(BaseModel):
100 None,
101 description="Portainer stack ID for the customer",
102 )
106 -
107 - class Config:
108 - orm_mode = True
109 - schema_extra = {
110 - "example": {
111 - "customer_meta_graylog_index": "graylog_index",
112 - "customer_meta_graylog_stream": "graylog_stream",
113 - "customer_meta_grafana_org_id": "grafana_org",
114 - "customer_meta_wazuh_group": "wazuh_group",
115 - "customer_meta_index_retention": "30D",
116 - "customer_meta_wazuh_registration_port": "1514",
117 - "customer_meta_wazuh_log_ingestion_port": "1515",
118 - "customer_meta_wazuh_auth_password": "wazuh_password",
119 - },
120 - }
103 + model_config = ConfigDict(from_attributes=True, json_schema_extra={
104 + "example": {
105 + "customer_meta_graylog_index": "graylog_index",
106 + "customer_meta_graylog_stream": "graylog_stream",
107 + "customer_meta_grafana_org_id": "grafana_org",
108 + "customer_meta_wazuh_group": "wazuh_group",
109 + "customer_meta_index_retention": "30D",
110 + "customer_meta_wazuh_registration_port": "1514",
111 + "customer_meta_wazuh_log_ingestion_port": "1515",
112 + "customer_meta_wazuh_auth_password": "wazuh_password",
113 + },
114 + })
115
116
117 class CustomerMetaResponse(BaseModel):
124 - customer_meta: Optional[CustomerMetaRequestBody]
118 + customer_meta: Optional[CustomerMetaRequestBody] = None
119 success: bool
120 message: str
121
122
123 ############# Customer Full Response
124 class CustomerFullResponse(BaseModel):
131 - customer: Optional[CustomerRequestBody]
132 - customer_meta: Optional[CustomerMetaRequestBody]
125 + customer: Optional[CustomerRequestBody] = None
126 + customer_meta: Optional[CustomerMetaRequestBody] = None
127 success: bool
128 message: str
129
130
131 ############# Agent Model #############
132 class AgentModel(BaseModel):
139 - id: Optional[int]
140 - os: Optional[str]
141 - label: Optional[str]
142 - wazuh_last_seen: Optional[datetime]
143 - velociraptor_last_seen: Optional[datetime]
144 - velociraptor_agent_version: Optional[str]
145 - ip_address: Optional[str]
146 - agent_id: Optional[str]
147 - hostname: Optional[str]
148 - critical_asset: Optional[bool]
149 - velociraptor_id: Optional[str]
150 - wazuh_agent_version: Optional[str]
151 - customer_code: Optional[str]
152 -
153 - class Config:
154 - orm_mode = True
133 + id: Optional[int] = None
134 + os: Optional[str] = None
135 + label: Optional[str] = None
136 + wazuh_last_seen: Optional[datetime] = None
137 + velociraptor_last_seen: Optional[datetime] = None
138 + velociraptor_agent_version: Optional[str] = None
139 + ip_address: Optional[str] = None
140 + agent_id: Optional[str] = None
141 + hostname: Optional[str] = None
142 + critical_asset: Optional[bool] = None
143 + velociraptor_id: Optional[str] = None
144 + wazuh_agent_version: Optional[str] = None
145 + customer_code: Optional[str] = None
146 + model_config = ConfigDict(from_attributes=True)
147
148
149 class AgentsResponse(BaseModel):
@@ -167,16 +159,9 @@ class DeleteCustomerResponse(BaseModel):
159
160 success: bool
161 message: str
170 -
171 - class Config:
172 - """
173 - Pydantic configuration class.
174 - """
175 -
176 - from_attributes = True
177 - json_schema_extra = {
178 - "example": {
179 - "success": True,
180 - "message": "Customer 'customer_code' deleted successfully",
181 - },
182 - }
162 + model_config = ConfigDict(from_attributes=True, json_schema_extra={
163 + "example": {
164 + "success": True,
165 + "message": "Customer 'customer_code' deleted successfully",
166 + },
167 + })
backend/app/data_store/data_store_schema.py
+2 -4
@@ -1,7 +1,7 @@
1 from datetime import datetime
2 from typing import Optional
3
4 -from pydantic import BaseModel
4 +from pydantic import ConfigDict, BaseModel
5 from pydantic import Field
6
7
@@ -69,9 +69,7 @@ class AgentDataStoreData(BaseModel):
69 uploaded_by: Optional[int] = None
70 notes: Optional[str] = None
71 status: str
72 -
73 - class Config:
74 - from_attributes = True
72 + model_config = ConfigDict(from_attributes=True)
73
74
75 class AgentDataStoreResponse(BaseModel):
backend/app/db/db_setup.py
+4 -4
@@ -39,11 +39,11 @@ async def create_database_if_not_exists(db_url: str, db_name: str):
39 conn = engine.connect()
40 try:
41 # Check if database exists
42 - conn.execute("commit")
42 + conn.execute(text("commit"))
43 exists = conn.execute(text(f"SHOW DATABASES LIKE '{db_name}';")).fetchone()
44 if not exists:
45 # Create database if it does not exist
46 - conn.execute("commit")
46 + conn.execute(text("commit"))
47 conn.execute(text(f"CREATE DATABASE {db_name};"))
48 logger.info(f"Database '{db_name}' created successfully.")
49 else:
@@ -68,11 +68,11 @@ async def create_copilot_user_if_not_exists(db_url: str, db_user_name: str):
68 conn = engine.connect()
69 try:
70 # Check if user exists
71 - conn.execute("commit")
71 + conn.execute(text("commit"))
72 exists = conn.execute(text(f"SELECT * FROM mysql.user WHERE user = '{db_user_name}';")).fetchone()
73 if not exists:
74 # Create user if it does not exist
75 - conn.execute("commit")
75 + conn.execute(text("commit"))
76 conn.execute(text(f"CREATE USER '{db_user_name}'@'%' IDENTIFIED BY '{db_password}';"))
77 logger.info(f"User '{db_user_name}' created successfully with password '{db_password}'.")
78 conn.execute(text(f"GRANT ALL PRIVILEGES ON {db_name}.* TO '{db_user_name}'@'%';"))
backend/app/db/universal_models.py
+8 -12
@@ -247,11 +247,11 @@ class AgentDataStore(SQLModel, table=True):
247
248 # Metadata
249 uploaded_by: Optional[int] = Field(default=None) # User ID who initiated the collection
250 - notes: Optional[str] = Field(sa_column=Column(Text), nullable=True)
250 + notes: Optional[str] = Field(sa_column=Column(Text, nullable=True))
251
252 # Status tracking
253 status: str = Field(max_length=50, default="completed", index=True) # completed, failed, processing
254 - error_message: Optional[str] = Field(sa_column=Column(Text), nullable=True)
254 + error_message: Optional[str] = Field(sa_column=Column(Text, nullable=True))
255
256 # Relationship to Agents table
257 agent: Optional["Agents"] = Relationship(back_populates="data_store")
@@ -448,7 +448,7 @@ class VulnerabilityReport(SQLModel, table=True):
448 generated_by: int = Field(nullable=False) # User ID who generated the report
449
450 # Report filters applied
451 - filters_json: Optional[str] = Field(sa_column=Column(Text), nullable=True) # JSON string of filters used
451 + filters_json: Optional[str] = Field(sa_column=Column(Text, nullable=True)) # JSON string of filters used
452
453 # Statistics
454 total_vulnerabilities: int = Field(default=0)
@@ -459,7 +459,7 @@ class VulnerabilityReport(SQLModel, table=True):
459
460 # Status
461 status: str = Field(max_length=50, default="completed", index=True) # completed, failed, processing
462 - error_message: Optional[str] = Field(sa_column=Column(Text), nullable=True)
462 + error_message: Optional[str] = Field(sa_column=Column(Text, nullable=True))
463
464 # Relationship to Customers table
465 customer: Optional["Customers"] = Relationship()
@@ -486,7 +486,7 @@ class SCAReport(SQLModel, table=True):
486 generated_by: int = Field(nullable=False) # User ID who generated the report
487
488 # Report filters applied
489 - filters_json: Optional[str] = Field(sa_column=Column(Text), nullable=True) # JSON string of filters used
489 + filters_json: Optional[str] = Field(sa_column=Column(Text, nullable=True)) # JSON string of filters used
490
491 # SCA Statistics
492 total_policies: int = Field(default=0) # Number of policy results in report
@@ -497,7 +497,7 @@ class SCAReport(SQLModel, table=True):
497
498 # Status tracking (for background generation)
499 status: str = Field(max_length=50, default="processing", index=True) # processing, completed, failed
500 - error_message: Optional[str] = Field(sa_column=Column(Text), nullable=True)
500 + error_message: Optional[str] = Field(sa_column=Column(Text, nullable=True))
501
502 # Relationship to Customers table
503 customer: Optional["Customers"] = Relationship()
@@ -523,10 +523,6 @@ class EventSources(SQLModel, table=True):
523
524 customer: Optional["Customers"] = Relationship()
525
526 - class Config:
527 - # Enforce event_type values at the application level
528 - pass
529 -
526 def update_from_model(self, source_data):
527 if hasattr(source_data, "name"):
528 self.name = source_data.name
@@ -680,7 +676,7 @@ class AiAnalystPalaceLesson(SQLModel, table=True):
676 review_id: Optional[int] = Field(foreign_key="ai_analyst_review.id", default=None, index=True) # nullable — can be standalone
677 customer_code: str = Field(foreign_key="customers.customer_code", max_length=64, index=True, nullable=False)
678 lesson_type: str = Field(max_length=20, nullable=False) # environment, false_positives, assets, threat_intel
683 - lesson_text: str = Field(sa_column=Column(Text), nullable=False)
679 + lesson_text: str = Field(sa_column=Column(Text, nullable=False))
680 durability: str = Field(default="durable", max_length=8) # one_off, durable
681 status: str = Field(default="pending", max_length=8, index=True) # pending, ingested, failed, expired
682 # drawer_id returned by mempalace add_drawer — required to call
@@ -744,7 +740,7 @@ class CustomerNotificationRoute(SQLModel, table=True):
740 # Free-form destination hint (Slack channel, email recipient,
741 # handle, etc.) — Shuffle's app agent figures out how to route it
742 # within the authenticated app at dispatch time.
747 - destination: str = Field(sa_column=Column(Text), nullable=False)
743 + destination: str = Field(sa_column=Column(Text, nullable=False))
744
745 # 'Critical' | 'High' | 'Medium' | 'Low' | 'Informational'. Inclusive
746 # — a 'High' route fires on Critical and High.
backend/app/healthchecks/agents/schema/agents.py
+23 -25
@@ -4,28 +4,25 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
7 +from pydantic import field_validator, ConfigDict, BaseModel
8 from pydantic import Field
9 -from pydantic import validator
9
10
11 class AgentModel(BaseModel):
13 - id: Optional[int]
14 - os: Optional[str]
15 - label: Optional[str]
16 - wazuh_last_seen: Optional[datetime]
17 - velociraptor_last_seen: Optional[datetime]
18 - velociraptor_agent_version: Optional[str]
19 - ip_address: Optional[str]
20 - agent_id: Optional[str]
21 - hostname: Optional[str]
22 - critical_asset: Optional[bool]
23 - velociraptor_id: Optional[str]
24 - wazuh_agent_version: Optional[str]
25 - customer_code: Optional[str]
26 -
27 - class Config:
28 - orm_mode = True
12 + id: Optional[int] = None
13 + os: Optional[str] = None
14 + label: Optional[str] = None
15 + wazuh_last_seen: Optional[datetime] = None
16 + velociraptor_last_seen: Optional[datetime] = None
17 + velociraptor_agent_version: Optional[str] = None
18 + ip_address: Optional[str] = None
19 + agent_id: Optional[str] = None
20 + hostname: Optional[str] = None
21 + critical_asset: Optional[bool] = None
22 + velociraptor_id: Optional[str] = None
23 + wazuh_agent_version: Optional[str] = None
24 + customer_code: Optional[str] = None
25 + model_config = ConfigDict(from_attributes=True)
26
27
28 class ExtendedAgentModel(AgentModel):
@@ -44,12 +41,12 @@ class ExtendedAgentModel(AgentModel):
41
42
43 class AgentHealthCheckResponse(BaseModel):
47 - healthy_wazuh_agents: Optional[List[ExtendedAgentModel]]
48 - unhealthy_wazuh_agents: Optional[List[ExtendedAgentModel]]
49 - healthy_velociraptor_agents: Optional[List[ExtendedAgentModel]]
50 - unhealthy_velociraptor_agents: Optional[List[ExtendedAgentModel]]
51 - healthy_recent_logs_collected: Optional[List[ExtendedAgentModel]]
52 - unhealthy_recent_logs_collected: Optional[List[ExtendedAgentModel]]
44 + healthy_wazuh_agents: Optional[List[ExtendedAgentModel]] = None
45 + unhealthy_wazuh_agents: Optional[List[ExtendedAgentModel]] = None
46 + healthy_velociraptor_agents: Optional[List[ExtendedAgentModel]] = None
47 + unhealthy_velociraptor_agents: Optional[List[ExtendedAgentModel]] = None
48 + healthy_recent_logs_collected: Optional[List[ExtendedAgentModel]] = None
49 + unhealthy_recent_logs_collected: Optional[List[ExtendedAgentModel]] = None
50 message: str
51 success: bool
52
@@ -91,7 +88,8 @@ class LogsSearchBody(BaseModel):
88 description="The timestamp field to search logs in.",
89 )
90
94 - @validator("timerange")
91 + @field_validator("timerange")
92 + @classmethod
93 def validate_timerange(cls, value):
94 if value[-1] not in ("h", "d", "w", "m"):
95 raise ValueError(
backend/app/incidents/models.py
+16 -40
@@ -17,7 +17,7 @@ class IoC(SQLModel, table=True):
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=Text, nullable=True)
20 + description: Optional[str] = Field(sa_column=Column(Text, nullable=True))
21
22 alerts: List["AlertToIoC"] = Relationship(back_populates="ioc")
23
@@ -34,8 +34,8 @@ class AlertToIoC(SQLModel, table=True):
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=Text, nullable=False)
38 - alert_description: str = Field(sa_column=Text, nullable=False)
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)
@@ -83,7 +83,7 @@ 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)
86 + context: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True))
87
88 assets: List["Asset"] = Relationship(back_populates="alert_context")
89
@@ -238,18 +238,14 @@ class VeloSigmaExclusion(SQLModel, table=True):
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=Text, nullable=True, description="Description of why this exclusion exists")
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),
250 - nullable=True,
251 - description="JSON of field names and values to match in the event data",
252 - )
248 + field_matches: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True), description="JSON of field names and values to match in the event data")
249
250 # Metadata
251 customer_code: Optional[str] = Field(
@@ -272,19 +268,11 @@ class ThresholdAlertMetadata(SQLModel, table=True):
268 id: Optional[int] = Field(default=None, primary_key=True)
269 alert_id: int = Field(foreign_key="incident_management_alert.id", nullable=False, unique=True)
270 event_definition_id: str = Field(max_length=255, nullable=False, description="Graylog event definition ID")
275 - replay_query: str = Field(sa_column=Text, nullable=False, description="Lucene query from Graylog replay_info")
271 + replay_query: str = Field(sa_column=Column(Text, nullable=False), description="Lucene query from Graylog replay_info")
272 timerange_start: datetime = Field(nullable=False, description="Start of the threshold evaluation window")
273 timerange_end: datetime = Field(nullable=False, description="End of the threshold evaluation window")
278 - group_by_fields: Optional[Dict] = Field(
279 - sa_column=Column(JSON),
280 - nullable=True,
281 - description="Group-by field key/value pairs from the threshold event",
282 - )
283 - source_streams: Optional[List] = Field(
284 - sa_column=Column(JSON),
285 - nullable=True,
286 - description="Graylog source stream IDs",
287 - )
274 + group_by_fields: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True), description="Group-by field key/value pairs from the threshold event")
275 + source_streams: Optional[List] = Field(sa_column=Column(JSON, nullable=True), description="Graylog source stream IDs")
276 source: str = Field(max_length=50, nullable=False, description="SOURCE field value (e.g. wazuh)")
277 resolved_index_name: str = Field(max_length=255, nullable=False, description="OpenSearch index of the resolved event")
278 resolved_index_id: str = Field(max_length=255, nullable=False, description="OpenSearch document ID of the resolved event")
@@ -307,7 +295,7 @@ class CaseTemplate(SQLModel, table=True):
295
296 id: Optional[int] = Field(default=None, primary_key=True)
297 name: str = Field(max_length=255, nullable=False, description="Friendly template name")
310 - description: Optional[str] = Field(sa_column=Text, nullable=True, description="What this template is for")
298 + description: Optional[str] = Field(sa_column=Column(Text, nullable=True), description="What this template is for")
299 customer_code: Optional[str] = Field(
300 max_length=50,
301 nullable=True,
@@ -338,12 +326,8 @@ class CaseTemplateTask(SQLModel, table=True):
326 id: Optional[int] = Field(default=None, primary_key=True)
327 template_id: int = Field(foreign_key="incident_management_case_template.id", nullable=False)
328 title: str = Field(max_length=500, nullable=False)
341 - description: Optional[str] = Field(sa_column=Text, nullable=True)
342 - guidelines: Optional[str] = Field(
343 - sa_column=Text,
344 - nullable=True,
345 - description="Best practices / steps the analyst should follow when executing this task",
346 - )
329 + description: Optional[str] = Field(sa_column=Column(Text, nullable=True))
330 + guidelines: Optional[str] = Field(sa_column=Column(Text, nullable=True), description="Best practices / steps the analyst should follow when executing this task")
331 mandatory: bool = Field(
332 default=False,
333 nullable=False,
@@ -378,8 +362,8 @@ class CaseTask(SQLModel, table=True):
362
363 # Snapshot of template task definition at the time of application.
364 title: str = Field(max_length=500, nullable=False)
381 - description: Optional[str] = Field(sa_column=Text, nullable=True)
382 - guidelines: Optional[str] = Field(sa_column=Text, nullable=True)
365 + description: Optional[str] = Field(sa_column=Column(Text, nullable=True))
366 + guidelines: Optional[str] = Field(sa_column=Column(Text, nullable=True))
367 mandatory: bool = Field(default=False, nullable=False)
368 order_index: int = Field(default=0, nullable=False)
369
@@ -390,11 +374,7 @@ class CaseTask(SQLModel, table=True):
374 nullable=False,
375 description="One of TODO, DONE, NOT_NECESSARY (NOT_NECESSARY only valid when mandatory=False).",
376 )
393 - evidence_comment: Optional[str] = Field(
394 - sa_column=Text,
395 - nullable=True,
396 - description="Free-form notes / evidence (logs, command output) attached when status changes.",
397 - )
377 + evidence_comment: Optional[str] = Field(sa_column=Column(Text, nullable=True), description="Free-form notes / evidence (logs, command output) attached when status changes.")
378 completed_by: Optional[str] = Field(max_length=100, nullable=True)
379 completed_at: Optional[datetime] = Field(default=None, nullable=True)
380
@@ -428,11 +408,7 @@ class CaseEvent(SQLModel, table=True):
408 )
409 actor: str = Field(max_length=100, nullable=False, description="user_name that performed the action")
410 timestamp: datetime = Field(default_factory=datetime.utcnow, index=True)
431 - payload: Optional[Dict] = Field(
432 - sa_column=Column(JSON),
433 - nullable=True,
434 - description="Event-type-specific JSON payload (e.g., from_status/to_status, alert_id, task_id).",
435 - )
411 + payload: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True), description="Event-type-specific JSON payload (e.g., from_status/to_status, alert_id, task_id).")
412
413
414 class TagAccessSettings(SQLModel, table=True):
backend/app/incidents/schema/alert_collection.py
+2
@@ -36,6 +36,8 @@ class Source(BaseModel):
36 original_alert_id: Optional[str] = Field(None, alias="original_alert_id")
37 original_alert_index_name: Optional[str] = Field(None, alias="original_alert_index_name")
38
39 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
40 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
41 @validator("original_alert_id", "original_alert_index_name", allow_reuse=True, pre=True)
42 def extract_origin_context(cls, v, values, **kwargs):
43 origin_context = values.get("origin_context", "")
backend/app/incidents/schema/case_templates.py
+7 -15
@@ -14,9 +14,8 @@ from typing import Dict
14 from typing import List
15 from typing import Optional
16
17 -from pydantic import BaseModel
17 +from pydantic import field_validator, ConfigDict, BaseModel
18 from pydantic import Field
19 -from pydantic import validator
19
20 # ---------------------------------------------------------------------------
21 # Enums
@@ -82,9 +81,7 @@ class CaseTemplateTaskResponse(BaseModel):
81 guidelines: Optional[str] = None
82 mandatory: bool
83 order_index: int
85 -
86 - class Config:
87 - orm_mode = True
84 + model_config = ConfigDict(from_attributes=True)
85
86
87 # ---------------------------------------------------------------------------
@@ -139,9 +136,7 @@ class CaseTemplateResponse(BaseModel):
136 created_at: datetime
137 updated_at: datetime
138 tasks: List[CaseTemplateTaskResponse] = Field(default_factory=list)
142 -
143 - class Config:
144 - orm_mode = True
139 + model_config = ConfigDict(from_attributes=True)
140
141
142 class CaseTemplateListResponse(BaseModel):
@@ -189,7 +184,8 @@ class CaseTaskUpdate(BaseModel):
184 status: Optional[CaseTaskStatus] = None
185 evidence_comment: Optional[str] = None
186
192 - @validator("status")
187 + @field_validator("status")
188 + @classmethod
189 def _status_must_be_known(cls, v: Optional[CaseTaskStatus]) -> Optional[CaseTaskStatus]:
190 # Pydantic already enforces enum membership; this guard is for clarity
191 # and to catch any future string-coercion shenanigans.
@@ -214,9 +210,7 @@ class CaseTaskResponse(BaseModel):
210 created_by: str
211 created_at: datetime
212 updated_at: datetime
217 -
218 - class Config:
219 - orm_mode = True
213 + model_config = ConfigDict(from_attributes=True)
214
215
216 class CaseTaskListResponse(BaseModel):
@@ -264,9 +258,7 @@ class CaseEventResponse(BaseModel):
258 actor: str
259 timestamp: datetime
260 payload: Optional[Dict[str, Any]] = None
267 -
268 - class Config:
269 - orm_mode = True
261 + model_config = ConfigDict(from_attributes=True)
262
263
264 class CaseTimelineResponse(BaseModel):
backend/app/incidents/schema/db_operations.py
+24 -14
@@ -5,7 +5,7 @@ from typing import List
5 from typing import Optional
6
7 from fastapi import HTTPException
8 -from pydantic import BaseModel
8 +from pydantic import field_validator, BaseModel
9 from pydantic import validator
10
11 from app.incidents.models import Alert
@@ -174,7 +174,8 @@ class AlertIoCCreate(BaseModel):
174 ioc_type: AlertIocValue
175 ioc_description: Optional[str] = None
176
177 - @validator("ioc_type")
177 + @field_validator("ioc_type")
178 + @classmethod
179 def validate_ioc_type(cls, v):
180 if v not in AlertIocValue:
181 raise HTTPException(
@@ -265,7 +266,7 @@ class AlertCreate(BaseModel):
266 status: str
267 alert_creation_time: datetime
268 customer_code: str
268 - time_closed: Optional[datetime]
269 + time_closed: Optional[datetime] = None
270 source: str
271 assigned_to: str
272
@@ -322,7 +323,8 @@ class LinkedCaseCreate(BaseModel):
323 assigned_to: Optional[str] = None
324 id: int
325
325 - @validator("case_creation_time", pre=True)
326 + @field_validator("case_creation_time", mode="before")
327 + @classmethod
328 def format_case_creation_time(cls, v):
329 if isinstance(v, datetime):
330 return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
@@ -352,8 +354,8 @@ class AssetCreate(BaseModel):
354 alert_linked: int
355 asset_name: str
356 alert_context_id: int
355 - agent_id: Optional[str]
356 - velociraptor_id: Optional[str]
357 + agent_id: Optional[str] = None
358 + velociraptor_id: Optional[str] = None
359 customer_code: str
360 index_name: str
361 index_id: str
@@ -386,7 +388,8 @@ class CommentBase(BaseModel):
388 comment: str
389 created_at: str
390
389 - @validator("created_at", pre=True)
391 + @field_validator("created_at", mode="before")
392 + @classmethod
393 def format_created_at(cls, v):
394 if isinstance(v, datetime):
395 return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
@@ -400,7 +403,8 @@ class CaseCommentBase(BaseModel):
403 comment: str
404 created_at: str
405
403 - @validator("created_at", pre=True)
406 + @field_validator("created_at", mode="before")
407 + @classmethod
408 def format_created_at(cls, v):
409 if isinstance(v, datetime):
410 return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
@@ -443,7 +447,8 @@ class AlertOut(BaseModel):
447 linked_cases: List[LinkedCaseCreate] = []
448 iocs: List[IoCBase] = []
449
446 - @validator("alert_creation_time", "time_closed", pre=True)
450 + @field_validator("alert_creation_time", "time_closed", mode="before")
451 + @classmethod
452 def format_datetime(cls, v):
453 if isinstance(v, datetime):
454 return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
@@ -474,7 +479,8 @@ class CaseOut(BaseModel):
479 escalated: bool = False
480 comments: List[CaseCommentBase] = []
481
477 - @validator("case_creation_time", pre=True)
482 + @field_validator("case_creation_time", mode="before")
483 + @classmethod
484 def format_case_creation_time(cls, v):
485 if isinstance(v, datetime):
486 return v.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
@@ -559,6 +565,8 @@ class CaseDownloadDocxRequest(BaseModel):
565 template_name: str
566 file_name: Optional[str] = "case_report.docx"
567
568 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
569 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
570 @validator("file_name", pre=True, always=True)
571 def ensure_docx_extension(cls, v):
572 if v and not v.endswith(".docx"):
@@ -657,6 +665,8 @@ class TagAccessSettingsUpdate(BaseModel):
665 untagged_alert_behavior: UntaggedAlertBehavior = UntaggedAlertBehavior.VISIBLE_TO_ALL
666 default_tag_id: Optional[int] = None
667
668 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
669 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
670 @validator("default_tag_id")
671 def validate_default_tag(cls, v, values):
672 if values.get("untagged_alert_behavior") == UntaggedAlertBehavior.DEFAULT_TAG and v is None:
@@ -672,8 +682,8 @@ class TagAccessSettingsItem(BaseModel):
682
683 enabled: bool
684 untagged_alert_behavior: str
675 - default_tag_id: Optional[int]
676 - default_tag_name: Optional[str]
685 + default_tag_id: Optional[int] = None
686 + default_tag_name: Optional[str] = None
687
688
689 class TagAccessSettingsResponse(BaseModel):
@@ -689,8 +699,8 @@ class UserEffectiveAccessResponse(BaseModel):
699
700 user_id: int
701 username: str
692 - role_id: Optional[int]
693 - role_name: Optional[str]
702 + role_id: Optional[int] = None
703 + role_name: Optional[str] = None
704 accessible_customers: List[str]
705 accessible_tags: List[AlertTagItem]
706 is_tag_unrestricted: bool
backend/app/incidents/schema/incident_alert.py
+13 -20
@@ -5,8 +5,7 @@ from typing import Dict
5 from typing import List
6 from typing import Optional
7
8 -from pydantic import BaseModel
9 -from pydantic import Extra
8 +from pydantic import ConfigDict, BaseModel
9 from pydantic import Field
10
11
@@ -56,18 +55,16 @@ class AutoCreateAlertResponse(BaseModel):
55 alerts_failed: int = 0
56 batches_processed: int = 0
57 alerts_remaining: int = 0
59 -
60 - class Config:
61 - json_schema_extra = {
62 - "example": {
63 - "success": True,
64 - "message": "Processed 5 batches: 487 alerts created, 13 failed. 2000 alerts remaining for next run",
65 - "alerts_created": 487,
66 - "alerts_failed": 13,
67 - "batches_processed": 5,
68 - "alerts_remaining": 2000,
69 - },
70 - }
58 + model_config = ConfigDict(json_schema_extra={
59 + "example": {
60 + "success": True,
61 + "message": "Processed 5 batches: 487 alerts created, 13 failed. 2000 alerts remaining for next run",
62 + "alerts_created": 487,
63 + "alerts_failed": 13,
64 + "batches_processed": 5,
65 + "alerts_remaining": 2000,
66 + },
67 + })
68
69
70 class IndexNamesResponse(BaseModel):
@@ -110,9 +107,7 @@ class GenericSourceModel(BaseModel):
107 None,
108 description="The agent name of the alert.",
109 )
113 -
114 - class Config:
115 - extra = Extra.allow
110 + model_config = ConfigDict(extra="allow")
111
112 def to_dict(self):
113 return self.dict(exclude_none=True)
@@ -143,9 +138,7 @@ class GenericAlertModel(BaseModel):
138 None,
139 description="The type of the alert to be used when creating the CoPilot alert.",
140 )
146 -
147 - class Config:
148 - extra = Extra.allow
141 + model_config = ConfigDict(extra="allow")
142
143
144 class AlertDetailsResponse(BaseModel):
backend/app/incidents/schema/velo_sigma.py
+75 -91
@@ -7,9 +7,8 @@ from typing import Optional
7 from typing import Union
8
9 from loguru import logger
10 -from pydantic import BaseModel
10 +from pydantic import field_validator, ConfigDict, BaseModel
11 from pydantic import Field
12 -from pydantic import validator
12
13
14 class SystemProvider(BaseModel):
@@ -67,9 +66,7 @@ class SysmonEventData(BaseModel):
66 CallTrace: str
67 SourceUser: str
68 TargetUser: str
70 -
71 - class Config:
72 - extra = "allow" # Allow additional fields not specified in the model
69 + model_config = ConfigDict(extra="allow")
70
71
72 # class DefenderEventData(BaseModel):
@@ -121,10 +118,7 @@ class DefenderEventData(BaseModel):
118 remediation_user: Optional[str] = Field(None, alias="Remediation User")
119 security_intelligence_version: Optional[str] = Field(None, alias="Security intelligence Version")
120 engine_version: Optional[str] = Field(None, alias="Engine Version")
124 -
125 - class Config:
126 - allow_population_by_field_name = True
127 - extra = "allow" # Allow additional fields not specified in the model
121 + model_config = ConfigDict(populate_by_name=True, extra="allow")
122
123
124 class PowerShellEventData(BaseModel):
@@ -146,17 +140,13 @@ class PowerShellEventData(BaseModel):
140 CommandName: Optional[str] = None
141 CommandType: Optional[str] = None
142 ConnectedUser: Optional[str] = None
149 -
150 - class Config:
151 - extra = "allow" # Allow additional fields not specified in the model
143 + model_config = ConfigDict(extra="allow")
144
145
146 # Generic event data model that accepts any fields
147 class GenericEventData(BaseModel):
148 """Generic event data structure that accepts any fields"""
157 -
158 - class Config:
159 - extra = "allow"
149 + model_config = ConfigDict(extra="allow")
150
151
152 class EventBase(BaseModel):
@@ -206,7 +196,8 @@ class VelociraptorSigmaAlert(BaseModel):
196 index_pattern: str
197 sourceRef: str
198
209 - @validator("event", pre=True)
199 + @field_validator("event", mode="before")
200 + @classmethod
201 def parse_event(cls, v):
202 """Parse the event if it's a string"""
203 if isinstance(v, str):
@@ -316,64 +307,62 @@ class VelociraptorSigmaAlert(BaseModel):
307
308 # Use generic model for other event types
309 return GenericEvent(**event_data)
319 -
320 - class Config:
321 - schema_extra = {
322 - "example": {
323 - "computer": "WIN-HFOU106TD7K",
324 - "clientID": "C.475df76785008b04",
325 - "channel": "Microsoft-Windows-Sysmon/Operational",
326 - "title": "Proc Access (Sysmon Alert)",
327 - "level": "high",
328 - "event": (
329 - '{"System":{"Provider":{"Name":"Microsoft-Windows-Sysmon","Guid":"5770385F-C22A-43E0-BF4C-06F5698FFBD9"},'
330 - '"EventID":{"Value":10},"Version":3,"Level":4,"Task":10,"Opcode":0,"Keywords":9223372036854775808,'
331 - '"TimeCreated":{"SystemTime":1744233485.0778975},"EventRecordID":564617,"Correlation":{},'
332 - '"Execution":{"ProcessID":2320,"ThreadID":3540},"Channel":"Microsoft-Windows-Sysmon/Operational",'
333 - '"Computer":"WIN-HFOU106TD7K","Security":{"UserID":"S-1-5-18"}},"EventData":{"RuleName":"technique_id=T1003,'
334 - 'technique_name=Credential Dumping","UtcTime":"2025-04-09 21:18:05.064",'
335 - '"SourceProcessGUID":"691FF406-E40B-67F6-2901-000000003A00","SourceProcessId":4964,"SourceThreadId":4448,'
336 - '"SourceImage":"C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe",'
337 - '"TargetProcessGUID":"691FF406-DDC8-67F6-0C00-000000003A00","TargetProcessId":668,'
338 - '"TargetImage":"C:\\\\Windows\\\\system32\\\\lsass.exe","GrantedAccess":2097151,'
339 - '"CallTrace":"C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|'
340 - "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
341 - "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
342 - "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
343 - "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
344 - "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
345 - "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
346 - "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
347 - 'C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)","SourceUser":"WIN-HFOU106TD7K\\\\Administrator",'
348 - '"TargetUser":"NT AUTHORITY\\\\SYSTEM"},'
349 - '"Message":"Process accessed:\\nRuleName: technique_id=T1003,technique_name=Credential Dumping!s!\\n'
350 - "UtcTime: 2025-04-09 21:18:05.064!s!\\n"
351 - "SourceProcessGUID: 691FF406-E40B-67F6-2901-000000003A00!s!\\n"
352 - "SourceProcessId: 4964!s!\\n"
353 - "SourceThreadId: 4448!s!\\n"
354 - "SourceImage: C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe!s!\\n"
355 - "TargetProcessGUID: 691FF406-DDC8-67F6-0C00-000000003A00!s!\\n"
356 - "TargetProcessId: 668!s!\\n"
357 - "TargetImage: C:\\\\Windows\\\\system32\\\\lsass.exe!s!\\n"
358 - "GrantedAccess: 2097151!s!\\n"
359 - "CallTrace: C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|"
360 - "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
361 - "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
362 - "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
363 - "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
364 - "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
365 - "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
366 - "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
367 - "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)!s!\\n"
368 - "SourceUser: WIN-HFOU106TD7K\\\\Administrator!s!\\n"
369 - 'TargetUser: NT AUTHORITY\\\\SYSTEM!s!\\r\\n"}'
370 - ),
371 - "type": "sigma-alert",
372 - "source": "velociraptor",
373 - "index_pattern": "wazuh-*",
374 - "sourceRef": "754600692",
375 - },
376 - }
310 + model_config = ConfigDict(json_schema_extra={
311 + "example": {
312 + "computer": "WIN-HFOU106TD7K",
313 + "clientID": "C.475df76785008b04",
314 + "channel": "Microsoft-Windows-Sysmon/Operational",
315 + "title": "Proc Access (Sysmon Alert)",
316 + "level": "high",
317 + "event": (
318 + '{"System":{"Provider":{"Name":"Microsoft-Windows-Sysmon","Guid":"5770385F-C22A-43E0-BF4C-06F5698FFBD9"},'
319 + '"EventID":{"Value":10},"Version":3,"Level":4,"Task":10,"Opcode":0,"Keywords":9223372036854775808,'
320 + '"TimeCreated":{"SystemTime":1744233485.0778975},"EventRecordID":564617,"Correlation":{},'
321 + '"Execution":{"ProcessID":2320,"ThreadID":3540},"Channel":"Microsoft-Windows-Sysmon/Operational",'
322 + '"Computer":"WIN-HFOU106TD7K","Security":{"UserID":"S-1-5-18"}},"EventData":{"RuleName":"technique_id=T1003,'
323 + 'technique_name=Credential Dumping","UtcTime":"2025-04-09 21:18:05.064",'
324 + '"SourceProcessGUID":"691FF406-E40B-67F6-2901-000000003A00","SourceProcessId":4964,"SourceThreadId":4448,'
325 + '"SourceImage":"C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe",'
326 + '"TargetProcessGUID":"691FF406-DDC8-67F6-0C00-000000003A00","TargetProcessId":668,'
327 + '"TargetImage":"C:\\\\Windows\\\\system32\\\\lsass.exe","GrantedAccess":2097151,'
328 + '"CallTrace":"C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|'
329 + "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
330 + "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
331 + "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
332 + "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
333 + "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
334 + "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
335 + "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
336 + 'C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)","SourceUser":"WIN-HFOU106TD7K\\\\Administrator",'
337 + '"TargetUser":"NT AUTHORITY\\\\SYSTEM"},'
338 + '"Message":"Process accessed:\\nRuleName: technique_id=T1003,technique_name=Credential Dumping!s!\\n'
339 + "UtcTime: 2025-04-09 21:18:05.064!s!\\n"
340 + "SourceProcessGUID: 691FF406-E40B-67F6-2901-000000003A00!s!\\n"
341 + "SourceProcessId: 4964!s!\\n"
342 + "SourceThreadId: 4448!s!\\n"
343 + "SourceImage: C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe!s!\\n"
344 + "TargetProcessGUID: 691FF406-DDC8-67F6-0C00-000000003A00!s!\\n"
345 + "TargetProcessId: 668!s!\\n"
346 + "TargetImage: C:\\\\Windows\\\\system32\\\\lsass.exe!s!\\n"
347 + "GrantedAccess: 2097151!s!\\n"
348 + "CallTrace: C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|"
349 + "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
350 + "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
351 + "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
352 + "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
353 + "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
354 + "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
355 + "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
356 + "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)!s!\\n"
357 + "SourceUser: WIN-HFOU106TD7K\\\\Administrator!s!\\n"
358 + 'TargetUser: NT AUTHORITY\\\\SYSTEM!s!\\r\\n"}'
359 + ),
360 + "type": "sigma-alert",
361 + "source": "velociraptor",
362 + "index_pattern": "wazuh-*",
363 + "sourceRef": "754600692",
364 + },
365 + })
366
367
368 class VelociraptorSigmaAlertResponse(BaseModel):
@@ -403,20 +392,17 @@ class VeloSigmaExclusionCreate(VeloSigmaExclusionBase):
392
393 # Make created_by optional so it can be set by the server
394 created_by: Optional[str] = Field(None, description="User who created this exclusion rule")
406 -
407 - class Config:
408 - # Example showing the expected request format
409 - schema_extra = {
410 - "example": {
411 - "name": "Chainsaw Batch Script Exclusion",
412 - "description": "Exclude alerts from chainsaw batch scripts in Windows Temp folder",
413 - "channel": "Microsoft-Windows-Sysmon/Operational",
414 - "title": "HackTool - Powerup Write Hijack DLL",
415 - "field_matches": {"TargetFilename": "C:\\Windows\\Temp\\chainsaw_batch.bat"},
416 - "customer_code": None, # Optional, NULL means apply to all customers
417 - "enabled": True,
418 - },
419 - }
395 + model_config = ConfigDict(json_schema_extra={
396 + "example": {
397 + "name": "Chainsaw Batch Script Exclusion",
398 + "description": "Exclude alerts from chainsaw batch scripts in Windows Temp folder",
399 + "channel": "Microsoft-Windows-Sysmon/Operational",
400 + "title": "HackTool - Powerup Write Hijack DLL",
401 + "field_matches": {"TargetFilename": "C:\\Windows\\Temp\\chainsaw_batch.bat"},
402 + "customer_code": None, # Optional, NULL means apply to all customers
403 + "enabled": True,
404 + },
405 + })
406
407
408 class VeloSigmaExclusionUpdate(BaseModel):
@@ -439,9 +425,7 @@ class VeloSigmaExclusionResponse(VeloSigmaExclusionBase):
425 created_at: datetime
426 last_matched_at: Optional[datetime] = None
427 match_count: int
442 -
443 - class Config:
444 - orm_mode = True
428 + model_config = ConfigDict(from_attributes=True)
429
430
431 class VeloSigmaExlcusionRouteResponse(BaseModel):
backend/app/integrations/alert_creation_settings/schema/alert_creation_settings.py
+24 -24
@@ -18,18 +18,18 @@ class EventOrderCreate(BaseModel):
18 class AlertCreationSettingsCreate(BaseModel):
19 customer_code: str
20 customer_name: str
21 - excluded_wazuh_rules: Optional[str]
22 - excluded_suricata_rules: Optional[str]
23 - timefield: Optional[str]
24 - office365_organization_id: Optional[str]
25 - iris_customer_id: Optional[int]
26 - iris_customer_name: Optional[str]
27 - iris_index: Optional[str]
28 - grafana_url: Optional[str]
29 - misp_url: Optional[str]
30 - opencti_url: Optional[str]
31 - custom_message: Optional[str]
32 - shuffle_endpoint: Optional[str]
21 + excluded_wazuh_rules: Optional[str] = None
22 + excluded_suricata_rules: Optional[str] = None
23 + timefield: Optional[str] = None
24 + office365_organization_id: Optional[str] = None
25 + iris_customer_id: Optional[int] = None
26 + iris_customer_name: Optional[str] = None
27 + iris_index: Optional[str] = None
28 + grafana_url: Optional[str] = None
29 + misp_url: Optional[str] = None
30 + opencti_url: Optional[str] = None
31 + custom_message: Optional[str] = None
32 + shuffle_endpoint: Optional[str] = None
33 nvd_url: Optional[str] = "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId"
34 event_orders: Optional[List[EventOrderCreate]] = None
35
@@ -48,17 +48,17 @@ class EventOrderResponse(BaseModel):
48 class AlertCreationSettingsResponse(BaseModel):
49 customer_code: str
50 customer_name: str
51 - excluded_wazuh_rules: Optional[str]
52 - excluded_suricata_rules: Optional[str]
53 - timefield: Optional[str]
54 - office365_organization_id: Optional[str]
55 - iris_customer_id: Optional[int]
56 - iris_customer_name: Optional[str]
57 - iris_index: Optional[str]
58 - grafana_url: Optional[str]
59 - misp_url: Optional[str]
60 - opencti_url: Optional[str]
61 - custom_message: Optional[str]
62 - shuffle_endpoint: Optional[str]
51 + excluded_wazuh_rules: Optional[str] = None
52 + excluded_suricata_rules: Optional[str] = None
53 + timefield: Optional[str] = None
54 + office365_organization_id: Optional[str] = None
55 + iris_customer_id: Optional[int] = None
56 + iris_customer_name: Optional[str] = None
57 + iris_index: Optional[str] = None
58 + grafana_url: Optional[str] = None
59 + misp_url: Optional[str] = None
60 + opencti_url: Optional[str] = None
61 + custom_message: Optional[str] = None
62 + shuffle_endpoint: Optional[str] = None
63 nvd_url: Optional[str] = "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId"
64 event_orders: Optional[List[EventOrderResponse]] = None
backend/app/integrations/alert_escalation/schema/escalate_alert.py
+24 -34
@@ -4,8 +4,7 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
8 -from pydantic import Extra
7 +from pydantic import ConfigDict, BaseModel
8 from pydantic import Field
9
10
@@ -73,9 +72,7 @@ class GenericSourceModel(BaseModel):
72 "No autogenerated syslog_level found",
73 description="The timefield of the alert to be used when creating the IRIS alert.",
74 )
76 -
77 - class Config:
78 - extra = Extra.allow
75 + model_config = ConfigDict(extra="allow")
76
77 def to_dict(self):
78 return self.dict(exclude_none=True)
@@ -110,9 +107,7 @@ class GenericAlertModel(BaseModel):
107 "No autogenerated syslog_level found",
108 description="The timefield of the alert to be used when creating the IRIS alert.",
109 )
113 -
114 - class Config:
115 - extra = Extra.allow
110 + model_config = ConfigDict(extra="allow")
111
112
113 # Sample data from `get_single_alert_details`
@@ -131,22 +126,22 @@ sample_data = {
126
127 ########### Create Alerts Schemas ###########
128 class IrisAsset(BaseModel):
134 - asset_name: str = Field(..., description="Name of the asset", example="Server01")
129 + asset_name: str = Field(..., description="Name of the asset", examples=["Server01"])
130 asset_ip: str = Field(
131 ...,
132 description="IP address of the asset",
138 - example="192.168.1.1",
133 + examples=["192.168.1.1"],
134 )
135 asset_description: str = Field(
136 ...,
137 description="Description of the asset",
143 - example="Windows Server",
138 + examples=["Windows Server"],
139 )
145 - asset_type_id: int = Field(..., description="Type ID of the asset", example=1)
140 + asset_type_id: int = Field(..., description="Type ID of the asset", examples=[1])
141 asset_tags: Optional[str] = Field(
142 "Agent ID not found. Ensure the agent has been registered with Wazuh Manager and synced to the Agents table.",
143 description="Tags of the asset",
149 - example="001",
144 + examples=["001"],
145 )
146
147 def to_dict(self):
@@ -157,59 +152,57 @@ class IrisIoc(BaseModel):
152 ioc_value: str = Field(
153 ...,
154 description="Value of the IoC",
160 - example="www.google.com",
155 + examples=["www.google.com"],
156 )
157 ioc_description: str = Field(
158 ...,
159 description="Description of the IoC",
165 - example="Google",
160 + examples=["Google"],
161 )
167 - ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
168 - ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
162 + ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", examples=[1])
163 + ioc_type_id: int = Field(20, description="Type ID of the IoC", examples=[20])
164
165 def to_dict(self):
166 return self.dict(exclude_none=True)
167
168
169 class IrisAlertContext(BaseModel):
175 - alert_id: str = Field(..., description="ID of the alert", example="123")
170 + alert_id: str = Field(..., description="ID of the alert", examples=["123"])
171 alert_name: str = Field(
172 ...,
173 description="Name of the alert",
179 - example="Intrusion Detected",
174 + examples=["Intrusion Detected"],
175 )
181 - alert_level: int = Field(..., description="Severity level of the alert", example=3)
176 + alert_level: int = Field(..., description="Severity level of the alert", examples=[3])
177 process_name: Optional[List[str]] = Field(
183 - example=["No process name found"],
178 + None, examples=[["No process name found"]],
179 description="Name of the process",
180 )
186 -
187 - class Config:
188 - extra = Extra.allow
181 + model_config = ConfigDict(extra="allow")
182
183
184 class IrisAlertPayload(BaseModel):
185 alert_title: str = Field(
186 ...,
187 description="Title of the alert",
195 - example="Intrusion Detected",
188 + examples=["Intrusion Detected"],
189 )
190 alert_description: str = Field(
191 ...,
192 description="Description of the alert",
200 - example="Intrusion Detected by Firewall",
193 + examples=["Intrusion Detected by Firewall"],
194 )
202 - alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
203 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
195 + alert_source: str = Field(..., description="Source of the alert", examples=["Wazuh"])
196 + alert_status_id: int = Field(..., description="Status ID of the alert", examples=[3])
197 alert_severity_id: int = Field(
198 ...,
199 description="Severity ID of the alert",
207 - example=5,
200 + examples=[5],
201 )
202 alert_customer_id: int = Field(
203 ...,
204 description="Customer ID related to the alert",
212 - example=1,
205 + examples=[1],
206 )
207 alert_source_content: Dict[str, Any] = Field(
208 ...,
@@ -219,10 +212,7 @@ class IrisAlertPayload(BaseModel):
212 ...,
213 description="Contextual information about the alert",
214 )
222 -
223 - # Allow extra fields
224 - class Config:
225 - extra = Extra.allow
215 + model_config = ConfigDict(extra="allow")
216
217 def to_dict(self):
218 return self.dict(exclude_none=True)
backend/app/integrations/bitdefender/schema/provision.py
+7 -7
@@ -2,9 +2,8 @@ from typing import Any
2 from typing import Dict
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class ProvisionBitdefenderRequest(BaseModel):
@@ -20,17 +19,18 @@ class ProvisionBitdefenderRequest(BaseModel):
19 )
20 hot_data_retention: Optional[int] = Field(
21 30,
23 - example=30,
22 + examples=[30],
23 description="Number of days to retain hot data",
24 )
25 index_replicas: Optional[int] = Field(
26 0,
28 - example=1,
27 + examples=[1],
28 description="Number of replicas for the customer's Graylog instance",
29 )
30
31 # ensure the `integration_name` is always set to "Bitdefender"
33 - @root_validator(pre=True)
32 + @model_validator(mode="before")
33 + @classmethod
34 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
35 values["integration_name"] = "BitDefender"
36 return values
@@ -97,11 +97,11 @@ class BitdefenderCustomerDetails(BaseModel):
97 )
98 hot_data_retention: int = Field(
99 ...,
100 - example=30,
100 + examples=[30],
101 description="Number of days to retain hot data",
102 )
103 index_replicas: int = Field(
104 ...,
105 - example=1,
105 + examples=[1],
106 description="Number of replicas for the customer's Graylog instance",
107 )
backend/app/integrations/carbonblack/schema/provision.py
+27 -29
@@ -3,9 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class ProvisionCarbonBlackRequest(BaseModel):
@@ -26,7 +25,8 @@ class ProvisionCarbonBlackRequest(BaseModel):
25 )
26
27 # ensure the `integration_name` is always set to "Mimecast"
29 - @root_validator(pre=True)
28 + @model_validator(mode="before")
29 + @classmethod
30 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 values["integration_name"] = "CarbonBlack"
32 return values
@@ -59,29 +59,27 @@ class CarbonBlackEventStream(BaseModel):
59 None,
60 description="Associated content pack, if any",
61 )
62 -
63 - class Config:
64 - schema_extra = {
65 - "example": {
66 - "title": "CarbonBlack SIEM EVENTS - Example Company",
67 - "description": "CarbonBlack SIEM EVENTS - Example Company",
68 - "index_set_id": "12345",
69 - "rules": [
70 - {
71 - "field": "customer_code",
72 - "type": 1,
73 - "inverted": False,
74 - "value": "ExampleCode",
75 - },
76 - {
77 - "field": "integration",
78 - "type": 1,
79 - "inverted": False,
80 - "value": "huntress",
81 - },
82 - ],
83 - "matching_type": "AND",
84 - "remove_matches_from_default_stream": True,
85 - "content_pack": None,
86 - },
87 - }
62 + model_config = ConfigDict(json_schema_extra={
63 + "example": {
64 + "title": "CarbonBlack SIEM EVENTS - Example Company",
65 + "description": "CarbonBlack SIEM EVENTS - Example Company",
66 + "index_set_id": "12345",
67 + "rules": [
68 + {
69 + "field": "customer_code",
70 + "type": 1,
71 + "inverted": False,
72 + "value": "ExampleCode",
73 + },
74 + {
75 + "field": "integration",
76 + "type": 1,
77 + "inverted": False,
78 + "value": "huntress",
79 + },
80 + ],
81 + "matching_type": "AND",
82 + "remove_matches_from_default_stream": True,
83 + "content_pack": None,
84 + },
85 + })
backend/app/integrations/cato/schema/provision.py
+27 -29
@@ -3,9 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class ProvisionCatoRequest(BaseModel):
@@ -26,7 +25,8 @@ class ProvisionCatoRequest(BaseModel):
25 )
26
27 # ensure the `integration_name` is always set to "Mimecast"
29 - @root_validator(pre=True)
28 + @model_validator(mode="before")
29 + @classmethod
30 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 values["integration_name"] = "Cato"
32 return values
@@ -59,29 +59,27 @@ class CatoEventStream(BaseModel):
59 None,
60 description="Associated content pack, if any",
61 )
62 -
63 - class Config:
64 - schema_extra = {
65 - "example": {
66 - "title": "Cato SIEM EVENTS - Example Company",
67 - "description": "Cato SIEM EVENTS - Example Company",
68 - "index_set_id": "12345",
69 - "rules": [
70 - {
71 - "field": "customer_code",
72 - "type": 1,
73 - "inverted": False,
74 - "value": "ExampleCode",
75 - },
76 - {
77 - "field": "integration",
78 - "type": 1,
79 - "inverted": False,
80 - "value": "cato",
81 - },
82 - ],
83 - "matching_type": "AND",
84 - "remove_matches_from_default_stream": True,
85 - "content_pack": None,
86 - },
87 - }
62 + model_config = ConfigDict(json_schema_extra={
63 + "example": {
64 + "title": "Cato SIEM EVENTS - Example Company",
65 + "description": "Cato SIEM EVENTS - Example Company",
66 + "index_set_id": "12345",
67 + "rules": [
68 + {
69 + "field": "customer_code",
70 + "type": 1,
71 + "inverted": False,
72 + "value": "ExampleCode",
73 + },
74 + {
75 + "field": "integration",
76 + "type": 1,
77 + "inverted": False,
78 + "value": "cato",
79 + },
80 + ],
81 + "matching_type": "AND",
82 + "remove_matches_from_default_stream": True,
83 + "content_pack": None,
84 + },
85 + })
backend/app/integrations/copilot_action/schema/copilot_action.py
+5 -2
@@ -6,7 +6,7 @@ from typing import List
6 from typing import Optional
7 from typing import Union
8
9 -from pydantic import BaseModel
9 +from pydantic import field_validator, BaseModel
10 from pydantic import Field
11 from pydantic import validator
12
@@ -34,7 +34,8 @@ class ScriptParameter(BaseModel):
34 enum: Optional[List[str]] = None
35 arg_position: Optional[str] = None
36
37 - @validator("type")
37 + @field_validator("type")
38 + @classmethod
39 def validate_type(cls, v):
40 allowed = {"string", "int", "float", "bool", "path", "enum", "list", "json", "integer", "boolean"}
41 if v not in allowed:
@@ -57,6 +58,8 @@ class ActiveResponseItem(BaseModel):
58 category: Optional[str] = None
59 tags: Optional[List[str]] = None
60
61 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
62 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
63 @validator("icon", always=True)
64 def set_icon_default(cls, v, values):
65 if v is None and "technology" in values:
backend/app/integrations/copilot_mcp/schema/copilot_mcp.py
+3 -3
@@ -5,9 +5,8 @@ from typing import List
5 from typing import Optional
6
7 from fastapi import HTTPException
8 -from pydantic import BaseModel
8 +from pydantic import field_validator, BaseModel
9 from pydantic import Field
10 -from pydantic import validator
10
11
12 class MCPServerType(str, Enum):
@@ -68,7 +67,8 @@ class MCPQueryRequest(BaseModel):
67 mcp_server: MCPServerType = Field(..., description="MCP server to use for the query")
68 verbose: Optional[bool] = Field(default=True, description="Enable verbose output")
69
71 - @validator("mcp_server", pre=True)
70 + @field_validator("mcp_server", mode="before")
71 + @classmethod
72 def validate_mcp_server(cls, v):
73 """Validate that the MCP server type is one of the allowed values"""
74 if isinstance(v, str):
backend/app/integrations/copilot_searches/schema/copilot_searches.py
+6 -6
@@ -117,7 +117,7 @@ class RuleStatsResponse(BaseModel):
117 by_severity: dict[str, int]
118 by_mitre_tactic: dict[str, int]
119 rules_with_graylog: int
120 - last_refreshed: Optional[datetime]
120 + last_refreshed: Optional[datetime] = None
121 cache_ttl_minutes: int
122 success: bool = True
123 message: str = "Statistics fetched successfully"
@@ -152,17 +152,17 @@ class ExecuteSearchRequest(BaseModel):
152 index_pattern: str = Field(
153 ...,
154 description="The index pattern to search (e.g., 'wazuh-alerts-*')",
155 - example="wazuh-alerts-*",
155 + examples=["wazuh-alerts-*"],
156 )
157 parameters: dict[str, Any] = Field(
158 default_factory=dict,
159 description="Parameter values to substitute in the query",
160 - example={
160 + examples=[{
161 "AGENT_NAME": "my-server",
162 "CUSTOMER_CODE": "lab",
163 "START_TIME": "now-24h",
164 "END_TIME": "now",
165 - },
165 + }],
166 )
167 size: Optional[int] = Field(
168 default=None,
@@ -226,10 +226,10 @@ class ExecuteGraylogQueryRequest(BaseModel):
226 parameters: dict[str, Any] = Field(
227 default_factory=dict,
228 description="Parameter values to substitute in the query",
229 - example={
229 + examples=[{
230 "AGENT_NAME": "my-server",
231 "CUSTOMER_CODE": "lab",
232 - },
232 + }],
233 )
234
235
backend/app/integrations/crowdstrike/schema/provision.py
+7 -7
@@ -2,9 +2,8 @@ from typing import Any
2 from typing import Dict
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class ProvisionCrowdstrikeRequest(BaseModel):
@@ -20,17 +19,18 @@ class ProvisionCrowdstrikeRequest(BaseModel):
19 )
20 hot_data_retention: Optional[int] = Field(
21 30,
23 - example=30,
22 + examples=[30],
23 description="Number of days to retain hot data",
24 )
25 index_replicas: Optional[int] = Field(
26 0,
28 - example=1,
27 + examples=[1],
28 description="Number of replicas for the customer's Graylog instance",
29 )
30
31 # ensure the `integration_name` is always set to "Crowdstrike"
33 - @root_validator(pre=True)
32 + @model_validator(mode="before")
33 + @classmethod
34 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
35 values["integration_name"] = "Crowdstrike"
36 return values
@@ -87,11 +87,11 @@ class CrowdstrikeCustomerDetails(BaseModel):
87 )
88 hot_data_retention: int = Field(
89 ...,
90 - example=30,
90 + examples=[30],
91 description="Number of days to retain hot data",
92 )
93 index_replicas: int = Field(
94 ...,
95 - example=1,
95 + examples=[1],
96 description="Number of replicas for the customer's Graylog instance",
97 )
backend/app/integrations/darktrace/schema/provision.py
+27 -29
@@ -3,9 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class ProvisionDarktraceRequest(BaseModel):
@@ -26,7 +25,8 @@ class ProvisionDarktraceRequest(BaseModel):
25 )
26
27 # ensure the `integration_name` is always set to "Mimecast"
29 - @root_validator(pre=True)
28 + @model_validator(mode="before")
29 + @classmethod
30 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 values["integration_name"] = "Darktrace"
32 return values
@@ -59,29 +59,27 @@ class DarktraceEventStream(BaseModel):
59 None,
60 description="Associated content pack, if any",
61 )
62 -
63 - class Config:
64 - schema_extra = {
65 - "example": {
66 - "title": "Darktrace SIEM EVENTS - Example Company",
67 - "description": "Darktrace SIEM EVENTS - Example Company",
68 - "index_set_id": "12345",
69 - "rules": [
70 - {
71 - "field": "customer_code",
72 - "type": 1,
73 - "inverted": False,
74 - "value": "ExampleCode",
75 - },
76 - {
77 - "field": "integration",
78 - "type": 1,
79 - "inverted": False,
80 - "value": "darktrace",
81 - },
82 - ],
83 - "matching_type": "AND",
84 - "remove_matches_from_default_stream": True,
85 - "content_pack": None,
86 - },
87 - }
62 + model_config = ConfigDict(json_schema_extra={
63 + "example": {
64 + "title": "Darktrace SIEM EVENTS - Example Company",
65 + "description": "Darktrace SIEM EVENTS - Example Company",
66 + "index_set_id": "12345",
67 + "rules": [
68 + {
69 + "field": "customer_code",
70 + "type": 1,
71 + "inverted": False,
72 + "value": "ExampleCode",
73 + },
74 + {
75 + "field": "integration",
76 + "type": 1,
77 + "inverted": False,
78 + "value": "darktrace",
79 + },
80 + ],
81 + "matching_type": "AND",
82 + "remove_matches_from_default_stream": True,
83 + "content_pack": None,
84 + },
85 + })
backend/app/integrations/defender_for_endpoint/schema/provision.py
+7 -7
@@ -2,9 +2,8 @@ from typing import Any
2 from typing import Dict
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class ProvisionDefenderForEndpointRequest(BaseModel):
@@ -20,17 +19,18 @@ class ProvisionDefenderForEndpointRequest(BaseModel):
19 )
20 hot_data_retention: Optional[int] = Field(
21 30,
23 - example=30,
22 + examples=[30],
23 description="Number of days to retain hot data",
24 )
25 index_replicas: Optional[int] = Field(
26 0,
28 - example=1,
27 + examples=[1],
28 description="Number of replicas for the customer's Graylog instance",
29 )
30
31 # ensure the `integration_name` is always set to "DefenderForEndpoint"
33 - @root_validator(pre=True)
32 + @model_validator(mode="before")
33 + @classmethod
34 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
35 values["integration_name"] = "DefenderForEndpoint"
36 return values
@@ -87,11 +87,11 @@ class DefenderForEndpointCustomerDetails(BaseModel):
87 )
88 hot_data_retention: int = Field(
89 ...,
90 - example=30,
90 + examples=[30],
91 description="Number of days to retain hot data",
92 )
93 index_replicas: int = Field(
94 ...,
95 - example=1,
95 + examples=[1],
96 description="Number of replicas for the customer's Graylog instance",
97 )
backend/app/integrations/duo/schema/provision.py
+27 -29
@@ -3,9 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class ProvisionDuoRequest(BaseModel):
@@ -26,7 +25,8 @@ class ProvisionDuoRequest(BaseModel):
25 )
26
27 # ensure the `integration_name` is always set to "Mimecast"
29 - @root_validator(pre=True)
28 + @model_validator(mode="before")
29 + @classmethod
30 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 values["integration_name"] = "Duo"
32 return values
@@ -59,29 +59,27 @@ class DuoEventStream(BaseModel):
59 None,
60 description="Associated content pack, if any",
61 )
62 -
63 - class Config:
64 - schema_extra = {
65 - "example": {
66 - "title": "Duo SIEM EVENTS - Example Company",
67 - "description": "Duo SIEM EVENTS - Example Company",
68 - "index_set_id": "12345",
69 - "rules": [
70 - {
71 - "field": "customer_code",
72 - "type": 1,
73 - "inverted": False,
74 - "value": "ExampleCode",
75 - },
76 - {
77 - "field": "integration",
78 - "type": 1,
79 - "inverted": False,
80 - "value": "huntress",
81 - },
82 - ],
83 - "matching_type": "AND",
84 - "remove_matches_from_default_stream": True,
85 - "content_pack": None,
86 - },
87 - }
62 + model_config = ConfigDict(json_schema_extra={
63 + "example": {
64 + "title": "Duo SIEM EVENTS - Example Company",
65 + "description": "Duo SIEM EVENTS - Example Company",
66 + "index_set_id": "12345",
67 + "rules": [
68 + {
69 + "field": "customer_code",
70 + "type": 1,
71 + "inverted": False,
72 + "value": "ExampleCode",
73 + },
74 + {
75 + "field": "integration",
76 + "type": 1,
77 + "inverted": False,
78 + "value": "huntress",
79 + },
80 + ],
81 + "matching_type": "AND",
82 + "remove_matches_from_default_stream": True,
83 + "content_pack": None,
84 + },
85 + })
backend/app/integrations/github_audit/model.py
+7 -23
@@ -58,11 +58,7 @@ class GitHubAuditConfig(SQLModel, table=True):
58 default="all",
59 description="'all', 'include', or 'exclude'",
60 )
61 - repo_filter_list: Optional[List[str]] = Field(
62 - sa_column=Column(JSON),
63 - nullable=True,
64 - description="List of repos to include/exclude based on filter_mode",
65 - )
61 + repo_filter_list: Optional[List[str]] = Field(sa_column=Column(JSON, nullable=True), description="List of repos to include/exclude based on filter_mode")
62
63 # Notification settings
64 notify_on_critical: bool = Field(default=True, description="Send notification on critical findings")
@@ -136,21 +132,13 @@ class GitHubAuditReport(SQLModel, table=True):
132 default="running",
133 description="'running', 'completed', 'failed'",
134 )
139 - error_message: Optional[str] = Field(sa_column=Text, nullable=True)
135 + error_message: Optional[str] = Field(sa_column=Column(Text, nullable=True))
136
137 # Full report data stored as JSON
142 - full_report: Optional[Dict] = Field(
143 - sa_column=Column(JSON),
144 - nullable=True,
145 - description="Complete audit report data",
146 - )
138 + full_report: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True), description="Complete audit report data")
139
140 # Top findings for quick access
149 - top_findings: Optional[List[Dict]] = Field(
150 - sa_column=Column(JSON),
151 - nullable=True,
152 - description="Top priority findings",
153 - )
141 + top_findings: Optional[List[Dict]] = Field(sa_column=Column(JSON, nullable=True), description="Top priority findings")
142
143 # Triggered by
144 triggered_by: str = Field(
@@ -191,7 +179,7 @@ class GitHubAuditCheckExclusion(SQLModel, table=True):
179 )
180
181 # Why excluded
194 - reason: str = Field(sa_column=Text, nullable=False, description="Reason for exclusion")
182 + reason: str = Field(sa_column=Column(Text, nullable=False), description="Reason for exclusion")
183 approved_by: Optional[str] = Field(max_length=100, nullable=True)
184 approved_at: Optional[datetime] = Field(nullable=True)
185
@@ -218,14 +206,10 @@ class GitHubAuditBaseline(SQLModel, table=True):
206
207 # Baseline name
208 name: str = Field(max_length=255, nullable=False)
221 - description: Optional[str] = Field(sa_column=Text, nullable=True)
209 + description: Optional[str] = Field(sa_column=Column(Text, nullable=True))
210
211 # Expected values
224 - expected_checks: Optional[Dict] = Field(
225 - sa_column=Column(JSON),
226 - nullable=True,
227 - description="Expected check results by check_id: {check_id: expected_status}",
228 - )
212 + expected_checks: Optional[Dict] = Field(sa_column=Column(JSON, nullable=True), description="Expected check results by check_id: {check_id: expected_status}")
213
214 # Baseline from a previous report
215 baseline_report_id: Optional[int] = Field(
backend/app/integrations/huntress/schema/provision.py
+27 -29
@@ -3,9 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class ProvisionHuntressRequest(BaseModel):
@@ -26,7 +25,8 @@ class ProvisionHuntressRequest(BaseModel):
25 )
26
27 # ensure the `integration_name` is always set to "Mimecast"
29 - @root_validator(pre=True)
28 + @model_validator(mode="before")
29 + @classmethod
30 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 values["integration_name"] = "Huntress"
32 return values
@@ -59,29 +59,27 @@ class HuntressEventStream(BaseModel):
59 None,
60 description="Associated content pack, if any",
61 )
62 -
63 - class Config:
64 - schema_extra = {
65 - "example": {
66 - "title": "Huntress SIEM EVENTS - Example Company",
67 - "description": "Huntress SIEM EVENTS - Example Company",
68 - "index_set_id": "12345",
69 - "rules": [
70 - {
71 - "field": "customer_code",
72 - "type": 1,
73 - "inverted": False,
74 - "value": "ExampleCode",
75 - },
76 - {
77 - "field": "integration",
78 - "type": 1,
79 - "inverted": False,
80 - "value": "huntress",
81 - },
82 - ],
83 - "matching_type": "AND",
84 - "remove_matches_from_default_stream": True,
85 - "content_pack": None,
86 - },
87 - }
62 + model_config = ConfigDict(json_schema_extra={
63 + "example": {
64 + "title": "Huntress SIEM EVENTS - Example Company",
65 + "description": "Huntress SIEM EVENTS - Example Company",
66 + "index_set_id": "12345",
67 + "rules": [
68 + {
69 + "field": "customer_code",
70 + "type": 1,
71 + "inverted": False,
72 + "value": "ExampleCode",
73 + },
74 + {
75 + "field": "integration",
76 + "type": 1,
77 + "inverted": False,
78 + "value": "huntress",
79 + },
80 + ],
81 + "matching_type": "AND",
82 + "remove_matches_from_default_stream": True,
83 + "content_pack": None,
84 + },
85 + })
backend/app/integrations/mimecast/schema/mimecast.py
+7 -11
@@ -9,10 +9,9 @@ from typing import Dict
9 from typing import List
10 from typing import Optional
11
12 -from pydantic import BaseModel
12 +from pydantic import model_validator, ConfigDict, BaseModel
13 from pydantic import Field
14 from pydantic import HttpUrl
15 -from pydantic import root_validator
15
16
17 class PipelineRuleTitles(Enum):
@@ -76,7 +75,7 @@ class MimecastAuthKeys(BaseModel):
75 description="SECRET KEY FOR YOUR ADMINISTRATOR",
76 examples=["00002"],
77 )
79 - URI = str = Field(
78 + URI: str = Field(
79 "/api/audit/get-siem-logs",
80 description="URI FOR YOUR API Endpoint",
81 examples=["/api/audit/get-siem-logs"],
@@ -145,9 +144,7 @@ class MimecastHeaders(BaseModel):
144 alias="Content-Type",
145 description="The type of content, usually application/json.",
146 )
148 -
149 - class Config:
150 - allow_population_by_field_name = True # This allows field population by both alias and field name
147 + model_config = ConfigDict(populate_by_name=True)
148
149
150 class MimecastTTPURLSRequest(BaseModel):
@@ -185,7 +182,8 @@ class MimecastTTPURLSRequest(BaseModel):
182 super().__init__(*args, **kwargs)
183 self.generate_headers("/api/ttp/url/get-logs") # default URI
184
188 - @root_validator(pre=True)
185 + @model_validator(mode="before")
186 + @classmethod
187 def set_time_bounds(cls, values):
188 time_range = values.get("time_range")
189 if time_range:
@@ -248,9 +246,7 @@ class DataItem(BaseModel):
246 to: datetime = Field(..., description="End date-time in ISO 8601 format.")
247 route: str = Field(..., description="Routing information.")
248 scanResult: str = Field(..., description="Scan result.")
251 -
252 - class Config:
253 - allow_population_by_field_name = True # This allows field population by both alias and field name
249 + model_config = ConfigDict(populate_by_name=True)
250
251
252 class RequestBody(BaseModel):
@@ -286,7 +282,7 @@ class TTPResponseDataItem(BaseModel):
282 class TTPResponsePagination(BaseModel):
283 pageSize: int
284 totalCount: int
289 - next: Optional[str]
285 + next: Optional[str] = None
286
287
288 class ResponseMeta(BaseModel):
backend/app/integrations/mimecast/schema/provision.py
+27 -29
@@ -3,9 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class ProvisionMimecastRequest(BaseModel):
@@ -21,7 +20,8 @@ class ProvisionMimecastRequest(BaseModel):
20 )
21
22 # ensure the `integration_name` is always set to "Mimecast"
24 - @root_validator(pre=True)
23 + @model_validator(mode="before")
24 + @classmethod
25 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
26 values["integration_name"] = "Mimecast"
27 return values
@@ -54,29 +54,27 @@ class MimecastEventStream(BaseModel):
54 None,
55 description="Associated content pack, if any",
56 )
57 -
58 - class Config:
59 - schema_extra = {
60 - "example": {
61 - "title": "Mimecast EVENTS - Example Company",
62 - "description": "Mimecast EVENTS - Example Company",
63 - "index_set_id": "12345",
64 - "rules": [
65 - {
66 - "field": "agent_labels_customer",
67 - "type": 1,
68 - "inverted": False,
69 - "value": "ExampleCode",
70 - },
71 - {
72 - "field": "agent_labels_integration",
73 - "type": 1,
74 - "inverted": False,
75 - "value": "Office365",
76 - },
77 - ],
78 - "matching_type": "AND",
79 - "remove_matches_from_default_stream": True,
80 - "content_pack": None,
81 - },
82 - }
57 + model_config = ConfigDict(json_schema_extra={
58 + "example": {
59 + "title": "Mimecast EVENTS - Example Company",
60 + "description": "Mimecast EVENTS - Example Company",
61 + "index_set_id": "12345",
62 + "rules": [
63 + {
64 + "field": "agent_labels_customer",
65 + "type": 1,
66 + "inverted": False,
67 + "value": "ExampleCode",
68 + },
69 + {
70 + "field": "agent_labels_integration",
71 + "type": 1,
72 + "inverted": False,
73 + "value": "Office365",
74 + },
75 + ],
76 + "matching_type": "AND",
77 + "remove_matches_from_default_stream": True,
78 + "content_pack": None,
79 + },
80 + })
backend/app/integrations/modules/schema/carbonblack.py
+17 -17
@@ -1,9 +1,8 @@
1 from typing import Optional
2
3 from fastapi import HTTPException
4 -from pydantic import BaseModel
4 +from pydantic import field_validator, BaseModel
5 from pydantic import Field
6 -from pydantic import validator
6
7
8 class InvokeCarbonBlackRequest(BaseModel):
@@ -20,13 +19,13 @@ class InvokeCarbonBlackRequest(BaseModel):
19
20
21 class CarbonBlackAuthKeys(BaseModel):
23 - carbonblack_api_url: str = Field(..., example="https://127.0.0.1")
24 - carbonblack_api_key: str = Field(..., example="1234567890")
25 - carbonblack_api_id: str = Field(..., example="1234567890")
26 - carbonblack_org_key: str = Field(..., example="1234567890")
22 + carbonblack_api_url: str = Field(..., examples=["https://127.0.0.1"])
23 + carbonblack_api_key: str = Field(..., examples=["1234567890"])
24 + carbonblack_api_id: str = Field(..., examples=["1234567890"])
25 + carbonblack_org_key: str = Field(..., examples=["1234567890"])
26 time_range: Optional[str] = Field(
27 "-15m",
29 - example="-15m",
28 + examples=["-15m"],
29 description="The time range to collect events.",
30 )
31
@@ -45,20 +44,21 @@ class InvokeCarbonBlackResponse(BaseModel):
44
45
46 class CollectCarbonBlack(BaseModel):
48 - integration: str = Field(..., example="carbonblack")
49 - customer_code: str = Field(..., example="socfortress")
50 - graylog_host: str = Field(..., example="127.0.0.1")
51 - graylog_port: str = Field(..., example=12201)
52 - carbonblack_api_url: str = Field(..., example="https://127.0.0.1")
53 - carbonblack_api_key: str = Field(..., example="1234567890")
54 - carbonblack_api_id: str = Field(..., example="1234567890")
55 - carbonblack_org_key: str = Field(..., example="1234567890")
47 + integration: str = Field(..., examples=["carbonblack"])
48 + customer_code: str = Field(..., examples=["socfortress"])
49 + graylog_host: str = Field(..., examples=["127.0.0.1"])
50 + graylog_port: str = Field(..., examples=[12201])
51 + carbonblack_api_url: str = Field(..., examples=["https://127.0.0.1"])
52 + carbonblack_api_key: str = Field(..., examples=["1234567890"])
53 + carbonblack_api_id: str = Field(..., examples=["1234567890"])
54 + carbonblack_org_key: str = Field(..., examples=["1234567890"])
55 time_range: Optional[str] = Field(
56 "-15m",
58 - example="-15m",
57 + examples=["-15m"],
58 )
59
61 - @validator("integration")
60 + @field_validator("integration")
61 + @classmethod
62 def check_integration(cls, v):
63 if v != "carbonblack":
64 raise HTTPException(
backend/app/integrations/modules/schema/cato.py
+11 -11
@@ -1,7 +1,6 @@
1 from fastapi import HTTPException
2 -from pydantic import BaseModel
2 +from pydantic import field_validator, BaseModel
3 from pydantic import Field
4 -from pydantic import validator
4
5
6 class InvokeCatoRequest(BaseModel):
@@ -55,16 +54,17 @@ class InvokeCatoResponse(BaseModel):
54
55
56 class CollectCato(BaseModel):
58 - integration: str = Field(..., example="cato")
59 - customer_code: str = Field(..., example="socfortress")
60 - graylog_host: str = Field(..., example="127.0.0.1")
61 - graylog_port: str = Field(..., example=12201)
62 - api_key: str = Field(..., example="1234567890")
63 - account_id: int = Field(..., example=123456)
64 - event_types: str = Field(..., example="Security")
65 - event_sub_types: str = Field(..., example="NG Anti Malware,Anti Malware,IPS")
57 + integration: str = Field(..., examples=["cato"])
58 + customer_code: str = Field(..., examples=["socfortress"])
59 + graylog_host: str = Field(..., examples=["127.0.0.1"])
60 + graylog_port: str = Field(..., examples=[12201])
61 + api_key: str = Field(..., examples=["1234567890"])
62 + account_id: int = Field(..., examples=[123456])
63 + event_types: str = Field(..., examples=["Security"])
64 + event_sub_types: str = Field(..., examples=["NG Anti Malware,Anti Malware,IPS"])
65
67 - @validator("integration")
66 + @field_validator("integration")
67 + @classmethod
68 def check_integration(cls, v):
69 if v != "cato":
70 raise HTTPException(
backend/app/integrations/modules/schema/darktrace.py
+14 -13
@@ -1,7 +1,6 @@
1 from fastapi import HTTPException
2 -from pydantic import BaseModel
2 +from pydantic import field_validator, BaseModel
3 from pydantic import Field
4 -from pydantic import validator
4
5
6 class InvokeDarktraceRequest(BaseModel):
@@ -54,17 +53,18 @@ class InvokeDarktraceResponse(BaseModel):
53
54
55 class CollectDarktrace(BaseModel):
57 - integration: str = Field(..., example="darktrace")
58 - customer_code: str = Field(..., example="socfortress")
59 - graylog_host: str = Field(..., example="127.0.0.1")
60 - graylog_port: str = Field(..., example=12201)
61 - public_token: str = Field(..., example="public_token")
62 - private_token: str = Field(..., example="private_token")
63 - darktrace_host: str = Field(..., example="https://darktrace.local")
64 - darktrace_port: str = Field(..., example=2026)
65 - timeframe: str = Field(..., example="15m")
56 + integration: str = Field(..., examples=["darktrace"])
57 + customer_code: str = Field(..., examples=["socfortress"])
58 + graylog_host: str = Field(..., examples=["127.0.0.1"])
59 + graylog_port: str = Field(..., examples=[12201])
60 + public_token: str = Field(..., examples=["public_token"])
61 + private_token: str = Field(..., examples=["private_token"])
62 + darktrace_host: str = Field(..., examples=["https://darktrace.local"])
63 + darktrace_port: str = Field(..., examples=[2026])
64 + timeframe: str = Field(..., examples=["15m"])
65
67 - @validator("integration")
66 + @field_validator("integration")
67 + @classmethod
68 def check_integration(cls, v):
69 if v != "darktrace":
70 raise HTTPException(
@@ -73,7 +73,8 @@ class CollectDarktrace(BaseModel):
73 )
74 return v
75
76 - @validator("timeframe")
76 + @field_validator("timeframe")
77 + @classmethod
78 def validate_range(cls, v):
79 if not v.endswith(("m", "h", "d")):
80 raise HTTPException(
backend/app/integrations/modules/schema/duo.py
+14 -13
@@ -1,7 +1,6 @@
1 from fastapi import HTTPException
2 -from pydantic import BaseModel
2 +from pydantic import field_validator, BaseModel
3 from pydantic import Field
4 -from pydantic import validator
4
5
6 class InvokeDuoRequest(BaseModel):
@@ -49,17 +48,18 @@ class InvokeDuoResponse(BaseModel):
48
49
50 class CollectDuo(BaseModel):
52 - integration: str = Field(..., example="duo")
53 - customer_code: str = Field(..., example="socfortress")
54 - integration_key: str = Field(..., example="1234567890")
55 - secret_key: str = Field(..., example="1234567890")
56 - api_host: str = Field(..., example="api-1234567890.duosecurity.com")
57 - api_endpoint: str = Field(..., example="/admin/v2/logs/authentication")
58 - graylog_host: str = Field(..., example="127.0.0.1")
59 - graylog_port: str = Field(..., example=12201)
60 - range: str = Field(..., example="15m") # New field for range
51 + integration: str = Field(..., examples=["duo"])
52 + customer_code: str = Field(..., examples=["socfortress"])
53 + integration_key: str = Field(..., examples=["1234567890"])
54 + secret_key: str = Field(..., examples=["1234567890"])
55 + api_host: str = Field(..., examples=["api-1234567890.duosecurity.com"])
56 + api_endpoint: str = Field(..., examples=["/admin/v2/logs/authentication"])
57 + graylog_host: str = Field(..., examples=["127.0.0.1"])
58 + graylog_port: str = Field(..., examples=[12201])
59 + range: str = Field(..., examples=["15m"]) # New field for range
60
62 - @validator("integration")
61 + @field_validator("integration")
62 + @classmethod
63 def check_integration(cls, v):
64 if v != "duo":
65 raise HTTPException(
@@ -68,7 +68,8 @@ class CollectDuo(BaseModel):
68 )
69 return v
70
71 - @validator("range")
71 + @field_validator("range")
72 + @classmethod
73 def validate_range(cls, v):
74 if not v.endswith(("m", "h", "d")):
75 raise HTTPException(
backend/app/integrations/modules/schema/huntress.py
+12 -12
@@ -1,7 +1,6 @@
1 from fastapi import HTTPException
2 -from pydantic import BaseModel
2 +from pydantic import field_validator, BaseModel
3 from pydantic import Field
4 -from pydantic import validator
4
5
6 class InvokeHuntressRequest(BaseModel):
@@ -44,17 +43,18 @@ class InvokeHuntressResponse(BaseModel):
43
44
45 class CollectHuntress(BaseModel):
47 - integration: str = Field(..., example="huntress")
48 - customer_code: str = Field(..., example="socfortress")
49 - graylog_host: str = Field(..., example="127.0.0.1")
50 - graylog_port: str = Field(..., example=12201)
51 - wazuh_indexer_host: str = Field(..., example="127.0.0.1")
52 - wazuh_indexer_username: str = Field(..., example="admin")
53 - wazuh_indexer_password: str = Field(..., example="admin")
54 - api_key: str = Field(..., example="1234567890")
55 - api_secret: str = Field(..., example="1234567890")
46 + integration: str = Field(..., examples=["huntress"])
47 + customer_code: str = Field(..., examples=["socfortress"])
48 + graylog_host: str = Field(..., examples=["127.0.0.1"])
49 + graylog_port: str = Field(..., examples=[12201])
50 + wazuh_indexer_host: str = Field(..., examples=["127.0.0.1"])
51 + wazuh_indexer_username: str = Field(..., examples=["admin"])
52 + wazuh_indexer_password: str = Field(..., examples=["admin"])
53 + api_key: str = Field(..., examples=["1234567890"])
54 + api_secret: str = Field(..., examples=["1234567890"])
55
57 - @validator("integration")
56 + @field_validator("integration")
57 + @classmethod
58 def check_integration(cls, v):
59 if v != "huntress":
60 raise HTTPException(
backend/app/integrations/modules/schema/mimecast.py
+8 -8
@@ -1,9 +1,8 @@
1 from typing import Optional
2
3 from fastapi import HTTPException
4 -from pydantic import BaseModel
4 +from pydantic import field_validator, BaseModel
5 from pydantic import Field
6 -from pydantic import validator
6
7
8 class InvokeMimecastRequest(BaseModel):
@@ -45,7 +44,7 @@ class MimecastAuthKeys(BaseModel):
44 description="SECRET KEY FOR YOUR ADMINISTRATOR",
45 examples=["00002"],
46 )
48 - URI = str = Field(
47 + URI: str = Field(
48 "/api/audit/get-siem-logs",
49 description="URI FOR YOUR API Endpoint",
50 examples=["/api/audit/get-siem-logs"],
@@ -66,10 +65,10 @@ class InvokeMimecastResponse(BaseModel):
65
66
67 class CollectMimecast(BaseModel):
69 - integration: str = Field(..., example="mimecast")
70 - customer_code: str = Field(..., example="socfortress")
71 - graylog_host: str = Field(..., example="127.0.0.1")
72 - graylog_port: str = Field(..., example=12201)
68 + integration: str = Field(..., examples=["mimecast"])
69 + customer_code: str = Field(..., examples=["socfortress"])
70 + graylog_host: str = Field(..., examples=["127.0.0.1"])
71 + graylog_port: str = Field(..., examples=[12201])
72 app_id: str = Field(
73 ...,
74 description="YOUR DEVELOPER APPLICATION ID",
@@ -106,7 +105,8 @@ class CollectMimecast(BaseModel):
105 description="Time range for the query (1m, 1h, 1d, 1w)",
106 )
107
109 - @validator("integration")
108 + @field_validator("integration")
109 + @classmethod
110 def check_integration(cls, v):
111 if v != "mimecast":
112 raise HTTPException(
backend/app/integrations/modules/schema/sap_siem.py
+5 -4
@@ -2,9 +2,8 @@ from datetime import datetime
2 from datetime import timedelta
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class InvokeSapSiemRequest(BaseModel):
@@ -31,7 +30,8 @@ class InvokeSapSiemRequest(BaseModel):
30 lower_bound: str = None
31 upper_bound: str = None
32
34 - @root_validator(pre=True)
33 + @model_validator(mode="before")
34 + @classmethod
35 def set_time_bounds(cls, values):
36 time_range = values.get("time_range")
37 if time_range:
@@ -127,7 +127,8 @@ class CollectSapSiemRequest(BaseModel):
127 description="The customer details.",
128 )
129
130 - @root_validator(pre=True)
130 + @model_validator(mode="before")
131 + @classmethod
132 def set_time_bounds(cls, values):
133 time_range = values.get("time_range")
134 if time_range:
backend/app/integrations/monitoring_alert/schema/provision.py
+16 -14
@@ -4,9 +4,8 @@ from typing import List
4 from typing import Optional
5
6 from fastapi import HTTPException
7 -from pydantic import BaseModel
7 +from pydantic import field_validator, BaseModel
8 from pydantic import Field
9 -from pydantic import validator
9
10
11 class AvailableMonitoringAlerts(str, Enum):
@@ -293,7 +292,8 @@ class ProvisionMonitoringAlertRequest(BaseModel):
292 description="The name of the alert to provision.",
293 )
294
296 - @validator("alert_name")
295 + @field_validator("alert_name")
296 + @classmethod
297 def validate_alert_name(cls, v):
298 v = v.replace(" ", "_").upper()
299 if v not in AvailableMonitoringAlerts.__members__:
@@ -303,7 +303,8 @@ class ProvisionMonitoringAlertRequest(BaseModel):
303 )
304 return v
305
306 - @validator("search_within_last", "execute_every")
306 + @field_validator("search_within_last", "execute_every")
307 + @classmethod
308 def validate_non_zero(cls, v):
309 if v == 0:
310 raise HTTPException(
@@ -429,7 +430,8 @@ class CustomFields(BaseModel):
430 name: str
431 value: str
432
432 - @validator("name")
433 + @field_validator("name")
434 + @classmethod
435 def replace_spaces_with_underscores(cls, v):
436 return v.replace(" ", "_")
437
@@ -438,7 +440,7 @@ class CustomMonitoringAlertProvisionModel(BaseModel):
440 alert_name: str = Field(
441 ...,
442 description="The name of the alert to provision.",
441 - example="WAZUH_SYSLOG_LEVEL_ALERT",
443 + examples=["WAZUH_SYSLOG_LEVEL_ALERT"],
444 )
445 alert_description: str = Field(
446 ...,
@@ -449,42 +451,42 @@ class CustomMonitoringAlertProvisionModel(BaseModel):
451 "have a pipeline rule that sets the SYSLOG_LEVEL field to ALERT when "
452 "the Wazuh rule level is greater than 11."
453 ),
452 - example=(
454 + examples=[(
455 "This alert monitors the SYSLOG_LEVEL field in the Wazuh logs. When "
456 "the level is ALERT, it triggers an alert that is created within "
457 "DFIR-IRIS. Ensure that you have a pipeline rule that sets the "
458 "SYSLOG_LEVEL field to ALERT when the Wazuh rule level is greater than 11."
457 - ),
459 + )],
460 )
461 alert_priority: AlertPriority = Field(
462 ...,
463 description="The priority of the alert to provision.",
462 - example=2,
464 + examples=[2],
465 )
466 search_query: str = Field(
467 ...,
468 description="The search query to use for the alert.",
467 - example="syslog_type:wazuh AND syslog_level:alert",
469 + examples=["syslog_type:wazuh AND syslog_level:alert"],
470 )
471 streams: Optional[List[str]] = Field(
472 [],
473 description="The streams to use for the alert.",
472 - example=["5f3e4c3b3f37b70001f3d7b3"],
474 + examples=[["5f3e4c3b3f37b70001f3d7b3"]],
475 )
476 custom_fields: Optional[List[CustomFields]] = Field(
477 None,
478 description="The custom fields to use for the alert.",
477 - example=[{"name": "source", "value": "Wazuh"}],
479 + examples=[[{"name": "source", "value": "Wazuh"}]],
480 )
481 search_within_ms: int = Field(
482 ...,
483 description="The time in milliseconds to search within for the alert.",
482 - example=300000,
484 + examples=[300000],
485 )
486 execute_every_ms: int = Field(
487 ...,
488 description="The time in milliseconds to execute the alert search.",
487 - example=300000,
489 + examples=[300000],
490 )
491
492 # ! I think I can remove the requirement for the CUSTOMER_CODE field.
backend/app/integrations/office365/schema/provision.py
+3 -3
@@ -2,9 +2,8 @@ from enum import Enum
2 from typing import Any
3 from typing import Dict
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class PipelineRuleTitles(Enum):
@@ -32,7 +31,8 @@ class ProvisionOffice365Request(BaseModel):
31 )
32
33 # ensure the `integration_name` is always set to "Office365"
35 - @root_validator(pre=True)
34 + @model_validator(mode="before")
35 + @classmethod
36 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
37 values["integration_name"] = "Office365"
38 return values
backend/app/integrations/sap_siem/schema/provision.py
+27 -29
@@ -3,9 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import model_validator, ConfigDict, BaseModel
7 from pydantic import Field
8 -from pydantic import root_validator
8
9
10 class ProvisionSapSiemRequest(BaseModel):
@@ -26,7 +25,8 @@ class ProvisionSapSiemRequest(BaseModel):
25 )
26
27 # ensure the `integration_name` is always set to "Mimecast"
29 - @root_validator(pre=True)
28 + @model_validator(mode="before")
29 + @classmethod
30 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 values["integration_name"] = "SAP SIEM"
32 return values
@@ -59,29 +59,27 @@ class SapSiemEventStream(BaseModel):
59 None,
60 description="Associated content pack, if any",
61 )
62 -
63 - class Config:
64 - schema_extra = {
65 - "example": {
66 - "title": "SAP SIEM EVENTS - Example Company",
67 - "description": "SAP SIEM EVENTS - Example Company",
68 - "index_set_id": "12345",
69 - "rules": [
70 - {
71 - "field": "customer_code",
72 - "type": 1,
73 - "inverted": False,
74 - "value": "ExampleCode",
75 - },
76 - {
77 - "field": "integration",
78 - "type": 1,
79 - "inverted": False,
80 - "value": "sap_siem",
81 - },
82 - ],
83 - "matching_type": "AND",
84 - "remove_matches_from_default_stream": True,
85 - "content_pack": None,
86 - },
87 - }
62 + model_config = ConfigDict(json_schema_extra={
63 + "example": {
64 + "title": "SAP SIEM EVENTS - Example Company",
65 + "description": "SAP SIEM EVENTS - Example Company",
66 + "index_set_id": "12345",
67 + "rules": [
68 + {
69 + "field": "customer_code",
70 + "type": 1,
71 + "inverted": False,
72 + "value": "ExampleCode",
73 + },
74 + {
75 + "field": "integration",
76 + "type": 1,
77 + "inverted": False,
78 + "value": "sap_siem",
79 + },
80 + ],
81 + "matching_type": "AND",
82 + "remove_matches_from_default_stream": True,
83 + "content_pack": None,
84 + },
85 + })
backend/app/integrations/sap_siem/schema/sap_siem.py
+9 -9
@@ -5,9 +5,8 @@ from typing import Dict
5 from typing import List
6 from typing import Optional
7
8 -from pydantic import BaseModel
8 +from pydantic import model_validator, BaseModel
9 from pydantic import Field
10 -from pydantic import root_validator
10
11
12 class InvokeSapSiemRequest(BaseModel):
@@ -34,7 +33,8 @@ class InvokeSapSiemRequest(BaseModel):
33 lower_bound: str = None
34 upper_bound: str = None
35
37 - @root_validator(pre=True)
36 + @model_validator(mode="before")
37 + @classmethod
38 def set_time_bounds(cls, values):
39 time_range = values.get("time_range")
40 if time_range:
@@ -298,7 +298,7 @@ class SuspiciousLogin(BaseModel):
298 customer_code: str
299 logSource: Optional[str] = Field(None)
300 loginID: str
301 - country: Optional[str]
301 + country: Optional[str] = None
302 ip: str
303 event_timestamp: str
304 errMessage: str
@@ -351,20 +351,20 @@ class CaseData(BaseModel):
351 case_id: int
352 open_date: str
353 modification_history: Dict[str, ModificationHistoryEntry]
354 - close_date: Optional[str]
354 + close_date: Optional[str] = None
355 case_description: str
356 classification_id: int
357 case_soc_id: str
358 case_name: str
359 - custom_attributes: Optional[Dict[str, str]]
359 + custom_attributes: Optional[Dict[str, str]] = None
360 case_uuid: str
361 - review_status_id: Optional[int]
361 + review_status_id: Optional[int] = None
362 state_id: int
363 case_customer: int
364 - reviewer_id: Optional[int]
364 + reviewer_id: Optional[int] = None
365 user_id: int
366 owner_id: int
367 - closing_note: Optional[str]
367 + closing_note: Optional[str] = None
368 status_id: int
369
370
backend/app/integrations/schema.py
+2 -4
@@ -1,7 +1,7 @@
1 from typing import List
2 from typing import Optional
3
4 -from pydantic import BaseModel
4 +from pydantic import ConfigDict, BaseModel
5 from pydantic import Field
6
7
@@ -227,9 +227,7 @@ class CustomerIntegrationsMetaSchema(BaseModel):
227 grafana_org_id: str
228 grafana_dashboard_folder_id: str
229 grafana_datasource_uid: Optional[str] = None
230 -
231 - class Config:
232 - orm_mode = True
230 + model_config = ConfigDict(from_attributes=True)
231
232
233 class CustomerIntegrationsMetaResponse(BaseModel):
backend/app/integrations/scoutsuite/schema/scoutsuite.py
+23 -25
@@ -4,7 +4,7 @@ from typing import List
4 from fastapi import HTTPException
5 from pydantic import BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7 +from pydantic import model_validator
8
9
10 class ScoutSuiteReportOptions(str, Enum):
@@ -17,44 +17,42 @@ class ScoutSuiteReportOptionsResponse(BaseModel):
17 options: List[ScoutSuiteReportOptions] = Field(
18 ...,
19 description="The available report generation options",
20 - example=["aws", "azure", "gcp"],
20 + examples=[["aws", "azure", "gcp"]],
21 )
22 success: bool
23 message: str
24
25
26 class AWSScoutSuiteReportRequest(BaseModel):
27 - report_type: str = Field(..., description="The type of report to generate", example="aws")
28 - access_key_id: str = Field(..., description="The AWS access key ID", example="AKIAIOSFODNN7EXAMPLE")
29 - secret_access_key: str = Field(..., description="The AWS secret access key", example="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
30 - report_name: str = Field(..., description="The name of the report", example="aws-report")
31 -
32 - @root_validator
33 - def validate_report_type(cls, values):
34 - report_type = values.get("report_type")
35 - if report_type != ScoutSuiteReportOptions.aws:
27 + report_type: str = Field(..., description="The type of report to generate", examples=["aws"])
28 + access_key_id: str = Field(..., description="The AWS access key ID", examples=["AKIAIOSFODNN7EXAMPLE"])
29 + secret_access_key: str = Field(..., description="The AWS secret access key", examples=["wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"])
30 + report_name: str = Field(..., description="The name of the report", examples=["aws-report"])
31 +
32 + @model_validator(mode="after")
33 + def validate_report_type(self):
34 + if self.report_type != ScoutSuiteReportOptions.aws:
35 raise HTTPException(status_code=400, detail="Invalid report type.")
37 - return values
36 + return self
37
38
39 class AzureScoutSuiteReportRequest(BaseModel):
41 - report_type: str = Field(..., description="The type of report to generate", example="azure")
42 - username: str = Field(..., description="The username used to auth to Azure", example="scoutsuite@socfortress.co")
43 - password: str = Field(..., description="The password used to auth to Azure", example="EXAMPLE_PASSWORD")
44 - tenant_id: str = Field(..., description="The tenant ID used to auth to Azure", example="EXAMPLE_TENANT_ID")
45 - report_name: str = Field(..., description="The name of the report", example="aws-report")
46 -
47 - @root_validator
48 - def validate_report_type(cls, values):
49 - report_type = values.get("report_type")
50 - if report_type != ScoutSuiteReportOptions.azure:
40 + report_type: str = Field(..., description="The type of report to generate", examples=["azure"])
41 + username: str = Field(..., description="The username used to auth to Azure", examples=["scoutsuite@socfortress.co"])
42 + password: str = Field(..., description="The password used to auth to Azure", examples=["EXAMPLE_PASSWORD"])
43 + tenant_id: str = Field(..., description="The tenant ID used to auth to Azure", examples=["EXAMPLE_TENANT_ID"])
44 + report_name: str = Field(..., description="The name of the report", examples=["aws-report"])
45 +
46 + @model_validator(mode="after")
47 + def validate_report_type(self):
48 + if self.report_type != ScoutSuiteReportOptions.azure:
49 raise HTTPException(status_code=400, detail="Invalid report type.")
52 - return values
50 + return self
51
52
53 class GCPScoutSuiteReportRequest(BaseModel):
56 - report_name: str = Field(..., description="The name of the report", example="gcp-report")
57 - file_path: str = Field(..., description="The path to the GCP credentials file", example="gcp-credentials.json")
54 + report_name: str = Field(..., description="The name of the report", examples=["gcp-report"])
55 + file_path: str = Field(..., description="The path to the GCP credentials file", examples=["gcp-credentials.json"])
56
57
58 class GCPScoutSuiteJSON(BaseModel):
backend/app/integrations/utils/schema.py
+5 -14
@@ -1,8 +1,7 @@
1 from typing import List
2 from typing import Optional
3
4 -from pydantic import BaseModel
5 -from pydantic import Extra
4 +from pydantic import ConfigDict, BaseModel
5 from pydantic import Field
6
7
@@ -112,9 +111,7 @@ class WazuhSocketPayload(BaseModel):
111 description="The integration name.",
112 examples="sublime",
113 )
115 -
116 - class Config:
117 - extra = Extra.allow
114 + model_config = ConfigDict(extra="allow")
115
116 def to_dict(self):
117 return self.dict(exclude_none=True)
@@ -192,9 +189,7 @@ class ShufflePayload(BaseModel):
189 description="The hostname of the affected asset.",
190 examples="test-hostname",
191 )
195 -
196 - class Config:
197 - extra = Extra.allow
192 + model_config = ConfigDict(extra="allow")
193
194 def to_dict(self):
195 return self.dict(exclude_none=True)
@@ -212,9 +207,7 @@ class EventShipperPayload(BaseModel):
207 description="The customer code.",
208 examples="socfortress",
209 )
215 -
216 - class Config:
217 - extra = Extra.allow
210 + model_config = ConfigDict(extra="allow")
211
212 def to_dict(self):
213 return self.dict(exclude_none=True)
@@ -269,9 +262,7 @@ class PraecoAlertConfig(BaseModel):
262 timestamp_type: str
263 type: str
264 use_strftime_index: bool
272 -
273 - class Config:
274 - allow_population_by_field_name = True
265 + model_config = ConfigDict(populate_by_name=True)
266
267
268 class PraecoProvisionAlertResponse(BaseModel):
backend/app/middleware/license.py
+9 -9
@@ -78,7 +78,7 @@ class CreateCustomerKeyResult(BaseModel):
78 customerId: int
79 key: str
80 result: int
81 - message: Optional[str]
81 + message: Optional[str] = None
82
83
84 class CreateCustomerKeyResponseModel(BaseModel):
@@ -104,7 +104,7 @@ class RawResponse(BaseModel):
104 signature: str
105 result: int
106 message: str
107 - metadata: Optional[Any]
107 + metadata: Optional[Any] = None
108
109
110 class LicenseResponse(BaseModel):
@@ -129,7 +129,7 @@ class LicenseResponse(BaseModel):
129 activatedMachines: List
130 trialActivation: bool
131 maxNoOfMachines: int
132 - allowedMachines: Optional[Any]
132 + allowedMachines: Optional[Any] = None
133 dataObjects: List
134 signDate: dt
135 reseller: Optional[Any] = None
@@ -177,15 +177,15 @@ class GetSubscriptionCatalogFeaturesResponse(BaseModel):
177
178
179 class FeatureSubscriptionRequest(BaseModel):
180 - feature_id: int = Field(..., example=1)
181 - cancel_url: str = Field(..., example="https://example.com/cancel")
182 - success_url: str = Field(..., example="https://example.com/success")
183 - customer_email: str = Field(..., example="info@socfortress.co")
184 - company_name: str = Field(..., example="SOCFORTRESS")
180 + feature_id: int = Field(..., examples=[1])
181 + cancel_url: str = Field(..., examples=["https://example.com/cancel"])
182 + success_url: str = Field(..., examples=["https://example.com/success"])
183 + customer_email: str = Field(..., examples=["info@socfortress.co"])
184 + company_name: str = Field(..., examples=["SOCFORTRESS"])
185
186
187 class GetLicenseByEmailRequest(BaseModel):
188 - email: str = Field(..., example="info@socfortress.co")
188 + email: str = Field(..., examples=["info@socfortress.co"])
189
190
191 class AddLicenseToDB(BaseModel):
backend/app/network_connectors/schema.py
+2 -4
@@ -1,7 +1,7 @@
1 from typing import List
2 from typing import Optional
3
4 -from pydantic import BaseModel
4 +from pydantic import ConfigDict, BaseModel
5 from pydantic import Field
6
7
@@ -219,9 +219,7 @@ class CustomerNetworkConnectorsMetaSchema(BaseModel):
219 graylog_stream_id: str
220 grafana_org_id: str
221 grafana_dashboard_folder_id: str
222 -
223 - class Config:
224 - orm_mode = True
222 + model_config = ConfigDict(from_attributes=True)
223
224
225 class CustomerNetworkConnectorsMetaResponse(BaseModel):
backend/app/notifications/schema/notifications.py
+14 -13
@@ -15,7 +15,7 @@ from enum import Enum
15 from typing import List
16 from typing import Optional
17
18 -from pydantic import BaseModel
18 +from pydantic import field_validator, ConfigDict, BaseModel
19 from pydantic import Field
20 from pydantic import validator
21
@@ -98,7 +98,8 @@ class NotificationRouteBase(BaseModel):
98 trigger: NotificationTrigger
99 channel: NotificationChannel
100
101 - @validator("trigger", pre=True)
101 + @field_validator("trigger", mode="before")
102 + @classmethod
103 def _coerce_legacy_trigger(cls, v):
104 """Coerce legacy `severity_critical_or_high` rows on read.
105
@@ -144,16 +145,21 @@ class NotificationRouteBase(BaseModel):
145 description="Human-readable Shuffle app name cached for the UI list (e.g. 'Slack').",
146 )
147
147 - @validator("destination")
148 + @field_validator("destination")
149 + @classmethod
150 def _strip_destination(cls, v: str) -> str:
151 return v.strip()
152
153 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
154 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
155 @validator("shuffle_integration_id", always=True)
156 def _shuffle_integration_required(cls, v, values):
157 if values.get("channel") == NotificationChannel.SHUFFLE and not v:
158 raise ValueError("shuffle_integration_id is required when channel='shuffle'")
159 return v
160
161 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
162 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
163 @validator("shuffle_app_id", always=True)
164 def _shuffle_app_required(cls, v, values):
165 if values.get("channel") == NotificationChannel.SHUFFLE and not v:
@@ -191,9 +197,7 @@ class NotificationRouteRead(NotificationRouteBase):
197 created_by: Optional[str] = None
198 created_at: datetime
199 updated_at: Optional[datetime] = None
194 -
195 - class Config:
196 - orm_mode = True
200 + model_config = ConfigDict(from_attributes=True)
201
202
203 # ---------------------------------------------------------------------------
@@ -211,7 +215,8 @@ class ShuffleIntegrationBase(BaseModel):
215 )
216 enabled: bool = True
217
214 - @validator("shuffle_org_id")
218 + @field_validator("shuffle_org_id")
219 + @classmethod
220 def _strip_org(cls, v: str) -> str:
221 return v.strip()
222
@@ -235,9 +240,7 @@ class ShuffleIntegrationRead(ShuffleIntegrationBase):
240 created_by: Optional[str] = None
241 created_at: datetime
242 updated_at: Optional[datetime] = None
238 -
239 - class Config:
240 - orm_mode = True
243 + model_config = ConfigDict(from_attributes=True)
244
245
246 class ShuffleIntegrationListResponse(BaseModel):
@@ -334,9 +337,7 @@ class DispatchLogRead(BaseModel):
337 latency_ms: Optional[int] = None
338 payload_preview: Optional[str] = None
339 shuffle_execution_id: Optional[str] = None
337 -
338 - class Config:
339 - orm_mode = True
340 + model_config = ConfigDict(from_attributes=True)
341
342
343 class DispatchLogListResponse(BaseModel):
backend/app/schedulers/schema/scheduler.py
+2 -2
@@ -10,8 +10,8 @@ class Job(BaseModel):
10 name: str
11 enabled: bool
12 time_interval: int
13 - last_success: Optional[datetime]
14 - description: Optional[str]
13 + last_success: Optional[datetime] = None
14 + description: Optional[str] = None
15
16
17 class JobsResponse(BaseModel):
backend/app/siem/schema/dashboards.py
+2 -4
@@ -4,7 +4,7 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 -from pydantic import BaseModel
7 +from pydantic import ConfigDict, BaseModel
8 from pydantic import Field
9
10 # ── Template browsing (read from disk) ───────────────────────────
@@ -75,9 +75,7 @@ class EnabledDashboardResponse(BaseModel):
75 template_id: str
76 display_name: str
77 created_at: datetime
78 -
79 - class Config:
80 - orm_mode = True
78 + model_config = ConfigDict(from_attributes=True)
79
80
81 class EnabledDashboardsListResponse(BaseModel):
backend/app/siem/schema/event_sources.py
+2 -4
@@ -3,7 +3,7 @@ from enum import Enum
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel
6 +from pydantic import ConfigDict, BaseModel
7 from pydantic import Field
8
9
@@ -41,9 +41,7 @@ class EventSourceResponse(BaseModel):
41 enabled: bool
42 created_at: datetime
43 updated_at: datetime
44 -
45 - class Config:
46 - orm_mode = True
44 + model_config = ConfigDict(from_attributes=True)
45
46
47 class EventSourcesListResponse(BaseModel):
backend/app/stack_provisioning/graylog/schema/decommission.py
+4 -4
@@ -25,13 +25,13 @@ class AvailableNetworkConnectors(str, Enum):
25 class DecommissionNetworkContentPackRequest(BaseModel):
26 network_connector: AvailableNetworkConnectors = Field(
27 ...,
28 - example=AvailableNetworkConnectors.FORTINET.name,
28 + examples=[AvailableNetworkConnectors.FORTINET.name],
29 description="The name of the content pack to provision in Graylog",
30 )
31 customer_code: str = Field(
32 ...,
33 description="The customer code for the content pack to provision in Graylog",
34 - example="00001",
34 + examples=["00001"],
35 )
36
37 def __init__(self, **data: Any):
@@ -52,11 +52,11 @@ class DecommissionNetworkContentPackRequest(BaseModel):
52 class DecommissionNetworkContentPackResponse(BaseModel):
53 message: str = Field(
54 ...,
55 - example="FORTINET Content Pack decommissioned successfully",
55 + examples=["FORTINET Content Pack decommissioned successfully"],
56 description="Message from the request to decommission a content pack",
57 )
58 success: bool = Field(
59 ...,
60 - example=True,
60 + examples=[True],
61 description="Success of the request to decommission a content pack",
62 )
backend/app/stack_provisioning/graylog/schema/fortinet.py
+7 -7
@@ -2,9 +2,8 @@ from typing import Any
2 from typing import Dict
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class ProvisionFortinetRequest(BaseModel):
@@ -30,17 +29,18 @@ class ProvisionFortinetRequest(BaseModel):
29 )
30 hot_data_retention: int = Field(
31 ...,
33 - example=30,
32 + examples=[30],
33 description="Number of days to retain hot data",
34 )
35 index_replicas: int = Field(
36 ...,
38 - example=1,
37 + examples=[1],
38 description="Number of replicas for the customer's Graylog instance",
39 )
40
41 # ensure the `integration_name` is always set to "Fortinet"
43 - @root_validator(pre=True)
42 + @model_validator(mode="before")
43 + @classmethod
44 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
45 values["integration_name"] = "Fortinet"
46 return values
@@ -82,11 +82,11 @@ class FortinetCustomerDetails(BaseModel):
82 )
83 hot_data_retention: int = Field(
84 ...,
85 - example=30,
85 + examples=[30],
86 description="Number of days to retain hot data",
87 )
88 index_replicas: int = Field(
89 ...,
90 - example=1,
90 + examples=[1],
91 description="Number of replicas for the customer's Graylog instance",
92 )
backend/app/stack_provisioning/graylog/schema/provision.py
+19 -19
@@ -41,22 +41,22 @@ class ContentPackKeywords(BaseModel):
41 customer_code: Optional[str] = Field(None, description="Code of the customer")
42 protocol_type: Optional[str] = Field(
43 None,
44 - example="TCP",
44 + examples=["TCP"],
45 description="The protocol type of the content pack",
46 )
47 syslog_port: Optional[int] = Field(
48 None,
49 - example=514,
49 + examples=[514],
50 description="The syslog port of the content pack",
51 )
52 tls_cert_file: Optional[str] = Field(
53 None,
54 - example="/etc/graylog/sonicwall/cert.pem",
54 + examples=["/etc/graylog/sonicwall/cert.pem"],
55 description="The TLS certificate file path of the content pack",
56 )
57 tls_key_file: Optional[str] = Field(
58 None,
59 - example="/etc/graylog/sonicwall/key.pem",
59 + examples=["/etc/graylog/sonicwall/key.pem"],
60 description="The TLS key file path of the content pack",
61 )
62
@@ -69,20 +69,20 @@ class ContentPack(BaseModel):
69 class AvailableContentPacksResponse(BaseModel):
70 available_content_packs: List[ContentPack] = Field(
71 ...,
72 - example={
72 + examples=[{
73 "name": AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK.name,
74 "description": AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK.value,
75 - },
75 + }],
76 description="The available content packs for provisioning in Graylog",
77 )
78 success: bool = Field(
79 ...,
80 - example=True,
80 + examples=[True],
81 description="Success of the request to get available content packs",
82 )
83 message: str = Field(
84 ...,
85 - example="Available content packs retrieved successfully",
85 + examples=["Available content packs retrieved successfully"],
86 description="Message from the request to get available content packs",
87 )
88
@@ -90,7 +90,7 @@ class AvailableContentPacksResponse(BaseModel):
90 class ProvisionNetworkContentPackRequest(BaseModel):
91 content_pack_name: str = Field(
92 ...,
93 - example="FORTINET",
93 + examples=["FORTINET"],
94 description="The name of the content pack to provision in Graylog",
95 )
96 keywords: Optional[ContentPackKeywords] = Field(
@@ -102,7 +102,7 @@ class ProvisionNetworkContentPackRequest(BaseModel):
102 class ProvisionContentPackRequest(BaseModel):
103 content_pack_name: AvailableContentPacks = Field(
104 ...,
105 - example=AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK,
105 + examples=[AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK],
106 description="The name of the content pack to provision in Graylog",
107 )
108 keywords: Optional[ContentPackKeywords] = Field(
@@ -125,12 +125,12 @@ class ProvisionContentPackRequest(BaseModel):
125 class ProvisionGraylogResponse(BaseModel):
126 success: bool = Field(
127 ...,
128 - example=True,
128 + examples=[True],
129 description="Success of the Graylog provisioning",
130 )
131 message: str = Field(
132 ...,
133 - example="Graylog provisioned successfully",
133 + examples=["Graylog provisioned successfully"],
134 description="Message from the Graylog provisioning",
135 )
136
@@ -138,36 +138,36 @@ class ProvisionGraylogResponse(BaseModel):
138 class ReplaceContentPackKeywords(BaseModel):
139 REPLACE_UUID_GLOBAL: str = Field(
140 ...,
141 - example="12345678-1234-1234-1234-123456789012",
141 + examples=["12345678-1234-1234-1234-123456789012"],
142 description="The UUID of the content pack",
143 )
144 REPLACE_UUID_SPECIFIC: str = Field(
145 ...,
146 - example="12345678-1234-1234-1234-123456789012",
146 + examples=["12345678-1234-1234-1234-123456789012"],
147 description="The UUID of the input",
148 )
149 customer_name: str = Field(
150 ...,
151 - example="SOCFortress",
151 + examples=["SOCFortress"],
152 description="The name of the customer",
153 )
154 customer_code: str = Field(
155 ...,
156 - example="00001",
156 + examples=["00001"],
157 description="The code of the customer",
158 )
159 SYSLOG_PORT: int = Field(
160 ...,
161 - example=514,
161 + examples=[514],
162 description="The syslog port",
163 )
164 TLS_CERT_FILE: Optional[str] = Field(
165 None,
166 - example="/etc/graylog/sonicwall/cert.pem",
166 + examples=["/etc/graylog/sonicwall/cert.pem"],
167 description="The TLS certificate file path",
168 )
169 TLS_KEY_FILE: Optional[str] = Field(
170 None,
171 - example="/etc/graylog/sonicwall/key.pem",
171 + examples=["/etc/graylog/sonicwall/key.pem"],
172 description="The TLS key file path",
173 )
backend/app/stack_provisioning/graylog/schema/sentinelone.py
+7 -7
@@ -2,9 +2,8 @@ from typing import Any
2 from typing import Dict
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class ProvisionSentinelOneRequest(BaseModel):
@@ -25,17 +24,18 @@ class ProvisionSentinelOneRequest(BaseModel):
24 )
25 hot_data_retention: int = Field(
26 ...,
28 - example=30,
27 + examples=[30],
28 description="Number of days to retain hot data",
29 )
30 index_replicas: int = Field(
31 ...,
33 - example=1,
32 + examples=[1],
33 description="Number of replicas for the customer's Graylog instance",
34 )
35
36 # ensure the `integration_name` is always set to "Sentinelone"
38 - @root_validator(pre=True)
37 + @model_validator(mode="before")
38 + @classmethod
39 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
40 values["integration_name"] = "Sentinelone"
41 return values
@@ -92,11 +92,11 @@ class SentinelOneCustomerDetails(BaseModel):
92 )
93 hot_data_retention: int = Field(
94 ...,
95 - example=30,
95 + examples=[30],
96 description="Number of days to retain hot data",
97 )
98 index_replicas: int = Field(
99 ...,
100 - example=1,
100 + examples=[1],
101 description="Number of replicas for the customer's Graylog instance",
102 )
backend/app/stack_provisioning/graylog/schema/sonicwall.py
+7 -7
@@ -2,9 +2,8 @@ from typing import Any
2 from typing import Dict
3 from typing import Optional
4
5 -from pydantic import BaseModel
5 +from pydantic import model_validator, BaseModel
6 from pydantic import Field
7 -from pydantic import root_validator
7
8
9 class ProvisionSonicwallRequest(BaseModel):
@@ -25,17 +24,18 @@ class ProvisionSonicwallRequest(BaseModel):
24 )
25 hot_data_retention: int = Field(
26 ...,
28 - example=30,
27 + examples=[30],
28 description="Number of days to retain hot data",
29 )
30 index_replicas: int = Field(
31 ...,
33 - example=1,
32 + examples=[1],
33 description="Number of replicas for the customer's Graylog instance",
34 )
35
36 # ensure the `integration_name` is always set to "Sonicwall"
38 - @root_validator(pre=True)
37 + @model_validator(mode="before")
38 + @classmethod
39 def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
40 values["integration_name"] = "Sonicwall"
41 return values
@@ -92,11 +92,11 @@ class SonicwallCustomerDetails(BaseModel):
92 )
93 hot_data_retention: int = Field(
94 ...,
95 - example=30,
95 + examples=[30],
96 description="Number of days to retain hot data",
97 )
98 index_replicas: int = Field(
99 ...,
100 - example=1,
100 + examples=[1],
101 description="Number of replicas for the customer's Graylog instance",
102 )
backend/app/threat_intel/schema/epss.py
+1 -1
@@ -23,7 +23,7 @@ class EpssApiResponse(BaseModel):
23 status: str
24 status_code: int
25 version: str
26 - access_control_allow_headers: Optional[str]
26 + access_control_allow_headers: Optional[str] = None
27 access: str
28 total: int
29 offset: int
backend/app/threat_intel/schema/socfortress.py
+17 -12
@@ -4,9 +4,8 @@ from typing import List
4 from typing import Optional
5
6 from fastapi import HTTPException
7 -from pydantic import BaseModel
7 +from pydantic import field_validator, BaseModel
8 from pydantic import Field
9 -from pydantic import validator
9
10
11 class SocfortressThreatIntelRequest(BaseModel):
@@ -43,7 +42,8 @@ class IoCMapping(BaseModel):
42 description="URL to the VirusTotal report",
43 )
44
46 - @validator("score", pre=True)
45 + @field_validator("score", mode="before")
46 + @classmethod
47 def convert_score_to_int(cls, v):
48 """Convert score from string to integer"""
49 if v is None:
@@ -78,7 +78,8 @@ class SocfortressProcessNameAnalysisRequest(BaseModel):
78 description="The process name to evaluate.",
79 )
80
81 - @validator("process_name", pre=True)
81 + @field_validator("process_name", mode="before")
82 + @classmethod
83 def extract_filename(cls, v):
84 match = re.search(r"[^\\]+$", v)
85 return match.group() if match else v
@@ -91,10 +92,11 @@ class SyslogType(str, Enum):
92
93
94 class SocfortressAiAlertRequest(BaseModel):
94 - integration: str = Field(..., example="SOCFORTRESS AI")
95 - alert_payload: dict = Field(..., example={"alert": "test"})
95 + integration: str = Field(..., examples=["SOCFORTRESS AI"])
96 + alert_payload: dict = Field(..., examples=[{"alert": "test"}])
97
97 - @validator("integration")
98 + @field_validator("integration")
99 + @classmethod
100 def check_integration(cls, v):
101 if v != "SOCFORTRESS AI":
102 raise HTTPException(
@@ -103,7 +105,8 @@ class SocfortressAiAlertRequest(BaseModel):
105 )
106 return v
107
106 - @validator("alert_payload")
108 + @field_validator("alert_payload")
109 + @classmethod
110 def check_syslog_type(cls, v):
111 if v.get("syslog_type") not in SyslogType.__members__.values():
112 raise HTTPException(
@@ -229,15 +232,16 @@ class OS(str, Enum):
232
233
234 class VelociraptorArtifactRecommendationRequest(BaseModel):
232 - integration: str = Field(..., example="SOCFORTRESS AI")
235 + integration: str = Field(..., examples=["SOCFORTRESS AI"])
236 artifacts: Optional[List[Artifacts]] = Field(
237 None,
238 description="List of artifacts to recommend.",
239 )
240 os: OS = Field(..., description="The operating system of the endpoint.")
238 - alert_payload: dict = Field(..., example={"alert": "test"})
241 + alert_payload: dict = Field(..., examples=[{"alert": "test"}])
242
240 - @validator("integration")
243 + @field_validator("integration")
244 + @classmethod
245 def check_integration(cls, v):
246 if v != "SOCFORTRESS AI":
247 raise HTTPException(
@@ -246,7 +250,8 @@ class VelociraptorArtifactRecommendationRequest(BaseModel):
250 )
251 return v
252
249 - @validator("alert_payload")
253 + @field_validator("alert_payload")
254 + @classmethod
255 def check_syslog_type(cls, v):
256 if v.get("syslog_type") != "wazuh":
257 raise HTTPException(
backend/app/threat_intel/schema/virustotal.py
+17 -50
@@ -2,8 +2,7 @@ from typing import Dict
2 from typing import List
3 from typing import Optional
4
5 -from pydantic import BaseModel
6 -from pydantic import Extra
5 +from pydantic import ConfigDict, BaseModel
6 from pydantic import Field
7
8
@@ -12,17 +11,13 @@ class AnalysisResult(BaseModel):
11 engine_name: str
12 category: str
13 result: Optional[str] = Field(default=None)
15 -
16 - class Config:
17 - extra = Extra.allow
14 + model_config = ConfigDict(extra="allow")
15
16
17 class TotalVotes(BaseModel):
18 harmless: int
19 malicious: int
23 -
24 - class Config:
25 - extra = Extra.allow
20 + model_config = ConfigDict(extra="allow")
21
22
23 class Attributes(BaseModel):
@@ -44,16 +39,12 @@ class Attributes(BaseModel):
39 last_analysis_stats: Optional[Dict[str, int]] = Field(default=None)
40 last_https_certificate_date: Optional[int] = Field(default=None)
41 network: Optional[str] = Field(default=None)
47 -
48 - class Config:
49 - extra = Extra.allow
42 + model_config = ConfigDict(extra="allow")
43
44
45 class Links(BaseModel):
46 self: str
54 -
55 - class Config:
56 - extra = Extra.allow
47 + model_config = ConfigDict(extra="allow")
48
49
50 class Data(BaseModel):
@@ -61,16 +52,12 @@ class Data(BaseModel):
52 type: str
53 links: Links
54 attributes: Attributes
64 -
65 - class Config:
66 - extra = Extra.allow
55 + model_config = ConfigDict(extra="allow")
56
57
58 class VirusTotalResponse(BaseModel):
59 data: Data
71 -
72 - class Config:
73 - extra = Extra.allow
60 + model_config = ConfigDict(extra="allow")
61
62
63 class VirusTotalRouteResponse(BaseModel):
@@ -82,26 +69,20 @@ class VirusTotalRouteResponse(BaseModel):
69 # New schemas for file submission
70 class FileSubmissionRequest(BaseModel):
71 password: Optional[str] = Field(default=None, description="Password for encrypted files")
85 -
86 - class Config:
87 - extra = Extra.allow
72 + model_config = ConfigDict(extra="allow")
73
74
75 class FileSubmissionData(BaseModel):
76 type: str
77 id: str
93 -
94 - class Config:
95 - extra = Extra.allow
78 + model_config = ConfigDict(extra="allow")
79
80
81 class FileSubmissionResponse(BaseModel):
82 data: FileSubmissionData
83 success: bool
84 message: str
102 -
103 - class Config:
104 - extra = Extra.allow
85 + model_config = ConfigDict(extra="allow")
86
87
88 class FileAnalysisStats(BaseModel):
@@ -113,36 +94,28 @@ class FileAnalysisStats(BaseModel):
94 confirmed_timeout: int = 0
95 failure: int = 0
96 type_unsupported: int = 0
116 -
117 - class Config:
118 - extra = Extra.allow
97 + model_config = ConfigDict(extra="allow")
98
99
100 class FileAnalysisAttributes(BaseModel):
101 date: int
102 status: str
103 stats: FileAnalysisStats
125 -
126 - class Config:
127 - extra = Extra.allow
104 + model_config = ConfigDict(extra="allow")
105
106
107 class FileAnalysisData(BaseModel):
108 type: str
109 id: str
110 attributes: FileAnalysisAttributes
134 -
135 - class Config:
136 - extra = Extra.allow
111 + model_config = ConfigDict(extra="allow")
112
113
114 class FileAnalysisResponse(BaseModel):
115 data: FileAnalysisData
116 success: bool
117 message: str
143 -
144 - class Config:
145 - extra = Extra.allow
118 + model_config = ConfigDict(extra="allow")
119
120
121 class FileReportAttributes(BaseModel):
@@ -161,24 +134,18 @@ class FileReportAttributes(BaseModel):
134 reputation: Optional[int] = None
135 times_submitted: Optional[int] = None
136 total_votes: Optional[TotalVotes] = None
164 -
165 - class Config:
166 - extra = Extra.allow
137 + model_config = ConfigDict(extra="allow")
138
139
140 class FileReportData(BaseModel):
141 type: str
142 id: str
143 attributes: FileReportAttributes
173 -
174 - class Config:
175 - extra = Extra.allow
144 + model_config = ConfigDict(extra="allow")
145
146
147 class FileReportResponse(BaseModel):
148 data: FileReportData
149 success: bool
150 message: str
182 -
183 - class Config:
184 - extra = Extra.allow
151 + model_config = ConfigDict(extra="allow")
backend/app/utils.py
+13 -10
@@ -15,7 +15,7 @@ from fastapi import Request
15 from fastapi import Security
16 from fastapi.exceptions import RequestValidationError
17 from loguru import logger
18 -from pydantic import BaseModel
18 +from pydantic import field_validator, BaseModel
19 from pydantic import Field
20 from pydantic import validator
21 from sqlalchemy.ext.asyncio import AsyncSession
@@ -72,6 +72,8 @@ class ValidationErrorItem(BaseModel):
72 error_type: ErrorType
73 message: str = None # Initialize as None
74
75 + # TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually.
76 + # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information.
77 @validator("message", pre=True, always=True)
78 def set_message(cls, value, values):
79 error_type = values.get("error_type")
@@ -109,21 +111,21 @@ class ValidationErrorResponse(BaseModel):
111 ################## ! LOGGING TO `log_entry` table ! ##################
112 # #######! MODELS !########
113 class LogEntryModel(BaseModel):
112 - event_type: str = Field(..., example="Info", description="Event type")
113 - user_id: Optional[int] = Field(None, example=1, description="User ID")
114 - route: str = Field(..., example="/wazuh_indexer/health", description="Route")
115 - method: str = Field(..., example="GET", description="Method")
116 - status_code: int = Field(..., example=200, description="Status code")
117 - message: str = Field(..., example="Route accessed", description="Message")
114 + event_type: str = Field(..., examples=["Info"], description="Event type")
115 + user_id: Optional[int] = Field(None, examples=[1], description="User ID")
116 + route: str = Field(..., examples=["/wazuh_indexer/health"], description="Route")
117 + method: str = Field(..., examples=["GET"], description="Method")
118 + status_code: int = Field(..., examples=[200], description="Status code")
119 + message: str = Field(..., examples=["Route accessed"], description="Message")
120 additional_info: Optional[str] = Field(
121 None,
120 - example="Additional details here",
122 + examples=["Additional details here"],
123 description="Additional info",
124 )
125
126
127 class LogRetrieveModel(LogEntryModel):
126 - timestamp: datetime = Field(..., example=datetime.now(), description="Timestamp")
128 + timestamp: datetime = Field(..., examples=[datetime.now()], description="Timestamp")
129
130
131 class LogsResponse(BaseModel):
@@ -144,7 +146,8 @@ class TimeRangeModel(BaseModel):
146 description="Time range to fetch logs for, e.g., 1, 1h, 1d, 1w",
147 )
148
147 - @validator("time_range")
149 + @field_validator("time_range")
150 + @classmethod
151 def validate_time_range(cls, value):
152 """
153 Validate the time range value.
backend/app/version/schema/version.py
+14 -16
@@ -1,28 +1,26 @@
1 from typing import Optional
2
3 -from pydantic import BaseModel
3 +from pydantic import ConfigDict, BaseModel
4
5
6 class VersionCheckResponse(BaseModel):
7 success: bool
8 message: str
9 current_version: str
10 - latest_version: Optional[str]
10 + latest_version: Optional[str] = None
11 is_outdated: bool
12 release_url: Optional[str] = None
13 release_notes: Optional[str] = None
14 published_at: Optional[str] = None
15 -
16 - class Config:
17 - json_schema_extra = {
18 - "example": {
19 - "success": True,
20 - "message": "New version v0.1.5 available!",
21 - "current_version": "0.1.4",
22 - "latest_version": "0.1.5",
23 - "is_outdated": True,
24 - "release_url": "https://github.com/socfortress/CoPilot/releases/tag/v0.1.5",
25 - "release_notes": "## What's Changed\n* Feature 1\n* Feature 2",
26 - "published_at": "2025-12-03T18:48:20Z",
27 - },
28 - }
15 + model_config = ConfigDict(json_schema_extra={
16 + "example": {
17 + "success": True,
18 + "message": "New version v0.1.5 available!",
19 + "current_version": "0.1.4",
20 + "latest_version": "0.1.5",
21 + "is_outdated": True,
22 + "release_url": "https://github.com/socfortress/CoPilot/releases/tag/v0.1.5",
23 + "release_notes": "## What's Changed\n* Feature 1\n* Feature 2",
24 + "published_at": "2025-12-03T18:48:20Z",
25 + },
26 + })
backend/requirements.in
+3 -3
@@ -25,7 +25,7 @@ passlib
25 passlib[bcrypt]
26 pdfkit
27 playwright
28 -pydantic[email]<2
28 +pydantic[email]
29 PyJWT
30 PyMySQL
31 pyotp
@@ -38,8 +38,8 @@ qrcode
38 regex
39 requests
40 ScoutSuite
41 -SQLAlchemy<2
42 -sqlmodel<0.0.10
41 +SQLAlchemy
42 +sqlmodel
43 starlette
44 uvicorn[standard]
45 werkzeug
backend/requirements.txt
+7 -5
@@ -17,6 +17,7 @@ aliyun-python-sdk-rds==2.7.53
17 aliyun-python-sdk-sts==3.1.3
18 aliyun-python-sdk-vpc==3.0.48
19 annotated-doc==0.0.4
20 +annotated-types==0.7.0
21 anyio==4.13.0
22 apscheduler==3.11.2
23 argon2-cffi==23.1.0
@@ -26,7 +27,7 @@ asyncio==4.0.0
27 asyncio-throttle==0.1.1
28 attrs==26.1.0
29 azure-common==1.1.28
29 -azure-core==1.40.0
30 +azure-core==1.41.0
31 azure-identity==1.5.0
32 azure-mgmt-authorization==3.0.0
33 azure-mgmt-compute==18.2.0
@@ -144,7 +145,8 @@ pyasn1==0.6.3
145 pyasn1-modules==0.4.2
146 pycparser==3.0
147 pycryptodome==3.23.0
147 -pydantic[email]==1.10.26
148 +pydantic[email]==2.13.4
149 +pydantic-core==2.46.4
150 pydo==0.34.0
151 pyee==13.0.1
152 pygments==2.20.0
@@ -173,16 +175,16 @@ s3transfer==0.17.0
175 scoutsuite==5.14.0
176 shellingham==1.5.4
177 six==1.17.0
176 -sqlalchemy==1.4.54
177 -sqlalchemy2-stubs==0.0.2a38
178 +sqlalchemy==2.0.49
179 sqlitedict==2.1.0
179 -sqlmodel==0.0.9
180 +sqlmodel==0.0.38
181 starlette==0.46.2
182 tempora==5.9.0
183 typer==0.25.1
184 typer-slim==0.24.0
185 typing==3.7.4.3
186 typing-extensions==4.15.0
187 +typing-inspection==0.4.2
188 tzlocal==5.3.1
189 uritemplate==4.2.0
190 urllib3==1.26.20