diff --git a/backend/apps/accounts/ldap.py b/backend/apps/accounts/ldap.py index d4c0d37..9b0ddec 100644 --- a/backend/apps/accounts/ldap.py +++ b/backend/apps/accounts/ldap.py @@ -32,8 +32,8 @@ def ldap_authenticates(username, password, config=None): try: from ldap3 import SUBTREE, Connection, Server from ldap3.core.exceptions import LDAPException - except ImportError as exc: - logger.warning("LDAP login failed for %s: ldap3 is not installed", username, exc_info=exc) + except ImportError: + logger.warning("LDAP login failed for %s: ldap3 is not installed", username, exc_info=True) return False connections = [] @@ -146,8 +146,9 @@ def test_ldap_config(config, *, test_username="", test_password=""): try: from ldap3 import Connection, Server from ldap3.core.exceptions import LDAPException - except ImportError as exc: - return {"success": False, "detail": f"ldap3 is not installed: {exc}", "response_preview": ""} + except ImportError: + logger.warning("LDAP test failed: ldap3 is not installed", exc_info=True) + return {"success": False, "detail": "LDAP support is not installed on the server.", "response_preview": ""} try: server = Server(config["server_uri"]) @@ -158,5 +159,6 @@ def test_ldap_config(config, *, test_username="", test_password=""): conn = Connection(server, auto_bind=True, **bind_kwargs) conn.unbind() return {"success": True, "detail": "LDAP bind succeeded.", "response_preview": ""} - except LDAPException as exc: - return {"success": False, "detail": f"LDAP bind failed: {exc}", "response_preview": ""} + except LDAPException: + logger.warning("LDAP bind test failed", exc_info=True) + return {"success": False, "detail": "LDAP bind failed.", "response_preview": ""} diff --git a/backend/apps/agent_api/views.py b/backend/apps/agent_api/views.py index be4088d..886ebb9 100644 --- a/backend/apps/agent_api/views.py +++ b/backend/apps/agent_api/views.py @@ -1,4 +1,5 @@ import json +import logging import mimetypes from django.conf import settings @@ -54,6 +55,7 @@ from .utils import bool_param, list_param, parse_tags, parse_timezone_aware_date API_VERSION = "v1" MIN_CLI_VERSION = "0.1.0" SERVER_VERSION = "0.5.0" +logger = logging.getLogger(__name__) FOUNDATION_CAPABILITIES = [ "agent.version", "case.list", @@ -448,7 +450,8 @@ class PlaybookRunView(APIView): user_input=request.data.get("user_input", ""), ) except ValueError as exc: - raise ValidationError({"detail": str(exc)}) from exc + logger.info("Invalid agent playbook run request", exc_info=True) + raise ValidationError({"detail": "Unknown playbook definition."}) from exc return agent_response(request, operation="playbook.run", data=serialize_playbook(playbook), status=status.HTTP_201_CREATED) @@ -521,7 +524,8 @@ class ThreatIntelQueryView(APIView): provider=provider, ) except ValueError as exc: - raise ValidationError({"detail": str(exc)}) from exc + logger.info("Invalid agent threat intelligence query", exc_info=True) + raise ValidationError({"detail": "Invalid threat intelligence query."}) from exc return agent_response(request, operation="ti.query", data=_dump(result)) @@ -541,7 +545,8 @@ class CMDBLookupView(APIView): provider=provider, ) except ValueError as exc: - raise ValidationError({"detail": str(exc)}) from exc + logger.info("Invalid agent CMDB lookup request", exc_info=True) + raise ValidationError({"detail": "Invalid CMDB lookup request."}) from exc return agent_response(request, operation="cmdb.lookup", data=_dump(result)) diff --git a/backend/apps/agentic/runtime/module.py b/backend/apps/agentic/runtime/module.py index 819ad5d..6b2c401 100644 --- a/backend/apps/agentic/runtime/module.py +++ b/backend/apps/agentic/runtime/module.py @@ -66,8 +66,9 @@ def scan_module_definitions(*, scripts_dir=None, scripts_dirs=None): for path in paths: try: definition = _definition_from_script(path) - except Exception as exc: - errors.append({"path": str(path), "error": f"{type(exc).__name__}: {exc}"}) + except Exception: + logger.exception("Failed to load module definition from %s", path) + errors.append({"path": str(path), "error": "Failed to load module definition."}) continue if definition is not None: definitions.append(definition) diff --git a/backend/apps/agentic/services/custom.py b/backend/apps/agentic/services/custom.py index 2e9eec1..d95f9ef 100644 --- a/backend/apps/agentic/services/custom.py +++ b/backend/apps/agentic/services/custom.py @@ -101,14 +101,15 @@ def _module_record_with_stream_health(definition, *, redis_client=None): record = _module_record(definition) try: record["stream_health"] = _stream_health(definition.stream_name, redis_client=redis_client) - except redis.RedisError as exc: + except redis.RedisError: + logger.exception("Failed to read module stream health for %s", definition.stream_name) record["stream_health"] = { "available": False, "length": 0, "first_id": "", "last_id": "", "groups": [], - "warning": f"{type(exc).__name__}: {exc}", + "warning": "Stream health is unavailable.", } return record diff --git a/backend/apps/agentic/services/playbooks.py b/backend/apps/agentic/services/playbooks.py index 7302bc1..ca96a34 100644 --- a/backend/apps/agentic/services/playbooks.py +++ b/backend/apps/agentic/services/playbooks.py @@ -1,3 +1,4 @@ +import logging import uuid from pathlib import Path @@ -9,6 +10,8 @@ from apps.agentic.runtime.loader import discover_script_class, iter_overlaid_pyt from apps.inbox.notifications import notify_playbook_completion from apps.playbooks.models import Playbook, PlaybookJobStatus +logger = logging.getLogger(__name__) + def default_playbook_scripts_dir(): return Path(settings.BASE_DIR) / "playbooks" @@ -57,8 +60,9 @@ def scan_playbook_definitions(*, scripts_dir=None, scripts_dirs=None): class_name="Playbook", base_class=BasePlaybook, ) - except Exception as exc: - errors.append({"path": str(path), "error": f"{type(exc).__name__}: {exc}"}) + except Exception: + logger.exception("Failed to load playbook definition from %s", path) + errors.append({"path": str(path), "error": "Failed to load playbook definition."}) continue if definition is not None: definitions.append(definition) diff --git a/backend/apps/playbooks/views.py b/backend/apps/playbooks/views.py index e95f9c8..2981340 100644 --- a/backend/apps/playbooks/views.py +++ b/backend/apps/playbooks/views.py @@ -1,3 +1,5 @@ +import logging + from django.core.exceptions import ValidationError from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets, permissions, status @@ -14,6 +16,8 @@ from apps.common.advanced_filters import AdvancedFilterBackend from .models import Playbook from .serializers import PlaybookSerializer +logger = logging.getLogger(__name__) + class PlaybookViewSet(AuditActorMixin, viewsets.ModelViewSet): queryset = Playbook.objects.select_related("user", "case") @@ -63,7 +67,8 @@ class PlaybookViewSet(AuditActorMixin, viewsets.ModelViewSet): user=request.user, user_input=user_input, ) - except ValueError as exc: - return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) + except ValueError: + logger.info("Invalid playbook run request", exc_info=True) + return Response({"detail": "Unknown playbook definition."}, status=status.HTTP_400_BAD_REQUEST) return Response(self.get_serializer(playbook).data, status=status.HTTP_201_CREATED) diff --git a/backend/apps/settings/services.py b/backend/apps/settings/services.py index 0906c6f..3ca5197 100644 --- a/backend/apps/settings/services.py +++ b/backend/apps/settings/services.py @@ -1,6 +1,10 @@ +import logging + import httpx from pycti import OpenCTIApiClient +logger = logging.getLogger(__name__) + def _chat_completions_url(base_url): normalized = base_url.rstrip("/") @@ -57,10 +61,11 @@ def test_llm_provider(config): "detail": f"LLM provider test failed with HTTP {response.status_code}.", "response_preview": _redact(response.text, [api_key])[:500], } - except Exception as exc: + except Exception: + logger.exception("LLM provider test failed") return { "success": False, - "detail": _redact(exc, [api_key]), + "detail": "LLM provider test failed due to a connection error.", "response_preview": "", } @@ -98,10 +103,11 @@ def test_alienvault_otx_config(config): "detail": f"AlienVault OTX test failed with HTTP {response.status_code}.", "response_preview": _redact(response.text, [api_key])[:500], } - except Exception as exc: + except Exception: + logger.exception("AlienVault OTX configuration test failed") return { "success": False, - "detail": _redact(exc, [api_key]), + "detail": "AlienVault OTX test failed due to a connection error.", "response_preview": "", } @@ -159,10 +165,11 @@ def test_opencti_config(config): "detail": "OpenCTI responded successfully.", "response_preview": str(preview)[:500], } - except Exception as exc: + except Exception: + logger.exception("OpenCTI configuration test failed") return { "success": False, - "detail": _redact(exc, [token]), + "detail": "OpenCTI test failed due to a connection error.", "response_preview": "", } @@ -186,10 +193,11 @@ def test_splunk_config(config): "detail": "Splunk responded successfully.", "response_preview": str({key: info.get(key) for key in ("serverName", "version", "guid")})[:500], } - except Exception as exc: + except Exception: + logger.exception("Splunk configuration test failed") return { "success": False, - "detail": _redact(exc, [password]), + "detail": "Splunk test failed due to a connection error.", "response_preview": "", } @@ -213,9 +221,10 @@ def test_elk_config(config): "version": (info.get("version") or {}).get("number") if isinstance(info.get("version"), dict) else "", })[:500], } - except Exception as exc: + except Exception: + logger.exception("ELK configuration test failed") return { "success": False, - "detail": _redact(exc, [api_key]), + "detail": "ELK test failed due to a connection error.", "response_preview": "", } diff --git a/backend/apps/settings/views.py b/backend/apps/settings/views.py index 6530538..649c215 100644 --- a/backend/apps/settings/views.py +++ b/backend/apps/settings/views.py @@ -1,3 +1,5 @@ +import logging + from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db import transaction @@ -33,6 +35,8 @@ from .serializers import ( ) from .services import test_alienvault_otx_config, test_elk_config, test_llm_provider, test_opencti_config, test_splunk_config +logger = logging.getLogger(__name__) + LLM_AUDIT_FIELDS = ("name", "base_url", "model", "proxy", "tags", "enabled", "priority", "api_key") OTX_AUDIT_FIELDS = ("enabled", "api_key", "base_url", "proxy") OPENCTI_AUDIT_FIELDS = ("enabled", "url", "token", "ssl_verify", "proxy") @@ -98,8 +102,9 @@ def _run_config_test(operation, func, config): config, timeout_seconds=settings.CONFIG_TEST_TIMEOUT_SECONDS, ) - except OperationTimeoutError as exc: - return {"success": False, "detail": str(exc), "response_preview": ""} + except OperationTimeoutError: + logger.warning("Configuration test timed out: %s", operation, exc_info=True) + return {"success": False, "detail": "Configuration test timed out.", "response_preview": ""} def _config_from_instance(instance, values): diff --git a/backend/apps/webhook/tests.py b/backend/apps/webhook/tests.py index 75c7696..b623287 100644 --- a/backend/apps/webhook/tests.py +++ b/backend/apps/webhook/tests.py @@ -198,7 +198,7 @@ class WebhookAPITests(SimpleTestCase): ) self.assertEqual(response.status_code, 503) - self.assertEqual(response.json()["detail"], "Failed to write Redis stream search: RuntimeError") + self.assertEqual(response.json()["detail"], "Webhook stream service is unavailable.") def test_old_agentic_forwarder_route_is_removed(self): with self.assertRaises(Resolver404): diff --git a/backend/apps/webhook/views.py b/backend/apps/webhook/views.py index 922f251..7165b0b 100644 --- a/backend/apps/webhook/views.py +++ b/backend/apps/webhook/views.py @@ -1,3 +1,5 @@ +import logging + from pydantic import ValidationError from rest_framework import permissions, status from rest_framework.response import Response @@ -9,6 +11,10 @@ from apps.webhook.service import ( handle_splunk_webhook, ) +logger = logging.getLogger(__name__) +INVALID_WEBHOOK_PAYLOAD_DETAIL = "Invalid webhook payload." +WEBHOOK_STREAM_UNAVAILABLE_DETAIL = "Webhook stream service is unavailable." + class SplunkWebhookView(APIView): authentication_classes = [] @@ -17,10 +23,12 @@ class SplunkWebhookView(APIView): def post(self, request): try: result = handle_splunk_webhook(request.data) - except (ValidationError, ValueError) as exc: - return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) - except WebhookRedisError as exc: - return Response({"detail": str(exc)}, status=status.HTTP_503_SERVICE_UNAVAILABLE) + except (ValidationError, ValueError): + logger.info("Invalid Splunk webhook payload", exc_info=True) + return Response({"detail": INVALID_WEBHOOK_PAYLOAD_DETAIL}, status=status.HTTP_400_BAD_REQUEST) + except WebhookRedisError: + logger.exception("Failed to process Splunk webhook") + return Response({"detail": WEBHOOK_STREAM_UNAVAILABLE_DETAIL}, status=status.HTTP_503_SERVICE_UNAVAILABLE) return Response(result.model_dump(), status=status.HTTP_200_OK) @@ -31,8 +39,10 @@ class KibanaWebhookView(APIView): def post(self, request): try: result = handle_kibana_webhook(request.data) - except (ValidationError, ValueError) as exc: - return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST) - except WebhookRedisError as exc: - return Response({"detail": str(exc)}, status=status.HTTP_503_SERVICE_UNAVAILABLE) + except (ValidationError, ValueError): + logger.info("Invalid Kibana webhook payload", exc_info=True) + return Response({"detail": INVALID_WEBHOOK_PAYLOAD_DETAIL}, status=status.HTTP_400_BAD_REQUEST) + except WebhookRedisError: + logger.exception("Failed to process Kibana webhook") + return Response({"detail": WEBHOOK_STREAM_UNAVAILABLE_DETAIL}, status=status.HTTP_503_SERVICE_UNAVAILABLE) return Response(result.model_dump(), status=status.HTTP_200_OK) diff --git a/backend/integrations/cmdb/service.py b/backend/integrations/cmdb/service.py index 354343c..f8415e7 100644 --- a/backend/integrations/cmdb/service.py +++ b/backend/integrations/cmdb/service.py @@ -1,6 +1,10 @@ +import logging + from integrations.cmdb.models import CMDBQueryOutput from integrations.cmdb.providers import get_providers +logger = logging.getLogger(__name__) + def list_providers(): return list(get_providers().keys()) @@ -25,8 +29,9 @@ def lookup_artifact_context(artifact_type, artifact_value, provider=None): results.append(result) if result.error: errors.append(f"[{provider_name}] {result.error}") - except Exception as exc: - errors.append(f"[{provider_name}] {type(exc).__name__}: {exc}") + except Exception: + logger.exception("CMDB provider lookup failed: %s", provider_name) + errors.append(f"[{provider_name}] Provider lookup failed.") return CMDBQueryOutput( artifact_type=artifact_type, diff --git a/backend/integrations/siem/registry.py b/backend/integrations/siem/registry.py index c928dea..e848fe5 100644 --- a/backend/integrations/siem/registry.py +++ b/backend/integrations/siem/registry.py @@ -1,3 +1,4 @@ +import logging from functools import lru_cache from pathlib import Path @@ -6,6 +7,8 @@ import yaml from integrations.siem.models import IndexInfo, SchemaFieldInfo from asp import settings +logger = logging.getLogger(__name__) + CUSTOM_REGISTRY_DIR = Path(settings.CUSTOM_DIR) / "data" / "siem" @@ -54,8 +57,9 @@ def scan_registry_configs(): for yaml_file in _iter_overlaid_yaml_files(*default_registry_dirs()): try: index_info = _load_yaml_file(yaml_file) - except Exception as exc: - errors.append({"path": str(yaml_file), "error": f"{type(exc).__name__}: {exc}"}) + except Exception: + logger.exception("Failed to load SIEM registry config from %s", yaml_file) + errors.append({"path": str(yaml_file), "error": "Failed to load SIEM registry config."}) continue fields = [field.model_dump() for field in index_info.fields] indices.append({ diff --git a/backend/integrations/threat_intel/providers.py b/backend/integrations/threat_intel/providers.py index f88254b..a42bf8a 100644 --- a/backend/integrations/threat_intel/providers.py +++ b/backend/integrations/threat_intel/providers.py @@ -1,3 +1,4 @@ +import logging import ipaddress import re from urllib.parse import quote @@ -45,6 +46,8 @@ OPENCTI_UNSUPPORTED_TYPES = { ArtifactType.SCRIPT_CONTENT, } +logger = logging.getLogger(__name__) + class BaseThreatIntelProvider: name = "" @@ -135,8 +138,9 @@ class OpenCTIProvider(BaseThreatIntelProvider): matches = self._find_matches(client, indicator, plan) context = self._load_context(client, matches) return self._summarize(indicator, artifact_type, plan, matches, context) - except Exception as exc: - return self._error(indicator, artifact_type, _redact(str(exc), [self.token]), indicator_type=plan.get("indicator_type")) + except Exception: + logger.exception("OpenCTI query failed") + return self._error(indicator, artifact_type, "OpenCTI query failed.", indicator_type=plan.get("indicator_type")) def _client(self): if self.client is not None: @@ -251,9 +255,8 @@ class OpenCTIProvider(BaseThreatIntelProvider): @staticmethod def _pattern_contains_value(pattern, indicator): - values = re.findall(r"'((?:\\.|[^'])*)'", pattern) lowered = indicator.lower() - for value in values: + for value in _single_quoted_values(pattern): unescaped = value.replace("\\'", "'").replace("\\\\", "\\") if unescaped.lower() == lowered: return True @@ -565,11 +568,14 @@ class AlienVaultOTXProvider(BaseThreatIntelProvider): response.raise_for_status() return response.json() except httpx.HTTPStatusError as exc: - return {"error": f"OTX HTTP {exc.response.status_code}: {exc.response.text}"} - except httpx.HTTPError as exc: - return {"error": f"OTX request failed: {type(exc).__name__}: {exc}"} - except ValueError as exc: - return {"error": f"OTX response is not valid JSON: {exc}"} + logger.info("OTX request returned HTTP %s", exc.response.status_code) + return {"error": f"OTX HTTP {exc.response.status_code}."} + except httpx.HTTPError: + logger.exception("OTX request failed") + return {"error": "OTX request failed."} + except ValueError: + logger.exception("OTX response is not valid JSON") + return {"error": "OTX response is not valid JSON."} def summarize_result(self, attributes, indicator_type, indicator): if attributes.get("error"): @@ -744,6 +750,32 @@ def _looks_like_url(value): return "." in value and "/" in value +def _single_quoted_values(value): + values = [] + current = [] + in_quote = False + escaped = False + for char in value: + if not in_quote: + if char == "'": + in_quote = True + current = [] + continue + if escaped: + current.append(f"\\{char}") + escaped = False + continue + if char == "\\": + escaped = True + continue + if char == "'": + values.append("".join(current)) + in_quote = False + continue + current.append(char) + return values + + def _redact(value, secrets): redacted = str(value) for secret in secrets: diff --git a/backend/integrations/threat_intel/service.py b/backend/integrations/threat_intel/service.py index 131665e..720216d 100644 --- a/backend/integrations/threat_intel/service.py +++ b/backend/integrations/threat_intel/service.py @@ -1,6 +1,10 @@ +import logging + from integrations.threat_intel.models import TIQueryOutput from integrations.threat_intel.providers import get_providers +logger = logging.getLogger(__name__) + RISK_PRIORITY = {"high": 3, "medium": 2, "low": 1, None: 0} @@ -35,8 +39,9 @@ def query_indicator(indicator, *, artifact_type, provider=None): results.append(result) if result.error: errors.append(f"[{provider_name}] {result.error}") - except Exception as exc: - errors.append(f"[{provider_name}] {type(exc).__name__}: {exc}") + except Exception: + logger.exception("Threat intelligence provider query failed: %s", provider_name) + errors.append(f"[{provider_name}] Provider query failed.") return TIQueryOutput( indicator=indicator,