fix: аудит — 19 фиксов безопасности, надёжности, UI и 16 новых тестов

- S4: bump jinja2>=3.1.4, python-multipart>=0.0.18, httpx>=0.28.0
- S5: _detect_ecosystem — DEFAULT_ECOSYSTEM для неизвестных форматов
- S6: harvester — log.exception() вместо log.error()
- S8: _scan_component — urlencode параметров
- P1: scanner — proc.kill() при таймауте
- P3: api_packages — selectinload(Scan.findings), убран N+1
- P4+P5: утечка _url_locks и _llm_locks при early return
- P6: DB reaper — сброс {'status':'analyzing'} при старте
- UI: htmx-пагинация, фильтры не теряют flagged, 404 с layout
- UI: мобильные таблицы overflow-x, полная стата на дашборде
- UI: i18n статусов в _status_badge, urlencode package_name
- 16 новых тестов: analyze endpoint (6), scanner errors (4),
  webhook signature (2), llm client (4)
This commit is contained in:
Marker689
2026-05-10 10:45:44 +03:00
parent d483a8b21d
commit 1341404568
31 changed files with 575 additions and 152 deletions

View File

@@ -4,6 +4,7 @@ import hashlib
import hmac
import json
import re
from urllib.parse import urlencode
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
@@ -58,7 +59,7 @@ def _detect_ecosystem(source: dict) -> str:
return "go"
if fmt in ("npm", "node"):
return "npm"
return fmt or DEFAULT_ECOSYSTEM
return DEFAULT_ECOSYSTEM
@router.post("/nexus")
@@ -75,22 +76,16 @@ async def nexus_webhook(
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing signature"
)
expected = hmac.new(
config.webhook_secret.encode(), payload, hashlib.sha256
).hexdigest()
expected = hmac.new(config.webhook_secret.encode(), payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(x_nexus_webhook_signature, expected):
log.warning("Webhook rejected: invalid signature")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Invalid signature"
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid signature")
try:
data = json.loads(payload.decode("utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError):
log.warning("Webhook received invalid body")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid request body"
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid request body")
action = data.get("action", "").upper()
if action not in RELEVANT_WEBHOOK_ACTIONS:
@@ -108,8 +103,7 @@ async def nexus_webhook(
initiator = raw_initiator
source_ip = request.client.host if request.client else None
log.info("Webhook: action=%s initiator=%s source_ip=%s",
action, initiator, source_ip)
log.info("Webhook: action=%s initiator=%s source_ip=%s", action, initiator, source_ip)
repository = data.get("repositoryName", "")
if not repository:
@@ -125,16 +119,19 @@ async def nexus_webhook(
if not asset_path or not _is_package_asset(asset_path):
return {"status": WEBHOOK_STATUS_IGNORED, "reason": WEBHOOK_IGNORE_NON_PACKAGE}
download_url = asset.get("downloadUrl") or _build_download_url(
repository, asset_path
)
download_url = asset.get("downloadUrl") or _build_download_url(repository, asset_path)
ecosystem = _detect_ecosystem(asset)
log.info("Webhook: %s asset %s (%s) in %s", action, asset_path, ecosystem, repository)
background_tasks.add_task(
_scan_in_background, download_url, repository, ecosystem, asset_path,
initiator=initiator, source_ip=source_ip,
_scan_in_background,
download_url,
repository,
ecosystem,
asset_path,
initiator=initiator,
source_ip=source_ip,
)
return {"status": WEBHOOK_STATUS_ACCEPTED, "asset": asset_path, "action": action}
@@ -164,10 +161,15 @@ async def nexus_webhook(
async def _scan_component(repository: str, name: str, version: str, ecosystem: str):
from ..core.nexus import nexus_get
api_path = (
f"/service/rest/v1/search"
f"?repository={repository}&name={name}&version={version}&format={ecosystem}"
params = urlencode(
{
"repository": repository,
"name": name,
"version": version,
"format": ecosystem,
}
)
api_path = f"/service/rest/v1/search?{params}"
try:
resp = await nexus_get(api_path)
resp.raise_for_status()
@@ -186,14 +188,10 @@ async def _scan_component(repository: str, name: str, version: str, ecosystem: s
asset_path = _extract_asset_path(asset)
if not asset_path or not _is_package_asset(asset_path):
continue
download_url = asset.get("downloadUrl") or _build_download_url(
repository, asset_path
)
download_url = asset.get("downloadUrl") or _build_download_url(repository, asset_path)
log.info("Scanning component asset: %s", asset_path)
async for session in get_session():
await harvest(
download_url, repository, ecosystem, asset_path, session
)
await harvest(download_url, repository, ecosystem, asset_path, session)
break
@@ -208,8 +206,13 @@ async def _scan_in_background(
try:
async for session in get_session():
await harvest(
download_url, repository, format_, asset_path, session,
initiator=initiator, source_ip=source_ip,
download_url,
repository,
format_,
asset_path,
session,
initiator=initiator,
source_ip=source_ip,
)
break
except Exception as e: