From 33bc81131a2348db4e465282bf5d7994e3bc3b75 Mon Sep 17 00:00:00 2001 From: taylor_socfortress <111797488+taylorwalton@users.noreply.github.com> Date: Sat, 18 May 2024 16:23:27 -0500 Subject: [PATCH] Manual update agent client (#217) * feat: Add optional fields for Velociraptor agent in Agents model The code changes in `universal_models.py` modify the `Agents` model to include optional fields for the Velociraptor agent. The `velociraptor_id` and `velociraptor_last_seen` fields are now optional, allowing for cases where the Velociraptor agent is not present. Similarly, the `velociraptor_agent_version` field is also made optional. This change improves the flexibility of the model and accommodates scenarios where the Velociraptor agent may not be used. Note: This commit message follows the established convention of using a prefix to indicate the type of change (`feat` for a new feature) and provides a clear and concise description of the changes made. * agent rewrite * feat: Add endpoint to update agent's Velociraptor ID The code changes in `agents.py` add a new endpoint `/update` to update an agent's Velociraptor ID. This endpoint requires the `agent_id` and `velociraptor_id` as parameters and updates the corresponding agent's `velociraptor_id` field in the database. If the agent is not found, a 404 error is returned. This feature improves the functionality of the application by allowing users to easily update the Velociraptor ID of an agent. Note: This commit message follows the established convention of using a prefix to indicate the type of change (`feat` for a new feature) and provides a clear and concise description of the changes made. * feat: Refactor agent_sync to remove unnecessary session parameter The code changes in `agent_sync.py` remove the unnecessary `session` parameter from the `sync_all_agents` function. Since the function is now using the `get_db_session` context manager, there is no need to pass the session as a parameter. This refactor simplifies the code and improves readability. Note: This commit message follows the established convention of using a prefix to indicate the type of change (`feat` for a new feature) and provides a clear and concise description of the changes made. * updated dependencies * updated agent api * added AgentVelociraptorIdForm * precommit fixes --------- Co-authored-by: Davide Di Modica --- .../39c3aaec0084_new_agents_table_schema.py | 35 +++ backend/app/agents/routes/agents.py | 60 ++++- backend/app/agents/schema/agents.py | 4 + backend/app/agents/services/sync.py | 232 ++++++++++++----- .../app/agents/velociraptor/schema/agents.py | 36 +++ .../agents/velociraptor/services/agents.py | 72 ++++++ .../velociraptor/utils/universal.py | 34 +++ backend/app/db/universal_models.py | 80 +++++- backend/app/schedulers/services/agent_sync.py | 2 +- frontend/package-lock.json | 237 ++++++++---------- frontend/package.json | 12 +- frontend/src/api/agents.ts | 50 ++-- .../agents/AgentVelociraptorIdForm.vue | 92 +++++++ .../src/components/agents/OverviewSection.vue | 27 +- frontend/src/components/common/Markdown.vue | 1 + frontend/src/views/agents/Overview.vue | 9 +- frontend/src/vite-env.d.ts | 2 - 17 files changed, 733 insertions(+), 252 deletions(-) create mode 100644 backend/alembic/versions/39c3aaec0084_new_agents_table_schema.py create mode 100644 frontend/src/components/agents/AgentVelociraptorIdForm.vue diff --git a/backend/alembic/versions/39c3aaec0084_new_agents_table_schema.py b/backend/alembic/versions/39c3aaec0084_new_agents_table_schema.py new file mode 100644 index 00000000..f56c4e4a --- /dev/null +++ b/backend/alembic/versions/39c3aaec0084_new_agents_table_schema.py @@ -0,0 +1,35 @@ +"""New agents table schema + +Revision ID: 39c3aaec0084 +Revises: ec63589cc24d +Create Date: 2024-05-17 17:17:39.140779 + +""" +from typing import Sequence +from typing import Union + +from sqlalchemy.dialects import mysql + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "39c3aaec0084" +down_revision: Union[str, None] = "ec63589cc24d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("agents", "velociraptor_id", existing_type=mysql.VARCHAR(length=256), nullable=True) + op.alter_column("agents", "velociraptor_last_seen", existing_type=mysql.DATETIME(), nullable=True) + op.alter_column("agents", "velociraptor_agent_version", existing_type=mysql.VARCHAR(length=256), nullable=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column("agents", "velociraptor_agent_version", existing_type=mysql.VARCHAR(length=256), nullable=False) + op.alter_column("agents", "velociraptor_last_seen", existing_type=mysql.DATETIME(), nullable=False) + op.alter_column("agents", "velociraptor_id", existing_type=mysql.VARCHAR(length=256), nullable=False) + # ### end Alembic commands ### diff --git a/backend/app/agents/routes/agents.py b/backend/app/agents/routes/agents.py index 7b093ff5..2485ff68 100644 --- a/backend/app/agents/routes/agents.py +++ b/backend/app/agents/routes/agents.py @@ -18,7 +18,8 @@ from app.agents.schema.agents import SyncedAgentsResponse from app.agents.services.status import get_outdated_agents_velociraptor from app.agents.services.status import get_outdated_agents_wazuh -from app.agents.services.sync import sync_agents +from app.agents.services.sync import sync_agents_velociraptor +from app.agents.services.sync import sync_agents_wazuh from app.agents.velociraptor.services.agents import delete_agent_velociraptor from app.agents.wazuh.schema.agents import WazuhAgentScaPolicyResultsResponse from app.agents.wazuh.schema.agents import WazuhAgentScaResponse @@ -223,10 +224,7 @@ async def get_agent_by_hostname( Security(AuthHandler().require_any_scope("admin", "analyst", "scheduler")), ], ) -async def sync_all_agents( - # backgroud_tasks: BackgroundTasks, - session: AsyncSession = Depends(get_db), -) -> SyncedAgentsResponse: +async def sync_all_agents() -> SyncedAgentsResponse: """ Sync all agents from Wazuh Manager. @@ -241,10 +239,10 @@ async def sync_all_agents( - SyncedAgentsResponse: The response model indicating the success of the sync operation. """ - logger.info("Syncing agents from Wazuh Manager") - # backgroud_tasks.add_task(sync_agents, session) + logger.info("Syncing agents as part of scheduled job") loop = asyncio.get_event_loop() - loop.create_task(sync_agents(session=session)) + await loop.create_task(sync_agents_wazuh()) + await loop.create_task(sync_agents_velociraptor()) return SyncedAgentsResponse( success=True, message="Agents synced started successfully", @@ -474,7 +472,51 @@ async def get_outdated_velociraptor_agents( return await get_outdated_agents_velociraptor(session) -# ! TODO: FINISH THIS +@agents_router.put( + "/{agent_id}/update", + response_model=AgentModifyResponse, + description="Update agent", + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))], +) +async def update_agent( + agent_id: str, + velociraptor_id: str, + session: AsyncSession = Depends(get_db), +) -> AgentModifyResponse: + """ + Updates an agent's velociraptor_id + + Args: + agent_id (str): The ID of the agent to be updated. + velociraptor_id (str): The new velociraptor_id of the agent. + session (AsyncSession, optional): The database session. Defaults to Depends(get_db). + + Returns: + AgentModifyResponse: The response indicating the success or failure of the update. + """ + logger.info(f"Updating agent {agent_id} with Velociraptor ID: {velociraptor_id}") + try: + result = await session.execute(select(Agents).filter(Agents.agent_id == agent_id)) + agent = result.scalars().first() + if not agent: + raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found") + agent.velociraptor_id = velociraptor_id + await session.commit() + logger.info(f"Agent {agent_id} updated with Velociraptor ID: {velociraptor_id}") + return AgentModifyResponse( + success=True, + message=f"Agent {agent_id} updated with Velociraptor ID: {velociraptor_id}", + ) + except Exception as e: + if not agent: + raise HTTPException(status_code=404, detail=f"Agent with agent_id {agent_id} not found") + logger.error(f"Failed to update agent {agent_id} with Velociraptor ID: {velociraptor_id}: {e}") + raise HTTPException( + status_code=500, + detail=f"Failed to update agent {agent_id} with Velociraptor ID: {velociraptor_id}: {e}", + ) + + @agents_router.delete( "/{agent_id}/delete", response_model=AgentModifyResponse, diff --git a/backend/app/agents/schema/agents.py b/backend/app/agents/schema/agents.py index def6dc44..88570cf1 100644 --- a/backend/app/agents/schema/agents.py +++ b/backend/app/agents/schema/agents.py @@ -18,6 +18,10 @@ class SyncedAgent(WazuhAgent, VelociraptorAgent): pass +class SyncedWazuhAgent(WazuhAgent): + pass + + class SyncedAgentsResponse(BaseModel): # agents_added: List[SyncedAgent] success: bool diff --git a/backend/app/agents/services/sync.py b/backend/app/agents/services/sync.py index 34030295..6051a494 100644 --- a/backend/app/agents/services/sync.py +++ b/backend/app/agents/services/sync.py @@ -1,3 +1,5 @@ +from datetime import datetime +from datetime import timezone from typing import List from fastapi import HTTPException @@ -7,12 +9,14 @@ import app.agents.velociraptor.services.agents as velociraptor_services import app.agents.wazuh.services.agents as wazuh_services -from app.agents.schema.agents import SyncedAgent from app.agents.schema.agents import SyncedAgentsResponse +from app.agents.schema.agents import SyncedWazuhAgent from app.agents.velociraptor.schema.agents import VelociraptorAgent +from app.agents.velociraptor.schema.agents import VelociraptorClients from app.agents.wazuh.schema.agents import WazuhAgent from app.agents.wazuh.schema.agents import WazuhAgentsList from app.connectors.models import Connectors +from app.db.db_session import get_db_session from app.db.universal_models import Agents @@ -35,6 +39,22 @@ async def fetch_wazuh_agents() -> WazuhAgentsList: ) +async def fetch_velociraptor_clients() -> VelociraptorClients: + """ + Fetches clients from Velociraptor service. + + Args: + None + + Returns: + VelociraptorClientsList: The fetched clients. + """ + collected_velociraptor_agents = await velociraptor_services.collect_velociraptor_clients() + return VelociraptorClients( + clients=collected_velociraptor_agents, + ) + + async def fetch_velociraptor_agent(agent_name: str) -> VelociraptorAgent: """ Fetches agent details from Velociraptor service. @@ -48,10 +68,22 @@ async def fetch_velociraptor_agent(agent_name: str) -> VelociraptorAgent: return await velociraptor_services.collect_velociraptor_agent(agent_name) -async def add_agent_to_db( +async def fetch_velociraptor_agent_via_client_id(client_id: str) -> VelociraptorAgent: + """ + Fetches agent details from Velociraptor service. + + Args: + client_id (str): The client_id of the agent to fetch. + + Returns: + VelociraptorAgent: The fetched agent details. + """ + return await velociraptor_services.collect_velociraptor_agent_via_client_id(client_id) + + +async def add_wazuh_agent_in_db( session: AsyncSession, agent: WazuhAgent, - client: VelociraptorAgent, customer_code: str, ): """Add new agent to database. @@ -59,14 +91,13 @@ async def add_agent_to_db( Args: session (AsyncSession): The asynchronous session object for database operations. agent (WazuhAgent): The Wazuh agent object to be added. - client (VelociraptorAgent): The Velociraptor agent object associated with the Wazuh agent. customer_code (str): The customer code for the agent. Returns: None """ - new_agent = Agents.create_from_model(agent, client, customer_code) + new_agent = Agents.create_wazuh_agent_from_model(agent, customer_code) session.add(new_agent) logger.info(f"Adding agent {agent.agent_name} to the database") try: @@ -78,11 +109,10 @@ async def add_agent_to_db( logger.info(f"Agent {agent.agent_name} added to the database") -async def update_agent_in_db( +async def update_wazuh_agent_in_db( session: AsyncSession, existing_agent: Agents, agent: WazuhAgent, - client: VelociraptorAgent, customer_code: str, ): """Update existing agent in database. @@ -91,14 +121,13 @@ async def update_agent_in_db( session (AsyncSession): The async session object for database operations. existing_agent (Agents): The existing agent object in the database. agent (WazuhAgent): The updated agent object. - client (VelociraptorAgent): The updated client object. customer_code (str): The customer code associated with the agent. Returns: None """ - existing_agent.update_from_model(agent, client, customer_code) + existing_agent.update_wazuh_agent_from_model(agent, customer_code) await session.commit() # Use the await keyword to commit asynchronously logger.info(f"Agent {agent.agent_name} updated in the database") @@ -150,7 +179,24 @@ async def get_velociraptor_agent(agent_name): return None -async def process_velociraptor_agent(session, wazuh_agent): +async def get_velociraptor_agent_by_client_id(client_id): + """ + Retrieves a Velociraptor agent with the specified client_id. + + Args: + client_id (str): The client_id of the agent to retrieve. + + Returns: + VelociraptorAgent: The retrieved Velociraptor agent, or None if retrieval fails. + """ + try: + return await fetch_velociraptor_agent_via_client_id(client_id) + except Exception as e: + logger.error(f"Failed to collect Velociraptor Agent for {client_id}: {e}") + return None + + +async def process_velociraptor_agent(session, agent, client_id=None): """ Process the Velociraptor agent for a given Wazuh agent. @@ -164,7 +210,9 @@ async def process_velociraptor_agent(session, wazuh_agent): try: velociraptor_connector = await get_velociraptor_connector(session) if velociraptor_connector.connector_verified: - velociraptor_agent = await get_velociraptor_agent(wazuh_agent.agent_name) + velociraptor_agent = await get_velociraptor_agent(agent) + if client_id is not None: + velociraptor_agent = await get_velociraptor_agent_by_client_id(client_id) else: velociraptor_agent = VelociraptorAgent( client_id="Unknown", @@ -173,67 +221,129 @@ async def process_velociraptor_agent(session, wazuh_agent): ) return velociraptor_agent except Exception as e: - logger.error(f"Failed to process agent {wazuh_agent.agent_name}: {e}") + logger.error(f"Failed to process agent {agent}: {e}") return None -async def sync_agents(session: AsyncSession) -> SyncedAgentsResponse: - """ - Synchronize agents from Wazuh and Velociraptor services. - - This function fetches the list of Wazuh agents, collects the corresponding Velociraptor agent for each Wazuh agent, - and synchronizes the agents in the database. It returns a response indicating the success of the synchronization - operation and the list of agents that were added. - - :param session: The database session to use for querying and updating agents. - :type session: AsyncSession - :return: The response indicating the success of the synchronization operation and the list of agents added. - :rtype: SyncedAgentsResponse - """ +async def sync_agents_wazuh() -> SyncedAgentsResponse: wazuh_agents_list = await fetch_wazuh_agents() logger.info(f"Collected Wazuh Agents: {wazuh_agents_list}") agents_added_list: List[WazuhAgent] = [] - for wazuh_agent in wazuh_agents_list.agents: - logger.info(f"Collecting Velociraptor Agent for {wazuh_agent.agent_name}") + async with get_db_session() as session: # Create a new session here + for wazuh_agent in wazuh_agents_list.agents: + customer_code = extract_customer_code(wazuh_agent.agent_label) - try: - velociraptor_agent = await process_velociraptor_agent(session, wazuh_agent) - except Exception as e: - logger.error( - f"Failed to collect Velociraptor Agent for {wazuh_agent.agent_name}: {e}", - ) - continue - - customer_code = extract_customer_code(wazuh_agent.agent_label) - - # Asynchronously fetch the existing agent - existing_agent_query = select(Agents).filter( - Agents.hostname == wazuh_agent.agent_name, - ) - result = await session.execute(existing_agent_query) - existing_agent = result.scalars().first() - - if existing_agent: - await update_agent_in_db( - session, - existing_agent, - wazuh_agent, - velociraptor_agent, - customer_code, - ) - else: - await add_agent_to_db( - session, - wazuh_agent, - velociraptor_agent, - customer_code, + existing_agent_query = select(Agents).filter( + Agents.hostname == wazuh_agent.agent_name, ) + result = await session.execute(existing_agent_query) + existing_agent = result.scalars().first() + + if existing_agent: + await update_wazuh_agent_in_db(session, existing_agent, wazuh_agent, customer_code) + else: + await add_wazuh_agent_in_db(session, wazuh_agent, customer_code) + + synced_wazuh_agent = SyncedWazuhAgent(**wazuh_agent.dict()) + agents_added_list.append(synced_wazuh_agent) + + logger.info(f"Agents Added List: {agents_added_list}") + + # Close the session + await session.close() + + return SyncedAgentsResponse( + success=True, + message="Agents synced successfully", + ) + + +async def update_agent_with_velociraptor_in_db( + session: AsyncSession, + agent: Agents, + velociraptor_agent: VelociraptorAgent, +): + """Update existing agent in database with Velociraptor details. + + Args: + session (AsyncSession): The async session object for database operations. + agent (Agents): The existing agent object in the database. + client (VelociraptorAgent): The updated client object. - # Combine the wazuh agent and velociraptor agent into one object - synced_agent = SyncedAgent(**wazuh_agent.dict(), **velociraptor_agent.dict()) - agents_added_list.append(synced_agent) + Returns: + None + + """ + logger.info(f"Updating agent {agent.hostname} with Velociraptor details in the database") + agent.update_velociraptor_details(velociraptor_agent) + session.add(agent) # Add the updated agent back to the session + await session.commit() # Use the await keyword to commit asynchronously + logger.info("Agent updated with Velociraptor details in the database") + + +async def sync_agents_velociraptor() -> SyncedAgentsResponse: + """ + Syncronizes the agents with Velociraptor. This function retrieves all the + agents from the `Agents` table and invokes the velociraptor API with the + hostname. If the hostname cannot be found within Velociraptor, and the agent's + `velociraptor_id` is not None, invoke the Velociraptor API and pass it the + `velociraptor_id`. + + :param session: The database session to use for querying and updating agents. + :type session: AsyncSession + :return: The response indicating the success of the synchronization operation and the list of agents added. + :rtype: SyncedAgentsResponse + """ + agents_added_list: List[VelociraptorAgent] = [] + + velociraptor_clients = await fetch_velociraptor_clients() + velociraptor_clients = velociraptor_clients.clients if hasattr(velociraptor_clients, "clients") else [] + + async with get_db_session() as session: # Create a new session here + existing_agents_query = select(Agents) + result = await session.execute(existing_agents_query) + existing_agents = result.scalars().all() + + for agent in existing_agents: + logger.info(f"Collecting Velociraptor Agent for {agent.hostname}") + + try: + # Build the velociraptor_agent where the hostname or `client_id` is that equal to the `agents` + velociraptor_agent = next( + ( + client + for client in velociraptor_clients + if client.os_info.hostname == agent.hostname or client.client_id == agent.velociraptor_id + ), + None, + ) + # Convert Unix epoch timestamp to datetime + last_seen_at = datetime.fromtimestamp( + int(velociraptor_agent.last_seen_at) / 1e6, + ) # Divide by 1e6 to convert from microseconds to seconds + # Convert datetime to ISO 8601 format without fractional seconds + last_seen_at_iso = last_seen_at.replace(tzinfo=timezone.utc).isoformat(timespec="seconds") + velociraptor_agent = VelociraptorAgent( + velociraptor_id=velociraptor_agent.client_id, + velociraptor_last_seen=last_seen_at_iso, + velociraptor_agent_version=velociraptor_agent.agent_information.version, + ) + + except Exception as e: + logger.error( + f"Failed to collect Velociraptor Agent for {agent.hostname}: {e}", + ) + continue + + if velociraptor_agent: + # Update the agent with the Velociraptor client's details + await update_agent_with_velociraptor_in_db(session, agent, velociraptor_agent) + agents_added_list.append(velociraptor_agent) + + # Close the session + await session.close() logger.info(f"Agents Added List: {agents_added_list}") return SyncedAgentsResponse( diff --git a/backend/app/agents/velociraptor/schema/agents.py b/backend/app/agents/velociraptor/schema/agents.py index e3446df7..10027db3 100644 --- a/backend/app/agents/velociraptor/schema/agents.py +++ b/backend/app/agents/velociraptor/schema/agents.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import List from typing import Optional from pydantic import BaseModel @@ -17,3 +18,38 @@ def client_last_seen_as_datetime(self): class Config: allow_population_by_field_name = True + + +class VelociraptorAgentInformation(BaseModel): + version: str + name: str + build_time: str + build_url: str + + +class VelociraptorOSInfo(BaseModel): + system: str + hostname: str + release: str + machine: str + fqdn: str + mac_addresses: List[str] + + +class VelociraptorClient(BaseModel): + client_id: str + agent_information: VelociraptorAgentInformation + os_info: VelociraptorOSInfo + first_seen_at: int + last_seen_at: int + last_ip: str + last_interrogate_flow_id: str + last_interrogate_artifact_name: str + labels: List[str] + last_hunt_timestamp: int + last_event_table_version: int + last_label_timestamp: int + + +class VelociraptorClients(BaseModel): + clients: List[VelociraptorClient] diff --git a/backend/app/agents/velociraptor/services/agents.py b/backend/app/agents/velociraptor/services/agents.py index 700fcfd5..c81c9943 100644 --- a/backend/app/agents/velociraptor/services/agents.py +++ b/backend/app/agents/velociraptor/services/agents.py @@ -21,6 +21,21 @@ def create_query(query: str) -> str: return query +async def collect_velociraptor_clients() -> list: + """ + Collects all clients from Velociraptor. + + Returns: + list: A list of all clients. + """ + velociraptor_service = await UniversalService.create("Velociraptor") + query = create_query( + "SELECT * FROM clients()", + ) + flow = velociraptor_service.execute_query(query) + return flow["results"] + + async def collect_velociraptor_agent(agent_name: str) -> VelociraptorAgent: """ Retrieves the client ID, last_seen_at and client version based on the agent name from Velociraptor. @@ -78,6 +93,63 @@ async def collect_velociraptor_agent(agent_name: str) -> VelociraptorAgent: ) +async def collect_velociraptor_agent_via_client_id(client_id: str) -> VelociraptorAgent: + """ + Retrieves the client ID, last_seen_at and client version based on the agent name from Velociraptor. + + Args: + agent_name (str): The name of the agent. + + Returns: + str: The client ID if found, None otherwise. + str: The last seen at timestamp if found, Default timsetamp otherwise. + """ + logger.info(f"Collecting agent {client_id} from Velociraptor") + velociraptor_service = await UniversalService.create("Velociraptor") + try: + client_id = await velociraptor_service.get_client_id_via_client_id(client_id) + client_id = client_id["results"][0]["client_id"] + except (KeyError, IndexError, TypeError) as e: + logger.error(f"Failed to get client ID for {client_id}. Error: {e}") + return VelociraptorAgent( + client_id="Unknown", + client_last_seen="Unknown", + client_version="Unknown", + ) + + try: + vql_last_seen_at = f"select last_seen_at from clients(search='host:{client_id}')" + last_seen_at = await velociraptor_service._get_last_seen_timestamp( + vql_last_seen_at, + ) + client_last_seen = datetime.fromtimestamp( + int(last_seen_at) / 1000000, + ).strftime( + "%Y-%m-%dT%H:%M:%S+00:00", + ) # Converting to string format + except Exception as e: + logger.error( + f"Failed to get or convert last seen at for {client_id}. Error: {e}", + ) + client_last_seen = "1970-01-01T00:00:00+00:00" + + try: + vql_client_version = f"select * from clients(search='host:{client_id}')" + # client_version = UniversalService()._get_client_version(vql_client_version) + client_version = await velociraptor_service._get_client_version( + vql_client_version, + ) + except Exception as e: + logger.error(f"Failed to get client version for {client_id}. Error: {e}") + client_version = "Unknown" + + return VelociraptorAgent( + client_id=client_id, + client_last_seen=client_last_seen, + client_version=client_version, + ) + + def execute_query(universal_service, query: str) -> dict: """ Executes a query using the provided universal service. diff --git a/backend/app/connectors/velociraptor/utils/universal.py b/backend/app/connectors/velociraptor/utils/universal.py index 5c4b5dac..b13df0f5 100644 --- a/backend/app/connectors/velociraptor/utils/universal.py +++ b/backend/app/connectors/velociraptor/utils/universal.py @@ -270,6 +270,40 @@ async def get_client_id(self, client_name: str): "results": [{"client_id": None}], } + async def get_client_id_via_client_id(self, client_id: str): + """ + Get the client_id associated with a given client_id. + + Args: + client_id (str): The client_id to search for. + + Returns: + dict: A dictionary with the success status, a message, and potentially the client_id. + """ + # Formulate queries + try: + vql_client_id = f"select client_id,os_info from clients(search='client_id:{client_id}')" + vql_last_seen_at = f"select last_seen_at from clients(search='client_id:{client_id}')" + + # Get the last seen timestamp + logger.info(f"Getting last seen at timestamp for {client_id}") + + last_seen_at = await self._get_last_seen_timestamp(vql_last_seen_at) + + logger.info(f"Last seen at timestamp for {client_id}: {last_seen_at}") + + # if last_seen_at is longer than 30 seconds from now, return False + if await self._is_offline(last_seen_at): + return self.execute_query(vql_client_id) + + return self.execute_query(vql_client_id) + except Exception as e: + return { + "success": False, + "message": f"Failed to get Client ID for {client_id}: {e}", + "results": [{"client_id": None}], + } + async def _get_last_seen_timestamp(self, vql: str): """ Executes the VQL query and returns the last_seen_at timestamp. diff --git a/backend/app/db/universal_models.py b/backend/app/db/universal_models.py index e12099d6..5a3dea3c 100644 --- a/backend/app/db/universal_models.py +++ b/backend/app/db/universal_models.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Optional +from loguru import logger from sqlalchemy import Column from sqlalchemy import Float from sqlalchemy import LargeBinary @@ -93,11 +94,11 @@ class Agents(SQLModel, table=True): label: str = Field(max_length=256) critical_asset: bool = Field(default=False) wazuh_last_seen: datetime - velociraptor_id: str = Field(max_length=256) - velociraptor_last_seen: datetime + velociraptor_id: Optional[str] = Field(max_length=256) + velociraptor_last_seen: Optional[datetime] wazuh_agent_version: str = Field(max_length=256) wazuh_agent_status: str = Field("not found", max_length=256) - velociraptor_agent_version: str = Field(max_length=256) + velociraptor_agent_version: Optional[str] = Field(max_length=256) customer_code: Optional[str] = Field(foreign_key="customers.customer_code", max_length=256) quarantined: bool = Field(default=False) @@ -120,11 +121,32 @@ def create_from_model(cls, wazuh_agent, velociraptor_agent, customer_code): wazuh_last_seen=wazuh_last_seen_value, wazuh_agent_version=wazuh_agent.wazuh_agent_version, wazuh_agent_status=wazuh_agent.wazuh_agent_status if wazuh_agent.wazuh_agent_status else "not found", - velociraptor_id=velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a", + velociraptor_id=velociraptor_agent.client_id if velociraptor_agent and velociraptor_agent.client_id else None, velociraptor_last_seen=velociraptor_agent.client_last_seen_as_datetime - if velociraptor_agent.client_last_seen_as_datetime - else "1970-01-01T00:00:00+00:00", - velociraptor_agent_version=velociraptor_agent.client_version if velociraptor_agent.client_version else "n/a", + if velociraptor_agent and velociraptor_agent.client_last_seen_as_datetime + else None, + velociraptor_agent_version=velociraptor_agent.client_version + if velociraptor_agent and velociraptor_agent.client_version + else None, + customer_code=customer_code, + ) + + @classmethod + def create_wazuh_agent_from_model(cls, wazuh_agent, customer_code): + if wazuh_agent.agent_last_seen == "Unknown": + wazuh_last_seen_value = "1970-01-01T00:00:00+00:00" + else: + wazuh_last_seen_value = wazuh_agent.agent_last_seen_as_datetime + + return cls( + agent_id=wazuh_agent.agent_id, + hostname=wazuh_agent.agent_name, + ip_address=wazuh_agent.agent_ip, + os=wazuh_agent.agent_os, + label=wazuh_agent.agent_label, + wazuh_last_seen=wazuh_last_seen_value, + wazuh_agent_version=wazuh_agent.wazuh_agent_version, + wazuh_agent_status=wazuh_agent.wazuh_agent_status if wazuh_agent.wazuh_agent_status else "not found", customer_code=customer_code, ) @@ -145,11 +167,49 @@ def update_from_model(self, wazuh_agent, velociraptor_agent, customer_code): self.wazuh_last_seen = wazuh_last_seen_value self.wazuh_agent_version = wazuh_agent.wazuh_agent_version self.wazuh_agent_status = wazuh_agent.wazuh_agent_status if wazuh_agent.wazuh_agent_status else "not found" - self.velociraptor_id = velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a" - self.velociraptor_last_seen = velociraptor_agent.client_last_seen_as_datetime - self.velociraptor_agent_version = velociraptor_agent.client_version + self.velociraptor_id = velociraptor_agent.client_id if velociraptor_agent and velociraptor_agent.client_id else None + self.velociraptor_last_seen = ( + velociraptor_agent.client_last_seen_as_datetime + if velociraptor_agent and velociraptor_agent.client_last_seen_as_datetime + else None + ) + self.velociraptor_agent_version = ( + velociraptor_agent.client_version if velociraptor_agent and velociraptor_agent.client_version else None + ) self.customer_code = customer_code + def update_wazuh_agent_from_model(self, wazuh_agent, customer_code): + if wazuh_agent.agent_last_seen == "Unknown" or wazuh_agent.agent_last_seen == "1970-01-01T00:00:00+00:00": + wazuh_last_seen_value = datetime.strptime( + "1970-01-01T00:00:00+00:00", + "%Y-%m-%dT%H:%M:%S%z", + ) + else: + wazuh_last_seen_value = wazuh_agent.agent_last_seen_as_datetime + + self.agent_id = wazuh_agent.agent_id + self.hostname = wazuh_agent.agent_name + self.ip_address = wazuh_agent.agent_ip + self.os = wazuh_agent.agent_os + self.label = wazuh_agent.agent_label + self.wazuh_last_seen = wazuh_last_seen_value + self.wazuh_agent_version = wazuh_agent.wazuh_agent_version + self.wazuh_agent_status = wazuh_agent.wazuh_agent_status if wazuh_agent.wazuh_agent_status else "not found" + self.customer_code = customer_code + + def update_velociraptor_details(self, velociraptor_agent): + logger.info(f"Updating Velociraptor details for agent {self}") + self.velociraptor_id = velociraptor_agent.client_id if velociraptor_agent and velociraptor_agent.client_id else None + self.velociraptor_last_seen = ( + velociraptor_agent.client_last_seen_as_datetime + if velociraptor_agent and velociraptor_agent.client_last_seen_as_datetime + else None + ) + self.velociraptor_agent_version = ( + velociraptor_agent.client_version if velociraptor_agent and velociraptor_agent.client_version else None + ) + logger.info(f"Updated with Velociraptor details: {self}") + class LogEntry(SQLModel, table=True): __tablename__ = "log_entries" diff --git a/backend/app/schedulers/services/agent_sync.py b/backend/app/schedulers/services/agent_sync.py index c393a323..9deeaae2 100644 --- a/backend/app/schedulers/services/agent_sync.py +++ b/backend/app/schedulers/services/agent_sync.py @@ -23,7 +23,7 @@ async def agent_sync(): """ logger.info("Synchronizing agents via scheduler...") async with get_db_session() as session: - await sync_all_agents(session=session) + await sync_all_agents() stmt = select(JobMetadata).where(JobMetadata.job_id == "agent_sync") result = await session.execute(stmt) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c995ff3a..4bdfeb13 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,7 +28,7 @@ "jose": "^5.3.0", "js-md5": "^0.8.3", "lodash": "^4.17.21", - "markdown-it-highlightjs": "^4.0.1", + "markdown-it-highlightjs": "^4.1.0", "mitt": "^3.0.1", "naive-ui": "^2.38.2", "password-validator": "^5.3.0", @@ -49,7 +49,7 @@ "devDependencies": { "@clack/prompts": "^0.7.0", "@iconify/vue": "^4.1.2", - "@rushstack/eslint-patch": "^1.10.2", + "@rushstack/eslint-patch": "^1.10.3", "@tsconfig/node18": "^18.2.4", "@types/bytes": "^3.1.4", "@types/file-saver": "^2.0.7", @@ -58,7 +58,7 @@ "@types/html2canvas": "^1.0.0", "@types/inquirer": "^9.0.7", "@types/jsdom": "^21.1.6", - "@types/lodash": "^4.17.3", + "@types/lodash": "^4.17.4", "@types/markdown-it": "^14.1.1", "@types/markdown-it-highlightjs": "^3.3.4", "@types/node": "^20.12.12", @@ -83,7 +83,7 @@ "picocolors": "^1.0.1", "postcss": "^8.4.38", "prettier": "^3.2.5", - "sass": "^1.77.1", + "sass": "^1.77.2", "shiki": "^1.5.2", "start-server-and-test": "^2.0.3", "tailwind-config-viewer": "^2.0.2", @@ -150,9 +150,9 @@ } }, "node_modules/@antfu/utils": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.7.tgz", - "integrity": "sha512-gFPqTG7otEJ8uP6wrhDv6mqwGWYZKNvAcCq6u9hOj0c+IKCEsY4L1oC9trPq2SaWIzAfHvqfBDxF591JkMf+kg==", + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.8.tgz", + "integrity": "sha512-rWQkqXRESdjXtc+7NRfK9lASQjpXJu1ayp7qi1d23zZorY+wBHVLHHoVcMsEnkqEBWTFqbztO7/QdJFzyEcLTg==", "funding": { "url": "https://github.com/sponsors/antfu" } @@ -664,6 +664,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -1837,9 +1838,9 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz", - "integrity": "sha512-hw437iINopmQuxWPSUEvqE56NCPsiU8N4AYtfHmJFckclktzK9YQJieD3XkDCDH4OjL+C7zgPUh73R/nrcHrqw==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", "dev": true }, "node_modules/@shikijs/core": { @@ -1985,9 +1986,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.3.tgz", - "integrity": "sha512-zmNrEJaBvNskZXQWaUQq6bktF4IDGVfDS78M+YEk5aCn9M/b94/mB/6WCyfH2/MjwBdc6QuOor95CIlKWYRL3A==" + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==" }, "node_modules/@types/lodash-es": { "version": "4.17.12", @@ -2663,21 +2664,19 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", - "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", + "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/type-utils": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", - "debug": "^4.3.4", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/type-utils": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { @@ -2698,15 +2697,15 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/parser": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", - "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", + "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", "debug": "^4.3.4" }, "engines": { @@ -2726,13 +2725,13 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/scope-manager": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", - "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", + "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0" + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2743,13 +2742,13 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/type-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", - "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", + "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/utils": "7.9.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2770,9 +2769,9 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", - "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", + "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2783,13 +2782,13 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", - "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", + "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2811,18 +2810,15 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", - "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", + "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "semver": "^7.6.0" + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2836,12 +2832,12 @@ } }, "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", - "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", + "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/types": "7.9.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3739,9 +3735,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001616", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz", - "integrity": "sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==", + "version": "1.0.30001620", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", + "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", "dev": true, "funding": [ { @@ -3907,9 +3903,9 @@ } }, "node_modules/cli-table3": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", - "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "dependencies": { "string-width": "^4.2.0" @@ -4894,9 +4890,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.756", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz", - "integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==", + "version": "1.4.774", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.774.tgz", + "integrity": "sha512-132O1XCd7zcTkzS3FgkAzKmnBuNJjK8WjcTtNuoylj7MYbqw5eXehjQ5OK91g0zm7OTKIPeaAG4CPoRfD9M1Mg==", "dev": true }, "node_modules/emoji-regex": { @@ -5989,22 +5985,22 @@ } }, "node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.3.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", + "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.6", "minimatch": "^9.0.1", "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "path-scurry": "^1.11.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6422,9 +6418,9 @@ } }, "node_modules/immutable": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", - "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", "dev": true }, "node_modules/import-fresh": { @@ -7782,11 +7778,11 @@ } }, "node_modules/markdown-it-highlightjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/markdown-it-highlightjs/-/markdown-it-highlightjs-4.0.1.tgz", - "integrity": "sha512-EPXwFEN6P5nqR3G4KjT20r20xbGYKMMA/360hhSYFmeoGXTE6hsLtJAiB/8ID8slVH4CWHHEL7GX0YenyIstVQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/markdown-it-highlightjs/-/markdown-it-highlightjs-4.1.0.tgz", + "integrity": "sha512-aYcgme5aYn10BHEvLZaCNgwxU2oaAX9inK9dwCv38wJdq7tal5FzZrLdQQY8MR3I1H07S3BKgYGRX2kKuPT+sA==", "dependencies": { - "highlight.js": "^11.5.1" + "highlight.js": "^11.9.0" } }, "node_modules/mdn-data": { @@ -7907,9 +7903,9 @@ } }, "node_modules/minipass": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.0.tgz", - "integrity": "sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", + "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -8230,9 +8226,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.9.tgz", - "integrity": "sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==", + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", + "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", "dev": true }, "node_modules/object-assign": { @@ -8501,16 +8497,16 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8680,13 +8676,13 @@ } }, "node_modules/pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", + "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", "dev": true, "dependencies": { "confbox": "^0.1.7", - "mlly": "^1.6.1", + "mlly": "^1.7.0", "pathe": "^1.1.2" } }, @@ -9649,9 +9645,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.77.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", - "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==", + "version": "1.77.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz", + "integrity": "sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -9692,12 +9688,9 @@ "integrity": "sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -9705,22 +9698,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10397,9 +10374,9 @@ } }, "node_modules/svgo": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", - "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dev": true, "dependencies": { "@trysound/sax": "0.2.0", @@ -11021,9 +10998,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", - "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "dev": true, "funding": [ { @@ -11041,7 +11018,7 @@ ], "dependencies": { "escalade": "^3.1.2", - "picocolors": "^1.0.0" + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -11511,9 +11488,9 @@ } }, "node_modules/vue-component-type-helpers": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.16.tgz", - "integrity": "sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.19.tgz", + "integrity": "sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==", "dev": true }, "node_modules/vue-eslint-parser": { diff --git a/frontend/package.json b/frontend/package.json index 51a05d0c..81c37432 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,13 +54,14 @@ "jose": "^5.3.0", "js-md5": "^0.8.3", "lodash": "^4.17.21", - "markdown-it-highlightjs": "^4.0.1", + "markdown-it-highlightjs": "^4.1.0", "mitt": "^3.0.1", "naive-ui": "^2.38.2", "password-validator": "^5.3.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "secure-ls": "^1.2.6", + "shiki": "^1.5.2", "validator": "^13.12.0", "vue": "^3.4.27", "vue-advanced-cropper": "^2.8.8", @@ -75,16 +76,14 @@ "devDependencies": { "@clack/prompts": "^0.7.0", "@iconify/vue": "^4.1.2", - "@rushstack/eslint-patch": "^1.10.2", + "@rushstack/eslint-patch": "^1.10.3", "@tsconfig/node18": "^18.2.4", "@types/bytes": "^3.1.4", "@types/file-saver": "^2.0.7", "@types/fs-extra": "^11.0.4", - "@types/highlight.js": "^10.1.0", - "@types/html2canvas": "^1.0.0", "@types/inquirer": "^9.0.7", "@types/jsdom": "^21.1.6", - "@types/lodash": "^4.17.3", + "@types/lodash": "^4.17.4", "@types/markdown-it": "^14.1.1", "@types/markdown-it-highlightjs": "^3.3.4", "@types/node": "^20.12.12", @@ -109,8 +108,7 @@ "picocolors": "^1.0.1", "postcss": "^8.4.38", "prettier": "^3.2.5", - "sass": "^1.77.1", - "shiki": "^1.5.2", + "sass": "^1.77.2", "start-server-and-test": "^2.0.3", "tailwind-config-viewer": "^2.0.2", "tailwindcss": "^3.4.3", diff --git a/frontend/src/api/agents.ts b/frontend/src/api/agents.ts index 965f6375..ab47f90f 100644 --- a/frontend/src/api/agents.ts +++ b/frontend/src/api/agents.ts @@ -9,42 +9,60 @@ import type { ScaPolicyResult } from "@/types/agents.d" +export interface AgentPayload { + velociraptor_id: string +} + export default { - getAgents(id?: string) { - return HttpClient.get(`/agents${id ? "/" + id : ""}`) + getAgents(agentId?: string) { + return HttpClient.get(`/agents${agentId ? "/" + agentId : ""}`) }, - markCritical(id: string) { - return HttpClient.post(`/agents/${id}/critical`) + markCritical(agentId: string) { + return HttpClient.post(`/agents/${agentId}/critical`) }, - markNonCritical(id: string) { - return HttpClient.post(`/agents/${id}/noncritical`) + markNonCritical(agentId: string) { + return HttpClient.post(`/agents/${agentId}/noncritical`) }, - deleteAgent(id: string) { - return HttpClient.delete(`/agents/${id}/delete`) + deleteAgent(agentId: string) { + return HttpClient.delete(`/agents/${agentId}/delete`) }, syncAgents() { return HttpClient.post(`/agents/sync`) }, - agentVulnerabilities(id: string) { + agentVulnerabilities(agentId: string) { return HttpClient.get( - `/agents/${id}/vulnerabilities` + `/agents/${agentId}/vulnerabilities` ) }, - getSocCases(id: string | number, signal?: AbortSignal) { + getSocCases(agentId: string | number, signal?: AbortSignal) { return HttpClient.get( - `/agents/${id}/soc_cases`, + `/agents/${agentId}/soc_cases`, signal ? { signal } : {} ) }, - getSCA(id: string | number, signal?: AbortSignal) { - return HttpClient.get(`/agents/${id}/sca`, signal ? { signal } : {}) + getSCA(agentId: string | number, signal?: AbortSignal) { + return HttpClient.get( + `/agents/${agentId}/sca`, + signal ? { signal } : {} + ) }, - getSCAResults(id: string | number, policyId: string, signal?: AbortSignal) { + getSCAResults(agentId: string | number, policyId: string, signal?: AbortSignal) { return HttpClient.get( - `/agents/${id}/sca/${policyId}`, + `/agents/${agentId}/sca/${policyId}`, signal ? { signal } : {} ) }, + updateAgent(agentId: string, payload: AgentPayload) { + return HttpClient.put( + `/agents/${agentId}/update`, + {}, + { + params: { + velociraptor_id: payload.velociraptor_id + } + } + ) + }, // IGNORE AT THE MOMENT ! agentsWazuhOutdated() { diff --git a/frontend/src/components/agents/AgentVelociraptorIdForm.vue b/frontend/src/components/agents/AgentVelociraptorIdForm.vue new file mode 100644 index 00000000..68b50199 --- /dev/null +++ b/frontend/src/components/agents/AgentVelociraptorIdForm.vue @@ -0,0 +1,92 @@ + + + diff --git a/frontend/src/components/agents/OverviewSection.vue b/frontend/src/components/agents/OverviewSection.vue index adfe84af..480d84f8 100644 --- a/frontend/src/components/agents/OverviewSection.vue +++ b/frontend/src/components/agents/OverviewSection.vue @@ -4,16 +4,18 @@