Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/secuscan/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions backend/secuscan/notification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]


Expand Down
11 changes: 11 additions & 0 deletions backend/secuscan/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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"""
Expand Down
24 changes: 24 additions & 0 deletions testing/backend/integration/test_notification_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading