@cryptotaxi247 / CoPilot / commits / 57d58cd2

Velociraptor artifacts (#103)

* added artifacts interfaces * added artifacts api * updated alerts filters * added artifacts page * added artifacts list * added artifacts collect * added artifacts command * added artifacts quarantine * updated agent overview page * updated artifact collect item layout * return 401 for expired jwt token * update agent table for quarantine status * added scheduler role and user scheduler user is created at app startup and removed at app shutdown * agent_sync schedule to 60 minutes * make user register and refresh token async * precommit fixes * no sqllite echo * echo false * router tag * updated agents pages * updated 401 flow * JWT token to expire after 24 hours * updated agent overview page --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Nov 20, 2023 at 10:01 UTC 57d58cd29a4eb36a4a26804106b7c002b3ad7599
56 files changed +3508 -341
backend/app/agents/routes/agents.py
+1 -1
@@ -137,7 +137,7 @@ async def get_agent_by_hostname(hostname: str, db: AsyncSession = Depends(get_se
137 "/sync",
138 response_model=SyncedAgentsResponse,
139 description="Sync agents from Wazuh Manager",
140 - dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
140 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "scheduler"))],
141 )
142 async def sync_all_agents(backgroud_tasks: BackgroundTasks, session: AsyncSession = Depends(get_session)) -> SyncedAgentsResponse:
143 logger.info("Syncing agents from Wazuh Manager")
backend/app/auth/models/users.py
+47
@@ -1,7 +1,11 @@
1 import datetime
2 +import random
3 +import string
4 from enum import Enum
5 from typing import Optional
6
7 +import bcrypt
8 +from pydantic import BaseModel
9 from pydantic import EmailStr
10 from pydantic import validator
11 from sqlmodel import Field
@@ -33,6 +37,7 @@ class User(SQLModel, table=True):
37 class RoleEnum(int, Enum):
38 admin = 1
39 analyst = 2
40 + scheduler = 3
41
42
43 class UserInput(SQLModel):
@@ -75,3 +80,45 @@ class SMTPInput(SQLModel):
80 if "smtp_password" in values and v != values["smtp_password"]:
81 raise ValueError("passwords don't match")
82 return v
83 +
84 +
85 +class Password(BaseModel):
86 + length: int = Field(default=12, ge=8, le=128, description="The length of the password")
87 + hashed: str # Holds the hashed password
88 + plain: str # Holds the plain password
89 +
90 + @validator("length")
91 + def validate_length(cls, value):
92 + if value < 8 or value > 128:
93 + raise ValueError("Password length must be between 8 and 128 characters.")
94 + return value
95 +
96 + @classmethod
97 + def generate(cls, length: int = 12) -> "Password":
98 + if length < 8: # Ensure the password is a reasonable length
99 + raise ValueError("Password length should be at least 8 characters.")
100 +
101 + # Define the characters that can be used in the password
102 + lowercase = string.ascii_lowercase
103 + uppercase = string.ascii_uppercase
104 + digits = string.digits
105 + punctuation = string.punctuation
106 +
107 + # Ensure the password has at least one lowercase, one uppercase, one digit, and one symbol
108 + password_chars = [random.choice(lowercase), random.choice(uppercase), random.choice(digits), random.choice(punctuation)]
109 +
110 + # Fill the rest of the password length with a random mix of characters
111 + if length > 4:
112 + password_chars += random.choices(lowercase + uppercase + digits + punctuation, k=length - 4)
113 +
114 + # Shuffle the resulting password list to avoid predictable patterns
115 + random.shuffle(password_chars)
116 +
117 + # Convert the list of characters into a string
118 + password = "".join(password_chars)
119 +
120 + # Hash the password
121 + hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
122 +
123 + # Return the Password object with both the plain and hashed password
124 + return cls(length=length, hashed=hashed_password.decode("utf-8"), plain=password)
backend/app/auth/routes/auth.py
+7 -3
@@ -6,6 +6,8 @@ from fastapi import HTTPException
6 from fastapi import status
7 from fastapi.security import OAuth2PasswordRequestForm
8 from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 from sqlmodel import Session
12 from sqlmodel import engine
13
@@ -18,6 +20,7 @@ from app.auth.schema.auth import UserResponse
20 from app.auth.services.universal import find_user
21 from app.auth.services.universal import select_all_users
22 from app.auth.utils import AuthHandler
23 +from app.db.db_session import get_session
24 from app.db.db_session import session
25
26 ACCESS_TOKEN_EXPIRE_MINUTES = 1440
@@ -44,20 +47,21 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
47 @auth_router.get("/refresh", response_model=Token)
48 async def refresh_token(current_user: User = Depends(auth_handler.get_current_user)):
49 access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
47 - access_token = auth_handler.encode_token(current_user.username, access_token_expires)
50 + access_token = await auth_handler.encode_token(current_user.username, access_token_expires)
51 return {"access_token": access_token, "token_type": "bearer"}
52
53
54 @auth_router.post("/register", response_model=UserResponse, status_code=201, description="Register new user")
52 -async def register(user: UserInput):
55 +async def register(user: UserInput, session: AsyncSession = Depends(get_session)):
56 # users = select_all_users()
57 users = await select_all_users()
58 if any(x.username == user.username for x in users):
59 raise HTTPException(status_code=400, detail="Username is taken")
60 hashed_pwd = auth_handler.get_password_hash(user.password)
61 u = User(username=user.username, password=hashed_pwd, email=user.email, role_id=user.role_id)
62 + logger.info(f"User: {u}")
63 session.add(u)
60 - session.commit()
64 + await session.commit()
65 return {"message": "User created successfully", "success": True}
66
67
backend/app/auth/services/universal.py
+105
@@ -1,3 +1,7 @@
1 +import asyncio
2 +import random
3 +import string
4 +
5 from loguru import logger
6
7 # ! New with Async
@@ -5,10 +9,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
9 from sqlmodel import Session
10 from sqlmodel import select
11
12 +from app.auth.models.users import Password
13 from app.auth.models.users import Role
14 from app.auth.models.users import User
15 from app.db.db_session import async_engine
16
17 +passwords_in_memory = {}
18 +
19 # def select_all_users():
20 # with Session(engine) as session:
21 # statement = select(User)
@@ -54,3 +61,101 @@ async def get_role(name: str):
61 result = await session.execute(statement)
62 role = result.scalars().first()
63 return role.name
64 +
65 +
66 +async def check_admin_user_exists(session: AsyncSession) -> bool:
67 + """
68 + Check if admin user exists in the database
69 + If not, return False
70 + """
71 + statement = select(User).where(User.username == "admin")
72 + result = await session.execute(statement)
73 + user = result.scalars().first()
74 + return user is not None
75 +
76 +
77 +async def check_scheduler_user_exists(session: AsyncSession) -> bool:
78 + """
79 + Check if scheduler user exists in the database
80 + If not, return False
81 + """
82 + statement = select(User).where(User.username == "scheduler")
83 + result = await session.execute(statement)
84 + user = result.scalars().first()
85 + return user is not None
86 +
87 +
88 +async def create_admin_user(session: AsyncSession):
89 + """
90 + Check if the admin user exists in the database.
91 + If not, create the admin user.
92 + """
93 + if not await check_admin_user_exists(session): # The check function needs to be passed the session as well
94 + # Create the admin user
95 + password_model = Password.generate(length=12)
96 + admin_user = User(
97 + username="admin",
98 + password=password_model.hashed, # Assuming you store the hashed password
99 + email="admin@admin.com",
100 + role_id=1, # Make sure the role_id corresponds to the admin role in your DB
101 + )
102 + session.add(admin_user)
103 + admin_username = admin_user.username
104 + await session.commit()
105 + logger.info(f"Added new admin user with username: {admin_username}")
106 + logger.info(f"Admin user password: {password_model}")
107 + else:
108 + logger.info("Admin user already exists.")
109 + return
110 +
111 +
112 +async def create_scheduler_user(session: AsyncSession):
113 + """
114 + Check if the scheduler user exists in the database.
115 + If not, create the scheduler user.
116 + """
117 + if not await check_scheduler_user_exists(session): # The check function needs to be passed the session as well
118 + # Create the scheduler user
119 + password_model = Password.generate(length=12)
120 + scheduler_user = User(
121 + username="scheduler",
122 + password=password_model.hashed, # Assuming you store the hashed password
123 + email="scheduler@scheduler.com",
124 + role_id=3, # Make sure the role_id corresponds to the scheduler role in your DB
125 + )
126 + session.add(scheduler_user)
127 + scheduler_username = scheduler_user.username
128 + password_plain = password_model.plain
129 + await session.commit()
130 + logger.info(f"Added new scheduler user with username: {scheduler_username}")
131 + logger.info(f"Scheduler user password: {password_plain}")
132 + passwords_in_memory["scheduler"] = password_plain
133 + else:
134 + logger.info("Scheduler user already exists.")
135 + return
136 +
137 +
138 +async def remove_scheduler_user(session: AsyncSession):
139 + """
140 + Check if the scheduler user exists in the database.
141 + If so, remove the scheduler user.
142 + """
143 + # Check if the scheduler user exists
144 + statement = select(User).where(User.username == "scheduler")
145 + result = await session.execute(statement)
146 + scheduler_user = result.scalars().first()
147 +
148 + if scheduler_user:
149 + # Remove the scheduler user
150 + await session.delete(scheduler_user)
151 + await session.commit() # This is awaited because commit is async
152 + logger.info("Scheduler user removed.")
153 + else:
154 + logger.info("Scheduler user does not exist.")
155 +
156 +
157 +def get_scheduler_password():
158 + """
159 + Retrieve the scheduler user's unhashed password from memory.
160 + """
161 + return passwords_in_memory.get("scheduler")
backend/app/auth/utils.py
+31 -4
@@ -16,7 +16,7 @@ from app.auth.services.universal import get_role
16 class AuthHandler:
17 security = OAuth2PasswordBearer(
18 tokenUrl="auth/token",
19 - scopes={"admin": "Admin users", "analyst": "SOC Analysts"},
19 + scopes={"admin": "Admin users", "analyst": "SOC Analysts", "scheduler": "Scheduler for automated tasks"},
20 )
21 pwd_context = CryptContext(schemes=["bcrypt"])
22 secret = "bL4unrkoxtFs1MT6A7Ns2yMLkduyuqrkTxDV9CjlbNc="
@@ -53,7 +53,7 @@ class AuthHandler:
53 # return jwt.encode(payload, self.secret, algorithm="HS256")
54
55 # ! New with Async
56 - async def encode_token(self, username: str, access_token_expires: timedelta = timedelta(minutes=60)):
56 + async def encode_token(self, username: str, access_token_expires: timedelta = timedelta(hours=24)):
57 role = await get_role(username)
58 payload = {
59 "exp": datetime.utcnow() + access_token_expires,
@@ -68,9 +68,11 @@ class AuthHandler:
68 payload = jwt.decode(token, self.secret, algorithms=["HS256"])
69 return payload["sub"], payload.get("scopes", [])
70 except jwt.ExpiredSignatureError:
71 - raise HTTPException(status_code=401, detail="Expired signature")
71 + # raise HTTPException(status_code=401, detail="Expired signature")
72 + return "Expired signature", []
73 except jwt.InvalidTokenError:
73 - raise HTTPException(status_code=401, detail="Invalid token")
74 + # raise HTTPException(status_code=401, detail="Invalid token")
75 + return "Invalid token", []
76
77 async def get_current_user(self, security_scopes: SecurityScopes, token: str = Depends(security)):
78 if security_scopes.scopes:
@@ -86,6 +88,18 @@ class AuthHandler:
88
89 try:
90 username, token_scopes = self.decode_token(token)
91 + if username == "Expired signature":
92 + raise HTTPException(
93 + status_code=401,
94 + detail="Expired signature",
95 + headers={"WWW-Authenticate": authenticate_value},
96 + )
97 + if username == "Invalid token":
98 + raise HTTPException(
99 + status_code=401,
100 + detail="Invalid token",
101 + headers={"WWW-Authenticate": authenticate_value},
102 + )
103 except Exception as e:
104 raise HTTPException(
105 status_code=401,
@@ -133,6 +147,19 @@ class AuthHandler:
147
148 username, token_scopes = self.decode_token(token)
149
150 + if username == "Expired signature":
151 + raise HTTPException(
152 + status_code=401,
153 + detail="Expired signature",
154 + headers={"WWW-Authenticate": "Bearer"},
155 + )
156 + if username == "Invalid token":
157 + raise HTTPException(
158 + status_code=401,
159 + detail="Invalid token",
160 + headers={"WWW-Authenticate": "Bearer"},
161 + )
162 +
163 if not any(scope in token_scopes for scope in required_scopes):
164 raise HTTPException(
165 status_code=401,
backend/app/connectors/velociraptor/routes/artifacts.py
+32 -1
@@ -86,6 +86,32 @@ async def get_velociraptor_id(session: AsyncSession, hostname: str) -> str:
86 return agent.velociraptor_id
87
88
89 +async def update_agent_quarantine_status(session: AsyncSession, quarantine_body: QuarantineBody, quarantine_response: QuarantineResponse):
90 + logger.info(f"Updating agent quarantine status for hostname {quarantine_body.hostname}")
91 + result = await session.execute(select(Agents).filter(Agents.hostname == quarantine_body.hostname))
92 + agent = result.scalars().first()
93 +
94 + if not agent:
95 + raise HTTPException(status_code=404, detail=f"Agent with hostname {quarantine_body.hostname} not found")
96 +
97 + if quarantine_body.action == "quarantine":
98 + if quarantine_response.success:
99 + agent.quarantined = True
100 + else:
101 + raise HTTPException(status_code=500, detail=f"Failed to quarantine hostname {quarantine_body.hostname}")
102 + elif quarantine_body.action == "remove_quarantine":
103 + if quarantine_response.success:
104 + agent.quarantined = False
105 + else:
106 + raise HTTPException(status_code=500, detail=f"Failed to remove quarantine for hostname {quarantine_body.hostname}")
107 +
108 + await session.commit()
109 +
110 + logger.info(f"Agent quarantine status for hostname {quarantine_body.hostname} updated to {agent.quarantined}")
111 +
112 + return None
113 +
114 +
115 @velociraptor_artifacts_router.get(
116 "",
117 response_model=ArtifactsResponse,
@@ -230,4 +256,9 @@ async def quarantine(quarantine_body: QuarantineBody, session: AsyncSession = De
256 # Add the velociraptor_id to the quarantine_body object
257 quarantine_body.velociraptor_id = await get_velociraptor_id(session, quarantine_body.hostname)
258 # Quarantine the host
233 - return await quarantine_host(quarantine_body)
259 + quarantine_response = await quarantine_host(quarantine_body)
260 +
261 + # If the host was successfully quarantined, update the database
262 + await update_agent_quarantine_status(session, quarantine_body, quarantine_response)
263 +
264 + return quarantine_response
backend/app/connectors/velociraptor/services/artifacts.py
+1
@@ -10,6 +10,7 @@ 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.db.universal_models import Agents
14
15 # universal_service = UniversalService()
16
backend/app/db/db_populate.py
+13 -15
@@ -1,4 +1,6 @@
1 from loguru import logger
2 +from sqlalchemy.ext.asyncio import AsyncSession
3 +from sqlalchemy.future import select
4 from sqlmodel import Session
5
6 from app.auth.models.users import Role
@@ -144,28 +146,24 @@ def add_connectors_if_not_exist(session: Session):
146 session.commit()
147
148
147 -def add_roles_if_not_exist(session: Session):
149 +async def add_roles_if_not_exist(session: AsyncSession) -> None:
150 # List of roles to add
151 role_list = [
150 - {
151 - "name": "admin",
152 - "description": "Administrator",
153 - },
154 - {
155 - "name": "analyst",
156 - "description": "SOC Analyst",
157 - },
152 + {"name": "admin", "description": "Administrator"},
153 + {"name": "analyst", "description": "SOC Analyst"},
154 + {"name": "scheduler", "description": "Scheduler for automated tasks"},
155 ]
156
157 for role_data in role_list:
161 - # Check if role already exists in the database
162 - existing_role = session.query(Role).filter_by(name=role_data["name"]).first()
158 + logger.info(f"Checking for existence of role {role_data['name']}")
159 + query = select(Role).where(Role.name == role_data["name"])
160 + result = await session.execute(query)
161 + existing_role = result.scalars().first()
162
163 if existing_role is None:
165 - # If role does not exist, create new role entry
164 new_role = Role(**role_data)
167 - session.add(new_role)
165 + session.add(new_role) # Use session.add() to add new objects
166 logger.info(f"Added new role: {role_data['name']}")
167
170 - # Commit the changes if any new roles were added
171 - session.commit()
168 + await session.commit() # Commit the transaction
169 + logger.info("Role check and addition completed.")
backend/app/db/db_session.py
+21 -1
@@ -1,5 +1,6 @@
1 # ! Old Testing without Async
2 from contextlib import asynccontextmanager
3 +from contextlib import contextmanager
4
5 from sqlmodel import Session
6 from sqlmodel import create_engine
@@ -14,6 +15,7 @@ session = "placeholder"
15 #! New Testings with Async
16
17 from loguru import logger
18 +from sqlalchemy import create_engine
19 from sqlalchemy.ext.asyncio import AsyncSession
20 from sqlalchemy.ext.asyncio import create_async_engine
21 from sqlalchemy.orm import sessionmaker
@@ -21,10 +23,12 @@ from sqlalchemy.orm import sessionmaker
23 from settings import SQLALCHEMY_DATABASE_URI
24
25 # create async engine for SQLite using aiosqlite
24 -async_engine = create_async_engine(SQLALCHEMY_DATABASE_URI, echo=True)
26 +async_engine = create_async_engine(SQLALCHEMY_DATABASE_URI, echo=False)
27 +sync_engine = create_engine(SQLALCHEMY_DATABASE_URI.replace("+aiosqlite", ""), echo=False)
28
29 # create a configured "AsyncSession" class
30 AsyncSessionLocal = sessionmaker(bind=async_engine, class_=AsyncSession, expire_on_commit=False)
31 +SyncSessionLocal = sessionmaker(bind=sync_engine, class_=Session, expire_on_commit=False)
32
33
34 # Dependency to get DB session for each request
@@ -47,6 +51,22 @@ async def get_db_session():
51 await session.close()
52
53
54 +# Synchronous context manager to get DB session for each request
55 +@contextmanager
56 +def get_sync_db_session():
57 + session = SyncSessionLocal()
58 + logger.info("Sync DB session created")
59 + try:
60 + yield session
61 + except Exception as e:
62 + logger.error(f"Error during sync DB session: {e}")
63 + session.rollback()
64 + raise e
65 + finally:
66 + logger.info("Closing sync DB session")
67 + session.close()
68 +
69 +
70 async def get_session():
71 async with get_db_session() as session:
72 return session
backend/app/db/db_setup.py
+37
@@ -1,9 +1,15 @@
1 from loguru import logger
2 +from sqlalchemy.ext.asyncio import AsyncSession
3 from sqlalchemy.ext.asyncio import create_async_engine
4
5 # ! New with Async
6 from sqlmodel import SQLModel
7
8 +from app.auth.services.universal import create_admin_user
9 +from app.auth.services.universal import create_scheduler_user
10 +from app.auth.services.universal import remove_scheduler_user
11 +from app.db.db_populate import add_roles_if_not_exist
12 +
13 # from sqlalchemy import inspect
14 # from sqlmodel import Session
15 # from sqlmodel import SQLModel
@@ -42,3 +48,34 @@ async def create_tables(async_engine):
48 async with async_engine.begin() as conn:
49 # This will create all tables
50 await conn.run_sync(SQLModel.metadata.create_all)
51 +
52 +
53 +async def create_roles(async_engine):
54 + logger.info("Creating roles")
55 + async with AsyncSession(async_engine) as session: # Create an AsyncSession, not just a connection
56 + async with session.begin(): # Start a transaction
57 + await add_roles_if_not_exist(session)
58 +
59 +
60 +async def ensure_admin_user(async_engine):
61 + logger.info("Ensuring admin user exists")
62 + async with AsyncSession(async_engine) as session:
63 + async with session.begin():
64 + # Pass the session to the inner function
65 + await create_admin_user(session)
66 +
67 +
68 +async def ensure_scheduler_user(async_engine):
69 + logger.info("Ensuring scheduler user exists")
70 + async with AsyncSession(async_engine) as session:
71 + async with session.begin():
72 + # Pass the session to the inner function
73 + await create_scheduler_user(session)
74 +
75 +
76 +async def ensure_scheduler_user_removed(async_engine):
77 + logger.info("Ensuring scheduler user exists")
78 + async with AsyncSession(async_engine) as session:
79 + async with session.begin():
80 + # Pass the session to the inner function
81 + await remove_scheduler_user(session)
backend/app/db/universal_models.py
+1
@@ -89,6 +89,7 @@ class Agents(SQLModel, table=True):
89 wazuh_agent_version: str = Field(max_length=256)
90 velociraptor_agent_version: str = Field(max_length=256)
91 customer_code: Optional[str] = Field(foreign_key="customers.customer_code")
92 + quarantined: bool = Field(default=False)
93
94 customer: Optional[Customers] = Relationship(back_populates="agents")
95
backend/app/routers/healthcheck.py
+1 -1
@@ -6,4 +6,4 @@ from app.healthchecks.agents.routes.agents import healtcheck_agents_router
6 router = APIRouter()
7
8 # Include the Healthcheck related routes
9 -router.include_router(healtcheck_agents_router, prefix="/healthcheck", tags=["healthcheck"])
9 +router.include_router(healtcheck_agents_router, prefix="/healthcheck", tags=["healthcheck agents"])
backend/app/schedulers/scheduler.py
+22 -27
@@ -6,40 +6,35 @@ import requests
6 from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
7 from apscheduler.schedulers.asyncio import AsyncIOScheduler
8 from apscheduler.triggers.interval import IntervalTrigger
9 -
10 -from app.db.db_session import session
9 +from fastapi import BackgroundTasks
10 +from sqlalchemy.ext.asyncio import AsyncSession
11 +
12 +from app.agents.services.sync import sync_agents
13 +from app.db.db_session import SyncSessionLocal
14 +from app.db.db_session import async_engine
15 +from app.db.db_session import get_sync_db_session
16 +from app.db.db_session import sync_engine
17 from app.schedulers.models.scheduler import JobMetadata
18 +from app.schedulers.services.agent_sync import agent_sync
19 from settings import SQLALCHEMY_DATABASE_URI
20
21
15 -def scheduled_task():
16 - # Your actual task
17 - response = requests.get("http://127.0.0.1:5000/agents/sync")
18 - print(response.json())
19 -
20 - # Update the last_success in the metadata table
21 - job_metadata = session.get(JobMetadata, "scheduled_task")
22 - if job_metadata:
23 - job_metadata.last_success = datetime.utcnow()
24 - session.add(job_metadata)
25 - session.commit()
26 -
27 -
22 def init_scheduler():
23 scheduler = AsyncIOScheduler()
30 - jobstores = {"default": SQLAlchemyJobStore(url=SQLALCHEMY_DATABASE_URI)}
24 + jobstores = {"default": SQLAlchemyJobStore(engine=sync_engine)}
25 scheduler.configure(jobstores=jobstores)
32 - job = scheduler.add_job(scheduled_task, "interval", minutes=1, id="scheduled_task", replace_existing=True)
26
34 - # Initialize or update the metadata in the database
35 - job_metadata = session.get(JobMetadata, job.id)
36 - if not job_metadata:
37 - job_metadata = JobMetadata(job_id=job.id, last_success=None, time_interval=1, enabled=True)
38 - session.add(job_metadata)
39 - else:
40 - # Update existing metadata if needed
41 - job_metadata.time_interval = 1 # Update interval if it's changed
42 - job_metadata.enabled = True # Make sure the job is enabled
43 - session.commit()
27 + # Use SyncSessionLocal to create a synchronous session
28 + with SyncSessionLocal() as session:
29 + # Synchronous ORM operations
30 + job_metadata = session.query(JobMetadata).filter_by(job_id="agent_sync").one_or_none()
31 + if not job_metadata:
32 + job_metadata = JobMetadata(job_id="agent_sync", last_success=None, time_interval=60, enabled=True)
33 + session.add(job_metadata)
34 + else:
35 + job_metadata.time_interval = 1
36 + job_metadata.enabled = True
37 + session.commit()
38
39 + job = scheduler.add_job(agent_sync, "interval", minutes=60, id="agent_sync", replace_existing=True)
40 return scheduler
backend/app/schedulers/services/agent_sync.py new
+35
@@ -0,0 +1,35 @@
1 +from datetime import datetime
2 +
3 +import requests
4 +from loguru import logger
5 +
6 +from app.db.db_session import get_sync_db_session
7 +from app.schedulers.models.scheduler import JobMetadata
8 +from app.schedulers.utils.universal import scheduler_login
9 +
10 +
11 +def agent_sync():
12 + # Get the scheduler auth token
13 + headers = scheduler_login()
14 +
15 + # Check if the token was successfully retrieved
16 + if headers:
17 + # Your actual task
18 + response = requests.post("http://localhost:5000/agents/sync", headers=headers)
19 +
20 + # Process the response here if needed
21 + print(response.json())
22 + else:
23 + print("Failed to retrieve token")
24 +
25 + # Use get_sync_db_session to create and manage a synchronous session
26 + with get_sync_db_session() as session:
27 + # Synchronous ORM operations
28 + job_metadata = session.query(JobMetadata).filter_by(job_id="agent_sync").one_or_none()
29 + if job_metadata:
30 + job_metadata.last_success = datetime.utcnow()
31 + session.add(job_metadata)
32 + session.commit()
33 + else:
34 + # Handle the case where job_metadata does not exist
35 + print("JobMetadata for 'agent_sync' not found.")
backend/app/schedulers/utils/universal.py new
+25
@@ -0,0 +1,25 @@
1 +import requests
2 +
3 +from app.auth.services.universal import get_scheduler_password
4 +
5 +
6 +def scheduler_login():
7 + # Get the password
8 + password = get_scheduler_password()
9 +
10 + # Get an auth token
11 + token_response = requests.post(
12 + "http://localhost:5000/auth/token",
13 + headers={"accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"},
14 + data={"grant_type": "", "username": "scheduler", "password": password, "scope": ""},
15 + )
16 +
17 + # Check if the token was successfully retrieved
18 + if token_response.status_code == 200:
19 + token = token_response.json().get("access_token")
20 + # Use the token in the header of your subsequent requests
21 + headers = {"Authorization": f"Bearer {token}"}
22 + return headers
23 + else:
24 + print("Failed to retrieve token")
25 + return None
backend/copilot.py
+14 -9
@@ -1,8 +1,4 @@
1 -import requests
1 import uvicorn
3 -from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
4 -from apscheduler.schedulers.asyncio import AsyncIOScheduler
5 -from apscheduler.triggers.interval import IntervalTrigger
2 from dotenv import load_dotenv
3 from fastapi import FastAPI
4 from fastapi import HTTPException
@@ -12,8 +8,11 @@ from loguru import logger
8
9 from app.auth.utils import AuthHandler
10 from app.db.db_session import async_engine
15 -from app.db.db_session import engine
11 +from app.db.db_setup import create_roles
12 from app.db.db_setup import create_tables
13 +from app.db.db_setup import ensure_admin_user
14 +from app.db.db_setup import ensure_scheduler_user
15 +from app.db.db_setup import ensure_scheduler_user_removed
16 from app.middleware.exception_handlers import custom_http_exception_handler
17 from app.middleware.exception_handlers import validation_exception_handler
18 from app.middleware.logger import log_requests
@@ -83,12 +82,16 @@ app.include_router(logs.router)
82 async def init_db():
83 # create_tables(engine)
84 await create_tables(async_engine)
85 + await create_roles(async_engine)
86 + await ensure_admin_user(async_engine)
87 + await ensure_scheduler_user(async_engine)
88 +
89 # Initialize the scheduler
87 - # scheduler = init_scheduler()
90 + scheduler = init_scheduler()
91
89 - # logger.info("Starting scheduler")
90 - # if not scheduler.running:
91 - # scheduler.start()
92 + logger.info("Starting scheduler")
93 + if not scheduler.running:
94 + scheduler.start()
95
96
97 @app.get("/")
@@ -104,6 +107,8 @@ async def shutdown_scheduler():
107 if scheduler.running:
108 scheduler.shutdown()
109
110 + await ensure_scheduler_user_removed(async_engine)
111 +
112
113 if __name__ == "__main__":
114 uvicorn.run(app, host="localhost", port=5000)
package-lock.json
+195 -195
@@ -41,8 +41,8 @@
41 "@tiptap/starter-kit": "^2.1.12",
42 "@tiptap/vue-3": "^2.1.12",
43 "@vueup/vue-quill": "^1.2.0",
44 - "@vueuse/components": "^10.5.0",
45 - "@vueuse/core": "^10.5.0",
44 + "@vueuse/components": "^10.6.1",
45 + "@vueuse/core": "^10.6.1",
46 "apexcharts": "^3.44.0",
47 "bytes": "^3.1.2",
48 "chart.js": "^4.4.0",
@@ -52,9 +52,9 @@
52 "echarts": "^5.4.3",
53 "geojson": "^0.5.0",
54 "highlight.js": "^11.9.0",
55 - "jose": "^5.1.0",
55 + "jose": "^5.1.1",
56 "lodash": "^4.17.21",
57 - "maplibre-gl": "^3.5.2",
57 + "maplibre-gl": "^3.6.1",
58 "mitt": "^3.0.1",
59 "naive-ui": "^2.35.0",
60 "password-validator": "^5.3.0",
@@ -70,7 +70,7 @@
70 "vue-cal": "^4.8.1",
71 "vue-chartjs": "^5.2.0",
72 "vue-highlight-words": "^3.0.1",
73 - "vue-i18n": "^9.6.5",
73 + "vue-i18n": "^9.7.0",
74 "vue-maplibre-gl": "^3.0.3",
75 "vue-router": "^4.2.5",
76 "vue-sjv": "^0.0.6",
@@ -82,26 +82,26 @@
82 "devDependencies": {
83 "@clack/prompts": "^0.7.0",
84 "@css-render/vue3-ssr": "^0.15.12",
85 - "@faker-js/faker": "^8.2.0",
85 + "@faker-js/faker": "^8.3.1",
86 "@iconify/vue": "^4.1.1",
87 "@rushstack/eslint-patch": "^1.5.1",
88 "@tsconfig/node18": "^18.2.2",
89 "@types/bytes": "^3.1.4",
90 - "@types/fs-extra": "^11.0.3",
91 - "@types/inquirer": "^9.0.6",
90 + "@types/fs-extra": "^11.0.4",
91 + "@types/inquirer": "^9.0.7",
92 "@types/jsdom": "^21.1.5",
93 - "@types/lodash": "^4.14.200",
94 - "@types/node": "^20.8.10",
95 - "@types/validator": "^13.11.5",
96 - "@vitejs/plugin-vue": "^4.4.0",
93 + "@types/lodash": "^4.14.201",
94 + "@types/node": "^20.9.0",
95 + "@types/validator": "^13.11.6",
96 + "@vitejs/plugin-vue": "^4.4.1",
97 "@vitejs/plugin-vue-jsx": "^3.0.2",
98 "@vue-leaflet/vue-leaflet": "^0.10.1",
99 "@vue/eslint-config-prettier": "^8.0.0",
100 "@vue/eslint-config-typescript": "^12.0.0",
101 - "@vue/test-utils": "^2.4.1",
101 + "@vue/test-utils": "^2.4.2",
102 "@vue/tsconfig": "^0.4.0",
103 "autoprefixer": "^10.4.16",
104 - "cypress": "^13.4.0",
104 + "cypress": "^13.5.1",
105 "eslint": "^8.53.0",
106 "eslint-plugin-cypress": "^2.15.1",
107 "eslint-plugin-vue": "^9.18.1",
@@ -112,7 +112,7 @@
112 "npm-run-all": "^4.1.5",
113 "picocolors": "^1.0.0",
114 "postcss": "^8.4.31",
115 - "prettier": "^3.0.3",
115 + "prettier": "^3.1.0",
116 "sass": "^1.69.5",
117 "start-server-and-test": "^2.0.2",
118 "tailwind-config-viewer": "^1.7.3",
@@ -1212,9 +1212,9 @@
1212 }
1213 },
1214 "node_modules/@faker-js/faker": {
1215 - "version": "8.2.0",
1216 - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.2.0.tgz",
1217 - "integrity": "sha512-VacmzZqVxdWdf9y64lDOMZNDMM/FQdtM9IsaOPKOm2suYwEatb8VkdHqOzXcDnZbk7YDE2BmsJmy/2Hmkn563g==",
1215 + "version": "8.3.1",
1216 + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz",
1217 + "integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==",
1218 "dev": true,
1219 "funding": [
1220 {
@@ -1403,12 +1403,12 @@
1403 }
1404 },
1405 "node_modules/@intlify/core-base": {
1406 - "version": "9.6.5",
1407 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.6.5.tgz",
1408 - "integrity": "sha512-LzbGXiZkMWPIHnHI0g6q554S87Cmh2mmCmjytK/3pDQfjI84l+dgGoeQuKj02q7EbULRuUUgYVZVqAwEUawXGg==",
1406 + "version": "9.7.0",
1407 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.7.0.tgz",
1408 + "integrity": "sha512-1tBnfnCI23jXqGW15cagCjn2GgD487VST1dMG8P5LRzrSfx+kUzqFyTrjMNIwgq1tVaF4HnDpFMUuyrzTLKphw==",
1409 "dependencies": {
1410 - "@intlify/message-compiler": "9.6.5",
1411 - "@intlify/shared": "9.6.5"
1410 + "@intlify/message-compiler": "9.7.0",
1411 + "@intlify/shared": "9.7.0"
1412 },
1413 "engines": {
1414 "node": ">= 16"
@@ -1418,11 +1418,11 @@
1418 }
1419 },
1420 "node_modules/@intlify/message-compiler": {
1421 - "version": "9.6.5",
1422 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.6.5.tgz",
1423 - "integrity": "sha512-WeJ499thIj0p7JaIO1V3JaJbqdqfBykS5R8fElFs5hNeotHtPAMBs4IiA+8/KGFkAbjJusgFefCq6ajP7F7+4Q==",
1421 + "version": "9.7.0",
1422 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.7.0.tgz",
1423 + "integrity": "sha512-/YdZCio2L2tCM5bZ2eMHbSEIQNPh1QqvZIOLI/yCVKXLscis7O0SsR2nmuU/DfCJ3iSeI8juw82C2wLvfsAeww==",
1424 "dependencies": {
1425 - "@intlify/shared": "9.6.5",
1425 + "@intlify/shared": "9.7.0",
1426 "source-map-js": "^1.0.2"
1427 },
1428 "engines": {
@@ -1433,9 +1433,9 @@
1433 }
1434 },
1435 "node_modules/@intlify/shared": {
1436 - "version": "9.6.5",
1437 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.6.5.tgz",
1438 - "integrity": "sha512-gD7Ey47Xi4h/t6P+S04ymMSoA3wVRxGqjxuIMglwRO8POki9h164Epu2N8wk/GHXM/hR6ZGcsx2HArCCENjqSQ==",
1436 + "version": "9.7.0",
1437 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.7.0.tgz",
1438 + "integrity": "sha512-PUkEuk//YKu4CHS5ah3mNa3XL/+TZj6rAY/6yYN+GCNFd2u+uWUkeuwE4Q6t8dydRWlErOePHHS0KyNoof/oBw==",
1439 "engines": {
1440 "node": ">= 16"
1441 },
@@ -3243,9 +3243,9 @@
3243 "dev": true
3244 },
3245 "node_modules/@types/fs-extra": {
3246 - "version": "11.0.3",
3247 - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.3.tgz",
3248 - "integrity": "sha512-sF59BlXtUdzEAL1u0MSvuzWd7PdZvZEtnaVkzX5mjpdWTJ8brG0jUqve3jPCzSzvAKKMHTG8F8o/WMQLtleZdQ==",
3246 + "version": "11.0.4",
3247 + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
3248 + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
3249 "dev": true,
3250 "dependencies": {
3251 "@types/jsonfile": "*",
@@ -3253,14 +3253,14 @@
3253 }
3254 },
3255 "node_modules/@types/geojson": {
3256 - "version": "7946.0.12",
3257 - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.12.tgz",
3258 - "integrity": "sha512-uK2z1ZHJyC0nQRbuovXFt4mzXDwf27vQeUWNhfKGwRcWW429GOhP8HxUHlM6TLH4bzmlv/HlEjpvJh3JfmGsAA=="
3256 + "version": "7946.0.13",
3257 + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz",
3258 + "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ=="
3259 },
3260 "node_modules/@types/inquirer": {
3261 - "version": "9.0.6",
3262 - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.6.tgz",
3263 - "integrity": "sha512-1Go1AAP/yOy3Pth5Xf1DC3nfZ03cJLCPx6E2YnSN/5I3w1jHBVH4170DkZ+JxfmA7c9kL9+bf9z3FRGa4kNAqg==",
3261 + "version": "9.0.7",
3262 + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz",
3263 + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==",
3264 "dev": true,
3265 "dependencies": {
3266 "@types/through": "*",
@@ -3299,9 +3299,9 @@
3299 "integrity": "sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg=="
3300 },
3301 "node_modules/@types/lodash": {
3302 - "version": "4.14.200",
3303 - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz",
3304 - "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q=="
3302 + "version": "4.14.201",
3303 + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz",
3304 + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
3305 },
3306 "node_modules/@types/lodash-es": {
3307 "version": "4.17.9",
@@ -3312,9 +3312,9 @@
3312 }
3313 },
3314 "node_modules/@types/mapbox__point-geometry": {
3315 - "version": "0.1.3",
3316 - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.3.tgz",
3317 - "integrity": "sha512-2W46IOXlu7vC8m3+M5rDqSnuY22GFxxx3xhkoyqyPWrD+eP2iAwNst0A1+umLYjCTJMJTSpiofphn9h9k+Kw+w=="
3315 + "version": "0.1.4",
3316 + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
3317 + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
3318 },
3319 "node_modules/@types/mapbox__vector-tile": {
3320 "version": "1.3.3",
@@ -3340,9 +3340,9 @@
3340 "integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ=="
3341 },
3342 "node_modules/@types/node": {
3343 - "version": "20.8.10",
3344 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
3345 - "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
3343 + "version": "20.9.0",
3344 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
3345 + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
3346 "dev": true,
3347 "dependencies": {
3348 "undici-types": "~5.26.4"
@@ -3392,9 +3392,9 @@
3392 "dev": true
3393 },
3394 "node_modules/@types/supercluster": {
3395 - "version": "7.1.2",
3396 - "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.2.tgz",
3397 - "integrity": "sha512-qMhofL945Z4njQUuntadexAgPtpiBC014WvVqU70Prj42LC77Xgmz04us7hSMmwjs7KbgAwGBmje+FSOvDbP0Q==",
3395 + "version": "7.1.3",
3396 + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
3397 + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
3398 "dependencies": {
3399 "@types/geojson": "*"
3400 }
@@ -3425,15 +3425,15 @@
3425 "integrity": "sha512-ue/hDUpPjC85m+PM9OQDMZr3LywT+CT6mPsQq8OJtCLiERkGRcQUFvu9XASF5XWqyZFXbf15lvb3JFJ4dRLWPg=="
3426 },
3427 "node_modules/@types/validator": {
3428 - "version": "13.11.5",
3429 - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.5.tgz",
3430 - "integrity": "sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==",
3428 + "version": "13.11.6",
3429 + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz",
3430 + "integrity": "sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==",
3431 "dev": true
3432 },
3433 "node_modules/@types/web-bluetooth": {
3434 - "version": "0.0.18",
3435 - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
3436 - "integrity": "sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw=="
3434 + "version": "0.0.20",
3435 + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
3436 + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
3437 },
3438 "node_modules/@types/yauzl": {
3439 "version": "2.10.0",
@@ -3740,9 +3740,9 @@
3740 "dev": true
3741 },
3742 "node_modules/@vitejs/plugin-vue": {
3743 - "version": "4.4.0",
3744 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
3745 - "integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==",
3743 + "version": "4.4.1",
3744 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.1.tgz",
3745 + "integrity": "sha512-HCQG8VDFDM7YDAdcj5QI5DvUi+r6xvo9LgvYdk7LSkUNwdpempdB5horkMSZsbdey9Ywsf5aaU8kEPw9M5kREA==",
3746 "dev": true,
3747 "engines": {
3748 "node": "^14.18.0 || >=16.0.0"
@@ -4135,13 +4135,13 @@
4135 "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw=="
4136 },
4137 "node_modules/@vue/test-utils": {
4138 - "version": "2.4.1",
4139 - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.1.tgz",
4140 - "integrity": "sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==",
4138 + "version": "2.4.2",
4139 + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.2.tgz",
4140 + "integrity": "sha512-07lLjpG1o9tEBoWQfVOFhDT7+WFCdDeECoeSdzOuVgIi6nxb2JDLGNNOV6+3crPpyg/jMlIocj96UROcgomiGg==",
4141 "dev": true,
4142 "dependencies": {
4143 - "js-beautify": "1.14.9",
4144 - "vue-component-type-helpers": "1.8.4"
4143 + "js-beautify": "^1.14.9",
4144 + "vue-component-type-helpers": "^1.8.21"
4145 },
4146 "peerDependencies": {
4147 "@vue/server-renderer": "^3.0.1",
@@ -4172,12 +4172,12 @@
4172 }
4173 },
4174 "node_modules/@vueuse/components": {
4175 - "version": "10.5.0",
4176 - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.5.0.tgz",
4177 - "integrity": "sha512-zWQZ8zkNBvX++VHfyiUaQ4otb+4PWI8679GR8FvdrNnj+01LXnqvrkyKd8yTCMJ9nHqwRRTJikS5fu4Zspn9DQ==",
4175 + "version": "10.6.1",
4176 + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.6.1.tgz",
4177 + "integrity": "sha512-Yx7h201xJG3V4+rY1wRAYy8EI9Q1r+gpwCJzgyZ0CWPyDWyZCxPXNjPhBJsXcSzJ1h1ph9tE5cVqEXHtEs6bjg==",
4178 "dependencies": {
4179 - "@vueuse/core": "10.5.0",
4180 - "@vueuse/shared": "10.5.0",
4179 + "@vueuse/core": "10.6.1",
4180 + "@vueuse/shared": "10.6.1",
4181 "vue-demi": ">=0.14.6"
4182 }
4183 },
@@ -4207,13 +4207,13 @@
4207 }
4208 },
4209 "node_modules/@vueuse/core": {
4210 - "version": "10.5.0",
4211 - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.5.0.tgz",
4212 - "integrity": "sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==",
4210 + "version": "10.6.1",
4211 + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.6.1.tgz",
4212 + "integrity": "sha512-Pc26IJbqgC9VG1u6VY/xrXXfxD33hnvxBnKrLlA2LJlyHII+BSrRoTPJgGYq7qZOu61itITFUnm6QbacwZ4H8Q==",
4213 "dependencies": {
4214 - "@types/web-bluetooth": "^0.0.18",
4215 - "@vueuse/metadata": "10.5.0",
4216 - "@vueuse/shared": "10.5.0",
4214 + "@types/web-bluetooth": "^0.0.20",
4215 + "@vueuse/metadata": "10.6.1",
4216 + "@vueuse/shared": "10.6.1",
4217 "vue-demi": ">=0.14.6"
4218 },
4219 "funding": {
@@ -4246,17 +4246,17 @@
4246 }
4247 },
4248 "node_modules/@vueuse/metadata": {
4249 - "version": "10.5.0",
4250 - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.5.0.tgz",
4251 - "integrity": "sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==",
4249 + "version": "10.6.1",
4250 + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.6.1.tgz",
4251 + "integrity": "sha512-qhdwPI65Bgcj23e5lpGfQsxcy0bMjCAsUGoXkJ7DsoeDUdasbZ2DBa4dinFCOER3lF4gwUv+UD2AlA11zdzMFw==",
4252 "funding": {
4253 "url": "https://github.com/sponsors/antfu"
4254 }
4255 },
4256 "node_modules/@vueuse/shared": {
4257 - "version": "10.5.0",
4258 - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.5.0.tgz",
4259 - "integrity": "sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==",
4257 + "version": "10.6.1",
4258 + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.6.1.tgz",
4259 + "integrity": "sha512-TECVDTIedFlL0NUfHWncf3zF9Gc4VfdxfQc8JFwoVZQmxpONhLxFrlm0eHQeidHj4rdTPL3KXJa0TZCk1wnc5Q==",
4260 "dependencies": {
4261 "vue-demi": ">=0.14.6"
4262 },
@@ -5731,9 +5731,9 @@
5731 "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
5732 },
5733 "node_modules/cypress": {
5734 - "version": "13.4.0",
5735 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.4.0.tgz",
5736 - "integrity": "sha512-KeWNC9xSHG/ewZURVbaQsBQg2mOKw4XhjJZFKjWbEjgZCdxpPXLpJnfq5Jns1Gvnjp6AlnIfpZfWFlDgVKXdWQ==",
5734 + "version": "13.5.1",
5735 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.1.tgz",
5736 + "integrity": "sha512-yqLViT0D/lPI8Kkm7ciF/x/DCK/H/DnogdGyiTnQgX4OVR2aM30PtK+kvklTOD1u3TuItiD9wUQAF8EYWtyZug==",
5737 "dev": true,
5738 "hasInstallScript": true,
5739 "dependencies": {
@@ -8899,9 +8899,9 @@
8899 }
8900 },
8901 "node_modules/jose": {
8902 - "version": "5.1.0",
8903 - "resolved": "https://registry.npmjs.org/jose/-/jose-5.1.0.tgz",
8904 - "integrity": "sha512-H+RVqxA6apaJ0rcQYupKYhos7uosAiF42gUcWZiwhICWMphDULFj/CRr1R0tV/JCv9DEeJaSyYYpc9luHHNT4g==",
8902 + "version": "5.1.1",
8903 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.1.1.tgz",
8904 + "integrity": "sha512-bfB+lNxowY49LfrBO0ITUn93JbUhxUN8I11K6oI5hJu/G6PO6fEUddVLjqdD0cQ9SXIHWXuWh7eJYwZF7Z0N/g==",
8905 "funding": {
8906 "url": "https://github.com/sponsors/panva"
8907 }
@@ -9773,9 +9773,9 @@
9773 "dev": true
9774 },
9775 "node_modules/maplibre-gl": {
9776 - "version": "3.5.2",
9777 - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.5.2.tgz",
9778 - "integrity": "sha512-deqYA/RiEyXMGroZMDbOWNQTLnFsxREC+mDkQnuyCUNdBWm1KHafsXJYZP7rlLa5RLQNq05IAUAizY9aHTpIUw==",
9776 + "version": "3.6.1",
9777 + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.6.1.tgz",
9778 + "integrity": "sha512-XQpLkNTD6WYJXqF7vTxgHbAyShoZMm5o8fohXCn9PC/S/g3zBk92m7GUsN6KfuECh2rO01uiYbSNCSURkOODyQ==",
9779 "dependencies": {
9780 "@mapbox/geojson-rewind": "^0.5.2",
9781 "@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -9785,11 +9785,11 @@
9785 "@mapbox/vector-tile": "^1.3.1",
9786 "@mapbox/whoots-js": "^3.1.0",
9787 "@maplibre/maplibre-gl-style-spec": "^19.3.3",
9788 - "@types/geojson": "^7946.0.12",
9789 - "@types/mapbox__point-geometry": "^0.1.3",
9788 + "@types/geojson": "^7946.0.13",
9789 + "@types/mapbox__point-geometry": "^0.1.4",
9790 "@types/mapbox__vector-tile": "^1.3.3",
9791 "@types/pbf": "^3.0.4",
9792 - "@types/supercluster": "^7.1.2",
9792 + "@types/supercluster": "^7.1.3",
9793 "earcut": "^2.2.4",
9794 "geojson-vt": "^3.2.1",
9795 "gl-matrix": "^3.4.3",
@@ -12054,9 +12054,9 @@
12054 }
12055 },
12056 "node_modules/prettier": {
12057 - "version": "3.0.3",
12058 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
12059 - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
12057 + "version": "3.1.0",
12058 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
12059 + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
12060 "dev": true,
12061 "bin": {
12062 "prettier": "bin/prettier.cjs"
@@ -15711,9 +15711,9 @@
15711 }
15712 },
15713 "node_modules/vue-component-type-helpers": {
15714 - "version": "1.8.4",
15715 - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.4.tgz",
15716 - "integrity": "sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==",
15714 + "version": "1.8.22",
15715 + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.22.tgz",
15716 + "integrity": "sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw==",
15717 "dev": true
15718 },
15719 "node_modules/vue-eslint-parser": {
@@ -15785,12 +15785,12 @@
15785 }
15786 },
15787 "node_modules/vue-i18n": {
15788 - "version": "9.6.5",
15789 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.6.5.tgz",
15790 - "integrity": "sha512-dpUEjKHg7pEsaS7ZPPxp1CflaR7bGmsvZJEhnszHPKl9OTNyno5j/DvMtMSo41kpddq4felLA7GK2prjpnXVlw==",
15788 + "version": "9.7.0",
15789 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.7.0.tgz",
15790 + "integrity": "sha512-8Z8kSz9U2juzuAf+6mjW1HTd5pIlYuFJZkC+HvYOglFdpzwc2rTUGjxKwN8xGdtGur1MFnyJ44TSr+TksJtY8A==",
15791 "dependencies": {
15792 - "@intlify/core-base": "9.6.5",
15793 - "@intlify/shared": "9.6.5",
15792 + "@intlify/core-base": "9.7.0",
15793 + "@intlify/shared": "9.7.0",
15794 "@vue/devtools-api": "^6.5.0"
15795 },
15796 "engines": {
@@ -17090,9 +17090,9 @@
17090 "dev": true
17091 },
17092 "@faker-js/faker": {
17093 - "version": "8.2.0",
17094 - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.2.0.tgz",
17095 - "integrity": "sha512-VacmzZqVxdWdf9y64lDOMZNDMM/FQdtM9IsaOPKOm2suYwEatb8VkdHqOzXcDnZbk7YDE2BmsJmy/2Hmkn563g==",
17093 + "version": "8.3.1",
17094 + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz",
17095 + "integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==",
17096 "dev": true
17097 },
17098 "@fawmi/vue-google-maps": {
@@ -17243,27 +17243,27 @@
17243 }
17244 },
17245 "@intlify/core-base": {
17246 - "version": "9.6.5",
17247 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.6.5.tgz",
17248 - "integrity": "sha512-LzbGXiZkMWPIHnHI0g6q554S87Cmh2mmCmjytK/3pDQfjI84l+dgGoeQuKj02q7EbULRuUUgYVZVqAwEUawXGg==",
17246 + "version": "9.7.0",
17247 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.7.0.tgz",
17248 + "integrity": "sha512-1tBnfnCI23jXqGW15cagCjn2GgD487VST1dMG8P5LRzrSfx+kUzqFyTrjMNIwgq1tVaF4HnDpFMUuyrzTLKphw==",
17249 "requires": {
17250 - "@intlify/message-compiler": "9.6.5",
17251 - "@intlify/shared": "9.6.5"
17250 + "@intlify/message-compiler": "9.7.0",
17251 + "@intlify/shared": "9.7.0"
17252 }
17253 },
17254 "@intlify/message-compiler": {
17255 - "version": "9.6.5",
17256 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.6.5.tgz",
17257 - "integrity": "sha512-WeJ499thIj0p7JaIO1V3JaJbqdqfBykS5R8fElFs5hNeotHtPAMBs4IiA+8/KGFkAbjJusgFefCq6ajP7F7+4Q==",
17255 + "version": "9.7.0",
17256 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.7.0.tgz",
17257 + "integrity": "sha512-/YdZCio2L2tCM5bZ2eMHbSEIQNPh1QqvZIOLI/yCVKXLscis7O0SsR2nmuU/DfCJ3iSeI8juw82C2wLvfsAeww==",
17258 "requires": {
17259 - "@intlify/shared": "9.6.5",
17259 + "@intlify/shared": "9.7.0",
17260 "source-map-js": "^1.0.2"
17261 }
17262 },
17263 "@intlify/shared": {
17264 - "version": "9.6.5",
17265 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.6.5.tgz",
17266 - "integrity": "sha512-gD7Ey47Xi4h/t6P+S04ymMSoA3wVRxGqjxuIMglwRO8POki9h164Epu2N8wk/GHXM/hR6ZGcsx2HArCCENjqSQ=="
17264 + "version": "9.7.0",
17265 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.7.0.tgz",
17266 + "integrity": "sha512-PUkEuk//YKu4CHS5ah3mNa3XL/+TZj6rAY/6yYN+GCNFd2u+uWUkeuwE4Q6t8dydRWlErOePHHS0KyNoof/oBw=="
17267 },
17268 "@isaacs/cliui": {
17269 "version": "8.0.2",
@@ -18559,9 +18559,9 @@
18559 "dev": true
18560 },
18561 "@types/fs-extra": {
18562 - "version": "11.0.3",
18563 - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.3.tgz",
18564 - "integrity": "sha512-sF59BlXtUdzEAL1u0MSvuzWd7PdZvZEtnaVkzX5mjpdWTJ8brG0jUqve3jPCzSzvAKKMHTG8F8o/WMQLtleZdQ==",
18562 + "version": "11.0.4",
18563 + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
18564 + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
18565 "dev": true,
18566 "requires": {
18567 "@types/jsonfile": "*",
@@ -18569,14 +18569,14 @@
18569 }
18570 },
18571 "@types/geojson": {
18572 - "version": "7946.0.12",
18573 - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.12.tgz",
18574 - "integrity": "sha512-uK2z1ZHJyC0nQRbuovXFt4mzXDwf27vQeUWNhfKGwRcWW429GOhP8HxUHlM6TLH4bzmlv/HlEjpvJh3JfmGsAA=="
18572 + "version": "7946.0.13",
18573 + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz",
18574 + "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ=="
18575 },
18576 "@types/inquirer": {
18577 - "version": "9.0.6",
18578 - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.6.tgz",
18579 - "integrity": "sha512-1Go1AAP/yOy3Pth5Xf1DC3nfZ03cJLCPx6E2YnSN/5I3w1jHBVH4170DkZ+JxfmA7c9kL9+bf9z3FRGa4kNAqg==",
18577 + "version": "9.0.7",
18578 + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz",
18579 + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==",
18580 "dev": true,
18581 "requires": {
18582 "@types/through": "*",
@@ -18615,9 +18615,9 @@
18615 "integrity": "sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg=="
18616 },
18617 "@types/lodash": {
18618 - "version": "4.14.200",
18619 - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz",
18620 - "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q=="
18618 + "version": "4.14.201",
18619 + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz",
18620 + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ=="
18621 },
18622 "@types/lodash-es": {
18623 "version": "4.17.9",
@@ -18628,9 +18628,9 @@
18628 }
18629 },
18630 "@types/mapbox__point-geometry": {
18631 - "version": "0.1.3",
18632 - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.3.tgz",
18633 - "integrity": "sha512-2W46IOXlu7vC8m3+M5rDqSnuY22GFxxx3xhkoyqyPWrD+eP2iAwNst0A1+umLYjCTJMJTSpiofphn9h9k+Kw+w=="
18631 + "version": "0.1.4",
18632 + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
18633 + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
18634 },
18635 "@types/mapbox__vector-tile": {
18636 "version": "1.3.3",
@@ -18656,9 +18656,9 @@
18656 "integrity": "sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ=="
18657 },
18658 "@types/node": {
18659 - "version": "20.8.10",
18660 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
18661 - "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
18659 + "version": "20.9.0",
18660 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
18661 + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
18662 "dev": true,
18663 "requires": {
18664 "undici-types": "~5.26.4"
@@ -18708,9 +18708,9 @@
18708 "dev": true
18709 },
18710 "@types/supercluster": {
18711 - "version": "7.1.2",
18712 - "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.2.tgz",
18713 - "integrity": "sha512-qMhofL945Z4njQUuntadexAgPtpiBC014WvVqU70Prj42LC77Xgmz04us7hSMmwjs7KbgAwGBmje+FSOvDbP0Q==",
18711 + "version": "7.1.3",
18712 + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
18713 + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
18714 "requires": {
18715 "@types/geojson": "*"
18716 }
@@ -18741,15 +18741,15 @@
18741 "integrity": "sha512-ue/hDUpPjC85m+PM9OQDMZr3LywT+CT6mPsQq8OJtCLiERkGRcQUFvu9XASF5XWqyZFXbf15lvb3JFJ4dRLWPg=="
18742 },
18743 "@types/validator": {
18744 - "version": "13.11.5",
18745 - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.5.tgz",
18746 - "integrity": "sha512-xW4qsT4UIYILu+7ZrBnfQdBYniZrMLYYK3wN9M/NdeIHgBN5pZI2/8Q7UfdWIcr5RLJv/OGENsx91JIpUUoC7Q==",
18744 + "version": "13.11.6",
18745 + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz",
18746 + "integrity": "sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==",
18747 "dev": true
18748 },
18749 "@types/web-bluetooth": {
18750 - "version": "0.0.18",
18751 - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
18752 - "integrity": "sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw=="
18750 + "version": "0.0.20",
18751 + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
18752 + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
18753 },
18754 "@types/yauzl": {
18755 "version": "2.10.0",
@@ -18946,9 +18946,9 @@
18946 "dev": true
18947 },
18948 "@vitejs/plugin-vue": {
18949 - "version": "4.4.0",
18950 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
18951 - "integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==",
18949 + "version": "4.4.1",
18950 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.1.tgz",
18951 + "integrity": "sha512-HCQG8VDFDM7YDAdcj5QI5DvUi+r6xvo9LgvYdk7LSkUNwdpempdB5horkMSZsbdey9Ywsf5aaU8kEPw9M5kREA==",
18952 "dev": true,
18953 "requires": {}
18954 },
@@ -19261,13 +19261,13 @@
19261 "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw=="
19262 },
19263 "@vue/test-utils": {
19264 - "version": "2.4.1",
19265 - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.1.tgz",
19266 - "integrity": "sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==",
19264 + "version": "2.4.2",
19265 + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.2.tgz",
19266 + "integrity": "sha512-07lLjpG1o9tEBoWQfVOFhDT7+WFCdDeECoeSdzOuVgIi6nxb2JDLGNNOV6+3crPpyg/jMlIocj96UROcgomiGg==",
19267 "dev": true,
19268 "requires": {
19269 - "js-beautify": "1.14.9",
19270 - "vue-component-type-helpers": "1.8.4"
19269 + "js-beautify": "^1.14.9",
19270 + "vue-component-type-helpers": "^1.8.21"
19271 }
19272 },
19273 "@vue/tsconfig": {
@@ -19286,12 +19286,12 @@
19286 }
19287 },
19288 "@vueuse/components": {
19289 - "version": "10.5.0",
19290 - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.5.0.tgz",
19291 - "integrity": "sha512-zWQZ8zkNBvX++VHfyiUaQ4otb+4PWI8679GR8FvdrNnj+01LXnqvrkyKd8yTCMJ9nHqwRRTJikS5fu4Zspn9DQ==",
19289 + "version": "10.6.1",
19290 + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.6.1.tgz",
19291 + "integrity": "sha512-Yx7h201xJG3V4+rY1wRAYy8EI9Q1r+gpwCJzgyZ0CWPyDWyZCxPXNjPhBJsXcSzJ1h1ph9tE5cVqEXHtEs6bjg==",
19292 "requires": {
19293 - "@vueuse/core": "10.5.0",
19294 - "@vueuse/shared": "10.5.0",
19293 + "@vueuse/core": "10.6.1",
19294 + "@vueuse/shared": "10.6.1",
19295 "vue-demi": ">=0.14.6"
19296 },
19297 "dependencies": {
@@ -19304,13 +19304,13 @@
19304 }
19305 },
19306 "@vueuse/core": {
19307 - "version": "10.5.0",
19308 - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.5.0.tgz",
19309 - "integrity": "sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==",
19307 + "version": "10.6.1",
19308 + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.6.1.tgz",
19309 + "integrity": "sha512-Pc26IJbqgC9VG1u6VY/xrXXfxD33hnvxBnKrLlA2LJlyHII+BSrRoTPJgGYq7qZOu61itITFUnm6QbacwZ4H8Q==",
19310 "requires": {
19311 - "@types/web-bluetooth": "^0.0.18",
19312 - "@vueuse/metadata": "10.5.0",
19313 - "@vueuse/shared": "10.5.0",
19311 + "@types/web-bluetooth": "^0.0.20",
19312 + "@vueuse/metadata": "10.6.1",
19313 + "@vueuse/shared": "10.6.1",
19314 "vue-demi": ">=0.14.6"
19315 },
19316 "dependencies": {
@@ -19323,14 +19323,14 @@
19323 }
19324 },
19325 "@vueuse/metadata": {
19326 - "version": "10.5.0",
19327 - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.5.0.tgz",
19328 - "integrity": "sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw=="
19326 + "version": "10.6.1",
19327 + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.6.1.tgz",
19328 + "integrity": "sha512-qhdwPI65Bgcj23e5lpGfQsxcy0bMjCAsUGoXkJ7DsoeDUdasbZ2DBa4dinFCOER3lF4gwUv+UD2AlA11zdzMFw=="
19329 },
19330 "@vueuse/shared": {
19331 - "version": "10.5.0",
19332 - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.5.0.tgz",
19333 - "integrity": "sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==",
19331 + "version": "10.6.1",
19332 + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.6.1.tgz",
19333 + "integrity": "sha512-TECVDTIedFlL0NUfHWncf3zF9Gc4VfdxfQc8JFwoVZQmxpONhLxFrlm0eHQeidHj4rdTPL3KXJa0TZCk1wnc5Q==",
19334 "requires": {
19335 "vue-demi": ">=0.14.6"
19336 },
@@ -20390,9 +20390,9 @@
20390 "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="
20391 },
20392 "cypress": {
20393 - "version": "13.4.0",
20394 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.4.0.tgz",
20395 - "integrity": "sha512-KeWNC9xSHG/ewZURVbaQsBQg2mOKw4XhjJZFKjWbEjgZCdxpPXLpJnfq5Jns1Gvnjp6AlnIfpZfWFlDgVKXdWQ==",
20393 + "version": "13.5.1",
20394 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.1.tgz",
20395 + "integrity": "sha512-yqLViT0D/lPI8Kkm7ciF/x/DCK/H/DnogdGyiTnQgX4OVR2aM30PtK+kvklTOD1u3TuItiD9wUQAF8EYWtyZug==",
20396 "dev": true,
20397 "requires": {
20398 "@cypress/request": "^3.0.0",
@@ -22696,9 +22696,9 @@
22696 }
22697 },
22698 "jose": {
22699 - "version": "5.1.0",
22700 - "resolved": "https://registry.npmjs.org/jose/-/jose-5.1.0.tgz",
22701 - "integrity": "sha512-H+RVqxA6apaJ0rcQYupKYhos7uosAiF42gUcWZiwhICWMphDULFj/CRr1R0tV/JCv9DEeJaSyYYpc9luHHNT4g=="
22699 + "version": "5.1.1",
22700 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.1.1.tgz",
22701 + "integrity": "sha512-bfB+lNxowY49LfrBO0ITUn93JbUhxUN8I11K6oI5hJu/G6PO6fEUddVLjqdD0cQ9SXIHWXuWh7eJYwZF7Z0N/g=="
22702 },
22703 "js-beautify": {
22704 "version": "1.14.9",
@@ -23385,9 +23385,9 @@
23385 "dev": true
23386 },
23387 "maplibre-gl": {
23388 - "version": "3.5.2",
23389 - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.5.2.tgz",
23390 - "integrity": "sha512-deqYA/RiEyXMGroZMDbOWNQTLnFsxREC+mDkQnuyCUNdBWm1KHafsXJYZP7rlLa5RLQNq05IAUAizY9aHTpIUw==",
23388 + "version": "3.6.1",
23389 + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.6.1.tgz",
23390 + "integrity": "sha512-XQpLkNTD6WYJXqF7vTxgHbAyShoZMm5o8fohXCn9PC/S/g3zBk92m7GUsN6KfuECh2rO01uiYbSNCSURkOODyQ==",
23391 "requires": {
23392 "@mapbox/geojson-rewind": "^0.5.2",
23393 "@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -23397,11 +23397,11 @@
23397 "@mapbox/vector-tile": "^1.3.1",
23398 "@mapbox/whoots-js": "^3.1.0",
23399 "@maplibre/maplibre-gl-style-spec": "^19.3.3",
23400 - "@types/geojson": "^7946.0.12",
23401 - "@types/mapbox__point-geometry": "^0.1.3",
23400 + "@types/geojson": "^7946.0.13",
23401 + "@types/mapbox__point-geometry": "^0.1.4",
23402 "@types/mapbox__vector-tile": "^1.3.3",
23403 "@types/pbf": "^3.0.4",
23404 - "@types/supercluster": "^7.1.2",
23404 + "@types/supercluster": "^7.1.3",
23405 "earcut": "^2.2.4",
23406 "geojson-vt": "^3.2.1",
23407 "gl-matrix": "^3.4.3",
@@ -24979,9 +24979,9 @@
24979 "dev": true
24980 },
24981 "prettier": {
24982 - "version": "3.0.3",
24983 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
24984 - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
24982 + "version": "3.1.0",
24983 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
24984 + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
24985 "dev": true
24986 },
24987 "prettier-linter-helpers": {
@@ -27664,9 +27664,9 @@
27664 "requires": {}
27665 },
27666 "vue-component-type-helpers": {
27667 - "version": "1.8.4",
27668 - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.4.tgz",
27669 - "integrity": "sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==",
27667 + "version": "1.8.22",
27668 + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.22.tgz",
27669 + "integrity": "sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw==",
27670 "dev": true
27671 },
27672 "vue-eslint-parser": {
@@ -27719,12 +27719,12 @@
27719 }
27720 },
27721 "vue-i18n": {
27722 - "version": "9.6.5",
27723 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.6.5.tgz",
27724 - "integrity": "sha512-dpUEjKHg7pEsaS7ZPPxp1CflaR7bGmsvZJEhnszHPKl9OTNyno5j/DvMtMSo41kpddq4felLA7GK2prjpnXVlw==",
27722 + "version": "9.7.0",
27723 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.7.0.tgz",
27724 + "integrity": "sha512-8Z8kSz9U2juzuAf+6mjW1HTd5pIlYuFJZkC+HvYOglFdpzwc2rTUGjxKwN8xGdtGur1MFnyJ44TSr+TksJtY8A==",
27725 "requires": {
27726 - "@intlify/core-base": "9.6.5",
27727 - "@intlify/shared": "9.6.5",
27726 + "@intlify/core-base": "9.7.0",
27727 + "@intlify/shared": "9.7.0",
27728 "@vue/devtools-api": "^6.5.0"
27729 }
27730 },
package.json
+15 -15
@@ -61,8 +61,8 @@
61 "@tiptap/starter-kit": "^2.1.12",
62 "@tiptap/vue-3": "^2.1.12",
63 "@vueup/vue-quill": "^1.2.0",
64 - "@vueuse/components": "^10.5.0",
65 - "@vueuse/core": "^10.5.0",
64 + "@vueuse/components": "^10.6.1",
65 + "@vueuse/core": "^10.6.1",
66 "apexcharts": "^3.44.0",
67 "bytes": "^3.1.2",
68 "chart.js": "^4.4.0",
@@ -72,9 +72,9 @@
72 "echarts": "^5.4.3",
73 "geojson": "^0.5.0",
74 "highlight.js": "^11.9.0",
75 - "jose": "^5.1.0",
75 + "jose": "^5.1.1",
76 "lodash": "^4.17.21",
77 - "maplibre-gl": "^3.5.2",
77 + "maplibre-gl": "^3.6.1",
78 "mitt": "^3.0.1",
79 "naive-ui": "^2.35.0",
80 "password-validator": "^5.3.0",
@@ -90,7 +90,7 @@
90 "vue-cal": "^4.8.1",
91 "vue-chartjs": "^5.2.0",
92 "vue-highlight-words": "^3.0.1",
93 - "vue-i18n": "^9.6.5",
93 + "vue-i18n": "^9.7.0",
94 "vue-maplibre-gl": "^3.0.3",
95 "vue-router": "^4.2.5",
96 "vue-sjv": "^0.0.6",
@@ -102,26 +102,26 @@
102 "devDependencies": {
103 "@clack/prompts": "^0.7.0",
104 "@css-render/vue3-ssr": "^0.15.12",
105 - "@faker-js/faker": "^8.2.0",
105 + "@faker-js/faker": "^8.3.1",
106 "@iconify/vue": "^4.1.1",
107 "@rushstack/eslint-patch": "^1.5.1",
108 "@tsconfig/node18": "^18.2.2",
109 "@types/bytes": "^3.1.4",
110 - "@types/fs-extra": "^11.0.3",
111 - "@types/inquirer": "^9.0.6",
110 + "@types/fs-extra": "^11.0.4",
111 + "@types/inquirer": "^9.0.7",
112 "@types/jsdom": "^21.1.5",
113 - "@types/lodash": "^4.14.200",
114 - "@types/node": "^20.8.10",
115 - "@types/validator": "^13.11.5",
116 - "@vitejs/plugin-vue": "^4.4.0",
113 + "@types/lodash": "^4.14.201",
114 + "@types/node": "^20.9.0",
115 + "@types/validator": "^13.11.6",
116 + "@vitejs/plugin-vue": "^4.4.1",
117 "@vitejs/plugin-vue-jsx": "^3.0.2",
118 "@vue-leaflet/vue-leaflet": "^0.10.1",
119 "@vue/eslint-config-prettier": "^8.0.0",
120 "@vue/eslint-config-typescript": "^12.0.0",
121 - "@vue/test-utils": "^2.4.1",
121 + "@vue/test-utils": "^2.4.2",
122 "@vue/tsconfig": "^0.4.0",
123 "autoprefixer": "^10.4.16",
124 - "cypress": "^13.4.0",
124 + "cypress": "^13.5.1",
125 "eslint": "^8.53.0",
126 "eslint-plugin-cypress": "^2.15.1",
127 "eslint-plugin-vue": "^9.18.1",
@@ -132,7 +132,7 @@
132 "npm-run-all": "^4.1.5",
133 "picocolors": "^1.0.0",
134 "postcss": "^8.4.31",
135 - "prettier": "^3.0.3",
135 + "prettier": "^3.1.0",
136 "sass": "^1.69.5",
137 "start-server-and-test": "^2.0.2",
138 "tailwind-config-viewer": "^1.7.3",
src/api/artifacts.ts new
+52
@@ -0,0 +1,52 @@
1 +import { HttpClient } from "./httpClient"
2 +import type { FlaskBaseResponse } from "@/types/flask.d"
3 +import type { Artifact, CollectResult, CommandResult, QuarantineResult } from "@/types/artifacts.d"
4 +
5 +export interface ArtifactsQuery {
6 + os?: "windows" | "linux" | "macos"
7 + hostname?: string
8 +}
9 +
10 +export interface CollectRequest {
11 + hostname: string
12 + velociraptor_id?: string
13 + artifact_name: string
14 +}
15 +
16 +export interface CommandRequest {
17 + hostname: string
18 + velociraptor_id?: string
19 + command: string
20 + artifact_name: "Windows.System.PowerShell" | "Windows.System.CmdShell" | "Linux.Sys.BashShell"
21 +}
22 +
23 +export interface QuarantineRequest {
24 + hostname: string
25 + velociraptor_id?: string
26 + action: "quarantine" | "remove_quarantine"
27 + artifact_name: "Windows.Remediation.Quarantine" | "Linux.Remediation.Quarantine"
28 +}
29 +
30 +export default {
31 + getAll(filters?: ArtifactsQuery) {
32 + let url = "/artifacts"
33 +
34 + if (filters?.os) {
35 + url = "/artifacts/" + filters.os
36 + }
37 + if (filters?.hostname) {
38 + url = "/artifacts/hostname/" + filters.hostname
39 + }
40 +
41 + return HttpClient.get<FlaskBaseResponse & { artifacts: Artifact[] }>(url)
42 + },
43 + collect(payload: CollectRequest) {
44 + return HttpClient.post<FlaskBaseResponse & { results: CollectResult[] }>(`/artifacts/collect`, payload)
45 + },
46 + command(payload: CommandRequest) {
47 + return HttpClient.post<FlaskBaseResponse & { results: CommandResult[] }>(`/artifacts/command`, payload)
48 + },
49 + quarantine(payload: QuarantineRequest) {
50 + return HttpClient.post<FlaskBaseResponse & { results: QuarantineResult[] }>(`/artifacts/quarantine`, payload)
51 + }
52 +}
src/api/httpClient.ts
+6 -1
@@ -1,7 +1,7 @@
1 import { useAuthStore } from "@/stores/auth"
2 import { isDebounceTimeOver, isJwtExpiring } from "@/utils/auth"
3 import axios from "axios"
4 -import { useGlobalActions } from "@/composables/useGlobalActions"
4 +// import { useGlobalActions } from "@/composables/useGlobalActions"
5
6 const BASE_URL = import.meta.env.VITE_API_URL
7
@@ -37,7 +37,12 @@ HttpClient.interceptors.response.use(
37 response => response,
38 error => {
39 if (error.response && error.response.status === 401) {
40 + if (window.location.pathname.indexOf("login") === -1) {
41 + window.location.href = "/logout"
42 + }
43 + /*
44 useGlobalActions().message("You are not authorized to access the resource", { type: "error" })
45 + */
46 }
47
48 return Promise.reject(error)
src/api/index.ts
+2
@@ -3,6 +3,7 @@ import indices from "./indices"
3 import agents from "./agents"
4 import graylog from "./graylog"
5 import alerts from "./alerts"
6 +import artifacts from "./artifacts"
7 import auth from "./auth"
8
9 export default {
@@ -11,5 +12,6 @@ export default {
12 agents,
13 graylog,
14 alerts,
15 + artifacts,
16 auth
17 }
src/components/agents/AgentCard.vue
+16
@@ -27,6 +27,14 @@
27 </template>
28 </n-tooltip>
29 </div>
30 + <div class="quarantined" v-show="agent.quarantined">
31 + <n-tooltip>
32 + Quarantined
33 + <template #trigger>
34 + <Icon :name="QuarantinedIcon" :size="18"></Icon>
35 + </template>
36 + </n-tooltip>
37 + </div>
38 </div>
39 <div class="info">#{{ agent.agent_id }} / {{ agent.label }}</div>
40 </div>
@@ -65,6 +73,7 @@ import { NTooltip, NButton, NSpin, NCard, useMessage, useDialog } from "naive-ui
73 import Icon from "@/components/common/Icon.vue"
74 import { useSettingsStore } from "@/stores/settings"
75
76 +const QuarantinedIcon = "ph:seal-warning-light"
77 const StarIcon = "carbon:star"
78 const DeleteIcon = "ph:trash"
79
@@ -136,6 +145,7 @@ function toggleCritical(agentId: string, criticalStatus: boolean) {
145 box-sizing: border-box;
146 cursor: pointer;
147 transition: all 0.3s;
148 + border: var(--border-small-050);
149
150 .wrapper {
151 display: flex;
@@ -173,6 +183,12 @@ function toggleCritical(agentId: string, criticalStatus: boolean) {
183 border-color: var(--primary-color);
184 }
185 }
186 +
187 + .quarantined {
188 + display: flex;
189 + padding-top: 1px;
190 + color: var(--warning-color);
191 + }
192 }
193 .info {
194 font-family: var(--font-family-mono);
src/components/agents/OverviewSection.vue
+26 -5
@@ -1,10 +1,10 @@
1 <template>
2 <div class="overview-section">
3 <div class="property-group">
4 - <n-card v-for="item of propsSanitized" :key="item.key">
5 - <template #action>{{ item.key }}</template>
6 - <div class="font-bold">{{ item.val }}</div>
7 - </n-card>
4 + <div v-for="item of propsSanitized" :key="item.key" class="property">
5 + <div class="key">{{ item.key }}</div>
6 + <div class="value">{{ item.val }}</div>
7 + </div>
8 </div>
9 </div>
10 </template>
@@ -13,7 +13,6 @@
13 import { computed, toRefs } from "vue"
14 import dayjs from "@/utils/dayjs"
15 import { type Agent } from "@/types/agents.d"
16 -import { NCard } from "naive-ui"
16 import { useSettingsStore } from "@/stores/settings"
17
18 const props = defineProps<{
@@ -56,6 +55,28 @@ const formatDate = (date: string) => {
55 @apply gap-2;
56 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
57 grid-auto-flow: row dense;
58 +
59 + .property {
60 + border: var(--border-small-100);
61 + background-color: var(--bg-secondary-color);
62 + border-radius: var(--border-radius);
63 + overflow: hidden;
64 + flex-basis: 140px;
65 + flex-grow: 1;
66 +
67 + .key {
68 + border-bottom: var(--border-small-050);
69 + padding: 8px 12px;
70 + font-size: 12px;
71 + }
72 + .value {
73 + font-size: 14px;
74 + padding: 8px 12px;
75 + background-color: var(--bg-color);
76 + font-family: var(--font-family-mono);
77 + height: 100%;
78 + }
79 + }
80 }
81
82 @container (max-width: 500px) {
src/components/agents/VulnerabilityCard.vue
+30 -9
@@ -44,16 +44,15 @@
44 <n-modal
45 preset="card"
46 class="vulnerability-dialog"
47 + :title="vulnerability.title"
48 v-model:show="showDialog"
49 style="width: 90vw; max-width: 1000px"
50 >
51 <div class="vulnerability-property-group" v-if="vulnerabilitySanitized">
51 - <n-card v-for="item of vulnerabilitySanitized" :key="item.label">
52 - <div class="font-bold">{{ item.value ?? "-" }}</div>
53 - <template #action>
54 - {{ item.label }}
55 - </template>
56 - </n-card>
52 + <div v-for="item of vulnerabilitySanitized" :key="item.label" class="property">
53 + <div class="key">{{ item.label }}</div>
54 + <div class="value">{{ item.value ?? "-" }}</div>
55 + </div>
56 </div>
57 <div class="vulnerability-references">
58 <div class="title">External references</div>
@@ -71,7 +70,7 @@ import { computed, ref, toRefs } from "vue"
70 import { type AgentVulnerabilities } from "@/types/agents.d"
71 import dayjs from "@/utils/dayjs"
72 import { cloneDeep } from "lodash"
74 -import { NModal, NTooltip, NCard } from "naive-ui"
73 +import { NModal, NTooltip } from "naive-ui"
74 import Icon from "@/components/common/Icon.vue"
75 import { useSettingsStore } from "@/stores/settings"
76
@@ -220,13 +219,35 @@ const showDialog = ref(false)
219 width: 100%;
220 display: grid;
221 box-sizing: border-box;
223 - @apply py-2 px-4 gap-5;
222 + @apply gap-5;
223 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
224 grid-auto-flow: row dense;
225 +
226 + .property {
227 + border: var(--border-small-100);
228 + background-color: var(--bg-secondary-color);
229 + border-radius: var(--border-radius);
230 + overflow: hidden;
231 + flex-basis: 140px;
232 + flex-grow: 1;
233 +
234 + .key {
235 + border-bottom: var(--border-small-050);
236 + padding: 8px 12px;
237 + font-size: 12px;
238 + }
239 + .value {
240 + font-size: 14px;
241 + padding: 8px 12px;
242 + background-color: var(--bg-color);
243 + font-family: var(--font-family-mono);
244 + height: 100%;
245 + }
246 + }
247 }
248
249 .vulnerability-references {
229 - @apply py-2 px-4 gap-5 pt-4;
250 + @apply gap-5 mt-4;
251 overflow: hidden;
252
253 .list {
src/components/alerts/AlertsFilters.vue
+4 -1
@@ -23,7 +23,10 @@
23 </n-button>
24 </template>
25 <template #empty>
26 - <n-empty description="Empty Field history" class="text-center"></n-empty>
26 + <n-empty
27 + description="Empty Field history"
28 + class="text-center justify-center h-48"
29 + ></n-empty>
30 </template>
31 </n-select>
32 <n-input v-model:value="filters.alertValue" clearable placeholder="Field value" class="basis-1/2" />
src/components/alerts/AlertsList.vue
+39 -17
@@ -52,7 +52,7 @@
52 />
53 </template>
54 <template v-else>
55 - <n-empty description="No items found" v-if="!loading" />
55 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
56 </template>
57 </div>
58 </n-spin>
@@ -79,26 +79,49 @@
79 >
80 <n-drawer-content title="Alerts filters" closable :native-scrollbar="false">
81 <AlertsFilters :filters="filters" @search="startSearch(true)">
82 - <n-form-item label="Agent" v-if="!isFilterPreselected">
82 + <div v-if="!isFilterPreselected" class="mb-6">
83 <n-select
84 + v-model:value="filterType"
85 + @update:value="
86 + () => {
87 + filters.agentHostname = undefined
88 + filters.indexName = undefined
89 + }
90 + "
91 + :options="[
92 + {
93 + label: 'Filter by Agent Hostname',
94 + value: 'agentHostname'
95 + },
96 + {
97 + label: 'Filter by Index name',
98 + value: 'indexName'
99 + }
100 + ]"
101 + placeholder="Filter by Agent or Index"
102 + clearable
103 + />
104 + <n-select
105 + v-if="filterType === 'agentHostname'"
106 v-model:value="filters.agentHostname"
107 :options="agentHostnameOptions"
108 placeholder="Agents list"
109 clearable
110 filterable
111 :loading="loadingAgents"
112 + class="mt-2"
113 />
91 - </n-form-item>
92 - <n-form-item label="Index" v-if="!isFilterPreselected">
114 <n-select
115 + v-if="filterType === 'indexName'"
116 v-model:value="filters.indexName"
117 :options="indexNameOptions"
118 clearable
119 filterable
120 placeholder="Indices list"
121 :loading="loadingIndex"
122 + class="mt-2"
123 />
101 - </n-form-item>
124 + </div>
125 </AlertsFilters>
126 </n-drawer-content>
127 </n-drawer>
@@ -107,7 +130,7 @@
130
131 <script setup lang="ts">
132 import { ref, onBeforeMount, toRefs, computed, nextTick, onMounted } from "vue"
110 -import { useMessage, NSpin, NPopover, NButton, NEmpty, NDrawer, NDrawerContent, NFormItem, NSelect } from "naive-ui"
133 +import { useMessage, NSpin, NPopover, NButton, NEmpty, NDrawer, NDrawerContent, NSelect } from "naive-ui"
134 import Api from "@/api"
135 import AlertsStats, { type AlertsStatsCTX } from "./AlertsStats.vue"
136 import AlertsFilters from "./AlertsFilters.vue"
@@ -153,6 +176,8 @@ const totalAlerts = computed<number>(() => {
176
177 const filters = ref<AlertsSummaryQuery>({})
178
179 +const filterType = ref<string | null>(null)
180 +
181 const isFilterPreselected = computed(() => {
182 return !!agentHostname?.value || !!indexName?.value
183 })
@@ -301,11 +326,15 @@ onBeforeMount(() => {
326 filters.value.indexName = indexName.value
327 }
328
304 - getIndices()
305 - getAgents()
329 + nextTick(() => {
330 + if (!isFilterPreselected.value) {
331 + getAgents()
332 + getIndices()
333 + }
334
307 - // alertsSummaryList.value = alerts_summary as AlertsSummary[]
308 - startSearch()
335 + // alertsSummaryList.value = alerts_summary as AlertsSummary[]
336 + startSearch()
337 + })
338 })
339
340 onMounted(() => {
@@ -323,13 +352,6 @@ onBeforeUnmount(() => {
352
353 <style lang="scss" scoped>
354 .alerts-list {
326 - :deep() {
327 - .n-spin-body {
328 - top: 100px;
329 - text-align: center;
330 - width: 80%;
331 - }
332 - }
355 .list {
356 container-type: inline-size;
357 min-height: 200px;
src/components/alerts/AlertsStats.vue
+15 -3
@@ -15,7 +15,11 @@
15 />
16 </template>
17 <template v-else>
18 - <n-empty description="No items found" v-if="!loadingCountByHost" />
18 + <n-empty
19 + description="No items found"
20 + class="justify-center h-48"
21 + v-if="!loadingCountByHost"
22 + />
23 </template>
24 </div>
25 </n-spin>
@@ -34,7 +38,11 @@
38 />
39 </template>
40 <template v-else>
37 - <n-empty description="No items found" v-if="!loadingCountByRule" />
41 + <n-empty
42 + description="No items found"
43 + class="justify-center h-48"
44 + v-if="!loadingCountByRule"
45 + />
46 </template>
47 </div>
48 </n-spin>
@@ -53,7 +61,11 @@
61 />
62 </template>
63 <template v-else>
56 - <n-empty description="No items found" v-if="!loadingCountByRuleHost" />
64 + <n-empty
65 + description="No items found"
66 + class="justify-center h-48"
67 + v-if="!loadingCountByRuleHost"
68 + />
69 </template>
70 </div>
71 </n-spin>
src/components/artifacts/ArtifactItem.vue new
+22
@@ -0,0 +1,22 @@
1 +<template>
2 + <div class="artifact-item flex flex-col gap-1 px-5 py-3">
3 + <div class="name text-secondary-color">
4 + {{ artifact.name }}
5 + </div>
6 + <div class="description">{{ artifact.description }}</div>
7 + </div>
8 +</template>
9 +
10 +<script setup lang="ts">
11 +import type { Artifact } from "@/types/artifacts.d"
12 +
13 +const { artifact } = defineProps<{ artifact: Artifact }>()
14 +</script>
15 +
16 +<style lang="scss" scoped>
17 +.artifact-item {
18 + border-radius: var(--border-radius);
19 + background-color: var(--bg-color);
20 + border: var(--border-small-050);
21 +}
22 +</style>
src/components/artifacts/ArtifactsCollect.vue new
+293
@@ -0,0 +1,293 @@
1 +<template>
2 + <div class="artifacts-collect">
3 + <div class="header flex justify-end items-start gap-2">
4 + <div class="info flex gap-5">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total :
18 + <code>{{ total }}</code>
19 + </div>
20 + </div>
21 + </n-popover>
22 + </div>
23 + <div class="grow flex items-center justify-end gap-2 flex-wrap">
24 + <div class="grow basis-56">
25 + <n-select
26 + v-if="!isFilterPreselected"
27 + v-model:value="filters.hostname"
28 + :options="agentHostnameOptions"
29 + placeholder="Agent hostname"
30 + clearable
31 + filterable
32 + :disabled="loading"
33 + size="small"
34 + :loading="loadingAgents"
35 + />
36 + </div>
37 + <div class="grow basis-56">
38 + <n-select
39 + v-model:value="filters.artifact_name"
40 + :options="artifactsOptions"
41 + placeholder="Artifact name"
42 + clearable
43 + :disabled="loading"
44 + filterable
45 + size="small"
46 + :loading="loadingArtifacts"
47 + />
48 + </div>
49 + <div class="grow basis-56">
50 + <n-input
51 + v-model:value="filters.velociraptor_id"
52 + placeholder="Velociraptor id"
53 + clearable
54 + :readonly="loading"
55 + size="small"
56 + />
57 + </div>
58 + <div>
59 + <n-button
60 + size="small"
61 + @click="getData()"
62 + type="primary"
63 + secondary
64 + :loading="loading"
65 + :disabled="!areFiltersValid"
66 + >
67 + Submit
68 + </n-button>
69 + </div>
70 + </div>
71 + </div>
72 + <n-spin :show="loading">
73 + <div class="list grid gap-3 my-7">
74 + <template v-if="collectList.length">
75 + <CollectItem v-for="collect of collectList" :key="collect.___id" :collect="collect" />
76 + </template>
77 + <template v-else>
78 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
79 + </template>
80 + </div>
81 + </n-spin>
82 + </div>
83 +</template>
84 +
85 +<script setup lang="ts">
86 +import { ref, onBeforeMount, toRefs, computed, nextTick } from "vue"
87 +import { useMessage, NSpin, NPopover, NButton, NEmpty, NSelect, NInput } from "naive-ui"
88 +import Api from "@/api"
89 +import Icon from "@/components/common/Icon.vue"
90 +import CollectItem from "./CollectItem.vue"
91 +import type { Agent } from "@/types/agents.d"
92 +import type { CollectRequest } from "@/api/artifacts"
93 +import type { Artifact, CollectResult } from "@/types/artifacts.d"
94 +import { nanoid } from "nanoid"
95 +// import { collectResult } from "./mock"
96 +
97 +interface CollectResultExt extends CollectResult {
98 + ___id?: string
99 +}
100 +
101 +const emit = defineEmits<{
102 + (e: "loaded-agents", value: Agent[]): void
103 + (e: "loaded-artifacts", value: Artifact[]): void
104 +}>()
105 +
106 +const props = defineProps<{ agentHostname?: string; agents?: Agent[]; artifacts?: Artifact[] }>()
107 +const { agentHostname, agents, artifacts } = toRefs(props)
108 +
109 +const message = useMessage()
110 +const loadingAgents = ref(false)
111 +const loadingArtifacts = ref(false)
112 +const loading = ref(false)
113 +const agentsList = ref<Agent[]>([])
114 +const artifactsList = ref<Artifact[]>([])
115 +const collectList = ref<CollectResultExt[]>([])
116 +
117 +const InfoIcon = "carbon:information"
118 +
119 +const total = computed<number>(() => {
120 + return collectList.value.length || 0
121 +})
122 +
123 +const filters = ref<Partial<CollectRequest>>({})
124 +
125 +const isFilterPreselected = computed(() => {
126 + return !!agentHostname?.value
127 +})
128 +
129 +const areFiltersValid = computed(() => {
130 + return !!filters.value.artifact_name && !!filters.value.hostname
131 +})
132 +
133 +const agentHostnameOptions = computed(() => {
134 + if (agentHostname?.value) {
135 + return [{ value: agentHostname.value, label: agentHostname.value }]
136 + }
137 + return (agentsList.value || []).map(o => ({ value: o.hostname, label: o.hostname }))
138 +})
139 +
140 +const artifactsOptions = computed(() => {
141 + return (artifactsList.value || []).map(o => ({ value: o.name, label: o.name }))
142 +})
143 +
144 +function getData() {
145 + if (areFiltersValid.value) {
146 + loading.value = true
147 +
148 + Api.artifacts
149 + .collect(filters.value as CollectRequest)
150 + .then(res => {
151 + if (res.data.success) {
152 + collectList.value = (res.data?.results || []).map(o => {
153 + o.___id = nanoid()
154 + return o
155 + })
156 + } else {
157 + message.warning(res.data?.message || "An error occurred. Please try again later.")
158 + }
159 + })
160 + .catch(err => {
161 + collectList.value = []
162 +
163 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
164 + })
165 + .finally(() => {
166 + loading.value = false
167 + })
168 + }
169 +}
170 +
171 +function getAgents(cb?: (agents: Agent[]) => void) {
172 + loadingAgents.value = true
173 +
174 + Api.agents
175 + .getAgents()
176 + .then(res => {
177 + if (res.data.success) {
178 + agentsList.value = res.data.agents || []
179 +
180 + if (cb && typeof cb === "function") {
181 + cb(agentsList.value)
182 + }
183 + } else {
184 + message.error(res.data?.message || "An error occurred. Please try again later.")
185 + }
186 + })
187 + .catch(err => {
188 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
189 + })
190 + .finally(() => {
191 + loadingAgents.value = false
192 + })
193 +}
194 +
195 +function getArtifacts(cb?: (artifacts: Artifact[]) => void) {
196 + loadingArtifacts.value = true
197 +
198 + Api.artifacts
199 + .getAll()
200 + .then(res => {
201 + if (res.data.success) {
202 + artifactsList.value = res.data.artifacts || []
203 +
204 + if (cb && typeof cb === "function") {
205 + cb(artifactsList.value)
206 + }
207 + } else {
208 + message.error(res.data?.message || "An error occurred. Please try again later.")
209 + }
210 + })
211 + .catch(err => {
212 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
213 + })
214 + .finally(() => {
215 + loadingArtifacts.value = false
216 + })
217 +}
218 +
219 +onBeforeMount(() => {
220 + if (agentHostname?.value) {
221 + filters.value.hostname = agentHostname.value
222 + }
223 +
224 + if (agents?.value?.length && !agentsList.value.length) {
225 + agentsList.value = agents.value
226 + }
227 +
228 + if (artifacts?.value?.length && !artifactsList.value.length) {
229 + artifactsList.value = artifacts.value
230 + }
231 +
232 + nextTick(() => {
233 + if (!agentsList.value.length && !agentHostname?.value) {
234 + getAgents((agents: Agent[]) => {
235 + emit("loaded-agents", agents)
236 + })
237 + }
238 + if (!artifactsList.value.length) {
239 + getArtifacts((artifacts: Artifact[]) => {
240 + emit("loaded-artifacts", artifacts)
241 + })
242 + }
243 + })
244 +
245 + // MOCK
246 + /*
247 + collectList.value = collectResult.map(o => {
248 + // @ts-ignore
249 + o.___id = nanoid()
250 + return o
251 + }) as CollectResultExt[]
252 + */
253 +})
254 +</script>
255 +
256 +<style lang="scss" scoped>
257 +.artifacts-collect {
258 + .list {
259 + container-type: inline-size;
260 + min-height: 200px;
261 + grid-template-columns: repeat(auto-fit, minmax(390px, 1fr));
262 + grid-auto-flow: row dense;
263 +
264 + .collect-item {
265 + animation: artifacts-collect-fade 0.3s forwards;
266 + opacity: 0;
267 +
268 + @for $i from 0 through 30 {
269 + &:nth-child(#{$i}) {
270 + animation-delay: $i * 0.05s;
271 + }
272 + }
273 +
274 + @keyframes artifacts-collect-fade {
275 + from {
276 + opacity: 0;
277 + transform: translateY(10px);
278 + }
279 + to {
280 + opacity: 1;
281 + }
282 + }
283 + }
284 + }
285 +
286 + @media (max-width: 490px) {
287 + .list {
288 + display: flex;
289 + flex-direction: column;
290 + }
291 + }
292 +}
293 +</style>
src/components/artifacts/ArtifactsCommand.vue new
+339
@@ -0,0 +1,339 @@
1 +<template>
2 + <div class="artifacts-command">
3 + <div class="header flex items-start gap-2">
4 + <div class="flex flex-col gap-2 w-full">
5 + <div class="grow flex items-center gap-2 flex-wrap">
6 + <div class="grow basis-56" v-if="!isFilterPreselected">
7 + <n-select
8 + v-model:value="filters.hostname"
9 + :options="agentHostnameOptions"
10 + placeholder="Agent hostname"
11 + clearable
12 + filterable
13 + size="small"
14 + :disabled="loading"
15 + :loading="loadingAgents"
16 + />
17 + </div>
18 + <div class="grow basis-56">
19 + <n-select
20 + v-model:value="filters.artifact_name"
21 + :options="artifactsOptions"
22 + placeholder="Artifact name"
23 + clearable
24 + filterable
25 + size="small"
26 + :disabled="loading"
27 + :loading="loadingArtifacts"
28 + />
29 + </div>
30 + <div class="grow basis-56">
31 + <n-input
32 + v-model:value="filters.velociraptor_id"
33 + placeholder="Velociraptor id"
34 + :readonly="loading"
35 + clearable
36 + size="small"
37 + />
38 + </div>
39 + </div>
40 + <div class="grow flex items-center gap-2 flex-wrap">
41 + <n-input
42 + v-model:value="filters.command"
43 + placeholder="Command"
44 + clearable
45 + :readonly="loading"
46 + type="textarea"
47 + :autosize="{
48 + minRows: 3,
49 + maxRows: 10
50 + }"
51 + />
52 + </div>
53 + <div class="grow flex items-center justify-end gap-2 flex-wrap-reverse">
54 + <div class="badges-box flex gap-2 flex-wrap grow">
55 + <div class="badge" v-if="commandTime">
56 + <span class="flex flex-col justify-center">
57 + <n-tooltip trigger="hover">
58 + <template #trigger>
59 + <Icon :name="TimeIcon"></Icon>
60 + </template>
61 + Last request time / last response time
62 + </n-tooltip>
63 + </span>
64 + <span class="flex">
65 + {{ formatDate(commandTime) }}
66 +
67 + <n-spin :size="12" v-if="loading" class="ml-2" />
68 +
69 + {{ responseTime ? " / " + formatDate(responseTime) : "" }}
70 + </span>
71 + </div>
72 + <div class="badge" v-if="diffTime">
73 + <span class="flex flex-col justify-center">
74 + <Icon :name="StopWatchIcon" :size="15"></Icon>
75 + </span>
76 + <span>{{ diffTime }}</span>
77 + </div>
78 + </div>
79 + <n-button
80 + size="small"
81 + @click="getData()"
82 + type="primary"
83 + secondary
84 + :loading="loading"
85 + :disabled="!areFiltersValid"
86 + >
87 + Submit
88 + </n-button>
89 + </div>
90 + </div>
91 + </div>
92 + <n-spin :show="loading">
93 + <div class="list flex flex-col gap-3 my-7">
94 + <template v-if="commandList.length">
95 + <CommandItem v-for="command of commandList" :key="command.Stdout" :command="command" />
96 + </template>
97 + <template v-else>
98 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
99 + </template>
100 + </div>
101 + </n-spin>
102 + </div>
103 +</template>
104 +
105 +<script setup lang="ts">
106 +import { ref, onBeforeMount, toRefs, computed, nextTick } from "vue"
107 +import { useMessage, NSpin, NButton, NEmpty, NSelect, NInput, NTooltip } from "naive-ui"
108 +import Api from "@/api"
109 +import CommandItem from "./CommandItem.vue"
110 +import type { Agent } from "@/types/agents.d"
111 +import type { CommandRequest } from "@/api/artifacts"
112 +import type { Artifact, CommandResult } from "@/types/artifacts.d"
113 +import dayjs from "@/utils/dayjs"
114 +import Icon from "@/components/common/Icon.vue"
115 +import { useSettingsStore } from "@/stores/settings"
116 +// import { commandResult } from "./mock"
117 +
118 +const emit = defineEmits<{
119 + (e: "loaded-agents", value: Agent[]): void
120 + (e: "loaded-artifacts", value: Artifact[]): void
121 +}>()
122 +
123 +const props = defineProps<{ agentHostname?: string; agents?: Agent[]; artifacts?: Artifact[] }>()
124 +const { agentHostname, agents, artifacts } = toRefs(props)
125 +
126 +const TimeIcon = "carbon:time"
127 +const StopWatchIcon = "quill:stopwatch"
128 +
129 +const message = useMessage()
130 +const loadingAgents = ref(false)
131 +const loadingArtifacts = ref(false)
132 +const loading = ref(false)
133 +const agentsList = ref<Agent[]>([])
134 +const artifactsList = ref<Artifact[]>([])
135 +const commandList = ref<CommandResult[]>([])
136 +const commandTime = ref<Date | null>(null)
137 +const responseTime = ref<Date | null>(null)
138 +const dFormats = useSettingsStore().dateFormat
139 +
140 +const diffTime = computed(() => {
141 + if (commandTime.value && responseTime.value) {
142 + return dayjs.duration(dayjs(responseTime.value).diff(commandTime.value, "ms", true)).asSeconds() + "s"
143 + } else {
144 + return null
145 + }
146 +})
147 +
148 +const filters = ref<Partial<CommandRequest>>({})
149 +
150 +const isFilterPreselected = computed(() => {
151 + return !!agentHostname?.value
152 +})
153 +
154 +const areFiltersValid = computed(() => {
155 + return !!filters.value.artifact_name && !!filters.value.hostname && !!filters.value.command
156 +})
157 +
158 +const agentHostnameOptions = computed(() => {
159 + if (agentHostname?.value) {
160 + return [{ value: agentHostname.value, label: agentHostname.value }]
161 + }
162 + return (agentsList.value || []).map(o => ({ value: o.hostname, label: o.hostname }))
163 +})
164 +
165 +const artifactsOptions = computed(() => {
166 + return (artifactsList.value || []).map(o => ({ value: o.name, label: o.name }))
167 +})
168 +
169 +function formatDate(timestamp: string | Date): string {
170 + return dayjs(timestamp).format(dFormats.timesec)
171 +}
172 +
173 +function getData() {
174 + if (areFiltersValid.value) {
175 + loading.value = true
176 + commandList.value = []
177 + commandTime.value = new Date()
178 + responseTime.value = null
179 +
180 + Api.artifacts
181 + .command(filters.value as CommandRequest)
182 + .then(res => {
183 + if (res.data.success) {
184 + commandList.value = res.data?.results || []
185 + } else {
186 + message.warning(res.data?.message || "An error occurred. Please try again later.")
187 + }
188 + })
189 + .catch(err => {
190 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
191 + })
192 + .finally(() => {
193 + responseTime.value = new Date()
194 + loading.value = false
195 + })
196 + }
197 +}
198 +
199 +function getAgents(cb?: (agents: Agent[]) => void) {
200 + loadingAgents.value = true
201 +
202 + Api.agents
203 + .getAgents()
204 + .then(res => {
205 + if (res.data.success) {
206 + agentsList.value = res.data.agents || []
207 +
208 + if (cb && typeof cb === "function") {
209 + cb(agentsList.value)
210 + }
211 + } else {
212 + message.error(res.data?.message || "An error occurred. Please try again later.")
213 + }
214 + })
215 + .catch(err => {
216 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
217 + })
218 + .finally(() => {
219 + loadingAgents.value = false
220 + })
221 +}
222 +
223 +function getArtifacts(cb?: (artifacts: Artifact[]) => void) {
224 + loadingArtifacts.value = true
225 +
226 + Api.artifacts
227 + .getAll()
228 + .then(res => {
229 + if (res.data.success) {
230 + artifactsList.value = res.data.artifacts || []
231 +
232 + if (cb && typeof cb === "function") {
233 + cb(artifactsList.value)
234 + }
235 + } else {
236 + message.error(res.data?.message || "An error occurred. Please try again later.")
237 + }
238 + })
239 + .catch(err => {
240 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
241 + })
242 + .finally(() => {
243 + loadingArtifacts.value = false
244 + })
245 +}
246 +
247 +onBeforeMount(() => {
248 + artifactsList.value = ["Windows.System.PowerShell", "Windows.System.CmdShell", "Linux.Sys.BashShell"].map(
249 + o => ({ name: o }) as Artifact
250 + )
251 +
252 + if (agentHostname?.value) {
253 + filters.value.hostname = agentHostname.value
254 + }
255 +
256 + if (agents?.value?.length && !agentsList.value.length) {
257 + agentsList.value = agents.value
258 + }
259 +
260 + if (artifacts?.value?.length && !artifactsList.value.length) {
261 + artifactsList.value = artifacts.value
262 + }
263 +
264 + nextTick(() => {
265 + if (!agentsList.value.length && !agentHostname?.value) {
266 + getAgents((agents: Agent[]) => {
267 + emit("loaded-agents", agents)
268 + })
269 + }
270 + if (!artifactsList.value.length) {
271 + getArtifacts((artifacts: Artifact[]) => {
272 + emit("loaded-artifacts", artifacts)
273 + })
274 + }
275 + })
276 +
277 + // MOCK
278 + // commandList.value = commandResult as CommandResult[]
279 +})
280 +</script>
281 +
282 +<style lang="scss" scoped>
283 +.artifacts-command {
284 + .badges-box {
285 + .badge {
286 + border-radius: var(--border-radius);
287 + border: var(--border-small-100);
288 + display: flex;
289 + align-items: center;
290 + font-size: 14px;
291 + height: 28px;
292 + line-height: 1;
293 + transition: all 0.3s var(--bezier-ease);
294 +
295 + padding: 0px;
296 + gap: 0;
297 + overflow: hidden;
298 +
299 + span {
300 + padding: 0px 8px;
301 + height: 100%;
302 + line-height: 26px;
303 + opacity: 1;
304 +
305 + &:first-child {
306 + border-right: var(--border-small-100);
307 + background-color: var(--primary-005-color);
308 + }
309 + }
310 + }
311 + }
312 +
313 + .list {
314 + container-type: inline-size;
315 + min-height: 200px;
316 +
317 + .command-item {
318 + animation: artifacts-command-fade 0.3s forwards;
319 + opacity: 0;
320 +
321 + @for $i from 0 through 10 {
322 + &:nth-child(#{$i}) {
323 + animation-delay: $i * 0.05s;
324 + }
325 + }
326 +
327 + @keyframes artifacts-command-fade {
328 + from {
329 + opacity: 0;
330 + transform: translateY(10px);
331 + }
332 + to {
333 + opacity: 1;
334 + }
335 + }
336 + }
337 + }
338 +}
339 +</style>
src/components/artifacts/ArtifactsList.vue new
+347
@@ -0,0 +1,347 @@
1 +<template>
2 + <div class="artifacts-list">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <div class="info grow flex gap-2">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total :
18 + <code>{{ totalArtifacts }}</code>
19 + </div>
20 + </div>
21 + </n-popover>
22 + </div>
23 + <n-pagination
24 + v-model:page="currentPage"
25 + v-model:page-size="pageSize"
26 + :page-slot="pageSlot"
27 + :show-size-picker="showSizePicker"
28 + :page-sizes="pageSizes"
29 + :item-count="totalArtifacts"
30 + :simple="simpleMode"
31 + />
32 + <n-popover
33 + :show="showFilters"
34 + trigger="manual"
35 + overlap
36 + placement="right"
37 + style="padding-left: 0; padding-right: 0"
38 + >
39 + <template #trigger>
40 + <div class="bg-color border-radius">
41 + <n-badge
42 + :show="!!lastFilters.hostname || !!lastFilters.os"
43 + dot
44 + type="success"
45 + :offset="[-4, 0]"
46 + >
47 + <n-button size="small" v-show="!isFilterPreselected" @click="showFilters = true">
48 + <template #icon>
49 + <Icon :name="FilterIcon"></Icon>
50 + </template>
51 + </n-button>
52 + </n-badge>
53 + </div>
54 + </template>
55 + <div class="py-1 flex flex-col gap-2">
56 + <div class="px-3">
57 + <n-input-group class="artifacts-list-filter-combo" :class="{ 'filters-active': filterType }">
58 + <n-select
59 + class="artifacts-list-filter-type"
60 + v-model:value="filterType"
61 + @update:value="
62 + () => {
63 + filters.hostname = undefined
64 + filters.os = undefined
65 + }
66 + "
67 + :options="[
68 + {
69 + label: 'Agent ',
70 + value: 'agentHostname'
71 + },
72 + {
73 + label: 'OS',
74 + value: 'os'
75 + }
76 + ]"
77 + placeholder="Filters..."
78 + clearable
79 + />
80 +
81 + <n-select
82 + v-if="filterType === 'agentHostname'"
83 + v-model:value="filters.hostname"
84 + :options="agentHostnameOptions"
85 + placeholder="Select Agent"
86 + clearable
87 + filterable
88 + :loading="loadingAgents"
89 + />
90 + <n-select
91 + v-if="filterType === 'os'"
92 + v-model:value="filters.os"
93 + :options="osOptions"
94 + clearable
95 + placeholder="Select OS"
96 + />
97 + </n-input-group>
98 + </div>
99 + <div class="px-3 flex justify-end gap-2">
100 + <n-button size="small" @click="showFilters = false" secondary>Close</n-button>
101 + <n-button size="small" @click="getData()" type="primary" secondary>Submit</n-button>
102 + </div>
103 + </div>
104 + </n-popover>
105 + </div>
106 + <n-spin :show="loading">
107 + <div class="list my-3">
108 + <template v-if="artifactsList.length">
109 + <ArtifactItem
110 + v-for="artifact of itemsPaginated"
111 + :key="artifact.name"
112 + :artifact="artifact"
113 + class="mb-2"
114 + />
115 + </template>
116 + <template v-else>
117 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
118 + </template>
119 + </div>
120 + </n-spin>
121 + <div class="footer flex justify-end">
122 + <n-pagination
123 + v-model:page="currentPage"
124 + :page-size="pageSize"
125 + :item-count="totalArtifacts"
126 + :page-slot="6"
127 + v-if="itemsPaginated.length > 3"
128 + />
129 + </div>
130 + </div>
131 +</template>
132 +
133 +<script setup lang="ts">
134 +import { ref, onBeforeMount, toRefs, computed, nextTick, watch } from "vue"
135 +import { useMessage, NSpin, NPopover, NButton, NEmpty, NSelect, NPagination, NInputGroup, NBadge } from "naive-ui"
136 +import Api from "@/api"
137 +import _cloneDeep from "lodash/cloneDeep"
138 +import Icon from "@/components/common/Icon.vue"
139 +import ArtifactItem from "./ArtifactItem.vue"
140 +import type { Agent } from "@/types/agents.d"
141 +import type { ArtifactsQuery } from "@/api/artifacts"
142 +import type { Artifact } from "@/types/artifacts.d"
143 +import { useResizeObserver } from "@vueuse/core"
144 +
145 +const emit = defineEmits<{
146 + (e: "loaded-agents", value: Agent[]): void
147 + (e: "loaded-artifacts", value: Artifact[]): void
148 +}>()
149 +
150 +const props = defineProps<{ agentHostname?: string; agents?: Agent[]; artifacts?: Artifact[] }>()
151 +const { agentHostname, agents, artifacts } = toRefs(props)
152 +
153 +const message = useMessage()
154 +const loadingAgents = ref(false)
155 +const loading = ref(false)
156 +const showFilters = ref(false)
157 +const agentsList = ref<Agent[]>([])
158 +const artifactsList = ref<Artifact[]>([])
159 +
160 +const pageSize = ref(25)
161 +const currentPage = ref(1)
162 +const simpleMode = ref(false)
163 +const showSizePicker = ref(true)
164 +const pageSizes = [10, 25, 50, 100]
165 +const header = ref()
166 +const pageSlot = ref(8)
167 +
168 +const itemsPaginated = computed(() => {
169 + const from = (currentPage.value - 1) * pageSize.value
170 + const to = currentPage.value * pageSize.value
171 +
172 + return artifactsList.value.slice(from, to)
173 +})
174 +
175 +const FilterIcon = "carbon:filter-edit"
176 +const InfoIcon = "carbon:information"
177 +
178 +const totalArtifacts = computed<number>(() => {
179 + return artifactsList.value.length || 0
180 +})
181 +
182 +const filters = ref<ArtifactsQuery>({})
183 +const lastFilters = ref<ArtifactsQuery>({})
184 +
185 +const filterType = ref<string | null>(null)
186 +
187 +const isFilterPreselected = computed(() => {
188 + return !!agentHostname?.value
189 +})
190 +
191 +const agentHostnameOptions = computed(() => {
192 + if (agentHostname?.value) {
193 + return [{ value: agentHostname.value, label: agentHostname.value }]
194 + }
195 + return (agentsList.value || []).map(o => ({ value: o.hostname, label: o.hostname }))
196 +})
197 +
198 +const osOptions = [
199 + { label: "Windows", value: "windows" },
200 + { label: "Linux", value: "linux" },
201 + { label: "MacOS", value: "macos" }
202 +]
203 +
204 +watch(showFilters, val => {
205 + if (!val) {
206 + filters.value = _cloneDeep(lastFilters.value)
207 + }
208 +})
209 +
210 +function getData(cb?: (artifacts: Artifact[]) => void) {
211 + showFilters.value = false
212 + loading.value = true
213 +
214 + lastFilters.value = _cloneDeep(filters.value)
215 +
216 + Api.artifacts
217 + .getAll(filters.value)
218 + .then(res => {
219 + if (res.data.success) {
220 + artifactsList.value = res.data?.artifacts || []
221 +
222 + if (cb && typeof cb === "function") {
223 + cb(artifactsList.value)
224 + }
225 + } else {
226 + message.warning(res.data?.message || "An error occurred. Please try again later.")
227 + }
228 + })
229 + .catch(err => {
230 + artifactsList.value = []
231 +
232 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
233 + })
234 + .finally(() => {
235 + loading.value = false
236 + })
237 +}
238 +
239 +function getAgents(cb?: (agents: Agent[]) => void) {
240 + loadingAgents.value = true
241 +
242 + Api.agents
243 + .getAgents()
244 + .then(res => {
245 + if (res.data.success) {
246 + agentsList.value = res.data.agents || []
247 +
248 + if (cb && typeof cb === "function") {
249 + cb(agentsList.value)
250 + }
251 + } else {
252 + message.error(res.data?.message || "An error occurred. Please try again later.")
253 + }
254 + })
255 + .catch(err => {
256 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
257 + })
258 + .finally(() => {
259 + loadingAgents.value = false
260 + })
261 +}
262 +
263 +useResizeObserver(header, entries => {
264 + const entry = entries[0]
265 + const { width } = entry.contentRect
266 +
267 + pageSlot.value = width < 650 ? 5 : 8
268 + simpleMode.value = width < 450
269 +})
270 +
271 +onBeforeMount(() => {
272 + if (agentHostname?.value) {
273 + filters.value.hostname = agentHostname.value
274 + }
275 +
276 + if (agents?.value?.length) {
277 + agentsList.value = agents.value
278 + }
279 +
280 + if (artifacts?.value?.length) {
281 + artifactsList.value = artifacts.value
282 + }
283 +
284 + nextTick(() => {
285 + if (!agentsList.value.length && !agentHostname?.value) {
286 + getAgents((agents: Agent[]) => {
287 + emit("loaded-agents", agents)
288 + })
289 + }
290 + if (!artifactsList.value.length) {
291 + getData((artifacts: Artifact[]) => {
292 + emit("loaded-artifacts", artifacts)
293 + })
294 + }
295 + })
296 +})
297 +</script>
298 +
299 +<style lang="scss" scoped>
300 +.artifacts-list {
301 + .list {
302 + container-type: inline-size;
303 + min-height: 200px;
304 +
305 + .artifact-item {
306 + animation: artifacts-item-fade 0.3s forwards;
307 + opacity: 0;
308 +
309 + @for $i from 0 through 30 {
310 + &:nth-child(#{$i}) {
311 + animation-delay: $i * 0.05s;
312 + }
313 + }
314 +
315 + @keyframes artifacts-item-fade {
316 + from {
317 + opacity: 0;
318 + transform: translateY(10px);
319 + }
320 + to {
321 + opacity: 1;
322 + }
323 + }
324 + }
325 + }
326 +}
327 +</style>
328 +
329 +<style lang="scss">
330 +.artifacts-list-filter-combo {
331 + .artifacts-list-filter-type {
332 + min-width: 130px;
333 + max-width: 130px;
334 + }
335 +
336 + &.filters-active {
337 + min-width: 270px;
338 + width: 50vw;
339 + max-width: 400px;
340 +
341 + .artifacts-list-filter-type {
342 + min-width: 100px;
343 + max-width: 100px;
344 + }
345 + }
346 +}
347 +</style>
src/components/artifacts/ArtifactsQuarantine.vue new
+274
@@ -0,0 +1,274 @@
1 +<template>
2 + <div class="artifacts-quarantine">
3 + <div class="header flex justify-end items-start gap-2">
4 + <div class="grow flex items-center justify-end gap-2 flex-wrap">
5 + <div class="grow basis-56">
6 + <n-select
7 + v-if="!isFilterPreselected"
8 + v-model:value="filters.hostname"
9 + :options="agentHostnameOptions"
10 + placeholder="Agent hostname"
11 + clearable
12 + filterable
13 + :disabled="loading"
14 + size="small"
15 + :loading="loadingAgents"
16 + />
17 + </div>
18 + <div class="grow basis-56">
19 + <n-select
20 + v-model:value="filters.artifact_name"
21 + :options="artifactsOptions"
22 + placeholder="Artifact name"
23 + clearable
24 + :disabled="loading"
25 + filterable
26 + size="small"
27 + :loading="loadingArtifacts"
28 + />
29 + </div>
30 + <div class="grow basis-56">
31 + <n-input
32 + v-model:value="filters.velociraptor_id"
33 + placeholder="Velociraptor id"
34 + clearable
35 + :readonly="loading"
36 + size="small"
37 + />
38 + </div>
39 + <div>
40 + <n-input-group>
41 + <n-select
42 + v-model:value="filters.action"
43 + :options="actionsOptions"
44 + :disabled="loading"
45 + size="small"
46 + class="!w-32"
47 + status="success"
48 + />
49 + <n-button
50 + size="small"
51 + @click="getData()"
52 + type="primary"
53 + secondary
54 + :loading="loading"
55 + :disabled="!areFiltersValid"
56 + >
57 + <Icon :name="SubmitIcon"></Icon>
58 + </n-button>
59 + </n-input-group>
60 + </div>
61 + </div>
62 + </div>
63 + <n-spin :show="loading">
64 + <div class="list grid gap-3 my-7">
65 + <template v-if="quarantineList.length">
66 + <QuarantineItem
67 + v-for="quarantine of quarantineList"
68 + :key="quarantine.Result + quarantine.Time"
69 + :quarantine="quarantine"
70 + />
71 + </template>
72 + <template v-else>
73 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
74 + </template>
75 + </div>
76 + </n-spin>
77 + </div>
78 +</template>
79 +
80 +<script setup lang="ts">
81 +import { ref, onBeforeMount, toRefs, computed, nextTick } from "vue"
82 +import { useMessage, NSpin, NButton, NEmpty, NSelect, NInput, NInputGroup } from "naive-ui"
83 +import Api from "@/api"
84 +import Icon from "@/components/common/Icon.vue"
85 +import QuarantineItem from "./QuarantineItem.vue"
86 +import type { Agent } from "@/types/agents.d"
87 +import type { QuarantineRequest } from "@/api/artifacts"
88 +import type { Artifact, QuarantineResult } from "@/types/artifacts.d"
89 +// import { quarantineResult } from "./mock"
90 +
91 +const emit = defineEmits<{
92 + (e: "loaded-agents", value: Agent[]): void
93 + (e: "loaded-artifacts", value: Artifact[]): void
94 + (e: "action-performed"): void
95 +}>()
96 +
97 +const props = defineProps<{ agentHostname?: string; agents?: Agent[]; artifacts?: Artifact[] }>()
98 +const { agentHostname, agents, artifacts } = toRefs(props)
99 +
100 +const message = useMessage()
101 +const loadingAgents = ref(false)
102 +const loadingArtifacts = ref(false)
103 +const loading = ref(false)
104 +const agentsList = ref<Agent[]>([])
105 +const artifactsList = ref<Artifact[]>([])
106 +const quarantineList = ref<QuarantineResult[]>([])
107 +
108 +const SubmitIcon = "carbon:play"
109 +
110 +const filters = ref<Partial<QuarantineRequest>>({})
111 +
112 +const isFilterPreselected = computed(() => {
113 + return !!agentHostname?.value
114 +})
115 +
116 +const areFiltersValid = computed(() => {
117 + return !!filters.value.artifact_name && !!filters.value.hostname
118 +})
119 +
120 +const agentHostnameOptions = computed(() => {
121 + if (agentHostname?.value) {
122 + return [{ value: agentHostname.value, label: agentHostname.value }]
123 + }
124 + return (agentsList.value || []).map(o => ({ value: o.hostname, label: o.hostname }))
125 +})
126 +
127 +const artifactsOptions = computed(() => {
128 + return (artifactsList.value || []).map(o => ({ value: o.name, label: o.name }))
129 +})
130 +
131 +const actionsOptions = ref([
132 + { label: "Quarantine", value: "quarantine" },
133 + { label: "Remove", value: "remove_quarantine" }
134 +])
135 +
136 +function getData() {
137 + if (areFiltersValid.value) {
138 + loading.value = true
139 +
140 + Api.artifacts
141 + .quarantine(filters.value as QuarantineRequest)
142 + .then(res => {
143 + if (res.data.success) {
144 + quarantineList.value = res.data?.results || []
145 + emit("action-performed")
146 + } else {
147 + message.warning(res.data?.message || "An error occurred. Please try again later.")
148 + }
149 + })
150 + .catch(err => {
151 + quarantineList.value = []
152 +
153 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
154 + })
155 + .finally(() => {
156 + loading.value = false
157 + })
158 + }
159 +}
160 +
161 +function getAgents(cb?: (agents: Agent[]) => void) {
162 + loadingAgents.value = true
163 +
164 + Api.agents
165 + .getAgents()
166 + .then(res => {
167 + if (res.data.success) {
168 + agentsList.value = res.data.agents || []
169 +
170 + if (cb && typeof cb === "function") {
171 + cb(agentsList.value)
172 + }
173 + } else {
174 + message.error(res.data?.message || "An error occurred. Please try again later.")
175 + }
176 + })
177 + .catch(err => {
178 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
179 + })
180 + .finally(() => {
181 + loadingAgents.value = false
182 + })
183 +}
184 +
185 +function getArtifacts(cb?: (artifacts: Artifact[]) => void) {
186 + loadingArtifacts.value = true
187 +
188 + Api.artifacts
189 + .getAll()
190 + .then(res => {
191 + if (res.data.success) {
192 + artifactsList.value = res.data.artifacts || []
193 +
194 + if (cb && typeof cb === "function") {
195 + cb(artifactsList.value)
196 + }
197 + } else {
198 + message.error(res.data?.message || "An error occurred. Please try again later.")
199 + }
200 + })
201 + .catch(err => {
202 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
203 + })
204 + .finally(() => {
205 + loadingArtifacts.value = false
206 + })
207 +}
208 +
209 +onBeforeMount(() => {
210 + artifactsList.value = ["Windows.Remediation.Quarantine", "Linux.Remediation.Quarantine"].map(
211 + o => ({ name: o }) as Artifact
212 + )
213 +
214 + if (agentHostname?.value) {
215 + filters.value.hostname = agentHostname.value
216 + }
217 +
218 + if (agents?.value?.length && !agentsList.value.length) {
219 + agentsList.value = agents.value
220 + }
221 +
222 + if (artifacts?.value?.length && !artifactsList.value.length) {
223 + artifactsList.value = artifacts.value
224 + }
225 +
226 + filters.value.action = actionsOptions.value[0].value as QuarantineRequest["action"]
227 +
228 + nextTick(() => {
229 + if (!agentsList.value.length && !agentHostname?.value) {
230 + getAgents((agents: Agent[]) => {
231 + emit("loaded-agents", agents)
232 + })
233 + }
234 + if (!artifactsList.value.length) {
235 + getArtifacts((artifacts: Artifact[]) => {
236 + emit("loaded-artifacts", artifacts)
237 + })
238 + }
239 + })
240 +
241 + // MOCK
242 + // quarantineList.value = quarantineResult as QuarantineResult[]
243 +})
244 +</script>
245 +
246 +<style lang="scss" scoped>
247 +.artifacts-quarantine {
248 + .list {
249 + container-type: inline-size;
250 + min-height: 100px;
251 +
252 + .quarantine-item {
253 + animation: artifacts-quarantine-fade 0.3s forwards;
254 + opacity: 0;
255 +
256 + @for $i from 0 through 30 {
257 + &:nth-child(#{$i}) {
258 + animation-delay: $i * 0.05s;
259 + }
260 + }
261 +
262 + @keyframes artifacts-quarantine-fade {
263 + from {
264 + opacity: 0;
265 + transform: translateY(10px);
266 + }
267 + to {
268 + opacity: 1;
269 + }
270 + }
271 + }
272 + }
273 +}
274 +</style>
src/components/artifacts/CollectItem.vue new
+166
@@ -0,0 +1,166 @@
1 +<template>
2 + <div class="collect-item flex flex-wrap gap-2 p-2">
3 + <div class="property" v-for="prop of displayData" :key="prop.key" :class="{ 'hide-mobile': prop.hideMobile }">
4 + <div class="key">{{ prop.key }}</div>
5 + <div class="value">{{ prop.value }}</div>
6 + </div>
7 + <div class="property more" @click="showDetails = true">
8 + <div class="key">
9 + <Icon :name="MoreIcon" />
10 + </div>
11 + </div>
12 +
13 + <n-modal
14 + v-model:show="showDetails"
15 + preset="card"
16 + :style="{ maxWidth: 'min(800px, 90vw)', overflow: 'hidden' }"
17 + :bordered="false"
18 + >
19 + <SimpleJsonViewer class="vuesjv-override" :model-value="jsonData" :initialExpandedDepth="2" />
20 + </n-modal>
21 + </div>
22 +</template>
23 +
24 +<script setup lang="ts">
25 +import { NModal } from "naive-ui"
26 +import { useSettingsStore } from "@/stores/settings"
27 +import type { CollectResult } from "@/types/artifacts.d"
28 +import dayjs from "@/utils/dayjs"
29 +import { SimpleJsonViewer } from "vue-sjv"
30 +import "@/assets/scss/vuesjv-override.scss"
31 +import { onBeforeMount, ref } from "vue"
32 +import _isString from "lodash/isString"
33 +import _isNumber from "lodash/isNumber"
34 +import Icon from "@/components/common/Icon.vue"
35 +
36 +const MoreIcon = "mdi:code-json"
37 +
38 +interface Prop {
39 + key: string
40 + value: string | number
41 + hideMobile: boolean
42 +}
43 +
44 +const { collect } = defineProps<{ collect: CollectResult }>()
45 +
46 +const jsonData = ref<CollectResult>({})
47 +const displayData = ref<Prop[]>([])
48 +
49 +const showDetails = ref(false)
50 +const dFormats = useSettingsStore().dateFormat
51 +
52 +function formatDate(timestamp: string | number): string {
53 + return dayjs(timestamp).format(dFormats.datetimesec)
54 +}
55 +
56 +onBeforeMount(() => {
57 + for (const key in collect) {
58 + const value = collect[key]
59 + console.log(key, value, typeof value)
60 +
61 + const prop: Prop = {
62 + key: "",
63 + value: "",
64 + hideMobile: false
65 + }
66 +
67 + if ((_isString(value) || _isNumber(value)) && value !== "" && key !== "___id") {
68 + prop.key = key
69 + prop.value = value
70 + }
71 +
72 + if (prop.value && typeof prop.value === "string") {
73 + prop.value = dayjs(value).isValid() ? formatDate(value) : value.toString()
74 + }
75 +
76 + if (prop.value && typeof prop.value === "number") {
77 + const numText = prop.value.toString()
78 +
79 + if (numText.length === 10 || numText.length === 13) {
80 + if (dayjs(value).isValid()) {
81 + prop.value = formatDate(value)
82 + }
83 + }
84 + }
85 +
86 + if (prop.key && displayData.value.length < 5) {
87 + if (displayData.value.length > 2) {
88 + prop.hideMobile = true
89 + }
90 + displayData.value.push(prop)
91 + }
92 + }
93 +
94 + jsonData.value = collect
95 +
96 + delete jsonData.value.___id
97 +})
98 +</script>
99 +
100 +<style lang="scss" scoped>
101 +.collect-item {
102 + border-radius: var(--border-radius);
103 + background-color: var(--bg-color);
104 + border: var(--border-small-050);
105 + transition: all 0.2s var(--bezier-ease);
106 + min-height: 160px;
107 + max-width: 100%;
108 + overflow: hidden;
109 +
110 + .property {
111 + border: var(--border-small-100);
112 + background-color: var(--bg-secondary-color);
113 + border-radius: var(--border-radius);
114 + overflow: hidden;
115 + flex-basis: 140px;
116 + flex-grow: 1;
117 +
118 + .key {
119 + border-bottom: var(--border-small-050);
120 + padding: 8px 12px;
121 + font-size: 12px;
122 + }
123 + .value {
124 + font-size: 14px;
125 + padding: 8px 12px;
126 + background-color: var(--bg-color);
127 + font-family: var(--font-family-mono);
128 + height: 100%;
129 + }
130 +
131 + &.more {
132 + cursor: pointer;
133 + transition: all 0.2s;
134 + .key {
135 + border-bottom: none;
136 + font-size: 26px;
137 + text-align: center;
138 + height: 100%;
139 + display: flex;
140 + justify-content: center;
141 + align-items: center;
142 + }
143 +
144 + &:hover {
145 + border-color: var(--primary-color);
146 + }
147 + }
148 + }
149 +
150 + &:hover {
151 + border-color: var(--primary-color);
152 + }
153 +
154 + @container (max-width: 500px) {
155 + flex-direction: column;
156 +
157 + .property {
158 + flex-basis: initial;
159 + flex-grow: initial;
160 + }
161 + .hide-mobile {
162 + display: none;
163 + }
164 + }
165 +}
166 +</style>
src/components/artifacts/CommandItem.vue new
+111
@@ -0,0 +1,111 @@
1 +<template>
2 + <div class="command-item flex flex-col">
3 + <div class="header-box flex justify-between">
4 + <div class="status">
5 + Complete:
6 + <strong class="font-mono" :class="{ success: command.Complete + '' === 'true' }">
7 + {{ command.Complete }}
8 + </strong>
9 + </div>
10 + <div class="code">
11 + Code:
12 + <strong class="font-mono">{{ command.ReturnCode }}</strong>
13 + </div>
14 + </div>
15 + <div class="main-box">
16 + <div class="output stdout" v-if="command.Stdout">
17 + <label>Stdout</label>
18 + <n-input
19 + :value="command.Stdout"
20 + type="textarea"
21 + readonly
22 + placeholder="Empty"
23 + :autosize="{
24 + minRows: 3
25 + }"
26 + />
27 + </div>
28 + <div class="output stderr" v-if="command.Stderr">
29 + <label class="flex items-center">
30 + <Icon :name="DangerIcon" class="mr-1"></Icon>
31 + Stderr
32 + </label>
33 + <n-input
34 + :value="command.Stderr"
35 + type="textarea"
36 + readonly
37 + placeholder="Empty"
38 + :autosize="{
39 + minRows: 3
40 + }"
41 + />
42 + </div>
43 + </div>
44 + </div>
45 +</template>
46 +
47 +<script setup lang="ts">
48 +import { NInput } from "naive-ui"
49 +import type { CommandResult } from "@/types/artifacts.d"
50 +import Icon from "@/components/common/Icon.vue"
51 +
52 +const { command } = defineProps<{ command: CommandResult }>()
53 +
54 +const DangerIcon = "majesticons:exclamation-line"
55 +</script>
56 +
57 +<style lang="scss" scoped>
58 +.command-item {
59 + border-radius: var(--border-radius);
60 + background-color: var(--bg-color);
61 + transition: all 0.2s var(--bezier-ease);
62 + border: var(--border-small-050);
63 + max-width: 100%;
64 + overflow: hidden;
65 +
66 + .header-box {
67 + padding: 16px 20px;
68 + font-family: var(--font-family-mono);
69 + font-size: 14px;
70 + border-bottom: var(--border-small-100);
71 + .status {
72 + strong {
73 + color: var(--warning-color);
74 + &.success {
75 + color: var(--primary-color);
76 + }
77 + }
78 + }
79 + }
80 + .main-box {
81 + .output {
82 + margin-top: 20px;
83 +
84 + label {
85 + padding: 0px 20px;
86 + line-height: 1;
87 + }
88 +
89 + .n-input {
90 + margin-top: 8px;
91 + border-radius: 0;
92 +
93 + background-color: var(--n-color) !important;
94 +
95 + :deep() {
96 + .n-input__border,
97 + .n-input__state-border {
98 + display: none;
99 + }
100 + }
101 + }
102 +
103 + &.stderr {
104 + label {
105 + color: var(--warning-color);
106 + }
107 + }
108 + }
109 + }
110 +}
111 +</style>
src/components/artifacts/QuarantineItem.vue new
+30
@@ -0,0 +1,30 @@
1 +<template>
2 + <div class="quarantine-item flex flex-col gap-1 px-5 py-3">
3 + <div class="time text-secondary-color">
4 + {{ formatDate(quarantine.Time) }}
5 + </div>
6 + <div class="result">{{ quarantine.Result }}</div>
7 + </div>
8 +</template>
9 +
10 +<script setup lang="ts">
11 +import { useSettingsStore } from "@/stores/settings"
12 +import type { QuarantineResult } from "@/types/artifacts.d"
13 +import dayjs from "@/utils/dayjs"
14 +
15 +const { quarantine } = defineProps<{ quarantine: QuarantineResult }>()
16 +
17 +const dFormats = useSettingsStore().dateFormat
18 +
19 +function formatDate(timestamp: string): string {
20 + return dayjs(timestamp).format(dFormats.datetimesec)
21 +}
22 +</script>
23 +
24 +<style lang="scss" scoped>
25 +.quarantine-item {
26 + border-radius: var(--border-radius);
27 + background-color: var(--bg-color);
28 + border: var(--border-small-050);
29 +}
30 +</style>
src/components/artifacts/mock.ts new
+939
@@ -0,0 +1,939 @@
1 +const collectResult = [
2 + {
3 + Pid: 928,
4 + Name: "svchost.exe",
5 + Family: "IPv4",
6 + Type: "TCP",
7 + Status: "LISTEN",
8 + "Laddr.IP": "0.0.0.0",
9 + "Laddr.Port": 135,
10 + "Raddr.IP": "0.0.0.0",
11 + "Raddr.Port": 0,
12 + Timestamp: "2023-11-07T16:32:23Z"
13 + },
14 + {
15 + Pid: 4,
16 + Name: "System",
17 + Family: "IPv4",
18 + Type: "TCP",
19 + Status: "LISTEN",
20 + "Laddr.IP": "192.168.200.3",
21 + "Laddr.Port": 139,
22 + "Raddr.IP": "0.0.0.0",
23 + "Raddr.Port": 0,
24 + Timestamp: "2023-11-07T16:32:31Z"
25 + },
26 + {
27 + Pid: 344,
28 + Name: "svchost.exe",
29 + Family: "IPv4",
30 + Type: "TCP",
31 + Status: "LISTEN",
32 + "Laddr.IP": "0.0.0.0",
33 + "Laddr.Port": 3389,
34 + "Raddr.IP": "0.0.0.0",
35 + "Raddr.Port": 0,
36 + Timestamp: "2023-11-07T16:32:28Z"
37 + },
38 + {
39 + Pid: 540,
40 + Name: "wininit.exe",
41 + Family: "IPv4",
42 + Type: "TCP",
43 + Status: "LISTEN",
44 + "Laddr.IP": "0.0.0.0",
45 + "Laddr.Port": 49664,
46 + "Raddr.IP": "0.0.0.0",
47 + "Raddr.Port": 0,
48 + Timestamp: "2023-11-07T16:32:23Z"
49 + },
50 + {
51 + Name: "System",
52 + PebBaseAddress: "0x0",
53 + Pid: 4,
54 + ImagePathName: null,
55 + CommandLine: null,
56 + CurrentDirectory: null,
57 + Env: {}
58 + },
59 + {
60 + Name: "Registry",
61 + PebBaseAddress: "0x0",
62 + Pid: 104,
63 + ImagePathName: null,
64 + CommandLine: null,
65 + CurrentDirectory: null,
66 + Env: {}
67 + },
68 + {
69 + Name: "smss.exe",
70 + PebBaseAddress: "0xc24b5b6000",
71 + Pid: 304,
72 + ImagePathName: null,
73 + CommandLine: null,
74 + CurrentDirectory: null,
75 + Env: {}
76 + },
77 + {
78 + Name: "csrss.exe",
79 + PebBaseAddress: "0xa5397a7000",
80 + Pid: 440,
81 + ImagePathName: null,
82 + CommandLine: null,
83 + CurrentDirectory: null,
84 + Env: {}
85 + },
86 + {
87 + Name: "csrss.exe",
88 + PebBaseAddress: "0x94c1180000",
89 + Pid: 516,
90 + ImagePathName: null,
91 + CommandLine: null,
92 + CurrentDirectory: null,
93 + Env: {}
94 + },
95 + {
96 + Name: "wininit.exe",
97 + PebBaseAddress: "0xc37cb67000",
98 + Pid: 540,
99 + ImagePathName: null,
100 + CommandLine: null,
101 + CurrentDirectory: null,
102 + Env: {}
103 + },
104 + {
105 + Name: "winlogon.exe",
106 + PebBaseAddress: "0xff437be000",
107 + Pid: 580,
108 + ImagePathName: "C:\\Windows\\system32\\winlogon.exe",
109 + CommandLine: "winlogon.exe",
110 + CurrentDirectory: "C:\\Windows\\system32\\",
111 + Env: {
112 + ALLUSERSPROFILE: "C:\\ProgramData",
113 + CommonProgramFiles: "C:\\Program Files\\Common Files",
114 + "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files",
115 + CommonProgramW6432: "C:\\Program Files\\Common Files",
116 + COMPUTERNAME: "WIN-HFOU106TD7K",
117 + ComSpec: "C:\\Windows\\system32\\cmd.exe",
118 + DriverData: "C:\\Windows\\System32\\Drivers\\DriverData",
119 + NUMBER_OF_PROCESSORS: "4",
120 + OS: "Windows_NT",
121 + Path: "C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\",
122 + PATHEXT: ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC",
123 + PROCESSOR_ARCHITECTURE: "AMD64",
124 + PROCESSOR_IDENTIFIER: "Intel64 Family 6 Model 44 Stepping 2, GenuineIntel",
125 + PROCESSOR_LEVEL: "6",
126 + PROCESSOR_REVISION: "2c02",
127 + ProgramData: "C:\\ProgramData",
128 + ProgramFiles: "C:\\Program Files",
129 + "ProgramFiles(x86)": "C:\\Program Files (x86)",
130 + ProgramW6432: "C:\\Program Files",
131 + PSModulePath:
132 + "%ProgramFiles%\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules",
133 + PUBLIC: "C:\\Users\\Public",
134 + SystemDrive: "C:",
135 + SystemRoot: "C:\\Windows",
136 + TEMP: "C:\\Windows\\TEMP",
137 + TMP: "C:\\Windows\\TEMP",
138 + USERNAME: "SYSTEM",
139 + USERPROFILE: "C:\\Windows\\system32\\config\\systemprofile",
140 + windir: "C:\\Windows"
141 + }
142 + },
143 + {
144 + Name: "services.exe",
145 + PebBaseAddress: "0xee24dc5000",
146 + Pid: 660,
147 + ImagePathName: null,
148 + CommandLine: null,
149 + CurrentDirectory: null,
150 + Env: {}
151 + },
152 + {
153 + Name: "lsass.exe",
154 + PebBaseAddress: "0xc168a3a000",
155 + Pid: 680,
156 + ImagePathName: "C:\\Windows\\system32\\lsass.exe",
157 + CommandLine: "C:\\Windows\\system32\\lsass.exe",
158 + CurrentDirectory: "C:\\Windows\\system32\\",
159 + Env: {
160 + ALLUSERSPROFILE: "C:\\ProgramData",
161 + CommonProgramFiles: "C:\\Program Files\\Common Files",
162 + "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files",
163 + CommonProgramW6432: "C:\\Program Files\\Common Files",
164 + COMPUTERNAME: "WIN-HFOU106TD7K",
165 + ComSpec: "C:\\Windows\\system32\\cmd.exe",
166 + DriverData: "C:\\Windows\\System32\\Drivers\\DriverData",
167 + NUMBER_OF_PROCESSORS: "4",
168 + OS: "Windows_NT",
169 + Path: "C:\\Windows\\System32",
170 + PATHEXT: ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC",
171 + PROCESSOR_ARCHITECTURE: "AMD64",
172 + PROCESSOR_IDENTIFIER: "Intel64 Family 6 Model 44 Stepping 2, GenuineIntel",
173 + PROCESSOR_LEVEL: "6",
174 + PROCESSOR_REVISION: "2c02",
175 + ProgramData: "C:\\ProgramData",
176 + ProgramFiles: "C:\\Program Files",
177 + "ProgramFiles(x86)": "C:\\Program Files (x86)",
178 + ProgramW6432: "C:\\Program Files",
179 + PSModulePath:
180 + "%ProgramFiles%\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules",
181 + PUBLIC: "C:\\Users\\Public",
182 + SystemDrive: "C:",
183 + SystemRoot: "C:\\Windows",
184 + TEMP: "C:\\Windows\\TEMP",
185 + TMP: "C:\\Windows\\TEMP",
186 + USERNAME: "SYSTEM",
187 + USERPROFILE: "C:\\Windows\\system32\\config\\systemprofile",
188 + windir: "C:\\Windows"
189 + }
190 + },
191 + {
192 + Name: "svchost.exe",
193 + PebBaseAddress: "0xbc2c40000",
194 + Pid: 808,
195 + ImagePathName: "C:\\Windows\\system32\\svchost.exe",
196 + CommandLine: "C:\\Windows\\system32\\svchost.exe -k DcomLaunch -p",
197 + CurrentDirectory: "C:\\Windows\\system32\\",
198 + Env: {
199 + ALLUSERSPROFILE: "C:\\ProgramData",
200 + APPDATA: "C:\\Windows\\system32\\config\\systemprofile\\AppData\\Roaming",
201 + CommonProgramFiles: "C:\\Program Files\\Common Files",
202 + "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files",
203 + CommonProgramW6432: "C:\\Program Files\\Common Files",
204 + COMPUTERNAME: "WIN-HFOU106TD7K",
205 + ComSpec: "C:\\Windows\\system32\\cmd.exe",
206 + DriverData: "C:\\Windows\\System32\\Drivers\\DriverData",
207 + LOCALAPPDATA: "C:\\Windows\\system32\\config\\systemprofile\\AppData\\Local",
208 + NUMBER_OF_PROCESSORS: "4",
209 + OS: "Windows_NT",
210 + Path: "C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\;C:\\Windows\\system32\\config\\systemprofile\\AppData\\Local\\Microsoft\\WindowsApps",
211 + PATHEXT: ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC",
212 + PROCESSOR_ARCHITECTURE: "AMD64",
213 + PROCESSOR_IDENTIFIER: "Intel64 Family 6 Model 44 Stepping 2, GenuineIntel",
214 + PROCESSOR_LEVEL: "6",
215 + PROCESSOR_REVISION: "2c02",
216 + ProgramData: "C:\\ProgramData",
217 + ProgramFiles: "C:\\Program Files",
218 + "ProgramFiles(x86)": "C:\\Program Files (x86)",
219 + ProgramW6432: "C:\\Program Files",
220 + PSModulePath:
221 + "%ProgramFiles%\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules",
222 + PUBLIC: "C:\\Users\\Public",
223 + SystemDrive: "C:",
224 + SystemRoot: "C:\\Windows",
225 + TEMP: "C:\\Windows\\TEMP",
226 + TMP: "C:\\Windows\\TEMP",
227 + USERDOMAIN: "WORKGROUP",
228 + USERNAME: "WIN-HFOU106TD7K$",
229 + USERPROFILE: "C:\\Windows\\system32\\config\\systemprofile",
230 + windir: "C:\\Windows"
231 + }
232 + },
233 + {
234 + Name: "fontdrvhost.exe",
235 + PebBaseAddress: "0x61189f8000",
236 + Pid: 832,
237 + ImagePathName: "C:\\Windows\\system32\\fontdrvhost.exe",
238 + CommandLine: '"fontdrvhost.exe"',
239 + CurrentDirectory: "C:\\Windows\\system32\\",
240 + Env: {
241 + ALLUSERSPROFILE: "C:\\ProgramData",
242 + CommonProgramFiles: "C:\\Program Files\\Common Files",
243 + "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files",
244 + CommonProgramW6432: "C:\\Program Files\\Common Files",
245 + COMPUTERNAME: "WIN-HFOU106TD7K",
246 + ComSpec: "C:\\Windows\\system32\\cmd.exe",
247 + DriverData: "C:\\Windows\\System32\\Drivers\\DriverData",
248 + LOCALAPPDATA: "%TEMP%\\Packages\\microsoft.windows.fontdrvhost\\AC",
249 + NUMBER_OF_PROCESSORS: "4",
250 + OS: "Windows_NT",
251 + Path: "C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\",
252 + PATHEXT: ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC",
253 + PROCESSOR_ARCHITECTURE: "AMD64",
254 + PROCESSOR_IDENTIFIER: "Intel64 Family 6 Model 44 Stepping 2, GenuineIntel",
255 + PROCESSOR_LEVEL: "6",
256 + PROCESSOR_REVISION: "2c02",
257 + ProgramData: "C:\\ProgramData",
258 + ProgramFiles: "C:\\Program Files",
259 + "ProgramFiles(x86)": "C:\\Program Files (x86)",
260 + ProgramW6432: "C:\\Program Files",
261 + PSModulePath:
262 + "%ProgramFiles%\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules",
263 + PUBLIC: "C:\\Users\\Public",
264 + SystemDrive: "C:",
265 + SystemRoot: "C:\\Windows",
266 + TEMP: "%TEMP%\\Packages\\microsoft.windows.fontdrvhost\\AC\\Temp",
267 + TMP: "%TEMP%\\Packages\\microsoft.windows.fontdrvhost\\AC\\Temp",
268 + USERDOMAIN: "Font Driver Host",
269 + USERNAME: "UMFD-1",
270 + USERPROFILE: "C:\\Users\\Default",
271 + windir: "C:\\Windows"
272 + }
273 + },
274 + {
275 + Name: "fontdrvhost.exe",
276 + PebBaseAddress: "0xb19d6b6000",
277 + Pid: 840,
278 + ImagePathName: "C:\\Windows\\system32\\fontdrvhost.exe",
279 + CommandLine: '"fontdrvhost.exe"',
280 + CurrentDirectory: "C:\\Windows\\system32\\",
281 + Env: {
282 + ALLUSERSPROFILE: "C:\\ProgramData",
283 + CommonProgramFiles: "C:\\Program Files\\Common Files",
284 + "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files",
285 + CommonProgramW6432: "C:\\Program Files\\Common Files",
286 + COMPUTERNAME: "WIN-HFOU106TD7K",
287 + ComSpec: "C:\\Windows\\system32\\cmd.exe",
288 + DriverData: "C:\\Windows\\System32\\Drivers\\DriverData",
289 + LOCALAPPDATA: "%TEMP%\\Packages\\microsoft.windows.fontdrvhost\\AC",
290 + NUMBER_OF_PROCESSORS: "4",
291 + OS: "Windows_NT",
292 + Path: "C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\",
293 + PATHEXT: ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC",
294 + PROCESSOR_ARCHITECTURE: "AMD64",
295 + PROCESSOR_IDENTIFIER: "Intel64 Family 6 Model 44 Stepping 2, GenuineIntel",
296 + PROCESSOR_LEVEL: "6",
297 + PROCESSOR_REVISION: "2c02",
298 + ProgramData: "C:\\ProgramData",
299 + ProgramFiles: "C:\\Program Files",
300 + "ProgramFiles(x86)": "C:\\Program Files (x86)",
301 + ProgramW6432: "C:\\Program Files",
302 + PSModulePath:
303 + "%ProgramFiles%\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules",
304 + PUBLIC: "C:\\Users\\Public",
305 + SystemDrive: "C:",
306 + SystemRoot: "C:\\Windows",
307 + TEMP: "%TEMP%\\Packages\\microsoft.windows.fontdrvhost\\AC\\Temp",
308 + TMP: "%TEMP%\\Packages\\microsoft.windows.fontdrvhost\\AC\\Temp",
309 + USERDOMAIN: "Font Driver Host",
310 + USERNAME: "UMFD-0",
311 + USERPROFILE: "C:\\Users\\Default",
312 + windir: "C:\\Windows"
313 + }
314 + },
315 + {
316 + Name: "svchost.exe",
317 + PebBaseAddress: "0xd3e48df000",
318 + Pid: 928,
319 + ImagePathName: "C:\\Windows\\system32\\svchost.exe",
320 + CommandLine: "C:\\Windows\\system32\\svchost.exe -k RPCSS -p",
321 + CurrentDirectory: "C:\\Windows\\system32\\",
322 + Env: {
323 + ALLUSERSPROFILE: "C:\\ProgramData",
324 + APPDATA: "C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Roaming",
325 + CommonProgramFiles: "C:\\Program Files\\Common Files",
326 + "CommonProgramFiles(x86)": "C:\\Program Files (x86)\\Common Files",
327 + CommonProgramW6432: "C:\\Program Files\\Common Files",
328 + COMPUTERNAME: "WIN-HFOU106TD7K",
329 + ComSpec: "C:\\Windows\\system32\\cmd.exe",
330 + DriverData: "C:\\Windows\\System32\\Drivers\\DriverData",
331 + LOCALAPPDATA: "C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local",
332 + NUMBER_OF_PROCESSORS: "4",
333 + OS: "Windows_NT",
334 + Path: "C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Windows\\System32\\OpenSSH\\;C:\\Windows\\ServiceProfiles\\NetworkService\\AppData\\Local\\Microsoft\\WindowsApps",
335 + PATHEXT: ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC",
336 + PROCESSOR_ARCHITECTURE: "AMD64",
337 + PROCESSOR_IDENTIFIER: "Intel64 Family 6 Model 44 Stepping 2, GenuineIntel",
338 + PROCESSOR_LEVEL: "6",
339 + PROCESSOR_REVISION: "2c02",
340 + ProgramData: "C:\\ProgramData",
341 + ProgramFiles: "C:\\Program Files",
342 + "ProgramFiles(x86)": "C:\\Program Files (x86)",
343 + ProgramW6432: "C:\\Program Files",
344 + PSModulePath:
345 + "%ProgramFiles%\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules",
346 + PUBLIC: "C:\\Users\\Public",
347 + SystemDrive: "C:",
348 + SystemRoot: "C:\\Windows",
349 + TEMP: "C:\\Windows\\SERVIC~1\\NETWOR~1\\AppData\\Local\\Temp",
350 + TMP: "C:\\Windows\\SERVIC~1\\NETWOR~1\\AppData\\Local\\Temp",
351 + USERDOMAIN: "WORKGROUP",
352 + USERNAME: "WIN-HFOU106TD7K$",
353 + USERPROFILE: "C:\\Windows\\ServiceProfiles\\NetworkService",
354 + windir: "C:\\Windows"
355 + }
356 + },
357 + {
358 + Pid: 1188,
359 + Name: "svchost.exe",
360 + Family: "IPv4",
361 + Type: "TCP",
362 + Status: "LISTEN",
363 + "Laddr.IP": "0.0.0.0",
364 + "Laddr.Port": 49665,
365 + "Raddr.IP": "0.0.0.0",
366 + "Raddr.Port": 0,
367 + Timestamp: "2023-11-07T16:32:28Z"
368 + },
369 + {
370 + Pid: 680,
371 + Name: "lsass.exe",
372 + Family: "IPv4",
373 + Type: "TCP",
374 + Status: "LISTEN",
375 + "Laddr.IP": "0.0.0.0",
376 + "Laddr.Port": 49666,
377 + "Raddr.IP": "0.0.0.0",
378 + "Raddr.Port": 0,
379 + Timestamp: "2023-11-07T16:32:31Z"
380 + },
381 + {
382 + Pid: 464,
383 + Name: "svchost.exe",
384 + Family: "IPv4",
385 + Type: "TCP",
386 + Status: "LISTEN",
387 + "Laddr.IP": "0.0.0.0",
388 + "Laddr.Port": 49667,
389 + "Raddr.IP": "0.0.0.0",
390 + "Raddr.Port": 0,
391 + Timestamp: "2023-11-07T16:32:33Z"
392 + },
393 + {
394 + Pid: 464,
395 + Name: "svchost.exe",
396 + Family: "IPv4",
397 + Type: "TCP",
398 + Status: "LISTEN",
399 + "Laddr.IP": "0.0.0.0",
400 + "Laddr.Port": 49668,
401 + "Raddr.IP": "0.0.0.0",
402 + "Raddr.Port": 0,
403 + Timestamp: "2023-11-07T16:32:33Z"
404 + },
405 + {
406 + Pid: 1524,
407 + Name: "spoolsv.exe",
408 + Family: "IPv4",
409 + Type: "TCP",
410 + Status: "LISTEN",
411 + "Laddr.IP": "0.0.0.0",
412 + "Laddr.Port": 49670,
413 + "Raddr.IP": "0.0.0.0",
414 + "Raddr.Port": 0,
415 + Timestamp: "2023-11-07T16:32:55Z"
416 + },
417 + {
418 + Pid: 660,
419 + Name: "services.exe",
420 + Family: "IPv4",
421 + Type: "TCP",
422 + Status: "LISTEN",
423 + "Laddr.IP": "0.0.0.0",
424 + "Laddr.Port": 49676,
425 + "Raddr.IP": "0.0.0.0",
426 + "Raddr.Port": 0,
427 + Timestamp: "2023-11-07T16:33:00Z"
428 + },
429 + {
430 + Pid: 4312,
431 + Name: "Velociraptor.exe",
432 + Family: "IPv4",
433 + Type: "TCP",
434 + Status: "ESTAB",
435 + "Laddr.IP": "192.168.200.3",
436 + "Laddr.Port": 49876,
437 + "Raddr.IP": "5.161.59.220",
438 + "Raddr.Port": 8000,
439 + Timestamp: "2023-11-08T06:27:07Z"
440 + },
441 + {
442 + Pid: 464,
443 + Name: "svchost.exe",
444 + Family: "IPv4",
445 + Type: "TCP",
446 + Status: "ESTAB",
447 + "Laddr.IP": "192.168.200.3",
448 + "Laddr.Port": 49964,
449 + "Raddr.IP": "20.7.1.246",
450 + "Raddr.Port": 443,
451 + Timestamp: "2023-11-09T08:38:11Z"
452 + },
453 + {
454 + Pid: 2172,
455 + Name: "wazuh-agent.exe",
456 + Family: "IPv4",
457 + Type: "TCP",
458 + Status: "ESTAB",
459 + "Laddr.IP": "192.168.200.3",
460 + "Laddr.Port": 50109,
461 + "Raddr.IP": "5.161.59.220",
462 + "Raddr.Port": 1514,
463 + Timestamp: "2023-11-10T17:58:50Z"
464 + },
465 + {
466 + Pid: 4,
467 + Name: "System",
468 + Family: "IPv4",
469 + Type: "TCP",
470 + Status: "LISTEN",
471 + "Laddr.IP": "0.0.0.0",
472 + "Laddr.Port": 445,
473 + "Raddr.IP": "0.0.0.0",
474 + "Raddr.Port": 0,
475 + Timestamp: "2023-11-07T16:32:58Z"
476 + },
477 + {
478 + Pid: 4,
479 + Name: "System",
480 + Family: "IPv4",
481 + Type: "TCP",
482 + Status: "LISTEN",
483 + "Laddr.IP": "0.0.0.0",
484 + "Laddr.Port": 5357,
485 + "Raddr.IP": "0.0.0.0",
486 + "Raddr.Port": 0,
487 + Timestamp: "2023-11-07T16:32:57Z"
488 + },
489 + {
490 + Pid: 4,
491 + Name: "System",
492 + Family: "IPv4",
493 + Type: "TCP",
494 + Status: "LISTEN",
495 + "Laddr.IP": "0.0.0.0",
496 + "Laddr.Port": 5985,
497 + "Raddr.IP": "0.0.0.0",
498 + "Raddr.Port": 0,
499 + Timestamp: "2023-11-07T16:32:58Z"
500 + },
501 + {
502 + Pid: 4,
503 + Name: "System",
504 + Family: "IPv4",
505 + Type: "TCP",
506 + Status: "LISTEN",
507 + "Laddr.IP": "0.0.0.0",
508 + "Laddr.Port": 47001,
509 + "Raddr.IP": "0.0.0.0",
510 + "Raddr.Port": 0,
511 + Timestamp: "2023-11-07T16:32:56Z"
512 + },
513 + {
514 + Pid: 928,
515 + Name: "svchost.exe",
516 + Family: "IPv6",
517 + Type: "TCP",
518 + Status: "LISTEN",
519 + "Laddr.IP": "::",
520 + "Laddr.Port": 135,
521 + "Raddr.IP": "::",
522 + "Raddr.Port": 0,
523 + Timestamp: "2023-11-07T16:32:23Z"
524 + },
525 + {
526 + Pid: 4,
527 + Name: "System",
528 + Family: "IPv6",
529 + Type: "TCP",
530 + Status: "LISTEN",
531 + "Laddr.IP": "::",
532 + "Laddr.Port": 445,
533 + "Raddr.IP": "::",
534 + "Raddr.Port": 0,
535 + Timestamp: "2023-11-07T16:32:58Z"
536 + },
537 + {
538 + Pid: 344,
539 + Name: "svchost.exe",
540 + Family: "IPv6",
541 + Type: "TCP",
542 + Status: "LISTEN",
543 + "Laddr.IP": "::",
544 + "Laddr.Port": 3389,
545 + "Raddr.IP": "::",
546 + "Raddr.Port": 0,
547 + Timestamp: "2023-11-07T16:32:28Z"
548 + },
549 + {
550 + Pid: 4,
551 + Name: "System",
552 + Family: "IPv6",
553 + Type: "TCP",
554 + Status: "LISTEN",
555 + "Laddr.IP": "::",
556 + "Laddr.Port": 5357,
557 + "Raddr.IP": "::",
558 + "Raddr.Port": 0,
559 + Timestamp: "2023-11-07T16:32:57Z"
560 + },
561 + {
562 + Pid: 4,
563 + Name: "System",
564 + Family: "IPv6",
565 + Type: "TCP",
566 + Status: "LISTEN",
567 + "Laddr.IP": "::",
568 + "Laddr.Port": 5985,
569 + "Raddr.IP": "::",
570 + "Raddr.Port": 0,
571 + Timestamp: "2023-11-07T16:32:58Z"
572 + },
573 + {
574 + Pid: 4,
575 + Name: "System",
576 + Family: "IPv6",
577 + Type: "TCP",
578 + Status: "LISTEN",
579 + "Laddr.IP": "::",
580 + "Laddr.Port": 47001,
581 + "Raddr.IP": "::",
582 + "Raddr.Port": 0,
583 + Timestamp: "2023-11-07T16:32:56Z"
584 + },
585 + {
586 + Pid: 540,
587 + Name: "wininit.exe",
588 + Family: "IPv6",
589 + Type: "TCP",
590 + Status: "LISTEN",
591 + "Laddr.IP": "::",
592 + "Laddr.Port": 49664,
593 + "Raddr.IP": "::",
594 + "Raddr.Port": 0,
595 + Timestamp: "2023-11-07T16:32:23Z"
596 + },
597 + {
598 + Pid: 1188,
599 + Name: "svchost.exe",
600 + Family: "IPv6",
601 + Type: "TCP",
602 + Status: "LISTEN",
603 + "Laddr.IP": "::",
604 + "Laddr.Port": 49665,
605 + "Raddr.IP": "::",
606 + "Raddr.Port": 0,
607 + Timestamp: "2023-11-07T16:32:28Z"
608 + },
609 + {
610 + Pid: 680,
611 + Name: "lsass.exe",
612 + Family: "IPv6",
613 + Type: "TCP",
614 + Status: "LISTEN",
615 + "Laddr.IP": "::",
616 + "Laddr.Port": 49666,
617 + "Raddr.IP": "::",
618 + "Raddr.Port": 0,
619 + Timestamp: "2023-11-07T16:32:31Z"
620 + },
621 + {
622 + Pid: 464,
623 + Name: "svchost.exe",
624 + Family: "IPv6",
625 + Type: "TCP",
626 + Status: "LISTEN",
627 + "Laddr.IP": "::",
628 + "Laddr.Port": 49667,
629 + "Raddr.IP": "::",
630 + "Raddr.Port": 0,
631 + Timestamp: "2023-11-07T16:32:33Z"
632 + },
633 + {
634 + Pid: 464,
635 + Name: "svchost.exe",
636 + Family: "IPv6",
637 + Type: "TCP",
638 + Status: "LISTEN",
639 + "Laddr.IP": "::",
640 + "Laddr.Port": 49668,
641 + "Raddr.IP": "::",
642 + "Raddr.Port": 0,
643 + Timestamp: "2023-11-07T16:32:33Z"
644 + },
645 + {
646 + Pid: 1524,
647 + Name: "spoolsv.exe",
648 + Family: "IPv6",
649 + Type: "TCP",
650 + Status: "LISTEN",
651 + "Laddr.IP": "::",
652 + "Laddr.Port": 49670,
653 + "Raddr.IP": "::",
654 + "Raddr.Port": 0,
655 + Timestamp: "2023-11-07T16:32:55Z"
656 + },
657 + {
658 + Pid: 660,
659 + Name: "services.exe",
660 + Family: "IPv6",
661 + Type: "TCP",
662 + Status: "LISTEN",
663 + "Laddr.IP": "::",
664 + "Laddr.Port": 49676,
665 + "Raddr.IP": "::",
666 + "Raddr.Port": 0,
667 + Timestamp: "2023-11-07T16:33:00Z"
668 + },
669 + {
670 + Pid: 2140,
671 + Name: "svchost.exe",
672 + Family: "IPv4",
673 + Type: "UDP",
674 + Status: "",
675 + "Laddr.IP": "0.0.0.0",
676 + "Laddr.Port": 123,
677 + "Raddr.IP": "",
678 + "Raddr.Port": 0,
679 + Timestamp: "2023-11-07T16:32:56Z"
680 + },
681 + {
682 + Pid: 4,
683 + Name: "System",
684 + Family: "IPv4",
685 + Type: "UDP",
686 + Status: "",
687 + "Laddr.IP": "192.168.200.3",
688 + "Laddr.Port": 137,
689 + "Raddr.IP": "",
690 + "Raddr.Port": 0,
691 + Timestamp: "2023-11-07T16:32:31Z"
692 + },
693 + {
694 + Pid: 4,
695 + Name: "System",
696 + Family: "IPv4",
697 + Type: "UDP",
698 + Status: "",
699 + "Laddr.IP": "192.168.200.3",
700 + "Laddr.Port": 138,
701 + "Raddr.IP": "",
702 + "Raddr.Port": 0,
703 + Timestamp: "2023-11-07T16:32:31Z"
704 + },
705 + {
706 + Pid: 344,
707 + Name: "svchost.exe",
708 + Family: "IPv4",
709 + Type: "UDP",
710 + Status: "",
711 + "Laddr.IP": "0.0.0.0",
712 + "Laddr.Port": 3389,
713 + "Raddr.IP": "",
714 + "Raddr.Port": 0,
715 + Timestamp: "2023-11-07T16:32:28Z"
716 + },
717 + {
718 + Pid: 2384,
719 + Name: "svchost.exe",
720 + Family: "IPv4",
721 + Type: "UDP",
722 + Status: "",
723 + "Laddr.IP": "0.0.0.0",
724 + "Laddr.Port": 3702,
725 + "Raddr.IP": "",
726 + "Raddr.Port": 0,
727 + Timestamp: "2023-11-07T16:32:57Z"
728 + },
729 + {
730 + Pid: 2384,
731 + Name: "svchost.exe",
732 + Family: "IPv4",
733 + Type: "UDP",
734 + Status: "",
735 + "Laddr.IP": "0.0.0.0",
736 + "Laddr.Port": 3702,
737 + "Raddr.IP": "",
738 + "Raddr.Port": 0,
739 + Timestamp: "2023-11-07T16:32:57Z"
740 + },
741 + {
742 + Pid: 1424,
743 + Name: "svchost.exe",
744 + Family: "IPv4",
745 + Type: "UDP",
746 + Status: "",
747 + "Laddr.IP": "0.0.0.0",
748 + "Laddr.Port": 5353,
749 + "Raddr.IP": "",
750 + "Raddr.Port": 0,
751 + Timestamp: "2023-11-07T16:32:31Z"
752 + },
753 + {
754 + Pid: 1424,
755 + Name: "svchost.exe",
756 + Family: "IPv4",
757 + Type: "UDP",
758 + Status: "",
759 + "Laddr.IP": "0.0.0.0",
760 + "Laddr.Port": 5355,
761 + "Raddr.IP": "",
762 + "Raddr.Port": 0,
763 + Timestamp: "2023-11-13T20:35:42Z"
764 + },
765 + {
766 + Pid: 2384,
767 + Name: "svchost.exe",
768 + Family: "IPv4",
769 + Type: "UDP",
770 + Status: "",
771 + "Laddr.IP": "0.0.0.0",
772 + "Laddr.Port": 49866,
773 + "Raddr.IP": "",
774 + "Raddr.Port": 0,
775 + Timestamp: "2023-11-07T16:32:57Z"
776 + },
777 + {
778 + Pid: 464,
779 + Name: "svchost.exe",
780 + Family: "IPv4",
781 + Type: "UDP",
782 + Status: "",
783 + "Laddr.IP": "127.0.0.1",
784 + "Laddr.Port": 50471,
785 + "Raddr.IP": "",
786 + "Raddr.Port": 0,
787 + Timestamp: "2023-11-07T16:32:59Z"
788 + },
789 + {
790 + Pid: 2140,
791 + Name: "svchost.exe",
792 + Family: "IPv6",
793 + Type: "UDP",
794 + Status: "",
795 + "Laddr.IP": "::",
796 + "Laddr.Port": 123,
797 + "Raddr.IP": "",
798 + "Raddr.Port": 0,
799 + Timestamp: "2023-11-07T16:32:56Z"
800 + },
801 + {
802 + Pid: 344,
803 + Name: "svchost.exe",
804 + Family: "IPv6",
805 + Type: "UDP",
806 + Status: "",
807 + "Laddr.IP": "::",
808 + "Laddr.Port": 3389,
809 + "Raddr.IP": "",
810 + "Raddr.Port": 0,
811 + Timestamp: "2023-11-07T16:32:28Z"
812 + },
813 + {
814 + Pid: 2384,
815 + Name: "svchost.exe",
816 + Family: "IPv6",
817 + Type: "UDP",
818 + Status: "",
819 + "Laddr.IP": "::",
820 + "Laddr.Port": 3702,
821 + "Raddr.IP": "",
822 + "Raddr.Port": 0,
823 + Timestamp: "2023-11-07T16:32:57Z"
824 + },
825 + {
826 + Pid: 2384,
827 + Name: "svchost.exe",
828 + Family: "IPv6",
829 + Type: "UDP",
830 + Status: "",
831 + "Laddr.IP": "::",
832 + "Laddr.Port": 3702,
833 + "Raddr.IP": "",
834 + "Raddr.Port": 0,
835 + Timestamp: "2023-11-07T16:32:57Z"
836 + },
837 + {
838 + Pid: 1424,
839 + Name: "svchost.exe",
840 + Family: "IPv6",
841 + Type: "UDP",
842 + Status: "",
843 + "Laddr.IP": "::",
844 + "Laddr.Port": 5353,
845 + "Raddr.IP": "",
846 + "Raddr.Port": 0,
847 + Timestamp: "2023-11-07T16:32:31Z"
848 + },
849 + {
850 + Pid: 1424,
851 + Name: "svchost.exe",
852 + Family: "IPv6",
853 + Type: "UDP",
854 + Status: "",
855 + "Laddr.IP": "::",
856 + "Laddr.Port": 5355,
857 + "Raddr.IP": "",
858 + "Raddr.Port": 0,
859 + Timestamp: "2023-11-13T20:35:42Z"
860 + },
861 + {
862 + Pid: 2384,
863 + Name: "svchost.exe",
864 + Family: "IPv6",
865 + Type: "UDP",
866 + Status: "",
867 + "Laddr.IP": "::",
868 + "Laddr.Port": 49867,
869 + "Raddr.IP": "",
870 + "Raddr.Port": 0,
871 + Timestamp: "2023-11-07T16:32:57Z"
872 + }
873 +]
874 +
875 +const commandResult = [
876 + {
877 + Stdout: "\r\n\r\nAccountType : 512\r\nCaption : WIN-HFOU106TD7K\\Administrator\r\nDomain : WIN-HFOU106TD7K\r\nSID : S-1-5-21-1287727892-3649796646-2502983199-500\r\nFullName : \r\nName : Administrator\r\n\r\nAccountType : 512\r\nCaption : WIN-HFOU106TD7K\\DefaultAccount\r\nDomain : WIN-HFOU106TD7K\r\nSID : S-1-5-21-1287727892-3649796646-2502983199-503\r\nFullName : \r\nName : DefaultAccount\r\n\r\nAccountType : 512\r\nCaption : WIN-HFOU106TD7K\\Guest\r\nDomain : WIN-HFOU106TD7K\r\nSID : S-1-5-21-1287727892-3649796646-2502983199-501\r\nFullName : \r\nName : Guest\r\n\r\nAccountType : 512\r\nCaption : WIN-HFOU106TD7K\\WDAGUtilityAccount\r\nDomain : WIN-HFOU106TD7K\r\nSID : S-1-5-21-1287727892-3649796646-2502983199-504\r\nFullName : \r\nName : WDAGUtilityAccount\r\n\r\n\r\n\r\n",
878 + Stderr: '#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj></Objs>',
879 + ReturnCode: 0,
880 + Complete: true
881 + }
882 +]
883 +
884 +const quarantineResult = [
885 + {
886 + Time: "2023-11-14T21:03:03Z",
887 + Result: "VelociraptorQuarantine IPSec policy removed."
888 + },
889 + {
890 + Time: "2023-11-14T21:03:03Z",
891 + Result: "VelociraptorQuarantine IPSec policy created."
892 + },
893 + {
894 + Time: "2023-11-14T21:03:03Z",
895 + Result: "Entry added: netsh ipsec static add filter filterlist=VelociraptorQuarantine PermitFilterList srcaddr=me srcport=0 dstaddr=velo.socfortress.co dstport=8000 protocol=tcp mirrored=yes description=VelociraptorFrontEnd"
896 + },
897 + {
898 + Time: "2023-11-14T21:03:04Z",
899 + Result: "Entry added: netsh ipsec static add filter filterlist=VelociraptorQuarantine PermitFilterList srcaddr=me srcport=0 dstaddr=any dstport=53 protocol=udp mirrored=yes description=DNS"
900 + },
901 + {
902 + Time: "2023-11-14T21:03:04Z",
903 + Result: "Entry added: netsh ipsec static add filter filterlist=VelociraptorQuarantine PermitFilterList srcaddr=me srcport=0 dstaddr=any dstport=53 protocol=tcp mirrored=yes description=DNS TCP"
904 + },
905 + {
906 + Time: "2023-11-14T21:03:04Z",
907 + Result: "Entry added: netsh ipsec static add filter filterlist=VelociraptorQuarantine PermitFilterList srcaddr=me srcport=68 dstaddr=any dstport=67 protocol=udp mirrored=yes description=DHCP"
908 + },
909 + {
910 + Time: "2023-11-14T21:03:04Z",
911 + Result: "Entry added: netsh ipsec static add filter filterlist=VelociraptorQuarantine BlockFilterList srcaddr=any dstaddr=any mirrored=yes description=All other traffic"
912 + },
913 + {
914 + Time: "2023-11-14T21:03:04Z",
915 + Result: "FilterAction added: netsh ipsec static add filteraction name=VelociraptorQuarantine PermitAction action=Permit"
916 + },
917 + {
918 + Time: "2023-11-14T21:03:04Z",
919 + Result: "FilterAction added: netsh ipsec static add filteraction name=VelociraptorQuarantine BlockAction action=Block"
920 + },
921 + {
922 + Time: "2023-11-14T21:03:05Z",
923 + Result: "Rule added: netsh ipsec static add rule name=VelociraptorQuarantine PermitRule policy=VelociraptorQuarantine filterlist=VelociraptorQuarantine PermitFilterList filteraction=VelociraptorQuarantine PermitAction"
924 + },
925 + {
926 + Time: "2023-11-14T21:03:05Z",
927 + Result: "Rule added: netsh ipsec static add rule name=VelociraptorQuarantine BlockRule policy=VelociraptorQuarantine filterlist=VelociraptorQuarantine BlockFilterList filteraction=VelociraptorQuarantine BlockAction"
928 + },
929 + {
930 + Time: "2023-11-14T21:03:05Z",
931 + Result: "VelociraptorQuarantine IPSec policy applied."
932 + },
933 + {
934 + Time: "2023-11-14T21:03:05Z",
935 + Result: "VelociraptorQuarantine connection test successful."
936 + }
937 +]
938 +
939 +export { collectResult, commandResult, quarantineResult }
src/components/graylog/Alerts/Item.vue
+1
@@ -121,6 +121,7 @@ function gotoEventsPage(event_definition_id: string) {
121 border-radius: var(--border-radius);
122 background-color: var(--bg-color);
123 transition: all 0.2s var(--bezier-ease);
124 + border: var(--border-small-050);
125
126 .header-box {
127 font-family: var(--font-family-mono);
src/components/graylog/Alerts/List.vue
+2 -1
@@ -61,7 +61,7 @@
61 />
62 </template>
63 <template v-else>
64 - <n-empty description="No items found" v-if="!loading" />
64 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
65 </template>
66 </div>
67 <div class="footer flex justify-end">
@@ -214,5 +214,6 @@ onBeforeMount(() => {
214 <style lang="scss" scoped>
215 .list {
216 container-type: inline-size;
217 + min-height: 200px;
218 }
219 </style>
src/components/graylog/Events/Item.vue
+1
@@ -92,6 +92,7 @@ const showDetails = ref(false)
92 border-radius: var(--border-radius);
93 background-color: var(--bg-color);
94 transition: all 0.2s var(--bezier-ease);
95 + border: var(--border-small-050);
96
97 .header-box {
98 font-family: var(--font-family-mono);
src/components/graylog/Events/List.vue
+2 -1
@@ -40,7 +40,7 @@
40 />
41 </template>
42 <template v-else>
43 - <n-empty description="No items found" v-if="!loading" />
43 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
44 </template>
45 </div>
46 </n-spin>
@@ -140,5 +140,6 @@ onBeforeMount(() => {
140 <style lang="scss" scoped>
141 .list {
142 container-type: inline-size;
143 + min-height: 200px;
144 }
145 </style>
src/components/graylog/Inputs/List.vue
+2 -1
@@ -45,7 +45,7 @@
45 />
46 </template>
47 <template v-else>
48 - <n-empty description="No items found" v-if="!loading" />
48 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
49 </template>
50 </div>
51 </n-scrollbar>
@@ -167,5 +167,6 @@ onBeforeMount(() => {
167 padding-bottom: 0;
168 container-type: inline-size;
169 box-sizing: border-box;
170 + min-height: 200px;
171 }
172 </style>
src/components/graylog/Messages/Item.vue
+1
@@ -32,6 +32,7 @@ function formatDate(timestamp: string): string {
32 border-radius: var(--border-radius);
33 background-color: var(--bg-color);
34 transition: all 0.2s var(--bezier-ease);
35 + border: var(--border-small-050);
36
37 .header-box {
38 font-family: var(--font-family-mono);
src/components/graylog/Messages/List.vue
+2 -1
@@ -27,7 +27,7 @@
27 <MessageItem v-for="msg of messages" :key="msg.id" :message="msg" class="mb-2" />
28 </template>
29 <template v-else>
30 - <n-empty description="No items found" v-if="!loading" />
30 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
31 </template>
32 </div>
33 <div class="footer flex justify-end">
@@ -98,5 +98,6 @@ onBeforeMount(() => {
98 <style lang="scss" scoped>
99 .list {
100 container-type: inline-size;
101 + min-height: 200px;
102 }
103 </style>
src/components/graylog/Metrics/List.vue
+1
@@ -87,6 +87,7 @@ function sanitizeMetrics(metrics: ThroughputMetric[]): Metrics[] {
87 .metrics-list {
88 .metrics-group {
89 @apply mb-6;
90 + overflow: hidden;
91
92 .list {
93 background-color: var(--bg-secondary-color);
src/components/graylog/Pipelines/RulesList.vue
+1
@@ -99,6 +99,7 @@ onBeforeMount(() => {
99 padding: var(--n-body-padding);
100 container-type: inline-size;
101 box-sizing: border-box;
102 + min-height: 200px;
103 }
104 }
105 </style>
src/components/graylog/Streams/Item.vue
+1
@@ -156,6 +156,7 @@ function start() {
156 border-radius: var(--border-radius);
157 background-color: var(--bg-color);
158 transition: all 0.2s var(--bezier-ease);
159 + border: var(--border-small-050);
160
161 .header-box {
162 font-family: var(--font-family-mono);
src/components/graylog/Streams/List.vue
+2 -1
@@ -75,7 +75,7 @@
75 <StreamItem v-for="stream of itemsPaginated" :key="stream.id" :stream="stream" class="mb-2" />
76 </template>
77 <template v-else>
78 - <n-empty description="No items found" v-if="!loading" />
78 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
79 </template>
80 </div>
81 <div class="footer flex justify-end">
@@ -190,5 +190,6 @@ onBeforeMount(() => {
190 <style lang="scss" scoped>
191 .list {
192 container-type: inline-size;
193 + min-height: 200px;
194 }
195 </style>
src/layouts/common/Navbar/items.tsx
+14
@@ -126,6 +126,20 @@ export default function getItems(mode: "vertical" | "horizontal", collapsed: boo
126 key: "Alerts",
127 icon: renderIcon(BlankIcon)
128 },
129 + {
130 + label: () =>
131 + h(
132 + RouterLink,
133 + {
134 + to: {
135 + name: "Artifacts"
136 + }
137 + },
138 + { default: () => "Artifacts" }
139 + ),
140 + key: "Artifacts",
141 + icon: renderIcon(BlankIcon)
142 + },
143 {
144 type: "divider"
145 },
src/router/index.ts
+6
@@ -70,6 +70,12 @@ const router = createRouter({
70 component: () => import("@/views/socfortress/Alerts.vue"),
71 meta: { title: "Alerts", auth: true, roles: UserRole.All }
72 },
73 + {
74 + path: "/artifacts",
75 + name: "Artifacts",
76 + component: () => import("@/views/socfortress/Artifacts.vue"),
77 + meta: { title: "Artifacts", auth: true, roles: UserRole.All }
78 + },
79
80 // DEMO PAGES ==========================================================
81
src/stores/settings.ts
+3 -1
@@ -35,7 +35,9 @@ export const useSettingsStore = defineStore("settings", {
35 return {
36 date: `${date}`,
37 datetime: `${date}${separator}${time}`,
38 - datetimesec: `${date}${separator}${timesec}`
38 + datetimesec: `${date}${separator}${timesec}`,
39 + time,
40 + timesec
41 }
42 }
43 },
src/types/agents.d.ts
+1
@@ -14,6 +14,7 @@ export interface Agent {
14 customer_code: null | string
15 vulnerabilities?: AgentVulnerabilities[]
16 online?: boolean
17 + quarantined?: boolean
18 }
19
20 export interface AgentVulnerabilities {
src/types/artifacts.d.ts new
+20
@@ -0,0 +1,20 @@
1 +export interface Artifact {
2 + description: string
3 + name: string
4 +}
5 +
6 +export interface CollectResult {
7 + [key: string]: any
8 +}
9 +
10 +export interface CommandResult {
11 + Stdout: string
12 + Stderr: string
13 + ReturnCode: number
14 + Complete: boolean
15 +}
16 +
17 +export interface QuarantineResult {
18 + Time: string
19 + Result: string
20 +}
src/utils/dayjs.ts
+3 -3
@@ -2,16 +2,16 @@ import dayjs from "dayjs"
2 import timezone from "dayjs/plugin/timezone"
3 import locale_en from "dayjs/locale/en.js"
4 import customParseFormat from "dayjs/plugin/customParseFormat"
5 +import duration from "dayjs/plugin/duration"
6 +import relativeTime from "dayjs/plugin/relativeTime"
7 /*
8 import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
9 import utc from "dayjs/plugin/utc"
8 -import duration from "dayjs/plugin/duration"
9 -import relativeTime from "dayjs/plugin/relativeTime"
10 dayjs.extend(isSameOrAfter)
11 dayjs.extend(utc)
12 +*/
13 dayjs.extend(relativeTime)
14 dayjs.extend(duration)
14 -*/
15 dayjs.extend(customParseFormat)
16 dayjs.extend(timezone)
17 dayjs.locale(locale_en)
src/views/socfortress/AgentOverview.vue
+79 -23
@@ -30,16 +30,23 @@
30 </template>
31 </n-tooltip>
32 </div>
33 +
34 <h1 v-if="agent?.hostname">
35 {{ agent?.hostname }}
36 </h1>
37 +
38 <span class="online-badge" v-if="isOnline">ONLINE</span>
39 +
40 + <span class="quarantined-badge flex items-center gap-1" v-if="isQuarantined">
41 + <Icon :name="QuarantinedIcon" :size="15"></Icon>
42 + <span>QUARANTINED</span>
43 + </span>
44 </div>
45 <div class="label text-secondary-color mt-2">Agent #{{ agent?.agent_id }}</div>
46 </n-spin>
40 - <n-card class="p-2" content-style="padding:0">
47 + <n-card class="py-1 px-4 pb-4" content-style="padding:0">
48 <n-spin :show="loadingAgent">
42 - <n-tabs type="segment" animated default-value="Overview">
49 + <n-tabs type="line" animated default-value="Overview">
50 <n-tab-pane name="Overview" tab="Overview" display-directive="show">
51 <div class="section">
52 <OverviewSection v-if="agent" :agent="agent" />
@@ -55,6 +62,31 @@
62 <AlertsList v-if="agent" :agent-hostname="agent.hostname" />
63 </div>
64 </n-tab-pane>
65 + <n-tab-pane name="collect" tab="Collect" display-directive="show:lazy">
66 + <ArtifactsCollect
67 + v-if="agent"
68 + @loaded-artifacts="artifacts = $event"
69 + :agent-hostname="agent.hostname"
70 + :artifacts="artifacts"
71 + />
72 + </n-tab-pane>
73 + <n-tab-pane name="command" tab="Command" display-directive="show:lazy">
74 + <ArtifactsCommand
75 + v-if="agent"
76 + @loaded-artifacts="artifacts = $event"
77 + :agent-hostname="agent.hostname"
78 + :artifacts="artifacts"
79 + />
80 + </n-tab-pane>
81 + <n-tab-pane name="quarantine" tab="Quarantine" display-directive="show:lazy">
82 + <ArtifactsQuarantine
83 + v-if="agent"
84 + @action-performed="getAgent()"
85 + @loaded-artifacts="artifacts = $event"
86 + :agent-hostname="agent.hostname"
87 + :artifacts="artifacts"
88 + />
89 + </n-tab-pane>
90 </n-tabs>
91 </n-spin>
92 </n-card>
@@ -62,7 +94,7 @@
94 </template>
95
96 <script setup lang="ts">
65 -import { ref, onBeforeMount, computed } from "vue"
97 +import { ref, onBeforeMount, computed, nextTick } from "vue"
98 import { useRoute } from "vue-router"
99 import Api from "@/api"
100 import { type Agent } from "@/types/agents.d"
@@ -73,8 +105,13 @@ import AlertsList from "@/components/alerts/AlertsList.vue"
105 import OverviewSection from "@/components/agents/OverviewSection.vue"
106 import { useMessage, NSpin, NTooltip, NButton, NTabs, NTabPane, NCard, useDialog } from "naive-ui"
107 import Icon from "@/components/common/Icon.vue"
108 +import type { Artifact } from "@/types/artifacts.d"
109 +import ArtifactsCollect from "@/components/artifacts/ArtifactsCollect.vue"
110 +import ArtifactsCommand from "@/components/artifacts/ArtifactsCommand.vue"
111 +import ArtifactsQuarantine from "@/components/artifacts/ArtifactsQuarantine.vue"
112
113 const StarIcon = "carbon:star"
114 +const QuarantinedIcon = "ph:seal-warning-light"
115 const ArrowIcon = "carbon:arrow-left"
116
117 const message = useMessage()
@@ -83,31 +120,40 @@ const dialog = useDialog()
120 const route = useRoute()
121 const loadingAgent = ref(false)
122 const agent = ref<Agent | null>(null)
123 +const agentId = ref<string | null>(null)
124 +
125 +const artifacts = ref<Artifact[]>([])
126
127 const isOnline = computed(() => {
128 return isAgentOnline(agent.value?.wazuh_last_seen ?? "")
129 })
130
91 -function getAgent(id: string) {
92 - loadingAgent.value = true
131 +const isQuarantined = computed(() => {
132 + return !!agent.value?.quarantined
133 +})
134
94 - Api.agents
95 - .getAgents(id)
96 - .then(res => {
97 - if (res.data.success) {
98 - agent.value = res.data.agents[0] || null
99 - } else {
100 - message.error(res.data?.message || "An error occurred. Please try again later.")
135 +function getAgent() {
136 + if (agentId.value) {
137 + loadingAgent.value = true
138 +
139 + Api.agents
140 + .getAgents(agentId.value)
141 + .then(res => {
142 + if (res.data.success) {
143 + agent.value = res.data.agents[0] || null
144 + } else {
145 + message.error(res.data?.message || "An error occurred. Please try again later.")
146 + router.push(`/agents`).catch(() => {})
147 + }
148 + })
149 + .catch(err => {
150 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
151 router.push(`/agents`).catch(() => {})
102 - }
103 - })
104 - .catch(err => {
105 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
106 - router.push(`/agents`).catch(() => {})
107 - })
108 - .finally(() => {
109 - loadingAgent.value = false
110 - })
152 + })
153 + .finally(() => {
154 + loadingAgent.value = false
155 + })
156 + }
157 }
158
159 function toggleCritical(agentId: string, criticalStatus: boolean) {
@@ -154,7 +200,11 @@ function gotoAgents() {
200
201 onBeforeMount(() => {
202 if (route.params.id) {
157 - getAgent(route.params.id.toString())
203 + agentId.value = route.params.id.toString()
204 +
205 + nextTick(() => {
206 + getAgent()
207 + })
208 } else {
209 router.replace(`/agents`).catch(() => {})
210 }
@@ -212,13 +262,19 @@ onBeforeMount(() => {
262 display: flex;
263 align-items: center;
264 }
215 - .online-badge {
265 + .online-badge,
266 + .quarantined-badge {
267 border: 2px solid var(--primary-color);
268 color: var(--primary-color);
269 font-weight: bold;
270 border-radius: var(--border-radius);
271 @apply text-xs py-1 px-2;
272 }
273 +
274 + .quarantined-badge {
275 + border-color: var(--warning-color);
276 + color: var(--warning-color);
277 + }
278 }
279
280 &.critical {
src/views/socfortress/Artifacts.vue new
+51
@@ -0,0 +1,51 @@
1 +<template>
2 + <div class="page">
3 + <n-tabs type="line" animated v-model:value="activeTab">
4 + <n-tab-pane name="artifacts" tab="Artifacts" display-directive="show:lazy">
5 + <ArtifactsList @loaded-agents="agents = $event" @loaded-artifacts="artifacts = $event" />
6 + </n-tab-pane>
7 + <n-tab-pane name="collect" tab="Collect" display-directive="show:lazy">
8 + <ArtifactsCollect
9 + @loaded-agents="agents = $event"
10 + @loaded-artifacts="artifacts = $event"
11 + :agents="agents"
12 + :artifacts="artifacts"
13 + />
14 + </n-tab-pane>
15 + <n-tab-pane name="command" tab="Command" display-directive="show:lazy">
16 + <ArtifactsCommand
17 + @loaded-agents="agents = $event"
18 + @loaded-artifacts="artifacts = $event"
19 + :agents="agents"
20 + :artifacts="artifacts"
21 + />
22 + </n-tab-pane>
23 + <n-tab-pane name="quarantine" tab="Quarantine" display-directive="show:lazy">
24 + <ArtifactsQuarantine
25 + @loaded-agents="agents = $event"
26 + @loaded-artifacts="artifacts = $event"
27 + :agents="agents"
28 + :artifacts="artifacts"
29 + />
30 + </n-tab-pane>
31 + </n-tabs>
32 + </div>
33 +</template>
34 +
35 +<script setup lang="ts">
36 +import { ref } from "vue"
37 +import { NTabs, NTabPane } from "naive-ui"
38 +import ArtifactsList from "@/components/artifacts/ArtifactsList.vue"
39 +import ArtifactsCollect from "@/components/artifacts/ArtifactsCollect.vue"
40 +import ArtifactsCommand from "@/components/artifacts/ArtifactsCommand.vue"
41 +import ArtifactsQuarantine from "@/components/artifacts/ArtifactsQuarantine.vue"
42 +import type { Artifact } from "@/types/artifacts.d"
43 +import type { Agent } from "@/types/agents.d"
44 +
45 +const artifacts = ref<Artifact[]>([])
46 +const agents = ref<Agent[]>([])
47 +
48 +const activeTab = ref<string | undefined>(undefined)
49 +</script>
50 +
51 +<style lang="scss" scoped></style>
src/views/socfortress/graylog/Pipelines.vue
+1 -1
@@ -10,7 +10,7 @@
10 </div>
11 <n-card>
12 <n-spin :show="loading">
13 - <n-collapse v-model:expanded-names="selectedPipeline" accordion>
13 + <n-collapse v-model:expanded-names="selectedPipeline" accordion style="min-height: 100px">
14 <n-collapse-item :title="pipe.title" :name="pipe.id" v-for="pipe of pipelines" :key="pipe.id">
15 <template #header>
16 <PipeTitle :pipeline="pipe" />