@cryptotaxi247 / CoPilot / commits / ba629f0c

Agent cases (#115)

* Add close case route and service * Add reopen case functionality * added agent case list * Add Event Shipper integration and log shipper test router * Add customer integration settings model * Add available integrations to the database * Add available integrations endpoint * Add fetch_available_integrations function and refactor get_available_integrations endpoint * Add customer integration creation endpoint This commit adds a new endpoint for creating customer integrations. It includes functions for validating integration names and customer codes, checking for existing customer integrations, creating integration services, customer integrations, and integration subscriptions. The commit also includes the necessary schema definitions for the request and response models. * updated dependencies * updated soc case type * added soc case actions * updated soc api * added soc case purge * updated soc case delete confirm message * list customer integrations by customer code * fixed soc case delete listener * return customer integration metadata * remove old route --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Jan 13, 2024 at 09:05 UTC ba629f0ceee803b6fdbcf997e8d3a8afeb5467bc
30 files changed +1390 -267
.env.example
+3
@@ -54,3 +54,6 @@ GRAFANA_USERNAME=dummy
54 GRAFANA_PASSWORD=dummy
55
56 WAZUH_WORKER_PROVISIONING_URL=http://example.com
57 +
58 +EVENT_SHIPPER_URL=graylog_host
59 +GELF_INPUT_PORT=gelf_port
backend/app/connectors/dfir_iris/routes/cases.py
+42 -2
@@ -7,7 +7,7 @@ from fastapi import Security
7 from loguru import logger
8
9 from app.auth.utils import AuthHandler
10 -from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody, PurgeCaseResponse
10 +from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody, PurgeCaseResponse, ClosedCaseResponse, ReopenedCaseResponse
11 from app.connectors.dfir_iris.schema.cases import CaseResponse
12 from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
13 from app.connectors.dfir_iris.schema.cases import SingleCaseBody
@@ -17,7 +17,7 @@ from app.connectors.dfir_iris.services.cases import get_all_cases
17 from app.connectors.dfir_iris.services.cases import get_cases_older_than
18 from app.connectors.dfir_iris.services.cases import get_single_case
19 from app.connectors.dfir_iris.utils.universal import check_case_exists
20 -from app.connectors.dfir_iris.services.cases import purge_cases, delete_single_case
20 +from app.connectors.dfir_iris.services.cases import purge_cases, delete_single_case, close_case, reopen_case
21
22
23 async def verify_case_exists(case_id: int) -> int:
@@ -155,3 +155,43 @@ async def get_single_case_route(case_id: int = Depends(verify_case_exists)) -> S
155 logger.info(f"Fetching case {case_id}")
156 single_case_body = SingleCaseBody(case_id=case_id)
157 return await get_single_case(single_case_body.case_id)
158 +
159 +@dfir_iris_cases_router.put(
160 + "/close/{case_id}",
161 + response_model=ClosedCaseResponse,
162 + description="Close a single case",
163 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
164 +)
165 +async def close_single_case_route(case_id: int = Depends(verify_case_exists)) -> ClosedCaseResponse:
166 + """
167 + Close a single case by its ID.
168 +
169 + Args:
170 + case_id (int): The ID of the case to close.
171 +
172 + Returns:
173 + ClosedCaseResponse: The response containing the closed case information.
174 + """
175 + logger.info(f"Closing case {case_id}")
176 + single_case_body = SingleCaseBody(case_id=case_id)
177 + return await close_case(single_case_body.case_id)
178 +
179 +@dfir_iris_cases_router.put(
180 + "/open/{case_id}",
181 + response_model=ReopenedCaseResponse,
182 + description="Open a single case",
183 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
184 +)
185 +async def reopen_single_case_route(case_id: int = Depends(verify_case_exists)) -> ReopenedCaseResponse:
186 + """
187 + Open a single case by its ID.
188 +
189 + Args:
190 + case_id (int): The ID of the case to open.
191 +
192 + Returns:
193 + ReopenedCaseResponse: The response containing the opened case information.
194 + """
195 + logger.info(f"Opening case {case_id}")
196 + single_case_body = SingleCaseBody(case_id=case_id)
197 + return await reopen_case(single_case_body.case_id)
backend/app/connectors/dfir_iris/schema/cases.py
+57 -2
@@ -4,6 +4,7 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6 from typing import Union
7 +from datetime import date
8
9 from pydantic import BaseModel
10 from pydantic import Field
@@ -38,13 +39,11 @@ class PurgeCaseResponse(BaseModel):
39 message: str
40 success: bool
41
41 -
42 class ModificationHistoryItem(BaseModel):
43 action: str
44 user: str
45 user_id: int
46
47 -
47 class SingleCaseModel(BaseModel):
48 case_description: str
49 case_id: int
@@ -99,3 +98,59 @@ class CasesBreachedResponse(BaseModel):
98 cases_breached: List[CaseModel]
99 message: str
100 success: bool
101 +
102 +class CaseModificationHistoryItem(BaseModel):
103 + user: str
104 + user_id: int
105 + action: str
106 +
107 +class CaseData(BaseModel):
108 + owner_id: int
109 + case_soc_id: str
110 + status_id: int
111 + case_name: str
112 + custom_attributes: Optional[str] = None
113 + open_date: date
114 + close_date: date
115 + state_id: int
116 + case_description: str
117 + reviewer_id: Optional[int] = None
118 + closing_note: Optional[str] = None
119 + case_id: int
120 + modification_history: Dict[str, CaseModificationHistoryItem]
121 + classification_id: Optional[int] = None
122 + review_status_id: Optional[int] = None
123 + user_id: int
124 + case_uuid: str
125 + case_customer: int
126 +
127 +class ClosedCaseResponse(BaseModel):
128 + success: bool
129 + case: CaseData
130 + message: str
131 +
132 +class ReopenedCaseData(BaseModel):
133 + owner_id: int
134 + case_soc_id: str
135 + status_id: int
136 + case_name: str
137 + custom_attributes: Optional[str] = None
138 + open_date: date
139 + close_date: Optional[date] = None
140 + state_id: int
141 + case_description: str
142 + reviewer_id: Optional[int] = None
143 + closing_note: Optional[str] = None
144 + case_id: int
145 + modification_history: Dict[str, CaseModificationHistoryItem]
146 + classification_id: Optional[int] = None
147 + review_status_id: Optional[int] = None
148 + user_id: int
149 + case_uuid: str
150 + case_customer: int
151 +
152 +class ReopenedCaseResponse(BaseModel):
153 + success: bool
154 + case: ReopenedCaseData
155 + message: str
156 +
backend/app/connectors/dfir_iris/services/cases.py
+42 -1
@@ -6,7 +6,7 @@ from dfir_iris_client.case import Case
6 from fastapi import HTTPException
7 from loguru import logger
8
9 -from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody, PurgeCaseResponse
9 +from app.connectors.dfir_iris.schema.cases import CaseOlderThanBody, PurgeCaseResponse, ClosedCaseResponse, ReopenedCaseResponse
10 from app.connectors.dfir_iris.schema.cases import CaseResponse
11 from app.connectors.dfir_iris.schema.cases import CasesBreachedResponse
12 from app.connectors.dfir_iris.schema.cases import SingleCaseBody
@@ -130,6 +130,47 @@ async def get_single_case(case_id: SingleCaseBody) -> SingleCaseResponse:
130 result = await fetch_and_parse_data(dfir_iris_client, case.get_case, case_id)
131 return SingleCaseResponse(success=True, message="Successfully fetched single case", case=result["data"])
132
133 +async def close_case(case_id: SingleCaseBody) -> ClosedCaseResponse:
134 + """
135 + Closes a single case from DFIR-IRIS based on the provided case ID.
136 +
137 + Args:
138 + case_id (SingleCaseBody): The ID of the case to close.
139 +
140 + Returns:
141 + ClosedCaseResponse: The response containing the closed case.
142 +
143 + Raises:
144 + Any exceptions raised during the execution of the function will be propagated.
145 + """
146 + logger.info(f"Closing case: {case_id}")
147 + dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
148 + case = Case(session=dfir_iris_client)
149 + result = await fetch_and_parse_data(dfir_iris_client, case.close_case, case_id)
150 + return ClosedCaseResponse(success=True, case=result["data"], message="Successfully closed case")
151 +
152 +
153 +async def reopen_case(case_id: SingleCaseBody) -> ReopenedCaseResponse:
154 + """
155 + Opens a single case from DFIR-IRIS based on the provided case ID.
156 +
157 + Args:
158 + case_id (SingleCaseBody): The ID of the case to open.
159 +
160 + Returns:
161 + OpenCaseResponse: The response containing the opened case.
162 +
163 + Raises:
164 + Any exceptions raised during the execution of the function will be propagated.
165 + """
166 + logger.info(f"Opening case: {case_id}")
167 + dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
168 + case = Case(session=dfir_iris_client)
169 + result = await fetch_and_parse_data(dfir_iris_client, case.reopen_case, case_id)
170 + logger.info(f"Successfully opened case: {result}")
171 + return ReopenedCaseResponse(success=True, case=result["data"], message="Successfully opened case")
172 +
173 +############# ! DELETE ACTIONS ! #############
174 async def purge_cases() -> PurgeCaseResponse:
175 """
176 Purges all cases from DFIR-IRIS.
backend/app/connectors/event_shipper/utils/universal.py new
+41
@@ -0,0 +1,41 @@
1 +from loguru import logger
2 +from typing import Optional
3 +
4 +import asyncgelf
5 +
6 +from app.connectors.utils import get_connector_info_from_db
7 +from app.db.db_session import AsyncSessionLocal
8 +from app.db.db_session import get_db_session
9 +
10 +class GelfLogger:
11 + def __init__(self, host: str, port: str, compress: Optional[bool] = False):
12 + self.host = host
13 + self.port = port
14 + self.compress = compress
15 +
16 + # async def tcp_handler(self, message):
17 + # handler = asyncgelf.GelfTcp(
18 + # host=self.host,
19 + # port=self.port,
20 + # compress=self.compress,
21 + # )
22 +
23 + # response = await handler.tcp_handler(message)
24 + # return response
25 + async def tcp_handler(self, message):
26 + if not isinstance(message, dict):
27 + message = message.to_dict()
28 +
29 + handler = asyncgelf.GelfTcp(
30 + host=self.host,
31 + port=self.port,
32 + compress=self.compress,
33 + )
34 +
35 + response = await handler.tcp_handler(message)
36 + return response
37 +
38 +async def create_gelf_logger():
39 + async with get_db_session() as session:
40 + connector_info = await get_connector_info_from_db("Event Shipper", session)
41 + return GelfLogger(host=connector_info['connector_url'], port=str(connector_info['connector_extra_data']))
backend/app/db/all_models.py
+3
@@ -11,3 +11,6 @@ from app.integrations.alert_creation_settings.models.alert_creation_settings imp
11 AlertCreationSettings,
12 )
13 from app.schedulers.models.scheduler import JobMetadata
14 +from app.integrations.models.customer_integration_settings import (
15 + CustomerIntegrations,
16 +)
backend/app/db/db_populate.py
+58 -33
@@ -7,43 +7,11 @@ from sqlalchemy.future import select
7
8 from app.auth.models.users import Role
9 from app.connectors.models import Connectors
10 +from app.integrations.models.customer_integration_settings import AvailableIntegrations
11
12 load_dotenv()
13
14
14 -# def load_connector_data(connector_name, connector_type, accepts_key, extra_data_key=None):
15 -# """
16 -# Load connector data from environment variables.
17 -
18 -# Args:
19 -# connector_name (str): The name of the connector.
20 -# connector_type (str): The type of the connector.
21 -# accepts_key (str): The type of key the connector accepts.
22 -# extra_data_key (str, optional): The key for extra data. Defaults to None.
23 -
24 -# Returns:
25 -# dict: A dictionary containing the connector data.
26 -# """
27 -# env_prefix = connector_name.upper().replace("-", "_").replace(" ", "_")
28 -# url = os.getenv(f"{env_prefix}_URL")
29 -# logger.info(f"Loading connector data for {connector_name} from environment variables with URL: {url}")
30 -# return {
31 -# "connector_name": connector_name,
32 -# "connector_type": connector_type,
33 -# "connector_url": os.getenv(f"{env_prefix}_URL"),
34 -# "connector_username": os.getenv(f"{env_prefix}_USERNAME"),
35 -# "connector_password": os.getenv(f"{env_prefix}_PASSWORD"),
36 -# "connector_api_key": os.getenv(f"{env_prefix}_API_KEY"),
37 -# "connector_description": os.getenv(f"{env_prefix}_DESCRIPTION", "Not specified."),
38 -# "connector_supports": os.getenv(f"{env_prefix}_SUPPORTS", "Not specified."),
39 -# "connector_configured": True,
40 -# "connector_verified": bool(os.getenv(f"{env_prefix}_VERIFIED", False)),
41 -# "connector_accepts_api_key": accepts_key == "api_key",
42 -# "connector_accepts_username_password": accepts_key == "username_password",
43 -# "connector_accepts_file": accepts_key == "file",
44 -# "connector_extra_data": os.getenv(extra_data_key) if extra_data_key else None,
45 -# }
46 -
15 def load_connector_data(connector_name, connector_type, accepts_key, description, extra_data_key=None):
16 """
17 Load connector data from environment variables.
@@ -100,6 +68,7 @@ def get_connectors_list():
68 ("Cortex", "3", "api_key", "Connection to Cortex. Make sure you have created an API key."),
69 ("Grafana", "3", "username_password", "Connection to Grafana. Make sure to use the an admin role user."),
70 ("Wazuh Worker Provisioning", "3", "api_key", "Connection to Wazuh Worker Provisioning. Make sure you have deployed the Wazuh Worker Provisioning Application provided by SOCFortress: https://github.com/socfortress/Customer-Provisioning-Worker"),
71 + ("Event Shipper", "3", "api_key", "Connection to Graylog GELF Input to receive events from integrations. Make sure you have created a GELF Input in Graylog.", "GELF_INPUT_PORT"),
72 # ... Add more connectors as needed ...
73 ]
74
@@ -161,3 +130,59 @@ async def add_roles_if_not_exist(session: AsyncSession) -> None:
130
131 await session.commit() # Commit the transaction
132 logger.info("Role check and addition completed.")
133 +
134 +def load_available_integrations_data(integration_name: str, description: str):
135 + """
136 + Load available integrations data from environment variables.
137 +
138 + Args:
139 + integration_name (str): The name of the integration.
140 + description (str): The description of the integration.
141 +
142 + Returns:
143 + dict: A dictionary containing the integration data.
144 + """
145 + logger.info(f"Loading available integrations data for {integration_name}.")
146 + return {
147 + "integration_name": integration_name,
148 + "description": description,
149 + }
150 +
151 +def get_available_integrations_list():
152 + """
153 + Get a list of available integrations.
154 +
155 + Returns:
156 + list: A list of available integrations data, where each item contains the integration name and description.
157 + """
158 + available_integrations = [
159 + ("Office Defender For Endpoint", "Integrate Office Defender For Endpoint with SOCFortress."),
160 + ("Mimecast", "Integrate Mimecast with SOCFortress."),
161 + # ... Add more available integrations as needed ...
162 + ]
163 +
164 + return [load_available_integrations_data(*available_integration) for available_integration in available_integrations]
165 +
166 +async def add_available_integrations_if_not_exist(session: AsyncSession):
167 + """
168 + Adds available integrations to the database if they do not already exist.
169 +
170 + Args:
171 + session (AsyncSession): The database session.
172 +
173 + Returns:
174 + None
175 + """
176 + available_integrations_list = get_available_integrations_list()
177 +
178 + for available_integration_data in available_integrations_list:
179 + query = select(AvailableIntegrations).where(AvailableIntegrations.integration_name == available_integration_data["integration_name"])
180 + result = await session.execute(query)
181 + existing_available_integration = result.scalars().first()
182 +
183 + if existing_available_integration is None:
184 + new_available_integration = AvailableIntegrations(**available_integration_data)
185 + session.add(new_available_integration)
186 + logger.info(f"Added new available integration: {available_integration_data['integration_name']}")
187 +
188 + await session.commit()
backend/app/db/db_setup.py
+16
@@ -9,6 +9,7 @@ from app.auth.services.universal import create_scheduler_user
9 from app.auth.services.universal import remove_scheduler_user
10 from app.db.db_populate import add_connectors_if_not_exist
11 from app.db.db_populate import add_roles_if_not_exist
12 +from app.db.db_populate import add_available_integrations_if_not_exist
13
14
15 async def create_tables(async_engine):
@@ -46,6 +47,21 @@ async def create_roles(async_engine):
47 async with session.begin(): # Start a transaction
48 await add_roles_if_not_exist(session)
49
50 +async def create_available_integrations(async_engine):
51 + """
52 + Creates available integrations in the database.
53 +
54 + Args:
55 + async_engine (AsyncEngine): The async engine used to connect to the database.
56 +
57 + Returns:
58 + None
59 + """
60 + logger.info("Creating available integrations")
61 + async with AsyncSession(async_engine) as session: # Create an AsyncSession, not just a connection
62 + async with session.begin(): # Start a transaction
63 + await add_available_integrations_if_not_exist(session)
64 +
65
66 async def ensure_admin_user(async_engine):
67 """
backend/app/integrations/log_shipper_test/routes/event_shipper.py new
+29
@@ -0,0 +1,29 @@
1 +from fastapi import APIRouter
2 +from loguru import logger
3 +
4 +from fastapi import HTTPException
5 +from fastapi import Depends
6 +import json
7 +from app.integrations.utils.event_shipper import event_shipper_test
8 +from app.integrations.utils.schema import EventShipperPayload
9 +
10 +log_shipper_test_router = APIRouter()
11 +
12 +
13 +@log_shipper_test_router.get("")
14 +async def event_shipper_test_route():
15 + """
16 + Test the log shipper.
17 + """
18 + message = EventShipperPayload(
19 + customer_code="test",
20 + integration="test",
21 + version="1.1",
22 + host="example.org",
23 + )
24 + try:
25 + return await event_shipper_test(message)
26 + except Exception as e:
27 + logger.error(f"Failed to send test message to log shipper: {e}")
28 + raise HTTPException(status_code=500, detail=f"Failed to send test message to log shipper: {e}")
29 +
backend/app/integrations/models/customer_integration_settings.py new
+54
@@ -0,0 +1,54 @@
1 +from typing import List, Optional
2 +from sqlmodel import Field, Relationship, SQLModel
3 +
4 +class AvailableIntegrations(SQLModel, table=True):
5 + __tablename__ = "available_integrations"
6 + id: Optional[int] = Field(default=None, primary_key=True)
7 + integration_name: str = Field(max_length=255, nullable=False)
8 + description: str = Field(max_length=1024)
9 +
10 +class CustomerIntegrations(SQLModel, table=True):
11 + __tablename__ = "customer_integrations"
12 + id: Optional[int] = Field(default=None, primary_key=True)
13 + customer_code: str = Field(max_length=50, nullable=False)
14 + customer_name: str = Field(max_length=255, nullable=False)
15 + # Relationships
16 + integration_subscriptions: List["IntegrationSubscription"] = Relationship(back_populates="customer_integrations")
17 +
18 +class IntegrationService(SQLModel, table=True):
19 + __tablename__ = "integration_services"
20 + id: Optional[int] = Field(default=None, primary_key=True)
21 + service_name: str = Field(max_length=255, nullable=False)
22 + auth_type: str = Field(max_length=50) # e.g., OAuth, API Key, etc.
23 + # Relationships
24 + integration_subscriptions: List["IntegrationSubscription"] = Relationship(back_populates="integration_service")
25 + configs: List["IntegrationConfig"] = Relationship(back_populates="integration_service")
26 +
27 +class IntegrationSubscription(SQLModel, table=True):
28 + __tablename__ = "integration_subscriptions"
29 + id: Optional[int] = Field(default=None, primary_key=True)
30 + customer_id: int = Field(default=None, foreign_key="customer_integrations.id")
31 + integration_service_id: int = Field(default=None, foreign_key="integration_services.id")
32 + # Relationships
33 + customer_integrations: "CustomerIntegrations" = Relationship(back_populates="integration_subscriptions")
34 + integration_service: "IntegrationService" = Relationship(back_populates="integration_subscriptions")
35 + integration_metadata: List["IntegrationMetadata"] = Relationship(back_populates="integration_subscription") # Moved here
36 +
37 +class IntegrationConfig(SQLModel, table=True):
38 + __tablename__ = "integration_configs"
39 + id: Optional[int] = Field(default=None, primary_key=True)
40 + integration_service_id: int = Field(default=None, foreign_key="integration_services.id")
41 + config_key: str = Field(max_length=255) # e.g., 'endpoint', 'port', etc.
42 + config_value: str = Field(max_length=1024) # e.g., 'https://api.service.com/v1'
43 + # Relationships
44 + integration_service: "IntegrationService" = Relationship(back_populates="configs")
45 +
46 +class IntegrationMetadata(SQLModel, table=True):
47 + __tablename__ = "integration_metadata"
48 + id: Optional[int] = Field(default=None, primary_key=True)
49 + subscription_id: int = Field(default=None, foreign_key="integration_subscriptions.id")
50 + metadata_key: str = Field(max_length=255) # e.g., 'credentials', 'rate_limit'
51 + metadata_value: str = Field(max_length=1024) # e.g., JSON/encrypted credentials
52 + # Relationships
53 + integration_subscription: "IntegrationSubscription" = Relationship(back_populates="integration_metadata") # Adjusted relationship
54 +
backend/app/integrations/routes.py new
+214
@@ -0,0 +1,214 @@
1 +from typing import List
2 +
3 +from fastapi import APIRouter
4 +from fastapi import Depends
5 +from fastapi import HTTPException
6 +from loguru import logger
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +from sqlalchemy.future import select
9 +from sqlalchemy.orm import joinedload
10 +
11 +from app.db.db_session import get_db
12 +from app.integrations.models.customer_integration_settings import (
13 + CustomerIntegrations,
14 +)
15 +from app.integrations.models.customer_integration_settings import (
16 + IntegrationService,
17 +)
18 +from app.integrations.models.customer_integration_settings import (
19 + IntegrationSubscription,
20 +)
21 +from app.integrations.models.customer_integration_settings import (
22 + IntegrationConfig,
23 +)
24 +from app.integrations.models.customer_integration_settings import (
25 + IntegrationMetadata, AvailableIntegrations
26 +)
27 +from app.db.universal_models import Customers
28 +from app.integrations.schema import AvailableIntegrationsResponse, CustomerIntegrationCreate, CreateIntegrationService, CreateIntegrationMetadata, CustomerIntegrationCreateResponse, CustomerIntegrationsResponse
29 +
30 +integration_settings_router = APIRouter()
31 +
32 +async def fetch_available_integrations(session: AsyncSession):
33 + """
34 + Fetches available integrations from the database.
35 +
36 + Args:
37 + session (AsyncSession): The database session.
38 +
39 + Returns:
40 + List[AvailableIntegrations]: A list of available integrations.
41 + """
42 + stmt = select(AvailableIntegrations)
43 + result = await session.execute(stmt)
44 + return result.scalars().all()
45 +
46 +async def validate_integration_name(integration_name: str, session: AsyncSession):
47 + """
48 + Validate if the integration name exists in available integrations.
49 + """
50 + available_integrations = await fetch_available_integrations(session)
51 + if integration_name not in [ai.integration_name for ai in available_integrations]:
52 + raise HTTPException(status_code=400, detail=f"Integration {integration_name} does not exist.")
53 +
54 +async def validate_customer_code(customer_code: str, session: AsyncSession):
55 + """
56 + Validate if the customer code exists in the customers table.
57 + """
58 + stmt = select(Customers).where(Customers.customer_code == customer_code)
59 + result = await session.execute(stmt)
60 + if result.scalars().first() is None:
61 + raise HTTPException(status_code=400, detail=f"Customer {customer_code} does not exist.")
62 +
63 +async def check_existing_customer_integration(customer_code: str, integration_name: str, session: AsyncSession):
64 + """
65 + Check if the customer integration already exists.
66 + """
67 + # Assuming IntegrationService has an 'integration_name' field or similar
68 + stmt = select(CustomerIntegrations).join(CustomerIntegrations.integration_subscriptions).join(IntegrationSubscription.integration_service).where(
69 + CustomerIntegrations.customer_code == customer_code,
70 + IntegrationService.service_name == integration_name
71 + )
72 + result = await session.execute(stmt)
73 + if result.scalars().first() is not None:
74 + raise HTTPException(status_code=400, detail=f"Customer integration {customer_code} {integration_name} already exists.")
75 +
76 +async def create_integration_service(integration_name: str, settings: CreateIntegrationService, session: AsyncSession) -> IntegrationService:
77 + """
78 + Create or fetch IntegrationService instance with custom configuration.
79 + """
80 + integration_service = IntegrationService(
81 + service_name=integration_name,
82 + auth_type=settings.auth_type,
83 + configs=[IntegrationConfig(config_key=settings.config_key, config_value=settings.config_value)]
84 + )
85 + session.add(integration_service)
86 + await session.flush()
87 + return integration_service
88 +
89 +async def create_customer_integrations(customer_code: str, customer_name: str, session: AsyncSession) -> CustomerIntegrations:
90 + """
91 + Create CustomerIntegrations instance.
92 + """
93 + customer_integrations = CustomerIntegrations(
94 + customer_code=customer_code,
95 + customer_name=customer_name,
96 + )
97 + session.add(customer_integrations)
98 + await session.flush()
99 + return customer_integrations
100 +
101 +async def create_integration_subscription(customer_integrations: CustomerIntegrations, integration_service: IntegrationService, integration_metadata: CreateIntegrationMetadata, session: AsyncSession):
102 + """
103 + Create IntegrationSubscription instance.
104 + """
105 + new_integration_subscription = IntegrationSubscription(
106 + customer_integrations=customer_integrations,
107 + integration_service=integration_service,
108 + integration_metadata=[IntegrationMetadata(metadata_key=integration_metadata.metadata_key, metadata_value=integration_metadata.metadata_value)]
109 + )
110 + session.add(new_integration_subscription)
111 + await session.commit()
112 +
113 +@integration_settings_router.get(
114 + "/available_integrations",
115 + response_model=AvailableIntegrationsResponse,
116 + description="Get a list of available integrations."
117 +)
118 +async def get_available_integrations(
119 + session: AsyncSession = Depends(get_db),
120 +):
121 + """
122 + Endpoint to get a list of available integrations.
123 + """
124 + available_integrations = await fetch_available_integrations(session)
125 + return AvailableIntegrationsResponse(
126 + available_integrations=available_integrations,
127 + message="Available integrations successfully retrieved.",
128 + success=True,
129 + )
130 +
131 +
132 +@integration_settings_router.get(
133 + "/customer_integrations",
134 + response_model=CustomerIntegrationsResponse,
135 + description="Get a list of customer integrations."
136 +)
137 +async def get_customer_integrations(
138 + session: AsyncSession = Depends(get_db),
139 +):
140 + """
141 + Endpoint to get a list of customer integrations.
142 + """
143 + stmt = (
144 + select(CustomerIntegrations)
145 + .options(
146 + joinedload(CustomerIntegrations.integration_subscriptions)
147 + .joinedload(IntegrationSubscription.integration_service),
148 + joinedload(CustomerIntegrations.integration_subscriptions)
149 + .subqueryload(IntegrationSubscription.integration_metadata) # Load IntegrationMetadata
150 + )
151 + )
152 + result = await session.execute(stmt)
153 + customer_integrations = result.scalars().unique().all()
154 + return CustomerIntegrationsResponse(
155 + available_integrations=customer_integrations,
156 + message="Customer integrations successfully retrieved.",
157 + success=True,
158 + )
159 +
160 +@integration_settings_router.get(
161 + "/customer_integrations/{customer_code}",
162 + response_model=CustomerIntegrationsResponse,
163 + description="Get a list of customer integrations for a specific customer."
164 +)
165 +async def get_customer_integrations_by_customer_code(
166 + customer_code: str,
167 + session: AsyncSession = Depends(get_db),
168 +):
169 + """
170 + Endpoint to get a list of customer integrations for a specific customer.
171 + """
172 + stmt = (
173 + select(CustomerIntegrations)
174 + .options(
175 + joinedload(CustomerIntegrations.integration_subscriptions)
176 + .joinedload(IntegrationSubscription.integration_service),
177 + joinedload(CustomerIntegrations.integration_subscriptions)
178 + .subqueryload(IntegrationSubscription.integration_metadata) # Load IntegrationMetadata
179 + )
180 + .where(CustomerIntegrations.customer_code == customer_code)
181 + )
182 + result = await session.execute(stmt)
183 + customer_integrations = result.scalars().unique().all()
184 + return CustomerIntegrationsResponse(
185 + available_integrations=customer_integrations,
186 + message="Customer integrations successfully retrieved.",
187 + success=True,
188 + )
189 +
190 +@integration_settings_router.post(
191 + "/create_integration",
192 + response_model=CustomerIntegrationCreateResponse,
193 + description="Create a new customer integration."
194 +)
195 +async def create_integration(
196 + customer_integration_create: CustomerIntegrationCreate,
197 + session: AsyncSession = Depends(get_db),
198 +):
199 + """
200 + Endpoint to create a new customer integration.
201 + """
202 +
203 + await validate_integration_name(customer_integration_create.integration_name, session)
204 + await validate_customer_code(customer_integration_create.customer_code, session)
205 + await check_existing_customer_integration(customer_integration_create.customer_code, customer_integration_create.integration_name, session)
206 +
207 + integration_service = await create_integration_service(customer_integration_create.integration_name, settings=customer_integration_create.integration_details, session=session)
208 + customer_integrations = await create_customer_integrations(customer_integration_create.customer_code, customer_integration_create.customer_name, session)
209 + await create_integration_subscription(customer_integrations, integration_service, integration_metadata=customer_integration_create.integration_metadata, session=session)
210 +
211 + return CustomerIntegrationCreateResponse(
212 + message=f"Customer integration {customer_integration_create.customer_code} {customer_integration_create.integration_name} successfully created.",
213 + success=True,
214 + )
backend/app/integrations/schema.py new
+142
@@ -0,0 +1,142 @@
1 +from pydantic import BaseModel
2 +from pydantic import Field
3 +from app.integrations.models.customer_integration_settings import AvailableIntegrations
4 +from typing import List
5 +
6 +class AvailableIntegrationsResponse(BaseModel):
7 + """
8 + The response model for the /integrations/available_integrations endpoint.
9 + """
10 + available_integrations: list[AvailableIntegrations] = Field(
11 + ...,
12 + description="The available integrations.",
13 + )
14 + message: str = Field(
15 + ...,
16 + description="The message.",
17 + )
18 + success: bool = Field(
19 + ...,
20 + description="The success status.",
21 + )
22 +
23 +class CreateIntegrationService(BaseModel):
24 + auth_type: str = Field(
25 + ...,
26 + description="The authentication type.",
27 + examples=["OAuth"],
28 + )
29 + config_key: str = Field(
30 + ...,
31 + description="The configuration key.",
32 + examples=["endpoint"],
33 + )
34 + config_value: str = Field(
35 + ...,
36 + description="The configuration value.",
37 + examples=["https://api.mimecast.com"],
38 + )
39 +
40 +class CreateIntegrationMetadata(BaseModel):
41 + metadata_key: str = Field(
42 + ...,
43 + description="The metadata key.",
44 + examples=["username"],
45 + )
46 + metadata_value: str = Field(
47 + ...,
48 + description="The metadata value.",
49 + examples=["test-user"],
50 + )
51 +
52 +class CustomerIntegrationCreate(BaseModel):
53 + customer_code: str = Field(
54 + ...,
55 + description="The customer code.",
56 + examples=["00002"],
57 + )
58 + customer_name: str = Field(
59 + ...,
60 + description="The customer name.",
61 + examples=["SOCFortress"],
62 + )
63 + integration_name: str = Field(
64 + ...,
65 + description="The integration name.",
66 + examples=["mimecast"],
67 + )
68 + integration_details: CreateIntegrationService = Field(
69 + ...,
70 + description="The integration service.",
71 + )
72 + integration_metadata: CreateIntegrationMetadata = Field(
73 + ...,
74 + description="The integration metadata.",
75 + )
76 +
77 +class CustomerIntegrationCreateResponse(BaseModel):
78 + message: str = Field(
79 + ...,
80 + description="The message.",
81 + )
82 + success: bool = Field(
83 + ...,
84 + description="The success status.",
85 + )
86 +
87 +# class IntegrationConfig(BaseModel):
88 +# config_id: int
89 +# config_value: str
90 +# config_key: str
91 +
92 +# class IntegrationService(BaseModel):
93 +# auth_type: str
94 +# service_name: str
95 +# id: int
96 +
97 +# class IntegrationSubscription(BaseModel):
98 +# id: int
99 +# customer_id: int
100 +# integration_service_id: int
101 +# integration_service: IntegrationService
102 +# integration_config: IntegrationConfig
103 +
104 +# class CustomerIntegrations(BaseModel):
105 +# customer_code: str
106 +# id: int
107 +# customer_name: str
108 +# integration_subscriptions: List[IntegrationSubscription]
109 +
110 +# class CustomerIntegrationsResponse(BaseModel):
111 +# available_integrations: List[CustomerIntegrations]
112 +# message: str
113 +# success: bool
114 +
115 +class IntegrationMetadata(BaseModel):
116 + id: int
117 + metadata_value: str
118 + metadata_key: str
119 + subscription_id: int
120 +
121 +class IntegrationService(BaseModel):
122 + auth_type: str
123 + service_name: str
124 + id: int
125 +
126 +class IntegrationSubscription(BaseModel):
127 + id: int
128 + customer_id: int
129 + integration_service_id: int
130 + integration_service: IntegrationService
131 + integration_metadata: List[IntegrationMetadata] # Changed from IntegrationConfig
132 +
133 +class CustomerIntegrations(BaseModel):
134 + customer_code: str
135 + id: int
136 + customer_name: str
137 + integration_subscriptions: List[IntegrationSubscription]
138 +
139 +class CustomerIntegrationsResponse(BaseModel):
140 + available_integrations: List[CustomerIntegrations]
141 + message: str
142 + success: bool
backend/app/integrations/utils/event_shipper.py new
+29
@@ -0,0 +1,29 @@
1 +from loguru import logger
2 +from app.integrations.utils.schema import EventShipperPayload, EventShipperPayloadResponse
3 +
4 +from app.connectors.event_shipper.utils.universal import create_gelf_logger
5 +from fastapi import HTTPException
6 +
7 +
8 +async def get_gelf_logger():
9 + try:
10 + gelf_logger = await create_gelf_logger()
11 + return gelf_logger
12 + except Exception as e:
13 + logger.error(f"Failed to initialize GelfLogger: {e}")
14 + raise HTTPException(status_code=500, detail=f"Failed to initialize GelfLogger: {e}")
15 +
16 +async def event_shipper_test(message: EventShipperPayload) -> EventShipperPayloadResponse:
17 + """
18 + Test the log shipper.
19 + """
20 + gelf_logger = await get_gelf_logger()
21 +
22 + try:
23 + await gelf_logger.tcp_handler(message=message)
24 + except Exception as e:
25 + logger.error(f"Failed to send test message to log shipper: {e}")
26 + raise HTTPException(status_code=500, detail=f"Failed to send test message to log shipper: {e}")
27 +
28 + return EventShipperPayloadResponse(success=True, message="Successfully sent test message to log shipper.")
29 +
backend/app/integrations/utils/schema.py
+29
@@ -195,3 +195,32 @@ class ShufflePayload(BaseModel):
195
196 def to_dict(self):
197 return self.dict(exclude_none=True)
198 +
199 +
200 +######### ! SEND TO EVENT SHIPPER ! #########
201 +class EventShipperPayload(BaseModel):
202 + integration: str = Field(
203 + ...,
204 + description="The integration name.",
205 + examples="mimecast",
206 + )
207 + customer_code: str = Field(
208 + ...,
209 + description="The customer code.",
210 + examples="socfortress",
211 + )
212 + class Config:
213 + extra = Extra.allow
214 +
215 + def to_dict(self):
216 + return self.dict(exclude_none=True)
217 +
218 +class EventShipperPayloadResponse(BaseModel):
219 + message: str
220 + success: bool
221 + data: Optional[dict] = Field(
222 + None,
223 + description="The Event Shipper response data.",
224 + )
225 +
226 +
backend/app/routers/integrations.py new
+10
@@ -0,0 +1,10 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.routes import integration_settings_router
4 +
5 +
6 +# Instantiate the APIRouter
7 +router = APIRouter()
8 +
9 +# Include the Inntegration Settings related routes
10 +router.include_router(integration_settings_router, prefix="/integrations", tags=["Integration Settings"])
backend/app/routers/log_shipper_test.py new
+9
@@ -0,0 +1,9 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.log_shipper_test.routes.event_shipper import log_shipper_test_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the Log Shipper Test related routes
9 +router.include_router(log_shipper_test_router, prefix="/log_shipper_test", tags=["Log Shipper Test"])
backend/copilot.py
+6
@@ -11,6 +11,7 @@ from app.auth.utils import AuthHandler
11 from app.db.db_session import async_engine
12 from app.db.db_setup import create_roles
13 from app.db.db_setup import create_tables
14 +from app.db.db_setup import create_available_integrations
15 from app.db.db_setup import ensure_admin_user
16 from app.db.db_setup import ensure_scheduler_user
17 from app.db.db_setup import ensure_scheduler_user_removed
@@ -41,6 +42,8 @@ from app.routers import threat_intel
42 from app.routers import velociraptor
43 from app.routers import wazuh_indexer
44 from app.routers import wazuh_manager
45 +from app.routers import log_shipper_test
46 +from app.routers import integrations
47 from app.schedulers.scheduler import init_scheduler
48
49 auth_handler = AuthHandler()
@@ -95,6 +98,8 @@ app.include_router(threat_intel.router)
98 app.include_router(ask_socfortress.router)
99 app.include_router(alert_creation.router)
100 app.include_router(alert_creation_settings.router)
101 +app.include_router(log_shipper_test.router)
102 +app.include_router(integrations.router)
103
104
105 @app.on_event("startup")
@@ -102,6 +107,7 @@ async def init_db():
107 # create_tables(engine)
108 await create_tables(async_engine)
109 await create_roles(async_engine)
110 + await create_available_integrations(async_engine)
111 await ensure_admin_user(async_engine)
112 await ensure_scheduler_user(async_engine)
113
backend/requirements.txt
+1
@@ -12,6 +12,7 @@ APScheduler==3.10.4
12 arrow==1.3.0
13 async-timeout==4.0.3
14 attrs==23.1.0
15 +asyncgelf==1.1.0
16 bcrypt==4.0.1
17 billiard==4.1.0
18 blueprint==3.4.2
package-lock.json
+214 -209
@@ -32,7 +32,7 @@
32 "pinia-plugin-persistedstate": "^3.2.1",
33 "secure-ls": "^1.2.6",
34 "validator": "^13.11.0",
35 - "vue": "^3.4.7",
35 + "vue": "^3.4.11",
36 "vue-advanced-cropper": "^2.8.8",
37 "vue-highlight-words": "^3.0.1",
38 "vue-i18n": "^9.9.0",
@@ -50,7 +50,7 @@
50 "@types/inquirer": "^9.0.7",
51 "@types/jsdom": "^21.1.6",
52 "@types/lodash": "^4.14.202",
53 - "@types/node": "^20.10.8",
53 + "@types/node": "^20.11.0",
54 "@types/validator": "^13.11.8",
55 "@vitejs/plugin-vue": "^5.0.3",
56 "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -62,14 +62,14 @@
62 "cypress": "^13.6.2",
63 "eslint": "^8.56.0",
64 "eslint-plugin-cypress": "^2.15.1",
65 - "eslint-plugin-vue": "^9.19.2",
65 + "eslint-plugin-vue": "^9.20.0",
66 "fs-extra": "^11.2.0",
67 "jsdom": "^23.2.0",
68 "json5": "^2.2.3",
69 "npm-run-all": "^4.1.5",
70 "picocolors": "^1.0.0",
71 "postcss": "^8.4.33",
72 - "prettier": "^3.1.1",
72 + "prettier": "^3.2.1",
73 "sass": "^1.69.7",
74 "start-server-and-test": "^2.0.3",
75 "tailwind-config-viewer": "^1.7.3",
@@ -82,7 +82,7 @@
82 "vite-bundle-analyzer": "^0.6.1",
83 "vite-bundle-visualizer": "^1.0.0",
84 "vite-svg-loader": "^5.1.0",
85 - "vitest": "^1.1.3",
85 + "vitest": "^1.2.0",
86 "vue-tsc": "^1.8.27"
87 },
88 "engines": {
@@ -156,9 +156,9 @@
156 }
157 },
158 "node_modules/@asamuzakjp/dom-selector": {
159 - "version": "2.0.1",
160 - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.1.tgz",
161 - "integrity": "sha512-QJAJffmCiymkv6YyQ7voyQb5caCth6jzZsQncYCpHXrJ7RqdYG5y43+is8mnFcYubdOkr7cn1+na9BdFMxqw7w==",
159 + "version": "2.0.2",
160 + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz",
161 + "integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==",
162 "dev": true,
163 "dependencies": {
164 "bidi-js": "^1.0.3",
@@ -1251,13 +1251,13 @@
1251 }
1252 },
1253 "node_modules/@humanwhocodes/config-array": {
1254 - "version": "0.11.13",
1255 - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
1256 - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
1254 + "version": "0.11.14",
1255 + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
1256 + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
1257 "dev": true,
1258 "dependencies": {
1259 - "@humanwhocodes/object-schema": "^2.0.1",
1260 - "debug": "^4.1.1",
1259 + "@humanwhocodes/object-schema": "^2.0.2",
1260 + "debug": "^4.3.1",
1261 "minimatch": "^3.0.5"
1262 },
1263 "engines": {
@@ -1300,9 +1300,9 @@
1300 }
1301 },
1302 "node_modules/@humanwhocodes/object-schema": {
1303 - "version": "2.0.1",
1304 - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
1305 - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
1303 + "version": "2.0.2",
1304 + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
1305 + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
1306 "dev": true
1307 },
1308 "node_modules/@iconify/types": {
@@ -1484,9 +1484,9 @@
1484 "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
1485 },
1486 "node_modules/@jridgewell/trace-mapping": {
1487 - "version": "0.3.20",
1488 - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
1489 - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
1487 + "version": "0.3.21",
1488 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz",
1489 + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==",
1490 "dev": true,
1491 "dependencies": {
1492 "@jridgewell/resolve-uri": "^3.1.0",
@@ -1575,9 +1575,9 @@
1575 }
1576 },
1577 "node_modules/@npmcli/config": {
1578 - "version": "8.0.3",
1579 - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-8.0.3.tgz",
1580 - "integrity": "sha512-rqRX7/UORvm2YRImY67kyfwD9rpi5+KXXb1j/cpTUKRcUqvpJ9/PMMc7Vv57JVqmrFj8siBBFEmXI3Gg7/TonQ==",
1578 + "version": "8.1.0",
1579 + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-8.1.0.tgz",
1580 + "integrity": "sha512-61LNEybTFaa9Z/f8y6X9s2Blc75aijZK67LxqC5xicBcfkw8M/88nYrRXGXxAUKm6GRlxTZ216dp1UK2+TbaYw==",
1581 "dev": true,
1582 "dependencies": {
1583 "@npmcli/map-workspaces": "^3.0.2",
@@ -1971,9 +1971,9 @@
1971 }
1972 },
1973 "node_modules/@rollup/rollup-android-arm-eabi": {
1974 - "version": "4.9.4",
1975 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.4.tgz",
1976 - "integrity": "sha512-ub/SN3yWqIv5CWiAZPHVS1DloyZsJbtXmX4HxUTIpS0BHm9pW5iYBo2mIZi+hE3AeiTzHz33blwSnhdUo+9NpA==",
1974 + "version": "4.9.5",
1975 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz",
1976 + "integrity": "sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==",
1977 "cpu": [
1978 "arm"
1979 ],
@@ -1984,9 +1984,9 @@
1984 ]
1985 },
1986 "node_modules/@rollup/rollup-android-arm64": {
1987 - "version": "4.9.4",
1988 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.4.tgz",
1989 - "integrity": "sha512-ehcBrOR5XTl0W0t2WxfTyHCR/3Cq2jfb+I4W+Ch8Y9b5G+vbAecVv0Fx/J1QKktOrgUYsIKxWAKgIpvw56IFNA==",
1987 + "version": "4.9.5",
1988 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz",
1989 + "integrity": "sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==",
1990 "cpu": [
1991 "arm64"
1992 ],
@@ -1997,9 +1997,9 @@
1997 ]
1998 },
1999 "node_modules/@rollup/rollup-darwin-arm64": {
2000 - "version": "4.9.4",
2001 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.4.tgz",
2002 - "integrity": "sha512-1fzh1lWExwSTWy8vJPnNbNM02WZDS8AW3McEOb7wW+nPChLKf3WG2aG7fhaUmfX5FKw9zhsF5+MBwArGyNM7NA==",
2000 + "version": "4.9.5",
2001 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz",
2002 + "integrity": "sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==",
2003 "cpu": [
2004 "arm64"
2005 ],
@@ -2010,9 +2010,9 @@
2010 ]
2011 },
2012 "node_modules/@rollup/rollup-darwin-x64": {
2013 - "version": "4.9.4",
2014 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.4.tgz",
2015 - "integrity": "sha512-Gc6cukkF38RcYQ6uPdiXi70JB0f29CwcQ7+r4QpfNpQFVHXRd0DfWFidoGxjSx1DwOETM97JPz1RXL5ISSB0pA==",
2013 + "version": "4.9.5",
2014 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz",
2015 + "integrity": "sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==",
2016 "cpu": [
2017 "x64"
2018 ],
@@ -2023,9 +2023,9 @@
2023 ]
2024 },
2025 "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
2026 - "version": "4.9.4",
2027 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.4.tgz",
2028 - "integrity": "sha512-g21RTeFzoTl8GxosHbnQZ0/JkuFIB13C3T7Y0HtKzOXmoHhewLbVTFBQZu+z5m9STH6FZ7L/oPgU4Nm5ErN2fw==",
2026 + "version": "4.9.5",
2027 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz",
2028 + "integrity": "sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==",
2029 "cpu": [
2030 "arm"
2031 ],
@@ -2036,9 +2036,9 @@
2036 ]
2037 },
2038 "node_modules/@rollup/rollup-linux-arm64-gnu": {
2039 - "version": "4.9.4",
2040 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.4.tgz",
2041 - "integrity": "sha512-TVYVWD/SYwWzGGnbfTkrNpdE4HON46orgMNHCivlXmlsSGQOx/OHHYiQcMIOx38/GWgwr/po2LBn7wypkWw/Mg==",
2039 + "version": "4.9.5",
2040 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz",
2041 + "integrity": "sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==",
2042 "cpu": [
2043 "arm64"
2044 ],
@@ -2049,9 +2049,9 @@
2049 ]
2050 },
2051 "node_modules/@rollup/rollup-linux-arm64-musl": {
2052 - "version": "4.9.4",
2053 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.4.tgz",
2054 - "integrity": "sha512-XcKvuendwizYYhFxpvQ3xVpzje2HHImzg33wL9zvxtj77HvPStbSGI9czrdbfrf8DGMcNNReH9pVZv8qejAQ5A==",
2052 + "version": "4.9.5",
2053 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz",
2054 + "integrity": "sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==",
2055 "cpu": [
2056 "arm64"
2057 ],
@@ -2062,9 +2062,9 @@
2062 ]
2063 },
2064 "node_modules/@rollup/rollup-linux-riscv64-gnu": {
2065 - "version": "4.9.4",
2066 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.4.tgz",
2067 - "integrity": "sha512-LFHS/8Q+I9YA0yVETyjonMJ3UA+DczeBd/MqNEzsGSTdNvSJa1OJZcSH8GiXLvcizgp9AlHs2walqRcqzjOi3A==",
2065 + "version": "4.9.5",
2066 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz",
2067 + "integrity": "sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==",
2068 "cpu": [
2069 "riscv64"
2070 ],
@@ -2075,9 +2075,9 @@
2075 ]
2076 },
2077 "node_modules/@rollup/rollup-linux-x64-gnu": {
2078 - "version": "4.9.4",
2079 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.4.tgz",
2080 - "integrity": "sha512-dIYgo+j1+yfy81i0YVU5KnQrIJZE8ERomx17ReU4GREjGtDW4X+nvkBak2xAUpyqLs4eleDSj3RrV72fQos7zw==",
2078 + "version": "4.9.5",
2079 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz",
2080 + "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==",
2081 "cpu": [
2082 "x64"
2083 ],
@@ -2088,9 +2088,9 @@
2088 ]
2089 },
2090 "node_modules/@rollup/rollup-linux-x64-musl": {
2091 - "version": "4.9.4",
2092 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.4.tgz",
2093 - "integrity": "sha512-RoaYxjdHQ5TPjaPrLsfKqR3pakMr3JGqZ+jZM0zP2IkDtsGa4CqYaWSfQmZVgFUCgLrTnzX+cnHS3nfl+kB6ZQ==",
2091 + "version": "4.9.5",
2092 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz",
2093 + "integrity": "sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==",
2094 "cpu": [
2095 "x64"
2096 ],
@@ -2101,9 +2101,9 @@
2101 ]
2102 },
2103 "node_modules/@rollup/rollup-win32-arm64-msvc": {
2104 - "version": "4.9.4",
2105 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.4.tgz",
2106 - "integrity": "sha512-T8Q3XHV+Jjf5e49B4EAaLKV74BbX7/qYBRQ8Wop/+TyyU0k+vSjiLVSHNWdVd1goMjZcbhDmYZUYW5RFqkBNHQ==",
2104 + "version": "4.9.5",
2105 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz",
2106 + "integrity": "sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==",
2107 "cpu": [
2108 "arm64"
2109 ],
@@ -2114,9 +2114,9 @@
2114 ]
2115 },
2116 "node_modules/@rollup/rollup-win32-ia32-msvc": {
2117 - "version": "4.9.4",
2118 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.4.tgz",
2119 - "integrity": "sha512-z+JQ7JirDUHAsMecVydnBPWLwJjbppU+7LZjffGf+Jvrxq+dVjIE7By163Sc9DKc3ADSU50qPVw0KonBS+a+HQ==",
2117 + "version": "4.9.5",
2118 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz",
2119 + "integrity": "sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==",
2120 "cpu": [
2121 "ia32"
2122 ],
@@ -2127,9 +2127,9 @@
2127 ]
2128 },
2129 "node_modules/@rollup/rollup-win32-x64-msvc": {
2130 - "version": "4.9.4",
2131 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.4.tgz",
2132 - "integrity": "sha512-LfdGXCV9rdEify1oxlN9eamvDSjv9md9ZVMAbNHA87xqIfFCxImxan9qZ8+Un54iK2nnqPlbnSi4R54ONtbWBw==",
2130 + "version": "4.9.5",
2131 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz",
2132 + "integrity": "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==",
2133 "cpu": [
2134 "x64"
2135 ],
@@ -2352,9 +2352,9 @@
2352 }
2353 },
2354 "node_modules/@types/node": {
2355 - "version": "20.10.8",
2356 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
2357 - "integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
2355 + "version": "20.11.0",
2356 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
2357 + "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
2358 "dev": true,
2359 "dependencies": {
2360 "undici-types": "~5.26.4"
@@ -2741,13 +2741,13 @@
2741 }
2742 },
2743 "node_modules/@vitest/expect": {
2744 - "version": "1.1.3",
2745 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.3.tgz",
2746 - "integrity": "sha512-MnJqsKc1Ko04lksF9XoRJza0bGGwTtqfbyrsYv5on4rcEkdo+QgUdITenBQBUltKzdxW7K3rWh+nXRULwsdaVg==",
2744 + "version": "1.2.0",
2745 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.0.tgz",
2746 + "integrity": "sha512-H+2bHzhyvgp32o7Pgj2h9RTHN0pgYaoi26Oo3mE+dCi1PAqV31kIIVfTbqMO3Bvshd5mIrJLc73EwSRrbol9Lw==",
2747 "dev": true,
2748 "dependencies": {
2749 - "@vitest/spy": "1.1.3",
2750 - "@vitest/utils": "1.1.3",
2749 + "@vitest/spy": "1.2.0",
2750 + "@vitest/utils": "1.2.0",
2751 "chai": "^4.3.10"
2752 },
2753 "funding": {
@@ -2755,12 +2755,12 @@
2755 }
2756 },
2757 "node_modules/@vitest/runner": {
2758 - "version": "1.1.3",
2759 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.1.3.tgz",
2760 - "integrity": "sha512-Va2XbWMnhSdDEh/OFxyUltgQuuDRxnarK1hW5QNN4URpQrqq6jtt8cfww/pQQ4i0LjoYxh/3bYWvDFlR9tU73g==",
2758 + "version": "1.2.0",
2759 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.0.tgz",
2760 + "integrity": "sha512-vaJkDoQaNUTroT70OhM0NPznP7H3WyRwt4LvGwCVYs/llLaqhoSLnlIhUClZpbF5RgAee29KRcNz0FEhYcgxqA==",
2761 "dev": true,
2762 "dependencies": {
2763 - "@vitest/utils": "1.1.3",
2763 + "@vitest/utils": "1.2.0",
2764 "p-limit": "^5.0.0",
2765 "pathe": "^1.1.1"
2766 },
@@ -2796,9 +2796,9 @@
2796 }
2797 },
2798 "node_modules/@vitest/snapshot": {
2799 - "version": "1.1.3",
2800 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.1.3.tgz",
2801 - "integrity": "sha512-U0r8pRXsLAdxSVAyGNcqOU2H3Z4Y2dAAGGelL50O0QRMdi1WWeYHdrH/QWpN1e8juWfVKsb8B+pyJwTC+4Gy9w==",
2799 + "version": "1.2.0",
2800 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.0.tgz",
2801 + "integrity": "sha512-P33EE7TrVgB3HDLllrjK/GG6WSnmUtWohbwcQqmm7TAk9AVHpdgf7M3F3qRHKm6vhr7x3eGIln7VH052Smo6Kw==",
2802 "dev": true,
2803 "dependencies": {
2804 "magic-string": "^0.30.5",
@@ -2810,9 +2810,9 @@
2810 }
2811 },
2812 "node_modules/@vitest/spy": {
2813 - "version": "1.1.3",
2814 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.1.3.tgz",
2815 - "integrity": "sha512-Ec0qWyGS5LhATFQtldvChPTAHv08yHIOZfiNcjwRQbFPHpkih0md9KAbs7TfeIfL7OFKoe7B/6ukBTqByubXkQ==",
2813 + "version": "1.2.0",
2814 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.0.tgz",
2815 + "integrity": "sha512-MNxSAfxUaCeowqyyGwC293yZgk7cECZU9wGb8N1pYQ0yOn/SIr8t0l9XnGRdQZvNV/ZHBYu6GO/W3tj5K3VN1Q==",
2816 "dev": true,
2817 "dependencies": {
2818 "tinyspy": "^2.2.0"
@@ -2822,9 +2822,9 @@
2822 }
2823 },
2824 "node_modules/@vitest/utils": {
2825 - "version": "1.1.3",
2826 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.1.3.tgz",
2827 - "integrity": "sha512-Dyt3UMcdElTll2H75vhxfpZu03uFpXRCHxWnzcrFjZxT1kTbq8ALUYIeBgGolo1gldVdI0YSlQRacsqxTwNqwg==",
2825 + "version": "1.2.0",
2826 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.0.tgz",
2827 + "integrity": "sha512-FyD5bpugsXlwVpTcGLDf3wSPYy8g541fQt14qtzo8mJ4LdEpDKZ9mQy2+qdJm2TZRpjY5JLXihXCgIxiRJgi5g==",
2828 "dev": true,
2829 "dependencies": {
2830 "diff-sequences": "^29.6.3",
@@ -2874,62 +2874,67 @@
2874 }
2875 },
2876 "node_modules/@vue/babel-helper-vue-transform-on": {
2877 - "version": "1.1.5",
2878 - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.1.5.tgz",
2879 - "integrity": "sha512-SgUymFpMoAyWeYWLAY+MkCK3QEROsiUnfaw5zxOVD/M64KQs8D/4oK6Q5omVA2hnvEOE0SCkH2TZxs/jnnUj7w==",
2877 + "version": "1.1.6",
2878 + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.1.6.tgz",
2879 + "integrity": "sha512-XxM2tZHjYHTd9yiKHHt7fKCN0e2BK2z78UxU5rpjH3YCstEV/tcrW29CaOdrxIdeD0c/9mHHebvXWwDxlphjKA==",
2880 "dev": true
2881 },
2882 "node_modules/@vue/babel-plugin-jsx": {
2883 - "version": "1.1.5",
2884 - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.5.tgz",
2885 - "integrity": "sha512-nKs1/Bg9U1n3qSWnsHhCVQtAzI6aQXqua8j/bZrau8ywT1ilXQbK4FwEJGmU8fV7tcpuFvWmmN7TMmV1OBma1g==",
2883 + "version": "1.1.6",
2884 + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.6.tgz",
2885 + "integrity": "sha512-s2pK8Wwg0LiR25lyCKWGJePt8aXF0DsXOmTHYJnlKNdT3yTKfdvkKmsWjaHBctFvwWmetedObrAoINc9BeYZlA==",
2886 "dev": true,
2887 "dependencies": {
2888 - "@babel/helper-module-imports": "^7.22.5",
2889 - "@babel/plugin-syntax-jsx": "^7.22.5",
2890 - "@babel/template": "^7.22.5",
2891 - "@babel/traverse": "^7.22.5",
2892 - "@babel/types": "^7.22.5",
2893 - "@vue/babel-helper-vue-transform-on": "^1.1.5",
2888 + "@babel/helper-module-imports": "^7.22.15",
2889 + "@babel/plugin-syntax-jsx": "^7.23.3",
2890 + "@babel/template": "^7.22.15",
2891 + "@babel/traverse": "^7.23.7",
2892 + "@babel/types": "^7.23.6",
2893 + "@vue/babel-helper-vue-transform-on": "^1.1.6",
2894 "camelcase": "^6.3.0",
2895 "html-tags": "^3.3.1",
2896 "svg-tags": "^1.0.0"
2897 },
2898 "peerDependencies": {
2899 "@babel/core": "^7.0.0-0"
2900 + },
2901 + "peerDependenciesMeta": {
2902 + "@babel/core": {
2903 + "optional": true
2904 + }
2905 }
2906 },
2907 "node_modules/@vue/compiler-core": {
2903 - "version": "3.4.7",
2904 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.7.tgz",
2905 - "integrity": "sha512-hhCaE3pTMrlIJK7M/o3Xf7HV8+JoNTGOQ/coWS+V+pH6QFFyqtoXqQzpqsNp7UK17xYKua/MBiKj4e1vgZOBYw==",
2908 + "version": "3.4.11",
2909 + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.11.tgz",
2910 + "integrity": "sha512-xFD+p14L4J0DkzHMdgLiQBU5g861fuOTzag30GsfPXBpghLZOvmd22lKiBMTRRpQRpp7qxPnBlFMoeiGMM4MBg==",
2911 "dependencies": {
2912 "@babel/parser": "^7.23.6",
2908 - "@vue/shared": "3.4.7",
2913 + "@vue/shared": "3.4.11",
2914 "entities": "^4.5.0",
2915 "estree-walker": "^2.0.2",
2916 "source-map-js": "^1.0.2"
2917 }
2918 },
2919 "node_modules/@vue/compiler-dom": {
2915 - "version": "3.4.7",
2916 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.7.tgz",
2917 - "integrity": "sha512-qDKBAIurCTub4n/6jDYkXwgsFuriqqmmLrIq1N2QDfYJA/mwiwvxi09OGn28g+uDdERX9NaKDLji0oTjE3sScg==",
2920 + "version": "3.4.11",
2921 + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.11.tgz",
2922 + "integrity": "sha512-cRVLROlY7D72WK2xS91L126Dd6xHNTWDWPUBRh1Syk7+TahCk8Eown1/fSi+VX9c76sMMqEZROQSbwV0HSJnhg==",
2923 "dependencies": {
2919 - "@vue/compiler-core": "3.4.7",
2920 - "@vue/shared": "3.4.7"
2924 + "@vue/compiler-core": "3.4.11",
2925 + "@vue/shared": "3.4.11"
2926 }
2927 },
2928 "node_modules/@vue/compiler-sfc": {
2924 - "version": "3.4.7",
2925 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.7.tgz",
2926 - "integrity": "sha512-Gec6CLkReVswDYjQFq79O5rktri4R7TsD/VPCiUoJw40JhNNxaNJJa8mrQrWoJluW4ETy6QN0NUyC/JO77OCOw==",
2929 + "version": "3.4.11",
2930 + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.11.tgz",
2931 + "integrity": "sha512-1y5xHAD4a/AhK5+dgsZwFg145J6/rl1c8ILC7Gokca+ql51tTpduz/njCHeNmU15XiE7O62LjJFNOtSZ9vxKOQ==",
2932 "dependencies": {
2933 "@babel/parser": "^7.23.6",
2929 - "@vue/compiler-core": "3.4.7",
2930 - "@vue/compiler-dom": "3.4.7",
2931 - "@vue/compiler-ssr": "3.4.7",
2932 - "@vue/shared": "3.4.7",
2934 + "@vue/compiler-core": "3.4.11",
2935 + "@vue/compiler-dom": "3.4.11",
2936 + "@vue/compiler-ssr": "3.4.11",
2937 + "@vue/shared": "3.4.11",
2938 "estree-walker": "^2.0.2",
2939 "magic-string": "^0.30.5",
2940 "postcss": "^8.4.32",
@@ -2937,12 +2942,12 @@
2942 }
2943 },
2944 "node_modules/@vue/compiler-ssr": {
2940 - "version": "3.4.7",
2941 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.7.tgz",
2942 - "integrity": "sha512-PvYeSOvnCkST5mGS0TLwEn5w+4GavtEn6adcq8AspbHaIr+mId5hp7cG3ASy3iy8b+LuXEG2/QaV/nj5BQ/Aww==",
2945 + "version": "3.4.11",
2946 + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.11.tgz",
2947 + "integrity": "sha512-cP9Z2ArRgciYmNraqE0gQkuYInfdn66+LE4pR+16uyBiQeswcU4kEzGA+mF1MdhqYXuENpyGQsTkZapq4cy9YA==",
2948 "dependencies": {
2944 - "@vue/compiler-dom": "3.4.7",
2945 - "@vue/shared": "3.4.7"
2949 + "@vue/compiler-dom": "3.4.11",
2950 + "@vue/shared": "3.4.11"
2951 }
2952 },
2953 "node_modules/@vue/devtools-api": {
@@ -3014,48 +3019,48 @@
3019 }
3020 },
3021 "node_modules/@vue/reactivity": {
3017 - "version": "3.4.7",
3018 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.7.tgz",
3019 - "integrity": "sha512-F539DO0ogH0+L8F9Pnw7cjqibcmSOh5UTk16u5f4MKQ8fraqepI9zdh+sozPX6VmEHOcjo8qw3Or9ZcFFw4SZA==",
3022 + "version": "3.4.11",
3023 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.11.tgz",
3024 + "integrity": "sha512-KscADwKpSynT3S2iJEX8EfPqc9kPFR261sHIQnDh1xhOBf8qd4ait9tEgLt1/uVxyrAgFj/TNGmjDkcsytyA8w==",
3025 "dependencies": {
3021 - "@vue/shared": "3.4.7"
3026 + "@vue/shared": "3.4.11"
3027 }
3028 },
3029 "node_modules/@vue/runtime-core": {
3025 - "version": "3.4.7",
3026 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.7.tgz",
3027 - "integrity": "sha512-QMMsWRQaD3BpGyjjChthpl4Mji4Fjx1qfdufsXlDkKU3HV+hWNor2z+29F+E1MmVcP0ZfRZUfqYgtsQoL7IGwQ==",
3030 + "version": "3.4.11",
3031 + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.11.tgz",
3032 + "integrity": "sha512-wduRf9w1OtSORFs5KVpKEQ1bRwW5D9/E8mB0I4m0f5Wrd53OZridzWWVZaowSKNMXXIF5Y/lYFP9GOM/IL5i2g==",
3033 "dependencies": {
3029 - "@vue/reactivity": "3.4.7",
3030 - "@vue/shared": "3.4.7"
3034 + "@vue/reactivity": "3.4.11",
3035 + "@vue/shared": "3.4.11"
3036 }
3037 },
3038 "node_modules/@vue/runtime-dom": {
3034 - "version": "3.4.7",
3035 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.7.tgz",
3036 - "integrity": "sha512-XwegyUY1rw8zxsX1Z36vwYcqo+uOgih5ti7y9vx+pPFhNdSQmN4LqK2RmSeAJG1oKV8NqSUmjpv92f/x6h0SeQ==",
3039 + "version": "3.4.11",
3040 + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.11.tgz",
3041 + "integrity": "sha512-pWlCTzo6Ad3pSBjzgcZ9maPaz+N/SngLOMfkSKIx7rIWJgcHBoFp4GAbhnkR3jxT4BqIvti6EH3aNSC02VtgOg==",
3042 "dependencies": {
3038 - "@vue/runtime-core": "3.4.7",
3039 - "@vue/shared": "3.4.7",
3043 + "@vue/runtime-core": "3.4.11",
3044 + "@vue/shared": "3.4.11",
3045 "csstype": "^3.1.3"
3046 }
3047 },
3048 "node_modules/@vue/server-renderer": {
3044 - "version": "3.4.7",
3045 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.7.tgz",
3046 - "integrity": "sha512-3bWnYLEkLLhkDWqvNk7IvbQD4UcxvFKxELBiOO2iG3m6AniFIsBWfHOO5tLVQnjdWkODu4rq0GipmfEenVAK5Q==",
3049 + "version": "3.4.11",
3050 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.11.tgz",
3051 + "integrity": "sha512-19rLK9N0yNNzQ83ieyoO9ZT/iBt0S8IkxQ4eVmnqPLCbZgSRMm7GRXnjTFvo0n5vTVVeyaYosBzZ2559L/rP+w==",
3052 "dependencies": {
3048 - "@vue/compiler-ssr": "3.4.7",
3049 - "@vue/shared": "3.4.7"
3053 + "@vue/compiler-ssr": "3.4.11",
3054 + "@vue/shared": "3.4.11"
3055 },
3056 "peerDependencies": {
3052 - "vue": "3.4.7"
3057 + "vue": "3.4.11"
3058 }
3059 },
3060 "node_modules/@vue/shared": {
3056 - "version": "3.4.7",
3057 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.7.tgz",
3058 - "integrity": "sha512-G+i4glX1dMJk88sbJEcQEGWRQnVm9eIY7CcQbO5dpdsD9SF8jka3Mr5OqZYGjczGN1+D6EUwdu6phcmcx9iuPA=="
3061 + "version": "3.4.11",
3062 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.11.tgz",
3063 + "integrity": "sha512-BtC+vE8kHf/jZoyJnTFd0PmY8NejyUeUkshXm8LriHs8KmQUmcZXIbrifjA3WDmvzg7C8D6gBSvdl49pOfU2lQ=="
3064 },
3065 "node_modules/@vue/test-utils": {
3066 "version": "2.4.3",
@@ -3249,9 +3254,9 @@
3254 }
3255 },
3256 "node_modules/acorn-walk": {
3252 - "version": "8.3.1",
3253 - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
3254 - "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
3257 + "version": "8.3.2",
3258 + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
3259 + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
3260 "dev": true,
3261 "engines": {
3262 "node": ">=0.4.0"
@@ -3985,9 +3990,9 @@
3990 "dev": true
3991 },
3992 "node_modules/chai": {
3988 - "version": "4.4.0",
3989 - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz",
3990 - "integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==",
3993 + "version": "4.4.1",
3994 + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
3995 + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
3996 "dev": true,
3997 "dependencies": {
3998 "assertion-error": "^1.1.0",
@@ -5312,9 +5317,9 @@
5317 "dev": true
5318 },
5319 "node_modules/electron-to-chromium": {
5315 - "version": "1.4.626",
5316 - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.626.tgz",
5317 - "integrity": "sha512-f7/be56VjRRQk+Ric6PmIrEtPcIqsn3tElyAu9Sh6egha2VLJ82qwkcOdcnT06W+Pb6RUulV1ckzrGbKzVcTHg==",
5320 + "version": "1.4.630",
5321 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.630.tgz",
5322 + "integrity": "sha512-osHqhtjojpCsACVnuD11xO5g9xaCyw7Qqn/C2KParkMv42i8jrJJgx3g7mkHfpxwhy9MnOJr8+pKOdZ7qzgizg==",
5323 "dev": true
5324 },
5325 "node_modules/emoji-regex": {
@@ -5682,9 +5687,9 @@
5687 }
5688 },
5689 "node_modules/eslint-plugin-vue": {
5685 - "version": "9.19.2",
5686 - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.19.2.tgz",
5687 - "integrity": "sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==",
5690 + "version": "9.20.0",
5691 + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.20.0.tgz",
5692 + "integrity": "sha512-9/DV5CM7ItfgWmXjL6j3zyDtVTrslYdnEm+rnYNajdElx17b3erxi/Wc6FY7t3BQ6dgo0t/UBpgiWCOKtJyN8Q==",
5693 "dev": true,
5694 "dependencies": {
5695 "@eslint-community/eslint-utils": "^4.4.0",
@@ -5692,7 +5697,7 @@
5697 "nth-check": "^2.1.1",
5698 "postcss-selector-parser": "^6.0.13",
5699 "semver": "^7.5.4",
5695 - "vue-eslint-parser": "^9.3.1",
5700 + "vue-eslint-parser": "^9.4.0",
5701 "xml-name-validator": "^4.0.0"
5702 },
5703 "engines": {
@@ -6206,9 +6211,9 @@
6211 "dev": true
6212 },
6213 "node_modules/follow-redirects": {
6209 - "version": "1.15.4",
6210 - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
6211 - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
6214 + "version": "1.15.5",
6215 + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
6216 + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
6217 "dev": true,
6218 "funding": [
6219 {
@@ -8641,15 +8646,15 @@
8646 }
8647 },
8648 "node_modules/mlly": {
8644 - "version": "1.4.2",
8645 - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz",
8646 - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==",
8649 + "version": "1.5.0",
8650 + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz",
8651 + "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==",
8652 "dev": true,
8653 "dependencies": {
8649 - "acorn": "^8.10.0",
8650 - "pathe": "^1.1.1",
8654 + "acorn": "^8.11.3",
8655 + "pathe": "^1.1.2",
8656 "pkg-types": "^1.0.3",
8652 - "ufo": "^1.3.0"
8657 + "ufo": "^1.3.2"
8658 }
8659 },
8660 "node_modules/mrmime": {
@@ -9589,9 +9594,9 @@
9594 }
9595 },
9596 "node_modules/pathe": {
9592 - "version": "1.1.1",
9593 - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz",
9594 - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==",
9597 + "version": "1.1.2",
9598 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
9599 + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
9600 "dev": true
9601 },
9602 "node_modules/pathval": {
@@ -9927,9 +9932,9 @@
9932 }
9933 },
9934 "node_modules/prettier": {
9930 - "version": "3.1.1",
9931 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz",
9932 - "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==",
9935 + "version": "3.2.1",
9936 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.1.tgz",
9937 + "integrity": "sha512-qSUWshj1IobVbKc226Gw2pync27t0Kf0EdufZa9j7uBSJay1CC+B3K5lAAZoqgX3ASiKuWsk6OmzKRetXNObWg==",
9938 "dev": true,
9939 "bin": {
9940 "prettier": "bin/prettier.cjs"
@@ -10657,9 +10662,9 @@
10662 }
10663 },
10664 "node_modules/rollup": {
10660 - "version": "4.9.4",
10661 - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.4.tgz",
10662 - "integrity": "sha512-2ztU7pY/lrQyXSCnnoU4ICjT/tCG9cdH3/G25ERqE3Lst6vl2BCM5hL2Nw+sslAvAf+ccKsAq1SkKQALyqhR7g==",
10665 + "version": "4.9.5",
10666 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz",
10667 + "integrity": "sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==",
10668 "dev": true,
10669 "dependencies": {
10670 "@types/estree": "1.0.5"
@@ -10672,19 +10677,19 @@
10677 "npm": ">=8.0.0"
10678 },
10679 "optionalDependencies": {
10675 - "@rollup/rollup-android-arm-eabi": "4.9.4",
10676 - "@rollup/rollup-android-arm64": "4.9.4",
10677 - "@rollup/rollup-darwin-arm64": "4.9.4",
10678 - "@rollup/rollup-darwin-x64": "4.9.4",
10679 - "@rollup/rollup-linux-arm-gnueabihf": "4.9.4",
10680 - "@rollup/rollup-linux-arm64-gnu": "4.9.4",
10681 - "@rollup/rollup-linux-arm64-musl": "4.9.4",
10682 - "@rollup/rollup-linux-riscv64-gnu": "4.9.4",
10683 - "@rollup/rollup-linux-x64-gnu": "4.9.4",
10684 - "@rollup/rollup-linux-x64-musl": "4.9.4",
10685 - "@rollup/rollup-win32-arm64-msvc": "4.9.4",
10686 - "@rollup/rollup-win32-ia32-msvc": "4.9.4",
10687 - "@rollup/rollup-win32-x64-msvc": "4.9.4",
10680 + "@rollup/rollup-android-arm-eabi": "4.9.5",
10681 + "@rollup/rollup-android-arm64": "4.9.5",
10682 + "@rollup/rollup-darwin-arm64": "4.9.5",
10683 + "@rollup/rollup-darwin-x64": "4.9.5",
10684 + "@rollup/rollup-linux-arm-gnueabihf": "4.9.5",
10685 + "@rollup/rollup-linux-arm64-gnu": "4.9.5",
10686 + "@rollup/rollup-linux-arm64-musl": "4.9.5",
10687 + "@rollup/rollup-linux-riscv64-gnu": "4.9.5",
10688 + "@rollup/rollup-linux-x64-gnu": "4.9.5",
10689 + "@rollup/rollup-linux-x64-musl": "4.9.5",
10690 + "@rollup/rollup-win32-arm64-msvc": "4.9.5",
10691 + "@rollup/rollup-win32-ia32-msvc": "4.9.5",
10692 + "@rollup/rollup-win32-x64-msvc": "4.9.5",
10693 "fsevents": "~2.3.2"
10694 }
10695 },
@@ -10867,9 +10872,9 @@
10872 ]
10873 },
10874 "node_modules/safe-regex-test": {
10870 - "version": "1.0.1",
10871 - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.1.tgz",
10872 - "integrity": "sha512-Y5NejJTTliTyY4H7sipGqY+RX5P87i3F7c4Rcepy72nq+mNLhIsD0W4c7kEmduMDQCSqtPsXPlSTsFhh2LQv+g==",
10875 + "version": "1.0.2",
10876 + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
10877 + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==",
10878 "dev": true,
10879 "dependencies": {
10880 "call-bind": "^1.0.5",
@@ -12312,9 +12317,9 @@
12317 }
12318 },
12319 "node_modules/tuf-js": {
12315 - "version": "2.1.0",
12316 - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.1.0.tgz",
12317 - "integrity": "sha512-eD7YPPjVlMzdggrOeE8zwoegUaG/rt6Bt3jwoQPunRiNVzgcCE009UDFJKJjG+Gk9wFu6W/Vi+P5d/5QpdD9jA==",
12320 + "version": "2.2.0",
12321 + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz",
12322 + "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==",
12323 "dev": true,
12324 "dependencies": {
12325 "@tufjs/models": "2.0.0",
@@ -12848,9 +12853,9 @@
12853 }
12854 },
12855 "node_modules/vite-node": {
12851 - "version": "1.1.3",
12852 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.1.3.tgz",
12853 - "integrity": "sha512-BLSO72YAkIUuNrOx+8uznYICJfTEbvBAmWClY3hpath5+h1mbPS5OMn42lrTxXuyCazVyZoDkSRnju78GiVCqA==",
12856 + "version": "1.2.0",
12857 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.0.tgz",
12858 + "integrity": "sha512-ETnQTHeAbbOxl7/pyBck9oAPZZZo+kYnFt1uQDD+hPReOc+wCjXw4r4jHriBRuVDB5isHmPXxrfc1yJnfBERqg==",
12859 "dev": true,
12860 "dependencies": {
12861 "cac": "^6.7.14",
@@ -12882,16 +12887,16 @@
12887 }
12888 },
12889 "node_modules/vitest": {
12885 - "version": "1.1.3",
12886 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.1.3.tgz",
12887 - "integrity": "sha512-2l8om1NOkiA90/Y207PsEvJLYygddsOyr81wLQ20Ra8IlLKbyQncWsGZjnbkyG2KwwuTXLQjEPOJuxGMG8qJBQ==",
12890 + "version": "1.2.0",
12891 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.0.tgz",
12892 + "integrity": "sha512-Ixs5m7BjqvLHXcibkzKRQUvD/XLw0E3rvqaCMlrm/0LMsA0309ZqYvTlPzkhh81VlEyVZXFlwWnkhb6/UMtcaQ==",
12893 "dev": true,
12894 "dependencies": {
12890 - "@vitest/expect": "1.1.3",
12891 - "@vitest/runner": "1.1.3",
12892 - "@vitest/snapshot": "1.1.3",
12893 - "@vitest/spy": "1.1.3",
12894 - "@vitest/utils": "1.1.3",
12895 + "@vitest/expect": "1.2.0",
12896 + "@vitest/runner": "1.2.0",
12897 + "@vitest/snapshot": "1.2.0",
12898 + "@vitest/spy": "1.2.0",
12899 + "@vitest/utils": "1.2.0",
12900 "acorn-walk": "^8.3.1",
12901 "cac": "^6.7.14",
12902 "chai": "^4.3.10",
@@ -12906,7 +12911,7 @@
12911 "tinybench": "^2.5.1",
12912 "tinypool": "^0.8.1",
12913 "vite": "^5.0.0",
12909 - "vite-node": "1.1.3",
12914 + "vite-node": "1.2.0",
12915 "why-is-node-running": "^2.2.2"
12916 },
12917 "bin": {
@@ -13109,15 +13114,15 @@
13114 }
13115 },
13116 "node_modules/vue": {
13112 - "version": "3.4.7",
13113 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.7.tgz",
13114 - "integrity": "sha512-4urmkWpudekq0CPNMO7p6mBGa9qmTXwJMO2r6CT4EzIJVG7WoSReiysiNb7OSi/WI113oX0Srn9Rz1k/DCXKFQ==",
13115 - "dependencies": {
13116 - "@vue/compiler-dom": "3.4.7",
13117 - "@vue/compiler-sfc": "3.4.7",
13118 - "@vue/runtime-dom": "3.4.7",
13119 - "@vue/server-renderer": "3.4.7",
13120 - "@vue/shared": "3.4.7"
13117 + "version": "3.4.11",
13118 + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.11.tgz",
13119 + "integrity": "sha512-iaA98z14ZrrVJlclpHX/HCNeacbMOLdX5foYN7/vt4cHFhDkBRzojjbLQZ2UDRAeNV1v4V5I21+QpdCXWlpG5Q==",
13120 + "dependencies": {
13121 + "@vue/compiler-dom": "3.4.11",
13122 + "@vue/compiler-sfc": "3.4.11",
13123 + "@vue/runtime-dom": "3.4.11",
13124 + "@vue/server-renderer": "3.4.11",
13125 + "@vue/shared": "3.4.11"
13126 },
13127 "peerDependencies": {
13128 "typescript": "*"
package.json
+5 -5
@@ -55,7 +55,7 @@
55 "pinia-plugin-persistedstate": "^3.2.1",
56 "secure-ls": "^1.2.6",
57 "validator": "^13.11.0",
58 - "vue": "^3.4.7",
58 + "vue": "^3.4.11",
59 "vue-advanced-cropper": "^2.8.8",
60 "vue-highlight-words": "^3.0.1",
61 "vue-i18n": "^9.9.0",
@@ -73,7 +73,7 @@
73 "@types/inquirer": "^9.0.7",
74 "@types/jsdom": "^21.1.6",
75 "@types/lodash": "^4.14.202",
76 - "@types/node": "^20.10.8",
76 + "@types/node": "^20.11.0",
77 "@types/validator": "^13.11.8",
78 "@vitejs/plugin-vue": "^5.0.3",
79 "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -85,14 +85,14 @@
85 "cypress": "^13.6.2",
86 "eslint": "^8.56.0",
87 "eslint-plugin-cypress": "^2.15.1",
88 - "eslint-plugin-vue": "^9.19.2",
88 + "eslint-plugin-vue": "^9.20.0",
89 "fs-extra": "^11.2.0",
90 "jsdom": "^23.2.0",
91 "json5": "^2.2.3",
92 "npm-run-all": "^4.1.5",
93 "picocolors": "^1.0.0",
94 "postcss": "^8.4.33",
95 - "prettier": "^3.1.1",
95 + "prettier": "^3.2.1",
96 "sass": "^1.69.7",
97 "start-server-and-test": "^2.0.3",
98 "tailwind-config-viewer": "^1.7.3",
@@ -105,7 +105,7 @@
105 "vite-bundle-analyzer": "^0.6.1",
106 "vite-bundle-visualizer": "^1.0.0",
107 "vite-svg-loader": "^5.1.0",
108 - "vitest": "^1.1.3",
108 + "vitest": "^1.2.0",
109 "vue-tsc": "^1.8.27"
110 },
111 "engines": {
src/api/agents.ts
+7
@@ -23,6 +23,13 @@ export default {
23 `/agents/${id}/vulnerabilities`
24 )
25 },
26 + getSocCases(id: string | number, signal?: AbortSignal) {
27 + return HttpClient.get<FlaskBaseResponse & { case_ids: number[] }>(
28 + `/agents/${id}/soc_cases`,
29 + signal ? { signal } : {}
30 + )
31 + },
32 +
33 // IGNORE AT THE MOMENT !
34 agentsWazuhOutdated() {
35 return HttpClient.get<FlaskBaseResponse & { outdated_wazuh_agents: OutdatedWazuhAgents }>(
src/api/soc.ts
+12
@@ -124,5 +124,17 @@ export default {
124 },
125 removeUserAlertAssign(alertId: string, userId: string) {
126 return HttpClient.delete<FlaskBaseResponse & { alert: SocAlert }>(`/soc/users/assign/${alertId}/${userId}`)
127 + },
128 + closeCase(caseId: string) {
129 + return HttpClient.put<FlaskBaseResponse & { case: SocAlertCaseResponse }>(`/soc/cases/close/${caseId}`)
130 + },
131 + reopenCase(caseId: string) {
132 + return HttpClient.put<FlaskBaseResponse & { case: SocAlertCaseResponse }>(`/soc/cases/open/${caseId}`)
133 + },
134 + deleteCase(caseId: string) {
135 + return HttpClient.delete<FlaskBaseResponse>(`/soc/cases/purge/${caseId}`)
136 + },
137 + purgeAllCases() {
138 + return HttpClient.delete<FlaskBaseResponse>(`/soc/cases/purge`)
139 }
140 }
src/components/agents/AgentCases.vue new
+84
@@ -0,0 +1,84 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <div class="header flex items-center justify-end gap-2">
4 + <div class="info grow flex gap-5">
5 + <div class="box">
6 + Total:
7 + <code>{{ casesList.length }}</code>
8 + </div>
9 + </div>
10 + </div>
11 + <div class="list my-3">
12 + <template v-if="casesList.length">
13 + <SocCaseItem
14 + v-for="item of casesList"
15 + :key="item"
16 + :caseId="item"
17 + @deleted="getData()"
18 + class="mb-2 item-appear item-appear-bottom item-appear-005"
19 + />
20 + </template>
21 + <template v-else>
22 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
23 + </template>
24 + </div>
25 + </n-spin>
26 +</template>
27 +
28 +<script setup lang="ts">
29 +import { ref, onBeforeMount, toRefs, onBeforeUnmount } from "vue"
30 +import { useMessage, NSpin, NEmpty } from "naive-ui"
31 +import SocCaseItem from "@/components/soc/SocCases/SocCaseItem.vue"
32 +import Api from "@/api"
33 +import type { Agent } from "@/types/agents.d"
34 +import axios from "axios"
35 +
36 +const props = defineProps<{
37 + agent: Agent
38 +}>()
39 +const { agent } = toRefs(props)
40 +
41 +const message = useMessage()
42 +const loading = ref(false)
43 +const casesList = ref<number[]>([])
44 +let abortController: AbortController | null = null
45 +
46 +function getData() {
47 + loading.value = true
48 +
49 + abortController = new AbortController()
50 +
51 + Api.agents
52 + .getSocCases(agent.value.agent_id, abortController.signal)
53 + .then(res => {
54 + if (res.data.success) {
55 + casesList.value = res.data.case_ids || []
56 + } else {
57 + message.warning(res.data?.message || "An error occurred. Please try again later.")
58 + }
59 + })
60 + .catch(err => {
61 + if (!axios.isCancel(err)) {
62 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
63 + }
64 + })
65 + .finally(() => {
66 + loading.value = false
67 + })
68 +}
69 +
70 +onBeforeMount(() => {
71 + getData()
72 +})
73 +
74 +onBeforeUnmount(() => {
75 + abortController?.abort()
76 +})
77 +</script>
78 +
79 +<style lang="scss" scoped>
80 +.list {
81 + container-type: inline-size;
82 + min-height: 200px;
83 +}
84 +</style>
src/components/soc/SocAlerts/SocAlertItem.vue
+8 -1
@@ -180,7 +180,14 @@
180 segmented
181 >
182 <div class="h-full w-full flex items-center justify-center">
183 - <SocCaseItem v-if="caseId" :caseId="caseId" embedded hideSocAlertLink class="w-full" />
183 + <SocCaseItem
184 + v-if="caseId"
185 + :caseId="caseId"
186 + embedded
187 + hideSocAlertLink
188 + hide-soc-case-action
189 + class="w-full"
190 + />
191 </div>
192 </n-modal>
193
src/components/soc/SocCases/SocCaseAssetLink.vue
+9 -3
@@ -41,7 +41,13 @@
41 </template>
42 <div style="min-height: 50px">
43 <n-spin :show="loadingCase">
44 - <SocCaseItem :case-data="socCase" v-if="socCase" :embedded="true" class="py-2 -mt-4" />
44 + <SocCaseItem
45 + :case-data="socCase"
46 + v-if="socCase"
47 + :embedded="true"
48 + @deleted="getSocCase(link.case_id)"
49 + class="py-2 -mt-4"
50 + />
51 <template v-else>
52 <n-empty
53 description="No Case found"
@@ -88,11 +94,11 @@ function showSocCase(show: boolean) {
94 }
95 }
96
91 -function getSocCase(caseId: string) {
97 +function getSocCase(caseId: string | number) {
98 loadingCase.value = true
99
100 Api.soc
95 - .getCases(caseId)
101 + .getCases(caseId.toString())
102 .then(res => {
103 if (res.data.success) {
104 socCase.value = (res.data?.case as unknown as SocCase) || null
src/components/soc/SocCases/SocCaseItem.vue
+58 -5
@@ -1,5 +1,8 @@
1 <template>
2 - <n-spin :show="loadingDetails">
2 + <n-spin
3 + :show="loadingDetails || loadingDelete"
4 + :description="loadingDelete ? 'Deleting Soc Case' : 'Loading Soc Case'"
5 + >
6 <div class="soc-case-item" :class="{ embedded }">
7 <div class="flex flex-col gap-2 px-5 py-3" v-if="baseInfo">
8 <div class="header-box flex justify-between">
@@ -36,7 +39,7 @@
39 </n-popover>
40 </div>
41 </div>
39 - <div class="main-box flex justify-between gap-4">
42 + <div class="main-box flex items-center justify-between gap-4">
43 <div class="content">
44 <div class="title" v-html="baseInfo.case_name"></div>
45 <div class="description mt-2" v-if="baseInfo.case_description">{{ excerpt }}</div>
@@ -79,8 +82,27 @@
82 </Badge>
83 </div>
84 </div>
85 + <SocCaseItemActions
86 + v-if="!hideSocCaseAction"
87 + class="actions-box"
88 + :caseData="baseInfo"
89 + @closed="setClosed()"
90 + @reopened="setReopened()"
91 + @deleted="deleteCase()"
92 + @startDeleting="loadingDelete = true"
93 + />
94 </div>
83 - <div class="footer-box flex justify-end items-center gap-3">
95 + <div class="footer-box flex justify-between items-center gap-3">
96 + <SocCaseItemActions
97 + v-if="!hideSocCaseAction"
98 + class="actions-box !flex-row"
99 + :caseData="baseInfo"
100 + :size="'small'"
101 + @closed="setClosed()"
102 + @reopened="setReopened()"
103 + @deleted="deleteCase()"
104 + @startDeleting="loadingDelete = true"
105 + />
106 <div class="time" v-if="caseOpenDate">{{ formatDate(caseOpenDate) }}</div>
107 </div>
108 </div>
@@ -218,6 +240,7 @@ import SocCaseTimeline from "./SocCaseTimeline.vue"
240 import SocCaseAssetsList from "./SocCaseAssetsList.vue"
241 import SocCaseNoteForm from "./SocCaseNoteForm.vue"
242 import SocCaseNotesList from "./SocCaseNotesList.vue"
243 +import SocCaseItemActions from "./SocCaseItemActions.vue"
244 import SocAlertItem from "../SocAlerts/SocAlertItem.vue"
245 import Api from "@/api"
246 import {
@@ -240,11 +263,16 @@ import { type SocCase, StateName, type SocCaseExt } from "@/types/soc/case.d"
263 import _omit from "lodash/omit"
264 import _split from "lodash/split"
265
243 -const { caseData, caseId, embedded } = defineProps<{
266 +const { caseData, caseId, embedded, hideSocCaseAction } = defineProps<{
267 caseData?: SocCase
268 caseId?: number | string
269 embedded?: boolean
270 hideSocAlertLink?: boolean
271 + hideSocCaseAction?: boolean
272 +}>()
273 +
274 +const emit = defineEmits<{
275 + (e: "deleted"): void
276 }>()
277
278 const TimeIcon = "carbon:time"
@@ -258,6 +286,7 @@ const AddIcon = "carbon:add-alt"
286 const showSocAlertDetails = ref(false)
287 const showDetails = ref(false)
288 const loadingDetails = ref(false)
289 +const loadingDelete = ref(false)
290 const message = useMessage()
291 const noteFormVisible = ref([])
292 const updateNotes = ref(false)
@@ -343,6 +372,23 @@ function formatDate(timestamp: string | number | Date, utc: boolean = true): str
372 return dayjs(timestamp).utc(utc).format(dFormats.date)
373 }
374
375 +function setClosed() {
376 + if (baseInfo.value) {
377 + baseInfo.value.state_name = StateName.Closed
378 + }
379 +}
380 +
381 +function setReopened() {
382 + if (baseInfo.value) {
383 + baseInfo.value.state_name = StateName.Open
384 + }
385 +}
386 +
387 +function deleteCase() {
388 + loadingDetails.value = true
389 + emit("deleted")
390 +}
391 +
392 function openSocAlert() {
393 showSocAlertDetails.value = true
394 }
@@ -433,13 +479,13 @@ onBeforeMount(() => {
479 }
480
481 .footer-box {
436 - font-family: var(--font-family-mono);
482 font-size: 13px;
483 margin-top: 10px;
484 display: none;
485
486 .time {
487 text-align: right;
488 + font-family: var(--font-family-mono);
489 color: var(--fg-secondary-color);
490 }
491 }
@@ -456,6 +502,13 @@ onBeforeMount(() => {
502 display: none;
503 }
504 }
505 +
506 + .main-box {
507 + .actions-box {
508 + display: none;
509 + }
510 + }
511 +
512 .footer-box {
513 display: flex;
514 }
src/components/soc/SocCases/SocCaseItemActions.vue new
+148
@@ -0,0 +1,148 @@
1 +<template>
2 + <div class="soc-case-actions flex flex-col gap-2 justify-center">
3 + <n-button
4 + v-if="isCaseClosed"
5 + :loading="loadingCaseReopen"
6 + :size="size"
7 + type="warning"
8 + secondary
9 + @click="reopenCase()"
10 + >
11 + <template #icon><Icon :name="OpenIcon"></Icon></template>
12 + Reopen
13 + </n-button>
14 + <n-button v-else :loading="loadingCaseClose" type="success" secondary :size="size" @click="closeCase()">
15 + <template #icon><Icon :name="CloseIcon"></Icon></template>
16 + Close
17 + </n-button>
18 + <n-button :loading="loadingCaseDelete" :size="size" type="error" secondary @click="handleDelete()">
19 + <template #icon><Icon :name="DeleteIcon"></Icon></template>
20 + Delete
21 + </n-button>
22 + </div>
23 +</template>
24 +
25 +<script setup lang="ts">
26 +import { NButton, useDialog, useMessage } from "naive-ui"
27 +import Icon from "@/components/common/Icon.vue"
28 +import Api from "@/api"
29 +import { computed, h, ref } from "vue"
30 +import { watch } from "vue"
31 +import { StateName, type SocCase, type SocCaseExt } from "@/types/soc/case.d"
32 +
33 +const emit = defineEmits<{
34 + (e: "startLoading"): void
35 + (e: "stopLoading"): void
36 + (e: "closed"): void
37 + (e: "reopened"): void
38 + (e: "deleted"): void
39 + (e: "startDeleting"): void
40 +}>()
41 +
42 +const { caseData, size } = defineProps<{
43 + caseData: SocCase | SocCaseExt | null
44 + size?: "tiny" | "small" | "medium" | "large"
45 +}>()
46 +
47 +const DeleteIcon = "ph:trash"
48 +const CloseIcon = "ph:circle-wavy-check"
49 +const OpenIcon = "ph:circle-wavy-warning"
50 +
51 +const dialog = useDialog()
52 +const message = useMessage()
53 +const loadingCaseClose = ref(false)
54 +const loadingCaseReopen = ref(false)
55 +const loadingCaseDelete = ref(false)
56 +const loading = computed(() => loadingCaseClose.value || loadingCaseReopen.value || loadingCaseDelete.value)
57 +
58 +const isCaseClosed = computed(() => caseData?.state_name === StateName.Closed)
59 +
60 +watch(loading, val => {
61 + emit(val ? "startLoading" : "startLoading")
62 +})
63 +
64 +function closeCase() {
65 + if (caseData?.case_id) {
66 + loadingCaseClose.value = true
67 +
68 + Api.soc
69 + .closeCase(caseData.case_id.toString())
70 + .then(res => {
71 + if (res.data.success) {
72 + emit("closed")
73 + message.success(res.data?.message || "SOC Case closed.")
74 + } else {
75 + message.warning(res.data?.message || "An error occurred. Please try again later.")
76 + }
77 + })
78 + .catch(err => {
79 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
80 + })
81 + .finally(() => {
82 + loadingCaseClose.value = false
83 + })
84 + }
85 +}
86 +
87 +function reopenCase() {
88 + if (caseData?.case_id) {
89 + loadingCaseReopen.value = true
90 +
91 + Api.soc
92 + .reopenCase(caseData.case_id.toString())
93 + .then(res => {
94 + if (res.data.success) {
95 + emit("reopened")
96 + message.success(res.data?.message || "SOC Case reopened.")
97 + } else {
98 + message.warning(res.data?.message || "An error occurred. Please try again later.")
99 + }
100 + })
101 + .catch(err => {
102 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
103 + })
104 + .finally(() => {
105 + loadingCaseReopen.value = false
106 + })
107 + }
108 +}
109 +
110 +function handleDelete() {
111 + dialog.warning({
112 + title: "Confirm",
113 + content: "This will delete the case are you sure you want to proceed?",
114 + positiveText: "Yes I'm sure",
115 + negativeText: "Cancel",
116 + onPositiveClick: () => {
117 + deleteCase()
118 + },
119 + onNegativeClick: () => {
120 + message.info("Delete canceled")
121 + }
122 + })
123 +}
124 +
125 +function deleteCase() {
126 + if (caseData?.case_id) {
127 + loadingCaseDelete.value = true
128 + emit("startDeleting")
129 +
130 + Api.soc
131 + .deleteCase(caseData.case_id.toString())
132 + .then(res => {
133 + if (res.data.success) {
134 + emit("deleted")
135 + message.success(res.data?.message || "SOC Case deleted.")
136 + } else {
137 + message.warning(res.data?.message || "An error occurred. Please try again later.")
138 + }
139 + })
140 + .catch(err => {
141 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
142 + })
143 + .finally(() => {
144 + loadingCaseDelete.value = false
145 + })
146 + }
147 +}
148 +</script>
src/components/soc/SocCases/SocCasesList.vue
+51 -3
@@ -19,6 +19,13 @@
19 </div>
20 </div>
21 </n-popover>
22 +
23 + <n-button size="small" type="error" ghost @click="handlePurge()" :loading="loadingPurge">
24 + <div class="flex items-center gap-2">
25 + <Icon :name="TrashIcon" :size="16"></Icon>
26 + <span class="hidden xs:block">Purge</span>
27 + </div>
28 + </n-button>
29 </div>
30 <n-pagination
31 v-model:page="currentPage"
@@ -82,6 +89,7 @@
89 v-for="caseData of itemsPaginated"
90 :key="caseData.case_id"
91 :caseData="caseData"
92 + @deleted="getData()"
93 class="item-appear item-appear-bottom item-appear-005 mb-2"
94 />
95 </template>
@@ -114,7 +122,8 @@ import {
122 NPagination,
123 NInputGroup,
124 NBadge,
117 - NInputNumber
125 + NInputNumber,
126 + useDialog
127 } from "naive-ui"
128 import Api from "@/api"
129 import _cloneDeep from "lodash/cloneDeep"
@@ -126,7 +135,9 @@ import type { DateFormatted, SocCase } from "@/types/soc/case.d"
135 import SocCaseItem from "./SocCaseItem.vue"
136 import dayjs from "@/utils/dayjs"
137
138 +const dialog = useDialog()
139 const message = useMessage()
140 +const loadingPurge = ref(false)
141 const loading = ref(false)
142 const showFilters = ref(false)
143 const casesList = ref<SocCase[]>([])
@@ -157,6 +168,7 @@ const itemsPaginated = computed(() => {
168
169 const FilterIcon = "carbon:filter-edit"
170 const InfoIcon = "carbon:information"
171 +const TrashIcon = "carbon:trash-can"
172
173 const total = computed<number>(() => {
174 return casesList.value.length || 0
@@ -206,12 +218,48 @@ function getData() {
218 })
219 }
220
221 +function handlePurge() {
222 + dialog.warning({
223 + title: "Confirm",
224 + content: "This will remove ALL cases, are you sure you want to proceed?",
225 + positiveText: "Yes I'm sure",
226 + negativeText: "Cancel",
227 + onPositiveClick: () => {
228 + purge()
229 + },
230 + onNegativeClick: () => {
231 + message.info("Purge canceled")
232 + }
233 + })
234 +}
235 +
236 +function purge() {
237 + loadingPurge.value = true
238 +
239 + Api.soc
240 + .purgeAllCases()
241 + .then(res => {
242 + if (res.data.success) {
243 + getData()
244 + message.success(res.data?.message || "SOC Cases purged successfully")
245 + } else {
246 + message.warning(res.data?.message || "An error occurred. Please try again later.")
247 + }
248 + })
249 + .catch(err => {
250 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
251 + })
252 + .finally(() => {
253 + loadingPurge.value = false
254 + })
255 +}
256 +
257 useResizeObserver(header, entries => {
258 const entry = entries[0]
259 const { width } = entry.contentRect
260
213 - pageSlot.value = width < 650 ? 5 : 8
214 - simpleMode.value = width < 450
261 + pageSlot.value = width < 700 ? 5 : 8
262 + simpleMode.value = width < 550
263 })
264
265 onBeforeMount(() => {
src/types/soc/case.d.ts
+3 -3
@@ -7,13 +7,13 @@ export interface SocCase {
7 case_open_date: DateFormatted
8 case_soc_id: string
9 case_uuid: string
10 - classification: string | null
10 classification_id: number | null
11 + classification: string | null
12 client_name: string
13 - opened_by: string
13 opened_by_user_id: number
15 - owner: string
14 + opened_by: string
15 owner_id: number
16 + owner: string
17 state_id: number
18 state_name: StateName
19 }
src/views/AgentOverview.vue
+6
@@ -57,6 +57,11 @@
57 <VulnerabilitiesSection v-if="agent" :agent="agent" />
58 </div>
59 </n-tab-pane>
60 + <n-tab-pane name="Cases" tab="Cases" display-directive="show:lazy">
61 + <div class="section">
62 + <AgentCases v-if="agent" :agent="agent" />
63 + </div>
64 + </n-tab-pane>
65 <n-tab-pane name="Artifacts" tab="Artifacts" display-directive="show:lazy">
66 <div class="section">
67 <AgentFlowList v-if="agent" :agent="agent" />
@@ -108,6 +113,7 @@ import { useRouter } from "vue-router"
113 import VulnerabilitiesSection from "@/components/agents/VulnerabilitiesSection.vue"
114 import AlertsList from "@/components/alerts/AlertsList.vue"
115 import OverviewSection from "@/components/agents/OverviewSection.vue"
116 +import AgentCases from "@/components/agents/AgentCases.vue"
117 import AgentFlowList from "@/components/agents/agentFlow/AgentFlowList.vue"
118 import { useMessage, NSpin, NTooltip, NButton, NTabs, NTabPane, NCard, useDialog } from "naive-ui"
119 import Icon from "@/components/common/Icon.vue"