@cryptotaxi247 / CoPilot / commits / ccdb0530

Cleanup (#279)

* feat: Add alert title payload to handle_customer_notifications function * feat: Update branch name in Docker workflow The code changes update the branch name in the Docker workflow file from "incident-management" to "cleanup". This change ensures that the workflow is triggered only when changes are pushed to the "cleanup" branch. * feat: Add processed value for CustomerCodeKeys.CLUSTER_NODE in get_customer_code This commit adds a processed value for the CustomerCodeKeys.CLUSTER_NODE key in the get_customer_code function of incident_alert.py. The processed value is obtained by calling the get_processed_value method of the CustomerCodeKeys enum. This change ensures that the processed value is returned when the key is CLUSTER_NODE. Recent user commits: - Update branch name in Docker workflow - Add alert title payload to handle_customer_notifications function Recent repository commits: - feat: Update branch name in Docker workflow - feat: Add alert title payload to handle_customer_notifications function * feat: Remove unnecessary code for deleting DFIR-IRIS customer The code changes in the `decommission_wazuh_customer` function of `decommission.py` remove the unnecessary code for deleting the DFIR-IRIS customer. * feat: Add processed value for CustomerCodeKeys.CLUSTER_NODE in get_customer_code * try exception for if graylog index events is blocked for writing * cleanup unused connectors allow for `get_connector_attribute` function to use connector_name * add sigma api/types * update dependencies * add code highlighter component * improve badge component * update sigma types * add sigma components * add incident/sigma page * refactor code components * remove unused files * remove webhook notification when creating graylog alert * chore: Add order asc|desc parameter to list_alerts_endpoint * chore: Add order parameter to listing of alert filter endpoints * chore: Remove unused environment variables * update SOCFORTRESS_WAZUH_CONTENT_PACK * update wazuh content pack * chore: Add endpoint to fetch alerts for a specific agent * update incident/alerts lists page added sort * chore: Update agent routes to fetch cases by asset name * removed alerts by agents route * update index.html file * improve incident/cases api * update incident cases components * update agent cases tab * chore: Skip decommissioning if Wazuh Worker Provisioning connector is not verified * chore: Decommission HAProxy worker during customer decommissioning * test of disabling wazuh_monitoring_alert * disable wazuh_monitoring_alert in scheduler * chore: Remove DFIR-IRIS connector service * chore: Add logging for Wazuh Worker response and status code * chore: Refactor provision.py for better logging and error handling * update incident alerts list component * update agent alerts tab * update sigma api * refactor upload components * chore: Update agent vulnerabilities CSV export endpoint to include EPSS score * feat: Add endpoint to export agent sca results as CSV * updated all sigma rules components * update dependencies * fix CodeSource component * prettify * chore: Fix formatting issue in file collection parameters * chore: Update Docker image tags for backend and frontend * chore: Update branch name in Docker workflow * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Sep 2, 2024 at 11:14 UTC ccdb0530dcbbca79ba582b5cd3edbb6df398b70f
117 files changed +3027 -17085
.env.example
+1 -12
@@ -8,6 +8,7 @@ MYSQL_PASSWORD=REPLACE_WITH_PASSWORD
8
9 # ! ALERT FORWARDING IP
10 # Set this to the IP of the host running CoPilot. This is used by Graylog to forward alerts to CoPilot
11 +# ! Not needed anymore since we are reading from the index now
12 # ! Ensure Graylog is able to reach this IP and port 5000
13 ALERT_FORWARDING_IP=0.0.0.0
14
@@ -29,9 +30,6 @@ SHUFFLE_URL=https://127.1.1.1
30 SHUFFLER_API_KEY=dummy
31 SHUFFLE_WORKFLOW_ID=dummy
32
32 -DFIR_IRIS_URL=https://127.1.1.1
33 -DFIR_IRIS_API_KEY=dummy
34 -
33 VELOCIRAPTOR_URL=https://127.1.1.1
34 VELOCIRAPTOR_API_KEY_PATH=dummy
35
@@ -42,15 +40,6 @@ INFLUXDB_URL=http://127.1.1.1
40 INFLUXDB_API_KEY=dummy
41 INFLUXDB_ORG_AND_BUCKET=dummy,dummy
42
45 -ASKSOCFORTRESS_URL=https://knowledge.socfortress.co
46 -ASKSOCFORTRESS_API_KEY=dummy
47 -
48 -SOCFORTRESSTHREATINTEL_URL=https://intel.socfortress.co/search
49 -SOCFORTRESSTHREATINTEL_API_KEY=dummy
50 -
51 -CORTEX_URL=http://127.1.1.1
52 -CORTEX_API_KEY=dummy
53 -
43 GRAFANA_URL=http://127.1.1.1
44 GRAFANA_USERNAME=dummy
45 GRAFANA_PASSWORD=dummy
.github/workflows/docker.yml
+15 -15
@@ -2,7 +2,7 @@ name: Docker
2
3 on:
4 push:
5 - branches: [incident-management]
5 + branches: [main]
6
7 jobs:
8 build-backend:
@@ -27,16 +27,16 @@ jobs:
27 with:
28 context: ./backend
29 push: true
30 - tags: ghcr.io/socfortress/copilot-backend:lab
30 + tags: ghcr.io/socfortress/copilot-backend:latest
31 build-args: |
32 COPILOT_API_KEY=${{ secrets.COPILOT_API_KEY }}
33
34 - # - name: Notify Discord
35 - # uses: appleboy/discord-action@v1.0.0
36 - # with:
37 - # webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
38 - # webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
39 - # message: "Docker image for backend has been updated."
34 + - name: Notify Discord
35 + uses: appleboy/discord-action@v1.0.0
36 + with:
37 + webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
38 + webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
39 + message: "Docker image for backend has been updated."
40
41 build-frontend:
42 runs-on: ubuntu-latest
@@ -60,11 +60,11 @@ jobs:
60 with:
61 context: ./frontend
62 push: true
63 - tags: ghcr.io/socfortress/copilot-frontend:lab
63 + tags: ghcr.io/socfortress/copilot-frontend:latest
64
65 - # - name: Notify Discord
66 - # uses: appleboy/discord-action@v1.0.0
67 - # with:
68 - # webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
69 - # webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
70 - # message: "Docker image for frontend has been updated."
65 + - name: Notify Discord
66 + uses: appleboy/discord-action@v1.0.0
67 + with:
68 + webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
69 + webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
70 + message: "Docker image for frontend has been updated."
backend/app/agents/dfir_iris/schema/cases.py deleted
-9
@@ -1,9 +0,0 @@
1 -from typing import List
2 -
3 -from pydantic import BaseModel
4 -
5 -
6 -class AssetCaseIDResponse(BaseModel):
7 - message: str
8 - success: bool
9 - case_ids: List[int]
backend/app/agents/dfir_iris/services/cases.py deleted
-71
@@ -1,71 +0,0 @@
1 -from typing import List
2 -
3 -from loguru import logger
4 -from sqlalchemy.ext.asyncio import AsyncSession
5 -
6 -from app.agents.dfir_iris.schema.cases import AssetCaseIDResponse
7 -from app.connectors.dfir_iris.services.assets import get_case_assets
8 -from app.connectors.dfir_iris.services.cases import get_all_cases
9 -
10 -
11 -async def collect_agent_soc_cases(
12 - agent_id: int,
13 - session: AsyncSession,
14 -) -> AssetCaseIDResponse:
15 - """
16 - Get all cases for the given agent ID.
17 -
18 - Args:
19 - agent_id (int): The ID of the agent to get cases for.
20 -
21 - Returns:
22 - AssetCaseIDResponse: An instance of AssetCaseIDResponse containing the cases for the given agent ID.
23 - """
24 - logger.info(f"Getting cases for agent: {agent_id}")
25 - all_cases = await get_all_cases(session=session)
26 - case_ids = await filter_cases_by_agent_id(all_cases, agent_id)
27 -
28 - logger.info(f"Found cases: {case_ids}")
29 - return AssetCaseIDResponse(
30 - case_ids=case_ids,
31 - success=True,
32 - message="Successfully retrieved cases for agent",
33 - )
34 -
35 -
36 -async def filter_cases_by_agent_id(cases, agent_id: int) -> List[int]:
37 - """
38 - Filters cases by the given agent ID and collects all associated case IDs.
39 -
40 - Args:
41 - cases: All available cases.
42 - agent_id (int): Agent ID to filter by.
43 -
44 - Returns:
45 - List[int]: List of case IDs associated with the given agent ID.
46 - """
47 - case_ids = []
48 - for case in cases.cases:
49 - if await is_agent_in_case(case.case_id, agent_id):
50 - case_ids.append(case.case_id)
51 - return case_ids
52 -
53 -
54 -async def is_agent_in_case(case_id: int, agent_id: int) -> bool:
55 - """
56 - Checks if a given agent ID is associated with a case.
57 -
58 - Args:
59 - case_id (int): The case ID to check.
60 - agent_id (int): The agent ID to check.
61 -
62 - Returns:
63 - bool: True if the agent is associated with the case, False otherwise.
64 - """
65 - logger.info(f"Getting assets for case: {case_id}")
66 - assets = await get_case_assets(case_id)
67 - for asset in assets.assets:
68 - if f"agent_id:{agent_id}" in asset.asset_tags:
69 - logger.info(f"Found case with agent: {agent_id} in case: {case_id}")
70 - return True
71 - return False
backend/app/agents/routes/agents.py
+160 -7
@@ -1,4 +1,6 @@
1 import asyncio
2 +import csv
3 +import io
4
5 # from fastapi import BackgroundTasks
6 from fastapi import APIRouter
@@ -6,13 +8,13 @@ from fastapi import Depends
8 from fastapi import HTTPException
9 from fastapi import Path
10 from fastapi import Security
11 +from fastapi.responses import StreamingResponse
12 from loguru import logger
13 from packaging import version
14 from sqlalchemy import delete
15 from sqlalchemy.ext.asyncio import AsyncSession
16 from sqlalchemy.future import select
17
15 -from app.agents.dfir_iris.services.cases import collect_agent_soc_cases
18 from app.agents.schema.agents import AgentModifyResponse
19 from app.agents.schema.agents import AgentsResponse
20 from app.agents.schema.agents import AgentWazuhUpgradeResponse
@@ -43,6 +45,10 @@ from app.db.db_session import get_db
45 # App specific imports
46 # from app.db.db_session import session
47 from app.db.universal_models import Agents
48 +from app.incidents.schema.db_operations import CaseOutResponse
49 +from app.incidents.services.db_operations import list_cases_by_asset_name
50 +from app.threat_intel.schema.epss import EpssThreatIntelRequest
51 +from app.threat_intel.services.epss import collect_epss_score
52
53
54 async def get_wazuh_manager_version() -> str:
@@ -460,6 +466,84 @@ async def get_agent_vulnerabilities(
466 return await collect_agent_vulnerabilities(agent_id, vulnerability_severity.value)
467
468
469 +@agents_router.get(
470 + "/{agent_id}/csv/vulnerabilities/{vulnerability_severity}",
471 + description="Get agent vulnerabilities as CSV",
472 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
473 +)
474 +async def get_agent_vulnerabilities_csv(agent_id: str, vulnerability_severity: VulnSeverity = Path(...)) -> StreamingResponse:
475 + """
476 + Fetches the vulnerabilities of a specific agent and returns them as a CSV file.
477 +
478 + Args:
479 + agent_id (str): The ID of the agent.
480 +
481 + Returns:
482 + StreamingResponse: The response containing the agent vulnerabilities in CSV format.
483 + """
484 + logger.info(f"Fetching agent {agent_id} vulnerabilities as CSV")
485 + wazuh_new = await check_wazuh_manager_version()
486 + if wazuh_new is True:
487 + logger.info("Wazuh Manager version is 4.8.0 or higher. Fetching vulnerabilities using new API")
488 + vulnerabilities = (
489 + await collect_agent_vulnerabilities_new(agent_id, vulnerability_severity=vulnerability_severity.value)
490 + ).vulnerabilities
491 + else:
492 + vulnerabilities = (
493 + await collect_agent_vulnerabilities(agent_id, vulnerability_severity=vulnerability_severity.value)
494 + ).vulnerabilities
495 + # Create a CSV file
496 + logger.info(f"Creating CSV file for agent {agent_id} with {len(vulnerabilities)} vulnerabilities")
497 + logger.info(f"Vulnerabilities: {vulnerabilities}")
498 + output = io.StringIO()
499 + writer = csv.writer(output)
500 + # Write the header
501 + writer.writerow(
502 + [
503 + "Severity",
504 + "Version",
505 + "Type",
506 + "Name",
507 + "External References",
508 + "Detection Time",
509 + "CVSS3 Score",
510 + "Published",
511 + "Architecture",
512 + "CVE",
513 + "Status",
514 + "Title",
515 + "EPSS Score",
516 + ],
517 + )
518 + # Write the rows
519 + for vulnerability in vulnerabilities:
520 + epss_score = await collect_epss_score(EpssThreatIntelRequest(cve=vulnerability.cve))
521 + writer.writerow(
522 + [
523 + vulnerability.severity,
524 + vulnerability.version,
525 + vulnerability.type,
526 + vulnerability.name,
527 + ", ".join(vulnerability.external_references) if vulnerability.external_references else "",
528 + vulnerability.detection_time,
529 + vulnerability.cvss3_score,
530 + vulnerability.published,
531 + vulnerability.architecture,
532 + vulnerability.cve,
533 + vulnerability.status,
534 + vulnerability.title,
535 + epss_score.data[0].epss if epss_score.data else "",
536 + ],
537 + )
538 + # Return the CSV file as a streaming response
539 + output.seek(0)
540 + return StreamingResponse(
541 + output, # Use the StringIO object directly
542 + media_type="text/csv",
543 + headers={"Content-Disposition": f"attachment; filename={agent_id}_vulnerabilities.csv"},
544 + )
545 +
546 +
547 @agents_router.get(
548 "/{agent_id}/sca",
549 response_model=WazuhAgentScaResponse,
@@ -501,12 +585,77 @@ async def get_agent_sca_policy_results(agent_id: str, policy_id: str) -> WazuhAg
585
586
587 @agents_router.get(
504 - "/{agent_id}/soc_cases",
505 - # response_model=SocCasesResponse,
506 - description="Get SOC cases for agent",
588 + "/{agent_id}/csv/sca/{policy_id}",
589 + description="Get agent sca results as CSV",
590 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
591 )
509 -async def get_agent_soc_cases(agent_id: str, session: AsyncSession = Depends(get_db)):
592 +async def get_agent_sca_policy_results_csv(agent_id: str, policy_id: str) -> StreamingResponse:
593 + """
594 + Fetches the sca results of a specific agent and returns them as a CSV file.
595 +
596 + Args:
597 + agent_id (str): The ID of the agent.
598 +
599 + Returns:
600 + StreamingResponse: The response containing the agent sca in CSV format.
601 + """
602 + logger.info(f"Fetching agent {agent_id} sca policy results as CSV")
603 + sca_results = (await collect_agent_sca_policy_results(agent_id, policy_id)).sca_policy_results
604 + # Create a CSV file
605 + logger.info(f"Creating CSV file for agent {agent_id} with {len(sca_results)} sca policy results")
606 + output = io.StringIO()
607 + writer = csv.writer(output)
608 + # Write the header
609 + writer.writerow(
610 + [
611 + "Description",
612 + "Policy ID",
613 + "Reason",
614 + "Command",
615 + "Rationale",
616 + "Condition",
617 + "Title",
618 + "Result",
619 + "Remediation",
620 + "Compliance",
621 + "Rules",
622 + ],
623 + )
624 + # Write the rows
625 + for sca_result in sca_results:
626 + writer.writerow(
627 + [
628 + sca_result.description,
629 + sca_result.policy_id,
630 + sca_result.reason,
631 + sca_result.command,
632 + sca_result.rationale,
633 + sca_result.condition,
634 + sca_result.title,
635 + sca_result.result,
636 + sca_result.remediation,
637 + ", ".join([f"{compliance.key}: {compliance.value}" for compliance in sca_result.compliance])
638 + if sca_result.compliance
639 + else "",
640 + ", ".join([f"{rule.type}: {rule.rule}" for rule in sca_result.rules]) if sca_result.rules else "",
641 + ],
642 + )
643 + # Return the CSV file as a streaming response
644 + output.seek(0)
645 + return StreamingResponse(
646 + output, # Use the StringIO object directly
647 + media_type="text/csv",
648 + headers={"Content-Disposition": f"attachment; filename={agent_id}_sca_policy_results.csv"},
649 + )
650 +
651 +
652 +@agents_router.get(
653 + "/{agent_hostname}/cases",
654 + response_model=CaseOutResponse,
655 + description="Get cases for agent",
656 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
657 +)
658 +async def get_agent_soc_cases(agent_hostname: str, session: AsyncSession = Depends(get_db)):
659 """
660 Fetches the SOC cases of a specific agent.
661
@@ -516,8 +665,12 @@ async def get_agent_soc_cases(agent_id: str, session: AsyncSession = Depends(get
665 Returns:
666 SocCasesResponse: The response containing the agent SOC cases.
667 """
519 - logger.info(f"Fetching agent {agent_id} SOC cases")
520 - return await collect_agent_soc_cases(agent_id, session)
668 + logger.info(f"Fetching agent {agent_hostname} cases")
669 + return CaseOutResponse(
670 + cases=await list_cases_by_asset_name(asset_name=agent_hostname, db=session),
671 + success=True,
672 + message="Cases retrieved successfully",
673 + )
674
675
676 @agents_router.get(
backend/app/connectors/dfir_iris/routes/alerts.py deleted
-311
@@ -1,311 +0,0 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -
8 -from app.auth.utils import AuthHandler
9 -from app.connectors.dfir_iris.schema.alerts import AlertAssetsResponse
10 -from app.connectors.dfir_iris.schema.alerts import AlertResponse
11 -from app.connectors.dfir_iris.schema.alerts import AlertsResponse
12 -from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
13 -from app.connectors.dfir_iris.schema.alerts import CaseCreationResponse
14 -from app.connectors.dfir_iris.schema.alerts import DeleteAlertResponse
15 -from app.connectors.dfir_iris.schema.alerts import DeleteMultipleAlertsRequest
16 -from app.connectors.dfir_iris.schema.alerts import FilterAlertsRequest
17 -from app.connectors.dfir_iris.schema.alerts import IrisAsset
18 -from app.connectors.dfir_iris.services.alerts import bookmark_alert
19 -from app.connectors.dfir_iris.services.alerts import create_case
20 -from app.connectors.dfir_iris.services.alerts import delete_alert
21 -from app.connectors.dfir_iris.services.alerts import get_alert
22 -from app.connectors.dfir_iris.services.alerts import get_alerts
23 -from app.connectors.dfir_iris.services.alerts import get_bookmarked_alerts
24 -from app.connectors.dfir_iris.utils.universal import check_alert_exists
25 -from app.db.db_session import get_db
26 -
27 -# App specific imports
28 -
29 -
30 -async def verify_alert_exists(alert_id: str) -> str:
31 - """
32 - Verifies if an alert with the given ID exists.
33 -
34 - Args:
35 - alert_id (str): The ID of the alert to verify.
36 -
37 - Returns:
38 - str: The ID of the alert if it exists.
39 -
40 - Raises:
41 - HTTPException: If the alert does not exist.
42 - """
43 - logger.info(f"Verifying alert {alert_id} exists")
44 - if not await check_alert_exists(alert_id):
45 - raise HTTPException(status_code=400, detail=f"Alert {alert_id} does not exist.")
46 - return alert_id
47 -
48 -
49 -dfir_iris_alerts_router = APIRouter()
50 -
51 -
52 -@dfir_iris_alerts_router.get(
53 - "/bookmark",
54 - response_model=BookmarkedAlertsResponse,
55 - description="Get all bookmarked alerts",
56 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
57 -)
58 -async def get_all_bookmarked_alerts(
59 - session: AsyncSession = Depends(get_db),
60 -) -> BookmarkedAlertsResponse:
61 - """
62 - Fetches all bookmarked alerts.
63 -
64 - Returns:
65 - BookmarkedAlertsResponse: The response containing the bookmarked alerts.
66 - """
67 - logger.info("Fetching all bookmarked alerts")
68 - return await get_bookmarked_alerts(session=session)
69 -
70 -
71 -@dfir_iris_alerts_router.post(
72 - "",
73 - response_model=AlertsResponse,
74 - description="Get alerts from IRIS based on the provided filters",
75 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
76 -)
77 -async def get_alerts_filtered(
78 - request: FilterAlertsRequest,
79 - session: AsyncSession = Depends(get_db),
80 -) -> AlertsResponse:
81 - """
82 - Retrieve alerts from DFIR-IRIS based on the provided filters.
83 -
84 - Returns:
85 - AlertsResponse: The response containing all alerts.
86 - """
87 - logger.info("Fetching all alerts")
88 - logger.info(f"Request: {request}")
89 - return await get_alerts(request, session=session)
90 -
91 -
92 -@dfir_iris_alerts_router.get(
93 - "/{alert_id}",
94 - response_model=AlertResponse,
95 - description="Get an alert by ID",
96 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
97 -)
98 -async def get_alert_by_id(
99 - alert_id: str = Depends(verify_alert_exists),
100 - session: AsyncSession = Depends(get_db),
101 -) -> AlertResponse:
102 - """
103 - Retrieve an alert by its ID.
104 -
105 - Args:
106 - alert_id (str): The ID of the alert to retrieve.
107 -
108 - Returns:
109 - AlertResponse: The response containing the alert information.
110 - """
111 - logger.info(f"Fetching alert {alert_id}")
112 - return await get_alert(alert_id=alert_id, session=session)
113 -
114 -
115 -@dfir_iris_alerts_router.get(
116 - "/assets/{alert_id}",
117 - response_model=AlertAssetsResponse,
118 - description="Get all assets associated with an alert",
119 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
120 -)
121 -async def get_assets_by_alert_id(
122 - alert_id: str = Depends(verify_alert_exists),
123 - session: AsyncSession = Depends(get_db),
124 -) -> AlertAssetsResponse:
125 - """
126 - Retrieve the list of assets associated with an alert by its ID.
127 -
128 - Args:
129 - alert_id (str): The ID of the alert to retrieve.
130 -
131 - Returns:
132 - AlertResponse: The response containing the alert information.
133 - """
134 - logger.info(f"Fetching assets for alert {alert_id}")
135 - assets_data = (await get_alert(alert_id=alert_id, session=session)).alert["assets"]
136 - assets = [IrisAsset(**asset) for asset in assets_data] # Parse the assets data into Asset objects
137 - return AlertAssetsResponse(
138 - success=True,
139 - message="Successfully fetched assets",
140 - assets=assets,
141 - )
142 -
143 -
144 -@dfir_iris_alerts_router.get(
145 - "/alerts_by_user/{user_id}",
146 - response_model=AlertsResponse,
147 - description="Get all alerts assigned to a user",
148 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
149 -)
150 -async def get_all_alerts_assigned_to_user(user_id: int, session: AsyncSession = Depends(get_db)) -> AlertsResponse:
151 - """
152 - Fetches all alerts assigned to a specific user.
153 -
154 - Args:
155 - user_id (int): The ID of the user.
156 -
157 - Returns:
158 - AlertsResponse: The response containing the fetched alerts assigned to the user.
159 - """
160 - logger.info(f"Fetching all alerts assigned to user {user_id}")
161 - alerts = (
162 - await get_alerts(
163 - request=FilterAlertsRequest(alert_owner_id=user_id, per_page=1000),
164 - session=session,
165 - )
166 - ).alerts
167 - alerts_assigned_to_user = []
168 - for alert in alerts:
169 - if alert["alert_owner_id"] == user_id:
170 - alerts_assigned_to_user.append(alert)
171 -
172 - return AlertsResponse(
173 - success=True,
174 - message="Successfully fetched alerts assigned to user",
175 - alerts=alerts_assigned_to_user,
176 - )
177 -
178 -
179 -@dfir_iris_alerts_router.post(
180 - "/create_case/{alert_id}",
181 - response_model=CaseCreationResponse,
182 - description="Assign an alert to a user",
183 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
184 -)
185 -async def create_case_from_alert(
186 - alert_id: str = Depends(verify_alert_exists),
187 -) -> CaseCreationResponse:
188 - """
189 - Create a case from an alert.
190 -
191 - Args:
192 - alert_id (str): The ID of the alert to create a case from.
193 -
194 - Returns:
195 - CaseCreationResponse: The response containing the created case.
196 - """
197 - logger.info(f"Creating case from alert {alert_id}")
198 - return await create_case(alert_id)
199 -
200 -
201 -@dfir_iris_alerts_router.post(
202 - "/bookmark/{alert_id}",
203 - response_model=AlertResponse,
204 - description="Bookmark an alert",
205 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
206 -)
207 -async def bookmark_alert_route(
208 - alert_id: str = Depends(verify_alert_exists),
209 -) -> AlertResponse:
210 - """
211 - Bookmark an alert.
212 -
213 - Args:
214 - alert_id (str): The ID of the alert to be bookmarked.
215 -
216 - Returns:
217 - DeleteAlertResponse: The response containing the bookmarked alert.
218 - """
219 - logger.info(f"Bookmarking alert {alert_id}")
220 - return await bookmark_alert(alert_id, bookmarked=True)
221 -
222 -
223 -@dfir_iris_alerts_router.delete(
224 - "/bookmark/{alert_id}",
225 - response_model=AlertResponse,
226 - description="Unbookmark an alert",
227 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
228 -)
229 -async def unbookmark_alert_route(
230 - alert_id: str = Depends(verify_alert_exists),
231 -) -> AlertResponse:
232 - """
233 - Unbookmark an alert.
234 -
235 - Args:
236 - alert_id (str): The ID of the alert to unbookmark.
237 -
238 - Returns:
239 - AlertResponse: The response containing the unbookmarked alert.
240 - """
241 - logger.info(f"Unbookmarking alert {alert_id}")
242 - return await bookmark_alert(alert_id, bookmarked=False)
243 -
244 -
245 -@dfir_iris_alerts_router.post(
246 - "/delete_multiple",
247 - response_model=DeleteAlertResponse,
248 - description="Delete multiple alerts",
249 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
250 -)
251 -async def delete_multiple_alerts_route(
252 - request: DeleteMultipleAlertsRequest,
253 -) -> DeleteAlertResponse:
254 - """
255 - Delete multiple alerts.
256 -
257 - Args:
258 - request (DeleteMultipleAlertsRequest): The request containing the IDs of the alerts to delete.
259 -
260 - Returns:
261 - DeleteAlertResponse: The response containing the deleted alerts.
262 - """
263 - logger.info(f"Deleting alerts {request.alert_ids}")
264 - for alert_id in request.alert_ids:
265 - await verify_alert_exists(alert_id)
266 - await delete_alert(int(alert_id))
267 - return DeleteAlertResponse(success=True, message="Successfully deleted alerts.")
268 -
269 -
270 -@dfir_iris_alerts_router.delete(
271 - "/purge",
272 - response_model=DeleteAlertResponse,
273 - description="Delete all alerts",
274 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
275 -)
276 -async def purge_alerts_route(
277 - session: AsyncSession = Depends(get_db),
278 -) -> DeleteAlertResponse:
279 - """
280 - Delete all alerts.
281 -
282 - Returns:
283 - AlertResponse: The response containing the deleted alerts.
284 - """
285 - logger.info("Purging all alerts, up to 1000")
286 - alerts = (await get_alerts(request=FilterAlertsRequest(per_page=1000), session=session)).alerts
287 - for alert in alerts:
288 - await delete_alert(int(alert["alert_id"]))
289 - return DeleteAlertResponse(success=True, message="Successfully deleted alerts.")
290 -
291 -
292 -@dfir_iris_alerts_router.delete(
293 - "/{alert_id}",
294 - response_model=DeleteAlertResponse,
295 - description="Delete an alert",
296 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
297 -)
298 -async def delete_alert_route(
299 - alert_id: str = Depends(verify_alert_exists),
300 -) -> DeleteAlertResponse:
301 - """
302 - Delete an alert.
303 -
304 - Args:
305 - alert_id (str): The ID of the alert to delete.
306 -
307 - Returns:
308 - AlertResponse: The response containing the deleted alert.
309 - """
310 - logger.info(f"Deleting alert {alert_id}")
311 - return await delete_alert(int(alert_id))
backend/app/connectors/dfir_iris/routes/assets.py deleted
-55
@@ -1,55 +0,0 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -
7 -from app.auth.utils import AuthHandler
8 -from app.connectors.dfir_iris.schema.assets import AssetResponse
9 -from app.connectors.dfir_iris.services.assets import get_case_assets
10 -from app.connectors.dfir_iris.utils.universal import check_case_exists
11 -
12 -# App specific imports
13 -
14 -
15 -async def verify_case_exists(case_id: int) -> int:
16 - """
17 - Verify if a case exists based on the given case ID.
18 -
19 - Args:
20 - case_id (int): The ID of the case to verify.
21 -
22 - Returns:
23 - int: The verified case ID.
24 -
25 - Raises:
26 - HTTPException: If the case does not exist.
27 - """
28 - if not await check_case_exists(case_id):
29 - raise HTTPException(status_code=400, detail=f"Case {case_id} does not exist.")
30 - return case_id
31 -
32 -
33 -dfir_iris_assets_router = APIRouter()
34 -
35 -
36 -@dfir_iris_assets_router.get(
37 - "/{case_id}",
38 - response_model=AssetResponse,
39 - description="Get all assets for a case",
40 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
41 -)
42 -async def get_case_assets_route(
43 - case_id: int = Depends(verify_case_exists),
44 -) -> AssetResponse:
45 - """
46 - Retrieve all assets for a given case.
47 -
48 - Args:
49 - case_id (int): The ID of the case.
50 -
51 - Returns:
52 - AssetResponse: The response containing the assets for the case.
53 - """
54 - logger.info(f"Fetching assets for case {case_id}")
55 - return await get_case_assets(case_id)
backend/app/connectors/dfir_iris/routes/cases.py deleted
-222
@@ -1,222 +0,0 @@
1 -from datetime import timedelta
2 -
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -
10 -from app.auth.utils import AuthHandler
11 -from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody
12 -from app.connectors.dfir_iris.schema.cases import CaseResponse
13 -from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
14 -from app.connectors.dfir_iris.schema.cases import ClosedCaseResponse
15 -from app.connectors.dfir_iris.schema.cases import PurgeCaseResponse
16 -from app.connectors.dfir_iris.schema.cases import ReopenedCaseResponse
17 -from app.connectors.dfir_iris.schema.cases import SingleCaseBody
18 -from app.connectors.dfir_iris.schema.cases import SingleCaseResponse
19 -from app.connectors.dfir_iris.schema.cases import TimeUnit
20 -from app.connectors.dfir_iris.services.cases import close_case
21 -from app.connectors.dfir_iris.services.cases import delete_single_case
22 -from app.connectors.dfir_iris.services.cases import get_all_cases
23 -from app.connectors.dfir_iris.services.cases import get_cases_older_than
24 -from app.connectors.dfir_iris.services.cases import get_single_case
25 -from app.connectors.dfir_iris.services.cases import purge_cases
26 -from app.connectors.dfir_iris.services.cases import reopen_case
27 -from app.connectors.dfir_iris.utils.universal import check_case_exists
28 -from app.db.db_session import get_db
29 -
30 -
31 -async def verify_case_exists(case_id: int) -> int:
32 - """
33 - Verify if a case exists based on the given case ID.
34 -
35 - Args:
36 - case_id (int): The ID of the case to verify.
37 -
38 - Returns:
39 - int: The verified case ID.
40 -
41 - Raises:
42 - HTTPException: If the case does not exist.
43 - """
44 - if not await check_case_exists(case_id):
45 - raise HTTPException(status_code=400, detail=f"Case {case_id} does not exist.")
46 - return case_id
47 -
48 -
49 -dfir_iris_cases_router = APIRouter()
50 -
51 -
52 -def get_timedelta(older_than: int, time_unit: TimeUnit) -> CaseOlderThanBody:
53 - """
54 - Calculate a timedelta based on the given older_than value and time_unit.
55 -
56 - Args:
57 - older_than (int): The value representing the duration.
58 - time_unit (TimeUnit): The unit of time (hours, days, weeks).
59 -
60 - Returns:
61 - CaseOlderThanBody: An instance of CaseOlderThanBody with the calculated timedelta.
62 -
63 - """
64 - delta = None
65 - if time_unit == TimeUnit.HOURS:
66 - delta = timedelta(hours=older_than)
67 - elif time_unit == TimeUnit.DAYS:
68 - delta = timedelta(days=older_than)
69 - elif time_unit == TimeUnit.WEEKS:
70 - delta = timedelta(weeks=older_than)
71 - return CaseOlderThanBody(older_than=delta, time_unit=time_unit)
72 -
73 -
74 -@dfir_iris_cases_router.get(
75 - "",
76 - response_model=CaseResponse,
77 - description="Get all cases",
78 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
79 -)
80 -async def get_cases_route(session: AsyncSession = Depends(get_db)) -> CaseResponse:
81 - """
82 - Get all cases.
83 -
84 - Returns:
85 - CaseResponse: The response containing all cases.
86 - """
87 - logger.info("Fetching all cases")
88 - return await get_all_cases(session=session)
89 -
90 -
91 -@dfir_iris_cases_router.post(
92 - "/older_than",
93 - response_model=CasesBreachedResponse,
94 - description="Get all cases older than a specified date",
95 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
96 -)
97 -async def get_cases_older_than_route(
98 - case_older_than_body: CaseOlderThanBody = Depends(get_timedelta),
99 -) -> CaseResponse:
100 - """
101 - Fetches all cases older than a specified date.
102 -
103 - Args:
104 - case_older_than_body (CaseOlderThanBody): The request body containing the date and time unit.
105 -
106 - Returns:
107 - CaseResponse: The response containing the cases older than the specified date.
108 - """
109 - logger.info(
110 - f"Fetching all cases older than {case_older_than_body.older_than} ({case_older_than_body.time_unit.value})",
111 - )
112 - return await get_cases_older_than(case_older_than_body)
113 -
114 -
115 -@dfir_iris_cases_router.delete(
116 - "/purge",
117 - response_model=PurgeCaseResponse,
118 - description="Purge all cases",
119 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
120 -)
121 -async def purge_cases_route() -> PurgeCaseResponse:
122 - """
123 - Purge all cases.
124 -
125 - Returns:
126 - PurgeCaseResponse: The response containing the purge status.
127 - """
128 - logger.info("Purging all cases")
129 - return await purge_cases()
130 -
131 -
132 -@dfir_iris_cases_router.delete(
133 - "/purge/{case_id}",
134 - response_model=PurgeCaseResponse,
135 - description="Purge a single case",
136 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
137 -)
138 -async def purge_single_case_route(
139 - case_id: int = Depends(verify_case_exists),
140 -) -> PurgeCaseResponse:
141 - """
142 - Purge a single case by its ID.
143 -
144 - Args:
145 - case_id (int): The ID of the case to purge.
146 -
147 - Returns:
148 - PurgeCaseResponse: The response containing the purge status.
149 - """
150 - logger.info(f"Purging case {case_id}")
151 - single_case_body = SingleCaseBody(case_id=case_id)
152 - return await delete_single_case(single_case_body.case_id)
153 -
154 -
155 -@dfir_iris_cases_router.get(
156 - "/{case_id}",
157 - response_model=SingleCaseResponse,
158 - description="Get a single case",
159 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
160 -)
161 -async def get_single_case_route(
162 - case_id: int = Depends(verify_case_exists),
163 - session: AsyncSession = Depends(get_db),
164 -) -> SingleCaseResponse:
165 - """
166 - Retrieve a single case by its ID.
167 -
168 - Args:
169 - case_id (int): The ID of the case to retrieve.
170 -
171 - Returns:
172 - SingleCaseResponse: The response containing the single case information.
173 - """
174 - logger.info(f"Fetching case {case_id}")
175 - single_case_body = SingleCaseBody(case_id=case_id)
176 - return await get_single_case(single_case_body.case_id, session=session)
177 -
178 -
179 -@dfir_iris_cases_router.put(
180 - "/close/{case_id}",
181 - response_model=ClosedCaseResponse,
182 - description="Close a single case",
183 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
184 -)
185 -async def close_single_case_route(
186 - case_id: int = Depends(verify_case_exists),
187 -) -> ClosedCaseResponse:
188 - """
189 - Close a single case by its ID.
190 -
191 - Args:
192 - case_id (int): The ID of the case to close.
193 -
194 - Returns:
195 - ClosedCaseResponse: The response containing the closed case information.
196 - """
197 - logger.info(f"Closing case {case_id}")
198 - single_case_body = SingleCaseBody(case_id=case_id)
199 - return await close_case(single_case_body.case_id)
200 -
201 -
202 -@dfir_iris_cases_router.put(
203 - "/open/{case_id}",
204 - response_model=ReopenedCaseResponse,
205 - description="Open a single case",
206 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
207 -)
208 -async def reopen_single_case_route(
209 - case_id: int = Depends(verify_case_exists),
210 -) -> ReopenedCaseResponse:
211 - """
212 - Open a single case by its ID.
213 -
214 - Args:
215 - case_id (int): The ID of the case to open.
216 -
217 - Returns:
218 - ReopenedCaseResponse: The response containing the opened case information.
219 - """
220 - logger.info(f"Opening case {case_id}")
221 - single_case_body = SingleCaseBody(case_id=case_id)
222 - return await reopen_case(single_case_body.case_id)
backend/app/connectors/dfir_iris/routes/notes.py deleted
-88
@@ -1,88 +0,0 @@
1 -from typing import Optional
2 -
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -
9 -from app.auth.utils import AuthHandler
10 -from app.connectors.dfir_iris.schema.notes import NoteCreationBody
11 -from app.connectors.dfir_iris.schema.notes import NoteCreationResponse
12 -from app.connectors.dfir_iris.schema.notes import NotesResponse
13 -from app.connectors.dfir_iris.services.notes import create_case_note
14 -from app.connectors.dfir_iris.services.notes import get_case_notes
15 -from app.connectors.dfir_iris.utils.universal import check_case_exists
16 -
17 -
18 -async def verify_case_exists(case_id: int) -> int:
19 - """
20 - Verify if a case exists based on the given case ID.
21 -
22 - Args:
23 - case_id (int): The ID of the case to verify.
24 -
25 - Returns:
26 - int: The verified case ID.
27 -
28 - Raises:
29 - HTTPException: If the case does not exist.
30 - """
31 - if not await check_case_exists(case_id):
32 - raise HTTPException(status_code=400, detail=f"Case {case_id} does not exist.")
33 - return case_id
34 -
35 -
36 -dfir_iris_notes_router = APIRouter()
37 -
38 -
39 -@dfir_iris_notes_router.get(
40 - "/{case_id}",
41 - response_model=NotesResponse,
42 - description="Get all notes for a case",
43 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
44 -)
45 -async def get_case_notes_route(
46 - case_id: int = Depends(verify_case_exists),
47 - search_term: Optional[str] = "%",
48 -) -> NotesResponse:
49 - """
50 - Retrieve all notes for a specific case.
51 -
52 - Args:
53 - case_id (int): The ID of the case.
54 - search_term (str, optional): The search term to filter notes. Defaults to "%".
55 -
56 - Returns:
57 - NotesResponse: The response containing the notes for the case.
58 - """
59 - logger.info(f"Fetching notes for case {case_id}")
60 - return await get_case_notes(case_id, search_term)
61 -
62 -
63 -@dfir_iris_notes_router.post(
64 - "/{case_id}",
65 - response_model=NoteCreationResponse,
66 - description="Create a note for a case",
67 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
68 -)
69 -async def create_case_note_route(
70 - case_id: int,
71 - note_creation_body: NoteCreationBody,
72 -) -> NoteCreationResponse:
73 - """
74 - Create a note for a case.
75 -
76 - Args:
77 - case_id (int): The ID of the case.
78 - note_creation_body (NoteCreationBody): The body of the note creation request.
79 -
80 - Returns:
81 - NoteCreationResponse: The response containing the created note.
82 -
83 - Raises:
84 - CaseNotFoundError: If the case with the given ID does not exist.
85 - """
86 - verify_case_exists(case_id)
87 - logger.info(f"Creating a note for case {case_id}")
88 - return await create_case_note(case_id, note_creation_body)
backend/app/connectors/dfir_iris/routes/users.py deleted
-200
@@ -1,200 +0,0 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -
7 -from app.auth.utils import AuthHandler
8 -from app.connectors.dfir_iris.schema.alerts import AlertResponse
9 -from app.connectors.dfir_iris.schema.users import User
10 -from app.connectors.dfir_iris.schema.users import UserAddedToCustomerResponse
11 -from app.connectors.dfir_iris.schema.users import UserRemovedFromCustomerResponse
12 -from app.connectors.dfir_iris.schema.users import UsersResponse
13 -from app.connectors.dfir_iris.services.users import assign_user_to_alert
14 -from app.connectors.dfir_iris.services.users import delete_user_from_alert
15 -from app.connectors.dfir_iris.services.users import get_users
16 -from app.connectors.dfir_iris.utils.universal import add_user_to_customers
17 -from app.connectors.dfir_iris.utils.universal import check_alert_exists
18 -from app.connectors.dfir_iris.utils.universal import check_user_exists
19 -from app.connectors.dfir_iris.utils.universal import collect_all_customers
20 -
21 -
22 -def verify_user_exists(user_id: int) -> int:
23 - """
24 - Verify if a user exists based on the provided user ID.
25 -
26 - Args:
27 - user_id (int): The ID of the user to verify.
28 -
29 - Returns:
30 - int: The verified user ID.
31 -
32 - Raises:
33 - HTTPException: If the user does not exist.
34 - """
35 - if not check_user_exists(user_id):
36 - raise HTTPException(status_code=400, detail=f"User {user_id} does not exist.")
37 - return user_id
38 -
39 -
40 -async def verify_alert_exists(alert_id: str) -> str:
41 - """
42 - Verify if an alert exists based on the given alert ID.
43 -
44 - Args:
45 - alert_id (str): The ID of the alert to be verified.
46 -
47 - Returns:
48 - str: The verified alert ID.
49 -
50 - Raises:
51 - HTTPException: If the alert does not exist.
52 - """
53 - if not await check_alert_exists(alert_id):
54 - raise HTTPException(status_code=400, detail=f"Alert {alert_id} does not exist.")
55 - return alert_id
56 -
57 -
58 -dfir_iris_users_router = APIRouter()
59 -
60 -
61 -@dfir_iris_users_router.get(
62 - "",
63 - response_model=UsersResponse,
64 - description="Get all users",
65 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
66 -)
67 -async def get_all_users() -> UsersResponse:
68 - """
69 - Retrieves all users.
70 -
71 - Returns:
72 - UsersResponse: The response containing the list of users.
73 - """
74 - logger.info("Fetching all users")
75 - return await get_users()
76 -
77 -
78 -@dfir_iris_users_router.post(
79 - "/assign/{alert_id}/{user_id}",
80 - response_model=AlertResponse,
81 - description="Assign a user to an alert",
82 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
83 -)
84 -async def assign_user_to_alert_route(
85 - alert_id: str = Depends(verify_alert_exists),
86 - user_id: int = Depends(verify_user_exists),
87 -) -> User:
88 - """
89 - Assigns a user to an alert.
90 -
91 - Parameters:
92 - - alert_id (str): The ID of the alert.
93 - - user_id (int): The ID of the user.
94 -
95 - Returns:
96 - - User: The assigned user.
97 -
98 - Raises:
99 - - HTTPException: If the alert or user does not exist.
100 - """
101 - logger.info(f"Assigning user {user_id} to alert {alert_id}")
102 - return await assign_user_to_alert(alert_id, user_id)
103 -
104 -
105 -@dfir_iris_users_router.post(
106 - "/add/{user_id}",
107 - response_model=AlertResponse,
108 - description="Add a user to a list of customers",
109 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
110 -)
111 -async def add_user_to_customers_route(
112 - user_id: int,
113 -) -> UserAddedToCustomerResponse:
114 - """
115 - Add a user to a list of customers.
116 -
117 - Parameters:
118 - - customers (List[str]): The list of customer IDs.
119 - - user_id (int): The ID of the user.
120 -
121 - Returns:
122 - - AlertResponse: The response containing the added user.
123 -
124 - Raises:
125 - - HTTPException: If the customer or user does not exist.
126 - """
127 - customers = await collect_all_customers()
128 - customer_ids = [str(customer["customer_id"]) for customer in customers]
129 - logger.info(f"Customer IDs: {customer_ids}")
130 - logger.info(f"Adding user {user_id} to customers {customer_ids}")
131 - success = await add_user_to_customers(customer_ids, user_id)
132 - if success:
133 - return UserAddedToCustomerResponse(message=f"User {user_id} added to customers {customers}", success=True)
134 - else:
135 - raise HTTPException(status_code=400, detail=f"Failed to add user {user_id} to customers {customers}")
136 -
137 -
138 -@dfir_iris_users_router.delete(
139 - "/remove/{user_id}/{customer_id}",
140 - response_model=AlertResponse,
141 - description="Remove a user from a customer",
142 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
143 -)
144 -async def remove_user_from_customer_route(
145 - user_id: int,
146 - customer_id: str,
147 -) -> UserRemovedFromCustomerResponse:
148 - """
149 - Remove a user from a customer.
150 -
151 - Parameters:
152 - - customer_id (str): The ID of the customer.
153 - - user_id (int): The ID of the user.
154 -
155 - Returns:
156 - - AlertResponse: The response containing the removed user.
157 -
158 - Raises:
159 - - HTTPException: If the customer or user does not exist.
160 - """
161 - customers = await collect_all_customers()
162 - customer_ids = [str(customer["customer_id"]) for customer in customers]
163 - if customer_id in customer_ids:
164 - customer_ids.remove(customer_id)
165 - else:
166 - raise HTTPException(status_code=404, detail="Customer ID not found")
167 - logger.info(f"Customer IDs: {customer_ids}")
168 - logger.info(f"Removing user {user_id} from customers {customer_ids}")
169 - success = await add_user_to_customers(customer_ids, user_id)
170 - if success:
171 - return UserRemovedFromCustomerResponse(message=f"User {user_id} removed from customer {customer_id}", success=True)
172 - else:
173 - raise HTTPException(status_code=400, detail=f"Failed to remove user {user_id} from customer {customer_id}")
174 -
175 -
176 -@dfir_iris_users_router.delete(
177 - "/assign/{alert_id}/{user_id}",
178 - response_model=AlertResponse,
179 - description="Delete a user from an alert",
180 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
181 -)
182 -async def delete_user_from_alert_route(
183 - alert_id: str = Depends(verify_alert_exists),
184 - user_id: int = Depends(verify_user_exists),
185 -) -> User:
186 - """
187 - Delete a user from an alert.
188 -
189 - Args:
190 - alert_id (str): The ID of the alert.
191 - user_id (int): The ID of the user.
192 -
193 - Returns:
194 - User: The deleted user.
195 -
196 - Raises:
197 - HTTPException: If the alert or user does not exist.
198 - """
199 - logger.info(f"Deleting user {user_id} from alert {alert_id}")
200 - return await delete_user_from_alert(alert_id, user_id)
backend/app/connectors/dfir_iris/schema/admin.py deleted
-37
@@ -1,37 +0,0 @@
1 -import uuid
2 -from datetime import datetime
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
6 -
7 -from pydantic import UUID4
8 -from pydantic import BaseModel
9 -
10 -
11 -class CreateCustomerData(BaseModel):
12 - customer_id: int
13 - customer_sla: Optional[str] = None
14 - customer_name: str
15 - customer_description: Optional[str] = None
16 - creation_date: datetime
17 - custom_attributes: Dict
18 - last_update_date: datetime
19 - client_uuid: uuid.UUID
20 -
21 -
22 -class CreateCustomerResponse(BaseModel):
23 - success: bool
24 - data: CreateCustomerData
25 -
26 -
27 -class Customer(BaseModel):
28 - customer_name: str
29 - customer_id: int
30 - customer_uuid: UUID4
31 - customer_description: Optional[str] = None
32 - customer_sla: Optional[str] = None
33 -
34 -
35 -class ListCustomers(BaseModel):
36 - success: bool
37 - data: List[Customer]
backend/app/connectors/dfir_iris/schema/alerts.py deleted
-136
@@ -1,136 +0,0 @@
1 -from enum import Enum
2 -from typing import Any
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
6 -
7 -from pydantic import BaseModel
8 -from pydantic import Field
9 -from pydantic import validator
10 -
11 -
12 -class AlertsResponse(BaseModel):
13 - alerts: Optional[List[Dict[str, Any]]] = Field(
14 - [],
15 - description="The alerts returned from the search.",
16 - )
17 - message: str
18 - success: bool
19 -
20 -
21 -class AlertResponse(BaseModel):
22 - alert: Optional[Dict[str, Any]] = Field(
23 - {},
24 - description="The alert returned from the search.",
25 - )
26 - message: str
27 - success: bool
28 -
29 -
30 -class BookmarkedAlertsResponse(BaseModel):
31 - bookmarked_alerts: Optional[List[Dict[str, Any]]] = Field(
32 - [],
33 - description="The alerts returned from the search.",
34 - )
35 - message: str
36 - success: bool
37 -
38 -
39 -class DeleteMultipleAlertsRequest(BaseModel):
40 - alert_ids: List[str] = Field([], description="The IDs of the alerts to delete.")
41 -
42 -
43 -class DeleteAlertResponse(BaseModel):
44 - message: str
45 - success: bool
46 -
47 -
48 -class SortOrder(Enum):
49 - desc = "desc"
50 - asc = "asc"
51 -
52 -
53 -class FilterAlertsRequest(BaseModel):
54 - per_page: int = Field(1000, description="The number of alerts to return per page.")
55 - page: int = Field(1, description="The page number to return.")
56 - sort: SortOrder = Field(
57 - SortOrder.desc,
58 - description="The sort order for the alerts.",
59 - )
60 - alert_title: Optional[str] = Field(None, description="The title of the alert.")
61 - alert_owner_id: Optional[int] = Field(
62 - None,
63 - description="The ID of the alert owner.",
64 - )
65 -
66 -
67 -class CaseModificationHistory(BaseModel):
68 - user: str
69 - user_id: int
70 - action: str
71 -
72 -
73 -class CaseData(BaseModel):
74 - owner_id: int
75 - case_soc_id: str
76 - status_id: int
77 - case_name: str
78 - custom_attributes: Optional[Any]
79 - open_date: str
80 - close_date: Optional[str]
81 - state_id: int
82 - case_description: str
83 - reviewer_id: Optional[int]
84 - closing_note: Optional[str]
85 - case_id: int
86 - modification_history: Dict[str, CaseModificationHistory]
87 - classification_id: Optional[int]
88 - review_status_id: Optional[int]
89 - user_id: int
90 - case_uuid: str
91 - case_customer: int
92 -
93 -
94 -class CaseCreationResponse(BaseModel):
95 - success: bool
96 - case: CaseData
97 - message: str
98 -
99 -
100 -class IrisAssetType(BaseModel):
101 - asset_id: int
102 - asset_icon_compromised: str
103 - asset_name: str
104 - asset_icon_not_compromised: str
105 - asset_description: str
106 -
107 -
108 -class IrisAsset(BaseModel):
109 - asset_name: str
110 - asset_description: str
111 - asset_type: IrisAssetType
112 - custom_attributes: Optional[str]
113 - asset_tags: str
114 - asset_compromise_status_id: Optional[int]
115 - date_update: Optional[str]
116 - asset_enrichment: Optional[str]
117 - case_id: Optional[int]
118 - user_id: Optional[int]
119 - asset_type_id: int
120 - asset_id: int
121 - asset_ip: str
122 - asset_domain: Optional[str]
123 - asset_uuid: str
124 - analysis_status_id: Optional[int]
125 - asset_info: Optional[str]
126 - date_added: Optional[str]
127 -
128 - @validator("asset_tags", pre=True)
129 - def remove_agent_id(cls, v):
130 - return v.replace("agent_id:", "") if v else v
131 -
132 -
133 -class AlertAssetsResponse(BaseModel):
134 - success: bool
135 - message: str
136 - assets: List[IrisAsset]
backend/app/connectors/dfir_iris/schema/assets.py deleted
-50
@@ -1,50 +0,0 @@
1 -from typing import List
2 -from typing import Optional
3 -
4 -from pydantic import BaseModel
5 -from pydantic import Field
6 -
7 -
8 -class AssetState(BaseModel):
9 - object_last_update: str
10 - object_state: int
11 -
12 -
13 -class AssetIocLink(BaseModel):
14 - ioc_id: int
15 - ioc_value: str
16 - asset_id: int
17 -
18 -
19 -class Asset(BaseModel):
20 - analysis_status: str
21 - analysis_status_id: int
22 - asset_compromise_status_id: Optional[int]
23 - asset_description: Optional[str] = Field(
24 - None,
25 - description="The description of the asset.",
26 - )
27 - asset_domain: Optional[str]
28 - asset_icon_compromised: str
29 - asset_icon_not_compromised: str
30 - asset_id: int
31 - asset_ip: Optional[str] = Field(None, description="The IP address of the asset.")
32 - asset_name: str
33 - asset_tags: Optional[str]
34 - asset_type: Optional[str]
35 - asset_type_id: int
36 - asset_uuid: str
37 - ioc_links: Optional[List[AssetIocLink]] = None
38 - link: List
39 -
40 -
41 -class AssetData(BaseModel):
42 - assets: List[Asset]
43 - state: AssetState
44 -
45 -
46 -class AssetResponse(BaseModel):
47 - assets: List[Asset]
48 - state: AssetState
49 - message: str
50 - success: bool
backend/app/connectors/dfir_iris/schema/cases.py deleted
-177
@@ -1,177 +0,0 @@
1 -from datetime import date
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 -from typing import Union
8 -
9 -from pydantic import BaseModel
10 -from pydantic import Field
11 -
12 -
13 -class CaseModel(BaseModel):
14 - access_level: int
15 - case_close_date: str
16 - case_description: str
17 - case_id: int
18 - case_name: str
19 - case_open_date: str
20 - case_soc_id: str
21 - case_uuid: str
22 - classification: Optional[str]
23 - classification_id: Optional[int]
24 - client_name: str
25 - opened_by: str
26 - opened_by_user_id: int
27 - owner: str
28 - owner_id: int
29 - state_id: Optional[int] = Field(None, description="The ID of the state the case is in.")
30 - state_name: Optional[str] = Field(None, description="The name of the state the case is in.")
31 - customer_code: str
32 -
33 -
34 -class CaseResponse(BaseModel):
35 - cases: List[CaseModel]
36 - message: str
37 - success: bool
38 -
39 -
40 -class PurgeCaseResponse(BaseModel):
41 - message: str
42 - success: bool
43 -
44 -
45 -class ModificationHistoryItem(BaseModel):
46 - action: str
47 - user: str
48 - user_id: int
49 -
50 -
51 -class SingleCaseModel(BaseModel):
52 - case_description: str
53 - case_id: int
54 - case_name: str
55 - case_soc_id: str
56 - case_tags: Optional[str]
57 - case_uuid: str
58 - classification: Optional[Union[str, None]]
59 - classification_id: Optional[Union[int, None]]
60 - close_date: Optional[Union[str, None]]
61 - # custom_attributes: Optional[Union[str, None]] = Field(
62 - # None,
63 - # description="The custom attributes of the case.",
64 - # )
65 - custom_attributes: Optional[Dict[str, Union[str, None]]] = Field(
66 - None,
67 - description="The custom attributes of the case.",
68 - )
69 - customer_id: int
70 - customer_name: str
71 - initial_date: str
72 - modification_history: Dict[str, ModificationHistoryItem]
73 - open_by_user: str
74 - open_by_user_id: int
75 - open_date: str
76 - owner: str
77 - owner_id: int
78 - protagonists: List[str]
79 - reviewer: Optional[Union[str, None]]
80 - reviewer_id: Optional[Union[int, None]]
81 - state_id: int
82 - state_name: str
83 - status_id: int
84 - status_name: str
85 - customer_code: str
86 -
87 - def __init__(self, **data):
88 - if "custom_attributes" in data and not data["custom_attributes"]:
89 - data["custom_attributes"] = {"default_key": "no custom attributes found"} # Replace with your default entry
90 - super().__init__(**data)
91 -
92 -
93 -class SingleCaseBody(BaseModel):
94 - case_id: int
95 -
96 -
97 -class SingleCaseResponse(BaseModel):
98 - case: SingleCaseModel
99 - message: str
100 - success: bool
101 -
102 -
103 -class TimeUnit(str, Enum):
104 - HOURS = "hours"
105 - DAYS = "days"
106 - WEEKS = "weeks"
107 -
108 -
109 -class CaseOlderThanBody(BaseModel):
110 - older_than: timedelta = Field(..., description="Amount of time to filter cases by")
111 - time_unit: TimeUnit
112 -
113 -
114 -class CasesBreachedResponse(BaseModel):
115 - cases_breached: List[CaseModel]
116 - message: str
117 - success: bool
118 -
119 -
120 -class CaseModificationHistoryItem(BaseModel):
121 - user: str
122 - user_id: int
123 - action: str
124 -
125 -
126 -class CaseData(BaseModel):
127 - owner_id: int
128 - case_soc_id: str
129 - status_id: int
130 - case_name: str
131 - custom_attributes: Optional[str] = None
132 - open_date: date
133 - close_date: date
134 - state_id: int
135 - case_description: str
136 - reviewer_id: Optional[int] = None
137 - closing_note: Optional[str] = None
138 - case_id: int
139 - modification_history: Dict[str, CaseModificationHistoryItem]
140 - classification_id: Optional[int] = None
141 - review_status_id: Optional[int] = None
142 - user_id: int
143 - case_uuid: str
144 - case_customer: int
145 -
146 -
147 -class ClosedCaseResponse(BaseModel):
148 - success: bool
149 - case: CaseData
150 - message: str
151 -
152 -
153 -class ReopenedCaseData(BaseModel):
154 - owner_id: int
155 - case_soc_id: str
156 - status_id: int
157 - case_name: str
158 - custom_attributes: Optional[str] = None
159 - open_date: date
160 - close_date: Optional[date] = None
161 - state_id: int
162 - case_description: str
163 - reviewer_id: Optional[int] = None
164 - closing_note: Optional[str] = None
165 - case_id: int
166 - modification_history: Dict[str, CaseModificationHistoryItem]
167 - classification_id: Optional[int] = None
168 - review_status_id: Optional[int] = None
169 - user_id: int
170 - case_uuid: str
171 - case_customer: int
172 -
173 -
174 -class ReopenedCaseResponse(BaseModel):
175 - success: bool
176 - case: ReopenedCaseData
177 - message: str
backend/app/connectors/dfir_iris/schema/notes.py deleted
-80
@@ -1,80 +0,0 @@
1 -from typing import Dict
2 -from typing import List
3 -from typing import Optional
4 -
5 -from pydantic import BaseModel
6 -from pydantic import Field
7 -
8 -
9 -class Directory(BaseModel):
10 - id: int
11 - name: str
12 - parent_id: Optional[int]
13 - case_id: int
14 -
15 -
16 -class ModificationHistory(BaseModel):
17 - user: str
18 - user_id: int
19 - action: str
20 -
21 -
22 -class NoteDetails(BaseModel):
23 - directory: Directory
24 - note_id: int
25 - note_uuid: str
26 - note_title: str
27 - note_content: str
28 - note_user: int
29 - note_creationdate: str
30 - note_lastupdate: str
31 - note_case_id: int
32 - custom_attributes: Optional[Dict]
33 - directory_id: int
34 - modification_history: Dict[str, ModificationHistory]
35 - comments: List[str]
36 -
37 -
38 -class NoteDetailsResponse(BaseModel):
39 - note_details: NoteDetails
40 - message: str
41 - success: bool
42 -
43 -
44 -class NoteItem(BaseModel):
45 - note_details: NoteDetails
46 -
47 -
48 -class NotesResponse(BaseModel):
49 - notes: List[NoteItem]
50 - message: str
51 - success: bool
52 -
53 -
54 -class NotesQueryParams(BaseModel):
55 - case_id: int
56 - search_term: Optional[str] = Field(
57 - "%",
58 - description="Search term to filter notes by. Defaults to wildcard search (%).",
59 - )
60 -
61 -
62 -class NoteCreationBody(BaseModel):
63 - note_title: str = Field(..., description="Title of the note to be created.")
64 - note_content: str = Field(..., description="Content of the note to be created.")
65 -
66 -
67 -class NoteAttributes(BaseModel):
68 - custom_attributes: Dict[str, str] = Field(...)
69 - note_content: str = Field(...)
70 - note_creationdate: str = Field(...)
71 - note_id: int = Field(...)
72 - note_lastupdate: str = Field(...)
73 - note_title: str = Field(...)
74 - note_uuid: str = Field(...)
75 -
76 -
77 -class NoteCreationResponse(BaseModel):
78 - message: str = Field(...)
79 - note: NoteAttributes = Field(...)
80 - success: bool = Field(...)
backend/app/connectors/dfir_iris/schema/users.py deleted
-27
@@ -1,27 +0,0 @@
1 -from typing import List
2 -
3 -from pydantic import BaseModel
4 -
5 -
6 -class User(BaseModel):
7 - user_active: bool
8 - user_id: int
9 - user_login: str
10 - user_name: str
11 - user_uuid: str
12 -
13 -
14 -class UsersResponse(BaseModel):
15 - message: str
16 - success: bool
17 - users: List[User]
18 -
19 -
20 -class UserAddedToCustomerResponse(BaseModel):
21 - success: bool
22 - message: str
23 -
24 -
25 -class UserRemovedFromCustomerResponse(BaseModel):
26 - success: bool
27 - message: str
backend/app/connectors/dfir_iris/services/alerts.py deleted
-264
@@ -1,264 +0,0 @@
1 -from fastapi import HTTPException
2 -from loguru import logger
3 -from sqlalchemy.ext.asyncio import AsyncSession
4 -from sqlalchemy.future import select
5 -
6 -from app.connectors.dfir_iris.schema.alerts import AlertResponse
7 -from app.connectors.dfir_iris.schema.alerts import AlertsResponse
8 -from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
9 -from app.connectors.dfir_iris.schema.alerts import CaseCreationResponse
10 -from app.connectors.dfir_iris.schema.alerts import DeleteAlertResponse
11 -from app.connectors.dfir_iris.schema.alerts import FilterAlertsRequest
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
14 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
15 - AlertCreationSettings,
16 -)
17 -
18 -
19 -async def get_customer_code(session: AsyncSession, customer_id: int) -> str:
20 - """
21 - Retrieves the customer code for a given customer ID.
22 -
23 - Args:
24 - session (AsyncSession): The database session.
25 - customer_id (int): The ID of the customer.
26 -
27 - Returns:
28 - The customer code for the given customer ID.
29 - """
30 - logger.info(f"Retrieving customer code for customer ID {customer_id}")
31 - try:
32 - alert_creation_settings = await session.execute(
33 - select(AlertCreationSettings).filter(
34 - AlertCreationSettings.iris_customer_id == customer_id,
35 - ),
36 - )
37 - alert_creation_settings = alert_creation_settings.scalars().first()
38 - if alert_creation_settings is None:
39 - return "Customer Not Found"
40 - return alert_creation_settings.customer_code
41 - except Exception as e:
42 - logger.error(
43 - f"Error retrieving customer code for customer ID {customer_id}: {e}",
44 - )
45 - return "Customer Not Found"
46 -
47 -
48 -async def get_alerts(
49 - request: FilterAlertsRequest,
50 - session: AsyncSession,
51 -) -> AlertsResponse:
52 - """
53 - Retrieves alerts from the DFIR-IRIS service.
54 -
55 - Args:
56 - request (FilterAlertsRequest): The request object containing filtering criteria.
57 -
58 - Returns:
59 - AlertsResponse: The response object containing the fetched alerts.
60 - """
61 - try:
62 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
63 - params = construct_params(request)
64 - result = await fetch_and_validate_data(
65 - client,
66 - lambda: alert.filter_alerts(**params),
67 - )
68 - logger.info(
69 - f"Successfully fetched length {len(result['data']['alerts'])} alerts",
70 - )
71 - # Add the customer code to each alert
72 - for alert in result["data"]["alerts"]:
73 - customer_code = await get_customer_code(
74 - session,
75 - alert["customer"]["customer_id"],
76 - )
77 - alert["customer"]["customer_code"] = customer_code
78 - return AlertsResponse(
79 - success=True,
80 - message="Successfully fetched alerts",
81 - alerts=result["data"]["alerts"],
82 - )
83 - except Exception as e:
84 - logger.error(f"Error fetching alerts: {e}")
85 - raise HTTPException(status_code=500, detail=f"Error fetching alerts: {e}")
86 -
87 -
88 -def construct_params(request: FilterAlertsRequest) -> dict:
89 - """
90 - Constructs the parameters for the alert filtering request.
91 -
92 - Args:
93 - request (FilterAlertsRequest): The request object containing filtering criteria.
94 -
95 - Returns:
96 - dict: A dictionary of parameters for the alert filtering request.
97 - """
98 - params = {
99 - "page": request.page,
100 - "per_page": request.per_page,
101 - "sort": request.sort.value if request.sort else None,
102 - "alert_title": request.alert_title,
103 - # Add more parameters here as needed
104 - }
105 -
106 - # Remove parameters that have a value of None
107 - return {k: v for k, v in params.items() if v is not None}
108 -
109 -
110 -async def get_alert(alert_id: str, session: AsyncSession) -> AlertResponse:
111 - """
112 - Retrieves an alert by its ID.
113 -
114 - Args:
115 - alert_id (str): The ID of the alert to retrieve.
116 -
117 - Returns:
118 - AlertResponse: The response object containing the alert data.
119 -
120 - Raises:
121 - SomeException: If there is an error retrieving the alert.
122 - """
123 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
124 - result = await fetch_and_validate_data(client, alert.get_alert, alert_id)
125 - # Add the customer code to the alert
126 - customer_code = await get_customer_code(
127 - session,
128 - result["data"]["customer"]["customer_id"],
129 - )
130 - result["data"]["customer"]["customer_code"] = customer_code
131 - return AlertResponse(
132 - success=True,
133 - message="Successfully fetched alert",
134 - alert=result["data"],
135 - )
136 -
137 -
138 -async def create_case(alert_id: str) -> CaseCreationResponse:
139 - """
140 - Creates a case for an alert.
141 -
142 - Args:
143 - alert_id (str): The ID of the alert to create a case for.
144 -
145 - Returns:
146 - CaseCreationResponse: The response object containing the success status, message, and created case data.
147 - """
148 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
149 - # Get the alert
150 - alert_details = await fetch_and_validate_data(client, alert.get_alert, alert_id)
151 - params = construct_case_creation_params(alert_details["data"])
152 - logger.info(f"Creating case with params {params}")
153 - result = await fetch_and_validate_data(
154 - client,
155 - lambda: alert.escalate_alert(int(alert_id), **params),
156 - )
157 - logger.info(f"Successfully created case for alert: {result}")
158 - return CaseCreationResponse(
159 - success=True,
160 - message="Successfully created case for alert",
161 - case=result["data"],
162 - )
163 -
164 -
165 -def construct_case_creation_params(alert_details: dict) -> dict:
166 - """
167 - Constructs the parameters for the case creation request.
168 -
169 - Args:
170 - alert_details (dict): The alert details.
171 -
172 - Returns:
173 - dict: A dictionary of parameters for the case creation request.
174 - """
175 - params = {
176 - "case_title": alert_details["alert_title"],
177 - "case_tags": alert_details["alert_tags"],
178 - "escalation_note": "Case created from CoPilot",
179 - "iocs_import_list": [ioc["ioc_uuid"] for ioc in alert_details["iocs"]],
180 - "assets_import_list": [asset["asset_uuid"] for asset in alert_details["assets"]],
181 - }
182 -
183 - # Replace None values with the string "None"
184 - return {k: v if v is not None else "None" for k, v in params.items()}
185 -
186 -
187 -async def bookmark_alert(alert_id: str, bookmarked: bool) -> AlertResponse:
188 - """
189 - Bookmarks or removes bookmark from an alert.
190 -
191 - Args:
192 - alert_id (str): The ID of the alert.
193 - bookmarked (bool): Indicates whether to bookmark or remove bookmark from the alert.
194 -
195 - Returns:
196 - AlertResponse: The response containing the success status, message, and updated alert data.
197 - """
198 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
199 - if bookmarked:
200 - result = await fetch_and_validate_data(
201 - client,
202 - alert.update_alert,
203 - alert_id,
204 - {"alert_tags": "bookmarked"},
205 - )
206 - return AlertResponse(
207 - success=True,
208 - message="Successfully bookmarked alert",
209 - alert=result["data"],
210 - )
211 - result = await fetch_and_validate_data(
212 - client,
213 - alert.update_alert,
214 - alert_id,
215 - {"alert_tags": ""},
216 - )
217 - return AlertResponse(
218 - success=True,
219 - message="Successfully removed bookmark from alert",
220 - alert=result["data"],
221 - )
222 -
223 -
224 -async def get_bookmarked_alerts(session: AsyncSession) -> BookmarkedAlertsResponse:
225 - """
226 - Retrieves the bookmarked alerts from the system.
227 -
228 - Returns:
229 - BookmarkedAlertsResponse: The response object containing the bookmarked alerts.
230 - """
231 - alerts = await get_alerts(
232 - request=FilterAlertsRequest(per_page=10000),
233 - session=session,
234 - )
235 - alerts = alerts.alerts
236 - bookmarked_alerts = []
237 - for alert in alerts:
238 - if alert["alert_tags"] is not None and "bookmarked" in alert["alert_tags"]:
239 - bookmarked_alerts.append(alert)
240 -
241 - return BookmarkedAlertsResponse(
242 - success=True,
243 - message="Successfully fetched bookmarked alerts",
244 - bookmarked_alerts=bookmarked_alerts,
245 - )
246 -
247 -
248 -async def delete_alert(alert_id: int) -> DeleteAlertResponse:
249 - """
250 - Deletes an alert.
251 -
252 - Args:
253 - alert_id (int): The ID of the alert to delete.
254 -
255 - Returns:
256 - DeleteAlertResponse: The response object containing the success status, message, and deleted alert.
257 - """
258 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
259 - result = await fetch_and_validate_data(client, alert.delete_alert, alert_id)
260 - return DeleteAlertResponse(
261 - success=True,
262 - message="Successfully deleted alert",
263 - alert=result["data"],
264 - )
backend/app/connectors/dfir_iris/services/assets.py deleted
-36
@@ -1,36 +0,0 @@
1 -from fastapi import HTTPException
2 -
3 -from app.connectors.dfir_iris.schema.assets import Asset
4 -from app.connectors.dfir_iris.schema.assets import AssetResponse
5 -from app.connectors.dfir_iris.schema.assets import AssetState
6 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
7 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
8 -
9 -
10 -async def get_case_assets(case_id: int) -> AssetResponse:
11 - """
12 - Retrieves assets for a given case.
13 -
14 - Args:
15 - case_id (int): The ID of the case.
16 -
17 - Returns:
18 - AssetResponse: The response containing the fetched assets and their state.
19 - """
20 - client, case = await initialize_client_and_case("DFIR-IRIS")
21 - result = await fetch_and_validate_data(client, case.list_assets, case_id)
22 - try:
23 - asset_list = result["data"]["assets"]
24 - state_data = result["data"]["state"]
25 - except KeyError as e:
26 - raise HTTPException(
27 - status_code=500,
28 - detail=f"Failed to fetch assets for case {case_id}: {e}",
29 - )
30 -
31 - return AssetResponse(
32 - success=True,
33 - message="Successfully fetched assets for case",
34 - assets=[Asset(**asset) for asset in asset_list], # List[Asset]
35 - state=AssetState(**state_data), # AssetState
36 - )
backend/app/connectors/dfir_iris/services/cases.py deleted
-339
@@ -1,339 +0,0 @@
1 -from datetime import datetime
2 -from typing import Dict
3 -from typing import List
4 -
5 -from dfir_iris_client.case import Case
6 -from fastapi import HTTPException
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -from sqlalchemy.future import select
10 -
11 -from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody
12 -from app.connectors.dfir_iris.schema.cases import CaseResponse
13 -from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
14 -from app.connectors.dfir_iris.schema.cases import ClosedCaseResponse
15 -from app.connectors.dfir_iris.schema.cases import PurgeCaseResponse
16 -from app.connectors.dfir_iris.schema.cases import ReopenedCaseResponse
17 -from app.connectors.dfir_iris.schema.cases import SingleCaseBody
18 -from app.connectors.dfir_iris.schema.cases import SingleCaseResponse
19 -from app.connectors.dfir_iris.utils.universal import create_dfir_iris_client
20 -from app.connectors.dfir_iris.utils.universal import fetch_and_parse_data
21 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
22 - AlertCreationSettings,
23 -)
24 -
25 -
26 -async def get_client_and_cases() -> Dict:
27 - """
28 - Initialize the client session and fetch all cases.
29 -
30 - Returns:
31 - Dictionary containing the success status and either the case data or an error message.
32 - """
33 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
34 - case = Case(session=dfir_iris_client)
35 - logger.info("Fetching all cases after getting session")
36 - result = await fetch_and_parse_data(dfir_iris_client, case.list_cases)
37 - return result
38 -
39 -
40 -async def get_customer_code(session: AsyncSession, client_name: str) -> str:
41 - """
42 - Retrieves the customer code for a given customer ID.
43 -
44 - Args:
45 - session (AsyncSession): The database session.
46 - customer_id (int): The ID of the customer.
47 -
48 - Returns:
49 - The customer code for the given customer ID.
50 - """
51 - try:
52 - alert_creation_settings = await session.execute(
53 - select(AlertCreationSettings).filter(
54 - AlertCreationSettings.iris_customer_name == client_name,
55 - ),
56 - )
57 - alert_creation_settings = alert_creation_settings.scalars().first()
58 - if alert_creation_settings is None:
59 - return "Customer Not Found"
60 - return alert_creation_settings.customer_code
61 - except Exception as e:
62 - logger.error(
63 - f"Error retrieving customer code for customer ID {client_name}: {e}",
64 - )
65 - return "Customer Not Found"
66 -
67 -
68 -def filter_open_cases(cases: List[Dict]) -> List[Dict]:
69 - """
70 - Filters out cases that are still open.
71 -
72 - Args:
73 - cases (List): List of all cases.
74 -
75 - Returns:
76 - List of cases that are still open.
77 - """
78 - return [case for case in cases if case["case_close_date"] == ""]
79 -
80 -
81 -def filter_cases_older_than(cases: List[Dict], older_than: datetime) -> List[Dict]:
82 - """
83 - Filters out cases that are older than the specified time.
84 -
85 - Args:
86 - cases (List): List of all cases.
87 - older_than (datetime): The datetime to filter by.
88 -
89 - Returns:
90 - List of cases that are older than the specified time.
91 - """
92 - current_time = datetime.now()
93 - filtered_cases = []
94 - for case in cases:
95 - case_open_date = (
96 - datetime.strptime(case["case_open_date"], "%m/%d/%Y")
97 - if not isinstance(case["case_open_date"], datetime)
98 - else case["case_open_date"]
99 - )
100 - if case_open_date < current_time - older_than:
101 - case["case_open_date"] = case_open_date.strftime(
102 - "%m/%d/%Y",
103 - ) # Convert back to string to match the model
104 - filtered_cases.append(case)
105 - return filtered_cases
106 -
107 -
108 -async def get_all_cases(session: AsyncSession) -> CaseResponse:
109 - """
110 - Retrieves all cases from DFIR-IRIS.
111 -
112 - Returns:
113 - CaseResponse: The response object containing the success status, message, and cases data.
114 -
115 - Raises:
116 - HTTPException: If there is an error retrieving the cases.
117 - """
118 - result = await get_client_and_cases()
119 - try:
120 - if not result["success"]:
121 - logger.error(f"Failed to get all cases: {result['message']}")
122 - raise HTTPException(
123 - status_code=500,
124 - detail=f"Failed to get all cases: {result['message']}",
125 - )
126 - for case in result["data"]:
127 - case["customer_code"] = await get_customer_code(
128 - session,
129 - case["client_name"],
130 - )
131 - return CaseResponse(
132 - success=True,
133 - message="Successfully fetched all cases",
134 - cases=result["data"],
135 - )
136 - except Exception as err:
137 - logger.error(f"Failed to get all cases: {err}")
138 - raise HTTPException(status_code=500, detail=f"Failed to get all cases: {err}")
139 -
140 -
141 -async def get_cases_older_than(
142 - case_older_than_body: CaseOlderThanBody,
143 -) -> CasesBreachedResponse:
144 - """
145 - Retrieves cases that are older than a specified duration.
146 -
147 - Args:
148 - case_older_than_body (CaseOlderThanBody): The body containing the duration threshold.
149 -
150 - Returns:
151 - CasesBreachedResponse: The response object containing the breached cases.
152 - """
153 - result = await get_client_and_cases()
154 - if not result["success"]:
155 - logger.error(f"Failed to get all cases: {result['message']}")
156 - return HTTPException(
157 - status_code=500,
158 - detail=f"Failed to get all cases: {result['message']}",
159 - )
160 -
161 - open_cases = filter_open_cases(result["data"])
162 - breached_cases = filter_cases_older_than(
163 - open_cases,
164 - case_older_than_body.older_than,
165 - )
166 - return CasesBreachedResponse(
167 - success=True,
168 - message=f"Successfully fetched all cases older than {case_older_than_body.older_than}",
169 - cases_breached=breached_cases,
170 - )
171 -
172 -
173 -async def get_single_case(
174 - case_id: SingleCaseBody,
175 - session: AsyncSession,
176 -) -> SingleCaseResponse:
177 - """
178 - Fetches a single case from DFIR-IRIS based on the provided case ID.
179 -
180 - Args:
181 - case_id (SingleCaseBody): The ID of the case to fetch.
182 -
183 - Returns:
184 - SingleCaseResponse: The response containing the fetched case.
185 -
186 - Raises:
187 - Any exceptions raised during the execution of the function will be propagated.
188 - """
189 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
190 - case = Case(session=dfir_iris_client)
191 - result = await fetch_and_parse_data(dfir_iris_client, case.get_case, case_id)
192 - result["data"]["customer_code"] = await get_customer_code(
193 - session,
194 - result["data"]["customer_name"],
195 - )
196 - return SingleCaseResponse(
197 - success=True,
198 - message="Successfully fetched single case",
199 - case=result["data"],
200 - )
201 -
202 -
203 -async def close_case(case_id: SingleCaseBody) -> ClosedCaseResponse:
204 - """
205 - Closes a single case from DFIR-IRIS based on the provided case ID.
206 -
207 - Args:
208 - case_id (SingleCaseBody): The ID of the case to close.
209 -
210 - Returns:
211 - ClosedCaseResponse: The response containing the closed case.
212 -
213 - Raises:
214 - Any exceptions raised during the execution of the function will be propagated.
215 - """
216 - logger.info(f"Closing case: {case_id}")
217 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
218 - case = Case(session=dfir_iris_client)
219 - result = await fetch_and_parse_data(dfir_iris_client, case.close_case, case_id)
220 - return ClosedCaseResponse(
221 - success=True,
222 - case=result["data"],
223 - message="Successfully closed case",
224 - )
225 -
226 -
227 -async def reopen_case(case_id: SingleCaseBody) -> ReopenedCaseResponse:
228 - """
229 - Opens a single case from DFIR-IRIS based on the provided case ID.
230 -
231 - Args:
232 - case_id (SingleCaseBody): The ID of the case to open.
233 -
234 - Returns:
235 - OpenCaseResponse: The response containing the opened case.
236 -
237 - Raises:
238 - Any exceptions raised during the execution of the function will be propagated.
239 - """
240 - logger.info(f"Opening case: {case_id}")
241 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
242 - case = Case(session=dfir_iris_client)
243 - result = await fetch_and_parse_data(dfir_iris_client, case.reopen_case, case_id)
244 - logger.info(f"Successfully opened case: {result}")
245 - return ReopenedCaseResponse(
246 - success=True,
247 - case=result["data"],
248 - message="Successfully opened case",
249 - )
250 -
251 -
252 -############# ! DELETE ACTIONS ! #############
253 -async def purge_cases() -> PurgeCaseResponse:
254 - """
255 - Purges all cases from DFIR-IRIS.
256 -
257 - Returns:
258 - PurgeCaseResponse: The response containing the purge status.
259 -
260 - Raises:
261 - HTTPException: If there is an error purging the cases.
262 - """
263 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
264 - case = Case(session=dfir_iris_client)
265 -
266 - case_ids = await get_case_ids_to_purge()
267 -
268 - for case_id in case_ids:
269 - await purge_case(dfir_iris_client, case, case_id)
270 -
271 - return PurgeCaseResponse(success=True, message="Successfully purged all cases")
272 -
273 -
274 -async def get_case_ids_to_purge() -> List[int]:
275 - """
276 - Retrieves all case IDs to be purged, skipping over specific cases as needed.
277 -
278 - Returns:
279 - List[int]: List of case IDs to purge.
280 - """
281 - result = await get_client_and_cases()
282 - handle_cases_retrieval_failure(result)
283 -
284 - # Extract case IDs, skipping over specific cases
285 - return [case["case_id"] for case in result["data"] if case["case_id"] != 1]
286 -
287 -
288 -def handle_cases_retrieval_failure(result: Dict) -> None:
289 - """
290 - Handles failure in retrieving cases.
291 -
292 - Args:
293 - result (Dict): The result of the cases retrieval attempt.
294 - """
295 - if not result["success"]:
296 - error_message = f"Failed to get all cases: {result['message']}"
297 - logger.error(error_message)
298 - raise HTTPException(status_code=500, detail=error_message)
299 -
300 -
301 -async def purge_case(client, case, case_id) -> PurgeCaseResponse:
302 - """
303 - Purges a single case.
304 -
305 - Args:
306 - client: The DFIR-IRIS client.
307 - case: The case object.
308 - case_id (int): The ID of the case to purge.
309 - """
310 - try:
311 - logger.info(f"Purging case: {case_id}")
312 - await fetch_and_parse_data(client, case.delete_case, case_id)
313 - return PurgeCaseResponse(
314 - success=True,
315 - message=f"Successfully purged case {case_id}",
316 - )
317 - except Exception as err:
318 - error_message = f"Failed to purge case {case_id}: {err}"
319 - logger.error(error_message)
320 - raise HTTPException(status_code=500, detail=error_message)
321 -
322 -
323 -async def delete_single_case(case_id: SingleCaseBody) -> PurgeCaseResponse:
324 - """
325 - Deletes a single case from DFIR-IRIS based on the provided case ID.
326 -
327 - Args:
328 - case_id (SingleCaseBody): The ID of the case to delete.
329 -
330 - Returns:
331 - SingleCaseResponse: The response containing the deleted case.
332 -
333 - Raises:
334 - Any exceptions raised during the execution of the function will be propagated.
335 - """
336 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
337 - case = Case(session=dfir_iris_client)
338 - await fetch_and_parse_data(dfir_iris_client, case.delete_case, case_id)
339 - return PurgeCaseResponse(success=True, message="Successfully deleted single case")
backend/app/connectors/dfir_iris/services/notes.py deleted
-149
@@ -1,149 +0,0 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import List
4 -
5 -from dfir_iris_client.case import Case
6 -from loguru import logger
7 -
8 -from app.connectors.dfir_iris.schema.notes import NoteCreationBody
9 -from app.connectors.dfir_iris.schema.notes import NoteCreationResponse
10 -from app.connectors.dfir_iris.schema.notes import NoteDetails
11 -from app.connectors.dfir_iris.schema.notes import NoteDetailsResponse
12 -from app.connectors.dfir_iris.schema.notes import NotesResponse
13 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
14 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
15 -
16 -
17 -async def process_directories(directories: List[Dict], case_id: int) -> List[Dict]:
18 - """
19 - Process a list of notes for a given case.
20 -
21 - Args:
22 - notes (List[Dict]): The list of notes to be processed.
23 - case_id (int): The ID of the case.
24 -
25 - Returns:
26 - List[Dict]: The processed list of notes.
27 - """
28 - processed_notes = []
29 - for directory in directories:
30 - for note in directory["notes"]:
31 - logger.info(f"Note: {note}")
32 - note_details = await get_case_note_details(note["id"], case_id)
33 - logger.info(f"Note details: {note_details}")
34 - note["note_details"] = note_details.note_details
35 - processed_notes.append(note)
36 - return processed_notes
37 -
38 -
39 -async def get_case_notes(case_id: int, search_term: str) -> NotesResponse:
40 - """
41 - Retrieves case notes based on the provided case ID and search term.
42 -
43 - Args:
44 - case_id (int): The ID of the case.
45 - search_term (str): The search term to filter the notes.
46 -
47 - Returns:
48 - NotesResponse: An object containing the success status, message, and retrieved notes.
49 - """
50 - client, case = await initialize_client_and_case("DFIR-IRIS")
51 - result = await fetch_and_validate_data(
52 - client,
53 - case.list_notes_directories,
54 - case_id,
55 - )
56 - logger.info(f"Result: {result}")
57 - processed_notes = await process_directories(result["data"], case_id)
58 - return NotesResponse(
59 - success=True,
60 - message="Successfully fetched notes for case",
61 - notes=processed_notes,
62 - )
63 -
64 -
65 -async def get_case_note_details(directory_id: int, case_id: int) -> NoteDetailsResponse:
66 - """
67 - Retrieves the details of a specific case note.
68 -
69 - Args:
70 - note_id (int): The ID of the note.
71 - case_id (int): The ID of the case.
72 -
73 - Returns:
74 - NoteDetailsResponse: The response containing the note details.
75 -
76 - Raises:
77 - SomeException: If there is an error retrieving the note details.
78 - """
79 - client, case = await initialize_client_and_case("DFIR-IRIS")
80 - result = await fetch_and_validate_data(client, case.get_note, directory_id, case_id)
81 - logger.info(f"Result: {result}")
82 - note_details = NoteDetails(**result["data"])
83 - return NoteDetailsResponse(
84 - success=True,
85 - message="Successfully fetched note details",
86 - note_details=note_details,
87 - )
88 -
89 -
90 -async def perform_note_creation(
91 - client: Any,
92 - case: Case,
93 - note_creation_body: NoteCreationBody,
94 - case_id: int,
95 -) -> Dict:
96 - """
97 - Performs the creation of a note in a case.
98 -
99 - Args:
100 - client (Any): The client object used for making API requests.
101 - case (Case): The case object representing the case where the note will be created.
102 - note_creation_body (NoteCreationBody): The body containing the details of the note to be created.
103 - case_id (int): The ID of the case where the note will be created.
104 -
105 - Returns:
106 - Dict: The response data containing the created note information.
107 - """
108 - result = await fetch_and_validate_data(
109 - client,
110 - case.add_notes_directory,
111 - "CoPilot",
112 - None,
113 - case_id,
114 - )
115 - logger.info(f"Result: {result}")
116 - note_id = result["data"]["id"]
117 - custom_attributes = {}
118 - return await fetch_and_validate_data(
119 - client,
120 - case.add_note,
121 - note_creation_body.note_title,
122 - note_creation_body.note_content,
123 - note_id,
124 - custom_attributes,
125 - case_id,
126 - )
127 -
128 -
129 -async def create_case_note(
130 - case_id: int,
131 - note_creation_body: NoteCreationBody,
132 -) -> NoteCreationResponse:
133 - """
134 - Creates a note for a specific case.
135 -
136 - Args:
137 - case_id (int): The ID of the case.
138 - note_creation_body (NoteCreationBody): The body of the note creation request.
139 -
140 - Returns:
141 - NoteCreationResponse: The response containing the success status, message, and created note.
142 - """
143 - client, case = await initialize_client_and_case("DFIR-IRIS")
144 - result = await perform_note_creation(client, case, note_creation_body, case_id)
145 - return NoteCreationResponse(
146 - success=True,
147 - message="Successfully created note",
148 - note=result["data"],
149 - )
backend/app/connectors/dfir_iris/services/users.py deleted
-71
@@ -1,71 +0,0 @@
1 -from app.connectors.dfir_iris.schema.alerts import AlertResponse
2 -from app.connectors.dfir_iris.schema.users import UsersResponse
3 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
4 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
5 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_user
6 -
7 -
8 -async def get_users() -> UsersResponse:
9 - """
10 - Fetches the list of users from the DFIR-IRIS service.
11 -
12 - Returns:
13 - UsersResponse: The response object containing the list of users.
14 - """
15 - client, user = await initialize_client_and_user("DFIR-IRIS")
16 - result = await fetch_and_validate_data(client, user.list_users)
17 - return UsersResponse(
18 - success=True,
19 - message="Successfully fetched users",
20 - users=result["data"],
21 - )
22 -
23 -
24 -async def assign_user_to_alert(alert_id: str, user_id: int) -> AlertResponse:
25 - """
26 - Assigns a user to an alert.
27 -
28 - Args:
29 - alert_id (str): The ID of the alert.
30 - user_id (int): The ID of the user to be assigned.
31 -
32 - Returns:
33 - AlertResponse: The response containing the updated alert information.
34 - """
35 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
36 - result = await fetch_and_validate_data(
37 - client,
38 - alert.update_alert,
39 - alert_id,
40 - {"alert_owner_id": user_id},
41 - )
42 - return AlertResponse(
43 - success=True,
44 - message="Successfully assigned user to alert",
45 - alert=result["data"],
46 - )
47 -
48 -
49 -async def delete_user_from_alert(alert_id: str, user_id: int) -> AlertResponse:
50 - """
51 - Delete a user from an alert.
52 -
53 - Args:
54 - alert_id (str): The ID of the alert.
55 - user_id (int): The ID of the user to be deleted.
56 -
57 - Returns:
58 - AlertResponse: The response containing the result of the operation.
59 - """
60 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
61 - result = await fetch_and_validate_data(
62 - client,
63 - alert.update_alert,
64 - alert_id,
65 - {"alert_owner_id": None},
66 - )
67 - return AlertResponse(
68 - success=True,
69 - message="Successfully deleted user from alert",
70 - alert=result["data"],
71 - )
backend/app/connectors/dfir_iris/utils/universal.py deleted
-401
@@ -1,401 +0,0 @@
1 -from typing import Any
2 -from typing import Callable
3 -from typing import Dict
4 -from typing import List
5 -from typing import Optional
6 -from typing import Tuple
7 -from typing import Union
8 -
9 -import requests
10 -from dfir_iris_client.admin import AdminHelper
11 -from dfir_iris_client.alert import Alert
12 -from dfir_iris_client.case import Case
13 -from dfir_iris_client.customer import Customer
14 -from dfir_iris_client.helper.utils import assert_api_resp
15 -from dfir_iris_client.helper.utils import get_data_from_resp
16 -from dfir_iris_client.session import ClientSession
17 -from dfir_iris_client.users import User
18 -from fastapi import HTTPException
19 -from loguru import logger
20 -
21 -from app.connectors.utils import get_connector_info_from_db
22 -from app.db.db_session import get_db_session
23 -
24 -
25 -async def verify_dfir_iris_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
26 - """
27 - Verifies the connection to DFIR-IRIS service.
28 -
29 - Returns:
30 - dict: A dictionary containing 'connectionSuccessful' status and 'authToken' if the connection is successful.
31 - """
32 - logger.info(f"Verifying the DFIR-IRIS connection to {attributes['connector_url']}")
33 -
34 - try:
35 - headers = {
36 - "Authorization": f"Bearer {attributes['connector_api_key']}",
37 - }
38 - dfir_iris = requests.get(
39 - f"{attributes['connector_url']}/api/ping",
40 - headers=headers,
41 - verify=False,
42 - )
43 - # See if 401 is returned
44 - if dfir_iris.status_code == 401:
45 - logger.info(
46 - f"Connection to {attributes['connector_url']} unauthorized",
47 - )
48 - logger.debug("DFIR-IRIS connection unauthorized")
49 - return {
50 - "connectionSuccessful": False,
51 - "message": "DFIR-IRIS connection unauthorized",
52 - }
53 - # See if 200 is returned
54 - if dfir_iris.status_code == 200:
55 - logger.info(
56 - f"Connection to {attributes['connector_url']} successful",
57 - )
58 - logger.debug("DFIR-IRIS connection successful")
59 - return {
60 - "connectionSuccessful": True,
61 - "message": "DFIR-IRIS connection successful",
62 - }
63 - except Exception as e:
64 - logger.error(
65 - f"Connection to {attributes['connector_url']} failed with error: {e}",
66 - )
67 - return {
68 - "connectionSuccessful": False,
69 - "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
70 - }
71 -
72 -
73 -async def verify_dfir_iris_connection(connector_name: str) -> str:
74 - """
75 - Returns the authentication token for the DFIR-IRIS service.
76 -
77 - Returns:
78 - str: Authentication token for the DFIR-IRIS service.
79 - """
80 - async with get_db_session() as session: # This will correctly enter the context manager
81 - attributes = await get_connector_info_from_db(connector_name, session)
82 - if attributes is None:
83 - logger.error("No DFIR-IRIS connector found in the database")
84 - return None
85 - return await verify_dfir_iris_credentials(attributes)
86 -
87 -
88 -async def create_dfir_iris_client(connector_name: str) -> ClientSession:
89 - """
90 - Creates a session with DFIR-IRIS.
91 -
92 - This method creates a session with DFIR-IRIS and returns a dictionary with a success status and the session object.
93 - If a session cannot be established, an error is logged and a dictionary with "success" set to False and an error message is
94 - returned.
95 -
96 - Returns:
97 - dict: A dictionary containing the success status and either the session object or an error message.
98 - """
99 - try:
100 - async with get_db_session() as session: # This will correctly enter the context manager
101 - attributes = await get_connector_info_from_db(connector_name, session)
102 - logger.info("Creating session with DFIR-IRIS.")
103 - return ClientSession(
104 - host=attributes["connector_url"],
105 - apikey=attributes["connector_api_key"],
106 - agent="iris-client",
107 - ssl_verify=False,
108 - timeout=120,
109 - proxy=None,
110 - )
111 - except Exception as e:
112 - logger.error(f"Error creating session with DFIR-IRIS: {e}")
113 - raise HTTPException(
114 - status_code=500,
115 - detail=f"Error creating session with DFIR-IRIS: {e}",
116 - )
117 -
118 -
119 -async def fetch_and_parse_data(
120 - session: ClientSession,
121 - action: Callable,
122 - *args,
123 - **kwargs: Optional[Any],
124 -) -> Dict[str, Union[bool, Optional[Dict]]]:
125 - """
126 - Fetches and parses data from DFIR-IRIS using a specified action.
127 -
128 - Args:
129 - session (ClientSession): The DFIR-IRIS session object.
130 - action (Callable): The function to execute to fetch data from DFIR-IRIS. This function should accept *args.
131 - args: The arguments to pass to the action function.
132 -
133 - Returns:
134 - dict: A dictionary containing the success status and either the fetched data or None if the operation was unsuccessful.
135 - """
136 - try:
137 - logger.info(f"Executing {action.__name__}... on args: {args} and kwargs: {kwargs}")
138 - status = action(*args, **kwargs)
139 - assert_api_resp(status, soft_fail=False)
140 - data = get_data_from_resp(status)
141 - logger.info(f"Successfully executed {action.__name__}")
142 - return {"success": True, "data": data}
143 - except Exception as err:
144 - logger.error(f"Failed to execute {action.__name__}: {err}")
145 - raise HTTPException(
146 - status_code=500,
147 - detail=f"Failed to execute {action.__name__}: {err}",
148 - )
149 -
150 -
151 -async def initialize_client_and_case(service_name: str) -> Tuple[Any, Case]:
152 - """
153 - Initializes the DFIR Iris client for case operations.
154 -
155 - Args:
156 - service_name (str): The name of the DFIR Iris service.
157 -
158 - Returns:
159 - Tuple[Any, Case]: A tuple containing the DFIR Iris client and the newly created case.
160 - """
161 - dfir_iris_client = await create_dfir_iris_client(service_name)
162 - case = Case(session=dfir_iris_client)
163 - return dfir_iris_client, case
164 -
165 -
166 -async def initialize_client_and_alert(service_name: str) -> Tuple[Any, Alert]:
167 - """
168 - Initializes the DFIR Iris client for alert operations.
169 -
170 - Args:
171 - service_name (str): The name of the service.
172 -
173 - Returns:
174 - Tuple[Any, Alert]: A tuple containing the DFIR Iris client and the alert.
175 - """
176 - dfir_iris_client = await create_dfir_iris_client(service_name)
177 - alert = Alert(session=dfir_iris_client)
178 - return dfir_iris_client, alert
179 -
180 -
181 -async def initialize_client_and_user(service_name: str) -> Tuple[Any, Alert]:
182 - """
183 - Initializes the DFIR Iris client for user operations.
184 -
185 - Args:
186 - service_name (str): The name of the service.
187 -
188 - Returns:
189 - Tuple[Any, Alert]: A tuple containing the DFIR Iris client and the user.
190 - """
191 - dfir_iris_client = await create_dfir_iris_client(service_name)
192 - user = User(session=dfir_iris_client)
193 - return dfir_iris_client, user
194 -
195 -
196 -async def initialize_client_and_admin(service_name: str) -> Tuple[Any, Alert]:
197 - """
198 - Initializes the DFIR Iris client and admin helper.
199 -
200 - Args:
201 - service_name (str): The name of the service.
202 -
203 - Returns:
204 - Tuple[Any, Alert]: A tuple containing the DFIR Iris client and the admin helper.
205 - """
206 - dfir_iris_client = await create_dfir_iris_client(service_name)
207 - admin = AdminHelper(session=dfir_iris_client)
208 - return dfir_iris_client, admin
209 -
210 -
211 -async def initialize_client_and_customer(service_name: str) -> Tuple[Any, Alert]:
212 - """
213 - Initializes the DFIR Iris client for customer operations.
214 -
215 - Args:
216 - service_name (str): The name of the service.
217 -
218 - Returns:
219 - Tuple[Any, Alert]: A tuple containing the DFIR Iris client and the customer.
220 - """
221 - dfir_iris_client = await create_dfir_iris_client(service_name)
222 - customer = Customer(session=dfir_iris_client)
223 - return dfir_iris_client, customer
224 -
225 -
226 -def handle_error(error_message: str, status_code: int = 500):
227 - """
228 - Handles an error by logging the error message and raising an HTTPException.
229 -
230 - Args:
231 - error_message (str): The error message to be logged and included in the HTTPException detail.
232 - status_code (int, optional): The status code to be used in the HTTPException. Defaults to 500.
233 - """
234 - logger.error(error_message)
235 - raise HTTPException(status_code=status_code, detail=error_message)
236 -
237 -
238 -async def fetch_and_validate_data(client: Any, func: Callable, *args: Any, **kwargs: Optional[Any]) -> Dict:
239 - """
240 - Fetches and validates data using the provided client, function, and arguments.
241 -
242 - Args:
243 - client (Any): The client object used to fetch the data.
244 - func (Callable): The function to be called to fetch the data.
245 - *args (Any): Variable length argument list for the function.
246 -
247 - Returns:
248 - Dict: The fetched and validated data.
249 -
250 - Raises:
251 - Exception: If the data fetching fails.
252 - """
253 - result = await fetch_and_parse_data(client, func, *args, **kwargs)
254 - if not result["success"]:
255 - handle_error(f"Failed to fetch data: {result['message']}")
256 - return result
257 -
258 -
259 -async def check_case_exists(case_id: int) -> bool:
260 - """
261 - Check if a case exists in DFIR-IRIS.
262 -
263 - Args:
264 - case_id (int): The ID of the case to check.
265 -
266 - Returns:
267 - bool: True if the case exists, False otherwise.
268 - """
269 - try:
270 - logger.info(f"Checking if case {case_id} exists")
271 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
272 - case = Case(session=dfir_iris_client)
273 - data = case.get_case(case_id)
274 - assert_api_resp(data, soft_fail=False)
275 - result = get_data_from_resp(data)
276 - if result is None:
277 - logger.info(f"Case {case_id} does not exist")
278 - return False
279 - logger.info(f"Case {case_id} exists")
280 - return True
281 - except Exception as e:
282 - logger.error(f"Failed to check if case {case_id} exists: {e}")
283 - return False
284 -
285 -
286 -async def check_alert_exists(alert_id: str) -> bool:
287 - """
288 - Check if an alert with the given ID exists.
289 -
290 - Args:
291 - alert_id (str): The ID of the alert to check.
292 -
293 - Returns:
294 - bool: True if the alert exists, False otherwise.
295 - """
296 - try:
297 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
298 - except Exception as e:
299 - logger.error(f"Failed to create DFIR-IRIS client: {e}")
300 - raise HTTPException(
301 - status_code=500,
302 - detail="Failed to create DFIR-IRIS client. Make sure the DFIR-IRIS connector is configured correctly.",
303 - )
304 - try:
305 - logger.info(f"Checking if alert {alert_id} exists")
306 - # dfir_iris_client = create_dfir_iris_client("DFIR-IRIS")
307 - alert = Alert(session=dfir_iris_client)
308 - data = alert.get_alert(alert_id)
309 - assert_api_resp(data, soft_fail=False)
310 - result = get_data_from_resp(data)
311 - if result is None:
312 - logger.info(f"Alert {alert_id} does not exist")
313 - return False
314 - logger.info(f"Alert {alert_id} exists")
315 - return True
316 - except Exception as e:
317 - logger.error(f"Failed to check if alert {alert_id} exists: {e}")
318 - return False
319 -
320 -
321 -async def check_user_exists(user_id: int) -> bool:
322 - """
323 - Check if a user exists in the DFIR-IRIS system.
324 -
325 - Args:
326 - user_id (int): The ID of the user to check.
327 -
328 - Returns:
329 - bool: True if the user exists, False otherwise.
330 - """
331 - try:
332 - logger.info(f"Checking if user {user_id} exists")
333 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
334 - user = User(session=dfir_iris_client)
335 - data = user.get_user(user_id)
336 - assert_api_resp(data, soft_fail=False)
337 - result = get_data_from_resp(data)
338 - if result is None:
339 - logger.info(f"User {user_id} does not exist")
340 - return False
341 - logger.info(f"User {user_id} exists")
342 - return True
343 - except Exception as e:
344 - logger.error(f"Failed to check if user {user_id} exists: {e}")
345 - return False
346 -
347 -
348 -async def collect_all_customers() -> List[str]:
349 - """
350 - Collects all customers from the DFIR-IRIS system.
351 -
352 - Returns:
353 - List[str]: A list of customer IDs.
354 - """
355 - try:
356 - dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
357 - customers = Customer(session=dfir_iris_client).list_customers()
358 - assert_api_resp(customers, soft_fail=False)
359 - customers = get_data_from_resp(customers)
360 - logger.info(f"Collected all customers: {customers}")
361 - return customers
362 - except Exception as e:
363 - logger.error(f"Failed to collect all customers: {e}")
364 - return []
365 -
366 -
367 -async def add_user_to_customers(customers: List[str], user_id: int) -> bool:
368 - """
369 - Add a user to a list of customers.
370 -
371 - Args:
372 - customers (List[int]): The list of customer IDs.
373 - user_id (int): The ID of the user to add.
374 -
375 - Returns:
376 - bool: True if the user was added successfully, False otherwise.
377 - """
378 - try:
379 - logger.info(f"Adding user {user_id} to customers {customers}")
380 - async with get_db_session() as session: # This will correctly enter the context manager
381 - attributes = await get_connector_info_from_db("DFIR-IRIS", session)
382 - headers = {
383 - "Authorization": f"Bearer {attributes['connector_api_key']}",
384 - }
385 - logger.info(f"Headers: {headers}")
386 - data = {
387 - "customers_membership": customers,
388 - }
389 - url_endpoint = f"{attributes['connector_url']}/manage/users/{user_id}/customers/update"
390 -
391 - response = requests.post(url_endpoint, headers=headers, json=data, verify=False)
392 - logger.info(f"Response: {response.json()}")
393 - if response.status_code == 200:
394 - logger.info(f"User {user_id} added to customers {customers}")
395 - return True
396 - else:
397 - logger.error(f"Failed to add user {user_id} to customers {customers}")
398 - return False
399 - except Exception as e:
400 - logger.error(f"Failed to add user {user_id} to customers {customers}: {e}")
401 - return False
backend/app/connectors/services.py
-39
@@ -14,7 +14,6 @@ from sqlalchemy.future import select
14 from werkzeug.utils import secure_filename
15
16 from app.connectors.cortex.utils.universal import verify_cortex_connection
17 -from app.connectors.dfir_iris.utils.universal import verify_dfir_iris_connection
17 from app.connectors.grafana.utils.universal import verify_grafana_connection
18 from app.connectors.graylog.utils.universal import verify_graylog_connection
19 from app.connectors.influxdb.utils.universal import verify_influxdb_connection
@@ -25,13 +24,7 @@ from app.connectors.sublime.utils.universal import verify_sublime_connection
24 from app.connectors.velociraptor.utils.universal import verify_velociraptor_connection
25 from app.connectors.wazuh_indexer.utils.universal import verify_wazuh_indexer_connection
26 from app.connectors.wazuh_manager.utils.universal import verify_wazuh_manager_connection
28 -from app.integrations.ask_socfortress.services.ask_socfortress import (
29 - verify_ask_socfortress_connector,
30 -)
27 from app.integrations.utils.event_shipper import verify_event_shipper_connection
32 -from app.threat_intel.services.socfortress import (
33 - verifiy_socfortress_threat_intel_connector,
34 -)
28 from app.utils import verify_alert_creation_provisioning_connection
29 from app.utils import verify_haproxy_provisioning_connection
30 from app.utils import verify_wazuh_worker_provisioning_connection
@@ -89,15 +82,6 @@ class GraylogService(ConnectorServiceInterface):
82 return await verify_graylog_connection(connector.connector_name)
83
84
92 -# DFIR-IRIS Service
93 -class DfirIrisService(ConnectorServiceInterface):
94 - async def verify_authentication(
95 - self,
96 - connector: ConnectorResponse,
97 - ) -> Optional[ConnectorResponse]:
98 - return await verify_dfir_iris_connection(connector.connector_name)
99 -
100 -
85 # Cortex Service
86 class CortexService(ConnectorServiceInterface):
87 async def verify_authentication(
@@ -165,26 +149,6 @@ class HAProxyProvisioningService(ConnectorServiceInterface):
149 )
150
151
168 -# SOCFortress Threat Intel Service
169 -class SocfortressThreatIntelService(ConnectorServiceInterface):
170 - async def verify_authentication(
171 - self,
172 - connector: ConnectorResponse,
173 - ) -> Optional[ConnectorResponse]:
174 - return await verifiy_socfortress_threat_intel_connector(
175 - connector.connector_name,
176 - )
177 -
178 -
179 -# ASK SOCFortress Service
180 -class AskSocfortressService(ConnectorServiceInterface):
181 - async def verify_authentication(
182 - self,
183 - connector: ConnectorResponse,
184 - ) -> Optional[ConnectorResponse]:
185 - return await verify_ask_socfortress_connector(connector.connector_name)
186 -
187 -
152 # Event Shipper Service
153 class EventShipperService(ConnectorServiceInterface):
154 async def verify_authentication(
@@ -221,7 +185,6 @@ def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface
185 "Wazuh-Indexer": WazuhIndexerService,
186 "Velociraptor": VelociraptorService,
187 "Graylog": GraylogService,
224 - "DFIR-IRIS": DfirIrisService,
188 "Cortex": CortexService,
189 "Shuffle": ShuffleService,
190 "Sublime": SublimeService,
@@ -229,8 +192,6 @@ def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface
192 "Grafana": GrafanaService,
193 "Wazuh Worker Provisioning": WazuhWorkerProvisioningService,
194 "HAProxy Provisioning": HAProxyProvisioningService,
232 - "SocfortressThreatIntel": SocfortressThreatIntelService,
233 - "AskSocfortress": AskSocfortressService,
195 "Event Shipper": EventShipperService,
196 "Alert Creation Provisioning": AlertCreationService,
197 }
backend/app/connectors/velociraptor/services/artifacts.py
+2 -1
@@ -188,9 +188,10 @@ async def run_file_collection(
188 f"client_id='{collect_artifact_body.velociraptor_id}', "
189 f"artifacts=['{collect_artifact_body.artifact_name}'], "
190 f"specs=[{{"
191 + f" 'artifact': '{collect_artifact_body.artifact_name}',"
192 f" 'parameters': {{"
193 f" 'env': ["
193 - f" {{'key': 'collectionSpec', 'value': 'Glob\\n{collect_artifact_body.file}\\n'}},"
194 + f" {{'key': 'collectionSpec', 'value': 'Glob\n{collect_artifact_body.file}\n'}},"
195 f" {{'key': 'Root', 'value': '{collect_artifact_body.root_disk}'}}"
196 f" ]"
197 f" }}"
backend/app/customer_provisioning/services/decommission.py
+74 -5
@@ -5,7 +5,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
5 from app.customer_provisioning.schema.decommission import DecommissionCustomerResponse
6 from app.customer_provisioning.schema.wazuh_worker import DecommissionWorkerRequest
7 from app.customer_provisioning.schema.wazuh_worker import DecommissionWorkerResponse
8 -from app.customer_provisioning.services.dfir_iris import delete_customer
8 from app.customer_provisioning.services.grafana import delete_grafana_organization
9 from app.customer_provisioning.services.graylog import delete_index_set
10 from app.customer_provisioning.services.graylog import delete_stream
@@ -40,9 +39,6 @@ async def decomission_wazuh_customer(
39 """
40 logger.info(f"Decomissioning customer {customer_meta.customer_name}")
41
43 - # Delete DFIR-IRIS Customer
44 - await delete_customer(customer_id=customer_meta.customer_meta_iris_customer_id)
45 -
42 # Delete the Wazuh Agents
43 agents = await gather_wazuh_agents(customer_meta.customer_code)
44 agents_deleted = await delete_wazuh_agents(agents)
@@ -70,6 +66,12 @@ async def decomission_wazuh_customer(
66 session=session,
67 )
68
69 + # Decommission HAProxy
70 + await decommission_haproxy(
71 + request=DecommissionWorkerRequest(customer_name=customer_meta.customer_name),
72 + session=session,
73 + )
74 +
75 # Delete Customer Meta
76 await session.delete(customer_meta)
77 await session.commit()
@@ -102,8 +104,22 @@ async def decommission_wazuh_worker(
104 ProvisionWorkerResponse: The response object indicating the success or failure of the provisioning operation.
105 """
106 logger.info(f"Decommissioning Wazuh worker {request}")
107 + # Check if the connector is verified
108 + if (
109 + await get_connector_attribute(
110 + connector_name="Wazuh Worker Provisioning",
111 + column_name="connector_verified",
112 + session=session,
113 + )
114 + is False
115 + ):
116 + logger.info("Wazuh Worker Provisioning connector is not verified, skipping ...")
117 + return DecommissionWorkerResponse(
118 + success=False,
119 + message="Wazuh Worker Provisioning connector is not verified",
120 + )
121 api_endpoint = await get_connector_attribute(
106 - connector_id=13,
122 + connector_name="Wazuh Worker Provisioning",
123 column_name="connector_url",
124 session=session,
125 )
@@ -123,3 +139,56 @@ async def decommission_wazuh_worker(
139 success=True,
140 message="Wazuh worker provisioned successfully",
141 )
142 +
143 +
144 +######### ! Decommission HAProxy ! ############
145 +async def decommission_haproxy(
146 + request: DecommissionWorkerRequest,
147 + session: AsyncSession,
148 +) -> DecommissionWorkerResponse:
149 + """
150 + Decomissions a HAProxy worker.
151 +
152 + Args:
153 + request (DecommissionWorkerRequest): The request object containing the necessary information for provisioning.
154 + session (AsyncSession): The async session object for making HTTP requests.
155 +
156 + Returns:
157 + ProvisionWorkerResponse: The response object indicating the success or failure of the provisioning operation.
158 + """
159 + logger.info(f"Decommissioning HAProxy worker {request}")
160 + # Check if the connector is verified
161 + if (
162 + await get_connector_attribute(
163 + connector_name="HAProxy Provisioning",
164 + column_name="connector_verified",
165 + session=session,
166 + )
167 + is False
168 + ):
169 + logger.info("HAProxy Provisioning connector is not verified, skipping ...")
170 + return DecommissionWorkerResponse(
171 + success=False,
172 + message="HAProxy Provisioning connector is not verified",
173 + )
174 + api_endpoint = await get_connector_attribute(
175 + connector_name="HAProxy Provisioning",
176 + column_name="connector_url",
177 + session=session,
178 + )
179 + # Send the POST request to the HAProxy worker
180 + response = requests.post(
181 + url=f"{api_endpoint}/provision_worker/haproxy/decommission",
182 + json=request.dict(),
183 + )
184 + # Check the response status code
185 + if response.status_code != 200:
186 + return DecommissionWorkerResponse(
187 + success=False,
188 + message=f"Failed to provision HAProxy worker: {response.text}",
189 + )
190 + # Return the response
191 + return DecommissionWorkerResponse(
192 + success=True,
193 + message="HAProxy worker provisioned successfully",
194 + )
backend/app/customer_provisioning/services/dfir_iris.py deleted
-94
@@ -1,94 +0,0 @@
1 -from fastapi import HTTPException
2 -from loguru import logger
3 -
4 -from app.connectors.dfir_iris.routes.users import add_user_to_customers_route
5 -from app.connectors.dfir_iris.schema.admin import CreateCustomerResponse
6 -from app.connectors.dfir_iris.schema.admin import ListCustomers
7 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
8 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_admin
9 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_customer
10 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_user
11 -
12 -
13 -async def check_customer_exists(customer_name: str) -> bool:
14 - """
15 - Check if a customer with the given name exists.
16 -
17 - Args:
18 - customer_name (str): The name of the customer to check.
19 -
20 - Returns:
21 - bool: True if the customer exists, False otherwise.
22 - """
23 - client, customer = await initialize_client_and_customer("DFIR-IRIS")
24 - result = await fetch_and_validate_data(client, customer.list_customers)
25 - customers = ListCustomers(**result)
26 - for customer in customers.data:
27 - if customer.customer_name == customer_name:
28 - return True
29 -
30 -
31 -async def create_customer(customer_name: str) -> CreateCustomerResponse:
32 - """
33 - Creates a new customer with the given name.
34 -
35 - Args:
36 - customer_name (str): The name of the customer.
37 -
38 - Returns:
39 - CreateCustomerResponse: The response object indicating the success and data of the operation.
40 - """
41 - # check if the customer exists
42 - exists = await check_customer_exists(customer_name)
43 - if exists:
44 - raise HTTPException(
45 - status_code=400,
46 - detail=f"Customer {customer_name} already exists",
47 - )
48 - client, admin = await initialize_client_and_admin("DFIR-IRIS")
49 - result = await fetch_and_validate_data(client, admin.add_customer, customer_name)
50 - return CreateCustomerResponse(success=result["success"], data=result["data"])
51 -
52 -
53 -async def add_user_to_all_customers(username: str):
54 - """
55 - Add a user to all customers.
56 -
57 - Args:
58 - username (str): The username of the user to be added.
59 -
60 - Returns:
61 - None
62 - """
63 - client, user = await initialize_client_and_user("DFIR-IRIS")
64 - user = await fetch_and_validate_data(client, user.get_user, username)
65 - if user is None or not user["success"]:
66 - raise HTTPException(
67 - status_code=400,
68 - detail=f"User {username} does not exist",
69 - )
70 - logger.info(f"User: {user}")
71 - await add_user_to_customers_route(user["data"]["user_id"])
72 -
73 -
74 -async def delete_customer(customer_id: int):
75 - """
76 - Deletes a customer with the given customer_id.
77 -
78 - Args:
79 - customer_id (int): The ID of the customer to be deleted.
80 -
81 - Returns:
82 - None
83 - """
84 - client, admin = await initialize_client_and_admin("DFIR-IRIS")
85 - try:
86 - result = await fetch_and_validate_data(client, admin.delete_customer, customer_id)
87 - except Exception as e:
88 - logger.error(f"Failed to delete customer: please remove the user from the iris customer within DFIR-IRIS {e}")
89 - raise HTTPException(
90 - status_code=400,
91 - detail="Failed to delete IRIS customer: please remove the user from the iris customer within DFIR-IRIS",
92 - )
93 - logger.info(f"Result: {result}")
94 - return None
backend/app/customer_provisioning/services/provision.py
+4 -2
@@ -278,15 +278,17 @@ async def provision_wazuh_worker(
278 """
279 logger.info(f"Provisioning Wazuh worker {request}")
280 api_endpoint = await get_connector_attribute(
281 - connector_id=13,
281 + connector_name="Wazuh Worker Provisioning",
282 column_name="connector_url",
283 session=session,
284 )
285 + logger.info(f"Wazuh Worker API endpoint: {api_endpoint}")
286 # Send the POST request to the Wazuh worker
287 response = requests.post(
288 url=f"{api_endpoint}/provision_worker",
289 json=request.dict(),
290 )
291 + logger.info(f"Status code from Wazuh Worker: {response.status_code}")
292 # Check the response status code
293 if response.status_code != 200:
294 return ProvisionWorkerResponse(
@@ -317,7 +319,7 @@ async def provision_haproxy(
319 """
320 logger.info(f"Provisioning HAProxy {request}")
321 api_endpoint = await get_connector_attribute(
320 - connector_id=15,
322 + connector_name="HAProxy Provisioning",
323 column_name="connector_url",
324 session=session,
325 )
backend/app/db/db_populate.py
+21 -18
@@ -97,24 +97,24 @@ def get_connectors_list():
97 "Connection to InfluxDB.",
98 "INFLUXDB_ORG_AND_BUCKET",
99 ),
100 - (
101 - "AskSocfortress",
102 - "3",
103 - "api_key",
104 - "Connection to AskSocfortress. Make sure you have requested an API key.",
105 - ),
106 - (
107 - "SocfortressThreatIntel",
108 - "3",
109 - "api_key",
110 - "Connection to Socfortress Threat Intel. Make sure you have requested an API key.",
111 - ),
112 - (
113 - "Cortex",
114 - "3",
115 - "api_key",
116 - "Connection to Cortex. Make sure you have created an API key.",
117 - ),
100 + # (
101 + # "AskSocfortress",
102 + # "3",
103 + # "api_key",
104 + # "Connection to AskSocfortress. Make sure you have requested an API key.",
105 + # ),
106 + # (
107 + # "SocfortressThreatIntel",
108 + # "3",
109 + # "api_key",
110 + # "Connection to Socfortress Threat Intel. Make sure you have requested an API key.",
111 + # ),
112 + # (
113 + # "Cortex",
114 + # "3",
115 + # "api_key",
116 + # "Connection to Cortex. Make sure you have created an API key.",
117 + # ),
118 ("Grafana", "3", "username_password", "Connection to Grafana."),
119 (
120 "Wazuh Worker Provisioning",
@@ -158,6 +158,9 @@ def delete_connectors_list():
158 """
159 connectors = [
160 "DFIR-IRIS",
161 + "AskSocfortress",
162 + "SocfortressThreatIntel",
163 + "Cortex",
164 ]
165
166 return connectors
backend/app/db/db_setup.py
+19
@@ -24,6 +24,7 @@ from app.db.db_populate import add_roles_if_not_exist
24 from app.db.db_populate import delete_connectors_if_exist
25 from app.db.db_session import SQLALCHEMY_DATABASE_URI
26 from app.db.db_session import db_password
27 +from app.schedulers.routes.scheduler import delete_job
28
29
30 async def create_database_if_not_exists(db_url: str, db_name: str):
@@ -299,6 +300,24 @@ async def ensure_scheduler_user(async_engine):
300 await create_scheduler_user(session)
301
302
303 +async def delete_job_if_exists(async_engine):
304 + """
305 + Deletes a job from the database if it exists.
306 +
307 + Args:
308 + job_id (str): The ID of the job to delete.
309 +
310 + Returns:
311 + None
312 + """
313 + job_id = "wazuh_index_fields_resize"
314 + logger.info(f"Deleting job with ID {job_id}")
315 + async with AsyncSession(async_engine) as session:
316 + async with session.begin():
317 + # Pass the session to the inner function
318 + await delete_job(session, job_id)
319 +
320 +
321 async def ensure_scheduler_user_removed(async_engine):
322 """
323 Ensures that the scheduler user is removed from the database.
backend/app/incidents/routes/db_operations.py
+21 -6
@@ -109,6 +109,7 @@ from app.incidents.services.db_operations import list_alerts_by_asset_name
109 from app.incidents.services.db_operations import list_alerts_by_tag
110 from app.incidents.services.db_operations import list_alerts_by_title
111 from app.incidents.services.db_operations import list_cases
112 +from app.incidents.services.db_operations import list_cases_by_asset_name
113 from app.incidents.services.db_operations import list_cases_by_assigned_to
114 from app.incidents.services.db_operations import list_cases_by_status
115 from app.incidents.services.db_operations import put_customer_notification
@@ -421,9 +422,14 @@ async def create_case_from_alert_endpoint(alert_id: CaseCreateFromAlert, db: Asy
422
423
424 @incidents_db_operations_router.get("/alerts", response_model=AlertOutResponse)
424 -async def list_alerts_endpoint(page: int = Query(1, ge=1), page_size: int = Query(25, ge=1), db: AsyncSession = Depends(get_db)):
425 +async def list_alerts_endpoint(
426 + page: int = Query(1, ge=1),
427 + page_size: int = Query(25, ge=1),
428 + order: str = Query("desc", regex="^(asc|desc)$"),
429 + db: AsyncSession = Depends(get_db),
430 +):
431 return AlertOutResponse(
426 - alerts=await list_alerts(db, page=page, page_size=page_size),
432 + alerts=await list_alerts(db, page=page, page_size=page_size, order=order),
433 total=await alert_total(db),
434 open=await alerts_open(db),
435 in_progress=await alerts_in_progress(db),
@@ -450,12 +456,13 @@ async def list_alerts_by_status_endpoint(
456 status: AlertStatus,
457 page: int = Query(1, ge=1),
458 page_size: int = Query(25, ge=1),
459 + order: str = Query("desc", regex="^(asc|desc)$"),
460 db: AsyncSession = Depends(get_db),
461 ):
462 if status not in AlertStatus:
463 raise HTTPException(status_code=400, detail="Invalid status")
464 return AlertOutResponse(
458 - alerts=await list_alert_by_status(status.value, db, page=page, page_size=page_size),
465 + alerts=await list_alert_by_status(status.value, db, page=page, page_size=page_size, order=order),
466 total=await alert_total(db),
467 open=await alerts_open(db),
468 in_progress=await alerts_in_progress(db),
@@ -470,10 +477,11 @@ async def list_alerts_by_assigned_to_endpoint(
477 assigned_to: str,
478 page: int = Query(1, ge=1),
479 page_size: int = Query(25, ge=1),
480 + order: str = Query("desc", regex="^(asc|desc)$"),
481 db: AsyncSession = Depends(get_db),
482 ):
483 return AlertOutResponse(
476 - alerts=await list_alert_by_assigned_to(assigned_to, db, page=page, page_size=page_size),
484 + alerts=await list_alert_by_assigned_to(assigned_to, db, page=page, page_size=page_size, order=order),
485 total=await alerts_total_by_assigned_to(db, assigned_to),
486 open=await alerts_open_by_assigned_to(db, assigned_to),
487 in_progress=await alerts_in_progress_by_assigned_to(db, assigned_to),
@@ -488,10 +496,11 @@ async def list_alerts_by_asset_name_endpoint(
496 asset_name: str,
497 page: int = Query(1, ge=1),
498 page_size: int = Query(25, ge=1),
499 + order: str = Query("desc", regex="^(asc|desc)$"),
500 db: AsyncSession = Depends(get_db),
501 ):
502 return AlertOutResponse(
494 - alerts=await list_alerts_by_asset_name(asset_name, db, page=page, page_size=page_size),
503 + alerts=await list_alerts_by_asset_name(asset_name, db, page=page, page_size=page_size, order=order),
504 total=await alert_total_by_assest_name(db, asset_name),
505 open=await alerts_open_by_assest_name(db, asset_name),
506 in_progress=await alerts_in_progress_by_assest_name(db, asset_name),
@@ -506,10 +515,11 @@ async def list_alerts_by_title_endpoint(
515 title: str,
516 page: int = Query(1, ge=1),
517 page_size: int = Query(25, ge=1),
518 + order: str = Query("desc", regex="^(asc|desc)$"),
519 db: AsyncSession = Depends(get_db),
520 ):
521 return AlertOutResponse(
512 - alerts=await list_alerts_by_title(title, db, page=page, page_size=page_size),
522 + alerts=await list_alerts_by_title(title, db, page=page, page_size=page_size, order=order),
523 total=await alert_total_by_alert_title(db, title),
524 open=await alerts_open_by_alert_title(db, title),
525 in_progress=await alerts_in_progress_by_alert_title(db, title),
@@ -563,3 +573,8 @@ async def list_cases_by_status_endpoint(status: AlertStatus, db: AsyncSession =
573 @incidents_db_operations_router.get("/case/assigned-to/{assigned_to}", response_model=CaseOutResponse)
574 async def list_cases_by_assigned_to_endpoint(assigned_to: str, db: AsyncSession = Depends(get_db)):
575 return CaseOutResponse(cases=await list_cases_by_assigned_to(assigned_to, db), success=True, message="Cases retrieved successfully")
576 +
577 +
578 +@incidents_db_operations_router.get("/case/asset/{asset_name}", response_model=CaseOutResponse)
579 +async def list_cases_by_asset_name_endpoint(asset_name: str, db: AsyncSession = Depends(get_db)):
580 + return CaseOutResponse(cases=await list_cases_by_asset_name(asset_name, db), success=True, message="Cases retrieved successfully")
backend/app/incidents/services/alert_collection.py
+24 -2
@@ -101,6 +101,28 @@ async def add_copilot_alert_id(index_data: CreateAlertRequest, alert_id: int):
101 """
102 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
103 body = {"doc": {"fields": {"COPILOT_ALERT_ID": f"{alert_id}"}}}
104 - es_client.update(index=index_data.index_name, id=index_data.alert_id, body=body)
105 - logger.info(f"Added CoPilot alert ID {alert_id} to Graylog event {index_data.alert_id} in index {index_data.index_name}")
104 + try:
105 + es_client.update(index=index_data.index_name, id=index_data.alert_id, body=body)
106 + logger.info(f"Added CoPilot alert ID {alert_id} to Graylog event {index_data.alert_id} in index {index_data.index_name}")
107 + except Exception as e:
108 + logger.error(
109 + f"Failed to add CoPilot alert ID {alert_id} to Graylog event {index_data.alert_id} in index {index_data.index_name}: {e}",
110 + )
111 +
112 + # Attempt to remove read-only block
113 + try:
114 + es_client.indices.put_settings(index=index_data.index_name, body={"index.blocks.write": None})
115 + logger.info(f"Removed read-only block from index {index_data.index_name}. Retrying update.")
116 +
117 + # Retry the update operation
118 + es_client.update(index=index_data.index_name, id=index_data.alert_id, body=body)
119 + logger.info(
120 + f"Added CoPilot alert ID {alert_id} to Graylog event {index_data.alert_id} in index {index_data.index_name} after removing read-only block",
121 + )
122 +
123 + # Re-enable the write block
124 + es_client.indices.put_settings(index=index_data.index_name, body={"index.blocks.write": True})
125 + except Exception as e2:
126 + logger.error(f"Failed to remove read-only block from index {index_data.index_name}: {e2}")
127 +
128 return None
backend/app/incidents/services/db_operations.py
+94 -41
@@ -2,7 +2,9 @@ from typing import List
2
3 from fastapi import HTTPException
4 from loguru import logger
5 +from sqlalchemy import asc
6 from sqlalchemy import delete
7 +from sqlalchemy import desc
8 from sqlalchemy.exc import IntegrityError
9 from sqlalchemy.ext.asyncio import AsyncSession
10 from sqlalchemy.future import select
@@ -581,43 +583,10 @@ async def get_alert_by_id(alert_id: int, db: AsyncSession) -> AlertOut:
583 return alert_out
584
585
584 -# async def list_alerts(db: AsyncSession) -> List[AlertOut]:
585 -# result = await db.execute(
586 -# select(Alert).options(
587 -# selectinload(Alert.comments),
588 -# selectinload(Alert.assets),
589 -# selectinload(Alert.cases).selectinload(CaseAlertLink.case),
590 -# selectinload(Alert.tags).selectinload(AlertToTag.tag),
591 -# ),
592 -# )
593 -# alerts = result.scalars().all()
594 -# alerts_out = []
595 -# for alert in alerts:
596 -# comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
597 -# assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
598 -# tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
599 -# linked_cases = [LinkedCaseCreate(**case_alert_link.case.__dict__) for case_alert_link in alert.cases]
600 -# alert_out = AlertOut(
601 -# id=alert.id,
602 -# alert_creation_time=alert.alert_creation_time,
603 -# time_closed=alert.time_closed,
604 -# alert_name=alert.alert_name,
605 -# alert_description=alert.alert_description,
606 -# status=alert.status,
607 -# customer_code=alert.customer_code,
608 -# source=alert.source,
609 -# assigned_to=alert.assigned_to,
610 -# comments=comments,
611 -# assets=assets,
612 -# tags=tags,
613 -# linked_cases=linked_cases,
614 -# )
615 -# alerts_out.append(alert_out)
616 -# return alerts_out
617 -
618 -
619 -async def list_alerts(db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
586 +async def list_alerts(db: AsyncSession, page: int = 1, page_size: int = 25, order: str = "desc") -> List[AlertOut]:
587 offset = (page - 1) * page_size
588 + order_by = asc(Alert.id) if order == "asc" else desc(Alert.id)
589 +
590 result = await db.execute(
591 select(Alert)
592 .options(
@@ -626,9 +595,11 @@ async def list_alerts(db: AsyncSession, page: int = 1, page_size: int = 25) -> L
595 selectinload(Alert.cases).selectinload(CaseAlertLink.case),
596 selectinload(Alert.tags).selectinload(AlertToTag.tag),
597 )
598 + .order_by(order_by)
599 .offset(offset)
600 .limit(page_size),
601 )
602 +
603 alerts = result.scalars().all()
604 alerts_out = []
605 for alert in alerts:
@@ -899,6 +870,54 @@ async def list_cases_by_assigned_to(assigned_to: str, db: AsyncSession) -> List[
870 return cases_out
871
872
873 +async def list_cases_by_asset_name(asset_name: str, db: AsyncSession) -> List[CaseOut]:
874 + result = await db.execute(
875 + select(Case)
876 + .join(CaseAlertLink)
877 + .join(Alert)
878 + .join(Asset)
879 + .where(Asset.asset_name == asset_name)
880 + .options(
881 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.comments),
882 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.assets),
883 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.tags).selectinload(AlertToTag.tag),
884 + ),
885 + )
886 + cases = result.scalars().all()
887 + cases_out = []
888 + for case in cases:
889 + alerts_out = []
890 + for case_alert_link in case.alerts:
891 + alert = case_alert_link.alert
892 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
893 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
894 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
895 + alert_out = AlertOut(
896 + id=alert.id,
897 + alert_creation_time=alert.alert_creation_time,
898 + time_closed=alert.time_closed,
899 + alert_name=alert.alert_name,
900 + alert_description=alert.alert_description,
901 + status=alert.status,
902 + customer_code=alert.customer_code,
903 + source=alert.source,
904 + assigned_to=alert.assigned_to,
905 + comments=comments,
906 + assets=assets,
907 + tags=tags,
908 + )
909 + alerts_out.append(alert_out)
910 + case_out = CaseOut(
911 + id=case.id,
912 + case_name=case.case_name,
913 + case_description=case.case_description,
914 + assigned_to=case.assigned_to,
915 + alerts=alerts_out,
916 + )
917 + cases_out.append(case_out)
918 + return cases_out
919 +
920 +
921 async def get_alert_context_by_id(alert_context_id: int, db: AsyncSession) -> AlertContext:
922 result = await db.execute(select(AlertContext).where(AlertContext.id == alert_context_id))
923 alert_context = result.scalars().first()
@@ -907,8 +926,10 @@ async def get_alert_context_by_id(alert_context_id: int, db: AsyncSession) -> Al
926 return alert_context
927
928
910 -async def list_alerts_by_tag(tag: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
929 +async def list_alerts_by_tag(tag: str, db: AsyncSession, page: int = 1, page_size: int = 25, order: str = "desc") -> List[AlertOut]:
930 offset = (page - 1) * page_size
931 + order_by = asc(Alert.id) if order == "asc" else desc(Alert.id)
932 +
933 result = await db.execute(
934 select(Alert)
935 .join(AlertToTag)
@@ -920,6 +941,7 @@ async def list_alerts_by_tag(tag: str, db: AsyncSession, page: int = 1, page_siz
941 selectinload(Alert.cases),
942 selectinload(Alert.tags).selectinload(AlertToTag.tag),
943 )
944 + .order_by(order_by)
945 .offset(offset)
946 .limit(page_size),
947 )
@@ -947,8 +969,10 @@ async def list_alerts_by_tag(tag: str, db: AsyncSession, page: int = 1, page_siz
969 return alerts_out
970
971
950 -async def list_alert_by_status(status: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
972 +async def list_alert_by_status(status: str, db: AsyncSession, page: int = 1, page_size: int = 25, order: str = "desc") -> List[AlertOut]:
973 offset = (page - 1) * page_size
974 + order_by = asc(Alert.id) if order == "asc" else desc(Alert.id)
975 +
976 result = await db.execute(
977 select(Alert)
978 .where(Alert.status == status)
@@ -958,9 +982,11 @@ async def list_alert_by_status(status: str, db: AsyncSession, page: int = 1, pag
982 selectinload(Alert.cases),
983 selectinload(Alert.tags).selectinload(AlertToTag.tag),
984 )
985 + .order_by(order_by)
986 .offset(offset)
987 .limit(page_size),
988 )
989 +
990 alerts = result.scalars().all()
991 alerts_out = []
992 for alert in alerts:
@@ -985,8 +1011,16 @@ async def list_alert_by_status(status: str, db: AsyncSession, page: int = 1, pag
1011 return alerts_out
1012
1013
988 -async def list_alerts_by_asset_name(asset_name: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
1014 +async def list_alerts_by_asset_name(
1015 + asset_name: str,
1016 + db: AsyncSession,
1017 + page: int = 1,
1018 + page_size: int = 25,
1019 + order: str = "desc",
1020 +) -> List[AlertOut]:
1021 offset = (page - 1) * page_size
1022 + order_by = asc(Alert.id) if order == "asc" else desc(Alert.id)
1023 +
1024 result = await db.execute(
1025 select(Alert)
1026 .join(Asset)
@@ -997,6 +1031,7 @@ async def list_alerts_by_asset_name(asset_name: str, db: AsyncSession, page: int
1031 selectinload(Alert.cases),
1032 selectinload(Alert.tags).selectinload(AlertToTag.tag),
1033 )
1034 + .order_by(order_by)
1035 .offset(offset)
1036 .limit(page_size),
1037 )
@@ -1024,8 +1059,16 @@ async def list_alerts_by_asset_name(asset_name: str, db: AsyncSession, page: int
1059 return alerts_out
1060
1061
1027 -async def list_alert_by_assigned_to(assigned_to: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
1062 +async def list_alert_by_assigned_to(
1063 + assigned_to: str,
1064 + db: AsyncSession,
1065 + page: int = 1,
1066 + page_size: int = 25,
1067 + order: str = "desc",
1068 +) -> List[AlertOut]:
1069 offset = (page - 1) * page_size
1070 + order_by = asc(Alert.id) if order == "asc" else desc(Alert.id)
1071 +
1072 result = await db.execute(
1073 select(Alert)
1074 .where(Alert.assigned_to == assigned_to)
@@ -1035,6 +1078,7 @@ async def list_alert_by_assigned_to(assigned_to: str, db: AsyncSession, page: in
1078 selectinload(Alert.cases),
1079 selectinload(Alert.tags).selectinload(AlertToTag.tag),
1080 )
1081 + .order_by(order_by)
1082 .offset(offset)
1083 .limit(page_size),
1084 )
@@ -1062,8 +1106,16 @@ async def list_alert_by_assigned_to(assigned_to: str, db: AsyncSession, page: in
1106 return alerts_out
1107
1108
1065 -async def list_alerts_by_title(alert_title: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
1109 +async def list_alerts_by_title(
1110 + alert_title: str,
1111 + db: AsyncSession,
1112 + page: int = 1,
1113 + page_size: int = 25,
1114 + order: str = "desc",
1115 +) -> List[AlertOut]:
1116 offset = (page - 1) * page_size
1117 + order_by = asc(Alert.id) if order == "asc" else desc(Alert.id)
1118 +
1119 result = await db.execute(
1120 select(Alert)
1121 .where(Alert.alert_name.like(f"%{alert_title}%"))
@@ -1073,6 +1125,7 @@ async def list_alerts_by_title(alert_title: str, db: AsyncSession, page: int = 1
1125 selectinload(Alert.cases),
1126 selectinload(Alert.tags).selectinload(AlertToTag.tag),
1127 )
1128 + .order_by(order_by)
1129 .offset(offset)
1130 .limit(page_size),
1131 )
backend/app/incidents/services/incident_alert.py
+7 -1
@@ -199,7 +199,12 @@ async def get_customer_code(alert_details: dict):
199 for key in CustomerCodeKeys:
200 logger.info(f"Checking for key {key.value}")
201 if key.value in alert_details:
202 - return alert_details[key.value]
202 + value = alert_details[key.value]
203 + if key == CustomerCodeKeys.CLUSTER_NODE:
204 + processed_value = CustomerCodeKeys.get_processed_value(key, value)
205 + logger.info(f"Processed value for {key.value} is {processed_value}")
206 + return processed_value
207 + return value
208
209 # If none of the keys are present, raise an exception
210 logger.info(f"Failed to fetch customer code. Valid customer code field names are {', '.join([key.value for key in CustomerCodeKeys])}")
@@ -406,6 +411,7 @@ async def handle_customer_notifications(customer_code: str, alert_payload: Creat
411 execution_arguments={
412 "customer_code": customer_code,
413 "alert_context_payload": alert_payload.alert_context_payload,
414 + "alert_title": alert_payload.alert_title_payload,
415 },
416 start="",
417 ),
backend/app/integrations/alert_creation/general/routes/alert.py deleted
-116
@@ -1,116 +0,0 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -from sqlalchemy.future import select
7 -
8 -from app.db.db_session import get_db
9 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
10 -from app.integrations.alert_creation.general.schema.alert import CreateAlertResponse
11 -from app.integrations.alert_creation.general.services.alert import create_alert
12 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
13 - AlertCreationSettings,
14 -)
15 -
16 -general_alerts_router = APIRouter()
17 -
18 -
19 -async def is_rule_id_valid(
20 - create_alert_request: CreateAlertRequest,
21 - session: AsyncSession,
22 -) -> bool:
23 - """
24 - Checks if the given rule ID is valid for the specified customer.
25 -
26 - Args:
27 - create_alert_request (CreateAlertRequest): The request object containing the rule ID and customer information.
28 - session (AsyncSession): The database session.
29 -
30 - Returns:
31 - bool: True if the rule ID is valid for the customer, False otherwise.
32 - """
33 - logger.info(
34 - f"Checking if rule_id: {create_alert_request.rule_id} is valid for customer: {create_alert_request.agent_labels_customer}",
35 - )
36 -
37 - result = await session.execute(
38 - select(AlertCreationSettings).where(
39 - AlertCreationSettings.customer_code == create_alert_request.agent_labels_customer,
40 - ),
41 - )
42 - settings = result.scalars().first()
43 -
44 - if settings and str(create_alert_request.rule_id) in (settings.excluded_wazuh_rules or "").split(","):
45 - return False
46 -
47 - return True
48 -
49 -
50 -async def is_customer_code_valid(
51 - create_alert_request: CreateAlertRequest,
52 - session: AsyncSession,
53 -) -> bool:
54 - """
55 - Checks if the customer code provided in the create_alert_request is valid.
56 -
57 - Args:
58 - create_alert_request (CreateAlertRequest): The request object containing the alert creation details.
59 - session (AsyncSession): The database session.
60 -
61 - Returns:
62 - bool: True if the customer code is valid, False otherwise.
63 - """
64 - logger.info(
65 - f"Checking if customer_code: {create_alert_request.agent_labels_customer} is valid.",
66 - )
67 -
68 - result = await session.execute(
69 - select(AlertCreationSettings).where(
70 - AlertCreationSettings.customer_code == create_alert_request.agent_labels_customer,
71 - ),
72 - )
73 - settings = result.scalars().first()
74 -
75 - if settings:
76 - return True
77 -
78 - return False
79 -
80 -
81 -@general_alerts_router.post(
82 - "",
83 - response_model=CreateAlertResponse,
84 - description="Create a general alert in IRIS.",
85 -)
86 -async def create_general_alert(
87 - create_alert_request: CreateAlertRequest,
88 - session: AsyncSession = Depends(get_db),
89 -):
90 - """
91 - Create a general alert in IRIS. This route is to be used with Praeco.
92 -
93 - Args:
94 - create_alert_request (CreateAlertRequest): The request payload for creating the alert.
95 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
96 -
97 - Raises:
98 - HTTPException: If the customer code or rule ID is invalid.
99 -
100 - Returns:
101 - CreateAlertResponse: The response containing the created alert.
102 - """
103 - logger.info(f"create_alert_request: {create_alert_request.dict()}")
104 -
105 - if await is_customer_code_valid(create_alert_request, session) is False:
106 - logger.info(
107 - f"Invalid customer_code: {create_alert_request.agent_labels_customer}",
108 - )
109 - raise HTTPException(status_code=200, detail="Invalid customer_code.")
110 -
111 - if await is_rule_id_valid(create_alert_request, session) is False:
112 - logger.info(f"Invalid rule_id: {create_alert_request.rule_id}")
113 - raise HTTPException(status_code=200, detail="Invalid rule_id.")
114 -
115 - logger.info(f"Rule id is valid: {create_alert_request.rule_id}")
116 - return await create_alert(create_alert_request, session=session)
backend/app/integrations/alert_creation/general/schema/alert.py deleted
-338
@@ -1,338 +0,0 @@
1 -from enum import Enum
2 -from typing import Any
3 -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
9 -from pydantic import Field
10 -
11 -
12 -class ValidIocFields(Enum):
13 - MISP_VALUE = "misp_value"
14 - OPENCTI_VALUE = "opencti_value"
15 - THREAT_INTEL_VALUE = "threat_intel_value"
16 -
17 -
18 -class RawGenericSourceModel(BaseModel):
19 - timestamp: str = Field(..., description="The timestamp of the alert.")
20 - timestamp_utc: Optional[str] = Field(
21 - ...,
22 - description="The UTC timestamp of the alert.",
23 - )
24 - rule_description: Optional[str] = Field(
25 - "No autogenerated rule_description found",
26 - description="The timefield of the alert to be used when creating the IRIS alert.",
27 - )
28 - syslog_level: Optional[str] = Field(
29 - "No autogenerated syslog_level found",
30 - description="The timefield of the alert to be used when creating the IRIS alert.",
31 - )
32 -
33 - class Config:
34 - extra = Extra.allow
35 -
36 - def to_dict(self):
37 - return self.dict(exclude_none=True)
38 -
39 -
40 -class CreateAlertRequest(BaseModel):
41 - index: str = Field(
42 - ...,
43 - alias="_index",
44 - ) # Needing to alias these fields because they are reserved words in Python
45 - id: str = Field(
46 - ...,
47 - alias="_id",
48 - ) # Needing to alias these fields because they are reserved words in Python
49 - agent_name: str = Field(..., description="The name of the agent.")
50 - agent_ip: str = Field(
51 - ...,
52 - description="IP address of the agent that triggered the alert",
53 - example="1.1.1.1",
54 - )
55 - agent_id: str = Field(..., description="The id of the agent.")
56 - agent_labels_customer: str = Field(..., description="The customer of the agent.")
57 - rule_id: str = Field(..., description="The id of the rule.")
58 - rule_level: int = Field(..., description="The level of the rule.")
59 - rule_description: str = Field(..., description="The description of the rule.")
60 - timestamp: str = Field(..., description="The timestamp of the alert.")
61 - timestamp_utc: Optional[str] = Field(
62 - ...,
63 - description="The UTC timestamp of the alert.",
64 - )
65 - time_field: Optional[str] = Field(
66 - "timestamp",
67 - description="The timefield of the alert to be used when creating the IRIS alert.",
68 - )
69 - asset_type_id: Optional[int] = Field(
70 - 9,
71 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
72 - )
73 - ioc_value: Optional[str] = Field(
74 - None,
75 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
76 - )
77 - ioc_type: Optional[str] = Field(
78 - None,
79 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
80 - )
81 - _source: RawGenericSourceModel
82 -
83 - class Config:
84 - allow_population_by_field_name = True
85 - extra = Extra.allow
86 -
87 - def to_dict(self):
88 - return self.dict(exclude_none=True)
89 -
90 -
91 -class CreateAlertResponse(BaseModel):
92 - success: bool
93 - message: str
94 - alert_id: int = Field(..., description="The alert id as created in IRIS.")
95 - customer: str = Field(..., description="The customer name.")
96 - alert_source_link: str = Field(
97 - ...,
98 - description="The link to the alert within Grafana.",
99 - )
100 -
101 -
102 -class GenericSourceModel(BaseModel):
103 - agent_name: str = Field(..., description="The name of the agent.")
104 - agent_id: str = Field(..., description="The id of the agent.")
105 - agent_labels_customer: str = Field(..., description="The customer of the agent.")
106 - rule_id: str = Field(..., description="The id of the rule.")
107 - rule_level: int = Field(..., description="The level of the rule.")
108 - rule_description: str = Field(..., description="The description of the rule.")
109 - timestamp: str = Field(..., description="The timestamp of the alert.")
110 - timestamp_utc: Optional[str] = Field(
111 - None,
112 - description="The UTC timestamp of the alert.",
113 - )
114 -
115 - class Config:
116 - extra = Extra.allow
117 -
118 -
119 -class GenericAlertModel(BaseModel):
120 - _index: str
121 - _id: str
122 - _version: int
123 - _source: GenericSourceModel
124 - asset_type_id: Optional[int] = Field(
125 - None,
126 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
127 - )
128 - ioc_value: Optional[str] = Field(
129 - None,
130 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
131 - )
132 - ioc_type: Optional[str] = Field(
133 - None,
134 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
135 - )
136 -
137 - class Config:
138 - extra = Extra.allow
139 -
140 -
141 -# Sample data from `get_single_alert_details`
142 -sample_data = {
143 - "_index": "some_index",
144 - "_id": "some_id",
145 - "_version": 1,
146 - "_source": {
147 - "agent_name": "some_agent_name",
148 - "agent_id": "some_agent_id",
149 - # ... other fields
150 - },
151 - # ... other fields
152 -}
153 -
154 -
155 -########### ! Create Alerts Schemas ! ###########
156 -class IrisAsset(BaseModel):
157 - asset_name: Optional[str] = Field(
158 - "Asset Not Found. Verify Wazuh Manager API is Running",
159 - description="Name of the asset",
160 - example="Server01",
161 - )
162 - asset_ip: Optional[str] = Field(
163 - "Asset IP Not Found. Verify Wazuh Manager API is Running",
164 - description="IP address of the asset",
165 - example="192.168.1.1",
166 - )
167 - asset_description: Optional[str] = Field(
168 - "Asset Not Found. Verify Wazuh Manager API is Running",
169 - description="Description of the asset",
170 - example="Windows Server",
171 - )
172 - asset_type_id: Optional[int] = Field(
173 - 9,
174 - description="Type ID of the asset",
175 - example=1,
176 - )
177 - asset_tags: Optional[str] = Field(
178 - "Agent ID not found. Ensure the agent has been registered with Wazuh Manager and synced to the Agents table.",
179 - description="Tags of the asset",
180 - example="001",
181 - )
182 -
183 - def to_dict(self):
184 - return self.dict(exclude_none=True)
185 -
186 -
187 -class IrisIoc(BaseModel):
188 - ioc_value: str = Field(
189 - ...,
190 - description="Value of the IoC",
191 - example="www.google.com",
192 - )
193 - ioc_description: str = Field(
194 - ...,
195 - description="Description of the IoC",
196 - example="Google",
197 - )
198 - ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
199 - ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
200 -
201 - def to_dict(self):
202 - return self.dict(exclude_none=True)
203 -
204 -
205 -class IrisTags(BaseModel):
206 - rule_id: str = Field(..., description="Rule ID from the alert", example="001")
207 -
208 - def to_dict(self):
209 - return self.dict(exclude_none=True)
210 -
211 -
212 -class IrisAlertContext(BaseModel):
213 - customer_iris_id: int = Field(
214 - ...,
215 - description="IRIS ID of the customer",
216 - example=1,
217 - )
218 - customer_name: str = Field(
219 - ...,
220 - description="Name of the customer",
221 - example="SOCFortress",
222 - )
223 - customer_cases_index: str = Field(
224 - ...,
225 - description="IRIS case index name in the Wazuh-Indexer",
226 - example="dfir_iris_00001",
227 - )
228 - alert_id: str = Field(..., description="ID of the alert", example="123")
229 - alert_name: str = Field(
230 - ...,
231 - description="Name of the alert",
232 - example="Intrusion Detected",
233 - )
234 - alert_level: int = Field(..., description="Severity level of the alert", example=3)
235 - rule_id: str = Field(
236 - ...,
237 - description="ID of the rule that triggered the alert",
238 - example="2001",
239 - )
240 - asset_name: str = Field(
241 - ...,
242 - description="Name of the affected asset",
243 - example="Server01",
244 - )
245 - asset_ip: str = Field(
246 - ...,
247 - description="IP address of the affected asset",
248 - example="192.168.1.1",
249 - )
250 - asset_type: int = Field(..., description="Type ID of the affected asset", example=1)
251 - process_id: Optional[str] = Field(
252 - "No process ID found",
253 - description="Process ID involved in the alert",
254 - example="4567",
255 - )
256 - rule_mitre_id: Optional[str] = Field(
257 - "n/a",
258 - description="MITRE ATT&CK ID of the rule",
259 - example="T1234",
260 - )
261 - rule_mitre_tactic: Optional[str] = Field(
262 - "n/a",
263 - description="MITRE ATT&CK Tactic",
264 - example="Execution",
265 - )
266 - rule_mitre_technique: Optional[str] = Field(
267 - "n/a",
268 - description="MITRE ATT&CK Technique",
269 - example="Scripting",
270 - )
271 -
272 -
273 -class IrisAlertPayload(BaseModel):
274 - alert_title: str = Field(
275 - ...,
276 - description="Title of the alert",
277 - example="Intrusion Detected",
278 - )
279 - alert_description: str = Field(
280 - ...,
281 - description="Description of the alert",
282 - example="Intrusion Detected by Firewall",
283 - )
284 - alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
285 - assets: List[IrisAsset] = Field(..., description="List of affected assets")
286 - alert_source_link: str = Field(
287 - ...,
288 - description="Link to the alert within Grafana",
289 - example="https://grafana.com",
290 - )
291 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
292 - alert_severity_id: int = Field(
293 - ...,
294 - description="Severity ID of the alert",
295 - example=5,
296 - )
297 - alert_customer_id: int = Field(
298 - ...,
299 - description="Customer ID related to the alert",
300 - example=1,
301 - )
302 - alert_source_content: Dict[str, Any] = Field(
303 - ...,
304 - description="Original content from the alert source",
305 - )
306 - alert_context: IrisAlertContext = Field(
307 - ...,
308 - description="Contextual information about the alert",
309 - )
310 - alert_iocs: Optional[List[IrisIoc]] = Field(
311 - None,
312 - description="List of IoCs related to the alert",
313 - )
314 - alert_source_event_time: str = Field(
315 - ...,
316 - description="Timestamp of the alert",
317 - example="2021-01-01T00:00:00.000Z",
318 - )
319 -
320 - def to_dict(self):
321 - return self.dict(exclude_none=True)
322 -
323 -
324 -########### ! Send to Shuffle Schema ! ###########
325 -class ShuffleAlertPayload(BaseModel):
326 - alert_title: str = Field(
327 - ...,
328 - description="Title of the alert",
329 - example="Intrusion Detected",
330 - )
331 - alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
332 - asset_name: Optional[str] = Field(None, description="Name of the affected asset")
333 - alert_id: int = Field(..., description="Alert ID as created in IRIS")
334 - customer: str = Field(..., description="Customer name")
335 - alert_link: str = Field(..., description="Link to the alert in IRIS")
336 -
337 - def to_dict(self):
338 - return self.dict(exclude_none=True)
backend/app/integrations/alert_creation/general/services/alert.py deleted
-350
@@ -1,350 +0,0 @@
1 -from typing import Optional
2 -from typing import Set
3 -
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -
8 -from app.agents.routes.agents import get_agent
9 -from app.agents.schema.agents import AgentsResponse
10 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
11 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
12 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
13 -from app.integrations.alert_creation.general.schema.alert import CreateAlertResponse
14 -from app.integrations.alert_creation.general.schema.alert import IrisAlertContext
15 -from app.integrations.alert_creation.general.schema.alert import IrisAlertPayload
16 -from app.integrations.alert_creation.general.schema.alert import IrisAsset
17 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
18 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
19 -from app.integrations.alert_creation.general.services.alert_multi_exclude import (
20 - AlertDetailsService,
21 -)
22 -from app.integrations.utils.alerts import get_asset_type_id
23 -from app.integrations.utils.alerts import send_to_shuffle
24 -from app.integrations.utils.alerts import validate_ioc_type
25 -from app.integrations.utils.schema import ShufflePayload
26 -from app.utils import get_customer_alert_settings
27 -
28 -
29 -def valid_ioc_fields() -> Set[str]:
30 - """
31 - Getter for the set of valid IoC fields.
32 - Returns
33 - -------
34 - Set[str]
35 - The set of valid IoC fields.
36 - """
37 - return {field.value for field in ValidIocFields}
38 -
39 -
40 -async def construct_alert_source_link(
41 - alert_details: CreateAlertRequest,
42 - session: AsyncSession,
43 -) -> str:
44 - """
45 - Construct the alert source link for the alert details.
46 - Parameters
47 - ----------
48 - alert_details: CreateAlertRequest
49 - The alert details.
50 - Returns
51 - -------
52 - str
53 - The alert source link.
54 - """
55 - # Check if the alert has a process id and that it is not "No process ID found"
56 - if hasattr(alert_details, "process_id") and alert_details.process_id != "No process ID found":
57 - query_string = f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
58 - else:
59 - query_string = f"%22query%22:%22_id:%5C%22{alert_details.id}%5C%22%20AND%20"
60 -
61 - grafana_url = (
62 - await get_customer_alert_settings(
63 - customer_code=alert_details.agent_labels_customer,
64 - session=session,
65 - )
66 - ).grafana_url
67 -
68 - return (
69 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
70 - f"{query_string}"
71 - f"agent_name:%5C%22{alert_details.agent_name}%5C%22%22,"
72 - "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
73 - "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
74 - )
75 -
76 -
77 -async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
78 - """
79 - Builds an IoC payload based on the provided alert details.
80 -
81 - Args:
82 - alert_details (CreateAlertRequest): The details of the alert.
83 -
84 - Returns:
85 - Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
86 - """
87 - for field in valid_ioc_fields():
88 - if hasattr(alert_details, field):
89 - ioc_value = getattr(alert_details, field)
90 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
91 - return IrisIoc(
92 - ioc_value=ioc_value,
93 - ioc_description="IoC found in alert",
94 - ioc_tlp_id=1,
95 - ioc_type_id=ioc_type,
96 - )
97 - return None
98 -
99 -
100 -async def build_asset_payload(agent_data: AgentsResponse, alert_details) -> IrisAsset:
101 - """
102 - Build the payload for an IrisAsset object based on the agent data and alert details.
103 -
104 - Args:
105 - agent_data (AgentsResponse): The response containing agent data.
106 - alert_details: The details of the alert.
107 -
108 - Returns:
109 - IrisAsset: The constructed IrisAsset object.
110 - """
111 - # Get the agent_id based on the hostname from the Agents table
112 - if agent_data.success:
113 - return IrisAsset(
114 - asset_name=agent_data.agents[0].hostname,
115 - asset_ip=agent_data.agents[0].ip_address,
116 - asset_description=agent_data.agents[0].os,
117 - asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
118 - asset_tags=f"agent_id:{agent_data.agents[0].agent_id}",
119 - )
120 - return IrisAsset()
121 -
122 -
123 -async def build_alert_context_payload(
124 - alert_details: CreateAlertRequest,
125 - agent_data: AgentsResponse,
126 - session: AsyncSession,
127 -) -> IrisAlertContext:
128 - """
129 - Builds the payload for the alert context.
130 -
131 - Args:
132 - alert_details (CreateAlertRequest): The details of the alert.
133 - agent_data (AgentsResponse): The agent data.
134 - session (AsyncSession): The async session.
135 -
136 - Returns:
137 - IrisAlertContext: The built alert context payload.
138 - """
139 - return IrisAlertContext(
140 - customer_iris_id=(
141 - await get_customer_alert_settings(
142 - customer_code=alert_details.agent_labels_customer,
143 - session=session,
144 - )
145 - ).iris_customer_id,
146 - customer_name=(
147 - await get_customer_alert_settings(
148 - customer_code=alert_details.agent_labels_customer,
149 - session=session,
150 - )
151 - ).customer_name,
152 - customer_cases_index=(
153 - await get_customer_alert_settings(
154 - customer_code=alert_details.agent_labels_customer,
155 - session=session,
156 - )
157 - ).iris_index,
158 - alert_id=alert_details.id,
159 - alert_name=alert_details.rule_description,
160 - alert_level=alert_details.rule_level,
161 - rule_id=alert_details.rule_id,
162 - asset_name=alert_details.agent_name,
163 - asset_ip=alert_details.agent_ip,
164 - asset_type=await get_asset_type_id(agent_data.agents[0].os),
165 - process_id=getattr(alert_details, "process_id", "No process id found"),
166 - rule_mitre_id=getattr(alert_details, "rule_mitre_id", "No rule mitre id found"),
167 - rule_mitre_tactic=getattr(
168 - alert_details,
169 - "rule_mitre_tactic",
170 - "No rule mitre tactic found",
171 - ),
172 - rule_mitre_technique=getattr(
173 - alert_details,
174 - "rule_mitre_technique",
175 - "No rule mitre technique found",
176 - ),
177 - )
178 -
179 -
180 -async def build_alert_payload(
181 - alert_details: CreateAlertRequest,
182 - agent_data,
183 - ioc_payload: Optional[IrisIoc],
184 - session: AsyncSession,
185 -) -> IrisAlertPayload:
186 - """
187 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
188 -
189 - Args:
190 - alert_details (CreateAlertRequest): The details of the alert.
191 - agent_data: The agent data associated with the alert.
192 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
193 - session (AsyncSession): The session used for database operations.
194 -
195 - Returns:
196 - IrisAlertPayload: The built alert payload.
197 - """
198 - asset_payload = await build_asset_payload(agent_data, alert_details)
199 - context_payload = await build_alert_context_payload(
200 - alert_details=alert_details,
201 - agent_data=agent_data,
202 - session=session,
203 - )
204 - timefield = (
205 - await get_customer_alert_settings(
206 - customer_code=alert_details.agent_labels_customer,
207 - session=session,
208 - )
209 - ).timefield
210 - # Get the timefield value from the alert_details
211 - if hasattr(alert_details, timefield):
212 - alert_details.time_field = getattr(alert_details, timefield)
213 - logger.info(f"Alert has context: {context_payload}")
214 - if ioc_payload:
215 - logger.info(f"Alert has IoC: {ioc_payload}")
216 - return IrisAlertPayload(
217 - alert_title=alert_details.rule_description,
218 - alert_source_link=await construct_alert_source_link(
219 - alert_details,
220 - session=session,
221 - ),
222 - alert_description=alert_details.rule_description,
223 - alert_source="CoPilot",
224 - assets=[asset_payload],
225 - alert_status_id=3,
226 - alert_severity_id=5,
227 - alert_customer_id=(
228 - await get_customer_alert_settings(
229 - customer_code=alert_details.agent_labels_customer,
230 - session=session,
231 - )
232 - ).iris_customer_id,
233 - alert_source_content=alert_details.to_dict(),
234 - alert_context=context_payload,
235 - alert_iocs=[ioc_payload],
236 - alert_source_event_time=alert_details.time_field,
237 - )
238 - else:
239 - logger.info("Alert does not have IoC")
240 - return IrisAlertPayload(
241 - alert_title=alert_details.rule_description,
242 - alert_source_link=await construct_alert_source_link(
243 - alert_details,
244 - session=session,
245 - ),
246 - alert_description=alert_details.rule_description,
247 - alert_source="SOCFORTRESS RULE",
248 - assets=[asset_payload],
249 - alert_status_id=3,
250 - alert_severity_id=5,
251 - alert_customer_id=(
252 - await get_customer_alert_settings(
253 - customer_code=alert_details.agent_labels_customer,
254 - session=session,
255 - )
256 - ).iris_customer_id,
257 - alert_source_content=alert_details.to_dict(),
258 - alert_context=context_payload,
259 - alert_source_event_time=alert_details.time_field,
260 - )
261 -
262 -
263 -async def create_alert(
264 - alert: CreateAlertRequest,
265 - session: AsyncSession,
266 -) -> CreateAlertResponse:
267 - """
268 - Creates an alert in IRIS.
269 -
270 - Args:
271 - alert (CreateAlertRequest): The alert details.
272 - session (AsyncSession): The database session.
273 -
274 - Returns:
275 - CreateAlertResponse: The response containing the alert ID and other details.
276 - """
277 - logger.info(f"Creating alert with {alert.id} in IRIS.")
278 - alert_detail_service = await AlertDetailsService.create()
279 - event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
280 - agent_name=alert.agent_name,
281 - process_id=getattr(alert, "process_id", "n/a"),
282 - index=alert.index,
283 - session=session,
284 - )
285 - if event_exclude_result is True:
286 - raise HTTPException(
287 - status_code=400,
288 - detail="Alert excluded due to multi exclusion as set in the config.ini file.",
289 - )
290 - logger.info(f"Getting agent data for {alert.agent_name}")
291 - agent_details = await get_agent(agent_id=alert.agent_id, db=session)
292 - ioc_payload = await build_ioc_payload(alert_details=alert)
293 - iris_alert_payload = await build_alert_payload(
294 - alert_details=alert,
295 - agent_data=agent_details,
296 - ioc_payload=ioc_payload,
297 - session=session,
298 - )
299 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
300 - result = await fetch_and_validate_data(
301 - client,
302 - alert_client.add_alert,
303 - iris_alert_payload.to_dict(),
304 - )
305 - alert_id = result["data"]["alert_id"]
306 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
307 - # Update the alert with the asset payload
308 - await fetch_and_validate_data(
309 - client,
310 - alert_client.update_alert,
311 - alert_id,
312 - {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
313 - )
314 - # Updae the alert if the ioc_payload is not None
315 - if ioc_payload:
316 - await fetch_and_validate_data(
317 - client,
318 - alert_client.update_alert,
319 - alert_id,
320 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
321 - )
322 - customer_name = (
323 - await get_customer_alert_settings(
324 - customer_code=alert.agent_labels_customer,
325 - session=session,
326 - )
327 - ).customer_name
328 - await send_to_shuffle(
329 - ShufflePayload(
330 - alert_id=alert_id,
331 - customer=customer_name,
332 - customer_code=alert.agent_labels_customer,
333 - alert_source_link=await construct_alert_source_link(alert, session=session),
334 - rule_description=alert.rule_description,
335 - hostname=alert.agent_name,
336 - ),
337 - session=session,
338 - )
339 - return CreateAlertResponse(
340 - alert_id=alert_id,
341 - customer=(
342 - await get_customer_alert_settings(
343 - customer_code=alert.agent_labels_customer,
344 - session=session,
345 - )
346 - ).customer_name,
347 - alert_source_link=await construct_alert_source_link(alert, session=session),
348 - success=True,
349 - message=f"Successfully created alert {alert_id} in IRIS.",
350 - )
backend/app/integrations/alert_creation/general/services/alert_multi_exclude.py deleted
-269
@@ -1,269 +0,0 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import List
4 -from typing import Tuple
5 -
6 -from elasticsearch7 import NotFoundError
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -
10 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
11 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
12 - AlertCreationEventConfig,
13 -)
14 -from app.utils import get_customer_alert_event_configs
15 -
16 -
17 -class AlertDetailsService:
18 - """
19 - Service for handling alert lookup tasks in the Wazuh Indexer.
20 -
21 - Attributes:
22 - es: Elasticsearch object for accessing the Wazuh Indexer.
23 - config_manager: ConfigManager object for accessing the configuration file.
24 - """
25 -
26 - def __init__(self):
27 - self.es = None
28 -
29 - @classmethod
30 - async def create(cls):
31 - """
32 - Asynchronously create an instance of AlertDetailsService.
33 -
34 - Establish a session with the Wazuh indexer and a ConfigManager instance for reading the configuration file.
35 - """
36 - self = cls()
37 - self.es = await create_wazuh_indexer_client("Wazuh-Indexer")
38 - return self
39 -
40 - def _collect_indices(self) -> Dict[str, object]:
41 - """
42 - Collect the indices from the Elasticsearch cluster.
43 -
44 - Returns:
45 - dict: A dictionary containing the indices and their properties.
46 - """
47 - try:
48 - logger.info("Collecting indices but only return the index name.")
49 - indices = self.es.cat.indices(format="json")
50 - return [index["index"] for index in indices]
51 - except Exception as e:
52 - logger.error(f"Error collecting indices: {e}")
53 - return {}
54 -
55 - def search_alerts_with_syslog_level(self) -> List[Tuple[str, str]]:
56 - """
57 - Search all indexes and build a list of (index, id) pairs where the syslog_level field has a value of 'ALERT'.
58 -
59 - Returns:
60 - List[Tuple[str, str]]: A list of (index, id) pairs where the syslog_level field has a value of 'ALERT' within the last 1 hour.
61 - """
62 - try:
63 - logger.info("Searching for alerts with syslog_level of 'ALERT'.")
64 -
65 - # Build the query to search for syslog_level of 'ALERT' within the last 1 hour
66 - query = self.build_query(terms={"syslog_level": "ALERT"}, hours=1)
67 -
68 - # Search across all indexes
69 - result = self.es.search(index="_all", body=query)
70 -
71 - # Extract (index, id) pairs from the result
72 - index_id_pairs = [(hit["_index"], hit["_id"]) for hit in result["hits"]["hits"]]
73 - logger.info(
74 - f"Found {len(index_id_pairs)} alerts with syslog_level of 'ALERT' within the last 1 hour.",
75 - )
76 - return index_id_pairs
77 - except Exception as e:
78 - logger.error(
79 - f"Error searching for alerts with syslog_level of 'ALERT': {e}",
80 - )
81 - return []
82 -
83 - # collect the alert details via the index and id
84 - def alert_details(self, index: str, id: str) -> Dict[str, Any]:
85 - """
86 - Collect the alert details via the index and id.
87 -
88 - Args:
89 - index (str): The name of the Elasticsearch index to retrieve data from.
90 - id (str): The ID of the alert in the Elasticsearch index.
91 -
92 - Returns:
93 - dict: The Elasticsearch document matching the index and ID, or None if an error occurred.
94 - """
95 - try:
96 - logger.info("Collecting alert details.")
97 - return self.es.get(index=index, id=id)
98 - except Exception as e:
99 - logger.error(f"Error collecting alert details: {e}")
100 - return None
101 -
102 - def alert_details_wildcard(self, index: str, id: str) -> Dict[str, Any]:
103 - """
104 - Collect the alert details via a wildcard index search and provided id. I.E `mimecast_test*`.
105 -
106 - Args:
107 - index (str): The name of the Elasticsearch index to retrieve data from.
108 - id (str): The ID of the alert in the Elasticsearch index.
109 -
110 - Returns:
111 - dict: The Elasticsearch document matching the index and ID, or None if an error occurred.
112 - """
113 - # Collect the indices
114 - indices = self._collect_indices()
115 -
116 - # Loop through the indices that match the beginning of the index
117 - try:
118 - for index_name in indices:
119 - if index_name.startswith(index):
120 - try:
121 - logger.info(
122 - f"Collecting alert details from index: {index_name}",
123 - )
124 - # If a document is found, return it, otherwise continue to the next index
125 - return self.es.get(index=index_name, id=id)
126 - except NotFoundError:
127 - continue
128 - return None
129 - except Exception as e:
130 - logger.error(f"Error collecting alert details: {e}")
131 - return None
132 -
133 - def build_query(self, terms: Dict[str, str], hours: int = 1):
134 - """
135 - Build the query for alert timeline events.
136 -
137 - Args:
138 - terms (dict): A dictionary of field-value pairs to search for.
139 - hours (int): The time range in hours for the search.
140 -
141 - Returns:
142 - dict: An Elasticsearch query that searches for documents matching the terms within the specified time range.
143 - """
144 - must_terms = [{"term": {field: value}} for field, value in terms.items()]
145 - must_terms.append(
146 - {
147 - "range": {
148 - "timestamp": {
149 - "gte": f"now-{hours}h",
150 - },
151 - },
152 - },
153 - )
154 -
155 - return {
156 - "size": 10000,
157 - "query": {"bool": {"must": must_terms}},
158 - }
159 -
160 - async def process_events(
161 - self,
162 - events: list,
163 - event_configs: List[AlertCreationEventConfig],
164 - ):
165 - """
166 - Process the events and check for exclusions.
167 - For every event in the `config.ini` file there is a field and value to check for.
168 - If the first event is found, the second event is checked for.
169 - If the second event is found, the alert is excluded.
170 -
171 - Args:
172 - events (list): A list of events to process.
173 - event_configs (List[AlertCreationEventConfig]): A list of AlertCreationEventConfig instances.
174 -
175 - Returns:
176 - dict: A dictionary with a single key 'excluded' indicating whether the events match the exclusion criteria.
177 - """
178 - event_order = [config.event_id for config in event_configs]
179 - logger.info(f"Events: {events}")
180 -
181 - first_match_found = False
182 -
183 - for index, event in enumerate(events):
184 - event_id = event_order[0 if not first_match_found else 1]
185 - logger.info(f"Checking for event_id: {event_id}")
186 - event_config = next(
187 - (config for config in event_configs if config.event_id == event_id),
188 - None,
189 - )
190 - if event_config is None:
191 - continue
192 -
193 - field = event_config.field
194 - value = event_config.value
195 - logger.info(f"Checking for {field} containing {value}")
196 -
197 - if value == event.get(field, ""):
198 - logger.info(f"Event with {field} containing {value} found.")
199 - if not first_match_found:
200 - first_match_found = True # We found the first match
201 - elif first_match_found:
202 - logger.info("Both matches found.")
203 - return True # Both matches found, so return early
204 -
205 - # If we've checked all events and didn't find both matches, return {"excluded": False}
206 - return False
207 -
208 - async def collect_alert_timeline_process_id(
209 - self,
210 - agent_name: str,
211 - process_id: str,
212 - index: str,
213 - session: AsyncSession,
214 - ) -> Dict[str, Any]:
215 - """
216 - Collect the events where the process id and agent name match within a 24 hour window.
217 - This function is used to exclude an event where correlating events are found within a 24 hour window.
218 -
219 - Args:
220 - agent_name (str): The name of the agent.
221 - process_id (str): The ID of the process.
222 - index (str): The name of the Elasticsearch index to retrieve data from.
223 -
224 - Returns:
225 - dict: A dictionary containing the results of the event processing for each order key, or None if an error occurred.
226 - """
227 - try:
228 - logger.info(
229 - f"Collecting alert timeline events for Agent name: {agent_name}, Process id: {process_id}, Index: {index}",
230 - )
231 -
232 - query = self.build_query(
233 - {"agent_name": agent_name, "process_id": process_id},
234 - )
235 - logger.info(f"Query: {query}")
236 - alert_timeline_events = self.es.search(index=index, body=query)
237 -
238 - total_hits = alert_timeline_events["hits"]["total"]["value"]
239 - logger.info(f"Total alert timeline hits: {total_hits}")
240 -
241 - # Build and sort the list of events
242 - events = [event["_source"] for event in alert_timeline_events["hits"]["hits"]]
243 - events.sort(key=lambda x: x["timestamp_utc"])
244 -
245 - # return self.process_events(events)
246 - logger.info(f"Events: {events}")
247 -
248 - # Get all order keys from the 'Order' section in config.ini
249 - order_keys = await get_customer_alert_event_configs(
250 - customer_code=events[0]["agent_labels_customer"],
251 - session=session,
252 - )
253 - logger.info(f"Order keys: {order_keys}")
254 -
255 - # Process events for each order key
256 - results = {}
257 - for order_key in order_keys:
258 - logger.info(f"Processing events for order key: {order_key}")
259 - # results[order_key] = await self.process_events(events, order_key)
260 - results = await self.process_events(events, order_key)
261 - logger.info(f"Results: {results}")
262 - # if results[order_key]["excluded"] is True:
263 - if results is True:
264 - return True
265 -
266 - return False
267 - except Exception as e:
268 - logger.error(f"Error collecting alert timeline events: {e}")
269 - return None
backend/app/integrations/alert_creation/office365/routes/alert.py deleted
-107
@@ -1,107 +0,0 @@
1 -# from app.alerts.office365.services.threat_intel import create_threat_intel_alert
2 -from fastapi import APIRouter
3 -from fastapi import Depends
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -from sqlalchemy import select
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
9 -from app.db.db_session import get_db
10 -from app.integrations.alert_creation.office365.schema.exchange import (
11 - Office365ExchangeAlertBase,
12 -)
13 -from app.integrations.alert_creation.office365.schema.exchange import (
14 - Office365ExchangeAlertRequest,
15 -)
16 -from app.integrations.alert_creation.office365.schema.exchange import (
17 - Office365ExchangeAlertResponse,
18 -)
19 -from app.integrations.alert_creation.office365.schema.exchange import (
20 - ValidOffice365Workloads,
21 -)
22 -from app.integrations.alert_creation.office365.schema.threat_intel import (
23 - Office365ThreatIntelAlertRequest,
24 -)
25 -from app.integrations.alert_creation.office365.schema.threat_intel import (
26 - Office365ThreatIntelAlertResponse,
27 -)
28 -from app.integrations.alert_creation.office365.services.exchange import (
29 - create_exchange_alert,
30 -)
31 -from app.integrations.alert_creation.office365.services.threat_intel import (
32 - create_threat_intel_alert,
33 -)
34 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
35 - AlertCreationSettings,
36 -)
37 -
38 -office365_alerts_router = APIRouter()
39 -
40 -
41 -async def is_office365_organization_id_valid(
42 - create_alert_request: Office365ExchangeAlertBase,
43 - session: AsyncSession,
44 -) -> bool:
45 - """
46 - Checks if the given organization ID is valid for the specified customer.
47 -
48 - Args:
49 - create_alert_request (Office365ExchangeAlertRequest): The request object containing the organization ID and customer information.
50 - session (AsyncSession): The database session.
51 -
52 - Returns:
53 - bool: True if the organization ID is valid for the customer, False otherwise.
54 - """
55 - logger.info(
56 - f"Checking if organization_id: {create_alert_request.data_office365_OrganizationId} is valid for customer: {create_alert_request.data_office365_OrganizationId}",
57 - )
58 -
59 - result = await session.execute(
60 - select(AlertCreationSettings).where(
61 - AlertCreationSettings.office365_organization_id == create_alert_request.data_office365_OrganizationId,
62 - ),
63 - )
64 - settings = result.scalars().first()
65 - if settings is None:
66 - raise HTTPException(
67 - status_code=400,
68 - detail="Office365 organization ID is not valid, make sure to provision the customer.",
69 - )
70 -
71 - return True
72 -
73 -
74 -@office365_alerts_router.post(
75 - "/exchange",
76 - response_model=Office365ExchangeAlertResponse,
77 - description="Create an office365 exchange alert in IRIS.",
78 -)
79 -async def create_office365_exchange_alert(
80 - create_alert_request: Office365ExchangeAlertRequest,
81 - session: AsyncSession = Depends(get_db),
82 -):
83 - logger.info(f"create_alert_request: {create_alert_request}")
84 - if create_alert_request.data_office365_Workload not in [workload.value for workload in ValidOffice365Workloads]:
85 - logger.info(f"Invalid workload: {create_alert_request.data_office365_Workload}")
86 - raise HTTPException(status_code=400, detail="Invalid workload")
87 - logger.info(f"Workload is valid: {create_alert_request.data_office365_Workload}")
88 - await is_office365_organization_id_valid(create_alert_request, session)
89 - return await create_exchange_alert(alert=create_alert_request, session=session)
90 -
91 -
92 -@office365_alerts_router.post(
93 - "/threat_intel",
94 - response_model=Office365ThreatIntelAlertResponse,
95 - description="Create an office365 threat intel alert in IRIS.",
96 -)
97 -async def create_office365_threat_intel_alert(
98 - create_alert_request: Office365ThreatIntelAlertRequest,
99 - session: AsyncSession = Depends(get_db),
100 -):
101 - logger.info(f"create_alert_request: {create_alert_request}")
102 - if create_alert_request.data_office365_Workload not in [workload.value for workload in ValidOffice365Workloads]:
103 - logger.info(f"Invalid workload: {create_alert_request.data_office365_Workload}")
104 - raise HTTPException(status_code=400, detail="Invalid workload")
105 - logger.info(f"Workload is valid: {create_alert_request.data_office365_Workload}")
106 - await is_office365_organization_id_valid(create_alert_request, session)
107 - return await create_threat_intel_alert(alert=create_alert_request, session=session)
backend/app/integrations/alert_creation/office365/schema/exchange.py deleted
-244
@@ -1,244 +0,0 @@
1 -from enum import Enum
2 -from typing import Any
3 -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
9 -from pydantic import Field
10 -
11 -
12 -class ValidOffice365Workloads(Enum):
13 - THREAT_INTEL_VALUE = "ThreatIntelligence"
14 - EXCHANGE_VALUE = "Exchange"
15 - ACTIVE_DIRECTORY_VALUE = "AzureActiveDirectory"
16 -
17 -
18 -############### ! REQUEST BODY RECEIVED ! ################
19 -
20 -
21 -class Office365ExchangeAlertBase(BaseModel):
22 - id: str = Field(..., description="Unique identifier for the alert", alias="_id")
23 - index: str = Field(
24 - ...,
25 - description="Index of the alert in the database",
26 - alias="_index",
27 - )
28 - data_office365_OrganizationId: str = Field(
29 - ...,
30 - description="Organization ID of the alert",
31 - )
32 - data_office365_Operation: str = Field(..., description="Operation of the alert")
33 - data_office365_Workload: str = Field(..., description="Workload of the alert")
34 - data_office365_UserId: str = Field(..., description="User ID of the alert")
35 - data_office365_Id: str = Field(..., description="ID of the alert")
36 - rule_description: str = Field(..., description="Description of the alert")
37 - rule_id: str = Field(..., description="ID of the alert")
38 - rule_level: int = Field(..., description="Level of the alert")
39 - timestamp: str = Field(..., description="The timestamp of the alert.")
40 - timestamp_utc: Optional[str] = Field(
41 - ...,
42 - description="The UTC timestamp of the alert.",
43 - )
44 - time_field: Optional[str] = Field(
45 - "timestamp",
46 - description="The timefield of the alert to be used when creating the IRIS alert.",
47 - )
48 -
49 - class Config:
50 - allow_population_by_field_name = True
51 - extra = Extra.allow
52 -
53 - def to_dict(self):
54 - return self.dict(exclude_none=True)
55 -
56 -
57 -class Office365ExchangeAlertRequest(Office365ExchangeAlertBase):
58 - rule_mitre_tactic: Optional[str] = Field(
59 - None,
60 - description="MITRE tactic of the alert",
61 - )
62 - rule_mitre_id: Optional[str] = Field(None, description="MITRE ID of the alert")
63 - rule_mitre_technique: Optional[str] = Field(
64 - None,
65 - description="MITRE technique of the alert",
66 - )
67 -
68 - class Config:
69 - allow_population_by_field_name = True
70 - extra = Extra.allow
71 -
72 - def to_dict(self):
73 - return self.dict(exclude_none=True)
74 -
75 -
76 -class Office365ExchangeAlertResponse(BaseModel):
77 - success: bool
78 - message: str
79 - alert_id: int = Field(..., description="The alert id as created in IRIS.")
80 - customer: str = Field(..., description="The customer name.")
81 - alert_source_link: str = Field(
82 - ...,
83 - description="The link to the alert within Grafana.",
84 - )
85 -
86 -
87 -########### ! Create Alerts Schemas ! ###########
88 -class IrisAsset(BaseModel):
89 - asset_name: Optional[str] = Field(
90 - "Could not find data_office365_UserId in alert",
91 - description="Name of the asset",
92 - example="Server01",
93 - )
94 - asset_ip: Optional[str] = Field(
95 - "n/a",
96 - description="IP address of the asset",
97 - example="192.168.1.1",
98 - )
99 - asset_description: Optional[str] = Field(
100 - "Office365 User ID",
101 - description="Description of the asset",
102 - example="Windows Server",
103 - )
104 - asset_type_id: Optional[int] = Field(
105 - 1,
106 - description="Type ID of the asset",
107 - example=1,
108 - )
109 -
110 - def to_dict(self):
111 - return self.dict(exclude_none=True)
112 -
113 -
114 -class IrisIoc(BaseModel):
115 - ioc_value: str = Field(
116 - ...,
117 - description="Value of the IoC",
118 - example="www.google.com",
119 - )
120 - ioc_description: str = Field(
121 - ...,
122 - description="Description of the IoC",
123 - example="Google",
124 - )
125 - ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
126 - ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
127 -
128 - def to_dict(self):
129 - return self.dict(exclude_none=True)
130 -
131 -
132 -class IrisAlertContext(BaseModel):
133 - customer_iris_id: int = Field(
134 - ...,
135 - description="IRIS ID of the customer",
136 - example=1,
137 - )
138 - customer_name: str = Field(
139 - ...,
140 - description="Name of the customer",
141 - example="SOCFortress",
142 - )
143 - customer_cases_index: str = Field(
144 - ...,
145 - description="IRIS case index name in the Wazuh-Indexer",
146 - example="dfir_iris_00001",
147 - )
148 - alert_id: str = Field(..., description="ID of the alert", example="123")
149 - alert_name: str = Field(
150 - ...,
151 - description="Name of the alert",
152 - example="Office365 Exchange Alert",
153 - )
154 - alert_level: int = Field(..., description="Severity level of the alert", example=3)
155 - rule_id: int = Field(
156 - ...,
157 - description="ID of the Suricata rule that triggered the alert",
158 - example="2001",
159 - )
160 - asset_name: Optional[str] = Field(
161 - "Could not find data_office365_UserId in alert",
162 - description="Name of the asset",
163 - example="Server01",
164 - )
165 - asset_ip: Optional[str] = Field(
166 - "n/a",
167 - description="IP address of the asset",
168 - example="1.1.1.1",
169 - )
170 - asset_type: Optional[int] = Field(
171 - 1,
172 - description="Type ID of the asset",
173 - example=1,
174 - )
175 - office365_operation: str = Field(
176 - ...,
177 - description="Operation of the alert",
178 - example="MailItemsAccessed",
179 - )
180 - data_office365_Id: str = Field(
181 - ...,
182 - description="ID of the alert",
183 - example="123",
184 - )
185 - rule_mitre_id: Optional[str] = Field(None, description="MITRE ID of the alert")
186 - rule_mitre_tactic: Optional[str] = Field(
187 - None,
188 - description="MITRE tactic of the alert",
189 - )
190 - rule_mitre_technique: Optional[str] = Field(
191 - None,
192 - description="MITRE technique of the alert",
193 - )
194 -
195 -
196 -class IrisAlertPayload(BaseModel):
197 - alert_title: str = Field(
198 - ...,
199 - description="Title of the alert",
200 - example="Intrusion Detected",
201 - )
202 - alert_description: str = Field(
203 - ...,
204 - description="Description of the alert",
205 - example="Intrusion Detected by Firewall",
206 - )
207 - alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
208 - assets: List[IrisAsset] = Field(..., description="List of affected assets")
209 - alert_source_link: str = Field(
210 - ...,
211 - description="Link to the alert within Grafana",
212 - example="https://grafana.com",
213 - )
214 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
215 - alert_severity_id: int = Field(
216 - ...,
217 - description="Severity ID of the alert",
218 - example=5,
219 - )
220 - alert_customer_id: int = Field(
221 - ...,
222 - description="Customer ID related to the alert",
223 - example=1,
224 - )
225 - alert_source_content: Dict[str, Any] = Field(
226 - ...,
227 - description="Original content from the alert source",
228 - )
229 - alert_context: IrisAlertContext = Field(
230 - ...,
231 - description="Contextual information about the alert",
232 - )
233 - alert_iocs: Optional[List[IrisIoc]] = Field(
234 - None,
235 - description="List of IoCs related to the alert",
236 - )
237 - alert_source_event_time: str = Field(
238 - ...,
239 - description="Timestamp of the alert",
240 - example="2021-01-01T00:00:00.000Z",
241 - )
242 -
243 - def to_dict(self):
244 - return self.dict(exclude_none=True)
backend/app/integrations/alert_creation/office365/schema/threat_intel.py deleted
-279
@@ -1,279 +0,0 @@
1 -from enum import Enum
2 -from typing import Any
3 -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
9 -from pydantic import Field
10 -
11 -
12 -class ValidOffice365Workloads(Enum):
13 - THREAT_INTEL_VALUE = "ThreatIntelligence"
14 - EXCHANGE_VALUE = "Exchange"
15 -
16 -
17 -############### ! REQUEST BODY RECEIVED ! ################
18 -
19 -
20 -class Office365ThreatIntelAlertBase(BaseModel):
21 - id: str = Field(..., description="Unique identifier for the alert", alias="_id")
22 - index: str = Field(
23 - ...,
24 - description="Index of the alert in the database",
25 - alias="_index",
26 - )
27 - data_office365_OrganizationId: str = Field(
28 - ...,
29 - description="Organization ID of the alert",
30 - )
31 - data_office365_Operation: str = Field(..., description="Operation of the alert")
32 - data_office365_Workload: str = Field(..., description="Workload of the alert")
33 - data_office365_UserId: str = Field(..., description="User ID of the alert")
34 - data_office365_Id: str = Field(..., description="ID of the alert")
35 - rule_description: str = Field(..., description="Description of the alert")
36 - rule_id: str = Field(..., description="ID of the alert")
37 - rule_level: int = Field(..., description="Level of the alert")
38 - timestamp: str = Field(..., description="The timestamp of the alert.")
39 - timestamp_utc: Optional[str] = Field(
40 - ...,
41 - description="The UTC timestamp of the alert.",
42 - )
43 - time_field: Optional[str] = Field(
44 - "timestamp",
45 - description="The timefield of the alert to be used when creating the IRIS alert.",
46 - )
47 -
48 - class Config:
49 - allow_population_by_field_name = True
50 - extra = Extra.allow
51 -
52 - def to_dict(self):
53 - return self.dict(exclude_none=True)
54 -
55 -
56 -class Office365ThreatIntelAlertRequest(Office365ThreatIntelAlertBase):
57 - rule_mitre_tactic: Optional[str] = Field(
58 - None,
59 - description="MITRE tactic of the alert",
60 - )
61 - rule_mitre_id: Optional[str] = Field(None, description="MITRE ID of the alert")
62 - rule_mitre_technique: Optional[str] = Field(
63 - None,
64 - description="MITRE technique of the alert",
65 - )
66 - data_office365_SenderIp: Optional[str] = Field(
67 - None,
68 - description="Sender IP of the alert",
69 - )
70 - data_office365_Recipients: Optional[str] = Field(
71 - None,
72 - description="Recipients of the alert",
73 - )
74 - data_office365_EventDeepLink: Optional[str] = Field(
75 - None,
76 - description="Event Deep Link of the alert",
77 - )
78 - data_office365_ThreatsAndDetectionTech: Optional[str] = Field(
79 - None,
80 - description="Threats and Detection Techniques of the alert",
81 - )
82 -
83 - class Config:
84 - allow_population_by_field_name = True
85 - extra = Extra.allow
86 -
87 - def to_dict(self):
88 - return self.dict(exclude_none=True)
89 -
90 -
91 -class Office365ThreatIntelAlertResponse(BaseModel):
92 - success: bool
93 - message: str
94 - alert_id: int = Field(..., description="The alert id as created in IRIS.")
95 - customer: str = Field(..., description="The customer name.")
96 - alert_source_link: str = Field(
97 - ...,
98 - description="The link to the alert within Grafana.",
99 - )
100 -
101 -
102 -########### ! Create Alerts Schemas ! ###########
103 -class IrisAsset(BaseModel):
104 - asset_name: Optional[str] = Field(
105 - "Could not find data_office365_UserId in alert",
106 - description="Name of the asset",
107 - example="Server01",
108 - )
109 - asset_ip: Optional[str] = Field(
110 - "n/a",
111 - description="IP address of the asset",
112 - example="192.168.1.1",
113 - )
114 - asset_description: Optional[str] = Field(
115 - "Office365 User ID",
116 - description="Description of the asset",
117 - example="Windows Server",
118 - )
119 - asset_type_id: Optional[int] = Field(
120 - 1,
121 - description="Type ID of the asset",
122 - example=1,
123 - )
124 -
125 - def to_dict(self):
126 - return self.dict(exclude_none=True)
127 -
128 -
129 -class IrisIoc(BaseModel):
130 - ioc_value: str = Field(
131 - ...,
132 - description="Value of the IoC",
133 - example="www.google.com",
134 - )
135 - ioc_description: str = Field(
136 - ...,
137 - description="Description of the IoC",
138 - example="Google",
139 - )
140 - ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
141 - ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
142 -
143 - def to_dict(self):
144 - return self.dict(exclude_none=True)
145 -
146 -
147 -class IrisAlertContext(BaseModel):
148 - customer_iris_id: int = Field(
149 - ...,
150 - description="IRIS ID of the customer",
151 - example=1,
152 - )
153 - customer_name: str = Field(
154 - ...,
155 - description="Name of the customer",
156 - example="SOCFortress",
157 - )
158 - customer_cases_index: str = Field(
159 - ...,
160 - description="IRIS case index name in the Wazuh-Indexer",
161 - example="dfir_iris_00001",
162 - )
163 - alert_id: str = Field(..., description="ID of the alert", example="123")
164 - alert_name: str = Field(
165 - ...,
166 - description="Name of the alert",
167 - example="Office365 Exchange Alert",
168 - )
169 - alert_level: int = Field(..., description="Severity level of the alert", example=3)
170 - rule_id: int = Field(
171 - ...,
172 - description="ID of the Suricata rule that triggered the alert",
173 - example="2001",
174 - )
175 - asset_name: Optional[str] = Field(
176 - "Could not find data_office365_UserId in alert",
177 - description="Name of the asset",
178 - example="Server01",
179 - )
180 - asset_ip: Optional[str] = Field(
181 - "n/a",
182 - description="IP address of the asset",
183 - example="1.1.1.1",
184 - )
185 - asset_type: Optional[int] = Field(
186 - 1,
187 - description="Type ID of the asset",
188 - example=1,
189 - )
190 - office365_operation: str = Field(
191 - ...,
192 - description="Operation of the alert",
193 - example="MailItemsAccessed",
194 - )
195 - data_office365_Id: str = Field(
196 - ...,
197 - description="ID of the alert",
198 - example="123",
199 - )
200 - recipient: Optional[str] = Field(
201 - None,
202 - description="Recipient of the alert",
203 - example="info@socfortress.co",
204 - )
205 - sender_ip: Optional[str] = Field(
206 - None,
207 - description="Sender IP of the alert",
208 - example="1.1.1.1",
209 - )
210 - detection_type: Optional[str] = Field(
211 - None,
212 - description="Detection type of the alert",
213 - example="Malware",
214 - )
215 - event_link: Optional[str] = Field(
216 - None,
217 - description="Event link of the alert",
218 - example="https://www.google.com",
219 - )
220 - rule_mitre_id: Optional[str] = Field(None, description="MITRE ID of the alert")
221 - rule_mitre_tactic: Optional[str] = Field(
222 - None,
223 - description="MITRE tactic of the alert",
224 - )
225 - rule_mitre_technique: Optional[str] = Field(
226 - None,
227 - description="MITRE technique of the alert",
228 - )
229 -
230 -
231 -class IrisAlertPayload(BaseModel):
232 - alert_title: str = Field(
233 - ...,
234 - description="Title of the alert",
235 - example="Intrusion Detected",
236 - )
237 - alert_description: str = Field(
238 - ...,
239 - description="Description of the alert",
240 - example="Intrusion Detected by Firewall",
241 - )
242 - alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
243 - assets: List[IrisAsset] = Field(..., description="List of affected assets")
244 - alert_source_link: str = Field(
245 - ...,
246 - description="Link to the alert within Grafana",
247 - example="https://grafana.com",
248 - )
249 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
250 - alert_severity_id: int = Field(
251 - ...,
252 - description="Severity ID of the alert",
253 - example=5,
254 - )
255 - alert_customer_id: int = Field(
256 - ...,
257 - description="Customer ID related to the alert",
258 - example=1,
259 - )
260 - alert_source_content: Dict[str, Any] = Field(
261 - ...,
262 - description="Original content from the alert source",
263 - )
264 - alert_context: IrisAlertContext = Field(
265 - ...,
266 - description="Contextual information about the alert",
267 - )
268 - alert_iocs: Optional[List[IrisIoc]] = Field(
269 - None,
270 - description="List of IoCs related to the alert",
271 - )
272 - alert_source_event_time: str = Field(
273 - ...,
274 - description="Timestamp of the alert",
275 - example="2021-01-01T00:00:00.000Z",
276 - )
277 -
278 - def to_dict(self):
279 - return self.dict(exclude_none=True)
backend/app/integrations/alert_creation/office365/services/exchange.py deleted
-308
@@ -1,308 +0,0 @@
1 -from typing import Optional
2 -from typing import Set
3 -
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
7 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
8 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
9 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
10 -from app.integrations.alert_creation.office365.schema.exchange import IrisAlertContext
11 -from app.integrations.alert_creation.office365.schema.exchange import IrisAlertPayload
12 -from app.integrations.alert_creation.office365.schema.exchange import IrisAsset
13 -from app.integrations.alert_creation.office365.schema.exchange import IrisIoc
14 -from app.integrations.alert_creation.office365.schema.exchange import (
15 - Office365ExchangeAlertRequest,
16 -)
17 -from app.integrations.alert_creation.office365.schema.exchange import (
18 - Office365ExchangeAlertResponse,
19 -)
20 -from app.integrations.utils.alerts import send_to_shuffle
21 -from app.integrations.utils.alerts import validate_ioc_type
22 -from app.integrations.utils.schema import ShufflePayload
23 -from app.utils import get_customer_alert_settings_office365
24 -
25 -
26 -def valid_ioc_fields() -> Set[str]:
27 - """
28 - Getter for the set of valid IoC fields.
29 - Returns
30 - -------
31 - Set[str]
32 - The set of valid IoC fields.
33 - """
34 - return {field.value for field in ValidIocFields}
35 -
36 -
37 -async def construct_alert_source_link(
38 - alert_details: Office365ExchangeAlertRequest,
39 - session: AsyncSession,
40 -) -> str:
41 - """
42 - Construct the alert source link for the alert details.
43 - Parameters
44 - ----------
45 - alert_details: Office365ExchangeAlertRequest
46 - The alert details.
47 - Returns
48 - -------
49 - str
50 - The alert source link.
51 - """
52 - grafana_url = (
53 - await get_customer_alert_settings_office365(
54 - office365_organization_id=alert_details.data_office365_OrganizationId,
55 - session=session,
56 - )
57 - ).grafana_url
58 -
59 - return (
60 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22O365%22,%7B%22refId%22"
61 - ":%22A%22,%22query%22:%22data_office365_Id:%5C%22"
62 - f"{alert_details.data_office365_Id}%5C%22%22,%22alias%22"
63 - ":%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22"
64 - "%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
65 - )
66 -
67 -
68 -async def build_ioc_payload(
69 - alert_details: Office365ExchangeAlertRequest,
70 -) -> Optional[IrisIoc]:
71 - """
72 - Builds an IoC payload based on the provided alert details.
73 -
74 - Args:
75 - alert_details (Office365ExchangeAlertRequest): The details of the alert.
76 -
77 - Returns:
78 - Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
79 - """
80 - for field in valid_ioc_fields():
81 - if hasattr(alert_details, field):
82 - ioc_value = getattr(alert_details, field)
83 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
84 - return IrisIoc(
85 - ioc_value=ioc_value,
86 - ioc_description="IoC found in alert",
87 - ioc_tlp_id=1,
88 - ioc_type_id=ioc_type,
89 - )
90 - return None
91 -
92 -
93 -async def build_asset_payload(
94 - alert_details: Office365ExchangeAlertRequest,
95 -) -> IrisAsset:
96 - if alert_details.data_office365_UserId:
97 - return IrisAsset(
98 - asset_name=alert_details.data_office365_UserId,
99 - asset_ip="n/a",
100 - asset_description="Office365 User ID",
101 - asset_type_id=1,
102 - )
103 - return IrisAsset()
104 -
105 -
106 -async def build_alert_context_payload(
107 - alert_details: Office365ExchangeAlertRequest,
108 - session: AsyncSession,
109 -) -> IrisAlertContext:
110 - """
111 - Builds the payload for the alert context.
112 -
113 - Args:
114 - alert_details (CreateAlertRequest): The details of the alert.
115 - agent_data (AgentsResponse): The agent data.
116 - session (AsyncSession): The async session.
117 -
118 - Returns:
119 - IrisAlertContext: The built alert context payload.
120 - """
121 - return IrisAlertContext(
122 - customer_iris_id=(
123 - await get_customer_alert_settings_office365(
124 - office365_organization_id=alert_details.data_office365_OrganizationId,
125 - session=session,
126 - )
127 - ).iris_customer_id,
128 - customer_name=(
129 - await get_customer_alert_settings_office365(
130 - office365_organization_id=alert_details.data_office365_OrganizationId,
131 - session=session,
132 - )
133 - ).customer_name,
134 - customer_cases_index=(
135 - await get_customer_alert_settings_office365(
136 - office365_organization_id=alert_details.data_office365_OrganizationId,
137 - session=session,
138 - )
139 - ).iris_index,
140 - alert_id=alert_details.id,
141 - alert_name=alert_details.rule_description,
142 - alert_level=alert_details.rule_level,
143 - rule_id=alert_details.rule_id,
144 - asset_name=alert_details.data_office365_UserId,
145 - asset_ip="n/a",
146 - asset_type=1,
147 - office365_operation=alert_details.data_office365_Operation,
148 - data_office365_Id=alert_details.data_office365_Id,
149 - rule_mitre_id=alert_details.rule_mitre_id,
150 - rule_mitre_technique=alert_details.rule_mitre_technique,
151 - rule_mitre_tactic=alert_details.rule_mitre_tactic,
152 - )
153 -
154 -
155 -async def build_alert_payload(
156 - alert_details: Office365ExchangeAlertRequest,
157 - ioc_payload: Optional[IrisIoc],
158 - session: AsyncSession,
159 -) -> IrisAlertPayload:
160 - """
161 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
162 -
163 - Args:
164 - alert_details (CreateAlertRequest): The details of the alert.
165 - agent_data: The agent data associated with the alert.
166 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
167 - session (AsyncSession): The session used for database operations.
168 -
169 - Returns:
170 - IrisAlertPayload: The built alert payload.
171 - """
172 - asset_payload = await build_asset_payload(alert_details)
173 - context_payload = await build_alert_context_payload(
174 - alert_details=alert_details,
175 - session=session,
176 - )
177 - timefield = "timestamp_utc"
178 - # Get the timefield value from the alert_details
179 - if hasattr(alert_details, timefield):
180 - alert_details.time_field = getattr(alert_details, timefield)
181 - logger.info(f"Alert has context: {context_payload}")
182 - if ioc_payload:
183 - logger.info(f"Alert has IoC: {ioc_payload}")
184 - return IrisAlertPayload(
185 - alert_title=alert_details.data_office365_Operation,
186 - alert_source_link=await construct_alert_source_link(
187 - alert_details,
188 - session=session,
189 - ),
190 - alert_description=alert_details.rule_description,
191 - alert_source="Office365 Exchange Rule",
192 - assets=[asset_payload],
193 - alert_status_id=3,
194 - alert_severity_id=5,
195 - alert_customer_id=(
196 - await get_customer_alert_settings_office365(
197 - office365_organization_id=alert_details.data_office365_OrganizationId,
198 - session=session,
199 - )
200 - ).iris_customer_id,
201 - alert_source_content=alert_details.to_dict(),
202 - alert_context=context_payload,
203 - alert_iocs=[ioc_payload],
204 - alert_source_event_time=alert_details.time_field,
205 - )
206 - else:
207 - logger.info("Alert does not have IoC")
208 - return IrisAlertPayload(
209 - alert_title=alert_details.data_office365_Operation,
210 - alert_source_link=await construct_alert_source_link(
211 - alert_details,
212 - session=session,
213 - ),
214 - alert_description=alert_details.rule_description,
215 - alert_source="Office365 Exchange Rule",
216 - assets=[asset_payload],
217 - alert_status_id=3,
218 - alert_severity_id=5,
219 - alert_customer_id=(
220 - await get_customer_alert_settings_office365(
221 - office365_organization_id=alert_details.data_office365_OrganizationId,
222 - session=session,
223 - )
224 - ).iris_customer_id,
225 - alert_source_content=alert_details.to_dict(),
226 - alert_context=context_payload,
227 - alert_source_event_time=alert_details.time_field,
228 - )
229 -
230 -
231 -async def create_exchange_alert(
232 - alert: Office365ExchangeAlertRequest,
233 - session: AsyncSession,
234 -) -> Office365ExchangeAlertResponse:
235 - """
236 - Creates an Office365 Exchange alert in IRIS.
237 -
238 - Args:
239 - alert (CreateAlertRequest): The alert details.
240 - session (AsyncSession): The database session.
241 -
242 - Returns:
243 - CreateAlertResponse: The response containing the alert ID and other details.
244 - """
245 - logger.info(f"Creating alert with {alert.id} in IRIS.")
246 - ioc_payload = await build_ioc_payload(alert_details=alert)
247 - iris_alert_payload = await build_alert_payload(
248 - alert_details=alert,
249 - ioc_payload=ioc_payload,
250 - session=session,
251 - )
252 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
253 - result = await fetch_and_validate_data(
254 - client,
255 - alert_client.add_alert,
256 - iris_alert_payload.to_dict(),
257 - )
258 - alert_id = result["data"]["alert_id"]
259 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
260 - # Update the alert with the asset payload
261 - await fetch_and_validate_data(
262 - client,
263 - alert_client.update_alert,
264 - alert_id,
265 - {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
266 - )
267 - # Updae the alert if the ioc_payload is not None
268 - if ioc_payload:
269 - await fetch_and_validate_data(
270 - client,
271 - alert_client.update_alert,
272 - alert_id,
273 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
274 - )
275 -
276 - await send_to_shuffle(
277 - ShufflePayload(
278 - alert_id=alert_id,
279 - customer=(
280 - await get_customer_alert_settings_office365(
281 - office365_organization_id=alert.data_office365_OrganizationId,
282 - session=session,
283 - )
284 - ).customer_name,
285 - customer_code=(
286 - await get_customer_alert_settings_office365(
287 - office365_organization_id=alert.data_office365_OrganizationId,
288 - session=session,
289 - )
290 - ).customer_code,
291 - alert_source_link=await construct_alert_source_link(alert, session=session),
292 - rule_description=alert.rule_description,
293 - hostname=alert.data_office365_UserId,
294 - ),
295 - session=session,
296 - )
297 - return Office365ExchangeAlertResponse(
298 - alert_id=alert_id,
299 - customer=(
300 - await get_customer_alert_settings_office365(
301 - office365_organization_id=alert.data_office365_OrganizationId,
302 - session=session,
303 - )
304 - ).customer_name,
305 - alert_source_link=await construct_alert_source_link(alert, session=session),
306 - success=True,
307 - message=f"Successfully created alert {alert_id} in IRIS.",
308 - )
backend/app/integrations/alert_creation/office365/services/threat_intel.py deleted
-312
@@ -1,312 +0,0 @@
1 -from typing import Optional
2 -from typing import Set
3 -
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
7 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
8 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
9 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
10 -from app.integrations.alert_creation.office365.schema.threat_intel import (
11 - IrisAlertContext,
12 -)
13 -from app.integrations.alert_creation.office365.schema.threat_intel import (
14 - IrisAlertPayload,
15 -)
16 -from app.integrations.alert_creation.office365.schema.threat_intel import IrisAsset
17 -from app.integrations.alert_creation.office365.schema.threat_intel import IrisIoc
18 -from app.integrations.alert_creation.office365.schema.threat_intel import (
19 - Office365ThreatIntelAlertRequest,
20 -)
21 -from app.integrations.alert_creation.office365.schema.threat_intel import (
22 - Office365ThreatIntelAlertResponse,
23 -)
24 -from app.integrations.utils.alerts import send_to_shuffle
25 -from app.integrations.utils.alerts import validate_ioc_type
26 -from app.integrations.utils.schema import ShufflePayload
27 -from app.utils import get_customer_alert_settings_office365
28 -
29 -
30 -def valid_ioc_fields() -> Set[str]:
31 - """
32 - Getter for the set of valid IoC fields.
33 - Returns
34 - -------
35 - Set[str]
36 - The set of valid IoC fields.
37 - """
38 - return {field.value for field in ValidIocFields}
39 -
40 -
41 -async def construct_alert_source_link(
42 - alert_details: Office365ThreatIntelAlertRequest,
43 - session: AsyncSession,
44 -) -> str:
45 - """
46 - Construct the alert source link for the alert details.
47 - Parameters
48 - ----------
49 - alert_details: Office365ExchangeAlertRequest
50 - The alert details.
51 - Returns
52 - -------
53 - str
54 - The alert source link.
55 - """
56 - grafana_url = (
57 - await get_customer_alert_settings_office365(
58 - office365_organization_id=alert_details.data_office365_OrganizationId,
59 - session=session,
60 - )
61 - ).grafana_url
62 -
63 - return (
64 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22O365%22,%7B%22refId%22"
65 - ":%22A%22,%22query%22:%22data_office365_Id:%5C%22"
66 - f"{alert_details.data_office365_Id}%5C%22%22,%22alias%22"
67 - ":%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22"
68 - "%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
69 - )
70 -
71 -
72 -async def build_ioc_payload(
73 - alert_details: Office365ThreatIntelAlertRequest,
74 -) -> Optional[IrisIoc]:
75 - """
76 - Builds an IoC payload based on the provided alert details.
77 -
78 - Args:
79 - alert_details (Office365ThreatIntelAlertRequest): The details of the alert.
80 -
81 - Returns:
82 - Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
83 - """
84 - for field in valid_ioc_fields():
85 - if hasattr(alert_details, field):
86 - ioc_value = getattr(alert_details, field)
87 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
88 - return IrisIoc(
89 - ioc_value=ioc_value,
90 - ioc_description="IoC found in alert",
91 - ioc_tlp_id=1,
92 - ioc_type_id=ioc_type,
93 - )
94 - return None
95 -
96 -
97 -async def build_asset_payload(
98 - alert_details: Office365ThreatIntelAlertRequest,
99 -) -> IrisAsset:
100 - if alert_details.data_office365_UserId:
101 - return IrisAsset(
102 - asset_name=alert_details.data_office365_UserId,
103 - asset_ip="n/a",
104 - asset_description="Office365 User ID",
105 - asset_type_id=1,
106 - )
107 - return IrisAsset()
108 -
109 -
110 -async def build_alert_context_payload(
111 - alert_details: Office365ThreatIntelAlertRequest,
112 - session: AsyncSession,
113 -) -> IrisAlertContext:
114 - """
115 - Builds the payload for the alert context.
116 -
117 - Args:
118 - alert_details (CreateAlertRequest): The details of the alert.
119 - agent_data (AgentsResponse): The agent data.
120 - session (AsyncSession): The async session.
121 -
122 - Returns:
123 - IrisAlertContext: The built alert context payload.
124 - """
125 - return IrisAlertContext(
126 - customer_iris_id=(
127 - await get_customer_alert_settings_office365(
128 - office365_organization_id=alert_details.data_office365_OrganizationId,
129 - session=session,
130 - )
131 - ).iris_customer_id,
132 - customer_name=(
133 - await get_customer_alert_settings_office365(
134 - office365_organization_id=alert_details.data_office365_OrganizationId,
135 - session=session,
136 - )
137 - ).customer_name,
138 - customer_cases_index=(
139 - await get_customer_alert_settings_office365(
140 - office365_organization_id=alert_details.data_office365_OrganizationId,
141 - session=session,
142 - )
143 - ).iris_index,
144 - alert_id=alert_details.id,
145 - alert_name=alert_details.rule_description,
146 - alert_level=alert_details.rule_level,
147 - rule_id=alert_details.rule_id,
148 - asset_name=alert_details.data_office365_UserId,
149 - asset_ip="n/a",
150 - asset_type=1,
151 - office365_operation=alert_details.data_office365_Operation,
152 - data_office365_Id=alert_details.data_office365_Id,
153 - rule_mitre_id=alert_details.rule_mitre_id,
154 - rule_mitre_technique=alert_details.rule_mitre_technique,
155 - rule_mitre_tactic=alert_details.rule_mitre_tactic,
156 - )
157 -
158 -
159 -async def build_alert_payload(
160 - alert_details: Office365ThreatIntelAlertRequest,
161 - ioc_payload: Optional[IrisIoc],
162 - session: AsyncSession,
163 -) -> IrisAlertPayload:
164 - """
165 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
166 -
167 - Args:
168 - alert_details (CreateAlertRequest): The details of the alert.
169 - agent_data: The agent data associated with the alert.
170 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
171 - session (AsyncSession): The session used for database operations.
172 -
173 - Returns:
174 - IrisAlertPayload: The built alert payload.
175 - """
176 - asset_payload = await build_asset_payload(alert_details)
177 - context_payload = await build_alert_context_payload(
178 - alert_details=alert_details,
179 - session=session,
180 - )
181 - timefield = "timestamp_utc"
182 - # Get the timefield value from the alert_details
183 - if hasattr(alert_details, timefield):
184 - alert_details.time_field = getattr(alert_details, timefield)
185 - logger.info(f"Alert has context: {context_payload}")
186 - if ioc_payload:
187 - logger.info(f"Alert has IoC: {ioc_payload}")
188 - return IrisAlertPayload(
189 - alert_title=alert_details.data_office365_Operation,
190 - alert_source_link=await construct_alert_source_link(
191 - alert_details,
192 - session=session,
193 - ),
194 - alert_description=alert_details.rule_description,
195 - alert_source="Office365 Threat Intel Rule",
196 - assets=[asset_payload],
197 - alert_status_id=3,
198 - alert_severity_id=5,
199 - alert_customer_id=(
200 - await get_customer_alert_settings_office365(
201 - office365_organization_id=alert_details.data_office365_OrganizationId,
202 - session=session,
203 - )
204 - ).iris_customer_id,
205 - alert_source_content=alert_details.to_dict(),
206 - alert_context=context_payload,
207 - alert_iocs=[ioc_payload],
208 - alert_source_event_time=alert_details.time_field,
209 - )
210 - else:
211 - logger.info("Alert does not have IoC")
212 - return IrisAlertPayload(
213 - alert_title=alert_details.data_office365_Operation,
214 - alert_source_link=await construct_alert_source_link(
215 - alert_details,
216 - session=session,
217 - ),
218 - alert_description=alert_details.rule_description,
219 - alert_source="Office365 Threat Intel Rule",
220 - assets=[asset_payload],
221 - alert_status_id=3,
222 - alert_severity_id=5,
223 - alert_customer_id=(
224 - await get_customer_alert_settings_office365(
225 - office365_organization_id=alert_details.data_office365_OrganizationId,
226 - session=session,
227 - )
228 - ).iris_customer_id,
229 - alert_source_content=alert_details.to_dict(),
230 - alert_context=context_payload,
231 - alert_source_event_time=alert_details.time_field,
232 - )
233 -
234 -
235 -async def create_threat_intel_alert(
236 - alert: Office365ThreatIntelAlertRequest,
237 - session: AsyncSession,
238 -) -> Office365ThreatIntelAlertResponse:
239 - """
240 - Creates an Office365 Threat Intel alert in IRIS.
241 -
242 - Args:
243 - alert (Office365ThreatIntelAlertRequest): The alert details.
244 - session (AsyncSession): The database session.
245 -
246 - Returns:
247 - CreateAlertResponse: The response containing the alert ID and other details.
248 - """
249 - logger.info(f"Creating alert with {alert.id} in IRIS.")
250 - ioc_payload = await build_ioc_payload(alert_details=alert)
251 - iris_alert_payload = await build_alert_payload(
252 - alert_details=alert,
253 - ioc_payload=ioc_payload,
254 - session=session,
255 - )
256 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
257 - result = await fetch_and_validate_data(
258 - client,
259 - alert_client.add_alert,
260 - iris_alert_payload.to_dict(),
261 - )
262 - alert_id = result["data"]["alert_id"]
263 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
264 - # Update the alert with the asset payload
265 - await fetch_and_validate_data(
266 - client,
267 - alert_client.update_alert,
268 - alert_id,
269 - {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
270 - )
271 - # Updae the alert if the ioc_payload is not None
272 - if ioc_payload:
273 - await fetch_and_validate_data(
274 - client,
275 - alert_client.update_alert,
276 - alert_id,
277 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
278 - )
279 -
280 - await send_to_shuffle(
281 - ShufflePayload(
282 - alert_id=alert_id,
283 - customer=(
284 - await get_customer_alert_settings_office365(
285 - office365_organization_id=alert.data_office365_OrganizationId,
286 - session=session,
287 - )
288 - ).customer_name,
289 - customer_code=(
290 - await get_customer_alert_settings_office365(
291 - office365_organization_id=alert.data_office365_OrganizationId,
292 - session=session,
293 - )
294 - ).customer_code,
295 - alert_source_link=await construct_alert_source_link(alert, session=session),
296 - rule_description=alert.rule_description,
297 - hostname=alert.data_office365_UserId,
298 - ),
299 - session=session,
300 - )
301 - return Office365ThreatIntelAlertResponse(
302 - alert_id=alert_id,
303 - customer=(
304 - await get_customer_alert_settings_office365(
305 - office365_organization_id=alert.data_office365_OrganizationId,
306 - session=session,
307 - )
308 - ).customer_name,
309 - alert_source_link=await construct_alert_source_link(alert, session=session),
310 - success=True,
311 - message=f"Successfully created alert {alert_id} in IRIS.",
312 - )
backend/app/integrations/alert_escalation/routes/general_alert.py deleted
-41
@@ -1,41 +0,0 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import Security
4 -from loguru import logger
5 -from sqlalchemy.ext.asyncio import AsyncSession
6 -
7 -from app.auth.utils import AuthHandler
8 -from app.db.db_session import get_db
9 -from app.incidents.routes.incident_alert import create_alert_manual_route
10 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
11 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
12 -
13 -integration_general_alerts_router = APIRouter()
14 -
15 -
16 -# ! TODO: Remove this route and file after the new route is implemented ! #
17 -@integration_general_alerts_router.post(
18 - "/create",
19 - response_model=CreateAlertResponse,
20 - description="Manually create an alert in IRIS from Copilot WebUI",
21 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
22 - deprecated=True,
23 -)
24 -async def create_alert_route(
25 - create_alert_request: CreateAlertRequest,
26 - session: AsyncSession = Depends(get_db),
27 -) -> CreateAlertResponse:
28 - """
29 - Create an alert in IRIS. Manually create an alert in IRIS from Copilot WebUI.
30 -
31 - Args:
32 - create_alert_request (CreateAlertRequest): The request object containing the details of the alert to be created.
33 - session (AsyncSession, optional): The database session. Defaults to Depends(get_session).
34 -
35 - Returns:
36 - CreateAlertResponse: The response object containing the result of the alert creation.
37 - """
38 - logger.info(f"Creating alert {create_alert_request.alert_id} in IRIS")
39 - return None
40 - # return await create_alert(create_alert_request, session)
41 - return await create_alert_manual_route(create_alert_request, session)
backend/app/integrations/alert_escalation/schema/escalate_alert.py
+8
@@ -14,6 +14,14 @@ class CustomerCodeKeys(Enum):
14 DATA_OFFICE365_ORGANIZATION_ID = "data_office365_OrganizationId"
15 SYSLOG_CUSTOMER = "syslog_customer"
16 CUSTOMER_CODE = "customer_code"
17 + CLUSTER_NODE = "cluster_node"
18 +
19 + @staticmethod
20 + def get_processed_value(key, value):
21 + if key == CustomerCodeKeys.CLUSTER_NODE:
22 + if value.startswith("wazuh.worker."):
23 + return value.replace("wazuh.worker.", "").lower()
24 + return value
25
26
27 class SyslogLevelMapping(Enum):
backend/app/integrations/alert_escalation/schema/general_alert.py deleted
-217
@@ -1,217 +0,0 @@
1 -from enum import Enum
2 -from typing import Any
3 -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
9 -from pydantic import Field
10 -
11 -
12 -class ValidIocFields(Enum):
13 - MISP_VALUE = "misp_value"
14 - OPENCTI_VALUE = "opencti_value"
15 - THREAT_INTEL_VALUE = "threat_intel_value"
16 -
17 -
18 -class CreateAlertRequest(BaseModel):
19 - index_name: str = Field(
20 - ...,
21 - description="The name of the index to search alerts for.",
22 - )
23 - alert_id: str = Field(..., description="The alert id.")
24 -
25 -
26 -class CreateAlertResponse(BaseModel):
27 - success: bool
28 - message: str
29 - alert_id: int = Field(..., description="The alert id as created in IRIS.")
30 - alert_url: Optional[str] = Field(None, description="The alert url as created in IRIS.")
31 -
32 -
33 -class GenericSourceModel(BaseModel):
34 - agent_name: str = Field(..., description="The name of the agent.")
35 - agent_id: str = Field(..., description="The id of the agent.")
36 - agent_labels_customer: str = Field(..., description="The customer of the agent.")
37 - rule_id: str = Field(..., description="The id of the rule.")
38 - rule_level: int = Field(..., description="The level of the rule.")
39 - rule_description: str = Field(..., description="The description of the rule.")
40 - timestamp: str = Field(..., description="The timestamp of the alert.")
41 - timestamp_utc: Optional[str] = Field(
42 - ...,
43 - description="The UTC timestamp of the alert.",
44 - )
45 - process_id: Optional[str] = Field(None, description="The process id of the alert.")
46 -
47 - class Config:
48 - extra = Extra.allow
49 -
50 -
51 -class GenericAlertModel(BaseModel):
52 - _index: str
53 - _id: str
54 - _version: int
55 - _source: GenericSourceModel # Nested model
56 - asset_type_id: Optional[int] = Field(
57 - None,
58 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
59 - )
60 - ioc_value: Optional[str] = Field(
61 - None,
62 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
63 - )
64 - ioc_type: Optional[str] = Field(
65 - None,
66 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
67 - )
68 - time_field: Optional[str] = Field(
69 - "timestamp",
70 - description="The timefield of the alert to be used when creating the IRIS alert.",
71 - )
72 -
73 - class Config:
74 - extra = Extra.allow
75 -
76 -
77 -# Sample data from `get_single_alert_details`
78 -sample_data = {
79 - "_index": "some_index",
80 - "_id": "some_id",
81 - "_version": 1,
82 - "_source": {
83 - "agent_name": "some_agent_name",
84 - "agent_id": "some_agent_id",
85 - # ... other fields
86 - },
87 - # ... other fields
88 -}
89 -
90 -
91 -########### Create Alerts Schemas ###########
92 -class IrisAsset(BaseModel):
93 - asset_name: str = Field(..., description="Name of the asset", example="Server01")
94 - asset_ip: str = Field(
95 - ...,
96 - description="IP address of the asset",
97 - example="192.168.1.1",
98 - )
99 - asset_description: str = Field(
100 - ...,
101 - description="Description of the asset",
102 - example="Windows Server",
103 - )
104 - asset_type_id: int = Field(..., description="Type ID of the asset", example=1)
105 - asset_tags: Optional[str] = Field(
106 - "Agent ID not found. Ensure the agent has been registered with Wazuh Manager and synced to the Agents table.",
107 - description="Tags of the asset",
108 - example="001",
109 - )
110 -
111 - def to_dict(self):
112 - return self.dict(exclude_none=True)
113 -
114 -
115 -class IrisIoc(BaseModel):
116 - ioc_value: str = Field(
117 - ...,
118 - description="Value of the IoC",
119 - example="www.google.com",
120 - )
121 - ioc_description: str = Field(
122 - ...,
123 - description="Description of the IoC",
124 - example="Google",
125 - )
126 - ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
127 - ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
128 -
129 - def to_dict(self):
130 - return self.dict(exclude_none=True)
131 -
132 -
133 -class IrisAlertContext(BaseModel):
134 - alert_id: str = Field(..., description="ID of the alert", example="123")
135 - alert_name: str = Field(
136 - ...,
137 - description="Name of the alert",
138 - example="Intrusion Detected",
139 - )
140 - alert_level: int = Field(..., description="Severity level of the alert", example=3)
141 - rule_id: str = Field(
142 - ...,
143 - description="ID of the rule that triggered the alert",
144 - example="2001",
145 - )
146 - asset_name: str = Field(
147 - ...,
148 - description="Name of the affected asset",
149 - example="Server01",
150 - )
151 - asset_ip: str = Field(
152 - ...,
153 - description="IP address of the affected asset",
154 - example="192.168.1.1",
155 - )
156 - asset_type: int = Field(..., description="Type ID of the affected asset", example=1)
157 - process_id: Optional[str] = Field(
158 - "No process ID found",
159 - description="Process ID involved in the alert",
160 - example="4567",
161 - )
162 - rule_mitre_id: Optional[str] = Field(
163 - "n/a",
164 - description="MITRE ATT&CK ID of the rule",
165 - example="T1234",
166 - )
167 - rule_mitre_tactic: Optional[str] = Field(
168 - "n/a",
169 - description="MITRE ATT&CK Tactic",
170 - example="Execution",
171 - )
172 - rule_mitre_technique: Optional[str] = Field(
173 - "n/a",
174 - description="MITRE ATT&CK Technique",
175 - example="Scripting",
176 - )
177 -
178 -
179 -class IrisAlertPayload(BaseModel):
180 - alert_title: str = Field(
181 - ...,
182 - description="Title of the alert",
183 - example="Intrusion Detected",
184 - )
185 - alert_description: str = Field(
186 - ...,
187 - description="Description of the alert",
188 - example="Intrusion Detected by Firewall",
189 - )
190 - alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
191 - assets: List[IrisAsset] = Field(..., description="List of affected assets")
192 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
193 - alert_severity_id: int = Field(
194 - ...,
195 - description="Severity ID of the alert",
196 - example=5,
197 - )
198 - alert_customer_id: int = Field(
199 - ...,
200 - description="Customer ID related to the alert",
201 - example=1,
202 - )
203 - alert_source_content: Dict[str, Any] = Field(
204 - ...,
205 - description="Original content from the alert source",
206 - )
207 - alert_context: IrisAlertContext = Field(
208 - ...,
209 - description="Contextual information about the alert",
210 - )
211 - alert_iocs: Optional[List[IrisIoc]] = Field(
212 - None,
213 - description="List of IoCs related to the alert",
214 - )
215 -
216 - def to_dict(self):
217 - return self.dict(exclude_none=True)
backend/app/integrations/alert_escalation/services/general_alert.py deleted
-513
@@ -1,513 +0,0 @@
1 -from typing import Optional
2 -from typing import Set
3 -
4 -from fastapi import HTTPException
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -from sqlalchemy.future import select
8 -
9 -# from app.integrations.alert_escalation.utils.universal import get_agent_data
10 -from app.agents.routes.agents import get_agent
11 -from app.agents.schema.agents import AgentsResponse
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
14 -from app.connectors.utils import get_connector_info_from_db
15 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
16 -from app.integrations.alert_creation_settings.models.alert_creation_settings import (
17 - AlertCreationSettings,
18 -)
19 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
20 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
21 -from app.integrations.alert_escalation.schema.general_alert import GenericAlertModel
22 -from app.integrations.alert_escalation.schema.general_alert import GenericSourceModel
23 -from app.integrations.alert_escalation.schema.general_alert import IrisAlertContext
24 -from app.integrations.alert_escalation.schema.general_alert import IrisAlertPayload
25 -from app.integrations.alert_escalation.schema.general_alert import IrisAsset
26 -from app.integrations.alert_escalation.schema.general_alert import IrisIoc
27 -from app.integrations.alert_escalation.schema.general_alert import ValidIocFields
28 -from app.integrations.utils.alerts import get_asset_type_id
29 -from app.integrations.utils.alerts import validate_ioc_type
30 -from app.utils import get_customer_alert_settings
31 -
32 -
33 -async def is_customer_code_valid(customer_code: str, session: AsyncSession) -> bool:
34 - """
35 - Check if the customer code is valid.
36 -
37 - Args:
38 - customer_code (str): The customer code to check.
39 - session (AsyncSession): The database session.
40 -
41 - Returns:
42 - bool: True if the customer code is valid, False otherwise.
43 - """
44 - logger.info(f"Checking if customer_code: {customer_code} is valid.")
45 -
46 - result = await session.execute(
47 - select(AlertCreationSettings).where(
48 - AlertCreationSettings.customer_code == customer_code,
49 - ),
50 - )
51 - settings = result.scalars().first()
52 - logger.info(f"Settings: {settings}")
53 -
54 - if settings:
55 - return True
56 -
57 - return False
58 -
59 -
60 -def valid_ioc_fields() -> Set[str]:
61 - """
62 - Getter for the set of valid IoC fields.
63 - Returns
64 - -------
65 - Set[str]
66 - The set of valid IoC fields.
67 - """
68 - return {field.value for field in ValidIocFields}
69 -
70 -
71 -async def construct_alert_source_link(
72 - alert_details: GenericAlertModel,
73 - session: AsyncSession,
74 -) -> str:
75 - """
76 - Construct the alert source link for the alert details.
77 - Parameters
78 - ----------
79 - alert_details: CreateAlertRequest
80 - The alert details.
81 - Returns
82 - -------
83 - str
84 - The alert source link.
85 - """
86 - # Check if the alert has a process id and that it is not "No process ID found"
87 - if hasattr(alert_details, "process_id") and alert_details._source.process_id != "No process ID found":
88 - query_string = f"%22query%22:%22process_id:%5C%22{alert_details._source.process_id}%5C%22%20AND%20"
89 - else:
90 - query_string = f"%22query%22:%22_id:%5C%22{alert_details._id}%5C%22%20AND%20"
91 -
92 - grafana_url = (
93 - await get_customer_alert_settings(
94 - customer_code=alert_details._source.agent_labels_customer,
95 - session=session,
96 - )
97 - ).grafana_url
98 -
99 - return (
100 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
101 - f"{query_string}"
102 - f"agent_name:%5C%22{alert_details._source.agent_name}%5C%22%22,"
103 - "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
104 - "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
105 - )
106 -
107 -
108 -async def get_single_alert_details(
109 - alert_details: CreateAlertRequest,
110 -) -> GenericAlertModel:
111 - """
112 - Fetches the details of a single alert.
113 -
114 - Args:
115 - alert_details (CreateAlertRequest): The details of the alert to fetch.
116 -
117 - Returns:
118 - GenericAlertModel: The model representing the fetched alert.
119 -
120 - Raises:
121 - HTTPException: If there is an error while fetching the alert details.
122 - """
123 - logger.info(
124 - f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}",
125 - )
126 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
127 - try:
128 - alert = es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
129 - source_model = GenericSourceModel(**alert["_source"])
130 - return GenericAlertModel(
131 - _source=source_model,
132 - _id=alert["_id"],
133 - _index=alert["_index"],
134 - _version=alert["_version"],
135 - )
136 - except Exception as e:
137 - logger.debug(f"Failed to collect alert details: {e}")
138 - raise HTTPException(
139 - status_code=400,
140 - detail=f"Failed to collect alert details: {e}",
141 - )
142 -
143 -
144 -async def build_ioc_payload(alert_details: GenericAlertModel) -> Optional[IrisIoc]:
145 - """
146 - Builds an IoC payload based on the given alert details.
147 -
148 - Args:
149 - alert_details (GenericAlertModel): The alert details.
150 -
151 - Returns:
152 - Optional[IrisIoc]: The IoC payload if an IoC is found in the alert, otherwise None.
153 - """
154 - for field in valid_ioc_fields():
155 - if hasattr(alert_details._source, field):
156 - ioc_value = getattr(alert_details._source, field)
157 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
158 - return IrisIoc(
159 - ioc_value=ioc_value,
160 - ioc_description="IoC found in alert",
161 - ioc_tlp_id=1,
162 - ioc_type_id=ioc_type,
163 - )
164 - return None
165 -
166 -
167 -async def build_asset_payload(agent_data: AgentsResponse, alert_details) -> IrisAsset:
168 - """
169 - Builds the payload for an IrisAsset object based on the agent data and alert details.
170 -
171 - Args:
172 - agent_data (AgentsResponse): The response containing agent data.
173 - alert_details: The details of the alert.
174 -
175 - Returns:
176 - IrisAsset: The constructed IrisAsset object.
177 - """
178 - if agent_data.success:
179 - return IrisAsset(
180 - asset_name=agent_data.agents[0].hostname,
181 - asset_ip=agent_data.agents[0].ip_address,
182 - asset_description=agent_data.agents[0].os,
183 - asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
184 - asset_tags=f"agent_id:{agent_data.agents[0].agent_id}",
185 - )
186 - return IrisAsset()
187 -
188 -
189 -async def build_alert_context_payload(
190 - alert_details: GenericAlertModel,
191 - agent_data: AgentsResponse,
192 - session: AsyncSession,
193 -) -> IrisAlertContext:
194 - """
195 - Builds the payload for the alert context.
196 -
197 - Args:
198 - alert_details (GenericAlertModel): The details of the alert.
199 - agent_data (AgentsResponse): The data of the agent.
200 - session (AsyncSession): The async session.
201 -
202 - Returns:
203 - IrisAlertContext: The built alert context payload.
204 - """
205 - return IrisAlertContext(
206 - customer_iris_id=(
207 - await get_customer_alert_settings(
208 - customer_code=alert_details._source.agent_labels_customer,
209 - session=session,
210 - )
211 - ).iris_customer_id,
212 - customer_name=(
213 - await get_customer_alert_settings(
214 - customer_code=alert_details._source.agent_labels_customer,
215 - session=session,
216 - )
217 - ).customer_name,
218 - customer_cases_index=(
219 - await get_customer_alert_settings(
220 - customer_code=alert_details._source.agent_labels_customer,
221 - session=session,
222 - )
223 - ).iris_index,
224 - alert_id=alert_details._id,
225 - alert_name=alert_details._source.rule_description,
226 - alert_level=alert_details._source.rule_level,
227 - rule_id=alert_details._source.rule_id,
228 - asset_name=agent_data.agents[0].hostname,
229 - asset_ip=agent_data.agents[0].ip_address,
230 - asset_type=await get_asset_type_id(agent_data.agents[0].os),
231 - process_id=getattr(alert_details._source, "process_id", "No process id found"),
232 - rule_mitre_id=getattr(
233 - alert_details._source,
234 - "rule_mitre_id",
235 - "No rule mitre id found",
236 - ),
237 - rule_mitre_tactic=getattr(
238 - alert_details._source,
239 - "rule_mitre_tactic",
240 - "No rule mitre tactic found",
241 - ),
242 - rule_mitre_technique=getattr(
243 - alert_details._source,
244 - "rule_mitre_technique",
245 - "No rule mitre technique found",
246 - ),
247 - )
248 -
249 -
250 -async def build_alert_payload(
251 - alert_details: GenericAlertModel,
252 - agent_data,
253 - ioc_payload: Optional[IrisIoc],
254 - session: AsyncSession,
255 -) -> IrisAlertPayload:
256 - """
257 - Builds the alert payload based on the provided alert details, agent data, IoC payload, and session.
258 -
259 - Args:
260 - alert_details (GenericAlertModel): The details of the alert.
261 - agent_data: The data of the agent.
262 - ioc_payload (Optional[IrisIoc]): The IoC payload.
263 - session (AsyncSession): The session object for database operations.
264 -
265 - Returns:
266 - IrisAlertPayload: The built alert payload.
267 -
268 - Raises:
269 - HTTPException: If there is an error while building the alert payload.
270 - """
271 - asset_payload = await build_asset_payload(agent_data, alert_details)
272 - context_payload = await build_alert_context_payload(
273 - alert_details=alert_details,
274 - agent_data=agent_data,
275 - session=session,
276 - )
277 - timefield = (
278 - await get_customer_alert_settings(
279 - customer_code=alert_details._source.agent_labels_customer,
280 - session=session,
281 - )
282 - ).timefield
283 - # Get the timefield value from the alert_details
284 - if hasattr(alert_details, timefield):
285 - alert_details.time_field = getattr(alert_details, timefield)
286 - logger.info(f"Alert has context: {context_payload}")
287 - try:
288 - if ioc_payload:
289 - logger.info(f"Alert has IoC: {ioc_payload}")
290 - return IrisAlertPayload(
291 - alert_title=alert_details._source.rule_description,
292 - alert_source_link=await construct_alert_source_link(
293 - alert_details,
294 - session=session,
295 - ),
296 - alert_description=alert_details._source.rule_description,
297 - alert_source="CoPilot",
298 - assets=[asset_payload],
299 - alert_status_id=3,
300 - alert_severity_id=5,
301 - alert_customer_id=(
302 - await get_customer_alert_settings(
303 - customer_code=alert_details._source.agent_labels_customer,
304 - session=session,
305 - )
306 - ).iris_customer_id,
307 - alert_source_content=alert_details._source,
308 - alert_context=context_payload,
309 - alert_iocs=[ioc_payload],
310 - alert_source_event_time=alert_details.time_field,
311 - )
312 - else:
313 - logger.info("Alert does not have IoC")
314 - return IrisAlertPayload(
315 - alert_title=alert_details._source.rule_description,
316 - alert_source_link=await construct_alert_source_link(
317 - alert_details,
318 - session=session,
319 - ),
320 - alert_description=alert_details._source.rule_description,
321 - alert_source="CoPilot",
322 - assets=[asset_payload],
323 - alert_status_id=3,
324 - alert_severity_id=5,
325 - alert_customer_id=(
326 - await get_customer_alert_settings(
327 - customer_code=alert_details._source.agent_labels_customer,
328 - session=session,
329 - )
330 - ).iris_customer_id,
331 - alert_source_content=alert_details._source,
332 - alert_context=context_payload,
333 - alert_source_event_time=alert_details.time_field,
334 - )
335 - except Exception as e:
336 - logger.error(f"Failed to build alert payload: {e}")
337 - raise HTTPException(
338 - status_code=500,
339 - detail=f"Failed to build alert payload: {e}",
340 - )
341 -
342 -
343 -async def construct_soc_alert_url(root_url: str, soc_alert_id: int) -> str:
344 - """Constructs the full URL for the SOC alert.
345 -
346 - Args:
347 - root_url (str): The root URL of the SOC alert system.
348 - soc_alert_id (int): The ID of the SOC alert.
349 -
350 - Returns:
351 - str: The full URL for the SOC alert.
352 -
353 - """
354 - url_path = f"/alerts?cid=1&page=1&per_page=10&sort=desc&alert_ids={soc_alert_id}"
355 - return f"{root_url}{url_path}"
356 -
357 -
358 -async def add_alert_to_document(
359 - es_client,
360 - alert: CreateAlertRequest,
361 - soc_alert_id: int,
362 - session: AsyncSession,
363 -) -> Optional[str]:
364 - """
365 - Update the alert document in Elasticsearch with the provided SOC alert ID URL.
366 -
367 - Parameters:
368 - - es_client: The Elasticsearch client instance to use for the update.
369 - - alert: The alert request object containing alert_id and index_name.
370 - - soc_alert_id: The alert ID as it exists within IRIS.
371 - - session: The database session for retrieving connector information.
372 -
373 - Returns:
374 - - True if the update is successful, False otherwise.
375 - """
376 - try:
377 - connector_info = await get_connector_info_from_db("DFIR-IRIS", session)
378 - full_url = await construct_soc_alert_url(
379 - connector_info["connector_url"],
380 - soc_alert_id,
381 - )
382 - es_client.update(
383 - index=alert.index_name,
384 - id=alert.alert_id,
385 - body={"doc": {"alert_url": full_url}},
386 - )
387 - logger.info(
388 - f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}",
389 - )
390 - return full_url
391 - except Exception as e:
392 - logger.error(
393 - f"Failed to add alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}: {e}",
394 - )
395 - # Attempt to remove read-only block
396 - try:
397 - es_client.indices.put_settings(
398 - index=alert.index_name,
399 - body={"index.blocks.write": None},
400 - )
401 - logger.info(
402 - f"Removed read-only block from index {alert.index_name}. Retrying update.",
403 - )
404 -
405 - # Retry the update operation
406 - es_client.update(
407 - index=alert.index_name,
408 - id=alert.alert_id,
409 - body={"doc": {"alert_url": full_url}},
410 - )
411 - logger.info(
412 - f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name} after removing read-only block",
413 - )
414 -
415 - # Reenable the write block
416 - es_client.indices.put_settings(
417 - index=alert.index_name,
418 - body={"index.blocks.write": True},
419 - )
420 - return full_url
421 - except Exception as e2:
422 - logger.error(
423 - f"Failed to remove read-only block from index {alert.index_name}: {e2}",
424 - )
425 - return False
426 -
427 -
428 -async def create_alert(
429 - alert: CreateAlertRequest,
430 - session: AsyncSession,
431 -) -> CreateAlertResponse:
432 - """
433 - Creates an alert in IRIS.
434 -
435 - Args:
436 - alert (CreateAlertRequest): The request object containing the alert details.
437 - session (AsyncSession): The database session.
438 -
439 - Returns:
440 - CreateAlertResponse: The response object containing the created alert details.
441 -
442 - Raises:
443 - HTTPException: If there is an error creating the alert.
444 - """
445 - logger.info(f"Creating alert {alert.alert_id} in IRIS")
446 - alert_details = await get_single_alert_details(alert_details=alert)
447 - logger.info(f"Alert details: {alert_details}")
448 - if (
449 - await is_customer_code_valid(
450 - customer_code=alert_details._source.agent_labels_customer,
451 - session=session,
452 - )
453 - is False
454 - ):
455 - logger.info(
456 - f"Invalid customer_code: {alert_details._source.agent_labels_customer}",
457 - )
458 - raise HTTPException(
459 - status_code=200,
460 - detail="Invalid customer_code, or the customer is not configured for alert creation.",
461 - )
462 - agent_data = await get_agent(agent_id=alert_details._source.agent_id, db=session)
463 - ioc_payload = await build_ioc_payload(alert_details=alert_details)
464 - iris_alert_payload = await build_alert_payload(
465 - alert_details=alert_details,
466 - agent_data=agent_data,
467 - ioc_payload=ioc_payload,
468 - session=session,
469 - )
470 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
471 - result = await fetch_and_validate_data(
472 - client,
473 - alert_client.add_alert,
474 - iris_alert_payload.to_dict(),
475 - )
476 - alert_id = result["data"]["alert_id"]
477 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
478 - # Update the alert with the asset payload
479 - await fetch_and_validate_data(
480 - client,
481 - alert_client.update_alert,
482 - alert_id,
483 - {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
484 - )
485 - # Updae the alert if the ioc_payload is not None
486 - if ioc_payload:
487 - await fetch_and_validate_data(
488 - client,
489 - alert_client.update_alert,
490 - alert_id,
491 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
492 - )
493 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
494 - iris_url = await add_alert_to_document(
495 - es_client,
496 - alert,
497 - result["data"]["alert_id"],
498 - session=session,
499 - )
500 - try:
501 - alert_id = result["data"]["alert_id"]
502 - return CreateAlertResponse(
503 - alert_id=alert_id,
504 - success=True,
505 - message=f"Alert {alert_id} created successfully",
506 - alert_url=iris_url,
507 - )
508 - except Exception as e:
509 - logger.error(f"Failed to create alert {alert.alert_id}: {e}")
510 - raise HTTPException(
511 - status_code=500,
512 - detail=f"Failed to create alert for ID {alert.alert_id}: {e}",
513 - )
backend/app/integrations/ask_socfortress/routes/ask_socfortress.py
+66 -66
@@ -1,79 +1,79 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from fastapi import HTTPException
4 -from fastapi import Security
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
1 +# from fastapi import APIRouter
2 +# from fastapi import Depends
3 +# from fastapi import HTTPException
4 +# from fastapi import Security
5 +# from loguru import logger
6 +# from sqlalchemy.ext.asyncio import AsyncSession
7
8 -from app.auth.utils import AuthHandler
9 -from app.db.db_session import get_db
10 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
11 - AskSocfortressRequest,
12 -)
13 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
14 - AskSocfortressSigmaResponse,
15 -)
16 -from app.integrations.ask_socfortress.services.ask_socfortress import (
17 - ask_socfortress_lookup,
18 -)
19 -from app.utils import get_connector_attribute
8 +# from app.auth.utils import AuthHandler
9 +# from app.db.db_session import get_db
10 +# from app.integrations.ask_socfortress.schema.ask_socfortress import (
11 +# AskSocfortressRequest,
12 +# )
13 +# from app.integrations.ask_socfortress.schema.ask_socfortress import (
14 +# AskSocfortressSigmaResponse,
15 +# )
16 +# from app.integrations.ask_socfortress.services.ask_socfortress import (
17 +# ask_socfortress_lookup,
18 +# )
19 +# from app.utils import get_connector_attribute
20
21 -# App specific imports
21 +# # App specific imports
22
23 -ask_socfortress_router = APIRouter()
23 +# ask_socfortress_router = APIRouter()
24
25
26 -async def ensure_api_key_exists(session: AsyncSession = Depends(get_db)) -> bool:
27 - """
28 - Ensures that the Ask SocFortress API key exists in the database.
26 +# async def ensure_api_key_exists(session: AsyncSession = Depends(get_db)) -> bool:
27 +# """
28 +# Ensures that the Ask SocFortress API key exists in the database.
29
30 - Args:
31 - session (AsyncSession): The database session.
30 +# Args:
31 +# session (AsyncSession): The database session.
32
33 - Raises:
34 - HTTPException: Raised if the SocFortress API key is not found.
33 +# Raises:
34 +# HTTPException: Raised if the SocFortress API key is not found.
35
36 - Returns:
37 - bool: True if the API key exists, otherwise raises HTTPException.
38 - """
39 - api_key = await get_connector_attribute(
40 - connector_id=10,
41 - column_name="connector_api_key",
42 - session=session,
43 - )
44 - # Close the session
45 - await session.close()
46 - if not api_key:
47 - raise HTTPException(
48 - status_code=500,
49 - detail="Ask SocFortress API key not found in the database.",
50 - )
51 - return True
36 +# Returns:
37 +# bool: True if the API key exists, otherwise raises HTTPException.
38 +# """
39 +# api_key = await get_connector_attribute(
40 +# connector_id=10,
41 +# column_name="connector_api_key",
42 +# session=session,
43 +# )
44 +# # Close the session
45 +# await session.close()
46 +# if not api_key:
47 +# raise HTTPException(
48 +# status_code=500,
49 +# detail="Ask SocFortress API key not found in the database.",
50 +# )
51 +# return True
52
53
54 -@ask_socfortress_router.post(
55 - "/sigma",
56 - response_model=AskSocfortressSigmaResponse,
57 - description="Ask SOCFortress for a Sigma rule.",
58 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
59 -)
60 -async def ask_socfortress_sigma(
61 - alert: AskSocfortressRequest,
62 - session: AsyncSession = Depends(get_db),
63 - _key_exists: bool = Depends(ensure_api_key_exists),
64 -):
65 - """
66 - Endpoint to ask SOCFortress for a Sigma rule.
54 +# @ask_socfortress_router.post(
55 +# "/sigma",
56 +# response_model=AskSocfortressSigmaResponse,
57 +# description="Ask SOCFortress for a Sigma rule.",
58 +# dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
59 +# )
60 +# async def ask_socfortress_sigma(
61 +# alert: AskSocfortressRequest,
62 +# session: AsyncSession = Depends(get_db),
63 +# _key_exists: bool = Depends(ensure_api_key_exists),
64 +# ):
65 +# """
66 +# Endpoint to ask SOCFortress for a Sigma rule.
67
68 - Args:
69 - alert (AskSocfortressRequest): The alert data.
70 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
71 - _key_exists (bool, optional): The API key existence flag. Defaults to Depends(ensure_api_key_exists).
68 +# Args:
69 +# alert (AskSocfortressRequest): The alert data.
70 +# session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
71 +# _key_exists (bool, optional): The API key existence flag. Defaults to Depends(ensure_api_key_exists).
72
73 - Returns:
74 - AskSocfortressSigmaResponse: The response from SOCFortress.
75 - """
76 - logger.info("Running Ask SOCFortress Sigma lookup.")
73 +# Returns:
74 +# AskSocfortressSigmaResponse: The response from SOCFortress.
75 +# """
76 +# logger.info("Running Ask SOCFortress Sigma lookup.")
77
78 - ask_socfortress_result = await ask_socfortress_lookup(alert, session=session)
79 - return ask_socfortress_result
78 +# ask_socfortress_result = await ask_socfortress_lookup(alert, session=session)
79 +# return ask_socfortress_result
backend/app/integrations/ask_socfortress/services/ask_socfortress.py
+313 -313
@@ -1,313 +1,313 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import Optional
4 -
5 -import httpx
6 -from fastapi import HTTPException
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -
10 -from app.connectors.utils import get_connector_info_from_db
11 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12 -from app.db.db_session import get_db_session
13 -from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
14 -from app.integrations.alert_escalation.schema.general_alert import GenericAlertModel
15 -from app.integrations.alert_escalation.schema.general_alert import GenericSourceModel
16 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
17 - AskSocfortressRequest,
18 -)
19 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
20 - AskSocfortressSigmaRequest,
21 -)
22 -from app.integrations.ask_socfortress.schema.ask_socfortress import (
23 - AskSocfortressSigmaResponse,
24 -)
25 -from app.utils import get_connector_attribute
26 -
27 -
28 -async def get_single_alert_details(
29 - alert_details: CreateAlertRequest,
30 -) -> GenericAlertModel:
31 - """
32 - Fetches the details of a single alert.
33 -
34 - Args:
35 - alert_details (CreateAlertRequest): The details of the alert to fetch.
36 -
37 - Returns:
38 - GenericAlertModel: The model representing the fetched alert.
39 -
40 - Raises:
41 - HTTPException: If there is an error while fetching the alert details.
42 - """
43 - logger.info(
44 - f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}",
45 - )
46 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
47 - try:
48 - alert = es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
49 - source_model = GenericSourceModel(**alert["_source"])
50 - return GenericAlertModel(
51 - _source=source_model,
52 - _id=alert["_id"],
53 - _index=alert["_index"],
54 - _version=alert["_version"],
55 - )
56 - except Exception as e:
57 - logger.debug(f"Failed to collect alert details: {e}")
58 - raise HTTPException(
59 - status_code=400,
60 - detail=f"Failed to collect alert details: {e}",
61 - )
62 -
63 -
64 -async def get_ask_socfortress_attributes(
65 - column_name: str,
66 - session: AsyncSession,
67 -) -> str:
68 - """
69 - Gets the Ask SocFortress attribute from the database.
70 -
71 - Args:
72 - column_name (str): The column name of the Ask SocFortress attribute.
73 - session (AsyncSession): The database session.
74 -
75 - Raises:
76 - HTTPException: Raised if the Ask SocFortress Attribute is not found.
77 -
78 - Returns:
79 - str: The Ask SocFortress Attribute.
80 -
81 - """
82 - attribute_value = await get_connector_attribute(
83 - connector_id=9,
84 - column_name=column_name,
85 - session=session,
86 - )
87 - # Close the session
88 - await session.close()
89 - if not attribute_value:
90 - raise HTTPException(
91 - status_code=500,
92 - detail="Ask Socfortress attributes not found in the database.",
93 - )
94 - return attribute_value
95 -
96 -
97 -async def verify_ask_socfortress_credentials(
98 - attributes: Dict[str, Any],
99 -) -> Dict[str, Any]:
100 - """
101 - Verifies the Ask SocFortress credentials.
102 -
103 - Args:
104 - attributes (Dict[str, Any]): The connector attributes.
105 -
106 - Returns:
107 - Dict[str, Any]: The connector attributes.
108 -
109 - Raises:
110 - HTTPException: Raised if the Ask SocFortress credentials are invalid.
111 - """
112 - api_key = attributes.get("connector_api_key", None)
113 - url = attributes.get("connector_url", None)
114 - if api_key is None or url is None:
115 - logger.error("No Ask Socfortress credentials found in the database")
116 - raise HTTPException(
117 - status_code=500,
118 - detail="Ask Socfortress credentials not found in the database",
119 - )
120 - return attributes
121 -
122 -
123 -async def verify_ask_socfortress_connector(connector_name: str) -> str:
124 - """
125 - Verifies the Ask SocFortress connector.
126 -
127 - Args:
128 - connector_name (str): The name of the connector.
129 -
130 - Returns:
131 - str: The connector name.
132 -
133 - Raises:
134 - HTTPException: Raised if the connector name is not Ask SocFortress.
135 - """
136 - logger.info("Verifying Ask Socfortress connector")
137 - async with get_db_session() as session: # This will correctly enter the context manager
138 - attributes = await get_connector_info_from_db(connector_name, session)
139 - if attributes is None:
140 - logger.error("No Ask Socfortress connector found in the database")
141 - return None
142 - request = AskSocfortressSigmaRequest(
143 - sigma_rule_name="Process Explorer Driver Creation By Non-Sysinternals Binary",
144 - )
145 - response = await invoke_ask_socfortress_api(
146 - attributes["connector_api_key"],
147 - attributes["connector_url"],
148 - request,
149 - )
150 - if response["message"] != "Forbidden":
151 - logger.info("Ask Socfortress connector verified successfully")
152 - return {
153 - "connectionSuccessful": True,
154 - "message": "Successfully verified ASK SOCFortress connector",
155 - }
156 - else:
157 - logger.error("Failed to verify Ask Socfortress connector")
158 - return {
159 - "connectionSuccessful": False,
160 - "message": "Failed to verify ASK SOCFortress connector",
161 - }
162 -
163 -
164 -async def invoke_ask_socfortress_api(
165 - api_key: str,
166 - url: str,
167 - request: AskSocfortressSigmaRequest,
168 -) -> dict:
169 - """
170 - Invokes the Socfortress Threat Intel API with the provided API key, URL, and request parameters.
171 -
172 - Args:
173 - api_key (str): The API key for authentication.
174 - url (str): The URL of the Socfortress Threat Intel API.
175 - request (SocfortressThreatIntelRequest): The request object containing the IOC value and customer code.
176 -
177 - Returns:
178 - dict: The JSON response from the Socfortress Threat Intel API.
179 -
180 - Raises:
181 - httpx.HTTPStatusError: If the API request fails with a non-successful status code.
182 - """
183 - headers = {
184 - "module-version": "your_module_version",
185 - "x-api-key": api_key,
186 - "Content-Type": "application/json",
187 - }
188 - data = {"sigma_rule_name": request.sigma_rule_name}
189 - async with httpx.AsyncClient(timeout=60) as client:
190 - response = await client.post(url=f"{url}/v1/sigma", headers=headers, json=data)
191 - return response.json()
192 -
193 -
194 -async def get_ask_socfortress_response(
195 - request: AskSocfortressSigmaRequest,
196 - session: AsyncSession,
197 -) -> AskSocfortressSigmaResponse:
198 - """
199 - Retrieves IoC response from Socfortress Threat Intel API.
200 -
201 - Args:
202 - request (SocfortressThreatIntelRequest): The request object containing the IoC data.
203 - session (AsyncSession): The async session object for making HTTP requests.
204 -
205 - Returns:
206 - IoCResponse: The response object containing the IoC data and success status.
207 - """
208 - api_key = await get_ask_socfortress_attributes("connector_api_key", session)
209 - url = await get_ask_socfortress_attributes("connector_url", session)
210 - response_data = await invoke_ask_socfortress_api(api_key, url, request)
211 -
212 - # Using .get() with default values
213 - success = response_data.get("success", False)
214 - message = response_data.get("message", "No message provided")
215 -
216 - return AskSocfortressSigmaResponse(success=success, message=message)
217 -
218 -
219 -async def add_alert_to_document(
220 - es_client,
221 - alert: CreateAlertRequest,
222 - result: str,
223 - session: AsyncSession,
224 -) -> Optional[str]:
225 - """
226 - Update the alert document in Elasticsearch with the provided SOC alert ID URL.
227 -
228 - Parameters:
229 - - es_client: The Elasticsearch client instance to use for the update.
230 - - alert: The alert request object containing alert_id and index_name.
231 - - soc_alert_id: The alert ID as it exists within IRIS.
232 - - session: The database session for retrieving connector information.
233 -
234 - Returns:
235 - - True if the update is successful, False otherwise.
236 - """
237 - try:
238 - es_client.update(
239 - index=alert.index_name,
240 - id=alert.alert_id,
241 - body={"doc": {"ask_socfortress_message": result}},
242 - )
243 - logger.info(
244 - f"Added Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}",
245 - )
246 - return None
247 - except Exception as e:
248 - logger.error(
249 - f"Failed to add Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}: {e}",
250 - )
251 -
252 - # Attempt to remove read-only block
253 - try:
254 - es_client.indices.put_settings(
255 - index=alert.index_name,
256 - body={"index.blocks.write": None},
257 - )
258 - logger.info(
259 - f"Removed read-only block from index {alert.index_name}. Retrying update.",
260 - )
261 -
262 - # Retry the update operation
263 - es_client.update(
264 - index=alert.index_name,
265 - id=alert.alert_id,
266 - body={"doc": {"ask_socfortress": result}},
267 - )
268 - logger.info(
269 - f"Added Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name} after removing read-only block",
270 - )
271 -
272 - # Reenable the write block
273 - es_client.indices.put_settings(
274 - index=alert.index_name,
275 - body={"index.blocks.write": True},
276 - )
277 - return True
278 - except Exception as e2:
279 - logger.error(
280 - f"Failed to remove read-only block from index {alert.index_name}: {e2}",
281 - )
282 - return False
283 -
284 -
285 -async def ask_socfortress_lookup(
286 - alert: AskSocfortressRequest,
287 - session: AsyncSession,
288 -) -> AskSocfortressSigmaResponse:
289 - """
290 - Performs a Ask SOCFortress lookup using the Socfortress service.
291 -
292 - Args:
293 - request (SocfortressThreatIntelRequest): The request object containing the IoC to lookup.
294 - session (AsyncSession): The async session object for making HTTP requests.
295 -
296 - Returns:
297 - IoCResponse: The response object containing the Ask SOCFortress information.
298 - """
299 - alert_details = await get_single_alert_details(alert_details=alert)
300 - logger.info(f"Alert details: {alert_details}")
301 - if alert_details._source.rule_group3 != "sigma":
302 - raise HTTPException(status_code=400, detail="Alert is not a Sigma alert.")
303 - sigma_rule_name = AskSocfortressSigmaRequest(
304 - sigma_rule_name=alert_details._source.data_name,
305 - )
306 - ask_socfortress_response = await get_ask_socfortress_response(
307 - sigma_rule_name,
308 - session,
309 - )
310 - result = ask_socfortress_response.message
311 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
312 - await add_alert_to_document(es_client, alert, result, session=session)
313 - return ask_socfortress_response
1 +# from typing import Any
2 +# from typing import Dict
3 +# from typing import Optional
4 +
5 +# import httpx
6 +# from fastapi import HTTPException
7 +# from loguru import logger
8 +# from sqlalchemy.ext.asyncio import AsyncSession
9 +
10 +# from app.connectors.utils import get_connector_info_from_db
11 +# from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12 +# from app.db.db_session import get_db_session
13 +# from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
14 +# from app.integrations.alert_escalation.schema.general_alert import GenericAlertModel
15 +# from app.integrations.alert_escalation.schema.general_alert import GenericSourceModel
16 +# from app.integrations.ask_socfortress.schema.ask_socfortress import (
17 +# AskSocfortressRequest,
18 +# )
19 +# from app.integrations.ask_socfortress.schema.ask_socfortress import (
20 +# AskSocfortressSigmaRequest,
21 +# )
22 +# from app.integrations.ask_socfortress.schema.ask_socfortress import (
23 +# AskSocfortressSigmaResponse,
24 +# )
25 +# from app.utils import get_connector_attribute
26 +
27 +
28 +# async def get_single_alert_details(
29 +# alert_details: CreateAlertRequest,
30 +# ) -> GenericAlertModel:
31 +# """
32 +# Fetches the details of a single alert.
33 +
34 +# Args:
35 +# alert_details (CreateAlertRequest): The details of the alert to fetch.
36 +
37 +# Returns:
38 +# GenericAlertModel: The model representing the fetched alert.
39 +
40 +# Raises:
41 +# HTTPException: If there is an error while fetching the alert details.
42 +# """
43 +# logger.info(
44 +# f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}",
45 +# )
46 +# es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
47 +# try:
48 +# alert = es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
49 +# source_model = GenericSourceModel(**alert["_source"])
50 +# return GenericAlertModel(
51 +# _source=source_model,
52 +# _id=alert["_id"],
53 +# _index=alert["_index"],
54 +# _version=alert["_version"],
55 +# )
56 +# except Exception as e:
57 +# logger.debug(f"Failed to collect alert details: {e}")
58 +# raise HTTPException(
59 +# status_code=400,
60 +# detail=f"Failed to collect alert details: {e}",
61 +# )
62 +
63 +
64 +# async def get_ask_socfortress_attributes(
65 +# column_name: str,
66 +# session: AsyncSession,
67 +# ) -> str:
68 +# """
69 +# Gets the Ask SocFortress attribute from the database.
70 +
71 +# Args:
72 +# column_name (str): The column name of the Ask SocFortress attribute.
73 +# session (AsyncSession): The database session.
74 +
75 +# Raises:
76 +# HTTPException: Raised if the Ask SocFortress Attribute is not found.
77 +
78 +# Returns:
79 +# str: The Ask SocFortress Attribute.
80 +
81 +# """
82 +# attribute_value = await get_connector_attribute(
83 +# connector_id=9,
84 +# column_name=column_name,
85 +# session=session,
86 +# )
87 +# # Close the session
88 +# await session.close()
89 +# if not attribute_value:
90 +# raise HTTPException(
91 +# status_code=500,
92 +# detail="Ask Socfortress attributes not found in the database.",
93 +# )
94 +# return attribute_value
95 +
96 +
97 +# async def verify_ask_socfortress_credentials(
98 +# attributes: Dict[str, Any],
99 +# ) -> Dict[str, Any]:
100 +# """
101 +# Verifies the Ask SocFortress credentials.
102 +
103 +# Args:
104 +# attributes (Dict[str, Any]): The connector attributes.
105 +
106 +# Returns:
107 +# Dict[str, Any]: The connector attributes.
108 +
109 +# Raises:
110 +# HTTPException: Raised if the Ask SocFortress credentials are invalid.
111 +# """
112 +# api_key = attributes.get("connector_api_key", None)
113 +# url = attributes.get("connector_url", None)
114 +# if api_key is None or url is None:
115 +# logger.error("No Ask Socfortress credentials found in the database")
116 +# raise HTTPException(
117 +# status_code=500,
118 +# detail="Ask Socfortress credentials not found in the database",
119 +# )
120 +# return attributes
121 +
122 +
123 +# async def verify_ask_socfortress_connector(connector_name: str) -> str:
124 +# """
125 +# Verifies the Ask SocFortress connector.
126 +
127 +# Args:
128 +# connector_name (str): The name of the connector.
129 +
130 +# Returns:
131 +# str: The connector name.
132 +
133 +# Raises:
134 +# HTTPException: Raised if the connector name is not Ask SocFortress.
135 +# """
136 +# logger.info("Verifying Ask Socfortress connector")
137 +# async with get_db_session() as session: # This will correctly enter the context manager
138 +# attributes = await get_connector_info_from_db(connector_name, session)
139 +# if attributes is None:
140 +# logger.error("No Ask Socfortress connector found in the database")
141 +# return None
142 +# request = AskSocfortressSigmaRequest(
143 +# sigma_rule_name="Process Explorer Driver Creation By Non-Sysinternals Binary",
144 +# )
145 +# response = await invoke_ask_socfortress_api(
146 +# attributes["connector_api_key"],
147 +# attributes["connector_url"],
148 +# request,
149 +# )
150 +# if response["message"] != "Forbidden":
151 +# logger.info("Ask Socfortress connector verified successfully")
152 +# return {
153 +# "connectionSuccessful": True,
154 +# "message": "Successfully verified ASK SOCFortress connector",
155 +# }
156 +# else:
157 +# logger.error("Failed to verify Ask Socfortress connector")
158 +# return {
159 +# "connectionSuccessful": False,
160 +# "message": "Failed to verify ASK SOCFortress connector",
161 +# }
162 +
163 +
164 +# async def invoke_ask_socfortress_api(
165 +# api_key: str,
166 +# url: str,
167 +# request: AskSocfortressSigmaRequest,
168 +# ) -> dict:
169 +# """
170 +# Invokes the Socfortress Threat Intel API with the provided API key, URL, and request parameters.
171 +
172 +# Args:
173 +# api_key (str): The API key for authentication.
174 +# url (str): The URL of the Socfortress Threat Intel API.
175 +# request (SocfortressThreatIntelRequest): The request object containing the IOC value and customer code.
176 +
177 +# Returns:
178 +# dict: The JSON response from the Socfortress Threat Intel API.
179 +
180 +# Raises:
181 +# httpx.HTTPStatusError: If the API request fails with a non-successful status code.
182 +# """
183 +# headers = {
184 +# "module-version": "your_module_version",
185 +# "x-api-key": api_key,
186 +# "Content-Type": "application/json",
187 +# }
188 +# data = {"sigma_rule_name": request.sigma_rule_name}
189 +# async with httpx.AsyncClient(timeout=60) as client:
190 +# response = await client.post(url=f"{url}/v1/sigma", headers=headers, json=data)
191 +# return response.json()
192 +
193 +
194 +# async def get_ask_socfortress_response(
195 +# request: AskSocfortressSigmaRequest,
196 +# session: AsyncSession,
197 +# ) -> AskSocfortressSigmaResponse:
198 +# """
199 +# Retrieves IoC response from Socfortress Threat Intel API.
200 +
201 +# Args:
202 +# request (SocfortressThreatIntelRequest): The request object containing the IoC data.
203 +# session (AsyncSession): The async session object for making HTTP requests.
204 +
205 +# Returns:
206 +# IoCResponse: The response object containing the IoC data and success status.
207 +# """
208 +# api_key = await get_ask_socfortress_attributes("connector_api_key", session)
209 +# url = await get_ask_socfortress_attributes("connector_url", session)
210 +# response_data = await invoke_ask_socfortress_api(api_key, url, request)
211 +
212 +# # Using .get() with default values
213 +# success = response_data.get("success", False)
214 +# message = response_data.get("message", "No message provided")
215 +
216 +# return AskSocfortressSigmaResponse(success=success, message=message)
217 +
218 +
219 +# async def add_alert_to_document(
220 +# es_client,
221 +# alert: CreateAlertRequest,
222 +# result: str,
223 +# session: AsyncSession,
224 +# ) -> Optional[str]:
225 +# """
226 +# Update the alert document in Elasticsearch with the provided SOC alert ID URL.
227 +
228 +# Parameters:
229 +# - es_client: The Elasticsearch client instance to use for the update.
230 +# - alert: The alert request object containing alert_id and index_name.
231 +# - soc_alert_id: The alert ID as it exists within IRIS.
232 +# - session: The database session for retrieving connector information.
233 +
234 +# Returns:
235 +# - True if the update is successful, False otherwise.
236 +# """
237 +# try:
238 +# es_client.update(
239 +# index=alert.index_name,
240 +# id=alert.alert_id,
241 +# body={"doc": {"ask_socfortress_message": result}},
242 +# )
243 +# logger.info(
244 +# f"Added Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}",
245 +# )
246 +# return None
247 +# except Exception as e:
248 +# logger.error(
249 +# f"Failed to add Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name}: {e}",
250 +# )
251 +
252 +# # Attempt to remove read-only block
253 +# try:
254 +# es_client.indices.put_settings(
255 +# index=alert.index_name,
256 +# body={"index.blocks.write": None},
257 +# )
258 +# logger.info(
259 +# f"Removed read-only block from index {alert.index_name}. Retrying update.",
260 +# )
261 +
262 +# # Retry the update operation
263 +# es_client.update(
264 +# index=alert.index_name,
265 +# id=alert.alert_id,
266 +# body={"doc": {"ask_socfortress": result}},
267 +# )
268 +# logger.info(
269 +# f"Added Ask SOCFortress Message to alert {alert.alert_id} in index {alert.index_name} after removing read-only block",
270 +# )
271 +
272 +# # Reenable the write block
273 +# es_client.indices.put_settings(
274 +# index=alert.index_name,
275 +# body={"index.blocks.write": True},
276 +# )
277 +# return True
278 +# except Exception as e2:
279 +# logger.error(
280 +# f"Failed to remove read-only block from index {alert.index_name}: {e2}",
281 +# )
282 +# return False
283 +
284 +
285 +# async def ask_socfortress_lookup(
286 +# alert: AskSocfortressRequest,
287 +# session: AsyncSession,
288 +# ) -> AskSocfortressSigmaResponse:
289 +# """
290 +# Performs a Ask SOCFortress lookup using the Socfortress service.
291 +
292 +# Args:
293 +# request (SocfortressThreatIntelRequest): The request object containing the IoC to lookup.
294 +# session (AsyncSession): The async session object for making HTTP requests.
295 +
296 +# Returns:
297 +# IoCResponse: The response object containing the Ask SOCFortress information.
298 +# """
299 +# alert_details = await get_single_alert_details(alert_details=alert)
300 +# logger.info(f"Alert details: {alert_details}")
301 +# if alert_details._source.rule_group3 != "sigma":
302 +# raise HTTPException(status_code=400, detail="Alert is not a Sigma alert.")
303 +# sigma_rule_name = AskSocfortressSigmaRequest(
304 +# sigma_rule_name=alert_details._source.data_name,
305 +# )
306 +# ask_socfortress_response = await get_ask_socfortress_response(
307 +# sigma_rule_name,
308 +# session,
309 +# )
310 +# result = ask_socfortress_response.message
311 +# es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
312 +# await add_alert_to_document(es_client, alert, result, session=session)
313 +# return ask_socfortress_response
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py deleted
-611
@@ -1,611 +0,0 @@
1 -from typing import Optional
2 -
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from fastapi import HTTPException
6 -from fastapi import Security
7 -from loguru import logger
8 -from sqlalchemy.ext.asyncio import AsyncSession
9 -from sqlalchemy.future import select
10 -from sqlalchemy.sql.expression import delete
11 -
12 -from app.auth.utils import AuthHandler
13 -from app.db.db_session import get_db
14 -from app.db.universal_models import CustomersMeta
15 -from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
16 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
17 - AlertAnalysisResponse,
18 -)
19 -from app.integrations.monitoring_alert.schema.monitoring_alert import GraylogPostRequest
20 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
21 - GraylogPostResponse,
22 -)
23 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
24 - MonitoringAlertsResponseModel,
25 -)
26 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
27 - MonitoringWazuhAlertsRequestModel,
28 -)
29 -from app.integrations.monitoring_alert.services.custom import analyze_custom_alert
30 -from app.integrations.monitoring_alert.services.office365_exchange import (
31 - analyze_office365_exchange_online_alerts,
32 -)
33 -from app.integrations.monitoring_alert.services.office365_threatintel import (
34 - analyze_office365_threatintel_alerts,
35 -)
36 -from app.integrations.monitoring_alert.services.suricata import analyze_suricata_alerts
37 -from app.integrations.monitoring_alert.services.wazuh import analyze_wazuh_alerts
38 -from app.integrations.sap_siem.services.sap_siem_multiple_logins import (
39 - sap_siem_multiple_logins_same_ip,
40 -)
41 -from app.integrations.sap_siem.services.sap_siem_suspicious_logins import (
42 - sap_siem_suspicious_logins,
43 -)
44 -
45 -monitoring_alerts_router = APIRouter()
46 -
47 -ALERT_ANALYZERS = {
48 - "WAZUH": analyze_wazuh_alerts,
49 - "SURICATA": analyze_suricata_alerts,
50 - "OFFICE365_THREAT_INTEL": analyze_office365_threatintel_alerts,
51 - "OFFICE365_EXCHANGE_ONLINE": analyze_office365_exchange_online_alerts,
52 -}
53 -
54 -
55 -async def get_customer_meta(customer_code: str, session: AsyncSession) -> CustomersMeta:
56 - """
57 - Get the customer meta for the given customer_code.
58 -
59 - Args:
60 - customer_code (str): The customer code.
61 - session (AsyncSession): The database session.
62 -
63 - Returns:
64 - CustomersMeta: The customer meta.
65 - """
66 - logger.info(f"Getting customer meta for customer_code: {customer_code}")
67 -
68 - customer_meta = await session.execute(
69 - select(CustomersMeta).where(CustomersMeta.customer_code == customer_code),
70 - )
71 - customer_meta = customer_meta.scalars().first()
72 -
73 - if not customer_meta:
74 - logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {customer_code}")
75 - customer_meta = await session.execute(
76 - select(CustomersMeta).where(CustomersMeta.customer_meta_office365_organization_id == customer_code),
77 - )
78 - customer_meta = customer_meta.scalars().first()
79 -
80 - if not customer_meta:
81 - raise HTTPException(status_code=404, detail="Customer not found")
82 -
83 - return customer_meta
84 -
85 -
86 -@monitoring_alerts_router.get(
87 - "/list",
88 - response_model=MonitoringAlertsResponseModel,
89 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
90 -)
91 -async def list_monitoring_alerts(
92 - session: AsyncSession = Depends(get_db),
93 -) -> MonitoringAlertsResponseModel:
94 - """
95 - List all monitoring alerts.
96 -
97 - Args:
98 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
99 -
100 - Returns:
101 - List[MonitoringAlertsRequestModel]: The list of monitoring alerts.
102 - """
103 - logger.info("Listing monitoring alerts")
104 -
105 - monitoring_alerts = await session.execute(select(MonitoringAlerts))
106 - monitoring_alerts = monitoring_alerts.scalars().all()
107 -
108 - return MonitoringAlertsResponseModel(
109 - monitoring_alerts=monitoring_alerts,
110 - success=True,
111 - message="Monitoring alerts retrieved successfully",
112 - )
113 -
114 -
115 -@monitoring_alerts_router.post(
116 - "/invoke/{monitoring_alert_id}",
117 - response_model=AlertAnalysisResponse,
118 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
119 -)
120 -async def invoke_monitoring_alert(
121 - monitoring_alert_id: int,
122 - session: AsyncSession = Depends(get_db),
123 -) -> AlertAnalysisResponse:
124 - """
125 - Invoke a monitoring alert.
126 -
127 - Args:
128 - monitoring_alert_id (int): The ID of the monitoring alert to invoke.
129 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
130 -
131 - Returns:
132 - AlertAnalysisResponse: The monitoring alert that was invoked.
133 - """
134 - logger.info(f"Invoking monitoring alert: {monitoring_alert_id}")
135 - monitoring_alert = await session.execute(select(MonitoringAlerts).where(MonitoringAlerts.id == monitoring_alert_id))
136 - monitoring_alert = monitoring_alert.scalars().first()
137 - logger.info(f"Found monitoring alert: {monitoring_alert}")
138 -
139 - if not monitoring_alert:
140 - raise HTTPException(status_code=404, detail="Monitoring alert not found")
141 -
142 - customer_meta = await get_customer_meta(monitoring_alert.customer_code, session)
143 -
144 - analyze_alert = ALERT_ANALYZERS.get(monitoring_alert.alert_source)
145 - logger.info(f"Found alert analyzer: {analyze_alert}")
146 -
147 - if analyze_alert:
148 - return await analyze_alert([monitoring_alert], customer_meta, session)
149 - else:
150 - logger.warning(f"Unknown alert source: {monitoring_alert.alert_source}")
151 -
152 - raise HTTPException(status_code=500, detail="Unknown alert source")
153 -
154 -
155 -@monitoring_alerts_router.delete(
156 - "/single_alert/{monitoring_alert_id}",
157 - response_model=MonitoringAlertsResponseModel,
158 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
159 -)
160 -async def delete_monitoring_alert(
161 - monitoring_alert_id: int,
162 - session: AsyncSession = Depends(get_db),
163 -) -> MonitoringAlertsResponseModel:
164 - """
165 - Delete a monitoring alert.
166 -
167 - Args:
168 - monitoring_alert_id (int): The ID of the monitoring alert to delete.
169 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
170 -
171 - Returns:
172 - MonitoringAlertsResponseModel: The monitoring alert that was deleted.
173 - """
174 - logger.info(f"Deleting monitoring alert: {monitoring_alert_id}")
175 - monitoring_alert = await session.execute(select(MonitoringAlerts).where(MonitoringAlerts.id == monitoring_alert_id))
176 - monitoring_alert = monitoring_alert.scalars().first()
177 - logger.info(f"Found monitoring alert: {monitoring_alert}")
178 -
179 - if not monitoring_alert:
180 - raise HTTPException(status_code=404, detail="Monitoring alert not found")
181 -
182 - await session.delete(monitoring_alert)
183 - await session.commit()
184 -
185 - return MonitoringAlertsResponseModel(
186 - monitoring_alerts=[monitoring_alert],
187 - success=True,
188 - message="Monitoring alert deleted successfully",
189 - )
190 -
191 -
192 -@monitoring_alerts_router.delete(
193 - "/purge",
194 - response_model=MonitoringAlertsResponseModel,
195 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
196 -)
197 -async def purge_monitoring_alerts(
198 - session: AsyncSession = Depends(get_db),
199 -) -> MonitoringAlertsResponseModel:
200 - """
201 - Purge all monitoring alerts.
202 -
203 - Args:
204 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
205 -
206 - Returns:
207 - MonitoringAlertsResponseModel: The monitoring alerts that were purged.
208 - """
209 - logger.info("Purging monitoring alerts")
210 -
211 - monitoring_alerts = await session.execute(select(MonitoringAlerts))
212 - monitoring_alerts = monitoring_alerts.scalars().all()
213 -
214 - if not monitoring_alerts:
215 - raise HTTPException(status_code=404, detail="No monitoring alerts found")
216 -
217 - await session.execute(delete(MonitoringAlerts))
218 - await session.commit()
219 -
220 - return MonitoringAlertsResponseModel(
221 - monitoring_alerts=monitoring_alerts,
222 - success=True,
223 - message="Monitoring alerts purged successfully",
224 - )
225 -
226 -
227 -@monitoring_alerts_router.post("/create", response_model=GraylogPostResponse)
228 -async def create_monitoring_alert(
229 - monitoring_alert: GraylogPostRequest,
230 - session: AsyncSession = Depends(get_db),
231 -) -> GraylogPostResponse:
232 - """
233 - Create a new monitoring alert. This receives the alert from Graylog and stores it in the database.
234 -
235 - Args:
236 - monitoring_alert (MonitoringAlertsRequestModel): The monitoring alert details.
237 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
238 -
239 - Returns:
240 - MonitoringAlertsRequestModel: The created monitoring alert.
241 - """
242 - logger.info(f"Creating monitoring alert: {monitoring_alert}")
243 - logger.info(f"Found index name {monitoring_alert.event.alert_index}")
244 -
245 - customer_meta = await session.execute(
246 - select(CustomersMeta).where(
247 - CustomersMeta.customer_code == monitoring_alert.event.fields["CUSTOMER_CODE"],
248 - ),
249 - )
250 - customer_meta = customer_meta.scalars().first()
251 -
252 - if not customer_meta:
253 - logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields['CUSTOMER_CODE']}")
254 - customer_meta = await session.execute(
255 - select(CustomersMeta).where(
256 - CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields["CUSTOMER_CODE"],
257 - ),
258 - )
259 - try:
260 - customer_meta = customer_meta.scalars().first()
261 - except Exception as e:
262 - logger.error(
263 - f"Error {e} getting customer meta for the customer_meta_office365_organization_id: {monitoring_alert.event.fields['CUSTOMER_CODE']}",
264 - )
265 - raise HTTPException(status_code=500, detail="Error getting customer meta")
266 -
267 - if not customer_meta:
268 - raise HTTPException(status_code=404, detail="Customer not found")
269 -
270 - try:
271 - monitoring_alert = MonitoringAlerts(
272 - alert_id=monitoring_alert.event.fields["ALERT_ID"],
273 - alert_index=monitoring_alert.event.alert_index,
274 - customer_code=monitoring_alert.event.fields["CUSTOMER_CODE"],
275 - alert_source=monitoring_alert.event.fields["ALERT_SOURCE"],
276 - )
277 - session.add(monitoring_alert)
278 - await session.commit()
279 - await session.refresh(monitoring_alert)
280 - except Exception as e:
281 - logger.error(f"Error creating monitoring alert: {e}")
282 - raise HTTPException(status_code=500, detail="Error creating monitoring alert")
283 -
284 - return GraylogPostResponse(
285 - success=True,
286 - message="Monitoring alert created successfully",
287 - )
288 -
289 -
290 -@monitoring_alerts_router.post(
291 - "/custom",
292 - response_model=GraylogPostResponse,
293 -)
294 -async def create_custom_monitoring_alert(
295 - monitoring_alert: GraylogPostRequest,
296 - session: AsyncSession = Depends(get_db),
297 -) -> GraylogPostResponse:
298 - """
299 - Create a new monitoring alert. This receives the alert from Graylog and stores it in the database.
300 -
301 - Args:
302 - monitoring_alert (GraylogPostRequest): The monitoring alert details.
303 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
304 -
305 - Returns:
306 - GraylogPostRequest: The created monitoring alert.
307 - """
308 - logger.info(f"Creating monitoring alert: {monitoring_alert}")
309 - logger.info(f"Found index name {monitoring_alert.event.alert_index}")
310 -
311 - for field in monitoring_alert.event.fields:
312 - if field == "CUSTOMER_CODE":
313 - customer_meta = await session.execute(
314 - select(CustomersMeta).where(
315 - CustomersMeta.customer_code == monitoring_alert.event.fields[field],
316 - ),
317 - )
318 - try:
319 - customer_meta = customer_meta.scalars().first()
320 - except Exception as e:
321 - logger.error(f"Error {e} getting customer meta for the customer_code: {monitoring_alert.event.fields[field]}")
322 -
323 - if not customer_meta:
324 - logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields[field]}")
325 - customer_meta = await session.execute(
326 - select(CustomersMeta).where(
327 - CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields[field],
328 - ),
329 - )
330 - try:
331 - customer_meta = customer_meta.scalars().first()
332 - except Exception as e:
333 - logger.error(
334 - f"Error {e} getting customer meta for the customer_meta_office365_organization_id: {monitoring_alert.event.fields[field]}",
335 - )
336 - raise HTTPException(status_code=500, detail="Error getting customer meta")
337 -
338 - if not customer_meta:
339 - raise HTTPException(status_code=404, detail="Customer not found")
340 -
341 - await analyze_custom_alert(monitoring_alert, session)
342 -
343 - return GraylogPostResponse(
344 - success=True,
345 - message="Monitoring alert created successfully",
346 - )
347 -
348 -
349 -@monitoring_alerts_router.post(
350 - "/run_analysis/wazuh",
351 - response_model=AlertAnalysisResponse,
352 -)
353 -async def run_wazuh_analysis(
354 - request: MonitoringWazuhAlertsRequestModel,
355 - session: AsyncSession = Depends(get_db),
356 -) -> AlertAnalysisResponse:
357 - """
358 - This route is used to run analysis on the monitoring alerts.
359 -
360 - 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
361 - and the alert_source is WAZUH.
362 -
363 - 2. Call the anlayze_wazuh_alerts function to analyze the alerts.
364 -
365 - Args:
366 - request (MonitoringWazuhAlertsRequestModel): The customer code.
367 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
368 -
369 - Returns:
370 - WazuhAnalysisResponse: The response containing the analysis results.
371 - """
372 - logger.info(f"Running analysis for customer_code: {request.customer_code}")
373 -
374 - customer_meta = await get_customer_meta(request.customer_code, session)
375 -
376 - monitoring_alerts = await session.execute(
377 - select(MonitoringAlerts).where(
378 - (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "WAZUH"),
379 - ),
380 - )
381 - monitoring_alerts = monitoring_alerts.scalars().all()
382 -
383 - logger.info(f"Found {len(monitoring_alerts)} monitoring alerts")
384 -
385 - if not monitoring_alerts:
386 - logger.info(f"No monitoring alerts found for customer_code: {request.customer_code}")
387 - return AlertAnalysisResponse(
388 - success=True,
389 - message="No monitoring alerts found",
390 - )
391 -
392 - # Call the analyze_wazuh_alerts function to analyze the alerts
393 - await analyze_wazuh_alerts(monitoring_alerts, customer_meta, session)
394 -
395 - return AlertAnalysisResponse(
396 - success=True,
397 - message="Analysis completed successfully",
398 - )
399 -
400 -
401 -@monitoring_alerts_router.post(
402 - "/run_analysis/suricata",
403 - response_model=AlertAnalysisResponse,
404 -)
405 -async def run_suricata_analysis(
406 - request: MonitoringWazuhAlertsRequestModel,
407 - session: AsyncSession = Depends(get_db),
408 -) -> AlertAnalysisResponse:
409 - """
410 - This route is used to run analysis on the monitoring alerts.
411 -
412 - 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
413 - and the alert_source is SURICATA.
414 -
415 - 2. Call the anlayze_wazuh_alerts function to analyze the alerts.
416 -
417 - Args:
418 - request (MonitoringWazuhAlertsRequestModel): The customer code.
419 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
420 -
421 - Returns:
422 - WazuhAnalysisResponse: The response containing the analysis results.
423 - """
424 - logger.info(f"Running analysis for customer_code: {request.customer_code}")
425 -
426 - customer_meta = await get_customer_meta(request.customer_code, session)
427 -
428 - monitoring_alerts = await session.execute(
429 - select(MonitoringAlerts).where(
430 - (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "SURICATA"),
431 - ),
432 - )
433 - monitoring_alerts = monitoring_alerts.scalars().all()
434 -
435 - logger.info(f"Found {len(monitoring_alerts)} monitoring alerts")
436 -
437 - if not monitoring_alerts:
438 - raise HTTPException(status_code=404, detail="No monitoring alerts found")
439 -
440 - # Call the analyze_wazuh_alerts function to analyze the alerts
441 - await analyze_suricata_alerts(monitoring_alerts, customer_meta, session)
442 -
443 - return AlertAnalysisResponse(
444 - success=True,
445 - message="Analysis completed successfully",
446 - )
447 -
448 -
449 -@monitoring_alerts_router.post(
450 - "/run_analysis/office365/exchange_online",
451 - response_model=AlertAnalysisResponse,
452 -)
453 -async def run_office365_exchange_online_analysis(
454 - request: MonitoringWazuhAlertsRequestModel,
455 - session: AsyncSession = Depends(get_db),
456 -) -> AlertAnalysisResponse:
457 - """
458 - This route is used to run analysis on the monitoring alerts.
459 -
460 - 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
461 - and the alert_source is OFFICE365_EXCHANGE_ONLINE.
462 -
463 - 2. Call the analyze_office365_exchange_online_alerts function to analyze the alerts.
464 -
465 - Args:
466 - request (MonitoringWazuhAlertsRequestModel): The customer code.
467 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
468 -
469 - Returns:
470 - WazuhAnalysisResponse: The response containing the analysis results.
471 - """
472 - logger.info(f"Running analysis for customer_code: {request.customer_code}")
473 -
474 - customer_meta = await get_customer_meta(request.customer_code, session)
475 -
476 - monitoring_alerts = await session.execute(
477 - select(MonitoringAlerts).where(
478 - (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "OFFICE365_EXCHANGE_ONLINE"),
479 - ),
480 - )
481 - monitoring_alerts = monitoring_alerts.scalars().all()
482 -
483 - logger.info(f"Found {len(monitoring_alerts)} monitoring alerts")
484 -
485 - if not monitoring_alerts:
486 - raise HTTPException(status_code=404, detail="No monitoring alerts found")
487 -
488 - # Call the analyze_office365_exchange_online_alerts function to analyze the alerts
489 - await analyze_office365_exchange_online_alerts(monitoring_alerts, customer_meta, session)
490 -
491 - return AlertAnalysisResponse(
492 - success=True,
493 - message="Analysis completed successfully",
494 - )
495 -
496 -
497 -@monitoring_alerts_router.post(
498 - "/run_analysis/office365/threat_intel",
499 - response_model=AlertAnalysisResponse,
500 -)
501 -async def run_office365_threat_intel_analysis(
502 - request: MonitoringWazuhAlertsRequestModel,
503 - session: AsyncSession = Depends(get_db),
504 -) -> AlertAnalysisResponse:
505 - """
506 - This route is used to run analysis on the monitoring alerts.
507 -
508 - 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
509 - and the alert_source is OFFICE365_THREAT_INTEL.
510 -
511 - 2. Call the analyze_office365_threatintel_alerts function to analyze the alerts.
512 -
513 - Args:
514 - request (MonitoringWazuhAlertsRequestModel): The customer code.
515 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
516 -
517 - Returns:
518 - WazuhAnalysisResponse: The response containing the analysis results.
519 - """
520 - logger.info(f"Running analysis for customer_code: {request.customer_code}")
521 -
522 - customer_meta = await get_customer_meta(request.customer_code, session)
523 -
524 - monitoring_alerts = await session.execute(
525 - select(MonitoringAlerts).where(
526 - (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "OFFICE365_THREAT_INTEL"),
527 - ),
528 - )
529 - monitoring_alerts = monitoring_alerts.scalars().all()
530 -
531 - logger.info(f"Found {len(monitoring_alerts)} monitoring alerts")
532 -
533 - if not monitoring_alerts:
534 - raise HTTPException(status_code=404, detail="No monitoring alerts found")
535 -
536 - # Call the analyze_office365_threatintel_alerts function to analyze the alerts
537 - await analyze_office365_threatintel_alerts(monitoring_alerts, customer_meta, session)
538 -
539 - return AlertAnalysisResponse(
540 - success=True,
541 - message="Analysis completed successfully",
542 - )
543 -
544 -
545 -@monitoring_alerts_router.post(
546 - "/run_analysis/sap_siem/suspicious_logins",
547 - response_model=AlertAnalysisResponse,
548 -)
549 -async def run_sap_siem_suspicious_logins_analysis(
550 - threshold: Optional[int] = 3,
551 - session: AsyncSession = Depends(get_db),
552 -) -> AlertAnalysisResponse:
553 - """
554 - This route is used to run analysis on the monitoring alerts.
555 -
556 - 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
557 - and the alert_source is SAP SIEM.
558 -
559 - 2. Call the sap_siem_suspicious_logins function to analyze the alerts.
560 -
561 - Args:
562 - request (CollectSapSiemRequest): The customer code.
563 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
564 -
565 - Returns:
566 - WazuhAnalysisResponse: The response containing the analysis results.
567 - """
568 - logger.info("Running analysis for SAP SIEM suspicious logins")
569 -
570 - # Call the analyze_wazuh_alerts function to analyze the alerts
571 - await sap_siem_suspicious_logins(threshold=threshold, session=session)
572 -
573 - return AlertAnalysisResponse(
574 - success=True,
575 - message="Analysis completed successfully",
576 - )
577 -
578 -
579 -@monitoring_alerts_router.post(
580 - "/run_analysis/sap_siem/multiple_logins",
581 - response_model=AlertAnalysisResponse,
582 -)
583 -async def run_sap_siem_multiple_logins_same_ip_analysis(
584 - threshold: Optional[int] = 0,
585 - time_range: Optional[int] = 10,
586 - session: AsyncSession = Depends(get_db),
587 -) -> AlertAnalysisResponse:
588 - """
589 - This route is used to run analysis on the monitoring alerts.
590 -
591 - 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
592 - and the alert_source is SAP SIEM.
593 -
594 - 2. Call the sap_siem_multiple_logins_same_ip function to analyze the alerts.
595 -
596 - Args:
597 - request (CollectSapSiemRequest): The customer code.
598 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
599 -
600 - Returns:
601 - WazuhAnalysisResponse: The response containing the analysis results.
602 - """
603 - logger.info("Running analysis for SAP SIEM multiple logins")
604 -
605 - # Call the analyze_wazuh_alerts function to analyze the alerts
606 - await sap_siem_multiple_logins_same_ip(threshold=threshold, time_range=time_range, session=session)
607 -
608 - return AlertAnalysisResponse(
609 - success=True,
610 - message="Analysis completed successfully",
611 - )
backend/app/integrations/monitoring_alert/routes/provision.py
+33 -32
@@ -5,12 +5,13 @@ from fastapi import Depends
5 from fastapi import HTTPException
6 from loguru import logger
7 from sqlalchemy.ext.asyncio import AsyncSession
8 +from sqlalchemy.future import select
9
10 from app.connectors.graylog.routes.events import get_all_event_definitions
11 from app.connectors.graylog.schema.events import GraylogEventDefinitionsResponse
12 from app.connectors.graylog.services.streams import get_streams
13 from app.db.db_session import get_db
13 -from app.integrations.monitoring_alert.routes.monitoring_alert import get_customer_meta
14 +from app.db.universal_models import CustomersMeta
15 from app.integrations.monitoring_alert.schema.provision import AvailableMonitoringAlerts
16 from app.integrations.monitoring_alert.schema.provision import (
17 AvailableMonitoringAlertsResponse,
@@ -39,12 +40,41 @@ from app.integrations.monitoring_alert.services.provision import (
40 )
41 from app.integrations.utils.event_shipper import event_shipper
42 from app.integrations.utils.schema import EventShipperPayload
42 -from app.schedulers.models.scheduler import CreateSchedulerRequest
43 -from app.schedulers.scheduler import add_scheduler_jobs
43
44 monitoring_alerts_provision_router = APIRouter()
45
46
47 +async def get_customer_meta(customer_code: str, session: AsyncSession) -> CustomersMeta:
48 + """
49 + Get the customer meta for the given customer_code.
50 +
51 + Args:
52 + customer_code (str): The customer code.
53 + session (AsyncSession): The database session.
54 +
55 + Returns:
56 + CustomersMeta: The customer meta.
57 + """
58 + logger.info(f"Getting customer meta for customer_code: {customer_code}")
59 +
60 + customer_meta = await session.execute(
61 + select(CustomersMeta).where(CustomersMeta.customer_code == customer_code),
62 + )
63 + customer_meta = customer_meta.scalars().first()
64 +
65 + if not customer_meta:
66 + logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {customer_code}")
67 + customer_meta = await session.execute(
68 + select(CustomersMeta).where(CustomersMeta.customer_meta_office365_organization_id == customer_code),
69 + )
70 + customer_meta = customer_meta.scalars().first()
71 +
72 + if not customer_meta:
73 + raise HTTPException(status_code=404, detail="Customer not found")
74 +
75 + return customer_meta
76 +
77 +
78 async def return_stream_ids(stream_names: List[str]) -> List[str]:
79 """
80 Return the stream IDs for the given stream names.
@@ -68,14 +98,6 @@ async def invoke_provision_wazuh_monitoring_alert(
98 ):
99 # Provision the Wazuh monitoring alert
100 await provision_wazuh_monitoring_alert(request)
71 - # ! No longer needed since we have the invoke_alert_creation_collect scheduled job ! #
72 - # await add_scheduler_jobs(
73 - # CreateSchedulerRequest(
74 - # function_name="invoke_wazuh_monitoring_alert",
75 - # time_interval=5,
76 - # job_id="invoke_wazuh_monitoring_alert",
77 - # ),
78 - # )
101
102
103 async def invoke_provision_suricata_monitoring_alert(
@@ -83,13 +105,6 @@ async def invoke_provision_suricata_monitoring_alert(
105 ):
106 # Provision the Suricata monitoring alert
107 await provision_suricata_monitoring_alert(request)
86 - await add_scheduler_jobs(
87 - CreateSchedulerRequest(
88 - function_name="invoke_suricata_monitoring_alert",
89 - time_interval=5,
90 - job_id="invoke_suricata_monitoring_alert",
91 - ),
92 - )
108
109
110 async def invoke_provision_office365_exchange_online_alert(
@@ -97,13 +112,6 @@ async def invoke_provision_office365_exchange_online_alert(
112 ):
113 # Provision the Office365 Exchange Online monitoring alert
114 await provision_office365_exchange_online_alert(request)
100 - await add_scheduler_jobs(
101 - CreateSchedulerRequest(
102 - function_name="invoke_office365_exchange_online_alert",
103 - time_interval=5,
104 - job_id="invoke_office365_exchange_online_alert",
105 - ),
106 - )
115
116
117 async def invoke_provision_office365_threat_intel_alert(
@@ -111,13 +119,6 @@ async def invoke_provision_office365_threat_intel_alert(
119 ):
120 # Provision the Office365 Threat Intel monitoring alert
121 await provision_office365_threat_intel_alert(request)
114 - await add_scheduler_jobs(
115 - CreateSchedulerRequest(
116 - function_name="invoke_office365_threat_intel_alert",
117 - time_interval=5,
118 - job_id="invoke_office365_threat_intel_alert",
119 - ),
120 - )
122
123
124 async def invoke_provision_custom_monitoring_alert(
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py deleted
-1098
@@ -1,1098 +0,0 @@
1 -from enum import Enum
2 -from typing import 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
9 -from pydantic import Extra
10 -from pydantic import Field
11 -from pydantic import validator
12 -
13 -from app.integrations.alert_creation.general.schema.alert import IrisAsset
14 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
15 -
16 -
17 -class WazuhSourceFieldsToRemove(Enum):
18 - GL2 = "gl2"
19 - RULE_MITRE_TACTIC = "rule_mitre_tactic"
20 - RULE_MITRE_ID = "rule_mitre_id"
21 - RULE_MITRE_TECHNIQUE = "rule_mitre_technique"
22 - RULE_ID = "rule_id"
23 - MESSAGE = "message"
24 - # Add more fields as needed
25 -
26 -
27 -class MonitoringAlertsRequestModel(BaseModel):
28 - id: Optional[int] = None
29 - alert_id: str
30 - alert_index: str
31 - customer_code: str
32 - alert_source: str
33 -
34 - class Config:
35 - orm_mode = True
36 -
37 -
38 -class MonitoringAlertsResponseModel(BaseModel):
39 - success: bool
40 - message: str
41 - monitoring_alerts: List[MonitoringAlertsRequestModel]
42 -
43 -
44 -class MonitoringWazuhAlertsRequestModel(BaseModel):
45 - customer_code: str
46 -
47 -
48 -class GraylogEventFields(BaseModel):
49 - ALERT_ID: str = Field(
50 - ...,
51 - description="Unique identifier for the alert",
52 - example="65f6a260-c1f3-11ee-93bc-86000046278a",
53 - )
54 - ALERT_SOURCE: str = Field(..., description="Source of the alert", example="WAZUH")
55 - CUSTOMER_CODE: str = Field(
56 - ...,
57 - description="Customer code associated with the alert",
58 - example="00002",
59 - )
60 -
61 -
62 -class GraylogEvent(BaseModel):
63 - id: str = Field(
64 - ...,
65 - description="Unique identifier for the event",
66 - example="01HNNF2YCM5SSV3KDQJSRK0EV0",
67 - )
68 - event_definition_type: str = Field(
69 - ...,
70 - description="Type of event definition",
71 - example="aggregation-v1",
72 - )
73 - event_definition_id: str = Field(
74 - ...,
75 - description="Identifier for the event definition",
76 - example="65bd28505e9a2d550cf521e7",
77 - )
78 - origin_context: str = Field(
79 - ...,
80 - description="Context from which the event originated",
81 - example="urn:graylog:message:es:wazuh_00002_290:65f6a260-c1f3-11ee-93bc-86000046278a",
82 - )
83 - timestamp: str = Field(
84 - ...,
85 - description="Timestamp when the event occurred",
86 - example="2024-02-02T17:49:22.694Z",
87 - )
88 - timestamp_processing: str = Field(
89 - ...,
90 - description="Timestamp when the event was processed",
91 - example="2024-02-02T17:50:26.708Z",
92 - )
93 - timerange_start: Optional[str] = Field(
94 - None,
95 - description="Start of the timerange for the event",
96 - example=None,
97 - )
98 - timerange_end: Optional[str] = Field(
99 - None,
100 - description="End of the timerange for the event",
101 - example=None,
102 - )
103 - streams: List[str] = Field(
104 - ...,
105 - description="List of streams associated with the event",
106 - example=[],
107 - )
108 - source_streams: List[str] = Field(
109 - ...,
110 - description="List of source streams for the event",
111 - example=["645a3a6123e5cc30bbc0e5dc"],
112 - )
113 - message: str = Field(
114 - ...,
115 - description="Message associated with the event",
116 - example="COPILOT TESTING WAZUH",
117 - )
118 - source: str = Field(..., description="Source of the event", example="ASHGRL02")
119 - key_tuple: List[str] = Field(
120 - ...,
121 - description="Tuple keys associated with the event",
122 - example=[],
123 - )
124 - key: str = Field(..., description="Key associated with the event", example="")
125 - priority: int = Field(..., description="Priority of the event", example=2)
126 - alert: bool = Field(
127 - ...,
128 - description="Indicates if the event is an alert",
129 - example=True,
130 - )
131 - # fields: GraylogEventFields = Field(..., description="Custom fields for the event")
132 - fields: Dict[str, Any] = Field(..., description="Custom fields for the event")
133 - group_by_fields: Dict[str, Any] = Field(
134 - ...,
135 - description="Fields used to group events",
136 - example={},
137 - )
138 -
139 - @property
140 - def alert_index(self) -> str:
141 - return self.origin_context.split(":")[4]
142 -
143 - @property
144 - def alert_id(self) -> str:
145 - return self.origin_context.split(":")[5]
146 -
147 - @validator("fields")
148 - def check_customer_code(cls, fields):
149 - if "CUSTOMER_CODE" not in fields:
150 - raise HTTPException(
151 - status_code=400,
152 - detail="CUSTOMER_CODE is required in the fields",
153 - )
154 - return fields
155 -
156 -
157 -class GraylogPostRequest(BaseModel):
158 - event_definition_id: str = Field(
159 - ...,
160 - description="Identifier for the event definition",
161 - example="65bd28505e9a2d550cf521e7",
162 - )
163 - event_definition_type: str = Field(
164 - ...,
165 - description="Type of the event definition",
166 - example="aggregation-v1",
167 - )
168 - event_definition_title: str = Field(
169 - ...,
170 - description="Title of the event definition",
171 - example="COPILOT TESTING WAZUH",
172 - )
173 - event_definition_description: Optional[str] = Field(
174 - None,
175 - description="Description of the event definition",
176 - example="",
177 - )
178 - job_definition_id: str = Field(
179 - ...,
180 - description="Identifier for the job definition",
181 - example="65bd284b5e9a2d550cf521dc",
182 - )
183 - job_trigger_id: str = Field(
184 - ...,
185 - description="Identifier for the job trigger",
186 - example="65bd2b625e9a2d550cf528e4",
187 - )
188 - event: GraylogEvent = Field(..., description="Event details")
189 - backlog: List[str] = Field(
190 - ...,
191 - description="List of backlog items associated with the event",
192 - example=[],
193 - )
194 -
195 -
196 -class GraylogPostResponse(BaseModel):
197 - success: bool = Field(
198 - ...,
199 - description="Indicates if the request was successful",
200 - example=True,
201 - )
202 - message: str = Field(
203 - ...,
204 - description="Message associated with the response",
205 - example="Event processed successfully",
206 - )
207 -
208 -
209 -class AlertAnalysisResponse(BaseModel):
210 - success: bool = Field(
211 - ...,
212 - description="Indicates if the request was successful",
213 - example=True,
214 - )
215 - message: str = Field(
216 - ...,
217 - description="Message associated with the response",
218 - example="Analysis completed successfully",
219 - )
220 -
221 -
222 -# ! Wazuh Indexer Schema ! #
223 -class WazuhSourceModel(BaseModel):
224 - agent_name: str = Field(..., description="The name of the agent.")
225 - agent_id: str = Field(..., description="The id of the agent.")
226 - agent_labels_customer: str = Field(..., description="The customer of the agent.")
227 - rule_id: str = Field(..., description="The id of the rule.")
228 - rule_level: int = Field(..., description="The level of the rule.")
229 - rule_description: str = Field(..., description="The description of the rule.")
230 - timestamp: str = Field(..., description="The timestamp of the alert.")
231 - process_id: Optional[str] = Field(
232 - "n/a",
233 - description="The process id of the alert.",
234 - )
235 - timestamp_utc: Optional[str] = Field(
236 - None,
237 - description="The UTC timestamp of the alert.",
238 - )
239 - process_image: Optional[str] = Field(
240 - "n/a",
241 - description="The process image of the alert.",
242 - )
243 - data_win_eventdata_image: Optional[str] = Field(
244 - "n/a",
245 - description="The image of the event data.",
246 - )
247 -
248 - class Config:
249 - extra = Extra.allow
250 -
251 -
252 -class WazuhAlertModel(BaseModel):
253 - _index: str
254 - _id: str
255 - _version: int
256 - _source: WazuhSourceModel
257 - asset_type_id: Optional[int] = Field(
258 - None,
259 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
260 - )
261 - ioc_value: Optional[str] = Field(
262 - None,
263 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
264 - )
265 - ioc_type: Optional[str] = Field(
266 - None,
267 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
268 - )
269 -
270 - class Config:
271 - extra = Extra.allow
272 -
273 - def to_dict(self):
274 - return self.dict(exclude_none=True)
275 -
276 -
277 -class SortOrder(Enum):
278 - desc = "desc"
279 - asc = "asc"
280 -
281 -
282 -class FilterAlertsRequest(BaseModel):
283 - per_page: int = Field(1000, description="The number of alerts to return per page.")
284 - page: int = Field(1, description="The page number to return.")
285 - sort: SortOrder = Field(
286 - SortOrder.desc,
287 - description="The sort order for the alerts.",
288 - )
289 - alert_tags: str = Field(..., description="The tags of the alert.")
290 - alert_status_id: int = Field(
291 - 3,
292 - description="The status of the alert. Default to assigned.",
293 - example=3,
294 - )
295 - alert_customer_id: int = Field(
296 - ...,
297 - description="The customer id of the alert.",
298 - example=1,
299 - )
300 -
301 -
302 -class WazuhIrisAlertContext(BaseModel):
303 - customer_iris_id: int = Field(
304 - ...,
305 - description="IRIS ID of the customer",
306 - example=1,
307 - )
308 - customer_name: str = Field(
309 - ...,
310 - description="Name of the customer",
311 - example="SOCFortress",
312 - )
313 - customer_cases_index: str = Field(
314 - ...,
315 - description="IRIS case index name in the Wazuh-Indexer",
316 - example="dfir_iris_00001",
317 - )
318 - alert_name: str = Field(
319 - ...,
320 - description="Name of the alert",
321 - example="Intrusion Detected",
322 - )
323 - alert_level: int = Field(..., description="Severity level of the alert", example=3)
324 - rule_id: str = Field(
325 - ...,
326 - description="ID of the rule that triggered the alert",
327 - example="2001",
328 - )
329 - rule_mitre_id: Optional[str] = Field(
330 - "n/a",
331 - description="MITRE ATT&CK ID of the rule",
332 - example="T1234",
333 - )
334 - rule_mitre_tactic: Optional[str] = Field(
335 - "n/a",
336 - description="MITRE ATT&CK Tactic",
337 - example="Execution",
338 - )
339 - rule_mitre_technique: Optional[str] = Field(
340 - "n/a",
341 - description="MITRE ATT&CK Technique",
342 - example="Scripting",
343 - )
344 - process_name: Optional[List[str]] = Field(
345 - example=["No process name found"],
346 - description="Name of the process",
347 - )
348 -
349 - class Config:
350 - extra = Extra.allow
351 -
352 -
353 -class WazuhIrisAlertPayload(BaseModel):
354 - alert_title: str = Field(
355 - ...,
356 - description="Title of the alert",
357 - example="Intrusion Detected",
358 - )
359 - alert_description: str = Field(
360 - ...,
361 - description="Description of the alert",
362 - example="Intrusion Detected by Firewall",
363 - )
364 - alert_source: str = Field(..., description="Source of the alert", example="Wazuh")
365 - assets: List[IrisAsset] = Field(..., description="List of affected assets")
366 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
367 - alert_severity_id: int = Field(
368 - ...,
369 - description="Severity ID of the alert",
370 - example=5,
371 - )
372 - alert_customer_id: int = Field(
373 - ...,
374 - description="Customer ID related to the alert",
375 - example=1,
376 - )
377 - alert_source_content: Dict[str, Any] = Field(
378 - ...,
379 - description="Original content from the alert source",
380 - )
381 - alert_context: WazuhIrisAlertContext = Field(
382 - ...,
383 - description="Contextual information about the alert",
384 - )
385 - alert_iocs: Optional[List[IrisIoc]] = Field(
386 - None,
387 - description="List of IoCs related to the alert",
388 - )
389 - alert_source_event_time: str = Field(
390 - ...,
391 - description="Timestamp of the alert",
392 - example="2021-01-01T00:00:00.000Z",
393 - )
394 -
395 - def to_dict(self):
396 - return self.dict(exclude_none=True)
397 -
398 -
399 -########### ! CUSTOM ALERTS SCHEMA ! ###########
400 -class CustomSourceModel(BaseModel):
401 - timestamp: str = Field(..., description="The timestamp of the alert.")
402 - timestamp_utc: Optional[str] = Field(
403 - ...,
404 - description="The UTC timestamp of the alert.",
405 - )
406 - time_field: Optional[str] = Field(
407 - "timestamp",
408 - description="The timefield of the alert to be used when creating the IRIS alert.",
409 - )
410 - date: Optional[float] = Field(
411 - None,
412 - description="Date of the alert in Unix timestamp",
413 - )
414 - alert_metadata_tag: Optional[str] = Field(
415 - None,
416 - description="Metadata tag for the alert",
417 - )
418 - alert_gid: Optional[int] = Field(None, description="Alert group ID")
419 -
420 - class Config:
421 - allow_population_by_field_name = True
422 - extra = Extra.allow
423 -
424 - def to_dict(self):
425 - return self.dict(exclude_none=True)
426 -
427 -
428 -class CustomAlertModel(BaseModel):
429 - _index: str
430 - _id: str
431 - _version: int
432 - _source: CustomSourceModel
433 - asset_type_id: Optional[int] = Field(
434 - None,
435 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
436 - )
437 - ioc_value: Optional[str] = Field(
438 - None,
439 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
440 - )
441 - ioc_type: Optional[str] = Field(
442 - None,
443 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
444 - )
445 -
446 - class Config:
447 - extra = Extra.allow
448 -
449 - def to_dict(self):
450 - return self.dict(exclude_none=True)
451 -
452 -
453 -########### ! Create Custom Alerts In IRIS Schemas ! ###########
454 -class CustomIrisAsset(BaseModel):
455 - asset_name: Optional[str] = Field(
456 - "Asset Does Not Apply to Custom Alerts",
457 - description="Name of the asset",
458 - example="Server01",
459 - )
460 - asset_ip: Optional[str] = Field(
461 - "Asset Does Not Apply to Custom Alerts",
462 - description="IP address of the asset",
463 - example="192.168.1.1",
464 - )
465 - asset_description: Optional[str] = Field(
466 - "Asset Does Not Apply to Custom Alerts",
467 - description="Description of the asset",
468 - example="Windows Server",
469 - )
470 - asset_type_id: Optional[int] = Field(
471 - 9,
472 - description="Type ID of the asset",
473 - example=1,
474 - )
475 -
476 - def to_dict(self):
477 - return self.dict(exclude_none=True)
478 -
479 -
480 -class CustomIrisIoc(BaseModel):
481 - ioc_value: str = Field(
482 - ...,
483 - description="Value of the IoC",
484 - example="www.google.com",
485 - )
486 - ioc_description: str = Field(
487 - ...,
488 - description="Description of the IoC",
489 - example="Google",
490 - )
491 - ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
492 - ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
493 -
494 -
495 -class CustomIrisAlertContext(Dict[str, Any]):
496 - _source: CustomSourceModel
497 - alert_id: str = Field(..., description="ID of the alert", example="123")
498 - alert_name: str = Field(
499 - ...,
500 - description="Name of the alert",
501 - example="Intrusion Detected",
502 - )
503 - customer_iris_id: Optional[int] = Field(
504 - None,
505 - description="IRIS ID of the customer",
506 - )
507 - customer_name: Optional[str] = Field(
508 - None,
509 - description="Name of the customer",
510 - )
511 - customer_cases_index: Optional[str] = Field(
512 - None,
513 - description="IRIS case index name in the Wazuh-Indexer",
514 - )
515 - time_field: Optional[str] = Field(
516 - "timestamp_utc",
517 - description="The timefield of the alert to be used when creating the IRIS alert.",
518 - )
519 -
520 - def to_dict(self):
521 - return self.dict(exclude_none=True)
522 -
523 -
524 -class CustomIrisAlertPayload(BaseModel):
525 - alert_title: str = Field(
526 - ...,
527 - description="Title of the alert",
528 - example="Intrusion Detected",
529 - )
530 - alert_description: str = Field(
531 - ...,
532 - description="Description of the alert",
533 - example="Intrusion Detected by Firewall",
534 - )
535 - alert_source: str = Field(..., description="Source of the alert", example="Suricata")
536 - assets: List[CustomIrisAsset] = Field(..., description="List of affected assets")
537 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
538 - alert_severity_id: int = Field(
539 - ...,
540 - description="Severity ID of the alert",
541 - example=5,
542 - )
543 - alert_customer_id: int = Field(
544 - ...,
545 - description="Customer ID related to the alert",
546 - example=1,
547 - )
548 - alert_source_content: Dict[str, Any] = Field(
549 - ...,
550 - description="Original content from the alert source",
551 - )
552 - alert_context: CustomIrisAlertContext = Field(
553 - ...,
554 - description="Contextual information about the alert",
555 - )
556 - alert_iocs: Optional[List[IrisIoc]] = Field(
557 - None,
558 - description="List of IoCs related to the alert",
559 - )
560 - alert_source_event_time: str = Field(
561 - ...,
562 - description="Timestamp of the alert",
563 - example="2021-01-01T00:00:00.000Z",
564 - )
565 -
566 - def to_dict(self):
567 - return self.dict(exclude_none=True)
568 -
569 -
570 -########### ! SURICATA ALERTS SCHEMA ! ###########
571 -class SuricataSourceModel(BaseModel):
572 - alert_signature: str = Field(..., description="Signature of the alert")
573 - alert_severity: int = Field(..., description="Severity level of the alert")
574 - alert_signature_id: int = Field(..., description="Signature ID of the alert")
575 - src_ip: str = Field(..., description="Source IP address")
576 - dest_ip: str = Field(..., description="Destination IP address")
577 - app_proto: str = Field(..., description="Application protocol")
578 - agent_labels_customer: str = Field(..., description="Customer of the agent")
579 - timestamp: str = Field(..., description="The timestamp of the alert.")
580 - timestamp_utc: Optional[str] = Field(
581 - ...,
582 - description="The UTC timestamp of the alert.",
583 - )
584 - time_field: Optional[str] = Field(
585 - "timestamp",
586 - description="The timefield of the alert to be used when creating the IRIS alert.",
587 - )
588 - date: Optional[float] = Field(
589 - None,
590 - description="Date of the alert in Unix timestamp",
591 - )
592 - alert_metadata_tag: Optional[str] = Field(
593 - None,
594 - description="Metadata tag for the alert",
595 - )
596 - alert_gid: Optional[int] = Field(None, description="Alert group ID")
597 -
598 - class Config:
599 - allow_population_by_field_name = True
600 - extra = Extra.allow
601 -
602 - def to_dict(self):
603 - return self.dict(exclude_none=True)
604 -
605 -
606 -class SuricataAlertModel(BaseModel):
607 - _index: str
608 - _id: str
609 - _version: int
610 - _source: SuricataSourceModel
611 - asset_type_id: Optional[int] = Field(
612 - None,
613 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
614 - )
615 - ioc_value: Optional[str] = Field(
616 - None,
617 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
618 - )
619 - ioc_type: Optional[str] = Field(
620 - None,
621 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
622 - )
623 -
624 - class Config:
625 - extra = Extra.allow
626 -
627 -
628 -########### ! Create Suricata Alerts In IRIS Schemas ! ###########
629 -class SuricataIrisAsset(BaseModel):
630 - asset_name: Optional[str] = Field(
631 - "Asset Does Not Apply to Suricata Alerts",
632 - description="Name of the asset",
633 - example="Server01",
634 - )
635 - asset_ip: Optional[str] = Field(
636 - "Asset Does Not Apply to Suricata Alerts",
637 - description="IP address of the asset",
638 - example="192.168.1.1",
639 - )
640 - asset_description: Optional[str] = Field(
641 - "Asset Does Not Apply to Suricata Alerts",
642 - description="Description of the asset",
643 - example="Windows Server",
644 - )
645 - asset_type_id: Optional[int] = Field(
646 - 9,
647 - description="Type ID of the asset",
648 - example=1,
649 - )
650 -
651 - def to_dict(self):
652 - return self.dict(exclude_none=True)
653 -
654 -
655 -class SuricataIrisIoc(BaseModel):
656 - ioc_value: str = Field(
657 - ...,
658 - description="Value of the IoC",
659 - example="www.google.com",
660 - )
661 - ioc_description: str = Field(
662 - ...,
663 - description="Description of the IoC",
664 - example="Google",
665 - )
666 - ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
667 - ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
668 -
669 -
670 -class SuricataIrisAlertContext(BaseModel):
671 - _source: SuricataSourceModel
672 - alert_id: str = Field(..., description="ID of the alert", example="123")
673 - alert_name: str = Field(
674 - ...,
675 - description="Name of the alert",
676 - example="Intrusion Detected",
677 - )
678 - alert_level: int = Field(..., description="Severity level of the alert", example=3)
679 - rule_id: int = Field(
680 - ...,
681 - description="ID of the Suricata rule that triggered the alert",
682 - example="2001",
683 - )
684 - src_ip: str = Field(
685 - ...,
686 - description="Source IP address of the alert",
687 - example="1.1.1.1",
688 - )
689 - dest_ip: str = Field(
690 - ...,
691 - description="Destination IP address of the alert",
692 - example="8.8.8.8",
693 - )
694 - app_proto: str = Field(
695 - ...,
696 - description="Application protocol of the alert",
697 - example="TCP",
698 - )
699 - agent_labels_customer: str = Field(
700 - ...,
701 - description="Customer of the endpoint",
702 - example="SOCFortress",
703 - )
704 - customer_iris_id: Optional[int] = Field(
705 - None,
706 - description="IRIS ID of the customer",
707 - )
708 - customer_name: Optional[str] = Field(
709 - None,
710 - description="Name of the customer",
711 - )
712 - customer_cases_index: Optional[str] = Field(
713 - None,
714 - description="IRIS case index name in the Wazuh-Indexer",
715 - )
716 - time_field: Optional[str] = Field(
717 - "timestamp_utc",
718 - description="The timefield of the alert to be used when creating the IRIS alert.",
719 - )
720 -
721 - def to_dict(self):
722 - return self.dict(exclude_none=True)
723 -
724 -
725 -class SuricataIrisAlertPayload(BaseModel):
726 - alert_title: str = Field(
727 - ...,
728 - description="Title of the alert",
729 - example="Intrusion Detected",
730 - )
731 - alert_description: str = Field(
732 - ...,
733 - description="Description of the alert",
734 - example="Intrusion Detected by Firewall",
735 - )
736 - alert_source: str = Field(..., description="Source of the alert", example="Suricata")
737 - assets: List[SuricataIrisAsset] = Field(..., description="List of affected assets")
738 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
739 - alert_severity_id: int = Field(
740 - ...,
741 - description="Severity ID of the alert",
742 - example=5,
743 - )
744 - alert_customer_id: int = Field(
745 - ...,
746 - description="Customer ID related to the alert",
747 - example=1,
748 - )
749 - alert_source_content: Dict[str, Any] = Field(
750 - ...,
751 - description="Original content from the alert source",
752 - )
753 - alert_context: SuricataIrisAlertContext = Field(
754 - ...,
755 - description="Contextual information about the alert",
756 - )
757 - alert_iocs: Optional[List[IrisIoc]] = Field(
758 - None,
759 - description="List of IoCs related to the alert",
760 - )
761 - alert_source_event_time: str = Field(
762 - ...,
763 - description="Timestamp of the alert",
764 - example="2021-01-01T00:00:00.000Z",
765 - )
766 -
767 - def to_dict(self):
768 - return self.dict(exclude_none=True)
769 -
770 -
771 -########### ! Office365 Exchange ALERTS SCHEMA ! ###########
772 -class Office365ExchangeSourceModel(BaseModel):
773 - client_ip: Optional[str] = Field("Not found", description="Client IP address")
774 - operation: Optional[str] = Field("Not found", description="Operation")
775 - creation_time: Optional[str] = Field("Not found", description="Creation time")
776 - office365_id: str = Field(..., description="Office365 ID")
777 - organization_name: str = Field(..., description="Organization name")
778 - user_id: str = Field(..., description="User ID")
779 - workload: str = Field(..., description="Workload")
780 - organization_id: str = Field(..., description="Organization ID")
781 - timestamp: str = Field(..., description="The timestamp of the alert.")
782 - timestamp_utc: Optional[str] = Field(
783 - ...,
784 - description="The UTC timestamp of the alert.",
785 - )
786 - time_field: Optional[str] = Field(
787 - "timestamp",
788 - description="The timefield of the alert to be used when creating the IRIS alert.",
789 - )
790 - date: Optional[float] = Field(
791 - None,
792 - description="Date of the alert in Unix timestamp",
793 - )
794 - rule_description: str = Field(
795 - ...,
796 - description="Description of the rule",
797 - )
798 -
799 - class Config:
800 - allow_population_by_field_name = True
801 - extra = Extra.allow
802 -
803 - def to_dict(self):
804 - return self.dict(exclude_none=True)
805 -
806 -
807 -class Office365ExchangeAlertModel(BaseModel):
808 - _index: str
809 - _id: str
810 - _version: int
811 - _source: Office365ExchangeSourceModel
812 - asset_type_id: Optional[int] = Field(
813 - None,
814 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
815 - )
816 - ioc_value: Optional[str] = Field(
817 - None,
818 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
819 - )
820 - ioc_type: Optional[str] = Field(
821 - None,
822 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
823 - )
824 -
825 - class Config:
826 - extra = Extra.allow
827 -
828 -
829 -########### ! Create Office365 Exchange Alerts In IRIS Schemas ! ###########
830 -class Office365ExchangeIrisAsset(BaseModel):
831 - asset_name: Optional[str] = Field(
832 - "Asset Does Not Apply to Office365 Exchange Alerts",
833 - description="Name of the asset",
834 - example="test@socfortress.co",
835 - )
836 - asset_description: Optional[str] = Field(
837 - "Asset Does Not Apply to Office365 Exchange Alerts",
838 - description="Description of the asset",
839 - example="Windows Server",
840 - )
841 - asset_type_id: Optional[int] = Field(
842 - 1,
843 - description="Type ID of the asset",
844 - example=1,
845 - )
846 -
847 - def to_dict(self):
848 - return self.dict(exclude_none=True)
849 -
850 -
851 -class Office365ExchangeIrisAlertContext(BaseModel):
852 - _source: Office365ExchangeSourceModel = Field(..., description="Source of the alert")
853 - client_ip: Optional[str] = Field("Not found", description="Client IP address")
854 - operation: Optional[str] = Field("Not found", description="Operation")
855 - creation_time: Optional[str] = Field("Not found", description="Creation time")
856 - office365_id: str = Field(..., description="Office365 ID")
857 - organization_name: str = Field(..., description="Organization name")
858 - user_id: str = Field(..., description="User ID")
859 - workload: str = Field(..., description="Workload")
860 - organization_id: str = Field(..., description="Organization ID")
861 - customer_iris_id: Optional[int] = Field(
862 - None,
863 - description="IRIS ID of the customer",
864 - )
865 - customer_name: Optional[str] = Field(
866 - None,
867 - description="Name of the customer",
868 - )
869 - customer_cases_index: Optional[str] = Field(
870 - None,
871 - description="IRIS case index name in the Wazuh-Indexer",
872 - )
873 - time_field: Optional[str] = Field(
874 - "timestamp_utc",
875 - description="The timefield of the alert to be used when creating the IRIS alert.",
876 - )
877 - rule_description: str = Field(
878 - ...,
879 - description="Description of the rule",
880 - )
881 - rule_id: str = Field(
882 - ...,
883 - description="ID of the rule that triggered the alert",
884 - example="2001",
885 - )
886 -
887 - def to_dict(self):
888 - return self.dict(exclude_none=True)
889 -
890 -
891 -class Office365ExchangeIrisAlertPayload(BaseModel):
892 - alert_title: str = Field(
893 - ...,
894 - description="Title of the alert",
895 - example="Intrusion Detected",
896 - )
897 - alert_description: str = Field(
898 - ...,
899 - description="Description of the alert",
900 - example="Intrusion Detected by Firewall",
901 - )
902 - alert_source: str = Field(..., description="Source of the alert", example="Suricata")
903 - assets: List[Office365ExchangeIrisAsset] = Field(..., description="List of affected assets")
904 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
905 - alert_severity_id: int = Field(
906 - ...,
907 - description="Severity ID of the alert",
908 - example=5,
909 - )
910 - alert_customer_id: int = Field(
911 - ...,
912 - description="Customer ID related to the alert",
913 - example=1,
914 - )
915 - alert_source_content: Dict[str, Any] = Field(
916 - ...,
917 - description="Original content from the alert source",
918 - )
919 - alert_context: Office365ExchangeIrisAlertContext = Field(
920 - ...,
921 - description="Contextual information about the alert",
922 - )
923 - alert_iocs: Optional[List[IrisIoc]] = Field(
924 - None,
925 - description="List of IoCs related to the alert",
926 - )
927 - alert_source_event_time: str = Field(
928 - ...,
929 - description="Timestamp of the alert",
930 - example="2021-01-01T00:00:00.000Z",
931 - )
932 -
933 - def to_dict(self):
934 - return self.dict(exclude_none=True)
935 -
936 -
937 -########### ! Office365 Threat Intel ALERTS SCHEMA ! ###########
938 -class Office365ThreatIntelSourceModel(BaseModel):
939 - sender_ip: Optional[str] = Field("Not found", description="Sender IP address")
940 - operation: Optional[str] = Field("Not found", description="Operation")
941 - creation_time: Optional[str] = Field("Not found", description="Creation time")
942 - office365_id: str = Field(..., description="Office365 ID")
943 - recipients: str = Field(..., description="Recipients")
944 - workload: str = Field(..., description="Workload")
945 - organization_id: str = Field(..., description="Organization ID")
946 - timestamp: str = Field(..., description="The timestamp of the alert.")
947 - timestamp_utc: Optional[str] = Field(
948 - ...,
949 - description="The UTC timestamp of the alert.",
950 - )
951 - time_field: Optional[str] = Field(
952 - "timestamp",
953 - description="The timefield of the alert to be used when creating the IRIS alert.",
954 - )
955 - date: Optional[float] = Field(
956 - None,
957 - description="Date of the alert in Unix timestamp",
958 - )
959 - rule_description: str = Field(
960 - ...,
961 - description="Description of the rule",
962 - )
963 -
964 - class Config:
965 - allow_population_by_field_name = True
966 - extra = Extra.allow
967 -
968 - def to_dict(self):
969 - return self.dict(exclude_none=True)
970 -
971 -
972 -class Office365ThreatIntelAlertModel(BaseModel):
973 - _index: str
974 - _id: str
975 - _version: int
976 - _source: Office365ThreatIntelSourceModel
977 - asset_type_id: Optional[int] = Field(
978 - None,
979 - description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
980 - )
981 - ioc_value: Optional[str] = Field(
982 - None,
983 - description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
984 - )
985 - ioc_type: Optional[str] = Field(
986 - None,
987 - description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
988 - )
989 -
990 - class Config:
991 - extra = Extra.allow
992 -
993 -
994 -########### ! Create Office365 Threat Intel Alerts In IRIS Schemas ! ###########
995 -class Office365ThreatIntelIrisAsset(BaseModel):
996 - asset_name: Optional[str] = Field(
997 - "Asset Does Not Apply to Office365 Exchange Alerts",
998 - description="Name of the asset",
999 - example="test@socfortress.co",
1000 - )
1001 - asset_description: Optional[str] = Field(
1002 - "Asset Does Not Apply to Office365 Exchange Alerts",
1003 - description="Description of the asset",
1004 - example="Windows Server",
1005 - )
1006 - asset_type_id: Optional[int] = Field(
1007 - 1,
1008 - description="Type ID of the asset",
1009 - example=1,
1010 - )
1011 -
1012 - def to_dict(self):
1013 - return self.dict(exclude_none=True)
1014 -
1015 -
1016 -class Office365ThreatIntelIrisAlertContext(BaseModel):
1017 - _source: Office365ThreatIntelSourceModel = Field(..., description="Source of the alert")
1018 - sender_ip: Optional[str] = Field("Not found", description="Sender IP address")
1019 - operation: Optional[str] = Field("Not found", description="Operation")
1020 - creation_time: Optional[str] = Field("Not found", description="Creation time")
1021 - office365_id: str = Field(..., description="Office365 ID")
1022 - recipients: str = Field(..., description="Recipients")
1023 - workload: str = Field(..., description="Workload")
1024 - organization_id: str = Field(..., description="Organization ID")
1025 - customer_iris_id: Optional[int] = Field(
1026 - None,
1027 - description="IRIS ID of the customer",
1028 - )
1029 - customer_name: Optional[str] = Field(
1030 - None,
1031 - description="Name of the customer",
1032 - )
1033 - customer_cases_index: Optional[str] = Field(
1034 - None,
1035 - description="IRIS case index name in the Wazuh-Indexer",
1036 - )
1037 - time_field: Optional[str] = Field(
1038 - "timestamp_utc",
1039 - description="The timefield of the alert to be used when creating the IRIS alert.",
1040 - )
1041 - rule_description: str = Field(
1042 - ...,
1043 - description="Description of the rule",
1044 - )
1045 - rule_id: str = Field(
1046 - ...,
1047 - description="ID of the rule that triggered the alert",
1048 - example="2001",
1049 - )
1050 -
1051 - def to_dict(self):
1052 - return self.dict(exclude_none=True)
1053 -
1054 -
1055 -class Office365ThreatIntelIrisAlertPayload(BaseModel):
1056 - alert_title: str = Field(
1057 - ...,
1058 - description="Title of the alert",
1059 - example="Intrusion Detected",
1060 - )
1061 - alert_description: str = Field(
1062 - ...,
1063 - description="Description of the alert",
1064 - example="Intrusion Detected by Firewall",
1065 - )
1066 - alert_source: str = Field(..., description="Source of the alert", example="Suricata")
1067 - assets: List[Office365ThreatIntelIrisAsset] = Field(..., description="List of affected assets")
1068 - alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
1069 - alert_severity_id: int = Field(
1070 - ...,
1071 - description="Severity ID of the alert",
1072 - example=5,
1073 - )
1074 - alert_customer_id: int = Field(
1075 - ...,
1076 - description="Customer ID related to the alert",
1077 - example=1,
1078 - )
1079 - alert_source_content: Dict[str, Any] = Field(
1080 - ...,
1081 - description="Original content from the alert source",
1082 - )
1083 - alert_context: Office365ThreatIntelIrisAlertContext = Field(
1084 - ...,
1085 - description="Contextual information about the alert",
1086 - )
1087 - alert_iocs: Optional[List[IrisIoc]] = Field(
1088 - None,
1089 - description="List of IoCs related to the alert",
1090 - )
1091 - alert_source_event_time: str = Field(
1092 - ...,
1093 - description="Timestamp of the alert",
1094 - example="2021-01-01T00:00:00.000Z",
1095 - )
1096 -
1097 - def to_dict(self):
1098 - return self.dict(exclude_none=True)
backend/app/integrations/monitoring_alert/services/custom.py deleted
-215
@@ -1,215 +0,0 @@
1 -from loguru import logger
2 -from sqlalchemy.ext.asyncio import AsyncSession
3 -
4 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
5 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
6 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
7 -from app.integrations.alert_escalation.schema.general_alert import (
8 - CreateAlertRequest as AddAlertRequest,
9 -)
10 -from app.integrations.alert_escalation.services.general_alert import (
11 - add_alert_to_document,
12 -)
13 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
14 - AlertAnalysisResponse,
15 -)
16 -from app.integrations.monitoring_alert.schema.monitoring_alert import CustomAlertModel
17 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
18 - CustomIrisAlertContext,
19 -)
20 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
21 - CustomIrisAlertPayload,
22 -)
23 -from app.integrations.monitoring_alert.schema.monitoring_alert import GraylogPostRequest
24 -from app.utils import get_customer_alert_settings
25 -
26 -
27 -async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> CustomAlertModel:
28 - """
29 - Fetch the Custom alert details from the Wazuh-Indexer.
30 -
31 - Args:
32 - alert_id (str): The alert ID.
33 - index (str): The index.
34 -
35 - Returns:
36 - CollectAlertsResponse: The response from the Wazuh-Indexer.
37 - """
38 - logger.info(
39 - f"Fetching Custom alert details for alert_id: {alert_id} and index: {index}",
40 - )
41 -
42 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
43 - response = es_client.get(index=index, id=alert_id)
44 -
45 - return CustomAlertModel(**response)
46 -
47 -
48 -async def fetch_alert_details(alert: GraylogPostRequest) -> CustomAlertModel:
49 - logger.info(f"Analyzing custom alert: {alert.event.alert_id}")
50 - alert_details = await fetch_wazuh_indexer_details(alert_id=alert.event.alert_id, index=alert.event.alert_index)
51 - logger.info(f"Alert details: {alert_details}")
52 - return alert_details
53 -
54 -
55 -async def build_alert_context_payload(
56 - custom_details: dict,
57 - session: AsyncSession,
58 -) -> CustomIrisAlertContext:
59 - """
60 - Builds the payload for the alert context.
61 -
62 - Args:
63 - alert_details (CreateAlertRequest): The details of the alert.
64 - agent_data (AgentsResponse): The agent data.
65 - session (AsyncSession): The async session.
66 -
67 - Returns:
68 - CustomIrisAlertContext: The built alert context payload.
69 - """
70 - logger.info(f"Building alert context payload for alert with custom details: {custom_details.event.fields}")
71 - return CustomIrisAlertContext(
72 - customer_iris_id=(
73 - await get_customer_alert_settings(
74 - customer_code=custom_details.event.fields["CUSTOMER_CODE"],
75 - session=session,
76 - )
77 - ).iris_customer_id,
78 - customer_name=(
79 - await get_customer_alert_settings(
80 - customer_code=custom_details.event.fields["CUSTOMER_CODE"],
81 - session=session,
82 - )
83 - ).customer_name,
84 - customer_cases_index=(
85 - await get_customer_alert_settings(
86 - customer_code=custom_details.event.fields["CUSTOMER_CODE"],
87 - session=session,
88 - )
89 - ).iris_index,
90 - alert_id=custom_details.event.alert_id,
91 - alert_name=custom_details.event.message,
92 - **custom_details.event.fields,
93 - )
94 -
95 -
96 -async def build_alert_payload(
97 - alert_details: CustomIrisAlertContext,
98 - custom_details: dict,
99 - session: AsyncSession,
100 -) -> CustomIrisAlertPayload:
101 - """
102 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
103 -
104 - Args:
105 - alert_details (CustomAlertModel): The details of the alert.
106 - agent_data: The agent data associated with the alert.
107 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
108 - session (AsyncSession): The session used for database operations.
109 -
110 - Returns:
111 - CustomIrisAlertPayload: The built alert payload.
112 - """
113 - logger.info(f"Building alert payload for alert: {alert_details}")
114 -
115 - context_payload = await build_alert_context_payload(
116 - custom_details=custom_details,
117 - session=session,
118 - )
119 -
120 - logger.info(f"Alert has context: {context_payload}")
121 - return CustomIrisAlertPayload(
122 - alert_title=custom_details.event.message,
123 - alert_description=custom_details.event.message,
124 - alert_source="COPILOT Custom ANALYSIS",
125 - assets=[],
126 - alert_status_id=3,
127 - alert_severity_id=5,
128 - alert_customer_id=(
129 - await get_customer_alert_settings(
130 - customer_code=custom_details.event.fields["CUSTOMER_CODE"],
131 - session=session,
132 - )
133 - ).iris_customer_id,
134 - alert_source_content=alert_details.to_dict(),
135 - alert_context=context_payload,
136 - alert_source_event_time=custom_details.event.timestamp,
137 - )
138 -
139 -
140 -async def create_and_update_alert_in_iris(
141 - alert_details: CustomAlertModel,
142 - custom_details: dict,
143 - session: AsyncSession,
144 -) -> int:
145 - """
146 - Creates the alert, then updates the alert with the asset and IoC if available.
147 -
148 - Args:
149 - alert_details (CustomAlertModel): The details of the alert.
150 - session (AsyncSession): The async session object.
151 -
152 - Returns:
153 - int: The ID of the created alert in IRIS.
154 - """
155 - logger.info(f"Received custom fields: {custom_details}")
156 -
157 - iris_alert_payload = await build_alert_payload(
158 - alert_details=alert_details,
159 - custom_details=custom_details,
160 - session=session,
161 - )
162 -
163 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
164 - result = await fetch_and_validate_data(
165 - client,
166 - alert_client.add_alert,
167 - iris_alert_payload.to_dict(),
168 - )
169 - alert_id = result["data"]["alert_id"]
170 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
171 - return alert_id
172 -
173 -
174 -async def analyze_custom_alert(
175 - monitoring_alerts: GraylogPostRequest,
176 - session: AsyncSession,
177 -) -> AlertAnalysisResponse:
178 - """
179 - Analyze the given custom alerts. These are received straight from Graylog.
180 -
181 - 1. For each alert, extract the metadata from the Wazuh-Indexer.
182 - 2. Build the alert context payload which is based on the custom fields set within the Graylog alert.
183 -
184 - Args:
185 - monitoring_alerts (MonitoringAlerts): The monitoring alert details.
186 - session (AsyncSession): The database session.
187 -
188 - Returns:
189 - AlertAnalysisResponse: The analysis response.
190 - """
191 - logger.info(
192 - f"Analyzing custom alerts: alert_index: {monitoring_alerts.event.alert_index}, alert_id: {monitoring_alerts.event.alert_id}",
193 - )
194 - alert_details = await fetch_alert_details(monitoring_alerts)
195 -
196 - iris_alert_id = await create_and_update_alert_in_iris(
197 - alert_details,
198 - custom_details=monitoring_alerts,
199 - session=session,
200 - )
201 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
202 - await add_alert_to_document(
203 - es_client=es_client,
204 - alert=AddAlertRequest(
205 - alert_id=monitoring_alerts.event.alert_id,
206 - index_name=monitoring_alerts.event.alert_index,
207 - ),
208 - soc_alert_id=iris_alert_id,
209 - session=session,
210 - )
211 -
212 - return AlertAnalysisResponse(
213 - success=True,
214 - message="Custom alerts analyzed successfully",
215 - )
backend/app/integrations/monitoring_alert/services/office365_exchange.py deleted
-578
@@ -1,578 +0,0 @@
1 -import json
2 -from typing import Optional
3 -from typing import Set
4 -
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
9 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
10 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
11 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12 -from app.db.universal_models import CustomersMeta
13 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
14 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
15 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
16 -from app.integrations.alert_creation.general.services.alert_multi_exclude import (
17 - AlertDetailsService,
18 -)
19 -from app.integrations.alert_escalation.schema.general_alert import (
20 - CreateAlertRequest as AddAlertRequest,
21 -)
22 -from app.integrations.alert_escalation.services.general_alert import (
23 - add_alert_to_document,
24 -)
25 -from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
26 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
27 - AlertAnalysisResponse,
28 -)
29 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
30 - FilterAlertsRequest,
31 -)
32 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
33 - Office365ExchangeAlertModel,
34 -)
35 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
36 - Office365ExchangeIrisAlertContext,
37 -)
38 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
39 - Office365ExchangeIrisAlertPayload,
40 -)
41 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
42 - Office365ExchangeIrisAsset,
43 -)
44 -from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
45 -from app.integrations.utils.alerts import validate_ioc_type
46 -from app.utils import get_customer_alert_settings
47 -
48 -
49 -def valid_ioc_fields() -> Set[str]:
50 - """
51 - Getter for the set of valid IoC fields.
52 - Returns
53 - -------
54 - Set[str]
55 - The set of valid IoC fields.
56 - """
57 - return {field.value for field in ValidIocFields}
58 -
59 -
60 -async def construct_alert_source_link(
61 - alert_details: Office365ExchangeIrisAlertContext,
62 - session: AsyncSession,
63 -) -> str:
64 - """
65 - Construct the alert source link for the alert details.
66 - Parameters
67 - ----------
68 - alert_details: CreateAlertRequest
69 - The alert details.
70 - Returns
71 - -------
72 - str
73 - The alert source link.
74 - """
75 - logger.info(f"Constructing alert source link for alert: {alert_details}")
76 - query_string = f"%22query%22:%22data_office365_ClientIP:%5C%22{alert_details.client_ip}%5C%22%20AND%20"
77 - grafana_url = (
78 - await get_customer_alert_settings(
79 - customer_code=alert_details.organization_id,
80 - session=session,
81 - )
82 - ).grafana_url
83 -
84 - return (
85 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22O365%22,%7B%22refId%22:%22A%22,"
86 - f"{query_string}"
87 - f"data_office365_UserId:%5C%22{alert_details.user_id}%5C%22%22,"
88 - "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
89 - "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
90 - )
91 -
92 -
93 -async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
94 - """
95 - Builds an IoC payload based on the provided alert details.
96 -
97 - Args:
98 - alert_details (CreateAlertRequest): The details of the alert.
99 -
100 - Returns:
101 - Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
102 - """
103 - for field in valid_ioc_fields():
104 - if hasattr(alert_details, field):
105 - ioc_value = getattr(alert_details, field)
106 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
107 - return IrisIoc(
108 - ioc_value=ioc_value,
109 - ioc_description="IoC found in alert",
110 - ioc_tlp_id=1,
111 - ioc_type_id=ioc_type,
112 - )
113 - return None
114 -
115 -
116 -async def build_asset_payload(
117 - alert_details: Office365ExchangeIrisAlertContext,
118 - session: AsyncSession,
119 -) -> Office365ExchangeIrisAsset:
120 - """
121 - Build the payload for an IrisAsset object based on the agent data and alert details.
122 -
123 - Args:
124 - agent_data (AgentsResponse): The response containing agent data.
125 - alert_details: The details of the alert.
126 -
127 - Returns:
128 - IrisAsset: The constructed IrisAsset object.
129 - """
130 - # Get the agent_id based on the hostname from the Agents table
131 - logger.info(f"Building asset payload for alert: {alert_details}")
132 - if alert_details is not None:
133 - return Office365ExchangeIrisAsset(
134 - asset_name=alert_details.user_id,
135 - asset_ip=alert_details.client_ip,
136 - asset_description=await construct_alert_source_link(
137 - alert_details,
138 - session=session,
139 - ),
140 - asset_type_id=1,
141 - )
142 - return Office365ExchangeIrisAsset()
143 -
144 -
145 -async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> Office365ExchangeAlertModel:
146 - """
147 - Fetch the Office365 alert details from the Wazuh-Indexer.
148 -
149 - Args:
150 - alert_id (str): The alert ID.
151 - index (str): The index.
152 -
153 - Returns:
154 - CollectAlertsResponse: The response from the Wazuh-Indexer.
155 - """
156 - logger.info(
157 - f"Fetching Office365 alert details for alert_id: {alert_id} and index: {index}",
158 - )
159 -
160 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
161 - response = es_client.get(index=index, id=alert_id)
162 -
163 - return Office365ExchangeAlertModel(**response)
164 -
165 -
166 -async def fetch_alert_details(alert: MonitoringAlerts) -> Office365ExchangeAlertModel:
167 - logger.info(f"Analyzing Office365 Exchange Online alert: {alert}")
168 - alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
169 - logger.info(f"Alert details: {alert_details}")
170 - return alert_details
171 -
172 -
173 -async def check_event_exclusion(
174 - alert_details: Office365ExchangeAlertModel,
175 - alert_detail_service: AlertDetailsService,
176 - session: AsyncSession,
177 -):
178 - logger.info("Checking if alert is excluded due to multi exclusion.")
179 - logger.info(f"Alert details: {alert_details}")
180 - event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
181 - agent_name=alert_details._source["agent_name"],
182 - process_id=alert_details._source.get("process_id", "n/a"),
183 - index=alert_details._index,
184 - session=session,
185 - )
186 - if event_exclude_result is True:
187 - raise HTTPException(
188 - status_code=400,
189 - detail="Alert excluded due to multi exclusion as set in the config.ini file.",
190 - )
191 - logger.info("Alert is not excluded due to multi exclusion.")
192 -
193 -
194 -async def check_if_open_alert_exists_in_iris(alert_details: Office365ExchangeAlertModel, session: AsyncSession) -> list:
195 - """
196 - Check if the alert exists in IRIS.
197 -
198 - Args:
199 - alert_details (Office365AlertModel): The alert details.
200 - session (AsyncSession): The database session.
201 -
202 - Returns:
203 - bool: True if the alert exists in IRIS, False otherwise.
204 - """
205 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
206 - customer_iris_id = (
207 - await get_customer_alert_settings(
208 - customer_code=alert_details._source["data_office365_OrganizationId"],
209 - # customer_code="9668d0df-6e2e-40fd-947d-d568e520e084",
210 - session=session,
211 - )
212 - ).iris_customer_id
213 - request = FilterAlertsRequest(
214 - alert_tags=alert_details._source["rule_id"],
215 - alert_customer_id=customer_iris_id,
216 - )
217 - params = construct_params(request)
218 - alert_exists = await fetch_and_validate_data(
219 - client,
220 - lambda: alert_client.filter_alerts(**params),
221 - )
222 - logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
223 - return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
224 -
225 -
226 -def construct_params(request: FilterAlertsRequest) -> dict:
227 - """
228 - Constructs the parameters for the alert filtering request.
229 -
230 - Args:
231 - request (FilterAlertsRequest): The request object containing filtering criteria.
232 -
233 - Returns:
234 - dict: A dictionary of parameters for the alert filtering request.
235 - """
236 - params = {
237 - "page": request.page,
238 - "per_page": request.per_page,
239 - "sort": request.sort,
240 - "alert_tags": request.alert_tags,
241 - "alert_status_id": request.alert_status_id,
242 - "alert_customer_id": request.alert_customer_id,
243 - # Add more parameters here as needed
244 - }
245 -
246 - # Remove parameters that have a value of None
247 - return {k: v for k, v in params.items() if v is not None}
248 -
249 -
250 -async def build_alert_context_payload(
251 - alert_details: Office365ExchangeIrisAlertContext,
252 - session: AsyncSession,
253 -) -> Office365ExchangeIrisAlertContext:
254 - """
255 - Builds the payload for the alert context.
256 -
257 - Args:
258 - alert_details (CreateAlertRequest): The details of the alert.
259 - agent_data (AgentsResponse): The agent data.
260 - session (AsyncSession): The async session.
261 -
262 - Returns:
263 - Office365IrisAlertContext: The built alert context payload.
264 - """
265 - return Office365ExchangeIrisAlertContext(
266 - customer_iris_id=(
267 - await get_customer_alert_settings(
268 - customer_code=alert_details.organization_id,
269 - session=session,
270 - )
271 - ).iris_customer_id,
272 - customer_name=(
273 - await get_customer_alert_settings(
274 - customer_code=alert_details.organization_id,
275 - session=session,
276 - )
277 - ).customer_name,
278 - customer_cases_index=(
279 - await get_customer_alert_settings(
280 - customer_code=alert_details.organization_id,
281 - session=session,
282 - )
283 - ).iris_index,
284 - client_ip=alert_details.client_ip,
285 - operation=alert_details.operation,
286 - creation_time=alert_details.creation_time,
287 - office365_id=alert_details.office365_id,
288 - organization_name=alert_details.organization_name,
289 - user_id=alert_details.user_id,
290 - workload=alert_details.workload,
291 - organization_id=alert_details.organization_id,
292 - agent_labels_customer=alert_details.organization_id,
293 - rule_description=alert_details.rule_description,
294 - rule_id=alert_details.rule_id,
295 - )
296 -
297 -
298 -async def build_alert_payload(
299 - alert_details: Office365ExchangeIrisAlertContext,
300 - ioc_payload: Optional[IrisIoc],
301 - session: AsyncSession,
302 -) -> Office365ExchangeIrisAlertPayload:
303 - """
304 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
305 -
306 - Args:
307 - alert_details (Office365AlertModel): The details of the alert.
308 - agent_data: The agent data associated with the alert.
309 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
310 - session (AsyncSession): The session used for database operations.
311 -
312 - Returns:
313 - Office365IrisAlertPayload: The built alert payload.
314 - """
315 - asset_payload = await build_asset_payload(
316 - alert_details=alert_details,
317 - session=session,
318 - )
319 - logger.info(f"Asset payload: {asset_payload}")
320 -
321 - context_payload = await build_alert_context_payload(
322 - alert_details=alert_details,
323 - session=session,
324 - )
325 -
326 - logger.info(f"Alert has context: {context_payload}")
327 -
328 - if ioc_payload:
329 - logger.info(f"Alert has IoC: {ioc_payload}")
330 - return Office365ExchangeIrisAlertPayload(
331 - alert_title=alert_details.rule_description,
332 - alert_description=alert_details.rule_description,
333 - alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
334 - assets=[asset_payload],
335 - alert_status_id=3,
336 - alert_severity_id=5,
337 - alert_customer_id=(
338 - await get_customer_alert_settings(
339 - customer_code=alert_details.organization_id,
340 - session=session,
341 - )
342 - ).iris_customer_id,
343 - alert_source_content=alert_details.to_dict(),
344 - alert_context=context_payload,
345 - alert_iocs=[ioc_payload],
346 - alert_source_event_time=alert_details.time_field,
347 - )
348 - else:
349 - logger.info("Alert does not have IoC")
350 - return Office365ExchangeIrisAlertPayload(
351 - alert_title=alert_details.rule_description,
352 - alert_description=alert_details.rule_description,
353 - alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
354 - assets=[asset_payload],
355 - alert_status_id=3,
356 - alert_severity_id=5,
357 - alert_customer_id=(
358 - await get_customer_alert_settings(
359 - customer_code=alert_details.organization_id,
360 - session=session,
361 - )
362 - ).iris_customer_id,
363 - alert_source_content=alert_details.to_dict(),
364 - alert_context=context_payload,
365 - alert_source_event_time=alert_details.time_field,
366 - )
367 -
368 -
369 -async def create_alert_details(
370 - alert_details: Office365ExchangeAlertModel,
371 -) -> Office365ExchangeIrisAlertContext:
372 - """
373 - Create an alert details object from the Office365 alert details.
374 -
375 - Args:
376 - alert_details (Office365AlertModel): The Office365 alert details.
377 -
378 - Returns:
379 - Office365IrisAlertContext: The alert details object.
380 - """
381 - logger.info(f"Creating alert details for alert: {alert_details}")
382 - return Office365ExchangeIrisAlertContext(
383 - index=alert_details._index,
384 - id=alert_details._id,
385 - client_ip=alert_details._source["data_office365_ClientIP"],
386 - operation=alert_details._source["data_office365_Operation"],
387 - creation_time=alert_details._source["data_office365_CreationTime"],
388 - office365_id=alert_details._source["data_office365_Id"],
389 - organization_name=alert_details._source["data_office365_OrganizationName"],
390 - user_id=alert_details._source["data_office365_UserId"],
391 - workload=alert_details._source["data_office365_Workload"],
392 - organization_id=alert_details._source["data_office365_OrganizationId"],
393 - agent_labels_customer=alert_details._source["data_office365_OrganizationId"],
394 - time_field=alert_details._source.get("timestamp_utc", alert_details._source.get("timestamp")),
395 - rule_description=alert_details._source["rule_description"],
396 - rule_id=alert_details._source["rule_id"],
397 - )
398 -
399 -
400 -async def create_and_update_alert_in_iris(
401 - alert_details: Office365ExchangeAlertModel,
402 - session: AsyncSession,
403 -) -> int:
404 - """
405 - Creates the alert, then updates the alert with the asset and IoC if available.
406 -
407 - Args:
408 - alert_details (Office365AlertModel): The details of the alert.
409 - session (AsyncSession): The async session object.
410 -
411 - Returns:
412 - int: The ID of the created alert in IRIS.
413 - """
414 - logger.info("Alert does not exist in IRIS. Creating alert.")
415 - alert_details = await create_alert_details(alert_details)
416 - ioc_payload = await build_ioc_payload(alert_details)
417 - logger.info(f"Alert details: {alert_details}")
418 - iris_alert_payload = await build_alert_payload(
419 - alert_details=alert_details,
420 - ioc_payload=ioc_payload,
421 - session=session,
422 - )
423 - logger.info(f"Alert payload: {iris_alert_payload}")
424 -
425 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
426 - result = await fetch_and_validate_data(
427 - client,
428 - alert_client.add_alert,
429 - iris_alert_payload.to_dict(),
430 - )
431 - alert_id = result["data"]["alert_id"]
432 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
433 -
434 - await fetch_and_validate_data(
435 - client,
436 - alert_client.update_alert,
437 - alert_id,
438 - {"alert_tags": f"{alert_details.rule_id}"},
439 - )
440 - # Update the alert with the asset payload
441 - await fetch_and_validate_data(
442 - client,
443 - alert_client.update_alert,
444 - alert_id,
445 - {"assets": [dict(Office365ExchangeIrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
446 - )
447 - if ioc_payload:
448 - await fetch_and_validate_data(
449 - client,
450 - alert_client.update_alert,
451 - alert_id,
452 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
453 - )
454 - return alert_id
455 -
456 -
457 -async def get_current_assets(client, alert_client, iris_alert_id):
458 - result = await fetch_and_validate_data(
459 - client,
460 - alert_client.get_alert,
461 - iris_alert_id,
462 - )
463 - return result["data"]["assets"]
464 -
465 -
466 -async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
467 - await fetch_and_validate_data(
468 - client,
469 - alert_client.update_alert,
470 - iris_alert_id,
471 - {"assets": current_assets},
472 - )
473 -
474 -
475 -async def remove_duplicate_assets(current_assets):
476 - """
477 - Removes duplicate assets from the given list of current_assets.
478 -
479 - Args:
480 - current_assets (list): A list of dictionaries representing current assets.
481 -
482 - Returns:
483 - list: A list of dictionaries with duplicate assets removed.
484 - """
485 - current_assets = list({d["asset_name"]: d for d in current_assets}.values())
486 - current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
487 - current_assets_str = list(set(current_assets_str))
488 - current_assets = [json.loads(s) for s in current_assets_str]
489 - return current_assets
490 -
491 -
492 -async def analyze_office365_exchange_online_alerts(
493 - monitoring_alerts: MonitoringAlerts,
494 - customer_meta: CustomersMeta,
495 - session: AsyncSession,
496 -) -> AlertAnalysisResponse:
497 - """
498 - Analyze the given Office365 Exchange Online Alert and create an alert if necessary. Otherwise update the existing alert with the asset.
499 -
500 - 1. For each alert, extract the metadata from the Wazuh-Indexer.
501 - 2. Check if the alert exists in IRIS. If it does, update the alert with the asset. If it does not, create the alert in IRIS.
502 - The alert will contain the asset and IoC if available.
503 - 3. Get the current list of assets from the alert to avoid overwriting them.
504 -
505 - Args:
506 - monitoring_alerts (MonitoringAlerts): The monitoring alert details.
507 - session (AsyncSession): The database session.
508 -
509 - Returns:
510 - AlertAnalysisResponse: The analysis response.
511 - """
512 - logger.info(f"Analyzing Office365 Exchange Online alerts: {monitoring_alerts}")
513 - for alert in monitoring_alerts:
514 - alert_details = await fetch_alert_details(alert)
515 - iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details, session=session)
516 - if iris_alert_id == []:
517 - logger.info(
518 - f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
519 - )
520 - iris_alert_id = await create_and_update_alert_in_iris(
521 - alert_details,
522 - session,
523 - )
524 -
525 - logger.info(f"Alert {iris_alert_id} created in IRIS.")
526 - await remove_alert_id(alert.alert_id, session)
527 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
528 - await add_alert_to_document(
529 - es_client=es_client,
530 - alert=AddAlertRequest(
531 - alert_id=alert_details._id,
532 - index_name=alert_details._index,
533 - ),
534 - soc_alert_id=iris_alert_id,
535 - session=session,
536 - )
537 -
538 - else:
539 - logger.info(
540 - f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
541 - )
542 -
543 - # Fetch the current list of assets from the alert to avoid overwriting them
544 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
545 - current_assets = await get_current_assets(
546 - client,
547 - alert_client,
548 - iris_alert_id,
549 - )
550 - alert_details = await create_alert_details(alert_details)
551 - asset_payload = await build_asset_payload(
552 - alert_details=alert_details,
553 - session=session,
554 - )
555 - current_assets.append(dict(Office365ExchangeIrisAsset(**asset_payload.to_dict())))
556 - current_assets = await remove_duplicate_assets(current_assets)
557 - await update_alert_with_assets(
558 - client,
559 - alert_client,
560 - iris_alert_id,
561 - current_assets,
562 - )
563 - await remove_alert_id(alert.alert_id, session)
564 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
565 - await add_alert_to_document(
566 - es_client=es_client,
567 - alert=AddAlertRequest(
568 - alert_id=alert.alert_id,
569 - index_name=alert.alert_index,
570 - ),
571 - soc_alert_id=iris_alert_id,
572 - session=session,
573 - )
574 -
575 - return AlertAnalysisResponse(
576 - success=True,
577 - message="Office365 alerts analyzed successfully",
578 - )
backend/app/integrations/monitoring_alert/services/office365_threatintel.py deleted
-576
@@ -1,576 +0,0 @@
1 -import json
2 -from typing import Optional
3 -from typing import Set
4 -
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
9 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
10 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
11 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12 -from app.db.universal_models import CustomersMeta
13 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
14 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
15 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
16 -from app.integrations.alert_creation.general.services.alert_multi_exclude import (
17 - AlertDetailsService,
18 -)
19 -from app.integrations.alert_escalation.schema.general_alert import (
20 - CreateAlertRequest as AddAlertRequest,
21 -)
22 -from app.integrations.alert_escalation.services.general_alert import (
23 - add_alert_to_document,
24 -)
25 -from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
26 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
27 - AlertAnalysisResponse,
28 -)
29 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
30 - FilterAlertsRequest,
31 -)
32 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
33 - Office365ThreatIntelAlertModel,
34 -)
35 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
36 - Office365ThreatIntelIrisAlertContext,
37 -)
38 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
39 - Office365ThreatIntelIrisAlertPayload,
40 -)
41 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
42 - Office365ThreatIntelIrisAsset,
43 -)
44 -from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
45 -from app.integrations.utils.alerts import validate_ioc_type
46 -from app.utils import get_customer_alert_settings
47 -
48 -
49 -def valid_ioc_fields() -> Set[str]:
50 - """
51 - Getter for the set of valid IoC fields.
52 - Returns
53 - -------
54 - Set[str]
55 - The set of valid IoC fields.
56 - """
57 - return {field.value for field in ValidIocFields}
58 -
59 -
60 -async def construct_alert_source_link(
61 - alert_details: Office365ThreatIntelIrisAlertContext,
62 - session: AsyncSession,
63 -) -> str:
64 - """
65 - Construct the alert source link for the alert details.
66 - Parameters
67 - ----------
68 - alert_details: CreateAlertRequest
69 - The alert details.
70 - Returns
71 - -------
72 - str
73 - The alert source link.
74 - """
75 - logger.info(f"Constructing alert source link for alert: {alert_details}")
76 - query_string = f"%22query%22:%22data_office365_SenderIp:%5C%22{alert_details.sender_ip}%5C%22%20AND%20"
77 - grafana_url = (
78 - await get_customer_alert_settings(
79 - customer_code=alert_details.organization_id,
80 - session=session,
81 - )
82 - ).grafana_url
83 -
84 - return (
85 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22O365%22,%7B%22refId%22:%22A%22,"
86 - f"{query_string}"
87 - f"data_office365_Recipients:%5C%22{alert_details.recipients}%5C%22%22,"
88 - "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
89 - "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
90 - )
91 -
92 -
93 -async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
94 - """
95 - Builds an IoC payload based on the provided alert details.
96 -
97 - Args:
98 - alert_details (CreateAlertRequest): The details of the alert.
99 -
100 - Returns:
101 - Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
102 - """
103 - for field in valid_ioc_fields():
104 - if hasattr(alert_details, field):
105 - ioc_value = getattr(alert_details, field)
106 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
107 - return IrisIoc(
108 - ioc_value=ioc_value,
109 - ioc_description="IoC found in alert",
110 - ioc_tlp_id=1,
111 - ioc_type_id=ioc_type,
112 - )
113 - return None
114 -
115 -
116 -async def build_asset_payload(
117 - alert_details: Office365ThreatIntelIrisAlertContext,
118 - session: AsyncSession,
119 -) -> Office365ThreatIntelIrisAsset:
120 - """
121 - Build the payload for an IrisAsset object based on the agent data and alert details.
122 -
123 - Args:
124 - agent_data (AgentsResponse): The response containing agent data.
125 - alert_details: The details of the alert.
126 -
127 - Returns:
128 - IrisAsset: The constructed IrisAsset object.
129 - """
130 - # Get the agent_id based on the hostname from the Agents table
131 - logger.info(f"Building asset payload for alert: {alert_details}")
132 - if alert_details is not None:
133 - return Office365ThreatIntelIrisAsset(
134 - asset_name=alert_details.recipients,
135 - asset_description=await construct_alert_source_link(
136 - alert_details,
137 - session=session,
138 - ),
139 - asset_type_id=1,
140 - )
141 - return Office365ThreatIntelIrisAsset()
142 -
143 -
144 -async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> Office365ThreatIntelAlertModel:
145 - """
146 - Fetch the Office365 alert details from the Wazuh-Indexer.
147 -
148 - Args:
149 - alert_id (str): The alert ID.
150 - index (str): The index.
151 -
152 - Returns:
153 - CollectAlertsResponse: The response from the Wazuh-Indexer.
154 - """
155 - logger.info(
156 - f"Fetching Office365 alert details for alert_id: {alert_id} and index: {index}",
157 - )
158 -
159 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
160 - response = es_client.get(index=index, id=alert_id)
161 -
162 - return Office365ThreatIntelAlertModel(**response)
163 -
164 -
165 -async def fetch_alert_details(alert: MonitoringAlerts) -> Office365ThreatIntelAlertModel:
166 - logger.info(f"Analyzing Office365 Exchange Online alert: {alert}")
167 - alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
168 - logger.info(f"Alert details: {alert_details}")
169 - return alert_details
170 -
171 -
172 -async def check_event_exclusion(
173 - alert_details: Office365ThreatIntelAlertModel,
174 - alert_detail_service: AlertDetailsService,
175 - session: AsyncSession,
176 -):
177 - logger.info("Checking if alert is excluded due to multi exclusion.")
178 - logger.info(f"Alert details: {alert_details}")
179 - event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
180 - agent_name=alert_details._source["agent_name"],
181 - process_id=alert_details._source.get("process_id", "n/a"),
182 - index=alert_details._index,
183 - session=session,
184 - )
185 - if event_exclude_result is True:
186 - raise HTTPException(
187 - status_code=400,
188 - detail="Alert excluded due to multi exclusion as set in the config.ini file.",
189 - )
190 - logger.info("Alert is not excluded due to multi exclusion.")
191 -
192 -
193 -async def check_if_open_alert_exists_in_iris(alert_details: Office365ThreatIntelAlertModel, session: AsyncSession) -> list:
194 - """
195 - Check if the alert exists in IRIS.
196 -
197 - Args:
198 - alert_details (Office365AlertModel): The alert details.
199 - session (AsyncSession): The database session.
200 -
201 - Returns:
202 - bool: True if the alert exists in IRIS, False otherwise.
203 - """
204 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
205 - customer_iris_id = (
206 - await get_customer_alert_settings(
207 - customer_code=alert_details._source["data_office365_OrganizationId"],
208 - # customer_code="9668d0df-6e2e-40fd-947d-d568e520e084",
209 - session=session,
210 - )
211 - ).iris_customer_id
212 - request = FilterAlertsRequest(
213 - alert_tags=alert_details._source["rule_id"],
214 - alert_customer_id=customer_iris_id,
215 - )
216 - params = construct_params(request)
217 - alert_exists = await fetch_and_validate_data(
218 - client,
219 - lambda: alert_client.filter_alerts(**params),
220 - )
221 - logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
222 - return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
223 -
224 -
225 -def construct_params(request: FilterAlertsRequest) -> dict:
226 - """
227 - Constructs the parameters for the alert filtering request.
228 -
229 - Args:
230 - request (FilterAlertsRequest): The request object containing filtering criteria.
231 -
232 - Returns:
233 - dict: A dictionary of parameters for the alert filtering request.
234 - """
235 - params = {
236 - "page": request.page,
237 - "per_page": request.per_page,
238 - "sort": request.sort,
239 - "alert_tags": request.alert_tags,
240 - "alert_status_id": request.alert_status_id,
241 - "alert_customer_id": request.alert_customer_id,
242 - # Add more parameters here as needed
243 - }
244 -
245 - # Remove parameters that have a value of None
246 - return {k: v for k, v in params.items() if v is not None}
247 -
248 -
249 -async def build_alert_context_payload(
250 - alert_details: Office365ThreatIntelIrisAlertContext,
251 - session: AsyncSession,
252 -) -> Office365ThreatIntelIrisAlertContext:
253 - """
254 - Builds the payload for the alert context.
255 -
256 - Args:
257 - alert_details (CreateAlertRequest): The details of the alert.
258 - agent_data (AgentsResponse): The agent data.
259 - session (AsyncSession): The async session.
260 -
261 - Returns:
262 - Office365IrisAlertContext: The built alert context payload.
263 - """
264 - return Office365ThreatIntelIrisAlertContext(
265 - customer_iris_id=(
266 - await get_customer_alert_settings(
267 - customer_code=alert_details.organization_id,
268 - session=session,
269 - )
270 - ).iris_customer_id,
271 - customer_name=(
272 - await get_customer_alert_settings(
273 - customer_code=alert_details.organization_id,
274 - session=session,
275 - )
276 - ).customer_name,
277 - customer_cases_index=(
278 - await get_customer_alert_settings(
279 - customer_code=alert_details.organization_id,
280 - session=session,
281 - )
282 - ).iris_index,
283 - sender_ip=alert_details.sender_ip,
284 - operation=alert_details.operation,
285 - creation_time=alert_details.creation_time,
286 - office365_id=alert_details.office365_id,
287 - recipients=alert_details.recipients,
288 - workload=alert_details.workload,
289 - organization_id=alert_details.organization_id,
290 - agent_labels_customer=alert_details.organization_id,
291 - rule_description=alert_details.rule_description,
292 - rule_id=alert_details.rule_id,
293 - )
294 -
295 -
296 -async def build_alert_payload(
297 - alert_details: Office365ThreatIntelIrisAlertContext,
298 - ioc_payload: Optional[IrisIoc],
299 - session: AsyncSession,
300 -) -> Office365ThreatIntelIrisAlertPayload:
301 - """
302 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
303 -
304 - Args:
305 - alert_details (Office365AlertModel): The details of the alert.
306 - agent_data: The agent data associated with the alert.
307 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
308 - session (AsyncSession): The session used for database operations.
309 -
310 - Returns:
311 - Office365IrisAlertPayload: The built alert payload.
312 - """
313 - asset_payload = await build_asset_payload(
314 - alert_details=alert_details,
315 - session=session,
316 - )
317 - logger.info(f"Asset payload: {asset_payload}")
318 -
319 - context_payload = await build_alert_context_payload(
320 - alert_details=alert_details,
321 - session=session,
322 - )
323 -
324 - logger.info(f"Alert has context: {context_payload}")
325 -
326 - if ioc_payload:
327 - logger.info(f"Alert has IoC: {ioc_payload}")
328 - return Office365ThreatIntelIrisAlertPayload(
329 - alert_title=alert_details.rule_description,
330 - alert_description=alert_details.rule_description,
331 - alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
332 - assets=[asset_payload],
333 - alert_status_id=3,
334 - alert_severity_id=5,
335 - alert_customer_id=(
336 - await get_customer_alert_settings(
337 - customer_code=alert_details.organization_id,
338 - session=session,
339 - )
340 - ).iris_customer_id,
341 - alert_source_content=alert_details.to_dict(),
342 - alert_context=context_payload,
343 - alert_iocs=[ioc_payload],
344 - alert_source_event_time=alert_details.time_field,
345 - )
346 - else:
347 - logger.info("Alert does not have IoC")
348 - return Office365ThreatIntelIrisAlertPayload(
349 - alert_title=alert_details.rule_description,
350 - alert_description=alert_details.rule_description,
351 - alert_source="COPILOT OFFICE365 EXCHANGE ANALYSIS",
352 - assets=[asset_payload],
353 - alert_status_id=3,
354 - alert_severity_id=5,
355 - alert_customer_id=(
356 - await get_customer_alert_settings(
357 - customer_code=alert_details.organization_id,
358 - session=session,
359 - )
360 - ).iris_customer_id,
361 - alert_source_content=alert_details.to_dict(),
362 - alert_context=context_payload,
363 - alert_source_event_time=alert_details.time_field,
364 - )
365 -
366 -
367 -async def create_alert_details(
368 - alert_details: Office365ThreatIntelAlertModel,
369 -) -> Office365ThreatIntelIrisAlertContext:
370 - """
371 - Create an alert details object from the Office365 alert details.
372 -
373 - Args:
374 - alert_details (Office365AlertModel): The Office365 alert details.
375 -
376 - Returns:
377 - Office365IrisAlertContext: The alert details object.
378 - """
379 - logger.info(f"Creating alert details for alert: {alert_details}")
380 - return Office365ThreatIntelIrisAlertContext(
381 - index=alert_details._index,
382 - id=alert_details._id,
383 - sender_ip=alert_details._source["data_office365_SenderIp"],
384 - operation=alert_details._source["data_office365_Operation"],
385 - creation_time=alert_details._source["data_office365_CreationTime"],
386 - office365_id=alert_details._source["data_office365_Id"],
387 - recipients=alert_details._source["data_office365_Recipients"],
388 - user_id=alert_details._source["data_office365_UserId"],
389 - workload=alert_details._source["data_office365_Workload"],
390 - organization_id=alert_details._source["data_office365_OrganizationId"],
391 - agent_labels_customer=alert_details._source["data_office365_OrganizationId"],
392 - time_field=alert_details._source.get("timestamp_utc", alert_details._source.get("timestamp")),
393 - rule_description=alert_details._source["rule_description"],
394 - rule_id=alert_details._source["rule_id"],
395 - )
396 -
397 -
398 -async def create_and_update_alert_in_iris(
399 - alert_details: Office365ThreatIntelAlertModel,
400 - session: AsyncSession,
401 -) -> int:
402 - """
403 - Creates the alert, then updates the alert with the asset and IoC if available.
404 -
405 - Args:
406 - alert_details (Office365AlertModel): The details of the alert.
407 - session (AsyncSession): The async session object.
408 -
409 - Returns:
410 - int: The ID of the created alert in IRIS.
411 - """
412 - logger.info("Alert does not exist in IRIS. Creating alert.")
413 - alert_details = await create_alert_details(alert_details)
414 - ioc_payload = await build_ioc_payload(alert_details)
415 - logger.info(f"Alert details: {alert_details}")
416 - iris_alert_payload = await build_alert_payload(
417 - alert_details=alert_details,
418 - ioc_payload=ioc_payload,
419 - session=session,
420 - )
421 - logger.info(f"Alert payload: {iris_alert_payload}")
422 -
423 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
424 - result = await fetch_and_validate_data(
425 - client,
426 - alert_client.add_alert,
427 - iris_alert_payload.to_dict(),
428 - )
429 - alert_id = result["data"]["alert_id"]
430 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
431 -
432 - await fetch_and_validate_data(
433 - client,
434 - alert_client.update_alert,
435 - alert_id,
436 - {"alert_tags": f"{alert_details.rule_id}"},
437 - )
438 - # Update the alert with the asset payload
439 - await fetch_and_validate_data(
440 - client,
441 - alert_client.update_alert,
442 - alert_id,
443 - {"assets": [dict(Office365ThreatIntelIrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
444 - )
445 - if ioc_payload:
446 - await fetch_and_validate_data(
447 - client,
448 - alert_client.update_alert,
449 - alert_id,
450 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
451 - )
452 - return alert_id
453 -
454 -
455 -async def get_current_assets(client, alert_client, iris_alert_id):
456 - result = await fetch_and_validate_data(
457 - client,
458 - alert_client.get_alert,
459 - iris_alert_id,
460 - )
461 - return result["data"]["assets"]
462 -
463 -
464 -async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
465 - await fetch_and_validate_data(
466 - client,
467 - alert_client.update_alert,
468 - iris_alert_id,
469 - {"assets": current_assets},
470 - )
471 -
472 -
473 -async def remove_duplicate_assets(current_assets):
474 - """
475 - Removes duplicate assets from the given list of current_assets.
476 -
477 - Args:
478 - current_assets (list): A list of dictionaries representing current assets.
479 -
480 - Returns:
481 - list: A list of dictionaries with duplicate assets removed.
482 - """
483 - current_assets = list({d["asset_name"]: d for d in current_assets}.values())
484 - current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
485 - current_assets_str = list(set(current_assets_str))
486 - current_assets = [json.loads(s) for s in current_assets_str]
487 - return current_assets
488 -
489 -
490 -async def analyze_office365_threatintel_alerts(
491 - monitoring_alerts: MonitoringAlerts,
492 - customer_meta: CustomersMeta,
493 - session: AsyncSession,
494 -) -> AlertAnalysisResponse:
495 - """
496 - Analyze the given Office365 Exchange Online Alert and create an alert if necessary. Otherwise update the existing alert with the asset.
497 -
498 - 1. For each alert, extract the metadata from the Wazuh-Indexer.
499 - 2. Check if the alert exists in IRIS. If it does, update the alert with the asset. If it does not, create the alert in IRIS.
500 - The alert will contain the asset and IoC if available.
501 - 3. Get the current list of assets from the alert to avoid overwriting them.
502 -
503 - Args:
504 - monitoring_alerts (MonitoringAlerts): The monitoring alert details.
505 - session (AsyncSession): The database session.
506 -
507 - Returns:
508 - AlertAnalysisResponse: The analysis response.
509 - """
510 - logger.info(f"Analyzing Office365 ThreatIntel alerts: {monitoring_alerts}")
511 - for alert in monitoring_alerts:
512 - alert_details = await fetch_alert_details(alert)
513 - iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details, session=session)
514 - if iris_alert_id == []:
515 - logger.info(
516 - f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
517 - )
518 - iris_alert_id = await create_and_update_alert_in_iris(
519 - alert_details,
520 - session,
521 - )
522 -
523 - logger.info(f"Alert {iris_alert_id} created in IRIS.")
524 - await remove_alert_id(alert.alert_id, session)
525 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
526 - await add_alert_to_document(
527 - es_client=es_client,
528 - alert=AddAlertRequest(
529 - alert_id=alert_details._id,
530 - index_name=alert_details._index,
531 - ),
532 - soc_alert_id=iris_alert_id,
533 - session=session,
534 - )
535 -
536 - else:
537 - logger.info(
538 - f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
539 - )
540 -
541 - # Fetch the current list of assets from the alert to avoid overwriting them
542 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
543 - current_assets = await get_current_assets(
544 - client,
545 - alert_client,
546 - iris_alert_id,
547 - )
548 - alert_details = await create_alert_details(alert_details)
549 - asset_payload = await build_asset_payload(
550 - alert_details=alert_details,
551 - session=session,
552 - )
553 - current_assets.append(dict(Office365ThreatIntelIrisAsset(**asset_payload.to_dict())))
554 - current_assets = await remove_duplicate_assets(current_assets)
555 - await update_alert_with_assets(
556 - client,
557 - alert_client,
558 - iris_alert_id,
559 - current_assets,
560 - )
561 - await remove_alert_id(alert.alert_id, session)
562 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
563 - await add_alert_to_document(
564 - es_client=es_client,
565 - alert=AddAlertRequest(
566 - alert_id=alert.alert_id,
567 - index_name=alert.alert_index,
568 - ),
569 - soc_alert_id=iris_alert_id,
570 - session=session,
571 - )
572 -
573 - return AlertAnalysisResponse(
574 - success=True,
575 - message="Office365 alerts analyzed successfully",
576 - )
backend/app/integrations/monitoring_alert/services/provision.py
+2 -170
@@ -1,4 +1,3 @@
1 -import os
1 from typing import Optional
2
3 from dotenv import load_dotenv
@@ -403,38 +402,6 @@ async def provision_suricata_monitoring_alert(
402 logger.info(
403 f"Invoking provision_suricata_monitoring_alert with request: {request.dict()}",
404 )
406 - notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
407 - if not notification_exists:
408 - # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
409 - # ! Therefore, we need to send to API port of Copilot over HTTP
410 - url_whitelisted = await check_if_url_whitelist_entry_exists(
411 - f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
412 - )
413 - if not url_whitelisted:
414 - logger.info("Provisioning URL Whitelist")
415 - whitelisted_urls = await build_url_whitelisted_entries(
416 - whitelist_url_model=GraylogUrlWhitelistEntryConfig(
417 - id=await generate_random_id(),
418 - value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
419 - title="SEND TO COPILOT",
420 - type="literal",
421 - ),
422 - )
423 - await provision_webhook_url_whitelist(whitelisted_urls)
424 -
425 - logger.info("Provisioning SEND TO COPILOT Webhook")
426 - notification_id = await provision_webhook(
427 - GraylogAlertWebhookNotificationModel(
428 - title="SEND TO COPILOT",
429 - description="Send alert to Copilot",
430 - config={
431 - "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
432 - "type": "http-notification-v1",
433 - },
434 - ),
435 - )
436 - logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
437 - notification_id = await get_notification_id("SEND TO COPILOT")
405 await provision_alert_definition(
406 GraylogAlertProvisionModel(
407 title="SURICATA ALERT SEVERITY 1",
@@ -505,11 +472,6 @@ async def provision_suricata_monitoring_alert(
472 grace_period_ms=0,
473 backlog_size=None,
474 ),
508 - # notifications=[
509 - # GraylogAlertProvisionNotification(
510 - # notification_id=notification_id,
511 - # ),
512 - # ],
475 alert=True,
476 ),
477 )
@@ -533,38 +495,6 @@ async def provision_office365_exchange_online_alert(
495 logger.info(
496 f"Invoking provision_office365_exchange_online_alert with request: {request.dict()}",
497 )
536 - notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
537 - if not notification_exists:
538 - # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
539 - # ! Therefore, we need to send to API port of Copilot over HTTP
540 - url_whitelisted = await check_if_url_whitelist_entry_exists(
541 - f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
542 - )
543 - if not url_whitelisted:
544 - logger.info("Provisioning URL Whitelist")
545 - whitelisted_urls = await build_url_whitelisted_entries(
546 - whitelist_url_model=GraylogUrlWhitelistEntryConfig(
547 - id=await generate_random_id(),
548 - value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
549 - title="SEND TO COPILOT",
550 - type="literal",
551 - ),
552 - )
553 - await provision_webhook_url_whitelist(whitelisted_urls)
554 -
555 - logger.info("Provisioning SEND TO COPILOT Webhook")
556 - notification_id = await provision_webhook(
557 - GraylogAlertWebhookNotificationModel(
558 - title="SEND TO COPILOT",
559 - description="Send alert to Copilot",
560 - config={
561 - "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
562 - "type": "http-notification-v1",
563 - },
564 - ),
565 - )
566 - logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
567 - notification_id = await get_notification_id("SEND TO COPILOT")
498 await provision_alert_definition(
499 GraylogAlertProvisionModel(
500 title="OFFICE365 EXCHANGE ONLINE",
@@ -635,11 +565,6 @@ async def provision_office365_exchange_online_alert(
565 grace_period_ms=0,
566 backlog_size=None,
567 ),
638 - # notifications=[
639 - # GraylogAlertProvisionNotification(
640 - # notification_id=notification_id,
641 - # ),
642 - # ],
568 alert=True,
569 ),
570 )
@@ -663,38 +588,6 @@ async def provision_office365_threat_intel_alert(
588 logger.info(
589 f"Invoking provision_office365_threat_intel_alert with request: {request.dict()}",
590 )
666 - notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
667 - if not notification_exists:
668 - # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
669 - # ! Therefore, we need to send to API port of Copilot over HTTP
670 - url_whitelisted = await check_if_url_whitelist_entry_exists(
671 - f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
672 - )
673 - if not url_whitelisted:
674 - logger.info("Provisioning URL Whitelist")
675 - whitelisted_urls = await build_url_whitelisted_entries(
676 - whitelist_url_model=GraylogUrlWhitelistEntryConfig(
677 - id=await generate_random_id(),
678 - value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
679 - title="SEND TO COPILOT",
680 - type="literal",
681 - ),
682 - )
683 - await provision_webhook_url_whitelist(whitelisted_urls)
684 -
685 - logger.info("Provisioning SEND TO COPILOT Webhook")
686 - notification_id = await provision_webhook(
687 - GraylogAlertWebhookNotificationModel(
688 - title="SEND TO COPILOT",
689 - description="Send alert to Copilot",
690 - config={
691 - "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
692 - "type": "http-notification-v1",
693 - },
694 - ),
695 - )
696 - logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
697 - notification_id = await get_notification_id("SEND TO COPILOT")
591 await provision_alert_definition(
592 GraylogAlertProvisionModel(
593 title="OFFICE365 THREAT INTEL",
@@ -765,11 +658,6 @@ async def provision_office365_threat_intel_alert(
658 grace_period_ms=0,
659 backlog_size=None,
660 ),
768 - # notifications=[
769 - # GraylogAlertProvisionNotification(
770 - # notification_id=notification_id,
771 - # ),
772 - # ],
661 alert=True,
662 ),
663 )
@@ -791,38 +679,6 @@ async def provision_custom_alert(request: CustomMonitoringAlertProvisionModel) -
679 logger.info(
680 f"Invoking provision_custom_alert with request: {request.dict()}",
681 )
794 - notification_exists = await check_if_event_notification_exists("SEND TO COPILOT - CUSTOM")
795 - if not notification_exists:
796 - # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
797 - # ! Therefore, we need to send to API port of Copilot over HTTP
798 - url_whitelisted = await check_if_url_whitelist_entry_exists(
799 - f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/custom",
800 - )
801 - if not url_whitelisted:
802 - logger.info("Provisioning URL Whitelist")
803 - whitelisted_urls = await build_url_whitelisted_entries(
804 - whitelist_url_model=GraylogUrlWhitelistEntryConfig(
805 - id=await generate_random_id(),
806 - value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/custom",
807 - title="SEND TO COPILOT - CUSTOM",
808 - type="literal",
809 - ),
810 - )
811 - await provision_webhook_url_whitelist(whitelisted_urls)
812 -
813 - logger.info("Provisioning SEND TO COPILOT - CUSTOM Webhook")
814 - notification_id = await provision_webhook(
815 - GraylogAlertWebhookNotificationModel(
816 - title="SEND TO COPILOT - CUSTOM",
817 - description="Send alert to Copilot for custom alert",
818 - config={
819 - "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/custom",
820 - "type": "http-notification-v1",
821 - },
822 - ),
823 - )
824 - logger.info(f"SEND TO COPILOT - CUSTOM Webhook provisioned with id: {notification_id}")
825 - notification_id = await get_notification_id("SEND TO COPILOT - CUSTOM")
682 await provision_alert_definition(
683 GraylogAlertProvisionModel(
684 title=request.alert_name,
@@ -838,30 +694,11 @@ async def provision_custom_alert(request: CustomMonitoringAlertProvisionModel) -
694 conditions={
695 "expression": None,
696 },
841 - search_within_ms=await convert_seconds_to_milliseconds(
842 - request.search_within_ms,
843 - ),
844 - execute_every_ms=await convert_seconds_to_milliseconds(
845 - request.execute_every_ms,
846 - ),
697 + search_within_ms=request.search_within_ms,
698 + execute_every_ms=request.execute_every_ms,
699 event_limit=1000,
700 ),
701 field_spec={
850 - **{
851 - custom_field.name: GraylogAlertProvisionFieldSpecItem(
852 - data_type="string",
853 - providers=[
854 - GraylogAlertProvisionProvider(
855 - type="template-v1",
856 - template=f"${{source.{custom_field.value}}}"
857 - if custom_field.name != "CUSTOMER_CODE"
858 - else custom_field.value,
859 - require_values=True,
860 - ),
861 - ],
862 - )
863 - for custom_field in request.custom_fields
864 - },
702 "COPILOT_ALERT_ID": GraylogAlertProvisionFieldSpecItem(
703 data_type="string",
704 providers=[
@@ -878,11 +715,6 @@ async def provision_custom_alert(request: CustomMonitoringAlertProvisionModel) -
715 grace_period_ms=0,
716 backlog_size=None,
717 ),
881 - # notifications=[
882 - # GraylogAlertProvisionNotification(
883 - # notification_id=notification_id,
884 - # ),
885 - # ],
718 alert=True,
719 ),
720 )
backend/app/integrations/monitoring_alert/services/suricata.py deleted
-569
@@ -1,569 +0,0 @@
1 -import json
2 -from typing import Optional
3 -from typing import Set
4 -
5 -from fastapi import HTTPException
6 -from loguru import logger
7 -from sqlalchemy.ext.asyncio import AsyncSession
8 -
9 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
10 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
11 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
12 -from app.db.universal_models import CustomersMeta
13 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
14 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
15 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
16 -from app.integrations.alert_creation.general.services.alert_multi_exclude import (
17 - AlertDetailsService,
18 -)
19 -from app.integrations.alert_escalation.schema.general_alert import (
20 - CreateAlertRequest as AddAlertRequest,
21 -)
22 -from app.integrations.alert_escalation.services.general_alert import (
23 - add_alert_to_document,
24 -)
25 -from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
26 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
27 - AlertAnalysisResponse,
28 -)
29 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
30 - FilterAlertsRequest,
31 -)
32 -from app.integrations.monitoring_alert.schema.monitoring_alert import SuricataAlertModel
33 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
34 - SuricataIrisAlertContext,
35 -)
36 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
37 - SuricataIrisAlertPayload,
38 -)
39 -from app.integrations.monitoring_alert.schema.monitoring_alert import SuricataIrisAsset
40 -from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
41 -from app.integrations.utils.alerts import validate_ioc_type
42 -from app.utils import get_customer_alert_settings
43 -
44 -
45 -def valid_ioc_fields() -> Set[str]:
46 - """
47 - Getter for the set of valid IoC fields.
48 - Returns
49 - -------
50 - Set[str]
51 - The set of valid IoC fields.
52 - """
53 - return {field.value for field in ValidIocFields}
54 -
55 -
56 -async def construct_alert_source_link(
57 - alert_details: SuricataIrisAlertContext,
58 - session: AsyncSession,
59 -) -> str:
60 - """
61 - Construct the alert source link for the alert details.
62 - Parameters
63 - ----------
64 - alert_details: CreateAlertRequest
65 - The alert details.
66 - Returns
67 - -------
68 - str
69 - The alert source link.
70 - """
71 - logger.info(f"Constructing alert source link for alert: {alert_details}")
72 - query_string = f"%22query%22:%22alert_signature_id:%5C%22{alert_details.alert_id}%5C%22%20AND%20"
73 - grafana_url = (
74 - await get_customer_alert_settings(
75 - customer_code=alert_details.agent_labels_customer,
76 - session=session,
77 - )
78 - ).grafana_url
79 -
80 - return (
81 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22SURICATA%22,%7B%22refId%22:%22A%22,"
82 - f"{query_string}"
83 - f"src_ip:%5C%22{alert_details.src_ip}%5C%22%22,"
84 - "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
85 - "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
86 - )
87 -
88 -
89 -async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
90 - """
91 - Builds an IoC payload based on the provided alert details.
92 -
93 - Args:
94 - alert_details (CreateAlertRequest): The details of the alert.
95 -
96 - Returns:
97 - Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
98 - """
99 - for field in valid_ioc_fields():
100 - if hasattr(alert_details, field):
101 - ioc_value = getattr(alert_details, field)
102 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
103 - return IrisIoc(
104 - ioc_value=ioc_value,
105 - ioc_description="IoC found in alert",
106 - ioc_tlp_id=1,
107 - ioc_type_id=ioc_type,
108 - )
109 - return None
110 -
111 -
112 -async def build_asset_payload(
113 - alert_details: SuricataIrisAlertContext,
114 - session: AsyncSession,
115 -) -> SuricataIrisAsset:
116 - """
117 - Build the payload for an IrisAsset object based on the agent data and alert details.
118 -
119 - Args:
120 - agent_data (AgentsResponse): The response containing agent data.
121 - alert_details: The details of the alert.
122 -
123 - Returns:
124 - IrisAsset: The constructed IrisAsset object.
125 - """
126 - # Get the agent_id based on the hostname from the Agents table
127 - logger.info(f"Building asset payload for alert: {alert_details}")
128 - if alert_details is not None:
129 - return SuricataIrisAsset(
130 - asset_name=alert_details.src_ip,
131 - asset_ip=alert_details.src_ip,
132 - asset_description=await construct_alert_source_link(
133 - alert_details,
134 - session=session,
135 - ),
136 - asset_type_id=2,
137 - )
138 - return SuricataIrisAsset()
139 -
140 -
141 -async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> SuricataAlertModel:
142 - """
143 - Fetch the Suricata alert details from the Wazuh-Indexer.
144 -
145 - Args:
146 - alert_id (str): The alert ID.
147 - index (str): The index.
148 -
149 - Returns:
150 - CollectAlertsResponse: The response from the Wazuh-Indexer.
151 - """
152 - logger.info(
153 - f"Fetching Suricata alert details for alert_id: {alert_id} and index: {index}",
154 - )
155 -
156 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
157 - response = es_client.get(index=index, id=alert_id)
158 -
159 - return SuricataAlertModel(**response)
160 -
161 -
162 -async def fetch_alert_details(alert: MonitoringAlerts) -> SuricataAlertModel:
163 - logger.info(f"Analyzing Suricata alert: {alert.alert_id}")
164 - alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
165 - logger.info(f"Alert details: {alert_details}")
166 - return alert_details
167 -
168 -
169 -async def check_event_exclusion(
170 - alert_details: SuricataAlertModel,
171 - alert_detail_service: AlertDetailsService,
172 - session: AsyncSession,
173 -):
174 - logger.info("Checking if alert is excluded due to multi exclusion.")
175 - logger.info(f"Alert details: {alert_details}")
176 - event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
177 - agent_name=alert_details._source["agent_name"],
178 - process_id=alert_details._source.get("process_id", "n/a"),
179 - index=alert_details._index,
180 - session=session,
181 - )
182 - if event_exclude_result is True:
183 - raise HTTPException(
184 - status_code=400,
185 - detail="Alert excluded due to multi exclusion as set in the config.ini file.",
186 - )
187 - logger.info("Alert is not excluded due to multi exclusion.")
188 -
189 -
190 -async def check_if_open_alert_exists_in_iris(alert_details: SuricataAlertModel, session: AsyncSession) -> list:
191 - """
192 - Check if the alert exists in IRIS.
193 -
194 - Args:
195 - alert_details (SuricataAlertModel): The alert details.
196 - session (AsyncSession): The database session.
197 -
198 - Returns:
199 - bool: True if the alert exists in IRIS, False otherwise.
200 - """
201 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
202 - customer_iris_id = (
203 - await get_customer_alert_settings(
204 - customer_code=alert_details._source["agent_labels_customer"],
205 - session=session,
206 - )
207 - ).iris_customer_id
208 - request = FilterAlertsRequest(
209 - alert_tags=alert_details._source["alert_signature_id"],
210 - alert_customer_id=customer_iris_id,
211 - )
212 - params = construct_params(request)
213 - alert_exists = await fetch_and_validate_data(
214 - client,
215 - lambda: alert_client.filter_alerts(**params),
216 - )
217 - logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
218 - return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
219 -
220 -
221 -def construct_params(request: FilterAlertsRequest) -> dict:
222 - """
223 - Constructs the parameters for the alert filtering request.
224 -
225 - Args:
226 - request (FilterAlertsRequest): The request object containing filtering criteria.
227 -
228 - Returns:
229 - dict: A dictionary of parameters for the alert filtering request.
230 - """
231 - params = {
232 - "page": request.page,
233 - "per_page": request.per_page,
234 - "sort": request.sort,
235 - "alert_tags": request.alert_tags,
236 - "alert_status_id": request.alert_status_id,
237 - "alert_customer_id": request.alert_customer_id,
238 - # Add more parameters here as needed
239 - }
240 -
241 - # Remove parameters that have a value of None
242 - return {k: v for k, v in params.items() if v is not None}
243 -
244 -
245 -async def build_alert_context_payload(
246 - alert_details: SuricataIrisAlertContext,
247 - session: AsyncSession,
248 -) -> SuricataIrisAlertContext:
249 - """
250 - Builds the payload for the alert context.
251 -
252 - Args:
253 - alert_details (CreateAlertRequest): The details of the alert.
254 - agent_data (AgentsResponse): The agent data.
255 - session (AsyncSession): The async session.
256 -
257 - Returns:
258 - SuricataIrisAlertContext: The built alert context payload.
259 - """
260 - return SuricataIrisAlertContext(
261 - customer_iris_id=(
262 - await get_customer_alert_settings(
263 - customer_code=alert_details.agent_labels_customer,
264 - session=session,
265 - )
266 - ).iris_customer_id,
267 - customer_name=(
268 - await get_customer_alert_settings(
269 - customer_code=alert_details.agent_labels_customer,
270 - session=session,
271 - )
272 - ).customer_name,
273 - customer_cases_index=(
274 - await get_customer_alert_settings(
275 - customer_code=alert_details.agent_labels_customer,
276 - session=session,
277 - )
278 - ).iris_index,
279 - alert_id=alert_details.alert_id,
280 - alert_name=alert_details.alert_name,
281 - alert_level=alert_details.alert_level,
282 - rule_id=alert_details.rule_id,
283 - src_ip=alert_details.src_ip,
284 - dest_ip=alert_details.dest_ip,
285 - app_proto=alert_details.app_proto,
286 - agent_labels_customer=alert_details.agent_labels_customer,
287 - )
288 -
289 -
290 -async def build_alert_payload(
291 - alert_details: SuricataIrisAlertContext,
292 - ioc_payload: Optional[IrisIoc],
293 - session: AsyncSession,
294 -) -> SuricataIrisAlertPayload:
295 - """
296 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
297 -
298 - Args:
299 - alert_details (SuricataAlertModel): The details of the alert.
300 - agent_data: The agent data associated with the alert.
301 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
302 - session (AsyncSession): The session used for database operations.
303 -
304 - Returns:
305 - SuricataIrisAlertPayload: The built alert payload.
306 - """
307 - asset_payload = await build_asset_payload(
308 - alert_details=alert_details,
309 - session=session,
310 - )
311 - logger.info(f"Asset payload: {asset_payload}")
312 -
313 - context_payload = await build_alert_context_payload(
314 - alert_details=alert_details,
315 - session=session,
316 - )
317 -
318 - logger.info(f"Alert has context: {context_payload}")
319 -
320 - if ioc_payload:
321 - logger.info(f"Alert has IoC: {ioc_payload}")
322 - return SuricataIrisAlertPayload(
323 - alert_title=alert_details.alert_name,
324 - alert_description=alert_details.alert_name,
325 - alert_source="COPILOT SURICATA ANALYSIS",
326 - assets=[asset_payload],
327 - alert_status_id=3,
328 - alert_severity_id=5,
329 - alert_customer_id=(
330 - await get_customer_alert_settings(
331 - customer_code=alert_details.agent_labels_customer,
332 - session=session,
333 - )
334 - ).iris_customer_id,
335 - alert_source_content=alert_details.to_dict(),
336 - alert_context=context_payload,
337 - alert_iocs=[ioc_payload],
338 - alert_source_event_time=alert_details.time_field,
339 - )
340 - else:
341 - logger.info("Alert does not have IoC")
342 - return SuricataIrisAlertPayload(
343 - alert_title=alert_details.alert_name,
344 - alert_description=alert_details.alert_name,
345 - alert_source="COPILOT SURICATA ANALYSIS",
346 - assets=[asset_payload],
347 - alert_status_id=3,
348 - alert_severity_id=5,
349 - alert_customer_id=(
350 - await get_customer_alert_settings(
351 - customer_code=alert_details.agent_labels_customer,
352 - session=session,
353 - )
354 - ).iris_customer_id,
355 - alert_source_content=alert_details.to_dict(),
356 - alert_context=context_payload,
357 - alert_source_event_time=alert_details.time_field,
358 - )
359 -
360 -
361 -async def create_alert_details(
362 - alert_details: SuricataAlertModel,
363 -) -> SuricataIrisAlertContext:
364 - """
365 - Create an alert details object from the Suricata alert details.
366 -
367 - Args:
368 - alert_details (SuricataAlertModel): The Suricata alert details.
369 -
370 - Returns:
371 - SuricataIrisAlertContext: The alert details object.
372 - """
373 - logger.info(f"Creating alert details for alert: {alert_details}")
374 - return SuricataIrisAlertContext(
375 - index=alert_details._index,
376 - id=alert_details._id,
377 - alert_id=alert_details._source["alert_signature_id"],
378 - alert_name=alert_details._source["alert_signature"],
379 - alert_level=alert_details._source["alert_severity"],
380 - rule_id=alert_details._source["alert_signature_id"],
381 - src_ip=alert_details._source["src_ip"],
382 - dest_ip=alert_details._source["dest_ip"],
383 - app_proto=alert_details._source.get(
384 - "app_proto",
385 - "No application protocol found",
386 - ),
387 - agent_labels_customer=alert_details._source["agent_labels_customer"],
388 - time_field=alert_details._source.get("timestamp_utc", alert_details._source.get("timestamp")),
389 - )
390 -
391 -
392 -async def create_and_update_alert_in_iris(
393 - alert_details: SuricataAlertModel,
394 - session: AsyncSession,
395 -) -> int:
396 - """
397 - Creates the alert, then updates the alert with the asset and IoC if available.
398 -
399 - Args:
400 - alert_details (SuricataAlertModel): The details of the alert.
401 - session (AsyncSession): The async session object.
402 -
403 - Returns:
404 - int: The ID of the created alert in IRIS.
405 - """
406 - logger.info("Alert does not exist in IRIS. Creating alert.")
407 - alert_details = await create_alert_details(alert_details)
408 - ioc_payload = await build_ioc_payload(alert_details)
409 - logger.info(f"Alert details: {alert_details}")
410 - iris_alert_payload = await build_alert_payload(
411 - alert_details=alert_details,
412 - ioc_payload=ioc_payload,
413 - session=session,
414 - )
415 - logger.info(f"Alert payload: {iris_alert_payload}")
416 -
417 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
418 - result = await fetch_and_validate_data(
419 - client,
420 - alert_client.add_alert,
421 - iris_alert_payload.to_dict(),
422 - )
423 - alert_id = result["data"]["alert_id"]
424 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
425 -
426 - await fetch_and_validate_data(
427 - client,
428 - alert_client.update_alert,
429 - alert_id,
430 - {"alert_tags": f"{alert_details.alert_id}"},
431 - )
432 - # Update the alert with the asset payload
433 - await fetch_and_validate_data(
434 - client,
435 - alert_client.update_alert,
436 - alert_id,
437 - {"assets": [dict(SuricataIrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
438 - )
439 - if ioc_payload:
440 - await fetch_and_validate_data(
441 - client,
442 - alert_client.update_alert,
443 - alert_id,
444 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
445 - )
446 - return alert_id
447 -
448 -
449 -async def get_current_assets(client, alert_client, iris_alert_id):
450 - result = await fetch_and_validate_data(
451 - client,
452 - alert_client.get_alert,
453 - iris_alert_id,
454 - )
455 - return result["data"]["assets"]
456 -
457 -
458 -async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
459 - await fetch_and_validate_data(
460 - client,
461 - alert_client.update_alert,
462 - iris_alert_id,
463 - {"assets": current_assets},
464 - )
465 -
466 -
467 -async def remove_duplicate_assets(current_assets):
468 - """
469 - Removes duplicate assets from the given list of current_assets.
470 -
471 - Args:
472 - current_assets (list): A list of dictionaries representing current assets.
473 -
474 - Returns:
475 - list: A list of dictionaries with duplicate assets removed.
476 - """
477 - current_assets = list({d["asset_name"]: d for d in current_assets}.values())
478 - current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
479 - current_assets_str = list(set(current_assets_str))
480 - current_assets = [json.loads(s) for s in current_assets_str]
481 - return current_assets
482 -
483 -
484 -async def analyze_suricata_alerts(
485 - monitoring_alerts: MonitoringAlerts,
486 - customer_meta: CustomersMeta,
487 - session: AsyncSession,
488 -) -> AlertAnalysisResponse:
489 - """
490 - Analyze the given Suricata alerts and create an alert if necessary. Otherwise update the existing alert with the asset.
491 -
492 - 1. For each alert, extract the metadata from the Wazuh-Indexer.
493 - 2. Check if the alert exists in IRIS. If it does, update the alert with the asset. If it does not, create the alert in IRIS.
494 - The alert will contain the asset and IoC if available.
495 - 3. Get the current list of assets from the alert to avoid overwriting them.
496 -
497 - Args:
498 - monitoring_alerts (MonitoringAlerts): The monitoring alert details.
499 - session (AsyncSession): The database session.
500 -
501 - Returns:
502 - AlertAnalysisResponse: The analysis response.
503 - """
504 - logger.info(f"Analyzing Suricata alerts with customer_meta: {customer_meta}")
505 - for alert in monitoring_alerts:
506 - alert_details = await fetch_alert_details(alert)
507 - iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details, session=session)
508 - if iris_alert_id == []:
509 - logger.info(
510 - f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
511 - )
512 - iris_alert_id = await create_and_update_alert_in_iris(
513 - alert_details,
514 - session,
515 - )
516 - logger.info(f"Alert {iris_alert_id} created in IRIS.")
517 - await remove_alert_id(alert.alert_id, session)
518 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
519 - await add_alert_to_document(
520 - es_client=es_client,
521 - alert=AddAlertRequest(
522 - alert_id=alert_details._id,
523 - index_name=alert_details._index,
524 - ),
525 - soc_alert_id=iris_alert_id,
526 - session=session,
527 - )
528 -
529 - else:
530 - logger.info(
531 - f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
532 - )
533 -
534 - # Fetch the current list of assets from the alert to avoid overwriting them
535 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
536 - current_assets = await get_current_assets(
537 - client,
538 - alert_client,
539 - iris_alert_id,
540 - )
541 - alert_details = await create_alert_details(alert_details)
542 - asset_payload = await build_asset_payload(
543 - alert_details=alert_details,
544 - session=session,
545 - )
546 - current_assets.append(dict(SuricataIrisAsset(**asset_payload.to_dict())))
547 - current_assets = await remove_duplicate_assets(current_assets)
548 - await update_alert_with_assets(
549 - client,
550 - alert_client,
551 - iris_alert_id,
552 - current_assets,
553 - )
554 - await remove_alert_id(alert.alert_id, session)
555 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
556 - await add_alert_to_document(
557 - es_client=es_client,
558 - alert=AddAlertRequest(
559 - alert_id=alert.alert_id,
560 - index_name=alert.alert_index,
561 - ),
562 - soc_alert_id=iris_alert_id,
563 - session=session,
564 - )
565 -
566 - return AlertAnalysisResponse(
567 - success=True,
568 - message="Suricata alerts analyzed successfully",
569 - )
backend/app/integrations/monitoring_alert/services/wazuh.py deleted
-719
@@ -1,719 +0,0 @@
1 -import json
2 -import os
3 -from typing import List
4 -from typing import Optional
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -
11 -from app.agents.routes.agents import get_agent_by_hostname
12 -from app.agents.schema.agents import AgentsResponse
13 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
14 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
15 -from app.connectors.shuffle.schema.integrations import ExecuteWorkflowRequest
16 -from app.connectors.shuffle.services.integrations import execute_workflow
17 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
18 -from app.db.universal_models import CustomersMeta
19 -from app.incidents.services.db_operations import get_customer_notification
20 -from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
21 -from app.integrations.alert_creation.general.schema.alert import IrisAsset
22 -from app.integrations.alert_creation.general.schema.alert import IrisIoc
23 -from app.integrations.alert_creation.general.schema.alert import ValidIocFields
24 -from app.integrations.alert_creation.general.services.alert_multi_exclude import (
25 - AlertDetailsService,
26 -)
27 -from app.integrations.alert_escalation.schema.escalate_alert import GenericSourceModel
28 -from app.integrations.alert_escalation.schema.general_alert import (
29 - CreateAlertRequest as AddAlertRequest,
30 -)
31 -from app.integrations.alert_escalation.services.general_alert import (
32 - add_alert_to_document,
33 -)
34 -from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
35 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
36 - AlertAnalysisResponse,
37 -)
38 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
39 - FilterAlertsRequest,
40 -)
41 -from app.integrations.monitoring_alert.schema.monitoring_alert import WazuhAlertModel
42 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
43 - WazuhIrisAlertContext,
44 -)
45 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
46 - WazuhIrisAlertPayload,
47 -)
48 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
49 - WazuhSourceFieldsToRemove,
50 -)
51 -from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
52 -from app.integrations.utils.alerts import get_asset_type_id
53 -from app.integrations.utils.alerts import validate_ioc_type
54 -from app.utils import get_customer_alert_settings
55 -
56 -
57 -def valid_ioc_fields() -> Set[str]:
58 - """
59 - Getter for the set of valid IoC fields.
60 - Returns
61 - -------
62 - Set[str]
63 - The set of valid IoC fields.
64 - """
65 - return {field.value for field in ValidIocFields}
66 -
67 -
68 -async def construct_alert_source_link(
69 - alert_details: CreateAlertRequest,
70 - session: AsyncSession,
71 -) -> str:
72 - """
73 - Construct the alert source link for the alert details.
74 - Parameters
75 - ----------
76 - alert_details: CreateAlertRequest
77 - The alert details.
78 - Returns
79 - -------
80 - str
81 - The alert source link.
82 - """
83 - # Check if the alert has a process id and that it is not "No process ID found"
84 - if hasattr(alert_details, "process_id") and alert_details.process_id != "No process ID found":
85 - query_string = f"%22query%22:%22process_id:%5C%22{alert_details.process_id}%5C%22%20AND%20"
86 - else:
87 - query_string = f"%22query%22:%22_id:%5C%22{alert_details.id}%5C%22%20AND%20"
88 -
89 - grafana_url = (
90 - await get_customer_alert_settings(
91 - customer_code=alert_details.agent_labels_customer,
92 - session=session,
93 - )
94 - ).grafana_url
95 -
96 - return (
97 - f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,"
98 - f"{query_string}"
99 - f"agent_name:%5C%22{alert_details.agent_name}%5C%22%22,"
100 - "%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,"
101 - "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
102 - )
103 -
104 -
105 -async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
106 - """
107 - Builds an IoC payload based on the provided alert details.
108 -
109 - Args:
110 - alert_details (CreateAlertRequest): The details of the alert.
111 -
112 - Returns:
113 - Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
114 - """
115 - for field in valid_ioc_fields():
116 - if hasattr(alert_details, field):
117 - ioc_value = getattr(alert_details, field)
118 - ioc_type = await validate_ioc_type(ioc_value=ioc_value)
119 - return IrisIoc(
120 - ioc_value=ioc_value,
121 - ioc_description="IoC found in alert",
122 - ioc_tlp_id=1,
123 - ioc_type_id=ioc_type,
124 - )
125 - return None
126 -
127 -
128 -async def build_asset_payload(
129 - agent_data: AgentsResponse,
130 - alert_details: CreateAlertRequest,
131 - session: AsyncSession,
132 -) -> IrisAsset:
133 - """
134 - Build the payload for an IrisAsset object based on the agent data and alert details.
135 -
136 - Args:
137 - agent_data (AgentsResponse): The response containing agent data.
138 - alert_details: The details of the alert.
139 -
140 - Returns:
141 - IrisAsset: The constructed IrisAsset object.
142 - """
143 - # Get the agent_id based on the hostname from the Agents table
144 - if agent_data.success:
145 - return IrisAsset(
146 - asset_name=agent_data.agents[0].hostname,
147 - asset_ip=agent_data.agents[0].ip_address,
148 - asset_description=await construct_alert_source_link(
149 - alert_details,
150 - session=session,
151 - ),
152 - asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
153 - asset_tags=f"agent_id:{agent_data.agents[0].agent_id}",
154 - )
155 - return IrisAsset()
156 -
157 -
158 -async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> WazuhAlertModel:
159 - """
160 - Fetch the Wazuh alert details from the Wazuh-Indexer.
161 -
162 - Args:
163 - alert_id (str): The alert ID.
164 - index (str): The index.
165 -
166 - Returns:
167 - CollectAlertsResponse: The response from the Wazuh-Indexer.
168 - """
169 - logger.info(
170 - f"Fetching Wazuh alert details for alert_id: {alert_id} and index: {index}",
171 - )
172 -
173 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
174 - logger.info(f"Fetching alert from wazuh-indexer: {alert_id}")
175 - try:
176 - response = es_client.get(index=index, id=alert_id)
177 - except Exception as e:
178 - logger.info(f"Error fetching alert from wazuh-indexer: {e}")
179 - raise HTTPException(
180 - status_code=404,
181 - detail=f"Alert not found in Wazuh-Indexer index: {index} with ID: {alert_id}",
182 - )
183 - logger.info(f"Alert retrieved from wazuh-indexer: {response}")
184 -
185 - return WazuhAlertModel(**response)
186 -
187 -
188 -async def fetch_alert_details(alert: MonitoringAlerts) -> WazuhAlertModel:
189 - logger.info(f"Analyzing Wazuh alert: {alert.alert_id}")
190 - alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
191 - logger.info(f"Alert details: {alert_details}")
192 - return alert_details
193 -
194 -
195 -async def check_event_exclusion(
196 - alert_details: WazuhAlertModel,
197 - alert_detail_service: AlertDetailsService,
198 - session: AsyncSession,
199 -):
200 - logger.info("Checking if alert is excluded due to multi exclusion.")
201 - logger.info(f"Alert details: {alert_details}")
202 - event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
203 - agent_name=alert_details._source["agent_name"],
204 - process_id=alert_details._source.get("process_id", "n/a"),
205 - index=alert_details._index,
206 - session=session,
207 - )
208 - if event_exclude_result is True:
209 - raise HTTPException(
210 - status_code=400,
211 - detail="Alert excluded due to multi exclusion as set in the config.ini file.",
212 - )
213 - logger.info("Alert is not excluded due to multi exclusion.")
214 -
215 -
216 -async def check_if_open_alert_exists_in_iris(alert_details: WazuhAlertModel, session: AsyncSession) -> list:
217 - """
218 - Check if the alert exists in IRIS.
219 -
220 - Args:
221 - alert_details (WazuhAlertModel): The alert details.
222 - session (AsyncSession): The database session.
223 -
224 - Returns:
225 - bool: True if the alert exists in IRIS, False otherwise.
226 - """
227 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
228 - customer_iris_id = (
229 - await get_customer_alert_settings(
230 - customer_code=alert_details._source["agent_labels_customer"],
231 - session=session,
232 - )
233 - ).iris_customer_id
234 - request = FilterAlertsRequest(alert_tags=alert_details._source["rule_id"], alert_customer_id=customer_iris_id)
235 - params = construct_params(request)
236 - alert_exists = await fetch_and_validate_data(
237 - client,
238 - lambda: alert_client.filter_alerts(**params),
239 - )
240 - logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
241 - return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
242 -
243 -
244 -def construct_params(request: FilterAlertsRequest) -> dict:
245 - """
246 - Constructs the parameters for the alert filtering request.
247 -
248 - Args:
249 - request (FilterAlertsRequest): The request object containing filtering criteria.
250 -
251 - Returns:
252 - dict: A dictionary of parameters for the alert filtering request.
253 - """
254 - params = {
255 - "page": request.page,
256 - "per_page": request.per_page,
257 - "sort": request.sort,
258 - "alert_tags": request.alert_tags,
259 - "alert_status_id": request.alert_status_id,
260 - "alert_customer_id": request.alert_customer_id,
261 - # Add more parameters here as needed
262 - }
263 -
264 - # Remove parameters that have a value of None
265 - return {k: v for k, v in params.items() if v is not None}
266 -
267 -
268 -async def get_process_name(source_dict: dict) -> List[str]:
269 - """
270 - Get the process name from the source dictionary.
271 -
272 - Args:
273 - source_dict (dict): The source dictionary.
274 -
275 - Returns:
276 - List[str]: The process name as a list.
277 - """
278 - # Get the last part of the process_image path
279 - logger.info(f"Source dict: {source_dict}")
280 - source = source_dict.get("_source", {})
281 - process_image = source.get("process_image")
282 - if process_image is None:
283 - process_image = source.get("data_win_eventdata_image")
284 - if process_image is None:
285 - process_image = source.get("data_event_Image")
286 -
287 - process_name = os.path.basename(process_image) if process_image else None
288 - return [process_name] if process_name else ["No process name found"]
289 -
290 -
291 -async def build_alert_context_payload(
292 - alert_details: CreateAlertRequest,
293 - agent_data: AgentsResponse,
294 - session: AsyncSession,
295 -) -> WazuhIrisAlertContext:
296 - """
297 - Builds the payload for the alert context.
298 -
299 - Args:
300 - alert_details (CreateAlertRequest): The details of the alert.
301 - agent_data (AgentsResponse): The agent data.
302 - session (AsyncSession): The async session.
303 -
304 - Returns:
305 - WazuhIrisAlertContext: The built alert context payload.
306 - """
307 - # Convert the _source to a dictionary
308 - source_dict = alert_details._source.to_dict()
309 -
310 - # Remove fields that start with any prefix in SourceFieldsToRemove
311 - for field in WazuhSourceFieldsToRemove:
312 - source_dict = {k: v for k, v in source_dict.items() if not k.startswith(field.value)}
313 -
314 - return WazuhIrisAlertContext(
315 - customer_iris_id=(
316 - await get_customer_alert_settings(
317 - customer_code=alert_details.agent_labels_customer,
318 - session=session,
319 - )
320 - ).iris_customer_id,
321 - customer_name=(
322 - await get_customer_alert_settings(
323 - customer_code=alert_details.agent_labels_customer,
324 - session=session,
325 - )
326 - ).customer_name,
327 - customer_cases_index=(
328 - await get_customer_alert_settings(
329 - customer_code=alert_details.agent_labels_customer,
330 - session=session,
331 - )
332 - ).iris_index,
333 - alert_name=alert_details.rule_description,
334 - alert_level=alert_details.rule_level,
335 - rule_id=alert_details.rule_id,
336 - rule_mitre_id=getattr(alert_details, "rule_mitre_id", "No rule mitre id found"),
337 - rule_mitre_tactic=getattr(
338 - alert_details,
339 - "rule_mitre_tactic",
340 - "No rule mitre tactic found",
341 - ),
342 - rule_mitre_technique=getattr(
343 - alert_details,
344 - "rule_mitre_technique",
345 - "No rule mitre technique found",
346 - ),
347 - process_name=alert_details.process_name,
348 - **source_dict,
349 - )
350 -
351 -
352 -async def build_alert_payload(
353 - alert_details: CreateAlertRequest,
354 - agent_data,
355 - ioc_payload: Optional[IrisIoc],
356 - session: AsyncSession,
357 -) -> WazuhIrisAlertPayload:
358 - """
359 - Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
360 -
361 - Args:
362 - alert_details (CreateAlertRequest): The details of the alert.
363 - agent_data: The agent data associated with the alert.
364 - ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
365 - session (AsyncSession): The session used for database operations.
366 -
367 - Returns:
368 - WazuhIrisAlertPayload: The built alert payload.
369 - """
370 - asset_payload = await build_asset_payload(
371 - agent_data,
372 - alert_details=alert_details,
373 - session=session,
374 - )
375 - context_payload = await build_alert_context_payload(
376 - alert_details=alert_details,
377 - agent_data=agent_data,
378 - session=session,
379 - )
380 - timefield = (
381 - await get_customer_alert_settings(
382 - customer_code=alert_details.agent_labels_customer,
383 - session=session,
384 - )
385 - ).timefield or "timestamp"
386 - # Get the timefield value from the alert_details
387 - if hasattr(alert_details, timefield):
388 - alert_details.time_field = getattr(alert_details, timefield)
389 - logger.info(f"Alert has context: {context_payload}")
390 - if ioc_payload:
391 - logger.info(f"Alert has IoC: {ioc_payload}")
392 - return WazuhIrisAlertPayload(
393 - alert_title=alert_details.rule_description,
394 - alert_description=alert_details.rule_description,
395 - alert_source="COPILOT WAZUH ANALYSIS",
396 - assets=[asset_payload],
397 - alert_status_id=3,
398 - alert_severity_id=5,
399 - alert_customer_id=(
400 - await get_customer_alert_settings(
401 - customer_code=alert_details.agent_labels_customer,
402 - session=session,
403 - )
404 - ).iris_customer_id,
405 - alert_source_content=alert_details.to_dict(),
406 - alert_context=context_payload,
407 - alert_iocs=[ioc_payload],
408 - alert_source_event_time=alert_details.time_field,
409 - )
410 - else:
411 - logger.info("Alert does not have IoC")
412 - return WazuhIrisAlertPayload(
413 - alert_title=alert_details.rule_description,
414 - alert_description=alert_details.rule_description,
415 - alert_source="COPILOT WAZUH ANALYSIS",
416 - assets=[asset_payload],
417 - alert_status_id=3,
418 - alert_severity_id=5,
419 - alert_customer_id=(
420 - await get_customer_alert_settings(
421 - customer_code=alert_details.agent_labels_customer,
422 - session=session,
423 - )
424 - ).iris_customer_id,
425 - alert_source_content=alert_details.to_dict(),
426 - alert_context=context_payload,
427 - alert_source_event_time=alert_details.time_field,
428 - )
429 -
430 -
431 -async def handle_customer_notifications(customer_code: str, alert_payload: dict, session: AsyncSession):
432 - customer_notifications = await get_customer_notification(customer_code, session)
433 - if customer_notifications and customer_notifications[0].enabled:
434 - logger.info(f"Executing workflow for customer code {customer_code}")
435 - await execute_workflow(
436 - ExecuteWorkflowRequest(
437 - workflow_id=customer_notifications[0].shuffle_workflow_id,
438 - execution_arguments={
439 - "customer_code": customer_code,
440 - "alert_context_payload": alert_payload,
441 - },
442 - start="",
443 - ),
444 - )
445 -
446 -
447 -async def create_alert_details(alert_details: WazuhAlertModel) -> CreateAlertRequest:
448 - """
449 - Create an alert details object from the Wazuh alert details.
450 -
451 - Args:
452 - alert_details (WazuhAlertModel): The Wazuh alert details.
453 -
454 - Returns:
455 - CreateAlertRequest: The alert details object.
456 - """
457 - logger.info(f"Creating alert details for alert: {alert_details}")
458 - return CreateAlertRequest(
459 - index=alert_details._index,
460 - id=alert_details._id,
461 - rule_id=alert_details._source["rule_id"],
462 - rule_level=alert_details._source["rule_level"],
463 - rule_description=alert_details._source["rule_description"],
464 - agent_name=alert_details._source["agent_name"],
465 - agent_ip=alert_details._source["agent_ip"],
466 - agent_id=alert_details._source["agent_id"],
467 - agent_labels_customer=alert_details._source["agent_labels_customer"],
468 - timestamp=alert_details._source["timestamp"],
469 - timestamp_utc=alert_details._source.get("timestamp_utc", alert_details._source["timestamp"]),
470 - process_id=alert_details._source.get("process_id", "No process ID found"),
471 - process_name=await get_process_name(alert_details.to_dict()),
472 - _source=GenericSourceModel(**alert_details._source),
473 - )
474 -
475 -
476 -async def create_and_update_alert_in_iris(
477 - alert_details: WazuhAlertModel,
478 - session: AsyncSession,
479 -) -> int:
480 - """
481 - Creates the alert, then updates the alert with the asset and IoC if available.
482 -
483 - Args:
484 - alert_details (WazuhAlertModel): The details of the alert.
485 - session (AsyncSession): The async session object.
486 -
487 - Returns:
488 - int: The ID of the created alert in IRIS.
489 - """
490 - logger.info("Alert does not exist in IRIS. Creating alert.")
491 - alert_details = await create_alert_details(alert_details)
492 - logger.info(f"Alert details: {alert_details}")
493 - agent_details = await get_agent_by_hostname(alert_details.agent_name, session)
494 - ioc_payload = await build_ioc_payload(alert_details)
495 - iris_alert_payload = await build_alert_payload(
496 - alert_details=alert_details,
497 - agent_data=agent_details,
498 - ioc_payload=ioc_payload,
499 - session=session,
500 - )
501 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
502 - result = await fetch_and_validate_data(
503 - client,
504 - alert_client.add_alert,
505 - iris_alert_payload.to_dict(),
506 - )
507 - alert_id = result["data"]["alert_id"]
508 - logger.info(f"Successfully created alert {alert_id} in IRIS.")
509 - await fetch_and_validate_data(
510 - client,
511 - alert_client.update_alert,
512 - alert_id,
513 - {"alert_tags": f"{alert_details.rule_id}"},
514 - )
515 - # Update the alert with the asset payload
516 - await fetch_and_validate_data(
517 - client,
518 - alert_client.update_alert,
519 - alert_id,
520 - {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
521 - )
522 - if ioc_payload:
523 - await fetch_and_validate_data(
524 - client,
525 - alert_client.update_alert,
526 - alert_id,
527 - {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
528 - )
529 - await handle_customer_notifications(
530 - customer_code=alert_details._source.agent_labels_customer,
531 - alert_payload=iris_alert_payload,
532 - session=session,
533 - )
534 - return alert_id
535 -
536 -
537 -async def get_current_assets(client, alert_client, iris_alert_id):
538 - result = await fetch_and_validate_data(
539 - client,
540 - alert_client.get_alert,
541 - iris_alert_id,
542 - )
543 - return result["data"]["assets"]
544 -
545 -
546 -async def get_current_process_names(client, alert_client, iris_alert_id):
547 - result = await fetch_and_validate_data(
548 - client,
549 - alert_client.get_alert,
550 - iris_alert_id,
551 - )
552 - return result.get("data", {}).get("alert_context", {}).get("process_name", [])
553 -
554 -
555 -async def get_current_alert_context(client, alert_client, iris_alert_id):
556 - result = await fetch_and_validate_data(
557 - client,
558 - alert_client.get_alert,
559 - iris_alert_id,
560 - )
561 - return result["data"]["alert_context"]
562 -
563 -
564 -async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
565 - await fetch_and_validate_data(
566 - client,
567 - alert_client.update_alert,
568 - iris_alert_id,
569 - {"assets": current_assets},
570 - )
571 -
572 -
573 -async def update_alert_with_process_names(client, alert_client, iris_alert_id, current_process_names):
574 - await fetch_and_validate_data(
575 - client,
576 - alert_client.update_alert,
577 - iris_alert_id,
578 - {"alert_context": {"process_name": current_process_names}},
579 - )
580 -
581 -
582 -async def update_alert_context(client, alert_client, iris_alert_id, current_iris_alert_context, current_process_names):
583 - alert_context = await current_iris_alert_context
584 - alert_context["process_name"] = current_process_names
585 - await fetch_and_validate_data(
586 - client,
587 - alert_client.update_alert,
588 - iris_alert_id,
589 - {"alert_context": alert_context},
590 - )
591 -
592 -
593 -async def remove_duplicate_assets(current_assets):
594 - """
595 - Removes duplicate assets from the given list of current_assets.
596 -
597 - Args:
598 - current_assets (list): A list of dictionaries representing current assets.
599 -
600 - Returns:
601 - list: A list of dictionaries with duplicate assets removed.
602 - """
603 - current_assets = list({d["asset_name"]: d for d in current_assets}.values())
604 - current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
605 - current_assets_str = list(set(current_assets_str))
606 - current_assets = [json.loads(s) for s in current_assets_str]
607 - return current_assets
608 -
609 -
610 -async def analyze_wazuh_alerts(
611 - monitoring_alerts: MonitoringAlerts,
612 - customer_meta: CustomersMeta,
613 - session: AsyncSession,
614 -) -> AlertAnalysisResponse:
615 - """
616 - Analyze the given Wazuh alerts and create an alert if necessary. Otherwise update the existing alert with the asset.
617 -
618 - 1. For each alert, extract the metadata from the Wazuh-Indexer.
619 - 2. Check if the alert exists in IRIS. If it does, update the alert with the asset. If it does not, create the alert in IRIS.
620 - The alert will contain the asset and IoC if available.
621 - 3. Get the current list of assets from the alert to avoid overwriting them.
622 -
623 - Args:
624 - monitoring_alerts (MonitoringAlerts): The monitoring alert details.
625 - session (AsyncSession): The database session.
626 -
627 - Returns:
628 - WazuhAnalysisResponse: The analysis response.
629 - """
630 - logger.info(f"Analyzing Wazuh alerts with customer_meta: {customer_meta}")
631 - logger.info(f"Analyzing Wazuh alerts: {monitoring_alerts}")
632 - alert_detail_service = await AlertDetailsService.create()
633 - for alert in monitoring_alerts:
634 - logger.info(f"Analyzing Wazuh alert: {alert.alert_id}")
635 - alert_details = await fetch_alert_details(alert)
636 - await check_event_exclusion(alert_details, alert_detail_service, session)
637 - iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details, session=session)
638 - if iris_alert_id == []:
639 - logger.info(
640 - f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
641 - )
642 - iris_alert_id = await create_and_update_alert_in_iris(
643 - alert_details,
644 - session,
645 - )
646 - await remove_alert_id(alert.alert_id, session)
647 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
648 - await add_alert_to_document(
649 - es_client=es_client,
650 - alert=AddAlertRequest(
651 - alert_id=alert_details._id,
652 - index_name=alert_details._index,
653 - ),
654 - soc_alert_id=iris_alert_id,
655 - session=session,
656 - )
657 -
658 - else:
659 - logger.info(
660 - f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
661 - )
662 - # Fetch the current list of assets from the alert to avoid overwriting them
663 - client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
664 - current_assets = await get_current_assets(
665 - client,
666 - alert_client,
667 - iris_alert_id,
668 - )
669 - current_iris_alert_context = get_current_alert_context(
670 - client,
671 - alert_client,
672 - iris_alert_id,
673 - )
674 - current_process_names = await get_current_process_names(
675 - client,
676 - alert_client,
677 - iris_alert_id,
678 - )
679 - alert_details = await create_alert_details(alert_details)
680 - # Add the new `process_name` to the `current_process_names`` list
681 - current_process_names.extend(alert_details.process_name)
682 - logger.info(f"Current process names: {current_process_names}")
683 - agent_details = await get_agent_by_hostname(alert_details.agent_name, session)
684 - asset_payload = await build_asset_payload(
685 - agent_data=agent_details,
686 - alert_details=alert_details,
687 - session=session,
688 - )
689 - current_assets.append(dict(IrisAsset(**asset_payload.to_dict())))
690 - current_assets = await remove_duplicate_assets(current_assets)
691 - await update_alert_with_assets(
692 - client,
693 - alert_client,
694 - iris_alert_id,
695 - current_assets,
696 - )
697 - await update_alert_context(
698 - client,
699 - alert_client,
700 - iris_alert_id,
701 - current_iris_alert_context,
702 - current_process_names,
703 - )
704 - await remove_alert_id(alert.alert_id, session)
705 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
706 - await add_alert_to_document(
707 - es_client=es_client,
708 - alert=AddAlertRequest(
709 - alert_id=alert_details.id,
710 - index_name=alert_details.index,
711 - ),
712 - soc_alert_id=iris_alert_id,
713 - session=session,
714 - )
715 -
716 - return AlertAnalysisResponse(
717 - success=True,
718 - message="Wazuh alerts analyzed successfully",
719 - )
backend/app/integrations/sap_siem/routes/sap_siem.py deleted
-252
@@ -1,252 +0,0 @@
1 -from typing import Optional
2 -
3 -from fastapi import APIRouter
4 -from fastapi import Depends
5 -from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
7 -
8 -from app.db.db_session import get_db
9 -from app.integrations.routes import find_customer_integration
10 -from app.integrations.sap_siem.schema.sap_siem import CollectSapSiemRequest
11 -from app.integrations.sap_siem.schema.sap_siem import InvokeSapSiemRequest
12 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
13 -from app.integrations.sap_siem.schema.sap_siem import SapSiemAuthKeys
14 -from app.integrations.sap_siem.services.collect import collect_sap_siem
15 -from app.integrations.sap_siem.services.sap_siem_brute_force_same_ip import (
16 - sap_siem_brute_force_failed_same_ip,
17 -)
18 -from app.integrations.sap_siem.services.sap_siem_brute_forced_failed_logins import (
19 - sap_siem_brute_force_failed_multiple_ips,
20 -)
21 -from app.integrations.sap_siem.services.sap_siem_failed_same_user_different_geo_location import (
22 - sap_siem_failed_same_user_diff_geo,
23 -)
24 -from app.integrations.sap_siem.services.sap_siem_failed_same_user_from_different_ip import (
25 - sap_siem_failed_same_user_diff_ip,
26 -)
27 -from app.integrations.sap_siem.services.sap_siem_successful_login_same_ip_after_multiple_failures import (
28 - sap_siem_successful_login_after_failures,
29 -)
30 -from app.integrations.sap_siem.services.sap_siem_successful_same_user_different_geo_location import (
31 - sap_siem_successful_same_user_diff_geo,
32 -)
33 -from app.integrations.sap_siem.services.sap_siem_successful_user_login_after_using_different_ip import (
34 - sap_siem_successful_user_login_with_different_ip,
35 -)
36 -from app.integrations.utils.utils import extract_auth_keys
37 -from app.integrations.utils.utils import get_customer_integration_response
38 -
39 -integration_sap_siem_router = APIRouter()
40 -
41 -
42 -@integration_sap_siem_router.post(
43 - "",
44 - response_model=InvokeSAPSiemResponse,
45 - description="Pull down SAP SIEM Events.",
46 -)
47 -async def collect_sap_siem_route(sap_siem_request: InvokeSapSiemRequest, session: AsyncSession = Depends(get_db)):
48 - """Pull down SAP SIEM Events."""
49 - customer_integration_response = await get_customer_integration_response(
50 - sap_siem_request.customer_code,
51 - session,
52 - )
53 -
54 - customer_integration = await find_customer_integration(
55 - sap_siem_request.customer_code,
56 - sap_siem_request.integration_name,
57 - customer_integration_response,
58 - )
59 -
60 - sap_siem_auth_keys = extract_auth_keys(customer_integration, service_name="SAP SIEM")
61 -
62 - logger.info(f"SAP SIEM Auth Keys: {sap_siem_auth_keys}")
63 -
64 - auth_keys = SapSiemAuthKeys(**sap_siem_auth_keys)
65 - # if multiple apiKey values are present, make a loop to iterate through them
66 - # and collect the data for each apiKey
67 - if "," in auth_keys.API_KEY:
68 - api_keys = auth_keys.API_KEY.split(",")
69 - for key in api_keys:
70 - collect_sap_siem_request = CollectSapSiemRequest(
71 - apiKey=key,
72 - secretKey=auth_keys.SECRET_KEY,
73 - userKey=auth_keys.USER_KEY,
74 - apiDomain=auth_keys.API_DOMAIN,
75 - threshold=sap_siem_request.threshold,
76 - lower_bound=sap_siem_request.lower_bound,
77 - upper_bound=sap_siem_request.upper_bound,
78 - customer_code=sap_siem_request.customer_code,
79 - )
80 - await collect_sap_siem(sap_siem_request=collect_sap_siem_request)
81 - else:
82 - collect_sap_siem_request = CollectSapSiemRequest(
83 - apiKey=auth_keys.API_KEY,
84 - secretKey=auth_keys.SECRET_KEY,
85 - userKey=auth_keys.USER_KEY,
86 - apiDomain=auth_keys.API_DOMAIN,
87 - threshold=sap_siem_request.threshold,
88 - lower_bound=sap_siem_request.lower_bound,
89 - upper_bound=sap_siem_request.upper_bound,
90 - customer_code=sap_siem_request.customer_code,
91 - )
92 - await collect_sap_siem(sap_siem_request=collect_sap_siem_request)
93 -
94 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
95 -
96 -
97 -@integration_sap_siem_router.post(
98 - "/successful_user_login_with_different_ip",
99 - response_model=InvokeSAPSiemResponse,
100 - description="Rule: Successful user login after using different IP addresses\n\n"
101 - "Period: within 15 minutes\n\n"
102 - "Prerequisite: \n\n"
103 - "- Login attempts from different IP addresses, regardless of login status (at least 2 failed IP addresses)\n\n"
104 - "- Successful login afterwards (from the third successful IP address)\n\n"
105 - "Result: User compressed, IP addresses belong to an attack network",
106 -)
107 -async def invoke_sap_siem_successful_user_login_with_different_ip_route(
108 - threshold: Optional[int] = 0,
109 - time_range: Optional[int] = 15,
110 - session: AsyncSession = Depends(get_db),
111 -):
112 - logger.info("Invoking SAP SIEM integration for successful user login with different IP.")
113 - await sap_siem_successful_user_login_with_different_ip(threshold=threshold, time_range=time_range, session=session)
114 -
115 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
116 -
117 -
118 -@integration_sap_siem_router.post(
119 - "/same_user_failed_login_from_different_ip",
120 - response_model=InvokeSAPSiemResponse,
121 - description="Rule: Same user from different IP addresses\n\n"
122 - "Period: within 10 minutes\n\n"
123 - "Prerequisite: \n\n"
124 - "- At least 3 failed login attempts with the same user name from 3 different IP addresses\n\n"
125 - "Result: User compressed, IP addresses belong to an attack network",
126 -)
127 -async def invoke_sap_siem_same_user_failed_login_from_different_ip_route(
128 - threshold: Optional[int] = 0,
129 - time_range: Optional[int] = 10,
130 - session: AsyncSession = Depends(get_db),
131 -):
132 - logger.info("Invoking SAP SIEM integration for same user failed login from different IP.")
133 - await sap_siem_failed_same_user_diff_ip(threshold=threshold, time_range=time_range, session=session)
134 -
135 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
136 -
137 -
138 -@integration_sap_siem_router.post(
139 - "/same_user_failed_login_from_different_geo_location",
140 - response_model=InvokeSAPSiemResponse,
141 - description="Rule: Same user from different geo locations\n\n"
142 - "Period: within 20 minutes\n\n"
143 - "Prerequisite: \n\n"
144 - "- At least 3 failed login attempts with the same user name from at least two different GEO IP country locations\n\n"
145 - "Result: User compressed, IP addresses belong to an attack network",
146 -)
147 -async def invoke_sap_siem_same_user_failed_login_from_different_geo_location_route(
148 - threshold: Optional[int] = 0,
149 - time_range: Optional[int] = 20,
150 - session: AsyncSession = Depends(get_db),
151 -):
152 - logger.info("Invoking SAP SIEM integration for same user failed login from different geo location.")
153 - await sap_siem_failed_same_user_diff_geo(threshold=threshold, time_range=time_range, session=session)
154 -
155 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
156 -
157 -
158 -@integration_sap_siem_router.post(
159 - "/same_user_successful_login_from_different_geo_location",
160 - response_model=InvokeSAPSiemResponse,
161 - description="Rule: Same user from different geo locations\n\n"
162 - "Period: within 20 minutes\n\n"
163 - "Prerequisite: \n\n"
164 - "- At least 1 failed login attempt with the same username from two different GEO IP country locations\n\n"
165 - "- from the 2nd successful login thereafter in another GEO IP country location\n\n"
166 - "Result: User compressed, IP addresses belong to an attack network\n\n"
167 - "This function would trigger a suspicious login when the following conditions are met:\n\n"
168 - "1. There is at least one failed login attempt from the same user (identified by `login_id`) from two different GEO IP country locations within the last 20 minutes.\n"
169 - "2. There is at least one successful login attempt from the same user from a different GEO IP country location within the last 20 minutes.\n\n"
170 - "Here are some examples:\n\n"
171 - "Example 1:\n"
172 - "- At 12:00, a failed login attempt is made by user `user1` from IP `1.1.1.1` located in the US.\n"
173 - "- At 12:10, another failed login attempt is made by `user1` from IP `2.2.2.2` located in Canada.\n"
174 - "- At 12:15, a successful login attempt is made by `user1` from IP `3.3.3.3` located in the UK.\n"
175 - "- In this case, the function would trigger a suspicious login for `user1` because there are failed login attempts from two different countries (US and Canada) "
176 - "and a successful login from a different country (UK) within 20 minutes.\n\n"
177 - "Example 2:\n"
178 - "- At 12:00, a failed login attempt is made by user `user2` from IP `4.4.4.4` located in the US.\n"
179 - "- At 12:10, another failed login attempt is made by `user2` from IP `5.5.5.5` also located in the US.\n"
180 - "- At 12:15, a successful login attempt is made by `user2` from IP `6.6.6.6` located in the US.\n"
181 - "- In this case, the function would not trigger a suspicious login for `user2` because all the login attempts are from the same country (US).",
182 -)
183 -async def invoke_sap_siem_same_user_successful_login_from_different_geo_location_route(
184 - threshold: Optional[int] = 0,
185 - time_range: Optional[int] = 20,
186 - session: AsyncSession = Depends(get_db),
187 -):
188 - logger.info("Invoking SAP SIEM integration for same user successful login from different geo location.")
189 - await sap_siem_successful_same_user_diff_geo(threshold=threshold, time_range=time_range, session=session)
190 -
191 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
192 -
193 -
194 -@integration_sap_siem_router.post(
195 - "/brute_force_failed_logins_multiple_ips",
196 - response_model=InvokeSAPSiemResponse,
197 - description="Rule: Logins from different IP addresses\n\n"
198 - "Period: within 3 minutes\n\n"
199 - "Prerequisite: \n\n"
200 - "- At least 25 failed login attempts from different IP addresses\n\n"
201 - "Result: IP addresses belong to an attack network",
202 -)
203 -async def invoke_sap_siem_brute_force_failed_logins_route(
204 - threshold: Optional[int] = 0,
205 - time_range: Optional[int] = 3,
206 - session: AsyncSession = Depends(get_db),
207 -):
208 - logger.info("Invoking SAP SIEM integration for brute force failed logins.")
209 - await sap_siem_brute_force_failed_multiple_ips(threshold=threshold, time_range=time_range, session=session)
210 -
211 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
212 -
213 -
214 -@integration_sap_siem_router.post(
215 - "/brute_force_failed_logins_same_ip",
216 - response_model=InvokeSAPSiemResponse,
217 - description="Rule: Logins from the same IP address\n\n"
218 - "Period: within 5 minutes\n\n"
219 - "Prerequisite: \n\n"
220 - "- At least 10 different user name failed login attempts from the same IP address\n\n"
221 - "Result: IP addresses belong to an attack network",
222 -)
223 -async def invoke_sap_siem_brute_force_failed_logins_same_ip_route(
224 - threshold: Optional[int] = 0,
225 - time_range: Optional[int] = 5,
226 - session: AsyncSession = Depends(get_db),
227 -):
228 - logger.info("Invoking SAP SIEM integration for brute force failed logins from the same IP.")
229 - await sap_siem_brute_force_failed_same_ip(threshold=threshold, time_range=time_range, session=session)
230 -
231 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
232 -
233 -
234 -@integration_sap_siem_router.post(
235 - "/successful_login_after_multiple_failed_logins",
236 - response_model=InvokeSAPSiemResponse,
237 - description="Rule: Successful login after multiple failed logins\n\n"
238 - "Period: within 2 minutes\n\n"
239 - "Prerequisite: \n\n"
240 - "- At least 3 different user names that have failed from the same IP addressn\n"
241 - "- At least one successful login from the same IP address after 3 different user names. \n\n"
242 - "Result: User compromised, IP address belongs to an attack network",
243 -)
244 -async def invoke_sap_siem_successful_login_after_multiple_failed_logins_route(
245 - threshold: Optional[int] = 0,
246 - time_range: Optional[int] = 2,
247 - session: AsyncSession = Depends(get_db),
248 -):
249 - logger.info("Invoking SAP SIEM integration for successful login after multiple failed logins.")
250 - await sap_siem_successful_login_after_failures(threshold=threshold, time_range=time_range, session=session)
251 -
252 - return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
backend/app/integrations/sap_siem/services/collect.py deleted
-85
@@ -1,85 +0,0 @@
1 -import requests
2 -from loguru import logger
3 -
4 -from app.integrations.sap_siem.schema.sap_siem import CollectSapSiemRequest
5 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
6 -from app.integrations.sap_siem.schema.sap_siem import SapSiemResponseBody
7 -from app.integrations.utils.event_shipper import event_shipper
8 -from app.integrations.utils.schema import EventShipperPayload
9 -
10 -
11 -def build_request_payload(sap_siem_request: CollectSapSiemRequest) -> dict:
12 - return {
13 - "apiKey": sap_siem_request.apiKey,
14 - "secret": sap_siem_request.secretKey,
15 - "userKey": sap_siem_request.userKey,
16 - "query": f"SELECT * FROM auditLog WHERE endpoint = 'accounts.login' and @timestamp >= '{sap_siem_request.lower_bound}' "
17 - f"and @timestamp < '{sap_siem_request.upper_bound}'",
18 - }
19 -
20 -
21 -async def make_request(sap_siem_request: CollectSapSiemRequest) -> SapSiemResponseBody:
22 - """
23 - Makes a request to the SAP SIEM integration.
24 -
25 - Args:
26 - sap_siem_request (CollectSapSiemRequest): The request payload containing the necessary information for the SAP SIEM integration.
27 -
28 - Returns:
29 - SapSiemResponseBody: The response model containing the result of the SAP SIEM integration invocation.
30 -
31 - Raises:
32 - HTTPException: If the SAP SIEM integration fails.
33 - """
34 - logger.info("Making request to SAP SIEM")
35 - form_data = build_request_payload(sap_siem_request)
36 - response = requests.post(
37 - f"https://{sap_siem_request.apiDomain}/audit.search",
38 - data=form_data,
39 - )
40 - return SapSiemResponseBody(**response.json())
41 -
42 -
43 -async def send_to_event_shipper(message: EventShipperPayload) -> None:
44 - """
45 - Sends the message to the event shipper.
46 -
47 - Args:
48 - message (EventShipperPayload): The message to send to the event shipper.
49 - """
50 - await event_shipper(message)
51 -
52 -
53 -async def collect_sap_siem(sap_siem_request: CollectSapSiemRequest) -> InvokeSAPSiemResponse:
54 - """
55 - Collects SAP SIEM events.
56 -
57 - Args:
58 - sap_siem_request (CollectSapSiemRequest): The request payload containing the necessary information for the SAP SIEM integration.
59 -
60 - Returns:
61 - InvokeSAPSiemResponse: The response model containing the result of the SAP SIEM integration invocation.
62 -
63 - Raises:
64 - HTTPException: If the SAP SIEM integration fails.
65 - """
66 - logger.info(f"Collecting SAP SIEM Events for customer_code: {sap_siem_request.customer_code}")
67 -
68 - results = await make_request(sap_siem_request)
69 -
70 - for result in results.results:
71 - # write the `timestamp` field as `event_timestamp`
72 - result.event_timestamp = result.timestamp
73 - await send_to_event_shipper(
74 - EventShipperPayload(
75 - customer_code=sap_siem_request.customer_code,
76 - integration="sap_siem",
77 - version="1.0",
78 - **result.dict(),
79 - ),
80 - )
81 -
82 - return InvokeSAPSiemResponse(
83 - success=True,
84 - message="SAP SIEM Events collected successfully",
85 - )
backend/app/integrations/sap_siem/services/sap_siem_brute_force_same_ip.py deleted
-578
@@ -1,578 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 - await mark_as_checked(suspicious_login)
55 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 - await send_to_shuffle(
57 - ShufflePayload(
58 - alert_id=case.data.case_id,
59 - customer=suspicious_login.customer_code,
60 - customer_code=suspicious_login.customer_code,
61 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 - rule_description=f"{case.data.case_name}",
63 - hostname=suspicious_login.ip,
64 - rule_name="Rule:_Logins_from_the_same_IP_address",
65 - affected_ip=suspicious_login.ip,
66 - alert_type="ip",
67 - ),
68 - session=session,
69 - )
70 -
71 -
72 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
73 - """
74 - Handles suspicious login events with multiple logins.
75 -
76 - Args:
77 - suspicious_login: The suspicious login event.
78 - unique_instances: List of unique instances of the suspicious login event.
79 - case_ids: List of case IDs associated with the suspicious login event.
80 - session: The database session.
81 -
82 - Returns:
83 - None
84 - """
85 - await handle_common_suspicious_login_tasks(
86 - suspicious_login,
87 - unique_instances,
88 - case_ids,
89 - create_iris_case_multiple,
90 - session,
91 - )
92 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
93 -
94 -
95 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
96 - """
97 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
98 -
99 - :param suspicious_login: The suspicious login to update
100 -
101 - :return: None
102 - """
103 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
104 - try:
105 - es_client.update(
106 - index=index,
107 - id=id,
108 - body={
109 - "doc": {
110 - "event_analyzed_brute_force_same_ip": "True",
111 - },
112 - },
113 - )
114 - logger.info(f"Updated event_analyzed_brute_force_same_ip flag for suspicious login: {id}")
115 - except Exception as e:
116 - logger.error(
117 - f"Failed to update case created flag {e}",
118 - )
119 - # Attempt to remove read-only block
120 - try:
121 - es_client.indices.put_settings(
122 - index=index,
123 - body={"index.blocks.write": None},
124 - )
125 - logger.info(
126 - f"Removed read-only block from index {index}. Retrying update.",
127 - )
128 -
129 - # Retry the update operation
130 - es_client.update(
131 - index=index,
132 - id=id,
133 - body={"doc": {"event_analyzed_brute_force_same_ip": "True"}},
134 - )
135 - logger.info(
136 - f"Added event_analyzed_brute_force_same_ip flag to index {index} for suspicious login: {id}",
137 - )
138 -
139 - # Reenable the write block
140 - es_client.indices.put_settings(
141 - index=index,
142 - body={"index.blocks.write": True},
143 - )
144 - except Exception as e2:
145 - logger.error(
146 - f"Failed to remove read-only block from index {index}: {e2}",
147 - )
148 - return False
149 -
150 -
151 -async def mark_as_checked(suspicious_login):
152 - """
153 - Marks a suspicious login as checked by adding it to the set of checked IPs.
154 -
155 - Args:
156 - suspicious_login (Login): The suspicious login object to mark as checked.
157 -
158 - Returns:
159 - None
160 - """
161 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
162 -
163 -
164 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
165 - """
166 - Handles user activity by processing each hit in the user_activity and performing the following steps:
167 - 1. Extracts relevant information from the hit.
168 - 2. Checks if the current activity is already present in the unique_instances set.
169 - 3. If not present, adds the user activity to the IRIS case.
170 - 4. Creates an asset payload using the current activity.
171 - 5. Updates the case with the asset payload.
172 - 6. Updates the event analyzed multiple logins flag for the hit.
173 - 7. Adds the current activity to the unique_instances set.
174 -
175 - Parameters:
176 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
177 - - unique_instances (set): A set containing unique instances of user activity.
178 - - case_id (str): The ID of the IRIS case.
179 -
180 - Returns:
181 - None
182 - """
183 - for hit in user_activity.hits.hits:
184 - current_activity = {
185 - "loginID": hit.source.params_loginID,
186 - "ip": hit.source.ip,
187 - "country": hit.source.httpReq_country,
188 - "errMessage": hit.source.errMessage,
189 - "event_timestamp": hit.source.event_timestamp,
190 - "customer_code": hit.source.customer_code,
191 - "errDetails": hit.source.errDetails,
192 - }
193 - current_activity_frozenset = frozenset(current_activity.items())
194 - if current_activity_frozenset not in unique_instances:
195 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
196 - current_asset = SuspiciousLogin(**current_activity)
197 - asset_payload = create_asset_payload(asset=current_asset)
198 - logger.info(f"Asset Payload: {asset_payload}")
199 - await update_case_with_asset(case_id, asset_payload)
200 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
201 - unique_instances.add(current_activity_frozenset)
202 -
203 -
204 -def create_asset_payload(asset: SuspiciousLogin):
205 - """
206 - Create a payload for adding an asset based on a SuspiciousLogin object.
207 -
208 - Args:
209 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
210 -
211 - Returns:
212 - AddAssetModel: The payload for adding the asset.
213 -
214 - """
215 - if asset.errMessage == "OK":
216 - return AddAssetModel(
217 - name=asset.loginID,
218 - ip=asset.ip,
219 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
220 - asset_type=1,
221 - compromise_status=1,
222 - analysis_status=2,
223 - )
224 - return AddAssetModel(
225 - name=asset.loginID,
226 - ip=asset.ip,
227 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
228 - asset_type=1,
229 - analysis_status=2,
230 - )
231 -
232 -
233 -async def update_case_with_asset(case_id: str, asset_payload):
234 - """
235 - Update the case with the asset information.
236 -
237 - :param case_id: The ID of the case to update
238 - :param asset_payload: The payload to update the case with
239 -
240 - :return: None
241 - """
242 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
243 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
244 - return await fetch_and_validate_data(
245 - client,
246 - case_client.add_asset,
247 - cid=case_id,
248 - **asset_payload.to_dict(),
249 - )
250 -
251 -
252 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
253 - """
254 - Creates an IRIS case for multiple logins with the same IP address.
255 -
256 - Args:
257 - suspicious_login (SuspiciousLogin): The suspicious login information.
258 - session (AsyncSession): The async session for database operations.
259 -
260 - Returns:
261 - CaseResponse: The response containing the created case information.
262 - """
263 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
264 - case_name = (
265 - f"Log Source: {suspicious_login.logSource} SAP SIEM. "
266 - f"10 or more login failures for different users from the same IP within 5 minutes."
267 - )
268 -
269 - case_description = (
270 - f"Log Source: {suspicious_login.logSource}\n\n"
271 - f"IP Address: {suspicious_login.ip}\n\n"
272 - f"Country: {suspicious_login.country}\n\n"
273 - f"Timestamp: {suspicious_login.event_timestamp}"
274 - )
275 -
276 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
277 -
278 - payload = IrisCasePayload(
279 - case_name=case_name,
280 - case_description=case_description,
281 - case_customer=case_customer,
282 - case_classification=18,
283 - soc_id="1",
284 - create_customer=False,
285 - )
286 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
287 - result = await fetch_and_validate_data(
288 - client,
289 - case_client.add_case,
290 - **payload.to_dict(),
291 - )
292 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
293 -
294 - return CaseResponse(**result)
295 -
296 -
297 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
298 - """
299 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
300 - Collects a max of 1000 records.
301 -
302 - :param suspicious_logins: A list of suspicious logins
303 -
304 - :return: List of the user Activity collected from the sap_siem table
305 - """
306 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
307 - results = es_client.search(
308 - index="sap_siem_*",
309 - # index="new-integrations*",
310 - body={
311 - "size": 1000,
312 - "query": {"bool": {"must": [{"term": {"ip": suspicious_logins.ip}}]}},
313 - },
314 - )
315 - return SapSiemWazuhIndexerResponse(**results)
316 -
317 -
318 -async def get_initial_search_results(es_client):
319 - """
320 - Retrieves the initial search results from Elasticsearch.
321 -
322 - Args:
323 - es_client (Elasticsearch): The Elasticsearch client.
324 -
325 - Returns:
326 - dict: The search results.
327 - """
328 - return es_client.search(
329 - index="sap_siem_*",
330 - # index="new-integrations*",
331 - body={
332 - "size": 1000,
333 - "query": {"bool": {"must": [{"term": {"event_analyzed_brute_force_same_ip": "False"}}]}},
334 - "sort": [{"event_timestamp": {"order": "asc"}}],
335 - },
336 - scroll="1m",
337 - )
338 -
339 -
340 -async def get_next_batch_of_results(es_client, scroll_id):
341 - """
342 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
343 -
344 - Args:
345 - es_client (Elasticsearch): The Elasticsearch client.
346 - scroll_id (str): The scroll ID to retrieve the next batch of results.
347 -
348 - Returns:
349 - dict: The next batch of results.
350 - """
351 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
352 -
353 -
354 -async def process_hits(hits, ip_to_login_ids, suspicious_activity, time_range):
355 - """
356 - Process the hits received from SAP SIEM and update the IP to login IDs mapping and suspicious activity.
357 -
358 - Args:
359 - hits (list): List of hits received from SAP SIEM.
360 - ip_to_login_ids (dict): Dictionary mapping IP addresses to login IDs.
361 - suspicious_activity (dict): Dictionary mapping IP addresses to a list of suspicious login objects.
362 -
363 - Returns:
364 - None
365 - """
366 - ip_to_login_ids = defaultdict(lambda: defaultdict(list))
367 -
368 - for hit in hits:
369 - # Convert loginID to lowercase before comparing
370 - login_id = hit.source.params_loginID.lower()
371 - ip = hit.source.ip
372 - errCode = hit.source.errCode
373 -
374 - # Ignore loginID if it does not contain a '@'
375 - if "@" not in login_id:
376 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
377 - continue
378 -
379 - # Parse the event timestamp
380 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
381 -
382 - # Add the timestamp and errCode to the list for this IP for this loginID
383 - ip_to_login_ids[ip][login_id].append((event_timestamp, errCode))
384 -
385 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
386 -
387 - # Check if there are at least 10 different loginIDs for the same IP within the last 5 minutes
388 - login_ids_in_last_5_minutes = set()
389 - for other_login_id, other_info in ip_to_login_ids[ip].items():
390 - for other_timestamp, other_errCode in other_info:
391 - if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
392 - login_ids_in_last_5_minutes.add((other_login_id, other_errCode))
393 -
394 - # If there are at least 10 different failed loginIDs, log the suspicious activity
395 - failed_login_ids = [login_id for login_id, errCode in login_ids_in_last_5_minutes if errCode != "0"]
396 - logger.info(f"Failed loginIDs: {failed_login_ids}")
397 - if len(failed_login_ids) >= 10:
398 - logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
399 - suspicious_login = SuspiciousLogin(
400 - _index=hit.index,
401 - _id=hit.id,
402 - customer_code=hit.source.customer_code,
403 - logSource=hit.source.logSource,
404 - loginID=hit.source.params_loginID,
405 - country=hit.source.httpReq_country,
406 - ip=hit.source.ip,
407 - event_timestamp=hit.source.event_timestamp,
408 - errMessage=hit.source.errMessage,
409 - errDetails=hit.source.errDetails,
410 - )
411 - suspicious_activity[ip].append(suspicious_login)
412 - logger.info(f"Added suspicious login: {suspicious_login}")
413 -
414 -
415 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
416 - """
417 - Checks for multiple successful logins by IP address.
418 -
419 - Args:
420 - threshold (int): The minimum number of logins required to be considered suspicious.
421 -
422 - Returns:
423 - List[SuspiciousLogin]: A list of suspicious login objects.
424 - """
425 - ip_to_login_ids = defaultdict(set)
426 - suspicious_activity = defaultdict(list)
427 -
428 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
429 - scroll_id = None
430 -
431 - while True:
432 - if scroll_id is None:
433 - results = await get_initial_search_results(es_client)
434 - else:
435 - results = await get_next_batch_of_results(es_client, scroll_id)
436 -
437 - if not results["hits"]["hits"]:
438 - break
439 -
440 - results = SapSiemWazuhIndexerResponse(**results)
441 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
442 -
443 - scroll_id = results.scroll_id
444 -
445 - # Clear the scroll when you're done to free up resources
446 - if scroll_id is not None:
447 - es_client.clear_scroll(scroll_id=scroll_id)
448 -
449 - logger.info(f"Suspicious activity: {suspicious_activity}")
450 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
451 -
452 - return [login for sublist in suspicious_activity.values() for login in sublist]
453 -
454 -
455 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
456 - """
457 - Retrieves a list of suspicious login attempts based on the specified threshold.
458 -
459 - Args:
460 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
461 -
462 - Returns:
463 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
464 - """
465 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
466 -
467 -
468 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
469 - """
470 - Retrieves an existing database record for the given IP address.
471 -
472 - Args:
473 - session (AsyncSession): The async session object for database operations.
474 - ip (str): The IP address to search for.
475 -
476 - Returns:
477 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
478 - """
479 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
480 - return result.scalar_one_or_none() if result is not None else None
481 -
482 -
483 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
484 - """
485 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
486 -
487 - Args:
488 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
489 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
490 -
491 - Returns:
492 - None
493 - """
494 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
495 - if not new_login_ids.issubset(existing_loginIDs):
496 - updated_login_ids = existing_loginIDs.union(new_login_ids)
497 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
498 - existing_case.last_case_created_timestamp = datetime.now()
499 -
500 -
501 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
502 - """
503 - Creates a new database record for SAP SIEM multiple logins.
504 -
505 - Args:
506 - ip (str): The IP address associated with the multiple logins.
507 - new_login_ids (Set[str]): The set of new login IDs.
508 -
509 - Returns:
510 - SapSiemMultipleLogins: The newly created database record.
511 - """
512 - return SapSiemMultipleLogins(
513 - ip=ip,
514 - last_case_created_timestamp=datetime.now(),
515 - associated_loginIDs=",".join(new_login_ids),
516 - )
517 -
518 -
519 -async def sap_siem_brute_force_failed_same_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
520 - """
521 - Finds same IP with multiple users and handles suspicious logins.
522 -
523 - Args:
524 - threshold (int): The threshold value for determining suspicious logins.
525 - session (AsyncSession): The database session.
526 -
527 - Returns:
528 - InvokeSAPSiemResponse: The response indicating the success of the operation.
529 - """
530 - logger.info("Finding same user from different IP addresses")
531 -
532 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
533 - logger.info(f"Suspicious IPs: {suspicious_ips}")
534 -
535 - unique_instances = set()
536 - case_ids = []
537 - # Dictionary to aggregate suspicious logins by IP
538 - aggregated_logins_by_ip = defaultdict(list)
539 -
540 - for suspicious_login in suspicious_ips:
541 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
542 -
543 - for ip, associated_logins in aggregated_logins_by_ip.items():
544 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
545 - if session is not None:
546 - existing_case = await get_existing_database_record(session, ip)
547 -
548 - new_login_ids = {login.loginID for login in associated_logins}
549 - if existing_case:
550 - logger.info(f"Updating existing database record: {existing_case}")
551 - update_existing_database_record(existing_case, new_login_ids)
552 - else:
553 - logger.info(f"Creating new case for IP: {ip}")
554 - new_case = create_new_database_record(ip, new_login_ids)
555 - session.add(new_case)
556 -
557 - # Create a single new IRIS case for this IP
558 - # Modify this to include information from all associated_logins
559 - await handle_suspicious_login_multiple(
560 - associated_logins[0],
561 - unique_instances,
562 - case_ids,
563 - session=session,
564 - )
565 - else:
566 - raise HTTPException(
567 - status_code=500,
568 - detail="Failed to create IRIS case",
569 - )
570 - await session.commit()
571 -
572 - # Clear the global set
573 - checked_ips.clear()
574 -
575 - return InvokeSAPSiemResponse(
576 - success=True,
577 - message="SAP SIEM multiple logins invoked.",
578 - )
backend/app/integrations/sap_siem/services/sap_siem_brute_forced_failed_logins.py deleted
-575
@@ -1,575 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 - await mark_as_checked(suspicious_login)
55 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 - await send_to_shuffle(
57 - ShufflePayload(
58 - alert_id=case.data.case_id,
59 - customer=suspicious_login.customer_code,
60 - customer_code=suspicious_login.customer_code,
61 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 - rule_description=f"{case.data.case_name}",
63 - hostname=suspicious_login.ip,
64 - rule_name="Rule:_Logins_from_different_IP_addresses",
65 - affected_ip=suspicious_login.ip,
66 - alert_type="ip",
67 - ),
68 - session=session,
69 - )
70 -
71 -
72 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
73 - """
74 - Handles suspicious login events with multiple logins.
75 -
76 - Args:
77 - suspicious_login: The suspicious login event.
78 - unique_instances: List of unique instances of the suspicious login event.
79 - case_ids: List of case IDs associated with the suspicious login event.
80 - session: The database session.
81 -
82 - Returns:
83 - None
84 - """
85 - await handle_common_suspicious_login_tasks(
86 - suspicious_login,
87 - unique_instances,
88 - case_ids,
89 - create_iris_case_multiple,
90 - session,
91 - )
92 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
93 -
94 -
95 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
96 - """
97 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
98 -
99 - :param suspicious_login: The suspicious login to update
100 -
101 - :return: None
102 - """
103 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
104 - try:
105 - es_client.update(
106 - index=index,
107 - id=id,
108 - body={
109 - "doc": {
110 - "event_analyzed_brute_force_ip": "True",
111 - },
112 - },
113 - )
114 - logger.info(f"Updated event_analyzed_brute_force_ip flag for suspicious login: {id}")
115 - except Exception as e:
116 - logger.error(
117 - f"Failed to update case created flag {e}",
118 - )
119 - # Attempt to remove read-only block
120 - try:
121 - es_client.indices.put_settings(
122 - index=index,
123 - body={"index.blocks.write": None},
124 - )
125 - logger.info(
126 - f"Removed read-only block from index {index}. Retrying update.",
127 - )
128 -
129 - # Retry the update operation
130 - es_client.update(
131 - index=index,
132 - id=id,
133 - body={"doc": {"event_analyzed_brute_force_ip": "True"}},
134 - )
135 - logger.info(
136 - f"Added event_analyzed_brute_force_ip flag to index {index} for suspicious login: {id}",
137 - )
138 -
139 - # Reenable the write block
140 - es_client.indices.put_settings(
141 - index=index,
142 - body={"index.blocks.write": True},
143 - )
144 - except Exception as e2:
145 - logger.error(
146 - f"Failed to remove read-only block from index {index}: {e2}",
147 - )
148 - return False
149 -
150 -
151 -async def mark_as_checked(suspicious_login):
152 - """
153 - Marks a suspicious login as checked by adding it to the set of checked IPs.
154 -
155 - Args:
156 - suspicious_login (Login): The suspicious login object to mark as checked.
157 -
158 - Returns:
159 - None
160 - """
161 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
162 -
163 -
164 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
165 - """
166 - Handles user activity by processing each hit in the user_activity and performing the following steps:
167 - 1. Extracts relevant information from the hit.
168 - 2. Checks if the current activity is already present in the unique_instances set.
169 - 3. If not present, adds the user activity to the IRIS case.
170 - 4. Creates an asset payload using the current activity.
171 - 5. Updates the case with the asset payload.
172 - 6. Updates the event analyzed multiple logins flag for the hit.
173 - 7. Adds the current activity to the unique_instances set.
174 -
175 - Parameters:
176 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
177 - - unique_instances (set): A set containing unique instances of user activity.
178 - - case_id (str): The ID of the IRIS case.
179 -
180 - Returns:
181 - None
182 - """
183 - for hit in user_activity.hits.hits:
184 - current_activity = {
185 - "loginID": hit.source.params_loginID,
186 - "ip": hit.source.ip,
187 - "country": hit.source.httpReq_country,
188 - "errMessage": hit.source.errMessage,
189 - "event_timestamp": hit.source.event_timestamp,
190 - "customer_code": hit.source.customer_code,
191 - "errDetails": hit.source.errDetails,
192 - }
193 - current_activity_frozenset = frozenset(current_activity.items())
194 - if current_activity_frozenset not in unique_instances:
195 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
196 - current_asset = SuspiciousLogin(**current_activity)
197 - asset_payload = create_asset_payload(asset=current_asset)
198 - logger.info(f"Asset Payload: {asset_payload}")
199 - await update_case_with_asset(case_id, asset_payload)
200 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
201 - unique_instances.add(current_activity_frozenset)
202 -
203 -
204 -def create_asset_payload(asset: SuspiciousLogin):
205 - """
206 - Create a payload for adding an asset based on a SuspiciousLogin object.
207 -
208 - Args:
209 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
210 -
211 - Returns:
212 - AddAssetModel: The payload for adding the asset.
213 -
214 - """
215 - if asset.errMessage == "OK":
216 - return AddAssetModel(
217 - name=asset.loginID,
218 - ip=asset.ip,
219 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
220 - asset_type=1,
221 - compromise_status=1,
222 - analysis_status=2,
223 - )
224 - return AddAssetModel(
225 - name=asset.loginID,
226 - ip=asset.ip,
227 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
228 - asset_type=1,
229 - analysis_status=2,
230 - )
231 -
232 -
233 -async def update_case_with_asset(case_id: str, asset_payload):
234 - """
235 - Update the case with the asset information.
236 -
237 - :param case_id: The ID of the case to update
238 - :param asset_payload: The payload to update the case with
239 -
240 - :return: None
241 - """
242 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
243 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
244 - return await fetch_and_validate_data(
245 - client,
246 - case_client.add_asset,
247 - cid=case_id,
248 - **asset_payload.to_dict(),
249 - )
250 -
251 -
252 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
253 - """
254 - Creates an IRIS case for multiple logins with the same IP address.
255 -
256 - Args:
257 - suspicious_login (SuspiciousLogin): The suspicious login information.
258 - session (AsyncSession): The async session for database operations.
259 -
260 - Returns:
261 - CaseResponse: The response containing the created case information.
262 - """
263 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
264 - case_name = f"Log Source: {suspicious_login.logSource} SAP SIEM. " f"25 or more login failures from different IP addresses."
265 -
266 - case_description = (
267 - f"Log Source: {suspicious_login.logSource}\n\n"
268 - f"IP Address: {suspicious_login.ip}\n\n"
269 - f"Country: {suspicious_login.country}\n\n"
270 - f"Timestamp: {suspicious_login.event_timestamp}"
271 - )
272 -
273 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
274 -
275 - payload = IrisCasePayload(
276 - case_name=case_name,
277 - case_description=case_description,
278 - case_customer=case_customer,
279 - case_classification=18,
280 - soc_id="1",
281 - create_customer=False,
282 - )
283 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
284 - result = await fetch_and_validate_data(
285 - client,
286 - case_client.add_case,
287 - **payload.to_dict(),
288 - )
289 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
290 -
291 - return CaseResponse(**result)
292 -
293 -
294 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
295 - """
296 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
297 - Collects a max of 1000 records.
298 -
299 - :param suspicious_logins: A list of suspicious logins
300 -
301 - :return: List of the user Activity collected from the sap_siem table
302 - """
303 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
304 - results = es_client.search(
305 - index="sap_siem_*",
306 - # index="new-integrations*",
307 - body={
308 - "size": 1000,
309 - "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
310 - },
311 - )
312 - return SapSiemWazuhIndexerResponse(**results)
313 -
314 -
315 -async def get_initial_search_results(es_client):
316 - """
317 - Retrieves the initial search results from Elasticsearch.
318 -
319 - Args:
320 - es_client (Elasticsearch): The Elasticsearch client.
321 -
322 - Returns:
323 - dict: The search results.
324 - """
325 - return es_client.search(
326 - index="sap_siem_*",
327 - # index="new-integrations*",
328 - body={
329 - "size": 1000,
330 - "query": {"bool": {"must": [{"term": {"event_analyzed_brute_force_ip": "False"}}]}},
331 - "sort": [{"event_timestamp": {"order": "asc"}}],
332 - },
333 - scroll="1m",
334 - )
335 -
336 -
337 -async def get_next_batch_of_results(es_client, scroll_id):
338 - """
339 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
340 -
341 - Args:
342 - es_client (Elasticsearch): The Elasticsearch client.
343 - scroll_id (str): The scroll ID to retrieve the next batch of results.
344 -
345 - Returns:
346 - dict: The next batch of results.
347 - """
348 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
349 -
350 -
351 -async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
352 - """
353 - Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
354 -
355 - Args:
356 - hits (list): List of hits received from SAP SIEM.
357 - login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
358 - suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
359 -
360 - Returns:
361 - None
362 - """
363 - login_id_to_ips = defaultdict(lambda: defaultdict(list))
364 -
365 - for hit in hits:
366 - # Convert loginID to lowercase before comparing
367 - login_id = hit.source.params_loginID.lower()
368 - ip = hit.source.ip
369 - errCode = hit.source.errCode
370 -
371 - # Ignore loginID if it does not contain a '@'
372 - if "@" not in login_id:
373 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
374 - continue
375 -
376 - # Parse the event timestamp
377 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
378 -
379 - # Add the timestamp and errCode to the list for this loginID for this IP
380 - login_id_to_ips[login_id][ip].append((event_timestamp, errCode))
381 -
382 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
383 -
384 - # Check if there are at least 25 different IPs for the same loginID within the last 3 minutes
385 - ips_in_last_3_minutes = set()
386 - for other_ip, other_info in login_id_to_ips[login_id].items():
387 - for other_timestamp, other_errCode in other_info:
388 - if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
389 - ips_in_last_3_minutes.add((other_ip, other_errCode))
390 -
391 - # If there are at least 25 different failed IPs, log the suspicious activity
392 - failed_ips = [ip for ip, errCode in ips_in_last_3_minutes if errCode != "0"]
393 - logger.info(f"Failed IPs: {failed_ips}")
394 - if len(failed_ips) >= 25:
395 - logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
396 - suspicious_login = SuspiciousLogin(
397 - _index=hit.index,
398 - _id=hit.id,
399 - customer_code=hit.source.customer_code,
400 - logSource=hit.source.logSource,
401 - loginID=hit.source.params_loginID,
402 - country=hit.source.httpReq_country,
403 - ip=hit.source.ip,
404 - event_timestamp=hit.source.event_timestamp,
405 - errMessage=hit.source.errMessage,
406 - errDetails=hit.source.errDetails,
407 - )
408 - suspicious_activity[login_id].append(suspicious_login)
409 - logger.info(f"Added suspicious login: {suspicious_login}")
410 -
411 -
412 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
413 - """
414 - Checks for multiple successful logins by IP address.
415 -
416 - Args:
417 - threshold (int): The minimum number of logins required to be considered suspicious.
418 -
419 - Returns:
420 - List[SuspiciousLogin]: A list of suspicious login objects.
421 - """
422 - ip_to_login_ids = defaultdict(set)
423 - suspicious_activity = defaultdict(list)
424 -
425 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
426 - scroll_id = None
427 -
428 - while True:
429 - if scroll_id is None:
430 - results = await get_initial_search_results(es_client)
431 - else:
432 - results = await get_next_batch_of_results(es_client, scroll_id)
433 -
434 - if not results["hits"]["hits"]:
435 - break
436 -
437 - results = SapSiemWazuhIndexerResponse(**results)
438 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
439 -
440 - scroll_id = results.scroll_id
441 -
442 - # Clear the scroll when you're done to free up resources
443 - if scroll_id is not None:
444 - es_client.clear_scroll(scroll_id=scroll_id)
445 -
446 - logger.info(f"Suspicious activity: {suspicious_activity}")
447 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
448 -
449 - return [login for sublist in suspicious_activity.values() for login in sublist]
450 -
451 -
452 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
453 - """
454 - Retrieves a list of suspicious login attempts based on the specified threshold.
455 -
456 - Args:
457 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
458 -
459 - Returns:
460 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
461 - """
462 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
463 -
464 -
465 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
466 - """
467 - Retrieves an existing database record for the given IP address.
468 -
469 - Args:
470 - session (AsyncSession): The async session object for database operations.
471 - ip (str): The IP address to search for.
472 -
473 - Returns:
474 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
475 - """
476 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
477 - return result.scalar_one_or_none() if result is not None else None
478 -
479 -
480 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
481 - """
482 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
483 -
484 - Args:
485 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
486 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
487 -
488 - Returns:
489 - None
490 - """
491 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
492 - if not new_login_ids.issubset(existing_loginIDs):
493 - updated_login_ids = existing_loginIDs.union(new_login_ids)
494 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
495 - existing_case.last_case_created_timestamp = datetime.now()
496 -
497 -
498 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
499 - """
500 - Creates a new database record for SAP SIEM multiple logins.
501 -
502 - Args:
503 - ip (str): The IP address associated with the multiple logins.
504 - new_login_ids (Set[str]): The set of new login IDs.
505 -
506 - Returns:
507 - SapSiemMultipleLogins: The newly created database record.
508 - """
509 - return SapSiemMultipleLogins(
510 - ip=ip,
511 - last_case_created_timestamp=datetime.now(),
512 - associated_loginIDs=",".join(new_login_ids),
513 - )
514 -
515 -
516 -async def sap_siem_brute_force_failed_multiple_ips(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
517 - """
518 - Finds same IP with multiple users and handles suspicious logins.
519 -
520 - Args:
521 - threshold (int): The threshold value for determining suspicious logins.
522 - session (AsyncSession): The database session.
523 -
524 - Returns:
525 - InvokeSAPSiemResponse: The response indicating the success of the operation.
526 - """
527 - logger.info("Finding same user from different IP addresses")
528 -
529 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
530 - logger.info(f"Suspicious IPs: {suspicious_ips}")
531 -
532 - unique_instances = set()
533 - case_ids = []
534 - # Dictionary to aggregate suspicious logins by IP
535 - aggregated_logins_by_ip = defaultdict(list)
536 -
537 - for suspicious_login in suspicious_ips:
538 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
539 -
540 - for ip, associated_logins in aggregated_logins_by_ip.items():
541 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
542 - if session is not None:
543 - existing_case = await get_existing_database_record(session, ip)
544 -
545 - new_login_ids = {login.loginID for login in associated_logins}
546 - if existing_case:
547 - logger.info(f"Updating existing database record: {existing_case}")
548 - update_existing_database_record(existing_case, new_login_ids)
549 - else:
550 - logger.info(f"Creating new case for IP: {ip}")
551 - new_case = create_new_database_record(ip, new_login_ids)
552 - session.add(new_case)
553 -
554 - # Create a single new IRIS case for this IP
555 - # Modify this to include information from all associated_logins
556 - await handle_suspicious_login_multiple(
557 - associated_logins[0],
558 - unique_instances,
559 - case_ids,
560 - session=session,
561 - )
562 - else:
563 - raise HTTPException(
564 - status_code=500,
565 - detail="Failed to create IRIS case",
566 - )
567 - await session.commit()
568 -
569 - # Clear the global set
570 - checked_ips.clear()
571 -
572 - return InvokeSAPSiemResponse(
573 - success=True,
574 - message="SAP SIEM multiple logins invoked.",
575 - )
backend/app/integrations/sap_siem/services/sap_siem_failed_same_user_different_geo_location.py deleted
-580
@@ -1,580 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 - await mark_as_checked(suspicious_login)
55 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 - await send_to_shuffle(
57 - ShufflePayload(
58 - alert_id=case.data.case_id,
59 - customer=suspicious_login.customer_code,
60 - customer_code=suspicious_login.customer_code,
61 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 - rule_description=f"{case.data.case_name}",
63 - hostname=suspicious_login.ip,
64 - rule_name="Rule:_Same_user_from_different_geo_locations",
65 - affected_user=suspicious_login.loginID,
66 - alert_type="user",
67 - ),
68 - session=session,
69 - )
70 -
71 -
72 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
73 - """
74 - Handles suspicious login events with multiple logins.
75 -
76 - Args:
77 - suspicious_login: The suspicious login event.
78 - unique_instances: List of unique instances of the suspicious login event.
79 - case_ids: List of case IDs associated with the suspicious login event.
80 - session: The database session.
81 -
82 - Returns:
83 - None
84 - """
85 - await handle_common_suspicious_login_tasks(
86 - suspicious_login,
87 - unique_instances,
88 - case_ids,
89 - create_iris_case_multiple,
90 - session,
91 - )
92 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
93 -
94 -
95 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
96 - """
97 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
98 -
99 - :param suspicious_login: The suspicious login to update
100 -
101 - :return: None
102 - """
103 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
104 - try:
105 - es_client.update(
106 - index=index,
107 - id=id,
108 - body={
109 - "doc": {
110 - "event_analyzed_same_user_failed_diff_geo": "True",
111 - },
112 - },
113 - )
114 - logger.info(f"Updated event_analyzed_same_user_failed_diff_geo flag for suspicious login: {id}")
115 - except Exception as e:
116 - logger.error(
117 - f"Failed to update case created flag {e}",
118 - )
119 - # Attempt to remove read-only block
120 - try:
121 - es_client.indices.put_settings(
122 - index=index,
123 - body={"index.blocks.write": None},
124 - )
125 - logger.info(
126 - f"Removed read-only block from index {index}. Retrying update.",
127 - )
128 -
129 - # Retry the update operation
130 - es_client.update(
131 - index=index,
132 - id=id,
133 - body={"doc": {"event_analyzed_same_user_failed_diff_geo": "True"}},
134 - )
135 - logger.info(
136 - f"Added event_analyzed_same_user_failed_diff_geo flag to index {index} for suspicious login: {id}",
137 - )
138 -
139 - # Reenable the write block
140 - es_client.indices.put_settings(
141 - index=index,
142 - body={"index.blocks.write": True},
143 - )
144 - except Exception as e2:
145 - logger.error(
146 - f"Failed to remove read-only block from index {index}: {e2}",
147 - )
148 - return False
149 -
150 -
151 -async def mark_as_checked(suspicious_login):
152 - """
153 - Marks a suspicious login as checked by adding it to the set of checked IPs.
154 -
155 - Args:
156 - suspicious_login (Login): The suspicious login object to mark as checked.
157 -
158 - Returns:
159 - None
160 - """
161 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
162 -
163 -
164 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
165 - """
166 - Handles user activity by processing each hit in the user_activity and performing the following steps:
167 - 1. Extracts relevant information from the hit.
168 - 2. Checks if the current activity is already present in the unique_instances set.
169 - 3. If not present, adds the user activity to the IRIS case.
170 - 4. Creates an asset payload using the current activity.
171 - 5. Updates the case with the asset payload.
172 - 6. Updates the event analyzed multiple logins flag for the hit.
173 - 7. Adds the current activity to the unique_instances set.
174 -
175 - Parameters:
176 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
177 - - unique_instances (set): A set containing unique instances of user activity.
178 - - case_id (str): The ID of the IRIS case.
179 -
180 - Returns:
181 - None
182 - """
183 - for hit in user_activity.hits.hits:
184 - current_activity = {
185 - "loginID": hit.source.params_loginID,
186 - "ip": hit.source.ip,
187 - "country": hit.source.httpReq_country,
188 - "errMessage": hit.source.errMessage,
189 - "event_timestamp": hit.source.event_timestamp,
190 - "customer_code": hit.source.customer_code,
191 - "errDetails": hit.source.errDetails,
192 - }
193 - current_activity_frozenset = frozenset(current_activity.items())
194 - if current_activity_frozenset not in unique_instances:
195 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
196 - current_asset = SuspiciousLogin(**current_activity)
197 - asset_payload = create_asset_payload(asset=current_asset)
198 - logger.info(f"Asset Payload: {asset_payload}")
199 - await update_case_with_asset(case_id, asset_payload)
200 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
201 - unique_instances.add(current_activity_frozenset)
202 -
203 -
204 -def create_asset_payload(asset: SuspiciousLogin):
205 - """
206 - Create a payload for adding an asset based on a SuspiciousLogin object.
207 -
208 - Args:
209 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
210 -
211 - Returns:
212 - AddAssetModel: The payload for adding the asset.
213 -
214 - """
215 - if asset.errMessage == "OK":
216 - return AddAssetModel(
217 - name=asset.loginID,
218 - ip=asset.ip,
219 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
220 - asset_type=1,
221 - compromise_status=1,
222 - analysis_status=2,
223 - )
224 - return AddAssetModel(
225 - name=asset.loginID,
226 - ip=asset.ip,
227 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
228 - asset_type=1,
229 - analysis_status=2,
230 - )
231 -
232 -
233 -async def update_case_with_asset(case_id: str, asset_payload):
234 - """
235 - Update the case with the asset information.
236 -
237 - :param case_id: The ID of the case to update
238 - :param asset_payload: The payload to update the case with
239 -
240 - :return: None
241 - """
242 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
243 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
244 - return await fetch_and_validate_data(
245 - client,
246 - case_client.add_asset,
247 - cid=case_id,
248 - **asset_payload.to_dict(),
249 - )
250 -
251 -
252 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
253 - """
254 - Creates an IRIS case for multiple logins with the same IP address.
255 -
256 - Args:
257 - suspicious_login (SuspiciousLogin): The suspicious login information.
258 - session (AsyncSession): The async session for database operations.
259 -
260 - Returns:
261 - CaseResponse: The response containing the created case information.
262 - """
263 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
264 - case_name = (
265 - f"Log Source: {suspicious_login.logSource} SAP SIEM. "
266 - f"User {suspicious_login.loginID} had multiple login failures from different GEO locations."
267 - )
268 -
269 - case_description = (
270 - f"Log Source: {suspicious_login.logSource}\n\n"
271 - f"IP Address: {suspicious_login.ip}\n\n"
272 - f"Country: {suspicious_login.country}\n\n"
273 - f"Timestamp: {suspicious_login.event_timestamp}"
274 - )
275 -
276 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
277 -
278 - payload = IrisCasePayload(
279 - case_name=case_name,
280 - case_description=case_description,
281 - case_customer=case_customer,
282 - case_classification=18,
283 - soc_id="1",
284 - create_customer=False,
285 - )
286 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
287 - result = await fetch_and_validate_data(
288 - client,
289 - case_client.add_case,
290 - **payload.to_dict(),
291 - )
292 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
293 -
294 - return CaseResponse(**result)
295 -
296 -
297 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
298 - """
299 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
300 - Collects a max of 1000 records.
301 -
302 - :param suspicious_logins: A list of suspicious logins
303 -
304 - :return: List of the user Activity collected from the sap_siem table
305 - """
306 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
307 - results = es_client.search(
308 - index="sap_siem_*",
309 - # index="new-integrations*",
310 - body={
311 - "size": 1000,
312 - "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
313 - },
314 - )
315 - return SapSiemWazuhIndexerResponse(**results)
316 -
317 -
318 -async def get_initial_search_results(es_client):
319 - """
320 - Retrieves the initial search results from Elasticsearch.
321 -
322 - Args:
323 - es_client (Elasticsearch): The Elasticsearch client.
324 -
325 - Returns:
326 - dict: The search results.
327 - """
328 - return es_client.search(
329 - index="sap_siem_*",
330 - # index="new-integrations*",
331 - body={
332 - "size": 1000,
333 - "query": {"bool": {"must": [{"term": {"event_analyzed_same_user_failed_diff_geo": "False"}}]}},
334 - "sort": [{"event_timestamp": {"order": "asc"}}],
335 - },
336 - scroll="1m",
337 - )
338 -
339 -
340 -async def get_next_batch_of_results(es_client, scroll_id):
341 - """
342 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
343 -
344 - Args:
345 - es_client (Elasticsearch): The Elasticsearch client.
346 - scroll_id (str): The scroll ID to retrieve the next batch of results.
347 -
348 - Returns:
349 - dict: The next batch of results.
350 - """
351 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
352 -
353 -
354 -async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
355 - """
356 - Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
357 -
358 - Args:
359 - hits (list): List of hits received from SAP SIEM.
360 - login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
361 - suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
362 -
363 - Returns:
364 - None
365 - """
366 - login_id_to_ips = defaultdict(lambda: defaultdict(list))
367 -
368 - for hit in hits:
369 - # Convert loginID to lowercase before comparing
370 - login_id = hit.source.params_loginID.lower()
371 - ip = hit.source.ip
372 - errCode = hit.source.errCode
373 - country = hit.source.httpReq_country
374 -
375 - # Ignore loginID if it does not contain a '@'
376 - if "@" not in login_id:
377 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
378 - continue
379 -
380 - # Parse the event timestamp
381 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
382 -
383 - # Add the timestamp, errCode and country to the list for this loginID for this IP
384 - login_id_to_ips[login_id][ip].append((event_timestamp, errCode, country))
385 -
386 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
387 -
388 - # Check if there are at least 3 different IPs for the same loginID within the last 20 minutes
389 - ips_in_last_20_minutes = set()
390 - for other_ip, other_info in login_id_to_ips[login_id].items():
391 - for other_timestamp, other_errCode, other_country in other_info:
392 - if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
393 - ips_in_last_20_minutes.add((other_ip, other_errCode, other_country))
394 -
395 - # If there are at least 3 different failed IPs from at least two different GEO IP country locations, log the suspicious activity
396 - failed_ips = [ip for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"]
397 - countries = set([country for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"])
398 - logger.info(f"Failed IPs: {failed_ips}")
399 - if len(failed_ips) >= 3 and len(countries) >= 2:
400 - logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
401 - suspicious_login = SuspiciousLogin(
402 - _index=hit.index,
403 - _id=hit.id,
404 - customer_code=hit.source.customer_code,
405 - logSource=hit.source.logSource,
406 - loginID=hit.source.params_loginID,
407 - country=hit.source.httpReq_country,
408 - ip=hit.source.ip,
409 - event_timestamp=hit.source.event_timestamp,
410 - errMessage=hit.source.errMessage,
411 - errDetails=hit.source.errDetails,
412 - )
413 - suspicious_activity[login_id].append(suspicious_login)
414 - logger.info(f"Added suspicious login: {suspicious_login}")
415 -
416 -
417 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
418 - """
419 - Checks for multiple successful logins by IP address.
420 -
421 - Args:
422 - threshold (int): The minimum number of logins required to be considered suspicious.
423 -
424 - Returns:
425 - List[SuspiciousLogin]: A list of suspicious login objects.
426 - """
427 - ip_to_login_ids = defaultdict(set)
428 - suspicious_activity = defaultdict(list)
429 -
430 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
431 - scroll_id = None
432 -
433 - while True:
434 - if scroll_id is None:
435 - results = await get_initial_search_results(es_client)
436 - else:
437 - results = await get_next_batch_of_results(es_client, scroll_id)
438 -
439 - if not results["hits"]["hits"]:
440 - break
441 -
442 - results = SapSiemWazuhIndexerResponse(**results)
443 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
444 -
445 - scroll_id = results.scroll_id
446 -
447 - # Clear the scroll when you're done to free up resources
448 - if scroll_id is not None:
449 - es_client.clear_scroll(scroll_id=scroll_id)
450 -
451 - logger.info(f"Suspicious activity: {suspicious_activity}")
452 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
453 -
454 - return [login for sublist in suspicious_activity.values() for login in sublist]
455 -
456 -
457 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
458 - """
459 - Retrieves a list of suspicious login attempts based on the specified threshold.
460 -
461 - Args:
462 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
463 -
464 - Returns:
465 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
466 - """
467 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
468 -
469 -
470 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
471 - """
472 - Retrieves an existing database record for the given IP address.
473 -
474 - Args:
475 - session (AsyncSession): The async session object for database operations.
476 - ip (str): The IP address to search for.
477 -
478 - Returns:
479 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
480 - """
481 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
482 - return result.scalar_one_or_none() if result is not None else None
483 -
484 -
485 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
486 - """
487 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
488 -
489 - Args:
490 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
491 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
492 -
493 - Returns:
494 - None
495 - """
496 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
497 - if not new_login_ids.issubset(existing_loginIDs):
498 - updated_login_ids = existing_loginIDs.union(new_login_ids)
499 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
500 - existing_case.last_case_created_timestamp = datetime.now()
501 -
502 -
503 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
504 - """
505 - Creates a new database record for SAP SIEM multiple logins.
506 -
507 - Args:
508 - ip (str): The IP address associated with the multiple logins.
509 - new_login_ids (Set[str]): The set of new login IDs.
510 -
511 - Returns:
512 - SapSiemMultipleLogins: The newly created database record.
513 - """
514 - return SapSiemMultipleLogins(
515 - ip=ip,
516 - last_case_created_timestamp=datetime.now(),
517 - associated_loginIDs=",".join(new_login_ids),
518 - )
519 -
520 -
521 -async def sap_siem_failed_same_user_diff_geo(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
522 - """
523 - Finds same IP with multiple users and handles suspicious logins.
524 -
525 - Args:
526 - threshold (int): The threshold value for determining suspicious logins.
527 - session (AsyncSession): The database session.
528 -
529 - Returns:
530 - InvokeSAPSiemResponse: The response indicating the success of the operation.
531 - """
532 - logger.info("Finding same user from different IP addresses")
533 -
534 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
535 - logger.info(f"Suspicious IPs: {suspicious_ips}")
536 -
537 - unique_instances = set()
538 - case_ids = []
539 - # Dictionary to aggregate suspicious logins by IP
540 - aggregated_logins_by_ip = defaultdict(list)
541 -
542 - for suspicious_login in suspicious_ips:
543 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
544 -
545 - for ip, associated_logins in aggregated_logins_by_ip.items():
546 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
547 - if session is not None:
548 - existing_case = await get_existing_database_record(session, ip)
549 -
550 - new_login_ids = {login.loginID for login in associated_logins}
551 - if existing_case:
552 - logger.info(f"Updating existing database record: {existing_case}")
553 - update_existing_database_record(existing_case, new_login_ids)
554 - else:
555 - logger.info(f"Creating new case for IP: {ip}")
556 - new_case = create_new_database_record(ip, new_login_ids)
557 - session.add(new_case)
558 -
559 - # Create a single new IRIS case for this IP
560 - # Modify this to include information from all associated_logins
561 - await handle_suspicious_login_multiple(
562 - associated_logins[0],
563 - unique_instances,
564 - case_ids,
565 - session=session,
566 - )
567 - else:
568 - raise HTTPException(
569 - status_code=500,
570 - detail="Failed to create IRIS case",
571 - )
572 - await session.commit()
573 -
574 - # Clear the global set
575 - checked_ips.clear()
576 -
577 - return InvokeSAPSiemResponse(
578 - success=True,
579 - message="SAP SIEM multiple logins invoked.",
580 - )
backend/app/integrations/sap_siem/services/sap_siem_failed_same_user_from_different_ip.py deleted
-578
@@ -1,578 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 - await mark_as_checked(suspicious_login)
55 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 - await send_to_shuffle(
57 - ShufflePayload(
58 - alert_id=case.data.case_id,
59 - customer=suspicious_login.customer_code,
60 - customer_code=suspicious_login.customer_code,
61 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 - rule_description=f"{case.data.case_name}",
63 - hostname=suspicious_login.ip,
64 - rule_name="Rule:_Same_user_from_different_IP_addresses",
65 - affected_user=suspicious_login.loginID,
66 - alert_type="user",
67 - ),
68 - session=session,
69 - )
70 -
71 -
72 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
73 - """
74 - Handles suspicious login events with multiple logins.
75 -
76 - Args:
77 - suspicious_login: The suspicious login event.
78 - unique_instances: List of unique instances of the suspicious login event.
79 - case_ids: List of case IDs associated with the suspicious login event.
80 - session: The database session.
81 -
82 - Returns:
83 - None
84 - """
85 - await handle_common_suspicious_login_tasks(
86 - suspicious_login,
87 - unique_instances,
88 - case_ids,
89 - create_iris_case_multiple,
90 - session,
91 - )
92 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
93 -
94 -
95 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
96 - """
97 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
98 -
99 - :param suspicious_login: The suspicious login to update
100 -
101 - :return: None
102 - """
103 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
104 - try:
105 - es_client.update(
106 - index=index,
107 - id=id,
108 - body={
109 - "doc": {
110 - "event_analyzed_same_user_failed_diff_ip": "True",
111 - },
112 - },
113 - )
114 - logger.info(f"Updated event_analyzed_same_user_failed_diff_ip flag for suspicious login: {id}")
115 - except Exception as e:
116 - logger.error(
117 - f"Failed to update case created flag {e}",
118 - )
119 - # Attempt to remove read-only block
120 - try:
121 - es_client.indices.put_settings(
122 - index=index,
123 - body={"index.blocks.write": None},
124 - )
125 - logger.info(
126 - f"Removed read-only block from index {index}. Retrying update.",
127 - )
128 -
129 - # Retry the update operation
130 - es_client.update(
131 - index=index,
132 - id=id,
133 - body={"doc": {"event_analyzed_same_user_failed_diff_ip": "True"}},
134 - )
135 - logger.info(
136 - f"Added event_analyzed_same_user_failed_diff_ip flag to index {index} for suspicious login: {id}",
137 - )
138 -
139 - # Reenable the write block
140 - es_client.indices.put_settings(
141 - index=index,
142 - body={"index.blocks.write": True},
143 - )
144 - except Exception as e2:
145 - logger.error(
146 - f"Failed to remove read-only block from index {index}: {e2}",
147 - )
148 - return False
149 -
150 -
151 -async def mark_as_checked(suspicious_login):
152 - """
153 - Marks a suspicious login as checked by adding it to the set of checked IPs.
154 -
155 - Args:
156 - suspicious_login (Login): The suspicious login object to mark as checked.
157 -
158 - Returns:
159 - None
160 - """
161 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
162 -
163 -
164 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
165 - """
166 - Handles user activity by processing each hit in the user_activity and performing the following steps:
167 - 1. Extracts relevant information from the hit.
168 - 2. Checks if the current activity is already present in the unique_instances set.
169 - 3. If not present, adds the user activity to the IRIS case.
170 - 4. Creates an asset payload using the current activity.
171 - 5. Updates the case with the asset payload.
172 - 6. Updates the event analyzed multiple logins flag for the hit.
173 - 7. Adds the current activity to the unique_instances set.
174 -
175 - Parameters:
176 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
177 - - unique_instances (set): A set containing unique instances of user activity.
178 - - case_id (str): The ID of the IRIS case.
179 -
180 - Returns:
181 - None
182 - """
183 - for hit in user_activity.hits.hits:
184 - current_activity = {
185 - "loginID": hit.source.params_loginID,
186 - "ip": hit.source.ip,
187 - "country": hit.source.httpReq_country,
188 - "errMessage": hit.source.errMessage,
189 - "event_timestamp": hit.source.event_timestamp,
190 - "customer_code": hit.source.customer_code,
191 - "errDetails": hit.source.errDetails,
192 - }
193 - current_activity_frozenset = frozenset(current_activity.items())
194 - if current_activity_frozenset not in unique_instances:
195 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
196 - current_asset = SuspiciousLogin(**current_activity)
197 - asset_payload = create_asset_payload(asset=current_asset)
198 - logger.info(f"Asset Payload: {asset_payload}")
199 - await update_case_with_asset(case_id, asset_payload)
200 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
201 - unique_instances.add(current_activity_frozenset)
202 -
203 -
204 -def create_asset_payload(asset: SuspiciousLogin):
205 - """
206 - Create a payload for adding an asset based on a SuspiciousLogin object.
207 -
208 - Args:
209 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
210 -
211 - Returns:
212 - AddAssetModel: The payload for adding the asset.
213 -
214 - """
215 - if asset.errMessage == "OK":
216 - return AddAssetModel(
217 - name=asset.loginID,
218 - ip=asset.ip,
219 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
220 - asset_type=1,
221 - compromise_status=1,
222 - analysis_status=2,
223 - )
224 - return AddAssetModel(
225 - name=asset.loginID,
226 - ip=asset.ip,
227 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
228 - asset_type=1,
229 - analysis_status=2,
230 - )
231 -
232 -
233 -async def update_case_with_asset(case_id: str, asset_payload):
234 - """
235 - Update the case with the asset information.
236 -
237 - :param case_id: The ID of the case to update
238 - :param asset_payload: The payload to update the case with
239 -
240 - :return: None
241 - """
242 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
243 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
244 - return await fetch_and_validate_data(
245 - client,
246 - case_client.add_asset,
247 - cid=case_id,
248 - **asset_payload.to_dict(),
249 - )
250 -
251 -
252 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
253 - """
254 - Creates an IRIS case for multiple logins with the same IP address.
255 -
256 - Args:
257 - suspicious_login (SuspiciousLogin): The suspicious login information.
258 - session (AsyncSession): The async session for database operations.
259 -
260 - Returns:
261 - CaseResponse: The response containing the created case information.
262 - """
263 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
264 - case_name = (
265 - f"Log Source: {suspicious_login.logSource} SAP SIEM. "
266 - f"User {suspicious_login.loginID} had multiple login failures from different IP addresses."
267 - )
268 -
269 - case_description = (
270 - f"Log Source: {suspicious_login.logSource}\n\n"
271 - f"IP Address: {suspicious_login.ip}\n\n"
272 - f"Country: {suspicious_login.country}\n\n"
273 - f"Timestamp: {suspicious_login.event_timestamp}"
274 - )
275 -
276 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
277 -
278 - payload = IrisCasePayload(
279 - case_name=case_name,
280 - case_description=case_description,
281 - case_customer=case_customer,
282 - case_classification=18,
283 - soc_id="1",
284 - create_customer=False,
285 - )
286 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
287 - result = await fetch_and_validate_data(
288 - client,
289 - case_client.add_case,
290 - **payload.to_dict(),
291 - )
292 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
293 -
294 - return CaseResponse(**result)
295 -
296 -
297 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
298 - """
299 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
300 - Collects a max of 1000 records.
301 -
302 - :param suspicious_logins: A list of suspicious logins
303 -
304 - :return: List of the user Activity collected from the sap_siem table
305 - """
306 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
307 - results = es_client.search(
308 - index="sap_siem_*",
309 - # index="new-integrations*",
310 - body={
311 - "size": 1000,
312 - "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
313 - },
314 - )
315 - return SapSiemWazuhIndexerResponse(**results)
316 -
317 -
318 -async def get_initial_search_results(es_client):
319 - """
320 - Retrieves the initial search results from Elasticsearch.
321 -
322 - Args:
323 - es_client (Elasticsearch): The Elasticsearch client.
324 -
325 - Returns:
326 - dict: The search results.
327 - """
328 - return es_client.search(
329 - index="sap_siem_*",
330 - # index="new-integrations*",
331 - body={
332 - "size": 1000,
333 - "query": {"bool": {"must": [{"term": {"event_analyzed_same_user_failed_diff_ip": "False"}}]}},
334 - "sort": [{"event_timestamp": {"order": "asc"}}],
335 - },
336 - scroll="1m",
337 - )
338 -
339 -
340 -async def get_next_batch_of_results(es_client, scroll_id):
341 - """
342 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
343 -
344 - Args:
345 - es_client (Elasticsearch): The Elasticsearch client.
346 - scroll_id (str): The scroll ID to retrieve the next batch of results.
347 -
348 - Returns:
349 - dict: The next batch of results.
350 - """
351 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
352 -
353 -
354 -async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
355 - """
356 - Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
357 -
358 - Args:
359 - hits (list): List of hits received from SAP SIEM.
360 - login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
361 - suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
362 -
363 - Returns:
364 - None
365 - """
366 - login_id_to_ips = defaultdict(lambda: defaultdict(list))
367 -
368 - for hit in hits:
369 - # Convert loginID to lowercase before comparing
370 - login_id = hit.source.params_loginID.lower()
371 - ip = hit.source.ip
372 - errCode = hit.source.errCode
373 -
374 - # Ignore loginID if it does not contain a '@'
375 - if "@" not in login_id:
376 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
377 - continue
378 -
379 - # Parse the event timestamp
380 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
381 -
382 - # Add the timestamp and errCode to the list for this loginID for this IP
383 - login_id_to_ips[login_id][ip].append((event_timestamp, errCode))
384 -
385 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
386 -
387 - # Check if there are at least 3 different IPs for the same loginID within the last 10 minutes
388 - ips_in_last_10_minutes = set()
389 - for other_ip, other_info in login_id_to_ips[login_id].items():
390 - for other_timestamp, other_errCode in other_info:
391 - if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
392 - ips_in_last_10_minutes.add((other_ip, other_errCode))
393 -
394 - # If there are at least 3 different failed IPs, log the suspicious activity
395 - failed_ips = [ip for ip, errCode in ips_in_last_10_minutes if errCode != "0"]
396 - logger.info(f"Failed IPs: {failed_ips}")
397 - if len(failed_ips) >= 3:
398 - logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
399 - suspicious_login = SuspiciousLogin(
400 - _index=hit.index,
401 - _id=hit.id,
402 - customer_code=hit.source.customer_code,
403 - logSource=hit.source.logSource,
404 - loginID=hit.source.params_loginID,
405 - country=hit.source.httpReq_country,
406 - ip=hit.source.ip,
407 - event_timestamp=hit.source.event_timestamp,
408 - errMessage=hit.source.errMessage,
409 - errDetails=hit.source.errDetails,
410 - )
411 - suspicious_activity[login_id].append(suspicious_login)
412 - logger.info(f"Added suspicious login: {suspicious_login}")
413 -
414 -
415 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
416 - """
417 - Checks for multiple successful logins by IP address.
418 -
419 - Args:
420 - threshold (int): The minimum number of logins required to be considered suspicious.
421 -
422 - Returns:
423 - List[SuspiciousLogin]: A list of suspicious login objects.
424 - """
425 - ip_to_login_ids = defaultdict(set)
426 - suspicious_activity = defaultdict(list)
427 -
428 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
429 - scroll_id = None
430 -
431 - while True:
432 - if scroll_id is None:
433 - results = await get_initial_search_results(es_client)
434 - else:
435 - results = await get_next_batch_of_results(es_client, scroll_id)
436 -
437 - if not results["hits"]["hits"]:
438 - break
439 -
440 - results = SapSiemWazuhIndexerResponse(**results)
441 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
442 -
443 - scroll_id = results.scroll_id
444 -
445 - # Clear the scroll when you're done to free up resources
446 - if scroll_id is not None:
447 - es_client.clear_scroll(scroll_id=scroll_id)
448 -
449 - logger.info(f"Suspicious activity: {suspicious_activity}")
450 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
451 -
452 - return [login for sublist in suspicious_activity.values() for login in sublist]
453 -
454 -
455 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
456 - """
457 - Retrieves a list of suspicious login attempts based on the specified threshold.
458 -
459 - Args:
460 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
461 -
462 - Returns:
463 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
464 - """
465 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
466 -
467 -
468 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
469 - """
470 - Retrieves an existing database record for the given IP address.
471 -
472 - Args:
473 - session (AsyncSession): The async session object for database operations.
474 - ip (str): The IP address to search for.
475 -
476 - Returns:
477 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
478 - """
479 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
480 - return result.scalar_one_or_none() if result is not None else None
481 -
482 -
483 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
484 - """
485 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
486 -
487 - Args:
488 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
489 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
490 -
491 - Returns:
492 - None
493 - """
494 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
495 - if not new_login_ids.issubset(existing_loginIDs):
496 - updated_login_ids = existing_loginIDs.union(new_login_ids)
497 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
498 - existing_case.last_case_created_timestamp = datetime.now()
499 -
500 -
501 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
502 - """
503 - Creates a new database record for SAP SIEM multiple logins.
504 -
505 - Args:
506 - ip (str): The IP address associated with the multiple logins.
507 - new_login_ids (Set[str]): The set of new login IDs.
508 -
509 - Returns:
510 - SapSiemMultipleLogins: The newly created database record.
511 - """
512 - return SapSiemMultipleLogins(
513 - ip=ip,
514 - last_case_created_timestamp=datetime.now(),
515 - associated_loginIDs=",".join(new_login_ids),
516 - )
517 -
518 -
519 -async def sap_siem_failed_same_user_diff_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
520 - """
521 - Finds same IP with multiple users and handles suspicious logins.
522 -
523 - Args:
524 - threshold (int): The threshold value for determining suspicious logins.
525 - session (AsyncSession): The database session.
526 -
527 - Returns:
528 - InvokeSAPSiemResponse: The response indicating the success of the operation.
529 - """
530 - logger.info("Finding same user from different IP addresses")
531 -
532 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
533 - logger.info(f"Suspicious IPs: {suspicious_ips}")
534 -
535 - unique_instances = set()
536 - case_ids = []
537 - # Dictionary to aggregate suspicious logins by IP
538 - aggregated_logins_by_ip = defaultdict(list)
539 -
540 - for suspicious_login in suspicious_ips:
541 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
542 -
543 - for ip, associated_logins in aggregated_logins_by_ip.items():
544 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
545 - if session is not None:
546 - existing_case = await get_existing_database_record(session, ip)
547 -
548 - new_login_ids = {login.loginID for login in associated_logins}
549 - if existing_case:
550 - logger.info(f"Updating existing database record: {existing_case}")
551 - update_existing_database_record(existing_case, new_login_ids)
552 - else:
553 - logger.info(f"Creating new case for IP: {ip}")
554 - new_case = create_new_database_record(ip, new_login_ids)
555 - session.add(new_case)
556 -
557 - # Create a single new IRIS case for this IP
558 - # Modify this to include information from all associated_logins
559 - await handle_suspicious_login_multiple(
560 - associated_logins[0],
561 - unique_instances,
562 - case_ids,
563 - session=session,
564 - )
565 - else:
566 - raise HTTPException(
567 - status_code=500,
568 - detail="Failed to create IRIS case",
569 - )
570 - await session.commit()
571 -
572 - # Clear the global set
573 - checked_ips.clear()
574 -
575 - return InvokeSAPSiemResponse(
576 - success=True,
577 - message="SAP SIEM multiple logins invoked.",
578 - )
backend/app/integrations/sap_siem/services/sap_siem_multiple_logins.py deleted
-598
@@ -1,598 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 - await mark_as_checked(suspicious_login)
55 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 - await send_to_shuffle(
57 - ShufflePayload(
58 - alert_id=case.data.case_id,
59 - customer=suspicious_login.customer_code,
60 - customer_code=suspicious_login.customer_code,
61 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 - rule_description=f"{case.data.case_name}",
63 - hostname=suspicious_login.ip,
64 - ),
65 - session=session,
66 - )
67 -
68 -
69 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
70 - """
71 - Handles suspicious login events with multiple logins.
72 -
73 - Args:
74 - suspicious_login: The suspicious login event.
75 - unique_instances: List of unique instances of the suspicious login event.
76 - case_ids: List of case IDs associated with the suspicious login event.
77 - session: The database session.
78 -
79 - Returns:
80 - None
81 - """
82 - await handle_common_suspicious_login_tasks(
83 - suspicious_login,
84 - unique_instances,
85 - case_ids,
86 - create_iris_case_multiple,
87 - session,
88 - )
89 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
90 -
91 -
92 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 - """
94 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 -
96 - :param suspicious_login: The suspicious login to update
97 -
98 - :return: None
99 - """
100 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 - try:
102 - es_client.update(
103 - index=index,
104 - id=id,
105 - body={
106 - "doc": {
107 - "event_analyzed_multiple_logins": "True",
108 - },
109 - },
110 - )
111 - logger.info(f"Updated event_analyzed_multiple_logins flag for suspicious login: {id}")
112 - except Exception as e:
113 - logger.error(
114 - f"Failed to update case created flag {e}",
115 - )
116 - # Attempt to remove read-only block
117 - try:
118 - es_client.indices.put_settings(
119 - index=index,
120 - body={"index.blocks.write": None},
121 - )
122 - logger.info(
123 - f"Removed read-only block from index {index}. Retrying update.",
124 - )
125 -
126 - # Retry the update operation
127 - es_client.update(
128 - index=index,
129 - id=id,
130 - body={"doc": {"event_analyzed_multiple_logins": "True"}},
131 - )
132 - logger.info(
133 - f"Added event_analyzed_multiple_logins flag to index {index} for suspicious login: {id}",
134 - )
135 -
136 - # Reenable the write block
137 - es_client.indices.put_settings(
138 - index=index,
139 - body={"index.blocks.write": True},
140 - )
141 - except Exception as e2:
142 - logger.error(
143 - f"Failed to remove read-only block from index {index}: {e2}",
144 - )
145 - return False
146 -
147 -
148 -async def mark_as_checked(suspicious_login):
149 - """
150 - Marks a suspicious login as checked by adding it to the set of checked IPs.
151 -
152 - Args:
153 - suspicious_login (Login): The suspicious login object to mark as checked.
154 -
155 - Returns:
156 - None
157 - """
158 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 -
160 -
161 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 - """
163 - Handles user activity by processing each hit in the user_activity and performing the following steps:
164 - 1. Extracts relevant information from the hit.
165 - 2. Checks if the current activity is already present in the unique_instances set.
166 - 3. If not present, adds the user activity to the IRIS case.
167 - 4. Creates an asset payload using the current activity.
168 - 5. Updates the case with the asset payload.
169 - 6. Updates the event analyzed multiple logins flag for the hit.
170 - 7. Adds the current activity to the unique_instances set.
171 -
172 - Parameters:
173 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 - - unique_instances (set): A set containing unique instances of user activity.
175 - - case_id (str): The ID of the IRIS case.
176 -
177 - Returns:
178 - None
179 - """
180 - for hit in user_activity.hits.hits:
181 - current_activity = {
182 - "loginID": hit.source.params_loginID,
183 - "ip": hit.source.ip,
184 - "country": hit.source.httpReq_country,
185 - "errMessage": hit.source.errMessage,
186 - "event_timestamp": hit.source.event_timestamp,
187 - "customer_code": hit.source.customer_code,
188 - "errDetails": hit.source.errDetails,
189 - }
190 - current_activity_frozenset = frozenset(current_activity.items())
191 - if current_activity_frozenset not in unique_instances:
192 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 - current_asset = SuspiciousLogin(**current_activity)
194 - asset_payload = create_asset_payload(asset=current_asset)
195 - logger.info(f"Asset Payload: {asset_payload}")
196 - await update_case_with_asset(case_id, asset_payload)
197 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 - unique_instances.add(current_activity_frozenset)
199 -
200 -
201 -def create_asset_payload(asset: SuspiciousLogin):
202 - """
203 - Create a payload for adding an asset based on a SuspiciousLogin object.
204 -
205 - Args:
206 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 -
208 - Returns:
209 - AddAssetModel: The payload for adding the asset.
210 -
211 - """
212 - if asset.errMessage == "OK":
213 - return AddAssetModel(
214 - name=asset.loginID,
215 - ip=asset.ip,
216 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 - asset_type=1,
218 - compromise_status=1,
219 - analysis_status=2,
220 - )
221 - return AddAssetModel(
222 - name=asset.loginID,
223 - ip=asset.ip,
224 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 - asset_type=1,
226 - analysis_status=2,
227 - )
228 -
229 -
230 -async def update_case_with_asset(case_id: str, asset_payload):
231 - """
232 - Update the case with the asset information.
233 -
234 - :param case_id: The ID of the case to update
235 - :param asset_payload: The payload to update the case with
236 -
237 - :return: None
238 - """
239 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 - return await fetch_and_validate_data(
242 - client,
243 - case_client.add_asset,
244 - cid=case_id,
245 - **asset_payload.to_dict(),
246 - )
247 -
248 -
249 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 - """
251 - Creates an IRIS case for multiple logins with the same IP address.
252 -
253 - Args:
254 - suspicious_login (SuspiciousLogin): The suspicious login information.
255 - session (AsyncSession): The async session for database operations.
256 -
257 - Returns:
258 - CaseResponse: The response containing the created case information.
259 - """
260 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 - case_name = (
262 - f"Log Source: {suspicious_login.logSource} SAP SIEM. " f"IP Address: {suspicious_login.ip} found logging in with multiple users."
263 - )
264 -
265 - case_description = (
266 - f"Log Source: {suspicious_login.logSource}\n\n"
267 - f"IP Address: {suspicious_login.ip}\n\n"
268 - f"Country: {suspicious_login.country}\n\n"
269 - f"Timestamp: {suspicious_login.event_timestamp}"
270 - )
271 -
272 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
273 -
274 - payload = IrisCasePayload(
275 - case_name=case_name,
276 - case_description=case_description,
277 - case_customer=case_customer,
278 - case_classification=18,
279 - soc_id="1",
280 - create_customer=False,
281 - )
282 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
283 - result = await fetch_and_validate_data(
284 - client,
285 - case_client.add_case,
286 - **payload.to_dict(),
287 - )
288 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
289 -
290 - return CaseResponse(**result)
291 -
292 -
293 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
294 - """
295 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
296 - Collects a max of 1000 records.
297 -
298 - :param suspicious_logins: A list of suspicious logins
299 -
300 - :return: List of the user Activity collected from the sap_siem table
301 - """
302 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
303 - results = es_client.search(
304 - index="sap_siem_*",
305 - # index="new-integrations*",
306 - body={
307 - "size": 1000,
308 - "query": {"bool": {"must": [{"term": {"ip": suspicious_logins.ip}}]}},
309 - },
310 - )
311 - return SapSiemWazuhIndexerResponse(**results)
312 -
313 -
314 -async def get_initial_search_results(es_client):
315 - """
316 - Retrieves the initial search results from Elasticsearch.
317 -
318 - Args:
319 - es_client (Elasticsearch): The Elasticsearch client.
320 -
321 - Returns:
322 - dict: The search results.
323 - """
324 - return es_client.search(
325 - index="sap_siem_*",
326 - # index="new-integrations*",
327 - body={
328 - "size": 1000,
329 - "query": {"bool": {"must": [{"term": {"errMessage": "OK"}}, {"term": {"event_analyzed_multiple_logins": "False"}}]}},
330 - "sort": [{"event_timestamp": {"order": "asc"}}],
331 - },
332 - scroll="1m",
333 - )
334 -
335 -
336 -async def get_next_batch_of_results(es_client, scroll_id):
337 - """
338 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
339 -
340 - Args:
341 - es_client (Elasticsearch): The Elasticsearch client.
342 - scroll_id (str): The scroll ID to retrieve the next batch of results.
343 -
344 - Returns:
345 - dict: The next batch of results.
346 - """
347 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
348 -
349 -
350 -async def process_hits(hits, ip_to_login_ids, suspicious_activity, time_range):
351 - """
352 - Process the hits received from SAP SIEM and update the IP to login IDs mapping and suspicious activity.
353 -
354 - Args:
355 - hits (list): List of hits received from SAP SIEM.
356 - ip_to_login_ids (dict): Dictionary mapping IP addresses to login IDs.
357 - suspicious_activity (dict): Dictionary mapping IP addresses to a list of suspicious login objects.
358 -
359 - Returns:
360 - None
361 - """
362 - # for hit in hits:
363 - # if hit.source.errMessage == "OK":
364 - # # Convert loginID to lowercase before comparing
365 - # login_id = hit.source.params_loginID.lower()
366 - # ip_to_login_ids[hit.source.ip].add(login_id)
367 -
368 - # suspicious_login = SuspiciousLogin(
369 - # _index=hit.index,
370 - # _id=hit.id,
371 - # customer_code=hit.source.customer_code,
372 - # logSource=hit.source.logSource,
373 - # loginID=hit.source.params_loginID,
374 - # country=hit.source.httpReq_country,
375 - # ip=hit.source.ip,
376 - # event_timestamp=hit.source.event_timestamp,
377 - # errMessage=hit.source.errMessage,
378 - # errDetails=hit.source.errDetails,
379 - # )
380 -
381 - # suspicious_activity[hit.source.ip].append(suspicious_login)
382 - # Keep track of the timestamps for each loginID for each IP
383 - ip_to_login_timestamps = defaultdict(lambda: defaultdict(list))
384 -
385 - for hit in hits:
386 - if hit.source.errMessage == "OK":
387 - # logger.info(f"Processing hit: {hit}")
388 - # Convert loginID to lowercase before comparing
389 - login_id = hit.source.params_loginID.lower()
390 - ip = hit.source.ip
391 -
392 - # Ignore loginID if it does not contain a '@'
393 - if "@" not in login_id:
394 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
395 - continue
396 -
397 - # Parse the event timestamp
398 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
399 -
400 - # Add the timestamp to the list for this loginID for this IP
401 - ip_to_login_timestamps[ip][login_id].append(event_timestamp)
402 -
403 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
404 -
405 - # Check if there's another loginID for the same IP within the last 10 minutes
406 - for other_login_id, timestamps in ip_to_login_timestamps[ip].items():
407 - if other_login_id != login_id:
408 - if any(
409 - event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp
410 - for other_timestamp in timestamps
411 - ):
412 - # Add the loginID to the set for this IP
413 - ip_to_login_ids[ip].add(login_id)
414 -
415 - logger.info(f"Detected multiple logins within 10 minutes for IP {ip}: {login_id} and {other_login_id}")
416 -
417 - suspicious_login = SuspiciousLogin(
418 - _index=hit.index,
419 - _id=hit.id,
420 - customer_code=hit.source.customer_code,
421 - logSource=hit.source.logSource,
422 - loginID=hit.source.params_loginID,
423 - country=hit.source.httpReq_country,
424 - ip=hit.source.ip,
425 - event_timestamp=hit.source.event_timestamp,
426 - errMessage=hit.source.errMessage,
427 - errDetails=hit.source.errDetails,
428 - )
429 -
430 - suspicious_activity[ip].append(suspicious_login)
431 - logger.info(f"Added suspicious login: {suspicious_login}")
432 - break
433 -
434 -
435 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
436 - """
437 - Checks for multiple successful logins by IP address.
438 -
439 - Args:
440 - threshold (int): The minimum number of logins required to be considered suspicious.
441 -
442 - Returns:
443 - List[SuspiciousLogin]: A list of suspicious login objects.
444 - """
445 - ip_to_login_ids = defaultdict(set)
446 - suspicious_activity = defaultdict(list)
447 -
448 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
449 - scroll_id = None
450 -
451 - while True:
452 - if scroll_id is None:
453 - results = await get_initial_search_results(es_client)
454 - logger.info(f"Initial search results: {results}")
455 - else:
456 - results = await get_next_batch_of_results(es_client, scroll_id)
457 -
458 - if not results["hits"]["hits"]:
459 - break
460 -
461 - results = SapSiemWazuhIndexerResponse(**results)
462 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
463 -
464 - scroll_id = results.scroll_id
465 -
466 - # Clear the scroll when you're done to free up resources
467 - if scroll_id is not None:
468 - es_client.clear_scroll(scroll_id=scroll_id)
469 -
470 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items() if len(ip_to_login_ids[ip]) > threshold}
471 -
472 - return [login for sublist in suspicious_activity.values() for login in sublist]
473 -
474 -
475 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
476 - """
477 - Retrieves a list of suspicious login attempts based on the specified threshold.
478 -
479 - Args:
480 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
481 -
482 - Returns:
483 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
484 - """
485 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
486 -
487 -
488 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
489 - """
490 - Retrieves an existing database record for the given IP address.
491 -
492 - Args:
493 - session (AsyncSession): The async session object for database operations.
494 - ip (str): The IP address to search for.
495 -
496 - Returns:
497 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
498 - """
499 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
500 - return result.scalar_one_or_none() if result is not None else None
501 -
502 -
503 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
504 - """
505 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
506 -
507 - Args:
508 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
509 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
510 -
511 - Returns:
512 - None
513 - """
514 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
515 - if not new_login_ids.issubset(existing_loginIDs):
516 - updated_login_ids = existing_loginIDs.union(new_login_ids)
517 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
518 - existing_case.last_case_created_timestamp = datetime.now()
519 -
520 -
521 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
522 - """
523 - Creates a new database record for SAP SIEM multiple logins.
524 -
525 - Args:
526 - ip (str): The IP address associated with the multiple logins.
527 - new_login_ids (Set[str]): The set of new login IDs.
528 -
529 - Returns:
530 - SapSiemMultipleLogins: The newly created database record.
531 - """
532 - return SapSiemMultipleLogins(
533 - ip=ip,
534 - last_case_created_timestamp=datetime.now(),
535 - associated_loginIDs=",".join(new_login_ids),
536 - )
537 -
538 -
539 -async def sap_siem_multiple_logins_same_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
540 - """
541 - Finds same IP with multiple users and handles suspicious logins.
542 -
543 - Args:
544 - threshold (int): The threshold value for determining suspicious logins.
545 - session (AsyncSession): The database session.
546 -
547 - Returns:
548 - InvokeSAPSiemResponse: The response indicating the success of the operation.
549 - """
550 - logger.info("Finding same IP with multiple users")
551 -
552 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
553 - logger.info(f"Suspicious IPs: {suspicious_ips}")
554 -
555 - unique_instances = set()
556 - case_ids = []
557 - # Dictionary to aggregate suspicious logins by IP
558 - aggregated_logins_by_ip = defaultdict(list)
559 -
560 - for suspicious_login in suspicious_ips:
561 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
562 -
563 - for ip, associated_logins in aggregated_logins_by_ip.items():
564 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
565 - if session is not None:
566 - existing_case = await get_existing_database_record(session, ip)
567 -
568 - new_login_ids = {login.loginID for login in associated_logins}
569 - if existing_case:
570 - logger.info(f"Updating existing database record: {existing_case}")
571 - update_existing_database_record(existing_case, new_login_ids)
572 - else:
573 - logger.info(f"Creating new case for IP: {ip}")
574 - new_case = create_new_database_record(ip, new_login_ids)
575 - session.add(new_case)
576 -
577 - # Create a single new IRIS case for this IP
578 - # Modify this to include information from all associated_logins
579 - await handle_suspicious_login_multiple(
580 - associated_logins[0],
581 - unique_instances,
582 - case_ids,
583 - session=session,
584 - )
585 - else:
586 - raise HTTPException(
587 - status_code=500,
588 - detail="Failed to create IRIS case",
589 - )
590 - await session.commit()
591 -
592 - # Clear the global set
593 - checked_ips.clear()
594 -
595 - return InvokeSAPSiemResponse(
596 - success=True,
597 - message="SAP SIEM multiple logins invoked.",
598 - )
backend/app/integrations/sap_siem/services/sap_siem_successful_login_same_ip_after_multiple_failures.py deleted
-596
@@ -1,596 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 - await mark_as_checked(suspicious_login)
55 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 - await send_to_shuffle(
57 - ShufflePayload(
58 - alert_id=case.data.case_id,
59 - customer=suspicious_login.customer_code,
60 - customer_code=suspicious_login.customer_code,
61 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 - rule_description=f"{case.data.case_name}",
63 - hostname=suspicious_login.ip,
64 - rule_name="Rule:_Successful_login_after_multiple_failed_logins",
65 - affected_user=suspicious_login.loginID,
66 - alert_type="user",
67 - ),
68 - session=session,
69 - )
70 -
71 -
72 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
73 - """
74 - Handles suspicious login events with multiple logins.
75 -
76 - Args:
77 - suspicious_login: The suspicious login event.
78 - unique_instances: List of unique instances of the suspicious login event.
79 - case_ids: List of case IDs associated with the suspicious login event.
80 - session: The database session.
81 -
82 - Returns:
83 - None
84 - """
85 - await handle_common_suspicious_login_tasks(
86 - suspicious_login,
87 - unique_instances,
88 - case_ids,
89 - create_iris_case_multiple,
90 - session,
91 - )
92 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
93 -
94 -
95 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
96 - """
97 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
98 -
99 - :param suspicious_login: The suspicious login to update
100 -
101 - :return: None
102 - """
103 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
104 - try:
105 - es_client.update(
106 - index=index,
107 - id=id,
108 - body={
109 - "doc": {
110 - "event_analyzed_successful_login_after_failures_diff_loginID": "True",
111 - },
112 - },
113 - )
114 - logger.info(f"Updated event_analyzed_successful_login_after_failures_diff_loginID flag for suspicious login: {id}")
115 - except Exception as e:
116 - logger.error(
117 - f"Failed to update case created flag {e}",
118 - )
119 - # Attempt to remove read-only block
120 - try:
121 - es_client.indices.put_settings(
122 - index=index,
123 - body={"index.blocks.write": None},
124 - )
125 - logger.info(
126 - f"Removed read-only block from index {index}. Retrying update.",
127 - )
128 -
129 - # Retry the update operation
130 - es_client.update(
131 - index=index,
132 - id=id,
133 - body={"doc": {"event_analyzed_successful_login_after_failures_diff_loginID": "True"}},
134 - )
135 - logger.info(
136 - f"Added event_analyzed_successful_login_after_failures_diff_loginID flag to index {index} for suspicious login: {id}",
137 - )
138 -
139 - # Reenable the write block
140 - es_client.indices.put_settings(
141 - index=index,
142 - body={"index.blocks.write": True},
143 - )
144 - except Exception as e2:
145 - logger.error(
146 - f"Failed to remove read-only block from index {index}: {e2}",
147 - )
148 - return False
149 -
150 -
151 -async def mark_as_checked(suspicious_login):
152 - """
153 - Marks a suspicious login as checked by adding it to the set of checked IPs.
154 -
155 - Args:
156 - suspicious_login (Login): The suspicious login object to mark as checked.
157 -
158 - Returns:
159 - None
160 - """
161 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
162 -
163 -
164 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
165 - """
166 - Handles user activity by processing each hit in the user_activity and performing the following steps:
167 - 1. Extracts relevant information from the hit.
168 - 2. Checks if the current activity is already present in the unique_instances set.
169 - 3. If not present, adds the user activity to the IRIS case.
170 - 4. Creates an asset payload using the current activity.
171 - 5. Updates the case with the asset payload.
172 - 6. Updates the event analyzed multiple logins flag for the hit.
173 - 7. Adds the current activity to the unique_instances set.
174 -
175 - Parameters:
176 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
177 - - unique_instances (set): A set containing unique instances of user activity.
178 - - case_id (str): The ID of the IRIS case.
179 -
180 - Returns:
181 - None
182 - """
183 - for hit in user_activity.hits.hits:
184 - current_activity = {
185 - "loginID": hit.source.params_loginID,
186 - "ip": hit.source.ip,
187 - "country": hit.source.httpReq_country,
188 - "errMessage": hit.source.errMessage,
189 - "event_timestamp": hit.source.event_timestamp,
190 - "customer_code": hit.source.customer_code,
191 - "errDetails": hit.source.errDetails,
192 - }
193 - current_activity_frozenset = frozenset(current_activity.items())
194 - if current_activity_frozenset not in unique_instances:
195 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
196 - current_asset = SuspiciousLogin(**current_activity)
197 - asset_payload = create_asset_payload(asset=current_asset)
198 - logger.info(f"Asset Payload: {asset_payload}")
199 - await update_case_with_asset(case_id, asset_payload)
200 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
201 - unique_instances.add(current_activity_frozenset)
202 -
203 -
204 -def create_asset_payload(asset: SuspiciousLogin):
205 - """
206 - Create a payload for adding an asset based on a SuspiciousLogin object.
207 -
208 - Args:
209 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
210 -
211 - Returns:
212 - AddAssetModel: The payload for adding the asset.
213 -
214 - """
215 - if asset.errMessage == "OK":
216 - return AddAssetModel(
217 - name=asset.loginID,
218 - ip=asset.ip,
219 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
220 - asset_type=1,
221 - compromise_status=1,
222 - analysis_status=2,
223 - )
224 - return AddAssetModel(
225 - name=asset.loginID,
226 - ip=asset.ip,
227 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
228 - asset_type=1,
229 - analysis_status=2,
230 - )
231 -
232 -
233 -async def update_case_with_asset(case_id: str, asset_payload):
234 - """
235 - Update the case with the asset information.
236 -
237 - :param case_id: The ID of the case to update
238 - :param asset_payload: The payload to update the case with
239 -
240 - :return: None
241 - """
242 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
243 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
244 - return await fetch_and_validate_data(
245 - client,
246 - case_client.add_asset,
247 - cid=case_id,
248 - **asset_payload.to_dict(),
249 - )
250 -
251 -
252 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
253 - """
254 - Creates an IRIS case for multiple logins with the same IP address.
255 -
256 - Args:
257 - suspicious_login (SuspiciousLogin): The suspicious login information.
258 - session (AsyncSession): The async session for database operations.
259 -
260 - Returns:
261 - CaseResponse: The response containing the created case information.
262 - """
263 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
264 - case_name = (
265 - f"Log Source: {suspicious_login.logSource} SAP SIEM. "
266 - f"Succesful login after multiple failures from {suspicious_login.ip} using multiple user names."
267 - )
268 -
269 - case_description = (
270 - f"Log Source: {suspicious_login.logSource}\n\n"
271 - f"IP Address: {suspicious_login.ip}\n\n"
272 - f"Country: {suspicious_login.country}\n\n"
273 - f"Timestamp: {suspicious_login.event_timestamp}"
274 - )
275 -
276 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
277 -
278 - payload = IrisCasePayload(
279 - case_name=case_name,
280 - case_description=case_description,
281 - case_customer=case_customer,
282 - case_classification=18,
283 - soc_id="1",
284 - create_customer=False,
285 - )
286 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
287 - result = await fetch_and_validate_data(
288 - client,
289 - case_client.add_case,
290 - **payload.to_dict(),
291 - )
292 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
293 -
294 - return CaseResponse(**result)
295 -
296 -
297 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
298 - """
299 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
300 - Collects a max of 1000 records.
301 -
302 - :param suspicious_logins: A list of suspicious logins
303 -
304 - :return: List of the user Activity collected from the sap_siem table
305 - """
306 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
307 - results = es_client.search(
308 - index="sap_siem_*",
309 - # index="new-integrations*",
310 - body={
311 - "size": 1000,
312 - "query": {"bool": {"must": [{"term": {"ip": suspicious_logins.ip}}]}},
313 - },
314 - )
315 - return SapSiemWazuhIndexerResponse(**results)
316 -
317 -
318 -async def get_initial_search_results(es_client):
319 - """
320 - Retrieves the initial search results from Elasticsearch.
321 -
322 - Args:
323 - es_client (Elasticsearch): The Elasticsearch client.
324 -
325 - Returns:
326 - dict: The search results.
327 - """
328 - return es_client.search(
329 - index="sap_siem_*",
330 - # index="new-integrations*",
331 - body={
332 - "size": 1000,
333 - "query": {"bool": {"must": [{"term": {"event_analyzed_successful_login_after_failures_diff_loginID": "False"}}]}},
334 - "sort": [{"event_timestamp": {"order": "asc"}}],
335 - },
336 - scroll="1m",
337 - )
338 -
339 -
340 -async def get_next_batch_of_results(es_client, scroll_id):
341 - """
342 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
343 -
344 - Args:
345 - es_client (Elasticsearch): The Elasticsearch client.
346 - scroll_id (str): The scroll ID to retrieve the next batch of results.
347 -
348 - Returns:
349 - dict: The next batch of results.
350 - """
351 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
352 -
353 -
354 -async def process_hits(hits, ip_to_login_ids, suspicious_activity, time_range):
355 - """
356 - This function, `process_hits`, is designed to analyze a list of login attempts (or "hits") and identify suspicious activity based on certain criteria.
357 - It takes four arguments: `hits`, `ip_to_login_ids`, `suspicious_activity`, and `time_range`.
358 -
359 - Here's a simplified explanation of what it does:
360 -
361 - 1. It starts by creating a dictionary (`ip_to_login_ids`) that maps IP addresses to login IDs. Each login ID is associated with a list of timestamps and error codes.
362 -
363 - 2. It then loops through each login attempt in `hits`. For each attempt, it extracts the login ID, IP address, and error code. If the login ID doesn't contain a '@',
364 - it's ignored.
365 -
366 - 3. The function then checks if there are at least 3 different failed login attempts (identified by error codes not equal to "0") from the same IP address within the
367 - last 2 minutes. It also checks if there's at least one successful login attempt (identified by error code "0") from the same IP address after the 4th login attempt.
368 -
369 - 4. If these conditions are met, the function considers this as suspicious activity. It creates a `SuspiciousLogin` object with
370 - details about the suspicious login attempt and adds it to the `suspicious_activity` dictionary, which maps IP addresses to a list of suspicious login objects.
371 -
372 - For example, consider the following sequence of login attempts from the same IP address:
373 -
374 - - User1 fails to login at 12:00:00
375 - - User2 fails to login at 12:00:30
376 - - User3 fails to login at 12:01:00
377 - - User4 successfully logs in at 12:01:30
378 - - User5 successfully logs in at 12:02:00
379 -
380 - In this case, the function would identify the IP address as suspicious because there are 3 different failed login attempts within 2 minutes,
381 - followed by at least one successful login attempt.
382 - """
383 - ip_to_login_ids = defaultdict(lambda: defaultdict(list))
384 -
385 - for hit in hits:
386 - # Convert loginID to lowercase before comparing
387 - login_id = hit.source.params_loginID.lower()
388 - ip = hit.source.ip
389 - errCode = hit.source.errCode
390 -
391 - # Ignore loginID if it does not contain a '@'
392 - if "@" not in login_id:
393 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
394 - continue
395 -
396 - # Parse the event timestamp
397 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
398 -
399 - # Add the timestamp and errCode to the list for this IP for this loginID
400 - ip_to_login_ids[ip][login_id].append((event_timestamp, errCode))
401 -
402 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
403 -
404 - # Check if there are at least 3 different loginIDs for the same IP within the last 2 minutes
405 - login_ids_in_last_2_minutes = set()
406 - for other_login_id, other_info in ip_to_login_ids[ip].items():
407 - for other_timestamp, other_errCode in other_info:
408 - if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
409 - login_ids_in_last_2_minutes.add((other_login_id, other_errCode))
410 -
411 - # If there are at least 3 different failed loginIDs and at least one successful login after the 4th login, log the suspicious activity
412 - failed_login_ids = [login_id for login_id, errCode in login_ids_in_last_2_minutes if errCode != "0"]
413 - successful_login_ids = [login_id for login_id, errCode in login_ids_in_last_2_minutes if errCode == "0"]
414 - logger.info(f"Failed loginIDs: {failed_login_ids}, Successful loginIDs: {successful_login_ids}")
415 - if len(failed_login_ids) >= 3 and len(successful_login_ids) >= 1:
416 - logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
417 - suspicious_login = SuspiciousLogin(
418 - _index=hit.index,
419 - _id=hit.id,
420 - customer_code=hit.source.customer_code,
421 - logSource=hit.source.logSource,
422 - loginID=hit.source.params_loginID,
423 - country=hit.source.httpReq_country,
424 - ip=hit.source.ip,
425 - event_timestamp=hit.source.event_timestamp,
426 - errMessage=hit.source.errMessage,
427 - errDetails=hit.source.errDetails,
428 - )
429 - suspicious_activity[ip].append(suspicious_login)
430 - logger.info(f"Added suspicious login: {suspicious_login}")
431 -
432 -
433 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
434 - """
435 - Checks for multiple successful logins by IP address.
436 -
437 - Args:
438 - threshold (int): The minimum number of logins required to be considered suspicious.
439 -
440 - Returns:
441 - List[SuspiciousLogin]: A list of suspicious login objects.
442 - """
443 - ip_to_login_ids = defaultdict(set)
444 - suspicious_activity = defaultdict(list)
445 -
446 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
447 - scroll_id = None
448 -
449 - while True:
450 - if scroll_id is None:
451 - results = await get_initial_search_results(es_client)
452 - else:
453 - results = await get_next_batch_of_results(es_client, scroll_id)
454 -
455 - if not results["hits"]["hits"]:
456 - break
457 -
458 - results = SapSiemWazuhIndexerResponse(**results)
459 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
460 -
461 - scroll_id = results.scroll_id
462 -
463 - # Clear the scroll when you're done to free up resources
464 - if scroll_id is not None:
465 - es_client.clear_scroll(scroll_id=scroll_id)
466 -
467 - logger.info(f"Suspicious activity: {suspicious_activity}")
468 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
469 -
470 - return [login for sublist in suspicious_activity.values() for login in sublist]
471 -
472 -
473 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
474 - """
475 - Retrieves a list of suspicious login attempts based on the specified threshold.
476 -
477 - Args:
478 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
479 -
480 - Returns:
481 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
482 - """
483 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
484 -
485 -
486 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
487 - """
488 - Retrieves an existing database record for the given IP address.
489 -
490 - Args:
491 - session (AsyncSession): The async session object for database operations.
492 - ip (str): The IP address to search for.
493 -
494 - Returns:
495 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
496 - """
497 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
498 - return result.scalar_one_or_none() if result is not None else None
499 -
500 -
501 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
502 - """
503 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
504 -
505 - Args:
506 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
507 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
508 -
509 - Returns:
510 - None
511 - """
512 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
513 - if not new_login_ids.issubset(existing_loginIDs):
514 - updated_login_ids = existing_loginIDs.union(new_login_ids)
515 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
516 - existing_case.last_case_created_timestamp = datetime.now()
517 -
518 -
519 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
520 - """
521 - Creates a new database record for SAP SIEM multiple logins.
522 -
523 - Args:
524 - ip (str): The IP address associated with the multiple logins.
525 - new_login_ids (Set[str]): The set of new login IDs.
526 -
527 - Returns:
528 - SapSiemMultipleLogins: The newly created database record.
529 - """
530 - return SapSiemMultipleLogins(
531 - ip=ip,
532 - last_case_created_timestamp=datetime.now(),
533 - associated_loginIDs=",".join(new_login_ids),
534 - )
535 -
536 -
537 -async def sap_siem_successful_login_after_failures(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
538 - """
539 - Finds same IP with multiple users and handles suspicious logins.
540 -
541 - Args:
542 - threshold (int): The threshold value for determining suspicious logins.
543 - session (AsyncSession): The database session.
544 -
545 - Returns:
546 - InvokeSAPSiemResponse: The response indicating the success of the operation.
547 - """
548 - logger.info("Finding same user from different IP addresses")
549 -
550 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
551 - logger.info(f"Suspicious IPs: {suspicious_ips}")
552 -
553 - unique_instances = set()
554 - case_ids = []
555 - # Dictionary to aggregate suspicious logins by IP
556 - aggregated_logins_by_ip = defaultdict(list)
557 -
558 - for suspicious_login in suspicious_ips:
559 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
560 -
561 - for ip, associated_logins in aggregated_logins_by_ip.items():
562 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
563 - if session is not None:
564 - existing_case = await get_existing_database_record(session, ip)
565 -
566 - new_login_ids = {login.loginID for login in associated_logins}
567 - if existing_case:
568 - logger.info(f"Updating existing database record: {existing_case}")
569 - update_existing_database_record(existing_case, new_login_ids)
570 - else:
571 - logger.info(f"Creating new case for IP: {ip}")
572 - new_case = create_new_database_record(ip, new_login_ids)
573 - session.add(new_case)
574 -
575 - # Create a single new IRIS case for this IP
576 - # Modify this to include information from all associated_logins
577 - await handle_suspicious_login_multiple(
578 - associated_logins[0],
579 - unique_instances,
580 - case_ids,
581 - session=session,
582 - )
583 - else:
584 - raise HTTPException(
585 - status_code=500,
586 - detail="Failed to create IRIS case",
587 - )
588 - await session.commit()
589 -
590 - # Clear the global set
591 - checked_ips.clear()
592 -
593 - return InvokeSAPSiemResponse(
594 - success=True,
595 - message="SAP SIEM multiple logins invoked.",
596 - )
backend/app/integrations/sap_siem/services/sap_siem_successful_same_user_different_geo_location.py deleted
-594
@@ -1,594 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 - await mark_as_checked(suspicious_login)
55 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 - await send_to_shuffle(
57 - ShufflePayload(
58 - alert_id=case.data.case_id,
59 - customer=suspicious_login.customer_code,
60 - customer_code=suspicious_login.customer_code,
61 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 - rule_description=f"{case.data.case_name}",
63 - hostname=suspicious_login.ip,
64 - rule_name="Rule:_Same_user_from_different_geo_locations",
65 - affected_user=suspicious_login.loginID,
66 - alert_type="user",
67 - ),
68 - session=session,
69 - )
70 -
71 -
72 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
73 - """
74 - Handles suspicious login events with multiple logins.
75 -
76 - Args:
77 - suspicious_login: The suspicious login event.
78 - unique_instances: List of unique instances of the suspicious login event.
79 - case_ids: List of case IDs associated with the suspicious login event.
80 - session: The database session.
81 -
82 - Returns:
83 - None
84 - """
85 - await handle_common_suspicious_login_tasks(
86 - suspicious_login,
87 - unique_instances,
88 - case_ids,
89 - create_iris_case_multiple,
90 - session,
91 - )
92 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
93 -
94 -
95 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
96 - """
97 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
98 -
99 - :param suspicious_login: The suspicious login to update
100 -
101 - :return: None
102 - """
103 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
104 - try:
105 - es_client.update(
106 - index=index,
107 - id=id,
108 - body={
109 - "doc": {
110 - "event_analyzed_same_user_successful_diff_geo": "True",
111 - },
112 - },
113 - )
114 - logger.info(f"Updated event_analyzed_same_user_successful_diff_geo flag for suspicious login: {id}")
115 - except Exception as e:
116 - logger.error(
117 - f"Failed to update case created flag {e}",
118 - )
119 - # Attempt to remove read-only block
120 - try:
121 - es_client.indices.put_settings(
122 - index=index,
123 - body={"index.blocks.write": None},
124 - )
125 - logger.info(
126 - f"Removed read-only block from index {index}. Retrying update.",
127 - )
128 -
129 - # Retry the update operation
130 - es_client.update(
131 - index=index,
132 - id=id,
133 - body={"doc": {"event_analyzed_same_user_successful_diff_geo": "True"}},
134 - )
135 - logger.info(
136 - f"Added event_analyzed_same_user_successful_diff_geo flag to index {index} for suspicious login: {id}",
137 - )
138 -
139 - # Reenable the write block
140 - es_client.indices.put_settings(
141 - index=index,
142 - body={"index.blocks.write": True},
143 - )
144 - except Exception as e2:
145 - logger.error(
146 - f"Failed to remove read-only block from index {index}: {e2}",
147 - )
148 - return False
149 -
150 -
151 -async def mark_as_checked(suspicious_login):
152 - """
153 - Marks a suspicious login as checked by adding it to the set of checked IPs.
154 -
155 - Args:
156 - suspicious_login (Login): The suspicious login object to mark as checked.
157 -
158 - Returns:
159 - None
160 - """
161 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
162 -
163 -
164 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
165 - """
166 - Handles user activity by processing each hit in the user_activity and performing the following steps:
167 - 1. Extracts relevant information from the hit.
168 - 2. Checks if the current activity is already present in the unique_instances set.
169 - 3. If not present, adds the user activity to the IRIS case.
170 - 4. Creates an asset payload using the current activity.
171 - 5. Updates the case with the asset payload.
172 - 6. Updates the event analyzed multiple logins flag for the hit.
173 - 7. Adds the current activity to the unique_instances set.
174 -
175 - Parameters:
176 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
177 - - unique_instances (set): A set containing unique instances of user activity.
178 - - case_id (str): The ID of the IRIS case.
179 -
180 - Returns:
181 - None
182 - """
183 - for hit in user_activity.hits.hits:
184 - current_activity = {
185 - "loginID": hit.source.params_loginID,
186 - "ip": hit.source.ip,
187 - "country": hit.source.httpReq_country,
188 - "errMessage": hit.source.errMessage,
189 - "event_timestamp": hit.source.event_timestamp,
190 - "customer_code": hit.source.customer_code,
191 - "errDetails": hit.source.errDetails,
192 - }
193 - current_activity_frozenset = frozenset(current_activity.items())
194 - if current_activity_frozenset not in unique_instances:
195 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
196 - current_asset = SuspiciousLogin(**current_activity)
197 - asset_payload = create_asset_payload(asset=current_asset)
198 - logger.info(f"Asset Payload: {asset_payload}")
199 - await update_case_with_asset(case_id, asset_payload)
200 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
201 - unique_instances.add(current_activity_frozenset)
202 -
203 -
204 -def create_asset_payload(asset: SuspiciousLogin):
205 - """
206 - Create a payload for adding an asset based on a SuspiciousLogin object.
207 -
208 - Args:
209 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
210 -
211 - Returns:
212 - AddAssetModel: The payload for adding the asset.
213 -
214 - """
215 - if asset.errMessage == "OK":
216 - return AddAssetModel(
217 - name=asset.loginID,
218 - ip=asset.ip,
219 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
220 - asset_type=1,
221 - compromise_status=1,
222 - analysis_status=2,
223 - )
224 - return AddAssetModel(
225 - name=asset.loginID,
226 - ip=asset.ip,
227 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
228 - asset_type=1,
229 - analysis_status=2,
230 - )
231 -
232 -
233 -async def update_case_with_asset(case_id: str, asset_payload):
234 - """
235 - Update the case with the asset information.
236 -
237 - :param case_id: The ID of the case to update
238 - :param asset_payload: The payload to update the case with
239 -
240 - :return: None
241 - """
242 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
243 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
244 - return await fetch_and_validate_data(
245 - client,
246 - case_client.add_asset,
247 - cid=case_id,
248 - **asset_payload.to_dict(),
249 - )
250 -
251 -
252 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
253 - """
254 - Creates an IRIS case for multiple logins with the same IP address.
255 -
256 - Args:
257 - suspicious_login (SuspiciousLogin): The suspicious login information.
258 - session (AsyncSession): The async session for database operations.
259 -
260 - Returns:
261 - CaseResponse: The response containing the created case information.
262 - """
263 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
264 - case_name = (
265 - f"Log Source: {suspicious_login.logSource} SAP SIEM. "
266 - f"User {suspicious_login.loginID} had at least one failed login attempt from two different GEO IP country locations followed by a successful login."
267 - )
268 -
269 - case_description = (
270 - f"Log Source: {suspicious_login.logSource}\n\n"
271 - f"IP Address: {suspicious_login.ip}\n\n"
272 - f"Country: {suspicious_login.country}\n\n"
273 - f"Timestamp: {suspicious_login.event_timestamp}"
274 - )
275 -
276 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
277 -
278 - payload = IrisCasePayload(
279 - case_name=case_name,
280 - case_description=case_description,
281 - case_customer=case_customer,
282 - case_classification=18,
283 - soc_id="1",
284 - create_customer=False,
285 - )
286 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
287 - result = await fetch_and_validate_data(
288 - client,
289 - case_client.add_case,
290 - **payload.to_dict(),
291 - )
292 - await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
293 -
294 - return CaseResponse(**result)
295 -
296 -
297 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
298 - """
299 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
300 - Collects a max of 1000 records.
301 -
302 - :param suspicious_logins: A list of suspicious logins
303 -
304 - :return: List of the user Activity collected from the sap_siem table
305 - """
306 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
307 - results = es_client.search(
308 - index="sap_siem_*",
309 - # index="new-integrations*",
310 - body={
311 - "size": 1000,
312 - "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
313 - },
314 - )
315 - return SapSiemWazuhIndexerResponse(**results)
316 -
317 -
318 -async def get_initial_search_results(es_client):
319 - """
320 - Retrieves the initial search results from Elasticsearch.
321 -
322 - Args:
323 - es_client (Elasticsearch): The Elasticsearch client.
324 -
325 - Returns:
326 - dict: The search results.
327 - """
328 - return es_client.search(
329 - index="sap_siem_*",
330 - # index="new-integrations*",
331 - body={
332 - "size": 1000,
333 - "query": {"bool": {"must": [{"term": {"event_analyzed_same_user_successful_diff_geo": "False"}}]}},
334 - "sort": [{"event_timestamp": {"order": "asc"}}],
335 - },
336 - scroll="1m",
337 - )
338 -
339 -
340 -async def get_next_batch_of_results(es_client, scroll_id):
341 - """
342 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
343 -
344 - Args:
345 - es_client (Elasticsearch): The Elasticsearch client.
346 - scroll_id (str): The scroll ID to retrieve the next batch of results.
347 -
348 - Returns:
349 - dict: The next batch of results.
350 - """
351 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
352 -
353 -
354 -async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range=20):
355 - """
356 - This function would trigger a suspicious login when the following conditions are met:
357 -
358 - 1. There is at least one failed login attempt from the same user (identified by `login_id`) from two different GEO IP country locations within the last 20 minutes.
359 - 2. There is at least one successful login attempt from the same user from a different GEO IP country location within the last 20 minutes.
360 -
361 - Here are some examples:
362 -
363 - Example 1:
364 - - At 12:00, a failed login attempt is made by user `user1` from IP `1.1.1.1` located in the US.
365 - - At 12:10, another failed login attempt is made by `user1` from IP `2.2.2.2` located in Canada.
366 - - At 12:15, a successful login attempt is made by `user1` from IP `3.3.3.3` located in the UK.
367 - - In this case, the function would trigger a suspicious login for `user1` because there are failed login attempts from two different countries (US and Canada) and
368 - a successful login from a different country (UK) within 20 minutes.
369 -
370 - Example 2:
371 - - At 12:00, a failed login attempt is made by user `user2` from IP `4.4.4.4` located in the US.
372 - - At 12:10, another failed login attempt is made by `user2` from IP `5.5.5.5` also located in the US.
373 - - At 12:15, a successful login attempt is made by `user2` from IP `6.6.6.6` located in the US.
374 - - In this case, the function would not trigger a suspicious login for `user2` because all the login attempts are from the same country (US).
375 - """
376 - login_id_to_ips = defaultdict(lambda: defaultdict(list))
377 -
378 - for hit in hits:
379 - # Convert loginID to lowercase before comparing
380 - login_id = hit.source.params_loginID.lower()
381 - ip = hit.source.ip
382 - errCode = hit.source.errCode
383 - country = hit.source.httpReq_country
384 -
385 - # Ignore loginID if it does not contain a '@'
386 - if "@" not in login_id:
387 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
388 - continue
389 -
390 - # Parse the event timestamp
391 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
392 -
393 - # Add the timestamp, errCode and country to the list for this loginID for this IP
394 - login_id_to_ips[login_id][ip].append((event_timestamp, errCode, country))
395 -
396 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
397 -
398 - # Check if there are at least 2 different IPs for the same loginID within the last 20 minutes
399 - ips_in_last_20_minutes = set()
400 - for other_ip, other_info in login_id_to_ips[login_id].items():
401 - for other_timestamp, other_errCode, other_country in other_info:
402 - if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
403 - ips_in_last_20_minutes.add((other_ip, other_errCode, other_country))
404 - logger.info(f"IPs in last 20 minutes: {ips_in_last_20_minutes}")
405 -
406 - # If there is at least 1 failed IP from at least two different GEO IP country locations and a successful login from a different location,
407 - # log the suspicious activity
408 - failed_ips = [ip for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"]
409 - successful_ips = [ip for ip, errCode, country in ips_in_last_20_minutes if errCode == "0"]
410 - countries = set([country for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"])
411 - logger.info(f"Failed IPs: {failed_ips}, Successful IPs: {successful_ips}, Countries: {countries}")
412 - if len(failed_ips) >= 1 and len(successful_ips) >= 1 and len(countries) >= 2:
413 - logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
414 - suspicious_login = SuspiciousLogin(
415 - _index=hit.index,
416 - _id=hit.id,
417 - customer_code=hit.source.customer_code,
418 - logSource=hit.source.logSource,
419 - loginID=hit.source.params_loginID,
420 - country=hit.source.httpReq_country,
421 - ip=hit.source.ip,
422 - event_timestamp=hit.source.event_timestamp,
423 - errMessage=hit.source.errMessage,
424 - errDetails=hit.source.errDetails,
425 - )
426 - suspicious_activity[login_id].append(suspicious_login)
427 - logger.info(f"Added suspicious login: {suspicious_login}")
428 -
429 -
430 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
431 - """
432 - Checks for multiple successful logins by IP address.
433 -
434 - Args:
435 - threshold (int): The minimum number of logins required to be considered suspicious.
436 -
437 - Returns:
438 - List[SuspiciousLogin]: A list of suspicious login objects.
439 - """
440 - ip_to_login_ids = defaultdict(set)
441 - suspicious_activity = defaultdict(list)
442 -
443 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
444 - scroll_id = None
445 -
446 - while True:
447 - if scroll_id is None:
448 - results = await get_initial_search_results(es_client)
449 - # logger.info(f"Initial search results: {results}")
450 - else:
451 - results = await get_next_batch_of_results(es_client, scroll_id)
452 -
453 - if not results["hits"]["hits"]:
454 - break
455 -
456 - results = SapSiemWazuhIndexerResponse(**results)
457 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
458 -
459 - scroll_id = results.scroll_id
460 -
461 - # Clear the scroll when you're done to free up resources
462 - if scroll_id is not None:
463 - es_client.clear_scroll(scroll_id=scroll_id)
464 -
465 - logger.info(f"Suspicious activity: {suspicious_activity}")
466 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
467 -
468 - return [login for sublist in suspicious_activity.values() for login in sublist]
469 -
470 -
471 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
472 - """
473 - Retrieves a list of suspicious login attempts based on the specified threshold.
474 -
475 - Args:
476 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
477 -
478 - Returns:
479 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
480 - """
481 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
482 -
483 -
484 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
485 - """
486 - Retrieves an existing database record for the given IP address.
487 -
488 - Args:
489 - session (AsyncSession): The async session object for database operations.
490 - ip (str): The IP address to search for.
491 -
492 - Returns:
493 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
494 - """
495 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
496 - return result.scalar_one_or_none() if result is not None else None
497 -
498 -
499 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
500 - """
501 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
502 -
503 - Args:
504 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
505 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
506 -
507 - Returns:
508 - None
509 - """
510 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
511 - if not new_login_ids.issubset(existing_loginIDs):
512 - updated_login_ids = existing_loginIDs.union(new_login_ids)
513 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
514 - existing_case.last_case_created_timestamp = datetime.now()
515 -
516 -
517 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
518 - """
519 - Creates a new database record for SAP SIEM multiple logins.
520 -
521 - Args:
522 - ip (str): The IP address associated with the multiple logins.
523 - new_login_ids (Set[str]): The set of new login IDs.
524 -
525 - Returns:
526 - SapSiemMultipleLogins: The newly created database record.
527 - """
528 - return SapSiemMultipleLogins(
529 - ip=ip,
530 - last_case_created_timestamp=datetime.now(),
531 - associated_loginIDs=",".join(new_login_ids),
532 - )
533 -
534 -
535 -async def sap_siem_successful_same_user_diff_geo(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
536 - """
537 - Finds same IP with multiple users and handles suspicious logins.
538 -
539 - Args:
540 - threshold (int): The threshold value for determining suspicious logins.
541 - session (AsyncSession): The database session.
542 -
543 - Returns:
544 - InvokeSAPSiemResponse: The response indicating the success of the operation.
545 - """
546 - logger.info("Finding same user from different IP addresses")
547 -
548 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
549 - logger.info(f"Suspicious IPs: {suspicious_ips}")
550 -
551 - unique_instances = set()
552 - case_ids = []
553 - # Dictionary to aggregate suspicious logins by IP
554 - aggregated_logins_by_ip = defaultdict(list)
555 -
556 - for suspicious_login in suspicious_ips:
557 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
558 -
559 - for ip, associated_logins in aggregated_logins_by_ip.items():
560 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
561 - if session is not None:
562 - existing_case = await get_existing_database_record(session, ip)
563 -
564 - new_login_ids = {login.loginID for login in associated_logins}
565 - if existing_case:
566 - logger.info(f"Updating existing database record: {existing_case}")
567 - update_existing_database_record(existing_case, new_login_ids)
568 - else:
569 - logger.info(f"Creating new case for IP: {ip}")
570 - new_case = create_new_database_record(ip, new_login_ids)
571 - session.add(new_case)
572 -
573 - # Create a single new IRIS case for this IP
574 - # Modify this to include information from all associated_logins
575 - await handle_suspicious_login_multiple(
576 - associated_logins[0],
577 - unique_instances,
578 - case_ids,
579 - session=session,
580 - )
581 - else:
582 - raise HTTPException(
583 - status_code=500,
584 - detail="Failed to create IRIS case",
585 - )
586 - await session.commit()
587 -
588 - # Clear the global set
589 - checked_ips.clear()
590 -
591 - return InvokeSAPSiemResponse(
592 - success=True,
593 - message="SAP SIEM multiple logins invoked.",
594 - )
backend/app/integrations/sap_siem/services/sap_siem_successful_user_login_after_using_different_ip.py deleted
-578
@@ -1,578 +0,0 @@
1 -from collections import defaultdict
2 -from datetime import datetime
3 -from datetime import timedelta
4 -from typing import List
5 -from typing import Set
6 -
7 -from fastapi import HTTPException
8 -from loguru import logger
9 -from sqlalchemy.ext.asyncio import AsyncSession
10 -from sqlalchemy.future import select
11 -
12 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 -from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 -from app.integrations.utils.alerts import send_to_shuffle
23 -from app.integrations.utils.schema import ShufflePayload
24 -from app.utils import get_customer_alert_settings
25 -
26 -# Global set to keep track of IPs that have already been checked
27 -checked_ips = set()
28 -
29 -
30 -async def handle_common_suspicious_login_tasks(
31 - suspicious_login,
32 - unique_instances,
33 - case_ids,
34 - create_case_fn,
35 - session: AsyncSession,
36 -):
37 - """
38 - Handles common tasks for suspicious logins.
39 -
40 - Args:
41 - suspicious_login: The suspicious login object.
42 - unique_instances: List of unique instances.
43 - case_ids: List of case IDs.
44 - create_case_fn: Function to create a case.
45 - session: The async session.
46 -
47 - Returns:
48 - None
49 - """
50 - case = await create_case_fn(suspicious_login, session)
51 - case_ids.append(case.data.case_id)
52 - user_activity = await collect_user_activity(suspicious_login)
53 - logger.info(f"User activity: {user_activity}")
54 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
55 - await mark_as_checked(suspicious_login)
56 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
57 - await send_to_shuffle(
58 - ShufflePayload(
59 - alert_id=case.data.case_id,
60 - customer=suspicious_login.customer_code,
61 - customer_code=suspicious_login.customer_code,
62 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
63 - rule_description=f"{case.data.case_name}",
64 - hostname=suspicious_login.ip,
65 - rule_name="Rule:_Successful_user_login_after_using_different_IP_addresses",
66 - affected_user=suspicious_login.loginID,
67 - alert_type="user",
68 - ),
69 - session=session,
70 - )
71 -
72 -
73 -async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
74 - """
75 - Handles suspicious login events with multiple logins.
76 -
77 - Args:
78 - suspicious_login: The suspicious login event.
79 - unique_instances: List of unique instances of the suspicious login event.
80 - case_ids: List of case IDs associated with the suspicious login event.
81 - session: The database session.
82 -
83 - Returns:
84 - None
85 - """
86 - await handle_common_suspicious_login_tasks(
87 - suspicious_login,
88 - unique_instances,
89 - case_ids,
90 - create_iris_case_multiple,
91 - session,
92 - )
93 -
94 -
95 -async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
96 - """
97 - Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
98 -
99 - :param suspicious_login: The suspicious login to update
100 -
101 - :return: None
102 - """
103 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
104 - try:
105 - es_client.update(
106 - index=index,
107 - id=id,
108 - body={
109 - "doc": {
110 - "event_analyzed_success_login_diff_ip": "True",
111 - },
112 - },
113 - )
114 - logger.info(f"Updated event_analyzed_success_login_diff_ip flag for suspicious login: {id}")
115 - except Exception as e:
116 - logger.error(
117 - f"Failed to update case created flag {e}",
118 - )
119 - # Attempt to remove read-only block
120 - try:
121 - es_client.indices.put_settings(
122 - index=index,
123 - body={"index.blocks.write": None},
124 - )
125 - logger.info(
126 - f"Removed read-only block from index {index}. Retrying update.",
127 - )
128 -
129 - # Retry the update operation
130 - es_client.update(
131 - index=index,
132 - id=id,
133 - body={"doc": {"event_analyzed_multiple_logins": "True"}},
134 - )
135 - logger.info(
136 - f"Added event_analyzed_multiple_logins flag to index {index} for suspicious login: {id}",
137 - )
138 -
139 - # Reenable the write block
140 - es_client.indices.put_settings(
141 - index=index,
142 - body={"index.blocks.write": True},
143 - )
144 - except Exception as e2:
145 - logger.error(
146 - f"Failed to remove read-only block from index {index}: {e2}",
147 - )
148 - return False
149 -
150 -
151 -async def mark_as_checked(suspicious_login):
152 - """
153 - Marks a suspicious login as checked by adding it to the set of checked IPs.
154 -
155 - Args:
156 - suspicious_login (Login): The suspicious login object to mark as checked.
157 -
158 - Returns:
159 - None
160 - """
161 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
162 -
163 -
164 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
165 - """
166 - Handles user activity by processing each hit in the user_activity and performing the following steps:
167 - 1. Extracts relevant information from the hit.
168 - 2. Checks if the current activity is already present in the unique_instances set.
169 - 3. If not present, adds the user activity to the IRIS case.
170 - 4. Creates an asset payload using the current activity.
171 - 5. Updates the case with the asset payload.
172 - 6. Updates the event analyzed multiple logins flag for the hit.
173 - 7. Adds the current activity to the unique_instances set.
174 -
175 - Parameters:
176 - - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
177 - - unique_instances (set): A set containing unique instances of user activity.
178 - - case_id (str): The ID of the IRIS case.
179 -
180 - Returns:
181 - None
182 - """
183 - for hit in user_activity.hits.hits:
184 - current_activity = {
185 - "loginID": hit.source.params_loginID,
186 - "ip": hit.source.ip,
187 - "country": hit.source.httpReq_country,
188 - "errMessage": hit.source.errMessage,
189 - "event_timestamp": hit.source.event_timestamp,
190 - "customer_code": hit.source.customer_code,
191 - "errDetails": hit.source.errDetails,
192 - }
193 - current_activity_frozenset = frozenset(current_activity.items())
194 - if current_activity_frozenset not in unique_instances:
195 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
196 - current_asset = SuspiciousLogin(**current_activity)
197 - asset_payload = create_asset_payload(asset=current_asset)
198 - logger.info(f"Asset Payload: {asset_payload}")
199 - await update_case_with_asset(case_id, asset_payload)
200 - await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
201 - unique_instances.add(current_activity_frozenset)
202 -
203 -
204 -def create_asset_payload(asset: SuspiciousLogin):
205 - """
206 - Create a payload for adding an asset based on a SuspiciousLogin object.
207 -
208 - Args:
209 - asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
210 -
211 - Returns:
212 - AddAssetModel: The payload for adding the asset.
213 -
214 - """
215 - if asset.errMessage == "OK":
216 - return AddAssetModel(
217 - name=asset.loginID,
218 - ip=asset.ip,
219 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
220 - asset_type=1,
221 - compromise_status=1,
222 - analysis_status=2,
223 - )
224 - return AddAssetModel(
225 - name=asset.loginID,
226 - ip=asset.ip,
227 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
228 - asset_type=1,
229 - analysis_status=2,
230 - )
231 -
232 -
233 -async def update_case_with_asset(case_id: str, asset_payload):
234 - """
235 - Update the case with the asset information.
236 -
237 - :param case_id: The ID of the case to update
238 - :param asset_payload: The payload to update the case with
239 -
240 - :return: None
241 - """
242 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
243 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
244 - return await fetch_and_validate_data(
245 - client,
246 - case_client.add_asset,
247 - cid=case_id,
248 - **asset_payload.to_dict(),
249 - )
250 -
251 -
252 -async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
253 - """
254 - Creates an IRIS case for multiple logins with the same IP address.
255 -
256 - Args:
257 - suspicious_login (SuspiciousLogin): The suspicious login information.
258 - session (AsyncSession): The async session for database operations.
259 -
260 - Returns:
261 - CaseResponse: The response containing the created case information.
262 - """
263 - logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
264 - case_name = (
265 - f"Log Source: {suspicious_login.logSource} SAP SIEM. "
266 - f"User {suspicious_login.loginID} had a successful login after failed attempts from different IPs."
267 - )
268 -
269 - case_description = (
270 - f"Log Source: {suspicious_login.logSource}\n\n"
271 - f"IP Address: {suspicious_login.ip}\n\n"
272 - f"Country: {suspicious_login.country}\n\n"
273 - f"Timestamp: {suspicious_login.event_timestamp}"
274 - )
275 -
276 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
277 -
278 - payload = IrisCasePayload(
279 - case_name=case_name,
280 - case_description=case_description,
281 - case_customer=case_customer,
282 - case_classification=18,
283 - soc_id="1",
284 - create_customer=False,
285 - )
286 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
287 - result = await fetch_and_validate_data(
288 - client,
289 - case_client.add_case,
290 - **payload.to_dict(),
291 - )
292 -
293 - return CaseResponse(**result)
294 -
295 -
296 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
297 - """
298 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
299 - Collects a max of 1000 records.
300 -
301 - :param suspicious_logins: A list of suspicious logins
302 -
303 - :return: List of the user Activity collected from the sap_siem table
304 - """
305 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
306 - results = es_client.search(
307 - # index="sap_siem_*",
308 - index="new-integrations*",
309 - body={
310 - "size": 1000,
311 - "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
312 - },
313 - )
314 - return SapSiemWazuhIndexerResponse(**results)
315 -
316 -
317 -async def get_initial_search_results(es_client):
318 - """
319 - Retrieves the initial search results from Elasticsearch.
320 -
321 - Args:
322 - es_client (Elasticsearch): The Elasticsearch client.
323 -
324 - Returns:
325 - dict: The search results.
326 - """
327 - return es_client.search(
328 - # index="sap_siem_*",
329 - index="new-integrations*",
330 - body={
331 - "size": 1000,
332 - "query": {"bool": {"must": [{"term": {"event_analyzed_success_login_diff_ip": "False"}}]}},
333 - "sort": [{"event_timestamp": {"order": "asc"}}],
334 - },
335 - scroll="1m",
336 - )
337 -
338 -
339 -async def get_next_batch_of_results(es_client, scroll_id):
340 - """
341 - Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
342 -
343 - Args:
344 - es_client (Elasticsearch): The Elasticsearch client.
345 - scroll_id (str): The scroll ID to retrieve the next batch of results.
346 -
347 - Returns:
348 - dict: The next batch of results.
349 - """
350 - return es_client.scroll(scroll_id=scroll_id, scroll="1m")
351 -
352 -
353 -async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
354 - """
355 - Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
356 -
357 - Args:
358 - hits (list): List of hits received from SAP SIEM.
359 - login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
360 - suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
361 -
362 - Returns:
363 - None
364 - """
365 - logger.info(f"Processing hits: {hits} for sap_siem_successful_user_login_after_using_different_ip")
366 - login_id_to_ips = defaultdict(lambda: defaultdict(list))
367 -
368 - for hit in hits:
369 - # Convert loginID to lowercase before comparing
370 - login_id = hit.source.params_loginID.lower()
371 - ip = hit.source.ip
372 - errCode = hit.source.errCode
373 -
374 - # Ignore loginID if it does not contain a '@'
375 - if "@" not in login_id:
376 - logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
377 - continue
378 -
379 - # Parse the event timestamp
380 - event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
381 -
382 - # Add the timestamp and errCode to the list for this loginID for this IP
383 - login_id_to_ips[login_id][ip].append((event_timestamp, errCode))
384 -
385 - logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
386 -
387 - # Check if there are at least 2 different IPs for the same loginID within the last 15 minutes
388 - ips_in_last_15_minutes = set()
389 - for other_ip, other_info in login_id_to_ips[login_id].items():
390 - for other_timestamp, other_errCode in other_info:
391 - if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
392 - ips_in_last_15_minutes.add((other_ip, other_errCode))
393 -
394 - # If the current hit is a successful login and there are at least 2 different failed IPs, log the suspicious activity
395 - failed_ips = [ip for ip, errCode in ips_in_last_15_minutes if errCode != "0"]
396 - logger.info(f"Failed IPs: {failed_ips}")
397 - if errCode == "0" and len(failed_ips) >= 2:
398 - logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
399 - suspicious_login = SuspiciousLogin(
400 - _index=hit.index,
401 - _id=hit.id,
402 - customer_code=hit.source.customer_code,
403 - logSource=hit.source.logSource,
404 - loginID=hit.source.params_loginID,
405 - country=hit.source.httpReq_country,
406 - ip=hit.source.ip,
407 - event_timestamp=hit.source.event_timestamp,
408 - errMessage=hit.source.errMessage,
409 - errDetails=hit.source.errDetails,
410 - )
411 - suspicious_activity[login_id].append(suspicious_login)
412 - logger.info(f"Added suspicious login: {suspicious_login}")
413 -
414 -
415 -async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
416 - """
417 - Checks for multiple successful logins by IP address.
418 -
419 - Args:
420 - threshold (int): The minimum number of logins required to be considered suspicious.
421 -
422 - Returns:
423 - List[SuspiciousLogin]: A list of suspicious login objects.
424 - """
425 - ip_to_login_ids = defaultdict(set)
426 - suspicious_activity = defaultdict(list)
427 -
428 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
429 - scroll_id = None
430 -
431 - while True:
432 - if scroll_id is None:
433 - results = await get_initial_search_results(es_client)
434 - else:
435 - results = await get_next_batch_of_results(es_client, scroll_id)
436 -
437 - if not results["hits"]["hits"]:
438 - break
439 -
440 - results = SapSiemWazuhIndexerResponse(**results)
441 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
442 -
443 - scroll_id = results.scroll_id
444 -
445 - # Clear the scroll when you're done to free up resources
446 - if scroll_id is not None:
447 - es_client.clear_scroll(scroll_id=scroll_id)
448 -
449 - logger.info(f"Suspicious activity: {suspicious_activity}")
450 - suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
451 -
452 - return [login for sublist in suspicious_activity.values() for login in sublist]
453 -
454 -
455 -async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
456 - """
457 - Retrieves a list of suspicious login attempts based on the specified threshold.
458 -
459 - Args:
460 - threshold (int): The number of successful logins from the same IP address that is considered suspicious.
461 -
462 - Returns:
463 - List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
464 - """
465 - return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
466 -
467 -
468 -async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
469 - """
470 - Retrieves an existing database record for the given IP address.
471 -
472 - Args:
473 - session (AsyncSession): The async session object for database operations.
474 - ip (str): The IP address to search for.
475 -
476 - Returns:
477 - SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
478 - """
479 - result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
480 - return result.scalar_one_or_none() if result is not None else None
481 -
482 -
483 -def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
484 - """
485 - Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
486 -
487 - Args:
488 - existing_case (SapSiemMultipleLogins): The existing database record to be updated.
489 - new_login_ids (Set[str]): The new login IDs to be added to the existing record.
490 -
491 - Returns:
492 - None
493 - """
494 - existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
495 - if not new_login_ids.issubset(existing_loginIDs):
496 - updated_login_ids = existing_loginIDs.union(new_login_ids)
497 - existing_case.associated_loginIDs = ",".join(updated_login_ids)
498 - existing_case.last_case_created_timestamp = datetime.now()
499 -
500 -
501 -def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
502 - """
503 - Creates a new database record for SAP SIEM multiple logins.
504 -
505 - Args:
506 - ip (str): The IP address associated with the multiple logins.
507 - new_login_ids (Set[str]): The set of new login IDs.
508 -
509 - Returns:
510 - SapSiemMultipleLogins: The newly created database record.
511 - """
512 - return SapSiemMultipleLogins(
513 - ip=ip,
514 - last_case_created_timestamp=datetime.now(),
515 - associated_loginIDs=",".join(new_login_ids),
516 - )
517 -
518 -
519 -async def sap_siem_successful_user_login_with_different_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
520 - """
521 - Finds same IP with multiple users and handles suspicious logins.
522 -
523 - Args:
524 - threshold (int): The threshold value for determining suspicious logins.
525 - session (AsyncSession): The database session.
526 -
527 - Returns:
528 - InvokeSAPSiemResponse: The response indicating the success of the operation.
529 - """
530 - logger.info("Finding same IP with multiple users")
531 -
532 - suspicious_ips = await get_suspicious_ips(threshold, time_range)
533 - logger.info(f"Suspicious IPs: {suspicious_ips}")
534 -
535 - unique_instances = set()
536 - case_ids = []
537 - # Dictionary to aggregate suspicious logins by IP
538 - aggregated_logins_by_ip = defaultdict(list)
539 -
540 - for suspicious_login in suspicious_ips:
541 - aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
542 -
543 - for ip, associated_logins in aggregated_logins_by_ip.items():
544 - logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
545 - if session is not None:
546 - existing_case = await get_existing_database_record(session, ip)
547 -
548 - new_login_ids = {login.loginID for login in associated_logins}
549 - if existing_case:
550 - logger.info(f"Updating existing database record: {existing_case}")
551 - update_existing_database_record(existing_case, new_login_ids)
552 - else:
553 - logger.info(f"Creating new case for IP: {ip}")
554 - new_case = create_new_database_record(ip, new_login_ids)
555 - session.add(new_case)
556 -
557 - # Create a single new IRIS case for this IP
558 - # Modify this to include information from all associated_logins
559 - await handle_suspicious_login_multiple(
560 - associated_logins[0],
561 - unique_instances,
562 - case_ids,
563 - session=session,
564 - )
565 - else:
566 - raise HTTPException(
567 - status_code=500,
568 - detail="Failed to create IRIS case",
569 - )
570 - await session.commit()
571 -
572 - # Clear the global set
573 - checked_ips.clear()
574 -
575 - return InvokeSAPSiemResponse(
576 - success=True,
577 - message="SAP SIEM multiple logins invoked.",
578 - )
backend/app/integrations/sap_siem/services/sap_siem_suspicious_logins.py deleted
-414
@@ -1,414 +0,0 @@
1 -from typing import List
2 -
3 -from loguru import logger
4 -from sqlalchemy.ext.asyncio import AsyncSession
5 -
6 -from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
7 -from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
8 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
9 -from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
10 -from app.integrations.sap_siem.schema.sap_siem import CaseResponse
11 -from app.integrations.sap_siem.schema.sap_siem import ErrCode
12 -from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
13 -from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
14 -from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
15 -from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
16 -from app.integrations.utils.alerts import send_to_shuffle
17 -from app.integrations.utils.schema import ShufflePayload
18 -from app.utils import get_customer_alert_settings
19 -
20 -# global set to keep track of checked IPs
21 -checked_ips = set()
22 -
23 -
24 -async def check_for_suspicious_login(hit, last_invalid_login, suspicious_logins, threshold):
25 - logSource = hit.source.logSource
26 - loginID = hit.source.params_loginID
27 - errCode = hit.source.errCode
28 - country = hit.source.httpReq_country
29 - ip = hit.source.ip
30 - event_timestamp = hit.source.event_timestamp
31 - customer_code = hit.source.customer_code
32 - index = hit.index
33 - id = hit.id
34 - logger.info(f"Checking loginID: {loginID} with errCode: {errCode} and IP: {ip} and index: {index} and id: {id}")
35 -
36 - if ip not in last_invalid_login:
37 - last_invalid_login[ip] = {"count": 0, "event_timestamp": None}
38 -
39 - if errCode in [e.value for e in ErrCode] and errCode != ErrCode.OK.value:
40 - logger.info(f"Found invalid login: {loginID} with IP: {ip} and errCode: {errCode}")
41 - last_invalid_login[ip]["count"] += 1
42 - last_invalid_login[ip]["event_timestamp"] = event_timestamp
43 -
44 - logger.info(f"Checking if last_invalid_login[ip]['count'] >= threshold: {last_invalid_login[ip]['count']} >= {threshold}")
45 - logger.info(f"Hit: {hit}")
46 - if errCode == ErrCode.OK.value and last_invalid_login[ip]["count"] >= threshold:
47 - logger.info(f"Found suspicious login: {loginID} with IP: {ip} and errCode: {errCode}")
48 - suspicious_login = SuspiciousLogin(
49 - _index=index,
50 - _id=id,
51 - customer_code=customer_code,
52 - logSource=logSource,
53 - loginID=loginID,
54 - country=country,
55 - ip=ip,
56 - event_timestamp=event_timestamp,
57 - errMessage=str(errCode),
58 - errDetails=hit.source.errDetails,
59 - )
60 - suspicious_logins.append(suspicious_login)
61 - last_invalid_login[ip] = {"count": 0, "event_timestamp": None}
62 -
63 -
64 -async def find_suscpicious_logins(threshold: int) -> List[SuspiciousLogin]:
65 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
66 - scroll_id = None
67 - suspicious_logins = []
68 - last_invalid_login = {}
69 -
70 - while True:
71 - if scroll_id is None:
72 - # Initial search
73 - results = es_client.search(
74 - index="sap_siem_*",
75 - body={
76 - "size": 10,
77 - "query": {"bool": {"must": [{"term": {"case_created": "False"}}, {"term": {"event_analyzed": "False"}}]}},
78 - "_source": ["logSource", "params_loginID", "errCode", "httpReq_country", "ip", "event_timestamp", "customer_code"],
79 - "sort": [{"event_timestamp": {"order": "asc"}}],
80 - },
81 - scroll="1m", # Keep the search context open for 1 minute
82 - )
83 - else:
84 - # Get the next batch of results
85 - results = es_client.scroll(scroll_id=scroll_id, scroll="1m")
86 -
87 - # If there are no more results, break the loop
88 - if not results["hits"]["hits"]:
89 - logger.info("No more results")
90 - break
91 - else:
92 - logger.info(f"Results: {results}")
93 -
94 - results = SapSiemWazuhIndexerResponse(**results)
95 - for hit in results.hits.hits:
96 - logger.info(f"Hit: {hit}")
97 - await check_for_suspicious_login(hit, last_invalid_login, suspicious_logins, threshold=threshold)
98 - await update_event_analyzed_flag(hit.id, hit.index)
99 -
100 - # Update the scroll ID
101 - scroll_id = results.scroll_id
102 -
103 - # Clear the scroll when you're done to free up resources
104 - if scroll_id is not None:
105 - es_client.clear_scroll(scroll_id=scroll_id)
106 -
107 - return suspicious_logins
108 -
109 -
110 -async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
111 - """
112 - Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
113 - Collects a max of 1000 records.
114 -
115 - :param suspicious_logins: A list of suspicious logins
116 -
117 - :return: List of the user Activity collected from the sap_siem table
118 - """
119 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
120 - results = es_client.search(
121 - index="sap_siem_*",
122 - body={
123 - "size": 1000,
124 - "query": {"bool": {"must": [{"term": {"ip": suspicious_logins.ip}}]}},
125 - },
126 - )
127 - return SapSiemWazuhIndexerResponse(**results)
128 -
129 -
130 -def create_asset_payload(asset: SuspiciousLogin):
131 - if asset.errMessage == "OK":
132 - return AddAssetModel(
133 - name=asset.loginID,
134 - ip=asset.ip,
135 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
136 - asset_type=1,
137 - compromise_status=1,
138 - analysis_status=2,
139 - )
140 - return AddAssetModel(
141 - name=asset.loginID,
142 - ip=asset.ip,
143 - description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
144 - asset_type=1,
145 - analysis_status=2,
146 - )
147 -
148 -
149 -async def update_case_with_asset(case_id: str, asset_payload):
150 - """
151 - Update the case with the asset information.
152 -
153 - :param case_id: The ID of the case to update
154 - :param asset_payload: The payload to update the case with
155 -
156 - :return: None
157 - """
158 - logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
159 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
160 - return await fetch_and_validate_data(
161 - client,
162 - case_client.add_asset,
163 - cid=case_id,
164 - **asset_payload.to_dict(),
165 - )
166 -
167 -
168 -async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
169 - for hit in user_activity.hits.hits:
170 - current_activity = {
171 - "loginID": hit.source.params_loginID,
172 - "ip": hit.source.ip,
173 - "country": hit.source.httpReq_country,
174 - "errMessage": hit.source.errMessage,
175 - "event_timestamp": hit.source.event_timestamp,
176 - "customer_code": hit.source.customer_code,
177 - "errDetails": hit.source.errDetails,
178 - }
179 - current_activity_frozenset = frozenset(current_activity.items())
180 - if current_activity_frozenset not in unique_instances:
181 - logger.info(f"Adding user activity to IRIS case: {current_activity}")
182 - current_asset = SuspiciousLogin(**current_activity)
183 - asset_payload = create_asset_payload(asset=current_asset)
184 - logger.info(f"Asset Payload: {asset_payload}")
185 - await update_case_with_asset(case_id, asset_payload)
186 - unique_instances.add(current_activity_frozenset)
187 -
188 -
189 -async def mark_as_checked(suspicious_login):
190 - checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
191 -
192 -
193 -async def handle_common_suspicious_login_tasks(
194 - suspicious_login,
195 - unique_instances,
196 - case_ids,
197 - create_case_fn,
198 - session: AsyncSession,
199 -):
200 - logger.info(f"Handling common suspicious login tasks: {suspicious_login}")
201 - case = await create_case_fn(suspicious_login, session=session)
202 - logger.info(f"Case: {case}")
203 - case_ids.append(case.data.case_id)
204 - user_activity = await collect_user_activity(suspicious_login)
205 - logger.info(f"User Activity: {user_activity}")
206 - await handle_user_activity(user_activity, unique_instances, case.data.case_id)
207 - await mark_as_checked(suspicious_login)
208 - alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
209 - await send_to_shuffle(
210 - ShufflePayload(
211 - alert_id=case.data.case_id,
212 - customer=suspicious_login.customer_code,
213 - customer_code=suspicious_login.customer_code,
214 - alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
215 - rule_description=f"{case.data.case_name}",
216 - hostname=suspicious_login.ip,
217 - ),
218 - session=session,
219 - )
220 -
221 -
222 -async def handle_suspicious_login(suspicious_login, unique_instances, case_ids, session: AsyncSession):
223 - logger.info(f"Handling suspicious login: {suspicious_login}")
224 - await handle_common_suspicious_login_tasks(suspicious_login, unique_instances, case_ids, create_iris_case, session)
225 - logger.info(f"Marking suspicious login as checked: {suspicious_login}")
226 - await update_case_created_flag(
227 - id=suspicious_login.id,
228 - index=suspicious_login.index,
229 - )
230 -
231 -
232 -async def update_case_created_flag(id: str, index: str):
233 - """
234 - Update the case_created flag in the Elasticsearch document to True.
235 -
236 - :param suspicious_login: The suspicious login to update
237 -
238 - :return: None
239 - """
240 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
241 - try:
242 - es_client.update(
243 - index=index,
244 - id=id,
245 - body={
246 - "doc": {
247 - "case_created": "True",
248 - },
249 - },
250 - )
251 - logger.info(f"Updated case_created flag for suspicious login: {id}")
252 - except Exception as e:
253 - logger.error(
254 - f"Failed to update case created flag {e}",
255 - )
256 - # Attempt to remove read-only block
257 - try:
258 - es_client.indices.put_settings(
259 - index=index,
260 - body={"index.blocks.write": None},
261 - )
262 - logger.info(
263 - f"Removed read-only block from index {index}. Retrying update.",
264 - )
265 -
266 - # Retry the update operation
267 - es_client.update(
268 - index=index,
269 - id=id,
270 - body={"doc": {"case_created": "True"}},
271 - )
272 - logger.info(
273 - f"Added case_created flag to index {index} for suspicious login: {id}",
274 - )
275 -
276 - # Reenable the write block
277 - es_client.indices.put_settings(
278 - index=index,
279 - body={"index.blocks.write": True},
280 - )
281 - except Exception as e2:
282 - logger.error(
283 - f"Failed to remove read-only block from index {index}: {e2}",
284 - )
285 - return False
286 -
287 -
288 -async def update_event_analyzed_flag(id: str, index: str):
289 - """
290 - Update the event_analyzed flag in the Elasticsearch document to True.
291 -
292 - :param suspicious_login: The suspicious login to update
293 -
294 - :return: None
295 - """
296 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
297 - try:
298 - es_client.update(
299 - index=index,
300 - id=id,
301 - body={
302 - "doc": {
303 - "event_analyzed": "True",
304 - },
305 - },
306 - )
307 - logger.info(f"Updated event_analyzed flag for suspicious login: {id}")
308 - except Exception as e:
309 - logger.error(
310 - f"Failed to update event analyzed flag {e}",
311 - )
312 - # Attempt to remove read-only block
313 - try:
314 - es_client.indices.put_settings(
315 - index=index,
316 - body={"index.blocks.write": None},
317 - )
318 - logger.info(
319 - f"Removed read-only block from index {index}. Retrying update.",
320 - )
321 -
322 - # Retry the update operation
323 - es_client.update(
324 - index=index,
325 - id=id,
326 - body={"doc": {"event_analyzed": "True"}},
327 - )
328 - logger.info(
329 - f"Added event_analyzed flag to index {index} for suspicious login: {id}",
330 - )
331 -
332 - # Reenable the write block
333 - es_client.indices.put_settings(
334 - index=index,
335 - body={"index.blocks.write": True},
336 - )
337 - except Exception as e2:
338 - logger.error(
339 - f"Failed to remove read-only block from index {index}: {e2}",
340 - )
341 - return False
342 -
343 -
344 -async def create_iris_case(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
345 - """
346 - Create a case in IRIS for the suspicious activity.
347 -
348 - :param user_activity: A list of suspicious activity from the sap_siem table
349 -
350 - :return: None
351 - """
352 - logger.info(f"Creating IRIS case for suspicious activity: {suspicious_login}")
353 - case_name = (
354 - f"Log Source: {suspicious_login.logSource} "
355 - f"Potential SAP SIEM Unauthorized Access: "
356 - f"{suspicious_login.loginID} from {suspicious_login.ip}"
357 - )
358 -
359 - case_description = (
360 - f"Log Source: {suspicious_login.logSource}\n\n"
361 - f"IP Address: {suspicious_login.ip}\n\n"
362 - f"Country: {suspicious_login.country}\n\n"
363 - f"Timestamp: {suspicious_login.event_timestamp}"
364 - )
365 -
366 - case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
367 -
368 - payload = IrisCasePayload(
369 - case_name=case_name,
370 - case_description=case_description,
371 - case_customer=case_customer,
372 - case_classification=18,
373 - soc_id="1",
374 - create_customer=False,
375 - )
376 - client, case_client = await initialize_client_and_case("DFIR-IRIS")
377 - result = await fetch_and_validate_data(
378 - client,
379 - case_client.add_case,
380 - **payload.dict(),
381 - )
382 -
383 - return CaseResponse(**result)
384 -
385 -
386 -async def sap_siem_suspicious_logins(threshold: int, session: AsyncSession) -> InvokeSAPSiemResponse:
387 - """
388 - Collects SAP SIEM events.
389 -
390 - Args:
391 - sap_siem_request (CollectSapSiemRequest): The request payload containing the necessary information for the SAP SIEM integration.
392 -
393 - Returns:
394 - InvokeSAPSiemResponse: The response model containing the result of the SAP SIEM integration invocation.
395 -
396 - Raises:
397 - HTTPException: If the SAP SIEM integration fails.
398 - """
399 - logger.info("Checking for suspicious logins")
400 -
401 - suspicious_logins = await find_suscpicious_logins(threshold=threshold)
402 - logger.info(f"Suspicious Logins: {suspicious_logins}")
403 -
404 - unique_instaces = set()
405 - case_ids = []
406 - for suspicious_login in suspicious_logins:
407 - await handle_suspicious_login(suspicious_login, unique_instaces, case_ids, session=session)
408 -
409 - # Clear the global set
410 - checked_ips.clear()
411 - return InvokeSAPSiemResponse(
412 - success=True,
413 - message="SAP SIEM Events collected successfully",
414 - )
backend/app/routers/ask_socfortress.py
+12 -12
@@ -1,15 +1,15 @@
1 -from fastapi import APIRouter
1 +# from fastapi import APIRouter
2
3 -from app.integrations.ask_socfortress.routes.ask_socfortress import (
4 - ask_socfortress_router,
5 -)
3 +# from app.integrations.ask_socfortress.routes.ask_socfortress import (
4 +# ask_socfortress_router,
5 +# )
6
7 -# Instantiate the APIRouter
8 -router = APIRouter()
7 +# # Instantiate the APIRouter
8 +# router = APIRouter()
9
10 -# Include the Ask SocFortress related routes
11 -router.include_router(
12 - ask_socfortress_router,
13 - prefix="/ask_socfortress",
14 - tags=["Ask SocFortress Integration"],
15 -)
10 +# # Include the Ask SocFortress related routes
11 +# router.include_router(
12 +# ask_socfortress_router,
13 +# prefix="/ask_socfortress",
14 +# tags=["Ask SocFortress Integration"],
15 +# )
backend/app/routers/dfir_iris.py
-19
@@ -1,10 +1,5 @@
1 from fastapi import APIRouter
2
3 -from app.connectors.dfir_iris.routes.alerts import dfir_iris_alerts_router
4 -from app.connectors.dfir_iris.routes.assets import dfir_iris_assets_router
5 -from app.connectors.dfir_iris.routes.cases import dfir_iris_cases_router
6 -from app.connectors.dfir_iris.routes.notes import dfir_iris_notes_router
7 -from app.connectors.dfir_iris.routes.users import dfir_iris_users_router
3 from app.integrations.alert_escalation.routes.escalate_alert import (
4 integration_escalate_alerts_router,
5 )
@@ -12,20 +7,6 @@ from app.integrations.alert_escalation.routes.escalate_alert import (
7 # Instantiate the APIRouter
8 router = APIRouter()
9
15 -# Include the DFIR Iris related routes
16 -router.include_router(
17 - dfir_iris_alerts_router,
18 - prefix="/soc/alerts",
19 - tags=["soc-alerts"],
20 -)
21 -router.include_router(
22 - dfir_iris_assets_router,
23 - prefix="/soc/assets",
24 - tags=["soc-assets"],
25 -)
26 -router.include_router(dfir_iris_cases_router, prefix="/soc/cases", tags=["soc-cases"])
27 -router.include_router(dfir_iris_notes_router, prefix="/soc/notes", tags=["soc-notes"])
28 -router.include_router(dfir_iris_users_router, prefix="/soc/users", tags=["soc-users"])
10 router.include_router(
11 integration_escalate_alerts_router,
12 prefix="/soc/general_alert",
backend/app/routers/monitoring_alert.py
-9
@@ -1,8 +1,5 @@
1 from fastapi import APIRouter
2
3 -from app.integrations.monitoring_alert.routes.monitoring_alert import (
4 - monitoring_alerts_router,
5 -)
3 from app.integrations.monitoring_alert.routes.provision import (
4 monitoring_alerts_provision_router,
5 )
@@ -10,12 +7,6 @@ from app.integrations.monitoring_alert.routes.provision import (
7 # Instantiate the APIRouter
8 router = APIRouter()
9
13 -# Include the Monitoring Alert related routes
14 -router.include_router(
15 - monitoring_alerts_router,
16 - prefix="/monitoring_alert",
17 - tags=["monitoring_alert"],
18 -)
10 router.include_router(
11 monitoring_alerts_provision_router,
12 prefix="/monitoring_alert",
backend/app/routers/sap_siem.py
-8
@@ -3,18 +3,10 @@ from fastapi import APIRouter
3 from app.integrations.sap_siem.routes.provision import (
4 integration_sap_siem_provision_scheduler_router,
5 )
6 -from app.integrations.sap_siem.routes.sap_siem import integration_sap_siem_router
6
7 # Instantiate the APIRouter
8 router = APIRouter()
9
11 -# Include the SAP SIEM related routes
12 -router.include_router(
13 - integration_sap_siem_router,
14 - prefix="/sap_siem",
15 - tags=["sap_siem"],
16 -)
17 -
10 # Include the SAP SIEM provisioning related routes
11 router.include_router(
12 integration_sap_siem_provision_scheduler_router,
backend/app/schedulers/scheduler.py
+10 -11
@@ -55,12 +55,6 @@ from app.schedulers.services.invoke_sap_siem import (
55 from app.schedulers.services.invoke_sap_siem import (
56 invoke_sap_siem_integration_suspicious_logins_analysis,
57 )
58 -from app.schedulers.services.monitoring_alert import (
59 - invoke_office365_exchange_online_alert,
60 -)
61 -from app.schedulers.services.monitoring_alert import invoke_office365_threat_intel_alert
62 -from app.schedulers.services.monitoring_alert import invoke_suricata_monitoring_alert
63 -from app.schedulers.services.monitoring_alert import invoke_wazuh_monitoring_alert
58 from app.schedulers.services.wazuh_index_resize import resize_wazuh_index_fields
59
60
@@ -137,7 +131,6 @@ async def initialize_job_metadata():
131 "function": resize_wazuh_index_fields,
132 "description": "Resizes the Wazuh index fields.",
133 },
140 - # TODO: ! COMMENTING OUT UNTIL INCIDENT MANAGEMENT IS IMPLEMENTED ! #
134 {
135 "job_id": "invoke_alert_creation_collect",
136 "time_interval": 5,
@@ -171,6 +164,16 @@ async def schedule_enabled_jobs(scheduler):
164 Schedules jobs that are enabled in the database.
165 """
166 async with AsyncSession(async_engine) as session:
167 + # ! First disable the job of `invoke_wazuh_monitoring_alert` if it is enabled
168 + # TODO ! Inefficient as hell but I will come back to this later
169 + stmt = select(JobMetadata).where(JobMetadata.job_id == "invoke_wazuh_monitoring_alert")
170 + result = await session.execute(stmt)
171 + job_metadata = result.scalars().one_or_none()
172 + if job_metadata:
173 + logger.info("Disabling job: invoke_wazuh_monitoring_alert")
174 + job_metadata.enabled = False
175 + await session.commit()
176 +
177 stmt = select(JobMetadata).where(JobMetadata.enabled == True)
178 result = await session.execute(stmt)
179 job_metadatas = result.scalars().all()
@@ -215,10 +218,6 @@ def get_function_by_name(function_name: str):
218 "invoke_alert_creation_collect": invoke_alert_creation_collect,
219 "invoke_mimecast_integration": invoke_mimecast_integration,
220 "invoke_mimecast_integration_ttp": invoke_mimecast_integration_ttp,
218 - "invoke_wazuh_monitoring_alert": invoke_wazuh_monitoring_alert,
219 - "invoke_office365_exchange_online_alert": invoke_office365_exchange_online_alert,
220 - "invoke_office365_threat_intel_alert": invoke_office365_threat_intel_alert,
221 - "invoke_suricata_monitoring_alert": invoke_suricata_monitoring_alert,
221 "invoke_sap_siem_integration_collection": invoke_sap_siem_integration_collection,
222 "invoke_sap_siem_integration_suspicious_logins_analysis": invoke_sap_siem_integration_suspicious_logins_analysis,
223 "invoke_sap_siem_integration_multiple_logins_same_ip_analysis": invoke_sap_siem_integration_multiple_logins_same_ip_analysis,
backend/app/schedulers/services/invoke_sap_siem.py
+32 -28
@@ -30,12 +30,14 @@ from app.integrations.modules.routes.sap_siem import (
30 invoke_sap_siem_successful_user_login_with_different_ip_route,
31 )
32 from app.integrations.modules.schema.sap_siem import InvokeSapSiemAnalysis
33 -from app.integrations.monitoring_alert.routes.monitoring_alert import (
34 - run_sap_siem_multiple_logins_same_ip_analysis,
35 -)
36 -from app.integrations.monitoring_alert.routes.monitoring_alert import (
37 - run_sap_siem_suspicious_logins_analysis,
38 -)
33 +
34 +# ! Commenting out for now, will revist later if needed ! #
35 +# from app.integrations.monitoring_alert.routes.monitoring_alert import (
36 +# run_sap_siem_multiple_logins_same_ip_analysis,
37 +# )
38 +# from app.integrations.monitoring_alert.routes.monitoring_alert import (
39 +# run_sap_siem_suspicious_logins_analysis,
40 +# )
41 from app.integrations.sap_siem.schema.sap_siem import InvokeSapSiemRequest
42 from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
43 from app.schedulers.models.scheduler import JobMetadata
@@ -96,13 +98,14 @@ async def invoke_sap_siem_integration_suspicious_logins_analysis() -> InvokeSAPS
98 result = await session.execute(stmt)
99 customer_codes = [row.customer_code for row in result.scalars()]
100 logger.info(f"customer_codes: {customer_codes}")
99 - for customer_code in customer_codes:
100 - extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_suspicious_logins_analysis")).extra_data
101 - threshold = int(extra_data) if extra_data is not None else 3
102 - await run_sap_siem_suspicious_logins_analysis(
103 - threshold=threshold,
104 - session=session,
105 - )
101 + # ! Commenting out for now, will revist later if needed ! #
102 + # for customer_code in customer_codes:
103 + # extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_suspicious_logins_analysis")).extra_data
104 + # threshold = int(extra_data) if extra_data is not None else 3
105 + # await run_sap_siem_suspicious_logins_analysis(
106 + # threshold=threshold,
107 + # session=session,
108 + # )
109 # Close the session
110 await session.close()
111 with get_sync_db_session() as session:
@@ -132,21 +135,22 @@ async def invoke_sap_siem_integration_multiple_logins_same_ip_analysis() -> Invo
135 result = await session.execute(stmt)
136 customer_codes = [row.customer_code for row in result.scalars()]
137 logger.info(f"customer_codes: {customer_codes}")
135 - for customer_code in customer_codes:
136 - extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_multiple_logins_same_ip_analysis")).extra_data
137 - if extra_data is not None:
138 - data_parts = extra_data.split(",")
139 - for part in data_parts:
140 - key, value = part.split("=")
141 - if key == "threshold":
142 - threshold = int(value)
143 - elif key == "time_range":
144 - time_range = int(value)
145 - await run_sap_siem_multiple_logins_same_ip_analysis(
146 - threshold=threshold,
147 - time_range=time_range,
148 - session=session,
149 - )
138 + # ! Commenting out for now, will revist later if needed ! #
139 + # for customer_code in customer_codes:
140 + # extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_multiple_logins_same_ip_analysis")).extra_data
141 + # if extra_data is not None:
142 + # data_parts = extra_data.split(",")
143 + # for part in data_parts:
144 + # key, value = part.split("=")
145 + # if key == "threshold":
146 + # threshold = int(value)
147 + # elif key == "time_range":
148 + # time_range = int(value)
149 + # await run_sap_siem_multiple_logins_same_ip_analysis(
150 + # threshold=threshold,
151 + # time_range=time_range,
152 + # session=session,
153 + # )
154 # Close the session
155 await session.close()
156 with get_sync_db_session() as session:
backend/app/schedulers/services/monitoring_alert.py deleted
-142
@@ -1,142 +0,0 @@
1 -from datetime import datetime
2 -
3 -from dotenv import load_dotenv
4 -from loguru import logger
5 -from sqlalchemy import select
6 -
7 -from app.db.db_session import get_db_session
8 -from app.db.db_session import get_sync_db_session
9 -from app.db.universal_models import CustomersMeta
10 -from app.integrations.monitoring_alert.routes.monitoring_alert import (
11 - run_office365_exchange_online_analysis,
12 -)
13 -from app.integrations.monitoring_alert.routes.monitoring_alert import (
14 - run_suricata_analysis,
15 -)
16 -from app.integrations.monitoring_alert.routes.monitoring_alert import run_wazuh_analysis
17 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
18 - AlertAnalysisResponse,
19 -)
20 -from app.integrations.monitoring_alert.schema.monitoring_alert import (
21 - MonitoringWazuhAlertsRequestModel,
22 -)
23 -from app.schedulers.models.scheduler import JobMetadata
24 -
25 -load_dotenv()
26 -
27 -
28 -async def invoke_wazuh_monitoring_alert() -> AlertAnalysisResponse:
29 - """
30 - Invokes the Wazuh monitoring alerts scheduled job.
31 -
32 - Returns:
33 - AlertAnalysisResponse: The response indicating the success of invoking the monitoring alerts.
34 - """
35 - logger.info("Invoking Wazuh monitoring alerts scheduled job.")
36 - customer_codes = []
37 - async with get_db_session() as session:
38 - stmt = select(CustomersMeta)
39 - result = await session.execute(stmt)
40 - customer_codes = [row.customer_code for row in result.scalars()]
41 - logger.info(f"customer_codes: {customer_codes}")
42 - for customer_code in customer_codes:
43 - await run_wazuh_analysis(
44 - MonitoringWazuhAlertsRequestModel(customer_code=customer_code),
45 - session,
46 - )
47 - # Close the session
48 - await session.close()
49 - with get_sync_db_session() as session:
50 - # Synchronous ORM operations
51 - job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_wazuh_monitoring_alert").one_or_none()
52 - if job_metadata:
53 - job_metadata.last_success = datetime.utcnow()
54 - session.add(job_metadata)
55 - session.commit()
56 - else:
57 - # Handle the case where job_metadata does not exist
58 - logger.error("JobMetadata for 'invoke_wazuh_monitoring_alert' not found.")
59 -
60 - return AlertAnalysisResponse(
61 - success=True,
62 - message="Wazuh monitoring alerts invoked.",
63 - )
64 -
65 -
66 -async def invoke_suricata_monitoring_alert() -> AlertAnalysisResponse:
67 - """
68 - Invokes the Suricata monitoring alerts scheduled job.
69 -
70 - Returns:
71 - WazuhAnalysisResponse: The response indicating the success of invoking the monitoring alerts.
72 - """
73 - logger.info("Invoking Suricata monitoring alerts scheduled job.")
74 - customer_codes = []
75 - async with get_db_session() as session:
76 - stmt = select(CustomersMeta)
77 - result = await session.execute(stmt)
78 - customer_codes = [row.customer_code for row in result.scalars()]
79 - logger.info(f"customer_codes: {customer_codes}")
80 - for customer_code in customer_codes:
81 - await run_suricata_analysis(
82 - MonitoringWazuhAlertsRequestModel(customer_code=customer_code),
83 - session,
84 - )
85 - # Close the session
86 - await session.close()
87 - with get_sync_db_session() as session:
88 - # Synchronous ORM operations
89 - job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_suricata_monitoring_alert").one_or_none()
90 - if job_metadata:
91 - job_metadata.last_success = datetime.utcnow()
92 - session.add(job_metadata)
93 - session.commit()
94 - else:
95 - # Handle the case where job_metadata does not exist
96 - logger.error(
97 - "JobMetadata for 'invoke_suricata_monitoring_alert' not found.",
98 - )
99 -
100 - return AlertAnalysisResponse(
101 - success=True,
102 - message="Suricata monitoring alerts invoked.",
103 - )
104 -
105 -
106 -async def invoke_office365_exchange_online_alert() -> AlertAnalysisResponse:
107 - """
108 - Invokes the Office365 Exchange Online monitoring alerts scheduled job.
109 -
110 - Returns:
111 - AlertAnalysisResponse: The response indicating the success of invoking the monitoring alerts.
112 - """
113 - logger.info("Invoking Office365 Exchange Online monitoring alerts scheduled job.")
114 - async with get_db_session() as session:
115 - stmt = select(CustomersMeta).where(CustomersMeta.customer_meta_office365_organization_id.isnot(None))
116 - result = await session.execute(stmt)
117 - customer_codes = [row.customer_meta_office365_organization_id for row in result.scalars()]
118 - logger.info(f"customer_codes: {customer_codes}")
119 - for customer_code in customer_codes:
120 - await run_office365_exchange_online_analysis(
121 - MonitoringWazuhAlertsRequestModel(customer_code=customer_code),
122 - session,
123 - )
124 - return AlertAnalysisResponse(
125 - success=True,
126 - message="Office365 Exchange Online monitoring alerts invoked.",
127 - )
128 -
129 -
130 -async def invoke_office365_threat_intel_alert() -> AlertAnalysisResponse:
131 - """
132 - Invokes the Office365 Threat Intel monitoring alerts scheduled job.
133 -
134 - Returns:
135 - AlertAnalysisResponse: The response indicating the success of invoking the monitoring alerts.
136 - """
137 - logger.info("Invoking Office365 Threat Intel monitoring alerts scheduled job.")
138 - # Add the logic to invoke the Office365 Threat Intel monitoring alerts
139 - return AlertAnalysisResponse(
140 - success=True,
141 - message="Office365 Threat Intel monitoring alerts invoked.",
142 - )
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_WAZUH_CONTENT_PACK.json
+284 -202
@@ -1,23 +1,21 @@
1 {
2 - "id": "261577fe-d9a2-4141-af74-635f085eee54",
3 - "rev": 1,
2 "v": "1",
3 + "id": "261577fe-d9a2-4141-af74-635f085eee54",
4 + "rev": 3,
5 "name": "SOCFORTRESS_WAZUH_CONTENT_PACK",
6 "summary": "Content Pack for Wazuh",
7 "description": "",
8 "vendor": "SOCFortress - info@socfortress.co",
9 "url": "https://github.com/socfortress/CoPilot/tree/main/backend/app/stack_provisioning/graylog/templates/wazuh_content_pack.json",
10 - "created_at": "2024-02-17T15:43:29.597Z",
11 - "server_version": "5.0.13+083613e",
10 "parameters": [],
11 "entities": [
12 {
15 - "id": "4fbce4a8-897e-4c2f-8030-8fa884c34fd8",
13 + "v": "1",
14 "type": {
15 "name": "input",
16 "version": "1"
17 },
20 - "v": "1",
18 + "id": "4fbce4a8-897e-4c2f-8030-8fa884c34fd8",
19 "data": {
20 "title": {
21 "@type": "string",
@@ -74,7 +72,7 @@
72 },
73 "tls_key_password": {
74 "@type": "string",
77 - "@value": "na)R]Q`7q9>qxjqLdn-T[=sX"
75 + "@value": "na"
76 }
77 },
78 "static_fields": {
@@ -858,12 +856,12 @@
856 ]
857 },
858 {
861 - "id": "9b317b42-770d-46fb-b740-96c679c38f73",
859 + "v": "1",
860 "type": {
861 "name": "lookup_adapter",
862 "version": "1"
863 },
866 - "v": "1",
864 + "id": "9b317b42-770d-46fb-b740-96c679c38f73",
865 "data": {
866 "_scope": {
867 "@type": "string",
@@ -924,12 +922,12 @@
922 ]
923 },
924 {
927 - "id": "41437203-db84-46a2-9ea1-0db7ed2c699e",
925 + "v": "1",
926 "type": {
927 "name": "lookup_adapter",
928 "version": "1"
929 },
932 - "v": "1",
930 + "id": "41437203-db84-46a2-9ea1-0db7ed2c699e",
931 "data": {
932 "_scope": {
933 "@type": "string",
@@ -990,12 +988,12 @@
988 ]
989 },
990 {
993 - "id": "5f2980e7-a0ea-46f1-99aa-da8b93365663",
991 + "v": "1",
992 "type": {
993 "name": "lookup_adapter",
994 "version": "1"
995 },
998 - "v": "1",
996 + "id": "5f2980e7-a0ea-46f1-99aa-da8b93365663",
997 "data": {
998 "_scope": {
999 "@type": "string",
@@ -1044,12 +1042,12 @@
1042 ]
1043 },
1044 {
1047 - "id": "1405cf60-dda4-43d4-bb60-53ceb06e1bf1",
1045 + "v": "1",
1046 "type": {
1047 "name": "lookup_adapter",
1048 "version": "1"
1049 },
1052 - "v": "1",
1050 + "id": "1405cf60-dda4-43d4-bb60-53ceb06e1bf1",
1051 "data": {
1052 "_scope": {
1053 "@type": "string",
@@ -1112,12 +1110,12 @@
1110 ]
1111 },
1112 {
1115 - "id": "731af176-a847-4a28-bf32-bd5ba4462d79",
1113 + "v": "1",
1114 "type": {
1115 "name": "lookup_adapter",
1116 "version": "1"
1117 },
1120 - "v": "1",
1118 + "id": "731af176-a847-4a28-bf32-bd5ba4462d79",
1119 "data": {
1120 "_scope": {
1121 "@type": "string",
@@ -1178,12 +1176,12 @@
1176 ]
1177 },
1178 {
1181 - "id": "009f38c0-0cf3-4010-8c8c-e4ea87e6f4f5",
1179 + "v": "1",
1180 "type": {
1181 "name": "lookup_cache",
1182 "version": "1"
1183 },
1186 - "v": "1",
1184 + "id": "009f38c0-0cf3-4010-8c8c-e4ea87e6f4f5",
1185 "data": {
1186 "_scope": {
1187 "@type": "string",
@@ -1232,12 +1230,12 @@
1230 ]
1231 },
1232 {
1235 - "id": "d8301ead-3d48-46bf-875f-7994a5dbe558",
1233 + "v": "1",
1234 "type": {
1235 "name": "lookup_cache",
1236 "version": "1"
1237 },
1240 - "v": "1",
1238 + "id": "d8301ead-3d48-46bf-875f-7994a5dbe558",
1239 "data": {
1240 "_scope": {
1241 "@type": "string",
@@ -1286,12 +1284,12 @@
1284 ]
1285 },
1286 {
1289 - "id": "55ff2d16-b318-4717-893f-fed63ca005fc",
1287 + "v": "1",
1288 "type": {
1289 "name": "lookup_cache",
1290 "version": "1"
1291 },
1294 - "v": "1",
1292 + "id": "55ff2d16-b318-4717-893f-fed63ca005fc",
1293 "data": {
1294 "_scope": {
1295 "@type": "string",
@@ -1340,12 +1338,12 @@
1338 ]
1339 },
1340 {
1343 - "id": "2a2edc22-1d0b-4ebb-adc6-068d0017a602",
1341 + "v": "1",
1342 "type": {
1343 "name": "lookup_cache",
1344 "version": "1"
1345 },
1348 - "v": "1",
1346 + "id": "2a2edc22-1d0b-4ebb-adc6-068d0017a602",
1347 "data": {
1348 "_scope": {
1349 "@type": "string",
@@ -1394,12 +1392,12 @@
1392 ]
1393 },
1394 {
1397 - "id": "bf37d686-3cb7-4eeb-9eb9-e08c0adc724b",
1395 + "v": "1",
1396 "type": {
1397 "name": "lookup_cache",
1398 "version": "1"
1399 },
1402 - "v": "1",
1400 + "id": "bf37d686-3cb7-4eeb-9eb9-e08c0adc724b",
1401 "data": {
1402 "_scope": {
1403 "@type": "string",
@@ -1448,12 +1446,12 @@
1446 ]
1447 },
1448 {
1451 - "id": "5d81905e-723d-4e57-9b2b-e088ffb5241d",
1449 + "v": "1",
1450 "type": {
1451 "name": "lookup_table",
1452 "version": "1"
1453 },
1456 - "v": "1",
1454 + "id": "5d81905e-723d-4e57-9b2b-e088ffb5241d",
1455 "data": {
1456 "default_single_value_type": {
1457 "@type": "string",
@@ -1504,12 +1502,12 @@
1502 ]
1503 },
1504 {
1507 - "id": "55c75b17-7e24-4859-b230-a4122fc8ec19",
1505 + "v": "1",
1506 "type": {
1507 "name": "lookup_table",
1508 "version": "1"
1509 },
1512 - "v": "1",
1510 + "id": "55c75b17-7e24-4859-b230-a4122fc8ec19",
1511 "data": {
1512 "default_single_value_type": {
1513 "@type": "string",
@@ -1560,12 +1558,12 @@
1558 ]
1559 },
1560 {
1563 - "id": "d1b3ac8a-ac53-4e8b-be76-abc431a5a80f",
1561 + "v": "1",
1562 "type": {
1563 "name": "lookup_table",
1564 "version": "1"
1565 },
1568 - "v": "1",
1566 + "id": "d1b3ac8a-ac53-4e8b-be76-abc431a5a80f",
1567 "data": {
1568 "default_single_value_type": {
1569 "@type": "string",
@@ -1616,12 +1614,12 @@
1614 ]
1615 },
1616 {
1619 - "id": "37164c4c-c986-4c7c-a5e3-d9a8f1ddc337",
1617 + "v": "1",
1618 "type": {
1619 "name": "lookup_table",
1620 "version": "1"
1621 },
1624 - "v": "1",
1622 + "id": "37164c4c-c986-4c7c-a5e3-d9a8f1ddc337",
1623 "data": {
1624 "default_single_value_type": {
1625 "@type": "string",
@@ -1672,12 +1670,12 @@
1670 ]
1671 },
1672 {
1675 - "id": "0d0105b5-0256-4a15-8f26-eff5b18ae614",
1673 + "v": "1",
1674 "type": {
1675 "name": "lookup_table",
1676 "version": "1"
1677 },
1680 - "v": "1",
1678 + "id": "0d0105b5-0256-4a15-8f26-eff5b18ae614",
1679 "data": {
1680 "default_single_value_type": {
1681 "@type": "string",
@@ -1728,12 +1726,12 @@
1726 ]
1727 },
1728 {
1731 - "id": "aa38e373-4bad-4bed-b6fe-fe707d749fdb",
1729 + "v": "1",
1730 "type": {
1731 "name": "pipeline",
1732 "version": "1"
1733 },
1736 - "v": "1",
1734 + "id": "aa38e373-4bad-4bed-b6fe-fe707d749fdb",
1735 "data": {
1736 "title": {
1737 "@type": "string",
@@ -1745,7 +1743,7 @@
1743 },
1744 "source": {
1745 "@type": "string",
1748 - "@value": "pipeline \"WAZUH PROCESSING PIPELINES\"\nstage 0 match pass\nrule \"DROP OSQUERY SOCKET EVENTS - WELL KNOWN PROCESSES\"\nrule \"DROP OSQUERY PROCESS EVENTS - WELL KNOWN PROCESSES\"\nrule \"DROP SYSMON EVT 7 LOGS - WELL KNOWN PROCESS IMAGE\"\nrule \"DROP SYSMON EVT 12 LOGS - WELL KNOWN PROCESS IMAGE\"\nrule \"DROP SYSMON EVT 10 LOGS - WELL KNOWN SOURCE PROCESS IMAGE\"\nrule \"DROP SYSMON EVT 13 LOGS - WELL KNOWN PROCESS IMAGE\"\nstage 1 match either\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - NOTICE\"\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - WARNING\"\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - INFO\"\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - ALERT\"\nrule \"Linux Sysmon Timestamp - UTC\"\nrule \"Office365 Timestamp - UTC\"\nrule \"Osquery Epoch Timestamp - UTC\"\nrule \"Packetbeat Timestamp - UTC\"\nrule \"Windows Timestamp - UTC\"\nrule \"URL Encode data_name field for ASK SOCFortress API\"\nrule \"NIST 800-53 TO CUI MAPPING\"\nstage 2 match pass\nrule \"USER ADDED TO PRIVILEGE ADMIN GROUP\"\nrule \"ASK SOCFORTRESS WINDOWS - CHAINSAW\"\nstage 3 match either\nrule \"LINUX SYSMON EVENT 1 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 5 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 9 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 11 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 23 LOG NORMALIZATION\"\nrule \"LINUX PACKETBEAT DNS LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 1 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 11 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 2 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 22 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 23 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 5 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 9 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 7 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 10 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 12 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 13 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 17 LOG NORMALIZATION\"\nrule \"LINUX OSQUERY PROCESS LIST LOG NORMALIZATION\"\nrule \"LINUX PACKETBEAT NETWORK LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 3 INBOUND CONNECTION LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 3 OUTBOUND CONNECTION LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 3 INBOUND CONNECTION LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 3 OUTBOUND CONNECTION LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 15 LOG NORMALIZATION\"\nrule \"OSSEC SYSCHECK ADDED EVENT NORMALIZATION\"\nrule \"EPSS CVE Criticality\"\nrule \"EPSS CVE Criticality - NMAP\"\nrule \"WINDOWS PROCESS ID - No EventData Field\"\nrule \"WINDOWS PROCESS ID - Non SOCFortress Sysmon\"\nrule \"Timestamp Failsafe - UTC\"\nrule \"LINUX OSQUERY-BPF PROCESS LIST LOG NORMALIZATION\"\nrule \"WINDOWS PROCESS ID - SIGMA - data_system_Execution_attributes_ProcessID\"\nrule \"WINDOWS PROCESS ID - SIGMA - data_event_ProcessId\"\nrule \"WINDOWS PROCESS ID - SIGMA - data_event_SourceProcessId\"\nrule \"LINUX OSQUERY PROCESS ID - GENERIC\"\nrule \"MacOS OSQUERY PROCESS_EVENTS LOG NORMALIZATION\"\nrule \"OpenAudit Remove field - data_system_network_item_dhcp_lease_obtained\"\nrule \"OpenAudit Remove field - data_system_software_item_install_date\"\nstage 4 match pass\nrule \"WINDOWS SYSMON EVENT 1 - SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 1 - EMPTY SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 7 - SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 7 - EMPTY SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 3 - NETWORK PORTS\"\nstage 5 match pass\nrule \"WINDOWS SYSMON EVENT 1 - UNAPPROVED SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 7 - UNAPPROVED SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 3 - UNCOMMON PORT\"\nstage 7 match either\nrule \"OVERRIDE SYSLOG LEVEL AFTER IoC FOUND\"\nstage 6 match either\nrule \"LINUX SYSMON EVENT 3 - SOCFORTRESS THREAT INTEL\"\nrule \"OSSEC SYSCHECK ADDED - SOCFORTRESS THREAT INTEL\"\nrule \"PACKETBEAT DNS QUERY - SOCFORTRESS THREAT INTEL\"\nrule \"PACKETBEAT HTTP/S CONNECTION - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 1 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 15 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 22 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 3 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 6 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 7 - SOCFORTRESS THREAT INTEL\"\nend"
1746 + "@value": "pipeline \"WAZUH PROCESSING PIPELINES\"\nstage 0 match pass\nrule \"DROP OSQUERY SOCKET EVENTS - WELL KNOWN PROCESSES\"\nrule \"DROP OSQUERY PROCESS EVENTS - WELL KNOWN PROCESSES\"\nrule \"DROP SYSMON EVT 7 LOGS - WELL KNOWN PROCESS IMAGE\"\nrule \"DROP SYSMON EVT 12 LOGS - WELL KNOWN PROCESS IMAGE\"\nrule \"DROP SYSMON EVT 10 LOGS - WELL KNOWN SOURCE PROCESS IMAGE\"\nrule \"DROP SYSMON EVT 13 LOGS - WELL KNOWN PROCESS IMAGE\"\nstage 1 match either\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - NOTICE\"\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - WARNING\"\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - INFO\"\nrule \"WAZUH CREATE FIELD SYSLOG LEVEL - ALERT\"\nrule \"Linux Sysmon Timestamp - UTC\"\nrule \"Office365 Timestamp - UTC\"\nrule \"Osquery Epoch Timestamp - UTC\"\nrule \"Packetbeat Timestamp - UTC\"\nrule \"Windows Timestamp - UTC\"\nrule \"URL Encode data_name field for ASK SOCFortress API\"\nrule \"NIST 800-53 TO CUI MAPPING\"\nrule \"SYSLOG TYPE OFFICE365\"\nrule \"SYSLOG TYPE SURICATA\"\nstage 2 match pass\nrule \"USER ADDED TO PRIVILEGE ADMIN GROUP\"\nrule \"ASK SOCFORTRESS WINDOWS - CHAINSAW\"\nstage 3 match either\nrule \"LINUX SYSMON EVENT 1 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 5 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 9 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 11 LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 23 LOG NORMALIZATION\"\nrule \"LINUX PACKETBEAT DNS LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 1 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 11 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 2 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 22 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 23 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 5 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 9 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 7 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 10 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 12 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 13 LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 17 LOG NORMALIZATION\"\nrule \"LINUX OSQUERY PROCESS LIST LOG NORMALIZATION\"\nrule \"LINUX PACKETBEAT NETWORK LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 3 INBOUND CONNECTION LOG NORMALIZATION\"\nrule \"LINUX SYSMON EVENT 3 OUTBOUND CONNECTION LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 3 INBOUND CONNECTION LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 3 OUTBOUND CONNECTION LOG NORMALIZATION\"\nrule \"WINDOWS SYSMON EVENT 15 LOG NORMALIZATION\"\nrule \"OSSEC SYSCHECK ADDED EVENT NORMALIZATION\"\nrule \"EPSS CVE Criticality\"\nrule \"EPSS CVE Criticality - NMAP\"\nrule \"WINDOWS PROCESS ID - No EventData Field\"\nrule \"WINDOWS PROCESS ID - Non SOCFortress Sysmon\"\nrule \"Timestamp Failsafe - UTC\"\nrule \"LINUX OSQUERY-BPF PROCESS LIST LOG NORMALIZATION\"\nrule \"WINDOWS PROCESS ID - SIGMA - data_system_Execution_attributes_ProcessID\"\nrule \"WINDOWS PROCESS ID - SIGMA - data_event_ProcessId\"\nrule \"WINDOWS PROCESS ID - SIGMA - data_event_SourceProcessId\"\nrule \"LINUX OSQUERY PROCESS ID - GENERIC\"\nrule \"MacOS OSQUERY PROCESS_EVENTS LOG NORMALIZATION\"\nrule \"OpenAudit Remove field - data_system_network_item_dhcp_lease_obtained\"\nrule \"OpenAudit Remove field - data_system_software_item_install_date\"\nrule \"OpenAudit Remove field - data_system_network_item_dhcp_lease_expires\"\nstage 4 match pass\nrule \"WINDOWS SYSMON EVENT 1 - SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 1 - EMPTY SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 7 - SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 7 - EMPTY SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 3 - NETWORK PORTS\"\nstage 5 match pass\nrule \"WINDOWS SYSMON EVENT 1 - UNAPPROVED SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 7 - UNAPPROVED SOFTWARE VENDORS\"\nrule \"WINDOWS SYSMON EVENT 3 - UNCOMMON PORT\"\nstage 7 match either\nrule \"OVERRIDE SYSLOG LEVEL AFTER IoC FOUND\"\nstage 6 match either\nrule \"LINUX SYSMON EVENT 3 - SOCFORTRESS THREAT INTEL\"\nrule \"OSSEC SYSCHECK ADDED - SOCFORTRESS THREAT INTEL\"\nrule \"PACKETBEAT DNS QUERY - SOCFORTRESS THREAT INTEL\"\nrule \"PACKETBEAT HTTP/S CONNECTION - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 1 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 15 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 22 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 3 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 6 - SOCFORTRESS THREAT INTEL\"\nrule \"WINDOWS SYSMON EVENT 7 - SOCFORTRESS THREAT INTEL\"\nend"
1747 },
1748 "connected_streams": [
1749 {
@@ -1762,12 +1760,12 @@
1760 ]
1761 },
1762 {
1765 - "id": "5a6fdec9-fa76-41f2-be92-158fa380db42",
1763 + "v": "1",
1764 "type": {
1765 "name": "pipeline_rule",
1766 "version": "1"
1767 },
1770 - "v": "1",
1768 + "id": "5a6fdec9-fa76-41f2-be92-158fa380db42",
1769 "data": {
1770 "title": {
1771 "@type": "string",
@@ -1790,12 +1788,12 @@
1788 ]
1789 },
1790 {
1793 - "id": "1d4217e1-28ab-4950-b8be-a25fc8e9555c",
1791 + "v": "1",
1792 "type": {
1793 "name": "pipeline_rule",
1794 "version": "1"
1795 },
1798 - "v": "1",
1796 + "id": "1d4217e1-28ab-4950-b8be-a25fc8e9555c",
1797 "data": {
1798 "title": {
1799 "@type": "string",
@@ -1818,12 +1816,12 @@
1816 ]
1817 },
1818 {
1821 - "id": "5745b55a-7958-45d6-bdbf-67b8feca5b3a",
1819 + "v": "1",
1820 "type": {
1821 "name": "pipeline_rule",
1822 "version": "1"
1823 },
1826 - "v": "1",
1824 + "id": "5745b55a-7958-45d6-bdbf-67b8feca5b3a",
1825 "data": {
1826 "title": {
1827 "@type": "string",
@@ -1846,12 +1844,12 @@
1844 ]
1845 },
1846 {
1849 - "id": "cf069904-64d8-4b2f-b57a-25fbc07dd47e",
1847 + "v": "1",
1848 "type": {
1849 "name": "pipeline_rule",
1850 "version": "1"
1851 },
1854 - "v": "1",
1852 + "id": "cf069904-64d8-4b2f-b57a-25fbc07dd47e",
1853 "data": {
1854 "title": {
1855 "@type": "string",
@@ -1874,12 +1872,12 @@
1872 ]
1873 },
1874 {
1877 - "id": "fcf8bf1f-5280-474a-88bd-0ef50f2049db",
1875 + "v": "1",
1876 "type": {
1877 "name": "pipeline_rule",
1878 "version": "1"
1879 },
1882 - "v": "1",
1880 + "id": "fcf8bf1f-5280-474a-88bd-0ef50f2049db",
1881 "data": {
1882 "title": {
1883 "@type": "string",
@@ -1902,12 +1900,12 @@
1900 ]
1901 },
1902 {
1905 - "id": "81633691-fa79-4ca8-a5ff-c67fc55ede49",
1903 + "v": "1",
1904 "type": {
1905 "name": "pipeline_rule",
1906 "version": "1"
1907 },
1910 - "v": "1",
1908 + "id": "81633691-fa79-4ca8-a5ff-c67fc55ede49",
1909 "data": {
1910 "title": {
1911 "@type": "string",
@@ -1930,12 +1928,12 @@
1928 ]
1929 },
1930 {
1933 - "id": "ed57ba97-b339-41cf-91c1-1837feecf032",
1931 + "v": "1",
1932 "type": {
1933 "name": "pipeline_rule",
1934 "version": "1"
1935 },
1938 - "v": "1",
1936 + "id": "ed57ba97-b339-41cf-91c1-1837feecf032",
1937 "data": {
1938 "title": {
1939 "@type": "string",
@@ -1958,12 +1956,12 @@
1956 ]
1957 },
1958 {
1961 - "id": "a711be64-131f-494f-9068-9dcd56da1b02",
1959 + "v": "1",
1960 "type": {
1961 "name": "pipeline_rule",
1962 "version": "1"
1963 },
1966 - "v": "1",
1964 + "id": "a711be64-131f-494f-9068-9dcd56da1b02",
1965 "data": {
1966 "title": {
1967 "@type": "string",
@@ -1986,12 +1984,12 @@
1984 ]
1985 },
1986 {
1989 - "id": "de7d8e04-ad04-4185-8811-569f0cc17bd5",
1987 + "v": "1",
1988 "type": {
1989 "name": "pipeline_rule",
1990 "version": "1"
1991 },
1994 - "v": "1",
1992 + "id": "de7d8e04-ad04-4185-8811-569f0cc17bd5",
1993 "data": {
1994 "title": {
1995 "@type": "string",
@@ -2014,12 +2012,12 @@
2012 ]
2013 },
2014 {
2017 - "id": "73af67a1-87a4-4e61-bd96-f68557603a92",
2015 + "v": "1",
2016 "type": {
2017 "name": "pipeline_rule",
2018 "version": "1"
2019 },
2022 - "v": "1",
2020 + "id": "73af67a1-87a4-4e61-bd96-f68557603a92",
2021 "data": {
2022 "title": {
2023 "@type": "string",
@@ -2042,12 +2040,12 @@
2040 ]
2041 },
2042 {
2045 - "id": "4105240a-13b3-4cf0-b62f-566227d522cb",
2043 + "v": "1",
2044 "type": {
2045 "name": "pipeline_rule",
2046 "version": "1"
2047 },
2050 - "v": "1",
2048 + "id": "4105240a-13b3-4cf0-b62f-566227d522cb",
2049 "data": {
2050 "title": {
2051 "@type": "string",
@@ -2070,12 +2068,12 @@
2068 ]
2069 },
2070 {
2073 - "id": "546e667e-b3c4-482f-9f43-a836097290ed",
2071 + "v": "1",
2072 "type": {
2073 "name": "pipeline_rule",
2074 "version": "1"
2075 },
2078 - "v": "1",
2076 + "id": "546e667e-b3c4-482f-9f43-a836097290ed",
2077 "data": {
2078 "title": {
2079 "@type": "string",
@@ -2098,12 +2096,12 @@
2096 ]
2097 },
2098 {
2101 - "id": "5452ff04-d03a-4876-8417-63b00ecdd2ed",
2099 + "v": "1",
2100 "type": {
2101 "name": "pipeline_rule",
2102 "version": "1"
2103 },
2106 - "v": "1",
2104 + "id": "5452ff04-d03a-4876-8417-63b00ecdd2ed",
2105 "data": {
2106 "title": {
2107 "@type": "string",
@@ -2126,12 +2124,12 @@
2124 ]
2125 },
2126 {
2129 - "id": "04371a3b-aaaf-4440-8cb4-4f53d0a74508",
2127 + "v": "1",
2128 "type": {
2129 "name": "pipeline_rule",
2130 "version": "1"
2131 },
2134 - "v": "1",
2132 + "id": "04371a3b-aaaf-4440-8cb4-4f53d0a74508",
2133 "data": {
2134 "title": {
2135 "@type": "string",
@@ -2154,12 +2152,12 @@
2152 ]
2153 },
2154 {
2157 - "id": "d501207b-5195-4a20-be79-342b824e36fa",
2155 + "v": "1",
2156 "type": {
2157 "name": "pipeline_rule",
2158 "version": "1"
2159 },
2162 - "v": "1",
2160 + "id": "d501207b-5195-4a20-be79-342b824e36fa",
2161 "data": {
2162 "title": {
2163 "@type": "string",
@@ -2182,12 +2180,12 @@
2180 ]
2181 },
2182 {
2185 - "id": "ba83edb5-a196-42b8-9367-04d44e9c2402",
2183 + "v": "1",
2184 "type": {
2185 "name": "pipeline_rule",
2186 "version": "1"
2187 },
2190 - "v": "1",
2188 + "id": "ba83edb5-a196-42b8-9367-04d44e9c2402",
2189 "data": {
2190 "title": {
2191 "@type": "string",
@@ -2210,12 +2208,12 @@
2208 ]
2209 },
2210 {
2213 - "id": "f19ebc4f-2a74-4e0e-844c-4b86087ff753",
2211 + "v": "1",
2212 "type": {
2213 "name": "pipeline_rule",
2214 "version": "1"
2215 },
2218 - "v": "1",
2216 + "id": "f19ebc4f-2a74-4e0e-844c-4b86087ff753",
2217 "data": {
2218 "title": {
2219 "@type": "string",
@@ -2238,12 +2236,12 @@
2236 ]
2237 },
2238 {
2241 - "id": "90ec68d3-bf43-477d-8b1e-a9accf50b60e",
2239 + "v": "1",
2240 "type": {
2241 "name": "pipeline_rule",
2242 "version": "1"
2243 },
2246 - "v": "1",
2244 + "id": "90ec68d3-bf43-477d-8b1e-a9accf50b60e",
2245 "data": {
2246 "title": {
2247 "@type": "string",
@@ -2266,12 +2264,12 @@
2264 ]
2265 },
2266 {
2269 - "id": "7a45ad2d-e6f1-4d2b-8f9f-1f8b44a73f0b",
2267 + "v": "1",
2268 "type": {
2269 "name": "pipeline_rule",
2270 "version": "1"
2271 },
2274 - "v": "1",
2272 + "id": "7a45ad2d-e6f1-4d2b-8f9f-1f8b44a73f0b",
2273 "data": {
2274 "title": {
2275 "@type": "string",
@@ -2294,12 +2292,12 @@
2292 ]
2293 },
2294 {
2297 - "id": "b807c042-0124-4a55-a92c-230c13fc1ca9",
2295 + "v": "1",
2296 "type": {
2297 "name": "pipeline_rule",
2298 "version": "1"
2299 },
2302 - "v": "1",
2300 + "id": "b807c042-0124-4a55-a92c-230c13fc1ca9",
2301 "data": {
2302 "title": {
2303 "@type": "string",
@@ -2322,12 +2320,12 @@
2320 ]
2321 },
2322 {
2325 - "id": "b3d62cc1-de49-4e29-a39a-88d19ac6da02",
2323 + "v": "1",
2324 "type": {
2325 "name": "pipeline_rule",
2326 "version": "1"
2327 },
2330 - "v": "1",
2328 + "id": "b3d62cc1-de49-4e29-a39a-88d19ac6da02",
2329 "data": {
2330 "title": {
2331 "@type": "string",
@@ -2350,12 +2348,12 @@
2348 ]
2349 },
2350 {
2353 - "id": "f709c6a9-8e24-4a37-a86d-3507ffb67010",
2351 + "v": "1",
2352 "type": {
2353 "name": "pipeline_rule",
2354 "version": "1"
2355 },
2358 - "v": "1",
2356 + "id": "f709c6a9-8e24-4a37-a86d-3507ffb67010",
2357 "data": {
2358 "title": {
2359 "@type": "string",
@@ -2378,12 +2376,12 @@
2376 ]
2377 },
2378 {
2381 - "id": "dbad257a-932d-48e5-8a70-ac3313857a9c",
2379 + "v": "1",
2380 "type": {
2381 "name": "pipeline_rule",
2382 "version": "1"
2383 },
2386 - "v": "1",
2384 + "id": "dbad257a-932d-48e5-8a70-ac3313857a9c",
2385 "data": {
2386 "title": {
2387 "@type": "string",
@@ -2406,12 +2404,12 @@
2404 ]
2405 },
2406 {
2409 - "id": "31918fde-a64c-4ad6-b3c2-5a4407236ee4",
2407 + "v": "1",
2408 "type": {
2409 "name": "pipeline_rule",
2410 "version": "1"
2411 },
2414 - "v": "1",
2412 + "id": "31918fde-a64c-4ad6-b3c2-5a4407236ee4",
2413 "data": {
2414 "title": {
2415 "@type": "string",
@@ -2434,12 +2432,12 @@
2432 ]
2433 },
2434 {
2437 - "id": "2596c531-6959-4955-bfef-b32c5ce296fd",
2435 + "v": "1",
2436 "type": {
2437 "name": "pipeline_rule",
2438 "version": "1"
2439 },
2442 - "v": "1",
2440 + "id": "2596c531-6959-4955-bfef-b32c5ce296fd",
2441 "data": {
2442 "title": {
2443 "@type": "string",
@@ -2462,12 +2460,12 @@
2460 ]
2461 },
2462 {
2465 - "id": "3a47b63b-504a-4884-aac3-7c782988fa52",
2463 + "v": "1",
2464 "type": {
2465 "name": "pipeline_rule",
2466 "version": "1"
2467 },
2470 - "v": "1",
2468 + "id": "3a47b63b-504a-4884-aac3-7c782988fa52",
2469 "data": {
2470 "title": {
2471 "@type": "string",
@@ -2490,12 +2488,12 @@
2488 ]
2489 },
2490 {
2493 - "id": "731ca120-24f1-4d56-ab8c-6706d4592ac8",
2491 + "v": "1",
2492 "type": {
2493 "name": "pipeline_rule",
2494 "version": "1"
2495 },
2498 - "v": "1",
2496 + "id": "731ca120-24f1-4d56-ab8c-6706d4592ac8",
2497 "data": {
2498 "title": {
2499 "@type": "string",
@@ -2518,12 +2516,12 @@
2516 ]
2517 },
2518 {
2521 - "id": "794068e2-fc89-41e8-a390-5f947695ba28",
2519 + "v": "1",
2520 "type": {
2521 "name": "pipeline_rule",
2522 "version": "1"
2523 },
2526 - "v": "1",
2524 + "id": "794068e2-fc89-41e8-a390-5f947695ba28",
2525 "data": {
2526 "title": {
2527 "@type": "string",
@@ -2546,12 +2544,12 @@
2544 ]
2545 },
2546 {
2549 - "id": "b4de7d44-01b1-4ec6-b4e4-92e991dcf6be",
2547 + "v": "1",
2548 "type": {
2549 "name": "pipeline_rule",
2550 "version": "1"
2551 },
2554 - "v": "1",
2552 + "id": "b4de7d44-01b1-4ec6-b4e4-92e991dcf6be",
2553 "data": {
2554 "title": {
2555 "@type": "string",
@@ -2574,12 +2572,12 @@
2572 ]
2573 },
2574 {
2577 - "id": "0f3bd968-508b-4fd4-9662-953b5a34e1e9",
2575 + "v": "1",
2576 "type": {
2577 "name": "pipeline_rule",
2578 "version": "1"
2579 },
2582 - "v": "1",
2580 + "id": "0f3bd968-508b-4fd4-9662-953b5a34e1e9",
2581 "data": {
2582 "title": {
2583 "@type": "string",
@@ -2602,12 +2600,12 @@
2600 ]
2601 },
2602 {
2605 - "id": "f663f3fd-955e-4b78-85f4-93571a64c252",
2603 + "v": "1",
2604 "type": {
2605 "name": "pipeline_rule",
2606 "version": "1"
2607 },
2610 - "v": "1",
2608 + "id": "f663f3fd-955e-4b78-85f4-93571a64c252",
2609 "data": {
2610 "title": {
2611 "@type": "string",
@@ -2630,12 +2628,12 @@
2628 ]
2629 },
2630 {
2633 - "id": "694f2402-bb3e-4d24-b892-bcbcc73fecfb",
2631 + "v": "1",
2632 "type": {
2633 "name": "pipeline_rule",
2634 "version": "1"
2635 },
2638 - "v": "1",
2636 + "id": "694f2402-bb3e-4d24-b892-bcbcc73fecfb",
2637 "data": {
2638 "title": {
2639 "@type": "string",
@@ -2658,12 +2656,12 @@
2656 ]
2657 },
2658 {
2661 - "id": "bf6d21ee-ab54-465f-a786-44ac833d3416",
2659 + "v": "1",
2660 "type": {
2661 "name": "pipeline_rule",
2662 "version": "1"
2663 },
2666 - "v": "1",
2664 + "id": "bf6d21ee-ab54-465f-a786-44ac833d3416",
2665 "data": {
2666 "title": {
2667 "@type": "string",
@@ -2686,12 +2684,12 @@
2684 ]
2685 },
2686 {
2689 - "id": "76aec17e-8048-46ce-9bd3-6dad333b66e5",
2687 + "v": "1",
2688 "type": {
2689 "name": "pipeline_rule",
2690 "version": "1"
2691 },
2694 - "v": "1",
2692 + "id": "76aec17e-8048-46ce-9bd3-6dad333b66e5",
2693 "data": {
2694 "title": {
2695 "@type": "string",
@@ -2714,12 +2712,12 @@
2712 ]
2713 },
2714 {
2717 - "id": "96a20a8a-1da1-48d4-81f0-0ef51a2c9e8e",
2715 + "v": "1",
2716 "type": {
2717 "name": "pipeline_rule",
2718 "version": "1"
2719 },
2722 - "v": "1",
2720 + "id": "96a20a8a-1da1-48d4-81f0-0ef51a2c9e8e",
2721 "data": {
2722 "title": {
2723 "@type": "string",
@@ -2742,12 +2740,12 @@
2740 ]
2741 },
2742 {
2745 - "id": "a47cbbda-bf0f-4b22-85c2-4ca03472c8dc",
2743 + "v": "1",
2744 "type": {
2745 "name": "pipeline_rule",
2746 "version": "1"
2747 },
2750 - "v": "1",
2748 + "id": "a47cbbda-bf0f-4b22-85c2-4ca03472c8dc",
2749 "data": {
2750 "title": {
2751 "@type": "string",
@@ -2770,12 +2768,12 @@
2768 ]
2769 },
2770 {
2773 - "id": "cae472b1-4e92-43d1-9433-4f29eca82916",
2771 + "v": "1",
2772 "type": {
2773 "name": "pipeline_rule",
2774 "version": "1"
2775 },
2778 - "v": "1",
2776 + "id": "cae472b1-4e92-43d1-9433-4f29eca82916",
2777 "data": {
2778 "title": {
2779 "@type": "string",
@@ -2798,12 +2796,12 @@
2796 ]
2797 },
2798 {
2801 - "id": "b709bcd4-70e1-4bf0-9a9c-c09ed9b5b09e",
2799 + "v": "1",
2800 "type": {
2801 "name": "pipeline_rule",
2802 "version": "1"
2803 },
2806 - "v": "1",
2804 + "id": "b709bcd4-70e1-4bf0-9a9c-c09ed9b5b09e",
2805 "data": {
2806 "title": {
2807 "@type": "string",
@@ -2826,12 +2824,12 @@
2824 ]
2825 },
2826 {
2829 - "id": "891d4cde-6e67-4b9d-98a7-52e9e73c5f4d",
2827 + "v": "1",
2828 "type": {
2829 "name": "pipeline_rule",
2830 "version": "1"
2831 },
2834 - "v": "1",
2832 + "id": "891d4cde-6e67-4b9d-98a7-52e9e73c5f4d",
2833 "data": {
2834 "title": {
2835 "@type": "string",
@@ -2854,12 +2852,12 @@
2852 ]
2853 },
2854 {
2857 - "id": "286ba66f-f2c2-49c3-a1c4-e7360db4fb11",
2855 + "v": "1",
2856 "type": {
2857 "name": "pipeline_rule",
2858 "version": "1"
2859 },
2862 - "v": "1",
2860 + "id": "286ba66f-f2c2-49c3-a1c4-e7360db4fb11",
2861 "data": {
2862 "title": {
2863 "@type": "string",
@@ -2882,12 +2880,12 @@
2880 ]
2881 },
2882 {
2885 - "id": "c18f97ef-48f9-47b8-ac56-9c7d870bbe6a",
2883 + "v": "1",
2884 "type": {
2885 "name": "pipeline_rule",
2886 "version": "1"
2887 },
2890 - "v": "1",
2888 + "id": "c18f97ef-48f9-47b8-ac56-9c7d870bbe6a",
2889 "data": {
2890 "title": {
2891 "@type": "string",
@@ -2910,12 +2908,12 @@
2908 ]
2909 },
2910 {
2913 - "id": "9544ca5f-1bd5-4c23-afab-ea30aa39e4c3",
2911 + "v": "1",
2912 "type": {
2913 "name": "pipeline_rule",
2914 "version": "1"
2915 },
2918 - "v": "1",
2916 + "id": "9544ca5f-1bd5-4c23-afab-ea30aa39e4c3",
2917 "data": {
2918 "title": {
2919 "@type": "string",
@@ -2938,12 +2936,12 @@
2936 ]
2937 },
2938 {
2941 - "id": "917d7a35-5a98-4e8e-9b3a-daacc76e3d31",
2939 + "v": "1",
2940 "type": {
2941 "name": "pipeline_rule",
2942 "version": "1"
2943 },
2946 - "v": "1",
2944 + "id": "917d7a35-5a98-4e8e-9b3a-daacc76e3d31",
2945 "data": {
2946 "title": {
2947 "@type": "string",
@@ -2966,12 +2964,12 @@
2964 ]
2965 },
2966 {
2969 - "id": "81cfe56d-b154-4be5-a7db-c062a863915c",
2967 + "v": "1",
2968 "type": {
2969 "name": "pipeline_rule",
2970 "version": "1"
2971 },
2974 - "v": "1",
2972 + "id": "81cfe56d-b154-4be5-a7db-c062a863915c",
2973 "data": {
2974 "title": {
2975 "@type": "string",
@@ -2994,12 +2992,12 @@
2992 ]
2993 },
2994 {
2997 - "id": "6f33986b-32cf-4c5c-bc9a-8e5626e29cce",
2995 + "v": "1",
2996 "type": {
2997 "name": "pipeline_rule",
2998 "version": "1"
2999 },
3002 - "v": "1",
3000 + "id": "6f33986b-32cf-4c5c-bc9a-8e5626e29cce",
3001 "data": {
3002 "title": {
3003 "@type": "string",
@@ -3022,12 +3020,12 @@
3020 ]
3021 },
3022 {
3025 - "id": "f70e4aea-2343-45d1-8b19-224c5bdb8632",
3023 + "v": "1",
3024 "type": {
3025 "name": "pipeline_rule",
3026 "version": "1"
3027 },
3030 - "v": "1",
3028 + "id": "f70e4aea-2343-45d1-8b19-224c5bdb8632",
3029 "data": {
3030 "title": {
3031 "@type": "string",
@@ -3050,12 +3048,12 @@
3048 ]
3049 },
3050 {
3053 - "id": "4cf85cca-5ab5-4b1a-a503-e4f2d1d75bd6",
3051 + "v": "1",
3052 "type": {
3053 "name": "pipeline_rule",
3054 "version": "1"
3055 },
3058 - "v": "1",
3056 + "id": "4cf85cca-5ab5-4b1a-a503-e4f2d1d75bd6",
3057 "data": {
3058 "title": {
3059 "@type": "string",
@@ -3078,12 +3076,12 @@
3076 ]
3077 },
3078 {
3081 - "id": "50303dca-8cc3-4367-9974-6fa0091d73a7",
3079 + "v": "1",
3080 "type": {
3081 "name": "pipeline_rule",
3082 "version": "1"
3083 },
3086 - "v": "1",
3084 + "id": "50303dca-8cc3-4367-9974-6fa0091d73a7",
3085 "data": {
3086 "title": {
3087 "@type": "string",
@@ -3106,12 +3104,12 @@
3104 ]
3105 },
3106 {
3109 - "id": "bfe85101-5ed8-442d-a0c2-3b3c6d3f1cd6",
3107 + "v": "1",
3108 "type": {
3109 "name": "pipeline_rule",
3110 "version": "1"
3111 },
3114 - "v": "1",
3112 + "id": "bfe85101-5ed8-442d-a0c2-3b3c6d3f1cd6",
3113 "data": {
3114 "title": {
3115 "@type": "string",
@@ -3134,12 +3132,12 @@
3132 ]
3133 },
3134 {
3137 - "id": "66d12fb9-3e88-4060-b17f-512ac5dac6a7",
3135 + "v": "1",
3136 "type": {
3137 "name": "pipeline_rule",
3138 "version": "1"
3139 },
3142 - "v": "1",
3140 + "id": "66d12fb9-3e88-4060-b17f-512ac5dac6a7",
3141 "data": {
3142 "title": {
3143 "@type": "string",
@@ -3162,12 +3160,12 @@
3160 ]
3161 },
3162 {
3165 - "id": "da125db2-5391-4fa7-8cfc-effabd40641c",
3163 + "v": "1",
3164 "type": {
3165 "name": "pipeline_rule",
3166 "version": "1"
3167 },
3170 - "v": "1",
3168 + "id": "da125db2-5391-4fa7-8cfc-effabd40641c",
3169 "data": {
3170 "title": {
3171 "@type": "string",
@@ -3190,12 +3188,12 @@
3188 ]
3189 },
3190 {
3193 - "id": "571ffe1c-0220-4b91-bc0f-04302d1e800c",
3191 + "v": "1",
3192 "type": {
3193 "name": "pipeline_rule",
3194 "version": "1"
3195 },
3198 - "v": "1",
3196 + "id": "571ffe1c-0220-4b91-bc0f-04302d1e800c",
3197 "data": {
3198 "title": {
3199 "@type": "string",
@@ -3218,12 +3216,12 @@
3216 ]
3217 },
3218 {
3221 - "id": "721afb39-b970-4636-b1e9-a3bec5b3c8da",
3219 + "v": "1",
3220 "type": {
3221 "name": "pipeline_rule",
3222 "version": "1"
3223 },
3226 - "v": "1",
3224 + "id": "721afb39-b970-4636-b1e9-a3bec5b3c8da",
3225 "data": {
3226 "title": {
3227 "@type": "string",
@@ -3246,12 +3244,12 @@
3244 ]
3245 },
3246 {
3249 - "id": "056eeb4e-36ff-4ebe-9a79-77adc2f7106e",
3247 + "v": "1",
3248 "type": {
3249 "name": "pipeline_rule",
3250 "version": "1"
3251 },
3254 - "v": "1",
3252 + "id": "056eeb4e-36ff-4ebe-9a79-77adc2f7106e",
3253 "data": {
3254 "title": {
3255 "@type": "string",
@@ -3274,12 +3272,12 @@
3272 ]
3273 },
3274 {
3277 - "id": "0fd58b5c-991c-418a-92dd-dadc2c1df8ee",
3275 + "v": "1",
3276 "type": {
3277 "name": "pipeline_rule",
3278 "version": "1"
3279 },
3282 - "v": "1",
3280 + "id": "0fd58b5c-991c-418a-92dd-dadc2c1df8ee",
3281 "data": {
3282 "title": {
3283 "@type": "string",
@@ -3302,12 +3300,12 @@
3300 ]
3301 },
3302 {
3305 - "id": "169eaa47-8de6-4cd8-aa18-adb37ff884c0",
3303 + "v": "1",
3304 "type": {
3305 "name": "pipeline_rule",
3306 "version": "1"
3307 },
3310 - "v": "1",
3308 + "id": "169eaa47-8de6-4cd8-aa18-adb37ff884c0",
3309 "data": {
3310 "title": {
3311 "@type": "string",
@@ -3330,12 +3328,12 @@
3328 ]
3329 },
3330 {
3333 - "id": "09c78369-36ac-4956-a1de-fa636aadae92",
3331 + "v": "1",
3332 "type": {
3333 "name": "pipeline_rule",
3334 "version": "1"
3335 },
3338 - "v": "1",
3336 + "id": "09c78369-36ac-4956-a1de-fa636aadae92",
3337 "data": {
3338 "title": {
3339 "@type": "string",
@@ -3358,12 +3356,12 @@
3356 ]
3357 },
3358 {
3361 - "id": "64b2b33d-f5a9-4365-9233-e9901abbf4c1",
3359 + "v": "1",
3360 "type": {
3361 "name": "pipeline_rule",
3362 "version": "1"
3363 },
3366 - "v": "1",
3364 + "id": "64b2b33d-f5a9-4365-9233-e9901abbf4c1",
3365 "data": {
3366 "title": {
3367 "@type": "string",
@@ -3386,12 +3384,12 @@
3384 ]
3385 },
3386 {
3389 - "id": "e71eaeef-b11b-4c97-afca-7f5875cd7459",
3387 + "v": "1",
3388 "type": {
3389 "name": "pipeline_rule",
3390 "version": "1"
3391 },
3394 - "v": "1",
3392 + "id": "e71eaeef-b11b-4c97-afca-7f5875cd7459",
3393 "data": {
3394 "title": {
3395 "@type": "string",
@@ -3414,12 +3412,12 @@
3412 ]
3413 },
3414 {
3417 - "id": "af6d5827-98f8-4470-b580-05365c9562cc",
3415 + "v": "1",
3416 "type": {
3417 "name": "pipeline_rule",
3418 "version": "1"
3419 },
3422 - "v": "1",
3420 + "id": "af6d5827-98f8-4470-b580-05365c9562cc",
3421 "data": {
3422 "title": {
3423 "@type": "string",
@@ -3442,12 +3440,12 @@
3440 ]
3441 },
3442 {
3445 - "id": "be2a3538-26f8-4b0c-8df4-dea7ed241ecc",
3443 + "v": "1",
3444 "type": {
3445 "name": "pipeline_rule",
3446 "version": "1"
3447 },
3450 - "v": "1",
3448 + "id": "be2a3538-26f8-4b0c-8df4-dea7ed241ecc",
3449 "data": {
3450 "title": {
3451 "@type": "string",
@@ -3470,12 +3468,12 @@
3468 ]
3469 },
3470 {
3473 - "id": "ad3d829e-ca93-42dc-b49e-4cc5104a8f69",
3471 + "v": "1",
3472 "type": {
3473 "name": "pipeline_rule",
3474 "version": "1"
3475 },
3478 - "v": "1",
3476 + "id": "ad3d829e-ca93-42dc-b49e-4cc5104a8f69",
3477 "data": {
3478 "title": {
3479 "@type": "string",
@@ -3498,12 +3496,12 @@
3496 ]
3497 },
3498 {
3501 - "id": "af720a8a-6293-48cf-a8a6-a5edea7b7e0b",
3499 + "v": "1",
3500 "type": {
3501 "name": "pipeline_rule",
3502 "version": "1"
3503 },
3506 - "v": "1",
3504 + "id": "af720a8a-6293-48cf-a8a6-a5edea7b7e0b",
3505 "data": {
3506 "title": {
3507 "@type": "string",
@@ -3526,12 +3524,12 @@
3524 ]
3525 },
3526 {
3529 - "id": "1ab185cc-0e73-4a98-b9aa-2cb5045fd4e3",
3527 + "v": "1",
3528 "type": {
3529 "name": "pipeline_rule",
3530 "version": "1"
3531 },
3534 - "v": "1",
3532 + "id": "1ab185cc-0e73-4a98-b9aa-2cb5045fd4e3",
3533 "data": {
3534 "title": {
3535 "@type": "string",
@@ -3554,12 +3552,12 @@
3552 ]
3553 },
3554 {
3557 - "id": "bc32db42-cff2-4750-ad22-00d82a01924b",
3555 + "v": "1",
3556 "type": {
3557 "name": "pipeline_rule",
3558 "version": "1"
3559 },
3562 - "v": "1",
3560 + "id": "bc32db42-cff2-4750-ad22-00d82a01924b",
3561 "data": {
3562 "title": {
3563 "@type": "string",
@@ -3582,12 +3580,12 @@
3580 ]
3581 },
3582 {
3585 - "id": "76773053-f1c5-4e98-93be-69baf05f95b5",
3583 + "v": "1",
3584 "type": {
3585 "name": "pipeline_rule",
3586 "version": "1"
3587 },
3590 - "v": "1",
3588 + "id": "76773053-f1c5-4e98-93be-69baf05f95b5",
3589 "data": {
3590 "title": {
3591 "@type": "string",
@@ -3610,12 +3608,12 @@
3608 ]
3609 },
3610 {
3613 - "id": "0f68d0d0-3555-4da1-8ef0-6fa3a028b8af",
3611 + "v": "1",
3612 "type": {
3613 "name": "pipeline_rule",
3614 "version": "1"
3615 },
3618 - "v": "1",
3616 + "id": "0f68d0d0-3555-4da1-8ef0-6fa3a028b8af",
3617 "data": {
3618 "title": {
3619 "@type": "string",
@@ -3638,12 +3636,12 @@
3636 ]
3637 },
3638 {
3641 - "id": "46b3df32-92c5-4cc7-8026-a66cc2623a52",
3639 + "v": "1",
3640 "type": {
3641 "name": "pipeline_rule",
3642 "version": "1"
3643 },
3646 - "v": "1",
3644 + "id": "46b3df32-92c5-4cc7-8026-a66cc2623a52",
3645 "data": {
3646 "title": {
3647 "@type": "string",
@@ -3666,12 +3664,12 @@
3664 ]
3665 },
3666 {
3669 - "id": "bb76d44c-45e5-4b3c-9414-a72c44370a7f",
3667 + "v": "1",
3668 "type": {
3669 "name": "pipeline_rule",
3670 "version": "1"
3671 },
3674 - "v": "1",
3672 + "id": "bb76d44c-45e5-4b3c-9414-a72c44370a7f",
3673 "data": {
3674 "title": {
3675 "@type": "string",
@@ -3694,12 +3692,12 @@
3692 ]
3693 },
3694 {
3697 - "id": "992c3888-0fa5-42d8-8748-6a61c2b8464b",
3695 + "v": "1",
3696 "type": {
3697 "name": "pipeline_rule",
3698 "version": "1"
3699 },
3702 - "v": "1",
3700 + "id": "992c3888-0fa5-42d8-8748-6a61c2b8464b",
3701 "data": {
3702 "title": {
3703 "@type": "string",
@@ -3722,12 +3720,12 @@
3720 ]
3721 },
3722 {
3725 - "id": "4d70e0a5-2a88-43aa-b659-16933a41c5da",
3723 + "v": "1",
3724 "type": {
3725 "name": "pipeline_rule",
3726 "version": "1"
3727 },
3730 - "v": "1",
3728 + "id": "4d70e0a5-2a88-43aa-b659-16933a41c5da",
3729 "data": {
3730 "title": {
3731 "@type": "string",
@@ -3750,12 +3748,12 @@
3748 ]
3749 },
3750 {
3753 - "id": "d049995b-2c18-407a-bdd2-141c94b93568",
3751 + "v": "1",
3752 "type": {
3753 "name": "pipeline_rule",
3754 "version": "1"
3755 },
3758 - "v": "1",
3756 + "id": "d049995b-2c18-407a-bdd2-141c94b93568",
3757 "data": {
3758 "title": {
3759 "@type": "string",
@@ -3778,12 +3776,12 @@
3776 ]
3777 },
3778 {
3781 - "id": "fad6eb39-5771-42ec-b95c-0b363c33bdb1",
3779 + "v": "1",
3780 "type": {
3781 "name": "pipeline_rule",
3782 "version": "1"
3783 },
3786 - "v": "1",
3784 + "id": "fad6eb39-5771-42ec-b95c-0b363c33bdb1",
3785 "data": {
3786 "title": {
3787 "@type": "string",
@@ -3806,12 +3804,12 @@
3804 ]
3805 },
3806 {
3809 - "id": "833dcc65-5743-4525-a9a0-cb08f5358d67",
3807 + "v": "1",
3808 "type": {
3809 "name": "pipeline_rule",
3810 "version": "1"
3811 },
3814 - "v": "1",
3812 + "id": "833dcc65-5743-4525-a9a0-cb08f5358d67",
3813 "data": {
3814 "title": {
3815 "@type": "string",
@@ -3834,12 +3832,12 @@
3832 ]
3833 },
3834 {
3837 - "id": "862acb0a-e994-47fe-b3b7-ea8474cf953a",
3835 + "v": "1",
3836 "type": {
3837 "name": "pipeline_rule",
3838 "version": "1"
3839 },
3842 - "v": "1",
3840 + "id": "862acb0a-e994-47fe-b3b7-ea8474cf953a",
3841 "data": {
3842 "title": {
3843 "@type": "string",
@@ -3862,12 +3860,12 @@
3860 ]
3861 },
3862 {
3865 - "id": "f8767b41-6060-40d2-b47b-949421652dbc",
3863 + "v": "1",
3864 "type": {
3865 "name": "pipeline_rule",
3866 "version": "1"
3867 },
3870 - "v": "1",
3868 + "id": "f8767b41-6060-40d2-b47b-949421652dbc",
3869 "data": {
3870 "title": {
3871 "@type": "string",
@@ -3890,12 +3888,12 @@
3888 ]
3889 },
3890 {
3893 - "id": "f11d88a6-077a-4b62-9979-f78e79920c54",
3891 + "v": "1",
3892 "type": {
3893 "name": "pipeline_rule",
3894 "version": "1"
3895 },
3898 - "v": "1",
3896 + "id": "f11d88a6-077a-4b62-9979-f78e79920c54",
3897 "data": {
3898 "title": {
3899 "@type": "string",
@@ -3918,12 +3916,12 @@
3916 ]
3917 },
3918 {
3921 - "id": "a08b82b7-6df9-4643-9d6e-549ed2f87c6d",
3919 + "v": "1",
3920 "type": {
3921 "name": "pipeline_rule",
3922 "version": "1"
3923 },
3926 - "v": "1",
3924 + "id": "a08b82b7-6df9-4643-9d6e-549ed2f87c6d",
3925 "data": {
3926 "title": {
3927 "@type": "string",
@@ -3946,12 +3944,12 @@
3944 ]
3945 },
3946 {
3949 - "id": "6233a285-38ea-48ef-bb17-fc85f94a75fc",
3947 + "v": "1",
3948 "type": {
3949 "name": "pipeline_rule",
3950 "version": "1"
3951 },
3954 - "v": "1",
3952 + "id": "6233a285-38ea-48ef-bb17-fc85f94a75fc",
3953 "data": {
3954 "title": {
3955 "@type": "string",
@@ -3974,12 +3972,12 @@
3972 ]
3973 },
3974 {
3977 - "id": "5e400ee7-a24d-4e5f-970d-9f20e01278f5",
3975 + "v": "1",
3976 "type": {
3977 "name": "pipeline_rule",
3978 "version": "1"
3979 },
3982 - "v": "1",
3980 + "id": "5e400ee7-a24d-4e5f-970d-9f20e01278f5",
3981 "data": {
3982 "title": {
3983 "@type": "string",
@@ -4002,12 +4000,96 @@
4000 ]
4001 },
4002 {
4005 - "id": "feb9e1d7-9346-4a2a-9566-59ed483c1b83",
4003 + "v": "1",
4004 "type": {
4007 - "name": "stream",
4005 + "name": "pipeline_rule",
4006 + "version": "1"
4007 + },
4008 + "id": "679674f1-8e0e-4d87-b8a2-fb2b7a64bf56",
4009 + "data": {
4010 + "title": {
4011 + "@type": "string",
4012 + "@value": "OpenAudit Remove field - data_system_network_item_dhcp_lease_expires"
4013 + },
4014 + "description": {
4015 + "@type": "string",
4016 + "@value": "OpenAudit Remove field - data_system_network_item_dhcp_lease_expires"
4017 + },
4018 + "source": {
4019 + "@type": "string",
4020 + "@value": "rule \"OpenAudit Remove field - data_system_network_item_dhcp_lease_expires\"\nwhen\n // Apply the rule when the specified field is present\n has_field(\"data_system_network_item_dhcp_lease_expires\")\nthen\n // Remove the specified field\n remove_field(\"data_system_network_item_dhcp_lease_expires\");\nend"
4021 + }
4022 + },
4023 + "constraints": [
4024 + {
4025 + "type": "server-version",
4026 + "version": ">=5.0.13+083613e"
4027 + }
4028 + ]
4029 + },
4030 + {
4031 + "v": "1",
4032 + "type": {
4033 + "name": "pipeline_rule",
4034 + "version": "1"
4035 + },
4036 + "id": "e3d86661-b008-4a88-bfc9-2ea479ab56b3",
4037 + "data": {
4038 + "title": {
4039 + "@type": "string",
4040 + "@value": "SYSLOG TYPE OFFICE365"
4041 + },
4042 + "description": {
4043 + "@type": "string",
4044 + "@value": "SYSLOG TYPE OFFICE365"
4045 + },
4046 + "source": {
4047 + "@type": "string",
4048 + "@value": "rule \"SYSLOG TYPE OFFICE365\"\nwhen\n $message.rule_group1 == \"office365\"\nthen\n set_field(\"syslog_type\", \"office365\");\nend"
4049 + }
4050 + },
4051 + "constraints": [
4052 + {
4053 + "type": "server-version",
4054 + "version": ">=5.0.13+083613e"
4055 + }
4056 + ]
4057 + },
4058 + {
4059 + "v": "1",
4060 + "type": {
4061 + "name": "pipeline_rule",
4062 "version": "1"
4063 },
4064 + "id": "94285f06-b069-45f8-a88f-34d20d0d83e4",
4065 + "data": {
4066 + "title": {
4067 + "@type": "string",
4068 + "@value": "SYSLOG TYPE SURICATA"
4069 + },
4070 + "description": {
4071 + "@type": "string",
4072 + "@value": "SYSLOG TYPE SURICATA"
4073 + },
4074 + "source": {
4075 + "@type": "string",
4076 + "@value": "rule \"SYSLOG TYPE SURICATA\"\nwhen\n $message.rule_group1 == \"suricata\"\nthen\n set_field(\"syslog_type\", \"suricata\");\nend"
4077 + }
4078 + },
4079 + "constraints": [
4080 + {
4081 + "type": "server-version",
4082 + "version": ">=5.0.13+083613e"
4083 + }
4084 + ]
4085 + },
4086 + {
4087 "v": "1",
4088 + "type": {
4089 + "name": "stream",
4090 + "version": "1"
4091 + },
4092 + "id": "feb9e1d7-9346-4a2a-9566-59ed483c1b83",
4093 "data": {
4094 "alarm_callbacks": [],
4095 "outputs": [],
backend/app/utils.py
+14 -5
@@ -615,24 +615,33 @@ def allowed_file(filename):
615
616 ################## ! DATABASE UTILS ! ##################
617 async def get_connector_attribute(
618 - connector_id: int,
618 column_name: str,
619 + connector_id: Optional[int] = None,
620 + connector_name: Optional[str] = None,
621 session: AsyncSession = Depends(get_session),
622 ) -> Optional[Any]:
623 """
624 Retrieve the value of a specific column from a connector.
625
626 Args:
626 - connector_id (int): The ID of the connector.
627 + connector_id (Optional[int]): The ID of the connector.
628 + connector_name (Optional[str]): The name of the connector.
629 column_name (str): The name of the column to retrieve.
630 session (AsyncSession, optional): The database session. Defaults to Depends(get_session).
631
632 Returns:
633 Optional[Any]: The value of the column, or None if the connector or column does not exist.
634 """
633 - result = await session.execute(
634 - select(Connectors).filter(Connectors.id == connector_id),
635 - )
635 + if not connector_id and not connector_name:
636 + raise ValueError("Either connector_id or connector_name must be provided.")
637 +
638 + query = select(Connectors)
639 + if connector_id:
640 + query = query.filter(Connectors.id == connector_id)
641 + if connector_name:
642 + query = query.filter(Connectors.connector_name == connector_name)
643 +
644 + result = await session.execute(query)
645 connector = result.scalars().first()
646
647 if connector:
backend/copilot.py
+4 -4
@@ -28,11 +28,11 @@ from app.middleware.exception_handlers import custom_http_exception_handler
28 from app.middleware.exception_handlers import validation_exception_handler
29 from app.middleware.exception_handlers import value_error_handler
30 from app.middleware.logger import log_requests
31 +
32 +# from app.routers import ask_socfortress
33 from app.routers import active_response
34 from app.routers import agents
33 -from app.routers import alert_creation
35 from app.routers import alert_creation_settings
35 -from app.routers import ask_socfortress
36 from app.routers import auth
37 from app.routers import carbonblack
38 from app.routers import connectors
@@ -131,8 +131,8 @@ api_router.include_router(influxdb.router)
131 api_router.include_router(grafana.router)
132 api_router.include_router(customer_provisioning.router)
133 api_router.include_router(threat_intel.router)
134 -api_router.include_router(ask_socfortress.router)
135 -api_router.include_router(alert_creation.router)
134 +# ! Commenting out for now, will revist later if needed ! #
135 +# api_router.include_router(ask_socfortress.router)
136 api_router.include_router(alert_creation_settings.router)
137 api_router.include_router(integrations.router)
138 api_router.include_router(office365.router)
frontend/index.html
+2 -2
@@ -9,9 +9,9 @@
9 name="viewport"
10 content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
11 />
12 - <meta name="apple-mobile-web-app-capable" content="yes" />
12 + <meta name="mobile-web-app-capable" content="yes" />
13 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
14 - <meta name="apple-mobile-web-app-title" content="Pinx" />
14 + <meta name="apple-mobile-web-app-title" content="SOCFortress CoPilot" />
15 <meta name="apple-touch-fullscreen" content="yes" />
16 <meta name="HandheldFriendly" content="true" />
17
frontend/package-lock.json
+92 -83
@@ -14,11 +14,11 @@
14 "@fontsource/lexend": "^5.0.21",
15 "@fontsource/public-sans": "^5.0.18",
16 "@popperjs/core": "^2.11.8",
17 - "@shikijs/markdown-it": "^1.14.1",
17 + "@shikijs/markdown-it": "^1.16.1",
18 "@vueuse/components": "^11.0.3",
19 "@vueuse/core": "^11.0.3",
20 - "apexcharts": "^3.52.0",
21 - "axios": "^1.7.5",
20 + "apexcharts": "^3.53.0",
21 + "axios": "^1.7.7",
22 "bytes": "^3.1.2",
23 "colord": "^2.9.3",
24 "crypto-js": "^4.2.0",
@@ -34,9 +34,9 @@
34 "naive-ui": "^2.39.0",
35 "password-validator": "^5.3.0",
36 "pinia": "^2.2.2",
37 - "pinia-plugin-persistedstate": "^3.2.1",
37 + "pinia-plugin-persistedstate": "^3.2.3",
38 "secure-ls": "^2.0.0",
39 - "shiki": "^1.14.1",
39 + "shiki": "^1.16.1",
40 "validator": "^13.12.0",
41 "vue": "^3.4.38",
42 "vue-advanced-cropper": "^2.8.9",
@@ -44,8 +44,8 @@
44 "vue-i18n": "^9.14.0",
45 "vue-router": "^4.4.3",
46 "vue-sjv": "^0.0.6",
47 - "vue3-apexcharts": "^1.5.3",
48 - "vue3-marquee": "^4.2.1",
47 + "vue3-apexcharts": "^1.6.0",
48 + "vue3-marquee": "^4.2.2",
49 "vuedraggable": "^4.1.0"
50 },
51 "devDependencies": {
@@ -58,16 +58,16 @@
58 "@types/inquirer": "^9.0.7",
59 "@types/jsdom": "^21.1.7",
60 "@types/lodash": "^4.17.7",
61 - "@types/node": "^22.5.0",
61 + "@types/node": "^22.5.2",
62 "@types/validator": "^13.12.1",
63 - "@vitejs/plugin-vue": "^5.1.2",
63 + "@vitejs/plugin-vue": "^5.1.3",
64 "@vitejs/plugin-vue-jsx": "^4.0.1",
65 "@vue/eslint-config-prettier": "^9.0.0",
66 "@vue/eslint-config-typescript": "^13.0.0",
67 "@vue/test-utils": "^2.4.6",
68 "@vue/tsconfig": "^0.5.1",
69 "autoprefixer": "^10.4.20",
70 - "cypress": "^13.13.3",
70 + "cypress": "^13.14.1",
71 "eslint": "^9.9.1",
72 "eslint-plugin-cypress": "^3.5.0",
73 "eslint-plugin-vue": "^9.27.0",
@@ -78,14 +78,14 @@
78 "json5": "^2.2.3",
79 "npm-run-all2": "^6.2.2",
80 "picocolors": "^1.0.1",
81 - "postcss": "^8.4.41",
81 + "postcss": "^8.4.43",
82 "prettier": "^3.3.3",
83 "sass": "^1.77.8",
84 "start-server-and-test": "^2.0.5",
85 "tailwind-config-viewer": "^2.0.4",
86 "tailwindcss": "^3.4.10",
87 "taze": "^0.16.7",
88 - "type-fest": "^4.25.0",
88 + "type-fest": "^4.26.0",
89 "unplugin-vue-components": "^0.27.4",
90 "vite": "^5.4.2",
91 "vite-bundle-analyzer": "^0.10.6",
@@ -93,13 +93,13 @@
93 "vite-plugin-vue-devtools": "^7.3.9",
94 "vite-svg-loader": "^5.1.0",
95 "vitest": "^2.0.5",
96 - "vue-tsc": "^2.0.29"
96 + "vue-tsc": "^2.1.5"
97 },
98 "engines": {
99 "node": ">=18.0.0"
100 },
101 "optionalDependencies": {
102 - "@rollup/rollup-linux-x64-gnu": "^4.21.1"
102 + "@rollup/rollup-linux-x64-gnu": "^4.21.2"
103 }
104 },
105 "node_modules/@ajoelp/json-to-formdata": {
@@ -1944,9 +1944,9 @@
1944 ]
1945 },
1946 "node_modules/@rollup/rollup-linux-x64-gnu": {
1947 - "version": "4.21.1",
1948 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz",
1949 - "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==",
1947 + "version": "4.21.2",
1948 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz",
1949 + "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==",
1950 "cpu": [
1951 "x64"
1952 ],
@@ -2008,31 +2008,37 @@
2008 ]
2009 },
2010 "node_modules/@shikijs/core": {
2011 - "version": "1.14.1",
2012 - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.14.1.tgz",
2013 - "integrity": "sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==",
2011 + "version": "1.16.1",
2012 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.16.1.tgz",
2013 + "integrity": "sha512-aI0hBtw+a6KsJp2jcD4YuQqKpeCbURMZbhHVozDknJpm+KJqeMRkEnfBC8BaKE/5XC+uofPgCLsa/TkTk0Ba0w==",
2014 "dependencies": {
2015 + "@shikijs/vscode-textmate": "^9.2.0",
2016 "@types/hast": "^3.0.4"
2017 }
2018 },
2019 "node_modules/@shikijs/markdown-it": {
2019 - "version": "1.14.1",
2020 - "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.14.1.tgz",
2021 - "integrity": "sha512-bb7IHIKLqKMf8ufjdRmsw2wfvX6k9e1DJCfqWEj6RMP59aFohX3emhXIoY8M8uxVfwMTi5ik9QAXGBg6DDnG+A==",
2020 + "version": "1.16.1",
2021 + "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.16.1.tgz",
2022 + "integrity": "sha512-vVEm/htIykOHdUn4OqMFKCBpFWUx+jkGzb0mCWE37nEsjzuE7qY4dC2s9mDtsAO+CnGY5dQNBc3yzgj7poDjeg==",
2023 "dependencies": {
2023 - "@shikijs/transformers": "1.14.1",
2024 + "@shikijs/transformers": "1.16.1",
2025 "markdown-it": "^14.1.0",
2025 - "shiki": "1.14.1"
2026 + "shiki": "1.16.1"
2027 }
2028 },
2029 "node_modules/@shikijs/transformers": {
2029 - "version": "1.14.1",
2030 - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.14.1.tgz",
2031 - "integrity": "sha512-JJqL8QBVCJh3L61jqqEXgFq1cTycwjcGj7aSmqOEsbxnETM9hRlaB74QuXvY/fVJNjbNt8nvWo0VwAXKvMSLRg==",
2030 + "version": "1.16.1",
2031 + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.16.1.tgz",
2032 + "integrity": "sha512-mfbe4YMov+1eyIBU3F6BtaPmLgDkRQaVse8xsBlKTVAcNF3cbZMRCyUz2N6gJOMKLJiv9T5gapBPbRxrDMuoxA==",
2033 "dependencies": {
2033 - "shiki": "1.14.1"
2034 + "shiki": "1.16.1"
2035 }
2036 },
2037 + "node_modules/@shikijs/vscode-textmate": {
2038 + "version": "9.2.0",
2039 + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.2.0.tgz",
2040 + "integrity": "sha512-5FinaOp6Vdh/dl4/yaOTh0ZeKch+rYS8DUb38V3GMKYVkdqzxw53lViRKUYkVILRiVQT7dcPC7VvAKOR73zVtQ=="
2041 + },
2042 "node_modules/@sideway/address": {
2043 "version": "4.1.5",
2044 "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -2159,9 +2165,9 @@
2165 }
2166 },
2167 "node_modules/@types/node": {
2162 - "version": "22.5.0",
2163 - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
2164 - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
2168 + "version": "22.5.2",
2169 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz",
2170 + "integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==",
2171 "dev": true,
2172 "dependencies": {
2173 "undici-types": "~6.19.2"
@@ -2778,9 +2784,9 @@
2784 }
2785 },
2786 "node_modules/@vitejs/plugin-vue": {
2781 - "version": "5.1.2",
2782 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.2.tgz",
2783 - "integrity": "sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==",
2787 + "version": "5.1.3",
2788 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.3.tgz",
2789 + "integrity": "sha512-3xbWsKEKXYlmX82aOHufFQVnkbMC/v8fLpWwh6hWOUrK5fbbtBh9Q/WWse27BFgSy2/e2c0fz5Scgya9h2GLhw==",
2790 "dev": true,
2791 "engines": {
2792 "node": "^18.0.0 || >=20.0.0"
@@ -2899,27 +2905,27 @@
2905 }
2906 },
2907 "node_modules/@volar/language-core": {
2902 - "version": "2.4.0-alpha.18",
2903 - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz",
2904 - "integrity": "sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==",
2908 + "version": "2.4.1",
2909 + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.1.tgz",
2910 + "integrity": "sha512-9AKhC7Qn2mQYxj7Dz3bVxeOk7gGJladhWixUYKef/o0o7Bm4an+A3XvmcTHVqZ8stE6lBVH++g050tBtJ4TZPQ==",
2911 "dev": true,
2912 "dependencies": {
2907 - "@volar/source-map": "2.4.0-alpha.18"
2913 + "@volar/source-map": "2.4.1"
2914 }
2915 },
2916 "node_modules/@volar/source-map": {
2911 - "version": "2.4.0-alpha.18",
2912 - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz",
2913 - "integrity": "sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==",
2917 + "version": "2.4.1",
2918 + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.1.tgz",
2919 + "integrity": "sha512-Xq6ep3OZg9xUqN90jEgB9ztX5SsTz1yiV8wiQbcYNjWkek+Ie3dc8l7AVt3EhDm9mSIR58oWczHkzM2H6HIsmQ==",
2920 "dev": true
2921 },
2922 "node_modules/@volar/typescript": {
2917 - "version": "2.4.0-alpha.18",
2918 - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz",
2919 - "integrity": "sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==",
2923 + "version": "2.4.1",
2924 + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.1.tgz",
2925 + "integrity": "sha512-UoRzC0PXcwajFQTu8XxKSYNsWNBtVja6Y9gC8eLv7kYm+UEKJCcZ8g7dialsOYA0HKs3Vpg57MeCsawFLC6m9Q==",
2926 "dev": true,
2927 "dependencies": {
2922 - "@volar/language-core": "2.4.0-alpha.18",
2928 + "@volar/language-core": "2.4.1",
2929 "path-browserify": "^1.0.1",
2930 "vscode-uri": "^3.0.8"
2931 }
@@ -3138,12 +3144,13 @@
3144 }
3145 },
3146 "node_modules/@vue/language-core": {
3141 - "version": "2.0.29",
3142 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.29.tgz",
3143 - "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==",
3147 + "version": "2.1.5",
3148 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.5.tgz",
3149 + "integrity": "sha512-YwF5+2v+rnue2/17wP84tnZgaB2hUM4C1U+115vVGrDAPGhBf7YG0nPv1SO0yUeEpYMYtED4ClamkXAwpoz2Yw==",
3150 + "deprecated": "use 2.1.4 instead",
3151 "dev": true,
3152 "dependencies": {
3146 - "@volar/language-core": "~2.4.0-alpha.18",
3153 + "@volar/language-core": "~2.4.1",
3154 "@vue/compiler-dom": "^3.4.0",
3155 "@vue/compiler-vue2": "^2.7.16",
3156 "@vue/shared": "^3.4.0",
@@ -3502,9 +3509,9 @@
3509 }
3510 },
3511 "node_modules/apexcharts": {
3505 - "version": "3.52.0",
3506 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz",
3507 - "integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==",
3512 + "version": "3.53.0",
3513 + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.53.0.tgz",
3514 + "integrity": "sha512-QESZHZY3w9LPQ64PGh1gEdfjYjJ5Jp+Dfy0D/CLjsLOPTpXzdxwlNMqRj+vPbTcP0nAHgjWv1maDqcEq6u5olw==",
3515 "dependencies": {
3516 "@yr/monotone-cubic-spline": "^1.0.3",
3517 "svg.draggable.js": "^2.2.2",
@@ -3668,9 +3675,9 @@
3675 "dev": true
3676 },
3677 "node_modules/axios": {
3671 - "version": "1.7.5",
3672 - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
3673 - "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
3678 + "version": "1.7.7",
3679 + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
3680 + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
3681 "dependencies": {
3682 "follow-redirects": "^1.15.6",
3683 "form-data": "^4.0.0",
@@ -4552,9 +4559,9 @@
4559 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4560 },
4561 "node_modules/cypress": {
4555 - "version": "13.13.3",
4556 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.3.tgz",
4557 - "integrity": "sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==",
4562 + "version": "13.14.1",
4563 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.1.tgz",
4564 + "integrity": "sha512-Wo+byPmjps66hACEH5udhXINEiN3qS3jWNGRzJOjrRJF3D0+YrcP2LVB1T7oYaVQM/S+eanqEvBWYc8cf7Vcbg==",
4565 "dev": true,
4566 "hasInstallScript": true,
4567 "dependencies": {
@@ -8797,9 +8804,9 @@
8804 }
8805 },
8806 "node_modules/pinia-plugin-persistedstate": {
8800 - "version": "3.2.1",
8801 - "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.1.tgz",
8802 - "integrity": "sha512-MK++8LRUsGF7r45PjBFES82ISnPzyO6IZx3CH5vyPseFLZCk1g2kgx6l/nW8pEBKxxd4do0P6bJw+mUSZIEZUQ==",
8807 + "version": "3.2.3",
8808 + "resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.3.tgz",
8809 + "integrity": "sha512-Cm819WBj/s5K5DGw55EwbXDtx+EZzM0YR5AZbq9XE3u0xvXwvX2JnWoFpWIcdzISBHqy9H1UiSIUmXyXqWsQRQ==",
8810 "peerDependencies": {
8811 "pinia": "^2.0.0"
8812 }
@@ -8882,9 +8889,9 @@
8889 }
8890 },
8891 "node_modules/postcss": {
8885 - "version": "8.4.41",
8886 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
8887 - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
8892 + "version": "8.4.43",
8893 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.43.tgz",
8894 + "integrity": "sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ==",
8895 "funding": [
8896 {
8897 "type": "opencollective",
@@ -9806,11 +9813,12 @@
9813 }
9814 },
9815 "node_modules/shiki": {
9809 - "version": "1.14.1",
9810 - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.14.1.tgz",
9811 - "integrity": "sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==",
9816 + "version": "1.16.1",
9817 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.16.1.tgz",
9818 + "integrity": "sha512-tCJIMaxDVB1mEIJ5TvfZU7kCPB5eo9fli5+21Olc/bmyv+w8kye3JOp+LZRmGkAyT71hrkefQhTiY+o9mBikRQ==",
9819 "dependencies": {
9813 - "@shikijs/core": "1.14.1",
9820 + "@shikijs/core": "1.16.1",
9821 + "@shikijs/vscode-textmate": "^9.2.0",
9822 "@types/hast": "^3.0.4"
9823 }
9824 },
@@ -10859,9 +10867,9 @@
10867 }
10868 },
10869 "node_modules/type-fest": {
10862 - "version": "4.25.0",
10863 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz",
10864 - "integrity": "sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==",
10870 + "version": "4.26.0",
10871 + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz",
10872 + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==",
10873 "dev": true,
10874 "engines": {
10875 "node": ">=16"
@@ -11893,13 +11901,14 @@
11901 }
11902 },
11903 "node_modules/vue-tsc": {
11896 - "version": "2.0.29",
11897 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz",
11898 - "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==",
11904 + "version": "2.1.5",
11905 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.5.tgz",
11906 + "integrity": "sha512-r4uljp8HQkGXPnNHR0qKkoE/g5OYYycr9Kfblxg0pLaPAxVr7SU/wmYhD7Y/ETApDl4+ZwvgFgn3RTOE7Xgy0Q==",
11907 + "deprecated": "use 2.1.4 instead",
11908 "dev": true,
11909 "dependencies": {
11901 - "@volar/typescript": "~2.4.0-alpha.18",
11902 - "@vue/language-core": "2.0.29",
11910 + "@volar/typescript": "~2.4.1",
11911 + "@vue/language-core": "2.1.5",
11912 "semver": "^7.5.4"
11913 },
11914 "bin": {
@@ -11910,18 +11919,18 @@
11919 }
11920 },
11921 "node_modules/vue3-apexcharts": {
11913 - "version": "1.5.3",
11914 - "resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.5.3.tgz",
11915 - "integrity": "sha512-yaHTPoj0iVKAtEVg8wEwIwwvf0VG+lPYNufCf3txRzYQOqdKPoZaZ9P3Dj3X+2A1XY9O1kcTk9HVqvLo+rppvQ==",
11922 + "version": "1.6.0",
11923 + "resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.6.0.tgz",
11924 + "integrity": "sha512-gemKFXpw4TuVcllwyKJGYjTwiJQxxCUwbXsiiEEZjs0zc9jvOHvreN8frXz7QbnYqMqOHF9D1TBqwENvoPNjLw==",
11925 "peerDependencies": {
11926 "apexcharts": "> 3.0.0",
11927 "vue": "> 3.0.0"
11928 }
11929 },
11930 "node_modules/vue3-marquee": {
11922 - "version": "4.2.1",
11923 - "resolved": "https://registry.npmjs.org/vue3-marquee/-/vue3-marquee-4.2.1.tgz",
11924 - "integrity": "sha512-3nJs2ZNnEre9qZapeRcJ8WUrhE7Z8dm9pH7jR5TYBLmGre46w6vLCht6kcNruOO/c50wuYCx73wQCaXJ0Y/Kvg==",
11931 + "version": "4.2.2",
11932 + "resolved": "https://registry.npmjs.org/vue3-marquee/-/vue3-marquee-4.2.2.tgz",
11933 + "integrity": "sha512-FeFvGUVInKfFilXFcnl8sDRBJBZCZSNLlQDquJErB9db6W2xICRVqbRV/jtdzsEP0rftarLQhx9MeEAU0+TPuQ==",
11934 "engines": {
11935 "node": ">=12"
11936 },
frontend/package.json
+14 -14
@@ -36,11 +36,11 @@
36 "@fontsource/lexend": "^5.0.21",
37 "@fontsource/public-sans": "^5.0.18",
38 "@popperjs/core": "^2.11.8",
39 - "@shikijs/markdown-it": "^1.14.1",
39 + "@shikijs/markdown-it": "^1.16.1",
40 "@vueuse/components": "^11.0.3",
41 "@vueuse/core": "^11.0.3",
42 - "apexcharts": "^3.52.0",
43 - "axios": "^1.7.5",
42 + "apexcharts": "^3.53.0",
43 + "axios": "^1.7.7",
44 "bytes": "^3.1.2",
45 "colord": "^2.9.3",
46 "crypto-js": "^4.2.0",
@@ -56,9 +56,9 @@
56 "naive-ui": "^2.39.0",
57 "password-validator": "^5.3.0",
58 "pinia": "^2.2.2",
59 - "pinia-plugin-persistedstate": "^3.2.1",
59 + "pinia-plugin-persistedstate": "^3.2.3",
60 "secure-ls": "^2.0.0",
61 - "shiki": "^1.14.1",
61 + "shiki": "^1.16.1",
62 "validator": "^13.12.0",
63 "vue": "^3.4.38",
64 "vue-advanced-cropper": "^2.8.9",
@@ -66,8 +66,8 @@
66 "vue-i18n": "^9.14.0",
67 "vue-router": "^4.4.3",
68 "vue-sjv": "^0.0.6",
69 - "vue3-apexcharts": "^1.5.3",
70 - "vue3-marquee": "^4.2.1",
69 + "vue3-apexcharts": "^1.6.0",
70 + "vue3-marquee": "^4.2.2",
71 "vuedraggable": "^4.1.0"
72 },
73 "devDependencies": {
@@ -80,16 +80,16 @@
80 "@types/inquirer": "^9.0.7",
81 "@types/jsdom": "^21.1.7",
82 "@types/lodash": "^4.17.7",
83 - "@types/node": "^22.5.0",
83 + "@types/node": "^22.5.2",
84 "@types/validator": "^13.12.1",
85 - "@vitejs/plugin-vue": "^5.1.2",
85 + "@vitejs/plugin-vue": "^5.1.3",
86 "@vitejs/plugin-vue-jsx": "^4.0.1",
87 "@vue/eslint-config-prettier": "^9.0.0",
88 "@vue/eslint-config-typescript": "^13.0.0",
89 "@vue/test-utils": "^2.4.6",
90 "@vue/tsconfig": "^0.5.1",
91 "autoprefixer": "^10.4.20",
92 - "cypress": "^13.13.3",
92 + "cypress": "^13.14.1",
93 "eslint": "^9.9.1",
94 "eslint-plugin-cypress": "^3.5.0",
95 "eslint-plugin-vue": "^9.27.0",
@@ -100,14 +100,14 @@
100 "json5": "^2.2.3",
101 "npm-run-all2": "^6.2.2",
102 "picocolors": "^1.0.1",
103 - "postcss": "^8.4.41",
103 + "postcss": "^8.4.43",
104 "prettier": "^3.3.3",
105 "sass": "^1.77.8",
106 "start-server-and-test": "^2.0.5",
107 "tailwind-config-viewer": "^2.0.4",
108 "tailwindcss": "^3.4.10",
109 "taze": "^0.16.7",
110 - "type-fest": "^4.25.0",
110 + "type-fest": "^4.26.0",
111 "unplugin-vue-components": "^0.27.4",
112 "vite": "^5.4.2",
113 "vite-bundle-analyzer": "^0.10.6",
@@ -115,10 +115,10 @@
115 "vite-plugin-vue-devtools": "^7.3.9",
116 "vite-svg-loader": "^5.1.0",
117 "vitest": "^2.0.5",
118 - "vue-tsc": "^2.0.29"
118 + "vue-tsc": "^2.1.5"
119 },
120 "optionalDependencies": {
121 - "@rollup/rollup-linux-x64-gnu": "^4.21.1"
121 + "@rollup/rollup-linux-x64-gnu": "^4.21.2"
122 },
123 "overrides": {
124 "@vue/eslint-config-typescript": {
frontend/src/api/endpoints/incidentManagement.ts
+11 -5
@@ -23,10 +23,11 @@ export type AlertsFilter =
23 export interface AlertsQuery {
24 page: number
25 pageSize: number
26 + sort: "asc" | "desc"
27 filters: AlertsFilter
28 }
29
29 -export type CasesFilter = { status: AlertStatus } | { assignedTo: string }
30 +export type CasesFilter = { status: AlertStatus } | { assignedTo: string } | { hostname: string }
31
32 export type AlertCommentPayload = Omit<AlertComment, "id">
33
@@ -80,7 +81,7 @@ export default {
81 // #endregion
82
83 // #region Alerts
83 - getAlertsList(args?: Partial<AlertsQuery>) {
84 + getAlertsList(args: Partial<AlertsQuery>, signal?: AbortSignal) {
85 let url = `/incidents/db_operations/alerts`
86
87 if (args?.filters && "status" in args.filters) {
@@ -103,9 +104,11 @@ export default {
104 FlaskBaseResponse & { alerts: Alert[]; closed: number; in_progress: number; open: number; total: number }
105 >(url, {
106 params: {
106 - page: args?.page || 1,
107 - page_size: args?.pageSize || 25
108 - }
107 + page: args.page || 1,
108 + page_size: args.pageSize || 25,
109 + order: args.sort || "desc"
110 + },
111 + signal
112 })
113 },
114 getAlert(alertId: number) {
@@ -180,6 +183,9 @@ export default {
183 if (filters && "assignedTo" in filters) {
184 url = `/incidents/db_operations/case/assigned-to/${filters.assignedTo}`
185 }
186 + if (filters && "hostname" in filters) {
187 + url = `/agents/${filters.hostname}/cases`
188 + }
189
190 return HttpClient.get<FlaskBaseResponse & { cases: Case[] }>(url)
191 },
frontend/src/api/endpoints/sigma.ts new
+68
@@ -0,0 +1,68 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "../httpClient"
3 +import type { SigmaQuery, SigmaRuleLevels, SigmaTimeInterval } from "@/types/sigma.d"
4 +
5 +export default {
6 + /** client-side pagination (1k+ items) */
7 + getAvailable() {
8 + return HttpClient.get<FlaskBaseResponse & { sigma_queries: SigmaQuery[] }>(`/sigma/queries/available`)
9 + },
10 + /** client-side pagination (1k+ items) */
11 + getActive() {
12 + return HttpClient.get<FlaskBaseResponse & { sigma_queries: SigmaQuery[] }>(`/sigma/queries/active`)
13 + },
14 + /** client-side pagination (1k+ items) */
15 + getInactive() {
16 + return HttpClient.get<FlaskBaseResponse & { sigma_queries: SigmaQuery[] }>(`/sigma/queries/inactive`)
17 + },
18 + downloadRules() {
19 + return HttpClient.post<FlaskBaseResponse>(`/sigma/download`, {
20 + url: "https://github.com/SigmaHQ/sigma/releases/download/r2024-07-17/sigma_all_rules.zip",
21 + folder: "windows"
22 + })
23 + },
24 + /** It may take several minutes */
25 + uploadRules(ruleLevels: SigmaRuleLevels[]) {
26 + return HttpClient.post<FlaskBaseResponse>(`/sigma/bulk-upload-to-db`, {
27 + rule_levels: ruleLevels
28 + })
29 + },
30 + uploadRulesFile(file: File) {
31 + const form = new FormData()
32 + form.append("file", new Blob([file], { type: file.type }), file.name)
33 +
34 + return HttpClient.post<FlaskBaseResponse>(`/sigma/upload`, form)
35 + },
36 + /** It may take several minutes */
37 + activateAllQueries() {
38 + return HttpClient.post<FlaskBaseResponse & { enabled_queries: string[] }>(`/sigma/activate-all-queries`)
39 + },
40 + /** It may take several minutes */
41 + deactivateAllQueries() {
42 + return HttpClient.post<FlaskBaseResponse & { disabled_queries: string[] }>(`/sigma/deactivate-all-queries`)
43 + },
44 + /** returns the updated query */
45 + setQueryActive(ruleName: string, active: boolean) {
46 + return HttpClient.put<FlaskBaseResponse & { sigma_queries: SigmaQuery[] }>(`/sigma/queries/set-active`, {
47 + rule_name: ruleName,
48 + active
49 + })
50 + },
51 + /** returns the updated query */
52 + setQueryTimeInterval(ruleName: string, timeInterval: SigmaTimeInterval) {
53 + return HttpClient.put<FlaskBaseResponse & { sigma_queries: SigmaQuery[] }>(`/sigma/queries/set-time-interval`, {
54 + rule_name: ruleName,
55 + time_interval: timeInterval
56 + })
57 + },
58 + deleteRule(ruleName: string) {
59 + return HttpClient.delete<FlaskBaseResponse & { deleted_queries: string[] }>(`/sigma/queries/delete`, {
60 + params: {
61 + rule_name: ruleName
62 + }
63 + })
64 + },
65 + deleteAllRules() {
66 + return HttpClient.delete<FlaskBaseResponse & { deleted_queries: string[] }>(`/sigma/queries/delete-all`)
67 + }
68 +}
frontend/src/api/index.ts
+3 -1
@@ -23,6 +23,7 @@ import networkConnectors from "./endpoints/networkConnectors"
23 import cloudSecurityAssessment from "./endpoints/cloudSecurityAssessment"
24 import webVulnerabilityAssessment from "./endpoints/webVulnerabilityAssessment"
25 import incidentManagement from "./endpoints/incidentManagement"
26 +import sigma from "./endpoints/sigma"
27
28 export default {
29 agents,
@@ -49,5 +50,6 @@ export default {
50 networkConnectors,
51 cloudSecurityAssessment,
52 webVulnerabilityAssessment,
52 - incidentManagement
53 + incidentManagement,
54 + sigma
55 }
frontend/src/components/alerts/Alert.vue
+7 -20
@@ -177,14 +177,7 @@
177 </n-tab-pane>
178 <n-tab-pane name="Message" tab="Message" v-if="alert._source.message" display-directive="show">
179 <div class="p-7 pt-4">
180 - <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
181 - <div
182 - class="scrollbar-styled overflow-hidden code-bg-transparent"
183 - v-shiki="{ decode: false }"
184 - >
185 - <pre>{{ alert._source.message }}</pre>
186 - </div>
187 - </n-card>
180 + <CodeSource :code="alert._source.message" :decode="false" />
181 </div>
182 </n-tab-pane>
183 <n-tab-pane
@@ -208,14 +201,7 @@
201 </n-tab-pane>
202 <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
203 <div class="p-7 pt-4">
211 - <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
212 - <div
213 - class="scrollbar-styled overflow-hidden code-bg-transparent"
214 - v-shiki="{ lang: 'json', decode: false }"
215 - >
216 - <pre>{{ alert._source }}</pre>
217 - </div>
218 - </n-card>
204 + <CodeSource :code="alert._source" lang="json" :decode="false" />
205 </div>
206 </n-tab-pane>
207 </n-tabs>
@@ -225,19 +211,20 @@
211
212 <script setup lang="ts">
213 import { computed, defineAsyncComponent, inject, ref, toRefs } from "vue"
228 -import { NPopover, NModal, NTabs, NTabPane, NInput, NCard } from "naive-ui"
214 +import { NPopover, NModal, NTabs, NTabPane, NInput } from "naive-ui"
215 import { useSettingsStore } from "@/stores/settings"
216 import { formatDate } from "@/utils"
217 import Icon from "@/components/common/Icon.vue"
218 import Badge from "@/components/common/Badge.vue"
233 -const AlertActions = defineAsyncComponent(() => import("./AlertActions.vue"))
234 -import type { Alert } from "@/types/alerts.d"
235 -import vShiki from "@/directives/v-shiki"
219 import _pick from "lodash/pick"
220 import KVCard from "@/components/common/KVCard.vue"
221 import { useGoto } from "@/composables/useGoto"
222 +import type { Alert } from "@/types/alerts.d"
223 import type { SocAlertField } from "./type.d"
224
225 +const AlertActions = defineAsyncComponent(() => import("./AlertActions.vue"))
226 +const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
227 +
228 const props = defineProps<{ alert: Alert; hideActions?: boolean }>()
229 const { alert, hideActions } = toRefs(props)
230
frontend/src/components/alerts/AlertWazuhRules.vue
+5 -4
@@ -1,8 +1,8 @@
1 <template>
2 <n-tabs type="line" animated :tabs-padding="24">
3 <n-tab-pane name="Wazuh Rules" tab="Wazuh Rules" display-directive="show">
4 - <div class="p-7 pt-4 scrollbar-styled code-bg-transparent" v-shiki="{ lang: 'xml' }">
5 - <pre v-html="wazuh_rule"></pre>
4 + <div class="p-7 pt-4">
5 + <CodeSource :code="wazuh_rule" lang="xml" />
6 </div>
7 </n-tab-pane>
8 <n-tab-pane name="Explanation" tab="Explanation" display-directive="show">
@@ -23,11 +23,12 @@
23 </template>
24
25 <script setup lang="ts">
26 -import vShiki from "@/directives/v-shiki"
27 -import { computed, toRefs } from "vue"
26 +import { computed, defineAsyncComponent, toRefs } from "vue"
27 import { NTabs, NTabPane, NInput } from "naive-ui"
28 import type { WazuhRuleExclude } from "@/types/alerts.d"
29
30 +const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
31 +
32 const props = defineProps<{ data: WazuhRuleExclude }>()
33 const { data } = toRefs(props)
34
frontend/src/components/common/Badge.vue
+1 -1
@@ -3,7 +3,7 @@
3 :is="!!href ? 'a' : 'div'"
4 class="badge"
5 :href="href"
6 - :class="[type, color, { 'cursor-help': hintCursor, 'cursor-pointer': pointCursor, fluid, bright }]"
6 + :class="[type, color, { 'cursor-help': hintCursor, cursor: pointCursor, fluid, bright }]"
7 >
8 <span v-if="$slots.label || $slots.iconLeft || $slots.iconRight" class="flex items-center gap-2">
9 <slot name="iconLeft"></slot>
frontend/src/components/common/CodeSource.vue new
+41
@@ -0,0 +1,41 @@
1 +<template>
2 + <div class="flex flex-col gap-2">
3 + <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden" v-if="!showSource">
4 + <div class="scrollbar-styled overflow-hidden code-bg-transparent" v-shiki="{ lang, decode }">
5 + <pre v-html="source"></pre>
6 + </div>
7 + </n-card>
8 +
9 + <div class="flex justify-end">
10 + <n-button quaternary size="tiny" @click="showSource = !showSource">toggle source view</n-button>
11 + </div>
12 +
13 + <n-input
14 + v-if="showSource"
15 + :value="source"
16 + type="textarea"
17 + readonly
18 + placeholder="Empty"
19 + size="large"
20 + :autosize="{
21 + minRows: 3,
22 + maxRows: 18
23 + }"
24 + />
25 + </div>
26 +</template>
27 +
28 +<script setup lang="ts">
29 +import { computed, ref } from "vue"
30 +import { NCard, NInput, NButton } from "naive-ui"
31 +import vShiki from "@/directives/v-shiki"
32 +
33 +const { code, lang } = defineProps<{
34 + code: string | object | number
35 + lang?: string
36 + decode?: boolean
37 +}>()
38 +
39 +const showSource = ref(false)
40 +const source = computed(() => JSON.stringify(code, null, "\t"))
41 +</script>
frontend/src/components/common/ImageCropper.vue
+1 -7
@@ -6,13 +6,7 @@
6 <n-card class="image-cropper-modal flex flex-col" content-class="!p-5">
7 <div class="aspect-square">
8 <div class="upload-box" v-if="!img">
9 - <n-upload
10 - accept="image/*"
11 - :show-file-list="false"
12 - @change="setImage"
13 - v-show="!img"
14 - ref="uploader"
15 - >
9 + <n-upload accept="image/*" :show-file-list="false" @change="setImage" v-show="!img">
10 <n-upload-dragger>
11 <div>{{ placeholder }}</div>
12 </n-upload-dragger>
frontend/src/components/common/PaginationIndeterminate.vue
+1 -2
@@ -37,10 +37,9 @@
37 </template>
38
39 <script setup lang="ts">
40 -import { computed, toRefs } from "vue"
40 +import { computed, toRefs, watch } from "vue"
41 import { NSelect, NInputNumber } from "naive-ui"
42 import Icon from "@/components/common/Icon.vue"
43 -import { watch } from "vue"
43
44 const page = defineModel<number>("page", { default: 1 })
45 const pageSize = defineModel<number>("pageSize", { default: 10 })
frontend/src/components/connectors/ConfigForm/FormTypes/FileType.vue
+1 -4
@@ -7,8 +7,7 @@
7 :show-file-list="true"
8 @change="handleChange"
9 @remove="handleChange"
10 - ref="uploadRef"
11 - accept=".yaml, .YAML"
10 + accept=".yaml, .YAML, .yml, .YML"
11 >
12 <n-upload-dragger>
13 <div>
@@ -32,7 +31,6 @@ import {
31 type FormRules,
32 type FormInst,
33 type FormItemRule,
35 - type UploadInst,
34 type UploadFileInfo
35 } from "naive-ui"
36 import Icon from "@/components/common/Icon.vue"
@@ -53,7 +51,6 @@ const props = defineProps<{
51 const { form } = toRefs(props)
52
53 const formRef = ref<FormInst>()
56 -const uploadRef = ref<UploadInst>()
54
55 const handleChange = ({ file }: { file: UploadFileInfo }) => {
56 if (file.status === "removed") {
frontend/src/components/customers/healthcheck/CustomerHealthcheckList.vue
+4 -4
@@ -53,14 +53,14 @@
53 </template>
54
55 <script setup lang="ts">
56 -import Icon from "@/components/common/Icon.vue"
56 import { onBeforeMount, ref, watch } from "vue"
57 +import { useMessage, NSpin, NEmpty, NSelect, NInputGroup, NInputNumber } from "naive-ui"
58 +import CustomerHealthcheckItem from "./CustomerHealthcheckItem.vue"
59 +import Icon from "@/components/common/Icon.vue"
60 import _get from "lodash/get"
61 import Api from "@/api"
60 -import CustomerHealthcheckItem from "./CustomerHealthcheckItem.vue"
61 -import { useMessage, NSpin, NEmpty, NSelect, NInputGroup, NInputNumber } from "naive-ui"
62 -import type { CustomerAgentHealth, CustomerHealthcheckSource } from "@/types/customers.d"
62 import { watchDebounced } from "@vueuse/core"
63 +import type { CustomerAgentHealth, CustomerHealthcheckSource } from "@/types/customers.d"
64 import type { CustomerAgentsHealthcheckQuery } from "@/api/endpoints/customers"
65
66 const { source, customerCode } = defineProps<{
frontend/src/components/incidentManagement/alerts/AlertAsset.vue
+2 -9
@@ -78,14 +78,7 @@
78 </Badge>
79 </div>
80
81 - <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
82 - <div
83 - class="scrollbar-styled overflow-hidden code-bg-transparent"
84 - v-shiki="{ lang: 'json', decode: false }"
85 - >
86 - <pre>{{ alertContext.context }}</pre>
87 - </div>
88 - </n-card>
81 + <CodeSource :code="alertContext.context" lang="json" />
82 </div>
83 </n-spin>
84 </n-tab-pane>
@@ -142,7 +135,6 @@ import { NModal, NSpin, NCard, NTabs, NTabPane, useMessage } from "naive-ui"
135 import Api from "@/api"
136 import Icon from "@/components/common/Icon.vue"
137 import Badge from "@/components/common/Badge.vue"
145 -import vShiki from "@/directives/v-shiki"
138 import { useGoto } from "@/composables/useGoto"
139 import _truncate from "lodash/truncate"
140 import type { AlertAsset, AlertContext } from "@/types/incidentManagement/alerts.d"
@@ -154,6 +146,7 @@ const ThreatIntelProcessEvaluationProvider = defineAsyncComponent(
146 () => import("@/components/threatIntel/ThreatIntelProcessEvaluationProvider.vue")
147 )
148 const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
149 +const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
150
151 const props = defineProps<{ asset: AlertAsset; embedded?: boolean }>()
152 const { asset, embedded } = toRefs(props)
frontend/src/components/incidentManagement/alerts/AlertAssetInfo.vue
+5 -11
@@ -68,14 +68,7 @@
68 </n-tab-pane>
69 <n-tab-pane name="Source" tab="Source" display-directive="show">
70 <div class="p-7 pt-4" v-if="alertDetailsSource">
71 - <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
72 - <div
73 - class="scrollbar-styled overflow-hidden code-bg-transparent"
74 - v-shiki="{ lang: 'json', decode: false }"
75 - >
76 - <pre>{{ alertDetailsSource }}</pre>
77 - </div>
78 - </n-card>
71 + <CodeSource :code="alertDetailsSource" lang="json" />
72 </div>
73 </n-tab-pane>
74 </n-tabs>
@@ -84,16 +77,17 @@
77 </template>
78
79 <script setup lang="ts">
87 -import { computed, ref, toRefs, watch } from "vue"
88 -import { NModal, NSpin, NCard, NTabs, NTabPane, useMessage } from "naive-ui"
80 +import { computed, defineAsyncComponent, ref, toRefs, watch } from "vue"
81 +import { NModal, NSpin, NTabs, NTabPane, useMessage } from "naive-ui"
82 import Api from "@/api"
90 -import vShiki from "@/directives/v-shiki"
83 import KVCard from "@/components/common/KVCard.vue"
84 import Icon from "@/components/common/Icon.vue"
85 import { useGoto } from "@/composables/useGoto"
86 import _omit from "lodash/omit"
87 import type { AlertAsset, AlertDetails } from "@/types/incidentManagement/alerts.d"
88
89 +const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
90 +
91 const props = defineProps<{ asset: AlertAsset }>()
92 const { asset } = toRefs(props)
93
frontend/src/components/incidentManagement/alerts/AlertDetailTimelineItem.vue
+5 -11
@@ -41,14 +41,7 @@
41 </n-tab-pane>
42 <n-tab-pane name="Source" tab="Source" display-directive="show">
43 <div class="p-7 pt-4" v-if="timelineDetailsSource">
44 - <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
45 - <div
46 - class="scrollbar-styled overflow-hidden code-bg-transparent"
47 - v-shiki="{ lang: 'json', decode: false }"
48 - >
49 - <pre>{{ timelineDetailsSource }}</pre>
50 - </div>
51 - </n-card>
44 + <CodeSource :code="timelineDetailsSource" lang="json" />
45 </div>
46 </n-tab-pane>
47 </n-tabs>
@@ -57,15 +50,16 @@
50 </template>
51
52 <script setup lang="ts">
60 -import { computed, ref, toRefs } from "vue"
61 -import { NModal, NCard, NTabs, NTabPane } from "naive-ui"
62 -import vShiki from "@/directives/v-shiki"
53 +import { computed, defineAsyncComponent, ref, toRefs } from "vue"
54 +import { NModal, NTabs, NTabPane } from "naive-ui"
55 import KVCard from "@/components/common/KVCard.vue"
56 import Icon from "@/components/common/Icon.vue"
57 import { useGoto } from "@/composables/useGoto"
58 import _omit from "lodash/omit"
59 import type { AlertTimeline } from "@/types/incidentManagement/alerts.d"
60
61 +const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
62 +
63 const props = defineProps<{ timelineData: AlertTimeline; embedded?: boolean }>()
64 const { timelineData, embedded } = toRefs(props)
65
frontend/src/components/incidentManagement/alerts/AlertsList.vue
+42 -16
@@ -54,7 +54,15 @@
54 :item-count="total"
55 :simple="simpleMode"
56 />
57 - <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
57 + <n-select
58 + size="small"
59 + v-model:value="sort"
60 + :options="sortOptions"
61 + :show-checkmark="false"
62 + class="max-w-20"
63 + :disabled="loading"
64 + />
65 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0" v-if="!hideFilters">
66 <template #trigger>
67 <div class="bg-color border-radius">
68 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
@@ -166,22 +174,23 @@ import {
174 NInput
175 } from "naive-ui"
176 import Api from "@/api"
177 +import AlertItem from "./AlertItem.vue"
178 +import Icon from "@/components/common/Icon.vue"
179 import _cloneDeep from "lodash/cloneDeep"
180 import _orderBy from "lodash/orderBy"
171 -import Icon from "@/components/common/Icon.vue"
181 +import axios from "axios"
182 import { useResizeObserver } from "@vueuse/core"
183 import type { Alert, AlertStatus } from "@/types/incidentManagement/alerts.d"
184 import type { AlertsQuery } from "@/api/endpoints/incidentManagement"
175 -import AlertItem from "./AlertItem.vue"
185 import type { Case } from "@/types/incidentManagement/cases.d"
186
187 export interface AlertsListFilter {
179 - type: "status" | "assetName" | "assignedTo"
188 + type: "status" | "assetName" | "assignedTo" | "tag" | "title"
189 value: string | AlertStatus
190 }
191
183 -const props = defineProps<{ highlight: string | null | undefined }>()
184 -const { highlight } = toRefs(props)
192 +const props = defineProps<{ highlight?: string | null; preset?: AlertsListFilter; hideFilters?: boolean }>()
193 +const { highlight, preset, hideFilters } = toRefs(props)
194
195 const FilterIcon = "carbon:filter-edit"
196 const InfoIcon = "carbon:information"
@@ -192,6 +201,7 @@ const showFilters = ref(false)
201 const alertsList = ref<Alert[]>([])
202 const availableUsers = ref<string[]>([])
203 const linkableCases = ref<Case[]>([])
204 +let abortController: AbortController | null = null
205
206 const pageSize = ref(25)
207 const currentPage = ref(1)
@@ -200,6 +210,11 @@ const showSizePicker = ref(true)
210 const pageSizes = [10, 25, 50, 100]
211 const header = ref()
212 const pageSlot = ref(8)
213 +const sort = defineModel<"asc" | "desc">("sort", { default: "desc" })
214 +const sortOptions = [
215 + { label: "Desc", value: "desc" },
216 + { label: "Asc", value: "asc" }
217 +]
218
219 const total = ref(0)
220 const statusOpenTotal = ref(0)
@@ -241,11 +256,13 @@ watch(showFilters, val => {
256 watch(
257 () => filters.value.type,
258 () => {
244 - filters.value.value = undefined
259 + if (!preset.value) {
260 + filters.value.value = undefined
261 + }
262 }
263 )
264
248 -watch(currentPage, () => {
265 +watch([currentPage, sort], () => {
266 getData()
267 })
268
@@ -297,14 +314,17 @@ function updateAlert(updatedAlert: Alert) {
314 }
315
316 function getData() {
317 + abortController?.abort()
318 + abortController = new AbortController()
319 +
320 showFilters.value = false
321 loading.value = true
302 -
322 lastFilters.value = _cloneDeep(filters.value)
323
324 const query: Partial<AlertsQuery> = {
325 page: currentPage.value,
307 - pageSize: pageSize.value
326 + pageSize: pageSize.value,
327 + sort: sort.value
328 }
329
330 if (filtered.value) {
@@ -313,7 +333,7 @@ function getData() {
333 }
334
335 Api.incidentManagement
316 - .getAlertsList(query)
336 + .getAlertsList(query, abortController.signal)
337 .then(res => {
338 if (res.data.success) {
339 alertsList.value = res.data?.alerts || []
@@ -324,14 +344,15 @@ function getData() {
344 } else {
345 message.warning(res.data?.message || "An error occurred. Please try again later.")
346 }
347 + loading.value = false
348 })
349 .catch(err => {
329 - alertsList.value = []
350 + if (!axios.isCancel(err)) {
351 + alertsList.value = []
352
331 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
332 - })
333 - .finally(() => {
334 - loading.value = false
353 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
354 + loading.value = false
355 + }
356 })
357 }
358
@@ -374,6 +395,11 @@ useResizeObserver(header, entries => {
395 })
396
397 onBeforeMount(() => {
398 + if (preset.value?.type && preset.value.value) {
399 + filters.value.type = preset.value.type
400 + filters.value.value = preset.value.value
401 + }
402 +
403 getData()
404 getAvailableUsers()
405 getCases()
frontend/src/components/incidentManagement/cases/CaseItem.vue
+1 -1
@@ -149,7 +149,7 @@
149 <n-button quaternary size="tiny" @click="handleDelete()">Delete Case</n-button>
150 </div>
151 </template>
152 - <div>
152 + <div class="flex flex-col gap-2">
153 <template v-if="caseEntity.alerts.length">
154 <AlertItem
155 v-for="alert of caseEntity.alerts"
frontend/src/components/incidentManagement/cases/CasesList.vue
+14 -6
@@ -61,7 +61,7 @@
61 :item-count="total"
62 :simple="simpleMode"
63 />
64 - <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
64 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0" v-if="!hideFilters">
65 <template #trigger>
66 <div class="bg-color border-radius">
67 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
@@ -183,12 +183,12 @@ import type { CasesFilter } from "@/api/endpoints/incidentManagement"
183 import CaseItem from "./CaseItem.vue"
184
185 export interface CasesListFilter {
186 - type: "status" | "assignedTo"
186 + type: "status" | "assignedTo" | "hostname"
187 value: string | AlertStatus
188 }
189
190 -const props = defineProps<{ highlight: string | null | undefined }>()
191 -const { highlight } = toRefs(props)
190 +const props = defineProps<{ highlight?: string | null; preset?: CasesListFilter; hideFilters?: boolean }>()
191 +const { highlight, preset, hideFilters } = toRefs(props)
192
193 const message = useMessage()
194 const loading = ref(false)
@@ -241,7 +241,8 @@ const filtered = computed<boolean>(() => {
241
242 const typeOptions = [
243 { label: "Status", value: "status" },
244 - { label: "Assigned To", value: "assignedTo" }
244 + { label: "Assigned To", value: "assignedTo" },
245 + { label: "Hostname", value: "hostname" }
246 ]
247
248 const statusOptions: { label: string; value: AlertStatus }[] = [
@@ -270,7 +271,9 @@ watch(showFilters, val => {
271 watch(
272 () => filters.value.type,
273 () => {
273 - filters.value.value = undefined
274 + if (!preset.value) {
275 + filters.value.value = undefined
276 + }
277 }
278 )
279
@@ -367,6 +370,11 @@ useResizeObserver(header, entries => {
370 })
371
372 onBeforeMount(() => {
373 + if (preset.value?.type && preset.value.value) {
374 + filters.value.type = preset.value.type
375 + filters.value.value = preset.value.value
376 + }
377 +
378 getData()
379 getAvailableUsers()
380 })
frontend/src/components/sigma/QueriesActions.vue new
+72
@@ -0,0 +1,72 @@
1 +<template>
2 + <div class="sigma-queries-actions flex flex-wrap gap-2">
3 + <QueryActiveAllForm v-slot="{ loading, togglePopup }" @updated="emitUpdate()">
4 + <n-button size="small" type="primary" secondary strong :loading @click.stop="togglePopup()">
5 + <template #icon>
6 + <Icon :name="EditIcon" />
7 + </template>
8 + Update queries status
9 + </n-button>
10 + </QueryActiveAllForm>
11 +
12 + <QueryUploadFile v-slot="{ loading, togglePopup }" @updated="emitUpdate()">
13 + <n-button size="small" type="success" secondary strong :loading @click.stop="togglePopup()">
14 + <template #icon>
15 + <Icon :name="UploadFileIcon" />
16 + </template>
17 + Upload queries from FIle
18 + </n-button>
19 + </QueryUploadFile>
20 +
21 + <QueryUploadDB v-slot="{ loading, togglePopup }" @updated="emitUpdate()">
22 + <n-button size="small" type="success" secondary strong :loading @click.stop="togglePopup()">
23 + <template #icon>
24 + <Icon :name="UploadIcon" />
25 + </template>
26 + Upload queries to DB
27 + </n-button>
28 + </QueryUploadDB>
29 +
30 + <QueryDownloadAll v-slot="{ loading, togglePopup }" @updated="emitUpdate()">
31 + <n-button size="small" type="success" secondary strong :loading @click.stop="togglePopup()">
32 + <template #icon>
33 + <Icon :name="DownloadIcon" />
34 + </template>
35 + Download queries
36 + </n-button>
37 + </QueryDownloadAll>
38 +
39 + <QueryDeleteAll v-slot="{ loading, togglePopup }" @updated="emitUpdate()">
40 + <n-button size="small" type="error" secondary strong :loading @click.stop="togglePopup()">
41 + <template #icon>
42 + <Icon :name="DeleteIcon" />
43 + </template>
44 + Delete all queries
45 + </n-button>
46 + </QueryDeleteAll>
47 + </div>
48 +</template>
49 +
50 +<script setup lang="ts">
51 +import { NButton } from "naive-ui"
52 +import QueryActiveAllForm from "./actionsProviders/QueryActiveAllForm.vue"
53 +import QueryDeleteAll from "./actionsProviders/QueryDeleteAll.vue"
54 +import QueryDownloadAll from "./actionsProviders/QueryDownloadAll.vue"
55 +import QueryUploadDB from "./actionsProviders/QueryUploadDB.vue"
56 +import QueryUploadFile from "./actionsProviders/QueryUploadFile.vue"
57 +import Icon from "@/components/common/Icon.vue"
58 +
59 +const emit = defineEmits<{
60 + (e: "updated"): void
61 +}>()
62 +
63 +const EditIcon = "uil:edit-alt"
64 +const DeleteIcon = "ph:trash"
65 +const DownloadIcon = "carbon:cloud-download"
66 +const UploadIcon = "carbon:cloud-upload"
67 +const UploadFileIcon = "carbon:upload"
68 +
69 +function emitUpdate() {
70 + emit("updated")
71 +}
72 +</script>
frontend/src/components/sigma/QueriesList.vue new
+278
@@ -0,0 +1,278 @@
1 +<template>
2 + <div class="sigma-queries-list">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <div class="info grow flex gap-2">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total :
18 + <code>{{ total }}</code>
19 + </div>
20 + <div class="box">
21 + <span class="text-success-color">Active</span>
22 + :
23 + <code>{{ activeTotal }}</code>
24 + </div>
25 + <div class="box">
26 + <span class="text-secondary-color">Inactive</span>
27 + :
28 + <code>{{ inactiveTotal }}</code>
29 + </div>
30 + </div>
31 + </n-popover>
32 +
33 + <n-button size="small" type="primary" secondary strong @click="showActionsView = !showActionsView">
34 + <div class="flex items-center gap-2">
35 + <Icon :name="ToolsIcon" :size="16"></Icon>
36 + <span class="hidden xs:block">Actions</span>
37 + </div>
38 + </n-button>
39 + </div>
40 + <n-pagination
41 + v-model:page="currentPage"
42 + v-model:page-size="pageSize"
43 + :page-slot="pageSlot"
44 + :show-size-picker="showSizePicker"
45 + :page-sizes="pageSizes"
46 + :item-count="total"
47 + :simple="simpleMode"
48 + />
49 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
50 + <template #trigger>
51 + <div class="bg-color border-radius">
52 + <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
53 + <n-button size="small" @click="showFilters = true">
54 + <template #icon>
55 + <Icon :name="FilterIcon"></Icon>
56 + </template>
57 + </n-button>
58 + </n-badge>
59 + </div>
60 + </template>
61 + <div class="py-1 flex flex-col gap-2">
62 + <div class="px-3">
63 + <small>Status:</small>
64 + </div>
65 + <div class="px-3">
66 + <n-select
67 + v-model:value="filters.active"
68 + :options="activeOptions"
69 + placeholder="Active Status"
70 + clearable
71 + class="!w-56"
72 + />
73 + </div>
74 + <div class="px-3 flex justify-between gap-2">
75 + <div class="flex justify-start gap-2">
76 + <n-button size="small" @click="showFilters = false" quaternary>Close</n-button>
77 + </div>
78 + <div class="flex justify-end gap-2">
79 + <n-button size="small" @click="resetFilters()" secondary>Reset</n-button>
80 + <n-button size="small" @click="getData()" type="primary" secondary :loading>
81 + Submit
82 + </n-button>
83 + </div>
84 + </div>
85 + </div>
86 + </n-popover>
87 + </div>
88 +
89 + <div class="actions-box" :class="{ open: showActionsView }">
90 + <n-card size="small" content-class="bg-secondary-color" class="overflow-hidden" :bordered="false">
91 + <QueriesActions @updated="getData()" />
92 + </n-card>
93 + </div>
94 +
95 + <n-spin :show="loading">
96 + <div class="list flex flex-col gap-2 my-3">
97 + <template v-if="queriesList.length">
98 + <QueryItem
99 + v-for="query of itemsPaginated"
100 + :key="query.id"
101 + :query="query"
102 + @deleted="deleteQueryItem"
103 + @updated="updateQueryItem"
104 + class="item-appear item-appear-bottom item-appear-005"
105 + />
106 + </template>
107 + <template v-else>
108 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
109 + </template>
110 + </div>
111 + </n-spin>
112 +
113 + <div class="footer flex justify-end">
114 + <n-pagination
115 + v-model:page="currentPage"
116 + :page-size="pageSize"
117 + :item-count="total"
118 + :page-slot="6"
119 + v-if="itemsPaginated.length > 3"
120 + />
121 + </div>
122 + </div>
123 +</template>
124 +
125 +<script setup lang="ts">
126 +import { ref, onBeforeMount, computed, watch } from "vue"
127 +import { NCard, NSpin, NPopover, NButton, NEmpty, NSelect, NPagination, NBadge, useMessage } from "naive-ui"
128 +import Api from "@/api"
129 +import _cloneDeep from "lodash/cloneDeep"
130 +import _orderBy from "lodash/orderBy"
131 +import Icon from "@/components/common/Icon.vue"
132 +import { useResizeObserver, useStorage } from "@vueuse/core"
133 +import QueryItem from "./QueryItem.vue"
134 +import QueriesActions from "./QueriesActions.vue"
135 +import type { SigmaQuery } from "@/types/sigma.d"
136 +
137 +interface QueriesFilter {
138 + active: "active" | "inactive"
139 +}
140 +
141 +const FilterIcon = "carbon:filter-edit"
142 +const InfoIcon = "carbon:information"
143 +const ToolsIcon = "carbon:tools"
144 +
145 +const message = useMessage()
146 +const loading = ref(false)
147 +const showFilters = ref(false)
148 +const showActionsView = useStorage<boolean>("sigma-queries-list-actions-view-state", false, localStorage)
149 +const queriesList = ref<SigmaQuery[]>([])
150 +
151 +const pageSize = ref(25)
152 +const currentPage = ref(1)
153 +const simpleMode = ref(false)
154 +const showSizePicker = ref(true)
155 +const pageSizes = [10, 25, 50, 100]
156 +const header = ref()
157 +const pageSlot = ref(8)
158 +
159 +const itemsPaginated = computed(() => {
160 + const from = (currentPage.value - 1) * pageSize.value
161 + const to = currentPage.value * pageSize.value
162 +
163 + const list = _orderBy(queriesList.value, ["id"], ["desc"])
164 +
165 + return list.slice(from, to)
166 +})
167 +
168 +const total = computed<number>(() => {
169 + return queriesList.value.length || 0
170 +})
171 +const activeTotal = computed<number>(() => {
172 + return queriesList.value.filter(o => o.active).length || 0
173 +})
174 +const inactiveTotal = computed<number>(() => {
175 + return queriesList.value.filter(o => !o.active).length || 0
176 +})
177 +
178 +const filters = ref<Partial<QueriesFilter>>({})
179 +const lastFilters = ref<Partial<QueriesFilter>>({})
180 +
181 +const filtered = computed<boolean>(() => {
182 + return !!filters.value.active
183 +})
184 +
185 +const activeOptions = [
186 + { label: "Active", value: "active" },
187 + { label: "Inactive", value: "inactive" }
188 +]
189 +
190 +watch(showFilters, val => {
191 + if (!val) {
192 + filters.value = _cloneDeep(lastFilters.value)
193 + }
194 +})
195 +
196 +function updateQueryItem(query: SigmaQuery) {
197 + const index = queriesList.value.findIndex(o => o.id === query.id)
198 + if (index !== -1) {
199 + queriesList.value[index] = query
200 + }
201 +}
202 +
203 +function deleteQueryItem(query: SigmaQuery) {
204 + const index = queriesList.value.findIndex(o => o.id === query.id)
205 + queriesList.value.splice(index, 1)
206 +}
207 +
208 +function resetFilters() {
209 + filters.value.active = undefined
210 + showFilters.value = false
211 + getData()
212 +}
213 +
214 +function getData() {
215 + showFilters.value = false
216 + loading.value = true
217 +
218 + lastFilters.value = _cloneDeep(filters.value)
219 +
220 + const method = !filtered.value ? "getAvailable" : filters.value.active === "active" ? "getActive" : "getInactive"
221 +
222 + Api.sigma[method]()
223 + .then(res => {
224 + if (res.data.success) {
225 + queriesList.value = res.data?.sigma_queries || []
226 + } else {
227 + message.warning(res.data?.message || "An error occurred. Please try again later.")
228 + }
229 + })
230 + .catch(err => {
231 + queriesList.value = []
232 +
233 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
234 + })
235 + .finally(() => {
236 + loading.value = false
237 + })
238 +}
239 +
240 +useResizeObserver(header, entries => {
241 + const entry = entries[0]
242 + const { width } = entry.contentRect
243 +
244 + pageSlot.value = width < 700 ? 5 : 8
245 + simpleMode.value = width < 550
246 +})
247 +
248 +onBeforeMount(() => {
249 + getData()
250 +})
251 +</script>
252 +
253 +<style lang="scss" scoped>
254 +.sigma-queries-list {
255 + .list {
256 + container-type: inline-size;
257 + min-height: 200px;
258 + }
259 +
260 + .actions-box {
261 + overflow: hidden;
262 + display: grid;
263 + grid-template-rows: 0fr;
264 + padding-top: 0px;
265 + opacity: 0;
266 + transition:
267 + opacity var(--router-transition-duration) ease-out,
268 + grid-template-rows var(--router-transition-duration) ease-out,
269 + padding-top var(--router-transition-duration) ease-out;
270 +
271 + &.open {
272 + grid-template-rows: 1fr;
273 + opacity: 1;
274 + @apply pt-3;
275 + }
276 + }
277 +}
278 +</style>
frontend/src/components/sigma/QueryDetails.vue new
+37
@@ -0,0 +1,37 @@
1 +<template>
2 + <n-tabs type="line" animated :tabs-padding="24" class="grow" pane-wrapper-class="flex flex-col grow">
3 + <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy" class="flex flex-col grow">
4 + <div class="pt-1">
5 + <QueryOverview :query @updated="updateQuery($event)" @deleted="emit('deleted', query)" />
6 + </div>
7 + </n-tab-pane>
8 + <n-tab-pane name="Rule Query" tab="Rule Query" display-directive="show:lazy">
9 + <div class="p-7 pt-4">
10 + <CodeSource :code="query.rule_query" lang="sql" />
11 + </div>
12 + </n-tab-pane>
13 + </n-tabs>
14 +</template>
15 +
16 +<script setup lang="ts">
17 +import { defineAsyncComponent, toRefs } from "vue"
18 +import { NTabs, NTabPane } from "naive-ui"
19 +import type { SigmaQuery } from "@/types/sigma.d"
20 +
21 +const QueryOverview = defineAsyncComponent(() => import("./QueryOverview.vue"))
22 +const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
23 +
24 +const emit = defineEmits<{
25 + (e: "deleted", value: SigmaQuery): void
26 + (e: "updated", value: SigmaQuery): void
27 +}>()
28 +
29 +const props = defineProps<{
30 + query: SigmaQuery
31 +}>()
32 +const { query } = toRefs(props)
33 +
34 +function updateQuery(updatedQuery: SigmaQuery) {
35 + emit("updated", updatedQuery)
36 +}
37 +</script>
frontend/src/components/sigma/QueryItem.vue new
+192
@@ -0,0 +1,192 @@
1 +<template>
2 + <div class="sigma-query-item" :class="{ embedded }" @click="openDetails()">
3 + <n-spin :show="loading">
4 + <div class="flex flex-col">
5 + <div class="header-box px-5 py-3 pb-0 flex justify-between items-center">
6 + <div class="id flex items-center gap-2 cursor-pointer" @click="openDetails()">
7 + <span>#{{ query.id }}</span>
8 + </div>
9 + <div class="status flex gap-2 items-center">
10 + <span>{{ query.active ? "Active" : "Inactive" }}</span>
11 + <Icon :name="EnabledIcon" :size="14" class="text-success-color" v-if="query.active"></Icon>
12 + <Icon :name="DisabledIcon" :size="14" class="text-secondary-color" v-else></Icon>
13 + </div>
14 + </div>
15 +
16 + <div class="main-box flex flex-col gap-3 px-5 py-3">
17 + <div class="content flex flex-col gap-1 grow">
18 + <div class="title">
19 + {{ query.rule_name }}
20 + </div>
21 + </div>
22 + </div>
23 +
24 + <div class="footer-box px-5 py-3 flex justify-between items-center gap-4">
25 + <div class="badges-box flex flex-wrap items-center gap-3">
26 + <QueryTimeIntervalForm
27 + :query
28 + v-slot="{ loading: loadingTimeInterval, togglePopup: toggleTimeIntervalPopup }"
29 + @updated="updateQuery($event)"
30 + >
31 + <Badge type="splitted" bright pointCursor @click.stop="toggleTimeIntervalPopup()">
32 + <template #iconLeft>
33 + <n-spin
34 + :size="12"
35 + :show="loadingTimeInterval"
36 + content-class="flex flex-col justify-center"
37 + >
38 + <Icon :name="TimeIntervalIcon" />
39 + </n-spin>
40 + </template>
41 + <template #label>Time Interval</template>
42 + <template #value>
43 + <div class="flex gap-2 items-center">
44 + {{ query.time_interval || "n/d" }}
45 + <Icon :name="EditIcon" :size="13" />
46 + </div>
47 + </template>
48 + </Badge>
49 + </QueryTimeIntervalForm>
50 +
51 + <Badge type="splitted" bright fluid class="!hidden sm:!flex">
52 + <template #iconLeft>
53 + <Icon :name="TimeIcon" />
54 + </template>
55 + <template #label>Last execution time</template>
56 + <template #value>
57 + <div class="flex gap-2 items-center">
58 + {{
59 + query.last_execution_time
60 + ? formatDate(query.last_execution_time, dFormats.datetimesec)
61 + : "n/d"
62 + }}
63 + </div>
64 + </template>
65 + </Badge>
66 + </div>
67 + <div class="actions-box">
68 + <QueryDeleteOne
69 + :query
70 + v-slot="{ loading: loadingDelete, togglePopup: toggleDeletePopup }"
71 + @deleted="emit('deleted', query)"
72 + >
73 + <n-button quaternary size="tiny" :loading="loadingDelete" @click.stop="toggleDeletePopup()">
74 + Delete
75 + </n-button>
76 + </QueryDeleteOne>
77 + </div>
78 + </div>
79 + </div>
80 + </n-spin>
81 +
82 + <n-modal
83 + v-model:show="showDetails"
84 + :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(480px, 90vh)', overflow: 'hidden' }"
85 + display-directive="show"
86 + >
87 + <n-card
88 + content-class="flex flex-col !p-0"
89 + :title="`#${query.id}`"
90 + closable
91 + @close="closeDetails()"
92 + :bordered="false"
93 + segmented
94 + role="modal"
95 + >
96 + <QueryDetails :query @deleted="emitDelete(query)" @updated="updateQuery($event)" />
97 + </n-card>
98 + </n-modal>
99 + </div>
100 +</template>
101 +
102 +<script setup lang="ts">
103 +import { ref, toRefs } from "vue"
104 +import { NModal, NButton, NSpin, NCard } from "naive-ui"
105 +import { useSettingsStore } from "@/stores/settings"
106 +import { formatDate } from "@/utils"
107 +import Icon from "@/components/common/Icon.vue"
108 +import Badge from "@/components/common/Badge.vue"
109 +import QueryTimeIntervalForm from "./actionsProviders/QueryTimeIntervalForm.vue"
110 +import QueryDeleteOne from "./actionsProviders/QueryDeleteOne.vue"
111 +import QueryDetails from "./QueryDetails.vue"
112 +import type { SigmaQuery } from "@/types/sigma.d"
113 +
114 +const emit = defineEmits<{
115 + (e: "deleted", value: SigmaQuery): void
116 + (e: "updated", value: SigmaQuery): void
117 +}>()
118 +
119 +const props = defineProps<{
120 + query: SigmaQuery
121 + embedded?: boolean
122 +}>()
123 +const { query, embedded } = toRefs(props)
124 +
125 +const TimeIcon = "carbon:time"
126 +const EditIcon = "uil:edit-alt"
127 +const EnabledIcon = "carbon:circle-solid"
128 +const DisabledIcon = "carbon:subtract-alt"
129 +const TimeIntervalIcon = "material-symbols:autoplay"
130 +
131 +const loading = ref(false)
132 +const showDetails = ref(false)
133 +const dFormats = useSettingsStore().dateFormat
134 +
135 +function updateQuery(updatedQuery: SigmaQuery) {
136 + emit("updated", updatedQuery)
137 +}
138 +
139 +function emitDelete(updatedQuery: SigmaQuery) {
140 + closeDetails()
141 + emit("deleted", updatedQuery)
142 +}
143 +
144 +function openDetails() {
145 + showDetails.value = true
146 +}
147 +
148 +function closeDetails() {
149 + showDetails.value = false
150 +}
151 +</script>
152 +
153 +<style lang="scss" scoped>
154 +.sigma-query-item {
155 + border-radius: var(--border-radius);
156 + background-color: var(--bg-color);
157 + transition: all 0.2s var(--bezier-ease);
158 + border: var(--border-small-050);
159 + overflow: hidden;
160 + cursor: pointer;
161 +
162 + .header-box {
163 + font-size: 13px;
164 +
165 + .id {
166 + color: var(--fg-secondary-color);
167 + font-family: var(--font-family-mono);
168 + word-break: break-word;
169 + line-height: 1.2;
170 + }
171 + }
172 + .main-box {
173 + .content {
174 + word-break: break-word;
175 + }
176 + }
177 +
178 + .footer-box {
179 + border-top: var(--border-small-100);
180 + font-size: 13px;
181 + background-color: var(--bg-secondary-color);
182 + }
183 +
184 + &.embedded {
185 + background-color: var(--bg-secondary-color);
186 + }
187 +
188 + &:hover {
189 + box-shadow: 0px 0px 0px 1px var(--primary-color);
190 + }
191 +}
192 +</style>
frontend/src/components/sigma/QueryOverview.vue new
+164
@@ -0,0 +1,164 @@
1 +<template>
2 + <n-spin :show="loading" class="flex flex-col grow" content-class="flex flex-col grow">
3 + <div class="flex flex-col gap-4 grow justify-between">
4 + <div class="content-box flex flex-col gap-4 py-3">
5 + <div class="px-7 flex sm:!flex-row flex-col gap-4">
6 + <KVCard :color="query.active ? 'success' : undefined" size="lg" class="grow w-full">
7 + <template #key>
8 + <div class="flex gap-2 items-center">
9 + <Icon :name="query.active ? EnabledIcon : DisabledIcon" />
10 + <span>Status</span>
11 + </div>
12 + </template>
13 + <template #value>
14 + <div class="flex">
15 + <QueryActiveForm
16 + :query
17 + v-slot="{ loading: loadingActive, togglePopup: toggleActivePopup }"
18 + @updated="updateQuery($event)"
19 + >
20 + <div
21 + @click.stop="toggleActivePopup()"
22 + class="flex gap-3 items-center"
23 + :class="{
24 + 'cursor-not-allowed': loadingActive,
25 + 'cursor-pointer': !loadingActive
26 + }"
27 + >
28 + <span>{{ query.active ? "Active" : "Inactive" }}</span>
29 + <n-spin
30 + :size="14"
31 + :show="loadingActive"
32 + content-class="flex flex-col justify-center"
33 + >
34 + <Icon :name="EditIcon" />
35 + </n-spin>
36 + </div>
37 + </QueryActiveForm>
38 + </div>
39 + </template>
40 + </KVCard>
41 +
42 + <KVCard size="lg" class="grow w-full">
43 + <template #key>
44 + <div class="flex gap-2 items-center">
45 + <Icon :name="TimeIntervalIcon" />
46 + <span>Time Interval</span>
47 + </div>
48 + </template>
49 + <template #value>
50 + <div class="flex">
51 + <QueryTimeIntervalForm
52 + :query
53 + v-slot="{ loading: loadingTimeInterval, togglePopup: toggleTimeIntervalPopup }"
54 + @updated="updateQuery($event)"
55 + >
56 + <div
57 + @click.stop="toggleTimeIntervalPopup()"
58 + class="flex gap-3 items-center"
59 + :class="{
60 + 'cursor-not-allowed': loadingTimeInterval,
61 + 'cursor-pointer': !loadingTimeInterval
62 + }"
63 + >
64 + <span>{{ query.time_interval || "n/d" }}</span>
65 + <n-spin
66 + :size="14"
67 + :show="loadingTimeInterval"
68 + content-class="flex flex-col justify-center"
69 + >
70 + <Icon :name="EditIcon" />
71 + </n-spin>
72 + </div>
73 + </QueryTimeIntervalForm>
74 + </div>
75 + </template>
76 + </KVCard>
77 + </div>
78 +
79 + <div class="px-7">
80 + <KVCard>
81 + <template #key>name</template>
82 + <template #value>{{ query.rule_name ?? "-" }}</template>
83 + </KVCard>
84 + </div>
85 +
86 + <div class="px-7 grid gap-2 grid-auto-fit-250">
87 + <KVCard>
88 + <template #key>last execution time</template>
89 + <template #value>
90 + {{
91 + query.last_execution_time
92 + ? formatDate(query.last_execution_time, dFormats.datetimesec)
93 + : "n/d"
94 + }}
95 + </template>
96 + </KVCard>
97 +
98 + <KVCard>
99 + <template #key>last updated</template>
100 + <template #value>
101 + {{ query.last_updated ? formatDate(query.last_updated, dFormats.datetimesec) : "n/d" }}
102 + </template>
103 + </KVCard>
104 + </div>
105 + </div>
106 +
107 + <div class="footer-box px-7 py-4 flex items-center gap-2">
108 + <div class="grow"></div>
109 +
110 + <QueryDeleteOne
111 + :query
112 + v-slot="{ loading: loadingDelete, togglePopup: toggleDeletePopup }"
113 + @deleted="emit('deleted', query)"
114 + >
115 + <n-button type="error" secondary :loading="loadingDelete" @click.stop="toggleDeletePopup()">
116 + <template #icon><Icon :name="TrashIcon" /></template>
117 + Delete
118 + </n-button>
119 + </QueryDeleteOne>
120 + </div>
121 + </div>
122 + </n-spin>
123 +</template>
124 +
125 +<script setup lang="ts">
126 +import { ref, toRefs } from "vue"
127 +import { NButton, NSpin } from "naive-ui"
128 +import QueryActiveForm from "./actionsProviders/QueryActiveForm.vue"
129 +import QueryTimeIntervalForm from "./actionsProviders/QueryTimeIntervalForm.vue"
130 +import QueryDeleteOne from "./actionsProviders/QueryDeleteOne.vue"
131 +import KVCard from "@/components/common/KVCard.vue"
132 +import Icon from "@/components/common/Icon.vue"
133 +import { formatDate } from "@/utils"
134 +import { useSettingsStore } from "@/stores/settings"
135 +import type { SigmaQuery } from "@/types/sigma.d"
136 +
137 +const emit = defineEmits<{
138 + (e: "deleted", value: SigmaQuery): void
139 + (e: "updated", value: SigmaQuery): void
140 +}>()
141 +
142 +const props = defineProps<{ query: SigmaQuery }>()
143 +const { query } = toRefs(props)
144 +
145 +const TrashIcon = "carbon:trash-can"
146 +const TimeIntervalIcon = "material-symbols:autoplay"
147 +const EditIcon = "uil:edit-alt"
148 +const EnabledIcon = "carbon:circle-solid"
149 +const DisabledIcon = "carbon:subtract-alt"
150 +
151 +const dFormats = useSettingsStore().dateFormat
152 +const loading = ref(false)
153 +
154 +function updateQuery(updatedQuery: SigmaQuery) {
155 + emit("updated", updatedQuery)
156 +}
157 +</script>
158 +
159 +<style lang="scss" scoped>
160 +.footer-box {
161 + border-top: var(--border-small-100);
162 + background-color: var(--bg-secondary-color);
163 +}
164 +</style>
frontend/src/components/sigma/actionsProviders/QueryActiveAllForm.vue new
+69
@@ -0,0 +1,69 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-2 justify-center min-w-52">
8 + <div class="flex gap-2 items-center">
9 + <n-button :loading @click="updateActive(true)" type="success">Activates all Sigma queries</n-button>
10 + <n-button :loading @click="updateActive(false)">Deactivates all Sigma queries</n-button>
11 + </div>
12 +
13 + <p class="text-right">* It may take several minutes</p>
14 +
15 + <div class="flex gap-2 justify-between">
16 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
17 + </div>
18 + </div>
19 + </n-popover>
20 +</template>
21 +
22 +<script setup lang="ts">
23 +import { ref } from "vue"
24 +import { NButton, NPopover, useMessage } from "naive-ui"
25 +import Api from "@/api"
26 +
27 +const emit = defineEmits<{
28 + (e: "updated"): void
29 +}>()
30 +
31 +const loading = defineModel<boolean | undefined>("loading", { default: false })
32 +
33 +const show = ref(false)
34 +const lastShow = ref(new Date().getTime())
35 +const message = useMessage()
36 +
37 +function togglePopup() {
38 + if (new Date().getTime() - lastShow.value > 500) {
39 + show.value = !show.value
40 + }
41 +}
42 +
43 +function closePopup() {
44 + lastShow.value = new Date().getTime()
45 + show.value = false
46 +}
47 +
48 +function updateActive(active: boolean) {
49 + loading.value = true
50 +
51 + const methods = active ? "activateAllQueries" : "deactivateAllQueries"
52 +
53 + Api.sigma[methods]()
54 + .then(res => {
55 + if (res.data.success) {
56 + emit("updated")
57 + message.success(res.data?.message || "Sigma queries updated successfully")
58 + } else {
59 + message.warning(res.data?.message || "An error occurred. Please try again later.")
60 + }
61 + })
62 + .catch(err => {
63 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
64 + })
65 + .finally(() => {
66 + loading.value = false
67 + })
68 +}
69 +</script>
frontend/src/components/sigma/actionsProviders/QueryActiveForm.vue new
+95
@@ -0,0 +1,95 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-4 justify-center min-w-52">
8 + <div class="flex gap-2 items-center justify-center py-3">
9 + <span>Active</span>
10 + <n-switch v-model:value="model.active" :disabled="loading" />
11 + </div>
12 +
13 + <div class="flex gap-2 justify-between">
14 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
15 + <n-button :disabled="!dirty" :loading @click="updateActive()" type="primary" size="small">
16 + Save
17 + </n-button>
18 + </div>
19 + </div>
20 + </n-popover>
21 +</template>
22 +
23 +<script setup lang="ts">
24 +import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
25 +import { NButton, NPopover, NSwitch, useMessage } from "naive-ui"
26 +import Api from "@/api"
27 +import type { SigmaQuery } from "@/types/sigma.d"
28 +
29 +const emit = defineEmits<{
30 + (e: "updated", value: SigmaQuery): void
31 +}>()
32 +
33 +const props = defineProps<{
34 + query: SigmaQuery
35 +}>()
36 +const { query } = toRefs(props)
37 +
38 +const loading = defineModel<boolean | undefined>("loading", { default: false })
39 +
40 +const show = ref(false)
41 +const lastShow = ref(new Date().getTime())
42 +const message = useMessage()
43 +const model = ref<{ active: boolean }>({ active: false })
44 +const active = ref<boolean>(false)
45 +const dirty = computed(() => active.value !== model.value.active)
46 +
47 +watch(show, val => {
48 + if (val && !loading.value) {
49 + setModel()
50 + }
51 +})
52 +
53 +function togglePopup() {
54 + if (new Date().getTime() - lastShow.value > 500) {
55 + show.value = !show.value
56 + }
57 +}
58 +
59 +function closePopup() {
60 + lastShow.value = new Date().getTime()
61 + show.value = false
62 +}
63 +
64 +function setModel() {
65 + active.value = !!query.value.active
66 + model.value.active = active.value
67 +}
68 +
69 +function updateActive() {
70 + if (query.value.rule_name) {
71 + loading.value = true
72 +
73 + Api.sigma
74 + .setQueryActive(query.value.rule_name, model.value.active)
75 + .then(res => {
76 + if (res.data.success) {
77 + emit("updated", res.data.sigma_queries[0])
78 + message.success(res.data?.message || "Sigma query updated successfully")
79 + } else {
80 + message.warning(res.data?.message || "An error occurred. Please try again later.")
81 + }
82 + })
83 + .catch(err => {
84 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
85 + })
86 + .finally(() => {
87 + loading.value = false
88 + })
89 + }
90 +}
91 +
92 +onBeforeMount(() => {
93 + setModel()
94 +})
95 +</script>
frontend/src/components/sigma/actionsProviders/QueryDeleteAll.vue new
+66
@@ -0,0 +1,66 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-2 max-w-80">
8 + <div>This will remove ALL Sigma queries, are you sure you want to proceed?</div>
9 +
10 + <p class="text-right">* It may take several minutes</p>
11 +
12 + <div class="flex gap-2 justify-between">
13 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
14 + <n-button :loading @click="deleteQueries()" type="primary" size="small">Yes I'm sure</n-button>
15 + </div>
16 + </div>
17 + </n-popover>
18 +</template>
19 +
20 +<script setup lang="ts">
21 +import { ref } from "vue"
22 +import { NButton, NPopover, useMessage } from "naive-ui"
23 +import Api from "@/api"
24 +
25 +const emit = defineEmits<{
26 + (e: "updated"): void
27 +}>()
28 +
29 +const loading = defineModel<boolean | undefined>("loading", { default: false })
30 +
31 +const show = ref(false)
32 +const lastShow = ref(new Date().getTime())
33 +const message = useMessage()
34 +
35 +function togglePopup() {
36 + if (new Date().getTime() - lastShow.value > 500) {
37 + show.value = !show.value
38 + }
39 +}
40 +
41 +function closePopup() {
42 + lastShow.value = new Date().getTime()
43 + show.value = false
44 +}
45 +
46 +function deleteQueries() {
47 + loading.value = true
48 +
49 + Api.sigma
50 + .deleteAllRules()
51 + .then(res => {
52 + if (res.data.success) {
53 + emit("updated")
54 + message.success(res.data?.message || "Sigma queries deleted successfully")
55 + } else {
56 + message.warning(res.data?.message || "An error occurred. Please try again later.")
57 + }
58 + })
59 + .catch(err => {
60 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
61 + })
62 + .finally(() => {
63 + loading.value = false
64 + })
65 +}
66 +</script>
frontend/src/components/sigma/actionsProviders/QueryDeleteOne.vue new
+81
@@ -0,0 +1,81 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-4">
8 + <div>
9 + Are you sure you want to delete the Query
10 + <strong>#{{ query.id }}</strong>
11 + ?
12 + </div>
13 +
14 + <div class="flex gap-2 justify-between">
15 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
16 + <n-button :loading @click="deleteQuery()" type="error" size="small">
17 + <template #icon><Icon :name="TrashIcon" /></template>
18 + Delete Query
19 + </n-button>
20 + </div>
21 + </div>
22 + </n-popover>
23 +</template>
24 +
25 +<script setup lang="ts">
26 +import { ref, toRefs } from "vue"
27 +import { NButton, NPopover, useMessage } from "naive-ui"
28 +import Icon from "@/components/common/Icon.vue"
29 +import Api from "@/api"
30 +import type { SigmaQuery } from "@/types/sigma.d"
31 +
32 +const emit = defineEmits<{
33 + (e: "deleted", value: SigmaQuery): void
34 +}>()
35 +
36 +const props = defineProps<{
37 + query: SigmaQuery
38 +}>()
39 +const { query } = toRefs(props)
40 +
41 +const loading = defineModel<boolean | undefined>("loading", { default: false })
42 +
43 +const TrashIcon = "carbon:trash-can"
44 +const show = ref(false)
45 +const lastShow = ref(new Date().getTime())
46 +const message = useMessage()
47 +
48 +function togglePopup() {
49 + if (new Date().getTime() - lastShow.value > 500) {
50 + show.value = !show.value
51 + }
52 +}
53 +
54 +function closePopup() {
55 + lastShow.value = new Date().getTime()
56 + show.value = false
57 +}
58 +
59 +function deleteQuery() {
60 + if (query.value.rule_name) {
61 + loading.value = true
62 +
63 + Api.sigma
64 + .deleteRule(query.value.rule_name)
65 + .then(res => {
66 + if (res.data.success) {
67 + emit("deleted", query.value)
68 + message.success(res.data?.message || "Sigma query deleted successfully")
69 + } else {
70 + message.warning(res.data?.message || "An error occurred. Please try again later.")
71 + }
72 + })
73 + .catch(err => {
74 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
75 + })
76 + .finally(() => {
77 + loading.value = false
78 + })
79 + }
80 +}
81 +</script>
frontend/src/components/sigma/actionsProviders/QueryDownloadAll.vue new
+64
@@ -0,0 +1,64 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-4 max-w-80">
8 + <div>This will download ALL Sigma queries, are you sure you want to proceed?</div>
9 +
10 + <div class="flex gap-2 justify-between">
11 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
12 + <n-button :loading @click="downloadQueries()" type="primary" size="small">Yes I'm sure</n-button>
13 + </div>
14 + </div>
15 + </n-popover>
16 +</template>
17 +
18 +<script setup lang="ts">
19 +import { ref } from "vue"
20 +import { NButton, NPopover, useMessage } from "naive-ui"
21 +import Api from "@/api"
22 +
23 +const emit = defineEmits<{
24 + (e: "updated"): void
25 +}>()
26 +
27 +const loading = defineModel<boolean | undefined>("loading", { default: false })
28 +
29 +const show = ref(false)
30 +const lastShow = ref(new Date().getTime())
31 +const message = useMessage()
32 +
33 +function togglePopup() {
34 + if (new Date().getTime() - lastShow.value > 500) {
35 + show.value = !show.value
36 + }
37 +}
38 +
39 +function closePopup() {
40 + lastShow.value = new Date().getTime()
41 + show.value = false
42 +}
43 +
44 +function downloadQueries() {
45 + loading.value = true
46 +
47 + Api.sigma
48 + .downloadRules()
49 + .then(res => {
50 + if (res.data.success) {
51 + emit("updated")
52 + message.success(res.data?.message || "Sigma queries downloaded successfully")
53 + } else {
54 + message.warning(res.data?.message || "An error occurred. Please try again later.")
55 + }
56 + })
57 + .catch(err => {
58 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
59 + })
60 + .finally(() => {
61 + loading.value = false
62 + })
63 +}
64 +</script>
frontend/src/components/sigma/actionsProviders/QueryTimeIntervalForm.vue new
+134
@@ -0,0 +1,134 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-4">
8 + <div class="flex gap-2 items-center">
9 + <n-input-group>
10 + <n-select
11 + v-model:value="model.unit"
12 + :options="unitOptions"
13 + placeholder="Time unit"
14 + :disabled="loading"
15 + class="!w-28"
16 + />
17 + <n-input-number
18 + v-model:value="model.time"
19 + :min="1"
20 + clearable
21 + placeholder="Time"
22 + class="!w-32"
23 + :disabled="loading"
24 + />
25 + </n-input-group>
26 + </div>
27 +
28 + <div class="flex gap-2 justify-between">
29 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
30 + <n-button
31 + :disabled="!dirty || !isValid"
32 + :loading
33 + @click="updateTimeInterval()"
34 + type="primary"
35 + size="small"
36 + >
37 + Save
38 + </n-button>
39 + </div>
40 + </div>
41 + </n-popover>
42 +</template>
43 +
44 +<script setup lang="ts">
45 +import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
46 +import { NSelect, NInputGroup, NButton, NInputNumber, NPopover, useMessage } from "naive-ui"
47 +import Api from "@/api"
48 +import type { SigmaQuery, SigmaTimeInterval, SigmaTimeIntervalUnit } from "@/types/sigma.d"
49 +
50 +const emit = defineEmits<{
51 + (e: "updated", value: SigmaQuery): void
52 +}>()
53 +
54 +const props = defineProps<{
55 + query: SigmaQuery
56 +}>()
57 +const { query } = toRefs(props)
58 +
59 +const loading = defineModel<boolean | undefined>("loading", { default: false })
60 +
61 +const show = ref(false)
62 +const lastShow = ref(new Date().getTime())
63 +const message = useMessage()
64 +const model = ref<{ time: number; unit: SigmaTimeIntervalUnit }>({ time: 1, unit: "m" })
65 +const unitOptions = [
66 + { label: "Minutes", value: "m" },
67 + { label: "Hours", value: "h" },
68 + { label: "Days", value: "d" }
69 +]
70 +const timeUnit = ref<SigmaTimeIntervalUnit>("m")
71 +const timeValue = ref<number>(1)
72 +
73 +const timeInterval = computed<SigmaTimeInterval>(() => `${timeValue.value}${timeUnit.value}`)
74 +const modelTimeInterval = computed<SigmaTimeInterval>(() => `${model.value.time}${model.value.unit}`)
75 +
76 +const dirty = computed(() => timeInterval.value !== modelTimeInterval.value)
77 +const isValid = computed(() => model.value.time && model.value.unit)
78 +
79 +watch(show, val => {
80 + if (val && !loading.value) {
81 + setModel()
82 + }
83 +})
84 +
85 +function togglePopup() {
86 + if (new Date().getTime() - lastShow.value > 500) {
87 + show.value = !show.value
88 + }
89 +}
90 +
91 +function closePopup() {
92 + lastShow.value = new Date().getTime()
93 + show.value = false
94 +}
95 +
96 +function setModel() {
97 + if (query.value.time_interval) {
98 + timeUnit.value = (
99 + query.value.time_interval.match(/[a-z]/i)?.[0] || "m"
100 + ).toLocaleLowerCase() as SigmaTimeIntervalUnit
101 + timeValue.value = parseInt(query.value.time_interval.match(/\d+/)?.[0] || "1")
102 +
103 + model.value.unit = timeUnit.value
104 + model.value.time = timeValue.value
105 + }
106 +}
107 +
108 +function updateTimeInterval() {
109 + if (query.value.rule_name && modelTimeInterval.value) {
110 + loading.value = true
111 +
112 + Api.sigma
113 + .setQueryTimeInterval(query.value.rule_name, modelTimeInterval.value)
114 + .then(res => {
115 + if (res.data.success) {
116 + emit("updated", res.data.sigma_queries[0])
117 + message.success(res.data?.message || "Sigma query updated successfully")
118 + } else {
119 + message.warning(res.data?.message || "An error occurred. Please try again later.")
120 + }
121 + })
122 + .catch(err => {
123 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
124 + })
125 + .finally(() => {
126 + loading.value = false
127 + })
128 + }
129 +}
130 +
131 +onBeforeMount(() => {
132 + setModel()
133 +})
134 +</script>
frontend/src/components/sigma/actionsProviders/QueryUploadDB.vue new
+76
@@ -0,0 +1,76 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-2 max-w-80 min-w-72">
8 + <div>Choose the level to load:</div>
9 +
10 + <n-checkbox-group v-model:value="ruleLevels" class="flex gap-4" :disabled="loading">
11 + <n-checkbox value="high" label="High" />
12 + <n-checkbox value="critical" label="Critical" />
13 + </n-checkbox-group>
14 +
15 + <p class="text-right">* It may take several minutes</p>
16 +
17 + <div class="flex gap-2 justify-between">
18 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
19 + <n-button :disabled="!isValid" :loading @click="uploadQueries()" type="primary" size="small">
20 + Upload
21 + </n-button>
22 + </div>
23 + </div>
24 + </n-popover>
25 +</template>
26 +
27 +<script setup lang="ts">
28 +import { computed, ref } from "vue"
29 +import { NButton, NPopover, NCheckboxGroup, NCheckbox, useMessage } from "naive-ui"
30 +import Api from "@/api"
31 +import type { SigmaRuleLevels } from "@/types/sigma.d"
32 +
33 +const emit = defineEmits<{
34 + (e: "updated"): void
35 +}>()
36 +
37 +const loading = defineModel<boolean | undefined>("loading", { default: false })
38 +
39 +const show = ref(false)
40 +const lastShow = ref(new Date().getTime())
41 +const message = useMessage()
42 +const ruleLevels = ref<SigmaRuleLevels[]>([])
43 +const isValid = computed(() => !!ruleLevels.value.length)
44 +
45 +function togglePopup() {
46 + if (new Date().getTime() - lastShow.value > 500) {
47 + show.value = !show.value
48 + }
49 +}
50 +
51 +function closePopup() {
52 + lastShow.value = new Date().getTime()
53 + show.value = false
54 +}
55 +
56 +function uploadQueries() {
57 + loading.value = true
58 +
59 + Api.sigma
60 + .uploadRules(ruleLevels.value)
61 + .then(res => {
62 + if (res.data.success) {
63 + emit("updated")
64 + message.success(res.data?.message || "Successfully uploaded the Sigma queries to the database")
65 + } else {
66 + message.warning(res.data?.message || "An error occurred. Please try again later.")
67 + }
68 + })
69 + .catch(err => {
70 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
71 + })
72 + .finally(() => {
73 + loading.value = false
74 + })
75 +}
76 +</script>
frontend/src/components/sigma/actionsProviders/QueryUploadFile.vue new
+88
@@ -0,0 +1,88 @@
1 +<template>
2 + <n-popover trigger="manual" to="body" content-class="px-0" v-model:show="show" @clickoutside="closePopup()">
3 + <template #trigger>
4 + <slot :loading :togglePopup />
5 + </template>
6 +
7 + <div class="py-1 flex flex-col gap-2 max-w-80">
8 + <n-upload
9 + :max="1"
10 + accept="application/x-yaml, .yaml, .YAML, .yml, .YML"
11 + v-model:file-list="fileList"
12 + :disabled="loading"
13 + >
14 + <n-upload-dragger>
15 + <div>
16 + <Icon :name="UploadIcon" :size="28" :depth="3"></Icon>
17 + </div>
18 + <div class="font-semibold">Click or drag a file to this area to upload</div>
19 + <p class="mt-2">Only .yaml files are accepted</p>
20 + </n-upload-dragger>
21 + </n-upload>
22 +
23 + <div class="flex gap-2 justify-between">
24 + <n-button @click="closePopup()" quaternary size="small">Close</n-button>
25 + <n-button :disabled="!isValid" :loading @click="uploadQueries()" type="primary" size="small">
26 + Upload
27 + </n-button>
28 + </div>
29 + </div>
30 + </n-popover>
31 +</template>
32 +
33 +<script setup lang="ts">
34 +import { computed, ref } from "vue"
35 +import { NButton, NPopover, NUpload, NUploadDragger, useMessage, type UploadFileInfo } from "naive-ui"
36 +import Api from "@/api"
37 +import Icon from "@/components/common/Icon.vue"
38 +
39 +const emit = defineEmits<{
40 + (e: "updated"): void
41 +}>()
42 +
43 +const loading = defineModel<boolean | undefined>("loading", { default: false })
44 +
45 +const UploadIcon = "carbon:document-add"
46 +
47 +const show = ref(false)
48 +const lastShow = ref(new Date().getTime())
49 +const message = useMessage()
50 +const fileList = ref<UploadFileInfo[]>([])
51 +const yamlFile = computed<File | null>(() => fileList.value?.[0].file || null)
52 +const isValid = computed(() => fileList.value.length)
53 +
54 +function togglePopup() {
55 + if (new Date().getTime() - lastShow.value > 500) {
56 + show.value = !show.value
57 + }
58 +}
59 +
60 +function closePopup() {
61 + lastShow.value = new Date().getTime()
62 + show.value = false
63 +}
64 +
65 +function uploadQueries() {
66 + loading.value = true
67 +
68 + if (yamlFile.value) {
69 + Api.sigma
70 + .uploadRulesFile(yamlFile.value)
71 + .then(res => {
72 + if (res.data.success) {
73 + emit("updated")
74 + fileList.value = []
75 + message.success(res.data?.message || "Successfully uploaded the Sigma queries to the database")
76 + } else {
77 + message.warning(res.data?.message || "An error occurred. Please try again later.")
78 + }
79 + })
80 + .catch(err => {
81 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
82 + })
83 + .finally(() => {
84 + loading.value = false
85 + })
86 + }
87 +}
88 +</script>
frontend/src/layouts/common/Navbar/items.tsx
+13
@@ -255,6 +255,19 @@ export default function getItems(): MenuMixedOption[] {
255 { default: () => "Cases" }
256 ),
257 key: "IncidentManagement-Cases"
258 + },
259 + {
260 + label: () =>
261 + h(
262 + RouterLink,
263 + {
264 + to: {
265 + name: "IncidentManagement-Sigma"
266 + }
267 + },
268 + { default: () => "SIGMA" }
269 + ),
270 + key: "IncidentManagement-Sigma"
271 }
272 ]
273 },
frontend/src/router/index.ts
+6
@@ -153,6 +153,12 @@ const router = createRouter({
153 name: "IncidentManagement-Cases",
154 component: () => import("@/views/incidentManagement/Cases.vue"),
155 meta: { title: "Incident Cases" }
156 + },
157 + {
158 + path: "sigma",
159 + name: "IncidentManagement-Sigma",
160 + component: () => import("@/views/incidentManagement/Sigma.vue"),
161 + meta: { title: "Sigma rules" }
162 }
163 ]
164 },
frontend/src/types/sigma.d.ts new
+15
@@ -0,0 +1,15 @@
1 +export interface SigmaQuery {
2 + id: number
3 + rule_name: string
4 + rule_query: string
5 + active: boolean
6 + time_interval: SigmaTimeInterval
7 + last_updated: Date
8 + last_execution_time: Date
9 +}
10 +
11 +export type SigmaTimeIntervalValue = number
12 +export type SigmaTimeIntervalUnit = "m" | "h" | "d"
13 +export type SigmaTimeInterval = `${SigmaTimeIntervalValue}${SigmaTimeIntervalUnit}`
14 +
15 +export type SigmaRuleLevels = "high" | "critical"
frontend/src/utils/highlighter.ts
+2 -1
@@ -19,7 +19,8 @@ export async function getHighlighter() {
19 import("shiki/langs/scss.mjs"),
20 import("shiki/langs/css.mjs"),
21 import("shiki/langs/csharp.mjs"),
22 - import("shiki/langs/http.mjs")
22 + import("shiki/langs/http.mjs"),
23 + import("shiki/langs/sql.mjs")
24 ],
25 loadWasm: getWasm
26 })
frontend/src/views/agents/Overview.vue
+26 -8
@@ -57,32 +57,48 @@
57 <n-tabs type="line" animated default-value="Overview">
58 <n-tab-pane name="Overview" tab="Overview" display-directive="show">
59 <div class="section">
60 - <OverviewSection v-if="agent" :agent="agent" @updated="getAgent()" />
60 + <OverviewSection v-if="agent" :agent @updated="getAgent()" />
61 </div>
62 </n-tab-pane>
63 <n-tab-pane name="Vulnerabilities" tab="Vulnerabilities" display-directive="show:lazy">
64 <div class="section">
65 - <VulnerabilitiesGrid v-if="agent" :agent="agent" />
65 + <VulnerabilitiesGrid v-if="agent" :agent />
66 </div>
67 </n-tab-pane>
68 <n-tab-pane name="SCA" tab="SCA" display-directive="show:lazy">
69 <div class="section">
70 - <ScaTable v-if="agent" :agent="agent" />
70 + <ScaTable v-if="agent" :agent />
71 </div>
72 </n-tab-pane>
73 <n-tab-pane name="Cases" tab="Cases" display-directive="show:lazy">
74 <div class="section">
75 - <AgentCases v-if="agent" :agent="agent" />
75 + <!--
76 + <AgentCases v-if="agent" :agent />
77 + -->
78 + <CasesList
79 + v-if="agent"
80 + class="px-1"
81 + :preset="{ type: 'hostname', value: agent.hostname }"
82 + hide-filters
83 + />
84 </div>
85 </n-tab-pane>
86 <n-tab-pane name="Artifacts" tab="Artifacts" display-directive="show:lazy">
87 <div class="section">
80 - <AgentFlowList v-if="agent" :agent="agent" />
88 + <AgentFlowList v-if="agent" :agent />
89 </div>
90 </n-tab-pane>
91 <n-tab-pane name="Alerts" tab="Alerts" display-directive="show:lazy">
92 <div class="section">
85 - <AlertsList v-if="agent" :agent-hostname="agent.hostname" />
93 + <!--
94 + <AlertsList v-if="agent" :agent-hostname="agent.hostname" />
95 + -->
96 + <AlertsList
97 + v-if="agent"
98 + class="px-1"
99 + :preset="{ type: 'assetName', value: agent.hostname }"
100 + hide-filters
101 + />
102 </div>
103 </n-tab-pane>
104 <n-tab-pane name="collect" tab="Collect" display-directive="show:lazy">
@@ -137,9 +153,11 @@ const VulnerabilitiesGrid = defineAsyncComponent(
153 () => import("@/components/agents/vulnerabilities/VulnerabilitiesGrid.vue")
154 )
155 const ScaTable = defineAsyncComponent(() => import("@/components/agents/sca/ScaTable.vue"))
140 -const AlertsList = defineAsyncComponent(() => import("@/components/alerts/AlertsList.vue"))
156 +// const AlertsList = defineAsyncComponent(() => import("@/components/alerts/AlertsList.vue"))
157 +const AlertsList = defineAsyncComponent(() => import("@/components/incidentManagement/alerts/AlertsList.vue"))
158 const OverviewSection = defineAsyncComponent(() => import("@/components/agents/OverviewSection.vue"))
142 -const AgentCases = defineAsyncComponent(() => import("@/components/agents/AgentCases.vue"))
159 +// const AgentCases = defineAsyncComponent(() => import("@/components/agents/AgentCases.vue"))
160 +const CasesList = defineAsyncComponent(() => import("@/components/incidentManagement/cases/CasesList.vue"))
161 const AgentFlowList = defineAsyncComponent(() => import("@/components/agents/agentFlow/AgentFlowList.vue"))
162 const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
163 const ArtifactsCommand = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCommand.vue"))
frontend/src/views/incidentManagement/Sigma.vue new
+9
@@ -0,0 +1,9 @@
1 +<template>
2 + <div class="page">
3 + <QueriesList />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import QueriesList from "@/components/sigma/QueriesList.vue"
9 +</script>