feat: add Bitdefender GravityZone integration module
Adds full GravityZone API integration to ClaudeTools. Key additions: - api/services/gravityzone_service.py: JSON-RPC client with Basic auth, methods for company/endpoint/quarantine/licensing data, and security_sweep which paginates all endpoints, enriches with malware/agent status, and sorts infected > outdated > clean - api/schemas/gravityzone.py: Pydantic response models for all endpoints - api/routers/gravityzone.py: 7 REST endpoints at /api/gravityzone/*, JWT-protected, returns 502 on downstream GZ errors - api/config.py: GRAVITYZONE_API_KEY + GRAVITYZONE_API_BASE_URL settings - api/main.py: router registered under /api/gravityzone Vault entry: msp-tools/gravityzone.sops.yaml (partner-level key, 14 modules) Server .env updated, ticktick router synced, service restarted and verified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,10 @@ class Settings(BaseSettings):
|
|||||||
GRAPH_SENDER_EMAIL: str = "noreply@azcomputerguru.com"
|
GRAPH_SENDER_EMAIL: str = "noreply@azcomputerguru.com"
|
||||||
ADMIN_NOTIFICATION_EMAIL: str = "mike@azcomputerguru.com"
|
ADMIN_NOTIFICATION_EMAIL: str = "mike@azcomputerguru.com"
|
||||||
|
|
||||||
|
# Bitdefender GravityZone
|
||||||
|
GRAVITYZONE_API_KEY: str = ""
|
||||||
|
GRAVITYZONE_API_BASE_URL: str = "https://cloud.gravityzone.bitdefender.com/api/v1.0/jsonrpc"
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Pydantic configuration."""
|
"""Pydantic configuration."""
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from api.routers import (
|
|||||||
quotes,
|
quotes,
|
||||||
admin_quotes,
|
admin_quotes,
|
||||||
ticktick,
|
ticktick,
|
||||||
|
gravityzone,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import middleware
|
# Import middleware
|
||||||
@@ -133,6 +134,7 @@ app.include_router(admin_quotes.router, prefix="/api/admin/quotes", tags=["Admin
|
|||||||
|
|
||||||
# External integrations
|
# External integrations
|
||||||
app.include_router(ticktick.router, prefix="/api/ticktick", tags=["TickTick"])
|
app.include_router(ticktick.router, prefix="/api/ticktick", tags=["TickTick"])
|
||||||
|
app.include_router(gravityzone.router, prefix="/api/gravityzone", tags=["GravityZone"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
251
api/routers/gravityzone.py
Normal file
251
api/routers/gravityzone.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
|
||||||
|
from api.middleware.auth import get_current_user
|
||||||
|
from api.schemas.gravityzone import (
|
||||||
|
GZCompanyItem,
|
||||||
|
GZEndpointDetail,
|
||||||
|
GZEndpointItem,
|
||||||
|
GZStatusResponse,
|
||||||
|
GZSweepResult,
|
||||||
|
)
|
||||||
|
from api.services.gravityzone_service import (
|
||||||
|
ACG_COMPANIES_CONTAINER_ID,
|
||||||
|
get_gravityzone_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_on_failure(result, detail_prefix: str = "GravityZone error") -> dict:
|
||||||
|
if not result.success:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
|
detail=f"{detail_prefix}: {result.error or 'unknown error'}",
|
||||||
|
)
|
||||||
|
return result.data or {}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Status
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/status",
|
||||||
|
response_model=GZStatusResponse,
|
||||||
|
summary="GravityZone API key status and license info",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def get_status(current_user: dict = Depends(get_current_user)):
|
||||||
|
service = get_gravityzone_service()
|
||||||
|
result = await service.get_api_status()
|
||||||
|
data = _raise_on_failure(result, "GravityZone status")
|
||||||
|
|
||||||
|
return GZStatusResponse(
|
||||||
|
enabled_apis=data.get("enabledApis", []),
|
||||||
|
key_created_at=data.get("createdAt"),
|
||||||
|
used_slots=data.get("usedSlots"),
|
||||||
|
total_slots=data.get("totalSlots"),
|
||||||
|
expiry_date=data.get("expiryDate"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Companies
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/companies",
|
||||||
|
summary="List GravityZone client companies",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def list_companies(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
per_page: int = Query(100, ge=1, le=500),
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
service = get_gravityzone_service()
|
||||||
|
result = await service.list_client_companies(page=page, per_page=per_page)
|
||||||
|
data = _raise_on_failure(result, "GravityZone companies")
|
||||||
|
|
||||||
|
companies = [
|
||||||
|
GZCompanyItem(
|
||||||
|
id=item.get("id", ""),
|
||||||
|
name=item.get("name", ""),
|
||||||
|
type=item.get("type", 1),
|
||||||
|
)
|
||||||
|
for item in data.get("items", [])
|
||||||
|
]
|
||||||
|
return {"total": data.get("total", len(companies)), "companies": companies}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Endpoints
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/companies/{company_id}/endpoints",
|
||||||
|
summary="List endpoints for a GravityZone company",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def list_endpoints(
|
||||||
|
company_id: str,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
per_page: int = Query(50, ge=1, le=200),
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
service = get_gravityzone_service()
|
||||||
|
result = await service.list_endpoints(company_id, page=page, per_page=per_page)
|
||||||
|
data = _raise_on_failure(result, "GravityZone endpoints")
|
||||||
|
|
||||||
|
endpoints = [
|
||||||
|
GZEndpointItem(
|
||||||
|
id=item.get("id", ""),
|
||||||
|
name=item.get("name", ""),
|
||||||
|
fqdn=item.get("fqdn"),
|
||||||
|
ip=item.get("ip"),
|
||||||
|
os_version=item.get("operatingSystemVersion"),
|
||||||
|
is_managed=bool(item.get("isManaged", False)),
|
||||||
|
policy_name=(item.get("policy") or {}).get("name"),
|
||||||
|
)
|
||||||
|
for item in data.get("items", [])
|
||||||
|
]
|
||||||
|
return {"total": data.get("total", len(endpoints)), "endpoints": endpoints}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/endpoints/{endpoint_id}",
|
||||||
|
response_model=GZEndpointDetail,
|
||||||
|
summary="Get detailed info for a single GravityZone endpoint",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def get_endpoint(
|
||||||
|
endpoint_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
service = get_gravityzone_service()
|
||||||
|
result = await service.get_endpoint_details(endpoint_id)
|
||||||
|
data = _raise_on_failure(result, "GravityZone endpoint detail")
|
||||||
|
|
||||||
|
malware = data.get("malwareStatus", {})
|
||||||
|
agent = data.get("agent", {})
|
||||||
|
|
||||||
|
return GZEndpointDetail(
|
||||||
|
id=data.get("id", endpoint_id),
|
||||||
|
name=data.get("name", ""),
|
||||||
|
company_id=data.get("companyId"),
|
||||||
|
infected=bool(malware.get("infected", False)),
|
||||||
|
detection_active=bool(malware.get("detection", False)),
|
||||||
|
signature_outdated=bool(agent.get("signatureOutdated", False)),
|
||||||
|
product_outdated=bool(agent.get("productOutdated", False)),
|
||||||
|
agent_version=agent.get("productVersion"),
|
||||||
|
engine_version=agent.get("engineVersion"),
|
||||||
|
last_seen=data.get("lastSeen"),
|
||||||
|
last_update=agent.get("lastUpdate"),
|
||||||
|
state=data.get("state", 0),
|
||||||
|
modules=data.get("modules"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Quarantine
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/companies/{company_id}/quarantine",
|
||||||
|
summary="List quarantine items for a GravityZone company",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def list_quarantine(
|
||||||
|
company_id: str,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
per_page: int = Query(50, ge=1, le=200),
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
service = get_gravityzone_service()
|
||||||
|
result = await service.list_quarantine_items(company_id, page=page, per_page=per_page)
|
||||||
|
data = _raise_on_failure(result, "GravityZone quarantine")
|
||||||
|
|
||||||
|
return {"total": data.get("total", 0), "items": data.get("items", [])}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Security sweep
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _build_sweep_result(summaries) -> GZSweepResult:
|
||||||
|
stale_cutoff = datetime.now(timezone.utc) - timedelta(days=7)
|
||||||
|
not_seen_recently = 0
|
||||||
|
|
||||||
|
for s in summaries:
|
||||||
|
if s.last_seen:
|
||||||
|
try:
|
||||||
|
last_seen_dt = datetime.fromisoformat(
|
||||||
|
s.last_seen.replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
if last_seen_dt < stale_cutoff:
|
||||||
|
not_seen_recently += 1
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return GZSweepResult(
|
||||||
|
total=len(summaries),
|
||||||
|
infected=sum(1 for s in summaries if s.infected),
|
||||||
|
signature_outdated=sum(1 for s in summaries if s.signature_outdated),
|
||||||
|
product_outdated=sum(1 for s in summaries if s.product_outdated),
|
||||||
|
not_seen_recently=not_seen_recently,
|
||||||
|
endpoints=[
|
||||||
|
{
|
||||||
|
"endpoint_id": s.endpoint_id,
|
||||||
|
"name": s.name,
|
||||||
|
"company_id": s.company_id,
|
||||||
|
"infected": s.infected,
|
||||||
|
"detection_active": s.detection_active,
|
||||||
|
"signature_outdated": s.signature_outdated,
|
||||||
|
"product_outdated": s.product_outdated,
|
||||||
|
"last_seen": s.last_seen,
|
||||||
|
"agent_version": s.agent_version,
|
||||||
|
"state": s.state,
|
||||||
|
}
|
||||||
|
for s in summaries
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/sweep/{parent_id}",
|
||||||
|
response_model=GZSweepResult,
|
||||||
|
summary="Security sweep for all endpoints under a parent ID",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def sweep_parent(
|
||||||
|
parent_id: str,
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
service = get_gravityzone_service()
|
||||||
|
summaries = await service.security_sweep(parent_id)
|
||||||
|
return _build_sweep_result(summaries)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/sweep",
|
||||||
|
response_model=GZSweepResult,
|
||||||
|
summary="Security sweep across all ACG client companies",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def sweep_all_clients(
|
||||||
|
current_user: dict = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
service = get_gravityzone_service()
|
||||||
|
summaries = await service.security_sweep(ACG_COMPANIES_CONTAINER_ID)
|
||||||
|
return _build_sweep_result(summaries)
|
||||||
52
api/schemas/gravityzone.py
Normal file
52
api/schemas/gravityzone.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class GZEndpointItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
fqdn: Optional[str] = None
|
||||||
|
ip: Optional[str] = None
|
||||||
|
os_version: Optional[str] = None
|
||||||
|
is_managed: bool
|
||||||
|
policy_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class GZEndpointDetail(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
company_id: Optional[str] = None
|
||||||
|
infected: bool
|
||||||
|
detection_active: bool
|
||||||
|
signature_outdated: bool
|
||||||
|
product_outdated: bool
|
||||||
|
agent_version: Optional[str] = None
|
||||||
|
engine_version: Optional[str] = None
|
||||||
|
last_seen: Optional[str] = None
|
||||||
|
last_update: Optional[str] = None
|
||||||
|
state: int
|
||||||
|
modules: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
|
class GZCompanyItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
type: int
|
||||||
|
|
||||||
|
|
||||||
|
class GZSweepResult(BaseModel):
|
||||||
|
total: int
|
||||||
|
infected: int
|
||||||
|
signature_outdated: int
|
||||||
|
product_outdated: int
|
||||||
|
not_seen_recently: int
|
||||||
|
endpoints: list[dict]
|
||||||
|
|
||||||
|
|
||||||
|
class GZStatusResponse(BaseModel):
|
||||||
|
enabled_apis: list[str]
|
||||||
|
key_created_at: Optional[str] = None
|
||||||
|
used_slots: Optional[int] = None
|
||||||
|
total_slots: Optional[int] = None
|
||||||
|
expiry_date: Optional[str] = None
|
||||||
230
api/services/gravityzone_service.py
Normal file
230
api/services/gravityzone_service.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
GRAVITYZONE_API_BASE_URL = os.environ.get(
|
||||||
|
"GRAVITYZONE_API_BASE_URL",
|
||||||
|
"https://cloud.gravityzone.bitdefender.com/api/v1.0/jsonrpc",
|
||||||
|
)
|
||||||
|
GRAVITYZONE_API_KEY = os.environ.get("GRAVITYZONE_API_KEY", "")
|
||||||
|
|
||||||
|
GRAVITYZONE_TIMEOUT_SECONDS = 30.0
|
||||||
|
GRAVITYZONE_CONNECT_TIMEOUT_SECONDS = 10.0
|
||||||
|
|
||||||
|
ACG_ROOT_COMPANY_ID = "5c4280716c0318f3478b456a"
|
||||||
|
ACG_COMPANIES_CONTAINER_ID = "5c4280716c0318f3478b456e"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GZResult:
|
||||||
|
success: bool
|
||||||
|
data: Optional[dict] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GZEndpointSummary:
|
||||||
|
endpoint_id: str
|
||||||
|
name: str
|
||||||
|
company_id: str
|
||||||
|
infected: bool
|
||||||
|
detection_active: bool
|
||||||
|
signature_outdated: bool
|
||||||
|
product_outdated: bool
|
||||||
|
last_seen: Optional[str]
|
||||||
|
agent_version: Optional[str]
|
||||||
|
state: int
|
||||||
|
|
||||||
|
|
||||||
|
class GravityZoneService:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_base_url: str = GRAVITYZONE_API_BASE_URL,
|
||||||
|
api_key: str = GRAVITYZONE_API_KEY,
|
||||||
|
timeout: float = GRAVITYZONE_TIMEOUT_SECONDS,
|
||||||
|
connect_timeout: float = GRAVITYZONE_CONNECT_TIMEOUT_SECONDS,
|
||||||
|
):
|
||||||
|
self.api_base_url = api_base_url.rstrip("/")
|
||||||
|
self.api_key = api_key
|
||||||
|
self.timeout = httpx.Timeout(timeout, connect=connect_timeout)
|
||||||
|
|
||||||
|
async def _jsonrpc_request(
|
||||||
|
self, module: str, method: str, params: dict
|
||||||
|
) -> GZResult:
|
||||||
|
url = f"{self.api_base_url}/{module}"
|
||||||
|
payload = {"id": "1", "jsonrpc": "2.0", "method": method, "params": params}
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||||
|
response = await client.post(url, json=payload, auth=(self.api_key, ""))
|
||||||
|
response.raise_for_status()
|
||||||
|
body = response.json()
|
||||||
|
except httpx.TimeoutException as exc:
|
||||||
|
return GZResult(success=False, error=f"GravityZone API timeout: {exc}")
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
return GZResult(
|
||||||
|
success=False,
|
||||||
|
error=f"GravityZone HTTP error {exc.response.status_code}: {exc.response.text}",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return GZResult(success=False, error=f"GravityZone request failed: {exc}")
|
||||||
|
|
||||||
|
if "error" in body:
|
||||||
|
err = body["error"]
|
||||||
|
detail = err.get("data", {}).get("details") or err.get("message")
|
||||||
|
return GZResult(success=False, error=detail)
|
||||||
|
return GZResult(success=True, data=body.get("result"))
|
||||||
|
|
||||||
|
async def get_api_status(self) -> GZResult:
|
||||||
|
key_result = await self._jsonrpc_request("general", "getApiKeyDetails", {})
|
||||||
|
license_result = await self._jsonrpc_request("licensing", "getLicenseInfo", {})
|
||||||
|
|
||||||
|
if not key_result.success:
|
||||||
|
return key_result
|
||||||
|
|
||||||
|
combined = {**(key_result.data or {})}
|
||||||
|
if license_result.success:
|
||||||
|
combined.update(license_result.data or {})
|
||||||
|
else:
|
||||||
|
logger.warning(f"GravityZone getLicenseInfo failed: {license_result.error}")
|
||||||
|
|
||||||
|
return GZResult(success=True, data=combined)
|
||||||
|
|
||||||
|
async def get_own_company(self) -> GZResult:
|
||||||
|
return await self._jsonrpc_request("companies", "getCompanyDetails", {})
|
||||||
|
|
||||||
|
async def list_client_companies(self, page: int = 1, per_page: int = 100) -> GZResult:
|
||||||
|
result = await self._jsonrpc_request(
|
||||||
|
"network",
|
||||||
|
"getNetworkInventoryItems",
|
||||||
|
{
|
||||||
|
"parentId": ACG_COMPANIES_CONTAINER_ID,
|
||||||
|
"page": page,
|
||||||
|
"perPage": per_page,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not result.success:
|
||||||
|
return result
|
||||||
|
|
||||||
|
data = result.data or {}
|
||||||
|
items = data.get("items", [])
|
||||||
|
companies = [item for item in items if item.get("type") == 1]
|
||||||
|
return GZResult(
|
||||||
|
success=True,
|
||||||
|
data={"total": len(companies), "items": companies},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_endpoints(
|
||||||
|
self, parent_id: str, page: int = 1, per_page: int = 50
|
||||||
|
) -> GZResult:
|
||||||
|
return await self._jsonrpc_request(
|
||||||
|
"network",
|
||||||
|
"getEndpointsList",
|
||||||
|
{"parentId": parent_id, "page": page, "perPage": per_page},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_endpoint_details(self, endpoint_id: str) -> GZResult:
|
||||||
|
return await self._jsonrpc_request(
|
||||||
|
"network",
|
||||||
|
"getManagedEndpointDetails",
|
||||||
|
{"endpointId": endpoint_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_quarantine_items(
|
||||||
|
self, parent_id: str, page: int = 1, per_page: int = 50
|
||||||
|
) -> GZResult:
|
||||||
|
return await self._jsonrpc_request(
|
||||||
|
"quarantine",
|
||||||
|
"getQuarantineItemsList",
|
||||||
|
{"parentId": parent_id, "page": page, "perPage": per_page},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def security_sweep(self, parent_id: str) -> list[GZEndpointSummary]:
|
||||||
|
summaries: list[GZEndpointSummary] = []
|
||||||
|
page = 1
|
||||||
|
per_page = 100
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result = await self.list_endpoints(parent_id, page=page, per_page=per_page)
|
||||||
|
if not result.success:
|
||||||
|
logger.warning(
|
||||||
|
f"GravityZone security_sweep list_endpoints failed for "
|
||||||
|
f"{parent_id} page {page}: {result.error}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
data = result.data or {}
|
||||||
|
items = data.get("items", [])
|
||||||
|
if not items:
|
||||||
|
break
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
endpoint_id = item.get("id", "")
|
||||||
|
detail_result = await self.get_endpoint_details(endpoint_id)
|
||||||
|
if not detail_result.success:
|
||||||
|
logger.warning(
|
||||||
|
f"GravityZone getManagedEndpointDetails failed for "
|
||||||
|
f"{endpoint_id}: {detail_result.error}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
detail = detail_result.data or {}
|
||||||
|
malware = detail.get("malwareStatus", {})
|
||||||
|
agent = detail.get("agent", {})
|
||||||
|
|
||||||
|
infected = bool(malware.get("infected", False))
|
||||||
|
detection_active = bool(malware.get("detection", False))
|
||||||
|
signature_outdated = bool(agent.get("signatureOutdated", False))
|
||||||
|
product_outdated = bool(agent.get("productOutdated", False))
|
||||||
|
|
||||||
|
if infected:
|
||||||
|
logger.warning(
|
||||||
|
f"GravityZone: infected endpoint detected — "
|
||||||
|
f"id={endpoint_id} name={detail.get('name', '')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
summaries.append(
|
||||||
|
GZEndpointSummary(
|
||||||
|
endpoint_id=endpoint_id,
|
||||||
|
name=detail.get("name") or item.get("name", ""),
|
||||||
|
company_id=item.get("companyId", ""),
|
||||||
|
infected=infected,
|
||||||
|
detection_active=detection_active,
|
||||||
|
signature_outdated=signature_outdated,
|
||||||
|
product_outdated=product_outdated,
|
||||||
|
last_seen=detail.get("lastSeen"),
|
||||||
|
agent_version=agent.get("productVersion"),
|
||||||
|
state=detail.get("state", 0),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = data.get("total", 0)
|
||||||
|
if page * per_page >= total:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
def _sort_key(s: GZEndpointSummary) -> tuple:
|
||||||
|
return (
|
||||||
|
not s.infected,
|
||||||
|
not s.signature_outdated,
|
||||||
|
not s.product_outdated,
|
||||||
|
s.name.lower(),
|
||||||
|
)
|
||||||
|
|
||||||
|
summaries.sort(key=_sort_key)
|
||||||
|
return summaries
|
||||||
|
|
||||||
|
|
||||||
|
_gravityzone_service: Optional[GravityZoneService] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_gravityzone_service() -> GravityZoneService:
|
||||||
|
global _gravityzone_service
|
||||||
|
if _gravityzone_service is None:
|
||||||
|
_gravityzone_service = GravityZoneService()
|
||||||
|
return _gravityzone_service
|
||||||
Reference in New Issue
Block a user