@cryptotaxi247 / CoPilot / commits / 4ba91a3c

Velo artifact rec (#349)

* velo artifact recommendation route * Handle 429 error response in Socfortress AI Alert API invocation * Add notification_invoked_number column to Case model * chore: update dependencies in frontend * feat: add AI Velociraptor Artifact Recommendation Button * Implement case notification count increment and update schema * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Dec 4, 2024 at 15:52 UTC 4ba91a3c07efb6928445205380f332053242fec4
22 files changed +669 -154
backend/alembic/versions/b453170bbefc_add_case_notification_column_to_table.py new
+31
@@ -0,0 +1,31 @@
1 +"""Add case notification column to table
2 +
3 +Revision ID: b453170bbefc
4 +Revises: 21a945c2982b
5 +Create Date: 2024-12-04 15:27:29.341675
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +
13 +from alembic import op
14 +
15 +# revision identifiers, used by Alembic.
16 +revision: str = "b453170bbefc"
17 +down_revision: Union[str, None] = "21a945c2982b"
18 +branch_labels: Union[str, Sequence[str], None] = None
19 +depends_on: Union[str, Sequence[str], None] = None
20 +
21 +
22 +def upgrade() -> None:
23 + # ### commands auto generated by Alembic - please adjust! ###
24 + op.add_column("incident_management_case", sa.Column("notification_invoked_number", sa.Integer(), nullable=True))
25 + # ### end Alembic commands ###
26 +
27 +
28 +def downgrade() -> None:
29 + # ### commands auto generated by Alembic - please adjust! ###
30 + op.drop_column("incident_management_case", "notification_invoked_number")
31 + # ### end Alembic commands ###
backend/app/agents/services/status.py
+31
@@ -32,6 +32,37 @@ def get_agent(agent_id: str) -> List[Agents]:
32 )
33
34
35 +async def get_agent_os_by_id(agent_id: str, session: AsyncSession) -> str:
36 + """
37 + Retrieves the operating system of a specific agent from the database using its ID.
38 +
39 + Args:
40 + agent_id (str): The ID of the agent to retrieve.
41 + session (AsyncSession): The SQLAlchemy asynchronous session to use for the query.
42 +
43 + Returns:
44 + str: The operating system of the agent if found, otherwise None.
45 + """
46 + try:
47 + agent_result = await session.execute(select(Agents).filter(Agents.agent_id == agent_id))
48 + agent = agent_result.scalars().first()
49 +
50 + if agent is None:
51 + logger.error(f"Agent with agent_id {agent_id} not found.")
52 + raise HTTPException(
53 + status_code=404,
54 + detail=f"Agent with agent_id {agent_id} not found.",
55 + )
56 +
57 + return agent.os
58 + except Exception as e:
59 + logger.error(f"Failed to fetch agent with agent_id {agent_id}: {e}")
60 + raise HTTPException(
61 + status_code=500,
62 + detail=f"Failed to fetch agent with agent_id {agent_id}: {e}",
63 + )
64 +
65 +
66 async def get_outdated_agents_wazuh(
67 session: AsyncSession,
68 ) -> OutdatedWazuhAgentsResponse:
backend/app/incidents/models.py
+1
@@ -154,6 +154,7 @@ class Case(SQLModel, table=True):
154 case_status: str = Field(max_length=50, nullable=False)
155 assigned_to: Optional[str] = Field(max_length=50, nullable=True)
156 customer_code: Optional[str] = Field(max_length=50, nullable=True)
157 + notification_invoked_number: Optional[int] = Field(default=0, nullable=True)
158
159 alerts: List["CaseAlertLink"] = Relationship(back_populates="case")
160 data_store: List["CaseDataStore"] = Relationship(back_populates="case")
backend/app/incidents/routes/db_operations.py
+2
@@ -153,6 +153,7 @@ from app.incidents.services.db_operations import get_customer_notification
153 from app.incidents.services.db_operations import get_field_names
154 from app.incidents.services.db_operations import get_ioc_names
155 from app.incidents.services.db_operations import get_timefield_names
156 +from app.incidents.services.db_operations import increment_case_notification_count
157 from app.incidents.services.db_operations import is_alert_linked_to_case
158 from app.incidents.services.db_operations import list_alert_by_assigned_to
159 from app.incidents.services.db_operations import list_alert_by_status
@@ -972,6 +973,7 @@ async def create_case_notification_endpoint(request: CaseNotificationCreate, db:
973
974 logger.info(f"Creating case notification for case {case_notification_payload}")
975 await handle_customer_notifications_case(customer_code=case_details.customer_code, case_payload=case_notification_payload, session=db)
976 + await increment_case_notification_count(request.case_id, db)
977 return CaseNotificationResponse(success=True, message="Case notification created successfully")
978
979
backend/app/incidents/schema/db_operations.py
+1
@@ -375,6 +375,7 @@ class CaseOut(BaseModel):
375 case_status: Optional[str] = None
376 case_creation_time: Optional[datetime] = None
377 customer_code: Optional[str] = None
378 + notification_invoked_number: Optional[int] = 0
379
380
381 class CaseOutResponse(BaseModel):
backend/app/incidents/schema/incident_alert.py
+4
@@ -28,6 +28,10 @@ class CreateAlertRequestRoute(BaseModel):
28 description="The name of the index to search alerts for.",
29 )
30 index_id: str = Field(..., description="The index id.")
31 + agent_id: Optional[str] = Field(
32 + None,
33 + description="The agent id.",
34 + )
35
36
37 class CreateAlertResponse(BaseModel):
backend/app/incidents/services/db_operations.py
+17
@@ -802,6 +802,21 @@ async def update_alert_assigned_to(alert_id: int, assigned_to: str, db: AsyncSes
802 return alert
803
804
805 +async def increment_case_notification_count(case_id: int, db: AsyncSession) -> Case:
806 + result = await db.execute(select(Case).where(Case.id == case_id))
807 + case = result.scalars().first()
808 + if not case:
809 + raise HTTPException(status_code=404, detail="Case not found")
810 +
811 + # Initialize notification_invoked_number to 0 if it is None
812 + if case.notification_invoked_number is None:
813 + case.notification_invoked_number = 0
814 +
815 + case.notification_invoked_number += 1
816 + await db.commit()
817 + return case
818 +
819 +
820 async def create_comment(comment: CommentCreate, db: AsyncSession) -> Comment:
821 # Check if the alert exists
822 result = await db.execute(select(Alert).options(selectinload(Alert.comments)).where(Alert.id == comment.alert_id))
@@ -1130,6 +1145,7 @@ async def get_case_by_id(case_id: int, db: AsyncSession) -> CaseOut:
1145 alerts=alerts_out,
1146 case_creation_time=case.case_creation_time,
1147 customer_code=case.customer_code,
1148 + notification_invoked_number=case.notification_invoked_number or 0,
1149 )
1150 return case_out
1151
@@ -1181,6 +1197,7 @@ async def list_cases(db: AsyncSession) -> List[CaseOut]:
1197 case_creation_time=case.case_creation_time,
1198 case_status=case.case_status,
1199 customer_code=case.customer_code,
1200 + notification_invoked_number=case.notification_invoked_number or 0,
1201 )
1202 cases_out.append(case_out)
1203 return cases_out
backend/app/threat_intel/routes/socfortress.py
+114
@@ -5,7 +5,9 @@ from fastapi import Security
5 from loguru import logger
6 from sqlalchemy.ext.asyncio import AsyncSession
7
8 +from app.agents.services.status import get_agent_os_by_id
9 from app.auth.utils import AuthHandler
10 +from app.connectors.velociraptor.services.artifacts import get_artifacts
11 from app.db.db_session import get_db
12 from app.incidents.schema.incident_alert import CreateAlertRequest
13 from app.incidents.schema.incident_alert import CreateAlertRequestRoute
@@ -20,12 +22,21 @@ from app.threat_intel.schema.socfortress import SocfortressAiWazuhExclusionRuleR
22 from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisRequest
23 from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisResponse
24 from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
25 +from app.threat_intel.schema.socfortress import (
26 + VelociraptorArtifactRecommendationRequest,
27 +)
28 +from app.threat_intel.schema.socfortress import (
29 + VelociraptorArtifactRecommendationResponse,
30 +)
31 from app.threat_intel.schema.socfortress import VirusTotalThreatIntelRequest
32 from app.threat_intel.schema.virustotal import VirusTotalRouteResponse
33 from app.threat_intel.services.socfortress import invoke_virustotal_api
34 from app.threat_intel.services.socfortress import socfortress_ai_alert_lookup
35 from app.threat_intel.services.socfortress import socfortress_process_analysis_lookup
36 from app.threat_intel.services.socfortress import socfortress_threat_intel_lookup
37 +from app.threat_intel.services.socfortress import (
38 + socfortress_velociraptor_recommendation_lookup,
39 +)
40 from app.threat_intel.services.socfortress import (
41 socfortress_wazuh_exclusion_rule_lookup,
42 )
@@ -242,3 +253,106 @@ async def ai_wazuh_exclusion_rule_socfortress(
253 request=request,
254 )
255 return socfortress_lookup
256 +
257 +
258 +async def fetch_agent_os(agent_id: str, session: AsyncSession) -> str:
259 + """
260 + Fetch the operating system of the agent.
261 +
262 + Args:
263 + agent_id (str): The ID of the agent.
264 + session (AsyncSession): The database session.
265 +
266 + Returns:
267 + str: The normalized operating system name.
268 +
269 + Raises:
270 + HTTPException: If the agent OS is not found or unsupported.
271 + """
272 + agent_os = await get_agent_os_by_id(agent_id=agent_id, session=session)
273 +
274 + if agent_os is None:
275 + raise HTTPException(
276 + status_code=404,
277 + detail="Agent OS not found.",
278 + )
279 +
280 + # Normalize the OS name
281 + agent_os_lower = agent_os.lower()
282 + if "windows" in agent_os_lower:
283 + return "Windows"
284 + elif "linux" in agent_os_lower:
285 + return "Linux"
286 + elif "macos" in agent_os_lower or "mac" in agent_os_lower:
287 + return "MacOS"
288 + else:
289 + raise HTTPException(
290 + status_code=400,
291 + detail="Unsupported OS type.",
292 + )
293 +
294 +
295 +async def filter_artifacts_by_os(artifacts, os):
296 + # Only get the artifacts that start with `Windows.`, `Linux.`, `MacOS.`, or `Generic.`
297 + os_artifacts = ["Windows", "Linux", "MacOS", "Generic"]
298 + os_artifacts = [os_artifact for os_artifact in os_artifacts if os_artifact in os or os_artifact == "Generic"]
299 +
300 + # Artifacts to be stripped out
301 + excluded_artifacts = {
302 + "Windows.Sysinternals.SysmonInstall",
303 + "Windows.Sysinternals.SysmonLogForward",
304 + "Windows.Sysinternals.Autoruns",
305 + "Windows.Sigma.EventLogs",
306 + "Windows.Remediation.Quarantine",
307 + "Windows.Remediation.QuarantineMonitor",
308 + "Windows.Custom.InstallHuntress",
309 + "Windows.Applications.TeamViewer.Incoming",
310 + }
311 +
312 + return [
313 + artifact
314 + for artifact in artifacts
315 + if any(artifact.name.startswith(os_artifact + ".") for os_artifact in os_artifacts) and artifact.name not in excluded_artifacts
316 + ]
317 +
318 +
319 +async def fetch_artifacts(os: str) -> list:
320 + """
321 + Fetch the artifacts.
322 +
323 + Returns:
324 + list: The list of artifacts.
325 + """
326 + artifacts = await get_artifacts()
327 + return await filter_artifacts_by_os(artifacts.artifacts, os)
328 +
329 +
330 +@threat_intel_socfortress_router.post(
331 + "/ai/velociraptor-artifact-recommendation",
332 + response_model=VelociraptorArtifactRecommendationResponse,
333 + description="SocFortress Process Name Evaluation",
334 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
335 +)
336 +async def ai_velociraptor_artifact_recommendation_socfortress(
337 + request: CreateAlertRequestRoute,
338 + session: AsyncSession = Depends(get_db),
339 +):
340 + # Fetch alert details
341 + alert_payload = await get_single_alert_details(CreateAlertRequest(index_name=request.index_name, alert_id=request.index_id))
342 +
343 + assert isinstance(alert_payload, GenericAlertModel)
344 +
345 + os = await fetch_agent_os(request.agent_id, session)
346 +
347 + request = VelociraptorArtifactRecommendationRequest(
348 + integration="SOCFORTRESS AI",
349 + alert_payload=alert_payload._source.dict(),
350 + os=os,
351 + artifacts=await fetch_artifacts(os),
352 + )
353 +
354 + socfortress_lookup = await socfortress_velociraptor_recommendation_lookup(
355 + lincense_key=(await get_license(session)).license_key,
356 + request=request,
357 + )
358 + return socfortress_lookup
backend/app/threat_intel/schema/socfortress.py
+80
@@ -198,3 +198,83 @@ class SocfortressProcessNameAnalysisResponse(BaseModel):
198
199 def to_dict(self):
200 return self.dict()
201 +
202 +
203 +class Artifacts(BaseModel):
204 + description: str = Field(..., description="Description of the artifact.")
205 + name: str = Field(..., description="Name of the artifact.")
206 +
207 +
208 +class OS(str, Enum):
209 + Windows = "Windows"
210 + Linux = "Linux"
211 + MacOS = "MacOS"
212 +
213 +
214 +class VelociraptorArtifactRecommendationRequest(BaseModel):
215 + integration: str = Field(..., example="SOCFORTRESS AI")
216 + artifacts: Optional[List[Artifacts]] = Field(
217 + None,
218 + description="List of artifacts to recommend.",
219 + )
220 + os: OS = Field(..., description="The operating system of the endpoint.")
221 + alert_payload: dict = Field(..., example={"alert": "test"})
222 +
223 + @validator("integration")
224 + def check_integration(cls, v):
225 + if v != "SOCFORTRESS AI":
226 + raise HTTPException(
227 + status_code=400,
228 + detail="Invalid integration. Only 'SOCFORTRESS AI' is supported.",
229 + )
230 + return v
231 +
232 + @validator("alert_payload")
233 + def check_syslog_type(cls, v):
234 + if v.get("syslog_type") != "wazuh":
235 + raise HTTPException(
236 + status_code=400,
237 + detail="Invalid syslog_type. Only 'wazuh' is supported.",
238 + )
239 + # Remove 'message' and 'full_log' fields if they exist
240 + v.pop("message", None)
241 + v.pop("full_log", None)
242 + v.pop("gl2_processing_error", None)
243 + v.pop("gl2_accounted_message_size", None)
244 + v.pop("gl2_source_input", None)
245 + v.pop("gl2_remote_ip", None)
246 + v.pop("gl2_message_id", None)
247 + v.pop("gl2_remote_port", None)
248 + return v
249 +
250 +
251 +class VelociraptorArtifactRecommendation(BaseModel):
252 + name: str = Field(..., description="The name of the artifact.")
253 + description: str = Field(..., description="A description of the artifact.")
254 + explanation: str = Field(
255 + ...,
256 + description="A detailed explanation of the purpose and why the artifact was selected.",
257 + )
258 +
259 +
260 +class AiVelociraptorArtifactsRecommendationModel(BaseModel):
261 + artifact_recommendations: List[VelociraptorArtifactRecommendation] = Field(
262 + description="The recommended artifacts which detail the name, description, and explanation of why the artifact was selected.",
263 + )
264 + general_thoughts: str = Field(
265 + description="General thoughts on the artifacts and why they were selected.",
266 + )
267 +
268 +
269 +class VelociraptorArtifactRecommendationResponse(BaseModel):
270 + artifact_recommendations: List[VelociraptorArtifactRecommendation] = Field(
271 + description="The recommended artifacts which detail the name, description, and explanation of why the artifact was selected.",
272 + )
273 + success: bool = Field(..., description="Whether the request was successful.")
274 + message: str = Field(
275 + ...,
276 + description="A message describing the result of the request.",
277 + )
278 + general_thoughts: str = Field(
279 + description="General thoughts on the artifacts and why they were selected.",
280 + )
backend/app/threat_intel/services/socfortress.py
+59
@@ -20,6 +20,12 @@ from app.threat_intel.schema.socfortress import (
20 from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisRequest
21 from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisResponse
22 from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
23 +from app.threat_intel.schema.socfortress import (
24 + VelociraptorArtifactRecommendationRequest,
25 +)
26 +from app.threat_intel.schema.socfortress import (
27 + VelociraptorArtifactRecommendationResponse,
28 +)
29 from app.threat_intel.schema.virustotal import VirusTotalResponse
30 from app.utils import get_connector_attribute
31
@@ -409,6 +415,39 @@ async def get_wazuh_exclusion_rule_response(
415 return SocfortressAiWazuhExclusionRuleResponse(**response_data)
416
417
418 +async def get_velociraptor_artifact_recommendation_response(
419 + license_key: str,
420 + request: VelociraptorArtifactRecommendationRequest,
421 +) -> VelociraptorArtifactRecommendationResponse:
422 + """
423 + Retrieves Artifact recommendation response from Socfortress Threat Intel API.
424 +
425 + Args:
426 + request (VelociraptorArtifactRecommendationRequest): The request object containing the alert data.
427 + session (AsyncSession): The async session object for making HTTP requests.
428 +
429 + Returns:
430 + VelociraptorArtifactRecommendationResponse: The response object containing the artifact recommendation data and success status.
431 + """
432 + url = "https://ai.socfortress.co/velociraptor-artifact-recommendation"
433 +
434 + response_data = await invoke_socfortress_ai_alert_api(license_key, url, request)
435 +
436 + # If message is `Forbidden`, raise an HTTPException
437 + if response_data.get("message") == "Forbidden":
438 + raise HTTPException(
439 + status_code=403,
440 + detail="Forbidden access to the Socfortress AI Alert API",
441 + )
442 + elif "429" in response_data.get("detail", ""):
443 + raise HTTPException(
444 + status_code=429,
445 + detail="Message is too large. Please try again with a smaller message.",
446 + )
447 +
448 + return VelociraptorArtifactRecommendationResponse(**response_data)
449 +
450 +
451 async def get_process_analysis_response(
452 license_key: str,
453 request: SocfortressProcessNameAnalysisRequest,
@@ -524,3 +563,23 @@ async def socfortress_wazuh_exclusion_rule_lookup(
563 license_key=lincense_key,
564 request=request,
565 )
566 +
567 +
568 +async def socfortress_velociraptor_recommendation_lookup(
569 + lincense_key: str,
570 + request: VelociraptorArtifactRecommendationRequest,
571 +) -> VelociraptorArtifactRecommendationResponse:
572 + """
573 + Performs a AI alert lookup using the Socfortress service.
574 +
575 + Args:
576 + request (VelociraptorArtifactRecommendationRequest): The request object containing the IoC to lookup.
577 + session (AsyncSession): The async session object for making HTTP requests.
578 +
579 + Returns:
580 + IoCResponse: The response object containing the threat intelligence information.
581 + """
582 + return await get_velociraptor_artifact_recommendation_response(
583 + license_key=lincense_key,
584 + request=request,
585 + )
frontend/package-lock.json
+150 -117
@@ -16,7 +16,7 @@
16 "@shikijs/markdown-it": "^1.24.0",
17 "@tailwindcss/container-queries": "^0.1.1",
18 "@vueuse/core": "^12.0.0",
19 - "axios": "^1.7.8",
19 + "axios": "^1.7.9",
20 "bytes": "^3.1.2",
21 "colord": "^2.9.3",
22 "dayjs": "^1.11.13",
@@ -28,10 +28,10 @@
28 "js-md5": "^0.8.3",
29 "lodash": "^4.17.21",
30 "mitt": "^3.0.1",
31 - "naive-ui": "^2.40.2",
31 + "naive-ui": "^2.40.3",
32 "nanoid": "^5.0.9",
33 "password-validator": "^5.3.0",
34 - "pinia": "^2.2.8",
34 + "pinia": "^2.3.0",
35 "pinia-plugin-persistedstate": "^4.1.3",
36 "secure-ls": "^2.0.0",
37 "shiki": "^1.24.0",
@@ -51,7 +51,7 @@
51 "@clack/prompts": "^0.8.2",
52 "@iconify/vue": "^4.1.2",
53 "@tsconfig/node20": "^20.1.4",
54 - "@types/bytes": "^3.1.4",
54 + "@types/bytes": "^3.1.5",
55 "@types/file-saver": "^2.0.7",
56 "@types/fs-extra": "^11.0.4",
57 "@types/jsdom": "^21.1.7",
@@ -63,7 +63,7 @@
63 "@vue/test-utils": "^2.4.6",
64 "@vue/tsconfig": "^0.7.0",
65 "autoprefixer": "^10.4.20",
66 - "cypress": "^13.16.0",
66 + "cypress": "^13.16.1",
67 "depcheck": "^1.4.7",
68 "eslint": "^9.16.0",
69 "flourite": "^1.3.0",
@@ -71,28 +71,28 @@
71 "jsdom": "^25.0.1",
72 "npm-run-all2": "^7.0.1",
73 "postcss": "^8.4.49",
74 - "prettier": "^3.4.1",
74 + "prettier": "^3.4.2",
75 "prettier-plugin-tailwindcss": "^0.6.9",
76 - "sass": "^1.81.0",
76 + "sass": "^1.82.0",
77 "start-server-and-test": "^2.0.8",
78 "tailwind-config-viewer": "^2.0.4",
79 - "tailwindcss": "^3.4.15",
79 + "tailwindcss": "^3.4.16",
80 "taze": "^0.18.0",
81 - "type-fest": "^4.29.0",
81 + "type-fest": "^4.30.0",
82 "typescript": "~5.6.3",
83 "unplugin-vue-components": "^0.27.5",
84 "vite": "^5.4.11",
85 "vite-bundle-visualizer": "^1.2.1",
86 "vite-plugin-vue-devtools": "^7.6.7",
87 "vite-svg-loader": "^5.1.0",
88 - "vitest": "^2.1.6",
88 + "vitest": "^2.1.8",
89 "vue-tsc": "^2.1.10"
90 },
91 "engines": {
92 "node": ">=18.0.0"
93 },
94 "optionalDependencies": {
95 - "@rollup/rollup-linux-x64-gnu": "^4.27.4"
95 + "@rollup/rollup-linux-x64-gnu": "^4.28.0"
96 }
97 },
98 "node_modules/@ajoelp/json-to-formdata": {
@@ -2516,9 +2516,9 @@
2516 ]
2517 },
2518 "node_modules/@rollup/rollup-linux-x64-gnu": {
2519 - "version": "4.27.4",
2520 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz",
2521 - "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==",
2519 + "version": "4.28.0",
2520 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz",
2521 + "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==",
2522 "cpu": [
2523 "x64"
2524 ],
@@ -2796,9 +2796,9 @@
2796 "license": "MIT"
2797 },
2798 "node_modules/@types/bytes": {
2799 - "version": "3.1.4",
2800 - "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.4.tgz",
2801 - "integrity": "sha512-A0uYgOj3zNc4hNjHc5lYUfJQ/HVyBXiUMKdXd7ysclaE6k9oJdavQzODHuwjpUu2/boCP8afjQYi8z/GtvNCWA==",
2799 + "version": "3.1.5",
2800 + "resolved": "https://registry.npmjs.org/@types/bytes/-/bytes-3.1.5.tgz",
2801 + "integrity": "sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==",
2802 "dev": true,
2803 "license": "MIT"
2804 },
@@ -3117,14 +3117,14 @@
3117 }
3118 },
3119 "node_modules/@typescript-eslint/typescript-estree": {
3120 - "version": "8.16.0",
3121 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz",
3122 - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==",
3120 + "version": "8.17.0",
3121 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz",
3122 + "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==",
3123 "dev": true,
3124 "license": "BSD-2-Clause",
3125 "dependencies": {
3126 - "@typescript-eslint/types": "8.16.0",
3127 - "@typescript-eslint/visitor-keys": "8.16.0",
3126 + "@typescript-eslint/types": "8.17.0",
3127 + "@typescript-eslint/visitor-keys": "8.17.0",
3128 "debug": "^4.3.4",
3129 "fast-glob": "^3.3.2",
3130 "is-glob": "^4.0.3",
@@ -3145,6 +3145,38 @@
3145 }
3146 }
3147 },
3148 + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": {
3149 + "version": "8.17.0",
3150 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz",
3151 + "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==",
3152 + "dev": true,
3153 + "license": "MIT",
3154 + "engines": {
3155 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3156 + },
3157 + "funding": {
3158 + "type": "opencollective",
3159 + "url": "https://opencollective.com/typescript-eslint"
3160 + }
3161 + },
3162 + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": {
3163 + "version": "8.17.0",
3164 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz",
3165 + "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==",
3166 + "dev": true,
3167 + "license": "MIT",
3168 + "dependencies": {
3169 + "@typescript-eslint/types": "8.17.0",
3170 + "eslint-visitor-keys": "^4.2.0"
3171 + },
3172 + "engines": {
3173 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3174 + },
3175 + "funding": {
3176 + "type": "opencollective",
3177 + "url": "https://opencollective.com/typescript-eslint"
3178 + }
3179 + },
3180 "node_modules/@typescript-eslint/utils": {
3181 "version": "8.16.0",
3182 "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz",
@@ -3252,14 +3284,14 @@
3284 }
3285 },
3286 "node_modules/@vitest/expect": {
3255 - "version": "2.1.6",
3256 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.6.tgz",
3257 - "integrity": "sha512-9M1UR9CAmrhJOMoSwVnPh2rELPKhYo0m/CSgqw9PyStpxtkwhmdM6XYlXGKeYyERY1N6EIuzkQ7e3Lm1WKCoUg==",
3287 + "version": "2.1.8",
3288 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz",
3289 + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==",
3290 "dev": true,
3291 "license": "MIT",
3292 "dependencies": {
3261 - "@vitest/spy": "2.1.6",
3262 - "@vitest/utils": "2.1.6",
3293 + "@vitest/spy": "2.1.8",
3294 + "@vitest/utils": "2.1.8",
3295 "chai": "^5.1.2",
3296 "tinyrainbow": "^1.2.0"
3297 },
@@ -3268,13 +3300,13 @@
3300 }
3301 },
3302 "node_modules/@vitest/mocker": {
3271 - "version": "2.1.6",
3272 - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.6.tgz",
3273 - "integrity": "sha512-MHZp2Z+Q/A3am5oD4WSH04f9B0T7UvwEb+v5W0kCYMhtXGYbdyl2NUk1wdSMqGthmhpiThPDp/hEoVwu16+u1A==",
3303 + "version": "2.1.8",
3304 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz",
3305 + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==",
3306 "dev": true,
3307 "license": "MIT",
3308 "dependencies": {
3277 - "@vitest/spy": "2.1.6",
3309 + "@vitest/spy": "2.1.8",
3310 "estree-walker": "^3.0.3",
3311 "magic-string": "^0.30.12"
3312 },
@@ -3283,7 +3315,7 @@
3315 },
3316 "peerDependencies": {
3317 "msw": "^2.4.9",
3286 - "vite": "^5.0.0 || ^6.0.0"
3318 + "vite": "^5.0.0"
3319 },
3320 "peerDependenciesMeta": {
3321 "msw": {
@@ -3305,9 +3337,9 @@
3337 }
3338 },
3339 "node_modules/@vitest/pretty-format": {
3308 - "version": "2.1.6",
3309 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.6.tgz",
3310 - "integrity": "sha512-exZyLcEnHgDMKc54TtHca4McV4sKT+NKAe9ix/yhd/qkYb/TP8HTyXRFDijV19qKqTZM0hPL4753zU/U8L/gAA==",
3340 + "version": "2.1.8",
3341 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz",
3342 + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==",
3343 "dev": true,
3344 "license": "MIT",
3345 "dependencies": {
@@ -3318,13 +3350,13 @@
3350 }
3351 },
3352 "node_modules/@vitest/runner": {
3321 - "version": "2.1.6",
3322 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.6.tgz",
3323 - "integrity": "sha512-SjkRGSFyrA82m5nz7To4CkRSEVWn/rwQISHoia/DB8c6IHIhaE/UNAo+7UfeaeJRE979XceGl00LNkIz09RFsA==",
3353 + "version": "2.1.8",
3354 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz",
3355 + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==",
3356 "dev": true,
3357 "license": "MIT",
3358 "dependencies": {
3327 - "@vitest/utils": "2.1.6",
3359 + "@vitest/utils": "2.1.8",
3360 "pathe": "^1.1.2"
3361 },
3362 "funding": {
@@ -3332,13 +3364,13 @@
3364 }
3365 },
3366 "node_modules/@vitest/snapshot": {
3335 - "version": "2.1.6",
3336 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.6.tgz",
3337 - "integrity": "sha512-5JTWHw8iS9l3v4/VSuthCndw1lN/hpPB+mlgn1BUhFbobeIUj1J1V/Bj2t2ovGEmkXLTckFjQddsxS5T6LuVWw==",
3367 + "version": "2.1.8",
3368 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz",
3369 + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==",
3370 "dev": true,
3371 "license": "MIT",
3372 "dependencies": {
3341 - "@vitest/pretty-format": "2.1.6",
3373 + "@vitest/pretty-format": "2.1.8",
3374 "magic-string": "^0.30.12",
3375 "pathe": "^1.1.2"
3376 },
@@ -3347,9 +3379,9 @@
3379 }
3380 },
3381 "node_modules/@vitest/spy": {
3350 - "version": "2.1.6",
3351 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.6.tgz",
3352 - "integrity": "sha512-oTFObV8bd4SDdRka5O+mSh5w9irgx5IetrD5i+OsUUsk/shsBoHifwCzy45SAORzAhtNiprUVaK3hSCCzZh1jQ==",
3382 + "version": "2.1.8",
3383 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz",
3384 + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==",
3385 "dev": true,
3386 "license": "MIT",
3387 "dependencies": {
@@ -3360,13 +3392,13 @@
3392 }
3393 },
3394 "node_modules/@vitest/utils": {
3363 - "version": "2.1.6",
3364 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.6.tgz",
3365 - "integrity": "sha512-ixNkFy3k4vokOUTU2blIUvOgKq/N2PW8vKIjZZYsGJCMX69MRa9J2sKqX5hY/k5O5Gty3YJChepkqZ3KM9LyIQ==",
3395 + "version": "2.1.8",
3396 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz",
3397 + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==",
3398 "dev": true,
3399 "license": "MIT",
3400 "dependencies": {
3369 - "@vitest/pretty-format": "2.1.6",
3401 + "@vitest/pretty-format": "2.1.8",
3402 "loupe": "^3.1.2",
3403 "tinyrainbow": "^1.2.0"
3404 },
@@ -4121,9 +4153,9 @@
4153 "license": "MIT"
4154 },
4155 "node_modules/axios": {
4124 - "version": "1.7.8",
4125 - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
4126 - "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
4156 + "version": "1.7.9",
4157 + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
4158 + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
4159 "license": "MIT",
4160 "dependencies": {
4161 "follow-redirects": "^1.15.6",
@@ -5176,9 +5208,9 @@
5208 "license": "MIT"
5209 },
5210 "node_modules/cypress": {
5179 - "version": "13.16.0",
5180 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.0.tgz",
5181 - "integrity": "sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA==",
5211 + "version": "13.16.1",
5212 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.1.tgz",
5213 + "integrity": "sha512-17FtCaz0cx7ssWYKXzGB0Vub8xHwpVPr+iPt2fHhLMDhVAPVrplD+rTQsZUsfb19LVBn5iwkEUFjQ1yVVJXsLA==",
5214 "dev": true,
5215 "hasInstallScript": true,
5216 "license": "MIT",
@@ -8994,12 +9026,15 @@
9026 }
9027 },
9028 "node_modules/lilconfig": {
8997 - "version": "2.1.0",
8998 - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
8999 - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
9029 + "version": "3.1.3",
9030 + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
9031 + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
9032 "license": "MIT",
9033 "engines": {
9002 - "node": ">=10"
9034 + "node": ">=14"
9035 + },
9036 + "funding": {
9037 + "url": "https://github.com/sponsors/antonk52"
9038 }
9039 },
9040 "node_modules/lines-and-columns": {
@@ -10412,9 +10447,9 @@
10447 }
10448 },
10449 "node_modules/naive-ui": {
10415 - "version": "2.40.2",
10416 - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.40.2.tgz",
10417 - "integrity": "sha512-iOpk9/SdAxOPnbFDiN9c0N6vzRJiyC+xgs8rqeaulLvxg0ix39xA3noordy9wyq0YUbqS3ID6jPdld/kW4KSEg==",
10450 + "version": "2.40.3",
10451 + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.40.3.tgz",
10452 + "integrity": "sha512-TpgYfOg0SNlG4HHhTdFnFcPc1trZiX3r10Pn6biyEgRoi6ZC5qbsY8xgKsqQuG4nWj2PHLT8pPVEkt2pKOlxag==",
10453 "license": "MIT",
10454 "dependencies": {
10455 "@css-render/plugin-bem": "^0.15.14",
@@ -11327,9 +11362,9 @@
11362 }
11363 },
11364 "node_modules/pinia": {
11330 - "version": "2.2.8",
11331 - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.8.tgz",
11332 - "integrity": "sha512-NRTYy2g+kju5tBRe0oNlriZIbMNvma8ZJrpHsp3qudyiMEA8jMmPPKQ2QMHg0Oc4BkUyQYWagACabrwriCK9HQ==",
11365 + "version": "2.3.0",
11366 + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.0.tgz",
11367 + "integrity": "sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==",
11368 "license": "MIT",
11369 "dependencies": {
11370 "@vue/devtools-api": "^6.6.3",
@@ -11339,14 +11374,10 @@
11374 "url": "https://github.com/sponsors/posva"
11375 },
11376 "peerDependencies": {
11342 - "@vue/composition-api": "^1.4.0",
11377 "typescript": ">=4.4.4",
11344 - "vue": "^2.6.14 || ^3.5.11"
11378 + "vue": "^2.7.0 || ^3.5.11"
11379 },
11380 "peerDependenciesMeta": {
11347 - "@vue/composition-api": {
11348 - "optional": true
11349 - },
11381 "typescript": {
11382 "optional": true
11383 }
@@ -11576,18 +11607,6 @@
11607 }
11608 }
11609 },
11579 - "node_modules/postcss-load-config/node_modules/lilconfig": {
11580 - "version": "3.1.2",
11581 - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
11582 - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
11583 - "license": "MIT",
11584 - "engines": {
11585 - "node": ">=14"
11586 - },
11587 - "funding": {
11588 - "url": "https://github.com/sponsors/antonk52"
11589 - }
11590 - },
11610 "node_modules/postcss-load-config/node_modules/yaml": {
11611 "version": "2.6.1",
11612 "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
@@ -11673,9 +11692,9 @@
11692 }
11693 },
11694 "node_modules/prettier": {
11676 - "version": "3.4.1",
11677 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz",
11678 - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==",
11695 + "version": "3.4.2",
11696 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
11697 + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
11698 "dev": true,
11699 "license": "MIT",
11700 "bin": {
@@ -12504,6 +12523,20 @@
12523 "url": "https://github.com/sponsors/jonschlinkert"
12524 }
12525 },
12526 + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
12527 + "version": "4.27.4",
12528 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz",
12529 + "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==",
12530 + "cpu": [
12531 + "x64"
12532 + ],
12533 + "dev": true,
12534 + "license": "MIT",
12535 + "optional": true,
12536 + "os": [
12537 + "linux"
12538 + ]
12539 + },
12540 "node_modules/rrweb-cssom": {
12541 "version": "0.7.1",
12542 "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -12586,9 +12619,9 @@
12619 "license": "MIT"
12620 },
12621 "node_modules/sass": {
12589 - "version": "1.81.0",
12590 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz",
12591 - "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==",
12622 + "version": "1.82.0",
12623 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.82.0.tgz",
12624 + "integrity": "sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q==",
12625 "dev": true,
12626 "license": "MIT",
12627 "dependencies": {
@@ -13398,9 +13431,9 @@
13431 }
13432 },
13433 "node_modules/tailwindcss": {
13401 - "version": "3.4.15",
13402 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz",
13403 - "integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==",
13434 + "version": "3.4.16",
13435 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
13436 + "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==",
13437 "license": "MIT",
13438 "dependencies": {
13439 "@alloc/quick-lru": "^5.2.0",
@@ -13412,7 +13445,7 @@
13445 "glob-parent": "^6.0.2",
13446 "is-glob": "^4.0.3",
13447 "jiti": "^1.21.6",
13415 - "lilconfig": "^2.1.0",
13448 + "lilconfig": "^3.1.3",
13449 "micromatch": "^4.0.8",
13450 "normalize-path": "^3.0.0",
13451 "object-hash": "^3.0.0",
@@ -13889,9 +13922,9 @@
13922 }
13923 },
13924 "node_modules/type-fest": {
13892 - "version": "4.29.0",
13893 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.29.0.tgz",
13894 - "integrity": "sha512-RPYt6dKyemXJe7I6oNstcH24myUGSReicxcHTvCLgzm4e0n8y05dGvcGB15/SoPRBmhlMthWQ9pvKyL81ko8nQ==",
13925 + "version": "4.30.0",
13926 + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.0.tgz",
13927 + "integrity": "sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==",
13928 "dev": true,
13929 "license": "(MIT OR CC0-1.0)",
13930 "engines": {
@@ -14484,9 +14517,9 @@
14517 }
14518 },
14519 "node_modules/vite-node": {
14487 - "version": "2.1.6",
14488 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.6.tgz",
14489 - "integrity": "sha512-DBfJY0n9JUwnyLxPSSUmEePT21j8JZp/sR9n+/gBwQU6DcQOioPdb8/pibWfXForbirSagZCilseYIwaL3f95A==",
14520 + "version": "2.1.8",
14521 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz",
14522 + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==",
14523 "dev": true,
14524 "license": "MIT",
14525 "dependencies": {
@@ -14494,13 +14527,13 @@
14527 "debug": "^4.3.7",
14528 "es-module-lexer": "^1.5.4",
14529 "pathe": "^1.1.2",
14497 - "vite": "^5.0.0 || ^6.0.0"
14530 + "vite": "^5.0.0"
14531 },
14532 "bin": {
14533 "vite-node": "vite-node.mjs"
14534 },
14535 "engines": {
14503 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
14536 + "node": "^18.0.0 || >=20.0.0"
14537 },
14538 "funding": {
14539 "url": "https://opencollective.com/vitest"
@@ -15251,19 +15284,19 @@
15284 }
15285 },
15286 "node_modules/vitest": {
15254 - "version": "2.1.6",
15255 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.6.tgz",
15256 - "integrity": "sha512-isUCkvPL30J4c5O5hgONeFRsDmlw6kzFEdLQHLezmDdKQHy8Ke/B/dgdTMEgU0vm+iZ0TjW8GuK83DiahBoKWQ==",
15287 + "version": "2.1.8",
15288 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz",
15289 + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
15290 "dev": true,
15291 "license": "MIT",
15292 "dependencies": {
15260 - "@vitest/expect": "2.1.6",
15261 - "@vitest/mocker": "2.1.6",
15262 - "@vitest/pretty-format": "^2.1.6",
15263 - "@vitest/runner": "2.1.6",
15264 - "@vitest/snapshot": "2.1.6",
15265 - "@vitest/spy": "2.1.6",
15266 - "@vitest/utils": "2.1.6",
15293 + "@vitest/expect": "2.1.8",
15294 + "@vitest/mocker": "2.1.8",
15295 + "@vitest/pretty-format": "^2.1.8",
15296 + "@vitest/runner": "2.1.8",
15297 + "@vitest/snapshot": "2.1.8",
15298 + "@vitest/spy": "2.1.8",
15299 + "@vitest/utils": "2.1.8",
15300 "chai": "^5.1.2",
15301 "debug": "^4.3.7",
15302 "expect-type": "^1.1.0",
@@ -15274,24 +15307,24 @@
15307 "tinyexec": "^0.3.1",
15308 "tinypool": "^1.0.1",
15309 "tinyrainbow": "^1.2.0",
15277 - "vite": "^5.0.0 || ^6.0.0",
15278 - "vite-node": "2.1.6",
15310 + "vite": "^5.0.0",
15311 + "vite-node": "2.1.8",
15312 "why-is-node-running": "^2.3.0"
15313 },
15314 "bin": {
15315 "vitest": "vitest.mjs"
15316 },
15317 "engines": {
15285 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
15318 + "node": "^18.0.0 || >=20.0.0"
15319 },
15320 "funding": {
15321 "url": "https://opencollective.com/vitest"
15322 },
15323 "peerDependencies": {
15324 "@edge-runtime/vm": "*",
15292 - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
15293 - "@vitest/browser": "2.1.6",
15294 - "@vitest/ui": "2.1.6",
15325 + "@types/node": "^18.0.0 || >=20.0.0",
15326 + "@vitest/browser": "2.1.8",
15327 + "@vitest/ui": "2.1.8",
15328 "happy-dom": "*",
15329 "jsdom": "*"
15330 },
frontend/package.json
+14 -14
@@ -44,7 +44,7 @@
44 "@shikijs/markdown-it": "^1.24.0",
45 "@tailwindcss/container-queries": "^0.1.1",
46 "@vueuse/core": "^12.0.0",
47 - "axios": "^1.7.8",
47 + "axios": "^1.7.9",
48 "bytes": "^3.1.2",
49 "colord": "^2.9.3",
50 "dayjs": "^1.11.13",
@@ -56,10 +56,10 @@
56 "js-md5": "^0.8.3",
57 "lodash": "^4.17.21",
58 "mitt": "^3.0.1",
59 - "naive-ui": "^2.40.2",
59 + "naive-ui": "^2.40.3",
60 "nanoid": "^5.0.9",
61 "password-validator": "^5.3.0",
62 - "pinia": "^2.2.8",
62 + "pinia": "^2.3.0",
63 "pinia-plugin-persistedstate": "^4.1.3",
64 "secure-ls": "^2.0.0",
65 "shiki": "^1.24.0",
@@ -75,14 +75,14 @@
75 "vuedraggable": "^4.1.0"
76 },
77 "optionalDependencies": {
78 - "@rollup/rollup-linux-x64-gnu": "^4.27.4"
78 + "@rollup/rollup-linux-x64-gnu": "^4.28.0"
79 },
80 "devDependencies": {
81 "@antfu/eslint-config": "^3.11.2",
82 "@clack/prompts": "^0.8.2",
83 "@iconify/vue": "^4.1.2",
84 "@tsconfig/node20": "^20.1.4",
85 - "@types/bytes": "^3.1.4",
85 + "@types/bytes": "^3.1.5",
86 "@types/file-saver": "^2.0.7",
87 "@types/fs-extra": "^11.0.4",
88 "@types/jsdom": "^21.1.7",
@@ -94,7 +94,7 @@
94 "@vue/test-utils": "^2.4.6",
95 "@vue/tsconfig": "^0.7.0",
96 "autoprefixer": "^10.4.20",
97 - "cypress": "^13.16.0",
97 + "cypress": "^13.16.1",
98 "depcheck": "^1.4.7",
99 "eslint": "^9.16.0",
100 "flourite": "^1.3.0",
@@ -102,28 +102,28 @@
102 "jsdom": "^25.0.1",
103 "npm-run-all2": "^7.0.1",
104 "postcss": "^8.4.49",
105 - "prettier": "^3.4.1",
105 + "prettier": "^3.4.2",
106 "prettier-plugin-tailwindcss": "^0.6.9",
107 - "sass": "^1.81.0",
107 + "sass": "^1.82.0",
108 "start-server-and-test": "^2.0.8",
109 "tailwind-config-viewer": "^2.0.4",
110 - "tailwindcss": "^3.4.15",
110 + "tailwindcss": "^3.4.16",
111 "taze": "^0.18.0",
112 - "type-fest": "^4.29.0",
112 + "type-fest": "^4.30.0",
113 "typescript": "~5.6.3",
114 "unplugin-vue-components": "^0.27.5",
115 "vite": "^5.4.11",
116 "vite-bundle-visualizer": "^1.2.1",
117 "vite-plugin-vue-devtools": "^7.6.7",
118 "vite-svg-loader": "^5.1.0",
119 - "vitest": "^2.1.6",
119 + "vitest": "^2.1.8",
120 "vue-tsc": "^2.1.10"
121 },
122 "pnpm": {
123 "overrides": {
124 - "@typescript-eslint/eslint-plugin": "^8.16.0",
124 + "@typescript-eslint/eslint-plugin": "^8.17.0",
125 "@typescript-eslint/eslint-plugin>eslint": "$eslint",
126 - "@typescript-eslint/parser": "^8.16.0",
126 + "@typescript-eslint/parser": "^8.17.0",
127 "@typescript-eslint/parser>eslint": "$eslint",
128 "eslint": "$eslint"
129 }
@@ -135,6 +135,6 @@
135 "@typescript-eslint/parser": {
136 "eslint": "^9.16.0"
137 },
138 - "@typescript-eslint/typescript-estree": "^8.16.0"
138 + "@typescript-eslint/typescript-estree": "^8.17.0"
139 }
140 }
frontend/src/api/endpoints/threatIntel.ts
+19
@@ -1,6 +1,7 @@
1 import type { FlaskBaseResponse } from "@/types/flask.d"
2 import type {
3 AiAnalysisResponse,
4 + AiVelociraptorArtifactRecommendationResponse,
5 AiWazuhExclusionRuleResponse,
6 EpssScore,
7 EvaluationData,
@@ -46,6 +47,24 @@ export default {
47 }
48 )
49 },
50 + aiVelociraptorArtifactRecommendation({
51 + indexId,
52 + indexName,
53 + agentId
54 + }: {
55 + indexId: string
56 + indexName: string
57 + agentId: string
58 + }) {
59 + return HttpClient.post<FlaskBaseResponse & AiVelociraptorArtifactRecommendationResponse>(
60 + `/threat_intel/ai/velociraptor-artifact-recommendation`,
61 + {
62 + index_name: indexName,
63 + index_id: indexId,
64 + agent_id: agentId
65 + }
66 + )
67 + },
68 virusTotalEnrichment(iocValue: string) {
69 return HttpClient.post<FlaskBaseResponse & VirusTotalResponse>(`/threat_intel/virustotal`, {
70 ioc_value: iocValue
frontend/src/components/incidentManagement/alerts/AlertAsset.vue
+15 -12
@@ -49,19 +49,19 @@
49 content-class="!p-0"
50 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
51 :bordered="false"
52 + :title="assetNameTruncated"
53 segmented
54 >
54 - <template #header>
55 - <div class="min-h-8 whitespace-nowrap">
56 - {{ assetNameTruncated }}
57 - </div>
58 - </template>
59 - <template #header-extra>
60 - <div class="flex min-h-8 flex-wrap gap-3">
61 - <AIWazuhExclusionRuleButton :index-id="asset.index_id" :index-name="asset.index_name" />
62 - <AIAnalystButton :index-id="asset.index_id" :index-name="asset.index_name" />
63 - </div>
64 - </template>
55 + <div class="flex flex-wrap justify-end gap-3 p-6">
56 + <AIVelociraptorArtifactRecommendationButton
57 + :index-id="asset.index_id"
58 + :index-name="asset.index_name"
59 + :agent-id="asset.agent_id"
60 + />
61 + <AIWazuhExclusionRuleButton :index-id="asset.index_id" :index-name="asset.index_name" />
62 + <AIAnalystButton :index-id="asset.index_id" :index-name="asset.index_name" />
63 + </div>
64 + <n-divider class="!my-0" />
65 <n-tabs type="line" animated :tabs-padding="24">
66 <n-tab-pane name="Info" tab="Info" display-directive="show">
67 <AlertAssetInfo :asset />
@@ -141,7 +141,7 @@ import CardEntity from "@/components/common/cards/CardEntity.vue"
141 import Icon from "@/components/common/Icon.vue"
142 import { useGoto } from "@/composables/useGoto"
143 import _truncate from "lodash/truncate"
144 -import { NCard, NModal, NSpin, NTabPane, NTabs, useMessage } from "naive-ui"
144 +import { NCard, NDivider, NModal, NSpin, NTabPane, NTabs, useMessage } from "naive-ui"
145 import { computed, defineAsyncComponent, ref, watch } from "vue"
146
147 const { asset, embedded, badge } = defineProps<{ asset: AlertAsset; embedded?: boolean; badge?: boolean }>()
@@ -153,6 +153,9 @@ const AIAnalystButton = defineAsyncComponent(() => import("@/components/threatIn
153 const AIWazuhExclusionRuleButton = defineAsyncComponent(
154 () => import("@/components/threatIntel/AIWazuhExclusionRuleButton.vue")
155 )
156 +const AIVelociraptorArtifactRecommendationButton = defineAsyncComponent(
157 + () => import("@/components/threatIntel/AIVelociraptorArtifactRecommendationButton.vue")
158 +)
159 const ThreatIntelProcessEvaluationProvider = defineAsyncComponent(
160 () => import("@/components/threatIntel/ThreatIntelProcessEvaluationProvider.vue")
161 )
frontend/src/components/indices/TopIndices.vue
+1 -1
@@ -34,7 +34,7 @@ function getOptions() {
34 const data = _.chain(indices.value || [])
35 .map(i => {
36 if (typeof i.store_size === "string") {
37 - i.store_size_value = bytes(i.store_size)
37 + i.store_size_value = bytes(i.store_size) || undefined
38 } else {
39 i.store_size_value = i.store_size
40 }
frontend/src/components/threatIntel/AIVelociraptorArtifactRecommendationButton.vue new
+119
@@ -0,0 +1,119 @@
1 +<template>
2 + <div>
3 + <LicenseFeatureCheck
4 + feature="SOCFORTRESS AI"
5 + feedback="tooltip"
6 + @response="
7 + (() => {
8 + licenseChecked = true
9 + licenseResponse = $event
10 + })()
11 + "
12 + @start-loading="licenseChecking = true"
13 + @stop-loading="licenseChecking = false"
14 + >
15 + <n-button
16 + :size="size || 'small'"
17 + ghost
18 + type="primary"
19 + :loading="loading || licenseChecking"
20 + :disabled="!licenseChecked || !licenseResponse"
21 + @click="analysis()"
22 + >
23 + <template #icon>
24 + <Icon :name="AiIcon" />
25 + </template>
26 + <div class="flex items-center gap-2">
27 + <span>Velociraptor Artifact Recommendation</span>
28 + <Icon v-if="!licenseResponse && licenseChecked" :name="LockIcon" :size="14" />
29 + </div>
30 + </n-button>
31 + </LicenseFeatureCheck>
32 +
33 + <n-modal
34 + v-model:show="showModal"
35 + preset="card"
36 + content-class="!p-0"
37 + :style="{ maxWidth: 'min(710px, 90vw)', minHeight: 'min(500px, 90vh)' }"
38 + :bordered="false"
39 + title="Velociraptor Artifact Recommendation"
40 + segmented
41 + >
42 + <div v-if="analysisResponse?.general_thoughts" class="p-6 pb-3">
43 + {{ analysisResponse.general_thoughts }}
44 + </div>
45 + <div v-if="analysisResponse?.artifact_recommendations?.length" class="flex flex-col gap-3 p-6">
46 + <CardEntity
47 + v-for="recommendation of analysisResponse.artifact_recommendations"
48 + :key="recommendation.name + recommendation.explanation + recommendation.description"
49 + embedded
50 + >
51 + <template #header>
52 + {{ recommendation.name }}
53 + </template>
54 + <template #default>
55 + {{ recommendation.description }}
56 + </template>
57 + <template #footer>
58 + {{ recommendation.explanation }}
59 + </template>
60 + </CardEntity>
61 + </div>
62 + <n-empty v-else description="No Recommendations found" class="h-48 justify-center" />
63 + </n-modal>
64 + </div>
65 +</template>
66 +
67 +<script setup lang="ts">
68 +import type { AiVelociraptorArtifactRecommendationResponse } from "@/types/threatIntel.d"
69 +import type { Size } from "naive-ui/es/button/src/interface"
70 +import Api from "@/api"
71 +import CardEntity from "@/components/common/cards/CardEntity.vue"
72 +import Icon from "@/components/common/Icon.vue"
73 +import LicenseFeatureCheck from "@/components/license/LicenseFeatureCheck.vue"
74 +import { NButton, NEmpty, NModal, useMessage } from "naive-ui"
75 +import { ref } from "vue"
76 +
77 +const { indexName, indexId, agentId, size } = defineProps<{
78 + indexName: string
79 + indexId: string
80 + agentId: string
81 + size?: Size
82 +}>()
83 +
84 +const LockIcon = "carbon:locked"
85 +const AiIcon = "mage:stars-c"
86 +const showModal = ref<boolean>(false)
87 +const loading = ref<boolean>(false)
88 +const message = useMessage()
89 +const analysisResponse = ref<AiVelociraptorArtifactRecommendationResponse | null>(null)
90 +const licenseChecking = ref(false)
91 +const licenseChecked = ref(false)
92 +const licenseResponse = ref(false)
93 +
94 +function openResponse() {
95 + showModal.value = true
96 +}
97 +
98 +function analysis() {
99 + loading.value = true
100 +
101 + Api.threatIntel
102 + .aiVelociraptorArtifactRecommendation({ indexName, indexId, agentId })
103 + .then(res => {
104 + if (res.data.success) {
105 + analysisResponse.value = res.data
106 +
107 + openResponse()
108 + } else {
109 + message.warning(res.data?.message || "An error occurred. Please try again later.")
110 + }
111 + })
112 + .catch(err => {
113 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
114 + })
115 + .finally(() => {
116 + loading.value = false
117 + })
118 +}
119 +</script>
frontend/src/components/threatIntel/AIWazuhExclusionRuleButton.vue
+1 -1
@@ -41,7 +41,7 @@
41 >
42 <div
43 v-if="analysisResponse?.wazuh_exclusion_rule || analysisResponse?.wazuh_exclusion_rule_justification"
44 - class="flex flex-col gap-7 !p-7"
44 + class="flex flex-col gap-7 p-7"
45 >
46 <div v-if="analysisResponse?.wazuh_exclusion_rule">
47 <CodeSource :code="analysisResponse.wazuh_exclusion_rule" :decode="true" />
frontend/src/types/threatIntel.d.ts
+9
@@ -87,6 +87,15 @@ export interface AiWazuhExclusionRuleResponse {
87 wazuh_exclusion_rule_justification: string
88 }
89
90 +export interface AiVelociraptorArtifactRecommendationResponse {
91 + artifact_recommendations: {
92 + name: string
93 + description: string
94 + explanation: string
95 + }[]
96 + general_thoughts: string
97 +}
98 +
99 export interface VirusTotalResponse {
100 data: VirusTotal
101 }
frontend/src/views/AlertsGraylog.vue
-2
@@ -7,5 +7,3 @@
7 <script setup lang="ts">
8 import AlertsGraylogList from "@/components/alerts/AlertsGraylogList.vue"
9 </script>
10 -
11 -<style lang="scss" scoped></style>
frontend/src/views/AlertsOld.vue
-2
@@ -7,5 +7,3 @@
7 <script setup lang="ts">
8 import AlertsList from "@/components/alerts/AlertsList.vue"
9 </script>
10 -
11 -<style lang="scss" scoped></style>
frontend/src/views/Artifacts.vue
-2
@@ -48,5 +48,3 @@ const agents = ref<Agent[]>([])
48
49 const activeTab = ref<string | undefined>(undefined)
50 </script>
51 -
52 -<style lang="scss" scoped></style>
frontend/src/views/graylog/Management.vue
+1 -3
@@ -51,7 +51,7 @@ const Inputs = defineAsyncComponent(() => import("@/components/graylog/Inputs/Li
51 const tabsList = ["messages", "alerts", "events", "streams", "provisioning"]
52 const drawersList = ["inputs"]
53
54 -const activeTab = ref<string | undefined>(tabsList[0])
54 +const activeTab = ref<string>(tabsList[0])
55 const highlightEvent = ref<string | undefined>(undefined)
56 const showInputDrawer = ref(false)
57 const events = ref<EventDefinition[]>([])
@@ -79,5 +79,3 @@ onBeforeMount(() => {
79 }
80 })
81 </script>
82 -
83 -<style lang="scss" scoped></style>