@cryptotaxi247 / CoPilot / commits / b9409830

Recap (#114)

* begin password reset token * return 401 if User is `None` * Implement password reset logic but dont enable * Refactor password reset token generation and add password reset functionality * Add email field to UserBase schema * docker prep things * Update Dockerfile and add authentication dependency to get_users endpoint * Update Dockerfile and package.json * Add SERVER_IP configuration to .env.example and use it in copilot.py * Update Dockerfile and docker-compose.yml * Refactor password reset logic * updated dependencies * updated auth api * added users list page * updated login store action * added reset password form * edit docker compose to allow host network * updated users list page * updated users list page * updated avatar dropdown link * updated sidebar links * Update database path in settings.py * Add volume mapping for copilot.db * Refactor load_connector_data function and update connector descriptions * Update InfluxDB connection in db_populate.py * Update Dockerfile and package.json * Add filtered alerts endpoint and create case from alert * Add SortOrder enum for filter alerts request * updated login page * Add to_dict method to IrisAsset and update alert with asset payload * Add to_dict method to IrisIoc class and update alert with ioc_payload * Add asset_tags field to IrisAsset class and populate it with agent_id * gitignore journal * Refactor create_case function and add construct_case_creation_params * Update create_case_from_alert response model * Remove UPLOAD_FOLDER configuration * Update hello message in copilot.py * updated list of shortcuts * updated router triggers * updated dependencies * Update get_bookmarked_alerts function to fetch 1000 alerts per page * added soc case creation api * updated get soc alerts api * fixed vuesj style * updated alert component * refactored soc components * Refactor get_all_alerts_assigned_to_user to filter alerts by owner ID * Update per_page field in FilterAlertsRequest * refactored soc-alerts list * updated dependencies * updated overview cards * updated sidebar links * updated soc-alerts list * updated soc-alerts list * add velo get flows * updated agents list page * updated agents list page * updated layout and colors * Add logging for failed spec validation * Add retrieve_flow endpoint to fetch a specific flow * updated connectors page * updated connectors page * Add connector verification and update logic * Update flow_id to session_id in retrieve_flow function * Add validation for empty session_id in RetrieveFlowRequest * Fix message initialization in ValidationErrorItem class * added agent flow tab * updated agent flow tab * updated agent flow tab * updated connectors page * updated agent flow tab * Add connector verification after update * Add SOCFortress Threat Intel Service verification * Add AskSocfortress connector and verification service * Update worker provisioning healthcheck URL --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Jan 10, 2024 at 11:21 UTC b94098300ae7ca783d03a79c59c696e63b561006
134 files changed +4830 -1591
.devcontainer/devcontainer.json new
+33
@@ -0,0 +1,33 @@
1 +// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
3 +{
4 + "name": "Existing Dockerfile",
5 + "build": {
6 + // Sets the run context to one level up instead of the .devcontainer folder.
7 + "context": "..",
8 + // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
9 + "dockerfile": "../Dockerfile.deb"
10 + },
11 + "features": {
12 + "ghcr.io/devcontainers/features/node:1": {},
13 + "ghcr.io/devcontainers/features/python:1": {},
14 + "ghcr.io/devcontainers-contrib/features/npm-package:1": {},
15 + "ghcr.io/devcontainers-contrib/features/pipenv:2": {},
16 + "ghcr.io/akhildevelops/devcontainer-features/pip:0": {}
17 + }
18 +
19 + // Features to add to the dev container. More info: https://containers.dev/features.
20 + // "features": {},
21 +
22 + // Use 'forwardPorts' to make a list of ports inside the container available locally.
23 + // "forwardPorts": [],
24 +
25 + // Uncomment the next line to run commands after the container is created.
26 + // "postCreateCommand": "cat /etc/os-release",
27 +
28 + // Configure tool-specific properties.
29 + // "customizations": {},
30 +
31 + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
32 + // "remoteUser": "devcontainer"
33 +}
.dockerignore new
+2
@@ -0,0 +1,2 @@
1 +backend/data/copilot.db
2 +backend/copilot.db
.env.example
+1
@@ -1,4 +1,5 @@
1 # base url
2 +SERVER_IP=1.1.1.1
3 VITE_API_URL=http://127.0.0.1:5000
4
5 # value in seconds
.env.production deleted
-11
@@ -1,11 +0,0 @@
1 -# base url
2 -VITE_API_URL=http://127.0.0.1:5000
3 -
4 -# value in seconds
5 -VITE_TOKEN_DEBOUNCE_TIME=10
6 -
7 -# alert if value is over
8 -VITE_UNCOMMITTED_JOURNAL_ENTRIES_THRESHOLD=50000
9 -
10 -# value in seconds
11 -VITE_HEALTHCHECKS_INTERVAL=120
.gitignore
+1
@@ -39,6 +39,7 @@ wheels/
39
40 # Local development artifacts
41 *.db
42 +*.db-journal
43 .env
44 *.sqbpro
45 site/
Dockerfile.deb
+21 -5
@@ -36,12 +36,28 @@ ENV PATH="/opt/venv/bin:$PATH"
36 RUN /opt/venv/bin/pip install setuptools
37
38 # Install your application's dependencies
39 -WORKDIR /app
40 -COPY backend/requirements.in ./
41 -RUN /opt/venv/bin/pip install --no-cache-dir -r requirements.in
39 +WORKDIR /opt/copilot/backend
40 +COPY backend/requirements.txt ./
41 +RUN /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
42
43 # Copy your application into the Docker image
44 -COPY backend/ ./
44 +WORKDIR /opt/copilot
45 +COPY . .
46 +
47 +# Install Node.js and npm
48 +RUN curl -sL https://deb.nodesource.com/setup_18.x | bash -
49 +RUN apt-get install -y nodejs
50 +
51 +# Install concurrently
52 +RUN npm install -g concurrently
53 +
54 +# Install your Vue.js application's dependencies
55 +WORKDIR /opt/copilot
56 +RUN npm install
57 +
58 +# Expose ports
59 +EXPOSE 5000 5173
60
61 # Run your application
47 -CMD ["uvicorn", "copilot:app", "--host", "0.0.0.0", "--port", "5000", "--log-level", "debug"]
62 +#CMD ["sh", "-c", "cd backend && python copilot.py & cd /opt/copilot && npm run dev"]
63 +CMD ["sh", "-c", "cd /opt/copilot && npm run start"]
backend/app/auth/models/users.py
+34 -1
@@ -3,6 +3,7 @@ import random
3 import string
4 from enum import Enum
5 from typing import Optional
6 +import re
7
8 import bcrypt
9 from pydantic import BaseModel
@@ -11,7 +12,7 @@ from pydantic import validator
12 from sqlmodel import Field
13 from sqlmodel import Relationship
14 from sqlmodel import SQLModel
14 -
15 +from fastapi import HTTPException
16
17 class Role(SQLModel, table=True):
18 id: Optional[int] = Field(primary_key=True)
@@ -122,3 +123,35 @@ class Password(BaseModel):
123
124 # Return the Password object with both the plain and hashed password
125 return cls(length=length, hashed=hashed_password.decode("utf-8"), plain=password)
126 +
127 +# ! PASSWORD RESET TOKEN GENERATION NOT USING FOR NOW! #
128 +class PasswordResetRequest(BaseModel):
129 + username: str
130 +
131 +class PasswordResetToken(BaseModel):
132 + username: str
133 + reset_token: str
134 + new_password: str
135 +
136 + @validator('new_password')
137 + def validate_password(cls, password):
138 + if len(password) < 8 or len(password) > 256:
139 + raise ValueError("Password length must be between 8 and 256 characters.")
140 + if not re.search(r'[a-z]', password):
141 + raise ValueError("Password must contain at least one lowercase letter.")
142 + if not re.search(r'[A-Z]', password):
143 + raise ValueError("Password must contain at least one uppercase letter.")
144 + if not re.search(r'\d', password):
145 + raise ValueError("Password must contain at least one digit.")
146 + if not re.search(r'[@$!%*?&#]', password):
147 + raise ValueError("Password must contain at least one special character.")
148 + return password
149 +
150 +class PasswordReset(BaseModel):
151 + username: str
152 + new_password: str = Field(
153 + max_length=256,
154 + min_length=8,
155 + regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&#])[A-Za-z\\d@$!%*?&#]{8,}$",
156 + description="Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character",
157 + )
backend/app/auth/routes/auth.py
+91 -5
@@ -3,14 +3,16 @@ from datetime import timedelta
3 from fastapi import APIRouter
4 from fastapi import Depends
5 from fastapi import HTTPException
6 +from fastapi import Security
7 from fastapi import status
8 +from starlette.status import HTTP_401_UNAUTHORIZED
9 from fastapi.security import OAuth2PasswordRequestForm
10 from loguru import logger
11 from sqlalchemy.ext.asyncio import AsyncSession
12
13 from app.auth.models.users import User
14 from app.auth.models.users import UserInput
13 -from app.auth.models.users import UserLogin
15 +from app.auth.models.users import UserLogin, PasswordReset, PasswordResetToken
16 from app.auth.schema.auth import Token
17 from app.auth.schema.auth import UserLoginResponse
18 from app.auth.schema.auth import UserResponse
@@ -112,7 +114,7 @@ async def login(user: UserLogin):
114
115
116 # Get all users
115 -@auth_router.get("/users", response_model=UserBaseResponse, description="Get all users")
117 +@auth_router.get("/users", response_model=UserBaseResponse, description="Get all users", dependencies=[Security(AuthHandler().require_any_scope("analyst","admin"))],)
118 async def get_users(session: AsyncSession = Depends(get_db)):
119 """
120 Retrieve all users from the database.
@@ -130,7 +132,91 @@ async def get_users(session: AsyncSession = Depends(get_db)):
132 users = await select_all_users()
133 return UserBaseResponse(users=users, message="Users retrieved successfully", success=True)
134
135 +# ! TODO: HAVE LOGIC TO HANDLE PASSWORD RESET VIA A TOKEN BUT NOT IMPLEMENTED YET ! #
136 +@auth_router.post("/reset-token", status_code=200, description="Request password reset", include_in_schema=False)
137 +async def request_password_reset(password_reset_request: PasswordResetToken, session: AsyncSession = Depends(get_db)):
138 + """
139 + Request a password reset.
140 +
141 + Args:
142 + password_reset_request (PasswordResetRequest): The password reset request data.
143 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
144
134 -# @user_router.get("/users/me", description="Get current user")
135 -# def get_current_user(user: User = Depends(auth_handler.get_current_user)):
136 -# return user
145 + Returns:
146 + dict: A dictionary containing the reset token.
147 + """
148 + user = await find_user(password_reset_request.username)
149 + if not user:
150 + raise HTTPException(status_code=404, detail="User not found")
151 + reset_token = auth_handler.generate_reset_token(user.username)
152 + return {"reset_token": reset_token}
153 +
154 +
155 +# ! TODO: HAVE LOGIC TO HANDLE PASSWORD RESET VIA A TOKEN BUT NOT IMPLEMENTED YET ! #
156 +# @auth_router.post("/reset-password", status_code=200, description="Reset user's password", include_in_schema=False)
157 +# async def reset_password(password_reset: PasswordReset, session: AsyncSession = Depends(get_db)):
158 +# """
159 +# Reset a user's password.
160 +
161 +# Args:
162 +# password_reset (PasswordReset): The password reset data.
163 +# session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
164 +
165 +# Returns:
166 +# dict: A dictionary containing the message and success status.
167 +# """
168 +# user = await find_user(password_reset.username)
169 +# if not user or not auth_handler.verify_reset_token(password_reset.reset_token, user.username):
170 +# raise HTTPException(status_code=404, detail="Invalid username or reset token")
171 +# hashed_pwd = auth_handler.get_password_hash(password_reset.new_password)
172 +# user.password = hashed_pwd
173 +# session.add(user)
174 +# await session.commit()
175 +# return {"message": "Password reset successfully", "success": True}
176 +
177 +# Reset a user's password via the username, must be an admin
178 +@auth_router.post("/reset-password", status_code=200, description="Reset user's password via username", dependencies=[Security(AuthHandler().require_any_scope("admin"))],)
179 +async def reset_password_via_username(request: PasswordReset, session: AsyncSession = Depends(get_db)):
180 + """
181 + Reset a user's password via the username. Must be an admin.
182 +
183 + Args:
184 + request (PasswordReset): The password reset data.
185 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
186 +
187 + Returns:
188 + dict: A dictionary containing the message and success status.
189 + """
190 + user = await find_user(request.username)
191 + if not user:
192 + raise HTTPException(status_code=404, detail="User not found")
193 + hashed_pwd = auth_handler.get_password_hash(request.new_password)
194 + user.password = hashed_pwd
195 + session.add(user)
196 + await session.commit()
197 + return {"message": "Password reset successfully", "success": True}
198 +
199 +
200 +# Reset a users password for themselves. The user must be logged in and the token decoded to get the username
201 +@auth_router.post("/reset-password/me", status_code=200, description="Reset user's password", dependencies=[Security(AuthHandler().require_any_scope("analyst","admin"))],)
202 +async def reset_password_me(request: PasswordReset, token: str = Depends(AuthHandler().security), session: AsyncSession = Depends(get_db)):
203 + """
204 + Reset a user's password.
205 +
206 + Args:
207 + request (PasswordReset): The password reset data.
208 + token (str, optional): The authentication token. Defaults to Depends(AuthHandler().security).
209 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
210 +
211 + Returns:
212 + dict: A dictionary containing the message and success status.
213 + """
214 + user = await find_user(request.username)
215 + if not user:
216 + raise HTTPException(status_code=404, detail="User not found")
217 + await auth_handler.verify_reset_token_me(token, user)
218 + hashed_pwd = auth_handler.get_password_hash(request.new_password)
219 + user.password = hashed_pwd
220 + session.add(user)
221 + await session.commit()
222 + return {"message": "Password reset successfully", "success": True}
backend/app/auth/schema/user.py
+2
@@ -1,11 +1,13 @@
1 from typing import List
2
3 from pydantic import BaseModel
4 +from pydantic import EmailStr
5
6
7 class UserBase(BaseModel):
8 id: int
9 username: str
10 + email: EmailStr
11
12
13 class UserBaseResponse(BaseModel):
backend/app/auth/utils.py
+48 -15
@@ -27,31 +27,64 @@ class AuthHandler:
27 def verify_password(self, plain_password, hashed_password):
28 return self.pwd_context.verify(plain_password, hashed_password)
29
30 - def get_reset_token(self, username: str, expires_delta: timedelta = timedelta(minutes=30)):
30 +
31 + # ! TODO: HAVE LOGIC TO HANDLE PASSWORD RESET VIA A TOKEN BUT NOT IMPLEMENTED YET ! #
32 + def generate_reset_token(self, username: str, expires_delta: timedelta = timedelta(minutes=30)):
33 + """
34 + Generates a password reset token.
35 +
36 + Args:
37 + username (str): The username for which the token is being generated.
38 + expires_delta (timedelta, optional): The expiration time for the token.
39 + Defaults to 30 minutes.
40 +
41 + Returns:
42 + str: The generated reset token.
43 + """
44 to_encode = {"exp": datetime.utcnow() + expires_delta, "sub": username}
45 encoded_jwt = jwt.encode(to_encode, self.secret, algorithm="HS256")
46 return encoded_jwt
47
35 - # ! TODO: Password Reset Token Generation ! #
48
37 - # async def reset_password(self, token: str, new_password: str):
49 + # ! TODO: HAVE LOGIC TO HANDLE PASSWORD RESET VIA A TOKEN BUT NOT IMPLEMENTED YET ! #
50 + # def verify_reset_token(self, token: str, username: str):
51 + # """
52 + # Verifies a password reset token.
53 +
54 + # Args:
55 + # token (str): The reset token to verify.
56 + # username (str): The username for which the token was generated.
57 +
58 + # Returns:
59 + # bool: True if the token is valid and not expired, False otherwise.
60 + # """
61 # try:
62 # payload = jwt.decode(token, self.secret, algorithms=["HS256"])
40 - # username = payload.get("sub")
41 - # if username is None:
42 - # raise HTTPException(status_code=400, detail="Invalid token")
43 - # except jwt.PyJWTError:
44 - # raise HTTPException(status_code=400, detail="Invalid token")
45 -
46 - # user = await find_user(username)
47 - # if user is None:
63 + # return payload["sub"] == username
64 + # except jwt.ExpiredSignatureError:
65 # return False
66
50 - # hashed_password = self.get_password_hash(new_password)
51 - # # Here you would need to implement a method to update the user's password in your database
52 - # await update_user_password(username, hashed_password)
67 + async def verify_reset_token_me(self, token: str, user):
68 + """
69 + Verifies a password reset token and checks that the username in the token matches the provided user's username.
70 +
71 + Args:
72 + token (str): The reset token to verify.
73 + user: The user for which the token should be verified.
74
54 - # return True
75 + Returns:
76 + The username from the token if the token is valid, None otherwise.
77 + """
78 + try:
79 + payload = jwt.decode(token, self.secret, algorithms=["HS256"])
80 + if payload["sub"] == user.username:
81 + return payload["sub"]
82 + else:
83 + raise HTTPException(status_code=401, detail="Invalid token. Username does not match.")
84 + except jwt.ExpiredSignatureError:
85 + raise HTTPException(status_code=401, detail="Token expired")
86 + except jwt.InvalidTokenError:
87 + raise HTTPException(status_code=401, detail="Invalid token")
88
89 # ! New with Async
90 async def authenticate_user(self, username: str, password: str):
backend/app/connectors/dfir_iris/routes/alerts.py
+28 -7
@@ -7,10 +7,11 @@ from loguru import logger
7 from app.auth.utils import AuthHandler
8 from app.connectors.dfir_iris.schema.alerts import AlertResponse
9 from app.connectors.dfir_iris.schema.alerts import AlertsResponse
10 -from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
10 +from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse, FilterAlertsRequest, CaseCreationResponse
11 from app.connectors.dfir_iris.services.alerts import bookmark_alert
12 from app.connectors.dfir_iris.services.alerts import get_alert
13 from app.connectors.dfir_iris.services.alerts import get_alerts
14 +from app.connectors.dfir_iris.services.alerts import create_case
15 from app.connectors.dfir_iris.services.alerts import get_bookmarked_alerts
16 from app.connectors.dfir_iris.utils.universal import check_alert_exists
17
@@ -56,21 +57,22 @@ async def get_all_bookmarked_alerts() -> BookmarkedAlertsResponse:
57 return await get_bookmarked_alerts()
58
59
59 -@dfir_iris_alerts_router.get(
60 +@dfir_iris_alerts_router.post(
61 "",
62 response_model=AlertsResponse,
62 - description="Get all alerts",
63 + description="Get alerts from IRIS based on the provided filters",
64 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
65 )
65 -async def get_all_alerts() -> AlertsResponse:
66 +async def get_alerts_filtered(request: FilterAlertsRequest) -> AlertsResponse:
67 """
67 - Retrieve all alerts.
68 + Retrieve alerts from DFIR-IRIS based on the provided filters.
69
70 Returns:
71 AlertsResponse: The response containing all alerts.
72 """
73 logger.info("Fetching all alerts")
73 - return await get_alerts()
74 + logger.info(f"Request: {request}")
75 + return await get_alerts(request)
76
77
78 @dfir_iris_alerts_router.get(
@@ -110,7 +112,7 @@ async def get_all_alerts_assigned_to_user(user_id: int) -> AlertsResponse:
112 AlertsResponse: The response containing the fetched alerts assigned to the user.
113 """
114 logger.info(f"Fetching all alerts assigned to user {user_id}")
113 - alerts = (await get_alerts()).alerts
115 + alerts = (await get_alerts(request=FilterAlertsRequest(alert_owner_id=user_id,per_page=1000))).alerts
116 alerts_assigned_to_user = []
117 for alert in alerts:
118 if alert["alert_owner_id"] == user_id:
@@ -118,6 +120,25 @@ async def get_all_alerts_assigned_to_user(user_id: int) -> AlertsResponse:
120
121 return AlertsResponse(success=True, message="Successfully fetched alerts assigned to user", alerts=alerts_assigned_to_user)
122
123 +@dfir_iris_alerts_router.post(
124 + "/create_case/{alert_id}",
125 + response_model=CaseCreationResponse,
126 + description="Assign an alert to a user",
127 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
128 +)
129 +async def create_case_from_alert(alert_id: str = Depends(verify_alert_exists)) -> CaseCreationResponse:
130 + """
131 + Create a case from an alert.
132 +
133 + Args:
134 + alert_id (str): The ID of the alert to create a case from.
135 +
136 + Returns:
137 + CaseCreationResponse: The response containing the created case.
138 + """
139 + logger.info(f"Creating case from alert {alert_id}")
140 + return await create_case(alert_id)
141 +
142
143 @dfir_iris_alerts_router.post(
144 "/bookmark/{alert_id}",
backend/app/connectors/dfir_iris/schema/alerts.py
+43
@@ -2,6 +2,7 @@ from typing import Any
2 from typing import Dict
3 from typing import List
4 from typing import Optional
5 +from enum import Enum
6
7 from pydantic import BaseModel
8 from pydantic import Field
@@ -23,3 +24,45 @@ class BookmarkedAlertsResponse(BaseModel):
24 bookmarked_alerts: Optional[List[Dict[str, Any]]] = Field([], description="The alerts returned from the search.")
25 message: str
26 success: bool
27 +
28 +class SortOrder(Enum):
29 + desc = "desc"
30 + asc = "asc"
31 +
32 +class FilterAlertsRequest(BaseModel):
33 + per_page: int = Field(1000, description="The number of alerts to return per page.")
34 + page: int = Field(1, description="The page number to return.")
35 + sort: SortOrder = Field(SortOrder.desc, description="The sort order for the alerts.")
36 + alert_title: Optional[str] = Field(None, description="The title of the alert.")
37 + alert_owner_id: Optional[int] = Field(None, description="The ID of the alert owner.")
38 +
39 +
40 +class CaseModificationHistory(BaseModel):
41 + user: str
42 + user_id: int
43 + action: str
44 +
45 +class CaseData(BaseModel):
46 + owner_id: int
47 + case_soc_id: str
48 + status_id: int
49 + case_name: str
50 + custom_attributes: Optional[Any]
51 + open_date: str
52 + close_date: Optional[str]
53 + state_id: int
54 + case_description: str
55 + reviewer_id: Optional[int]
56 + closing_note: Optional[str]
57 + case_id: int
58 + modification_history: Dict[str, CaseModificationHistory]
59 + classification_id: Optional[int]
60 + review_status_id: Optional[int]
61 + user_id: int
62 + case_uuid: str
63 + case_customer: int
64 +
65 +class CaseCreationResponse(BaseModel):
66 + success: bool
67 + case: CaseData
68 + message: str
backend/app/connectors/dfir_iris/services/alerts.py
+79 -6
@@ -1,20 +1,54 @@
1 from app.connectors.dfir_iris.schema.alerts import AlertResponse
2 from app.connectors.dfir_iris.schema.alerts import AlertsResponse
3 -from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse
3 +from app.connectors.dfir_iris.schema.alerts import BookmarkedAlertsResponse, FilterAlertsRequest, CaseCreationResponse
4 from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
5 from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
6 +from loguru import logger
7 +from fastapi import HTTPException
8
9
8 -async def get_alerts() -> AlertsResponse:
10 +
11 +async def get_alerts(request: FilterAlertsRequest) -> AlertsResponse:
12 """
13 Retrieves alerts from the DFIR-IRIS service.
14
15 + Args:
16 + request (FilterAlertsRequest): The request object containing filtering criteria.
17 +
18 Returns:
19 AlertsResponse: The response object containing the fetched alerts.
20 """
15 - client, alert = await initialize_client_and_alert("DFIR-IRIS")
16 - result = await fetch_and_validate_data(client, alert.filter_alerts)
17 - return AlertsResponse(success=True, message="Successfully fetched alerts", alerts=result["data"]["alerts"])
21 + try:
22 + client, alert = await initialize_client_and_alert("DFIR-IRIS")
23 + params = construct_params(request)
24 + result = await fetch_and_validate_data(client, lambda: alert.filter_alerts(**params))
25 + logger.info(f"Successfully fetched length {len(result['data']['alerts'])} alerts")
26 + return AlertsResponse(success=True, message="Successfully fetched alerts", alerts=result["data"]["alerts"])
27 + except Exception as e:
28 + logger.error(f"Error fetching alerts: {e}")
29 + raise HTTPException(status_code=500, detail=f"Error fetching alerts: {e}")
30 +
31 +def construct_params(request: FilterAlertsRequest) -> dict:
32 + """
33 + Constructs the parameters for the alert filtering request.
34 +
35 + Args:
36 + request (FilterAlertsRequest): The request object containing filtering criteria.
37 +
38 + Returns:
39 + dict: A dictionary of parameters for the alert filtering request.
40 + """
41 + params = {
42 + 'page': request.page,
43 + 'per_page': request.per_page,
44 + 'sort': request.sort,
45 + 'alert_title': request.alert_title,
46 + # Add more parameters here as needed
47 + }
48 +
49 + # Remove parameters that have a value of None
50 + return {k: v for k, v in params.items() if v is not None}
51 +
52
53
54 async def get_alert(alert_id: str) -> AlertResponse:
@@ -34,6 +68,45 @@ async def get_alert(alert_id: str) -> AlertResponse:
68 result = await fetch_and_validate_data(client, alert.get_alert, alert_id)
69 return AlertResponse(success=True, message="Successfully fetched alert", alert=result["data"])
70
71 +async def create_case(alert_id: str) -> CaseCreationResponse:
72 + """
73 + Creates a case for an alert.
74 +
75 + Args:
76 + alert_id (str): The ID of the alert to create a case for.
77 +
78 + Returns:
79 + CaseCreationResponse: The response object containing the success status, message, and created case data.
80 + """
81 + client, alert = await initialize_client_and_alert("DFIR-IRIS")
82 + # Get the alert
83 + alert_details = await fetch_and_validate_data(client, alert.get_alert, alert_id)
84 + params = construct_case_creation_params(alert_details["data"])
85 + logger.info(f"Creating case with params {params}")
86 + result = await fetch_and_validate_data(client, lambda: alert.escalate_alert(int(alert_id), **params))
87 + logger.info(f"Successfully created case for alert: {result}")
88 + return CaseCreationResponse(success=True, message="Successfully created case for alert", case=result["data"])
89 +
90 +def construct_case_creation_params(alert_details: dict) -> dict:
91 + """
92 + Constructs the parameters for the case creation request.
93 +
94 + Args:
95 + alert_details (dict): The alert details.
96 +
97 + Returns:
98 + dict: A dictionary of parameters for the case creation request.
99 + """
100 + params = {
101 + 'case_title': alert_details["alert_title"],
102 + 'case_tags': alert_details["alert_tags"],
103 + 'escalation_note': 'Case created from CoPilot',
104 + 'iocs_import_list': [ioc['ioc_uuid'] for ioc in alert_details["iocs"]],
105 + 'assets_import_list': [asset['asset_uuid'] for asset in alert_details["assets"]],
106 + }
107 +
108 + # Replace None values with the string "None"
109 + return {k: v if v is not None else "None" for k, v in params.items()}
110
111 async def bookmark_alert(alert_id: str, bookmarked: bool) -> AlertResponse:
112 """
@@ -61,7 +134,7 @@ async def get_bookmarked_alerts() -> BookmarkedAlertsResponse:
134 Returns:
135 BookmarkedAlertsResponse: The response object containing the bookmarked alerts.
136 """
64 - alerts = await get_alerts()
137 + alerts = await get_alerts(request=FilterAlertsRequest(per_page=1000))
138 alerts = alerts.alerts
139 bookmarked_alerts = []
140 for alert in alerts:
backend/app/connectors/routes.py
+1
@@ -137,6 +137,7 @@ async def update_connector(
137 """
138 updated_connector = await ConnectorServices.update_connector_by_id(connector_id, connector, session=session)
139 if updated_connector is not None:
140 + await ConnectorServices.verify_connector_by_id(connector_id, session=session)
141 return {"connector": updated_connector, "success": True, "message": "Connector updated successfully"}
142 else:
143 raise HTTPException(status_code=404, detail=f"No connector found for ID: {connector_id}".format(connector_id=connector_id))
backend/app/connectors/services.py
+28
@@ -26,6 +26,8 @@ from app.connectors.sublime.utils.universal import verify_sublime_connection
26 from app.connectors.velociraptor.utils.universal import verify_velociraptor_connection
27 from app.connectors.wazuh_indexer.utils.universal import verify_wazuh_indexer_connection
28 from app.connectors.wazuh_manager.utils.universal import verify_wazuh_manager_connection
29 +from app.threat_intel.services.socfortress import verifiy_socfortress_threat_intel_connector
30 +from app.integrations.ask_socfortress.services.ask_socfortress import verify_ask_socfortress_connector
31
32 # from app.db.db_session import engine # Import the shared engine
33 from app.db.db_session import get_session
@@ -107,6 +109,16 @@ class WazuhWorkerProvisioningService(ConnectorServiceInterface):
109 async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
110 return await verify_wazuh_worker_provisioning_connection(connector.connector_name)
111
112 +# SOCFortress Threat Intel Service
113 +class SocfortressThreatIntelService(ConnectorServiceInterface):
114 + async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
115 + return await verifiy_socfortress_threat_intel_connector(connector.connector_name)
116 +
117 +# ASK SOCFortress Service
118 +class AskSocfortressService(ConnectorServiceInterface):
119 + async def verify_authentication(self, connector: ConnectorResponse) -> Optional[ConnectorResponse]:
120 + return await verify_ask_socfortress_connector(connector.connector_name)
121 +
122
123 # Factory function to create a service instance based on connector name
124 def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface]:
@@ -131,6 +143,8 @@ def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface
143 "InfluxDB": InfluxDBService,
144 "Grafana": GrafanaService,
145 "Wazuh Worker Provisioning": WazuhWorkerProvisioningService,
146 + "SocfortressThreatIntel": SocfortressThreatIntelService,
147 + "AskSocfortress": AskSocfortressService,
148 }
149 return service_map.get(connector_name, None)
150
@@ -210,6 +224,19 @@ class ConnectorServices:
224 service_instance = ServiceClass()
225 # If verify_authentication is an async function, you will need to await it
226 connector_response = await service_instance.verify_authentication(connector_response)
227 + # If the connector is verified, update the connector record in the database
228 + if connector_response['connectionSuccessful']:
229 + connector.connector_verified = True
230 + connector.connector_last_updated = datetime.now()
231 + session.add(connector)
232 + await session.commit()
233 + else:
234 + # If the connector is not verified, set the connector_verified field to False
235 + connector.connector_verified = False
236 + connector.connector_last_updated = datetime.now()
237 + session.add(connector)
238 + await session.commit()
239 +
240 else:
241 logger.error(f"Connector type {connector_response.connector_name} is not supported")
242 return None
@@ -267,6 +294,7 @@ class ConnectorServices:
294 session.rollback()
295 return Exception(f"Failed to update connector: {e}")
296
297 +
298 @staticmethod
299 def allowed_file(filename):
300 """
backend/app/connectors/velociraptor/routes/flows.py new
+106
@@ -0,0 +1,106 @@
1 +from typing import List
2 +
3 +from fastapi import APIRouter
4 +from fastapi import Depends
5 +from fastapi import HTTPException
6 +from fastapi import Security
7 +from loguru import logger
8 +from sqlalchemy.ext.asyncio import AsyncSession
9 +from sqlalchemy.future import select
10 +
11 +from app.auth.utils import AuthHandler
12 +from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
13 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
14 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
15 +from app.connectors.velociraptor.schema.artifacts import OSPrefixEnum
16 +from app.connectors.velociraptor.schema.artifacts import OSPrefixModel
17 +from app.connectors.velociraptor.schema.artifacts import QuarantineBody
18 +from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
19 +from app.connectors.velociraptor.schema.artifacts import RunCommandBody
20 +from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
21 +from app.connectors.velociraptor.services.artifacts import get_artifacts
22 +from app.connectors.velociraptor.services.artifacts import quarantine_host
23 +from app.connectors.velociraptor.services.artifacts import run_artifact_collection
24 +from app.connectors.velociraptor.services.artifacts import run_remote_command
25 +from app.db.db_session import get_db
26 +from app.connectors.velociraptor.schema.flows import FlowResponse, RetrieveFlowRequest
27 +from app.connectors.velociraptor.services.flows import get_flows, get_flow
28 +from app.db.universal_models import Agents
29 +
30 +velociraptor_flows_router = APIRouter()
31 +
32 +
33 +async def get_velociraptor_id(session: AsyncSession, hostname: str) -> str:
34 + """
35 + Retrieves the velociraptor_id associated with the given hostname.
36 +
37 + Args:
38 + session (AsyncSession): The database session.
39 + hostname (str): The hostname of the agent.
40 +
41 + Returns:
42 + str: The velociraptor_id associated with the hostname.
43 +
44 + Raises:
45 + HTTPException: If the agent with the given hostname is not found or if the velociraptor_id is not available.
46 + """
47 + logger.info(f"Getting velociraptor_id from hostname {hostname}")
48 + # log all the agents
49 + agents = await session.execute(select(Agents))
50 + for agent in agents.scalars().all():
51 + logger.info(f"agent: {agent}")
52 + result = await session.execute(select(Agents).filter(Agents.hostname == hostname))
53 + agent = result.scalars().first()
54 +
55 + if not agent:
56 + raise HTTPException(status_code=404, detail=f"Agent with hostname {hostname} not found")
57 +
58 + if agent.velociraptor_id == "n/a":
59 + raise HTTPException(status_code=404, detail=f"Velociraptor ID for hostname {hostname} is not available")
60 +
61 + logger.info(f"velociraptor_id for hostname {hostname} is {agent.velociraptor_id}")
62 + return agent.velociraptor_id
63 +
64 +
65 +@velociraptor_flows_router.get(
66 + "/{hostname}",
67 + response_model=FlowResponse,
68 + description="Get all artifacts for a specific host's OS prefix",
69 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
70 +)
71 +async def get_all_flows_for_hostname(hostname: str, session: AsyncSession = Depends(get_db)) -> FlowResponse:
72 + """
73 + Retrieve ran flows for a specific host.
74 +
75 + Args:
76 + hostname (str): The hostname of the host.
77 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
78 +
79 + Returns:
80 + FlowResponse: The response containing the retrieved flows.
81 + """
82 + logger.info(f"Fetching all flows for hostname {hostname}")
83 +
84 + velociraptor_id = await get_velociraptor_id(session, hostname)
85 + logger.info(f"velociraptor_id for hostname {hostname} is {velociraptor_id}")
86 + return await get_flows(velociraptor_id)
87 +
88 +@velociraptor_flows_router.post(
89 + "/retrieve",
90 + response_model=CollectArtifactResponse,
91 + description="Retrieve a flow based on the flow_id",
92 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
93 +)
94 +async def retrieve_flow(retrieve_flow_request: RetrieveFlowRequest) -> CollectArtifactResponse:
95 + """
96 + Retrieve ran flows for a specific host.
97 +
98 + Args:
99 + retrieve_flow_request (RetrieveFlowRequest): The request containing the flow_id.
100 +
101 +
102 + Returns:
103 + CollectArtifactResponse: The response containing the retrieved flows.
104 + """
105 + logger.info(f"Fetching flow for flow_id {retrieve_flow_request.session_id}")
106 + return await get_flow(retrieve_flow_request)
backend/app/connectors/velociraptor/schema/flows.py new
+106
@@ -0,0 +1,106 @@
1 +from pydantic import BaseModel, Field, root_validator
2 +from typing import List, Optional
3 +from fastapi import HTTPException
4 +from loguru import logger
5 +
6 +class FlowSpecParameter(BaseModel):
7 + key: str
8 + value: str
9 + comment: Optional[str]
10 +
11 +class FlowSpec(BaseModel):
12 + artifact: str
13 + parameters: Optional[List[FlowSpecParameter]] = Field(None, description="The parameters of the artifact.")
14 +
15 +class FlowRequest(BaseModel):
16 + creator: str
17 + user_data: str
18 + client_id: str
19 + flow_id: str
20 + urgent: bool
21 + artifacts: List[str]
22 + specs: Optional[List[FlowSpec]] = Field(None, description="The specs of the artifacts.")
23 + cpu_limit: int
24 + iops_limit: int
25 + progress_timeout: int
26 + timeout: int
27 + max_rows: int
28 + max_upload_bytes: int
29 + trace_freq_sec: int
30 + allow_custom_overrides: bool
31 + log_batch_time: int
32 + compiled_collector_args: List[str]
33 + ops_per_second: int
34 +
35 + @root_validator(pre=True)
36 + def validate_specs(cls, values):
37 + if 'specs' in values and values['specs'] is not None:
38 + validated_specs = []
39 + for spec in values['specs']:
40 + try:
41 + validated_spec = FlowSpec(**spec)
42 + validated_specs.append(validated_spec)
43 + except Exception as e:
44 + #raise HTTPException(status_code=400, detail=f"Failed to validate spec: {e}")
45 + logger.error(f"Failed to validate spec: {e}")
46 + values['specs'] = validated_specs
47 + return values
48 +
49 +class FlowQueryStat(BaseModel):
50 + status: str
51 + error_message: str
52 + backtrace: str
53 + duration: int
54 + last_active: int
55 + first_active: int
56 + names_with_response: List[str]
57 + Artifact: str
58 + log_rows: int
59 + uploaded_files: int
60 + uploaded_bytes: int
61 + expected_uploaded_bytes: int
62 + result_rows: int
63 + query_id: int
64 + total_queries: int
65 +
66 +class FlowClientSession(BaseModel):
67 + client_id: str
68 + session_id: str
69 + request: FlowRequest
70 + backtrace: str
71 + create_time: int
72 + start_time: int
73 + active_time: int
74 + total_uploaded_files: int
75 + total_expected_uploaded_bytes: int
76 + total_uploaded_bytes: int
77 + total_collected_rows: int
78 + total_logs: int
79 + total_requests: int
80 + outstanding_requests: int
81 + next_response_id: int
82 + execution_duration: int
83 + state: str
84 + status: str
85 + artifacts_with_results: List[str]
86 + query_stats: List[FlowQueryStat]
87 + uploaded_files: List[str]
88 + user_notified: bool
89 + logs: List[str]
90 + dirty: bool
91 + total_loads: int
92 +
93 +class FlowResponse(BaseModel):
94 + results: List[FlowClientSession]
95 + success: bool
96 + message: str
97 +
98 +class RetrieveFlowRequest(BaseModel):
99 + client_id: str
100 + session_id: str
101 +
102 + @root_validator(pre=True)
103 + def validate_session_id(cls, values):
104 + if 'session_id' in values and values['session_id'] == "":
105 + raise HTTPException(status_code=400, detail="The session_id cannot be an empty string")
106 + return values
backend/app/connectors/velociraptor/services/artifacts.py
-2
@@ -11,8 +11,6 @@ from app.connectors.velociraptor.schema.artifacts import RunCommandBody
11 from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
12 from app.connectors.velociraptor.utils.universal import UniversalService
13
14 -# universal_service = UniversalService()
15 -
14
15 def create_query(query: str) -> str:
16 """
backend/app/connectors/velociraptor/services/flows.py new
+77
@@ -0,0 +1,77 @@
1 +from fastapi import HTTPException
2 +from loguru import logger
3 +
4 +from app.connectors.velociraptor.schema.artifacts import Artifacts
5 +from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
6 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
7 +from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
8 +from app.connectors.velociraptor.schema.artifacts import QuarantineBody
9 +from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
10 +from app.connectors.velociraptor.schema.artifacts import RunCommandBody
11 +from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
12 +from app.connectors.velociraptor.utils.universal import UniversalService
13 +from app.connectors.velociraptor.schema.flows import FlowResponse, FlowClientSession, RetrieveFlowRequest
14 +
15 +
16 +def create_query(query: str) -> str:
17 + """
18 + Create a query string.
19 +
20 + Args:
21 + query (str): The query to be executed.
22 +
23 + Returns:
24 + str: The created query string.
25 + """
26 + return query
27 +
28 +async def get_flows(velociraptor_id: str) -> FlowResponse:
29 + """
30 + Get all artifacts from Velociraptor.
31 +
32 + Returns:
33 + ArtifactsResponse: A dictionary containing the artifacts.
34 + """
35 + logger.info("Fetching artifacts from Velociraptor")
36 + velociraptor_service = await UniversalService.create("Velociraptor")
37 + query = create_query(
38 + f"SELECT * FROM flows(client_id='{velociraptor_id}')",
39 + )
40 + all_flows = velociraptor_service.execute_query(query)
41 + logger.info(f"all_flows: {all_flows}")
42 + flows = [FlowClientSession(**flow) for flow in all_flows["results"]]
43 + logger.info(f"flows: {flows}")
44 + try:
45 + if all_flows["success"]:
46 + flows = [FlowClientSession(**flow) for flow in all_flows["results"]]
47 + logger.info(f"flows: {flows}")
48 + return FlowResponse(success=True, message="All flows retrieved.", results=flows)
49 + else:
50 + raise HTTPException(status_code=500, detail=f"Failed to retrieve flows from Velociraptor: {all_flows['message']}")
51 + except Exception as e:
52 + logger.error(f"Failed to retrieve flows from Velociraptor: {e}")
53 + raise HTTPException(status_code=500, detail=f"Failed to retrieve flows from Velociraptor: {e}")
54 +
55 +async def get_flow(retrieve_flow_request: RetrieveFlowRequest):
56 + """
57 + Get all artifacts from Velociraptor.
58 +
59 + Returns:
60 + ArtifactsResponse: A dictionary containing the artifacts.
61 + """
62 + logger.info("Fetching artifacts from Velociraptor")
63 + velociraptor_service = await UniversalService.create("Velociraptor")
64 + query = create_query(
65 + f"SELECT * FROM flow_results(client_id='{retrieve_flow_request.client_id}', flow_id='{retrieve_flow_request.session_id}')",
66 + )
67 + flow_results = velociraptor_service.execute_query(query)
68 + logger.info(f"flow_results: {flow_results}")
69 + try:
70 + if flow_results["success"]:
71 + return CollectArtifactResponse(success=flow_results["success"], message=flow_results["message"], results=flow_results["results"])
72 + else:
73 + raise HTTPException(status_code=500, detail=f"Failed to retrieve flow results from Velociraptor: {flow_results['message']}")
74 + except Exception as e:
75 + logger.error(f"Failed to retrieve flow results from Velociraptor: {e}")
76 + raise HTTPException(status_code=500, detail=f"Failed to retrieve flow results from Velociraptor: {e}")
77 +
backend/app/connectors/wazuh_manager/routes/rules.py
+5 -4
@@ -13,12 +13,12 @@ from app.connectors.wazuh_manager.schema.rules import AllDisabledRuleResponse
13 from app.connectors.wazuh_manager.schema.rules import RuleDisable
14 from app.connectors.wazuh_manager.schema.rules import RuleDisableResponse
15 from app.connectors.wazuh_manager.schema.rules import RuleEnable
16 -from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse
16 +from app.connectors.wazuh_manager.schema.rules import RuleEnableResponse, RuleExclude, RuleExcludeResponse
17
18 # from app.connectors.wazuh_manager.schema.rules import RuleExclude
19 # from app.connectors.wazuh_manager.schema.rules import RuleExcludeResponse
20 from app.connectors.wazuh_manager.services.rules import disable_rule
21 -from app.connectors.wazuh_manager.services.rules import enable_rule
21 +from app.connectors.wazuh_manager.services.rules import enable_rule, exclude_rule
22
23 # from app.connectors.wazuh_manager.services.rules import exclude_rule
24 from app.db.db_session import get_db
@@ -150,13 +150,14 @@ async def enable_wazuh_rule(rule: RuleEnable, session: AsyncSession = Depends(ge
150
151
152 # ! TODO: Implement this endpoint - Maybe use OpenAI?
153 -# @wazuh_manager_router.post(
153 +# @wazuh_manager_rules_router.post(
154 # "/rule/exclude",
155 # response_model=RuleExcludeResponse,
156 # description="Retrieve recommended exclusion for a Wazuh Rule",
157 # )
158 # async def exclude_wazuh_rule(rule: RuleExclude) -> RuleExcludeResponse:
159 -# recommended_exclusion = exclude_rule(rule)
159 +# logger.info(f"rule: {rule}")
160 +# recommended_exclusion = await exclude_rule(rule)
161 # if recommended_exclusion:
162 # return recommended_exclusion
163 # else:
backend/app/connectors/wazuh_manager/schema/rules.py
+2 -8
@@ -2,7 +2,7 @@ from typing import List
2 from typing import Optional
3
4 from pydantic import BaseModel
5 -from pydantic import Field
5 +from pydantic import Field, validator
6
7
8 class RuleDisable(BaseModel):
@@ -44,18 +44,12 @@ class AllDisabledRuleResponse(BaseModel):
44
45
46 class RuleExclude(BaseModel):
47 - rule_value: str = Field(
48 - ...,
49 - description="The value of the field trying to be exclude",
50 - example="C:\\Windows\\ServiceState\\EventLog\\Data\\lastalive1.dat",
51 - )
47 input_value: str = Field(
48 ...,
49 description="The proposed value of the field trying to be exclude that would result in an exclusiong",
55 - example="C:\\\\Windows\\\\ServiceState\\\\EventLog\\\\Data\\\\lastalive1\.dat",
50 + example="C:\\Windows\\ServiceState\\EventLog\\Data\\lastalive1.dat",
51 )
52
58 -
53 class RuleExcludeResponse(BaseModel):
54 success: bool
55 message: str
backend/app/connectors/wazuh_manager/services/rules.py
+60 -21
@@ -4,7 +4,9 @@ from typing import List
4 from typing import Tuple
5 from typing import Union
6
7 +import re
8 import pcre2
9 +from enum import Enum
10 import xmltodict
11 from fastapi import HTTPException
12 from loguru import logger
@@ -239,7 +241,37 @@ def make_pcre2_compatible(input_string: str) -> str:
241 return input_string.replace("\\", "\\\\")
242
243
242 -def exclude_rule(rule: RuleExclude) -> RuleExcludeResponse:
244 +class RegexSpecialCharacters(Enum):
245 + DOT = ('.', '\.')
246 + CARET = ('^', '\^')
247 + DOLLAR = ('$', '\$')
248 + STAR = ('*', '\*')
249 + PLUS = ('+', '\+')
250 + QUESTION = ('?', '\?')
251 + CURLY_OPEN = ('{', '\{')
252 + CURLY_CLOSE = ('}', '\}')
253 + SQUARE_OPEN = ('[', '\[')
254 + SQUARE_CLOSE = (']', '\]')
255 + SINGLE_BACKSLASH = ('\\', '\\\\')
256 + DOUBLE_BACKSLASH = ('\\\\', '\\\\\\\\')
257 + PIPE = ('|', '\|')
258 + PAREN_OPEN = ('(', '\(')
259 + PAREN_CLOSE = (')', '\)')
260 + COLON = (':', '\:')
261 + DASH = ('-', '\-')
262 +
263 +# Create a dictionary for easy lookup
264 +REGEX_REPLACE_DICT = {char.value[0]: char.value[1] for char in RegexSpecialCharacters}
265 +
266 +async def replace_special_chars(rule: RuleExclude):
267 + for char, replacement in REGEX_REPLACE_DICT.items():
268 + # Use Python's raw string notation for regular expressions
269 + pattern = re.compile(re.escape(char))
270 + input_string = pattern.sub(replacement, rule.input_value)
271 + logger.info(f"Input String: {input_string}")
272 + return input_string
273 +
274 +async def exclude_rule(rule: RuleExclude) -> RuleExcludeResponse:
275 """
276 Exclude a rule based on the provided input value and rule value.
277
@@ -252,23 +284,30 @@ def exclude_rule(rule: RuleExclude) -> RuleExcludeResponse:
284 Raises:
285 Exception: If an error occurs while excluding the rule.
286 """
255 - try:
256 - # Convert rule_value to a PCRE2 compatible regex pattern
257 - pcre2_pattern = make_pcre2_compatible(rule.rule_value)
258 -
259 - compiled_pattern = pcre2.compile(pcre2_pattern)
260 - print(f"Compiled Pattern: {compiled_pattern}") # Debugging line
261 -
262 - print(f"Input Value: {rule.input_value}") # Debugging line
263 -
264 - match_data = compiled_pattern.match(rule.input_value)
265 -
266 - if match_data:
267 - return RuleExcludeResponse(success=True, message="Successfully excluded rule", recommended_exclusion=rule.input_value)
268 - else:
269 - return RuleExcludeResponse(success=False, message="Failed to exclude rule", recommended_exclusion="")
270 -
271 - except Exception as e:
272 - print(f"Exception: {e}") # Debugging line
273 - logger.error(f"Failed to exclude rule: {e}")
274 - return RuleExcludeResponse(success=False, message=f"Failed to exclude rule: {e}", recommended_exclusion="")
287 + # Function to replace special characters
288 + # repr of the input string
289 + input_string = repr(rule.input_value)
290 + logger.info(f"Input String: {input_string}")
291 + excluded_string = await replace_special_chars(rule)
292 + logger.info(f"Excluded String: {excluded_string}")
293 + return None
294 + # try:
295 + # # Convert rule_value to a PCRE2 compatible regex pattern
296 + # pcre2_pattern = make_pcre2_compatible(rule.rule_value)
297 +
298 + # compiled_pattern = pcre2.compile(pcre2_pattern)
299 + # print(f"Compiled Pattern: {compiled_pattern}") # Debugging line
300 +
301 + # print(f"Input Value: {rule.input_value}") # Debugging line
302 +
303 + # match_data = compiled_pattern.match(rule.input_value)
304 +
305 + # if match_data:
306 + # return RuleExcludeResponse(success=True, message="Successfully excluded rule", recommended_exclusion=rule.input_value)
307 + # else:
308 + # return RuleExcludeResponse(success=False, message="Failed to exclude rule", recommended_exclusion="")
309 +
310 + # except Exception as e:
311 + # print(f"Exception: {e}") # Debugging line
312 + # logger.error(f"Failed to exclude rule: {e}")
313 + # return RuleExcludeResponse(success=False, message=f"Failed to exclude rule: {e}", recommended_exclusion="")
backend/app/db/db_populate.py
+49 -15
@@ -11,7 +11,40 @@ from app.connectors.models import Connectors
11 load_dotenv()
12
13
14 -def load_connector_data(connector_name, connector_type, accepts_key, extra_data_key=None):
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 +
47 +def load_connector_data(connector_name, connector_type, accepts_key, description, extra_data_key=None):
48 """
49 Load connector data from environment variables.
50
@@ -19,6 +52,7 @@ def load_connector_data(connector_name, connector_type, accepts_key, extra_data_
52 connector_name (str): The name of the connector.
53 connector_type (str): The type of the connector.
54 accepts_key (str): The type of key the connector accepts.
55 + description (str): The description of the connector.
56 extra_data_key (str, optional): The key for extra data. Defaults to None.
57
58 Returns:
@@ -34,7 +68,7 @@ def load_connector_data(connector_name, connector_type, accepts_key, extra_data_
68 "connector_username": os.getenv(f"{env_prefix}_USERNAME"),
69 "connector_password": os.getenv(f"{env_prefix}_PASSWORD"),
70 "connector_api_key": os.getenv(f"{env_prefix}_API_KEY"),
37 - "connector_description": os.getenv(f"{env_prefix}_DESCRIPTION", "No description available."),
71 + "connector_description": description,
72 "connector_supports": os.getenv(f"{env_prefix}_SUPPORTS", "Not specified."),
73 "connector_configured": True,
74 "connector_verified": bool(os.getenv(f"{env_prefix}_VERIFIED", False)),
@@ -53,19 +87,19 @@ def get_connectors_list():
87 list: A list of connector data, where each item contains the connector name, version, and authentication method.
88 """
89 connectors = [
56 - ("Wazuh-Indexer", "4.4.1", "username_password"),
57 - ("Wazuh-Manager", "4.4.1", "username_password"),
58 - ("Graylog", "5.0.7", "username_password"),
59 - ("Shuffle", "1.1.0", "api_key"),
60 - ("DFIR-IRIS", "2.0", "api_key"),
61 - ("Velociraptor", "0.6.8", "file"),
62 - ("Sublime", "3", "api_key"),
63 - ("InfluxDB", "3", "api_key", "INFLUXDB_ORG_AND_BUCKET"),
64 - ("AskSocfortress", "3", "api_key"),
65 - ("SocfortressThreatIntel", "3", "api_key"),
66 - ("Cortex", "3", "api_key"),
67 - ("Grafana", "3", "username_password"),
68 - ("Wazuh Worker Provisioning", "3", "api_key"),
90 + ("Wazuh-Indexer", "4.4.1", "username_password", "Connection to Wazuh-Indexer. Make sure to use the an admin role user."),
91 + ("Wazuh-Manager", "4.4.1", "username_password", "Connection to Wazuh-Manager. Default is wazuh-wui:wazuh-wui"),
92 + ("Graylog", "5.0.7", "username_password", "Connection to Graylog. Make sure to use the an admin role user."),
93 + ("Shuffle", "1.1.0", "api_key", "Connection to Shuffle. Make sure to use the an admin role user."),
94 + ("DFIR-IRIS", "2.0", "api_key", "Connection to DFIR-IRIS. Make sure to use the an admin role user."),
95 + ("Velociraptor", "0.6.8", "file", "Connection to Velociraptor. Make sure you have generated the api file first."),
96 + ("Sublime", "3", "api_key", "Connection to Sublime. Make sure to use the an admin role user."),
97 + ("InfluxDB", "3", "api_key", "Connection to InfluxDB. Make sure to use the an admin role user.", "INFLUXDB_ORG_AND_BUCKET"),
98 + ("AskSocfortress", "3", "api_key", "Connection to AskSocfortress. Make sure you have requested an API key."),
99 + ("SocfortressThreatIntel", "3", "api_key", "Connection to Socfortress Threat Intel. Make sure you have requested an API key."),
100 + ("Cortex", "3", "api_key", "Connection to Cortex. Make sure you have created an API key."),
101 + ("Grafana", "3", "username_password", "Connection to Grafana. Make sure to use the an admin role user."),
102 + ("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"),
103 # ... Add more connectors as needed ...
104 ]
105
backend/app/integrations/alert_creation/general/schema/alert.py
+11
@@ -151,6 +151,14 @@ class IrisAsset(BaseModel):
151 description="Type ID of the asset",
152 example=1,
153 )
154 + asset_tags: Optional[str] = Field(
155 + "Agent ID not found. Ensure the agent has been registered with Wazuh Manager and synced to the Agents table.",
156 + description="Tags of the asset",
157 + example="001",
158 + )
159 +
160 + def to_dict(self):
161 + return self.dict(exclude_none=True)
162
163
164 class IrisIoc(BaseModel):
@@ -167,6 +175,9 @@ class IrisIoc(BaseModel):
175 ioc_tlp_id: int = Field(1, description="TLP ID of the IoC", example=1)
176 ioc_type_id: int = Field(20, description="Type ID of the IoC", example=20)
177
178 + def to_dict(self):
179 + return self.dict(exclude_none=True)
180 +
181
182 class IrisAlertContext(BaseModel):
183 customer_iris_id: int = Field(
backend/app/integrations/alert_creation/general/services/alert.py
+17
@@ -100,12 +100,14 @@ async def build_asset_payload(agent_data: AgentsResponse, alert_details) -> Iris
100 Returns:
101 IrisAsset: The constructed IrisAsset object.
102 """
103 + # Get the agent_id based on the hostname from the Agents table
104 if agent_data.success:
105 return IrisAsset(
106 asset_name=agent_data.agents[0].hostname,
107 asset_ip=agent_data.agents[0].ip_address,
108 asset_description=agent_data.agents[0].os,
109 asset_type_id=await get_asset_type_id(agent_data.agents[0].os),
110 + asset_tags=agent_data.agents[0].agent_id,
111 )
112 return IrisAsset()
113
@@ -259,6 +261,21 @@ async def create_alert(alert: CreateAlertRequest, session: AsyncSession) -> Crea
261 )
262 alert_id = result["data"]["alert_id"]
263 logger.info(f"Successfully created alert {alert_id} in IRIS.")
264 + # Update the alert with the asset payload
265 + await fetch_and_validate_data(
266 + client,
267 + alert_client.update_alert,
268 + alert_id,
269 + {"assets": [dict(IrisAsset(**iris_alert_payload.assets[0].to_dict()))]},
270 + )
271 + # Updae the alert if the ioc_payload is not None
272 + if ioc_payload:
273 + await fetch_and_validate_data(
274 + client,
275 + alert_client.update_alert,
276 + alert_id,
277 + {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
278 + )
279 customer_name = (await get_customer_alert_settings(customer_code=alert.agent_labels_customer, session=session)).customer_name
280 await send_to_shuffle(
281 ShufflePayload(
backend/app/integrations/ask_socfortress/services/ask_socfortress.py
+52 -1
@@ -1,4 +1,6 @@
1 from typing import Optional
2 +from typing import Dict
3 +from typing import Any
4
5 import httpx
6 from fastapi import HTTPException
@@ -19,7 +21,8 @@ from app.integrations.ask_socfortress.schema.ask_socfortress import (
21 AskSocfortressSigmaResponse,
22 )
23 from app.utils import get_connector_attribute
22 -
24 +from app.db.db_session import get_db_session
25 +from app.connectors.utils import get_connector_info_from_db
26
27 async def get_single_alert_details(alert_details: CreateAlertRequest) -> GenericAlertModel:
28 """
@@ -68,6 +71,54 @@ async def get_ask_socfortress_attributes(column_name: str, session: AsyncSession
71 return attribute_value
72
73
74 +async def verify_ask_socfortress_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
75 + """
76 + Verifies the Ask SocFortress credentials.
77 +
78 + Args:
79 + attributes (Dict[str, Any]): The connector attributes.
80 +
81 + Returns:
82 + Dict[str, Any]: The connector attributes.
83 +
84 + Raises:
85 + HTTPException: Raised if the Ask SocFortress credentials are invalid.
86 + """
87 + api_key = attributes.get("connector_api_key", None)
88 + url = attributes.get("connector_url", None)
89 + if api_key is None or url is None:
90 + logger.error("No Ask Socfortress credentials found in the database")
91 + raise HTTPException(status_code=500, detail="Ask Socfortress credentials not found in the database")
92 + return attributes
93 +
94 +async def verify_ask_socfortress_connector(connector_name: str) -> str:
95 + """
96 + Verifies the Ask SocFortress connector.
97 +
98 + Args:
99 + connector_name (str): The name of the connector.
100 +
101 + Returns:
102 + str: The connector name.
103 +
104 + Raises:
105 + HTTPException: Raised if the connector name is not Ask SocFortress.
106 + """
107 + logger.info("Verifying Ask Socfortress connector")
108 + async with get_db_session() as session: # This will correctly enter the context manager
109 + attributes = await get_connector_info_from_db(connector_name, session)
110 + if attributes is None:
111 + logger.error("No Ask Socfortress connector found in the database")
112 + return None
113 + request = AskSocfortressSigmaRequest(sigma_rule_name="Process Explorer Driver Creation By Non-Sysinternals Binary")
114 + response = await invoke_ask_socfortress_api(attributes["connector_api_key"], attributes["connector_url"], request)
115 + if response["message"] != "Forbidden":
116 + logger.info("Ask Socfortress connector verified successfully")
117 + return {"connectionSuccessful": True, "message": "Successfully verified ASK SOCFortress connector"}
118 + else:
119 + logger.error("Failed to verify Ask Socfortress connector")
120 + return {"connectionSuccessful": False, "message": "Failed to verify ASK SOCFortress connector"}
121 +
122 async def invoke_ask_socfortress_api(api_key: str, url: str, request: AskSocfortressSigmaRequest) -> dict:
123 """
124 Invokes the Socfortress Threat Intel API with the provided API key, URL, and request parameters.
backend/app/middleware/logger.py
+4 -1
@@ -55,7 +55,10 @@ async def handle_exception(e, user_id, request, logger_instance):
55 Returns:
56 JSONResponse: The response containing the error message.
57 """
58 - user_id = await logger_instance.get_user_id_from_request(request) if user_id is None else user_id
58 + try:
59 + user_id = await logger_instance.get_user_id_from_request(request) if user_id is None else user_id
60 + except HTTPException as http_exc:
61 + return JSONResponse(status_code=http_exc.status_code, content={"message": str(http_exc), "success": False})
62 await logger_instance.log_error(user_id, request, e)
63 status_code = e.status_code if isinstance(e, HTTPException) else INTERNAL_SERVER_ERROR
64 return JSONResponse(status_code=status_code, content={"message": str(e), "success": False})
backend/app/routers/velociraptor.py
+2
@@ -1,9 +1,11 @@
1 from fastapi import APIRouter
2
3 from app.connectors.velociraptor.routes.artifacts import velociraptor_artifacts_router
4 +from app.connectors.velociraptor.routes.flows import velociraptor_flows_router
5
6 # Instantiate the APIRouter
7 router = APIRouter()
8
9 # Include the Velociraptor related routes
10 router.include_router(velociraptor_artifacts_router, prefix="/artifacts", tags=["velociraptor-artifacts"])
11 +router.include_router(velociraptor_flows_router, prefix="/flows", tags=["velociraptor-flows"])
backend/app/threat_intel/services/socfortress.py
+53
@@ -1,12 +1,17 @@
1 import httpx
2 from fastapi import HTTPException
3 from loguru import logger
4 +from typing import Any
5 +from typing import Dict
6 +from typing import Optional
7 from sqlalchemy.ext.asyncio import AsyncSession
8
9 from app.threat_intel.schema.socfortress import IoCMapping
10 from app.threat_intel.schema.socfortress import IoCResponse
11 from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
12 +from app.connectors.utils import get_connector_info_from_db
13 from app.utils import get_connector_attribute
14 +from app.db.db_session import get_db_session
15
16
17 async def get_socfortress_threat_intel_attributes(column_name: str, session: AsyncSession) -> str:
@@ -31,6 +36,54 @@ async def get_socfortress_threat_intel_attributes(column_name: str, session: Asy
36 raise HTTPException(status_code=500, detail="SocFortress Threat Intel attributes not found in the database.")
37 return attribute_value
38
39 +async def verify_socfortress_threat_intel_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
40 + """
41 + Verifies the SOCFortress Threat Intel credentials.
42 +
43 + Args:
44 + attributes (Dict[str, Any]): The connector attributes.
45 +
46 + Returns:
47 + Dict[str, Any]: The connector attributes.
48 +
49 + Raises:
50 + HTTPException: Raised if the SOCFortress Threat Intel credentials are invalid.
51 + """
52 + api_key = attributes.get("connector_api_key", None)
53 + url = attributes.get("connector_url", None)
54 + if api_key is None or url is None:
55 + logger.error("No SOCFortress Threat Intel credentials found in the database")
56 + raise HTTPException(status_code=500, detail="SOCFortress Threat Intel credentials not found in the database")
57 + return attributes
58 +
59 +async def verifiy_socfortress_threat_intel_connector(connector_name: str) -> str:
60 + """
61 + Verifies the SOCFortress Threat Intel connector.
62 +
63 + Args:
64 + connector_name (str): The name of the connector.
65 +
66 + Returns:
67 + str: The connector name.
68 +
69 + Raises:
70 + HTTPException: Raised if the connector name is not SOCFortress Threat Intel.
71 + """
72 + logger.info("Verifying SOCFortress Threat Intel connector")
73 + async with get_db_session() as session: # This will correctly enter the context manager
74 + attributes = await get_connector_info_from_db(connector_name, session)
75 + if attributes is None:
76 + logger.error("No SOCFortress Threat Intel connector found in the database")
77 + return None
78 + request = SocfortressThreatIntelRequest(ioc_value="evil.socfortress.co", customer_code="00001")
79 + response = await invoke_socfortress_threat_intel_api(attributes["connector_api_key"], attributes["connector_url"], request)
80 + if 'data' in response and response['data'].get('comment') == 'This is a test IoC':
81 + logger.info("Verified SOCFortress Threat Intel connector")
82 + return {"connectionSuccessful": True, "message": "Successfully verified SOCFortress Threat Intel connector"}
83 + else:
84 + logger.error("Failed to verify SOCFortress Threat Intel connector")
85 + return {"connectionSuccessful": False, "message": "Failed to verify SOCFortress Threat Intel connector"}
86 +
87
88 async def invoke_socfortress_threat_intel_api(api_key: str, url: str, request: SocfortressThreatIntelRequest) -> dict:
89 """
backend/app/utils.py
+7 -3
@@ -65,7 +65,7 @@ class ErrorType(str, Enum):
65 class ValidationErrorItem(BaseModel):
66 field: str
67 error_type: ErrorType
68 - message: str = None # Initialize as None or some default
68 + message: str = None # Initialize as None
69
70 @validator("message", pre=True, always=True)
71 def set_message(cls, value, values):
@@ -221,7 +221,10 @@ class Logger:
221 """
222 auth_header = request.headers.get("Authorization")
223 if auth_header:
224 - token = auth_header.split(" ")[1] # Better split by space and take the second part
224 + try:
225 + token = auth_header.split(" ")[1] # Better split by space and take the second part
226 + except IndexError:
227 + raise HTTPException(status_code=401, detail="Invalid token")
228 username, _ = self.auth_handler.decode_token(token)
229 user = await find_user(username) # Correctly using await for an async call
230 if user:
@@ -520,6 +523,7 @@ async def purge_logs_by_time_range(time_range: TimeRangeModel, session: AsyncSes
523 raise HTTPException(status_code=404, detail="No logs found")
524
525
526 +
527 ################## ! ALLOWED FILES ! ##################
528 def allowed_file(filename):
529 """
@@ -614,7 +618,7 @@ async def verify_wazuh_worker_provisioning_healtcheck(attributes: Dict[str, Any]
618
619 try:
620 wazuh_worker_provisioning_healthcheck = requests.get(
617 - f"{attributes['connector_url']}/healthcheck",
621 + f"{attributes['connector_url']}/provision_worker/healthcheck",
622 verify=False,
623 )
624
backend/copilot.py
+7 -3
@@ -4,6 +4,8 @@ from fastapi import HTTPException
4 from fastapi.exceptions import RequestValidationError
5 from fastapi.middleware.cors import CORSMiddleware
6 from loguru import logger
7 +import os
8 +from dotenv import load_dotenv
9
10 from app.auth.utils import AuthHandler
11 from app.db.db_session import async_engine
@@ -42,7 +44,9 @@ from app.routers import wazuh_manager
44 from app.schedulers.scheduler import init_scheduler
45
46 auth_handler = AuthHandler()
45 -
47 +# Get the `SERVER_IP` from the `.env` file
48 +load_dotenv()
49 +server_ip = os.getenv("SERVER_IP", "localhost")
50
51 app = FastAPI(description="CoPilot API", version="0.1.0", title="CoPilot API")
52
@@ -111,7 +115,7 @@ async def init_db():
115
116 @app.get("/")
117 def hello():
114 - return {"message": "Hello World"}
118 + return {"message": "We Made It!"}
119
120
121 @app.on_event("shutdown")
@@ -126,4 +130,4 @@ async def shutdown_scheduler():
130
131
132 if __name__ == "__main__":
129 - uvicorn.run(app, host="localhost", port=5000)
133 + uvicorn.run(app, host=server_ip, port=5000)
backend/requirements.txt new
+171
@@ -0,0 +1,171 @@
1 +aiocsv==1.2.5
2 +aiofiles==23.2.1
3 +aiohttp==3.8.5
4 +aiosignal==1.3.1
5 +aiosqlite==0.19.0
6 +amqp==5.1.1
7 +annotated-types==0.6.0
8 +antlr4-python3-runtime==4.9.3
9 +anyio==3.7.1
10 +appdirs==1.4.4
11 +APScheduler==3.10.4
12 +arrow==1.3.0
13 +async-timeout==4.0.3
14 +attrs==23.1.0
15 +bcrypt==4.0.1
16 +billiard==4.1.0
17 +blueprint==3.4.2
18 +cattrs==23.1.2
19 +celery==5.3.4
20 +certifi==2023.7.22
21 +cffi==1.16.0
22 +charset-normalizer==3.3.0
23 +click==8.1.7
24 +click-didyoumean==0.3.0
25 +click-plugins==1.1.1
26 +click-repl==0.3.0
27 +colorama==0.4.6
28 +colour==0.1.5
29 +contourpy==1.1.1
30 +cortex4py==2.1.0
31 +cpe==1.2.1
32 +cryptography==41.0.4
33 +cybox==2.1.0.21
34 +cycler==0.12.0
35 +deepdiff==6.5.0
36 +Deprecated==1.2.14
37 +dfir-iris-client==2.0.1
38 +dnspython==2.4.2
39 +dnstwist==20230918
40 +drawsvg==2.3.0
41 +ecdsa==0.18.0
42 +elasticsearch7==7.10.1
43 +email-validator==2.0.0.post2
44 +environs==9.5.0
45 +et-xmlfile==1.1.0
46 +fastapi==0.103.2
47 +fonttools==4.43.0
48 +fqdn==1.5.1
49 +frozenlist==1.4.0
50 +grafana-client==3.10.0
51 +greenlet==3.0.0
52 +grpcio==1.59.0
53 +grpcio-tools==1.59.0
54 +h11==0.14.0
55 +httpcore==0.18.0
56 +httptools==0.6.1
57 +httpx==0.25.0
58 +idna==3.4
59 +influxdb-client==1.38.0
60 +iniconfig==2.0.0
61 +isoduration==20.11.0
62 +itsdangerous==2.1.2
63 +Jinja2==3.1.2
64 +jsonpointer==2.4
65 +jsonschema==4.17.3
66 +kiwisolver==1.4.5
67 +kombu==5.3.2
68 +libmagic==1.0
69 +loguru==0.7.2
70 +lxml==4.9.3
71 +maec==4.1.0.17
72 +Markdown==3.4.4
73 +markdown-it-py==3.0.0
74 +MarkupSafe==2.1.3
75 +marshmallow==3.20.1
76 +marshmallow-sqlalchemy==0.29.0
77 +matplotlib==3.8.0
78 +mdurl==0.1.2
79 +mitreattack-python==2.0.14
80 +mixbox==1.0.5
81 +multidict==6.0.4
82 +netaddr==0.9.0
83 +numpy==1.26.0
84 +openai==0.28.1
85 +openpyxl==3.1.2
86 +ordered-set==4.1.0
87 +orjson==3.9.9
88 +packaging==23.2
89 +pandas==2.1.1
90 +passlib==1.7.4
91 +pcre2==0.3.0
92 +pika==1.3.2
93 +Pillow==10.0.1
94 +pip==22.3.1
95 +platformdirs==3.11.0
96 +pluggy==1.3.0
97 +pluralizer==1.2.0
98 +pooch==1.7.0
99 +prompt-toolkit==3.0.39
100 +protobuf==4.24.4
101 +psycopg2-binary==2.9.9
102 +pyasn1==0.5.0
103 +pycountry==22.3.5
104 +pycparser==2.21
105 +# pydantic==1.10.13
106 +# pydantic_core==2.10.1
107 +# pydantic-extra-types==2.1.0
108 +# pydantic-settings==2.0.3
109 +Pygments==2.16.1
110 +PyJWT==2.8.0
111 +pyparsing==3.1.1
112 +pyrsistent==0.19.3
113 +pytest==7.4.2
114 +python-dateutil==2.8.2
115 +python-dotenv==1.0.0
116 +python-jose==3.3.0
117 +python-magic==0.4.27
118 +python-multipart==0.0.6
119 +pytz==2023.3.post1
120 +pyvelociraptor==0.1.8
121 +PyYAML==6.0.1
122 +reactivex==4.0.4
123 +redis==4.6.0
124 +regex==2023.10.3
125 +reportlab==4.0.5
126 +requests==2.31.0
127 +requests-cache==1.1.0
128 +rfc3339-validator==0.1.4
129 +rfc3986-validator==0.1.1
130 +rich==13.6.0
131 +rsa==4.9
132 +setuptools==65.5.0
133 +simplejson==3.19.1
134 +six==1.16.0
135 +sniffio==1.3.0
136 +SQLAlchemy==1.4.41
137 +sqlalchemy2-stubs==0.0.2a35
138 +sqlmodel==0.0.8
139 +starlette==0.27.0
140 +stix==1.2.0.11
141 +stix2==3.0.1
142 +stix2-elevator==4.1.7
143 +stix2-patterns==2.0.0
144 +stix2-validator==3.1.4
145 +stixmarx==1.0.8
146 +tabulate==0.9.0
147 +taxii2-client==2.3.0
148 +tqdm==4.66.1
149 +typer==0.9.0
150 +types-python-dateutil==2.8.19.14
151 +typing==3.7.4.3
152 +typing_extensions==4.8.0
153 +tzdata==2023.3
154 +tzlocal==5.2
155 +ujson==5.8.0
156 +uri-template==1.3.0
157 +url-normalize==1.4.3
158 +urllib3==1.26.17
159 +uvicorn==0.23.2
160 +verlib2==0.2.0
161 +vine==5.1.0
162 +watchfiles==0.21.0
163 +wcwidth==0.2.9
164 +weakrefmethod==1.0.3
165 +webcolors==1.13
166 +websockets==11.0.3
167 +Werkzeug==3.0.0
168 +wrapt==1.15.0
169 +XlsxWriter==3.1.6
170 +xmltodict==0.13.0
171 +yarl==1.9.2
backend/settings.py
+1 -2
@@ -16,7 +16,7 @@ logger.info(f"Loading environment from {Path(__file__).parent.parent / '.env'}")
16
17
18 basedir = Path().absolute()
19 -db_path = str(basedir / "copilot.db")
19 +db_path = str(basedir / "data" / "copilot.db")
20
21 ENV = env.str("SECRET_KEY", default="production")
22 DEBUG = env.bool("FLASK_DEBUG", default=False)
@@ -27,4 +27,3 @@ SQLALCHEMY_TRACK_MODIFICATIONS = env.bool(
27 "SQLALCHEMY_TRACK_MODIFICATIONS",
28 default=False,
29 )
30 -UPLOAD_FOLDER = env.str("UPLOAD_FOLDER", str(Path.home() / "Desktop/copilot_uploads"))
docker-compose.yml new
+46
@@ -0,0 +1,46 @@
1 +version: '3.8'
2 +services:
3 + app:
4 + image: ghcr.io/socfortress/copilot:latest
5 + volumes:
6 + - ./backend/data:/opt/copilot/backend/data
7 + network_mode: "host"
8 + environment:
9 + SERVER_IP: ${SERVER_IP}
10 + VITE_API_URL: ${VITE_API_URL}
11 + VITE_TOKEN_DEBOUNCE_TIME: ${VITE_TOKEN_DEBOUNCE_TIME}
12 + VITE_UNCOMMITTED_JOURNAL_ENTRIES_THRESHOLD: ${VITE_UNCOMMITTED_JOURNAL_ENTRIES_THRESHOLD}
13 + VITE_HEALTHCHECKS_INTERVAL: ${VITE_HEALTHCHECKS_INTERVAL}
14 + WAZUH_INDEXER_URL: ${WAZUH_INDEXER_URL}
15 + WAZUH_INDEXER_USERNAME: ${WAZUH_INDEXER_USERNAME}
16 + WAZUH_INDEXER_PASSWORD: ${WAZUH_INDEXER_PASSWORD}
17 + WAZUH_MANAGER_URL: ${WAZUH_MANAGER_URL}
18 + WAZUH_MANAGER_USERNAME: ${WAZUH_MANAGER_USERNAME}
19 + WAZUH_MANAGER_PASSWORD: ${WAZUH_MANAGER_PASSWORD}
20 + GRAYLOG_URL: ${GRAYLOG_URL}
21 + GRAYLOG_USERNAME: ${GRAYLOG_USERNAME}
22 + GRAYLOG_PASSWORD: ${GRAYLOG_PASSWORD}
23 + SHUFFLE_URL: ${SHUFFLE_URL}
24 + SHUFFLER_API_KEY: ${SHUFFLER_API_KEY}
25 + DFIR_IRIS_URL: ${DFIR_IRIS_URL}
26 + DFIR_IRIS_API_KEY: ${DFIR_IRIS_API_KEY}
27 + VELOCIRAPTOR_URL: ${VELOCIRAPTOR_URL}
28 + VELOCIRAPTOR_API_KEY_PATH: ${VELOCIRAPTOR_API_KEY_PATH}
29 + SUBLIME_URL: ${SUBLIME_URL}
30 + SUBLIME_API_KEY: ${SUBLIME_API_KEY}
31 + INFLUXDB_URL: ${INFLUXDB_URL}
32 + INFLUXDB_API_KEY: ${INFLUXDB_API_KEY}
33 + INFLUXDB_ORG_AND_BUCKET: ${INFLUXDB_ORG_AND_BUCKET}
34 + ASK_SOCFORTRESS_URL: ${ASK_SOCFORTRESS_URL}
35 + ASK_SOCFORTRESS_API_KEY: ${ASK_SOCFORTRESS_API_KEY}
36 + SOCFORTRESS_THREAT_INTEL_URL: ${SOCFORTRESS_THREAT_INTEL_URL}
37 + SOCFORTRESS_THREAT_INTEL_API_KEY: ${SOCFORTRESS_THREAT_INTEL_API_KEY}
38 + CORTEX_URL: ${CORTEX_URL}
39 + CORTEX_API_KEY: ${CORTEX_API_KEY}
40 + GRAFANA_URL: ${GRAFANA_URL}
41 + GRAFANA_USERNAME: ${GRAFANA_USERNAME}
42 + GRAFANA_PASSWORD: ${GRAFANA_PASSWORD}
43 + WAZUH_WORKER_PROVISIONING_URL: ${WAZUH_WORKER_PROVISIONING_URL}
44 + ports:
45 + - "5000:5000"
46 + - "5173:5173"
package-lock.json
+483 -442
@@ -26,16 +26,16 @@
26 "js-md5": "^0.8.3",
27 "lodash": "^4.17.21",
28 "mitt": "^3.0.1",
29 - "naive-ui": "^2.36.0",
29 + "naive-ui": "^2.37.3",
30 "password-validator": "^5.3.0",
31 "pinia": "^2.1.7",
32 "pinia-plugin-persistedstate": "^3.2.1",
33 "secure-ls": "^1.2.6",
34 "validator": "^13.11.0",
35 - "vue": "^3.4.0",
35 + "vue": "^3.4.7",
36 "vue-advanced-cropper": "^2.8.8",
37 "vue-highlight-words": "^3.0.1",
38 - "vue-i18n": "^9.8.0",
38 + "vue-i18n": "^9.9.0",
39 "vue-router": "^4.2.5",
40 "vue-sjv": "^0.0.6",
41 "vue3-apexcharts": "^1.4.4",
@@ -50,9 +50,9 @@
50 "@types/inquirer": "^9.0.7",
51 "@types/jsdom": "^21.1.6",
52 "@types/lodash": "^4.14.202",
53 - "@types/node": "^20.10.5",
54 - "@types/validator": "^13.11.7",
55 - "@vitejs/plugin-vue": "^5.0.0",
53 + "@types/node": "^20.10.8",
54 + "@types/validator": "^13.11.8",
55 + "@vitejs/plugin-vue": "^5.0.3",
56 "@vitejs/plugin-vue-jsx": "^3.1.0",
57 "@vue/eslint-config-prettier": "^9.0.0",
58 "@vue/eslint-config-typescript": "^12.0.0",
@@ -64,29 +64,29 @@
64 "eslint-plugin-cypress": "^2.15.1",
65 "eslint-plugin-vue": "^9.19.2",
66 "fs-extra": "^11.2.0",
67 - "jsdom": "^23.0.1",
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.32",
71 + "postcss": "^8.4.33",
72 "prettier": "^3.1.1",
73 - "sass": "^1.69.6",
73 + "sass": "^1.69.7",
74 "start-server-and-test": "^2.0.3",
75 "tailwind-config-viewer": "^1.7.3",
76 - "tailwindcss": "^3.4.0",
76 + "tailwindcss": "^3.4.1",
77 "taze": "^0.13.1",
78 "ts-node": "^10.9.2",
79 - "typescript": "~5.3.3",
79 + "typescript": "~5.2.2",
80 "unplugin-vue-components": "^0.26.0",
81 - "vite": "^5.0.10",
82 - "vite-bundle-analyzer": "^0.5.0",
81 + "vite": "^5.0.11",
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.0",
85 + "vitest": "^1.1.3",
86 "vue-tsc": "^1.8.27"
87 },
88 "engines": {
89 - "node": ">=16.0.0 <=20.7.0"
89 + "node": ">=18.0.0"
90 }
91 },
92 "node_modules/@aashutoshrathi/word-wrap": {
@@ -155,6 +155,17 @@
155 "url": "https://github.com/sponsors/antfu"
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==",
162 + "dev": true,
163 + "dependencies": {
164 + "bidi-js": "^1.0.3",
165 + "css-tree": "^2.3.1",
166 + "is-potential-custom-element-name": "^1.0.1"
167 + }
168 + },
169 "node_modules/@babel/code-frame": {
170 "version": "7.23.5",
171 "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
@@ -178,9 +189,9 @@
189 }
190 },
191 "node_modules/@babel/core": {
181 - "version": "7.23.6",
182 - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.6.tgz",
183 - "integrity": "sha512-FxpRyGjrMJXh7X3wGLGhNDCRiwpWEF74sKjTLDJSG5Kyvow3QZaG0Adbqzi9ZrVjTWpsX+2cxWXD71NMg93kdw==",
192 + "version": "7.23.7",
193 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz",
194 + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==",
195 "dev": true,
196 "dependencies": {
197 "@ampproject/remapping": "^2.2.0",
@@ -188,10 +199,10 @@
199 "@babel/generator": "^7.23.6",
200 "@babel/helper-compilation-targets": "^7.23.6",
201 "@babel/helper-module-transforms": "^7.23.3",
191 - "@babel/helpers": "^7.23.6",
202 + "@babel/helpers": "^7.23.7",
203 "@babel/parser": "^7.23.6",
204 "@babel/template": "^7.22.15",
194 - "@babel/traverse": "^7.23.6",
205 + "@babel/traverse": "^7.23.7",
206 "@babel/types": "^7.23.6",
207 "convert-source-map": "^2.0.0",
208 "debug": "^4.1.0",
@@ -251,9 +262,9 @@
262 }
263 },
264 "node_modules/@babel/helper-create-class-features-plugin": {
254 - "version": "7.23.6",
255 - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.6.tgz",
256 - "integrity": "sha512-cBXU1vZni/CpGF29iTu4YRbOZt3Wat6zCoMDxRF1MayiEc4URxOj31tT65HUM0CRpMowA3HCJaAOVOUnMf96cw==",
265 + "version": "7.23.7",
266 + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz",
267 + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==",
268 "dev": true,
269 "dependencies": {
270 "@babel/helper-annotate-as-pure": "^7.22.5",
@@ -452,13 +463,13 @@
463 }
464 },
465 "node_modules/@babel/helpers": {
455 - "version": "7.23.6",
456 - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.6.tgz",
457 - "integrity": "sha512-wCfsbN4nBidDRhpDhvcKlzHWCTlgJYUUdSJfzXb2NuBssDSIjc3xcb+znA7l+zYsFljAcGM0aFkN40cR3lXiGA==",
466 + "version": "7.23.8",
467 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz",
468 + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==",
469 "dev": true,
470 "dependencies": {
471 "@babel/template": "^7.22.15",
461 - "@babel/traverse": "^7.23.6",
472 + "@babel/traverse": "^7.23.7",
473 "@babel/types": "^7.23.6"
474 },
475 "engines": {
@@ -539,9 +550,9 @@
550 }
551 },
552 "node_modules/@babel/runtime": {
542 - "version": "7.23.6",
543 - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz",
544 - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==",
553 + "version": "7.23.8",
554 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
555 + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==",
556 "dependencies": {
557 "regenerator-runtime": "^0.14.0"
558 },
@@ -564,9 +575,9 @@
575 }
576 },
577 "node_modules/@babel/traverse": {
567 - "version": "7.23.6",
568 - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz",
569 - "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==",
578 + "version": "7.23.7",
579 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz",
580 + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==",
581 "dev": true,
582 "dependencies": {
583 "@babel/code-frame": "^7.23.5",
@@ -737,9 +748,9 @@
748 "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
749 },
750 "node_modules/@esbuild/aix-ppc64": {
740 - "version": "0.19.10",
741 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.10.tgz",
742 - "integrity": "sha512-Q+mk96KJ+FZ30h9fsJl+67IjNJm3x2eX+GBWGmocAKgzp27cowCOOqSdscX80s0SpdFXZnIv/+1xD1EctFx96Q==",
751 + "version": "0.19.11",
752 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
753 + "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==",
754 "cpu": [
755 "ppc64"
756 ],
@@ -753,9 +764,9 @@
764 }
765 },
766 "node_modules/@esbuild/android-arm": {
756 - "version": "0.19.10",
757 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.10.tgz",
758 - "integrity": "sha512-7W0bK7qfkw1fc2viBfrtAEkDKHatYfHzr/jKAHNr9BvkYDXPcC6bodtm8AyLJNNuqClLNaeTLuwURt4PRT9d7w==",
767 + "version": "0.19.11",
768 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz",
769 + "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==",
770 "cpu": [
771 "arm"
772 ],
@@ -769,9 +780,9 @@
780 }
781 },
782 "node_modules/@esbuild/android-arm64": {
772 - "version": "0.19.10",
773 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.10.tgz",
774 - "integrity": "sha512-1X4CClKhDgC3by7k8aOWZeBXQX8dHT5QAMCAQDArCLaYfkppoARvh0fit3X2Qs+MXDngKcHv6XXyQCpY0hkK1Q==",
783 + "version": "0.19.11",
784 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz",
785 + "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==",
786 "cpu": [
787 "arm64"
788 ],
@@ -785,9 +796,9 @@
796 }
797 },
798 "node_modules/@esbuild/android-x64": {
788 - "version": "0.19.10",
789 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.10.tgz",
790 - "integrity": "sha512-O/nO/g+/7NlitUxETkUv/IvADKuZXyH4BHf/g/7laqKC4i/7whLpB0gvpPc2zpF0q9Q6FXS3TS75QHac9MvVWw==",
799 + "version": "0.19.11",
800 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz",
801 + "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==",
802 "cpu": [
803 "x64"
804 ],
@@ -801,9 +812,9 @@
812 }
813 },
814 "node_modules/@esbuild/darwin-arm64": {
804 - "version": "0.19.10",
805 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz",
806 - "integrity": "sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA==",
815 + "version": "0.19.11",
816 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz",
817 + "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==",
818 "cpu": [
819 "arm64"
820 ],
@@ -817,9 +828,9 @@
828 }
829 },
830 "node_modules/@esbuild/darwin-x64": {
820 - "version": "0.19.10",
821 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.10.tgz",
822 - "integrity": "sha512-alfGtT+IEICKtNE54hbvPg13xGBe4GkVxyGWtzr+yHO7HIiRJppPDhOKq3zstTcVf8msXb/t4eavW3jCDpMSmA==",
831 + "version": "0.19.11",
832 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz",
833 + "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==",
834 "cpu": [
835 "x64"
836 ],
@@ -833,9 +844,9 @@
844 }
845 },
846 "node_modules/@esbuild/freebsd-arm64": {
836 - "version": "0.19.10",
837 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.10.tgz",
838 - "integrity": "sha512-dMtk1wc7FSH8CCkE854GyGuNKCewlh+7heYP/sclpOG6Cectzk14qdUIY5CrKDbkA/OczXq9WesqnPl09mj5dg==",
847 + "version": "0.19.11",
848 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz",
849 + "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==",
850 "cpu": [
851 "arm64"
852 ],
@@ -849,9 +860,9 @@
860 }
861 },
862 "node_modules/@esbuild/freebsd-x64": {
852 - "version": "0.19.10",
853 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.10.tgz",
854 - "integrity": "sha512-G5UPPspryHu1T3uX8WiOEUa6q6OlQh6gNl4CO4Iw5PS+Kg5bVggVFehzXBJY6X6RSOMS8iXDv2330VzaObm4Ag==",
863 + "version": "0.19.11",
864 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz",
865 + "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==",
866 "cpu": [
867 "x64"
868 ],
@@ -865,9 +876,9 @@
876 }
877 },
878 "node_modules/@esbuild/linux-arm": {
868 - "version": "0.19.10",
869 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.10.tgz",
870 - "integrity": "sha512-j6gUW5aAaPgD416Hk9FHxn27On28H4eVI9rJ4az7oCGTFW48+LcgNDBN+9f8rKZz7EEowo889CPKyeaD0iw9Kg==",
879 + "version": "0.19.11",
880 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz",
881 + "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==",
882 "cpu": [
883 "arm"
884 ],
@@ -881,9 +892,9 @@
892 }
893 },
894 "node_modules/@esbuild/linux-arm64": {
884 - "version": "0.19.10",
885 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.10.tgz",
886 - "integrity": "sha512-QxaouHWZ+2KWEj7cGJmvTIHVALfhpGxo3WLmlYfJ+dA5fJB6lDEIg+oe/0//FuyVHuS3l79/wyBxbHr0NgtxJQ==",
895 + "version": "0.19.11",
896 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz",
897 + "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==",
898 "cpu": [
899 "arm64"
900 ],
@@ -897,9 +908,9 @@
908 }
909 },
910 "node_modules/@esbuild/linux-ia32": {
900 - "version": "0.19.10",
901 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.10.tgz",
902 - "integrity": "sha512-4ub1YwXxYjj9h1UIZs2hYbnTZBtenPw5NfXCRgEkGb0b6OJ2gpkMvDqRDYIDRjRdWSe/TBiZltm3Y3Q8SN1xNg==",
911 + "version": "0.19.11",
912 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz",
913 + "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==",
914 "cpu": [
915 "ia32"
916 ],
@@ -913,9 +924,9 @@
924 }
925 },
926 "node_modules/@esbuild/linux-loong64": {
916 - "version": "0.19.10",
917 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.10.tgz",
918 - "integrity": "sha512-lo3I9k+mbEKoxtoIbM0yC/MZ1i2wM0cIeOejlVdZ3D86LAcFXFRdeuZmh91QJvUTW51bOK5W2BznGNIl4+mDaA==",
927 + "version": "0.19.11",
928 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz",
929 + "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==",
930 "cpu": [
931 "loong64"
932 ],
@@ -929,9 +940,9 @@
940 }
941 },
942 "node_modules/@esbuild/linux-mips64el": {
932 - "version": "0.19.10",
933 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.10.tgz",
934 - "integrity": "sha512-J4gH3zhHNbdZN0Bcr1QUGVNkHTdpijgx5VMxeetSk6ntdt+vR1DqGmHxQYHRmNb77tP6GVvD+K0NyO4xjd7y4A==",
943 + "version": "0.19.11",
944 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz",
945 + "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==",
946 "cpu": [
947 "mips64el"
948 ],
@@ -945,9 +956,9 @@
956 }
957 },
958 "node_modules/@esbuild/linux-ppc64": {
948 - "version": "0.19.10",
949 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.10.tgz",
950 - "integrity": "sha512-tgT/7u+QhV6ge8wFMzaklOY7KqiyitgT1AUHMApau32ZlvTB/+efeCtMk4eXS+uEymYK249JsoiklZN64xt6oQ==",
959 + "version": "0.19.11",
960 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz",
961 + "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==",
962 "cpu": [
963 "ppc64"
964 ],
@@ -961,9 +972,9 @@
972 }
973 },
974 "node_modules/@esbuild/linux-riscv64": {
964 - "version": "0.19.10",
965 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.10.tgz",
966 - "integrity": "sha512-0f/spw0PfBMZBNqtKe5FLzBDGo0SKZKvMl5PHYQr3+eiSscfJ96XEknCe+JoOayybWUFQbcJTrk946i3j9uYZA==",
975 + "version": "0.19.11",
976 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz",
977 + "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==",
978 "cpu": [
979 "riscv64"
980 ],
@@ -977,9 +988,9 @@
988 }
989 },
990 "node_modules/@esbuild/linux-s390x": {
980 - "version": "0.19.10",
981 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.10.tgz",
982 - "integrity": "sha512-pZFe0OeskMHzHa9U38g+z8Yx5FNCLFtUnJtQMpwhS+r4S566aK2ci3t4NCP4tjt6d5j5uo4h7tExZMjeKoehAA==",
991 + "version": "0.19.11",
992 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz",
993 + "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==",
994 "cpu": [
995 "s390x"
996 ],
@@ -993,9 +1004,9 @@
1004 }
1005 },
1006 "node_modules/@esbuild/linux-x64": {
996 - "version": "0.19.10",
997 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.10.tgz",
998 - "integrity": "sha512-SpYNEqg/6pZYoc+1zLCjVOYvxfZVZj6w0KROZ3Fje/QrM3nfvT2llI+wmKSrWuX6wmZeTapbarvuNNK/qepSgA==",
1007 + "version": "0.19.11",
1008 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
1009 + "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
1010 "cpu": [
1011 "x64"
1012 ],
@@ -1009,9 +1020,9 @@
1020 }
1021 },
1022 "node_modules/@esbuild/netbsd-x64": {
1012 - "version": "0.19.10",
1013 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.10.tgz",
1014 - "integrity": "sha512-ACbZ0vXy9zksNArWlk2c38NdKg25+L9pr/mVaj9SUq6lHZu/35nx2xnQVRGLrC1KKQqJKRIB0q8GspiHI3J80Q==",
1023 + "version": "0.19.11",
1024 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz",
1025 + "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==",
1026 "cpu": [
1027 "x64"
1028 ],
@@ -1025,9 +1036,9 @@
1036 }
1037 },
1038 "node_modules/@esbuild/openbsd-x64": {
1028 - "version": "0.19.10",
1029 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.10.tgz",
1030 - "integrity": "sha512-PxcgvjdSjtgPMiPQrM3pwSaG4kGphP+bLSb+cihuP0LYdZv1epbAIecHVl5sD3npkfYBZ0ZnOjR878I7MdJDFg==",
1039 + "version": "0.19.11",
1040 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz",
1041 + "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==",
1042 "cpu": [
1043 "x64"
1044 ],
@@ -1041,9 +1052,9 @@
1052 }
1053 },
1054 "node_modules/@esbuild/sunos-x64": {
1044 - "version": "0.19.10",
1045 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.10.tgz",
1046 - "integrity": "sha512-ZkIOtrRL8SEJjr+VHjmW0znkPs+oJXhlJbNwfI37rvgeMtk3sxOQevXPXjmAPZPigVTncvFqLMd+uV0IBSEzqA==",
1055 + "version": "0.19.11",
1056 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz",
1057 + "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==",
1058 "cpu": [
1059 "x64"
1060 ],
@@ -1057,9 +1068,9 @@
1068 }
1069 },
1070 "node_modules/@esbuild/win32-arm64": {
1060 - "version": "0.19.10",
1061 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.10.tgz",
1062 - "integrity": "sha512-+Sa4oTDbpBfGpl3Hn3XiUe4f8TU2JF7aX8cOfqFYMMjXp6ma6NJDztl5FDG8Ezx0OjwGikIHw+iA54YLDNNVfw==",
1071 + "version": "0.19.11",
1072 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz",
1073 + "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==",
1074 "cpu": [
1075 "arm64"
1076 ],
@@ -1073,9 +1084,9 @@
1084 }
1085 },
1086 "node_modules/@esbuild/win32-ia32": {
1076 - "version": "0.19.10",
1077 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.10.tgz",
1078 - "integrity": "sha512-EOGVLK1oWMBXgfttJdPHDTiivYSjX6jDNaATeNOaCOFEVcfMjtbx7WVQwPSE1eIfCp/CaSF2nSrDtzc4I9f8TQ==",
1087 + "version": "0.19.11",
1088 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz",
1089 + "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==",
1090 "cpu": [
1091 "ia32"
1092 ],
@@ -1089,9 +1100,9 @@
1100 }
1101 },
1102 "node_modules/@esbuild/win32-x64": {
1092 - "version": "0.19.10",
1093 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.10.tgz",
1094 - "integrity": "sha512-whqLG6Sc70AbU73fFYvuYzaE4MNMBIlR1Y/IrUeOXFrWHxBEjjbZaQ3IXIQS8wJdAzue2GwYZCjOrgrU1oUHoA==",
1103 + "version": "0.19.11",
1104 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz",
1105 + "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==",
1106 "cpu": [
1107 "x64"
1108 ],
@@ -1316,12 +1327,12 @@
1327 }
1328 },
1329 "node_modules/@intlify/core-base": {
1319 - "version": "9.8.0",
1320 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.8.0.tgz",
1321 - "integrity": "sha512-UxaSZVZ1DwqC/CltUZrWZNaWNhfmKtfyV4BJSt/Zt4Or/fZs1iFj0B+OekYk1+MRHfIOe3+x00uXGQI4PbO/9g==",
1330 + "version": "9.9.0",
1331 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.9.0.tgz",
1332 + "integrity": "sha512-C7UXPymDIOlMGSNjAhNLtKgzITc/8BjINK5gNKXg8GiWCTwL6n3MWr55czksxn8RM5wTMz0qcLOFT+adtaVQaA==",
1333 "dependencies": {
1323 - "@intlify/message-compiler": "9.8.0",
1324 - "@intlify/shared": "9.8.0"
1334 + "@intlify/message-compiler": "9.9.0",
1335 + "@intlify/shared": "9.9.0"
1336 },
1337 "engines": {
1338 "node": ">= 16"
@@ -1331,11 +1342,11 @@
1342 }
1343 },
1344 "node_modules/@intlify/message-compiler": {
1334 - "version": "9.8.0",
1335 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.8.0.tgz",
1336 - "integrity": "sha512-McnYWhcoYmDJvssVu6QGR0shqlkJuL1HHdi5lK7fNqvQqRYaQ4lSLjYmZxwc8tRNMdIe9/KUKfyPxU9M6yCtNQ==",
1345 + "version": "9.9.0",
1346 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.9.0.tgz",
1347 + "integrity": "sha512-yDU/jdUm9KuhEzYfS+wuyja209yXgdl1XFhMlKtXEgSFTxz4COZQCRXXbbH8JrAjMsaJ7bdoPSLsKlY6mXG2iA==",
1348 "dependencies": {
1338 - "@intlify/shared": "9.8.0",
1349 + "@intlify/shared": "9.9.0",
1350 "source-map-js": "^1.0.2"
1351 },
1352 "engines": {
@@ -1346,9 +1357,9 @@
1357 }
1358 },
1359 "node_modules/@intlify/shared": {
1349 - "version": "9.8.0",
1350 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.8.0.tgz",
1351 - "integrity": "sha512-TmgR0RCLjzrSo+W3wT0ALf9851iFMlVI9EYNGeWvZFUQTAJx0bvfsMlPdgVtV1tDNRiAfhkFsMKu6jtUY1ZLKQ==",
1360 + "version": "9.9.0",
1361 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.9.0.tgz",
1362 + "integrity": "sha512-1ECUyAHRrzOJbOizyGufYP2yukqGrWXtkmTu4PcswVnWbkcjzk3YQGmJ0bLkM7JZ0ZYAaohLGdYvBYnTOGYJ9g==",
1363 "engines": {
1364 "node": ">= 16"
1365 },
@@ -1685,9 +1696,9 @@
1696 "dev": true
1697 },
1698 "node_modules/@npmcli/git": {
1688 - "version": "5.0.3",
1689 - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.3.tgz",
1690 - "integrity": "sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==",
1699 + "version": "5.0.4",
1700 + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz",
1701 + "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==",
1702 "dev": true,
1703 "dependencies": {
1704 "@npmcli/promise-spawn": "^7.0.0",
@@ -1819,9 +1830,9 @@
1830 }
1831 },
1832 "node_modules/@npmcli/promise-spawn": {
1822 - "version": "7.0.0",
1823 - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz",
1824 - "integrity": "sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==",
1833 + "version": "7.0.1",
1834 + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz",
1835 + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==",
1836 "dev": true,
1837 "dependencies": {
1838 "which": "^4.0.0"
@@ -1855,9 +1866,9 @@
1866 }
1867 },
1868 "node_modules/@npmcli/run-script": {
1858 - "version": "7.0.2",
1859 - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.2.tgz",
1860 - "integrity": "sha512-Omu0rpA8WXvcGeY6DDzyRoY1i5DkCBkzyJ+m2u7PD6quzb0TvSqdIPOkTn8ZBOj7LbbcbMfZ3c5skwSu6m8y2w==",
1869 + "version": "7.0.3",
1870 + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.3.tgz",
1871 + "integrity": "sha512-ZMWGLHpzMq3rBGIwPyeaoaleaLMvrBrH8nugHxTi5ACkJZXTxXPtVuEH91ifgtss5hUwJQ2VDnzDBWPmz78rvg==",
1872 "dev": true,
1873 "dependencies": {
1874 "@npmcli/node-gyp": "^3.0.0",
@@ -1960,9 +1971,9 @@
1971 }
1972 },
1973 "node_modules/@rollup/rollup-android-arm-eabi": {
1963 - "version": "4.9.1",
1964 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.1.tgz",
1965 - "integrity": "sha512-6vMdBZqtq1dVQ4CWdhFwhKZL6E4L1dV6jUjuBvsavvNJSppzi6dLBbuV+3+IyUREaj9ZFvQefnQm28v4OCXlig==",
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==",
1977 "cpu": [
1978 "arm"
1979 ],
@@ -1973,9 +1984,9 @@
1984 ]
1985 },
1986 "node_modules/@rollup/rollup-android-arm64": {
1976 - "version": "4.9.1",
1977 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.1.tgz",
1978 - "integrity": "sha512-Jto9Fl3YQ9OLsTDWtLFPtaIMSL2kwGyGoVCmPC8Gxvym9TCZm4Sie+cVeblPO66YZsYH8MhBKDMGZ2NDxuk/XQ==",
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==",
1990 "cpu": [
1991 "arm64"
1992 ],
@@ -1986,9 +1997,9 @@
1997 ]
1998 },
1999 "node_modules/@rollup/rollup-darwin-arm64": {
1989 - "version": "4.9.1",
1990 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.1.tgz",
1991 - "integrity": "sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA==",
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==",
2003 "cpu": [
2004 "arm64"
2005 ],
@@ -1999,9 +2010,9 @@
2010 ]
2011 },
2012 "node_modules/@rollup/rollup-darwin-x64": {
2002 - "version": "4.9.1",
2003 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.1.tgz",
2004 - "integrity": "sha512-KyP/byeXu9V+etKO6Lw3E4tW4QdcnzDG/ake031mg42lob5tN+5qfr+lkcT/SGZaH2PdW4Z1NX9GHEkZ8xV7og==",
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==",
2016 "cpu": [
2017 "x64"
2018 ],
@@ -2012,9 +2023,9 @@
2023 ]
2024 },
2025 "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
2015 - "version": "4.9.1",
2016 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.1.tgz",
2017 - "integrity": "sha512-Yqz/Doumf3QTKplwGNrCHe/B2p9xqDghBZSlAY0/hU6ikuDVQuOUIpDP/YcmoT+447tsZTmirmjgG3znvSCR0Q==",
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==",
2029 "cpu": [
2030 "arm"
2031 ],
@@ -2025,9 +2036,9 @@
2036 ]
2037 },
2038 "node_modules/@rollup/rollup-linux-arm64-gnu": {
2028 - "version": "4.9.1",
2029 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.1.tgz",
2030 - "integrity": "sha512-u3XkZVvxcvlAOlQJ3UsD1rFvLWqu4Ef/Ggl40WAVCuogf4S1nJPHh5RTgqYFpCOvuGJ7H5yGHabjFKEZGExk5Q==",
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==",
2042 "cpu": [
2043 "arm64"
2044 ],
@@ -2038,9 +2049,9 @@
2049 ]
2050 },
2051 "node_modules/@rollup/rollup-linux-arm64-musl": {
2041 - "version": "4.9.1",
2042 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.1.tgz",
2043 - "integrity": "sha512-0XSYN/rfWShW+i+qjZ0phc6vZ7UWI8XWNz4E/l+6edFt+FxoEghrJHjX1EY/kcUGCnZzYYRCl31SNdfOi450Aw==",
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==",
2055 "cpu": [
2056 "arm64"
2057 ],
@@ -2051,9 +2062,9 @@
2062 ]
2063 },
2064 "node_modules/@rollup/rollup-linux-riscv64-gnu": {
2054 - "version": "4.9.1",
2055 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.1.tgz",
2056 - "integrity": "sha512-LmYIO65oZVfFt9t6cpYkbC4d5lKHLYv5B4CSHRpnANq0VZUQXGcCPXHzbCXCz4RQnx7jvlYB1ISVNCE/omz5cw==",
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==",
2068 "cpu": [
2069 "riscv64"
2070 ],
@@ -2064,9 +2075,9 @@
2075 ]
2076 },
2077 "node_modules/@rollup/rollup-linux-x64-gnu": {
2067 - "version": "4.9.1",
2068 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.1.tgz",
2069 - "integrity": "sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg==",
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==",
2081 "cpu": [
2082 "x64"
2083 ],
@@ -2077,9 +2088,9 @@
2088 ]
2089 },
2090 "node_modules/@rollup/rollup-linux-x64-musl": {
2080 - "version": "4.9.1",
2081 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.1.tgz",
2082 - "integrity": "sha512-t4QSR7gN+OEZLG0MiCgPqMWZGwmeHhsM4AkegJ0Kiy6TnJ9vZ8dEIwHw1LcZKhbHxTY32hp9eVCMdR3/I8MGRw==",
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==",
2094 "cpu": [
2095 "x64"
2096 ],
@@ -2090,9 +2101,9 @@
2101 ]
2102 },
2103 "node_modules/@rollup/rollup-win32-arm64-msvc": {
2093 - "version": "4.9.1",
2094 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.1.tgz",
2095 - "integrity": "sha512-7XI4ZCBN34cb+BH557FJPmh0kmNz2c25SCQeT9OiFWEgf8+dL6ZwJ8f9RnUIit+j01u07Yvrsuu1rZGxJCc51g==",
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==",
2107 "cpu": [
2108 "arm64"
2109 ],
@@ -2103,9 +2114,9 @@
2114 ]
2115 },
2116 "node_modules/@rollup/rollup-win32-ia32-msvc": {
2106 - "version": "4.9.1",
2107 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.1.tgz",
2108 - "integrity": "sha512-yE5c2j1lSWOH5jp+Q0qNL3Mdhr8WuqCNVjc6BxbVfS5cAS6zRmdiw7ktb8GNpDCEUJphILY6KACoFoRtKoqNQg==",
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==",
2120 "cpu": [
2121 "ia32"
2122 ],
@@ -2116,9 +2127,9 @@
2127 ]
2128 },
2129 "node_modules/@rollup/rollup-win32-x64-msvc": {
2119 - "version": "4.9.1",
2120 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz",
2121 - "integrity": "sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA==",
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==",
2133 "cpu": [
2134 "x64"
2135 ],
@@ -2341,9 +2352,9 @@
2352 }
2353 },
2354 "node_modules/@types/node": {
2344 - "version": "20.10.5",
2345 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
2346 - "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
2355 + "version": "20.10.8",
2356 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
2357 + "integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
2358 "dev": true,
2359 "dependencies": {
2360 "undici-types": "~5.26.4"
@@ -2383,9 +2394,9 @@
2394 "dev": true
2395 },
2396 "node_modules/@types/validator": {
2386 - "version": "13.11.7",
2387 - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.7.tgz",
2388 - "integrity": "sha512-q0JomTsJ2I5Mv7dhHhQLGjMvX0JJm5dyZ1DXQySIUzU1UlwzB8bt+R6+LODUbz0UDIOvEzGc28tk27gBJw2N8Q==",
2397 + "version": "13.11.8",
2398 + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz",
2399 + "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==",
2400 "dev": true
2401 },
2402 "node_modules/@types/web-bluetooth": {
@@ -2404,16 +2415,16 @@
2415 }
2416 },
2417 "node_modules/@typescript-eslint/eslint-plugin": {
2407 - "version": "6.16.0",
2408 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz",
2409 - "integrity": "sha512-O5f7Kv5o4dLWQtPX4ywPPa+v9G+1q1x8mz0Kr0pXUtKsevo+gIJHLkGc8RxaZWtP8RrhwhSNIWThnW42K9/0rQ==",
2418 + "version": "6.18.1",
2419 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.1.tgz",
2420 + "integrity": "sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==",
2421 "dev": true,
2422 "dependencies": {
2423 "@eslint-community/regexpp": "^4.5.1",
2413 - "@typescript-eslint/scope-manager": "6.16.0",
2414 - "@typescript-eslint/type-utils": "6.16.0",
2415 - "@typescript-eslint/utils": "6.16.0",
2416 - "@typescript-eslint/visitor-keys": "6.16.0",
2424 + "@typescript-eslint/scope-manager": "6.18.1",
2425 + "@typescript-eslint/type-utils": "6.18.1",
2426 + "@typescript-eslint/utils": "6.18.1",
2427 + "@typescript-eslint/visitor-keys": "6.18.1",
2428 "debug": "^4.3.4",
2429 "graphemer": "^1.4.0",
2430 "ignore": "^5.2.4",
@@ -2472,15 +2483,15 @@
2483 "dev": true
2484 },
2485 "node_modules/@typescript-eslint/parser": {
2475 - "version": "6.16.0",
2476 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz",
2477 - "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
2486 + "version": "6.18.1",
2487 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.1.tgz",
2488 + "integrity": "sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==",
2489 "dev": true,
2490 "dependencies": {
2480 - "@typescript-eslint/scope-manager": "6.16.0",
2481 - "@typescript-eslint/types": "6.16.0",
2482 - "@typescript-eslint/typescript-estree": "6.16.0",
2483 - "@typescript-eslint/visitor-keys": "6.16.0",
2491 + "@typescript-eslint/scope-manager": "6.18.1",
2492 + "@typescript-eslint/types": "6.18.1",
2493 + "@typescript-eslint/typescript-estree": "6.18.1",
2494 + "@typescript-eslint/visitor-keys": "6.18.1",
2495 "debug": "^4.3.4"
2496 },
2497 "engines": {
@@ -2500,13 +2511,13 @@
2511 }
2512 },
2513 "node_modules/@typescript-eslint/scope-manager": {
2503 - "version": "6.16.0",
2504 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
2505 - "integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
2514 + "version": "6.18.1",
2515 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.1.tgz",
2516 + "integrity": "sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==",
2517 "dev": true,
2518 "dependencies": {
2508 - "@typescript-eslint/types": "6.16.0",
2509 - "@typescript-eslint/visitor-keys": "6.16.0"
2519 + "@typescript-eslint/types": "6.18.1",
2520 + "@typescript-eslint/visitor-keys": "6.18.1"
2521 },
2522 "engines": {
2523 "node": "^16.0.0 || >=18.0.0"
@@ -2517,13 +2528,13 @@
2528 }
2529 },
2530 "node_modules/@typescript-eslint/type-utils": {
2520 - "version": "6.16.0",
2521 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.16.0.tgz",
2522 - "integrity": "sha512-ThmrEOcARmOnoyQfYkHw/DX2SEYBalVECmoldVuH6qagKROp/jMnfXpAU/pAIWub9c4YTxga+XwgAkoA0pxfmg==",
2531 + "version": "6.18.1",
2532 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.1.tgz",
2533 + "integrity": "sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==",
2534 "dev": true,
2535 "dependencies": {
2525 - "@typescript-eslint/typescript-estree": "6.16.0",
2526 - "@typescript-eslint/utils": "6.16.0",
2536 + "@typescript-eslint/typescript-estree": "6.18.1",
2537 + "@typescript-eslint/utils": "6.18.1",
2538 "debug": "^4.3.4",
2539 "ts-api-utils": "^1.0.1"
2540 },
@@ -2544,9 +2555,9 @@
2555 }
2556 },
2557 "node_modules/@typescript-eslint/types": {
2547 - "version": "6.16.0",
2548 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz",
2549 - "integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
2558 + "version": "6.18.1",
2559 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.1.tgz",
2560 + "integrity": "sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==",
2561 "dev": true,
2562 "engines": {
2563 "node": "^16.0.0 || >=18.0.0"
@@ -2557,13 +2568,13 @@
2568 }
2569 },
2570 "node_modules/@typescript-eslint/typescript-estree": {
2560 - "version": "6.16.0",
2561 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
2562 - "integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
2571 + "version": "6.18.1",
2572 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.1.tgz",
2573 + "integrity": "sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==",
2574 "dev": true,
2575 "dependencies": {
2565 - "@typescript-eslint/types": "6.16.0",
2566 - "@typescript-eslint/visitor-keys": "6.16.0",
2576 + "@typescript-eslint/types": "6.18.1",
2577 + "@typescript-eslint/visitor-keys": "6.18.1",
2578 "debug": "^4.3.4",
2579 "globby": "^11.1.0",
2580 "is-glob": "^4.0.3",
@@ -2618,17 +2629,17 @@
2629 "dev": true
2630 },
2631 "node_modules/@typescript-eslint/utils": {
2621 - "version": "6.16.0",
2622 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.16.0.tgz",
2623 - "integrity": "sha512-T83QPKrBm6n//q9mv7oiSvy/Xq/7Hyw9SzSEhMHJwznEmQayfBM87+oAlkNAMEO7/MjIwKyOHgBJbxB0s7gx2A==",
2632 + "version": "6.18.1",
2633 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.1.tgz",
2634 + "integrity": "sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==",
2635 "dev": true,
2636 "dependencies": {
2637 "@eslint-community/eslint-utils": "^4.4.0",
2638 "@types/json-schema": "^7.0.12",
2639 "@types/semver": "^7.5.0",
2629 - "@typescript-eslint/scope-manager": "6.16.0",
2630 - "@typescript-eslint/types": "6.16.0",
2631 - "@typescript-eslint/typescript-estree": "6.16.0",
2640 + "@typescript-eslint/scope-manager": "6.18.1",
2641 + "@typescript-eslint/types": "6.18.1",
2642 + "@typescript-eslint/typescript-estree": "6.18.1",
2643 "semver": "^7.5.4"
2644 },
2645 "engines": {
@@ -2676,12 +2687,12 @@
2687 "dev": true
2688 },
2689 "node_modules/@typescript-eslint/visitor-keys": {
2679 - "version": "6.16.0",
2680 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
2681 - "integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
2690 + "version": "6.18.1",
2691 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.1.tgz",
2692 + "integrity": "sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==",
2693 "dev": true,
2694 "dependencies": {
2684 - "@typescript-eslint/types": "6.16.0",
2695 + "@typescript-eslint/types": "6.18.1",
2696 "eslint-visitor-keys": "^3.4.1"
2697 },
2698 "engines": {
@@ -2699,9 +2710,9 @@
2710 "dev": true
2711 },
2712 "node_modules/@vitejs/plugin-vue": {
2702 - "version": "5.0.0",
2703 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.0.tgz",
2704 - "integrity": "sha512-7x5e8X4J1Wi4NxudGjJBd2OFerAi/0nzF80ojCzvfj347WVr0YSn82C8BSsgwSHzlk9Kw5xnZfj0/7RLnNwP5w==",
2713 + "version": "5.0.3",
2714 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz",
2715 + "integrity": "sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==",
2716 "dev": true,
2717 "engines": {
2718 "node": "^18.0.0 || >=20.0.0"
@@ -2730,13 +2741,13 @@
2741 }
2742 },
2743 "node_modules/@vitest/expect": {
2733 - "version": "1.1.0",
2734 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.0.tgz",
2735 - "integrity": "sha512-9IE2WWkcJo2BR9eqtY5MIo3TPmS50Pnwpm66A6neb2hvk/QSLfPXBz2qdiwUOQkwyFuuXEUj5380CbwfzW4+/w==",
2744 + "version": "1.1.3",
2745 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.3.tgz",
2746 + "integrity": "sha512-MnJqsKc1Ko04lksF9XoRJza0bGGwTtqfbyrsYv5on4rcEkdo+QgUdITenBQBUltKzdxW7K3rWh+nXRULwsdaVg==",
2747 "dev": true,
2748 "dependencies": {
2738 - "@vitest/spy": "1.1.0",
2739 - "@vitest/utils": "1.1.0",
2749 + "@vitest/spy": "1.1.3",
2750 + "@vitest/utils": "1.1.3",
2751 "chai": "^4.3.10"
2752 },
2753 "funding": {
@@ -2744,12 +2755,12 @@
2755 }
2756 },
2757 "node_modules/@vitest/runner": {
2747 - "version": "1.1.0",
2748 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.1.0.tgz",
2749 - "integrity": "sha512-zdNLJ00pm5z/uhbWF6aeIJCGMSyTyWImy3Fcp9piRGvueERFlQFbUwCpzVce79OLm2UHk9iwaMSOaU9jVHgNVw==",
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==",
2761 "dev": true,
2762 "dependencies": {
2752 - "@vitest/utils": "1.1.0",
2763 + "@vitest/utils": "1.1.3",
2764 "p-limit": "^5.0.0",
2765 "pathe": "^1.1.1"
2766 },
@@ -2785,9 +2796,9 @@
2796 }
2797 },
2798 "node_modules/@vitest/snapshot": {
2788 - "version": "1.1.0",
2789 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.1.0.tgz",
2790 - "integrity": "sha512-5O/wyZg09V5qmNmAlUgCBqflvn2ylgsWJRRuPrnHEfDNT6tQpQ8O1isNGgo+VxofISHqz961SG3iVvt3SPK/QQ==",
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==",
2802 "dev": true,
2803 "dependencies": {
2804 "magic-string": "^0.30.5",
@@ -2799,9 +2810,9 @@
2810 }
2811 },
2812 "node_modules/@vitest/spy": {
2802 - "version": "1.1.0",
2803 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.1.0.tgz",
2804 - "integrity": "sha512-sNOVSU/GE+7+P76qYo+VXdXhXffzWZcYIPQfmkiRxaNCSPiLANvQx5Mx6ZURJ/ndtEkUJEpvKLXqAYTKEY+lTg==",
2813 + "version": "1.1.3",
2814 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.1.3.tgz",
2815 + "integrity": "sha512-Ec0qWyGS5LhATFQtldvChPTAHv08yHIOZfiNcjwRQbFPHpkih0md9KAbs7TfeIfL7OFKoe7B/6ukBTqByubXkQ==",
2816 "dev": true,
2817 "dependencies": {
2818 "tinyspy": "^2.2.0"
@@ -2811,12 +2822,13 @@
2822 }
2823 },
2824 "node_modules/@vitest/utils": {
2814 - "version": "1.1.0",
2815 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.1.0.tgz",
2816 - "integrity": "sha512-z+s510fKmYz4Y41XhNs3vcuFTFhcij2YF7F8VQfMEYAAUfqQh0Zfg7+w9xdgFGhPf3tX3TicAe+8BDITk6ampQ==",
2825 + "version": "1.1.3",
2826 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.1.3.tgz",
2827 + "integrity": "sha512-Dyt3UMcdElTll2H75vhxfpZu03uFpXRCHxWnzcrFjZxT1kTbq8ALUYIeBgGolo1gldVdI0YSlQRacsqxTwNqwg==",
2828 "dev": true,
2829 "dependencies": {
2830 "diff-sequences": "^29.6.3",
2831 + "estree-walker": "^3.0.3",
2832 "loupe": "^2.3.7",
2833 "pretty-format": "^29.7.0"
2834 },
@@ -2824,6 +2836,15 @@
2836 "url": "https://opencollective.com/vitest"
2837 }
2838 },
2839 + "node_modules/@vitest/utils/node_modules/estree-walker": {
2840 + "version": "3.0.3",
2841 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
2842 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
2843 + "dev": true,
2844 + "dependencies": {
2845 + "@types/estree": "^1.0.0"
2846 + }
2847 + },
2848 "node_modules/@volar/language-core": {
2849 "version": "1.11.1",
2850 "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
@@ -2879,36 +2900,36 @@
2900 }
2901 },
2902 "node_modules/@vue/compiler-core": {
2882 - "version": "3.4.0",
2883 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.0.tgz",
2884 - "integrity": "sha512-cw4S15PkNGTKkP9OFFl4wnQoJJk+HqaYBafgrpDnSukiQGpcYJeRpzmqnCVCIkl6V6Eqsv58E0OAdl6b592vuA==",
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==",
2906 "dependencies": {
2907 "@babel/parser": "^7.23.6",
2887 - "@vue/shared": "3.4.0",
2908 + "@vue/shared": "3.4.7",
2909 "entities": "^4.5.0",
2910 "estree-walker": "^2.0.2",
2911 "source-map-js": "^1.0.2"
2912 }
2913 },
2914 "node_modules/@vue/compiler-dom": {
2894 - "version": "3.4.0",
2895 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.0.tgz",
2896 - "integrity": "sha512-E957uOhpoE48YjZGWeAoLmNYd3UeU4oIP8kJi8Rcsb9l2tV8Z48Jn07Zgq1aW0v3vuhlmydEKkKKbhLpADHXEA==",
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==",
2918 "dependencies": {
2898 - "@vue/compiler-core": "3.4.0",
2899 - "@vue/shared": "3.4.0"
2919 + "@vue/compiler-core": "3.4.7",
2920 + "@vue/shared": "3.4.7"
2921 }
2922 },
2923 "node_modules/@vue/compiler-sfc": {
2903 - "version": "3.4.0",
2904 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.0.tgz",
2905 - "integrity": "sha512-PWE0mE2yW7bJS7PmaCrVDEG6KPaDJo0pb4AKnCxJ5lRRDO4IwL/fswBGhCpov+v/c+N/e+hQHpXNwvqU9BtUXg==",
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==",
2927 "dependencies": {
2928 "@babel/parser": "^7.23.6",
2908 - "@vue/compiler-core": "3.4.0",
2909 - "@vue/compiler-dom": "3.4.0",
2910 - "@vue/compiler-ssr": "3.4.0",
2911 - "@vue/shared": "3.4.0",
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",
2933 "estree-walker": "^2.0.2",
2934 "magic-string": "^0.30.5",
2935 "postcss": "^8.4.32",
@@ -2916,12 +2937,12 @@
2937 }
2938 },
2939 "node_modules/@vue/compiler-ssr": {
2919 - "version": "3.4.0",
2920 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.0.tgz",
2921 - "integrity": "sha512-+oXKy105g9DIYQKDi3Gwung0xqQX5gJHr0GR+Vf7yK/WkNDM6q61ummcKmKAB85EIst8y3vj2PA9z9YU5Oc4DQ==",
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==",
2943 "dependencies": {
2923 - "@vue/compiler-dom": "3.4.0",
2924 - "@vue/shared": "3.4.0"
2944 + "@vue/compiler-dom": "3.4.7",
2945 + "@vue/shared": "3.4.7"
2946 }
2947 },
2948 "node_modules/@vue/devtools-api": {
@@ -2993,53 +3014,48 @@
3014 }
3015 },
3016 "node_modules/@vue/reactivity": {
2996 - "version": "3.4.0",
2997 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.0.tgz",
2998 - "integrity": "sha512-X6BvQjNcgKKHWPQzlRJjZvIu72Kkn8xJSv6VNptqWh8dToMknD0Hch1l4N7llKgVt6Diq4lMeUnErbZFvuGlAA==",
3017 + "version": "3.4.7",
3018 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.7.tgz",
3019 + "integrity": "sha512-F539DO0ogH0+L8F9Pnw7cjqibcmSOh5UTk16u5f4MKQ8fraqepI9zdh+sozPX6VmEHOcjo8qw3Or9ZcFFw4SZA==",
3020 "dependencies": {
3000 - "@vue/shared": "3.4.0"
3021 + "@vue/shared": "3.4.7"
3022 }
3023 },
3024 "node_modules/@vue/runtime-core": {
3004 - "version": "3.4.0",
3005 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.0.tgz",
3006 - "integrity": "sha512-NYrj/JgMMqnSWcIud8lLzDQrBLu+EVEeQ56QE9DYJeKG2eFrnQy8o/h57R9nCprafHs0uImKL3xsdXjHseYVxw==",
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==",
3028 "dependencies": {
3008 - "@vue/reactivity": "3.4.0",
3009 - "@vue/shared": "3.4.0"
3029 + "@vue/reactivity": "3.4.7",
3030 + "@vue/shared": "3.4.7"
3031 }
3032 },
3033 "node_modules/@vue/runtime-dom": {
3013 - "version": "3.4.0",
3014 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.0.tgz",
3015 - "integrity": "sha512-1ZoHEsA5l77qbx2F+SWo/hQdBksPuOmww1t/jznidDG+xMB/iidafEFvo2ZTtZii0JfTIrlDhjshfYUvQC17wQ==",
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==",
3037 "dependencies": {
3017 - "@vue/runtime-core": "3.4.0",
3018 - "@vue/shared": "3.4.0",
3038 + "@vue/runtime-core": "3.4.7",
3039 + "@vue/shared": "3.4.7",
3040 "csstype": "^3.1.3"
3041 }
3042 },
3022 - "node_modules/@vue/runtime-dom/node_modules/csstype": {
3023 - "version": "3.1.3",
3024 - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
3025 - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
3026 - },
3043 "node_modules/@vue/server-renderer": {
3028 - "version": "3.4.0",
3029 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.0.tgz",
3030 - "integrity": "sha512-GuOVCyLDlWPu8nKo5AUxb8B+iB/Ik4I1WwqAlBqf5+y48z6D6rvKshp7KR3cJea+pte1tdTsb0+Ja82KizMZOw==",
3044 + "version": "3.4.7",
3045 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.7.tgz",
3046 + "integrity": "sha512-3bWnYLEkLLhkDWqvNk7IvbQD4UcxvFKxELBiOO2iG3m6AniFIsBWfHOO5tLVQnjdWkODu4rq0GipmfEenVAK5Q==",
3047 "dependencies": {
3032 - "@vue/compiler-ssr": "3.4.0",
3033 - "@vue/shared": "3.4.0"
3048 + "@vue/compiler-ssr": "3.4.7",
3049 + "@vue/shared": "3.4.7"
3050 },
3051 "peerDependencies": {
3036 - "vue": "3.4.0"
3052 + "vue": "3.4.7"
3053 }
3054 },
3055 "node_modules/@vue/shared": {
3040 - "version": "3.4.0",
3041 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.0.tgz",
3042 - "integrity": "sha512-Nhh3ed3G1R6HDAWiG6YYFt0Zmq/To6u5vjzwa9TIquGheCXPY6nEdIAO8ZdlwXsWqC2yNLj700FOvShpYt5CEA=="
3056 + "version": "3.4.7",
3057 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.7.tgz",
3058 + "integrity": "sha512-G+i4glX1dMJk88sbJEcQEGWRQnVm9eIY7CcQbO5dpdsD9SF8jka3Mr5OqZYGjczGN1+D6EUwdu6phcmcx9iuPA=="
3059 },
3060 "node_modules/@vue/test-utils": {
3061 "version": "2.4.3",
@@ -3562,12 +3578,12 @@
3578 "dev": true
3579 },
3580 "node_modules/axios": {
3565 - "version": "1.6.3",
3566 - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.3.tgz",
3567 - "integrity": "sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww==",
3581 + "version": "1.6.5",
3582 + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
3583 + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
3584 "dev": true,
3585 "dependencies": {
3570 - "follow-redirects": "^1.15.0",
3586 + "follow-redirects": "^1.15.4",
3587 "form-data": "^4.0.0",
3588 "proxy-from-env": "^1.1.0"
3589 }
@@ -3627,6 +3643,15 @@
3643 "tweetnacl": "^0.14.3"
3644 }
3645 },
3646 + "node_modules/bidi-js": {
3647 + "version": "1.0.3",
3648 + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
3649 + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
3650 + "dev": true,
3651 + "dependencies": {
3652 + "require-from-string": "^2.0.2"
3653 + }
3654 + },
3655 "node_modules/big-integer": {
3656 "version": "1.6.52",
3657 "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
@@ -3836,9 +3861,9 @@
3861 }
3862 },
3863 "node_modules/cacache": {
3839 - "version": "18.0.1",
3840 - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.1.tgz",
3841 - "integrity": "sha512-g4Uf2CFZPaxtJKre6qr4zqLDOOPU7bNVhWjlNhvzc51xaTOx2noMOLhfFkTAqwtrAZAKQUuDfyjitzilpA8WsQ==",
3864 + "version": "18.0.2",
3865 + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz",
3866 + "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==",
3867 "dev": true,
3868 "dependencies": {
3869 "@npmcli/fs": "^3.1.0",
@@ -3934,9 +3959,9 @@
3959 }
3960 },
3961 "node_modules/caniuse-lite": {
3937 - "version": "1.0.30001572",
3938 - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz",
3939 - "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==",
3962 + "version": "1.0.30001576",
3963 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz",
3964 + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==",
3965 "dev": true,
3966 "funding": [
3967 {
@@ -3960,9 +3985,9 @@
3985 "dev": true
3986 },
3987 "node_modules/chai": {
3963 - "version": "4.3.10",
3964 - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
3965 - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
3988 + "version": "4.4.0",
3989 + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz",
3990 + "integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==",
3991 "dev": true,
3992 "dependencies": {
3993 "assertion-error": "^1.1.0",
@@ -4076,9 +4101,9 @@
4101 }
4102 },
4103 "node_modules/classnames": {
4079 - "version": "2.5.0",
4080 - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz",
4081 - "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA=="
4104 + "version": "2.5.1",
4105 + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
4106 + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
4107 },
4108 "node_modules/clean-stack": {
4109 "version": "2.2.0",
@@ -4330,9 +4355,9 @@
4355 "dev": true
4356 },
4357 "node_modules/cookies": {
4333 - "version": "0.9.0",
4334 - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.0.tgz",
4335 - "integrity": "sha512-mtyMqy14RsH7+IRJglGcKtRLOq0SRt0DdXVrLgc+v1e/o0TNJUpdElhgr3AAi638LO0xZwEPcRRkJ3afxvGhUw==",
4358 + "version": "0.9.1",
4359 + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
4360 + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
4361 "dev": true,
4362 "dependencies": {
4363 "depd": "~2.0.0",
@@ -4382,6 +4407,11 @@
4407 "csstype": "~3.0.5"
4408 }
4409 },
4410 + "node_modules/css-render/node_modules/csstype": {
4411 + "version": "3.0.11",
4412 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
4413 + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
4414 + },
4415 "node_modules/css-select": {
4416 "version": "5.1.0",
4417 "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -4469,21 +4499,21 @@
4499 "dev": true
4500 },
4501 "node_modules/cssstyle": {
4472 - "version": "3.0.0",
4473 - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz",
4474 - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==",
4502 + "version": "4.0.1",
4503 + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
4504 + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==",
4505 "dev": true,
4506 "dependencies": {
4507 "rrweb-cssom": "^0.6.0"
4508 },
4509 "engines": {
4480 - "node": ">=14"
4510 + "node": ">=18"
4511 }
4512 },
4513 "node_modules/csstype": {
4484 - "version": "3.0.11",
4485 - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
4486 - "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
4514 + "version": "3.1.3",
4515 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
4516 + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4517 },
4518 "node_modules/cypress": {
4519 "version": "13.6.2",
@@ -4544,9 +4574,9 @@
4574 }
4575 },
4576 "node_modules/cypress/node_modules/@types/node": {
4547 - "version": "18.19.3",
4548 - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz",
4549 - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==",
4577 + "version": "18.19.6",
4578 + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.6.tgz",
4579 + "integrity": "sha512-X36s5CXMrrJOs2lQCdDF68apW4Rfx9ixYMawlepwmE4Anezv/AV2LSpKD1Ub8DAc+urp5bk0BGZ6NtmBitfnsg==",
4580 "dev": true,
4581 "dependencies": {
4582 "undici-types": "~5.26.4"
@@ -5002,9 +5032,9 @@
5032 }
5033 },
5034 "node_modules/defu": {
5005 - "version": "6.1.3",
5006 - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.3.tgz",
5007 - "integrity": "sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==",
5035 + "version": "6.1.4",
5036 + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
5037 + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
5038 "dev": true
5039 },
5040 "node_modules/delayed-stream": {
@@ -5282,9 +5312,9 @@
5312 "dev": true
5313 },
5314 "node_modules/electron-to-chromium": {
5285 - "version": "1.4.616",
5286 - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz",
5287 - "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==",
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==",
5318 "dev": true
5319 },
5320 "node_modules/emoji-regex": {
@@ -5454,9 +5484,9 @@
5484 }
5485 },
5486 "node_modules/esbuild": {
5457 - "version": "0.19.10",
5458 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.10.tgz",
5459 - "integrity": "sha512-S1Y27QGt/snkNYrRcswgRFqZjaTG5a5xM3EQo97uNBnH505pdzSNe/HLBq1v0RO7iK/ngdbhJB6mDAp0OK+iUA==",
5487 + "version": "0.19.11",
5488 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
5489 + "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
5490 "dev": true,
5491 "hasInstallScript": true,
5492 "bin": {
@@ -5466,29 +5496,29 @@
5496 "node": ">=12"
5497 },
5498 "optionalDependencies": {
5469 - "@esbuild/aix-ppc64": "0.19.10",
5470 - "@esbuild/android-arm": "0.19.10",
5471 - "@esbuild/android-arm64": "0.19.10",
5472 - "@esbuild/android-x64": "0.19.10",
5473 - "@esbuild/darwin-arm64": "0.19.10",
5474 - "@esbuild/darwin-x64": "0.19.10",
5475 - "@esbuild/freebsd-arm64": "0.19.10",
5476 - "@esbuild/freebsd-x64": "0.19.10",
5477 - "@esbuild/linux-arm": "0.19.10",
5478 - "@esbuild/linux-arm64": "0.19.10",
5479 - "@esbuild/linux-ia32": "0.19.10",
5480 - "@esbuild/linux-loong64": "0.19.10",
5481 - "@esbuild/linux-mips64el": "0.19.10",
5482 - "@esbuild/linux-ppc64": "0.19.10",
5483 - "@esbuild/linux-riscv64": "0.19.10",
5484 - "@esbuild/linux-s390x": "0.19.10",
5485 - "@esbuild/linux-x64": "0.19.10",
5486 - "@esbuild/netbsd-x64": "0.19.10",
5487 - "@esbuild/openbsd-x64": "0.19.10",
5488 - "@esbuild/sunos-x64": "0.19.10",
5489 - "@esbuild/win32-arm64": "0.19.10",
5490 - "@esbuild/win32-ia32": "0.19.10",
5491 - "@esbuild/win32-x64": "0.19.10"
5499 + "@esbuild/aix-ppc64": "0.19.11",
5500 + "@esbuild/android-arm": "0.19.11",
5501 + "@esbuild/android-arm64": "0.19.11",
5502 + "@esbuild/android-x64": "0.19.11",
5503 + "@esbuild/darwin-arm64": "0.19.11",
5504 + "@esbuild/darwin-x64": "0.19.11",
5505 + "@esbuild/freebsd-arm64": "0.19.11",
5506 + "@esbuild/freebsd-x64": "0.19.11",
5507 + "@esbuild/linux-arm": "0.19.11",
5508 + "@esbuild/linux-arm64": "0.19.11",
5509 + "@esbuild/linux-ia32": "0.19.11",
5510 + "@esbuild/linux-loong64": "0.19.11",
5511 + "@esbuild/linux-mips64el": "0.19.11",
5512 + "@esbuild/linux-ppc64": "0.19.11",
5513 + "@esbuild/linux-riscv64": "0.19.11",
5514 + "@esbuild/linux-s390x": "0.19.11",
5515 + "@esbuild/linux-x64": "0.19.11",
5516 + "@esbuild/netbsd-x64": "0.19.11",
5517 + "@esbuild/openbsd-x64": "0.19.11",
5518 + "@esbuild/sunos-x64": "0.19.11",
5519 + "@esbuild/win32-arm64": "0.19.11",
5520 + "@esbuild/win32-ia32": "0.19.11",
5521 + "@esbuild/win32-x64": "0.19.11"
5522 }
5523 },
5524 "node_modules/escalade": {
@@ -5622,9 +5652,9 @@
5652 }
5653 },
5654 "node_modules/eslint-plugin-prettier": {
5625 - "version": "5.1.2",
5626 - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz",
5627 - "integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==",
5655 + "version": "5.1.3",
5656 + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
5657 + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
5658 "dev": true,
5659 "dependencies": {
5660 "prettier-linter-helpers": "^1.0.0",
@@ -6176,9 +6206,9 @@
6206 "dev": true
6207 },
6208 "node_modules/follow-redirects": {
6179 - "version": "1.15.3",
6180 - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
6181 - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
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==",
6212 "dev": true,
6213 "funding": [
6214 {
@@ -7505,12 +7535,13 @@
7535 "dev": true
7536 },
7537 "node_modules/jsdom": {
7508 - "version": "23.0.1",
7509 - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.0.1.tgz",
7510 - "integrity": "sha512-2i27vgvlUsGEBO9+/kJQRbtqtm+191b5zAZrU/UezVmnC2dlDAFLgDYJvAEi94T4kjsRKkezEtLQTgsNEsW2lQ==",
7538 + "version": "23.2.0",
7539 + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz",
7540 + "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==",
7541 "dev": true,
7542 "dependencies": {
7513 - "cssstyle": "^3.0.0",
7543 + "@asamuzakjp/dom-selector": "^2.0.1",
7544 + "cssstyle": "^4.0.1",
7545 "data-urls": "^5.0.0",
7546 "decimal.js": "^10.4.3",
7547 "form-data": "^4.0.0",
@@ -7518,7 +7549,6 @@
7549 "http-proxy-agent": "^7.0.0",
7550 "https-proxy-agent": "^7.0.2",
7551 "is-potential-custom-element-name": "^1.0.1",
7521 - "nwsapi": "^2.2.7",
7552 "parse5": "^7.1.2",
7553 "rrweb-cssom": "^0.6.0",
7554 "saxes": "^6.0.0",
@@ -7529,7 +7559,7 @@
7559 "whatwg-encoding": "^3.1.1",
7560 "whatwg-mimetype": "^4.0.0",
7561 "whatwg-url": "^14.0.0",
7532 - "ws": "^8.14.2",
7562 + "ws": "^8.16.0",
7563 "xml-name-validator": "^5.0.0"
7564 },
7565 "engines": {
@@ -8655,9 +8685,9 @@
8685 }
8686 },
8687 "node_modules/naive-ui": {
8658 - "version": "2.36.0",
8659 - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.36.0.tgz",
8660 - "integrity": "sha512-r1ydtEm1Ryf/aWpbLCf32mQAGK99jd1eXgpkCtIomcBRZeAtusfy6zCtIpCppoCuIKM3BW5DMafhVxilubk/lQ==",
8688 + "version": "2.37.3",
8689 + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.37.3.tgz",
8690 + "integrity": "sha512-aUkHFXVIluSi8Me+npbcsdv1NYhVMj5t9YaruoCESlqmfqspj+R2QHEVXkTtUI1kQwVrABMCtAGq/wountqjZA==",
8691 "dependencies": {
8692 "@css-render/plugin-bem": "^0.15.12",
8693 "@css-render/vue3-ssr": "^0.15.12",
@@ -8666,6 +8696,7 @@
8696 "@types/lodash-es": "^4.17.9",
8697 "async-validator": "^4.2.5",
8698 "css-render": "^0.15.12",
8699 + "csstype": "^3.1.3",
8700 "date-fns": "^2.30.0",
8701 "date-fns-tz": "^2.0.0",
8702 "evtd": "^0.2.4",
@@ -8676,7 +8707,7 @@
8707 "treemate": "^0.3.11",
8708 "vdirs": "^0.1.8",
8709 "vooks": "^0.2.12",
8679 - "vueuc": "^0.4.54"
8710 + "vueuc": "^0.4.58"
8711 },
8712 "peerDependencies": {
8713 "vue": "^3.0.0"
@@ -8997,9 +9028,9 @@
9028 "dev": true
9029 },
9030 "node_modules/npm-packlist": {
9000 - "version": "8.0.1",
9001 - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.1.tgz",
9002 - "integrity": "sha512-MQpL27ZrsJQ2kiAuQPpZb5LtJwydNRnI15QWXsf3WHERu4rzjRj6Zju/My2fov7tLuu3Gle/uoIX/DDZ3u4O4Q==",
9031 + "version": "8.0.2",
9032 + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz",
9033 + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==",
9034 "dev": true,
9035 "dependencies": {
9036 "ignore-walk": "^6.0.4"
@@ -9212,12 +9243,6 @@
9243 "url": "https://github.com/fb55/nth-check?sponsor=1"
9244 }
9245 },
9215 - "node_modules/nwsapi": {
9216 - "version": "2.2.7",
9217 - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
9218 - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==",
9219 - "dev": true
9220 - },
9246 "node_modules/object-assign": {
9247 "version": "4.1.1",
9248 "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -9748,9 +9773,9 @@
9773 }
9774 },
9775 "node_modules/postcss": {
9751 - "version": "8.4.32",
9752 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
9753 - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
9776 + "version": "8.4.33",
9777 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz",
9778 + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==",
9779 "funding": [
9780 {
9781 "type": "opencollective",
@@ -9874,9 +9899,9 @@
9899 }
9900 },
9901 "node_modules/postcss-selector-parser": {
9877 - "version": "6.0.14",
9878 - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.14.tgz",
9879 - "integrity": "sha512-65xXYsT40i9GyWzlHQ5ShZoK7JZdySeOozi/tz2EezDo6c04q6+ckYMeoY7idaie1qp2dT5KoYQ2yky6JuoHnA==",
9902 + "version": "6.0.15",
9903 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
9904 + "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
9905 "dev": true,
9906 "dependencies": {
9907 "cssesc": "^3.0.0",
@@ -10437,6 +10462,15 @@
10462 "node": ">=0.10.0"
10463 }
10464 },
10465 + "node_modules/require-from-string": {
10466 + "version": "2.0.2",
10467 + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
10468 + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
10469 + "dev": true,
10470 + "engines": {
10471 + "node": ">=0.10.0"
10472 + }
10473 + },
10474 "node_modules/requires-port": {
10475 "version": "1.0.0",
10476 "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -10623,10 +10657,13 @@
10657 }
10658 },
10659 "node_modules/rollup": {
10626 - "version": "4.9.1",
10627 - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.1.tgz",
10628 - "integrity": "sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==",
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==",
10663 "dev": true,
10664 + "dependencies": {
10665 + "@types/estree": "1.0.5"
10666 + },
10667 "bin": {
10668 "rollup": "dist/bin/rollup"
10669 },
@@ -10635,19 +10672,19 @@
10672 "npm": ">=8.0.0"
10673 },
10674 "optionalDependencies": {
10638 - "@rollup/rollup-android-arm-eabi": "4.9.1",
10639 - "@rollup/rollup-android-arm64": "4.9.1",
10640 - "@rollup/rollup-darwin-arm64": "4.9.1",
10641 - "@rollup/rollup-darwin-x64": "4.9.1",
10642 - "@rollup/rollup-linux-arm-gnueabihf": "4.9.1",
10643 - "@rollup/rollup-linux-arm64-gnu": "4.9.1",
10644 - "@rollup/rollup-linux-arm64-musl": "4.9.1",
10645 - "@rollup/rollup-linux-riscv64-gnu": "4.9.1",
10646 - "@rollup/rollup-linux-x64-gnu": "4.9.1",
10647 - "@rollup/rollup-linux-x64-musl": "4.9.1",
10648 - "@rollup/rollup-win32-arm64-msvc": "4.9.1",
10649 - "@rollup/rollup-win32-ia32-msvc": "4.9.1",
10650 - "@rollup/rollup-win32-x64-msvc": "4.9.1",
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",
10688 "fsevents": "~2.3.2"
10689 }
10690 },
@@ -10830,15 +10867,18 @@
10867 ]
10868 },
10869 "node_modules/safe-regex-test": {
10833 - "version": "1.0.0",
10834 - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
10835 - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
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==",
10873 "dev": true,
10874 "dependencies": {
10838 - "call-bind": "^1.0.2",
10839 - "get-intrinsic": "^1.1.3",
10875 + "call-bind": "^1.0.5",
10876 + "get-intrinsic": "^1.2.2",
10877 "is-regex": "^1.1.4"
10878 },
10879 + "engines": {
10880 + "node": ">= 0.4"
10881 + },
10882 "funding": {
10883 "url": "https://github.com/sponsors/ljharb"
10884 }
@@ -10850,9 +10890,9 @@
10890 "dev": true
10891 },
10892 "node_modules/sass": {
10853 - "version": "1.69.6",
10854 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.6.tgz",
10855 - "integrity": "sha512-qbRr3k9JGHWXCvZU77SD2OTwUlC+gNT+61JOLcmLm+XqH4h/5D+p4IIsxvpkB89S9AwJOyb5+rWNpIucaFxSFQ==",
10893 + "version": "1.69.7",
10894 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz",
10895 + "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==",
10896 "dev": true,
10897 "dependencies": {
10898 "chokidar": ">=3.0.0 <4.0.0",
@@ -11620,17 +11660,17 @@
11660 }
11661 },
11662 "node_modules/svgo": {
11623 - "version": "3.1.0",
11624 - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.1.0.tgz",
11625 - "integrity": "sha512-R5SnNA89w1dYgNv570591F66v34b3eQShpIBcQtZtM5trJwm1VvxbIoMpRYY3ybTAutcKTLEmTsdnaknOHbiQA==",
11663 + "version": "3.2.0",
11664 + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz",
11665 + "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==",
11666 "dev": true,
11667 "dependencies": {
11668 "@trysound/sax": "0.2.0",
11669 "commander": "^7.2.0",
11670 "css-select": "^5.1.0",
11631 - "css-tree": "^2.2.1",
11671 + "css-tree": "^2.3.1",
11672 "css-what": "^6.1.0",
11633 - "csso": "5.0.5",
11673 + "csso": "^5.0.5",
11674 "picocolors": "^1.0.0"
11675 },
11676 "bin": {
@@ -11723,9 +11763,9 @@
11763 }
11764 },
11765 "node_modules/tailwindcss": {
11726 - "version": "3.4.0",
11727 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
11728 - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
11766 + "version": "3.4.1",
11767 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
11768 + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
11769 "dev": true,
11770 "dependencies": {
11771 "@alloc/quick-lru": "^5.2.0",
@@ -12415,9 +12455,9 @@
12455 }
12456 },
12457 "node_modules/typescript": {
12418 - "version": "5.3.3",
12419 - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
12420 - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
12458 + "version": "5.2.2",
12459 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
12460 + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
12461 "devOptional": true,
12462 "bin": {
12463 "tsc": "bin/tsc",
@@ -12695,9 +12735,9 @@
12735 }
12736 },
12737 "node_modules/vite": {
12698 - "version": "5.0.10",
12699 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz",
12700 - "integrity": "sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==",
12738 + "version": "5.0.11",
12739 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz",
12740 + "integrity": "sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==",
12741 "dev": true,
12742 "dependencies": {
12743 "esbuild": "^0.19.3",
@@ -12750,13 +12790,14 @@
12790 }
12791 },
12792 "node_modules/vite-bundle-analyzer": {
12753 - "version": "0.5.0",
12754 - "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.5.0.tgz",
12755 - "integrity": "sha512-/8NrWJDpEFnQv5M1EOLf4QwRXfZwhn2KiJOcHkrQdeh79aMOa1RUA0PXTBOIgwvRmumgvw9GX6g6rCzOAK4qsg==",
12793 + "version": "0.6.1",
12794 + "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.6.1.tgz",
12795 + "integrity": "sha512-uVQAZMkAzukKJdoBd4iXQSyFdDV5Akntsxak9xVzsngFG/yxe1tqDI+lbEAlzLPb4PtIyPtbRP7wgUhiR2sUyQ==",
12796 "dev": true,
12797 "dependencies": {
12798 "fast-glob": "^3.3.1",
12799 "open": "^9.1.0",
12800 + "picocolors": "^1.0.0",
12801 "sirv": "^2.0.3",
12802 "source-map": "^0.7.4"
12803 }
@@ -12807,9 +12848,9 @@
12848 }
12849 },
12850 "node_modules/vite-node": {
12810 - "version": "1.1.0",
12811 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.1.0.tgz",
12812 - "integrity": "sha512-jV48DDUxGLEBdHCQvxL1mEh7+naVy+nhUUUaPAZLd3FJgXuxQiewHcfeZebbJ6onDqNGkP4r3MhQ342PRlG81Q==",
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==",
12854 "dev": true,
12855 "dependencies": {
12856 "cac": "^6.7.14",
@@ -12841,17 +12882,17 @@
12882 }
12883 },
12884 "node_modules/vitest": {
12844 - "version": "1.1.0",
12845 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.1.0.tgz",
12846 - "integrity": "sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==",
12885 + "version": "1.1.3",
12886 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.1.3.tgz",
12887 + "integrity": "sha512-2l8om1NOkiA90/Y207PsEvJLYygddsOyr81wLQ20Ra8IlLKbyQncWsGZjnbkyG2KwwuTXLQjEPOJuxGMG8qJBQ==",
12888 "dev": true,
12889 "dependencies": {
12849 - "@vitest/expect": "1.1.0",
12850 - "@vitest/runner": "1.1.0",
12851 - "@vitest/snapshot": "1.1.0",
12852 - "@vitest/spy": "1.1.0",
12853 - "@vitest/utils": "1.1.0",
12854 - "acorn-walk": "^8.3.0",
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 + "acorn-walk": "^8.3.1",
12896 "cac": "^6.7.14",
12897 "chai": "^4.3.10",
12898 "debug": "^4.3.4",
@@ -12865,7 +12906,7 @@
12906 "tinybench": "^2.5.1",
12907 "tinypool": "^0.8.1",
12908 "vite": "^5.0.0",
12868 - "vite-node": "1.1.0",
12909 + "vite-node": "1.1.3",
12910 "why-is-node-running": "^2.2.2"
12911 },
12912 "bin": {
@@ -13068,15 +13109,15 @@
13109 }
13110 },
13111 "node_modules/vue": {
13071 - "version": "3.4.0",
13072 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.0.tgz",
13073 - "integrity": "sha512-iTE9Ve/7DO/H39+gXHrNkRdnh1jDwPe/fap4brbPKkp1APMkS03OiZ+UY0dwpqtRX0iPWQTkh8Fu3hKgLtaxfA==",
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": {
13075 - "@vue/compiler-dom": "3.4.0",
13076 - "@vue/compiler-sfc": "3.4.0",
13077 - "@vue/runtime-dom": "3.4.0",
13078 - "@vue/server-renderer": "3.4.0",
13079 - "@vue/shared": "3.4.0"
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"
13121 },
13122 "peerDependencies": {
13123 "typescript": "*"
@@ -13111,9 +13152,9 @@
13152 "dev": true
13153 },
13154 "node_modules/vue-eslint-parser": {
13114 - "version": "9.3.2",
13115 - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz",
13116 - "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==",
13155 + "version": "9.4.0",
13156 + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.0.tgz",
13157 + "integrity": "sha512-7KsNBb6gHFA75BtneJsoK/dbZ281whUIwFYdQxA68QrCrGMXYzUMbPDHGcOQ0OocIVKrWSKWXZ4mL7tonCXoUw==",
13158 "dev": true,
13159 "dependencies": {
13160 "debug": "^4.3.4",
@@ -13179,12 +13220,12 @@
13220 }
13221 },
13222 "node_modules/vue-i18n": {
13182 - "version": "9.8.0",
13183 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.8.0.tgz",
13184 - "integrity": "sha512-Izho+6PYjejsTq2mzjcRdBZ5VLRQoSuuexvR8029h5CpN03FYqiqBrShMyf2I1DKkN6kw/xmujcbvC+4QybpsQ==",
13223 + "version": "9.9.0",
13224 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.9.0.tgz",
13225 + "integrity": "sha512-xQ5SxszUAqK5n84N+uUyHH/PiQl9xZ24FOxyAaNonmOQgXeN+rD9z/6DStOpOxNFQn4Cgcquot05gZc+CdOujA==",
13226 "dependencies": {
13186 - "@intlify/core-base": "9.8.0",
13187 - "@intlify/shared": "9.8.0",
13227 + "@intlify/core-base": "9.9.0",
13228 + "@intlify/shared": "9.9.0",
13229 "@vue/devtools-api": "^6.5.0"
13230 },
13231 "engines": {
package.json
+21 -18
@@ -8,7 +8,7 @@
8 }
9 },
10 "scripts": {
11 - "dev": "vite --host 127.0.0.1",
11 + "dev": "vite --host 0.0.0.0",
12 "dev:debug": "DEBUG=vite:* vite",
13 "build": "run-p type-check build-only",
14 "build-only": "vite build",
@@ -22,7 +22,10 @@
22 "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
23 "tailwind-config-viewer": "tailwind-config-viewer -o",
24 "design-tokens": "node scripts/tokens-tool.js",
25 - "start-server": "cd backend && uvicorn copilot:app --reload --port=5000",
25 + "start-server-old": "cd backend && uvicorn copilot:app --reload --port=5000",
26 + "start-server": "cd backend && /opt/venv/bin/python copilot.py",
27 + "start-vue": "vite --host 0.0.0.0",
28 + "start": "concurrently \"npm run start-server\" \"npm run start-vue\"",
29 "libs-check": "taze",
30 "open:swagger": "open http://127.0.0.1:5000/docs#/",
31 "open:redoc": "open http://127.0.0.1:5000/redoc"
@@ -46,16 +49,16 @@
49 "js-md5": "^0.8.3",
50 "lodash": "^4.17.21",
51 "mitt": "^3.0.1",
49 - "naive-ui": "^2.36.0",
52 + "naive-ui": "^2.37.3",
53 "password-validator": "^5.3.0",
54 "pinia": "^2.1.7",
55 "pinia-plugin-persistedstate": "^3.2.1",
56 "secure-ls": "^1.2.6",
57 "validator": "^13.11.0",
55 - "vue": "^3.4.0",
58 + "vue": "^3.4.7",
59 "vue-advanced-cropper": "^2.8.8",
60 "vue-highlight-words": "^3.0.1",
58 - "vue-i18n": "^9.8.0",
61 + "vue-i18n": "^9.9.0",
62 "vue-router": "^4.2.5",
63 "vue-sjv": "^0.0.6",
64 "vue3-apexcharts": "^1.4.4",
@@ -70,9 +73,9 @@
73 "@types/inquirer": "^9.0.7",
74 "@types/jsdom": "^21.1.6",
75 "@types/lodash": "^4.14.202",
73 - "@types/node": "^20.10.5",
74 - "@types/validator": "^13.11.7",
75 - "@vitejs/plugin-vue": "^5.0.0",
76 + "@types/node": "^20.10.8",
77 + "@types/validator": "^13.11.8",
78 + "@vitejs/plugin-vue": "^5.0.3",
79 "@vitejs/plugin-vue-jsx": "^3.1.0",
80 "@vue/eslint-config-prettier": "^9.0.0",
81 "@vue/eslint-config-typescript": "^12.0.0",
@@ -84,28 +87,28 @@
87 "eslint-plugin-cypress": "^2.15.1",
88 "eslint-plugin-vue": "^9.19.2",
89 "fs-extra": "^11.2.0",
87 - "jsdom": "^23.0.1",
90 + "jsdom": "^23.2.0",
91 "json5": "^2.2.3",
92 "npm-run-all": "^4.1.5",
93 "picocolors": "^1.0.0",
91 - "postcss": "^8.4.32",
94 + "postcss": "^8.4.33",
95 "prettier": "^3.1.1",
93 - "sass": "^1.69.6",
96 + "sass": "^1.69.7",
97 "start-server-and-test": "^2.0.3",
98 "tailwind-config-viewer": "^1.7.3",
96 - "tailwindcss": "^3.4.0",
99 + "tailwindcss": "^3.4.1",
100 "taze": "^0.13.1",
101 "ts-node": "^10.9.2",
99 - "typescript": "~5.3.3",
102 + "typescript": "~5.2.2",
103 "unplugin-vue-components": "^0.26.0",
101 - "vite": "^5.0.10",
102 - "vite-bundle-analyzer": "^0.5.0",
104 + "vite": "^5.0.11",
105 + "vite-bundle-analyzer": "^0.6.1",
106 "vite-bundle-visualizer": "^1.0.0",
107 "vite-svg-loader": "^5.1.0",
105 - "vitest": "^1.1.0",
108 + "vitest": "^1.1.3",
109 "vue-tsc": "^1.8.27"
110 },
111 "engines": {
109 - "node": ">=16.0.0 <=20.7.0"
112 + "node": ">=18.0.0"
113 }
111 -}
114 +}
\ No newline at end of file
public/images/avatar-200.jpg
Binary files a/public/images/avatar-200.jpg and /dev/null differ
public/images/avatar-64.jpg
Binary files a/public/images/avatar-64.jpg and /dev/null differ
public/images/headphones.jpg
Binary files a/public/images/headphones.jpg and /dev/null differ
public/images/login/cover.webp
Binary files /dev/null and b/public/images/login/cover.webp differ
public/images/login/video.mp4
Binary files /dev/null and b/public/images/login/video.mp4 differ
public/images/logo-1.png
Binary files a/public/images/logo-1.png and /dev/null differ
public/images/logo-2.png
Binary files a/public/images/logo-2.png and /dev/null differ
src/App.vue
+4 -2
@@ -14,7 +14,9 @@
14
15 <SplashScreen :loading="loading" />
16 <SearchDialog v-if="isLogged" />
17 - <LayoutSettings />
17 + <!--
18 + <LayoutSettings />
19 + -->
20 </Provider>
21 </template>
22
@@ -28,7 +30,7 @@ import HorizontalNav from "@/layouts/HorizontalNav/index.vue"
30 import Blank from "@/layouts/Blank/index.vue"
31 import Provider from "@/layouts/common/Provider.vue"
32 import SplashScreen from "@/layouts/common/SplashScreen.vue"
31 -import LayoutSettings from "@/components/common/LayoutSettings.vue"
33 +// import LayoutSettings from "@/components/common/LayoutSettings.vue"
34 import SearchDialog from "@/components/common/SearchDialog.vue"
35 import { Layout, RouterTransition, type ThemeName } from "@/types/theme.d"
36 import { type RouteLocationNormalized, useRouter, useRoute } from "vue-router"
src/api/auth.ts
+13
@@ -19,5 +19,18 @@ export default {
19 },
20 getUsers() {
21 return HttpClient.get<FlaskBaseResponse & { users: AuthUser[] }>("/auth/users")
22 + },
23 + /** need admin role */
24 + resetPassword(username: string, password: string) {
25 + return HttpClient.post<FlaskBaseResponse>("/auth/reset-password", {
26 + username,
27 + new_password: password
28 + })
29 + },
30 + resetOwnPassword(username: string, password: string) {
31 + return HttpClient.post<FlaskBaseResponse>("/auth/reset-password/me", {
32 + username,
33 + new_password: password
34 + })
35 }
36 }
src/api/connectors.ts
+5
@@ -12,6 +12,11 @@ export default {
12 update(connectorId: string | number, payload: ConnectorRequestPayload) {
13 return HttpClient.put<FlaskBaseResponse & { connectors: Connector[] }>(`/connectors/${connectorId}`, payload)
14 },
15 + verify(connectorId: string | number) {
16 + return HttpClient.post<FlaskBaseResponse & { connectionSuccessful: boolean }>(
17 + `/connectors/verify/${connectorId}`
18 + )
19 + },
20 upload(connectorId: string | number, formData: FormData) {
21 return HttpClient.post<FlaskBaseResponse & { connectors: Connector[] }>(
22 `/connectors/upload/${connectorId}`,
src/api/flow.ts new
+15
@@ -0,0 +1,15 @@
1 +import { HttpClient } from "./httpClient"
2 +import type { FlaskBaseResponse } from "@/types/flask.d"
3 +import type { CollectResult, FlowResult } from "@/types/flow.d"
4 +
5 +export default {
6 + getAllByAgent(hostname: string) {
7 + return HttpClient.get<FlaskBaseResponse & { results: FlowResult[] }>(`/flows/${hostname}`)
8 + },
9 + retrieve(clientId: string, sessionId: string) {
10 + return HttpClient.post<FlaskBaseResponse & { results: CollectResult[] }>(`/flows/retrieve`, {
11 + client_id: clientId,
12 + session_id: sessionId
13 + })
14 + }
15 +}
src/api/index.ts
+3 -1
@@ -11,6 +11,7 @@ import threatIntel from "./threatIntel"
11 import askSocfortress from "./askSocfortress"
12 import customers from "./customers"
13 import logs from "./logs"
14 +import flow from "./flow"
15
16 export default {
17 agents,
@@ -25,5 +26,6 @@ export default {
26 threatIntel,
27 askSocfortress,
28 customers,
28 - logs
29 + logs,
30 + flow
31 }
src/api/soc.ts
+31 -6
@@ -1,6 +1,6 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 import { HttpClient } from "./httpClient"
3 -import type { SocAlert } from "@/types/soc/alert.d"
3 +import type { SocAlert, SocAlertCaseResponse } from "@/types/soc/alert.d"
4 import type { SocCase, SocCaseExt } from "@/types/soc/case.d"
5 import type { SocAsset, SocAssetsState } from "@/types/soc/asset.d"
6 import type { SocNewNote, SocNote } from "@/types/soc/note.d"
@@ -11,14 +11,36 @@ export interface CasesFilter {
11 unit: TimeUnit
12 }
13
14 +export interface AlertsFilter {
15 + pageSize: number
16 + page: number
17 + sort: "desc" | "asc"
18 + alertTitle: string
19 +}
20 +
21 type TimeUnit = "hours" | "days" | "weeks"
22
23 export default {
17 - getAlerts() {
18 - return HttpClient.get<FlaskBaseResponse & { alerts: SocAlert[] }>(`/soc/alerts`)
24 + getAlerts(filters?: Partial<AlertsFilter>, signal?: AbortSignal) {
25 + return HttpClient.post<FlaskBaseResponse & { alerts: SocAlert[] }>(
26 + `/soc/alerts`,
27 + {
28 + per_page: filters?.pageSize || 1000,
29 + page: filters?.page || 1,
30 + sort: filters?.sort || "desc",
31 + alert_title: filters?.alertTitle || ""
32 + },
33 + signal ? { signal } : {}
34 + )
35 },
20 - getAlertsBookmark() {
21 - return HttpClient.get<FlaskBaseResponse & { bookmarked_alerts: SocAlert[] }>(`/soc/alerts/bookmark`)
36 + getAlert(alertId: string) {
37 + return HttpClient.get<FlaskBaseResponse & { alert: SocAlert }>(`/soc/alerts/${alertId}`)
38 + },
39 + getAlertsBookmark(signal?: AbortSignal) {
40 + return HttpClient.get<FlaskBaseResponse & { bookmarked_alerts: SocAlert[] }>(
41 + `/soc/alerts/bookmark`,
42 + signal ? { signal } : {}
43 + )
44 },
45 getAlertsByUser(userId: string, signal?: AbortSignal) {
46 return HttpClient.get<FlaskBaseResponse & { alerts: SocAlert[] }>(
@@ -32,6 +54,9 @@ export default {
54 removeAlertBookmark(alertId: string) {
55 return HttpClient.delete<FlaskBaseResponse & { alert: SocAlert }>(`/soc/alerts/bookmark/${alertId}`)
56 },
57 + createCase(alertId: string) {
58 + return HttpClient.post<FlaskBaseResponse & { case: SocAlertCaseResponse }>(`/soc/alerts/create_case/${alertId}`)
59 + },
60 getCases(payload?: string | CasesFilter) {
61 let apiMethod: "get" | "post" = "get"
62 let url = `/soc/cases`
@@ -56,7 +81,7 @@ export default {
81 older_than: payload?.olderThan || 1,
82 time_unit: payload?.unit || "days"
83 }
59 - }
84 + }
85 : undefined
86 )
87 },
src/assets/scss/common.scss
-8
@@ -28,17 +28,9 @@
28
29 .page-wrapped {
30 height: calc(100svh - var(--toolbar-height) - var(--view-padding) - (var(--view-padding) / 2));
31 -
32 - &.layout-HorizontalNav {
33 - height: calc(100svh - var(--toolbar-height) - var(--view-padding) - var(--header-bar-height));
34 - }
31 }
32 .page-min-wrapped {
33 min-height: calc(100svh - var(--toolbar-height) - var(--view-padding) - (var(--view-padding) / 2));
38 -
39 - &.layout-HorizontalNav {
40 - min-height: calc(100svh - var(--toolbar-height) - var(--view-padding) - var(--header-bar-height));
41 - }
34 }
35
36 .page {
src/assets/scss/vuesjv-override.scss
+4 -4
@@ -1,10 +1,10 @@
1 .vuesjv-override {
2 - color: var(--fg-color);
3 - font-family: var(--font-family-mono);
4 - line-height: 1.7;
2 + color: var(--fg-color) !important;
3 + font-family: var(--font-family-mono) !important;
4 + line-height: 1.7 !important;
5
6 .font-mono {
7 - font-family: var(--font-family-mono);
7 + font-family: var(--font-family-mono) !important;
8 }
9
10 i {
src/components/AuthForm/SignIn.vue
+5 -3
@@ -21,9 +21,11 @@
21 />
22 </n-form-item>
23 <div class="flex flex-col items-end gap-6">
24 - <div class="flex justify-end w-full">
25 - <n-button text type="primary" @click="emit('goto-forgot-password')">Forgot Password?</n-button>
26 - </div>
24 + <!--
25 + <div class="flex justify-end w-full">
26 + <n-button text type="primary" @click="emit('goto-forgot-password')">Forgot Password?</n-button>
27 + </div>
28 + -->
29 <div class="w-full">
30 <n-button type="primary" @click="signIn" class="!w-full" size="large">Sign in</n-button>
31 </div>
src/components/AuthForm/index.vue
+6 -4
@@ -2,9 +2,7 @@
2 <div class="form-wrap">
3 <Logo mini :dark="isDark" class="mb-4" />
4 <div class="title mb-4">{{ title }}</div>
5 - <div class="text mb-12">
6 - Today is a new day. It's your day. You shape it. Sign in to start managing your projects.
7 - </div>
5 + <div class="text mb-12">Access the world of OpenSource security: Simplified, Streamlined, Accessible.</div>
6
7 <div class="form">
8 <transition name="form-fade" mode="out-in" appear>
@@ -49,7 +47,11 @@ const typeRef = ref<FormType>("signin")
47
48 const isDark = computed<boolean>(() => useThemeStore().isThemeDark)
49 const title = computed<string>(() =>
52 - typeRef.value === "signin" ? "Welcome Back" : typeRef.value === "signup" ? "Hello" : "Forgot Password"
50 + typeRef.value === "signin"
51 + ? "SOCFortress CoPilot"
52 + : typeRef.value === "signup"
53 + ? "SOCFortress CoPilot"
54 + : "Forgot Password"
55 )
56
57 function gotoSignIn() {
src/components/agents/AgentCard.vue
+2 -2
@@ -188,8 +188,8 @@ function toggleCritical(agentId: string, criticalStatus: boolean) {
188
189 &.online {
190 padding: 0px 15px;
191 - color: var(--primary-color);
192 - border-color: var(--primary-color);
191 + color: var(--success-color);
192 + border-color: var(--success-color);
193 }
194 }
195
src/components/agents/AgentToolbar.vue
+2 -2
@@ -1,6 +1,6 @@
1 <template>
2 - <n-card class="agent-toolbar">
3 - <div class="wrapper flex flex-col gap-6">
2 + <n-card class="agent-toolbar" content-style="padding:0">
3 + <div class="wrapper flex flex-col gap-6 py-3 px-4">
4 <div class="flex flex-col gap-2">
5 <div class="agent-search flex gap-3">
6 <n-input placeholder="Search for an agent" clearable v-model:value="textFilter">
src/components/agents/OverviewSection.vue
+6 -2
@@ -5,7 +5,11 @@
5 <template #key>{{ item.key }}</template>
6 <template #value>
7 <template v-if="item.key === 'customer_code'">
8 - <code class="cursor-pointer text-primary-color" @click="gotoCustomer(item.val)" v-if="item.val">
8 + <code
9 + class="cursor-pointer text-primary-color"
10 + @click="gotoCustomer(item.val)"
11 + v-if="item.val && item.val !== '-'"
12 + >
13 {{ item.val }}
14 <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
15 </code>
@@ -61,7 +65,7 @@ const formatDate = (date: string) => {
65 }
66
67 function gotoCustomer(code: string | number) {
64 - router.push(`/customers?code=${code}`).catch(() => {})
68 + router.push({ name: "Customers", query: { code } })
69 }
70 </script>
71
src/components/agents/agentFlow/AgentFlowCollectList.vue new
+81
@@ -0,0 +1,81 @@
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>{{ collectList.length }}</code>
8 + </div>
9 + </div>
10 + </div>
11 + <div class="list my-3">
12 + <template v-if="collectList.length">
13 + <CollectItem
14 + v-for="item of collectList"
15 + :key="item.___id"
16 + :collect="item"
17 + embedded
18 + class="mb-4 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 } from "vue"
30 +import { useMessage, NSpin, NEmpty } from "naive-ui"
31 +import CollectItem from "@/components/artifacts/CollectItem.vue"
32 +import Api from "@/api"
33 +import type { CollectResult, FlowResult } from "@/types/flow.d"
34 +import { nanoid } from "nanoid"
35 +
36 +interface CollectResultExt extends CollectResult {
37 + ___id?: string
38 +}
39 +
40 +const { flow } = defineProps<{
41 + flow: FlowResult
42 +}>()
43 +
44 +const message = useMessage()
45 +const loading = ref(false)
46 +const collectList = ref<CollectResultExt[]>([])
47 +
48 +function getData() {
49 + loading.value = true
50 +
51 + Api.flow
52 + .retrieve(flow.client_id, flow.session_id)
53 + .then(res => {
54 + if (res.data.success) {
55 + collectList.value = (res.data.results || []).map(o => {
56 + o.___id = nanoid()
57 + return o
58 + })
59 + } else {
60 + message.warning(res.data?.message || "An error occurred. Please try again later.")
61 + }
62 + })
63 + .catch(err => {
64 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
65 + })
66 + .finally(() => {
67 + loading.value = false
68 + })
69 +}
70 +
71 +onBeforeMount(() => {
72 + getData()
73 +})
74 +</script>
75 +
76 +<style lang="scss" scoped>
77 +.list {
78 + container-type: inline-size;
79 + min-height: 200px;
80 +}
81 +</style>
src/components/agents/agentFlow/AgentFlowItem.vue new
+266
@@ -0,0 +1,266 @@
1 +<template>
2 + <div class="item flex flex-col gap-2 px-5 py-3" :class="{ embedded }">
3 + <div class="header-box flex justify-between">
4 + <div class="flex items-center gap-2">
5 + <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
6 + <span>{{ flow.session_id }}</span>
7 + <Icon :name="InfoIcon" :size="16"></Icon>
8 + </div>
9 + </div>
10 + <div class="time">
11 + <n-popover overlap placement="top-end" style="max-height: 240px" scrollable to="body">
12 + <template #trigger>
13 + <div class="flex items-center gap-2 cursor-help">
14 + <span>
15 + {{ formatDate(flow.start_time) }}
16 + </span>
17 + <Icon :name="TimeIcon" :size="16"></Icon>
18 + </div>
19 + </template>
20 + <div class="flex flex-col py-2 px-1">
21 + <AgentFlowTimeline :flow="flow" />
22 + </div>
23 + </n-popover>
24 + </div>
25 + </div>
26 + <div class="main-box">
27 + <div class="content flex flex-wrap gap-2">
28 + <span v-for="artifact of flow.artifacts_with_results" :key="artifact" class="artifact-label">
29 + {{ artifact }}
30 + </span>
31 + </div>
32 + <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
33 + <Badge type="splitted">
34 + <template #label>State</template>
35 + <template #value>{{ flow.state || "-" }}</template>
36 + </Badge>
37 + <Badge type="splitted">
38 + <template #label>Status</template>
39 + <template #value>{{ flow.status || "-" }}</template>
40 + </Badge>
41 + <Badge type="splitted">
42 + <template #label>Exec. time</template>
43 + <template #value>{{ executionDuration }}</template>
44 + </Badge>
45 + <Badge :type="flow.dirty ? 'active' : 'muted'">
46 + <template #iconRight>
47 + <Icon :name="flow.dirty ? EnabledIcon : DisabledIcon" :size="14"></Icon>
48 + </template>
49 + <template #label>Dirty</template>
50 + </Badge>
51 + <Badge :type="flow.user_notified ? 'active' : 'muted'">
52 + <template #iconRight>
53 + <Icon :name="flow.user_notified ? EnabledIcon : DisabledIcon" :size="14"></Icon>
54 + </template>
55 + <template #label>User notified</template>
56 + </Badge>
57 + </div>
58 + </div>
59 + <div class="footer-box">
60 + <div class="time">{{ formatDate(flow.start_time) }}</div>
61 + </div>
62 +
63 + <n-modal
64 + v-model:show="showDetails"
65 + preset="card"
66 + content-style="padding:0px"
67 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
68 + :title="'Agent Flow: ' + flow.session_id"
69 + :bordered="false"
70 + segmented
71 + >
72 + <n-tabs type="line" animated :tabs-padding="24">
73 + <n-tab-pane name="Info" tab="Info" display-directive="show">
74 + <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
75 + <KVCard v-for="(value, key) of properties" :key="key">
76 + <template #key>{{ key }}</template>
77 + <template #value>{{ value === "" ? "-" : value ?? "-" }}</template>
78 + </KVCard>
79 + </div>
80 + </n-tab-pane>
81 + <n-tab-pane name="Timeline" tab="Timeline" display-directive="show:lazy">
82 + <div class="p-7 pt-4">
83 + <AgentFlowTimeline :flow="flow" />
84 + </div>
85 + </n-tab-pane>
86 + <n-tab-pane name="Logs" tab="Logs" display-directive="show:lazy">
87 + <div class="p-7 pt-4" v-if="flow.logs.length">
88 + <ul>
89 + <li v-for="log of flow.logs" :key="log">{{ log }}</li>
90 + </ul>
91 + </div>
92 + <n-empty v-else description="No items found" class="justify-center h-48" />
93 + </n-tab-pane>
94 + <n-tab-pane name="Uploaded files" tab="Uploaded files" display-directive="show:lazy">
95 + <div class="p-7 pt-4" v-if="flow.uploaded_files.length">
96 + <ul>
97 + <li v-for="file of flow.uploaded_files" :key="file">{{ file }}</li>
98 + </ul>
99 + </div>
100 + <n-empty v-else description="No items found" class="justify-center h-48" />
101 + </n-tab-pane>
102 + <n-tab-pane name="Query stats" tab="Query stats" display-directive="show:lazy">
103 + <div class="p-7 pt-4" style="container-type: inline-size">
104 + <template v-if="flow.query_stats.length">
105 + <AgentFlowQueryStat
106 + v-for="stat of flow.query_stats"
107 + :key="stat.first_active + stat.last_active"
108 + :stat="stat"
109 + embedded
110 + class="mb-2 item-appear item-appear-bottom item-appear-005"
111 + />
112 + </template>
113 + <n-empty description="No items found" class="justify-center h-48" v-else />
114 + </div>
115 + </n-tab-pane>
116 + <n-tab-pane name="Request" tab="Request" display-directive="show:lazy">
117 + <div class="p-7 pt-4">
118 + <SimpleJsonViewer
119 + class="vuesjv-override"
120 + :model-value="flow.request"
121 + :initialExpandedDepth="2"
122 + />
123 + </div>
124 + </n-tab-pane>
125 + <n-tab-pane name="Collect" tab="Collect" display-directive="show:lazy">
126 + <n-scrollbar style="max-height: 430px" trigger="none">
127 + <div class="px-7">
128 + <AgentFlowCollectList :flow="flow" />
129 + </div>
130 + </n-scrollbar>
131 + </n-tab-pane>
132 + </n-tabs>
133 + </n-modal>
134 + </div>
135 +</template>
136 +
137 +<script setup lang="ts">
138 +import { NPopover, NModal, NTabs, NTabPane, NEmpty, NScrollbar } from "naive-ui"
139 +import { useSettingsStore } from "@/stores/settings"
140 +import dayjs from "@/utils/dayjs"
141 +import type { FlowResult } from "@/types/flow.d"
142 +import Icon from "@/components/common/Icon.vue"
143 +import AgentFlowTimeline from "./AgentFlowTimeline.vue"
144 +import AgentFlowQueryStat from "./AgentFlowQueryStat.vue"
145 +import AgentFlowCollectList from "./AgentFlowCollectList.vue"
146 +import KVCard from "@/components/common/KVCard.vue"
147 +import { computed, ref } from "vue"
148 +import _pick from "lodash/pick"
149 +import Badge from "@/components/common/Badge.vue"
150 +import { SimpleJsonViewer } from "vue-sjv"
151 +import "@/assets/scss/vuesjv-override.scss"
152 +
153 +const { flow, embedded } = defineProps<{ flow: FlowResult; embedded?: boolean }>()
154 +
155 +const TimeIcon = "carbon:time"
156 +const InfoIcon = "carbon:information"
157 +const DisabledIcon = "carbon:subtract"
158 +const EnabledIcon = "carbon:checkmark"
159 +
160 +const showDetails = ref(false)
161 +const dFormats = useSettingsStore().dateFormat
162 +
163 +const executionDuration = computed(() => dayjs.duration(flow.execution_duration).humanize())
164 +
165 +const properties = computed(() => {
166 + return _pick(flow, [
167 + "backtrace",
168 + "client_id",
169 + "next_response_id",
170 + "outstanding_requests",
171 + "total_collected_rows",
172 + "total_expected_uploaded_bytes",
173 + "total_loads",
174 + "total_logs",
175 + "total_requests",
176 + "total_uploaded_bytes",
177 + "total_uploaded_files",
178 + "user_notified"
179 + ])
180 +})
181 +
182 +function formatDate(timestamp: number): string {
183 + return dayjs(timestamp / 1000).format(dFormats.datetimesec)
184 +}
185 +</script>
186 +
187 +<style lang="scss" scoped>
188 +.item {
189 + border-radius: var(--border-radius);
190 + background-color: var(--bg-color);
191 + transition: all 0.2s var(--bezier-ease);
192 + border: var(--border-small-050);
193 +
194 + .header-box {
195 + font-family: var(--font-family-mono);
196 + font-size: 13px;
197 + .id {
198 + word-break: break-word;
199 + color: var(--fg-secondary-color);
200 + line-height: 1.2;
201 +
202 + &:hover {
203 + color: var(--primary-color);
204 + }
205 + }
206 + .time {
207 + color: var(--fg-secondary-color);
208 +
209 + &:hover {
210 + color: var(--primary-color);
211 + }
212 + }
213 + }
214 +
215 + .main-box {
216 + .content {
217 + word-break: break-word;
218 +
219 + .artifact-label {
220 + background-color: var(--bg-secondary-color);
221 + padding: 3px 8px;
222 + border-radius: var(--border-radius);
223 + }
224 + }
225 + }
226 + .footer-box {
227 + font-family: var(--font-family-mono);
228 + display: none;
229 + text-align: right;
230 + font-size: 13px;
231 + margin-top: 10px;
232 +
233 + .time {
234 + color: var(--fg-secondary-color);
235 + width: 100%;
236 + }
237 + }
238 +
239 + &:hover {
240 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
241 + }
242 +
243 + &.embedded {
244 + background-color: var(--bg-secondary-color);
245 +
246 + .main-box {
247 + .content {
248 + .artifact-label {
249 + background-color: var(--bg-color);
250 + }
251 + }
252 + }
253 + }
254 +
255 + @container (max-width: 550px) {
256 + .header-box {
257 + .time {
258 + display: none;
259 + }
260 + }
261 + .footer-box {
262 + display: flex;
263 + }
264 + }
265 +}
266 +</style>
src/components/agents/agentFlow/AgentFlowList.vue new
+83
@@ -0,0 +1,83 @@
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>{{ flowList.length }}</code>
8 + </div>
9 + </div>
10 + </div>
11 + <div class="list my-3">
12 + <template v-if="flowList.length">
13 + <AgentFlowItem
14 + v-for="item of flowList"
15 + :key="item.id"
16 + :flow="item"
17 + embedded
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 } from "vue"
30 +import { useMessage, NSpin, NEmpty } from "naive-ui"
31 +import AgentFlowItem from "./AgentFlowItem.vue"
32 +import Api from "@/api"
33 +import type { Agent } from "@/types/agents.d"
34 +import type { FlowResult } from "@/types/flow.d"
35 +import { nanoid } from "nanoid"
36 +
37 +interface FlowResultExt extends FlowResult {
38 + id?: string
39 +}
40 +
41 +const props = defineProps<{
42 + agent: Agent
43 +}>()
44 +const { agent } = toRefs(props)
45 +
46 +const message = useMessage()
47 +const loading = ref(false)
48 +const flowList = ref<FlowResultExt[]>([])
49 +
50 +function getData() {
51 + loading.value = true
52 +
53 + Api.flow
54 + .getAllByAgent(agent.value.hostname)
55 + .then(res => {
56 + if (res.data.success) {
57 + flowList.value = ((res.data.results as FlowResultExt[]) || []).map(o => {
58 + o.id = nanoid()
59 + return o
60 + })
61 + } else {
62 + message.warning(res.data?.message || "An error occurred. Please try again later.")
63 + }
64 + })
65 + .catch(err => {
66 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
67 + })
68 + .finally(() => {
69 + loading.value = false
70 + })
71 +}
72 +
73 +onBeforeMount(() => {
74 + getData()
75 +})
76 +</script>
77 +
78 +<style lang="scss" scoped>
79 +.list {
80 + container-type: inline-size;
81 + min-height: 200px;
82 +}
83 +</style>
src/components/agents/agentFlow/AgentFlowQueryStat.vue new
+177
@@ -0,0 +1,177 @@
1 +<template>
2 + <div class="item flex flex-col gap-2 px-5 py-3" :class="{ embedded }">
3 + <div class="header-box flex justify-between items-center gap-3">
4 + <div class="id grow flex flex-wrap gap-2">
5 + <span>
6 + {{ formatDate(stat.first_active) }}
7 + </span>
8 + <span>•</span>
9 + <span>
10 + {{ formatDate(stat.last_active) }}
11 + </span>
12 + </div>
13 + <div class="actions whitespace-nowrap">
14 + <Badge type="cursor" @click="showDetails = true">
15 + <template #iconLeft>
16 + <Icon :name="InfoIcon" :size="14"></Icon>
17 + </template>
18 + <template #value>Details</template>
19 + </Badge>
20 + </div>
21 + </div>
22 + <div class="main-box flex flex-col gap-2 mt-2">
23 + <div class="content flex flex-wrap gap-2">
24 + <span v-for="artifact of stat.names_with_response" :key="artifact" class="artifact-label">
25 + {{ artifact }}
26 + </span>
27 + </div>
28 + <div class="error-message" v-if="stat.error_message">
29 + {{ stat.error_message }}
30 + </div>
31 + <div class="badges-box flex flex-wrap items-center gap-3 mt-1">
32 + <Badge type="splitted">
33 + <template #label>Status</template>
34 + <template #value>{{ stat.status || "-" }}</template>
35 + </Badge>
36 + <Badge type="splitted">
37 + <template #label>Duration</template>
38 + <template #value>{{ duration }}</template>
39 + </Badge>
40 + </div>
41 + </div>
42 + <div class="footer-box">
43 + <div class="actions whitespace-nowrap">
44 + <Badge type="cursor" @click="showDetails = true">
45 + <template #iconLeft>
46 + <Icon :name="InfoIcon" :size="14"></Icon>
47 + </template>
48 + <template #value>Details</template>
49 + </Badge>
50 + </div>
51 + </div>
52 +
53 + <n-modal
54 + v-model:show="showDetails"
55 + preset="card"
56 + content-style="padding:0px"
57 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
58 + :title="`Agent Query Stat ${stat.Artifact ? ': ' + stat.Artifact : ''}`"
59 + :bordered="false"
60 + segmented
61 + >
62 + <n-tabs type="line" animated :tabs-padding="24">
63 + <n-tab-pane name="Info" tab="Info" display-directive="show">
64 + <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
65 + <KVCard v-for="(value, key) of properties" :key="key">
66 + <template #key>{{ key }}</template>
67 + <template #value>{{ value === "" ? "-" : value ?? "-" }}</template>
68 + </KVCard>
69 + </div>
70 + </n-tab-pane>
71 + </n-tabs>
72 + </n-modal>
73 + </div>
74 +</template>
75 +
76 +<script setup lang="ts">
77 +import { NModal, NTabs, NTabPane } from "naive-ui"
78 +import { useSettingsStore } from "@/stores/settings"
79 +import dayjs from "@/utils/dayjs"
80 +import type { FlowQueryStat } from "@/types/flow.d"
81 +import Icon from "@/components/common/Icon.vue"
82 +import KVCard from "@/components/common/KVCard.vue"
83 +import { computed, ref } from "vue"
84 +import _pick from "lodash/pick"
85 +import Badge from "@/components/common/Badge.vue"
86 +
87 +const { stat, embedded } = defineProps<{ stat: FlowQueryStat; embedded?: boolean }>()
88 +
89 +const InfoIcon = "carbon:information"
90 +
91 +const showDetails = ref(false)
92 +const dFormats = useSettingsStore().dateFormat
93 +
94 +const duration = computed(() => dayjs.duration(stat.duration).humanize())
95 +
96 +const properties = computed(() => {
97 + return _pick(stat, [
98 + "backtrace",
99 + "Artifact",
100 + "log_rows",
101 + "uploaded_files",
102 + "uploaded_bytes",
103 + "expected_uploaded_bytes",
104 + "result_rows",
105 + "query_id",
106 + "total_queries"
107 + ])
108 +})
109 +
110 +function formatDate(timestamp: number): string {
111 + return dayjs(timestamp / 1000).format(dFormats.datetimesecmill)
112 +}
113 +</script>
114 +
115 +<style lang="scss" scoped>
116 +.item {
117 + background-color: var(--bg-color);
118 + border-radius: var(--border-radius);
119 + border: var(--border-small-050);
120 + transition: all 0.2s var(--bezier-ease);
121 +
122 + .header-box {
123 + font-size: 13px;
124 + .id {
125 + font-family: var(--font-family-mono);
126 + word-break: break-word;
127 + color: var(--fg-secondary-color);
128 + line-height: 1.2;
129 + }
130 + }
131 +
132 + .main-box {
133 + .content {
134 + word-break: break-word;
135 +
136 + .artifact-label {
137 + background-color: var(--bg-secondary-color);
138 + padding: 3px 8px;
139 + border-radius: var(--border-radius);
140 + }
141 + }
142 + }
143 + .footer-box {
144 + display: none;
145 + text-align: right;
146 + font-size: 13px;
147 + margin-top: 10px;
148 + }
149 +
150 + &:hover {
151 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
152 + }
153 +
154 + &.embedded {
155 + background-color: var(--bg-secondary-color);
156 +
157 + .main-box {
158 + .content {
159 + .artifact-label {
160 + background-color: var(--bg-color);
161 + }
162 + }
163 + }
164 + }
165 +
166 + @container (max-width: 550px) {
167 + .header-box {
168 + .actions {
169 + display: none;
170 + }
171 + }
172 + .footer-box {
173 + display: flex;
174 + }
175 + }
176 +}
177 +</style>
src/components/agents/agentFlow/AgentFlowTimeline.vue new
+27
@@ -0,0 +1,27 @@
1 +<template>
2 + <n-timeline>
3 + <n-timeline-item type="success" title="Start" :time="formatDate(flow.start_time)" />
4 + <n-timeline-item
5 + v-if="flow.create_time"
6 + title="Create"
7 + :time="formatDate(flow.create_time)"
8 + line-type="dashed"
9 + />
10 + <n-timeline-item v-if="flow.active_time" title="Active" :time="formatDate(flow.active_time)" />
11 + </n-timeline>
12 +</template>
13 +
14 +<script setup lang="ts">
15 +import { useSettingsStore } from "@/stores/settings"
16 +import dayjs from "@/utils/dayjs"
17 +import { NTimeline, NTimelineItem } from "naive-ui"
18 +import type { FlowResult } from "@/types/flow.d"
19 +
20 +const { flow } = defineProps<{ flow: FlowResult }>()
21 +
22 +const dFormats = useSettingsStore().dateFormat
23 +
24 +function formatDate(timestamp: number): string {
25 + return dayjs(timestamp / 1000).format(dFormats.datetimesec)
26 +}
27 +</script>
src/components/alerts/Alert.vue
+9 -8
@@ -2,7 +2,7 @@
2 <div class="alert-details flex flex-col gap-2 px-5 py-4">
3 <div class="header-box flex justify-between">
4 <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
5 - <span>#{{ alert._id }}</span>
5 + <span>#{{ alert._id || alert._source.id }}</span>
6 <Icon :name="InfoIcon" :size="16"></Icon>
7 </div>
8 <div class="time">
@@ -11,10 +11,10 @@
11 </div>
12 <div class="main-box flex justify-between gap-4">
13 <div class="content">
14 - <div class="rule-description">{{ alert._source.rule_description }}</div>
14 + <div class="rule-description">{{ alert._source.rule_description || alert._source.type }}</div>
15 <div class="rule-groups">{{ alert._source.rule_groups }}</div>
16
17 - <div class="badges-box flex flex-wrap items-center gap-3">
17 + <div class="badges-box flex flex-wrap items-center gap-3" v-if="alert._id">
18 <!--
19 <Badge type="cursor">
20 <template #iconLeft>
@@ -127,12 +127,12 @@
127 preset="card"
128 content-style="padding:0px"
129 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
130 - :title="`Alert: ${alert._id}`"
130 + :title="`Alert: ${alert._id || alert._source.id}`"
131 :bordered="false"
132 segmented
133 >
134 <n-tabs type="line" animated :tabs-padding="24">
135 - <n-tab-pane name="Agent" tab="Agent" display-directive="show">
135 + <n-tab-pane name="Agent" tab="Agent" display-directive="show" v-if="alert._id">
136 <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="agentProperties">
137 <KVCard v-for="(value, key) of agentProperties" :key="key">
138 <template #key>{{ key }}</template>
@@ -241,7 +241,7 @@ const { alert, hideActions } = toRefs(props)
241
242 const InfoIcon = "carbon:information"
243 const TargetIcon = "zondicons:target"
244 -const DisabledIcon = "ph:minus-bold"
244 +const DisabledIcon = "carbon:subtract"
245 const MailIcon = "carbon:email"
246 const AgentIcon = "carbon:police"
247 const LinkIcon = "carbon:launch"
@@ -269,10 +269,11 @@ function formatDate(timestamp: string): string {
269 }
270
271 function gotoAgentPage(agentId: string) {
272 - router.push(`/agent/${agentId}`).catch(() => {})
272 + router.push({ name: "Agent", params: { id: agentId } })
273 }
274 +
275 function gotoCustomer(code: string | number) {
275 - router.push(`/customers?code=${code}`).catch(() => {})
276 + router.push({ name: "Customers", query: { code } })
277 }
278 </script>
279
src/components/alerts/AlertActions.vue
+2 -2
@@ -1,6 +1,6 @@
1 <template>
2 <div class="alert-actions flex flex-col gap-2 justify-end">
3 - <n-button type="primary" secondary :size="size" v-if="alertUrl" tag="a" :href="alertUrl" target="_blank">
3 + <n-button type="success" secondary :size="size" v-if="alertUrl" tag="a" :href="alertUrl" target="_blank">
4 <template #icon><Icon :name="ViewIcon"></Icon></template>
5 View SOC Alert
6 </n-button>
@@ -15,7 +15,7 @@
15 <template #icon><Icon :name="DangerIcon"></Icon></template>
16 Create SOC Alert
17 </n-button>
18 - <n-button type="primary" secondary :size="size" v-if="alertAskMessage" @click="showSocResponse = true">
18 + <n-button type="success" secondary :size="size" v-if="alertAskMessage" @click="showSocResponse = true">
19 <template #icon><Icon :name="ViewIcon"></Icon></template>
20 View SOCFortress Response
21 </n-button>
src/components/alerts/AlertsSummary.vue
+1 -1
@@ -67,7 +67,7 @@ const router = useRouter()
67 const showAllAlerts = ref(false)
68
69 function gotoIndicesPage(index: string) {
70 - router.push(`/indices?index_name=${index}`).catch(() => {})
70 + router.push({ name: "Indices", query: { index_name: index } })
71 }
72 </script>
73
src/components/artifacts/CollectItem.vue
+6 -2
@@ -1,5 +1,5 @@
1 <template>
2 - <div class="collect-item flex flex-wrap gap-2 p-2">
2 + <div class="collect-item flex flex-wrap gap-2 p-2" :class="{ embedded }">
3 <KVCard v-for="prop of displayData" :key="prop.key" :class="{ 'hide-mobile': prop.hideMobile }">
4 <template #key>{{ prop.key }}</template>
5 <template #value>{{ prop.value }}</template>
@@ -39,7 +39,7 @@ interface Prop {
39 hideMobile: boolean
40 }
41
42 -const { collect } = defineProps<{ collect: CollectResult }>()
42 +const { collect, embedded } = defineProps<{ collect: CollectResult; embedded?: boolean }>()
43
44 const jsonData = ref<CollectResult>({})
45 const displayData = ref<Prop[]>([])
@@ -120,6 +120,10 @@ onBeforeMount(() => {
120 border-color: var(--primary-color);
121 }
122
123 + &.embedded {
124 + background-color: var(--bg-secondary-color);
125 + }
126 +
127 @container (max-width: 500px) {
128 flex-direction: column;
129
src/components/common/Badge.vue
+3 -3
@@ -47,9 +47,9 @@ const { type, hintCursor, pointCursor, color } = defineProps<{
47 }
48
49 &.active {
50 - color: var(--primary-color);
51 - background-color: var(--primary-005-color);
52 - border-color: var(--primary-color);
50 + color: var(--success-color);
51 + background-color: var(--success-005-color);
52 + border-color: var(--success-color);
53 }
54
55 &.cursor {
src/components/common/CardStats.vue
+7 -1
@@ -6,7 +6,10 @@
6 <slot name="icon"></slot>
7 </div>
8 <div class="info flex flex-col grow overflow-hidden">
9 - <div class="title">{{ title }}</div>
9 + <div class="title flex items-center gap-2">
10 + {{ title }}
11 + <Icon :name="ArrowRightIcon" v-if="hovered" :size="12"></Icon>
12 + </div>
13 <div class="value mt-1" v-if="value">{{ value }}</div>
14 </div>
15 </div>
@@ -17,6 +20,7 @@
20 <script setup lang="ts">
21 import { NCard } from "naive-ui"
22 import { toRefs } from "vue"
23 +import Icon from "@/components/common/Icon.vue"
24
25 const props = defineProps<{
26 title: string
@@ -25,6 +29,8 @@ const props = defineProps<{
29 hovered?: boolean
30 }>()
31 const { title, value, vertical } = toRefs(props)
32 +
33 +const ArrowRightIcon = "carbon:arrow-right"
34 </script>
35
36 <style scoped lang="scss">
src/components/common/CardStatsDouble.vue
+7 -1
@@ -2,7 +2,10 @@
2 <n-card content-style="padding:0" :class="{ hovered }">
3 <div class="flex flex-col overflow-hidden">
4 <div class="card-header flex gap-4 items-center justify-between">
5 - <div class="title">{{ title }}</div>
5 + <div class="title flex items-center gap-2">
6 + {{ title }}
7 + <Icon :name="ArrowRightIcon" v-if="hovered" :size="12"></Icon>
8 + </div>
9 <div class="icon">
10 <slot name="icon"></slot>
11 </div>
@@ -24,6 +27,7 @@
27 <script setup lang="ts">
28 import { NCard } from "naive-ui"
29 import { toRefs } from "vue"
30 +import Icon from "@/components/common/Icon.vue"
31
32 const props = defineProps<{
33 title: string
@@ -36,6 +40,8 @@ const props = defineProps<{
40 hovered?: boolean
41 }>()
42 const { title, value, subValue, firstLabel, secondLabel, firstStatus, secondStatus } = toRefs(props)
43 +
44 +const ArrowRightIcon = "carbon:arrow-right"
45 </script>
46
47 <style scoped lang="scss">
src/components/common/Notifications/List.vue
+1 -1
@@ -141,7 +141,7 @@ function formatDatetime(date: Date | string) {
141 &.success {
142 .icon-box {
143 .n-icon {
144 - background-color: var(--primary-005-color);
144 + background-color: var(--success-005-color);
145 color: var(--success-color);
146 }
147 }
src/components/common/PaginationIndeterminate.vue new
+84
@@ -0,0 +1,84 @@
1 +<template>
2 + <div class="pagination-indeterminate flex items-center gap-2">
3 + <n-input-number
4 + size="small"
5 + v-model:value="page"
6 + :min="1"
7 + button-placement="both"
8 + class="page"
9 + :disabled="disabled"
10 + >
11 + <template #minus-icon>
12 + <Icon :name="ArrowBackIcon"></Icon>
13 + </template>
14 + <template #add-icon>
15 + <Icon :name="ArrowForwardIcon"></Icon>
16 + </template>
17 + </n-input-number>
18 + <n-select
19 + size="small"
20 + v-if="showPageSizes"
21 + v-model:value="pageSize"
22 + :options="pageSizesOptions"
23 + :show-checkmark="false"
24 + class="page-sizes"
25 + :disabled="disabled"
26 + />
27 + <n-select
28 + size="small"
29 + v-model:value="sort"
30 + :options="sortOptions"
31 + :show-checkmark="false"
32 + class="sort"
33 + :disabled="disabled"
34 + />
35 + </div>
36 +</template>
37 +
38 +<script setup lang="ts">
39 +import { computed, toRefs } from "vue"
40 +import { NSelect, NInputNumber } from "naive-ui"
41 +import _uniqBy from "lodash/uniqBy"
42 +import Icon from "@/components/common/Icon.vue"
43 +import { watch } from "vue"
44 +
45 +const page = defineModel<number>("page", { default: 1 })
46 +const pageSize = defineModel<number>("pageSize", { default: 10 })
47 +const sort = defineModel<"asc" | "desc">("sort", { default: "desc" })
48 +
49 +const props = defineProps<{ showPageSizes?: boolean; pageSizes?: number[]; disabled?: boolean }>()
50 +const { pageSizes, disabled, showPageSizes } = toRefs(props)
51 +
52 +const pageSizesOptions = computed(() =>
53 + (pageSizes.value || [10, 25, 50, 100]).map(o => ({ label: o + " / page", value: o }))
54 +)
55 +
56 +const sortOptions = [
57 + { label: "Desc", value: "desc" },
58 + { label: "Asc", value: "asc" }
59 +]
60 +
61 +const ArrowForwardIcon = "ion:chevron-forward"
62 +const ArrowBackIcon = "ion:chevron-back"
63 +
64 +watch(page, val => {
65 + if (!val) {
66 + page.value = 1
67 + }
68 +})
69 +</script>
70 +
71 +<style lang="scss" scoped>
72 +.pagination-indeterminate {
73 + .page {
74 + width: 90px;
75 + text-align: center;
76 + }
77 + .page-sizes {
78 + width: auto;
79 + }
80 + .sort {
81 + width: auto;
82 + }
83 +}
84 +</style>
src/components/common/SearchDialog.vue
+22 -43
@@ -81,17 +81,20 @@ import { useFullscreenSwitch } from "@/composables/useFullscreenSwitch"
81 import { useSearchDialog } from "@/composables/useSearchDialog"
82 import { getOS } from "@/utils"
83 import Icon from "@/components/common/Icon.vue"
84 +import { emitter } from "@/emitter"
85
86 const SearchIcon = "ion:search-outline"
86 -const TodoIcon = "fluent:task-list-square-add-20-regular"
87 -const EmailIcon = "fluent:mail-edit-20-regular"
88 -const NotesIcon = "fluent:chart-person-20-regular"
87 const ArrowEnterIcon = "fluent:arrow-enter-left-24-regular"
88 const ArrowSortIcon = "fluent:arrow-sort-24-regular"
89 const FullScreenIcon = "fluent:full-screen-maximize-24-regular"
90 const DarkModeIcon = "ion:moon-outline"
91 const CloseIcon = "ion:close"
92
93 +const ConnectorsIcon = "carbon:hybrid-networking"
94 +const AlertsIcon = "carbon:warning-hex"
95 +const SocAlertsIcon = "carbon:security"
96 +const CustomerIcon = "carbon:user-follow"
97 +
98 interface GroupItem {
99 iconName: string | null
100 iconImage: string | null
@@ -121,68 +124,44 @@ const groups = ref<Groups>([
124 name: "Applications",
125 items: [
126 {
124 - iconName: TodoIcon,
127 + iconName: CustomerIcon,
128 iconImage: null,
129 key: 1,
127 - title: "Add todo list",
130 + title: "Add a Customer",
131 label: "Shortcut",
132 action() {
130 - router.push({ name: "Apps-Kanban" })
133 + router.push({ name: "Customers", query: { action: "add-customer" } })
134 + emitter.emit("action:add-customer")
135 }
136 },
137 {
134 - iconName: EmailIcon,
138 + iconName: ConnectorsIcon,
139 iconImage: null,
140 key: 2,
137 - title: "Compose new email",
141 + title: "Configure a connector",
142 label: "Shortcut",
143 action() {
140 - router.push({ name: "Apps-Mailbox" })
144 + router.push({ name: "Connectors" })
145 }
146 },
147 {
144 - iconName: NotesIcon,
148 + iconName: SocAlertsIcon,
149 iconImage: null,
150 key: 3,
147 - title: "View Notes",
151 + title: "View Escalated Alerts",
152 label: "Shortcut",
153 action() {
150 - router.push({ name: "Apps-Notes" })
151 - }
152 - }
153 - ]
154 - },
155 - {
156 - name: "Contacts",
157 - items: [
158 - {
159 - iconName: null,
160 - iconImage: "https://i.pravatar.cc/56?_=" + Math.random(),
161 - key: 4,
162 - title: "Mr. Carlos Keebler DVM",
163 - label: "maximillia_farrell85@yahoo.com",
164 - action() {
165 - router.push({ name: "Apps-Chat" })
166 - }
167 - },
168 - {
169 - iconName: null,
170 - iconImage: "https://i.pravatar.cc/56?_=" + Math.random(),
171 - key: 5,
172 - title: "Rosie Wisozk DDS",
173 - label: "susie_cummings@gmail.com",
174 - action() {
175 - router.push({ name: "Apps-Chat" })
154 + router.push({ name: "Soc-Alerts" })
155 }
156 },
157 {
179 - iconName: null,
180 - iconImage: "https://i.pravatar.cc/56?_=" + Math.random(),
181 - key: 6,
182 - title: "Craig Pollich",
183 - label: "koby_mayert@gmail.com",
158 + iconName: AlertsIcon,
159 + iconImage: null,
160 + key: 4,
161 + title: "View Identified Alerts",
162 + label: "Shortcut",
163 action() {
185 - router.push({ name: "Apps-Chat" })
164 + router.push({ name: "Alerts" })
165 }
166 }
167 ]
src/components/customers/CustomerAgents.vue
+1 -1
@@ -37,7 +37,7 @@ const message = useMessage()
37 const list = ref<Agent[] | []>([])
38
39 function gotoAgentPage(agent: Agent) {
40 - router.push(`/agent/${agent.agent_id}`).catch(() => {})
40 + router.push({ name: "Agent", params: { id: agent.agent_id } })
41 }
42
43 function getAgents() {
src/components/customers/CustomerCreationButton.vue new
+55
@@ -0,0 +1,55 @@
1 +<template>
2 + <n-button size="small" type="primary" @click="showAddCustomer = true">
3 + <template #icon>
4 + <Icon :name="AddUserIcon" :size="14"></Icon>
5 + </template>
6 + Add Customer
7 + </n-button>
8 +
9 + <n-drawer
10 + v-model:show="showAddCustomer"
11 + :width="500"
12 + style="max-width: 90vw"
13 + :trap-focus="false"
14 + display-directive="show"
15 + >
16 + <n-drawer-content title="Add Customer" closable :native-scrollbar="false">
17 + <CustomerForm @mounted="customerFormCTX = $event" @submitted="emit('submitted')" :resetOnSubmit="true" />
18 + </n-drawer-content>
19 + </n-drawer>
20 +</template>
21 +
22 +<script setup lang="ts">
23 +import { ref, watch, toRefs } from "vue"
24 +import { NButton, NDrawer, NDrawerContent } from "naive-ui"
25 +import Icon from "@/components/common/Icon.vue"
26 +import CustomerForm from "./CustomerForm.vue"
27 +
28 +const openForm = defineModel<boolean | undefined>("openForm", { default: false })
29 +
30 +const emit = defineEmits<{
31 + (e: "submitted"): void
32 +}>()
33 +
34 +const AddUserIcon = "carbon:user-follow"
35 +
36 +const customerFormCTX = ref<{ reset: () => void } | null>(null)
37 +const showAddCustomer = ref(false)
38 +
39 +watch(showAddCustomer, val => {
40 + if (!val) {
41 + openForm.value = false
42 + }
43 + customerFormCTX.value?.reset()
44 +})
45 +
46 +watch(
47 + openForm,
48 + val => {
49 + if (val) {
50 + showAddCustomer.value = true
51 + }
52 + },
53 + { immediate: true }
54 +)
55 +</script>
src/components/customers/CustomerHealthcheckItem.vue
+2 -2
@@ -144,7 +144,7 @@ function formatDate(timestamp: string | number, utc: boolean = true): string {
144 }
145
146 function gotoAgentPage(agentId: string) {
147 - router.push(`/agent/${agentId}`).catch(() => {})
147 + router.push({ name: "Agent", params: { id: agentId } })
148 }
149 </script>
150
@@ -208,7 +208,7 @@ function gotoAgentPage(agentId: string) {
208 box-shadow: 0px 0px 0px 1px inset var(--primary-color);
209
210 &.healthy {
211 - background-color: var(--primary-005-color);
211 + background-color: var(--success-005-color);
212 box-shadow: none;
213 }
214 &.unhealthy {
src/components/customers/CustomersList.vue
+13 -29
@@ -1,17 +1,12 @@
1 <template>
2 <div class="customers-list">
3 - <div class="header mb-4 flex gap-2 justify-between">
3 + <div class="header mb-4 flex gap-2 justify-between items-center">
4 <div>
5 Total:
6 <strong class="font-mono">{{ totalCustomers }}</strong>
7 </div>
8 <div>
9 - <n-button size="small" type="primary" @click="showAddCustomer = true">
10 - <template #icon>
11 - <Icon :name="AddUserIcon" :size="14"></Icon>
12 - </template>
13 - Add Customer
14 - </n-button>
9 + <slot></slot>
10 </div>
11 </div>
12 <n-spin :show="loadingCustomers">
@@ -32,39 +27,25 @@
27 </template>
28 </div>
29 </n-spin>
35 -
36 - <n-drawer
37 - v-model:show="showAddCustomer"
38 - :width="500"
39 - style="max-width: 90vw"
40 - :trap-focus="false"
41 - display-directive="show"
42 - >
43 - <n-drawer-content title="Add Customer" closable :native-scrollbar="false">
44 - <CustomerForm @mounted="customerFormCTX = $event" @submitted="getCustomers()" :resetOnSubmit="true" />
45 - </n-drawer-content>
46 - </n-drawer>
30 </div>
31 </template>
32
33 <script setup lang="ts">
34 import { ref, onBeforeMount, computed, watch, toRefs, nextTick } from "vue"
52 -import { useMessage, NSpin, NEmpty, NButton, NDrawer, NDrawerContent } from "naive-ui"
53 -import Icon from "@/components/common/Icon.vue"
35 +import { useMessage, NSpin, NEmpty } from "naive-ui"
36 import Api from "@/api"
55 -import CustomerForm from "./CustomerForm.vue"
37 import CustomerItem from "./CustomerItem.vue"
38 import type { Customer } from "@/types/customers.d"
39
59 -const props = defineProps<{ highlight: string | null | undefined }>()
60 -const { highlight } = toRefs(props)
40 +const props = defineProps<{ highlight: string | null | undefined; reload?: boolean }>()
41 +const { highlight, reload } = toRefs(props)
42
62 -const AddUserIcon = "carbon:user-follow"
43 +const emit = defineEmits<{
44 + (e: "reloaded"): void
45 +}>()
46
64 -const customerFormCTX = ref<{ reset: () => void } | null>(null)
47 const message = useMessage()
48 const loadingCustomers = ref(false)
67 -const showAddCustomer = ref(false)
49 const customersList = ref<Customer[]>([])
50
51 const totalCustomers = computed<number>(() => {
@@ -88,6 +69,7 @@ function getCustomers() {
69 })
70 .finally(() => {
71 loadingCustomers.value = false
72 + emit("reloaded")
73 })
74 }
75
@@ -102,8 +84,10 @@ function scrollToAlert(id: string) {
84 }
85 }
86
105 -watch(showAddCustomer, () => {
106 - customerFormCTX.value?.reset()
87 +watch(reload, val => {
88 + if (val) {
89 + getCustomers()
90 + }
91 })
92
93 watch(loadingCustomers, val => {
src/components/graylog/Alerts/Item.vue
+1 -1
@@ -115,7 +115,7 @@ function formatDate(timestamp: string): string {
115 }
116
117 function gotoIndicesPage(index: string) {
118 - router.push(`/indices?index_name=${index}`).catch(() => {})
118 + router.push({ name: "Indices", query: { index_name: index } })
119 }
120
121 function gotoEventsPage(event_definition_id: string) {
src/components/graylog/Inputs/Item.vue
+1 -1
@@ -135,7 +135,7 @@ const { input } = defineProps<{ input: InputExtended }>()
135
136 const UserIcon = "carbon:user"
137 const InfoIcon = "carbon:information"
138 -const DisabledIcon = "ph:minus-bold"
138 +const DisabledIcon = "carbon:subtract"
139 const TimeIcon = "carbon:time"
140 const GlobalIcon = "ph:globe-light"
141 const StopIcon = "carbon:stop"
src/components/graylog/Streams/Item.vue
+1 -1
@@ -103,7 +103,7 @@ const { stream } = toRefs(props)
103
104 const UserIcon = "carbon:user"
105 const InfoIcon = "carbon:information"
106 -const DisabledIcon = "ph:minus-bold"
106 +const DisabledIcon = "carbon:subtract"
107 const EnabledIcon = "ph:check-bold"
108 const StopIcon = "carbon:stop"
109 const StartIcon = "carbon:play"
src/components/healthcheck/HealthcheckItem.vue
-1
@@ -27,7 +27,6 @@
27
28 <script setup lang="ts">
29 import Icon from "@/components/common/Icon.vue"
30 -import "@/assets/scss/vuesjv-override.scss"
30 import { useSettingsStore } from "@/stores/settings"
31 import dayjs from "@/utils/dayjs"
32 import { InfluxDBAlertLevel, type InfluxDBAlert } from "@/types/healthchecks.d"
src/components/logs/LogItem.vue
-1
@@ -54,7 +54,6 @@
54
55 <script setup lang="ts">
56 import Icon from "@/components/common/Icon.vue"
57 -import "@/assets/scss/vuesjv-override.scss"
57 import { useSettingsStore } from "@/stores/settings"
58 import dayjs from "@/utils/dayjs"
59 import { LogEventType, type Log } from "@/types/logs.d"
src/components/overview/AgentsCard.vue
+1 -1
@@ -66,7 +66,7 @@ function getData() {
66 }
67
68 function gotoAgentsPage() {
69 - router.push(`/agents`).catch(() => {})
69 + router.push({ name: "Agents" })
70 }
71
72 onBeforeMount(() => {
src/components/overview/CustomersCard.vue
+1 -1
@@ -60,7 +60,7 @@ function getData() {
60 }
61
62 function gotoCustomersPage() {
63 - router.push(`/customers`).catch(() => {})
63 + router.push({ name: "Customers" })
64 }
65
66 onBeforeMount(() => {
src/components/overview/HealthcheckCard.vue
+10 -2
@@ -12,7 +12,12 @@
12 :secondStatus="criticalTotal ? 'warning' : undefined"
13 >
14 <template #icon>
15 - <CardStatsIcon :iconName="HealthcheckIcon" boxed :boxSize="30"></CardStatsIcon>
15 + <CardStatsIcon
16 + :iconName="HealthcheckIcon"
17 + boxed
18 + :boxSize="30"
19 + :color="criticalTotal ? style['--warning-color'] : undefined"
20 + ></CardStatsIcon>
21 </template>
22 </CardStatsDouble>
23 </n-spin>
@@ -26,6 +31,7 @@ import Api from "@/api"
31 import { useMessage, NSpin } from "naive-ui"
32 import { InfluxDBAlertLevel, type InfluxDBAlert } from "@/types/healthchecks.d"
33 import { useRouter } from "vue-router"
34 +import { useThemeStore } from "@/stores/theme"
35
36 const HealthcheckIcon = "ph:heartbeat"
37 const router = useRouter()
@@ -33,6 +39,8 @@ const message = useMessage()
39 const loading = ref(false)
40 const healthcheck = ref<InfluxDBAlert[]>([])
41
42 +const style = computed<{ [key: string]: any }>(() => useThemeStore().style)
43 +
44 const total = computed<number>(() => {
45 return healthcheck.value.length || 0
46 })
@@ -64,7 +72,7 @@ function getData() {
72 }
73
74 function gotoHealthcheckPage() {
67 - router.push(`/healthcheck`).catch(() => {})
75 + router.push({ name: "Healthcheck" })
76 }
77
78 onBeforeMount(() => {
src/components/overview/SocAlertsCard.vue
+1 -1
@@ -60,7 +60,7 @@ function getData() {
60 }
61
62 function gotoSocAlertsPage() {
63 - router.push(`/soc/alerts`).catch(() => {})
63 + router.push({ name: "Soc-Alerts" })
64 }
65
66 onBeforeMount(() => {
src/components/soc/SocAlerts/SocAlertItem.vue renamed
+278 -106
@@ -2,10 +2,10 @@
2 <n-spin
3 :show="loading"
4 class="soc-alert-item flex flex-col gap-0"
5 - :class="{ bookmarked: isBookmark, highlight }"
6 - :id="'alert-' + alert.alert_id"
5 + :class="{ bookmarked: isBookmark, highlight, embedded }"
6 + :id="'alert-' + alert?.alert_id"
7 >
8 - <div class="soc-alert-info px-5 py-3 flex flex-col gap-2">
8 + <div class="soc-alert-info px-5 py-3 flex flex-col gap-2" v-if="alert">
9 <div class="header-box flex justify-between">
10 <div class="flex items-center gap-2 cursor-pointer">
11 <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
@@ -13,7 +13,8 @@
13 <Icon :name="InfoIcon" :size="16"></Icon>
14 </div>
15 <Icon
16 - :name="isBookmark ? StarActiveIcon : StarIcon"
16 + v-if="!hideBookmarkAction"
17 + :name="loadingBookmark ? LoadingIcon : isBookmark ? StarActiveIcon : StarIcon"
18 :size="16"
19 @click="toggleBookmark()"
20 class="toggler-bookmark"
@@ -21,7 +22,7 @@
22 ></Icon>
23 </div>
24 <div class="time">
24 - <n-popover overlap placement="top-end" style="max-height: 240px" scrollable>
25 + <n-popover overlap placement="top-end" style="max-height: 240px" scrollable to="body">
26 <template #trigger>
27 <div class="flex items-center gap-2 cursor-help">
28 <span>
@@ -36,7 +37,7 @@
37 </n-popover>
38 </div>
39 </div>
39 - <div class="main-box">
40 + <div class="main-box flex justify-between gap-4">
41 <div class="content">
42 <div class="title">{{ alert.alert_title }}</div>
43 <div
@@ -46,69 +47,112 @@
47 {{ alert.alert_description }}
48 </div>
49 </div>
50 + <div class="actions flex flex-col gap-2 justify-end" v-if="!hideSocCaseAction">
51 + <n-button v-if="caseId" type="success" secondary @click="openSocCase()">
52 + <template #icon><Icon :name="ViewIcon"></Icon></template>
53 + View SOC Case
54 + </n-button>
55 + <n-button :loading="loadingCaseCreation" type="warning" secondary @click="createCase()" v-else>
56 + <template #icon><Icon :name="DangerIcon"></Icon></template>
57 + Create SOC Case
58 + </n-button>
59 + </div>
60 </div>
50 - <div class="badges-box flex flex-wrap items-center gap-3 mt-2">
51 - <n-tooltip placement="top-start" trigger="hover">
52 - <template #trigger>
53 - <Badge type="splitted" hint-cursor>
61 +
62 + <div>
63 + <div
64 + class="show-badges-toggle flex items-center gap-2"
65 + v-if="showBadgesToggle"
66 + @click="showBadges = !showBadges"
67 + >
68 + {{ showBadges ? "Less info" : "More info" }}
69 + <span class="transition-transform flex items-center" :class="{ 'rotate-90': showBadges }">
70 + <Icon :name="ChevronIcon" :size="14"></Icon>
71 + </span>
72 + </div>
73 + <n-collapse-transition :show="!showBadgesToggle || showBadges">
74 + <div class="badges-box flex flex-wrap items-center gap-3 mt-3">
75 + <n-tooltip placement="top-start" trigger="hover">
76 + <template #trigger>
77 + <Badge type="splitted" hint-cursor>
78 + <template #iconLeft>
79 + <Icon :name="StatusIcon" :size="14"></Icon>
80 + </template>
81 + <template #label>Status</template>
82 + <template #value>{{ alert.status?.status_name || "-" }}</template>
83 + </Badge>
84 + </template>
85 + {{ alert.status.status_description }}
86 + </n-tooltip>
87 + <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : undefined">
88 <template #iconLeft>
55 - <Icon :name="StatusIcon" :size="14"></Icon>
89 + <Icon :name="SeverityIcon" :size="13"></Icon>
90 </template>
57 - <template #label>Status</template>
58 - <template #value>{{ alert.status?.status_name || "-" }}</template>
91 + <template #label>Severity</template>
92 + <template #value>{{ alert.severity?.severity_name || "-" }}</template>
93 </Badge>
60 - </template>
61 - {{ alert.status.status_description }}
62 - </n-tooltip>
63 - <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : undefined">
64 - <template #iconLeft>
65 - <Icon :name="SeverityIcon" :size="13"></Icon>
66 - </template>
67 - <template #label>Severity</template>
68 - <template #value>{{ alert.severity?.severity_name || "-" }}</template>
69 - </Badge>
70 - <Badge type="splitted" class="hide-on-small">
71 - <template #iconLeft>
72 - <Icon :name="SourceIcon" :size="13"></Icon>
73 - </template>
74 - <template #label>Source</template>
75 - <template #value>{{ alert.alert_source || "-" }}</template>
76 - </Badge>
77 - <Badge type="splitted" class="hide-on-small">
78 - <template #iconLeft>
79 - <Icon :name="CustomerIcon" :size="13"></Icon>
80 - </template>
81 - <template #label>Customer</template>
82 - <template #value>{{ alert.customer?.customer_name || "-" }}</template>
83 - </Badge>
84 -
85 - <SocAssignUser :alert="alert" :users="users" v-slot="{ loading }" @updated="updateAlert">
86 - <Badge type="active" class="cursor-pointer">
87 - <template #iconLeft>
88 - <n-spin :size="16" :show="loading">
89 - <Icon :name="OwnerIcon" :size="16"></Icon>
90 - </n-spin>
91 - </template>
92 - <template #label>Owner</template>
93 - <template #value>{{ ownerName || "n/d" }}</template>
94 - </Badge>
95 - </SocAssignUser>
96 -
97 - <Badge
98 - v-if="alert.alert_source_link"
99 - type="active"
100 - :href="alert.alert_source_link"
101 - target="_blank"
102 - alt="Source link"
103 - rel="nofollow noopener noreferrer"
104 - >
105 - <template #iconRight>
106 - <Icon :name="LinkIcon" :size="14"></Icon>
107 - </template>
108 - <template #label>Source link</template>
109 - </Badge>
94 + <Badge type="splitted" class="hide-on-small">
95 + <template #iconLeft>
96 + <Icon :name="SourceIcon" :size="13"></Icon>
97 + </template>
98 + <template #label>Source</template>
99 + <template #value>{{ alert.alert_source || "-" }}</template>
100 + </Badge>
101 + <Badge type="splitted" class="hide-on-small">
102 + <template #iconLeft>
103 + <Icon :name="CustomerIcon" :size="13"></Icon>
104 + </template>
105 + <template #label>Customer</template>
106 + <template #value>{{ alert.customer?.customer_name || "-" }}</template>
107 + </Badge>
108 +
109 + <SocAssignUser :alert="alert" :users="users" v-slot="{ loading }" @updated="updateAlert">
110 + <Badge type="active" class="cursor-pointer">
111 + <template #iconLeft>
112 + <n-spin :size="16" :show="loading">
113 + <Icon :name="OwnerIcon" :size="16"></Icon>
114 + </n-spin>
115 + </template>
116 + <template #label>Owner</template>
117 + <template #value>{{ ownerName || "n/d" }}</template>
118 + </Badge>
119 + </SocAssignUser>
120 +
121 + <Badge
122 + v-if="alert.alert_source_link"
123 + type="active"
124 + :href="alert.alert_source_link"
125 + target="_blank"
126 + alt="Source link"
127 + rel="nofollow noopener noreferrer"
128 + >
129 + <template #iconRight>
130 + <Icon :name="LinkIcon" :size="14"></Icon>
131 + </template>
132 + <template #label>Source link</template>
133 + </Badge>
134 + </div>
135 + </n-collapse-transition>
136 </div>
111 - <div class="footer-box flex justify-end items-center gap-3">
137 +
138 + <div class="footer-box flex justify-between items-center gap-4">
139 + <div class="actions" v-if="!hideSocCaseAction">
140 + <n-button v-if="caseId" type="success" secondary size="small" @click="openSocCase()">
141 + <template #icon><Icon :name="ViewIcon"></Icon></template>
142 + View SOC Case
143 + </n-button>
144 + <n-button
145 + :loading="loadingCaseCreation"
146 + size="small"
147 + type="warning"
148 + secondary
149 + @click="createCase()"
150 + v-else
151 + >
152 + <template #icon><Icon :name="DangerIcon"></Icon></template>
153 + Create SOC Case
154 + </n-button>
155 + </div>
156 <div class="time">{{ formatDate(alert.alert_creation_time) }}</div>
157 </div>
158 </div>
@@ -126,16 +170,30 @@
170 </n-collapse-item>
171 </n-collapse>
172
173 + <n-modal
174 + v-model:show="showSocCaseDetails"
175 + preset="card"
176 + content-style="padding:0px"
177 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
178 + :title="`SOC Case: #${caseId}`"
179 + :bordered="false"
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" />
184 + </div>
185 + </n-modal>
186 +
187 <n-modal
188 v-model:show="showDetails"
189 preset="card"
190 content-style="padding:0px"
191 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
134 - :title="`SOC Alert: #${alert.alert_id} - ${alert.alert_uuid}`"
192 + :title="`SOC Alert: #${alert?.alert_id} - ${alert?.alert_uuid}`"
193 :bordered="false"
194 segmented
195 >
138 - <n-tabs type="line" animated :tabs-padding="24">
196 + <n-tabs type="line" animated :tabs-padding="24" v-if="alert">
197 <n-tab-pane name="Context" tab="Context" display-directive="show:lazy">
198 <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
199 <KVCard v-for="(value, key) of alert.alert_context" :key="key">
@@ -146,7 +204,7 @@
204 </n-tab-pane>
205 <n-tab-pane name="Note" tab="Note" display-directive="show:lazy">
206 <div class="p-7 pt-4">
149 - {{ alert.alert_note }}
207 + {{ alert.alert_note ?? "No notes for this alert" }}
208 </div>
209 </n-tab-pane>
210 <n-tab-pane name="Customer" tab="Customer" display-directive="show:lazy">
@@ -235,26 +293,45 @@ import { computed, onBeforeMount, ref, toRefs } from "vue"
293 import { SimpleJsonViewer } from "vue-sjv"
294 import KVCard from "@/components/common/KVCard.vue"
295 import SocAlertTimeline from "./SocAlertTimeline.vue"
296 +import SocCaseItem from "../SocCases/SocCaseItem.vue"
297 import SocAssignUser from "./SocAssignUser.vue"
298 import "@/assets/scss/vuesjv-override.scss"
299 import Api from "@/api"
241 -import { NCollapse, useMessage, NCollapseItem, NPopover, NModal, NTabs, NTabPane, NSpin, NTooltip } from "naive-ui"
300 +import {
301 + NCollapse,
302 + useMessage,
303 + NButton,
304 + NCollapseItem,
305 + NPopover,
306 + NModal,
307 + NTabs,
308 + NTabPane,
309 + NSpin,
310 + NTooltip,
311 + NCollapseTransition
312 +} from "naive-ui"
313 import { useSettingsStore } from "@/stores/settings"
314 import dayjs from "@/utils/dayjs"
315 import type { SocUser } from "@/types/soc/user.d"
316 import { useRouter } from "vue-router"
317
318 const emit = defineEmits<{
248 - (e: "bookmark"): void
319 + (e: "bookmark", value: boolean): void
320 }>()
321
322 const props = defineProps<{
252 - alert: SocAlert
323 + alertData?: SocAlert
324 + alertId?: string | number
325 isBookmark?: boolean
326 highlight?: boolean | null | undefined
327 + embedded?: boolean
328 users?: SocUser[]
329 + hideSocCaseAction?: boolean
330 + hideBookmarkAction?: boolean
331 + showBadgesToggle?: boolean
332 }>()
257 -const { alert, isBookmark, highlight, users } = toRefs(props)
333 +const { alertData, alertId, isBookmark, highlight, users, embedded, hideSocCaseAction, hideBookmarkAction } =
334 + toRefs(props)
335
336 const ChevronIcon = "carbon:chevron-right"
337 const InfoIcon = "carbon:information"
@@ -268,16 +345,27 @@ const StarActiveIcon = "carbon:star-filled"
345 const OwnerIcon = "carbon:user-military"
346 const StarIcon = "carbon:star"
347 const EditIcon = "uil:edit-alt"
348 +const DangerIcon = "majesticons:exclamation-line"
349 +const LoadingIcon = "eos-icons:loading"
350 +const ViewIcon = "iconoir:eye-alt"
351
352 const showDetails = ref(false)
273 -const loading = ref(false)
353 +const showBadges = ref(false)
354 +const showSocCaseDetails = ref(false)
355 +const loadingData = ref(false)
356 +const loadingBookmark = ref(false)
357 +const loadingCaseCreation = ref(false)
358 const router = useRouter()
359 const message = useMessage()
360
361 +const alert = ref(alertData.value || null)
362 +
363 const alertObject = ref<Alert>({} as Alert)
364
279 -const ownerName = computed(() => alert.value.owner?.user_login)
280 -const ownerId = computed(() => alert.value.owner?.id)
365 +const loading = computed(() => loadingBookmark.value || loadingCaseCreation.value || loadingData.value)
366 +const ownerName = computed(() => alert.value?.owner?.user_login)
367 +const ownerId = computed(() => alert.value?.owner?.id)
368 +const caseId = computed<number | null>(() => (alert.value?.cases?.length ? alert.value?.cases[0] : null))
369
370 const socAlertDetail = computed<Partial<SocAlert>>(() => {
371 const clone: Partial<SocAlert> = JSON.parse(JSON.stringify(alert.value))
@@ -299,15 +387,81 @@ function formatDate(timestamp: string | number, utc: boolean = true): string {
387 }
388
389 function toggleBookmark() {
302 - loading.value = true
390 + if (alert.value?.alert_id) {
391 + loadingBookmark.value = true
392 +
393 + const method = isBookmark.value ? "removeAlertBookmark" : "addAlertBookmark"
394 +
395 + Api.soc[method](alert.value.alert_id.toString())
396 + .then(res => {
397 + if (res.data.success) {
398 + emit("bookmark", method === "removeAlertBookmark" ? false : true)
399 + message.success(res.data?.message || "Stream started.")
400 + } else {
401 + message.warning(res.data?.message || "An error occurred. Please try again later.")
402 + }
403 + })
404 + .catch(err => {
405 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
406 + })
407 + .finally(() => {
408 + loadingBookmark.value = false
409 + })
410 + }
411 +}
412
304 - const method = isBookmark.value ? "removeAlertBookmark" : "addAlertBookmark"
413 +function createCase() {
414 + if (alert.value?.alert_id) {
415 + loadingCaseCreation.value = true
416
306 - Api.soc[method](alert.value.alert_id.toString())
417 + Api.soc
418 + .createCase(alert.value.alert_id.toString())
419 + .then(res => {
420 + if (res.data.success) {
421 + if (alert.value) {
422 + alert.value.cases = [res.data.case.case_id]
423 + }
424 + message.success(res.data?.message || "SOC Case created.")
425 + } else {
426 + message.warning(res.data?.message || "An error occurred. Please try again later.")
427 + }
428 + })
429 + .catch(err => {
430 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
431 + })
432 + .finally(() => {
433 + loadingCaseCreation.value = false
434 + })
435 + }
436 +}
437 +
438 +function updateAlert(alertUpdated: SocAlert) {
439 + const ownerObject = alertUpdated.owner
440 + const modificationHistory = alertUpdated.modification_history
441 +
442 + if (alert.value) {
443 + alert.value.owner = ownerObject
444 + alert.value.modification_history = modificationHistory
445 + }
446 +}
447 +
448 +function gotoUsersPage(userId?: string | number) {
449 + router.push({ name: "Soc-Users", query: userId ? { user_id: userId } : {} })
450 +}
451 +
452 +function openSocCase() {
453 + showSocCaseDetails.value = true
454 +}
455 +
456 +function getAlert(id: string | number, cb?: () => void) {
457 + loadingData.value = true
458 +
459 + Api.soc
460 + .getAlert(id.toString())
461 .then(res => {
462 if (res.data.success) {
309 - emit("bookmark")
310 - message.success(res.data?.message || "Stream started.")
463 + alert.value = res.data?.alert || null
464 + if (cb) cb()
465 } else {
466 message.warning(res.data?.message || "An error occurred. Please try again later.")
467 }
@@ -316,37 +470,37 @@ function toggleBookmark() {
470 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
471 })
472 .finally(() => {
319 - loading.value = false
473 + loadingData.value = false
474 })
475 }
476
323 -function updateAlert(alertUpdated: SocAlert) {
324 - const ownerObject = alertUpdated.owner
325 - const modificationHistory = alertUpdated.modification_history
326 -
327 - alert.value.owner = ownerObject
328 - alert.value.modification_history = modificationHistory
329 -}
330 -
331 -function gotoUsersPage(userId?: string | number) {
332 - router.push(`/soc/users${userId ? "?user_id=" + userId : ""}`).catch(() => {})
333 -}
334 -
335 -onBeforeMount(() => {
477 +function createAlertObject() {
478 alertObject.value = {
479 _index: "",
338 - _id: alert.value.alert_context.alert_id,
339 - _source: alert.value.alert_source_content
480 + _id: alert.value?.alert_context.alert_id,
481 + _source: alert.value?.alert_source_content
482 } as Alert
483 +}
484 +
485 +onBeforeMount(() => {
486 + createAlertObject()
487 +
488 + if (!alertData.value && alertId.value) {
489 + getAlert(alertId.value, () => {
490 + createAlertObject()
491 + })
492 + }
493 })
494 </script>
495
496 <style lang="scss" scoped>
497 .soc-alert-item {
346 - border-radius: var(--border-radius);
347 - background-color: var(--bg-color);
498 + &:not(.embedded) {
499 + border-radius: var(--border-radius);
500 + background-color: var(--bg-color);
501 + border: var(--border-small-050);
502 + }
503 transition: all 0.2s var(--bezier-ease);
349 - border: var(--border-small-050);
504
505 .soc-alert-info {
506 border-bottom: var(--border-small-050);
@@ -392,13 +546,23 @@ onBeforeMount(() => {
546 }
547 }
548
549 + .show-badges-toggle {
550 + font-size: 14px;
551 + cursor: pointer;
552 + transition: color 0.2s var(--bezier-ease);
553 +
554 + &:hover {
555 + color: var(--primary-color);
556 + }
557 + }
558 +
559 .footer-box {
396 - font-family: var(--font-family-mono);
560 font-size: 13px;
561 margin-top: 10px;
562 display: none;
563
564 .time {
565 + font-family: var(--font-family-mono);
566 text-align: right;
567 color: var(--fg-secondary-color);
568 }
@@ -410,9 +574,11 @@ onBeforeMount(() => {
574 box-shadow: 0px 0px 0px 1px inset var(--primary-030-color);
575 }
576
413 - &:hover,
414 - &.highlight {
415 - box-shadow: 0px 0px 0px 1px inset var(--primary-color);
577 + &:not(.embedded) {
578 + &:hover,
579 + &.highlight {
580 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
581 + }
582 }
583
584 @container (max-width: 650px) {
@@ -422,10 +588,16 @@ onBeforeMount(() => {
588 display: none;
589 }
590 }
425 - .badges-box {
426 - .badge {
427 - &.hide-on-small {
428 - display: none;
591 +
592 + .main-box {
593 + .actions {
594 + display: none;
595 + }
596 + .badges-box {
597 + .badge {
598 + &.hide-on-small {
599 + display: none;
600 + }
601 }
602 }
603 }
src/components/soc/SocAlerts/SocAlertTimeline.vue renamed
src/components/soc/SocAlerts/SocAlertsBookmarks.vue new
+134
@@ -0,0 +1,134 @@
1 +<template>
2 + <div class="soc-alerts-bookmarks">
3 + <div class="header flex items-center">
4 + <div class="info">
5 + Bookmarked:
6 + <code>
7 + <strong>{{ bookmarksList.length }}</strong>
8 + </code>
9 + </div>
10 + </div>
11 +
12 + <n-spin :show="loadingBookmarks">
13 + <div class="list">
14 + <template v-if="bookmarksList.length">
15 + <SocAlertItem
16 + v-for="alert of bookmarksList"
17 + :key="alert.alert_id"
18 + :alertData="alert"
19 + class="item-appear item-appear-bottom item-appear-005 mb-2"
20 + :is-bookmark="true"
21 + :users="usersList"
22 + @bookmark="bookmark()"
23 + />
24 + </template>
25 + <template v-else>
26 + <n-empty description="No items found" class="justify-center h-48" v-if="!loadingBookmarks" />
27 + </template>
28 + </div>
29 + </n-spin>
30 + </div>
31 +</template>
32 +
33 +<script setup lang="ts">
34 +import { ref, onBeforeMount, toRefs, onMounted, onBeforeUnmount } from "vue"
35 +import { useMessage, NSpin, NEmpty } from "naive-ui"
36 +import Api from "@/api"
37 +import SocAlertItem from "./SocAlertItem.vue"
38 +import type { SocAlert } from "@/types/soc/alert.d"
39 +import type { SocUser } from "@/types/soc/user.d"
40 +import axios from "axios"
41 +
42 +const props = defineProps<{
43 + usersList?: SocUser[]
44 +}>()
45 +const { usersList } = toRefs(props)
46 +
47 +const emit = defineEmits<{
48 + (e: "bookmark"): void
49 + (e: "loaded", value: SocAlert[]): void
50 + (
51 + e: "mounted",
52 + value: {
53 + reload: () => void
54 + }
55 + ): void
56 +}>()
57 +
58 +let reloadTimeout: NodeJS.Timeout | null = null
59 +const message = useMessage()
60 +const loadingBookmarks = ref(false)
61 +const bookmarksList = ref<SocAlert[]>([])
62 +
63 +let abortController: AbortController | null = null
64 +
65 +function bookmark() {
66 + emit("bookmark")
67 + safeReload()
68 +}
69 +
70 +function getBookmarks() {
71 + loadingBookmarks.value = true
72 +
73 + abortController = new AbortController()
74 +
75 + Api.soc
76 + .getAlertsBookmark(abortController.signal)
77 + .then(res => {
78 + if (res.data.success) {
79 + bookmarksList.value = res.data.bookmarked_alerts || []
80 + emit("loaded", bookmarksList.value)
81 + } else {
82 + message.error(res.data?.message || "An error occurred. Please try again later.")
83 + }
84 + })
85 + .catch(err => {
86 + if (!axios.isCancel(err)) {
87 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
88 + }
89 + })
90 + .finally(() => {
91 + loadingBookmarks.value = false
92 + })
93 +}
94 +
95 +function safeReload() {
96 + abortController?.abort()
97 +
98 + if (reloadTimeout) {
99 + clearTimeout(reloadTimeout)
100 + }
101 +
102 + reloadTimeout = setTimeout(() => {
103 + getBookmarks()
104 + }, 200)
105 +}
106 +
107 +onBeforeMount(() => {
108 + getBookmarks()
109 +})
110 +
111 +onMounted(() => {
112 + emit("mounted", {
113 + reload: () => {
114 + safeReload()
115 + }
116 + })
117 +})
118 +
119 +onBeforeUnmount(() => {
120 + abortController?.abort()
121 +})
122 +</script>
123 +
124 +<style lang="scss" scoped>
125 +.soc-alerts-bookmarks {
126 + .header {
127 + height: 50px;
128 + }
129 + .list {
130 + container-type: inline-size;
131 + min-height: 200px;
132 + }
133 +}
134 +</style>
src/components/soc/SocAlerts/SocAlertsFullList.vue new
+188
@@ -0,0 +1,188 @@
1 +<template>
2 + <div class="soc-alerts-list" ref="list">
3 + <n-split
4 + direction="horizontal"
5 + :default-size="splitDefault"
6 + :resize-trigger-size="26"
7 + :min="splitMin"
8 + :max="splitMax"
9 + v-if="!compactMode"
10 + >
11 + <template #1>
12 + <SocAlertsBookmarks
13 + :usersList="usersList"
14 + @bookmark="reloadAlerts()"
15 + @loaded="bookmarksList = $event"
16 + @mounted="socAlertsBookmarksCTX = $event"
17 + />
18 + </template>
19 + <template #2>
20 + <SocAlertsList
21 + :highlight="highlight"
22 + :bookmarksList="bookmarksList"
23 + :usersList="usersList"
24 + @bookmark="reloadBookmarks()"
25 + @mounted="socAlertsCTX = $event"
26 + />
27 + </template>
28 + <template #resize-trigger>
29 + <div class="split-trigger">
30 + <div class="split-trigger-icon">
31 + <Icon :name="SplitIcon"></Icon>
32 + </div>
33 + </div>
34 + </template>
35 + </n-split>
36 +
37 + <template v-else>
38 + <SocAlertsList
39 + :highlight="highlight"
40 + :bookmarksList="bookmarksList"
41 + :usersList="usersList"
42 + @bookmark="reloadBookmarks()"
43 + @mounted="socAlertsCTX = $event"
44 + >
45 + <template #header>
46 + <n-button size="small" @click="showBookmarkedDrawer = true">
47 + <template #icon>
48 + <Icon :name="StarIcon" :size="14"></Icon>
49 + </template>
50 + </n-button>
51 + </template>
52 + </SocAlertsList>
53 +
54 + <n-drawer
55 + v-model:show="showBookmarkedDrawer"
56 + :width="700"
57 + style="max-width: 90vw"
58 + :trap-focus="false"
59 + display-directive="show"
60 + placement="left"
61 + >
62 + <n-drawer-content title="Alerts list" closable :native-scrollbar="false">
63 + <SocAlertsBookmarks
64 + :usersList="usersList"
65 + @bookmark="reloadAlerts()"
66 + @loaded="bookmarksList = $event"
67 + @mounted="socAlertsBookmarksCTX = $event"
68 + />
69 + </n-drawer-content>
70 + </n-drawer>
71 + </template>
72 +
73 + <n-back-top :visibility-height="300"></n-back-top>
74 + </div>
75 +</template>
76 +
77 +<script setup lang="ts">
78 +import { ref, onBeforeMount, toRefs } from "vue"
79 +import { useMessage, NSplit, NBackTop, NButton, NDrawer, NDrawerContent } from "naive-ui"
80 +import Api from "@/api"
81 +import SocAlertsBookmarks from "./SocAlertsBookmarks.vue"
82 +import SocAlertsList from "./SocAlertsList.vue"
83 +import type { SocAlert } from "@/types/soc/alert.d"
84 +import Icon from "@/components/common/Icon.vue"
85 +import type { SocUser } from "@/types/soc/user.d"
86 +import { useResizeObserver } from "@vueuse/core"
87 +
88 +const props = defineProps<{ highlight: string | null | undefined }>()
89 +const { highlight } = toRefs(props)
90 +
91 +const SplitIcon = "carbon:draggable"
92 +const StarIcon = "carbon:star-filled"
93 +
94 +const message = useMessage()
95 +const bookmarksList = ref<SocAlert[]>([])
96 +const usersList = ref<SocUser[]>([])
97 +const socAlertsBookmarksCTX = ref<{ reload: () => void } | null>(null)
98 +const socAlertsCTX = ref<{ reload: () => void } | null>(null)
99 +
100 +const list = ref(null)
101 +const showBookmarkedDrawer = ref(false)
102 +const compactMode = ref(false)
103 +const splitMin = ref(0.25)
104 +const splitMax = ref(0.75)
105 +const splitDefault = ref(0.3)
106 +
107 +function reloadBookmarks() {
108 + socAlertsBookmarksCTX.value?.reload()
109 +}
110 +function reloadAlerts() {
111 + socAlertsCTX.value?.reload()
112 +}
113 +
114 +function getUsers() {
115 + Api.soc
116 + .getUsers()
117 + .then(res => {
118 + if (res.data.success) {
119 + usersList.value = res.data?.users || []
120 + } else {
121 + message.warning(res.data?.message || "An error occurred. Please try again later.")
122 + }
123 + })
124 + .catch(err => {
125 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
126 + })
127 +}
128 +
129 +useResizeObserver(list, entries => {
130 + const entry = entries[0]
131 + const { width } = entry.contentRect
132 +
133 + compactMode.value = width < 680
134 + splitMin.value = width < 850 ? 0.5 : 0.25
135 + splitMax.value = width < 850 ? 0.5 : 0.75
136 + splitDefault.value = width < 850 ? 0.5 : 0.3
137 +
138 + if (width > 680) {
139 + showBookmarkedDrawer.value = false
140 + }
141 +})
142 +
143 +onBeforeMount(() => {
144 + getUsers()
145 +})
146 +</script>
147 +
148 +<style lang="scss" scoped>
149 +.soc-alerts-list {
150 + .n-split {
151 + :deep() {
152 + .n-split-pane-1 {
153 + min-width: 290px;
154 + max-width: 500px;
155 + }
156 + }
157 + }
158 + .split-trigger {
159 + height: 100%;
160 + width: 3px;
161 + background-color: var(--border-color);
162 + display: flex;
163 + justify-content: center;
164 + transition: background-color 0.3s var(--bezier-ease);
165 + margin-left: 11px;
166 +
167 + .split-trigger-icon {
168 + position: relative;
169 + top: min(48%, 300px);
170 + background-color: var(--border-color);
171 + border-radius: var(--border-radius-small);
172 + height: 20px;
173 + display: flex;
174 + justify-content: center;
175 + align-items: center;
176 + transition: background-color 0.3s var(--bezier-ease);
177 + }
178 +
179 + &:hover {
180 + background-color: var(--primary-color);
181 +
182 + .split-trigger-icon {
183 + background-color: var(--primary-color);
184 + }
185 + }
186 + }
187 +}
188 +</style>
src/components/soc/SocAlerts/SocAlertsList.vue new
+223
@@ -0,0 +1,223 @@
1 +<template>
2 + <div class="soc-alerts-list">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <slot name="header"></slot>
5 + <div class="grow">
6 + <n-input v-model:value="alertTitle" size="small" placeholder="Search by title..." clearable />
7 + </div>
8 + <PaginationIndeterminate
9 + v-model:page="page"
10 + v-model:pageSize="pageSize"
11 + v-model:sort="sort"
12 + :pageSizes="pageSizes"
13 + :showPageSizes="!compactMode"
14 + />
15 + </div>
16 +
17 + <n-spin :show="loadingAlerts">
18 + <div class="list">
19 + <template v-if="alertsList.length">
20 + <SocAlertItem
21 + v-for="alert of alertsList"
22 + :key="alert.alert_id"
23 + :alertData="alert"
24 + class="item-appear item-appear-bottom item-appear-005 mb-2"
25 + :is-bookmark="isBookmarked(alert)"
26 + :users="usersList"
27 + :highlight="alert.alert_id.toString() === highlight"
28 + show-badges-toggle
29 + @bookmark="bookmark()"
30 + />
31 + </template>
32 + <template v-else>
33 + <n-empty description="No items found" class="justify-center h-48" v-if="!loadingAlerts" />
34 + </template>
35 + </div>
36 + </n-spin>
37 + </div>
38 +</template>
39 +
40 +<script setup lang="ts">
41 +import { ref, onBeforeMount, watch, toRefs, nextTick, onBeforeUnmount, onMounted } from "vue"
42 +import { useMessage, NSpin, NEmpty, NInput } from "naive-ui"
43 +import Api from "@/api"
44 +import SocAlertItem from "./SocAlertItem.vue"
45 +import type { SocAlert } from "@/types/soc/alert.d"
46 +import type { SocUser } from "@/types/soc/user.d"
47 +import type { AlertsFilter } from "@/api/soc"
48 +import { useResizeObserver, watchDebounced } from "@vueuse/core"
49 +import PaginationIndeterminate from "@/components/common/PaginationIndeterminate.vue"
50 +import axios from "axios"
51 +
52 +const props = defineProps<{
53 + highlight: string | null | undefined
54 + bookmarksList?: SocAlert[]
55 + usersList?: SocUser[]
56 +}>()
57 +const { highlight, bookmarksList, usersList } = toRefs(props)
58 +
59 +const emit = defineEmits<{
60 + (e: "bookmark"): void
61 + (
62 + e: "mounted",
63 + value: {
64 + reload: () => void
65 + }
66 + ): void
67 +}>()
68 +
69 +let reloadTimeout: NodeJS.Timeout | null = null
70 +const message = useMessage()
71 +const loadingAlerts = ref(false)
72 +const alertsList = ref<SocAlert[]>([])
73 +
74 +const pageSize = ref(50)
75 +const pageSizes = [25, 50, 100, 150, 200]
76 +const page = ref(1)
77 +const sort = ref<"desc" | "asc">("desc")
78 +const alertTitle = ref("")
79 +const header = ref()
80 +const compactMode = ref(false)
81 +
82 +let abortController: AbortController | null = null
83 +
84 +function isBookmarked(alert: SocAlert): boolean {
85 + return !!(bookmarksList.value || []).filter(o => o.alert_id === alert.alert_id).length
86 +}
87 +
88 +function bookmark() {
89 + emit("bookmark")
90 + safeReload()
91 +}
92 +
93 +function getAlerts() {
94 + loadingAlerts.value = true
95 +
96 + abortController = new AbortController()
97 +
98 + const filter: Partial<AlertsFilter> = {}
99 + if (pageSize.value) {
100 + filter.pageSize = pageSize.value
101 + }
102 + if (page.value) {
103 + filter.page = page.value
104 + }
105 + if (sort.value) {
106 + filter.sort = sort.value
107 + }
108 + if (alertTitle.value) {
109 + filter.alertTitle = alertTitle.value
110 + }
111 +
112 + Api.soc
113 + .getAlerts(filter, abortController.signal)
114 + .then(res => {
115 + if (res.data.success) {
116 + alertsList.value = res.data?.alerts || []
117 + } else {
118 + message.warning(res.data?.message || "An error occurred. Please try again later.")
119 + }
120 + })
121 + .catch(err => {
122 + if (!axios.isCancel(err)) {
123 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
124 + }
125 + })
126 + .finally(() => {
127 + loadingAlerts.value = false
128 + })
129 +}
130 +
131 +function scrollToAlert(id: string) {
132 + const element = document.getElementById(`alert-${id}`)
133 + const scrollContent = document.querySelector("#main > .n-scrollbar > .n-scrollbar-container") as HTMLElement
134 +
135 + if (element && scrollContent) {
136 + const wrap: HTMLElement = scrollContent
137 + const middle = element.offsetTop - wrap.offsetHeight / 2
138 + scrollContent?.scrollTo({ top: middle, behavior: "smooth" })
139 + }
140 +}
141 +
142 +watch(loadingAlerts, val => {
143 + if (!val) {
144 + nextTick(() => {
145 + setTimeout(() => {
146 + if (highlight.value) {
147 + scrollToAlert(highlight.value)
148 + }
149 + }, 300)
150 + })
151 + }
152 +})
153 +
154 +watch(highlight, val => {
155 + if (val) {
156 + nextTick(() => {
157 + setTimeout(() => {
158 + scrollToAlert(val)
159 + })
160 + })
161 + }
162 +})
163 +
164 +watchDebounced(
165 + [page, pageSize, sort, alertTitle],
166 + () => {
167 + safeReload()
168 + },
169 + { debounce: 500 }
170 +)
171 +
172 +useResizeObserver(header, entries => {
173 + const entry = entries[0]
174 + const { width } = entry.contentRect
175 +
176 + if (width < 500) {
177 + compactMode.value = true
178 + pageSize.value = pageSizes[0]
179 + } else {
180 + compactMode.value = false
181 + }
182 +})
183 +
184 +function safeReload() {
185 + abortController?.abort()
186 +
187 + if (reloadTimeout) {
188 + clearTimeout(reloadTimeout)
189 + }
190 +
191 + reloadTimeout = setTimeout(() => {
192 + getAlerts()
193 + }, 200)
194 +}
195 +
196 +onBeforeMount(() => {
197 + getAlerts()
198 +})
199 +
200 +onMounted(() => {
201 + emit("mounted", {
202 + reload: () => {
203 + safeReload()
204 + }
205 + })
206 +})
207 +
208 +onBeforeUnmount(() => {
209 + abortController?.abort()
210 +})
211 +</script>
212 +
213 +<style lang="scss" scoped>
214 +.soc-alerts-list {
215 + .header {
216 + height: 50px;
217 + }
218 + .list {
219 + container-type: inline-size;
220 + min-height: 200px;
221 + }
222 +}
223 +</style>
src/components/soc/SocAlerts/SocAssignUser.vue renamed
+1
@@ -6,6 +6,7 @@
6 :disabled="loadingUsers"
7 size="medium"
8 scrollable
9 + to="body"
10 >
11 <slot :loading="loadingUsers" />
12 </n-popselect>
src/components/soc/SocAlertsList.vue deleted
-198
@@ -1,198 +0,0 @@
1 -<template>
2 - <div class="soc-alerts-list">
3 - <div class="header mb-4 flex gap-2">
4 - <span>
5 - Total:
6 - <strong class="font-mono">{{ totalAlerts }}</strong>
7 - </span>
8 - <span>/</span>
9 - <span>
10 - Bookmarked:
11 - <strong class="font-mono">{{ bookmarksList.length }}</strong>
12 - </span>
13 - </div>
14 - <n-spin :show="loadingAlerts">
15 - <div class="list">
16 - <template v-if="list.length">
17 - <SocAlertItem
18 - v-for="alert of list"
19 - :key="alert.id"
20 - :alert="alert.item"
21 - class="item-appear item-appear-bottom item-appear-005 mb-2"
22 - :is-bookmark="alert.isBookmark"
23 - :users="usersList"
24 - :highlight="alert.id.toString() === highlight"
25 - @bookmark="switchAlert(alert.id, alert.isBookmark)"
26 - />
27 - </template>
28 - <template v-else>
29 - <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
30 - </template>
31 - </div>
32 - </n-spin>
33 - </div>
34 -</template>
35 -
36 -<script setup lang="ts">
37 -import { ref, onBeforeMount, computed, watch, toRefs, nextTick } from "vue"
38 -import { useMessage, NSpin, NEmpty } from "naive-ui"
39 -import Api from "@/api"
40 -import SocAlertItem from "./SocAlertItem.vue"
41 -import type { SocAlert } from "@/types/soc/alert.d"
42 -import _uniqBy from "lodash/uniqBy"
43 -import type { SocUser } from "@/types/soc/user.d"
44 -
45 -const props = defineProps<{ highlight: string | null | undefined }>()
46 -const { highlight } = toRefs(props)
47 -
48 -const message = useMessage()
49 -const loadingBookmarks = ref(false)
50 -const loadingAlerts = ref(false)
51 -const bookmarksList = ref<SocAlert[]>([])
52 -const alertsList = ref<SocAlert[]>([])
53 -const usersList = ref<SocUser[]>([])
54 -
55 -const list = computed(() => {
56 - const list = [
57 - ...bookmarksList.value.map(o => ({ item: o, id: o.alert_id, isBookmark: true })),
58 - ...alertsList.value.map(o => ({ item: o, id: o.alert_id, isBookmark: false }))
59 - ]
60 - return _uniqBy(list, o => o.id)
61 -})
62 -
63 -const loading = computed<boolean>(() => {
64 - return loadingBookmarks.value || loadingAlerts.value
65 -})
66 -
67 -const totalAlerts = computed<number>(() => {
68 - return list.value.length || 0
69 -})
70 -
71 -function switchAlert(alertId: number, isBookmark: boolean) {
72 - const fromList = isBookmark ? bookmarksList : alertsList
73 - const toList = isBookmark ? alertsList : bookmarksList
74 -
75 - const alert = fromList.value.find(o => o.alert_id === alertId)
76 - fromList.value = fromList.value.filter(o => o.alert_id !== alertId)
77 -
78 - if (alert) {
79 - toList.value.push(alert)
80 - }
81 -
82 - load(true)
83 -}
84 -
85 -function getAlerts(silent?: boolean) {
86 - if (!silent) {
87 - loadingAlerts.value = true
88 - }
89 -
90 - Api.soc
91 - .getAlerts()
92 - .then(res => {
93 - if (res.data.success) {
94 - alertsList.value = res.data?.alerts || []
95 - } else {
96 - message.warning(res.data?.message || "An error occurred. Please try again later.")
97 - }
98 - })
99 - .catch(err => {
100 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
101 - })
102 - .finally(() => {
103 - loadingAlerts.value = false
104 - })
105 -}
106 -
107 -function getBookmarks(silent?: boolean) {
108 - if (!silent) {
109 - loadingBookmarks.value = true
110 - }
111 -
112 - Api.soc
113 - .getAlertsBookmark()
114 - .then(res => {
115 - if (res.data.success) {
116 - bookmarksList.value = res.data.bookmarked_alerts || []
117 - } else {
118 - message.error(res.data?.message || "An error occurred. Please try again later.")
119 - }
120 - })
121 - .catch(err => {
122 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
123 - })
124 - .finally(() => {
125 - loadingBookmarks.value = false
126 - })
127 -}
128 -
129 -function getUsers() {
130 - Api.soc
131 - .getUsers()
132 - .then(res => {
133 - if (res.data.success) {
134 - usersList.value = res.data?.users || []
135 - } else {
136 - message.warning(res.data?.message || "An error occurred. Please try again later.")
137 - }
138 - })
139 - .catch(err => {
140 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
141 - })
142 -}
143 -
144 -function load(silent?: boolean) {
145 - getAlerts(silent)
146 - getBookmarks(silent)
147 -
148 - if (!usersList.value.length) {
149 - getUsers()
150 - }
151 -}
152 -
153 -function scrollToAlert(id: string) {
154 - const element = document.getElementById(`alert-${id}`)
155 - const scrollContent = document.querySelector("#main > .n-scrollbar > .n-scrollbar-container") as HTMLElement
156 -
157 - if (element && scrollContent) {
158 - const wrap: HTMLElement = scrollContent
159 - const middle = element.offsetTop - wrap.offsetHeight / 2
160 - scrollContent?.scrollTo({ top: middle, behavior: "smooth" })
161 - }
162 -}
163 -
164 -watch(loading, val => {
165 - if (!val) {
166 - nextTick(() => {
167 - setTimeout(() => {
168 - if (highlight.value) {
169 - scrollToAlert(highlight.value)
170 - }
171 - }, 300)
172 - })
173 - }
174 -})
175 -
176 -watch(highlight, val => {
177 - if (val) {
178 - nextTick(() => {
179 - setTimeout(() => {
180 - scrollToAlert(val)
181 - })
182 - })
183 - }
184 -})
185 -
186 -onBeforeMount(() => {
187 - load()
188 -})
189 -</script>
190 -
191 -<style lang="scss" scoped>
192 -.soc-alerts-list {
193 - .list {
194 - container-type: inline-size;
195 - min-height: 200px;
196 - }
197 -}
198 -</style>
src/components/soc/SocCaseItem.vue deleted
-382
@@ -1,382 +0,0 @@
1 -<template>
2 - <div class="soc-case-item" :class="{ embedded }">
3 - <div class="flex flex-col gap-2 px-5 py-3">
4 - <div class="header-box flex justify-between">
5 - <div class="flex items-center gap-2">
6 - <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
7 - <span>{{ caseData.case_uuid }}</span>
8 - <Icon :name="InfoIcon" :size="16"></Icon>
9 - </div>
10 - </div>
11 - <div class="time">
12 - <n-popover overlap placement="top-end">
13 - <template #trigger>
14 - <div class="flex items-center gap-2 cursor-help">
15 - <span>
16 - {{ formatDate(caseData.case_open_date) }}
17 - </span>
18 - <Icon :name="TimeIcon" :size="16"></Icon>
19 - </div>
20 - </template>
21 - <div class="flex flex-col py-2 px-1">
22 - <n-timeline>
23 - <n-timeline-item
24 - type="success"
25 - :title="`Open [${caseData.opened_by}]`"
26 - :time="formatDate(caseData.case_open_date)"
27 - />
28 - <n-timeline-item
29 - v-if="caseData.case_close_date"
30 - title="Close date"
31 - :time="formatDate(caseData.case_close_date)"
32 - />
33 - </n-timeline>
34 - </div>
35 - </n-popover>
36 - </div>
37 - </div>
38 - <div class="main-box flex justify-between gap-4">
39 - <div class="content">
40 - <div class="title" v-html="caseData.case_name"></div>
41 - <div class="description mt-2" v-if="caseData.case_description">{{ excerpt }}</div>
42 -
43 - <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
44 - <Badge type="splitted" :color="caseData.state_name === StateName.Open ? 'warning' : undefined">
45 - <template #iconLeft>
46 - <Icon :name="StatusIcon" :size="14"></Icon>
47 - </template>
48 - <template #label>State</template>
49 - <template #value>{{ caseData.state_name }}</template>
50 - </Badge>
51 - <Badge type="splitted">
52 - <template #iconLeft>
53 - <Icon :name="OwnerIcon" :size="16"></Icon>
54 - </template>
55 - <template #label>Owner</template>
56 - <template #value>{{ caseData.owner }}</template>
57 - </Badge>
58 - <Badge type="splitted">
59 - <template #iconLeft>
60 - <Icon :name="CustomerIcon" :size="13"></Icon>
61 - </template>
62 - <template #label>Client</template>
63 - <template #value>{{ caseData.client_name || "-" }}</template>
64 - </Badge>
65 - <Badge
66 - v-if="caseData.case_soc_id"
67 - type="active"
68 - @click="gotoSocAlert(caseData.case_soc_id)"
69 - class="cursor-pointer"
70 - >
71 - <template #iconRight>
72 - <Icon :name="LinkIcon" :size="14"></Icon>
73 - </template>
74 - <template #label>Alert #{{ caseData.case_soc_id }}</template>
75 - </Badge>
76 - </div>
77 - </div>
78 - </div>
79 - <div class="footer-box flex justify-end items-center gap-3">
80 - <div class="time">{{ formatDate(caseData.case_open_date) }}</div>
81 - </div>
82 - </div>
83 -
84 - <n-modal
85 - v-model:show="showDetails"
86 - preset="card"
87 - content-style="padding:0px"
88 - :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
89 - :title="'SOC Case: ' + caseData.case_uuid"
90 - :bordered="false"
91 - segmented
92 - >
93 - <n-tabs type="line" animated :tabs-padding="24">
94 - <n-tab-pane name="Info" tab="Info" display-directive="show">
95 - <n-spin :show="loadingDetails">
96 - <div class="px-7 py-4" v-if="extendedInfo">
97 - <div class="flex gap-2 mb-2" v-if="tags.length">
98 - <code v-for="tag of tags" :key="tag">{{ tag }}</code>
99 - </div>
100 - <div>{{ extendedInfo.case_name }}</div>
101 - </div>
102 - <div class="flex flex-col gap-2 px-7 py-4" v-if="extendedInfo">
103 - <div class="box">
104 - soc id:
105 - <code
106 - class="cursor-pointer text-primary-color"
107 - @click="gotoSocAlert(caseData.case_soc_id)"
108 - >
109 - #{{ caseData.case_soc_id }}
110 - <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
111 - </code>
112 - </div>
113 - <div class="box" v-if="extendedInfo?.protagonists && extendedInfo?.protagonists.length">
114 - protagonists:
115 - <code v-for="protagonist of extendedInfo.protagonists" :key="protagonist" class="mr-2">
116 - {{ protagonist }}
117 - </code>
118 - </div>
119 - </div>
120 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
121 - <KVCard v-for="(value, key) of properties" :key="key">
122 - <template #key>{{ key }}</template>
123 - <template #value>{{ value || "-" }}</template>
124 - </KVCard>
125 - </div>
126 - </n-spin>
127 - </n-tab-pane>
128 - <n-tab-pane name="Description" tab="Description" display-directive="show">
129 - <div class="p-7 pt-4">
130 - <n-input
131 - :value="caseData.case_description"
132 - type="textarea"
133 - readonly
134 - placeholder="Empty"
135 - :autosize="{
136 - minRows: 3,
137 - maxRows: 10
138 - }"
139 - />
140 - </div>
141 - </n-tab-pane>
142 - <n-tab-pane name="History" tab="History" display-directive="show:lazy">
143 - <n-spin :show="loadingDetails">
144 - <div class="p-7 pt-4">
145 - <SocCaseTimeline :caseData="extendedInfo" v-if="extendedInfo" />
146 - </div>
147 - </n-spin>
148 - </n-tab-pane>
149 - <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
150 - <SocCaseAssetsList :case-id="caseData.case_id" />
151 - </n-tab-pane>
152 - <n-tab-pane name="Notes" tab="Notes" display-directive="show:lazy">
153 - <div class="px-4">
154 - <n-collapse display-directive="show" v-model:expanded-names="noteFormVisible">
155 - <template #arrow>
156 - <div class="mx-4 flex">
157 - <Icon :name="AddIcon"></Icon>
158 - </div>
159 - </template>
160 - <n-collapse-item name="1">
161 - <template #header>
162 - <div class="py-3 -ml-2">New note</div>
163 - </template>
164 - <div class="p-3 pt-0 -mt-2">
165 - <SocCaseNoteForm
166 - :case-id="caseData.case_id"
167 - @close="noteFormVisible = []"
168 - @added="updateNotes = true"
169 - />
170 - </div>
171 - </n-collapse-item>
172 - </n-collapse>
173 - </div>
174 - <n-divider class="!my-2" />
175 - <SocCaseNotesList :case-id="caseData.case_id" v-model:requested="updateNotes" />
176 - </n-tab-pane>
177 - </n-tabs>
178 - </n-modal>
179 - </div>
180 -</template>
181 -
182 -<script setup lang="ts">
183 -// TODO: add customer goto function ??
184 -
185 -import Icon from "@/components/common/Icon.vue"
186 -import KVCard from "@/components/common/KVCard.vue"
187 -import Badge from "@/components/common/Badge.vue"
188 -import { computed, ref, watch } from "vue"
189 -import SocCaseTimeline from "./SocCaseTimeline.vue"
190 -import SocCaseAssetsList from "./SocCaseAssetsList.vue"
191 -import SocCaseNoteForm from "./SocCaseNoteForm.vue"
192 -import SocCaseNotesList from "./SocCaseNotesList.vue"
193 -import "@/assets/scss/vuesjv-override.scss"
194 -import Api from "@/api"
195 -import {
196 - useMessage,
197 - NPopover,
198 - NSpin,
199 - NTimeline,
200 - NTimelineItem,
201 - NModal,
202 - NTabs,
203 - NTabPane,
204 - NDivider,
205 - NInput,
206 - NCollapse,
207 - NCollapseItem
208 -} from "naive-ui"
209 -import { useSettingsStore } from "@/stores/settings"
210 -import dayjs from "@/utils/dayjs"
211 -import { type SocCase, StateName, type SocCaseExt } from "@/types/soc/case.d"
212 -import _omit from "lodash/omit"
213 -import _split from "lodash/split"
214 -import { useRouter } from "vue-router"
215 -
216 -const { caseData, embedded } = defineProps<{ caseData: SocCase; embedded?: boolean }>()
217 -
218 -const TimeIcon = "carbon:time"
219 -const InfoIcon = "carbon:information"
220 -const CustomerIcon = "carbon:user"
221 -const LinkIcon = "carbon:launch"
222 -const OwnerIcon = "carbon:user-military"
223 -const StatusIcon = "fluent:status-20-regular"
224 -const AddIcon = "carbon:add-alt"
225 -
226 -const showDetails = ref(false)
227 -const loadingDetails = ref(false)
228 -const message = useMessage()
229 -const router = useRouter()
230 -const noteFormVisible = ref([])
231 -const updateNotes = ref(false)
232 -
233 -const extendedInfo = ref<SocCaseExt | null>(null)
234 -
235 -const dFormats = useSettingsStore().dateFormat
236 -
237 -const excerpt = computed(() => {
238 - const text = caseData.case_description
239 - const truncated = text.split(" ").slice(0, 30).join(" ")
240 -
241 - return truncated + (truncated !== text ? "..." : "")
242 -})
243 -
244 -const tags = computed<string[]>(() => {
245 - if (!extendedInfo?.value?.case_tags) {
246 - return []
247 - }
248 -
249 - return _split(extendedInfo.value?.case_tags, ",").map(o => "#" + o)
250 -})
251 -
252 -const properties = computed(() => {
253 - return _omit(extendedInfo.value, [
254 - "case_description",
255 - "case_name",
256 - "case_soc_id",
257 - "case_tags",
258 - "case_uuid",
259 - "close_date",
260 - "initial_date",
261 - "modification_history",
262 - "open_by_user",
263 - "open_by_user_id",
264 - "open_date",
265 - "protagonists"
266 - ])
267 -})
268 -
269 -function formatDate(timestamp: string | number | Date, utc: boolean = true): string {
270 - return dayjs(timestamp).utc(utc).format(dFormats.date)
271 -}
272 -
273 -function gotoSocAlert(socId: string) {
274 - router.push(`/soc/alerts?id=${socId}`).catch(() => {})
275 -}
276 -
277 -function getDetails() {
278 - loadingDetails.value = true
279 -
280 - Api.soc
281 - .getCases(caseData.case_id.toString())
282 - .then(res => {
283 - if (res.data.success) {
284 - extendedInfo.value = res.data?.case || null
285 - } else {
286 - message.warning(res.data?.message || "An error occurred. Please try again later.")
287 - }
288 - })
289 - .catch(err => {
290 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
291 - })
292 - .finally(() => {
293 - loadingDetails.value = false
294 - })
295 -}
296 -
297 -watch(showDetails, val => {
298 - if (val && !extendedInfo.value) {
299 - getDetails()
300 - }
301 -})
302 -</script>
303 -
304 -<style lang="scss" scoped>
305 -.soc-case-item {
306 - &:not(.embedded) {
307 - border-radius: var(--border-radius);
308 - background-color: var(--bg-color);
309 - border: var(--border-small-050);
310 - }
311 - border-top: var(--border-small-050);
312 - transition: all 0.2s var(--bezier-ease);
313 -
314 - .header-box {
315 - font-family: var(--font-family-mono);
316 - font-size: 13px;
317 - .id {
318 - word-break: break-word;
319 - color: var(--fg-secondary-color);
320 - line-height: 1.2;
321 -
322 - &:hover {
323 - color: var(--primary-color);
324 - }
325 - }
326 -
327 - .toggler-bookmark {
328 - &.active {
329 - color: var(--primary-color);
330 - }
331 - &:hover {
332 - color: var(--primary-color);
333 - }
334 - }
335 - .time {
336 - color: var(--fg-secondary-color);
337 -
338 - &:hover {
339 - color: var(--primary-color);
340 - }
341 - }
342 - }
343 -
344 - .main-box {
345 - word-break: break-word;
346 -
347 - .description {
348 - color: var(--fg-secondary-color);
349 - font-size: 13px;
350 - }
351 - }
352 -
353 - .footer-box {
354 - font-family: var(--font-family-mono);
355 - font-size: 13px;
356 - margin-top: 10px;
357 - display: none;
358 -
359 - .time {
360 - text-align: right;
361 - color: var(--fg-secondary-color);
362 - }
363 - }
364 -
365 - &:not(.embedded) {
366 - &:hover {
367 - box-shadow: 0px 0px 0px 1px inset var(--primary-color);
368 - }
369 - }
370 -
371 - @container (max-width: 650px) {
372 - .header-box {
373 - .time {
374 - display: none;
375 - }
376 - }
377 - .footer-box {
378 - display: flex;
379 - }
380 - }
381 -}
382 -</style>
src/components/soc/SocCases/SocCaseAssetLink.vue renamed
-1
@@ -60,7 +60,6 @@
60 import Icon from "@/components/common/Icon.vue"
61 import Badge from "@/components/common/Badge.vue"
62 import { ref } from "vue"
63 -import "@/assets/scss/vuesjv-override.scss"
63 import Api from "@/api"
64 import { useMessage, NSpin, NCollapse, NEmpty, NCollapseItem } from "naive-ui"
65 import { useSettingsStore } from "@/stores/settings"
src/components/soc/SocCases/SocCaseAssetsItem.vue renamed
-1
@@ -87,7 +87,6 @@ import KVCard from "@/components/common/KVCard.vue"
87 import Badge from "@/components/common/Badge.vue"
88 import SocCaseAssetLink from "./SocCaseAssetLink.vue"
89 import { computed, ref } from "vue"
90 -import "@/assets/scss/vuesjv-override.scss"
90 import { NModal, NTabs, NTabPane, NInput } from "naive-ui"
91 import _omit from "lodash/omit"
92 import _split from "lodash/split"
src/components/soc/SocCases/SocCaseAssetsList.vue renamed
-1
@@ -24,7 +24,6 @@
24 <script setup lang="ts">
25 import { ref } from "vue"
26 import SocCaseAssetsItem from "./SocCaseAssetsItem.vue"
27 -import "@/assets/scss/vuesjv-override.scss"
27 import Api from "@/api"
28 import { useMessage, NSpin, NEmpty } from "naive-ui"
29 import { useSettingsStore } from "@/stores/settings"
src/components/soc/SocCases/SocCaseItem.vue new
+464
@@ -0,0 +1,464 @@
1 +<template>
2 + <n-spin :show="loadingDetails">
3 + <div class="soc-case-item" :class="{ embedded }">
4 + <div class="flex flex-col gap-2 px-5 py-3" v-if="baseInfo">
5 + <div class="header-box flex justify-between">
6 + <div class="flex items-center gap-2">
7 + <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
8 + <span>{{ baseInfo.case_uuid }}</span>
9 + <Icon :name="InfoIcon" :size="16"></Icon>
10 + </div>
11 + </div>
12 + <div class="time" v-if="caseOpenDate">
13 + <n-popover overlap placement="top-end">
14 + <template #trigger>
15 + <div class="flex items-center gap-2 cursor-help">
16 + <span>
17 + {{ formatDate(caseOpenDate) }}
18 + </span>
19 + <Icon :name="TimeIcon" :size="16"></Icon>
20 + </div>
21 + </template>
22 + <div class="flex flex-col py-2 px-1">
23 + <n-timeline>
24 + <n-timeline-item
25 + type="success"
26 + :title="`Open ${openedBy ? '[' + openedBy + ']' : ''}`"
27 + :time="formatDate(caseOpenDate)"
28 + />
29 + <n-timeline-item
30 + v-if="caseCloseDate"
31 + title="Close date"
32 + :time="formatDate(caseCloseDate)"
33 + />
34 + </n-timeline>
35 + </div>
36 + </n-popover>
37 + </div>
38 + </div>
39 + <div class="main-box flex justify-between gap-4">
40 + <div class="content">
41 + <div class="title" v-html="baseInfo.case_name"></div>
42 + <div class="description mt-2" v-if="baseInfo.case_description">{{ excerpt }}</div>
43 +
44 + <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
45 + <Badge
46 + type="splitted"
47 + :color="baseInfo.state_name === StateName.Open ? 'warning' : undefined"
48 + >
49 + <template #iconLeft>
50 + <Icon :name="StatusIcon" :size="14"></Icon>
51 + </template>
52 + <template #label>State</template>
53 + <template #value>{{ baseInfo.state_name }}</template>
54 + </Badge>
55 + <Badge type="splitted">
56 + <template #iconLeft>
57 + <Icon :name="OwnerIcon" :size="16"></Icon>
58 + </template>
59 + <template #label>Owner</template>
60 + <template #value>{{ baseInfo.owner }}</template>
61 + </Badge>
62 + <Badge type="splitted">
63 + <template #iconLeft>
64 + <Icon :name="CustomerIcon" :size="13"></Icon>
65 + </template>
66 + <template #label>Client</template>
67 + <template #value>{{ clientName || "-" }}</template>
68 + </Badge>
69 + <Badge
70 + v-if="baseInfo.case_soc_id && !hideSocAlertLink"
71 + type="active"
72 + @click="openSocAlert()"
73 + class="cursor-pointer"
74 + >
75 + <template #iconRight>
76 + <Icon :name="LinkIcon" :size="14"></Icon>
77 + </template>
78 + <template #label>Alert #{{ baseInfo.case_soc_id }}</template>
79 + </Badge>
80 + </div>
81 + </div>
82 + </div>
83 + <div class="footer-box flex justify-end items-center gap-3">
84 + <div class="time" v-if="caseOpenDate">{{ formatDate(caseOpenDate) }}</div>
85 + </div>
86 + </div>
87 +
88 + <n-modal
89 + v-model:show="showSocAlertDetails"
90 + preset="card"
91 + content-style="padding:0px"
92 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
93 + :title="`SOC Alert: #${baseInfo?.case_soc_id}`"
94 + :bordered="false"
95 + segmented
96 + >
97 + <div class="h-full w-full flex items-center justify-center">
98 + <SocAlertItem
99 + v-if="baseInfo?.case_soc_id"
100 + :alertId="baseInfo.case_soc_id"
101 + embedded
102 + hideBookmarkAction
103 + hideSocCaseAction
104 + class="w-full"
105 + />
106 + </div>
107 + </n-modal>
108 +
109 + <n-modal
110 + v-model:show="showDetails"
111 + preset="card"
112 + content-style="padding:0px"
113 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
114 + :title="'SOC Case: ' + baseInfo?.case_uuid"
115 + :bordered="false"
116 + segmented
117 + >
118 + <n-tabs type="line" animated :tabs-padding="24">
119 + <n-tab-pane name="Info" tab="Info" display-directive="show">
120 + <n-spin :show="loadingDetails">
121 + <div class="px-7 py-4" v-if="extendedInfo">
122 + <div class="flex gap-2 mb-2" v-if="tags.length">
123 + <code v-for="tag of tags" :key="tag">{{ tag }}</code>
124 + </div>
125 + <div>{{ extendedInfo.case_name }}</div>
126 + </div>
127 + <div class="flex flex-col gap-2 px-7 py-4" v-if="extendedInfo">
128 + <div class="box" v-if="baseInfo && !hideSocAlertLink">
129 + soc id:
130 + <code class="cursor-pointer text-primary-color" @click="openSocAlert()">
131 + #{{ baseInfo.case_soc_id }}
132 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
133 + </code>
134 + </div>
135 + <div class="box" v-if="extendedInfo?.protagonists && extendedInfo?.protagonists.length">
136 + protagonists:
137 + <code
138 + v-for="protagonist of extendedInfo.protagonists"
139 + :key="protagonist"
140 + class="mr-2"
141 + >
142 + {{ protagonist }}
143 + </code>
144 + </div>
145 + </div>
146 + <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
147 + <KVCard v-for="(value, key) of properties" :key="key">
148 + <template #key>{{ key }}</template>
149 + <template #value>{{ value || "-" }}</template>
150 + </KVCard>
151 + </div>
152 + </n-spin>
153 + </n-tab-pane>
154 + <n-tab-pane name="Description" tab="Description" display-directive="show">
155 + <div class="p-7 pt-4" v-if="baseInfo">
156 + <n-input
157 + :value="baseInfo.case_description"
158 + type="textarea"
159 + readonly
160 + placeholder="Empty"
161 + :autosize="{
162 + minRows: 3,
163 + maxRows: 10
164 + }"
165 + />
166 + </div>
167 + </n-tab-pane>
168 + <n-tab-pane name="History" tab="History" display-directive="show:lazy">
169 + <n-spin :show="loadingDetails">
170 + <div class="p-7 pt-4">
171 + <SocCaseTimeline :caseData="extendedInfo" v-if="extendedInfo" />
172 + </div>
173 + </n-spin>
174 + </n-tab-pane>
175 + <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
176 + <SocCaseAssetsList v-if="baseInfo" :case-id="baseInfo.case_id" />
177 + </n-tab-pane>
178 + <n-tab-pane name="Notes" tab="Notes" display-directive="show:lazy">
179 + <div class="px-4">
180 + <n-collapse display-directive="show" v-model:expanded-names="noteFormVisible">
181 + <template #arrow>
182 + <div class="mx-4 flex">
183 + <Icon :name="AddIcon"></Icon>
184 + </div>
185 + </template>
186 + <n-collapse-item name="1">
187 + <template #header>
188 + <div class="py-3 -ml-2">New note</div>
189 + </template>
190 + <div class="p-3 pt-0 -mt-2">
191 + <SocCaseNoteForm
192 + v-if="baseInfo"
193 + :case-id="baseInfo.case_id"
194 + @close="noteFormVisible = []"
195 + @added="updateNotes = true"
196 + />
197 + </div>
198 + </n-collapse-item>
199 + </n-collapse>
200 + </div>
201 + <n-divider class="!my-2" />
202 + <SocCaseNotesList v-if="baseInfo" :case-id="baseInfo.case_id" v-model:requested="updateNotes" />
203 + </n-tab-pane>
204 + </n-tabs>
205 + </n-modal>
206 + </div>
207 + </n-spin>
208 +</template>
209 +
210 +<script setup lang="ts">
211 +// TODO: add customer goto function ??
212 +
213 +import Icon from "@/components/common/Icon.vue"
214 +import KVCard from "@/components/common/KVCard.vue"
215 +import Badge from "@/components/common/Badge.vue"
216 +import { computed, onBeforeMount, ref, watch } from "vue"
217 +import SocCaseTimeline from "./SocCaseTimeline.vue"
218 +import SocCaseAssetsList from "./SocCaseAssetsList.vue"
219 +import SocCaseNoteForm from "./SocCaseNoteForm.vue"
220 +import SocCaseNotesList from "./SocCaseNotesList.vue"
221 +import SocAlertItem from "../SocAlerts/SocAlertItem.vue"
222 +import Api from "@/api"
223 +import {
224 + useMessage,
225 + NPopover,
226 + NSpin,
227 + NTimeline,
228 + NTimelineItem,
229 + NModal,
230 + NTabs,
231 + NTabPane,
232 + NDivider,
233 + NInput,
234 + NCollapse,
235 + NCollapseItem
236 +} from "naive-ui"
237 +import { useSettingsStore } from "@/stores/settings"
238 +import dayjs from "@/utils/dayjs"
239 +import { type SocCase, StateName, type SocCaseExt } from "@/types/soc/case.d"
240 +import _omit from "lodash/omit"
241 +import _split from "lodash/split"
242 +
243 +const { caseData, caseId, embedded } = defineProps<{
244 + caseData?: SocCase
245 + caseId?: number | string
246 + embedded?: boolean
247 + hideSocAlertLink?: boolean
248 +}>()
249 +
250 +const TimeIcon = "carbon:time"
251 +const InfoIcon = "carbon:information"
252 +const CustomerIcon = "carbon:user"
253 +const LinkIcon = "carbon:launch"
254 +const OwnerIcon = "carbon:user-military"
255 +const StatusIcon = "fluent:status-20-regular"
256 +const AddIcon = "carbon:add-alt"
257 +
258 +const showSocAlertDetails = ref(false)
259 +const showDetails = ref(false)
260 +const loadingDetails = ref(false)
261 +const message = useMessage()
262 +const noteFormVisible = ref([])
263 +const updateNotes = ref(false)
264 +
265 +const baseInfo = computed<SocCase | SocCaseExt | null>(() => caseData || extendedInfo.value)
266 +const extendedInfo = ref<SocCaseExt | null>(null)
267 +
268 +const dFormats = useSettingsStore().dateFormat
269 +
270 +const caseOpenDate = computed<string | null>(() => {
271 + if ("case_open_date" in baseInfo) {
272 + return baseInfo.case_open_date as string
273 + }
274 + if ("open_date" in baseInfo) {
275 + return baseInfo.open_date as string
276 + }
277 + if ("open_date" in extendedInfo) {
278 + return extendedInfo.open_date as string
279 + }
280 + return null
281 +})
282 +
283 +const caseCloseDate = computed<string | null>(() => {
284 + if ("case_close_date" in baseInfo) {
285 + return baseInfo.case_close_date as string
286 + }
287 + if ("close_date" in baseInfo) {
288 + return baseInfo.close_date as string
289 + }
290 + if ("close_date" in extendedInfo) {
291 + return extendedInfo.close_date as string
292 + }
293 + return null
294 +})
295 +
296 +const clientName = computed<string | null>(() => {
297 + if ("client_name" in baseInfo) {
298 + return baseInfo.client_name as string
299 + }
300 + return null
301 +})
302 +
303 +const openedBy = computed<string | null>(() => {
304 + if ("opened_by" in baseInfo) {
305 + return baseInfo.opened_by as string
306 + }
307 + return null
308 +})
309 +
310 +const excerpt = computed(() => {
311 + const text = caseData?.case_description || ""
312 + const truncated = text.split(" ").slice(0, 30).join(" ")
313 +
314 + return truncated + (truncated !== text ? "..." : "")
315 +})
316 +
317 +const tags = computed<string[]>(() => {
318 + if (!extendedInfo?.value?.case_tags) {
319 + return []
320 + }
321 +
322 + return _split(extendedInfo.value?.case_tags, ",").map(o => "#" + o)
323 +})
324 +
325 +const properties = computed(() => {
326 + return _omit(extendedInfo.value, [
327 + "case_description",
328 + "case_name",
329 + "case_soc_id",
330 + "case_tags",
331 + "case_uuid",
332 + "close_date",
333 + "initial_date",
334 + "modification_history",
335 + "open_by_user",
336 + "open_by_user_id",
337 + "open_date",
338 + "protagonists"
339 + ])
340 +})
341 +
342 +function formatDate(timestamp: string | number | Date, utc: boolean = true): string {
343 + return dayjs(timestamp).utc(utc).format(dFormats.date)
344 +}
345 +
346 +function openSocAlert() {
347 + showSocAlertDetails.value = true
348 +}
349 +
350 +function getDetails() {
351 + if (caseId || baseInfo.value) {
352 + loadingDetails.value = true
353 +
354 + Api.soc
355 + .getCases(caseId?.toString() || baseInfo.value?.case_id.toString())
356 + .then(res => {
357 + if (res.data.success) {
358 + extendedInfo.value = res.data?.case || null
359 + } else {
360 + message.warning(res.data?.message || "An error occurred. Please try again later.")
361 + }
362 + })
363 + .catch(err => {
364 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
365 + })
366 + .finally(() => {
367 + loadingDetails.value = false
368 + })
369 + }
370 +}
371 +
372 +watch(showDetails, val => {
373 + if (val && !extendedInfo.value) {
374 + getDetails()
375 + }
376 +})
377 +
378 +onBeforeMount(() => {
379 + if (!caseData && caseId) {
380 + getDetails()
381 + }
382 +})
383 +</script>
384 +
385 +<style lang="scss" scoped>
386 +.soc-case-item {
387 + &:not(.embedded) {
388 + border-radius: var(--border-radius);
389 + background-color: var(--bg-color);
390 + border: var(--border-small-050);
391 + }
392 + border-top: var(--border-small-050);
393 + transition: all 0.2s var(--bezier-ease);
394 + min-height: 100px;
395 +
396 + .header-box {
397 + font-family: var(--font-family-mono);
398 + font-size: 13px;
399 + .id {
400 + word-break: break-word;
401 + color: var(--fg-secondary-color);
402 + line-height: 1.2;
403 +
404 + &:hover {
405 + color: var(--primary-color);
406 + }
407 + }
408 +
409 + .toggler-bookmark {
410 + &.active {
411 + color: var(--primary-color);
412 + }
413 + &:hover {
414 + color: var(--primary-color);
415 + }
416 + }
417 + .time {
418 + color: var(--fg-secondary-color);
419 +
420 + &:hover {
421 + color: var(--primary-color);
422 + }
423 + }
424 + }
425 +
426 + .main-box {
427 + word-break: break-word;
428 +
429 + .description {
430 + color: var(--fg-secondary-color);
431 + font-size: 13px;
432 + }
433 + }
434 +
435 + .footer-box {
436 + font-family: var(--font-family-mono);
437 + font-size: 13px;
438 + margin-top: 10px;
439 + display: none;
440 +
441 + .time {
442 + text-align: right;
443 + color: var(--fg-secondary-color);
444 + }
445 + }
446 +
447 + &:not(.embedded) {
448 + &:hover {
449 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
450 + }
451 + }
452 +
453 + @container (max-width: 650px) {
454 + .header-box {
455 + .time {
456 + display: none;
457 + }
458 + }
459 + .footer-box {
460 + display: flex;
461 + }
462 + }
463 +}
464 +</style>
src/components/soc/SocCases/SocCaseNote.vue renamed
-1
@@ -99,7 +99,6 @@ import Icon from "@/components/common/Icon.vue"
99 import KVCard from "@/components/common/KVCard.vue"
100 import SocCaseNoteTimeline from "./SocCaseNoteTimeline.vue"
101 import { computed, ref } from "vue"
102 -import "@/assets/scss/vuesjv-override.scss"
102 import { NModal, NTabs, NTabPane, NInput, NPopover } from "naive-ui"
103 import _omit from "lodash/omit"
104 import type { SocNote } from "@/types/soc/note.d"
src/components/soc/SocCases/SocCaseNoteForm.vue renamed
-1
@@ -24,7 +24,6 @@
24
25 <script setup lang="ts">
26 import { ref } from "vue"
27 -import "@/assets/scss/vuesjv-override.scss"
27 import Api from "@/api"
28 import { useMessage, NSpin, NInput, NButton } from "naive-ui"
29 import type { SocNewNote } from "@/types/soc/note.d"
src/components/soc/SocCases/SocCaseNoteTimeline.vue renamed
src/components/soc/SocCases/SocCaseNotesList.vue renamed
+1 -1
@@ -17,7 +17,6 @@
17 <script setup lang="ts">
18 import { ref, watch, onBeforeMount } from "vue"
19 import SocCaseNote from "./SocCaseNote.vue"
20 -import "@/assets/scss/vuesjv-override.scss"
20 import Api from "@/api"
21 import { useMessage, NSpin, NInput, NEmpty } from "naive-ui"
22 import type { SocNote } from "@/types/soc/note.d"
@@ -86,5 +85,6 @@ watch(requested, val => {
85
86 onBeforeMount(() => {
87 getNotes()
88 + abortControllerNotes?.abort()
89 })
90 </script>
src/components/soc/SocCases/SocCaseTimeline.vue renamed
src/components/soc/SocCases/SocCasesList.vue renamed
src/components/soc/SocUsers/SocUserAlerts.vue renamed
+29 -6
@@ -7,21 +7,42 @@
7 <div class="flex flex-wrap gap-2">
8 <n-tooltip v-for="alert of alertsList" :key="alert.alert_id">
9 <template #trigger>
10 - <code class="alert-btn" @click="gotoSocAlert(alert.alert_id)">#{{ alert.alert_id }}</code>
10 + <code class="alert-btn" @click="openSocAlert(alert.alert_id)">#{{ alert.alert_id }}</code>
11 </template>
12 {{ alert.alert_title }}
13 </n-tooltip>
14 </div>
15 </div>
16 </n-spin>
17 +
18 + <n-modal
19 + v-model:show="showSocAlertDetails"
20 + preset="card"
21 + content-style="padding:0px"
22 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
23 + :title="`SOC Alert: #${selectedAlertId}`"
24 + :bordered="false"
25 + segmented
26 + >
27 + <div class="h-full w-full flex items-center justify-center">
28 + <SocAlertItem
29 + v-if="selectedAlertId"
30 + :alertId="selectedAlertId"
31 + embedded
32 + hideSocCaseAction
33 + hideBookmarkAction
34 + class="w-full"
35 + />
36 + </div>
37 + </n-modal>
38 </template>
39
40 <script setup lang="ts">
41 import type { SocAlert } from "@/types/soc/alert.d"
42 import { onBeforeMount, onBeforeUnmount, ref } from "vue"
43 import Api from "@/api"
23 -import { useMessage, NTooltip, NSpin } from "naive-ui"
24 -import { useRouter } from "vue-router"
44 +import { useMessage, NTooltip, NSpin, NModal } from "naive-ui"
45 +import SocAlertItem from "../SocAlerts/SocAlertItem.vue"
46 import axios from "axios"
47
48 const { userId } = defineProps<{
@@ -30,12 +51,14 @@ const { userId } = defineProps<{
51
52 const loadingAlerts = ref(false)
53 const alertsList = ref<SocAlert[]>([])
33 -const router = useRouter()
54 const message = useMessage()
55 let abortController: AbortController | null = null
56 +const showSocAlertDetails = ref(false)
57 +const selectedAlertId = ref<string | number | null>(null)
58
37 -function gotoSocAlert(socId: string | number) {
38 - router.push(`/soc/alerts?id=${socId}`).catch(() => {})
59 +function openSocAlert(socId: string | number) {
60 + selectedAlertId.value = socId
61 + showSocAlertDetails.value = true
62 }
63
64 function getAlerts() {
src/components/soc/SocUsers/SocUsersList.vue renamed
+6
@@ -135,6 +135,12 @@ onBeforeMount(() => {
135 }
136 }
137
138 + tr:hover {
139 + td {
140 + background-color: var(--primary-005-color);
141 + }
142 + }
143 +
144 .highlight {
145 td {
146 border-top: 1px solid var(--primary-030-color);
src/components/users/ChangePassword.vue new
+186
@@ -0,0 +1,186 @@
1 +<template>
2 + <n-button :size="size" :type="type" @click="showFormDrawer = true">
3 + <template #icon>
4 + <Icon :name="PasswordIcon" :size="14"></Icon>
5 + </template>
6 + Change Password
7 + </n-button>
8 +
9 + <n-drawer
10 + v-model:show="showFormDrawer"
11 + :width="500"
12 + style="max-width: 90vw"
13 + :trap-focus="false"
14 + display-directive="show"
15 + >
16 + <n-drawer-content title="Change Password" closable :native-scrollbar="false">
17 + <n-spin :show="loading">
18 + <n-form ref="formRef" :model="model" :rules="rules">
19 + <div class="flex flex-col gap-3">
20 + <n-form-item label="Username" required>
21 + <n-input :value="username" disabled readonly />
22 + </n-form-item>
23 + <n-form-item path="password" label="Password">
24 + <n-input
25 + v-model:value="model.password"
26 + type="password"
27 + @keydown.enter="submit"
28 + size="large"
29 + show-password-on="click"
30 + placeholder="At least 8 characters"
31 + />
32 + </n-form-item>
33 + <n-form-item path="confirmPassword" label="Confirm Password" first>
34 + <n-input
35 + v-model:value="model.confirmPassword"
36 + type="password"
37 + :disabled="!model.password"
38 + @keydown.enter="submit"
39 + size="large"
40 + show-password-on="click"
41 + placeholder="At least 8 characters"
42 + />
43 + </n-form-item>
44 +
45 + <div class="flex justify-end">
46 + <n-button type="primary" :disabled="!isValid" @click="submit">Send new password</n-button>
47 + </div>
48 + </div>
49 + </n-form>
50 + </n-spin>
51 + </n-drawer-content>
52 + </n-drawer>
53 +</template>
54 +
55 +<script setup lang="ts">
56 +import { ref, watch, computed } from "vue"
57 +import {
58 + NDrawer,
59 + NDrawerContent,
60 + type FormInst,
61 + type FormValidationError,
62 + useMessage,
63 + type FormRules,
64 + NForm,
65 + NFormItem,
66 + NInput,
67 + NButton,
68 + NSpin,
69 + type FormItemRule
70 +} from "naive-ui"
71 +import Api from "@/api"
72 +import _trim from "lodash/trim"
73 +import _toNumber from "lodash/toNumber"
74 +import { useAuthStore } from "@/stores/auth"
75 +import passwordValidator from "password-validator"
76 +import Icon from "@/components/common/Icon.vue"
77 +
78 +const { type, size, username } = defineProps<{
79 + username: string
80 + size?: "tiny" | "small" | "medium" | "large"
81 + type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
82 +}>()
83 +
84 +const showFormDrawer = ref(false)
85 +
86 +watch(showFormDrawer, () => {
87 + clear()
88 +})
89 +
90 +const PasswordIcon = "carbon:password"
91 +const message = useMessage()
92 +const loading = ref(false)
93 +const model = ref({
94 + password: "",
95 + confirmPassword: ""
96 +})
97 +const formRef = ref<FormInst | null>(null)
98 +const storedUserName = useAuthStore().userName
99 +const passwordSchema = new passwordValidator()
100 +
101 +passwordSchema
102 + .is()
103 + .min(8) // Minimum length 8
104 + .is()
105 + .max(100) // Maximum length 100
106 + .has()
107 + .uppercase() // Must have uppercase letters
108 + .has()
109 + .lowercase() // Must have lowercase letters
110 + .has()
111 + .digits(1) // Must have at least 1 digit
112 + .has()
113 + .symbols(1) // Must have at least 1 symbol
114 +
115 +const rules: FormRules = {
116 + password: [
117 + {
118 + required: true,
119 + trigger: ["blur"],
120 + message: "Password is required"
121 + },
122 + {
123 + validator: (rule: FormItemRule, value: string): boolean => {
124 + return !!passwordSchema.validate(value, { details: false })
125 + },
126 + message:
127 + "The string should have a minimum length of 8 characters, minimum of 1 uppercase and lowercase letter, minimum of 1 digit and 1 symbol",
128 + trigger: ["blur"]
129 + }
130 + ],
131 + confirmPassword: [
132 + {
133 + required: true,
134 + trigger: ["blur"],
135 + message: "Confirm Password is required"
136 + },
137 + {
138 + validator: (rule: FormItemRule, value: string): boolean => {
139 + return value === model.value.password
140 + },
141 + message: "Password is not same as re-entered password",
142 + trigger: ["blur", "password-input"]
143 + }
144 + ]
145 +}
146 +
147 +const isValid = computed(() => {
148 + return !!model.value.password && !!model.value.confirmPassword && !!username
149 +})
150 +
151 +function clear() {
152 + model.value.password = ""
153 + model.value.confirmPassword = ""
154 +}
155 +
156 +function submit(e: Event) {
157 + e.preventDefault()
158 + formRef.value?.validate((errors: Array<FormValidationError> | undefined) => {
159 + if (!errors) {
160 + loading.value = true
161 +
162 + const method = username === storedUserName ? "resetOwnPassword" : "resetPassword"
163 +
164 + Api.auth[method](username, model.value.password)
165 + .then(res => {
166 + if (res.data.success) {
167 + clear()
168 + message.success(res.data?.message || "New Password submitted.")
169 + } else {
170 + message.warning(res.data?.message || "An error occurred. Please try again later.")
171 + }
172 + })
173 + .catch(err => {
174 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
175 + })
176 + .finally(() => {
177 + loading.value = false
178 + })
179 + } else {
180 + for (const err of errors) {
181 + message.error(err[0].message || "Invalid fields")
182 + }
183 + }
184 + })
185 +}
186 +</script>
src/components/users/UsersList.vue new
+125
@@ -0,0 +1,125 @@
1 +<template>
2 + <div class="users-list">
3 + <n-spin :show="loadingUsers">
4 + <n-scrollbar x-scrollable style="width: 100%">
5 + <n-table :bordered="false" class="min-w-max">
6 + <thead>
7 + <tr>
8 + <th>ID</th>
9 + <th>Username</th>
10 + <th>Email</th>
11 + <th style="max-width: 300px"></th>
12 + </tr>
13 + </thead>
14 + <tbody>
15 + <tr
16 + v-for="user of usersList"
17 + :key="user.id"
18 + :class="{ highlight: highlight === user.id.toString() }"
19 + >
20 + <td>#{{ user.id }}</td>
21 + <td>
22 + {{ user.username }}
23 + </td>
24 + <td>
25 + {{ user.email }}
26 + </td>
27 + <td style="max-width: 300px">
28 + <div class="flex justify-end" v-if="isAdmin">
29 + <n-dropdown
30 + trigger="hover"
31 + :options="options"
32 + display-directive="show"
33 + :keyboard="false"
34 + @click="selectedUser = user.username"
35 + >
36 + <n-button text>
37 + <template #icon>
38 + <Icon :name="DropdownIcon" :size="24"></Icon>
39 + </template>
40 + </n-button>
41 + </n-dropdown>
42 + </div>
43 + </td>
44 + </tr>
45 + </tbody>
46 + </n-table>
47 + </n-scrollbar>
48 + </n-spin>
49 + </div>
50 +</template>
51 +
52 +<script setup lang="ts">
53 +import { ref, onBeforeMount, toRefs, h } from "vue"
54 +import { useMessage, NTable, NScrollbar, NSpin, NDropdown, NButton } from "naive-ui"
55 +import Api from "@/api"
56 +import type { AuthUser } from "@/types/auth.d"
57 +import ChangePassword from "./ChangePassword.vue"
58 +import { useAuthStore } from "@/stores/auth"
59 +import Icon from "@/components/common/Icon.vue"
60 +
61 +const props = defineProps<{ highlight: string | null | undefined }>()
62 +const { highlight } = toRefs(props)
63 +
64 +const DropdownIcon = "carbon:overflow-menu-horizontal"
65 +const message = useMessage()
66 +const loadingUsers = ref(false)
67 +const usersList = ref<AuthUser[]>([])
68 +const isAdmin = useAuthStore().isAdmin
69 +const selectedUser = ref("")
70 +
71 +const options = [
72 + {
73 + key: "ChangePassword",
74 + type: "render",
75 + render: () => h(ChangePassword, { username: selectedUser.value })
76 + }
77 +]
78 +
79 +function getUsers() {
80 + loadingUsers.value = true
81 +
82 + Api.auth
83 + .getUsers()
84 + .then(res => {
85 + if (res.data.success) {
86 + usersList.value = res.data?.users || []
87 + } else {
88 + message.warning(res.data?.message || "An error occurred. Please try again later.")
89 + }
90 + })
91 + .catch(err => {
92 + usersList.value = []
93 +
94 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
95 + })
96 + .finally(() => {
97 + loadingUsers.value = false
98 + })
99 +}
100 +
101 +onBeforeMount(() => {
102 + getUsers()
103 +})
104 +</script>
105 +
106 +<style lang="scss" scoped>
107 +.users-list {
108 + border-radius: var(--border-radius);
109 + overflow: hidden;
110 +
111 + tr:hover {
112 + td {
113 + background-color: var(--primary-005-color);
114 + }
115 + }
116 +
117 + .highlight {
118 + td {
119 + border-top: 1px solid var(--primary-030-color);
120 + border-bottom: 1px solid var(--primary-030-color);
121 + background-color: var(--primary-005-color);
122 + }
123 + }
124 +}
125 +</style>
src/design-tokens.json
+39 -31
@@ -66,19 +66,23 @@
66 "textSecondary": "#495465",
67 "background": "#ffffff",
68 "backgroundSecondary": "#fafbfc",
69 - "primary": "rgb(0, 178, 123)",
70 - "primary005": "rgba(0, 178, 123, 0.05)",
71 - "primary010": "rgba(0, 178, 123, 0.1)",
72 - "primary015": "rgba(0, 178, 123, 0.15)",
73 - "primary020": "rgba(0, 178, 123, 0.2)",
74 - "primary030": "rgba(0, 178, 123, 0.3)",
75 - "primary040": "rgba(0, 178, 123, 0.4)",
76 - "primary050": "rgba(0, 178, 123, 0.5)",
77 - "primary060": "rgba(0, 178, 123, 0.6)",
69 + "primary": "rgb(255, 182, 0)",
70 + "primary005": "rgba(255, 182, 0, 0.05)",
71 + "primary010": "rgba(255, 182, 0, 0.1)",
72 + "primary015": "rgba(255, 182, 0, 0.15)",
73 + "primary020": "rgba(255, 182, 0, 0.2)",
74 + "primary030": "rgba(255, 182, 0, 0.3)",
75 + "primary040": "rgba(255, 182, 0, 0.4)",
76 + "primary050": "rgba(255, 182, 0, 0.5)",
77 + "primary060": "rgba(255, 182, 0, 0.6)",
78 "info": "#6267FF",
79 "success": "#00B27B",
80 - "warning": "#FFB600",
80 + "warning": "#E3C22F",
81 "error": "#FF0156",
82 + "info005": "rgba(98, 103, 255, 0.05)",
83 + "success005": "rgba(0, 178, 123, 0.05)",
84 + "warning005": "rgba(227, 194, 47, 0.05)",
85 + "error005": "rgba(255, 1, 86, 0.05)",
86 "secondary1": "rgb(98, 103, 255)",
87 "secondary1Opacity005": "rgba(98, 103, 255, 0.05)",
88 "secondary1Opacity010": "rgba(98, 103, 255, 0.1)",
@@ -89,11 +93,11 @@
93 "secondary2Opacity010": "rgba(255, 97, 201, 0.1)",
94 "secondary2Opacity020": "rgba(255, 97, 201, 0.2)",
95 "secondary2Opacity030": "rgba(255, 97, 201, 0.3)",
92 - "secondary3": "rgb(255, 182, 0)",
93 - "secondary3Opacity005": "rgba(255, 182, 0, 0.05)",
94 - "secondary3Opacity010": "rgba(255, 182, 0, 0.1)",
95 - "secondary3Opacity020": "rgba(255, 182, 0, 0.2)",
96 - "secondary3Opacity030": "rgba(255, 182, 0, 0.3)",
96 + "secondary3": "rgb(227, 194, 47)",
97 + "secondary3Opacity005": "rgba(227, 194, 47, 0.05)",
98 + "secondary3Opacity010": "rgba(227, 194, 47, 0.1)",
99 + "secondary3Opacity020": "rgba(227, 194, 47, 0.2)",
100 + "secondary3Opacity030": "rgba(227, 194, 47, 0.3)",
101 "secondary4": "rgb(255, 1, 86)",
102 "secondary4Opacity005": "rgba(255, 1, 86, 0.05)",
103 "secondary4Opacity010": "rgba(255, 1, 86, 0.1)",
@@ -113,19 +117,23 @@
117 "textSecondary": "#ACB5BE",
118 "background": "#26282d",
119 "backgroundSecondary": "#1D1F25",
116 - "primary": "rgb(0, 225, 155)",
117 - "primary005": "rgba(0, 225, 155, 0.05)",
118 - "primary010": "rgba(0, 225, 155, 0.1)",
119 - "primary015": "rgba(0, 225, 155, 0.15)",
120 - "primary020": "rgba(0, 225, 155, 0.2)",
121 - "primary030": "rgba(0, 225, 155, 0.3)",
122 - "primary040": "rgba(0, 225, 155, 0.4)",
123 - "primary050": "rgba(0, 225, 155, 0.5)",
124 - "primary060": "rgba(0, 225, 155, 0.6)",
120 + "primary": "rgb(255, 182, 0)",
121 + "primary005": "rgba(255, 182, 0, 0.05)",
122 + "primary010": "rgba(255, 182, 0, 0.1)",
123 + "primary015": "rgba(255, 182, 0, 0.15)",
124 + "primary020": "rgba(255, 182, 0, 0.2)",
125 + "primary030": "rgba(255, 182, 0, 0.3)",
126 + "primary040": "rgba(255, 182, 0, 0.4)",
127 + "primary050": "rgba(255, 182, 0, 0.5)",
128 + "primary060": "rgba(255, 182, 0, 0.6)",
129 "info": "#6267FF",
130 "success": "#00E19B",
127 - "warning": "#FFB600",
131 + "warning": "#E3C22F",
132 "error": "#FF0156",
133 + "info005": "rgba(98, 103, 255, 0.05)",
134 + "success005": "rgba(0, 178, 123, 0.05)",
135 + "warning005": "rgba(227, 194, 47, 0.05)",
136 + "error005": "rgba(255, 1, 86, 0.05)",
137 "secondary1": "rgb(98, 103, 255)",
138 "secondary1Opacity005": "rgba(98, 103, 255, 0.05)",
139 "secondary1Opacity010": "rgba(98, 103, 255, 0.1)",
@@ -136,11 +144,11 @@
144 "secondary2Opacity010": "rgba(255, 97, 201, 0.1)",
145 "secondary2Opacity020": "rgba(255, 97, 201, 0.2)",
146 "secondary2Opacity030": "rgba(255, 97, 201, 0.3)",
139 - "secondary3": "rgb(255, 182, 0)",
140 - "secondary3Opacity005": "rgba(255, 182, 0, 0.05)",
141 - "secondary3Opacity010": "rgba(255, 182, 0, 0.1)",
142 - "secondary3Opacity020": "rgba(255, 182, 0, 0.2)",
143 - "secondary3Opacity030": "rgba(255, 182, 0, 0.3)",
147 + "secondary3": "rgb(227, 194, 47)",
148 + "secondary3Opacity005": "rgba(227, 194, 47, 0.05)",
149 + "secondary3Opacity010": "rgba(227, 194, 47, 0.1)",
150 + "secondary3Opacity020": "rgba(227, 194, 47, 0.2)",
151 + "secondary3Opacity030": "rgba(227, 194, 47, 0.3)",
152 "secondary4": "rgb(255, 1, 86)",
153 "secondary4Opacity005": "rgba(255, 1, 86, 0.05)",
154 "secondary4Opacity010": "rgba(255, 1, 86, 0.1)",
@@ -154,4 +162,4 @@
162 "hover050": "rgba(255, 255, 255, 0.5)"
163 }
164 }
157 -}
165 +}
\ No newline at end of file
src/emitter.ts
+6 -1
@@ -1,3 +1,8 @@
1 import mitt, { type Emitter as Mitt, type EventType } from "mitt"
2 -export const emitter = mitt()
2 +
3 +type Events = {
4 + "action:add-customer": void
5 +}
6 +
7 +export const emitter = mitt<Events>()
8 export type Emitter<T extends Record<EventType, unknown>> = Mitt<T>
src/layouts/HorizontalNav/SidebarFooter.vue
+5 -20
@@ -10,8 +10,7 @@ import { NMenu } from "naive-ui"
10 import { useThemeStore } from "@/stores/theme"
11 import { renderIcon } from "@/utils"
12
13 -const BuyIcon = "carbon:shopping-cart"
14 -const DocsIcon = "ion:book-outline"
13 +const ContactIcon = "ic:outline-alternate-email"
14
15 defineOptions({
16 name: "SidebarFooter"
@@ -31,28 +30,14 @@ const menuOptions = ref([
30 h(
31 "a",
32 {
34 - href: "https://pinx-docs.vercel.app/",
33 + href: "https://www.socfortress.co/contact_form.html",
34 target: "_blank",
35 rel: "noopenner noreferrer"
36 },
38 - "Documentation"
37 + "Contact SOCFortress"
38 ),
40 - key: "documentation",
41 - icon: renderIcon(DocsIcon)
42 - },
43 - {
44 - label: () =>
45 - h(
46 - "a",
47 - {
48 - href: "https://themeforest.net/item/pinx-vuejs-admin-template/47799543",
49 - target: "_blank",
50 - rel: "noopenner noreferrer"
51 - },
52 - "Buy now"
53 - ),
54 - key: "buy-now",
55 - icon: renderIcon(BuyIcon)
39 + key: "contact-socfortress",
40 + icon: renderIcon(ContactIcon)
41 }
42 ])
43
src/layouts/HorizontalNav/index.vue
+1
@@ -12,6 +12,7 @@
12 import HeaderBar from "./HeaderBar.vue"
13 import Sidebar from "./Sidebar.vue"
14 import MainContainer from "./MainContainer.vue"
15 +import "./main.scss"
16
17 defineOptions({
18 name: "VerticalNav"
src/layouts/HorizontalNav/main.scss new
+16
@@ -0,0 +1,16 @@
1 +@import "./variables";
2 +
3 +.page-wrapped {
4 + @media (min-width: calc($sidebar-bp + 1px)) {
5 + &.layout-HorizontalNav {
6 + height: calc(100svh - var(--toolbar-height) - var(--view-padding) - var(--header-bar-height));
7 + }
8 + }
9 +}
10 +.page-min-wrapped {
11 + @media (min-width: calc($sidebar-bp + 1px)) {
12 + &.layout-HorizontalNav {
13 + min-height: calc(100svh - var(--toolbar-height) - var(--view-padding) - var(--header-bar-height));
14 + }
15 + }
16 +}
src/layouts/VerticalNav/SidebarFooter.vue
+5 -20
@@ -14,8 +14,7 @@ defineOptions({
14 name: "SidebarFooter"
15 })
16
17 -const BuyIcon = "carbon:shopping-cart"
18 -const DocsIcon = "ion:book-outline"
17 +const ContactIcon = "ic:outline-alternate-email"
18
19 const props = withDefaults(
20 defineProps<{
@@ -31,28 +30,14 @@ const menuOptions = ref([
30 h(
31 "a",
32 {
34 - href: "https://pinx-docs.vercel.app/",
33 + href: "https://www.socfortress.co/contact_form.html",
34 target: "_blank",
35 rel: "noopenner noreferrer"
36 },
38 - "Documentation"
37 + "Contact SOCFortress"
38 ),
40 - key: "documentation",
41 - icon: renderIcon(DocsIcon)
42 - },
43 - {
44 - label: () =>
45 - h(
46 - "a",
47 - {
48 - href: "https://themeforest.net/item/pinx-vuejs-admin-template/47799543",
49 - target: "_blank",
50 - rel: "noopenner noreferrer"
51 - },
52 - "Buy now"
53 - ),
54 - key: "buy-now",
55 - icon: renderIcon(BuyIcon)
39 + key: "contact-socfortress",
40 + icon: renderIcon(ContactIcon)
41 }
42 ])
43
src/layouts/common/Navbar/items.tsx
+15
@@ -14,6 +14,7 @@ const SOCIcon = "carbon:security"
14 const HealthcheckIcon = "ph:heartbeat"
15 const CustomersIcon = "carbon:user-multiple"
16 const LogsIcon = "carbon:cloud-logging"
17 +const UsersIcon = "carbon:group-security"
18
19 export default function getItems(mode: "vertical" | "horizontal", collapsed: boolean): MenuMixedOption[] {
20 return [
@@ -234,6 +235,20 @@ export default function getItems(mode: "vertical" | "horizontal", collapsed: boo
235 ),
236 key: "Logs",
237 icon: renderIcon(LogsIcon)
238 + },
239 + {
240 + label: () =>
241 + h(
242 + RouterLink,
243 + {
244 + to: {
245 + name: "Users"
246 + }
247 + },
248 + { default: () => "Users" }
249 + ),
250 + key: "Users",
251 + icon: renderIcon(UsersIcon)
252 }
253 ]
254 }
src/layouts/common/Toolbar/Avatar.vue
+5 -5
@@ -13,7 +13,7 @@ import { useAuthStore } from "@/stores/auth"
13
14 const UserIcon = "ion:person-outline"
15 const LogoutIcon = "ion:log-out-outline"
16 -const DocsIcon = "ion:book-outline"
16 +const ContactIcon = "ic:outline-alternate-email"
17
18 defineOptions({
19 name: "Avatar"
@@ -34,14 +34,14 @@ const options = ref([
34 h(
35 "a",
36 {
37 - href: "https://pinx-docs.vercel.app/",
37 + href: "https://www.socfortress.co/contact_form.html",
38 target: "_blank",
39 rel: "noopenner noreferrer"
40 },
41 - "Documentation"
41 + "Contact SOCFortress"
42 ),
43 - key: "documentation",
44 - icon: renderIcon(DocsIcon)
43 + key: "contact-socfortress",
44 + icon: renderIcon(ContactIcon)
45 },
46 {
47 label: "Logout",
src/layouts/common/Toolbar/index.vue
+1 -1
@@ -102,7 +102,7 @@ onMounted(() => {
102 gap: 14px;
103 }
104
105 - @media (max-width: 1400px) {
105 + @media (max-width: 1250px) {
106 .pinned-pages {
107 display: none;
108 }
src/router/index.ts
+6
@@ -128,6 +128,12 @@ const router = createRouter({
128 component: () => import("@/views/Logs.vue"),
129 meta: { title: "Logs", auth: true, roles: UserRole.All }
130 },
131 + {
132 + path: "/users",
133 + name: "Users",
134 + component: () => import("@/views/Users.vue"),
135 + meta: { title: "Users", auth: true, roles: UserRole.All }
136 + },
137
138 {
139 path: "/profile",
src/stores/auth.ts
+19
@@ -7,6 +7,7 @@ import _toNumber from "lodash/toNumber"
7 import { scopeToRole } from "@/utils/auth"
8 import { hashMD5 } from "@/utils"
9 import _toSafeInteger from "lodash/toSafeInteger"
10 +import _toLower from "lodash/toLower"
11 import SecureLS from "secure-ls"
12 const ls = new SecureLS({ encodingType: "aes", isCompression: false })
13
@@ -15,6 +16,7 @@ export const useAuthStore = defineStore("auth", {
16 user: {
17 access_token: "",
18 username: "",
19 + email: "",
20 role: UserRole.Unknown
21 } as User,
22 tokenDebounceTime: _toNumber(import.meta.env.VITE_TOKEN_DEBOUNCE_TIME) as number // seconds
@@ -27,6 +29,7 @@ export const useAuthStore = defineStore("auth", {
29 this.user = {
30 access_token: token,
31 username: jwtPayload.sub || "",
32 + email: "",
33 role: scopeToRole(scopes)
34 }
35 },
@@ -37,6 +40,7 @@ export const useAuthStore = defineStore("auth", {
40 this.user = {
41 access_token: "",
42 username: "",
43 + email: "",
44 role: UserRole.Unknown
45 }
46 },
@@ -47,6 +51,7 @@ export const useAuthStore = defineStore("auth", {
51 .then(res => {
52 if (res.data.access_token) {
53 this.setLogged(res.data.access_token)
54 + this.getEmail()
55 resolve(res.data)
56 } else {
57 reject(res.data)
@@ -57,6 +62,14 @@ export const useAuthStore = defineStore("auth", {
62 })
63 })
64 },
65 + getEmail() {
66 + Api.auth.getUsers().then(res => {
67 + if (res.data.users) {
68 + const user = res.data.users.find(o => o.username === this.userName)
69 + this.user.email = user?.email || ""
70 + }
71 + })
72 + },
73 refreshToken() {
74 return new Promise((resolve, reject) => {
75 Api.auth
@@ -85,6 +98,9 @@ export const useAuthStore = defineStore("auth", {
98 userName(state): string {
99 return state.user?.username
100 },
101 + userEmail(state): string {
102 + return state.user?.email
103 + },
104 userRole(state): UserRole {
105 return state.user?.role
106 },
@@ -105,6 +121,9 @@ export const useAuthStore = defineStore("auth", {
121
122 return `https://avatar.vercel.sh/${seed}.svg?text=${text}`
123 },
124 + isAdmin(): boolean {
125 + return _toLower(this.userRoleName) === "admin"
126 + },
127 isRoleGranted() {
128 return (roles?: UserRole | UserRole[]) => {
129 if (!roles) {
src/stores/settings.ts
+4 -1
@@ -31,15 +31,18 @@ export const useSettingsStore = defineStore("settings", {
31 const date = state.settings.dateFormat
32 const time = state.settings.hours24 ? "HH:mm" : "h:mm a"
33 const timesec = state.settings.hours24 ? "HH:mm:ss" : "h:mm:ss a"
34 + const timesecmill = "HH:mm:ss.SSS"
35
36 return {
37 date: `${date}`,
38 datetime: `${date}${separator}${time}`,
39 datetimesec: `${date}${separator}${timesec}`,
40 + datetimesecmill: `${date}${separator}${timesecmill}`,
41 /** "HH:mm" or "h:mm a" */
42 time,
43 /** "HH:mm:ss" or "h:mm:ss a" */
42 - timesec
44 + timesec,
45 + timesecmill
46 }
47 }
48 },
src/stores/theme.ts
+9 -4
@@ -20,12 +20,12 @@ const osTheme = useOsTheme()
20
21 export const useThemeStore = defineStore("theme", {
22 state: () => ({
23 - layout: Layout.VerticalNav,
24 - themeName: osTheme.value || ThemeEnum.Light,
23 + layout: Layout.HorizontalNav,
24 + themeName: osTheme.value || ThemeEnum.Dark,
25 routerTransition: RouterTransition.FadeUp,
26 routerTransitionDuration: 0.3,
27 boxed: {
28 - enabled: true,
28 + enabled: false,
29 toolbar: true,
30 width: 1600
31 },
@@ -298,6 +298,8 @@ export const useThemeStore = defineStore("theme", {
298 const warningColor = naive.warningColor
299 const infoColor = naive.infoColor
300
301 + const { success005, warning005, error005, info005 } = state.colors[state.themeName]
302 +
303 const modalColor = naive.modalColor
304 const modalColorRGB = hex2rgb(modalColor).join(", ")
305 const codeColor = naive.codeColor
@@ -365,7 +367,6 @@ export const useThemeStore = defineStore("theme", {
367 "--fg-secondary-color": `${fgSecondaryColor}`,
368 "--bg-color": `${bgColor}`,
369 "--bg-secondary-color": `${bgSecondaryColor}`,
368 -
370 "--bg-color-rgb": `${bgColorRGB}`,
371
372 "--border-color": `${borderColor}`,
@@ -417,6 +418,10 @@ export const useThemeStore = defineStore("theme", {
418 "--error-color": `${errorColor}`,
419 "--warning-color": `${warningColor}`,
420 "--info-color": `${infoColor}`,
421 + "--success-005-color": `${success005}`,
422 + "--error-005-color": `${error005}`,
423 + "--warning-005-color": `${warning005}`,
424 + "--info-005-color": `${info005}`,
425
426 "--secondary1-color": `${secondary1}`,
427 "--secondary1-color-rgb": `${secondary1RGB}`,
src/types/alerts.d.ts
+1
@@ -228,6 +228,7 @@ export interface AlertSourceContent {
228 syslog_type: AlertSourceSyslogType
229 timestamp_utc: string
230 timestamp: string
231 + type?: string
232 true: number
233 }
234
src/types/auth.d.ts
+2
@@ -36,9 +36,11 @@ export interface User {
36 access_token: string
37 role: UserRole
38 username: string
39 + email: string
40 }
41
42 export interface AuthUser {
43 id: number
44 username: string
45 + email: string
46 }
src/types/flow.d.ts new
+91
@@ -0,0 +1,91 @@
1 +export interface FlowResult {
2 + client_id: string
3 + session_id: string
4 + request: FlowRequest
5 + backtrace: string
6 + create_time: number
7 + start_time: number
8 + active_time: number
9 + total_uploaded_files: number
10 + total_expected_uploaded_bytes: number
11 + total_uploaded_bytes: number
12 + total_collected_rows: number
13 + total_logs: number
14 + total_requests: number
15 + outstanding_requests: number
16 + next_response_id: number
17 + execution_duration: number
18 + state: string
19 + status: string
20 + artifacts_with_results: string[]
21 + query_stats: FlowQueryStat[]
22 + uploaded_files: string[]
23 + user_notified: boolean
24 + logs: string[]
25 + dirty: boolean
26 + total_loads: number
27 +}
28 +
29 +export interface FlowQueryStat {
30 + status: string
31 + error_message: string
32 + backtrace: string
33 + duration: number
34 + last_active: number
35 + first_active: number
36 + names_with_response: string[]
37 + Artifact: string
38 + log_rows: number
39 + uploaded_files: number
40 + uploaded_bytes: number
41 + expected_uploaded_bytes: number
42 + result_rows: number
43 + query_id: number
44 + total_queries: number
45 +}
46 +
47 +export interface FlowRequest {
48 + creator: string
49 + user_data: string
50 + client_id: string
51 + flow_id: string
52 + urgent: boolean
53 + artifacts: string[]
54 + specs: FlowRequestSpecs[]
55 + cpu_limit: number
56 + iops_limit: number
57 + progress_timeout: number
58 + timeout: number
59 + max_rows: number
60 + max_upload_bytes: number
61 + trace_freq_sec: number
62 + allow_custom_overrides: boolean
63 + log_batch_time: number
64 + compiled_collector_args: string[]
65 + ops_per_second: number
66 +}
67 +
68 +export interface FlowRequestSpecs {
69 + artifactstring: string
70 + parameters: {
71 + key: string
72 + value: string
73 + comment: string
74 + }[]
75 +}
76 +
77 +export interface CollectResult {
78 + [key: string]: any
79 +}
80 +
81 +export enum IPFamily {
82 + IPv4 = "IPv4",
83 + IPv6 = "IPv6"
84 +}
85 +
86 +export type ConnectionStatus = "ESTAB" | "LISTEN" | ""
87 +
88 +export enum ConnectionType {
89 + TCP = "TCP",
90 + UDP = "UDP"
91 +}
src/types/soc/alert.d.ts
+26
@@ -98,3 +98,29 @@ export interface Status {
98 export enum StatusName {
99 Assigned = "Assigned"
100 }
101 +
102 +type DateDay = number
103 +type DateMonth = number
104 +type DateYear = number
105 +type DayFormatted = `${DateYear}-${DateMonth}-${DateDay}`
106 +
107 +export interface SocAlertCaseResponse {
108 + case_customer: number
109 + case_description: string
110 + case_id: number
111 + case_name: string
112 + case_soc_id: string
113 + case_uuid: string
114 + classification_id: number | null
115 + close_date: DayFormatted
116 + closing_note: string | null
117 + custom_attributes: { [key: string]: any } | null
118 + modification_history: { [key: string]: ModificationHistory }
119 + open_date: DayFormatted
120 + owner_id: number
121 + review_status_id: string | number | null
122 + reviewer_id: number | null
123 + state_id: number
124 + status_id: number
125 + user_id: number
126 +}
src/views/AgentOverview.vue
+11 -5
@@ -57,6 +57,11 @@
57 <VulnerabilitiesSection v-if="agent" :agent="agent" />
58 </div>
59 </n-tab-pane>
60 + <n-tab-pane name="Artifacts" tab="Artifacts" display-directive="show:lazy">
61 + <div class="section">
62 + <AgentFlowList v-if="agent" :agent="agent" />
63 + </div>
64 + </n-tab-pane>
65 <n-tab-pane name="Alerts" tab="Alerts" display-directive="show:lazy">
66 <div class="section">
67 <AlertsList v-if="agent" :agent-hostname="agent.hostname" />
@@ -103,6 +108,7 @@ import { useRouter } from "vue-router"
108 import VulnerabilitiesSection from "@/components/agents/VulnerabilitiesSection.vue"
109 import AlertsList from "@/components/alerts/AlertsList.vue"
110 import OverviewSection from "@/components/agents/OverviewSection.vue"
111 +import AgentFlowList from "@/components/agents/agentFlow/AgentFlowList.vue"
112 import { useMessage, NSpin, NTooltip, NButton, NTabs, NTabPane, NCard, useDialog } from "naive-ui"
113 import Icon from "@/components/common/Icon.vue"
114 import type { Artifact } from "@/types/artifacts.d"
@@ -143,12 +149,12 @@ function getAgent() {
149 agent.value = res.data.agents[0] || null
150 } else {
151 message.error(res.data?.message || "An error occurred. Please try again later.")
146 - router.push(`/agents`).catch(() => {})
152 + gotoAgents()
153 }
154 })
155 .catch(err => {
156 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
151 - router.push(`/agents`).catch(() => {})
157 + gotoAgents()
158 })
159 .finally(() => {
160 loadingAgent.value = false
@@ -195,7 +201,7 @@ function handleDelete() {
201 }
202
203 function gotoAgents() {
198 - router.push(`/agents`).catch(() => {})
204 + router.push({ name: "Agents" })
205 }
206
207 onBeforeMount(() => {
@@ -264,8 +270,8 @@ onBeforeMount(() => {
270 }
271 .online-badge,
272 .quarantined-badge {
267 - border: 2px solid var(--primary-color);
268 - color: var(--primary-color);
273 + border: 2px solid var(--success-color);
274 + color: var(--success-color);
275 font-weight: bold;
276 border-radius: var(--border-radius);
277 @apply text-xs py-1 px-2;
src/views/Agents.vue
+66 -3
@@ -19,7 +19,7 @@
19 <div class="agents-list flex flex-grow flex-col gap-3">
20 <template v-if="agentsFiltered.length">
21 <AgentCard
22 - v-for="agent in agentsFiltered"
22 + v-for="agent in itemsPaginated"
23 :key="agent.agent_id"
24 :agent="agent"
25 show-actions
@@ -38,6 +38,15 @@
38 </div>
39 </n-scrollbar>
40 </n-spin>
41 +
42 + <div class="pagination-wrapper">
43 + <n-pagination
44 + v-model:page="page"
45 + :page-size="pageSize"
46 + :page-slot="5"
47 + :item-count="agentsFiltered.length"
48 + />
49 + </div>
50 </div>
51 </div>
52 </div>
@@ -51,7 +60,7 @@ import AgentToolbar from "@/components/agents/AgentToolbar.vue"
60 import { isAgentOnline } from "@/components/agents/utils"
61 import Api from "@/api"
62 import { useRouter } from "vue-router"
54 -import { useMessage, NSpin, NScrollbar, NEmpty } from "naive-ui"
63 +import { useMessage, NSpin, NScrollbar, NEmpty, NPagination } from "naive-ui"
64 import _debounce from "lodash/debounce"
65
66 const message = useMessage()
@@ -60,6 +69,8 @@ const loadingAgents = ref(false)
69 const loadingSync = ref(false)
70 const agents = ref<Agent[]>([])
71 const textFilter = ref("")
72 +const page = ref(1)
73 +const pageSize = ref(20)
74
75 const textFilterDebounced = ref("")
76
@@ -81,6 +92,13 @@ const agentsFiltered = computed(() => {
92 )
93 })
94
95 +const itemsPaginated = computed(() => {
96 + const from = (page.value - 1) * pageSize.value
97 + const to = page.value * pageSize.value
98 +
99 + return agentsFiltered.value.slice(from, to)
100 +})
101 +
102 const agentsCritical = computed(() => {
103 return agents.value.filter(({ critical_asset }) => critical_asset)
104 })
@@ -90,7 +108,7 @@ const agentsOnline = computed(() => {
108 })
109
110 function gotoAgentPage(agent: Agent) {
93 - router.push(`/agent/${agent.agent_id}`).catch(() => {})
111 + router.push({ name: "Agent", params: { id: agent.agent_id } })
112 }
113
114 function getAgents() {
@@ -169,15 +187,60 @@ onBeforeMount(() => {
187 }
188
189 .main {
190 + position: relative;
191 + border-radius: var(--border-radius);
192 +
193 :deep() {
194 .n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical {
195 right: 0;
196 + bottom: 50px;
197 + }
198 + }
199 +
200 + .pagination-wrapper {
201 + --size: 10px;
202 + position: absolute;
203 + bottom: 0;
204 + right: 0;
205 + background-color: var(--bg-body);
206 + padding-left: var(--size);
207 + padding-top: var(--size);
208 + border-top-left-radius: var(--size);
209 +
210 + &::before,
211 + &::after {
212 + content: "";
213 + position: absolute;
214 + width: var(--size);
215 + height: var(--size);
216 + left: calc(var(--size) * -1);
217 + display: block;
218 + bottom: 0px;
219 + z-index: 1;
220 + background-image: radial-gradient(
221 + circle at 0 0,
222 + rgba(0, 0, 0, 0) calc(var(--size) - 1px),
223 + var(--bg-body) calc(var(--size) + 0px)
224 + );
225 + }
226 +
227 + &::after {
228 + bottom: initial;
229 + left: initial;
230 + top: calc(var(--size) * -1);
231 + right: 0;
232 }
233 }
234 }
235
236 .agents-list {
237 width: 100%;
238 +
239 + .item-appear {
240 + &:last-child {
241 + margin-bottom: 50px;
242 + }
243 + }
244 }
245 }
246 @container (max-width: 770px) {
src/views/Auth/Login.vue
+66 -46
@@ -1,57 +1,65 @@
1 <template>
2 <div class="page">
3 - <div class="settings flex items-center justify-between" v-if="!isLogged">
4 - <div class="layout">
5 - <n-button quaternary circle @click="align = 'left'">
6 - <template #icon>
7 - <Icon>
8 - <Iconify :icon="AlignLeftActive" v-if="align === 'left'" />
9 - <Iconify :icon="AlignLeft" v-else />
10 - </Icon>
11 - </template>
12 - </n-button>
13 - <n-button quaternary circle @click="align = 'center'">
14 - <template #icon>
15 - <Icon>
16 - <Iconify :icon="AlignCenterActive" v-if="align === 'center'" />
17 - <Iconify :icon="AlignCenter" v-else />
18 - </Icon>
19 - </template>
20 - </n-button>
21 - <n-button quaternary circle @click="align = 'right'">
22 - <template #icon>
23 - <Icon>
24 - <Iconify :icon="AlignRightActive" v-if="align === 'right'" />
25 - <Iconify :icon="AlignRight" v-else />
26 - </Icon>
27 - </template>
28 - </n-button>
3 + <!--
4 + <div class="settings flex items-center justify-between" v-if="!isLogged">
5 + <div class="layout">
6 + <n-button quaternary circle @click="align = 'left'">
7 + <template #icon>
8 + <Icon>
9 + <Iconify :icon="AlignLeftActive" v-if="align === 'left'" />
10 + <Iconify :icon="AlignLeft" v-else />
11 + </Icon>
12 + </template>
13 + </n-button>
14 + <n-button quaternary circle @click="align = 'center'">
15 + <template #icon>
16 + <Icon>
17 + <Iconify :icon="AlignCenterActive" v-if="align === 'center'" />
18 + <Iconify :icon="AlignCenter" v-else />
19 + </Icon>
20 + </template>
21 + </n-button>
22 + <n-button quaternary circle @click="align = 'right'">
23 + <template #icon>
24 + <Icon>
25 + <Iconify :icon="AlignRightActive" v-if="align === 'right'" />
26 + <Iconify :icon="AlignRight" v-else />
27 + </Icon>
28 + </template>
29 + </n-button>
30 + </div>
31 + <div class="colors">
32 + <n-button quaternary circle v-for="color of colors" :key="color" @click="activeColor = color">
33 + <template #icon>
34 + <Icon :color="color">
35 + <Iconify :icon="SquareActive" v-if="activeColor === color" />
36 + <Iconify :icon="Square" v-else />
37 + </Icon>
38 + </template>
39 + </n-button>
40 + <n-button quaternary circle @click="activeColor = primaryColor">
41 + <template #icon>
42 + <Icon :color="primaryColor">
43 + <Iconify :icon="SquareActive" v-if="activeColor === primaryColor" />
44 + <Iconify :icon="Square" v-else />
45 + </Icon>
46 + </template>
47 + </n-button>
48 + </div>
49 </div>
30 - <div class="colors">
31 - <n-button quaternary circle v-for="color of colors" :key="color" @click="activeColor = color">
32 - <template #icon>
33 - <Icon :color="color">
34 - <Iconify :icon="SquareActive" v-if="activeColor === color" />
35 - <Iconify :icon="Square" v-else />
36 - </Icon>
37 - </template>
38 - </n-button>
39 - <n-button quaternary circle @click="activeColor = primaryColor">
40 - <template #icon>
41 - <Icon :color="primaryColor">
42 - <Iconify :icon="SquareActive" v-if="activeColor === primaryColor" />
43 - <Iconify :icon="Square" v-else />
44 - </Icon>
45 - </template>
46 - </n-button>
47 - </div>
48 - </div>
50 + -->
51 +
52 <div class="flex wrapper justify-center" v-if="!isLogged">
53 <div class="image-box basis-2/3" v-if="align === 'right'"></div>
54 <div class="form-box basis-1/3 flex items-center justify-center" :class="{ centered: align === 'center' }">
55 <AuthForm :type="type" />
56 </div>
54 - <div class="image-box basis-2/3" v-if="align === 'left'"></div>
57 + <div class="image-box basis-2/3" v-if="align === 'left'">
58 + <video playsinline autoplay muted loop poster="/images/login/cover.webp">
59 + <source src="/images/login/video.mp4" type="video/mp4" />
60 + Your browser does not support the video tag.
61 + </video>
62 + </div>
63 </div>
64 </div>
65 </template>
@@ -123,6 +131,17 @@ onBeforeMount(() => {
131 background-color: v-bind(activeColor);
132 position: relative;
133
134 + video {
135 + position: absolute;
136 + top: 0;
137 + left: 0;
138 + width: 100%;
139 + height: 100%;
140 + object-fit: cover;
141 + object-position: center;
142 + }
143 +
144 + /*
145 &::after {
146 content: "";
147 width: 100%;
@@ -134,6 +153,7 @@ onBeforeMount(() => {
153 background-size: 500px;
154 background-position: center center;
155 }
156 + */
157 }
158
159 .form-box {
src/views/Connectors.vue
+81 -36
@@ -13,40 +13,56 @@
13 <tr>
14 <th scope="col">Connector Name</th>
15 <th scope="col">Connector Description</th>
16 - <th scope="col">Connector Supports</th>
17 - <th scope="col">Connector Configured</th>
18 - <th scope="col">Connector Verified</th>
19 - <th scope="col">Connector Options</th>
16 + <th scope="col" style="width: 190px" class="!text-center">Connector Configured</th>
17 + <th scope="col" style="width: 170px" class="!text-center">Connector Verified</th>
18 + <th scope="col" style="width: 170px" class="!text-right">Connector Options</th>
19 </tr>
20 </thead>
21 <tbody>
22 <tr v-for="connector in connectors" :key="connector.id">
23 <!-- Display the connector details in the table -->
24 <td>{{ connector.connector_name }}</td>
26 - <td>{{ connector.connector_description }}</td>
27 - <td>{{ connector.connector_supports }}</td>
28 - <td>
29 - <n-button type="primary" v-if="connector.connector_configured">True</n-button>
30 - <n-button type="info" v-else>False</n-button>
25 + <td>{{ connector.connector_description || "-" }}</td>
26 + <td style="width: 190px" class="text-center">
27 + <strong
28 + class="flag-field"
29 + :class="{
30 + success: connector.connector_configured,
31 + warning: !connector.connector_configured
32 + }"
33 + >
34 + {{ connector.connector_configured ? "Yes" : "No" }}
35 + </strong>
36 </td>
37 <!-- Show the connector verified which is in the `connector` table -->
33 - <td>
34 - <n-button type="success" v-if="connector.connector_verified">True</n-button>
35 - <n-button type="error" v-else>False</n-button>
38 + <td style="width: 170px" class="text-center">
39 + <strong v-if="connector.connector_verified" class="flag-field success">Yes</strong>
40 + <n-button
41 + type="primary"
42 + v-else
43 + @click="verify(connector)"
44 + :loading="connector.loading"
45 + >
46 + Verify
47 + </n-button>
48 </td>
37 - <td>
38 - <div class="btn-group" role="group">
49 + <td style="width: 170px">
50 + <div class="flex justify-end items-center gap-3">
51 <!--If the connector is not already configured then display the configure button -->
52 <n-button
53 type="primary"
42 - round
54 + :disabled="connector.loading"
55 v-if="!connector.connector_configured"
56 @click="openConfigDialog(connector)"
57 >
58 Configure
59 </n-button>
60
49 - <n-button type="warning" round v-else @click="openConfigDialog(connector)">
61 + <n-button
62 + v-else
63 + @click="openConfigDialog(connector)"
64 + :disabled="connector.loading"
65 + >
66 Update
67 </n-button>
68 <!--<button type="button" class="btn btn-info btn-sm" @click="deleteConnector(connector)">Delete</button>-->
@@ -78,29 +94,18 @@ import Api from "@/api"
94 import { onBeforeMount, ref } from "vue"
95 import ConfigForm from "@/components/connectors/ConfigForm"
96 import { type Connector } from "@/types/connectors.d"
81 -import { NScrollbar, NSpin, NModal, NTable, NButton, NCard } from "naive-ui"
97 +import { NScrollbar, NSpin, NModal, NTable, NButton, NCard, useMessage } from "naive-ui"
98
83 -const connectors = ref<Connector[]>([])
84 -const currentConnector = ref<Connector | null>(null)
85 -
86 -// Configure Modal
87 -const isConfigureModalActive = ref(false)
88 -const isConfigureModalFileActive = ref(false)
99 +interface ConnectorExt extends Connector {
100 + loading?: boolean
101 +}
102
90 -// Update Modal
91 -const isUpdateModalActive = ref(false)
103 +const connectors = ref<ConnectorExt[]>([])
104 +const currentConnector = ref<Connector | null>(null)
105
106 const loading = ref(false)
107 const showConfigDialog = ref(false)
95 -
96 -const successMessage = ref("")
97 -const errorMessage = ref("")
98 -const connectorForm = ref({
99 - connector_url: "",
100 - username: "",
101 - password: "",
102 - connector_api_key: ""
103 -})
108 +const message = useMessage()
109
110 function openConfigDialog(connector: Connector) {
111 currentConnector.value = connector
@@ -121,17 +126,57 @@ function getConnectors() {
126 Api.connectors
127 .getAll()
128 .then(res => {
124 - connectors.value = res.data.connectors
129 + if (res.data.success) {
130 + connectors.value = res.data.connectors
131 + } else {
132 + message.warning(res.data?.message || "An error occurred. Please try again later.")
133 + }
134 })
135 .catch(err => {
127 - console.error(err)
136 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
137 })
138 .finally(() => {
139 loading.value = false
140 })
141 }
142
143 +function verify(connector: ConnectorExt) {
144 + connector.loading = true
145 +
146 + Api.connectors
147 + .verify(connector.id)
148 + .then(res => {
149 + message.success(res.data?.message || "Connector was successfully verified.")
150 + getConnectors()
151 + })
152 + .catch(err => {
153 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
154 + })
155 + .finally(() => {
156 + connector.loading = false
157 + })
158 +}
159 +
160 onBeforeMount(() => {
161 getConnectors()
162 })
163 </script>
164 +
165 +<style scoped lang="scss">
166 +.table-box {
167 + .flag-field {
168 + &.success {
169 + color: var(--success-color);
170 + }
171 + &.warning {
172 + color: var(--warning-color);
173 + }
174 + }
175 +
176 + tr:hover {
177 + td {
178 + background-color: var(--primary-005-color);
179 + }
180 + }
181 +}
182 +</style>
src/views/Customers.vue
+28 -3
@@ -1,21 +1,46 @@
1 <template>
2 <div class="page">
3 - <CustomersList :highlight="highlight" />
3 + <CustomersList :highlight="highlight" :reload="reload" @reloaded="reload = false">
4 + <CustomerCreationButton v-model:openForm="openForm" @submitted="reload = true" />
5 + </CustomersList>
6 </div>
7 </template>
8
9 <script setup lang="ts">
10 import CustomersList from "@/components/customers/CustomersList.vue"
9 -import { onBeforeMount, ref } from "vue"
10 -import { useRoute } from "vue-router"
11 +import CustomerCreationButton from "@/components/customers/CustomerCreationButton.vue"
12 +import { onBeforeMount, onMounted, onUnmounted, ref } from "vue"
13 +import { useRoute, useRouter } from "vue-router"
14 +import { emitter } from "@/emitter"
15
16 const route = useRoute()
17 +const router = useRouter()
18
19 const highlight = ref<string | undefined>(undefined)
20 +const reload = ref(false)
21 +const openForm = ref(false)
22 +
23 +function setOpenForm() {
24 + if (!openForm.value) {
25 + openForm.value = true
26 + router.replace({ name: "Customers" })
27 + }
28 +}
29
30 onBeforeMount(() => {
31 if (route.query?.code) {
32 highlight.value = route.query.code.toString()
33 }
34 +
35 + if (route.query?.action === "add-customer") {
36 + setOpenForm()
37 + }
38 +})
39 +
40 +onMounted(() => {
41 + emitter.on("action:add-customer", setOpenForm)
42 +})
43 +onUnmounted(() => {
44 + emitter.off("action:add-customer", setOpenForm)
45 })
46 </script>
src/views/Overview.vue
+2 -2
@@ -60,11 +60,11 @@ const page = ref()
60 const cardDirection = ref<"horizontal" | "vertical">("horizontal")
61
62 function gotoIndicesPage(index: IndexStats) {
63 - router.push(`/indices?index_name=${index.index}`).catch(() => {})
63 + router.push({ name: "Indices", query: { index_name: index.index } })
64 }
65
66 function gotoPipelinesPage(rule: string) {
67 - router.push(`/graylog/pipelines?rule=${rule}`).catch(() => {})
67 + router.push({ name: "Graylog-Pipelines", query: { rule } })
68 }
69
70 useResizeObserver(page, entries => {
src/views/Profile.vue
+14 -3
@@ -21,7 +21,7 @@
21 <div class="item">
22 <n-tooltip placement="top">
23 <template #trigger>
24 - <div class="tooltip-wrap">
24 + <div class="item-wrap">
25 <Icon :name="RoleIcon"></Icon>
26 <span>{{ userRole }}</span>
27 </div>
@@ -29,9 +29,17 @@
29 <span>Role</span>
30 </n-tooltip>
31 </div>
32 + <div class="item" v-if="userEmail">
33 + <div class="item-wrap">
34 + <Icon :name="EmailIcon"></Icon>
35 + <span>{{ userEmail }}</span>
36 + </div>
37 + </div>
38 </div>
39 </div>
40 <div class="actions">
41 + <ChangePassword :username="userName" size="small" />
42 +
43 <ImageCropper
44 v-if="propicEnabled"
45 v-slot="{ openCropper }"
@@ -65,16 +73,19 @@ import ImageCropper, { type ImageCropperResult } from "@/components/common/Image
73 import ProfileSettings from "@/components/profile/ProfileSettings.vue"
74 import Icon from "@/components/common/Icon.vue"
75 import { useAuthStore } from "@/stores/auth"
76 +import ChangePassword from "@/components/users/ChangePassword.vue"
77
78 const propicEnabled = false
79
80 const RoleIcon = "tabler:user"
81 const EditIcon = "uil:image-edit"
82 +const EmailIcon = "carbon:email"
83
84 const tabActive = ref("settings")
85
86 const userRole = useAuthStore().userRoleName
87 const userName = useAuthStore().userName
88 +const userEmail = useAuthStore().userEmail
89 const userPic = ref(useAuthStore().userPic)
90
91 function setCroppedImage(result: ImageCropperResult) {
@@ -128,7 +139,7 @@ function setCroppedImage(result: ImageCropperResult) {
139 gap: 24px;
140
141 .item {
131 - .tooltip-wrap {
142 + .item-wrap {
143 display: flex;
144 align-items: center;
145
@@ -148,7 +159,7 @@ function setCroppedImage(result: ImageCropperResult) {
159 }
160 }
161 .actions {
151 - display: none;
162 + // display: none;
163 }
164 }
165 }
src/views/Users.vue new
+21
@@ -0,0 +1,21 @@
1 +<template>
2 + <div class="page">
3 + <UsersList :highlight="highlight" />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import UsersList from "@/components/users/UsersList.vue"
9 +import { onBeforeMount, ref } from "vue"
10 +import { useRoute } from "vue-router"
11 +
12 +const route = useRoute()
13 +
14 +const highlight = ref<string | undefined>(undefined)
15 +
16 +onBeforeMount(() => {
17 + if (route.query?.user_id) {
18 + highlight.value = route.query.user_id.toString()
19 + }
20 +})
21 +</script>
src/views/soc/Alerts.vue
+2 -2
@@ -1,11 +1,11 @@
1 <template>
2 <div class="page">
3 - <SocAlertsList :highlight="highlight" />
3 + <SocAlertsFullList :highlight="highlight" />
4 </div>
5 </template>
6
7 <script setup lang="ts">
8 -import SocAlertsList from "@/components/soc/SocAlertsList.vue"
8 +import SocAlertsFullList from "@/components/soc/SocAlerts/SocAlertsFullList.vue"
9 import { onBeforeMount, ref } from "vue"
10 import { useRoute } from "vue-router"
11
src/views/soc/Cases.vue
+1 -1
@@ -5,5 +5,5 @@
5 </template>
6
7 <script setup lang="ts">
8 -import SocCasesList from "@/components/soc/SocCasesList.vue"
8 +import SocCasesList from "@/components/soc/SocCases/SocCasesList.vue"
9 </script>
src/views/soc/Users.vue
+1 -1
@@ -5,7 +5,7 @@
5 </template>
6
7 <script setup lang="ts">
8 -import SocUsersList from "@/components/soc/SocUsersList.vue"
8 +import SocUsersList from "@/components/soc/SocUsers/SocUsersList.vue"
9 import { onBeforeMount, ref } from "vue"
10 import { useRoute } from "vue-router"
11