| 1 | from datetime import datetime |
| 2 | from datetime import timedelta |
| 3 | from enum import Enum |
| 4 | from typing import Dict |
| 5 | from typing import List |
| 6 | from typing import Optional |
| 7 | |
| 8 | from pydantic import BaseModel |
| 9 | from pydantic import Field |
| 10 | from pydantic import model_validator |
| 11 | |
| 12 | |
| 13 | class InvokeSapSiemRequest(BaseModel): |
| 14 | customer_code: str = Field( |
| 15 | ..., |
| 16 | description="The customer code.", |
| 17 | examples=["00002"], |
| 18 | ) |
| 19 | integration_name: str = Field( |
| 20 | "SAP SIEM", |
| 21 | description="The integration name.", |
| 22 | examples=["SAP SIEM"], |
| 23 | ) |
| 24 | threshold: Optional[int] = Field( |
| 25 | 3, |
| 26 | description="Number of 'Invalid LoginID' before the first 'OK'", |
| 27 | ) |
| 28 | time_range: Optional[str] = Field( |
| 29 | "15m", |
| 30 | pattern="^[1-9][0-9]*[mhdw]$", |
| 31 | description="Time range for the query (1m, 1h, 1d, 1w)", |
| 32 | ) |
| 33 | |
| 34 | lower_bound: str = None |
| 35 | upper_bound: str = None |
| 36 | |
| 37 | @model_validator(mode="before") |
| 38 | @classmethod |
| 39 | def set_time_bounds(cls, values): |
| 40 | time_range = values.get("time_range") |
| 41 | if time_range: |
| 42 | unit = time_range[-1] |
| 43 | amount = int(time_range[:-1]) |
| 44 | |
| 45 | now = datetime.utcnow() |
| 46 | |
| 47 | if unit == "m": |
| 48 | lower_bound = now - timedelta(minutes=amount) |
| 49 | elif unit == "h": |
| 50 | lower_bound = now - timedelta(hours=amount) |
| 51 | elif unit == "d": |
| 52 | lower_bound = now - timedelta(days=amount) |
| 53 | elif unit == "w": |
| 54 | lower_bound = now - timedelta(weeks=amount) |
| 55 | |
| 56 | values["lower_bound"] = lower_bound.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" |
| 57 | values["upper_bound"] = now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" |
| 58 | return values |
| 59 | |
| 60 | |
| 61 | class InvokeSAPSiemResponse(BaseModel): |
| 62 | success: bool |
| 63 | message: str |
| 64 | |
| 65 | |
| 66 | class SapSiemAuthKeys(BaseModel): |
| 67 | API_KEY: str = Field( |
| 68 | ..., |
| 69 | description="YOUR API KEY", |
| 70 | examples=["3_yUWT3uDMs9E1N87r4Ey"], |
| 71 | ) |
| 72 | SECRET_KEY: str = Field( |
| 73 | ..., |
| 74 | description="YOUR SECRET KEY", |
| 75 | examples=["4ijD6uMCca"], |
| 76 | ) |
| 77 | USER_KEY: Optional[str] = Field( |
| 78 | None, |
| 79 | description="YOUR USER KEY", |
| 80 | examples=["AK9zAL"], |
| 81 | ) |
| 82 | API_DOMAIN: str = Field( |
| 83 | ..., |
| 84 | description="YOUR API DOMAIN", |
| 85 | examples=["audit.eu1.gigya.com"], |
| 86 | ) |
| 87 | |
| 88 | |
| 89 | class CollectSapSiemRequest(BaseModel): |
| 90 | customer_code: str = Field( |
| 91 | ..., |
| 92 | description="The customer code.", |
| 93 | examples=["00002"], |
| 94 | ) |
| 95 | apiKey: str = Field(..., description="API key for authorization") |
| 96 | secretKey: str = Field(..., description="Secret key for authorization") |
| 97 | userKey: str = Field(..., description="User key for identification") |
| 98 | apiDomain: str = Field(..., description="API domain") |
| 99 | threshold: Optional[int] = Field( |
| 100 | 1, |
| 101 | description="Number of 'Invalid LoginID' before the first 'OK'", |
| 102 | ) |
| 103 | lower_bound: str = None |
| 104 | upper_bound: str = None |
| 105 | |
| 106 | |
| 107 | ######### ! SAP API RESPONSE ! ######### |
| 108 | class HttpReq(BaseModel): |
| 109 | SDK: str = Field( |
| 110 | ..., |
| 111 | description="Software Development Kit used for the HTTP request", |
| 112 | ) |
| 113 | country: str = Field(..., description="Country code") |
| 114 | |
| 115 | |
| 116 | class Params(BaseModel): |
| 117 | clientContext: Optional[str] = Field(None, description="Client context information") |
| 118 | include: Optional[str] = Field(None, description="Data to include in the response") |
| 119 | password: str = Field(..., description="Password for the user") |
| 120 | loginID: str = Field(..., description="Login ID of the user") |
| 121 | apiKey: str = Field(..., description="API key for authorization") |
| 122 | format: Optional[str] = Field(None, description="Response format") |
| 123 | secret: Optional[str] = Field(None, description="Secret key for authorization") |
| 124 | userKey: Optional[str] = Field(None, description="User key for identification") |
| 125 | |
| 126 | |
| 127 | class UserAgent(BaseModel): |
| 128 | os: str = Field(..., description="Operating system of the user") |
| 129 | browser: str = Field(..., description="Browser used by the user") |
| 130 | raw: Optional[str] = Field(None, description="Raw user agent string") |
| 131 | version: str = Field(..., description="Browser version") |
| 132 | platform: str = Field(..., description="Platform type (desktop/mobile)") |
| 133 | |
| 134 | |
| 135 | class UserKeyDetails(BaseModel): |
| 136 | name: Optional[str] = Field(None, description="Name of the user") |
| 137 | emailDomain: Optional[str] = Field(None, description="Email domain of the user") |
| 138 | |
| 139 | |
| 140 | class Restrictions(BaseModel): |
| 141 | ipWhitelistRestricted: bool = Field( |
| 142 | ..., |
| 143 | description="Is IP whitelisting restricted", |
| 144 | ) |
| 145 | ipWhitelistRestrictionEnforced: bool = Field( |
| 146 | ..., |
| 147 | description="Is IP whitelist restriction enforced", |
| 148 | ) |
| 149 | ipBlacklistRestricted: bool = Field( |
| 150 | ..., |
| 151 | description="Is IP blacklisting restricted", |
| 152 | ) |
| 153 | ipBlacklistRestrictionEnforced: bool = Field( |
| 154 | ..., |
| 155 | description="Is IP blacklist restriction enforced", |
| 156 | ) |
| 157 | |
| 158 | |
| 159 | class Result(BaseModel): |
| 160 | callID: str = Field(..., description="Unique identifier for the call") |
| 161 | authType: Optional[str] = Field(None, description="Type of authentication used") |
| 162 | timestamp: str = Field( |
| 163 | ..., |
| 164 | alias="@timestamp", |
| 165 | description="Timestamp of the event", |
| 166 | ) |
| 167 | errCode: str = Field(..., description="Error code") |
| 168 | errDetails: Optional[str] = Field(None, description="Detailed error message") |
| 169 | errMessage: str = Field(..., description="Error message") |
| 170 | endpoint: str = Field(..., description="API endpoint hit") |
| 171 | userKey: Optional[str] = Field(None, description="User key for identification") |
| 172 | httpReq: HttpReq = Field(..., description="HTTP request details") |
| 173 | ip: str = Field(..., description="IP address of the user") |
| 174 | serverIP: str = Field(..., description="Server IP address") |
| 175 | params: Params = Field(..., description="Parameters passed in the request") |
| 176 | uid: Optional[str] = Field( |
| 177 | "No uid found", |
| 178 | description="Unique identifier for the user", |
| 179 | ) |
| 180 | apikey: str = Field(..., description="API key used for the request") |
| 181 | userAgent: UserAgent = Field(..., description="User agent details") |
| 182 | userKeyDetails: Optional[UserKeyDetails] = Field( |
| 183 | None, |
| 184 | description="Details related to user key", |
| 185 | ) |
| 186 | XffFirstIp: str = Field(..., description="First IP in the X-Forwarded-For header") |
| 187 | restrictions: Restrictions = Field(..., description="IP restrictions") |
| 188 | riskScore: Optional[str] = Field( |
| 189 | None, |
| 190 | description="Risk score associated with the request", |
| 191 | ) |
| 192 | event_timestamp: Optional[datetime] = Field( |
| 193 | None, |
| 194 | description="Timestamp of the event", |
| 195 | ) |
| 196 | case_created: Optional[str] = Field( |
| 197 | "False", |
| 198 | description="Whether a case has been created for the event", |
| 199 | ) |
| 200 | event_analyzed: Optional[str] = Field( |
| 201 | "False", |
| 202 | description="Whether the event has been analyzed", |
| 203 | ) |
| 204 | event_analyzed_multiple_logins: Optional[str] = Field( |
| 205 | "False", |
| 206 | description="Whether the event has been analyzed for multiple logins", |
| 207 | ) |
| 208 | event_analyzed_success_login_diff_ip: Optional[str] = Field( |
| 209 | "False", |
| 210 | description="Whether the event has been analyzed for successful login from different IP", |
| 211 | ) |
| 212 | event_analyzed_same_user_failed_diff_ip: Optional[str] = Field( |
| 213 | "False", |
| 214 | description="Whether the event has been analyzed for same user failed login from different IP", |
| 215 | ) |
| 216 | event_analyzed_same_user_failed_diff_geo: Optional[str] = Field( |
| 217 | "False", |
| 218 | description="Whether the event has been analyzed for same user failed login from different geo", |
| 219 | ) |
| 220 | event_analyzed_same_user_successful_diff_geo: Optional[str] = Field( |
| 221 | "False", |
| 222 | description="Whether the event has been analyzed for same user successful login from different geo", |
| 223 | ) |
| 224 | event_analyzed_brute_force_ip: Optional[str] = Field( |
| 225 | "False", |
| 226 | description="Whether the event has been analyzed for brute force IP", |
| 227 | ) |
| 228 | event_analyzed_brute_force_same_ip: Optional[str] = Field( |
| 229 | "False", |
| 230 | description="Whether the event has been analyzed for brute force same IP", |
| 231 | ) |
| 232 | event_analyzed_successful_login_after_failures_diff_loginID: Optional[str] = Field( |
| 233 | "False", |
| 234 | description="Whether the event has been analyzed for successful login after failures", |
| 235 | ) |
| 236 | |
| 237 | |
| 238 | class SapSiemResponseBody(BaseModel): |
| 239 | results: List[Result] = Field(..., description="List of result objects") |
| 240 | totalCount: int = Field(..., description="Total count of results") |
| 241 | statusCode: int = Field(..., description="Status code of the response") |
| 242 | errorCode: int = Field(..., description="Error code of the response") |
| 243 | statusReason: str = Field(..., description="Status reason of the response") |
| 244 | callId: str = Field(..., description="Unique identifier for the overall call") |
| 245 | time: str = Field(..., description="Time of the response") |
| 246 | objectsCount: int = Field(..., description="Count of objects in results") |
| 247 | |
| 248 | |
| 249 | #### ! WAZUH INDEXER RESULTS ! #### |
| 250 | |
| 251 | |
| 252 | class SapSiemSource(BaseModel): |
| 253 | logSource: Optional[str] = Field(None, description="The source of the log") |
| 254 | params_loginID: str = Field(..., description="The login ID of the user") |
| 255 | errCode: str = Field(..., description="The error code") |
| 256 | ip: str = Field(..., description="The IP address of the user") |
| 257 | httpReq_country: str = Field(..., description="The country from which the HTTP request originated") |
| 258 | event_timestamp: str = Field(..., description="The timestamp of the event") |
| 259 | errMessage: Optional[str] = Field(None, description="The error message") |
| 260 | customer_code: str = Field(..., description="The customer code") |
| 261 | errDetails: Optional[str] = Field(None, description="Detailed error message") |
| 262 | |
| 263 | |
| 264 | class SapSiemHit(BaseModel): |
| 265 | index: str = Field(..., description="The index of the hit", alias="_index") |
| 266 | id: str = Field(..., description="The ID of the hit", alias="_id") |
| 267 | score: Optional[float] = Field(None, description="The score of the hit", alias="_score") |
| 268 | source: SapSiemSource = Field(..., description="The source data of the hit", alias="_source") |
| 269 | sort: Optional[List[int]] = Field(None, description="The sort order of the hit") |
| 270 | |
| 271 | |
| 272 | class SapSiemTotal(BaseModel): |
| 273 | value: int = Field(..., description="The total number of hits") |
| 274 | relation: str = Field(..., description="The relation of the total hits") |
| 275 | |
| 276 | |
| 277 | class SapSiemHits(BaseModel): |
| 278 | total: SapSiemTotal = Field(..., description="The total hits data") |
| 279 | max_score: Optional[float] = Field(None, description="The maximum score among the hits") |
| 280 | hits: List[SapSiemHit] = Field(..., description="The list of hits") |
| 281 | |
| 282 | |
| 283 | class SapSiemShards(BaseModel): |
| 284 | total: int = Field(..., description="The total number of shards") |
| 285 | successful: int = Field(..., description="The number of successful shards") |
| 286 | skipped: int = Field(..., description="The number of skipped shards") |
| 287 | failed: int = Field(..., description="The number of failed shards") |
| 288 | |
| 289 | |
| 290 | class SapSiemWazuhIndexerResponse(BaseModel): |
| 291 | scroll_id: Optional[str] = Field(None, description="The scroll ID", alias="_scroll_id") |
| 292 | took: int = Field(..., description="The time it took to execute the request") |
| 293 | timed_out: bool = Field(..., description="Whether the request timed out") |
| 294 | shards: SapSiemShards = Field(..., description="The shards data", alias="_shards") |
| 295 | hits: SapSiemHits = Field(..., description="The hits data") |
| 296 | |
| 297 | |
| 298 | class SuspiciousLogin(BaseModel): |
| 299 | customer_code: str |
| 300 | logSource: Optional[str] = Field(None) |
| 301 | loginID: str |
| 302 | country: Optional[str] = None |
| 303 | ip: str |
| 304 | event_timestamp: str |
| 305 | errMessage: str |
| 306 | index: Optional[str] = Field(None, description="The index of the hit", alias="_index") |
| 307 | id: Optional[str] = Field(None, description="The ID of the hit", alias="_id") |
| 308 | errDetails: Optional[str] = Field(None, description="Detailed error message") |
| 309 | |
| 310 | |
| 311 | class ErrCode(Enum): |
| 312 | """ |
| 313 | Error codes for SAP SIEM |
| 314 | """ |
| 315 | |
| 316 | INVALID_LOGIN_ID = "403042" # Invalid LoginID |
| 317 | IP_BLOCKED = "403051" # IP is blocked |
| 318 | ACCOUNT_TEMPORARILY_LOCKED = "403120" # Account temporarily locked |
| 319 | OK = "0" # Successful login |
| 320 | |
| 321 | |
| 322 | ################# ! IRIS CASE CREATION SCHEMA ! ################# |
| 323 | class IrisCasePayload(BaseModel): |
| 324 | case_name: str = Field(..., description="The name of the case.") |
| 325 | case_description: str = Field(..., description="The description of the case.") |
| 326 | case_customer: int = Field(1, description="The customer of the case.") |
| 327 | case_classification: int = Field( |
| 328 | 1, |
| 329 | description="The classification of the case.", |
| 330 | ) |
| 331 | soc_id: str = Field("1", description="The SOC ID of the case.") |
| 332 | custom_attributes: Optional[Dict] = Field( |
| 333 | None, |
| 334 | description="The custom attributes of the case.", |
| 335 | ) |
| 336 | create_customer: bool = Field( |
| 337 | False, |
| 338 | description="The create customer flag of the case.", |
| 339 | ) |
| 340 | |
| 341 | def to_dict(self): |
| 342 | return self.model_dump(exclude_none=True) |
| 343 | |
| 344 | |
| 345 | class ModificationHistoryEntry(BaseModel): |
| 346 | user: str |
| 347 | user_id: int |
| 348 | action: str |
| 349 | |
| 350 | |
| 351 | class CaseData(BaseModel): |
| 352 | case_id: int |
| 353 | open_date: str |
| 354 | modification_history: Dict[str, ModificationHistoryEntry] |
| 355 | close_date: Optional[str] = None |
| 356 | case_description: str |
| 357 | classification_id: int |
| 358 | case_soc_id: str |
| 359 | case_name: str |
| 360 | custom_attributes: Optional[Dict[str, str]] = None |
| 361 | case_uuid: str |
| 362 | review_status_id: Optional[int] = None |
| 363 | state_id: int |
| 364 | case_customer: int |
| 365 | reviewer_id: Optional[int] = None |
| 366 | user_id: int |
| 367 | owner_id: int |
| 368 | closing_note: Optional[str] = None |
| 369 | status_id: int |
| 370 | |
| 371 | |
| 372 | class CaseResponse(BaseModel): |
| 373 | success: bool |
| 374 | data: CaseData |
| 375 | |
| 376 | |
| 377 | ################# ! IRIS ASSET ADD SCHEMA ! ################# |
| 378 | class AddAssetModel(BaseModel): |
| 379 | name: str |
| 380 | asset_type: int |
| 381 | analysis_status: int = Field( |
| 382 | None, |
| 383 | description="The analysis status ID of the asset.", |
| 384 | ) |
| 385 | compromise_status: int = Field( |
| 386 | None, |
| 387 | description="The asset compromise status ID of the asset.", |
| 388 | ) |
| 389 | asset_tags: Optional[List[str]] = Field( |
| 390 | None, |
| 391 | description="The asset tags of the asset.", |
| 392 | ) |
| 393 | description: Optional[str] = Field( |
| 394 | None, |
| 395 | description="The asset description of the asset.", |
| 396 | ) |
| 397 | asset_domain: Optional[str] = Field( |
| 398 | None, |
| 399 | description="The asset domain of the asset.", |
| 400 | ) |
| 401 | ip: Optional[str] = Field(None, description="The asset IP of the asset.") |
| 402 | ioc_links: Optional[List[int]] = Field( |
| 403 | None, |
| 404 | description="The IoC links of the asset.", |
| 405 | ) |
| 406 | custom_attributes: Optional[Dict[str, str]] = Field( |
| 407 | None, |
| 408 | description="The custom attributes of the asset.", |
| 409 | ) |
| 410 | |
| 411 | def to_dict(self): |
| 412 | return self.model_dump(exclude_none=True) |