@cryptotaxi247 / CoPilot / commits / 4756dc9b

Active response (#163)

* local-dev url change * Update ActiveResponse API response model * for wazuh content pack provision just return the raw json from the template * Add route to get available content packs in Graylog * Update content pack name in Graylog provisioning schema * make graylog content pack install functions more universal * added active response apis/types * added active response components * precommit fixes * updated agent page * updated details buttons * added ActiveResponseDetails component * Add active response route to get supported active responses for a specific agent * updated dependencies * updated active response apis * added active response agent form * fixed props and loading emits * Add MTU configuration to docker-compose.yml * Add DNS and MTU configuration to docker daemon.json * added active response wizard * add provision wazuh worker route * create office365 alert monitoring in graylog * office365 alert monitoring progress * office365 models * rename to office365 * Fix customer code retrieval in check_if_open_alert_exists_in_iris function * added customer creation default settings * updated ActiveResponseWizard * Add optional validation for agents_list in ParamsModel * Add custom active response rule for Windows Firewall * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Feb 22, 2024 at 12:11 UTC 4756dc9bfc9a93269b9feeac583759c92c1043c8
63 files changed +2635 -193
.vscode/settings.json
+8 -1
@@ -2,21 +2,28 @@
2 "cSpell.words": [
3 "ajoelp",
4 "apexchart",
5 + "arcticons",
6 "colord",
7 "datejs",
8 "datetimesec",
9 "echarts",
10 + "firedtimes",
11 "Healthcheck",
12 + "healthchecks",
13 "majesticons",
14 "mimecast",
15 + "mynaui",
16 "picocolors",
17 "redoc",
18 "rushstack",
19 + "Socfortress",
20 "sparkline",
21 "taze",
22 "uvicorn",
23 "venv",
24 + "vuesjv",
25 "Wazuh",
20 - "xaxis"
26 + "xaxis",
27 + "zondicons"
28 ]
29 }
README.md
+22 -1
@@ -51,7 +51,28 @@ nano /etc/docker/daemon.json
51 ```
52
53 ```json
54 -{ "dns": ["YOUR_DNS_SERVER"], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
54 +{
55 + "dns": ["YOUR_DNS_SERVER"],
56 + "log-driver": "json-file",
57 + "log-opts": {
58 + "max-size": "10m",
59 + "max-file": "3"
60 + }
61 +}
62 +```
63 +
64 +### In case you need to set MTU
65 +
66 +```json
67 +{
68 + "dns": ["YOUR_DNS_SERVER"],
69 + "log-driver": "json-file",
70 + "log-opts": {
71 + "max-size": "10m",
72 + "max-file": "3"
73 + },
74 + "mtu": 1450
75 +}
76 ```
77
78 ```
backend/app/active_response/README.md
+14
@@ -58,6 +58,20 @@ You can find the created `custom-ar.exe` executable in the `C:\Users\<USER>\dist
58 </active-response>
59 ```
60
61 +#. Create the rules file `/var/ossec/etc/rules/600000-active_response.xml` and add the following rule to trigger the custom active response:
62 +
63 +```xml
64 +<group name="active_response,">
65 + <rule id="600000" level="10">
66 + <decoded_as>json</decoded_as>
67 + <field name="active_response">windows_firewall</field>
68 + <description>Windows Firewall Active Response triggered.</description>
69 + <group>socfortress,</group>
70 + <options>no_full_log</options>
71 + </rule>
72 +</group>
73 +```
74 +
75 #. Restart the Wazuh manager to apply the changes:
76
77 ```console
backend/app/active_response/routes/active_response.py
+52 -4
@@ -3,19 +3,23 @@ from pathlib import Path
3
4 import aiofiles
5 from fastapi import APIRouter
6 +from fastapi import Depends
7 from fastapi import HTTPException
8 from fastapi import Security
8 -from fastapi.responses import JSONResponse
9 from loguru import logger
10 +from sqlalchemy.ext.asyncio import AsyncSession
11
12 from app.active_response.schema.active_response import ActiveResponse
13 from app.active_response.schema.active_response import ActiveResponseDetails
14 +from app.active_response.schema.active_response import ActiveResponseDetailsResponse
15 from app.active_response.schema.active_response import ActiveResponsesSupported
16 from app.active_response.schema.active_response import ActiveResponsesSupportedResponse
17 from app.active_response.schema.active_response import InvokeActiveResponseRequest
18 from app.active_response.schema.active_response import InvokeActiveResponseResponse
19 +from app.agents.routes.agents import get_agent
20 from app.auth.utils import AuthHandler
21 from app.connectors.wazuh_manager.utils.universal import send_put_request
22 +from app.db.db_session import get_db
23
24 active_response_router = APIRouter()
25
@@ -45,13 +49,39 @@ async def read_markdown_file(file_path: str) -> str:
49 return await file.read()
50
51
52 +async def return_supported_active_responses_based_on_os(os: str) -> ActiveResponsesSupportedResponse:
53 + # if os contains windows
54 + if "Windows" in os:
55 + logger.info("Agent OS is Windows")
56 + return ActiveResponsesSupportedResponse(
57 + supported_active_responses=[
58 + ActiveResponse(name=active_response.name, description=active_response.value)
59 + for active_response in ActiveResponsesSupported
60 + if "WINDOWS" in active_response.name
61 + ],
62 + success=True,
63 + message="Supported Active Responses retrieved successfully",
64 + )
65 + else:
66 + logger.info("Agent OS is Linux")
67 + return ActiveResponsesSupportedResponse(
68 + supported_active_responses=[
69 + ActiveResponse(name=active_response.name, description=active_response.value)
70 + for active_response in ActiveResponsesSupported
71 + if "LINUX" in active_response.name
72 + ],
73 + success=True,
74 + message="Supported Active Responses retrieved successfully",
75 + )
76 +
77 +
78 @active_response_router.get(
79 "/describe/{active_response_name}",
50 - response_model=ActiveResponse,
80 + response_model=ActiveResponseDetailsResponse,
81 description="Get the details of a specific active response",
82 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
83 )
54 -async def get_active_response_details_route(active_response_name: str) -> ActiveResponse:
84 +async def get_active_response_details_route(active_response_name: str) -> ActiveResponseDetailsResponse:
85 """
86 Get the details of a specific active response
87 """
@@ -64,7 +94,7 @@ async def get_active_response_details_route(active_response_name: str) -> Active
94 description=ActiveResponsesSupported[active_response_name.upper()].value,
95 markdown_content=await read_markdown_file(file_path),
96 )
67 - return JSONResponse(content=response.dict())
97 + return ActiveResponseDetailsResponse(active_response=response, success=True, message="Active Response details retrieved successfully")
98
99
100 @active_response_router.get(
@@ -86,6 +116,24 @@ async def get_supported_active_responses_route() -> ActiveResponsesSupportedResp
116 )
117
118
119 +@active_response_router.get(
120 + "/supported/{agent_id}",
121 + response_model=ActiveResponsesSupportedResponse,
122 + description="Get the list of supported active responses for a specific agent",
123 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
124 +)
125 +async def get_supported_active_responses_agent_route(agent_id: str, db: AsyncSession = Depends(get_db)) -> ActiveResponsesSupportedResponse:
126 + """
127 + Get the list of supported active responses for a specific agent
128 + """
129 + response = await get_agent(agent_id, db)
130 + agent = response.agents[0] if response.agents else None
131 + logger.info(f"Agent: {agent.os if agent else 'None'}")
132 + if agent and agent.os:
133 + return await return_supported_active_responses_based_on_os(agent.os)
134 + raise HTTPException(status_code=404, detail="Agent not found")
135 +
136 +
137 @active_response_router.post(
138 "/invoke",
139 response_model=InvokeActiveResponseResponse,
backend/app/active_response/schema/active_response.py
+15 -1
@@ -2,11 +2,13 @@ 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 Field
10 from pydantic import root_validator
11 +from pydantic import validator
12
13
14 class ActiveResponsesSupported(Enum):
@@ -34,6 +36,12 @@ class ActiveResponseDetails(BaseModel):
36 json_encoders = {str: lambda v: v.encode("utf-8", "ignore").decode("utf-8")}
37
38
39 +class ActiveResponseDetailsResponse(BaseModel):
40 + success: bool
41 + message: str
42 + active_response: ActiveResponseDetails
43 +
44 +
45 # ! Invoke Active Response ! #
46 class AlertAction(str, Enum):
47 unblock = "unblock"
@@ -77,7 +85,13 @@ class ActiveResponseCommand(str, Enum):
85
86 class ParamsModel(BaseModel):
87 wait_for_complete: bool
80 - agents_list: List[str]
88 + agents_list: Optional[List[str]]
89 +
90 + @validator("agents_list", pre=True)
91 + def check_agents_list(cls, v):
92 + if v == ["*"]:
93 + return []
94 + return v
95
96
97 class InvokeActiveResponseRequest(BaseModel):
backend/app/connectors/graylog/schema/content_packs.py
+1 -1
@@ -23,7 +23,7 @@ class Configuration(BaseModel):
23
24
25 class Data(BaseModel):
26 - configuration: Optional[Configuration]
26 + # configuration: Optional[Configuration]
27 description: Optional[str] = Field(None, alias="@value")
28 name: Optional[str] = Field(None, alias="@value")
29 title: Optional[str] = Field(None, alias="@value")
backend/app/customer_provisioning/models/default_settings.py new
+13
@@ -0,0 +1,13 @@
1 +from typing import Optional
2 +
3 +from sqlmodel import Field
4 +from sqlmodel import SQLModel
5 +
6 +
7 +class CustomerProvisioningDefaultSettings(SQLModel, table=True):
8 + __tablename__ = "customer_provisioning_default_settings"
9 + id: Optional[int] = Field(primary_key=True)
10 + cluster_name: str = Field(max_length=50, nullable=False)
11 + cluster_key: str = Field(max_length=1000, nullable=False)
12 + master_ip: str = Field(max_length=50, nullable=False)
13 + grafana_url: str = Field(max_length=1024, nullable=False)
backend/app/customer_provisioning/routes/default_settings.py new
+118
@@ -0,0 +1,118 @@
1 +from fastapi import APIRouter
2 +from fastapi import Body
3 +from fastapi import Depends
4 +from fastapi import HTTPException
5 +from fastapi import Security
6 +from loguru import logger
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +from sqlalchemy.future import select
9 +
10 +from app.auth.utils import AuthHandler
11 +from app.customer_provisioning.models.default_settings import (
12 + CustomerProvisioningDefaultSettings,
13 +)
14 +from app.customer_provisioning.schema.default import (
15 + CustomerProvisioningDefaultSettingsResponse,
16 +)
17 +from app.db.db_session import get_db
18 +
19 +customer_provisioning_default_settings_router = APIRouter()
20 +
21 +
22 +@customer_provisioning_default_settings_router.get(
23 + "/default_settings",
24 + response_model=CustomerProvisioningDefaultSettingsResponse,
25 + description="Get all default settings for customer provisioning",
26 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
27 +)
28 +async def get_all_customer_provisioning_default_settings(
29 + db: AsyncSession = Depends(get_db),
30 +):
31 + stmt = select(CustomerProvisioningDefaultSettings)
32 + result = await db.execute(stmt)
33 + customer_provisioning_default_settings = result.scalars().first()
34 + if not customer_provisioning_default_settings:
35 + raise HTTPException(
36 + status_code=404,
37 + detail="No customer provisioning default settings found",
38 + )
39 + logger.info(f"Customer Provisioning Default Settings retrieved successfully: {customer_provisioning_default_settings}")
40 + return CustomerProvisioningDefaultSettingsResponse(
41 + message="Customer Provisioning Default Settings retrieved successfully",
42 + success=True,
43 + customer_provisioning_default_settings=customer_provisioning_default_settings,
44 + )
45 +
46 +
47 +@customer_provisioning_default_settings_router.post(
48 + "/default_settings",
49 + response_model=CustomerProvisioningDefaultSettings,
50 + description="Create a new default settings for customer provisioning",
51 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
52 +)
53 +async def create_customer_provisioning_default_settings(
54 + customer_provisioning_default_settings: CustomerProvisioningDefaultSettings = Body(...),
55 + db: AsyncSession = Depends(get_db),
56 +):
57 + # Check if there's already an entry
58 + stmt = select(CustomerProvisioningDefaultSettings)
59 + result = await db.execute(stmt)
60 + existing_settings = result.scalars().first()
61 +
62 + if existing_settings:
63 + raise HTTPException(status_code=400, detail="Only one settings entry is allowed")
64 +
65 + db.add(customer_provisioning_default_settings)
66 + await db.commit()
67 + await db.refresh(customer_provisioning_default_settings)
68 + return customer_provisioning_default_settings
69 +
70 +
71 +@customer_provisioning_default_settings_router.put(
72 + "/default_settings",
73 + response_model=CustomerProvisioningDefaultSettings,
74 + description="Update default settings for customer provisioning",
75 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
76 +)
77 +async def update_customer_provisioning_default_settings(
78 + customer_provisioning_default_settings: CustomerProvisioningDefaultSettings = Body(...),
79 + db: AsyncSession = Depends(get_db),
80 +):
81 + # Fetch the existing record
82 + stmt = select(CustomerProvisioningDefaultSettings).where(
83 + CustomerProvisioningDefaultSettings.id == customer_provisioning_default_settings.id,
84 + )
85 + result = await db.execute(stmt)
86 + existing_settings = result.scalars().first()
87 +
88 + if not existing_settings:
89 + raise HTTPException(status_code=404, detail="Settings not found")
90 +
91 + # Update the fields
92 + for key, value in customer_provisioning_default_settings.dict().items():
93 + setattr(existing_settings, key, value)
94 +
95 + await db.commit()
96 + await db.refresh(existing_settings)
97 + return existing_settings
98 +
99 +
100 +@customer_provisioning_default_settings_router.delete(
101 + "/default_settings",
102 + response_model=CustomerProvisioningDefaultSettings,
103 + description="Delete default settings for customer provisioning",
104 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
105 +)
106 +async def delete_customer_provisioning_default_settings(
107 + db: AsyncSession = Depends(get_db),
108 +):
109 + stmt = select(CustomerProvisioningDefaultSettings)
110 + result = await db.execute(stmt)
111 + existing_settings = result.scalars().first()
112 +
113 + if not existing_settings:
114 + raise HTTPException(status_code=404, detail="Settings not found")
115 +
116 + db.delete(existing_settings)
117 + await db.commit()
118 + return existing_settings
backend/app/customer_provisioning/routes/provision.py
+27
@@ -16,7 +16,10 @@ from app.customer_provisioning.schema.provision import CustomerSubsctipion
16 from app.customer_provisioning.schema.provision import GetDashboardsResponse
17 from app.customer_provisioning.schema.provision import GetSubscriptionsResponse
18 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
19 +from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerRequest
20 +from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerResponse
21 from app.customer_provisioning.services.provision import provision_wazuh_customer
22 +from app.customer_provisioning.services.provision import provision_wazuh_worker
23 from app.db.db_session import get_db
24 from app.db.universal_models import Customers
25 from app.db.universal_models import CustomersMeta
@@ -215,6 +218,30 @@ async def provision_customer_route(
218 return customer_provision
219
220
221 +@customer_provisioning_router.post(
222 + "/provision/wazuh_worker",
223 + response_model=ProvisionWorkerResponse,
224 + description="Provision Wazuh Worker",
225 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
226 +)
227 +async def provision_wazuh_worker_route(
228 + request: ProvisionWorkerRequest = Body(...),
229 + session: AsyncSession = Depends(get_db),
230 +):
231 + """
232 + Provisions a new Wazuh worker.
233 +
234 + Args:
235 + request (ProvisionWorkerRequest): The request data for provisioning a new Wazuh worker.
236 + session (AsyncSession): The database session.
237 +
238 + Returns:
239 + ProvisionWorkerResponse: The response data for the provisioned Wazuh worker.
240 + """
241 + logger.info("Provisioning Wazuh worker")
242 + return await provision_wazuh_worker(request, session=session)
243 +
244 +
245 @customer_provisioning_router.get(
246 "/provision/dashboards",
247 response_model=GetDashboardsResponse,
backend/app/customer_provisioning/schema/default.py new
+23
@@ -0,0 +1,23 @@
1 +from pydantic import BaseModel
2 +from pydantic import Field
3 +
4 +from app.customer_provisioning.models.default_settings import (
5 + CustomerProvisioningDefaultSettings,
6 +)
7 +
8 +
9 +class CustomerProvisioningDefaultSettingsResponse(BaseModel):
10 + message: str = Field(
11 + ...,
12 + example="Customer Provisioning Default Settings retrieved successfully",
13 + description="Message indicating the customer provisioning default settings were retrieved successfully",
14 + )
15 + success: bool = Field(
16 + ...,
17 + example=True,
18 + description="Whether the customer provisioning default settings were retrieved successfully or not",
19 + )
20 + customer_provisioning_default_settings: CustomerProvisioningDefaultSettings = Field(
21 + ...,
22 + description="Customer Provisioning Default Settings",
23 + )
backend/app/db/all_models.py
+3
@@ -14,3 +14,6 @@ from app.integrations.models.customer_integration_settings import CustomerIntegr
14 from app.schedulers.models.scheduler import JobMetadata
15 from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
16 from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
17 +from app.customer_provisioning.models.default_settings import (
18 + CustomerProvisioningDefaultSettings,
19 +)
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+58
@@ -26,6 +26,9 @@ from app.integrations.monitoring_alert.schema.monitoring_alert import (
26 from app.integrations.monitoring_alert.schema.monitoring_alert import (
27 MonitoringWazuhAlertsRequestModel,
28 )
29 +from app.integrations.monitoring_alert.services.office365 import (
30 + analyze_office365_exchange_online_alerts,
31 +)
32 from app.integrations.monitoring_alert.services.suricata import analyze_suricata_alerts
33 from app.integrations.monitoring_alert.services.wazuh import analyze_wazuh_alerts
34 from app.integrations.sap_siem.services.sap_siem_multiple_logins import (
@@ -56,6 +59,13 @@ async def get_customer_meta(customer_code: str, session: AsyncSession) -> Custom
59 )
60 customer_meta = customer_meta.scalars().first()
61
62 + if not customer_meta:
63 + logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {customer_code}")
64 + customer_meta = await session.execute(
65 + select(CustomersMeta).where(CustomersMeta.customer_meta_office365_organization_id == customer_code),
66 + )
67 + customer_meta = customer_meta.scalars().first()
68 +
69 if not customer_meta:
70 raise HTTPException(status_code=404, detail="Customer not found")
71
@@ -235,6 +245,54 @@ async def run_suricata_analysis(
245 )
246
247
248 +@monitoring_alerts_router.post(
249 + "/run_analysis/office365/exchange_online",
250 + response_model=AlertAnalysisResponse,
251 +)
252 +async def run_office365_exchange_online_analysis(
253 + request: MonitoringWazuhAlertsRequestModel,
254 + session: AsyncSession = Depends(get_db),
255 +) -> AlertAnalysisResponse:
256 + """
257 + This route is used to run analysis on the monitoring alerts.
258 +
259 + 1. Get all the monitoring alerts from the database where the customer_code matches the customer_code provided
260 + and the alert_source is OFFICE365_EXCHANGE_ONLINE.
261 +
262 + 2. Call the analyze_office365_exchange_online_alerts function to analyze the alerts.
263 +
264 + Args:
265 + request (MonitoringWazuhAlertsRequestModel): The customer code.
266 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
267 +
268 + Returns:
269 + WazuhAnalysisResponse: The response containing the analysis results.
270 + """
271 + logger.info(f"Running analysis for customer_code: {request.customer_code}")
272 +
273 + customer_meta = await get_customer_meta(request.customer_code, session)
274 +
275 + monitoring_alerts = await session.execute(
276 + select(MonitoringAlerts).where(
277 + (MonitoringAlerts.customer_code == request.customer_code) & (MonitoringAlerts.alert_source == "OFFICE365_EXCHANGE_ONLINE"),
278 + ),
279 + )
280 + monitoring_alerts = monitoring_alerts.scalars().all()
281 +
282 + logger.info(f"Found {len(monitoring_alerts)} monitoring alerts")
283 +
284 + if not monitoring_alerts:
285 + raise HTTPException(status_code=404, detail="No monitoring alerts found")
286 +
287 + # Call the analyze_office365_exchange_online_alerts function to analyze the alerts
288 + await analyze_office365_exchange_online_alerts(monitoring_alerts, customer_meta, session)
289 +
290 + return AlertAnalysisResponse(
291 + success=True,
292 + message="Analysis completed successfully",
293 + )
294 +
295 +
296 @monitoring_alerts_router.post(
297 "/run_analysis/sap_siem/suspicious_logins",
298 response_model=AlertAnalysisResponse,
backend/app/integrations/monitoring_alert/routes/provision.py
+37 -4
@@ -14,6 +14,12 @@ from app.integrations.monitoring_alert.schema.provision import (
14 from app.integrations.monitoring_alert.schema.provision import (
15 ProvisionWazuhMonitoringAlertResponse,
16 )
17 +from app.integrations.monitoring_alert.services.provision import (
18 + provision_office365_exchange_online_alert,
19 +)
20 +from app.integrations.monitoring_alert.services.provision import (
21 + provision_office365_threat_intel_alert,
22 +)
23 from app.integrations.monitoring_alert.services.provision import (
24 provision_suricata_monitoring_alert,
25 )
@@ -57,10 +63,40 @@ async def invoke_provision_suricata_monitoring_alert(
63 )
64
65
66 +async def invoke_provision_office365_exchange_online_alert(
67 + request: ProvisionMonitoringAlertRequest,
68 +):
69 + # Provision the Office365 Exchange Online monitoring alert
70 + await provision_office365_exchange_online_alert(request)
71 + await add_scheduler_jobs(
72 + CreateSchedulerRequest(
73 + function_name="invoke_office365_exchange_online_alert",
74 + time_interval=5,
75 + job_id="invoke_office365_exchange_online_alert",
76 + ),
77 + )
78 +
79 +
80 +async def invoke_provision_office365_threat_intel_alert(
81 + request: ProvisionMonitoringAlertRequest,
82 +):
83 + # Provision the Office365 Threat Intel monitoring alert
84 + await provision_office365_threat_intel_alert(request)
85 + await add_scheduler_jobs(
86 + CreateSchedulerRequest(
87 + function_name="invoke_office365_threat_intel_alert",
88 + time_interval=5,
89 + job_id="invoke_office365_threat_intel_alert",
90 + ),
91 + )
92 +
93 +
94 # Create a dictionary that maps alert names to provision functions
95 PROVISION_FUNCTIONS = {
96 "WAZUH_SYSLOG_LEVEL_ALERT": invoke_provision_wazuh_monitoring_alert,
97 "SURICATA_ALERT_SEVERITY_1": invoke_provision_suricata_monitoring_alert,
98 + "OFFICE365_EXCHANGE_ONLINE": invoke_provision_office365_exchange_online_alert,
99 + "OFFICE365_THREAT_INTEL": invoke_provision_office365_threat_intel_alert,
100 # Add more alert names and functions as needed
101 }
102
@@ -134,10 +170,7 @@ async def provision_monitoring_alert_route(
170 # Invoke the provision function
171 await provision_function(request)
172
137 - return ProvisionWazuhMonitoringAlertResponse(
138 - success=True,
139 - message="Wazuh monitoring alerts provisioned.",
140 - )
173 + return ProvisionWazuhMonitoringAlertResponse(success=True, message=f"Monitoring alert {request.alert_name} provisioned successfully.")
174
175
176 @monitoring_alerts_provision_router.post(
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py
+201
@@ -545,3 +545,204 @@ class SuricataIrisAlertPayload(BaseModel):
545
546 def to_dict(self):
547 return self.dict(exclude_none=True)
548 +
549 +
550 +########### ! Office365 ALERTS SCHEMA ! ###########
551 +class Office365SourceModel(BaseModel):
552 + alert_signature: str = Field(..., description="Signature of the alert")
553 + alert_severity: int = Field(..., description="Severity level of the alert")
554 + alert_signature_id: int = Field(..., description="Signature ID of the alert")
555 + src_ip: str = Field(..., description="Source IP address")
556 + dest_ip: str = Field(..., description="Destination IP address")
557 + app_proto: str = Field(..., description="Application protocol")
558 + agent_labels_customer: str = Field(..., description="Customer of the agent")
559 + timestamp: str = Field(..., description="The timestamp of the alert.")
560 + timestamp_utc: Optional[str] = Field(
561 + ...,
562 + description="The UTC timestamp of the alert.",
563 + )
564 + time_field: Optional[str] = Field(
565 + "timestamp",
566 + description="The timefield of the alert to be used when creating the IRIS alert.",
567 + )
568 + date: Optional[float] = Field(
569 + None,
570 + description="Date of the alert in Unix timestamp",
571 + )
572 + alert_metadata_tag: Optional[str] = Field(
573 + None,
574 + description="Metadata tag for the alert",
575 + )
576 + alert_gid: Optional[int] = Field(None, description="Alert group ID")
577 +
578 + class Config:
579 + allow_population_by_field_name = True
580 + extra = Extra.allow
581 +
582 + def to_dict(self):
583 + return self.dict(exclude_none=True)
584 +
585 +
586 +class Office365AlertModel(BaseModel):
587 + _index: str
588 + _id: str
589 + _version: int
590 + _source: Office365SourceModel
591 + asset_type_id: Optional[int] = Field(
592 + None,
593 + description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
594 + )
595 + ioc_value: Optional[str] = Field(
596 + None,
597 + description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
598 + )
599 + ioc_type: Optional[str] = Field(
600 + None,
601 + description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
602 + )
603 +
604 + class Config:
605 + extra = Extra.allow
606 +
607 +
608 +########### ! Create Suricata Alerts In IRIS Schemas ! ###########
609 +class Office365IrisAsset(BaseModel):
610 + asset_name: Optional[str] = Field(
611 + "Asset Does Not Apply to Suricata Alerts",
612 + description="Name of the asset",
613 + example="Server01",
614 + )
615 + asset_ip: Optional[str] = Field(
616 + "Asset Does Not Apply to Suricata Alerts",
617 + description="IP address of the asset",
618 + example="192.168.1.1",
619 + )
620 + asset_description: Optional[str] = Field(
621 + "Asset Does Not Apply to Suricata Alerts",
622 + description="Description of the asset",
623 + example="Windows Server",
624 + )
625 + asset_type_id: Optional[int] = Field(
626 + 9,
627 + description="Type ID of the asset",
628 + example=1,
629 + )
630 +
631 + def to_dict(self):
632 + return self.dict(exclude_none=True)
633 +
634 +
635 +class Office365IrisIoc(BaseModel):
636 + ioc_value: str = Field(
637 + ...,
638 + description="Value of the IoC",
639 + example="www.google.com",
640 + )
641 + ioc_description: str = Field(
642 + ...,
643 + description="Description of the IoC",
644 + example="Google",
645 + )
646 + ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
647 + ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
648 +
649 +
650 +class Office365IrisAlertContext(BaseModel):
651 + _source: Office365SourceModel
652 + alert_id: str = Field(..., description="ID of the alert", example="123")
653 + alert_name: str = Field(
654 + ...,
655 + description="Name of the alert",
656 + example="Intrusion Detected",
657 + )
658 + alert_level: int = Field(..., description="Severity level of the alert", example=3)
659 + rule_id: int = Field(
660 + ...,
661 + description="ID of the Suricata rule that triggered the alert",
662 + example="2001",
663 + )
664 + src_ip: str = Field(
665 + ...,
666 + description="Source IP address of the alert",
667 + example="1.1.1.1",
668 + )
669 + dest_ip: str = Field(
670 + ...,
671 + description="Destination IP address of the alert",
672 + example="8.8.8.8",
673 + )
674 + app_proto: str = Field(
675 + ...,
676 + description="Application protocol of the alert",
677 + example="TCP",
678 + )
679 + agent_labels_customer: str = Field(
680 + ...,
681 + description="Customer of the endpoint",
682 + example="SOCFortress",
683 + )
684 + customer_iris_id: Optional[int] = Field(
685 + None,
686 + description="IRIS ID of the customer",
687 + )
688 + customer_name: Optional[str] = Field(
689 + None,
690 + description="Name of the customer",
691 + )
692 + customer_cases_index: Optional[str] = Field(
693 + None,
694 + description="IRIS case index name in the Wazuh-Indexer",
695 + )
696 + time_field: Optional[str] = Field(
697 + "timestamp_utc",
698 + description="The timefield of the alert to be used when creating the IRIS alert.",
699 + )
700 +
701 + def to_dict(self):
702 + return self.dict(exclude_none=True)
703 +
704 +
705 +class Office365IrisAlertPayload(BaseModel):
706 + alert_title: str = Field(
707 + ...,
708 + description="Title of the alert",
709 + example="Intrusion Detected",
710 + )
711 + alert_description: str = Field(
712 + ...,
713 + description="Description of the alert",
714 + example="Intrusion Detected by Firewall",
715 + )
716 + alert_source: str = Field(..., description="Source of the alert", example="Suricata")
717 + assets: List[SuricataIrisAsset] = Field(..., description="List of affected assets")
718 + alert_status_id: int = Field(..., description="Status ID of the alert", example=3)
719 + alert_severity_id: int = Field(
720 + ...,
721 + description="Severity ID of the alert",
722 + example=5,
723 + )
724 + alert_customer_id: int = Field(
725 + ...,
726 + description="Customer ID related to the alert",
727 + example=1,
728 + )
729 + alert_source_content: Dict[str, Any] = Field(
730 + ...,
731 + description="Original content from the alert source",
732 + )
733 + alert_context: SuricataIrisAlertContext = Field(
734 + ...,
735 + description="Contextual information about the alert",
736 + )
737 + alert_iocs: Optional[List[IrisIoc]] = Field(
738 + None,
739 + description="List of IoCs related to the alert",
740 + )
741 + alert_source_event_time: str = Field(
742 + ...,
743 + description="Timestamp of the alert",
744 + example="2021-01-01T00:00:00.000Z",
745 + )
746 +
747 + def to_dict(self):
748 + return self.dict(exclude_none=True)
backend/app/integrations/monitoring_alert/schema/provision.py
+10
@@ -23,6 +23,16 @@ class AvailableMonitoringAlerts(str, Enum):
23 "This alert monitors the Suricata logs. When an the alert_severity field is 1, it triggers "
24 "an alert that is created within DFIR-IRIS. Ensure that you have a pipeline rule that sets "
25 )
26 + OFFICE365_EXCHANGE_ONLINE = (
27 + "This alert monitors the Office365 Exchange events. When an alert is detected, it triggers an "
28 + "alert that is created within DFIR-IRIS. Ensure that you have a pipeline rule that sets the "
29 + "alert_severity field to 1 when the Office365 alert is detected."
30 + )
31 + OFFICE365_THREAT_INTEL = (
32 + "This alert monitors the Office365 Threat Intelligence events. When an alert is detected, it triggers an "
33 + "alert that is created within DFIR-IRIS. Ensure that you have a pipeline rule that sets the "
34 + "alert_severity field to 1 when the Office365 alert is detected."
35 + )
36
37
38 class AvailableMonitoringAlertsResponse(BaseModel):
backend/app/integrations/monitoring_alert/services/office365.py new
+572
@@ -0,0 +1,572 @@
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 + Office365AlertModel,
34 +)
35 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
36 + Office365IrisAlertContext,
37 +)
38 +from app.integrations.monitoring_alert.schema.monitoring_alert import (
39 + Office365IrisAlertPayload,
40 +)
41 +from app.integrations.monitoring_alert.schema.monitoring_alert import Office365IrisAsset
42 +from app.integrations.monitoring_alert.utils.db_operations import remove_alert_id
43 +from app.integrations.utils.alerts import validate_ioc_type
44 +from app.utils import get_customer_alert_settings
45 +
46 +
47 +def valid_ioc_fields() -> Set[str]:
48 + """
49 + Getter for the set of valid IoC fields.
50 + Returns
51 + -------
52 + Set[str]
53 + The set of valid IoC fields.
54 + """
55 + return {field.value for field in ValidIocFields}
56 +
57 +
58 +async def construct_alert_source_link(
59 + alert_details: Office365IrisAlertContext,
60 + session: AsyncSession,
61 +) -> str:
62 + """
63 + Construct the alert source link for the alert details.
64 + Parameters
65 + ----------
66 + alert_details: CreateAlertRequest
67 + The alert details.
68 + Returns
69 + -------
70 + str
71 + The alert source link.
72 + """
73 + logger.info(f"Constructing alert source link for alert: {alert_details}")
74 + query_string = f"%22query%22:%22alert_signature_id:%5C%22{alert_details.alert_id}%5C%22%20AND%20"
75 + grafana_url = (
76 + await get_customer_alert_settings(
77 + customer_code=alert_details.agent_labels_customer,
78 + session=session,
79 + )
80 + ).grafana_url
81 +
82 + return (
83 + f"{grafana_url}/explore?left=%5B%22now-6h%22,%22now%22,%22Office365%22,%7B%22refId%22:%22A%22,"
84 + f"{query_string}"
85 + f"src_ip:%5C%22{alert_details.src_ip}%5C%22%22,"
86 + "%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,"
87 + "%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
88 + )
89 +
90 +
91 +async def build_ioc_payload(alert_details: CreateAlertRequest) -> Optional[IrisIoc]:
92 + """
93 + Builds an IoC payload based on the provided alert details.
94 +
95 + Args:
96 + alert_details (CreateAlertRequest): The details of the alert.
97 +
98 + Returns:
99 + Optional[IrisIoc]: The constructed IoC payload, or None if no valid IoC fields are found.
100 + """
101 + for field in valid_ioc_fields():
102 + if hasattr(alert_details, field):
103 + ioc_value = getattr(alert_details, field)
104 + ioc_type = await validate_ioc_type(ioc_value=ioc_value)
105 + return IrisIoc(
106 + ioc_value=ioc_value,
107 + ioc_description="IoC found in alert",
108 + ioc_tlp_id=1,
109 + ioc_type_id=ioc_type,
110 + )
111 + return None
112 +
113 +
114 +async def build_asset_payload(
115 + alert_details: Office365IrisAlertContext,
116 + session: AsyncSession,
117 +) -> Office365IrisAsset:
118 + """
119 + Build the payload for an IrisAsset object based on the agent data and alert details.
120 +
121 + Args:
122 + agent_data (AgentsResponse): The response containing agent data.
123 + alert_details: The details of the alert.
124 +
125 + Returns:
126 + IrisAsset: The constructed IrisAsset object.
127 + """
128 + # Get the agent_id based on the hostname from the Agents table
129 + logger.info(f"Building asset payload for alert: {alert_details}")
130 + if alert_details is not None:
131 + return Office365IrisAsset(
132 + asset_name=alert_details.src_ip,
133 + asset_ip=alert_details.src_ip,
134 + asset_description=await construct_alert_source_link(
135 + alert_details,
136 + session=session,
137 + ),
138 + asset_type_id=2,
139 + )
140 + return Office365IrisAsset()
141 +
142 +
143 +async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> Office365AlertModel:
144 + """
145 + Fetch the Office365 alert details from the Wazuh-Indexer.
146 +
147 + Args:
148 + alert_id (str): The alert ID.
149 + index (str): The index.
150 +
151 + Returns:
152 + CollectAlertsResponse: The response from the Wazuh-Indexer.
153 + """
154 + logger.info(
155 + f"Fetching Office365 alert details for alert_id: {alert_id} and index: {index}",
156 + )
157 +
158 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
159 + response = es_client.get(index=index, id=alert_id)
160 +
161 + return Office365AlertModel(**response)
162 +
163 +
164 +async def fetch_alert_details(alert: MonitoringAlerts) -> Office365AlertModel:
165 + logger.info(f"Analyzing Office365 Exchange Online alert: {alert}")
166 + alert_details = await fetch_wazuh_indexer_details(alert.alert_id, alert.alert_index)
167 + logger.info(f"Alert details: {alert_details}")
168 + return alert_details
169 +
170 +
171 +async def check_event_exclusion(
172 + alert_details: Office365AlertModel,
173 + alert_detail_service: AlertDetailsService,
174 + session: AsyncSession,
175 +):
176 + logger.info("Checking if alert is excluded due to multi exclusion.")
177 + logger.info(f"Alert details: {alert_details}")
178 + event_exclude_result = await alert_detail_service.collect_alert_timeline_process_id(
179 + agent_name=alert_details._source["agent_name"],
180 + process_id=alert_details._source.get("process_id", "n/a"),
181 + index=alert_details._index,
182 + session=session,
183 + )
184 + if event_exclude_result is True:
185 + raise HTTPException(
186 + status_code=400,
187 + detail="Alert excluded due to multi exclusion as set in the config.ini file.",
188 + )
189 + logger.info("Alert is not excluded due to multi exclusion.")
190 +
191 +
192 +async def check_if_open_alert_exists_in_iris(alert_details: Office365AlertModel, session: AsyncSession) -> list:
193 + """
194 + Check if the alert exists in IRIS.
195 +
196 + Args:
197 + alert_details (Office365AlertModel): The alert details.
198 + session (AsyncSession): The database session.
199 +
200 + Returns:
201 + bool: True if the alert exists in IRIS, False otherwise.
202 + """
203 + client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
204 + customer_iris_id = (
205 + await get_customer_alert_settings(
206 + # customer_code=alert_details._source["agent_labels_customer"],
207 + customer_code="00002",
208 + session=session,
209 + )
210 + ).iris_customer_id
211 + request = FilterAlertsRequest(
212 + alert_tags=alert_details._source["alert_signature_id"],
213 + alert_customer_id=customer_iris_id,
214 + )
215 + params = construct_params(request)
216 + alert_exists = await fetch_and_validate_data(
217 + client,
218 + lambda: alert_client.filter_alerts(**params),
219 + )
220 + logger.info(f"Alert exists: {alert_exists['data']['alerts']}")
221 + return alert_exists["data"]["alerts"][0]["alert_id"] if alert_exists["data"]["alerts"] else []
222 +
223 +
224 +def construct_params(request: FilterAlertsRequest) -> dict:
225 + """
226 + Constructs the parameters for the alert filtering request.
227 +
228 + Args:
229 + request (FilterAlertsRequest): The request object containing filtering criteria.
230 +
231 + Returns:
232 + dict: A dictionary of parameters for the alert filtering request.
233 + """
234 + params = {
235 + "page": request.page,
236 + "per_page": request.per_page,
237 + "sort": request.sort,
238 + "alert_tags": request.alert_tags,
239 + "alert_status_id": request.alert_status_id,
240 + "alert_customer_id": request.alert_customer_id,
241 + # Add more parameters here as needed
242 + }
243 +
244 + # Remove parameters that have a value of None
245 + return {k: v for k, v in params.items() if v is not None}
246 +
247 +
248 +async def build_alert_context_payload(
249 + alert_details: Office365IrisAlertContext,
250 + session: AsyncSession,
251 +) -> Office365IrisAlertContext:
252 + """
253 + Builds the payload for the alert context.
254 +
255 + Args:
256 + alert_details (CreateAlertRequest): The details of the alert.
257 + agent_data (AgentsResponse): The agent data.
258 + session (AsyncSession): The async session.
259 +
260 + Returns:
261 + Office365IrisAlertContext: The built alert context payload.
262 + """
263 + return Office365IrisAlertContext(
264 + customer_iris_id=(
265 + await get_customer_alert_settings(
266 + customer_code=alert_details.agent_labels_customer,
267 + session=session,
268 + )
269 + ).iris_customer_id,
270 + customer_name=(
271 + await get_customer_alert_settings(
272 + customer_code=alert_details.agent_labels_customer,
273 + session=session,
274 + )
275 + ).customer_name,
276 + customer_cases_index=(
277 + await get_customer_alert_settings(
278 + customer_code=alert_details.agent_labels_customer,
279 + session=session,
280 + )
281 + ).iris_index,
282 + alert_id=alert_details.alert_id,
283 + alert_name=alert_details.alert_name,
284 + alert_level=alert_details.alert_level,
285 + rule_id=alert_details.rule_id,
286 + src_ip=alert_details.src_ip,
287 + dest_ip=alert_details.dest_ip,
288 + app_proto=alert_details.app_proto,
289 + agent_labels_customer=alert_details.agent_labels_customer,
290 + )
291 +
292 +
293 +async def build_alert_payload(
294 + alert_details: Office365IrisAlertContext,
295 + ioc_payload: Optional[IrisIoc],
296 + session: AsyncSession,
297 +) -> Office365IrisAlertPayload:
298 + """
299 + Builds the payload for an alert based on the provided alert details, agent data, IoC payload, and session.
300 +
301 + Args:
302 + alert_details (Office365AlertModel): The details of the alert.
303 + agent_data: The agent data associated with the alert.
304 + ioc_payload (Optional[IrisIoc]): The IoC payload associated with the alert.
305 + session (AsyncSession): The session used for database operations.
306 +
307 + Returns:
308 + Office365IrisAlertPayload: The built alert payload.
309 + """
310 + asset_payload = await build_asset_payload(
311 + alert_details=alert_details,
312 + session=session,
313 + )
314 + logger.info(f"Asset payload: {asset_payload}")
315 +
316 + context_payload = await build_alert_context_payload(
317 + alert_details=alert_details,
318 + session=session,
319 + )
320 +
321 + logger.info(f"Alert has context: {context_payload}")
322 +
323 + if ioc_payload:
324 + logger.info(f"Alert has IoC: {ioc_payload}")
325 + return Office365IrisAlertPayload(
326 + alert_title=alert_details.alert_name,
327 + alert_description=alert_details.alert_name,
328 + alert_source="COPILOT Office365 ANALYSIS",
329 + assets=[asset_payload],
330 + alert_status_id=3,
331 + alert_severity_id=5,
332 + alert_customer_id=(
333 + await get_customer_alert_settings(
334 + customer_code=alert_details.agent_labels_customer,
335 + session=session,
336 + )
337 + ).iris_customer_id,
338 + alert_source_content=alert_details.to_dict(),
339 + alert_context=context_payload,
340 + alert_iocs=[ioc_payload],
341 + alert_source_event_time=alert_details.time_field,
342 + )
343 + else:
344 + logger.info("Alert does not have IoC")
345 + return Office365IrisAlertPayload(
346 + alert_title=alert_details.alert_name,
347 + alert_description=alert_details.alert_name,
348 + alert_source="COPILOT Office365 ANALYSIS",
349 + assets=[asset_payload],
350 + alert_status_id=3,
351 + alert_severity_id=5,
352 + alert_customer_id=(
353 + await get_customer_alert_settings(
354 + customer_code=alert_details.agent_labels_customer,
355 + session=session,
356 + )
357 + ).iris_customer_id,
358 + alert_source_content=alert_details.to_dict(),
359 + alert_context=context_payload,
360 + alert_source_event_time=alert_details.time_field,
361 + )
362 +
363 +
364 +async def create_alert_details(
365 + alert_details: Office365AlertModel,
366 +) -> Office365IrisAlertContext:
367 + """
368 + Create an alert details object from the Office365 alert details.
369 +
370 + Args:
371 + alert_details (Office365AlertModel): The Office365 alert details.
372 +
373 + Returns:
374 + Office365IrisAlertContext: The alert details object.
375 + """
376 + logger.info(f"Creating alert details for alert: {alert_details}")
377 + return Office365IrisAlertContext(
378 + index=alert_details._index,
379 + id=alert_details._id,
380 + alert_id=alert_details._source["alert_signature_id"],
381 + alert_name=alert_details._source["alert_signature"],
382 + alert_level=alert_details._source["alert_severity"],
383 + rule_id=alert_details._source["alert_signature_id"],
384 + src_ip=alert_details._source["src_ip"],
385 + dest_ip=alert_details._source["dest_ip"],
386 + app_proto=alert_details._source.get(
387 + "app_proto",
388 + "No application protocol found",
389 + ),
390 + agent_labels_customer=alert_details._source["agent_labels_customer"],
391 + time_field=alert_details._source.get("timestamp_utc", alert_details._source.get("timestamp")),
392 + )
393 +
394 +
395 +async def create_and_update_alert_in_iris(
396 + alert_details: Office365AlertModel,
397 + session: AsyncSession,
398 +) -> int:
399 + """
400 + Creates the alert, then updates the alert with the asset and IoC if available.
401 +
402 + Args:
403 + alert_details (Office365AlertModel): The details of the alert.
404 + session (AsyncSession): The async session object.
405 +
406 + Returns:
407 + int: The ID of the created alert in IRIS.
408 + """
409 + logger.info("Alert does not exist in IRIS. Creating alert.")
410 + alert_details = await create_alert_details(alert_details)
411 + ioc_payload = await build_ioc_payload(alert_details)
412 + logger.info(f"Alert details: {alert_details}")
413 + iris_alert_payload = await build_alert_payload(
414 + alert_details=alert_details,
415 + ioc_payload=ioc_payload,
416 + session=session,
417 + )
418 + logger.info(f"Alert payload: {iris_alert_payload}")
419 +
420 + client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
421 + result = await fetch_and_validate_data(
422 + client,
423 + alert_client.add_alert,
424 + iris_alert_payload.to_dict(),
425 + )
426 + alert_id = result["data"]["alert_id"]
427 + logger.info(f"Successfully created alert {alert_id} in IRIS.")
428 +
429 + await fetch_and_validate_data(
430 + client,
431 + alert_client.update_alert,
432 + alert_id,
433 + {"alert_tags": f"{alert_details.alert_id}"},
434 + )
435 + # Update the alert with the asset payload
436 + await fetch_and_validate_data(
437 + client,
438 + alert_client.update_alert,
439 + alert_id,
440 + {"assets": [dict(Office365IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
441 + )
442 + if ioc_payload:
443 + await fetch_and_validate_data(
444 + client,
445 + alert_client.update_alert,
446 + alert_id,
447 + {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
448 + )
449 + return alert_id
450 +
451 +
452 +async def get_current_assets(client, alert_client, iris_alert_id):
453 + result = await fetch_and_validate_data(
454 + client,
455 + alert_client.get_alert,
456 + iris_alert_id,
457 + )
458 + return result["data"]["assets"]
459 +
460 +
461 +async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
462 + await fetch_and_validate_data(
463 + client,
464 + alert_client.update_alert,
465 + iris_alert_id,
466 + {"assets": current_assets},
467 + )
468 +
469 +
470 +async def remove_duplicate_assets(current_assets):
471 + """
472 + Removes duplicate assets from the given list of current_assets.
473 +
474 + Args:
475 + current_assets (list): A list of dictionaries representing current assets.
476 +
477 + Returns:
478 + list: A list of dictionaries with duplicate assets removed.
479 + """
480 + current_assets = list({d["asset_name"]: d for d in current_assets}.values())
481 + current_assets_str = [json.dumps(d, sort_keys=True) for d in current_assets]
482 + current_assets_str = list(set(current_assets_str))
483 + current_assets = [json.loads(s) for s in current_assets_str]
484 + return current_assets
485 +
486 +
487 +async def analyze_office365_exchange_online_alerts(
488 + monitoring_alerts: MonitoringAlerts,
489 + customer_meta: CustomersMeta,
490 + session: AsyncSession,
491 +) -> AlertAnalysisResponse:
492 + """
493 + Analyze the given Office365 Exchange Online Alert and create an alert if necessary. Otherwise update the existing alert with the asset.
494 +
495 + 1. For each alert, extract the metadata from the Wazuh-Indexer.
496 + 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.
497 + The alert will contain the asset and IoC if available.
498 + 3. Get the current list of assets from the alert to avoid overwriting them.
499 +
500 + Args:
501 + monitoring_alerts (MonitoringAlerts): The monitoring alert details.
502 + session (AsyncSession): The database session.
503 +
504 + Returns:
505 + AlertAnalysisResponse: The analysis response.
506 + """
507 + logger.info(f"Analyzing Office365 Exchange Online alerts: {monitoring_alerts}")
508 + for alert in monitoring_alerts:
509 + alert_details = await fetch_alert_details(alert)
510 + iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details, session=session)
511 + if iris_alert_id == []:
512 + logger.info(
513 + f"Alert {alert_details._id} does not exist in IRIS. Creating alert.",
514 + )
515 + iris_alert_id = await create_and_update_alert_in_iris(
516 + alert_details,
517 + session,
518 + )
519 + logger.info(f"Alert {iris_alert_id} created in IRIS.")
520 + await remove_alert_id(alert.alert_id, session)
521 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
522 + await add_alert_to_document(
523 + es_client=es_client,
524 + alert=AddAlertRequest(
525 + alert_id=alert_details._id,
526 + index_name=alert_details._index,
527 + ),
528 + soc_alert_id=iris_alert_id,
529 + session=session,
530 + )
531 +
532 + else:
533 + logger.info(
534 + f"Alert {iris_alert_id} exists in IRIS. Updating alert with the asset.",
535 + )
536 +
537 + # Fetch the current list of assets from the alert to avoid overwriting them
538 + client, alert_client = await initialize_client_and_alert("DFIR-IRIS")
539 + current_assets = await get_current_assets(
540 + client,
541 + alert_client,
542 + iris_alert_id,
543 + )
544 + alert_details = await create_alert_details(alert_details)
545 + asset_payload = await build_asset_payload(
546 + alert_details=alert_details,
547 + session=session,
548 + )
549 + current_assets.append(dict(Office365IrisAsset(**asset_payload.to_dict())))
550 + current_assets = await remove_duplicate_assets(current_assets)
551 + await update_alert_with_assets(
552 + client,
553 + alert_client,
554 + iris_alert_id,
555 + current_assets,
556 + )
557 + await remove_alert_id(alert.alert_id, session)
558 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
559 + await add_alert_to_document(
560 + es_client=es_client,
561 + alert=AddAlertRequest(
562 + alert_id=alert.alert_id,
563 + index_name=alert.alert_index,
564 + ),
565 + soc_alert_id=iris_alert_id,
566 + session=session,
567 + )
568 +
569 + return AlertAnalysisResponse(
570 + success=True,
571 + message="Office365 alerts analyzed successfully",
572 + )
backend/app/integrations/monitoring_alert/services/provision.py
+238
@@ -485,3 +485,241 @@ async def provision_suricata_monitoring_alert(
485 success=True,
486 message="Suricata monitoring alerts provisioned successfully",
487 )
488 +
489 +
490 +async def provision_office365_exchange_online_alert(
491 + request: ProvisionMonitoringAlertRequest,
492 +) -> ProvisionWazuhMonitoringAlertResponse:
493 + """
494 + Provisions Office365 Exchange Online monitoring alerts.
495 +
496 + Returns:
497 + ProvisionWazuhMonitoringAlertResponse: The response indicating the success of provisioning the monitoring alerts.
498 + """
499 + #
500 + logger.info(
501 + f"Invoking provision_office365_exchange_online_alert with request: {request.dict()}",
502 + )
503 + notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
504 + if not notification_exists:
505 + # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
506 + # ! Therefore, we need to send to API port of Copilot over HTTP
507 + url_whitelisted = await check_if_url_whitelist_entry_exists(
508 + f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
509 + )
510 + if not url_whitelisted:
511 + logger.info("Provisioning URL Whitelist")
512 + whitelisted_urls = await build_url_whitelisted_entries(
513 + whitelist_url_model=GraylogUrlWhitelistEntryConfig(
514 + id=await generate_random_id(),
515 + value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
516 + title="SEND TO COPILOT",
517 + type="literal",
518 + ),
519 + )
520 + await provision_webhook_url_whitelist(whitelisted_urls)
521 +
522 + logger.info("Provisioning SEND TO COPILOT Webhook")
523 + notification_id = await provision_webhook(
524 + GraylogAlertWebhookNotificationModel(
525 + title="SEND TO COPILOT",
526 + description="Send alert to Copilot",
527 + config={
528 + "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
529 + "type": "http-notification-v1",
530 + },
531 + ),
532 + )
533 + logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
534 + notification_id = await get_notification_id("SEND TO COPILOT")
535 + await provision_alert_definition(
536 + GraylogAlertProvisionModel(
537 + title="OFFICE365 EXCHANGE ONLINE ALERT",
538 + description="Alert on Office365 Exchange Online alerts",
539 + priority=2,
540 + config=GraylogAlertProvisionConfig(
541 + type="aggregation-v1",
542 + query="syslog_level:ALERT AND data_office365_Subscription:Audit.Exchange",
543 + query_parameters=[],
544 + streams=[],
545 + group_by=[],
546 + series=[],
547 + conditions={
548 + "expression": None,
549 + },
550 + search_within_ms=await convert_seconds_to_milliseconds(
551 + request.search_within_last,
552 + ),
553 + execute_every_ms=await convert_seconds_to_milliseconds(
554 + request.execute_every,
555 + ),
556 + ),
557 + field_spec={
558 + "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
559 + data_type="string",
560 + providers=[
561 + GraylogAlertProvisionProvider(
562 + type="template-v1",
563 + template="${source._id}",
564 + require_values=True,
565 + ),
566 + ],
567 + ),
568 + "CUSTOMER_CODE": GraylogAlertProvisionFieldSpecItem(
569 + data_type="string",
570 + providers=[
571 + GraylogAlertProvisionProvider(
572 + type="template-v1",
573 + template="${source.data_office365_OrganizationId}",
574 + require_values=True,
575 + ),
576 + ],
577 + ),
578 + "ALERT_SOURCE": GraylogAlertProvisionFieldSpecItem(
579 + data_type="string",
580 + providers=[
581 + GraylogAlertProvisionProvider(
582 + type="template-v1",
583 + template="OFFICE365_EXCHANGE_ONLINE",
584 + require_values=True,
585 + ),
586 + ],
587 + ),
588 + },
589 + key_spec=[],
590 + notification_settings=GraylogAlertProvisionNotificationSettings(
591 + grace_period_ms=0,
592 + backlog_size=None,
593 + ),
594 + notifications=[
595 + GraylogAlertProvisionNotification(
596 + notification_id=notification_id,
597 + ),
598 + ],
599 + alert=True,
600 + ),
601 + )
602 +
603 + return ProvisionWazuhMonitoringAlertResponse(
604 + success=True,
605 + message="Office365 Exchange Online monitoring alerts provisioned successfully",
606 + )
607 +
608 +
609 +async def provision_office365_threat_intel_alert(
610 + request: ProvisionMonitoringAlertRequest,
611 +) -> ProvisionWazuhMonitoringAlertResponse:
612 + """
613 + Provisions Office365 Threat Intel monitoring alerts.
614 +
615 + Returns:
616 + ProvisionWazuhMonitoringAlertResponse: The response indicating the success of provisioning the monitoring alerts.
617 + """
618 + #
619 + logger.info(
620 + f"Invoking provision_office365_threat_intel_alert with request: {request.dict()}",
621 + )
622 + notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
623 + if not notification_exists:
624 + # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
625 + # ! Therefore, we need to send to API port of Copilot over HTTP
626 + url_whitelisted = await check_if_url_whitelist_entry_exists(
627 + f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
628 + )
629 + if not url_whitelisted:
630 + logger.info("Provisioning URL Whitelist")
631 + whitelisted_urls = await build_url_whitelisted_entries(
632 + whitelist_url_model=GraylogUrlWhitelistEntryConfig(
633 + id=await generate_random_id(),
634 + value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
635 + title="SEND TO COPILOT",
636 + type="literal",
637 + ),
638 + )
639 + await provision_webhook_url_whitelist(whitelisted_urls)
640 +
641 + logger.info("Provisioning SEND TO COPILOT Webhook")
642 + notification_id = await provision_webhook(
643 + GraylogAlertWebhookNotificationModel(
644 + title="SEND TO COPILOT",
645 + description="Send alert to Copilot",
646 + config={
647 + "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
648 + "type": "http-notification-v1",
649 + },
650 + ),
651 + )
652 + logger.info(f"SEND TO COPILOT Webhook provisioned with id: {notification_id}")
653 + notification_id = await get_notification_id("SEND TO COPILOT")
654 + await provision_alert_definition(
655 + GraylogAlertProvisionModel(
656 + title="OFFICE365 THREAT INTEL ALERT",
657 + description="Alert on Office365 Threat Intel alerts",
658 + priority=2,
659 + config=GraylogAlertProvisionConfig(
660 + type="aggregation-v1",
661 + query="syslog_level:ALERT AND data_office365_UserId:ThreatIntel",
662 + query_parameters=[],
663 + streams=[],
664 + group_by=[],
665 + series=[],
666 + conditions={
667 + "expression": None,
668 + },
669 + search_within_ms=await convert_seconds_to_milliseconds(
670 + request.search_within_last,
671 + ),
672 + execute_every_ms=await convert_seconds_to_milliseconds(
673 + request.execute_every,
674 + ),
675 + ),
676 + field_spec={
677 + "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
678 + data_type="string",
679 + providers=[
680 + GraylogAlertProvisionProvider(
681 + type="template-v1",
682 + template="${source._id}",
683 + require_values=True,
684 + ),
685 + ],
686 + ),
687 + "CUSTOMER_CODE": GraylogAlertProvisionFieldSpecItem(
688 + data_type="string",
689 + providers=[
690 + GraylogAlertProvisionProvider(
691 + type="template-v1",
692 + template="${source.data_office365_OrganizationId}",
693 + require_values=True,
694 + ),
695 + ],
696 + ),
697 + "ALERT_SOURCE": GraylogAlertProvisionFieldSpecItem(
698 + data_type="string",
699 + providers=[
700 + GraylogAlertProvisionProvider(
701 + type="template-v1",
702 + template="OFFICE365_THREAT_INTEL",
703 + require_values=True,
704 + ),
705 + ],
706 + ),
707 + },
708 + key_spec=[],
709 + notification_settings=GraylogAlertProvisionNotificationSettings(
710 + grace_period_ms=0,
711 + backlog_size=None,
712 + ),
713 + notifications=[
714 + GraylogAlertProvisionNotification(
715 + notification_id=notification_id,
716 + ),
717 + ],
718 + alert=True,
719 + ),
720 + )
721 +
722 + return ProvisionWazuhMonitoringAlertResponse(
723 + success=True,
724 + message="Office365 Threat Intel monitoring alerts provisioned successfully",
725 + )
backend/app/integrations/monitoring_alert/services/suricata.py
+1 -2
@@ -201,8 +201,7 @@ async def check_if_open_alert_exists_in_iris(alert_details: SuricataAlertModel,
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 - customer_code="00002",
204 + customer_code=alert_details._source["agent_labels_customer"],
205 session=session,
206 )
207 ).iris_customer_id
backend/app/routers/customer_provisioning.py
+8
@@ -3,6 +3,9 @@ from fastapi import APIRouter
3 from app.customer_provisioning.routes.decommission import (
4 customer_decommissioning_router,
5 )
6 +from app.customer_provisioning.routes.default_settings import (
7 + customer_provisioning_default_settings_router,
8 +)
9 from app.customer_provisioning.routes.provision import customer_provisioning_router
10
11 # Instantiate the APIRouter
@@ -19,3 +22,8 @@ router.include_router(
22 prefix="/customer_provisioning",
23 tags=["Customer Provisioning"],
24 )
25 +router.include_router(
26 + customer_provisioning_default_settings_router,
27 + prefix="/customer_provisioning",
28 + tags=["Customer Provisioning"],
29 +)
backend/app/schedulers/scheduler.py
+6
@@ -16,6 +16,10 @@ from app.schedulers.services.invoke_sap_siem import (
16 from app.schedulers.services.invoke_sap_siem import (
17 invoke_sap_siem_integration_suspicious_logins_analysis,
18 )
19 +from app.schedulers.services.monitoring_alert import (
20 + invoke_office365_exchange_online_alert,
21 +)
22 +from app.schedulers.services.monitoring_alert import invoke_office365_threat_intel_alert
23 from app.schedulers.services.monitoring_alert import invoke_suricata_monitoring_alert
24 from app.schedulers.services.monitoring_alert import invoke_wazuh_monitoring_alert
25
@@ -93,6 +97,8 @@ def get_function_by_name(function_name: str):
97 "invoke_mimecast_integration": invoke_mimecast_integration,
98 "invoke_mimecast_integration_ttp": invoke_mimecast_integration_ttp,
99 "invoke_wazuh_monitoring_alert": invoke_wazuh_monitoring_alert,
100 + "invoke_office365_exchange_online_alert": invoke_office365_exchange_online_alert,
101 + "invoke_office365_threat_intel_alert": invoke_office365_threat_intel_alert,
102 "invoke_suricata_monitoring_alert": invoke_suricata_monitoring_alert,
103 "invoke_sap_siem_integration_collection": invoke_sap_siem_integration_collect,
104 "invoke_sap_siem_integration_suspicious_logins_analysis": invoke_sap_siem_integration_suspicious_logins_analysis,
backend/app/schedulers/services/monitoring_alert.py
+42
@@ -7,6 +7,9 @@ from sqlalchemy import select
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 )
@@ -98,3 +101,42 @@ async def invoke_suricata_monitoring_alert() -> 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/routes/provision.py
+57 -14
@@ -1,18 +1,18 @@
1 from fastapi import APIRouter
2 -from fastapi import Depends
2 from fastapi import HTTPException
3 from fastapi import Security
4 from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
5
6 from app.auth.utils import AuthHandler
7 from app.connectors.graylog.services.content_packs import get_content_packs
8 from app.connectors.graylog.services.management import get_system_info
11 -from app.db.db_session import get_db
12 -from app.stack_provisioning.graylog.schema.provision import ProvisionGraylogResponse
13 -from app.stack_provisioning.graylog.services.provision import (
14 - provision_wazuh_content_pack,
9 +from app.stack_provisioning.graylog.schema.provision import AvailableContentPacks
10 +from app.stack_provisioning.graylog.schema.provision import (
11 + AvailableContentPacksResponse,
12 )
13 +from app.stack_provisioning.graylog.schema.provision import ProvisionContentPackRequest
14 +from app.stack_provisioning.graylog.schema.provision import ProvisionGraylogResponse
15 +from app.stack_provisioning.graylog.services.provision import provision_content_pack
16
17 stack_provisioning_graylog_router = APIRouter()
18
@@ -58,6 +58,28 @@ async def system_version_check(compatible_version: str) -> bool:
58 )
59
60
61 +async def is_content_pack_available(content_pack_name: str) -> bool:
62 + """
63 + Check if the content pack is available for provisioning.
64 +
65 + Args:
66 + content_pack_name (str): The name of the content pack to check.
67 +
68 + Returns:
69 + bool: True if the content pack is available, False if it is not.
70 + """
71 + available_content_packs = [pack.name for pack in AvailableContentPacks]
72 + if content_pack_name in available_content_packs:
73 + logger.info(f"Content pack {content_pack_name} is available")
74 + return True
75 + else:
76 + logger.info(f"Content pack {content_pack_name} is not available")
77 + raise HTTPException(
78 + status_code=400,
79 + detail=f"Content pack {content_pack_name} is not available",
80 + )
81 +
82 +
83 async def does_content_pack_exist(content_pack_name: str) -> bool:
84 """
85 Check if the content pack exists in the list of content packs.
@@ -81,20 +103,41 @@ async def does_content_pack_exist(content_pack_name: str) -> bool:
103 return False
104
105
106 +@stack_provisioning_graylog_router.get(
107 + "/graylog/available/content_packs",
108 + response_model=AvailableContentPacksResponse,
109 + description="Get the available content packs for provisioning in Graylog",
110 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
111 +)
112 +async def get_available_content_packs_route() -> AvailableContentPacksResponse:
113 + """
114 + Get the available content packs for provisioning in Graylog
115 + """
116 + logger.info("Getting available content packs...")
117 + return AvailableContentPacksResponse(
118 + available_content_packs=[{"name": pack.name, "description": pack.value} for pack in AvailableContentPacks],
119 + success=True,
120 + message="Available content packs retrieved successfully",
121 + )
122 +
123 +
124 @stack_provisioning_graylog_router.post(
85 - "/graylog/wazuh",
125 + "/graylog/provision/content_pack",
126 response_model=ProvisionGraylogResponse,
127 description="Provision the Wazuh Content Pack in the Graylog instance",
128 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
129 )
90 -async def provision_wazuh_content_pack_route(
91 - session: AsyncSession = Depends(get_db),
130 +async def provision_content_pack_route(
131 + content_pack_request: ProvisionContentPackRequest,
132 ) -> ProvisionGraylogResponse:
133 """
94 - Provision the Wazuh Content Pack in the Graylog instance
134 + Provision the Content Pack in the Graylog instance
135 """
96 - logger.info("Provisioning Wazuh Content Pack...")
136 + logger.info(f"Provisioning content pack {content_pack_request.content_pack_name.name}...")
137 await system_version_check(compatible_version="5.0.13+083613e")
98 - await does_content_pack_exist("SOCFORTRESS_WAZUH_CONTENT_PACK")
99 - await provision_wazuh_content_pack(session)
100 - return ProvisionGraylogResponse(success=True, message="Wazuh Content Pack provisioned successfully")
138 + await does_content_pack_exist(content_pack_name=content_pack_request.content_pack_name.name)
139 + await provision_content_pack(content_pack_request.content_pack_name.name)
140 + return ProvisionGraylogResponse(
141 + success=True,
142 + message=f"{content_pack_request.content_pack_name.name} Content Pack provisioned successfully",
143 + )
backend/app/stack_provisioning/graylog/schema/provision.py
+57
@@ -1,7 +1,64 @@
1 +from enum import Enum
2 +from typing import Any
3 +from typing import List
4 +
5 +from fastapi import HTTPException
6 from pydantic import BaseModel
7 from pydantic import Field
8
9
10 +class AvailableContentPacks(str, Enum):
11 + SOCFORTRESS_WAZUH_CONTENT_PACK = (
12 + "The Wazuh Content Pack which includes Input, Stream, Pipeline Rules,"
13 + " Pipelines, and Lookup Tables for Wazuh logs and the SOCFortress SIEM stack."
14 + )
15 +
16 +
17 +class ContentPack(BaseModel):
18 + name: str
19 + description: str
20 +
21 +
22 +class AvailableContentPacksResponse(BaseModel):
23 + available_content_packs: List[ContentPack] = Field(
24 + ...,
25 + example={
26 + "name": AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK.name,
27 + "description": AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK.value,
28 + },
29 + description="The available content packs for provisioning in Graylog",
30 + )
31 + success: bool = Field(
32 + ...,
33 + example=True,
34 + description="Success of the request to get available content packs",
35 + )
36 + message: str = Field(
37 + ...,
38 + example="Available content packs retrieved successfully",
39 + description="Message from the request to get available content packs",
40 + )
41 +
42 +
43 +class ProvisionContentPackRequest(BaseModel):
44 + content_pack_name: AvailableContentPacks = Field(
45 + ...,
46 + example=AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK,
47 + description="The name of the content pack to provision in Graylog",
48 + )
49 +
50 + def __init__(self, **data: Any):
51 + content_pack_name = data.get("content_pack_name")
52 + try:
53 + data["content_pack_name"] = AvailableContentPacks[content_pack_name]
54 + except KeyError:
55 + raise HTTPException(
56 + status_code=400,
57 + detail=f"Content pack {content_pack_name} is not available. Please choose from the available content packs.",
58 + )
59 + super().__init__(**data)
60 +
61 +
62 class ProvisionGraylogResponse(BaseModel):
63 success: bool = Field(
64 ...,
backend/app/stack_provisioning/graylog/services/provision.py
+30 -12
@@ -3,9 +3,7 @@ from pathlib import Path
3
4 from fastapi import HTTPException
5 from loguru import logger
6 -from sqlalchemy.ext.asyncio import AsyncSession
6
8 -from app.connectors.graylog.schema.content_packs import ContentPack
7 from app.connectors.graylog.services.content_packs import insert_content_pack
8 from app.connectors.graylog.services.content_packs import install_content_pack
9 from app.stack_provisioning.graylog.schema.provision import ProvisionGraylogResponse
@@ -46,23 +44,43 @@ def load_content_pack_json(file_name: str) -> dict:
44 with open(file_path, "r") as file:
45 content_pack_data = json.load(file)
46
49 - return ContentPack(**content_pack_data).dict()
47 + return content_pack_data
48
49 except FileNotFoundError:
50 logger.error(f"Content pack JSON file not found at {file_path}")
51 raise HTTPException(status_code=404, detail="Content pack JSON file not found")
52
53
56 -async def provision_wazuh_content_pack(
57 - session: AsyncSession,
58 -) -> ProvisionGraylogResponse:
54 +async def get_id_and_rev(data: dict) -> tuple:
55 + return data.get("id"), data.get("rev")
56 +
57 +
58 +# ! Only for testing purposes
59 +async def write_content_pack_to_file(content_pack: dict) -> None:
60 + """
61 + Write the content pack to a file. Just for testing purposes.
62 +
63 + Args:
64 + content_pack (dict): The content pack to write to a file.
65 + """
66 + file_path = get_content_pack_path("wazuh_content_pack_testing.json")
67 + with open(file_path, "w") as file:
68 + json.dump(content_pack, file, indent=4)
69 +
70 +
71 +async def provision_content_pack(content_pack_name: str) -> ProvisionGraylogResponse:
72 """
73 Provision the Wazuh Content Pack in the Graylog instance
74 """
62 - logger.info("Provisioning Wazuh Content Pack...")
63 - content_pack = load_content_pack_json("wazuh_content_pack.json")
64 - logger.info("Inserting Wazuh Content Pack...")
75 + logger.info(f"Provisioning {content_pack_name} Content Pack...")
76 + content_pack = load_content_pack_json(f"{content_pack_name}.json")
77 + # ! Only for testing purposes
78 + # await write_content_pack_to_file(content_pack)
79 +
80 + logger.info(f"Inserting {content_pack_name} Content Pack...")
81 await insert_content_pack(content_pack)
66 - # ! Content Pack ID is found in the `wazuh_content_pack.json` file
67 - await install_content_pack(content_pack_id="261577fe-d9a2-4141-af74-635f085eee54", revision=1)
68 - return ProvisionGraylogResponse(success=True, message="Wazuh Content Pack provisioned successfully")
82 + # ! Content Pack ID is found in the first `id` field and the revision is found in the first `rev` field
83 + id, rev = await get_id_and_rev(content_pack)
84 + logger.info(f"Id: {id}, Rev: {rev}")
85 + await install_content_pack(content_pack_id=id, revision=rev)
86 + return ProvisionGraylogResponse(success=True, message=f"{content_pack_name} Content Pack provisioned successfully")
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_WAZUH_CONTENT_PACK.json renamed
docker-compose.yml
+3
@@ -23,3 +23,6 @@ services:
23 networks:
24 default:
25 driver: bridge
26 + # In case you need to set the MTU
27 + #driver_opts:
28 + # com.docker.network.driver.mtu: "1450"
frontend/.env.example
+1 -1
@@ -1,5 +1,5 @@
1 # base url
2 -VITE_API_URL=https://0.0.0.0/api
2 +VITE_API_URL=https://0.0.0.0:5000
3
4 # value in seconds
5 VITE_TOKEN_DEBOUNCE_TIME=10
frontend/package-lock.json
+64 -64
@@ -14,8 +14,8 @@
14 "@fontsource/lexend": "^5.0.18",
15 "@fontsource/public-sans": "^5.0.16",
16 "@popperjs/core": "^2.11.8",
17 - "@vueuse/components": "^10.7.2",
18 - "@vueuse/core": "^10.7.2",
17 + "@vueuse/components": "^10.8.0",
18 + "@vueuse/core": "^10.8.0",
19 "apexcharts": "^3.46.0",
20 "bytes": "^3.1.2",
21 "colord": "^2.9.3",
@@ -65,7 +65,7 @@
65 "@vue/test-utils": "^2.4.4",
66 "@vue/tsconfig": "^0.5.1",
67 "autoprefixer": "^10.4.17",
68 - "cypress": "^13.6.4",
68 + "cypress": "^13.6.5",
69 "eslint": "^8.56.0",
70 "eslint-plugin-cypress": "^2.15.1",
71 "eslint-plugin-vue": "^9.21.1",
@@ -77,7 +77,7 @@
77 "picocolors": "^1.0.0",
78 "postcss": "^8.4.35",
79 "prettier": "^3.2.5",
80 - "sass": "^1.71.0",
80 + "sass": "^1.71.1",
81 "start-server-and-test": "^2.0.3",
82 "tailwind-config-viewer": "^1.7.3",
83 "tailwindcss": "^3.4.1",
@@ -89,7 +89,7 @@
89 "vite-bundle-analyzer": "^0.8.0",
90 "vite-bundle-visualizer": "^1.0.1",
91 "vite-svg-loader": "^5.1.0",
92 - "vitest": "^1.3.0",
92 + "vitest": "^1.3.1",
93 "vue-tsc": "^1.8.27"
94 },
95 "engines": {
@@ -2751,13 +2751,13 @@
2751 }
2752 },
2753 "node_modules/@vitest/expect": {
2754 - "version": "1.3.0",
2755 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.0.tgz",
2756 - "integrity": "sha512-7bWt0vBTZj08B+Ikv70AnLRicohYwFgzNjFqo9SxxqHHxSlUJGSXmCRORhOnRMisiUryKMdvsi1n27Bc6jL9DQ==",
2754 + "version": "1.3.1",
2755 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
2756 + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
2757 "dev": true,
2758 "dependencies": {
2759 - "@vitest/spy": "1.3.0",
2760 - "@vitest/utils": "1.3.0",
2759 + "@vitest/spy": "1.3.1",
2760 + "@vitest/utils": "1.3.1",
2761 "chai": "^4.3.10"
2762 },
2763 "funding": {
@@ -2765,12 +2765,12 @@
2765 }
2766 },
2767 "node_modules/@vitest/runner": {
2768 - "version": "1.3.0",
2769 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.0.tgz",
2770 - "integrity": "sha512-1Jb15Vo/Oy7mwZ5bXi7zbgszsdIBNjc4IqP8Jpr/8RdBC4nF1CTzIAn2dxYvpF1nGSseeL39lfLQ2uvs5u1Y9A==",
2768 + "version": "1.3.1",
2769 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
2770 + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
2771 "dev": true,
2772 "dependencies": {
2773 - "@vitest/utils": "1.3.0",
2773 + "@vitest/utils": "1.3.1",
2774 "p-limit": "^5.0.0",
2775 "pathe": "^1.1.1"
2776 },
@@ -2806,9 +2806,9 @@
2806 }
2807 },
2808 "node_modules/@vitest/snapshot": {
2809 - "version": "1.3.0",
2810 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz",
2811 - "integrity": "sha512-swmktcviVVPYx9U4SEQXLV6AEY51Y6bZ14jA2yo6TgMxQ3h+ZYiO0YhAHGJNp0ohCFbPAis1R9kK0cvN6lDPQA==",
2809 + "version": "1.3.1",
2810 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
2811 + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
2812 "dev": true,
2813 "dependencies": {
2814 "magic-string": "^0.30.5",
@@ -2820,9 +2820,9 @@
2820 }
2821 },
2822 "node_modules/@vitest/spy": {
2823 - "version": "1.3.0",
2824 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.0.tgz",
2825 - "integrity": "sha512-AkCU0ThZunMvblDpPKgjIi025UxR8V7MZ/g/EwmAGpjIujLVV2X6rGYGmxE2D4FJbAy0/ijdROHMWa2M/6JVMw==",
2823 + "version": "1.3.1",
2824 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
2825 + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
2826 "dev": true,
2827 "dependencies": {
2828 "tinyspy": "^2.2.0"
@@ -2832,9 +2832,9 @@
2832 }
2833 },
2834 "node_modules/@vitest/utils": {
2835 - "version": "1.3.0",
2836 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.0.tgz",
2837 - "integrity": "sha512-/LibEY/fkaXQufi4GDlQZhikQsPO2entBKtfuyIpr1jV4DpaeasqkeHjhdOhU24vSHshcSuEyVlWdzvv2XmYCw==",
2835 + "version": "1.3.1",
2836 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
2837 + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
2838 "dev": true,
2839 "dependencies": {
2840 "diff-sequences": "^29.6.3",
@@ -3116,13 +3116,13 @@
3116 "dev": true
3117 },
3118 "node_modules/@vueuse/components": {
3119 - "version": "10.7.2",
3120 - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.7.2.tgz",
3121 - "integrity": "sha512-r39DLLtRo1hEKI/SQzVQjCts7yelwFyUrTxDFi821NdyU3EfQ9GCNNBcMirXcn3IQApFBRKrvTTtQ9cJGrb/+A==",
3119 + "version": "10.8.0",
3120 + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.8.0.tgz",
3121 + "integrity": "sha512-5k/4Cxgt+aoxeHIOWSet6kkHXY+96QuPkJzGpOHaCj9DD0ASBni6L/wHQUWL118Ac9xq5+QQJuK5VvFs/yBAEw==",
3122 "dependencies": {
3123 - "@vueuse/core": "10.7.2",
3124 - "@vueuse/shared": "10.7.2",
3125 - "vue-demi": ">=0.14.6"
3123 + "@vueuse/core": "10.8.0",
3124 + "@vueuse/shared": "10.8.0",
3125 + "vue-demi": ">=0.14.7"
3126 }
3127 },
3128 "node_modules/@vueuse/components/node_modules/vue-demi": {
@@ -3151,14 +3151,14 @@
3151 }
3152 },
3153 "node_modules/@vueuse/core": {
3154 - "version": "10.7.2",
3155 - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.7.2.tgz",
3156 - "integrity": "sha512-AOyAL2rK0By62Hm+iqQn6Rbu8bfmbgaIMXcE3TSr7BdQ42wnSFlwIdPjInO62onYsEMK/yDMU8C6oGfDAtZ2qQ==",
3154 + "version": "10.8.0",
3155 + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.8.0.tgz",
3156 + "integrity": "sha512-G9Ok9fjx10TkNIPn8V1dJmK1NcdJCtYmDRyYiTMUyJ1p0Tywc1zmOoCQ2xhHYyz8ULBU4KjIJQ9n+Lrty74iVw==",
3157 "dependencies": {
3158 "@types/web-bluetooth": "^0.0.20",
3159 - "@vueuse/metadata": "10.7.2",
3160 - "@vueuse/shared": "10.7.2",
3161 - "vue-demi": ">=0.14.6"
3159 + "@vueuse/metadata": "10.8.0",
3160 + "@vueuse/shared": "10.8.0",
3161 + "vue-demi": ">=0.14.7"
3162 },
3163 "funding": {
3164 "url": "https://github.com/sponsors/antfu"
@@ -3190,19 +3190,19 @@
3190 }
3191 },
3192 "node_modules/@vueuse/metadata": {
3193 - "version": "10.7.2",
3194 - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.7.2.tgz",
3195 - "integrity": "sha512-kCWPb4J2KGrwLtn1eJwaJD742u1k5h6v/St5wFe8Quih90+k2a0JP8BS4Zp34XUuJqS2AxFYMb1wjUL8HfhWsQ==",
3193 + "version": "10.8.0",
3194 + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.8.0.tgz",
3195 + "integrity": "sha512-Nim/Vle5OgXcXhAvGOgkJQXB1Yb+Kq/fMbLuv3YYDYbiQrwr39ljuD4k9fPeq4yUyokYRo2RaNQmbbIMWB/9+w==",
3196 "funding": {
3197 "url": "https://github.com/sponsors/antfu"
3198 }
3199 },
3200 "node_modules/@vueuse/shared": {
3201 - "version": "10.7.2",
3202 - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.7.2.tgz",
3203 - "integrity": "sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==",
3201 + "version": "10.8.0",
3202 + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.8.0.tgz",
3203 + "integrity": "sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==",
3204 "dependencies": {
3205 - "vue-demi": ">=0.14.6"
3205 + "vue-demi": ">=0.14.7"
3206 },
3207 "funding": {
3208 "url": "https://github.com/sponsors/antfu"
@@ -4510,9 +4510,9 @@
4510 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4511 },
4512 "node_modules/cypress": {
4513 - "version": "13.6.4",
4514 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.4.tgz",
4515 - "integrity": "sha512-pYJjCfDYB+hoOoZuhysbbYhEmNW7DEDsqn+ToCLwuVowxUXppIWRr7qk4TVRIU471ksfzyZcH+mkoF0CQUKnpw==",
4513 + "version": "13.6.5",
4514 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.5.tgz",
4515 + "integrity": "sha512-2NxSDcO2zHw5kTcosc6dzv2zppEqiXrFFhZw5cx/EWrSNZABTzpr/EyvYzGgrWm46o5173JUfuJfDQcaiZZPVQ==",
4516 "dev": true,
4517 "hasInstallScript": true,
4518 "dependencies": {
@@ -4523,7 +4523,7 @@
4523 "arch": "^2.2.0",
4524 "blob-util": "^2.0.2",
4525 "bluebird": "^3.7.2",
4526 - "buffer": "^5.6.0",
4526 + "buffer": "^5.7.1",
4527 "cachedir": "^2.3.0",
4528 "chalk": "^4.1.0",
4529 "check-more-types": "^2.24.0",
@@ -4541,7 +4541,7 @@
4541 "figures": "^3.2.0",
4542 "fs-extra": "^9.1.0",
4543 "getos": "^3.2.1",
4544 - "is-ci": "^3.0.0",
4544 + "is-ci": "^3.0.1",
4545 "is-installed-globally": "~0.4.0",
4546 "lazy-ass": "^1.6.0",
4547 "listr2": "^3.8.3",
@@ -10549,9 +10549,9 @@
10549 "dev": true
10550 },
10551 "node_modules/sass": {
10552 - "version": "1.71.0",
10553 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.0.tgz",
10554 - "integrity": "sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==",
10552 + "version": "1.71.1",
10553 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
10554 + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
10555 "dev": true,
10556 "dependencies": {
10557 "chokidar": ">=3.0.0 <4.0.0",
@@ -12497,9 +12497,9 @@
12497 }
12498 },
12499 "node_modules/vite-node": {
12500 - "version": "1.3.0",
12501 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.0.tgz",
12502 - "integrity": "sha512-D/oiDVBw75XMnjAXne/4feCkCEwcbr2SU1bjAhCcfI5Bq3VoOHji8/wCPAfUkDIeohJ5nSZ39fNxM3dNZ6OBOA==",
12500 + "version": "1.3.1",
12501 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
12502 + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
12503 "dev": true,
12504 "dependencies": {
12505 "cac": "^6.7.14",
@@ -12531,16 +12531,16 @@
12531 }
12532 },
12533 "node_modules/vitest": {
12534 - "version": "1.3.0",
12535 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.0.tgz",
12536 - "integrity": "sha512-V9qb276J1jjSx9xb75T2VoYXdO1UKi+qfflY7V7w93jzX7oA/+RtYE6TcifxksxsZvygSSMwu2Uw6di7yqDMwg==",
12534 + "version": "1.3.1",
12535 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
12536 + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
12537 "dev": true,
12538 "dependencies": {
12539 - "@vitest/expect": "1.3.0",
12540 - "@vitest/runner": "1.3.0",
12541 - "@vitest/snapshot": "1.3.0",
12542 - "@vitest/spy": "1.3.0",
12543 - "@vitest/utils": "1.3.0",
12539 + "@vitest/expect": "1.3.1",
12540 + "@vitest/runner": "1.3.1",
12541 + "@vitest/snapshot": "1.3.1",
12542 + "@vitest/spy": "1.3.1",
12543 + "@vitest/utils": "1.3.1",
12544 "acorn-walk": "^8.3.2",
12545 "chai": "^4.3.10",
12546 "debug": "^4.3.4",
@@ -12554,7 +12554,7 @@
12554 "tinybench": "^2.5.1",
12555 "tinypool": "^0.8.2",
12556 "vite": "^5.0.0",
12557 - "vite-node": "1.3.0",
12557 + "vite-node": "1.3.1",
12558 "why-is-node-running": "^2.2.2"
12559 },
12560 "bin": {
@@ -12569,8 +12569,8 @@
12569 "peerDependencies": {
12570 "@edge-runtime/vm": "*",
12571 "@types/node": "^18.0.0 || >=20.0.0",
12572 - "@vitest/browser": "1.3.0",
12573 - "@vitest/ui": "1.3.0",
12572 + "@vitest/browser": "1.3.1",
12573 + "@vitest/ui": "1.3.1",
12574 "happy-dom": "*",
12575 "jsdom": "*"
12576 },
frontend/package.json
+7 -7
@@ -39,8 +39,8 @@
39 "@fontsource/lexend": "^5.0.18",
40 "@fontsource/public-sans": "^5.0.16",
41 "@popperjs/core": "^2.11.8",
42 - "@vueuse/components": "^10.7.2",
43 - "@vueuse/core": "^10.7.2",
42 + "@vueuse/components": "^10.8.0",
43 + "@vueuse/core": "^10.8.0",
44 "apexcharts": "^3.46.0",
45 "bytes": "^3.1.2",
46 "colord": "^2.9.3",
@@ -63,7 +63,7 @@
63 "vue-advanced-cropper": "^2.8.8",
64 "vue-highlight-words": "^3.0.1",
65 "vue-i18n": "^9.9.1",
66 - "vue-router": "^4.2.5",
66 + "vue-router": "^4.3.0",
67 "vue-sjv": "^0.0.6",
68 "vue3-apexcharts": "^1.5.2",
69 "vue3-marquee": "^4.2.0"
@@ -90,7 +90,7 @@
90 "@vue/test-utils": "^2.4.4",
91 "@vue/tsconfig": "^0.5.1",
92 "autoprefixer": "^10.4.17",
93 - "cypress": "^13.6.4",
93 + "cypress": "^13.6.5",
94 "eslint": "^8.56.0",
95 "eslint-plugin-cypress": "^2.15.1",
96 "eslint-plugin-vue": "^9.21.1",
@@ -102,7 +102,7 @@
102 "picocolors": "^1.0.0",
103 "postcss": "^8.4.35",
104 "prettier": "^3.2.5",
105 - "sass": "^1.71.0",
105 + "sass": "^1.71.1",
106 "start-server-and-test": "^2.0.3",
107 "tailwind-config-viewer": "^1.7.3",
108 "tailwindcss": "^3.4.1",
@@ -110,11 +110,11 @@
110 "ts-node": "^10.9.2",
111 "typescript": "~5.3.3",
112 "unplugin-vue-components": "^0.26.0",
113 - "vite": "^5.1.3",
113 + "vite": "^5.1.4",
114 "vite-bundle-analyzer": "^0.8.0",
115 "vite-bundle-visualizer": "^1.0.1",
116 "vite-svg-loader": "^5.1.0",
117 - "vitest": "^1.3.0",
117 + "vitest": "^1.3.1",
118 "vue-tsc": "^1.8.27"
119 },
120 "engines": {
frontend/src/api/activeResponse.ts new
+42
@@ -0,0 +1,42 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "./httpClient"
3 +import type { ActiveResponseDetails, SupportedActiveResponse } from "@/types/activeResponse"
4 +
5 +export type InvokeRequestAction = "block" | "unblock"
6 +
7 +export interface InvokeRequest {
8 + activeResponseName: string
9 + action: InvokeRequestAction
10 + ip: string
11 + agentId?: string
12 +}
13 +
14 +export default {
15 + getSupported(agentId?: string) {
16 + return HttpClient.get<FlaskBaseResponse & { supported_active_responses: SupportedActiveResponse[] }>(
17 + `/active_response/supported${agentId ? "/" + agentId : ""}`
18 + )
19 + },
20 + getDetails(activeResponseName: string) {
21 + return HttpClient.get<FlaskBaseResponse & { active_response: ActiveResponseDetails }>(
22 + `/active_response/describe/${activeResponseName.toLowerCase()}`
23 + )
24 + },
25 + invoke(params: InvokeRequest) {
26 + const payload = {
27 + endpoint: "active-response",
28 + arguments: [],
29 + command: params.activeResponseName.toLowerCase(),
30 + custom: true,
31 + alert: {
32 + action: params.action,
33 + ip: params.ip
34 + },
35 + params: {
36 + wait_for_complete: true,
37 + agents_list: [params.agentId || "*"]
38 + }
39 + }
40 + return HttpClient.post<FlaskBaseResponse>(`/active_response/invoke`, payload)
41 + }
42 +}
frontend/src/api/index.ts
+3 -1
@@ -14,6 +14,7 @@ import logs from "./logs"
14 import flow from "./flow"
15 import integrations from "./integrations"
16 import monitoringAlerts from "./monitoringAlerts"
17 +import activeResponse from "./activeResponse"
18
19 export default {
20 agents,
@@ -31,5 +32,6 @@ export default {
32 logs,
33 flow,
34 integrations,
34 - monitoringAlerts
35 + monitoringAlerts,
36 + activeResponse
37 }
frontend/src/components/activeResponse/ActiveResponseActions.vue new
+71
@@ -0,0 +1,71 @@
1 +<template>
2 + <div class="active-response-actions flex gap-2 justify-end">
3 + <n-button type="success" secondary :size="size" @click="showInvokeForm = true" :loading="loadingInvoke">
4 + <template #icon><Icon :name="InvokeIcon"></Icon></template>
5 + Invoke Action
6 + </n-button>
7 +
8 + <n-modal
9 + v-model:show="showInvokeForm"
10 + display-directive="show"
11 + preset="card"
12 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
13 + :title="activeResponse.name"
14 + :bordered="false"
15 + content-class="flex flex-col"
16 + segmented
17 + >
18 + <ActiveResponseInvokeForm
19 + :activeResponse="activeResponse"
20 + :agentId="agentId"
21 + @mounted="activeResponseInvokeFormCTX = $event"
22 + @submitted="close()"
23 + @startLoading="loadingInvoke = true"
24 + @stopLoading="loadingInvoke = false"
25 + >
26 + <template #additionalActions>
27 + <n-button @click="close()" secondary>Close</n-button>
28 + </template>
29 + </ActiveResponseInvokeForm>
30 + </n-modal>
31 + </div>
32 +</template>
33 +
34 +<script setup lang="ts">
35 +import { NButton, NModal } from "naive-ui"
36 +import Icon from "@/components/common/Icon.vue"
37 +import { computed, ref } from "vue"
38 +import { watch } from "vue"
39 +import type { SupportedActiveResponse } from "@/types/activeResponse"
40 +import ActiveResponseInvokeForm from "./ActiveResponseInvokeForm.vue"
41 +
42 +const emit = defineEmits<{
43 + (e: "startLoading"): void
44 + (e: "stopLoading"): void
45 +}>()
46 +
47 +const { activeResponse, size, agentId } = defineProps<{
48 + activeResponse: SupportedActiveResponse
49 + agentId?: string | number
50 + size?: "tiny" | "small" | "medium" | "large"
51 +}>()
52 +
53 +const InvokeIcon = "solar:playback-speed-outline"
54 +const showInvokeForm = ref(false)
55 +const loadingInvoke = ref(false)
56 +const loading = computed(() => loadingInvoke.value)
57 +const activeResponseInvokeFormCTX = ref<{ reset: () => void } | null>(null)
58 +
59 +watch(loading, val => {
60 + if (val) {
61 + emit("startLoading")
62 + } else {
63 + emit("stopLoading")
64 + }
65 +})
66 +
67 +function close() {
68 + activeResponseInvokeFormCTX.value?.reset()
69 + showInvokeForm.value = false
70 +}
71 +</script>
frontend/src/components/activeResponse/ActiveResponseAgent.vue new
+72
@@ -0,0 +1,72 @@
1 +<template>
2 + <div class="active-response-list">
3 + <n-spin :show="loadingActiveResponse">
4 + <div class="list">
5 + <template v-if="activeResponseList.length">
6 + <ActiveResponseItem
7 + v-for="activeResponse of activeResponseList"
8 + :key="activeResponse.name"
9 + :activeResponse="activeResponse"
10 + :embedded="embedded"
11 + :agent-id="agent.agent_id"
12 + class="item-appear item-appear-bottom item-appear-005 mb-2"
13 + />
14 + </template>
15 + <template v-else>
16 + <n-empty description="No items found" class="justify-center h-48" v-if="!loadingActiveResponse" />
17 + </template>
18 + </div>
19 + </n-spin>
20 + </div>
21 +</template>
22 +
23 +<script setup lang="ts">
24 +import { ref, onBeforeMount } from "vue"
25 +import { useMessage, NSpin, NEmpty } from "naive-ui"
26 +import Api from "@/api"
27 +import ActiveResponseItem from "./ActiveResponseItem.vue"
28 +import type { Agent } from "@/types/agents"
29 +import type { SupportedActiveResponse } from "@/types/activeResponse"
30 +
31 +const { embedded, agent } = defineProps<{
32 + embedded?: boolean
33 + agent: Agent
34 +}>()
35 +
36 +const message = useMessage()
37 +const loadingActiveResponse = ref(false)
38 +const activeResponseList = ref<SupportedActiveResponse[]>([])
39 +
40 +function getAvailableIntegrations() {
41 + loadingActiveResponse.value = true
42 +
43 + Api.activeResponse
44 + .getSupported(agent.agent_id)
45 + .then(res => {
46 + if (res.data.success) {
47 + activeResponseList.value = res.data?.supported_active_responses || []
48 + } else {
49 + message.warning(res.data?.message || "An error occurred. Please try again later.")
50 + }
51 + })
52 + .catch(err => {
53 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
54 + })
55 + .finally(() => {
56 + loadingActiveResponse.value = false
57 + })
58 +}
59 +
60 +onBeforeMount(() => {
61 + getAvailableIntegrations()
62 +})
63 +</script>
64 +
65 +<style lang="scss" scoped>
66 +.active-response-list {
67 + .list {
68 + container-type: inline-size;
69 + min-height: 200px;
70 + }
71 +}
72 +</style>
frontend/src/components/activeResponse/ActiveResponseDetails.vue new
+50
@@ -0,0 +1,50 @@
1 +<template>
2 + <div class="active-response-details">
3 + <n-spin :show="loadingActiveResponse">
4 + <Markdown v-if="activeResponseDetails?.markdown_content" :source="activeResponseDetails.markdown_content" />
5 + <template v-else>
6 + <n-empty description="No description found" class="justify-center h-48" v-if="!loadingActiveResponse" />
7 + </template>
8 + </n-spin>
9 + </div>
10 +</template>
11 +
12 +<script setup lang="ts">
13 +import { ref, onBeforeMount, defineAsyncComponent } from "vue"
14 +import { useMessage, NSpin, NEmpty } from "naive-ui"
15 +import Api from "@/api"
16 +import type { ActiveResponseDetails, SupportedActiveResponse } from "@/types/activeResponse"
17 +const Markdown = defineAsyncComponent(() => import("@/components/common/Markdown.vue"))
18 +
19 +const { activeResponse } = defineProps<{
20 + activeResponse: SupportedActiveResponse
21 +}>()
22 +
23 +const message = useMessage()
24 +const loadingActiveResponse = ref(false)
25 +const activeResponseDetails = ref<ActiveResponseDetails>()
26 +
27 +function getAvailableIntegrations() {
28 + loadingActiveResponse.value = true
29 +
30 + Api.activeResponse
31 + .getDetails(activeResponse.name)
32 + .then(res => {
33 + if (res.data.success) {
34 + activeResponseDetails.value = res.data?.active_response
35 + } else {
36 + message.warning(res.data?.message || "An error occurred. Please try again later.")
37 + }
38 + })
39 + .catch(err => {
40 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
41 + })
42 + .finally(() => {
43 + loadingActiveResponse.value = false
44 + })
45 +}
46 +
47 +onBeforeMount(() => {
48 + getAvailableIntegrations()
49 +})
50 +</script>
frontend/src/components/activeResponse/ActiveResponseInvokeForm.vue new
+175
@@ -0,0 +1,175 @@
1 +<template>
2 + <div class="active-response-invoke-form flex flex-col justify-between grow">
3 + <div class="form-box">
4 + <n-spin v-model:show="loading">
5 + <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
6 + <div class="grid gap-6 grid-auto-flow-200">
7 + <n-form-item label="Action" path="action">
8 + <n-select v-model:value="form.action" :options="invokeActionOptions" />
9 + </n-form-item>
10 + <n-form-item label="IP Address" path="ip">
11 + <n-input v-model:value.trim="form.ip" placeholder="Input the IP Address..." />
12 + </n-form-item>
13 + </div>
14 + </n-form>
15 + <p v-if="agentId">
16 + This action will be submitted only for the Agent:
17 + <code>{{ agentId }}</code>
18 + </p>
19 + </n-spin>
20 + </div>
21 + <div class="buttons-box flex justify-end gap-3">
22 + <div class="flex gap-3">
23 + <slot name="additionalActions"></slot>
24 + </div>
25 + <n-button type="primary" :disabled="!isValid" @click="validate()" :loading="loading">Submit</n-button>
26 + </div>
27 + </div>
28 +</template>
29 +
30 +<script setup lang="ts">
31 +import {
32 + NButton,
33 + NForm,
34 + NFormItem,
35 + NInput,
36 + NSelect,
37 + NSpin,
38 + type FormItemRule,
39 + type FormRules,
40 + type FormInst,
41 + type FormValidationError,
42 + useMessage
43 +} from "naive-ui"
44 +import { computed, onMounted, ref } from "vue"
45 +import { watch } from "vue"
46 +import type { SupportedActiveResponse } from "@/types/activeResponse"
47 +import isIP from "validator/es/lib/isIP"
48 +import type { InvokeRequest, InvokeRequestAction } from "@/api/activeResponse"
49 +import Api from "@/api"
50 +
51 +interface InvokeForm {
52 + action: null | InvokeRequestAction
53 + ip: string
54 +}
55 +
56 +const emit = defineEmits<{
57 + (e: "submitted"): void
58 + (e: "startLoading"): void
59 + (e: "stopLoading"): void
60 + (
61 + e: "mounted",
62 + value: {
63 + reset: () => void
64 + }
65 + ): void
66 +}>()
67 +
68 +const { activeResponse, agentId } = defineProps<{
69 + activeResponse: SupportedActiveResponse
70 + agentId?: string | number
71 +}>()
72 +
73 +const message = useMessage()
74 +const form = ref<InvokeForm>(getClearForm())
75 +const formRef = ref<FormInst | null>(null)
76 +const invokeActionOptions = [
77 + { label: "Block", value: "block" },
78 + { label: "Unblock", value: "unblock" }
79 +]
80 +const isValid = computed(() => {
81 + return !!form.value.action && isIP(form.value.ip)
82 +})
83 +const loading = ref(false)
84 +
85 +watch(loading, val => {
86 + if (val) {
87 + emit("startLoading")
88 + } else {
89 + emit("stopLoading")
90 + }
91 +})
92 +
93 +const rules: FormRules = {
94 + action: {
95 + required: true,
96 + message: "Please Select an Action",
97 + trigger: ["input", "blur"]
98 + },
99 + ip: {
100 + required: true,
101 + validator: validateIp,
102 + trigger: ["blur"]
103 + }
104 +}
105 +
106 +function validateIp(rule: FormItemRule, value: string) {
107 + if (!value || !isIP(value)) {
108 + return new Error("Please input a valid IP Address")
109 + }
110 +
111 + return true
112 +}
113 +
114 +function getClearForm(): InvokeForm {
115 + return {
116 + action: null,
117 + ip: ""
118 + }
119 +}
120 +
121 +function reset() {
122 + form.value = getClearForm()
123 +}
124 +
125 +function validate() {
126 + if (!formRef.value) return
127 +
128 + formRef.value.validate((errors?: Array<FormValidationError>) => {
129 + if (!errors) {
130 + submit()
131 + } else {
132 + message.warning("You must fill in the required fields correctly.")
133 + return false
134 + }
135 + })
136 +}
137 +
138 +function submit() {
139 + loading.value = true
140 +
141 + const payload: InvokeRequest = {
142 + activeResponseName: activeResponse.name,
143 + action: form.value.action as InvokeRequestAction,
144 + ip: form.value.ip
145 + }
146 +
147 + if (agentId) {
148 + payload.agentId = agentId.toString()
149 + }
150 +
151 + Api.activeResponse
152 + .invoke(payload)
153 + .then(res => {
154 + if (res.data.success) {
155 + message.success(res.data?.message || "Active Response invoked successfully")
156 + emit("submitted")
157 + reset()
158 + } else {
159 + message.warning(res.data?.message || "An error occurred. Please try again later.")
160 + }
161 + })
162 + .catch(err => {
163 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
164 + })
165 + .finally(() => {
166 + loading.value = false
167 + })
168 +}
169 +
170 +onMounted(() => {
171 + emit("mounted", {
172 + reset
173 + })
174 +})
175 +</script>
frontend/src/components/activeResponse/ActiveResponseItem.vue new
+114
@@ -0,0 +1,114 @@
1 +<template>
2 + <div class="active-response-item" :class="{ embedded }">
3 + <div class="px-4 py-3 flex flex-col gap-2">
4 + <div class="header-box flex justify-between items-center">
5 + <div class="title">{{ activeResponse.name }}</div>
6 + <n-button size="small" @click.stop="showDetails = true">
7 + <template #icon><Icon :name="InfoIcon"></Icon></template>
8 + </n-button>
9 + </div>
10 + <div class="main-box flex items-center gap-3">
11 + <div class="content flex flex-col gap-1 grow">
12 + <div class="description">
13 + {{ activeResponse.description }}
14 + </div>
15 + </div>
16 + <ActiveResponseActions
17 + class="actions-box"
18 + v-if="!hideActions"
19 + :agentId="agentId"
20 + :activeResponse="activeResponse"
21 + @start-loading="loading = true"
22 + @stop-loading="loading = false"
23 + />
24 + </div>
25 + <div class="footer-box flex justify-between items-center gap-4">
26 + <ActiveResponseActions
27 + class="actions-box"
28 + v-if="!hideActions"
29 + :agentId="agentId"
30 + :activeResponse="activeResponse"
31 + :size="'small'"
32 + @start-loading="loading = true"
33 + @stop-loading="loading = false"
34 + />
35 + </div>
36 + </div>
37 +
38 + <n-modal
39 + v-model:show="showDetails"
40 + preset="card"
41 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(400px, 90vh)', overflow: 'hidden' }"
42 + :title="activeResponse.name"
43 + :bordered="false"
44 + segmented
45 + >
46 + <ActiveResponseDetails :activeResponse="activeResponse" />
47 + </n-modal>
48 + </div>
49 +</template>
50 +
51 +<script setup lang="ts">
52 +import { ref, toRefs } from "vue"
53 +import type { SupportedActiveResponse } from "@/types/activeResponse"
54 +import ActiveResponseActions from "./ActiveResponseActions.vue"
55 +import ActiveResponseDetails from "./ActiveResponseDetails.vue"
56 +import { NButton, NModal } from "naive-ui"
57 +import Icon from "@/components/common/Icon.vue"
58 +
59 +const props = defineProps<{
60 + activeResponse: SupportedActiveResponse
61 + embedded?: boolean
62 + hideActions?: boolean
63 + agentId?: string | number
64 +}>()
65 +const { activeResponse, embedded, agentId, hideActions } = toRefs(props)
66 +
67 +const InfoIcon = "carbon:information"
68 +const loading = ref(false)
69 +const showDetails = ref(false)
70 +</script>
71 +
72 +<style lang="scss" scoped>
73 +.active-response-item {
74 + border-radius: var(--border-radius);
75 + background-color: var(--bg-color);
76 + border: var(--border-small-050);
77 + transition: all 0.2s var(--bezier-ease);
78 +
79 + .main-box {
80 + .content {
81 + word-break: break-word;
82 +
83 + .description {
84 + color: var(--fg-secondary-color);
85 + font-size: 13px;
86 + }
87 + }
88 + }
89 +
90 + .footer-box {
91 + display: none;
92 + font-size: 13px;
93 + margin-top: 10px;
94 + }
95 +
96 + &.embedded {
97 + background-color: var(--bg-secondary-color);
98 + }
99 + &:hover {
100 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
101 + }
102 +
103 + @container (max-width: 650px) {
104 + .main-box {
105 + .actions-box {
106 + display: none;
107 + }
108 + }
109 + .footer-box {
110 + display: flex;
111 + }
112 + }
113 +}
114 +</style>
frontend/src/components/activeResponse/ActiveResponseWizard.vue new
+255
@@ -0,0 +1,255 @@
1 +<template>
2 + <n-spin :show="loading" class="active-response-wizard">
3 + <div class="wrapper flex flex-col">
4 + <div class="grow flex flex-col">
5 + <n-scrollbar x-scrollable trigger="none">
6 + <div class="p-7 pt-4">
7 + <n-steps :current="current" size="small" :status="currentStatus">
8 + <n-step title="Operative System" />
9 + <n-step title="Active Response" />
10 + <n-step title="Submission" />
11 + </n-steps>
12 + </div>
13 + </n-scrollbar>
14 +
15 + <div class="mt-4 grow flex flex-col">
16 + <Transition :name="`slide-form-${slideFormDirection}`">
17 + <div v-if="current === 1" class="px-7 flex flex-col gap-2">
18 + <div class="os-button" @click="setOs('linux')">
19 + <Icon :size="18" :name="iconFromOs('linux')"></Icon>
20 + <span>LINUX</span>
21 + </div>
22 + <div class="os-button" @click="setOs('windows')">
23 + <Icon :size="18" :name="iconFromOs('windows')"></Icon>
24 + <span>WINDOWS</span>
25 + </div>
26 + <div class="os-button" @click="setOs('macos')">
27 + <Icon :size="18" :name="iconFromOs('macos')"></Icon>
28 + <span>MACOS</span>
29 + </div>
30 + </div>
31 +
32 + <div v-else-if="current === 2" class="px-7">
33 + <n-spin :show="loadingActiveResponse">
34 + <div class="list">
35 + <template v-if="activeResponseFiltered.length">
36 + <ActiveResponseItem
37 + v-for="activeResponse of activeResponseFiltered"
38 + :key="activeResponse.name"
39 + :activeResponse="activeResponse"
40 + embedded
41 + hide-actions
42 + class="mb-2 cursor-pointer"
43 + @click="setActiveResponse(activeResponse)"
44 + />
45 + </template>
46 + <template v-else>
47 + <n-empty
48 + description="No items found"
49 + class="justify-center h-48"
50 + v-if="!loadingActiveResponse"
51 + />
52 + </template>
53 + </div>
54 + </n-spin>
55 + </div>
56 + <div v-else-if="current === 3" class="px-7 grow flex flex-col pb-7" style="min-height: 401px">
57 + <ActiveResponseInvokeForm
58 + v-if="selectedActiveResponse"
59 + :activeResponse="selectedActiveResponse"
60 + @mounted="activeResponseInvokeFormCTX = $event"
61 + @submitted="reset()"
62 + @startLoading="loadingActiveResponseInvoke = true"
63 + @stopLoading="loadingActiveResponseInvoke = false"
64 + >
65 + <template #additionalActions>
66 + <n-button @click="prev()" :disabled="loadingActiveResponseInvoke">
67 + <template #icon>
68 + <Icon :name="ArrowLeftIcon"></Icon>
69 + </template>
70 + Prev
71 + </n-button>
72 + </template>
73 + </ActiveResponseInvokeForm>
74 + </div>
75 + </Transition>
76 + </div>
77 + </div>
78 +
79 + <div class="flex justify-between gap-4 p-7 pt-4" v-if="current !== 3">
80 + <div class="flex gap-4">
81 + <slot name="additionalActions"></slot>
82 + </div>
83 + <div class="flex gap-4">
84 + <n-button @click="prev()" v-if="isPrevStepEnabled">
85 + <template #icon>
86 + <Icon :name="ArrowLeftIcon"></Icon>
87 + </template>
88 + Prev
89 + </n-button>
90 + </div>
91 + </div>
92 + </div>
93 + </n-spin>
94 +</template>
95 +
96 +<script setup lang="ts">
97 +import { computed, onMounted, ref, watch } from "vue"
98 +import { NSteps, NStep, useMessage, NScrollbar, NButton, NEmpty, NSpin, type StepsProps } from "naive-ui"
99 +import Icon from "@/components/common/Icon.vue"
100 +import Api from "@/api"
101 +import { onBeforeMount } from "vue"
102 +import type { SupportedActiveResponse } from "@/types/activeResponse"
103 +import ActiveResponseItem from "./ActiveResponseItem.vue"
104 +import ActiveResponseInvokeForm from "./ActiveResponseInvokeForm.vue"
105 +import { iconFromOs } from "@/utils"
106 +
107 +type OS = "linux" | "windows" | "macos"
108 +
109 +const emit = defineEmits<{
110 + (e: "update:loading", value: boolean): void
111 + (
112 + e: "mounted",
113 + value: {
114 + reset: () => void
115 + }
116 + ): void
117 +}>()
118 +
119 +const ArrowLeftIcon = "carbon:arrow-left"
120 +
121 +const loadingActiveResponse = ref(false)
122 +const loadingActiveResponseInvoke = ref(false)
123 +const loading = computed(() => loadingActiveResponseInvoke.value)
124 +const slideFormDirection = ref<"right" | "left">("right")
125 +const activeResponseList = ref<SupportedActiveResponse[]>([])
126 +const message = useMessage()
127 +const current = ref<number>(1)
128 +const currentStatus = ref<StepsProps["status"]>("process")
129 +const selectedOS = ref<OS | null>(null)
130 +const selectedActiveResponse = ref<SupportedActiveResponse | null>(null)
131 +const activeResponseInvokeFormCTX = ref<{ reset: () => void } | null>(null)
132 +
133 +watch(loading, val => {
134 + emit("update:loading", val)
135 +})
136 +
137 +const activeResponseFiltered = computed(() => {
138 + if (selectedOS.value === null) {
139 + return activeResponseList.value
140 + }
141 + return activeResponseList.value.filter(o => o.name.toLowerCase().indexOf(selectedOS.value || "") === 0)
142 +})
143 +const isPrevStepEnabled = computed(() => current.value > 1)
144 +
145 +function next() {
146 + currentStatus.value = "process"
147 + slideFormDirection.value = "right"
148 + current.value++
149 +}
150 +
151 +function prev() {
152 + currentStatus.value = "process"
153 + slideFormDirection.value = "left"
154 + current.value--
155 + activeResponseInvokeFormCTX.value?.reset()
156 +}
157 +
158 +function reset() {
159 + if (!loadingActiveResponseInvoke.value) {
160 + currentStatus.value = "process"
161 + slideFormDirection.value = "right"
162 + current.value = 1
163 + selectedOS.value = null
164 + selectedActiveResponse.value = null
165 + activeResponseInvokeFormCTX.value?.reset()
166 + }
167 +}
168 +
169 +function getActiveResponseList() {
170 + loadingActiveResponse.value = true
171 +
172 + Api.activeResponse
173 + .getSupported()
174 + .then(res => {
175 + if (res.data.success) {
176 + activeResponseList.value = res.data?.supported_active_responses || []
177 + } else {
178 + message.warning(res.data?.message || "An error occurred. Please try again later.")
179 + }
180 + })
181 + .catch(err => {
182 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
183 + })
184 + .finally(() => {
185 + loadingActiveResponse.value = false
186 + })
187 +}
188 +
189 +function setOs(os: OS) {
190 + selectedOS.value = os
191 + next()
192 +}
193 +
194 +function setActiveResponse(activeResponse: SupportedActiveResponse) {
195 + selectedActiveResponse.value = activeResponse
196 + next()
197 +}
198 +
199 +onBeforeMount(() => {
200 + getActiveResponseList()
201 +})
202 +
203 +onMounted(() => {
204 + emit("mounted", {
205 + reset
206 + })
207 +})
208 +</script>
209 +
210 +<style lang="scss" scoped>
211 +.active-response-wizard {
212 + .wrapper {
213 + min-height: 480px;
214 + }
215 +
216 + .os-button {
217 + border-radius: var(--border-radius);
218 + background-color: var(--bg-secondary-color);
219 + border: var(--border-small-050);
220 + transition: all 0.2s var(--bezier-ease);
221 + cursor: pointer;
222 + line-height: 1;
223 + @apply p-4 flex gap-3 items-center;
224 +
225 + &:hover {
226 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
227 + }
228 + }
229 +
230 + .slide-form-right-enter-active,
231 + .slide-form-right-leave-active,
232 + .slide-form-left-enter-active,
233 + .slide-form-left-leave-active {
234 + transition: all 0.2s ease-out;
235 + position: absolute;
236 + width: 100%;
237 + }
238 +
239 + .slide-form-left-enter-from {
240 + transform: translateX(-100%);
241 + }
242 +
243 + .slide-form-left-leave-to {
244 + transform: translateX(100%);
245 + }
246 +
247 + .slide-form-right-enter-from {
248 + transform: translateX(100%);
249 + }
250 +
251 + .slide-form-right-leave-to {
252 + transform: translateX(-100%);
253 + }
254 +}
255 +</style>
frontend/src/components/activeResponse/ActiveResponseWizardButton.vue new
+41
@@ -0,0 +1,41 @@
1 +<template>
2 + <n-button :size="size" :type="type" @click="showInvokeWizard = true" :loading="loading">
3 + <template #icon><Icon :name="InvokeIcon"></Icon></template>
4 + Active Response
5 + </n-button>
6 +
7 + <n-modal
8 + v-model:show="showInvokeWizard"
9 + display-directive="show"
10 + preset="card"
11 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
12 + title="Active Response Wizard"
13 + :bordered="false"
14 + content-style="padding:0"
15 + content-class="flex flex-col"
16 + segmented
17 + >
18 + <ActiveResponseWizard @mounted="activeResponseWizardCTX = $event" v-model:loading="loading" />
19 + </n-modal>
20 +</template>
21 +
22 +<script setup lang="ts">
23 +import { ref, watch } from "vue"
24 +import { NButton, NModal } from "naive-ui"
25 +import Icon from "@/components/common/Icon.vue"
26 +import ActiveResponseWizard from "./ActiveResponseWizard.vue"
27 +
28 +const { type, size } = defineProps<{
29 + size?: "tiny" | "small" | "medium" | "large"
30 + type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
31 +}>()
32 +
33 +const InvokeIcon = "solar:playback-speed-outline"
34 +const showInvokeWizard = ref(false)
35 +const activeResponseWizardCTX = ref<{ reset: () => void } | null>(null)
36 +const loading = ref(false)
37 +
38 +watch(showInvokeWizard, () => {
39 + activeResponseWizardCTX.value?.reset()
40 +})
41 +</script>
frontend/src/components/agents/agentFlow/AgentFlowQueryStat.vue
+11 -11
@@ -11,12 +11,12 @@
11 </span>
12 </div>
13 <div class="actions whitespace-nowrap">
14 - <Badge type="cursor" @click="showDetails = true">
15 - <template #iconLeft>
16 - <Icon :name="InfoIcon" :size="14"></Icon>
14 + <n-button size="small" @click.stop="showDetails = true">
15 + <template #icon>
16 + <Icon :name="InfoIcon"></Icon>
17 </template>
18 - <template #value>Details</template>
19 - </Badge>
18 + Details
19 + </n-button>
20 </div>
21 </div>
22 <div class="main-box flex flex-col gap-2 mt-2">
@@ -41,12 +41,12 @@
41 </div>
42 <div class="footer-box">
43 <div class="actions whitespace-nowrap">
44 - <Badge type="cursor" @click="showDetails = true">
45 - <template #iconLeft>
46 - <Icon :name="InfoIcon" :size="14"></Icon>
44 + <n-button size="small" @click.stop="showDetails = true">
45 + <template #icon>
46 + <Icon :name="InfoIcon"></Icon>
47 </template>
48 - <template #value>Details</template>
49 - </Badge>
48 + Details
49 + </n-button>
50 </div>
51 </div>
52
@@ -88,7 +88,7 @@
88 </template>
89
90 <script setup lang="ts">
91 -import { NModal, NTabs, NTabPane, NInput } from "naive-ui"
91 +import { NModal, NTabs, NTabPane, NInput, NButton } from "naive-ui"
92 import { useSettingsStore } from "@/stores/settings"
93 import dayjs from "@/utils/dayjs"
94 import type { FlowQueryStat } from "@/types/flow.d"
frontend/src/components/alerts/Alert.vue
-7
@@ -15,13 +15,6 @@
15 <div class="rule-groups">{{ alert._source.rule_groups }}</div>
16
17 <div class="badges-box flex flex-wrap items-center gap-3" v-if="alert._id">
18 - <!--
19 - <Badge type="cursor">
20 - <template #iconLeft>
21 - <Icon :name="InfoIcon" :size="14"></Icon>
22 - </template>
23 - </Badge>
24 - -->
18 <Badge type="splitted">
19 <template #iconLeft>
20 <Icon :name="TargetIcon" :size="13" class="!opacity-80"></Icon>
frontend/src/components/alerts/AlertActions.vue
+5 -1
@@ -85,7 +85,11 @@ const alertAskMessage = ref("")
85 const isAskVisible = computed(() => alert._source?.rule_group3 === "sigma" && !alertAskMessage.value)
86
87 watch(loading, val => {
88 - emit(val ? "startLoading" : "startLoading")
88 + if (val) {
89 + emit("startLoading")
90 + } else {
91 + emit("stopLoading")
92 + }
93 })
94
95 watch(alertUrl, val => {
frontend/src/components/alerts/ThreatIntelButton.vue
+6 -1
@@ -1,5 +1,8 @@
1 <template>
2 - <n-button :size="size" :type="type" @click="showThreatIntelDrawer = true">Threat Intel</n-button>
2 + <n-button :size="size" :type="type" @click="showThreatIntelDrawer = true">
3 + <template #icon><Icon :name="ThreatIcon"></Icon></template>
4 + Threat Intel
5 + </n-button>
6
7 <n-drawer
8 v-model:show="showThreatIntelDrawer"
@@ -18,12 +21,14 @@
21 import { ref, watch } from "vue"
22 import { NButton, NDrawer, NDrawerContent } from "naive-ui"
23 import ThreatIntelForm from "./ThreatIntelForm.vue"
24 +import Icon from "@/components/common/Icon.vue"
25
26 const { type, size } = defineProps<{
27 size?: "tiny" | "small" | "medium" | "large"
28 type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
29 }>()
30
31 +const ThreatIcon = "mynaui:info-waves"
32 const showThreatIntelDrawer = ref(false)
33 const threatIntelCTX = ref<{ restore: () => void } | null>(null)
34
frontend/src/components/common/Badge.vue
+1 -1
@@ -17,7 +17,7 @@
17 </template>
18
19 <script setup lang="ts">
20 -const { type, hintCursor, pointCursor, color } = defineProps<{
20 +const { type, hintCursor, pointCursor, color, href } = defineProps<{
21 type?: "splitted" | "muted" | "active" | "cursor"
22 hintCursor?: boolean
23 pointCursor?: boolean
frontend/src/components/common/CardStats.vue
+1 -1
@@ -28,7 +28,7 @@ const props = defineProps<{
28 vertical?: boolean
29 hovered?: boolean
30 }>()
31 -const { title, value, vertical } = toRefs(props)
31 +const { title, value, vertical, hovered } = toRefs(props)
32
33 const ArrowRightIcon = "carbon:arrow-right"
34 </script>
frontend/src/components/common/CardStatsDouble.vue
+1 -1
@@ -39,7 +39,7 @@ const props = defineProps<{
39 secondStatus?: "success" | "warning" | "error"
40 hovered?: boolean
41 }>()
42 -const { title, value, subValue, firstLabel, secondLabel, firstStatus, secondStatus } = toRefs(props)
42 +const { title, value, subValue, firstLabel, secondLabel, firstStatus, secondStatus, hovered } = toRefs(props)
43
44 const ArrowRightIcon = "carbon:arrow-right"
45 </script>
frontend/src/components/common/CardStatsIcon.vue
+1 -1
@@ -23,7 +23,7 @@ const props = withDefaults(
23 }>(),
24 { boxSize: 40, iconSize: 28, boxed: false }
25 )
26 -const { boxed, boxSize, iconSize, color } = toRefs(props)
26 +const { boxed, boxSize, iconSize, iconName, color } = toRefs(props)
27
28 const style = computed<{ [key: string]: any }>(() => useThemeStore().style)
29
frontend/src/components/common/Markdown.vue
+2
@@ -15,10 +15,12 @@ import { VueMarkdownIt } from "@f3ve/vue-markdown-it"
15
16 import hljs from "highlight.js/lib/core"
17 import powershell from "highlight.js/lib/languages/powershell"
18 +import bash from "highlight.js/lib/languages/bash"
19 import json from "highlight.js/lib/languages/json"
20 import xml from "highlight.js/lib/languages/xml"
21
22 hljs.registerLanguage("powershell", powershell)
23 +hljs.registerLanguage("bash", bash)
24 hljs.registerLanguage("json", json)
25 hljs.registerLanguage("xml", xml)
26
frontend/src/components/customers/CustomerItem.vue
+6 -6
@@ -4,12 +4,12 @@
4 <div class="header-box flex justify-between items-center">
5 <div class="id">#{{ customer.customer_code }}</div>
6 <div class="actions" v-if="!hideCardActions">
7 - <Badge type="cursor" @click="showDetails = true">
8 - <template #iconLeft>
9 - <Icon :name="DetailsIcon" :size="14"></Icon>
7 + <n-button size="small" @click.stop="showDetails = true">
8 + <template #icon>
9 + <Icon :name="DetailsIcon"></Icon>
10 </template>
11 - <template #value>Details</template>
12 - </Badge>
11 + Details
12 + </n-button>
13 </div>
14 </div>
15 <div class="main-box flex items-center gap-3">
@@ -185,7 +185,7 @@ import CustomerProvision from "./provision/CustomerProvision.vue"
185 import CustomerHealthcheckList from "./healthcheck/CustomerHealthcheckList.vue"
186 import CustomerIntegrations from "./integrations/CustomerIntegrations.vue"
187 import Api from "@/api"
188 -import { NAvatar, useMessage, NPopover, NModal, NTabs, NTabPane, NSpin, NScrollbar } from "naive-ui"
188 +import { NAvatar, useMessage, NPopover, NModal, NTabs, NTabPane, NSpin, NScrollbar, NButton } from "naive-ui"
189 import type { Customer, CustomerMeta } from "@/types/customers.d"
190 import { hashMD5 } from "@/utils"
191 import _toSafeInteger from "lodash/toSafeInteger"
frontend/src/components/customers/integrations/CustomerIntegrationActions.vue
+6 -2
@@ -55,7 +55,7 @@ const emit = defineEmits<{
55 (e: "deleted"): void
56 }>()
57
58 -const { integration, size } = defineProps<{
58 +const { integration, hideDeleteButton, size } = defineProps<{
59 integration: CustomerIntegration
60 hideDeleteButton?: boolean
61 size?: "tiny" | "small" | "medium" | "large"
@@ -77,7 +77,11 @@ const serviceName = computed(() => integration.integration_service_name)
77 const customerCode = computed(() => integration.customer_code)
78
79 watch(loading, val => {
80 - emit(val ? "startLoading" : "startLoading")
80 + if (val) {
81 + emit("startLoading")
82 + } else {
83 + emit("stopLoading")
84 + }
85 })
86
87 function office365Provision() {
frontend/src/components/customers/integrations/CustomerIntegrationItem.vue
+6 -7
@@ -10,12 +10,11 @@
10 </template>
11 <template #value>Deployed</template>
12 </Badge>
13 - <Badge type="cursor" @click.stop="showDetails = true">
14 - <template #iconLeft>
15 - <Icon :name="DetailsIcon" :size="14"></Icon>
13 + <n-button size="small" @click.stop="showDetails = true">
14 + <template #icon>
15 + <Icon :name="InfoIcon"></Icon>
16 </template>
17 - <template #value>Details</template>
18 - </Badge>
17 + </n-button>
18 </div>
19 </div>
20 <div class="main-box flex items-center gap-3">
@@ -64,7 +63,7 @@
63 import Icon from "@/components/common/Icon.vue"
64 import Badge from "@/components/common/Badge.vue"
65 import { computed, ref, toRefs } from "vue"
67 -import { NModal } from "naive-ui"
66 +import { NModal, NButton } from "naive-ui"
67 import type { CustomerIntegration } from "@/types/integrations"
68 import CustomerIntegrationActions from "./CustomerIntegrationActions.vue"
69 import KVCard from "@/components/common/KVCard.vue"
@@ -82,7 +81,7 @@ const emit = defineEmits<{
81 }>()
82
83 const DeployIcon = "carbon:deploy"
85 -const DetailsIcon = "carbon:settings-adjust"
84 +const InfoIcon = "carbon:information"
85
86 const showDetails = ref(false)
87 const serviceName = computed(() => integration.value.integration_service_name)
frontend/src/components/customers/integrations/CustomerIntegrations.vue
+1 -1
@@ -56,7 +56,7 @@ import CustomerIntegrationForm from "./CustomerIntegrationForm.vue"
56 import CustomerIntegrationItem from "./CustomerIntegrationItem.vue"
57 import type { CustomerIntegration } from "@/types/integrations"
58
59 -const { customerCode } = defineProps<{
59 +const { customerCode, customerName } = defineProps<{
60 customerCode: string
61 customerName: string
62 }>()
frontend/src/components/graylog/Inputs/Item.vue
+7 -7
@@ -1,24 +1,24 @@
1 <template>
2 <div class="item flex flex-col gap-2 px-5 py-3">
3 - <div class="header-box flex justify-between">
4 - <div class="info flex items-center gap-2">
3 + <div class="header-box flex items-center gap-3">
4 + <div class="info flex items-center gap-2 grow">
5 <div class="user flex items-center gap-2">
6 <Icon :name="UserIcon" :size="14"></Icon>
7 {{ input.creator_user_id }}
8 </div>
9 </div>
10 <div class="time">{{ formatDate(input.created_at) }}</div>
11 + <n-button size="small" @click.stop="showDetails = true">
12 + <template #icon>
13 + <Icon :name="InfoIcon"></Icon>
14 + </template>
15 + </n-button>
16 </div>
17 <div class="main-box flex justify-between">
18 <div class="content">
19 <div class="title">{{ input.title }}</div>
20 <div class="name mb-2">{{ input.name }}</div>
21 <div class="badges-box flex flex-wrap items-center gap-3">
17 - <Badge type="cursor" @click="showDetails = true">
18 - <template #iconLeft>
19 - <Icon :name="InfoIcon" :size="14"></Icon>
20 - </template>
21 - </Badge>
22 <Badge :type="input.global ? 'active' : 'muted'">
23 <template #iconRight>
24 <Icon :name="input.global ? GlobalIcon : DisabledIcon" :size="14"></Icon>
frontend/src/components/graylog/Streams/Item.vue
+7 -7
@@ -1,24 +1,24 @@
1 <template>
2 <div class="item flex flex-col gap-2 px-5 py-3" :class="{ default: stream.is_default }">
3 - <div class="header-box flex justify-between">
4 - <div class="info flex items-center gap-2">
3 + <div class="header-box flex items-center gap-3">
4 + <div class="info flex items-center gap-2 grow">
5 <div class="user flex items-center gap-2">
6 <Icon :name="UserIcon" :size="14"></Icon>
7 {{ stream.creator_user_id }}
8 </div>
9 </div>
10 <div class="time">{{ formatDate(stream.created_at) }}</div>
11 + <n-button size="small" @click.stop="showDetails = true">
12 + <template #icon>
13 + <Icon :name="InfoIcon"></Icon>
14 + </template>
15 + </n-button>
16 </div>
17 <div class="main-box flex justify-between">
18 <div class="content">
19 <div class="title">{{ stream.title }}</div>
20 <div class="description mb-2">{{ stream.description }}</div>
21 <div class="badges-box flex flex-wrap items-center gap-3">
17 - <Badge type="cursor" @click="showDetails = true">
18 - <template #iconLeft>
19 - <Icon :name="InfoIcon" :size="14"></Icon>
20 - </template>
21 - </Badge>
22 <Badge :type="stream.disabled ? 'muted' : 'active'">
23 <template #iconRight>
24 <Icon :name="stream.disabled ? DisabledIcon : EnabledIcon" :size="14"></Icon>
frontend/src/components/integrations/IntegrationItem.vue
+6 -7
@@ -9,12 +9,11 @@
9 <div class="id">#{{ integration.id }}</div>
10 </div>
11 <div class="actions">
12 - <Badge type="cursor" @click.stop="showDetails = true">
13 - <template #iconLeft>
14 - <Icon :name="DetailsIcon" :size="14"></Icon>
12 + <n-button size="small" @click.stop="showDetails = true">
13 + <template #icon>
14 + <Icon :name="InfoIcon"></Icon>
15 </template>
16 - <template #value>Details</template>
17 - </Badge>
16 + </n-button>
17 </div>
18 </div>
19 <div class="main-box flex items-center gap-3">
@@ -51,7 +50,7 @@
50 import Icon from "@/components/common/Icon.vue"
51 import Badge from "@/components/common/Badge.vue"
52 import { defineAsyncComponent, ref, toRefs } from "vue"
54 -import { NModal, NRadio } from "naive-ui"
53 +import { NModal, NRadio, NButton } from "naive-ui"
54 import type { AvailableIntegration } from "@/types/integrations"
55 const Markdown = defineAsyncComponent(() => import("@/components/common/Markdown.vue"))
56
@@ -64,7 +63,7 @@ const props = defineProps<{
63 }>()
64 const { integration, embedded, checked, selectable, disabled } = toRefs(props)
65
67 -const DetailsIcon = "carbon:settings-adjust"
66 +const InfoIcon = "carbon:information"
67
68 const showDetails = ref(false)
69 </script>
frontend/src/components/soc/SocAlerts/SocAlertItem.vue
+13 -4
@@ -297,7 +297,6 @@
297 </template>
298
299 <script setup lang="ts">
300 -// TODO: add customer goto function ??
300 import AlertItem from "@/components/alerts/Alert.vue"
301 import type { SocAlert } from "@/types/soc/alert.d"
302 import type { Alert } from "@/types/alerts.d"
@@ -352,8 +351,18 @@ const props = defineProps<{
351 showBadgesToggle?: boolean
352 showCheckbox?: boolean
353 }>()
355 -const { alertData, alertId, isBookmark, highlight, users, embedded, hideSocCaseAction, hideBookmarkAction } =
356 - toRefs(props)
354 +const {
355 + alertData,
356 + alertId,
357 + isBookmark,
358 + highlight,
359 + users,
360 + embedded,
361 + hideSocCaseAction,
362 + hideBookmarkAction,
363 + showBadgesToggle,
364 + showCheckbox
365 +} = toRefs(props)
366
367 const ChevronIcon = "carbon:chevron-right"
368 const InfoIcon = "carbon:information"
@@ -483,7 +492,7 @@ function deleted() {
492 emit("deleted")
493 }
494
486 -function gotoCustomer(code: any) {
495 +function gotoCustomer(code: string | number | { [key: string]: any }) {
496 router.push({ name: "Customers", query: { code: code.toString() } })
497 }
498
frontend/src/components/soc/SocAlerts/SocAlertItemActions.vue
+5 -1
@@ -85,7 +85,11 @@ const loading = computed(() => loadingCaseCreation.value || loadingAlertDelete.v
85 const existCase = ref(!!caseId)
86
87 watch(loading, val => {
88 - emit(val ? "startLoading" : "startLoading")
88 + if (val) {
89 + emit("startLoading")
90 + } else {
91 + emit("stopLoading")
92 + }
93 })
94
95 function openSocCase() {
frontend/src/components/soc/SocCases/SocCaseItem.vue
+1 -1
@@ -276,7 +276,7 @@ import _omit from "lodash/omit"
276 import _split from "lodash/split"
277 import { useRouter } from "vue-router"
278
279 -const { caseData, caseId, embedded, hideSocCaseAction } = defineProps<{
279 +const { caseData, caseId, embedded, hideSocCaseAction, hideSocAlertLink } = defineProps<{
280 caseData?: SocCase
281 caseId?: number | string
282 embedded?: boolean
frontend/src/components/soc/SocCases/SocCaseItemActions.vue
+7 -3
@@ -30,12 +30,12 @@ import { computed, watch, ref } from "vue"
30 import { StateName, type SocCase, type SocCaseExt } from "@/types/soc/case.d"
31
32 const emit = defineEmits<{
33 - (e: "startLoading"): void
34 - (e: "stopLoading"): void
33 (e: "closed"): void
34 (e: "reopened"): void
35 (e: "deleted"): void
36 (e: "startDeleting"): void
37 + (e: "startLoading"): void
38 + (e: "stopLoading"): void
39 }>()
40
41 const { caseData, size } = defineProps<{
@@ -57,7 +57,11 @@ const loading = computed(() => loadingCaseClose.value || loadingCaseReopen.value
57 const isCaseClosed = computed(() => caseData?.state_name === StateName.Closed)
58
59 watch(loading, val => {
60 - emit(val ? "startLoading" : "startLoading")
60 + if (val) {
61 + emit("startLoading")
62 + } else {
63 + emit("stopLoading")
64 + }
65 })
66
67 function closeCase() {
frontend/src/types/activeResponse.d.ts new
+9
@@ -0,0 +1,9 @@
1 +export interface SupportedActiveResponse {
2 + name: string
3 + description: string
4 +}
5 +export interface ActiveResponseDetails {
6 + name: string
7 + description: string
8 + markdown_content: string
9 +}
frontend/src/utils/index.ts
+12 -3
@@ -44,13 +44,22 @@ export function renderIcon(icon: Component | string) {
44 export function iconFromOs(os: string): string {
45 const test = os.toLowerCase()
46 if (test.indexOf("mac") !== -1 || test.indexOf("darwin") !== -1 || test.indexOf("apple") !== -1) {
47 - return "uit:apple-alt"
47 + return "mdi:apple"
48 }
49 if (test.indexOf("win") !== -1 || test.indexOf("microsoft") !== -1) {
50 - return "arcticons:microsoft-alt"
50 + return "mdi:microsoft"
51 + }
52 + if (
53 + test.indexOf("linux") !== -1 ||
54 + test.indexOf("unix") !== -1 ||
55 + test.indexOf("x11") !== -1 ||
56 + test.indexOf("debian") !== -1 ||
57 + test.indexOf("centos") !== -1
58 + ) {
59 + return "mdi:linux"
60 }
61
53 - return "uil:linux"
62 + return "mdi:help-box"
63 }
64
65 export function getOS(): OS {
frontend/src/views/AgentOverview.vue
+4
@@ -97,6 +97,9 @@
97 :artifacts="artifacts"
98 />
99 </n-tab-pane>
100 + <n-tab-pane name="active-response" tab="Active Response" display-directive="show:lazy">
101 + <ActiveResponseAgent v-if="agent" :agent="agent" embedded />
102 + </n-tab-pane>
103 </n-tabs>
104 </n-spin>
105 </n-card>
@@ -121,6 +124,7 @@ import type { Artifact } from "@/types/artifacts.d"
124 import ArtifactsCollect from "@/components/artifacts/ArtifactsCollect.vue"
125 import ArtifactsCommand from "@/components/artifacts/ArtifactsCommand.vue"
126 import ArtifactsQuarantine from "@/components/artifacts/ArtifactsQuarantine.vue"
127 +import ActiveResponseAgent from "@/components/activeResponse/ActiveResponseAgent.vue"
128
129 const StarIcon = "carbon:star"
130 const QuarantinedIcon = "ph:seal-warning-light"
frontend/src/views/Overview.vue
+3 -1
@@ -1,6 +1,7 @@
1 <template>
2 <div class="page" ref="page">
3 - <div class="section justify-end flex">
3 + <div class="section justify-end flex gap-3">
4 + <ActiveResponseWizardButton size="small" type="primary" />
5 <ThreatIntelButton size="small" type="primary" />
6 </div>
7 <div class="section">
@@ -47,6 +48,7 @@ import ClusterHealth from "@/components/indices/ClusterHealth.vue"
48 import NodeAllocation from "@/components/indices/NodeAllocation.vue"
49 import IndicesMarquee from "@/components/indices/Marquee.vue"
50 import ThreatIntelButton from "@/components/alerts/ThreatIntelButton.vue"
51 +import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
52 import AgentsCard from "@/components/overview/AgentsCard.vue"
53 import HealthcheckCard from "@/components/overview/HealthcheckCard.vue"
54 import SocAlertsCard from "@/components/overview/SocAlertsCard.vue"
frontend/vite.config.mts
+3 -6
@@ -39,15 +39,12 @@ export default (args: any) => {
39 server: {
40 https:
41 fs.existsSync("/certs/key.pem") && fs.existsSync("/certs/cert.pem")
42 - ? {
43 - key: fs.readFileSync("/certs/key.pem"),
44 - cert: fs.readFileSync("/certs/cert.pem")
45 - }
42 + ? { key: fs.readFileSync("/certs/key.pem"), cert: fs.readFileSync("/certs/cert.pem") }
43 : undefined,
44 proxy: {
45 "/api": {
49 - target: "http://copilot-backend:5000",
50 - //target: process.env.VITE_API_URL, // for local development
46 + // target: "http://copilot-backend:5000",
47 + target: process.env.VITE_API_URL, // for local development
48 changeOrigin: true
49 }
50 }