| 1 | from typing import List |
| 2 | from typing import Optional |
| 3 | |
| 4 | from fastapi import APIRouter |
| 5 | from fastapi import Depends |
| 6 | from fastapi import HTTPException |
| 7 | from fastapi import Query |
| 8 | from fastapi import Security |
| 9 | from loguru import logger |
| 10 | from sqlalchemy import select as sa_select |
| 11 | from sqlalchemy.ext.asyncio import AsyncSession |
| 12 | |
| 13 | from app.agents.wazuh.syscollector.schema.packages import AgentPackagesResponse |
| 14 | from app.agents.wazuh.syscollector.schema.packages import IndexerPackagesResponse |
| 15 | from app.agents.wazuh.syscollector.services.packages import collect_agent_packages |
| 16 | from app.agents.wazuh.syscollector.services.packages import search_packages_in_indexer |
| 17 | from app.auth.models.users import User |
| 18 | from app.auth.routes.auth import AuthHandler |
| 19 | from app.db.db_session import get_db |
| 20 | from app.db.universal_models import Agents |
| 21 | from app.middleware.customer_access import customer_access_handler |
| 22 | |
| 23 | packages_router = APIRouter() |
| 24 | |
| 25 | |
| 26 | @packages_router.get( |
| 27 | "/search/packages", |
| 28 | response_model=IndexerPackagesResponse, |
| 29 | description="Search installed packages across all agents via the Wazuh Indexer", |
| 30 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))], |
| 31 | ) |
| 32 | async def search_packages( |
| 33 | package_name: Optional[str] = Query(None, description="Filter by package name (wildcard match)"), |
| 34 | agent_name: Optional[str] = Query(None, description="Filter by agent name (wildcard match)"), |
| 35 | agent_id: Optional[str] = Query(None, description="Filter by agent ID (exact match)"), |
| 36 | architecture: Optional[str] = Query(None, description="Filter by architecture (e.g. amd64, x86_64)"), |
| 37 | package_type: Optional[str] = Query(None, description="Filter by package type (e.g. deb, rpm)"), |
| 38 | vendor: Optional[str] = Query(None, description="Filter by vendor (wildcard match)"), |
| 39 | package_version: Optional[str] = Query(None, description="Filter by package version (wildcard match)"), |
| 40 | size: int = Query(500, ge=1, le=10000, description="Maximum number of results to return"), |
| 41 | ) -> IndexerPackagesResponse: |
| 42 | """ |
| 43 | Search the Wazuh Indexer for package inventory data across all agents. |
| 44 | |
| 45 | Unlike the per-agent endpoint that queries the Wazuh Manager API, this |
| 46 | endpoint queries the ``wazuh-states-inventory-packages-*`` index in the |
| 47 | Wazuh Indexer (OpenSearch), allowing you to search for packages across |
| 48 | every agent without specifying an agent ID. |
| 49 | |
| 50 | **Use Cases:** |
| 51 | - Find all agents that have a specific package installed |
| 52 | - Search for outdated versions of a package across the fleet |
| 53 | - Inventory packages by type, architecture, or vendor |
| 54 | """ |
| 55 | return await search_packages_in_indexer( |
| 56 | package_name=package_name, |
| 57 | agent_name=agent_name, |
| 58 | agent_id=agent_id, |
| 59 | architecture=architecture, |
| 60 | package_type=package_type, |
| 61 | vendor=vendor, |
| 62 | package_version=package_version, |
| 63 | size=size, |
| 64 | ) |
| 65 | |
| 66 | |
| 67 | @packages_router.get( |
| 68 | "/{agent_id}/packages", |
| 69 | response_model=AgentPackagesResponse, |
| 70 | description="Get installed packages for a specific agent from the Wazuh Manager syscollector", |
| 71 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))], |
| 72 | ) |
| 73 | async def get_agent_packages( |
| 74 | agent_id: str, |
| 75 | limit: int = Query(500, ge=1, le=100000, description="Maximum number of packages to return"), |
| 76 | offset: int = Query(0, ge=0, description="First element to return"), |
| 77 | sort: Optional[str] = Query(None, description="Sort by field(s). Use +/- prefix for asc/desc order"), |
| 78 | search: Optional[str] = Query(None, description="Free-text search string"), |
| 79 | select: Optional[List[str]] = Query(None, description="Fields to return"), |
| 80 | vendor: Optional[str] = Query(None, description="Filter by vendor"), |
| 81 | name: Optional[str] = Query(None, description="Filter by package name"), |
| 82 | architecture: Optional[str] = Query(None, description="Filter by architecture"), |
| 83 | format: Optional[str] = Query(None, alias="format", description="Filter by package format (e.g. deb, rpm)"), |
| 84 | version: Optional[str] = Query(None, description="Filter by package version"), |
| 85 | q: Optional[str] = Query(None, description='Advanced query filter (e.g. q="name=openssl")'), |
| 86 | current_user: User = Depends(AuthHandler().get_current_user), |
| 87 | session: AsyncSession = Depends(get_db), |
| 88 | ) -> AgentPackagesResponse: |
| 89 | """ |
| 90 | Fetch installed packages for a specific agent via the Wazuh Manager |
| 91 | syscollector API. |
| 92 | |
| 93 | Returns package name, version, architecture, vendor, format, and other |
| 94 | metadata for every installed package on the agent. |
| 95 | """ |
| 96 | logger.info(f"Fetching packages for agent {agent_id}") |
| 97 | |
| 98 | # Verify the user has access to this agent's customer |
| 99 | base_query = sa_select(Agents).filter(Agents.agent_id == agent_id) |
| 100 | filtered_query = await customer_access_handler.filter_query_by_customer_access( |
| 101 | current_user, |
| 102 | session, |
| 103 | base_query, |
| 104 | Agents.customer_code, |
| 105 | ) |
| 106 | result = await session.execute(filtered_query) |
| 107 | agent = result.scalars().first() |
| 108 | |
| 109 | if not agent: |
| 110 | raise HTTPException( |
| 111 | status_code=404, |
| 112 | detail=f"Agent with agent_id {agent_id} not found or access denied", |
| 113 | ) |
| 114 | |
| 115 | return await collect_agent_packages( |
| 116 | agent_id=agent_id, |
| 117 | limit=limit, |
| 118 | offset=offset, |
| 119 | sort=sort, |
| 120 | search=search, |
| 121 | select=select, |
| 122 | vendor=vendor, |
| 123 | name=name, |
| 124 | architecture=architecture, |
| 125 | format=format, |
| 126 | version=version, |
| 127 | q=q, |
| 128 | ) |