@cryptotaxi247 / CoPilot / commits / b5258a9d

Process insights (#222)

* feat: Add process_name field to IrisAlertContext model This commit adds a new optional field, `process_name`, to the `IrisAlertContext` model in the `escalate_alert.py` file. The `process_name` field represents the name of the process associated with the alert. This change improves the alert context by providing additional information about the alert. * feat: Remove commented code for process analysis in SocFortress route The code changes in `socfortress.py` remove the commented code for process analysis in the `threat_intel_socfortress` route. This improves the code readability and removes unnecessary code that is not being used. Note: This commit message follows the established convention of using a prefix to indicate the type of change (`feat` for a new feature) and provides a clear and concise description of the changes made. * update command to grab windows_firewall active response in MD * updated dependencies * added processNameEvaluation api * refactor soc alerts * refactor soc alerts * added Evaluation modal * updated Evaluation modal * add process_name to alert creation * updated Evaluation modal * updated Evaluation modal * updated soc alert context tab * chore: Add endpoint for retrieving artifact recommendation based on alert * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed May 27, 2024 at 13:43 UTC b5258a9da73b431d0bff74dc8bc2300ad8b06f82
38 files changed +1482 -649
.vscode/settings.json
+3
@@ -20,6 +20,7 @@
20 "forgotpassword",
21 "Healthcheck",
22 "healthchecks",
23 + "iconoir",
24 "Indicies",
25 "Logsource",
26 "majesticons",
@@ -31,6 +32,7 @@
32 "Osquery",
33 "picocolors",
34 "popconfirm",
35 + "Popselect",
36 "redoc",
37 "rushstack",
38 "Shiki",
@@ -43,6 +45,7 @@
45 "timerange",
46 "uvicorn",
47 "venv",
48 + "virustotal",
49 "vuesjv",
50 "Wazuh",
51 "xaxis",
backend/app/active_response/scripts/windows/windows_firewall.md
+1 -1
@@ -20,7 +20,7 @@ Invoke-WebRequest -Uri "https://www.python.org/ftp/python/3.11.0/python-3.11.0-a
20 ### Download Script Via PowerShell
21
22 ```powershell
23 -Invoke-WebRequest -Uri "https://repo.socfortress.co/repository/socfortress/active-response/windows_firewall.exe" -OutFile "C:\Program Files (x86)\ossec-agent\active-response\bin\windows_firewall.exe"
23 +Invoke-WebRequest -Uri "https://repo.socfortress.co/repository/socfortress/active-response/windows_firewall.exe" -OutFile "C:\Program Files (x86)\ossec-agent\active-response\bin\windows_firewall.exe" -Credential (New-Object System.Management.Automation.PSCredential ("socfortress_installer", (ConvertTo-SecureString "6cV8uJqnQffDa3Upx" -AsPlainText -Force)))
24 ```
25
26 ## Wazuh Manager Configuration
backend/app/connectors/velociraptor/routes/artifacts.py
+23
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
9 from sqlalchemy.future import select
10
11 from app.auth.utils import AuthHandler
12 +from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationRequest
13 from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
14 from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
15 from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
@@ -19,6 +20,7 @@ from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
20 from app.connectors.velociraptor.schema.artifacts import RunCommandBody
21 from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
22 from app.connectors.velociraptor.services.artifacts import get_artifacts
23 +from app.connectors.velociraptor.services.artifacts import post_to_copilot_ai_module
24 from app.connectors.velociraptor.services.artifacts import quarantine_host
25 from app.connectors.velociraptor.services.artifacts import run_artifact_collection
26 from app.connectors.velociraptor.services.artifacts import run_remote_command
@@ -400,3 +402,24 @@ async def quarantine(
402 await update_agent_quarantine_status(session, quarantine_body, quarantine_response)
403
404 return quarantine_response
405 +
406 +
407 +@velociraptor_artifacts_router.post(
408 + "/velociraptor-artifact-recommendation",
409 + description="Retrieve artifact to run based on alert. Invokes the `copilot-ai-module",
410 +)
411 +async def get_artifact_recommendation():
412 + """
413 + Retrieve the artifact to run based on the alert.
414 +
415 + Returns:
416 + str: The artifact to run based on the alert.
417 + """
418 + logger.info("Fetching artifact recommendation based on alert")
419 + artifacts = await get_artifacts()
420 + logger.info(f"Artifacts: {artifacts.artifacts}")
421 + return await post_to_copilot_ai_module(
422 + data=ArtifactReccomendationRequest(
423 + artifacts=artifacts.artifacts,
424 + ),
425 + )
backend/app/connectors/velociraptor/schema/artifacts.py
+9
@@ -117,3 +117,12 @@ class RunCommandResponse(BaseResponse):
117
118 class QuarantineResponse(BaseResponse):
119 pass # If you have additional fields, you can define them here
120 +
121 +
122 +class ArtifactReccomendationRequest(BaseModel):
123 + artifacts: List[Artifacts] = Field(..., description="List of artifacts to be recommended")
124 +
125 +
126 +class ArtifactReccomendationResponse(BaseModel):
127 + message: str = Field(...)
128 + success: bool = Field(...)
backend/app/connectors/velociraptor/services/artifacts.py
+22
@@ -1,6 +1,9 @@
1 +import httpx
2 from fastapi import HTTPException
3 from loguru import logger
4
5 +from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationRequest
6 +from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationResponse
7 from app.connectors.velociraptor.schema.artifacts import Artifacts
8 from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
9 from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
@@ -267,3 +270,22 @@ async def quarantine_host(quarantine_body: QuarantineBody) -> QuarantineResponse
270 status_code=500,
271 detail=f"Failed to run artifact collection on {quarantine_body}: {err}",
272 )
273 +
274 +
275 +################# ! ARTIFACT RECOMMENDATION ! #################
276 +async def post_to_copilot_ai_module(data: ArtifactReccomendationRequest) -> ArtifactReccomendationResponse:
277 + """
278 + Send a POST request to the copilot-ai-module Docker container.
279 +
280 + Args:
281 + data (ArtifactReccomendationRequest): The data to send to the copilot-ai-module Docker container.
282 + """
283 + logger.info(f"Sending POST request to http://copilot-ai-module/velociraptor-artifact-recommendation with data: {data.dict()}")
284 + # raise HTTPException(status_code=501, detail="Not Implemented Yet")
285 + async with httpx.AsyncClient() as client:
286 + data = await client.post(
287 + "http://127.0.0.1:5001/velociraptor-artifact-recommendation",
288 + json=data.dict(),
289 + timeout=120,
290 + )
291 + return ArtifactReccomendationResponse(**data.json())
backend/app/integrations/alert_escalation/schema/escalate_alert.py
+5
@@ -1,6 +1,7 @@
1 from enum import Enum
2 from typing import Any
3 from typing import Dict
4 +from typing import List
5 from typing import Optional
6
7 from pydantic import BaseModel
@@ -167,6 +168,10 @@ class IrisAlertContext(BaseModel):
168 example="Intrusion Detected",
169 )
170 alert_level: int = Field(..., description="Severity level of the alert", example=3)
171 + process_name: Optional[List[str]] = Field(
172 + example=["No process name found"],
173 + description="Name of the process",
174 + )
175
176 class Config:
177 extra = Extra.allow
backend/app/integrations/alert_escalation/services/escalate_alert.py
+22
@@ -1,3 +1,5 @@
1 +import os
2 +from typing import List
3 from typing import Optional
4
5 from fastapi import HTTPException
@@ -129,6 +131,25 @@ async def set_alert_level(syslog_level: str):
131 return 3
132
133
134 +async def get_process_name(source_dict: dict) -> List[str]:
135 + """
136 + Get the process name from the source dictionary.
137 +
138 + Args:
139 + source_dict (dict): The source dictionary.
140 +
141 + Returns:
142 + List[str]: The process name as a list.
143 + """
144 + # Get the last part of the process_image path
145 + process_image = source_dict.get("process_image")
146 + if process_image is None:
147 + process_image = source_dict.get("data_win_eventdata_image")
148 +
149 + process_name = os.path.basename(process_image) if process_image else None
150 + return [process_name] if process_name else []
151 +
152 +
153 async def build_alert_context_payload(
154 alert_details: GenericAlertModel,
155 customer_alert_creation_settings: AlertCreationSettings,
@@ -158,6 +179,7 @@ async def build_alert_context_payload(
179 alert_id=alert_details._id,
180 alert_name=alert_details.rule_description,
181 alert_level=await set_alert_level(alert_details.syslog_level),
182 + process_name=await get_process_name(source_dict),
183 **source_dict,
184 )
185
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py
+15
@@ -226,6 +226,14 @@ class WazuhSourceModel(BaseModel):
226 None,
227 description="The UTC timestamp of the alert.",
228 )
229 + process_image: Optional[str] = Field(
230 + "n/a",
231 + description="The process image of the alert.",
232 + )
233 + data_win_eventdata_image: Optional[str] = Field(
234 + "n/a",
235 + description="The image of the event data.",
236 + )
237
238 class Config:
239 extra = Extra.allow
@@ -252,6 +260,9 @@ class WazuhAlertModel(BaseModel):
260 class Config:
261 extra = Extra.allow
262
263 + def to_dict(self):
264 + return self.dict(exclude_none=True)
265 +
266
267 class SortOrder(Enum):
268 desc = "desc"
@@ -320,6 +331,10 @@ class WazuhIrisAlertContext(BaseModel):
331 description="MITRE ATT&CK Technique",
332 example="Scripting",
333 )
334 + process_name: Optional[List[str]] = Field(
335 + example=["No process name found"],
336 + description="Name of the process",
337 + )
338
339
340 class WazuhIrisAlertPayload(BaseModel):
backend/app/integrations/monitoring_alert/services/wazuh.py
+83
@@ -1,4 +1,6 @@
1 import json
2 +import os
3 +from typing import List
4 from typing import Optional
5 from typing import Set
6
@@ -256,6 +258,27 @@ def construct_params(request: FilterAlertsRequest) -> dict:
258 return {k: v for k, v in params.items() if v is not None}
259
260
261 +async def get_process_name(source_dict: dict) -> List[str]:
262 + """
263 + Get the process name from the source dictionary.
264 +
265 + Args:
266 + source_dict (dict): The source dictionary.
267 +
268 + Returns:
269 + List[str]: The process name as a list.
270 + """
271 + # Get the last part of the process_image path
272 + logger.info(f"Source dict: {source_dict}")
273 + source = source_dict.get("_source", {})
274 + process_image = source.get("process_image")
275 + if process_image is None:
276 + process_image = source.get("data_win_eventdata_image")
277 +
278 + process_name = os.path.basename(process_image) if process_image else None
279 + return [process_name] if process_name else ["No process name found"]
280 +
281 +
282 async def build_alert_context_payload(
283 alert_details: CreateAlertRequest,
284 agent_data: AgentsResponse,
@@ -305,6 +328,7 @@ async def build_alert_context_payload(
328 "rule_mitre_technique",
329 "No rule mitre technique found",
330 ),
331 + process_name=alert_details.process_name,
332 )
333
334
@@ -410,6 +434,7 @@ async def create_alert_details(alert_details: WazuhAlertModel) -> CreateAlertReq
434 timestamp=alert_details._source["timestamp"],
435 timestamp_utc=alert_details._source.get("timestamp_utc", alert_details._source["timestamp"]),
436 process_id=alert_details._source.get("process_id", "No process ID found"),
437 + process_name=await get_process_name(alert_details.to_dict()),
438 )
439
440
@@ -477,6 +502,24 @@ async def get_current_assets(client, alert_client, iris_alert_id):
502 return result["data"]["assets"]
503
504
505 +async def get_current_process_names(client, alert_client, iris_alert_id):
506 + result = await fetch_and_validate_data(
507 + client,
508 + alert_client.get_alert,
509 + iris_alert_id,
510 + )
511 + return result["data"]["alert_context"]["process_name"]
512 +
513 +
514 +async def get_current_alert_context(client, alert_client, iris_alert_id):
515 + result = await fetch_and_validate_data(
516 + client,
517 + alert_client.get_alert,
518 + iris_alert_id,
519 + )
520 + return result["data"]["alert_context"]
521 +
522 +
523 async def update_alert_with_assets(client, alert_client, iris_alert_id, current_assets):
524 await fetch_and_validate_data(
525 client,
@@ -486,6 +529,26 @@ async def update_alert_with_assets(client, alert_client, iris_alert_id, current_
529 )
530
531
532 +async def update_alert_with_process_names(client, alert_client, iris_alert_id, current_process_names):
533 + await fetch_and_validate_data(
534 + client,
535 + alert_client.update_alert,
536 + iris_alert_id,
537 + {"alert_context": {"process_name": current_process_names}},
538 + )
539 +
540 +
541 +async def update_alert_context(client, alert_client, iris_alert_id, current_iris_alert_context, current_process_names):
542 + alert_context = await current_iris_alert_context
543 + alert_context["process_name"] = current_process_names
544 + await fetch_and_validate_data(
545 + client,
546 + alert_client.update_alert,
547 + iris_alert_id,
548 + {"alert_context": alert_context},
549 + )
550 +
551 +
552 async def remove_duplicate_assets(current_assets):
553 """
554 Removes duplicate assets from the given list of current_assets.
@@ -562,7 +625,20 @@ async def analyze_wazuh_alerts(
625 alert_client,
626 iris_alert_id,
627 )
628 + current_iris_alert_context = get_current_alert_context(
629 + client,
630 + alert_client,
631 + iris_alert_id,
632 + )
633 + current_process_names = await get_current_process_names(
634 + client,
635 + alert_client,
636 + iris_alert_id,
637 + )
638 alert_details = await create_alert_details(alert_details)
639 + # Add the new `process_name` to the `current_process_names`` list
640 + current_process_names.extend(alert_details.process_name)
641 + logger.info(f"Current process names: {current_process_names}")
642 agent_details = await get_agent_by_hostname(alert_details.agent_name, session)
643 asset_payload = await build_asset_payload(
644 agent_data=agent_details,
@@ -577,6 +653,13 @@ async def analyze_wazuh_alerts(
653 iris_alert_id,
654 current_assets,
655 )
656 + await update_alert_context(
657 + client,
658 + alert_client,
659 + iris_alert_id,
660 + current_iris_alert_context,
661 + current_process_names,
662 + )
663 await remove_alert_id(alert.alert_id, session)
664 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
665 await add_alert_to_document(
backend/app/threat_intel/routes/socfortress.py
+37
@@ -10,7 +10,10 @@ from app.db.db_session import get_db
10 from app.middleware.license import get_license
11 from app.middleware.license import is_feature_enabled
12 from app.threat_intel.schema.socfortress import IoCResponse
13 +from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisRequest
14 +from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisResponse
15 from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
16 +from app.threat_intel.services.socfortress import socfortress_process_analysis_lookup
17 from app.threat_intel.services.socfortress import socfortress_threat_intel_lookup
18 from app.utils import get_connector_attribute
19
@@ -80,3 +83,37 @@ async def threat_intel_socfortress(
83 session=session,
84 )
85 return socfortress_lookup
86 +
87 +
88 +@threat_intel_socfortress_router.post(
89 + "/process_name",
90 + response_model=SocfortressProcessNameAnalysisResponse,
91 + description="SocFortress Process Name Evaluation",
92 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
93 +)
94 +async def process_name_intel_socfortress(
95 + request: SocfortressProcessNameAnalysisRequest,
96 + session: AsyncSession = Depends(get_db),
97 +):
98 + """
99 + Endpoint for SocFortress Process Name Evaluation.
100 +
101 + This endpoint allows authorized users with 'admin' or 'analyst' scope to perform SocFortress process name evaluation.
102 +
103 + Parameters:
104 + - request: SocfortressThreatIntelRequest - The request payload containing the necessary information for the lookup.
105 + - session: AsyncSession (optional) - The database session to use for the lookup.
106 + - _key_exists: bool (optional) - A dependency to ensure the API key exists.
107 +
108 + Returns:
109 + - SocfortressProcessNameAnalysisResponse: The response model containing the results of the SocFortress process name analysis lookup.
110 + """
111 + # await is_feature_enabled("PROCESS ANALYSIS", session=session)
112 + logger.info("Running SOCFortress Process Name Analysis. Grabbing License")
113 +
114 + socfortress_lookup = await socfortress_process_analysis_lookup(
115 + lincense_key=(await get_license(session)).license_key,
116 + request=request,
117 + session=session,
118 + )
119 + return socfortress_lookup
backend/app/threat_intel/schema/socfortress.py
+69
@@ -1,3 +1,4 @@
1 +from typing import List
2 from typing import Optional
3
4 from pydantic import BaseModel
@@ -45,3 +46,71 @@ class IoCResponse(BaseModel):
46
47 def to_dict(self):
48 return self.dict()
49 +
50 +
51 +class SocfortressProcessNameAnalysisRequest(BaseModel):
52 + process_name: str = Field(
53 + ...,
54 + description="The process name to evaluate.",
55 + )
56 +
57 +
58 +class Path(BaseModel):
59 + directory: str
60 + percentage: float
61 +
62 +
63 +class ProcessInfo(BaseModel):
64 + name: str
65 + percentage: float
66 +
67 +
68 +class HashInfo(BaseModel):
69 + hash: str
70 + percentage: float
71 +
72 +
73 +class NetworkInfo(BaseModel):
74 + port: str
75 + usage: float
76 +
77 +
78 +class TagInfo(BaseModel):
79 + category: str
80 + type: str
81 + description: str
82 + field4: Optional[str] = None
83 + field5: Optional[str] = None
84 + color: str
85 +
86 +
87 +class TruncatedInfo(BaseModel):
88 + paths: int
89 + parents: int
90 + grandparents: int
91 + children: int
92 + network: int
93 + hashes: int
94 +
95 +
96 +class SocfortressProcessNameAnalysisAPIResponse(BaseModel):
97 + rank: int
98 + host_prev: str
99 + eps: str
100 + paths: List[Path]
101 + parents: List[ProcessInfo]
102 + hashes: List[HashInfo]
103 + network: List[NetworkInfo]
104 + description: str
105 + intel: str
106 + truncated: TruncatedInfo
107 + tags: Optional[List[TagInfo]] = None
108 +
109 +
110 +class SocfortressProcessNameAnalysisResponse(BaseModel):
111 + success: bool
112 + message: str
113 + data: SocfortressProcessNameAnalysisAPIResponse
114 +
115 + def to_dict(self):
116 + return self.dict()
backend/app/threat_intel/services/socfortress.py
+88 -1
@@ -10,6 +10,11 @@ from app.connectors.utils import get_connector_info_from_db
10 from app.db.db_session import get_db_session
11 from app.threat_intel.schema.socfortress import IoCMapping
12 from app.threat_intel.schema.socfortress import IoCResponse
13 +from app.threat_intel.schema.socfortress import (
14 + SocfortressProcessNameAnalysisAPIResponse,
15 +)
16 +from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisRequest
17 +from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisResponse
18 from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
19 from app.utils import get_connector_attribute
20
@@ -142,6 +147,33 @@ async def invoke_socfortress_threat_intel_api(
147 return response.json()
148
149
150 +async def invoke_socfortress_process_name_api(
151 + api_key: str,
152 + url: str,
153 + request: SocfortressProcessNameAnalysisRequest,
154 +) -> dict:
155 + """
156 + Invokes the Socfortress Process Analysis API with the provided API key, URL, and request parameters.
157 +
158 + Args:
159 + api_key (str): The API key for authentication.
160 + url (str): The URL of the Socfortress Intel URL
161 + request (SocfortressProcessNameAnalysisRequest): The request object containing the Process Name
162 +
163 + Returns:
164 + dict: The JSON response from the Process Name Analysis API.
165 +
166 + Raises:
167 + httpx.HTTPStatusError: If the API request fails with a non-successful status code.
168 + """
169 + headers = {"module-version": "your_module_version", "x-api-key": api_key}
170 + params = {"value": f"{request.process_name}"}
171 + logger.info(f"Invoking Socfortress Process Name Analysis with params: {params} and headers: {headers} and url: {url}")
172 + async with httpx.AsyncClient() as client:
173 + response = await client.get(url, headers=headers, params=params)
174 + return response.json()
175 +
176 +
177 async def get_ioc_response(
178 license_key: str,
179 request: SocfortressThreatIntelRequest,
@@ -168,11 +200,44 @@ async def get_ioc_response(
200 return IoCResponse(data=IoCMapping(**data), success=success, message=message)
201
202
203 +async def get_process_analysis_response(
204 + license_key: str,
205 + request: SocfortressProcessNameAnalysisRequest,
206 + session: AsyncSession,
207 +) -> SocfortressProcessNameAnalysisResponse:
208 + """
209 + Retrieves IoC response from Socfortress Threat Intel API.
210 +
211 + Args:
212 + request (SocfortressProcessNameAnalysisRequest): The request object containing the IoC data.
213 + session (AsyncSession): The async session object for making HTTP requests.
214 +
215 + Returns:
216 + SocfortressProcessNameAnalysisResponse: The response object containing the IoC data and success status.
217 + """
218 + url = "https://processname.socfortress.co/search"
219 + response_data = await invoke_socfortress_process_name_api(license_key, url, request)
220 +
221 + # If message is `Forbidden`, raise an HTTPException
222 + if response_data.get("message") == "Forbidden":
223 + raise HTTPException(
224 + status_code=403,
225 + detail="Forbidden access to the Socfortress Process Name Analysis API",
226 + )
227 +
228 + # Using .get() with default values
229 + data = response_data.get("data", {})
230 + success = response_data.get("success", False)
231 + message = response_data.get("message", "No message provided")
232 +
233 + return SocfortressProcessNameAnalysisResponse(data=SocfortressProcessNameAnalysisAPIResponse(**data), success=success, message=message)
234 +
235 +
236 async def socfortress_threat_intel_lookup(
237 lincense_key: str,
238 request: SocfortressThreatIntelRequest,
239 session: AsyncSession,
175 -) -> IoCResponse:
240 +) -> SocfortressProcessNameAnalysisResponse:
241 """
242 Performs a threat intelligence lookup using the Socfortress service.
243
@@ -188,3 +253,25 @@ async def socfortress_threat_intel_lookup(
253 request=request,
254 session=session,
255 )
256 +
257 +
258 +async def socfortress_process_analysis_lookup(
259 + lincense_key: str,
260 + request: SocfortressProcessNameAnalysisRequest,
261 + session: AsyncSession,
262 +) -> IoCResponse:
263 + """
264 + Performs a process analysis intelligence lookup using the Socfortress service.
265 +
266 + Args:
267 + request (SocfortressThreatIntelRequest): The request object containing the IoC to lookup.
268 + session (AsyncSession): The async session object for making HTTP requests.
269 +
270 + Returns:
271 + IoCResponse: The response object containing the threat intelligence information.
272 + """
273 + return await get_process_analysis_response(
274 + license_key=lincense_key,
275 + request=request,
276 + session=session,
277 + )
frontend/package-lock.json
+8 -8
@@ -43,7 +43,7 @@
43 "vue-i18n": "^9.13.1",
44 "vue-router": "^4.3.2",
45 "vue-sjv": "^0.0.6",
46 - "vue3-apexcharts": "^1.5.2",
46 + "vue3-apexcharts": "^1.5.3",
47 "vue3-marquee": "^4.2.0",
48 "vuedraggable": "^4.1.0"
49 },
@@ -67,7 +67,7 @@
67 "@vue/test-utils": "^2.4.6",
68 "@vue/tsconfig": "^0.5.1",
69 "autoprefixer": "^10.4.19",
70 - "cypress": "^13.9.0",
70 + "cypress": "^13.10.0",
71 "eslint": "^8.57.0",
72 "eslint-plugin-cypress": "^3.2.0",
73 "eslint-plugin-vue": "^9.26.0",
@@ -4219,9 +4219,9 @@
4219 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4220 },
4221 "node_modules/cypress": {
4222 - "version": "13.9.0",
4223 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
4224 - "integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
4222 + "version": "13.10.0",
4223 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz",
4224 + "integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==",
4225 "dev": true,
4226 "hasInstallScript": true,
4227 "dependencies": {
@@ -11489,9 +11489,9 @@
11489 }
11490 },
11491 "node_modules/vue3-apexcharts": {
11492 - "version": "1.5.2",
11493 - "resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.5.2.tgz",
11494 - "integrity": "sha512-rGbgUJDjtsyjfRF0uzwDjzt8+M7ICSRAbm1N9KCDiczW8BSpbEZuaEsJDJYnJuLFIIVXIGilYzIcjNBf6NbeYA==",
11492 + "version": "1.5.3",
11493 + "resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.5.3.tgz",
11494 + "integrity": "sha512-yaHTPoj0iVKAtEVg8wEwIwwvf0VG+lPYNufCf3txRzYQOqdKPoZaZ9P3Dj3X+2A1XY9O1kcTk9HVqvLo+rppvQ==",
11495 "peerDependencies": {
11496 "apexcharts": "> 3.0.0",
11497 "vue": "> 3.0.0"
frontend/package.json
+2 -2
@@ -69,7 +69,7 @@
69 "vue-i18n": "^9.13.1",
70 "vue-router": "^4.3.2",
71 "vue-sjv": "^0.0.6",
72 - "vue3-apexcharts": "^1.5.2",
72 + "vue3-apexcharts": "^1.5.3",
73 "vue3-marquee": "^4.2.0",
74 "vuedraggable": "^4.1.0"
75 },
@@ -93,7 +93,7 @@
93 "@vue/test-utils": "^2.4.6",
94 "@vue/tsconfig": "^0.5.1",
95 "autoprefixer": "^10.4.19",
96 - "cypress": "^13.9.0",
96 + "cypress": "^13.10.0",
97 "eslint": "^8.57.0",
98 "eslint-plugin-cypress": "^3.2.0",
99 "eslint-plugin-vue": "^9.26.0",
frontend/src/api/threatIntel.ts
+7 -1
@@ -1,6 +1,6 @@
1 import { HttpClient } from "./httpClient"
2 import type { FlaskBaseResponse } from "@/types/flask.d"
3 -import type { ThreatIntelResponse } from "@/types/threatIntel.d"
3 +import type { EvaluationData, ThreatIntelResponse } from "@/types/threatIntel.d"
4
5 export default {
6 create(iocValue: string) {
@@ -8,5 +8,11 @@ export default {
8 ioc_value: iocValue
9 }
10 return HttpClient.post<FlaskBaseResponse & { data: ThreatIntelResponse }>(`/threat_intel/socfortress`, body)
11 + },
12 + processNameEvaluation(processName: string) {
13 + const body = {
14 + process_name: processName
15 + }
16 + return HttpClient.post<FlaskBaseResponse & { data: EvaluationData }>(`/threat_intel/process_name`, body)
17 }
18 }
frontend/src/components/common/ExpandableText.vue new
+62
@@ -0,0 +1,62 @@
1 +<template>
2 + <n-popover
3 + placement="top"
4 + content-class="expandable-text-popover"
5 + scrollable
6 + to="body"
7 + :disabled="text.length < maxLength"
8 + >
9 + <template #trigger>
10 + <span v-if="text.length < maxLength">{{ text }}</span>
11 + <span v-else class="cursor-help underline">{{ truncate(text) }}</span>
12 + </template>
13 +
14 + <div
15 + class="expandable-text-popover-container scrollbar-styled"
16 + v-shiki="{ fallbackLang: 'json', decode: true }"
17 + >
18 + <pre> {{ text }} </pre>
19 + </div>
20 + </n-popover>
21 +</template>
22 +
23 +<script setup lang="ts">
24 +import vShiki from "@/directives/v-shiki"
25 +import { toRefs } from "vue"
26 +import { NPopover } from "naive-ui"
27 +import _truncate from "lodash/truncate"
28 +
29 +const props = defineProps<{
30 + text: string
31 + maxLength: number
32 +}>()
33 +const { text, maxLength } = toRefs(props)
34 +
35 +function truncate(val: string): string {
36 + return _truncate(val || "", {
37 + length: maxLength.value
38 + })
39 +}
40 +</script>
41 +
42 +<style lang="scss">
43 +.expandable-text-popover {
44 + @apply max-w-96;
45 +
46 + .expandable-text-popover-container {
47 + overflow-x: hidden;
48 + overflow-y: auto;
49 + max-height: 40svh;
50 +
51 + pre {
52 + white-space: pre-wrap;
53 +
54 + code {
55 + overflow: hidden;
56 + white-space: pre-wrap;
57 + background-color: transparent !important;
58 + }
59 + }
60 + }
61 +}
62 +</style>
frontend/src/components/common/ListPercentage.vue new
+49
@@ -0,0 +1,49 @@
1 +<template>
2 + <div class="flex flex-col gap-2">
3 + <n-empty description="No items found" class="justify-center h-48" v-if="!list.length" />
4 +
5 + <div class="flex gap-4 justify-between items-center list-header font-mono text-secondary-color text-sm">
6 + <div class="basis-2/3 truncate borde">{{ labelKey }}</div>
7 + <div class="grow">{{ percentageKey }}</div>
8 + </div>
9 + <div class="flex gap-4 justify-between items-center" v-for="item of list" :key="item[labelKey]">
10 + <div class="basis-2/3 truncate font-mono">{{ item[labelKey] }}</div>
11 + <div class="grow">
12 + <n-progress
13 + type="line"
14 + :percentage="item[percentageKey]"
15 + :indicator-placement="'inside'"
16 + :indicator-text-color="style['--bg-color']"
17 + :color="style['--fg-color']"
18 + :rail-color="style['--divider-020-color']"
19 + class="font-mono font-bold"
20 + />
21 + </div>
22 + </div>
23 + </div>
24 +</template>
25 +
26 +<script setup lang="ts">
27 +import { useThemeStore } from "@/stores/theme"
28 +import { NEmpty, NProgress } from "naive-ui"
29 +import { computed } from "vue"
30 +
31 +const { list, labelKey, percentageKey } = defineProps<{
32 + list: any[]
33 + labelKey: string
34 + percentageKey: string
35 +}>()
36 +
37 +const themeStore = useThemeStore()
38 +
39 +const style = computed<{ [key: string]: any }>(() => themeStore.style)
40 +</script>
41 +
42 +<style scoped lang="scss">
43 +.list-header {
44 + & > * {
45 + border-bottom: var(--border-small-100);
46 + @apply pb-1;
47 + }
48 +}
49 +</style>
frontend/src/components/soc/SocAlerts/SocAlertAssets/SocAlertAssetsItem.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertAssets/SocAlertAssetsList.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertItem.vue deleted
-628
@@ -1,628 +0,0 @@
1 -<template>
2 - <n-spin
3 - :show="loading"
4 - :description="loadingDelete ? 'Deleting Soc Alert' : 'Loading Soc Alert'"
5 - class="soc-alert-item flex flex-col gap-0 min-h-36 pb-2"
6 - :class="{ bookmarked: isBookmark, highlight, embedded }"
7 - :id="'alert-' + alert?.alert_id"
8 - >
9 - <div class="soc-alert-info px-5 py-3 flex flex-col gap-3" v-if="alert">
10 - <div class="header-box flex justify-between">
11 - <div class="flex items-center gap-2 cursor-pointer">
12 - <div v-if="showCheckbox" class="check-box mr-2">
13 - <n-checkbox size="large" v-model:checked="checked" />
14 - </div>
15 - <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
16 - <span>#{{ alert.alert_id }} - {{ alert.alert_uuid }}</span>
17 - <Icon :name="InfoIcon" :size="16"></Icon>
18 - </div>
19 - <Icon
20 - v-if="!hideBookmarkAction"
21 - :name="loadingBookmark ? LoadingIcon : isBookmark ? StarActiveIcon : StarIcon"
22 - :size="16"
23 - @click="toggleBookmark()"
24 - class="toggler-bookmark"
25 - :class="{ active: isBookmark }"
26 - ></Icon>
27 - </div>
28 - <div class="time">
29 - <n-popover overlap placement="top-end" style="max-height: 240px" scrollable to="body">
30 - <template #trigger>
31 - <div class="flex items-center gap-2 cursor-help">
32 - <span>
33 - {{ formatDate(alert.alert_creation_time) }}
34 - </span>
35 - <Icon :name="TimeIcon" :size="16"></Icon>
36 - </div>
37 - </template>
38 - <div class="flex flex-col py-2 px-1">
39 - <SocAlertTimeline :alert="alert" />
40 - </div>
41 - </n-popover>
42 - </div>
43 - </div>
44 - <div class="main-box flex justify-between gap-4">
45 - <div class="content">
46 - <div class="title">{{ alert.alert_title }}</div>
47 - <div
48 - class="description mb-2"
49 - v-if="alert.alert_description && alert.alert_title !== alert.alert_description"
50 - >
51 - {{ alert.alert_description }}
52 - </div>
53 - </div>
54 - <SocAlertItemActions
55 - v-if="!hideSocCaseAction"
56 - class="actions-box"
57 - :caseId="caseId"
58 - :alertId="alert.alert_id"
59 - @caseCreated="caseCreated($event)"
60 - @deleted="deleted()"
61 - @startDeleting="loadingDelete = true"
62 - />
63 - </div>
64 -
65 - <div>
66 - <div
67 - class="show-badges-toggle flex items-center gap-2"
68 - v-if="showBadgesToggle"
69 - @click="showBadges = !showBadges"
70 - >
71 - {{ showBadges ? "Less info" : "More info" }}
72 - <span class="transition-transform flex items-center" :class="{ 'rotate-90': showBadges }">
73 - <Icon :name="ChevronIcon" :size="14"></Icon>
74 - </span>
75 - </div>
76 - <n-collapse-transition :show="!showBadgesToggle || showBadges">
77 - <div class="badges-box flex flex-wrap items-center gap-3 mt-3">
78 - <n-tooltip placement="top-start" trigger="hover">
79 - <template #trigger>
80 - <Badge type="splitted" hint-cursor>
81 - <template #iconLeft>
82 - <Icon :name="StatusIcon" :size="14"></Icon>
83 - </template>
84 - <template #label>Status</template>
85 - <template #value>{{ alert.status?.status_name || "-" }}</template>
86 - </Badge>
87 - </template>
88 - {{ alert.status.status_description }}
89 - </n-tooltip>
90 - <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : undefined">
91 - <template #iconLeft>
92 - <Icon :name="SeverityIcon" :size="13"></Icon>
93 - </template>
94 - <template #label>Severity</template>
95 - <template #value>{{ alert.severity?.severity_name || "-" }}</template>
96 - </Badge>
97 - <Badge type="splitted" class="hide-on-small">
98 - <template #iconLeft>
99 - <Icon :name="SourceIcon" :size="13"></Icon>
100 - </template>
101 - <template #label>Source</template>
102 - <template #value>{{ alert.alert_source || "-" }}</template>
103 - </Badge>
104 - <Badge type="splitted" class="hide-on-small">
105 - <template #iconLeft>
106 - <Icon :name="CustomerIcon" :size="13"></Icon>
107 - </template>
108 - <template #label>Customer</template>
109 - <template #value>
110 - <template
111 - v-if="
112 - alert.customer?.customer_code &&
113 - alert.customer.customer_code !== 'Customer Not Found'
114 - "
115 - >
116 - <code
117 - class="cursor-pointer text-primary-color"
118 - @click="gotoCustomer({ code: alert.customer.customer_code })"
119 - >
120 - {{ alert.customer?.customer_name || alert.customer.customer_code || "-" }}
121 - <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
122 - </code>
123 - </template>
124 - <template v-else>
125 - {{ alert.customer?.customer_name || "-" }}
126 - </template>
127 - </template>
128 - </Badge>
129 -
130 - <SocAssignUser :alert="alert" :users="users" v-slot="{ loading }" @updated="updateAlert">
131 - <Badge type="active" class="cursor-pointer">
132 - <template #iconLeft>
133 - <n-spin :size="16" :show="loading">
134 - <Icon :name="OwnerIcon" :size="16"></Icon>
135 - </n-spin>
136 - </template>
137 - <template #label>Owner</template>
138 - <template #value>{{ ownerName || "n/d" }}</template>
139 - </Badge>
140 - </SocAssignUser>
141 -
142 - <Badge
143 - v-if="alert.alert_source_link"
144 - type="active"
145 - :href="alert.alert_source_link"
146 - target="_blank"
147 - alt="Source link"
148 - rel="nofollow noopener noreferrer"
149 - >
150 - <template #iconRight>
151 - <Icon :name="LinkIcon" :size="14"></Icon>
152 - </template>
153 - <template #label>Source link</template>
154 - </Badge>
155 - </div>
156 - </n-collapse-transition>
157 - </div>
158 -
159 - <div class="footer-box flex justify-between items-center gap-4">
160 - <SocAlertItemActions
161 - v-if="!hideSocCaseAction"
162 - class="actions-box grow !flex-wrap !justify-start"
163 - style="flex-direction: initial"
164 - size="small"
165 - :caseId="caseId"
166 - :alertId="alert.alert_id"
167 - @caseCreated="caseCreated($event)"
168 - @deleted="deleted()"
169 - @startDeleting="loadingDelete = true"
170 - />
171 - <div class="time">{{ formatDate(alert.alert_creation_time) }}</div>
172 - </div>
173 - </div>
174 - <n-collapse>
175 - <template #arrow>
176 - <div class="mx-5 flex">
177 - <Icon :name="ChevronIcon"></Icon>
178 - </div>
179 - </template>
180 - <n-collapse-item>
181 - <template #header>
182 - <div class="py-3 -ml-2">Alert details</div>
183 - </template>
184 - <AlertItem :alert="alertObject" :hide-actions="true" class="-mt-4" />
185 - </n-collapse-item>
186 - </n-collapse>
187 -
188 - <n-modal
189 - v-model:show="showDetails"
190 - preset="card"
191 - content-class="!p-0"
192 - :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
193 - :title="`SOC Alert: #${alert?.alert_id} - ${alert?.alert_uuid}`"
194 - :bordered="false"
195 - segmented
196 - >
197 - <n-tabs type="line" animated :tabs-padding="24" v-if="alert">
198 - <n-tab-pane name="Context" tab="Context" display-directive="show:lazy">
199 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
200 - <KVCard v-for="(value, key) of alert.alert_context" :key="key">
201 - <template #key>{{ key }}</template>
202 - <template #value>{{ value ?? "-" }}</template>
203 - </KVCard>
204 - </div>
205 - </n-tab-pane>
206 - <n-tab-pane name="Note" tab="Note" display-directive="show:lazy">
207 - <div class="p-7 pt-4">
208 - {{ alert.alert_note ?? "No notes for this alert" }}
209 - </div>
210 - </n-tab-pane>
211 - <n-tab-pane name="Customer" tab="Customer" display-directive="show:lazy">
212 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
213 - <KVCard v-for="(value, key) of alert.customer" :key="key">
214 - <template #key>{{ key }}</template>
215 - <template #value>
216 - <template v-if="key === 'customer_code' && value && value !== 'Customer Not Found'">
217 - <code
218 - class="cursor-pointer text-primary-color"
219 - @click="gotoCustomer({ code: value })"
220 - >
221 - #{{ value }}
222 - <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
223 - </code>
224 - </template>
225 - <template v-else>
226 - {{ value || "-" }}
227 - </template>
228 - </template>
229 - </KVCard>
230 - </div>
231 - </n-tab-pane>
232 - <n-tab-pane name="Owner" tab="Owner" display-directive="show:lazy">
233 - <div class="grid gap-2 px-7 pt-4">
234 - <Badge
235 - type="active"
236 - style="max-width: 145px"
237 - class="cursor-pointer"
238 - @click="gotoSocUsers(ownerId)"
239 - >
240 - <template #iconRight>
241 - <Icon :name="LinkIcon" :size="14"></Icon>
242 - </template>
243 - <template #label>Go to users page</template>
244 - </Badge>
245 - </div>
246 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
247 - <KVCard>
248 - <template #key>user_login</template>
249 - <template #value>
250 - <SocAssignUser
251 - :alert="alert"
252 - :users="users"
253 - v-slot="{ loading }"
254 - @updated="updateAlert"
255 - >
256 - <div class="flex items-center gap-2 cursor-pointer text-primary-color">
257 - <n-spin :size="16" :show="loading">
258 - <Icon :name="EditIcon" :size="16"></Icon>
259 - </n-spin>
260 - <span>{{ ownerName || "Assign a user" }}</span>
261 - </div>
262 - </SocAssignUser>
263 - </template>
264 - </KVCard>
265 - <KVCard v-if="alert.owner">
266 - <template #key>user_name</template>
267 - <template #value>
268 - <span>#{{ alert.owner.id }}</span>
269 - {{ alert.owner.user_name }}
270 - </template>
271 - </KVCard>
272 - <KVCard v-if="alert.owner">
273 - <template #key>user_email</template>
274 - <template #value>
275 - {{ alert.owner.user_email }}
276 - </template>
277 - </KVCard>
278 - </div>
279 - </n-tab-pane>
280 - <n-tab-pane name="History" tab="History" display-directive="show:lazy">
281 - <div class="p-7 pt-4">
282 - <SocAlertTimeline :alert="alert" />
283 - </div>
284 - </n-tab-pane>
285 - <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
286 - <div class="p-7 pt-4">
287 - <SimpleJsonViewer
288 - class="vuesjv-override"
289 - :model-value="socAlertDetail"
290 - :initialExpandedDepth="1"
291 - />
292 - </div>
293 - </n-tab-pane>
294 - <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
295 - <SocAlertAssetsList v-if="alert" :alert-id="alert.alert_id" />
296 - </n-tab-pane>
297 - </n-tabs>
298 - </n-modal>
299 - </n-spin>
300 -</template>
301 -
302 -<script setup lang="ts">
303 -import AlertItem from "@/components/alerts/Alert.vue"
304 -import type { SocAlert } from "@/types/soc/alert.d"
305 -import type { Alert } from "@/types/alerts.d"
306 -import Icon from "@/components/common/Icon.vue"
307 -import Badge from "@/components/common/Badge.vue"
308 -import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
309 -import { SimpleJsonViewer } from "vue-sjv"
310 -import KVCard from "@/components/common/KVCard.vue"
311 -import SocAlertTimeline from "./SocAlertTimeline.vue"
312 -import SocAssignUser from "./SocAssignUser.vue"
313 -import SocAlertItemActions from "./SocAlertItemActions.vue"
314 -import SocAlertAssetsList from "./SocAlertAssetsList.vue"
315 -import "@/assets/scss/vuesjv-override.scss"
316 -import Api from "@/api"
317 -import {
318 - NCollapse,
319 - useMessage,
320 - NCollapseItem,
321 - NPopover,
322 - NModal,
323 - NTabs,
324 - NTabPane,
325 - NSpin,
326 - NCheckbox,
327 - NTooltip,
328 - NCollapseTransition
329 -} from "naive-ui"
330 -import { useSettingsStore } from "@/stores/settings"
331 -import dayjs from "@/utils/dayjs"
332 -import type { SocUser } from "@/types/soc/user.d"
333 -import { useGoto } from "@/composables/useGoto"
334 -
335 -const checked = defineModel<boolean>("checked", { default: false })
336 -
337 -const emit = defineEmits<{
338 - (e: "bookmark", value: boolean): void
339 - (e: "deleted"): void
340 - (e: "checked"): void
341 - (e: "unchecked"): void
342 - (e: "check", value: boolean): void
343 -}>()
344 -
345 -const props = defineProps<{
346 - alertData?: SocAlert
347 - alertId?: string | number
348 - isBookmark?: boolean
349 - highlight?: boolean | null | undefined
350 - embedded?: boolean
351 - users?: SocUser[]
352 - hideSocCaseAction?: boolean
353 - hideBookmarkAction?: boolean
354 - showBadgesToggle?: boolean
355 - showCheckbox?: boolean
356 -}>()
357 -const {
358 - alertData,
359 - alertId,
360 - isBookmark,
361 - highlight,
362 - users,
363 - embedded,
364 - hideSocCaseAction,
365 - hideBookmarkAction,
366 - showBadgesToggle,
367 - showCheckbox
368 -} = toRefs(props)
369 -
370 -const ChevronIcon = "carbon:chevron-right"
371 -const InfoIcon = "carbon:information"
372 -const TimeIcon = "carbon:time"
373 -const LinkIcon = "carbon:launch"
374 -const StatusIcon = "fluent:status-20-regular"
375 -const SeverityIcon = "bi:shield-exclamation"
376 -const SourceIcon = "lucide:arrow-down-right-from-circle"
377 -const CustomerIcon = "carbon:user"
378 -const StarActiveIcon = "carbon:star-filled"
379 -const OwnerIcon = "carbon:user-military"
380 -const StarIcon = "carbon:star"
381 -const EditIcon = "uil:edit-alt"
382 -const LoadingIcon = "eos-icons:loading"
383 -
384 -const showDetails = ref(false)
385 -const showBadges = ref(false)
386 -const loadingDelete = ref(false)
387 -const loadingData = ref(false)
388 -const loadingBookmark = ref(false)
389 -const message = useMessage()
390 -const { gotoCustomer, gotoSocUsers } = useGoto()
391 -
392 -const alert = ref(alertData.value || null)
393 -
394 -const alertObject = ref<Alert>({} as Alert)
395 -
396 -const loading = computed(() => loadingBookmark.value || loadingData.value || loadingDelete.value)
397 -const ownerName = computed(() => alert.value?.owner?.user_login)
398 -const ownerId = computed(() => alert.value?.owner?.id)
399 -const caseId = computed<number | null>(() => (alert.value?.cases?.length ? alert.value?.cases[0] : null))
400 -
401 -const socAlertDetail = computed<Partial<SocAlert>>(() => {
402 - const clone: Partial<SocAlert> = JSON.parse(JSON.stringify(alert.value))
403 -
404 - delete clone.alert_context
405 - delete clone.alert_source_content
406 - delete clone.customer
407 - delete clone.modification_history
408 - delete clone.alert_note
409 - delete clone.alert_source_link
410 -
411 - return clone
412 -})
413 -
414 -const dFormats = useSettingsStore().dateFormat
415 -
416 -function formatDate(timestamp: string | number, utc: boolean = true): string {
417 - return dayjs(timestamp).utc(utc).format(dFormats.datetimesec)
418 -}
419 -
420 -function toggleBookmark() {
421 - if (alert.value?.alert_id) {
422 - loadingBookmark.value = true
423 -
424 - const method = isBookmark.value ? "removeAlertBookmark" : "addAlertBookmark"
425 -
426 - Api.soc[method](alert.value.alert_id.toString())
427 - .then(res => {
428 - if (res.data.success) {
429 - emit("bookmark", method === "removeAlertBookmark" ? false : true)
430 - message.success(res.data?.message || "Stream started.")
431 - } else {
432 - message.warning(res.data?.message || "An error occurred. Please try again later.")
433 - }
434 - })
435 - .catch(err => {
436 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
437 - })
438 - .finally(() => {
439 - loadingBookmark.value = false
440 - })
441 - }
442 -}
443 -
444 -function updateAlert(alertUpdated: SocAlert) {
445 - const ownerObject = alertUpdated.owner
446 - const modificationHistory = alertUpdated.modification_history
447 -
448 - if (alert.value) {
449 - alert.value.owner = ownerObject
450 - alert.value.modification_history = modificationHistory
451 - }
452 -}
453 -
454 -function getAlert(id: string | number, cb?: () => void) {
455 - loadingData.value = true
456 -
457 - Api.soc
458 - .getAlert(id.toString())
459 - .then(res => {
460 - if (res.data.success) {
461 - alert.value = res.data?.alert || null
462 - if (cb) cb()
463 - } else {
464 - message.warning(res.data?.message || "An error occurred. Please try again later.")
465 - }
466 - })
467 - .catch(err => {
468 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
469 - })
470 - .finally(() => {
471 - loadingData.value = false
472 - })
473 -}
474 -
475 -function createAlertObject() {
476 - alertObject.value = {
477 - _index: "",
478 - _id: alert.value?.alert_context.alert_id,
479 - _source: alert.value?.alert_source_content
480 - } as Alert
481 -}
482 -
483 -function caseCreated(caseId: string | number) {
484 - if (alert.value) {
485 - alert.value.cases = [caseId]
486 - }
487 -}
488 -
489 -function deleted() {
490 - loadingDelete.value = false
491 - emit("deleted")
492 -}
493 -
494 -watch(checked, val => {
495 - emit("check", val)
496 - if (val) {
497 - emit("checked")
498 - } else {
499 - emit("unchecked")
500 - }
501 -})
502 -
503 -onBeforeMount(() => {
504 - createAlertObject()
505 -
506 - if (!alertData.value && alertId.value) {
507 - getAlert(alertId.value, () => {
508 - createAlertObject()
509 - })
510 - }
511 -})
512 -</script>
513 -
514 -<style lang="scss" scoped>
515 -.soc-alert-item {
516 - &:not(.embedded) {
517 - border-radius: var(--border-radius);
518 - background-color: var(--bg-color);
519 - border: var(--border-small-050);
520 - }
521 - transition: all 0.2s var(--bezier-ease);
522 -
523 - .soc-alert-info {
524 - border-bottom: var(--border-small-050);
525 -
526 - .header-box {
527 - font-family: var(--font-family-mono);
528 - font-size: 13px;
529 - .id {
530 - word-break: break-word;
531 - color: var(--fg-secondary-color);
532 - line-height: 1.2;
533 -
534 - &:hover {
535 - color: var(--primary-color);
536 - }
537 - }
538 -
539 - .toggler-bookmark {
540 - &.active {
541 - color: var(--primary-color);
542 - }
543 - &:hover {
544 - color: var(--primary-color);
545 - }
546 - }
547 - .time {
548 - color: var(--fg-secondary-color);
549 -
550 - &:hover {
551 - color: var(--primary-color);
552 - }
553 - }
554 - }
555 -
556 - .main-box {
557 - .content {
558 - word-break: break-word;
559 -
560 - .description {
561 - color: var(--fg-secondary-color);
562 - font-size: 13px;
563 - }
564 - }
565 - }
566 -
567 - .show-badges-toggle {
568 - font-size: 14px;
569 - cursor: pointer;
570 - transition: color 0.2s var(--bezier-ease);
571 -
572 - &:hover {
573 - color: var(--primary-color);
574 - }
575 - }
576 -
577 - .footer-box {
578 - font-size: 13px;
579 - margin-top: 10px;
580 - display: none;
581 -
582 - .time {
583 - font-family: var(--font-family-mono);
584 - text-align: right;
585 - color: var(--fg-secondary-color);
586 - }
587 - }
588 - }
589 -
590 - &.bookmarked {
591 - background-color: var(--primary-005-color);
592 - border-color: var(--primary-030-color);
593 - }
594 -
595 - &:not(.embedded) {
596 - &:hover,
597 - &.highlight {
598 - border-color: var(--primary-color);
599 - }
600 - }
601 -
602 - @container (max-width: 650px) {
603 - .soc-alert-info {
604 - .header-box {
605 - .time {
606 - display: none;
607 - }
608 - }
609 -
610 - .main-box {
611 - .actions-box {
612 - display: none;
613 - }
614 - .badges-box {
615 - .badge {
616 - &.hide-on-small {
617 - display: none;
618 - }
619 - }
620 - }
621 - }
622 - .footer-box {
623 - display: flex;
624 - }
625 - }
626 - }
627 -}
628 -</style>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItem.vue new
+347
@@ -0,0 +1,347 @@
1 +<template>
2 + <n-spin
3 + :show="loading"
4 + :description="loadingDelete ? 'Deleting Soc Alert' : 'Loading Soc Alert'"
5 + class="soc-alert-item flex flex-col gap-0 min-h-36 pb-2"
6 + :class="{ bookmarked: isBookmark, highlight, embedded }"
7 + :id="'alert-' + alert?.alert_id"
8 + >
9 + <div class="soc-alert-info px-5 py-3 flex flex-col gap-3" v-if="alert">
10 + <div class="header-box flex justify-between">
11 + <div class="flex items-center gap-2 cursor-pointer">
12 + <div v-if="showCheckbox" class="check-box mr-2">
13 + <n-checkbox size="large" v-model:checked="checked" />
14 + </div>
15 + <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
16 + <span>#{{ alert.alert_id }} - {{ alert.alert_uuid }}</span>
17 + <Icon :name="InfoIcon" :size="16"></Icon>
18 + </div>
19 + <SocAlertItemBookmarkToggler
20 + v-if="!hideBookmarkAction && alert"
21 + :alert="alert"
22 + :isBookmark="isBookmark"
23 + @bookmark="emit('bookmark', $event)"
24 + />
25 + </div>
26 + <div class="time">
27 + <SocAlertItemTime :alert="alert" />
28 + </div>
29 + </div>
30 + <div class="main-box flex justify-between gap-4">
31 + <div class="content">
32 + <div class="title">{{ alert.alert_title }}</div>
33 + <div
34 + class="description mb-2"
35 + v-if="alert.alert_description && alert.alert_title !== alert.alert_description"
36 + >
37 + {{ alert.alert_description }}
38 + </div>
39 + </div>
40 + <SocAlertItemActions
41 + v-if="!hideSocCaseAction"
42 + class="actions-box"
43 + :caseId="caseId"
44 + :alertId="alert.alert_id"
45 + @caseCreated="caseCreated($event)"
46 + @deleted="deleted()"
47 + @startDeleting="loadingDelete = true"
48 + />
49 + </div>
50 +
51 + <div>
52 + <div
53 + class="show-badges-toggle flex items-center gap-2"
54 + v-if="showBadgesToggle"
55 + @click="showBadges = !showBadges"
56 + >
57 + {{ showBadges ? "Less info" : "More info" }}
58 + <span class="transition-transform flex items-center" :class="{ 'rotate-90': showBadges }">
59 + <Icon :name="ChevronIcon" :size="14"></Icon>
60 + </span>
61 + </div>
62 + <n-collapse-transition :show="!showBadgesToggle || showBadges">
63 + <SocAlertItemBadges
64 + class="badges-box"
65 + v-if="alert"
66 + :alert="alert"
67 + :users="users"
68 + @updated="updateAlert"
69 + />
70 + </n-collapse-transition>
71 + </div>
72 +
73 + <div class="footer-box flex justify-between items-center gap-4">
74 + <SocAlertItemActions
75 + v-if="!hideSocCaseAction"
76 + class="actions-box grow !flex-wrap !justify-start"
77 + style="flex-direction: initial"
78 + size="small"
79 + :caseId="caseId"
80 + :alertId="alert.alert_id"
81 + @caseCreated="caseCreated($event)"
82 + @deleted="deleted()"
83 + @startDeleting="loadingDelete = true"
84 + />
85 + <div class="time">
86 + <SocAlertItemTime :alert="alert" hide-timeline />
87 + </div>
88 + </div>
89 + </div>
90 + <n-collapse>
91 + <template #arrow>
92 + <div class="mx-5 flex">
93 + <Icon :name="ChevronIcon"></Icon>
94 + </div>
95 + </template>
96 + <n-collapse-item>
97 + <template #header>
98 + <div class="py-3 -ml-2">Alert details</div>
99 + </template>
100 + <AlertItem :alert="alertObject" :hide-actions="true" class="-mt-4" />
101 + </n-collapse-item>
102 + </n-collapse>
103 +
104 + <n-modal
105 + v-model:show="showDetails"
106 + preset="card"
107 + content-class="!p-0"
108 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
109 + :title="`SOC Alert: #${alert?.alert_id} - ${alert?.alert_uuid}`"
110 + :bordered="false"
111 + segmented
112 + >
113 + <SocAlertItemDetails v-if="alert" :alert="alert" :users="users" @updated="updateAlert" />
114 + </n-modal>
115 + </n-spin>
116 +</template>
117 +
118 +<script setup lang="ts">
119 +import type { SocAlert } from "@/types/soc/alert.d"
120 +import type { Alert } from "@/types/alerts.d"
121 +import Icon from "@/components/common/Icon.vue"
122 +import { computed, defineAsyncComponent, onBeforeMount, ref, toRefs, watch } from "vue"
123 +import SocAlertItemActions from "./SocAlertItemActions.vue"
124 +import SocAlertItemTime from "./SocAlertItemTime.vue"
125 +import SocAlertItemBookmarkToggler from "./SocAlertItemBookmarkToggler.vue"
126 +import Api from "@/api"
127 +import { NCollapse, useMessage, NCollapseItem, NModal, NSpin, NCheckbox, NCollapseTransition } from "naive-ui"
128 +import type { SocUser } from "@/types/soc/user.d"
129 +const SocAlertItemDetails = defineAsyncComponent(() => import("./SocAlertItemDetails.vue"))
130 +const SocAlertItemBadges = defineAsyncComponent(() => import("./SocAlertItemBadges.vue"))
131 +const AlertItem = defineAsyncComponent(() => import("@/components/alerts/Alert.vue"))
132 +
133 +const checked = defineModel<boolean>("checked", { default: false })
134 +
135 +const emit = defineEmits<{
136 + (e: "bookmark", value: boolean): void
137 + (e: "deleted"): void
138 + (e: "checked"): void
139 + (e: "unchecked"): void
140 + (e: "check", value: boolean): void
141 +}>()
142 +
143 +const props = defineProps<{
144 + alertData?: SocAlert
145 + alertId?: string | number
146 + isBookmark?: boolean
147 + highlight?: boolean | null | undefined
148 + embedded?: boolean
149 + users?: SocUser[]
150 + hideSocCaseAction?: boolean
151 + hideBookmarkAction?: boolean
152 + showBadgesToggle?: boolean
153 + showCheckbox?: boolean
154 +}>()
155 +const {
156 + alertData,
157 + alertId,
158 + isBookmark,
159 + highlight,
160 + users,
161 + embedded,
162 + hideSocCaseAction,
163 + hideBookmarkAction,
164 + showBadgesToggle,
165 + showCheckbox
166 +} = toRefs(props)
167 +
168 +const ChevronIcon = "carbon:chevron-right"
169 +const InfoIcon = "carbon:information"
170 +
171 +const showDetails = ref(false)
172 +const showBadges = ref(false)
173 +const loadingDelete = ref(false)
174 +const loadingData = ref(false)
175 +const loadingBookmark = ref(false)
176 +const message = useMessage()
177 +
178 +const alert = ref(alertData.value || null)
179 +const alertObject = ref<Alert>({} as Alert)
180 +const loading = computed(() => loadingBookmark.value || loadingData.value || loadingDelete.value)
181 +const caseId = computed<number | null>(() => (alert.value?.cases?.length ? alert.value?.cases[0] : null))
182 +
183 +function updateAlert(alertUpdated: SocAlert) {
184 + const ownerObject = alertUpdated.owner
185 + const modificationHistory = alertUpdated.modification_history
186 +
187 + if (alert.value) {
188 + alert.value.owner = ownerObject
189 + alert.value.modification_history = modificationHistory
190 + }
191 +}
192 +
193 +function getAlert(id: string | number, cb?: () => void) {
194 + loadingData.value = true
195 +
196 + Api.soc
197 + .getAlert(id.toString())
198 + .then(res => {
199 + if (res.data.success) {
200 + alert.value = res.data?.alert || null
201 + if (cb) cb()
202 + } else {
203 + message.warning(res.data?.message || "An error occurred. Please try again later.")
204 + }
205 + })
206 + .catch(err => {
207 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
208 + })
209 + .finally(() => {
210 + loadingData.value = false
211 + })
212 +}
213 +
214 +function createAlertObject() {
215 + alertObject.value = {
216 + _index: "",
217 + _id: alert.value?.alert_context.alert_id,
218 + _source: alert.value?.alert_source_content
219 + } as Alert
220 +}
221 +
222 +function caseCreated(caseId: string | number) {
223 + if (alert.value) {
224 + alert.value.cases = [caseId]
225 + }
226 +}
227 +
228 +function deleted() {
229 + loadingDelete.value = false
230 + emit("deleted")
231 +}
232 +
233 +watch(checked, val => {
234 + emit("check", val)
235 + if (val) {
236 + emit("checked")
237 + } else {
238 + emit("unchecked")
239 + }
240 +})
241 +
242 +onBeforeMount(() => {
243 + createAlertObject()
244 +
245 + if (!alertData.value && alertId.value) {
246 + getAlert(alertId.value, () => {
247 + createAlertObject()
248 + })
249 + }
250 +})
251 +</script>
252 +
253 +<style lang="scss" scoped>
254 +.soc-alert-item {
255 + &:not(.embedded) {
256 + border-radius: var(--border-radius);
257 + background-color: var(--bg-color);
258 + border: var(--border-small-050);
259 + }
260 + transition: all 0.2s var(--bezier-ease);
261 +
262 + .soc-alert-info {
263 + border-bottom: var(--border-small-050);
264 +
265 + .header-box {
266 + font-family: var(--font-family-mono);
267 + font-size: 13px;
268 + .id {
269 + word-break: break-word;
270 + color: var(--fg-secondary-color);
271 + line-height: 1.2;
272 +
273 + &:hover {
274 + color: var(--primary-color);
275 + }
276 + }
277 + }
278 +
279 + .main-box {
280 + .content {
281 + word-break: break-word;
282 +
283 + .description {
284 + color: var(--fg-secondary-color);
285 + font-size: 13px;
286 + }
287 + }
288 + }
289 +
290 + .show-badges-toggle {
291 + font-size: 14px;
292 + cursor: pointer;
293 + transition: color 0.2s var(--bezier-ease);
294 +
295 + &:hover {
296 + color: var(--primary-color);
297 + }
298 + }
299 +
300 + .footer-box {
301 + font-size: 13px;
302 + margin-top: 10px;
303 + display: none;
304 + }
305 + }
306 +
307 + &.bookmarked {
308 + background-color: var(--primary-005-color);
309 + border-color: var(--primary-030-color);
310 + }
311 +
312 + &:not(.embedded) {
313 + &:hover,
314 + &.highlight {
315 + border-color: var(--primary-color);
316 + }
317 + }
318 +
319 + @container (max-width: 650px) {
320 + .soc-alert-info {
321 + .header-box {
322 + .time {
323 + display: none;
324 + }
325 + }
326 +
327 + .main-box {
328 + .actions-box {
329 + display: none;
330 + }
331 + .badges-box {
332 + :deep() {
333 + .badge {
334 + &.hide-on-small {
335 + display: none;
336 + }
337 + }
338 + }
339 + }
340 + }
341 + .footer-box {
342 + display: flex;
343 + }
344 + }
345 + }
346 +}
347 +</style>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemActions.vue renamed
+1 -1
@@ -55,7 +55,7 @@ import { NButton, useDialog, useMessage, NModal } from "naive-ui"
55 import Icon from "@/components/common/Icon.vue"
56 import Api from "@/api"
57 import { computed, ref, watch } from "vue"
58 -import SocCaseItem from "../SocCases/SocCaseItem.vue"
58 +import SocCaseItem from "@/components/soc/SocCases/SocCaseItem.vue"
59 import type { Size } from "naive-ui/es/button/src/interface"
60
61 const emit = defineEmits<{
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemBadges.vue new
+108
@@ -0,0 +1,108 @@
1 +<template>
2 + <div class="flex flex-wrap items-center gap-3 mt-3">
3 + <n-tooltip placement="top-start" trigger="hover">
4 + <template #trigger>
5 + <Badge type="splitted" hint-cursor>
6 + <template #iconLeft>
7 + <Icon :name="StatusIcon" :size="14"></Icon>
8 + </template>
9 + <template #label>Status</template>
10 + <template #value>{{ alert.status?.status_name || "-" }}</template>
11 + </Badge>
12 + </template>
13 + {{ alert.status.status_description }}
14 + </n-tooltip>
15 + <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : undefined">
16 + <template #iconLeft>
17 + <Icon :name="SeverityIcon" :size="13"></Icon>
18 + </template>
19 + <template #label>Severity</template>
20 + <template #value>{{ alert.severity?.severity_name || "-" }}</template>
21 + </Badge>
22 + <Badge type="splitted" class="hide-on-small">
23 + <template #iconLeft>
24 + <Icon :name="SourceIcon" :size="13"></Icon>
25 + </template>
26 + <template #label>Source</template>
27 + <template #value>{{ alert.alert_source || "-" }}</template>
28 + </Badge>
29 + <Badge type="splitted" class="hide-on-small">
30 + <template #iconLeft>
31 + <Icon :name="CustomerIcon" :size="13"></Icon>
32 + </template>
33 + <template #label>Customer</template>
34 + <template #value>
35 + <template v-if="alert.customer?.customer_code && alert.customer.customer_code !== 'Customer Not Found'">
36 + <code
37 + class="cursor-pointer text-primary-color"
38 + @click="gotoCustomer({ code: alert.customer.customer_code })"
39 + >
40 + {{ alert.customer?.customer_name || alert.customer.customer_code || "-" }}
41 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
42 + </code>
43 + </template>
44 + <template v-else>
45 + {{ alert.customer?.customer_name || "-" }}
46 + </template>
47 + </template>
48 + </Badge>
49 +
50 + <SocAssignUser :alert="alert" :users="users" v-slot="{ loading }" @updated="emit('updated', $event)">
51 + <Badge type="active" class="cursor-pointer">
52 + <template #iconLeft>
53 + <n-spin :size="16" :show="loading">
54 + <Icon :name="OwnerIcon" :size="16"></Icon>
55 + </n-spin>
56 + </template>
57 + <template #label>Owner</template>
58 + <template #value>{{ ownerName || "n/d" }}</template>
59 + </Badge>
60 + </SocAssignUser>
61 +
62 + <Badge
63 + v-if="alert.alert_source_link"
64 + type="active"
65 + :href="alert.alert_source_link"
66 + target="_blank"
67 + alt="Source link"
68 + rel="nofollow noopener noreferrer"
69 + >
70 + <template #iconRight>
71 + <Icon :name="LinkIcon" :size="14"></Icon>
72 + </template>
73 + <template #label>Source link</template>
74 + </Badge>
75 + </div>
76 +</template>
77 +
78 +<script setup lang="ts">
79 +import type { SocAlert } from "@/types/soc/alert.d"
80 +import Icon from "@/components/common/Icon.vue"
81 +import Badge from "@/components/common/Badge.vue"
82 +import { computed, toRefs } from "vue"
83 +import SocAssignUser from "./SocAssignUser.vue"
84 +import { NSpin, NTooltip } from "naive-ui"
85 +import type { SocUser } from "@/types/soc/user.d"
86 +import { useGoto } from "@/composables/useGoto"
87 +
88 +const emit = defineEmits<{
89 + (e: "updated", value: SocAlert): void
90 +}>()
91 +
92 +const props = defineProps<{
93 + alert: SocAlert
94 + users?: SocUser[]
95 +}>()
96 +const { alert, users } = toRefs(props)
97 +
98 +const LinkIcon = "carbon:launch"
99 +const StatusIcon = "fluent:status-20-regular"
100 +const SeverityIcon = "bi:shield-exclamation"
101 +const SourceIcon = "lucide:arrow-down-right-from-circle"
102 +const CustomerIcon = "carbon:user"
103 +const OwnerIcon = "carbon:user-military"
104 +
105 +const { gotoCustomer } = useGoto()
106 +
107 +const ownerName = computed(() => alert.value?.owner?.user_login)
108 +</script>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemBookmarkToggler.vue new
+69
@@ -0,0 +1,69 @@
1 +<template>
2 + <Icon
3 + :name="loadingBookmark ? LoadingIcon : isBookmark ? StarActiveIcon : StarIcon"
4 + :size="16"
5 + @click="toggleBookmark()"
6 + class="toggler-bookmark"
7 + :class="{ active: isBookmark }"
8 + ></Icon>
9 +</template>
10 +
11 +<script setup lang="ts">
12 +import type { SocAlert } from "@/types/soc/alert.d"
13 +import Icon from "@/components/common/Icon.vue"
14 +import { ref, toRefs } from "vue"
15 +import Api from "@/api"
16 +import { useMessage } from "naive-ui"
17 +
18 +const emit = defineEmits<{
19 + (e: "bookmark", value: boolean): void
20 +}>()
21 +
22 +const props = defineProps<{
23 + alert: SocAlert
24 + isBookmark?: boolean
25 +}>()
26 +const { alert, isBookmark } = toRefs(props)
27 +
28 +const StarActiveIcon = "carbon:star-filled"
29 +const StarIcon = "carbon:star"
30 +const LoadingIcon = "eos-icons:loading"
31 +
32 +const loadingBookmark = ref(false)
33 +const message = useMessage()
34 +
35 +function toggleBookmark() {
36 + if (alert.value?.alert_id) {
37 + loadingBookmark.value = true
38 +
39 + const method = isBookmark.value ? "removeAlertBookmark" : "addAlertBookmark"
40 +
41 + Api.soc[method](alert.value.alert_id.toString())
42 + .then(res => {
43 + if (res.data.success) {
44 + emit("bookmark", method === "removeAlertBookmark" ? false : true)
45 + message.success(res.data?.message || "Stream started.")
46 + } else {
47 + message.warning(res.data?.message || "An error occurred. Please try again later.")
48 + }
49 + })
50 + .catch(err => {
51 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
52 + })
53 + .finally(() => {
54 + loadingBookmark.value = false
55 + })
56 + }
57 +}
58 +</script>
59 +
60 +<style lang="scss" scoped>
61 +.toggler-bookmark {
62 + &.active {
63 + color: var(--primary-color);
64 + }
65 + &:hover {
66 + color: var(--primary-color);
67 + }
68 +}
69 +</style>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemContext.vue new
+64
@@ -0,0 +1,64 @@
1 +<template>
2 + <div class="flex flex-col gap-3">
3 + <n-input placeholder="Search..." v-model:value="textFilter" clearable>
4 + <template #prefix>
5 + <Icon :name="SearchIcon" />
6 + </template>
7 + </n-input>
8 +
9 + <div class="grid gap-2 grid-auto-flow-200">
10 + <KVCard v-for="{ value, key } of contextFiltered" :key="key">
11 + <template #key>{{ key }}</template>
12 + <template #value>
13 + <template v-if="key === 'process_name'">
14 + <template v-if="value && value !== '-' && value.toString()">
15 + <div class="flex flex-wrap gap-2">
16 + <SocAlertItemEvaluation v-for="pn of processNameList" :key="pn" :process-name="pn" />
17 + </div>
18 + </template>
19 + <template v-else>-</template>
20 + </template>
21 + <template v-else>
22 + <ExpandableText :text="value.toString() ?? '-'" :maxLength="100" />
23 + </template>
24 + </template>
25 + </KVCard>
26 + </div>
27 + </div>
28 +</template>
29 +
30 +<script setup lang="ts">
31 +import type { SocAlert } from "@/types/soc/alert.d"
32 +import KVCard from "@/components/common/KVCard.vue"
33 +import { NInput } from "naive-ui"
34 +import Icon from "@/components/common/Icon.vue"
35 +import { computed, defineAsyncComponent, ref } from "vue"
36 +import _split from "lodash/split"
37 +import _compact from "lodash/compact"
38 +const SocAlertItemEvaluation = defineAsyncComponent(() => import("./SocAlertItemEvaluation.vue"))
39 +const ExpandableText = defineAsyncComponent(() => import("@/components/common/ExpandableText.vue"))
40 +
41 +const { alert } = defineProps<{
42 + alert: SocAlert
43 +}>()
44 +
45 +const SearchIcon = "carbon:search"
46 +
47 +const textFilter = ref("")
48 +const processNameList = computed(() => _compact(_split(alert.alert_context?.process_name || "", ",")))
49 +const contextNormalized = computed(() => {
50 + const list = []
51 + for (const key in alert.alert_context) {
52 + list.push({
53 + key,
54 + value: alert.alert_context[key]
55 + })
56 + }
57 +
58 + return list
59 +})
60 +
61 +const contextFiltered = computed(() =>
62 + contextNormalized.value.filter(o => o.key.toLowerCase().indexOf(textFilter.value.toLowerCase()) !== -1)
63 +)
64 +</script>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemDetails.vue new
+133
@@ -0,0 +1,133 @@
1 +<template>
2 + <n-tabs type="line" animated :tabs-padding="24" v-if="alert">
3 + <n-tab-pane name="Context" tab="Context" display-directive="show:lazy">
4 + <SocAlertItemContext :alert="alert" class="p-7 pt-4" />
5 + </n-tab-pane>
6 + <n-tab-pane name="Note" tab="Note" display-directive="show:lazy">
7 + <div class="p-7 pt-4">
8 + {{ alert.alert_note ?? "No notes for this alert" }}
9 + </div>
10 + </n-tab-pane>
11 + <n-tab-pane name="Customer" tab="Customer" display-directive="show:lazy">
12 + <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
13 + <KVCard v-for="(value, key) of alert.customer" :key="key">
14 + <template #key>{{ key }}</template>
15 + <template #value>
16 + <template v-if="key === 'customer_code' && value && value !== 'Customer Not Found'">
17 + <code class="cursor-pointer text-primary-color" @click="gotoCustomer({ code: value })">
18 + #{{ value }}
19 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
20 + </code>
21 + </template>
22 + <template v-else>
23 + {{ value || "-" }}
24 + </template>
25 + </template>
26 + </KVCard>
27 + </div>
28 + </n-tab-pane>
29 + <n-tab-pane name="Owner" tab="Owner" display-directive="show:lazy">
30 + <div class="grid gap-2 px-7 pt-4">
31 + <Badge type="active" style="max-width: 145px" class="cursor-pointer" @click="gotoSocUsers(ownerId)">
32 + <template #iconRight>
33 + <Icon :name="LinkIcon" :size="14"></Icon>
34 + </template>
35 + <template #label>Go to users page</template>
36 + </Badge>
37 + </div>
38 + <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
39 + <KVCard>
40 + <template #key>user_login</template>
41 + <template #value>
42 + <SocAssignUser
43 + :alert="alert"
44 + :users="users"
45 + v-slot="{ loading }"
46 + @updated="emit('updated', $event)"
47 + >
48 + <div class="flex items-center gap-2 cursor-pointer text-primary-color">
49 + <n-spin :size="16" :show="loading">
50 + <Icon :name="EditIcon" :size="16"></Icon>
51 + </n-spin>
52 + <span>{{ ownerName || "Assign a user" }}</span>
53 + </div>
54 + </SocAssignUser>
55 + </template>
56 + </KVCard>
57 + <KVCard v-if="alert.owner">
58 + <template #key>user_name</template>
59 + <template #value>
60 + <span>#{{ alert.owner.id }}</span>
61 + {{ alert.owner.user_name }}
62 + </template>
63 + </KVCard>
64 + <KVCard v-if="alert.owner">
65 + <template #key>user_email</template>
66 + <template #value>
67 + {{ alert.owner.user_email }}
68 + </template>
69 + </KVCard>
70 + </div>
71 + </n-tab-pane>
72 + <n-tab-pane name="History" tab="History" display-directive="show:lazy">
73 + <div class="p-7 pt-4">
74 + <SocAlertItemTimeline :alert="alert" />
75 + </div>
76 + </n-tab-pane>
77 + <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
78 + <div class="p-7 pt-4">
79 + <SimpleJsonViewer class="vuesjv-override" :model-value="socAlertDetail" :initialExpandedDepth="1" />
80 + </div>
81 + </n-tab-pane>
82 + <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
83 + <SocAlertAssetsList v-if="alert" :alert-id="alert.alert_id" />
84 + </n-tab-pane>
85 + </n-tabs>
86 +</template>
87 +
88 +<script setup lang="ts">
89 +import type { SocAlert } from "@/types/soc/alert.d"
90 +import Icon from "@/components/common/Icon.vue"
91 +import Badge from "@/components/common/Badge.vue"
92 +import { computed } from "vue"
93 +import { SimpleJsonViewer } from "vue-sjv"
94 +import KVCard from "@/components/common/KVCard.vue"
95 +import SocAlertItemTimeline from "./SocAlertItemTimeline.vue"
96 +import SocAssignUser from "./SocAssignUser.vue"
97 +import SocAlertItemContext from "./SocAlertItemContext.vue"
98 +import SocAlertAssetsList from "../SocAlertAssets/SocAlertAssetsList.vue"
99 +import "@/assets/scss/vuesjv-override.scss"
100 +import { NTabs, NTabPane, NSpin } from "naive-ui"
101 +import { useGoto } from "@/composables/useGoto"
102 +import type { SocUser } from "@/types/soc/user"
103 +
104 +const emit = defineEmits<{
105 + (e: "updated", value: SocAlert): void
106 +}>()
107 +
108 +const { alert } = defineProps<{
109 + alert: SocAlert
110 + users?: SocUser[]
111 +}>()
112 +
113 +const LinkIcon = "carbon:launch"
114 +const EditIcon = "uil:edit-alt"
115 +
116 +const { gotoCustomer, gotoSocUsers } = useGoto()
117 +
118 +const ownerName = computed(() => alert?.owner?.user_login)
119 +const ownerId = computed(() => alert?.owner?.id)
120 +
121 +const socAlertDetail = computed<Partial<SocAlert>>(() => {
122 + const clone: Partial<SocAlert> = JSON.parse(JSON.stringify(alert))
123 +
124 + delete clone.alert_context
125 + delete clone.alert_source_content
126 + delete clone.customer
127 + delete clone.modification_history
128 + delete clone.alert_note
129 + delete clone.alert_source_link
130 +
131 + return clone
132 +})
133 +</script>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemEvaluation.vue new
+133
@@ -0,0 +1,133 @@
1 +<template>
2 + <code class="cursor-pointer text-primary-color" @click="openEvaluation()">
3 + {{ processName }}
4 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
5 + </code>
6 +
7 + <n-modal
8 + v-model:show="showDetails"
9 + preset="card"
10 + content-class="!p-0"
11 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)' }"
12 + :title="`Evaluation: ${processName}`"
13 + :bordered="false"
14 + segmented
15 + >
16 + <n-spin :show="loading" class="min-h-48">
17 + <n-tabs type="line" animated :tabs-padding="24" v-if="evaluation">
18 + <n-tab-pane
19 + name="Overview"
20 + tab="Overview"
21 + display-directive="show:lazy"
22 + class="flex flex-col gap-4 !py-8"
23 + >
24 + <div class="px-7">
25 + <n-card content-class="bg-secondary-color" class="overflow-hidden">
26 + <div class="flex justify-between gap-8 flex-wrap">
27 + <n-statistic label="Rank" :value="evaluation.rank" tabular-nums />
28 + <n-statistic label="EPS" :value="eps" tabular-nums />
29 + <n-statistic label="Host Prevalence" :value="evaluation.host_prev + '%'" tabular-nums />
30 + </div>
31 + </n-card>
32 + </div>
33 +
34 + <div class="px-7">
35 + {{ evaluation.description }}
36 + </div>
37 + </n-tab-pane>
38 + <n-tab-pane name="Intel" tab="Intel" display-directive="show:lazy">
39 + <div class="p-7 pt-4">
40 + <n-input
41 + :value="evaluation.intel"
42 + type="textarea"
43 + readonly
44 + placeholder="Empty"
45 + size="large"
46 + :autosize="{
47 + minRows: 3,
48 + maxRows: 18
49 + }"
50 + />
51 + </div>
52 + </n-tab-pane>
53 + <n-tab-pane name="Hashes" tab="Hashes" display-directive="show:lazy">
54 + <ListPercentage
55 + class="p-7 pt-4"
56 + :list="evaluation.hashes"
57 + labelKey="hash"
58 + percentageKey="percentage"
59 + />
60 + </n-tab-pane>
61 + <n-tab-pane name="Network" tab="Network" display-directive="show:lazy">
62 + <ListPercentage class="p-7 pt-4" :list="evaluation.network" labelKey="port" percentageKey="usage" />
63 + </n-tab-pane>
64 + <n-tab-pane name="Parents" tab="Parents" display-directive="show:lazy">
65 + <ListPercentage
66 + class="p-7 pt-4"
67 + :list="evaluation.parents"
68 + labelKey="name"
69 + percentageKey="percentage"
70 + />
71 + </n-tab-pane>
72 + <n-tab-pane name="Paths" tab="Paths" display-directive="show:lazy">
73 + <ListPercentage
74 + class="p-7 pt-4"
75 + :list="evaluation.paths"
76 + labelKey="directory"
77 + percentageKey="percentage"
78 + />
79 + </n-tab-pane>
80 + </n-tabs>
81 + <n-empty description="Evaluation not found" class="justify-center h-48" v-if="!loading && !evaluation" />
82 + </n-spin>
83 + </n-modal>
84 +</template>
85 +
86 +<script setup lang="ts">
87 +import Icon from "@/components/common/Icon.vue"
88 +import { useMessage, NModal, NSpin, NTabs, NTabPane, NStatistic, NInput, NCard, NEmpty } from "naive-ui"
89 +import type { EvaluationData } from "@/types/threatIntel"
90 +import { computed, defineAsyncComponent, ref } from "vue"
91 +import Api from "@/api"
92 +import _toSafeInteger from "lodash/toSafeInteger"
93 +const ListPercentage = defineAsyncComponent(() => import("@/components/common/ListPercentage.vue"))
94 +
95 +const { processName } = defineProps<{
96 + processName: string
97 +}>()
98 +
99 +const LinkIcon = "carbon:launch"
100 +const evaluation = ref<EvaluationData | null>(null)
101 +const showDetails = ref<boolean>(false)
102 +const loading = ref<boolean>(false)
103 +const message = useMessage()
104 +
105 +const eps = computed(() => _toSafeInteger(evaluation.value?.eps || 0))
106 +
107 +function openEvaluation() {
108 + if (!evaluation.value) {
109 + getEvaluation()
110 + }
111 + showDetails.value = true
112 +}
113 +
114 +function getEvaluation() {
115 + loading.value = true
116 +
117 + Api.threatIntel
118 + .processNameEvaluation(processName)
119 + .then(res => {
120 + if (res.data.success) {
121 + evaluation.value = res.data?.data || null
122 + } else {
123 + message.warning(res.data?.message || "An error occurred. Please try again later.")
124 + }
125 + })
126 + .catch(err => {
127 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
128 + })
129 + .finally(() => {
130 + loading.value = false
131 + })
132 +}
133 +</script>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemTime.vue new
+54
@@ -0,0 +1,54 @@
1 +<template>
2 + <n-popover overlap placement="top-end" style="max-height: 240px" scrollable to="body" :disabled="hideTimeline">
3 + <template #trigger>
4 + <div class="time flex items-center gap-2" :class="{ hover: !hideTimeline }">
5 + <span>
6 + {{ formatDate(alert.alert_creation_time) }}
7 + </span>
8 + <Icon :name="TimeIcon" :size="16" v-if="!hideTimeline"></Icon>
9 + </div>
10 + </template>
11 + <div class="flex flex-col py-2 px-1">
12 + <SocAlertItemTimeline :alert="alert" />
13 + </div>
14 + </n-popover>
15 +</template>
16 +
17 +<script setup lang="ts">
18 +import type { SocAlert } from "@/types/soc/alert.d"
19 +import Icon from "@/components/common/Icon.vue"
20 +import { toRefs } from "vue"
21 +import SocAlertItemTimeline from "./SocAlertItemTimeline.vue"
22 +import { NPopover } from "naive-ui"
23 +import { useSettingsStore } from "@/stores/settings"
24 +import dayjs from "@/utils/dayjs"
25 +
26 +const props = defineProps<{
27 + alert: SocAlert
28 + hideTimeline?: boolean
29 +}>()
30 +const { alert, hideTimeline } = toRefs(props)
31 +
32 +const TimeIcon = "carbon:time"
33 +
34 +const dFormats = useSettingsStore().dateFormat
35 +
36 +function formatDate(timestamp: string | number, utc: boolean = true): string {
37 + return dayjs(timestamp).utc(utc).format(dFormats.datetimesec)
38 +}
39 +</script>
40 +
41 +<style lang="scss" scoped>
42 +.time {
43 + color: var(--fg-secondary-color);
44 + font-family: var(--font-family-mono);
45 +
46 + &.hover {
47 + @apply cursor-help;
48 +
49 + &:hover {
50 + color: var(--primary-color);
51 + }
52 + }
53 +}
54 +</style>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemTimeline.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAssignUser.vue renamed
frontend/src/components/soc/SocAlerts/SocAlertsBookmarks.vue
+1 -1
@@ -35,7 +35,7 @@
35 import { ref, onBeforeMount, toRefs, onMounted, onBeforeUnmount } from "vue"
36 import { useMessage, NSpin, NEmpty } from "naive-ui"
37 import Api from "@/api"
38 -import SocAlertItem from "./SocAlertItem.vue"
38 +import SocAlertItem from "./SocAlertItem/SocAlertItem.vue"
39 import type { SocAlert } from "@/types/soc/alert.d"
40 import type { SocUser } from "@/types/soc/user.d"
41 import axios from "axios"
frontend/src/components/soc/SocAlerts/SocAlertsList.vue
+1 -1
@@ -92,7 +92,7 @@
92 import { ref, onBeforeMount, watch, toRefs, nextTick, onBeforeUnmount, onMounted, computed } from "vue"
93 import { useMessage, NSpin, NEmpty, NInput, useDialog, NButton, NPopover } from "naive-ui"
94 import Api from "@/api"
95 -import SocAlertItem from "./SocAlertItem.vue"
95 +import SocAlertItem from "./SocAlertItem/SocAlertItem.vue"
96 import type { SocAlert } from "@/types/soc/alert.d"
97 import type { SocUser } from "@/types/soc/user.d"
98 import type { AlertsFilter } from "@/api/soc"
frontend/src/components/soc/SocCases/SocCaseItem.vue
+1 -1
@@ -254,7 +254,7 @@ import SocCaseAssetsList from "./SocCaseAssetsList.vue"
254 import SocCaseNoteForm from "./SocCaseNoteForm.vue"
255 import SocCaseNotesList from "./SocCaseNotesList.vue"
256 import SocCaseItemActions from "./SocCaseItemActions.vue"
257 -import SocAlertItem from "../SocAlerts/SocAlertItem.vue"
257 +import SocAlertItem from "../SocAlerts/SocAlertItem/SocAlertItem.vue"
258 import Api from "@/api"
259 import {
260 useMessage,
frontend/src/components/soc/SocUsers/SocUserAlerts.vue
+1 -1
@@ -42,7 +42,7 @@ import type { SocAlert } from "@/types/soc/alert.d"
42 import { onBeforeMount, onBeforeUnmount, ref } from "vue"
43 import Api from "@/api"
44 import { useMessage, NTooltip, NSpin, NModal } from "naive-ui"
45 -import SocAlertItem from "../SocAlerts/SocAlertItem.vue"
45 +import SocAlertItem from "../SocAlerts/SocAlertItem/SocAlertItem.vue"
46 import axios from "axios"
47
48 const { userId } = defineProps<{
frontend/src/directives/v-shiki.ts
+5 -2
@@ -3,7 +3,10 @@ import { decode } from "html-entities"
3 import { codeThemes, getHighlighter } from "@/utils/highlighter"
4
5 const vShiki = {
6 - created: async (el: HTMLElement, binding: { value: { lang?: string; decode?: boolean } }) => {
6 + created: async (
7 + el: HTMLElement,
8 + binding: { value: { lang?: string; fallbackLang?: string; decode?: boolean } }
9 + ) => {
10 const code = binding?.value?.decode ? decode(el.children[0].innerHTML) : el.children[0].innerHTML
11
12 let flouriteDetect = null
@@ -14,7 +17,7 @@ const vShiki = {
17 }
18 }
19
17 - const language = binding?.value?.lang || flouriteDetect || "text"
20 + const language = binding?.value?.lang || flouriteDetect || binding?.value?.fallbackLang || "text"
21 const html = (await getHighlighter()).codeToHtml(code, {
22 lang: language,
23 themes: codeThemes
frontend/src/types/soc/alert.d.ts
+2
@@ -44,10 +44,12 @@ export interface AlertContext {
44 asset_type: number
45 customer_id?: string
46 process_id: string
47 + process_name: string
48 rule_id: string
49 rule_mitre_id: string
50 rule_mitre_tactic: string
51 rule_mitre_technique: string
52 + [key: string]: string | number | boolean
53 }
54
55 export enum AlertSource {
frontend/src/types/threatIntel.d.ts
+55
@@ -8,3 +8,58 @@ export interface ThreatIntelResponse {
8 value: string | null
9 virustotal_url: string | null
10 }
11 +
12 +export interface EvaluationData {
13 + rank: number
14 + host_prev: string
15 + eps: string
16 + paths: EvaluationDataPath[]
17 + parents: EvaluationDataParent[]
18 + hashes: EvaluationDataHash[]
19 + network: EvaluationDataNetwork[]
20 + description: string
21 + intel: string
22 + /** ignore */
23 + truncated: EvaluationDataTruncated
24 + /** ignore */
25 + tags: EvaluationDataTag[]
26 +}
27 +
28 +export interface EvaluationDataHash {
29 + hash: string
30 + percentage: number
31 +}
32 +
33 +export interface EvaluationDataNetwork {
34 + port: string
35 + /** percentage */
36 + usage: number
37 +}
38 +
39 +export interface EvaluationDataParent {
40 + name: string
41 + percentage: number
42 +}
43 +
44 +export interface EvaluationDataPath {
45 + directory: string
46 + percentage: number
47 +}
48 +
49 +export interface EvaluationDataTag {
50 + category: string
51 + type: string
52 + description: string
53 + field4: string
54 + field5: string
55 + color: string
56 +}
57 +
58 +export interface EvaluationDataTruncated {
59 + paths: number
60 + parents: number
61 + grandparents: number
62 + children: number
63 + network: number
64 + hashes: number
65 +}
frontend/src/utils/highlighter.ts
+2 -1
@@ -17,7 +17,8 @@ export async function getHighlighter() {
17 import("shiki/langs/yaml.mjs"),
18 import("shiki/langs/html.mjs"),
19 import("shiki/langs/scss.mjs"),
20 - import("shiki/langs/css.mjs")
20 + import("shiki/langs/css.mjs"),
21 + import("shiki/langs/csharp.mjs")
22 ],
23 loadWasm: getWasm
24 })