From 61e2dbca07622b41c55b35ab24813670f7c992fb Mon Sep 17 00:00:00 2001 From: Diksha Dabhole Date: Sat, 20 Jun 2026 15:50:09 +0530 Subject: [PATCH] feat: expose notification retry backoff configuration in admin diagnostics --- backend/secuscan/models.py | 8 +++++++ backend/secuscan/notification_service.py | 9 +++++++ backend/secuscan/routes.py | 11 +++++++++ .../integration/test_notification_routes.py | 24 +++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/backend/secuscan/models.py b/backend/secuscan/models.py index b6cb61c03..e04f2f12d 100644 --- a/backend/secuscan/models.py +++ b/backend/secuscan/models.py @@ -348,6 +348,14 @@ class NotificationHistoryResponse(BaseModel): sent_at: datetime +class NotificationDiagnosticsResponse(BaseModel): + """Diagnostic configuration details for notification delivery.""" + webhook_timeout_seconds: float + webhook_connect_timeout_seconds: float + max_retries: int + backoff_factor_seconds: float + + class BulkDeleteRequest(RootModel[Annotated[List[str], Field(max_length=MAX_BULK_DELETE)]]): """Accepts a JSON array of task IDs directly. Max 500 per request.""" pass diff --git a/backend/secuscan/notification_service.py b/backend/secuscan/notification_service.py index a6d57e10b..ad65c0182 100644 --- a/backend/secuscan/notification_service.py +++ b/backend/secuscan/notification_service.py @@ -47,6 +47,15 @@ _WEBHOOK_CONNECT_TIMEOUT_SECONDS = 5.0 _USER_AGENT = "SecuScan-Notifications/1.0" +def get_delivery_configuration() -> Dict[str, Any]: + """Return the currently active configuration for notification delivery.""" + return { + "webhook_timeout_seconds": _WEBHOOK_TIMEOUT_SECONDS, + "webhook_connect_timeout_seconds": _WEBHOOK_CONNECT_TIMEOUT_SECONDS, + "max_retries": 0, + "backoff_factor_seconds": 0.0, + } + SOCKET_OPTION = Tuple[int, int, int | bytes] diff --git a/backend/secuscan/routes.py b/backend/secuscan/routes.py index 44d1103d6..e91d3ee7f 100644 --- a/backend/secuscan/routes.py +++ b/backend/secuscan/routes.py @@ -101,10 +101,12 @@ def build_report_filename(task: Dict[str, Any], extension: str) -> str: NotificationRuleCreate, NotificationRuleUpdate, NotificationChannelType, TaskStatus, ExecutionContext, WorkflowStep, ValidationMode, EvidenceLevel, + NotificationDiagnosticsResponse, ) from .config import settings from .database import get_db from .plugins import get_plugin_manager, init_plugins +from . import notification_service from .executor import executor from .redaction import redact_inputs from .ratelimit import ( @@ -2248,6 +2250,15 @@ def verify_admin_access( ) return candidate +@router.get( + "/admin/diagnostics/notifications", + response_model=NotificationDiagnosticsResponse, + dependencies=[Depends(verify_admin_access), Depends(admin_limiter)] +) +async def get_notification_diagnostics(): + """Get active notification delivery configuration and retry policy""" + return notification_service.get_delivery_configuration() + @router.get("/admin/network-policy", dependencies=[Depends(verify_admin_access), Depends(admin_limiter)]) async def get_network_policy(): """Get current network policy configuration""" diff --git a/testing/backend/integration/test_notification_routes.py b/testing/backend/integration/test_notification_routes.py index f519e5000..e77065570 100644 --- a/testing/backend/integration/test_notification_routes.py +++ b/testing/backend/integration/test_notification_routes.py @@ -153,3 +153,27 @@ async def seed_history(): assert filtered_data["history"][0]["rule_id"] == rule_id assert filtered_data["history"][0]["finding_id"] assert filtered_data["history"][0]["status"] == "success" + + +def test_admin_diagnostics_notifications(test_client, monkeypatch): + from backend.secuscan.config import settings + + monkeypatch.setattr(settings, "admin_api_key", "secret-test-key-long") + + # Unauthorized without key + unauth_resp = test_client.get("/api/v1/admin/diagnostics/notifications") + assert unauth_resp.status_code == 401 + + # Success with key + auth_resp = test_client.get( + "/api/v1/admin/diagnostics/notifications", + headers={"X-API-Key": "secret-test-key-long"}, + ) + assert auth_resp.status_code == 200 + data = auth_resp.json() + assert "webhook_timeout_seconds" in data + assert "webhook_connect_timeout_seconds" in data + assert "max_retries" in data + assert "backoff_factor_seconds" in data + assert type(data["max_retries"]) is int + assert type(data["webhook_timeout_seconds"]) is float