Fix code scanning exception exposure alerts

Sanitize API-facing exception details and replace OpenCTI pattern regex with linear parsing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
funnywolf
2026-07-11 19:44:15 +08:00
co-authored by Copilot
parent 6dc0abf891
commit 7e95846db7
14 changed files with 141 additions and 53 deletions
+8 -6
View File
@@ -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": ""}
+8 -3
View File
@@ -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))
+3 -2
View File
@@ -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)
+3 -2
View File
@@ -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
+6 -2
View File
@@ -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)
+7 -2
View File
@@ -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)
+19 -10
View File
@@ -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": "",
}
+7 -2
View File
@@ -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):
+1 -1
View File
@@ -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):
+18 -8
View File
@@ -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)
+7 -2
View File
@@ -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,
+6 -2
View File
@@ -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({
+41 -9
View File
@@ -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:
+7 -2
View File
@@ -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,