Files
agentic-soc-platform/backend/apps/settings/views.py

473 lines
19 KiB
Python

import logging
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import permissions, status, views, viewsets
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.response import Response
from apps.accounts.permissions import IsAdmin
from apps.audit.models import AuditLog
from apps.common.advanced_filters import AdvancedFilterBackend
from apps.common.operation_timeout import OperationTimeoutError, run_with_operation_timeout
from .models import (
LdapConfig,
LLMProviderConfig,
RuntimeConfig,
SiemElkConfig,
SiemSplunkConfig,
ThreatIntelAlienVaultOTXConfig,
ThreatIntelOpenCTIConfig,
)
from .runtime_config import invalidate
from .serializers import (
LLMProviderConfigSerializer,
LdapConfigSerializer,
SiemElkConfigSerializer,
SiemSplunkConfigSerializer,
RuntimeConfigSerializer,
ThreatIntelAlienVaultOTXConfigSerializer,
ThreatIntelOpenCTIConfigSerializer,
)
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")
SPLUNK_AUDIT_FIELDS = ("host", "port", "username", "password", "scheme", "verify")
ELK_AUDIT_FIELDS = (
"host",
"api_key",
"verify_certs",
"process_alert_from_index_enabled",
"action_index",
"action_poll_interval_seconds",
"action_size",
)
LDAP_AUDIT_FIELDS = (
"enabled",
"server_uri",
"domain",
"bind_dn",
"bind_password",
"user_search_base_dn",
"user_login_attr",
)
RUNTIME_AUDIT_FIELDS = (
"prompt_language",
"stream_maxlen",
"dashboard_refresh_interval_seconds",
)
def _snapshot(instance, fields):
return {field: getattr(instance, field) for field in fields}
def _audit_changes(before, after, secret_fields):
changes = {}
for field in after.keys():
old_value = before.get(field) if before else None
new_value = after.get(field)
if old_value == new_value:
continue
if field in secret_fields:
changes[field] = {"changed": True}
else:
changes[field] = {"from": old_value, "to": new_value}
return changes
def _write_audit(instance, action, actor, *, changes=None, metadata=None):
AuditLog.objects.create(
content_type=ContentType.objects.get_for_model(type(instance)),
object_id=str(instance.pk),
action=action,
actor=actor if getattr(actor, "is_authenticated", False) else None,
changes=changes or {},
metadata=metadata or {},
)
def _run_config_test(operation, func, config):
try:
return run_with_operation_timeout(
operation,
func,
config,
timeout_seconds=settings.CONFIG_TEST_TIMEOUT_SECONDS,
)
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):
config = _snapshot(instance, LLM_AUDIT_FIELDS) if instance else {}
config.update(values)
return config
class LLMProviderConfigViewSet(viewsets.ModelViewSet):
queryset = LLMProviderConfig.objects.all()
serializer_class = LLMProviderConfigSerializer
permission_classes = [permissions.IsAuthenticated, IsAdmin]
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter, AdvancedFilterBackend)
search_fields = ("name", "base_url", "model")
ordering_fields = ("name", "base_url", "model", "enabled", "priority", "created_at", "updated_at")
filterset_fields = ("enabled",)
advanced_filter_fields = {
"name": "text",
"base_url": "text",
"model": "text",
"enabled": "select",
"priority": "number",
}
def get_queryset(self):
queryset = super().get_queryset()
raw_tags = self.request.query_params.get("tags", "")
tags = [tag.strip() for tag in raw_tags.split(",") if tag.strip()]
if not tags:
return queryset
query = Q()
for tag in tags:
query |= Q(tags__contains=[tag])
return queryset.filter(query).distinct()
def get_serializer_context(self):
context = super().get_serializer_context()
context["reveal_secrets"] = self.request.query_params.get("reveal_secrets") in {"1", "true", "yes"}
return context
@transaction.atomic
def perform_create(self, serializer):
instance = serializer.save()
after = _snapshot(instance, LLM_AUDIT_FIELDS)
_write_audit(instance, "create", self.request.user, changes=_audit_changes(None, after, {"api_key"}))
transaction.on_commit(lambda: invalidate("llm"))
@transaction.atomic
def perform_update(self, serializer):
instance = self.get_object()
before = _snapshot(instance, LLM_AUDIT_FIELDS)
instance = serializer.save()
changes = _audit_changes(before, _snapshot(instance, LLM_AUDIT_FIELDS), {"api_key"})
if changes:
_write_audit(instance, "update", self.request.user, changes=changes)
transaction.on_commit(lambda: invalidate("llm"))
@transaction.atomic
def perform_destroy(self, instance):
before = _snapshot(instance, LLM_AUDIT_FIELDS)
_write_audit(instance, "delete", self.request.user, changes=_audit_changes(before, {}, {"api_key"}))
instance.delete()
transaction.on_commit(lambda: invalidate("llm"))
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
if self.get_serializer_context().get("reveal_secrets"):
_write_audit(instance, "reveal", request.user, metadata={"fields": ["api_key"]})
serializer = self.get_serializer(instance)
return Response(serializer.data)
@action(detail=False, methods=["post"], url_path="test")
def test_unsaved(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
result = _run_config_test("settings.llm.test", test_llm_provider, serializer.validated_data)
AuditLog.objects.create(
content_type=ContentType.objects.get_for_model(LLMProviderConfig),
object_id="unsaved",
action="test",
actor=request.user if getattr(request.user, "is_authenticated", False) else None,
metadata={"success": result["success"], "provider": serializer.validated_data.get("name", "")},
)
return Response(result, status=status.HTTP_200_OK)
@action(detail=True, methods=["post"], url_path="test")
def test_saved(self, request, pk=None):
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data or {}, partial=True)
serializer.is_valid(raise_exception=True)
result = _run_config_test("settings.llm.test", test_llm_provider, _config_from_instance(instance, serializer.validated_data))
_write_audit(instance, "test", request.user, metadata={"success": result["success"]})
return Response(result, status=status.HTTP_200_OK)
def _otx_config_from_instance(instance, values):
config = _snapshot(instance, OTX_AUDIT_FIELDS)
config.update(values)
return config
def _opencti_config_from_instance(instance, values):
config = _snapshot(instance, OPENCTI_AUDIT_FIELDS)
config.update(values)
return config
def _invalidate_siem(group):
invalidate(group)
from integrations.siem.clients import reset_clients
reset_clients()
class ThreatIntelAlienVaultOTXConfigView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def get_serializer_context(self):
return {
"reveal_secrets": self.request.query_params.get("reveal_secrets") in {"1", "true", "yes"},
}
def get(self, request):
instance = ThreatIntelAlienVaultOTXConfig.get_current()
if self.get_serializer_context().get("reveal_secrets"):
_write_audit(instance, "reveal", request.user, metadata={"fields": ["api_key"]})
serializer = ThreatIntelAlienVaultOTXConfigSerializer(instance, context=self.get_serializer_context())
return Response(serializer.data)
@transaction.atomic
def patch(self, request):
instance = ThreatIntelAlienVaultOTXConfig.get_current()
before = _snapshot(instance, OTX_AUDIT_FIELDS)
serializer = ThreatIntelAlienVaultOTXConfigSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
changes = _audit_changes(before, _snapshot(instance, OTX_AUDIT_FIELDS), {"api_key"})
if changes:
_write_audit(instance, "update", request.user, changes=changes)
transaction.on_commit(lambda: invalidate("otx"))
return Response(ThreatIntelAlienVaultOTXConfigSerializer(instance).data)
class ThreatIntelAlienVaultOTXTestView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def post(self, request):
instance = ThreatIntelAlienVaultOTXConfig.get_current()
serializer = ThreatIntelAlienVaultOTXConfigSerializer(instance, data=request.data or {}, partial=True)
serializer.is_valid(raise_exception=True)
result = _run_config_test(
"settings.threat_intel.otx.test",
test_alienvault_otx_config,
_otx_config_from_instance(instance, serializer.validated_data),
)
_write_audit(instance, "test", request.user, metadata={"success": result["success"]})
return Response(result, status=status.HTTP_200_OK)
class ThreatIntelOpenCTIConfigView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def get_serializer_context(self):
return {
"reveal_secrets": self.request.query_params.get("reveal_secrets") in {"1", "true", "yes"},
}
def get(self, request):
instance = ThreatIntelOpenCTIConfig.get_current()
if self.get_serializer_context().get("reveal_secrets"):
_write_audit(instance, "reveal", request.user, metadata={"fields": ["token"]})
serializer = ThreatIntelOpenCTIConfigSerializer(instance, context=self.get_serializer_context())
return Response(serializer.data)
@transaction.atomic
def patch(self, request):
instance = ThreatIntelOpenCTIConfig.get_current()
before = _snapshot(instance, OPENCTI_AUDIT_FIELDS)
serializer = ThreatIntelOpenCTIConfigSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
changes = _audit_changes(before, _snapshot(instance, OPENCTI_AUDIT_FIELDS), {"token"})
if changes:
_write_audit(instance, "update", request.user, changes=changes)
transaction.on_commit(lambda: invalidate("opencti"))
return Response(ThreatIntelOpenCTIConfigSerializer(instance).data)
class ThreatIntelOpenCTITestView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def post(self, request):
instance = ThreatIntelOpenCTIConfig.get_current()
serializer = ThreatIntelOpenCTIConfigSerializer(instance, data=request.data or {}, partial=True)
serializer.is_valid(raise_exception=True)
result = _run_config_test(
"settings.threat_intel.opencti.test",
test_opencti_config,
_opencti_config_from_instance(instance, serializer.validated_data),
)
_write_audit(instance, "test", request.user, metadata={"success": result["success"]})
return Response(result, status=status.HTTP_200_OK)
def _singleton_view_context(request):
return {
"reveal_secrets": request.query_params.get("reveal_secrets") in {"1", "true", "yes"},
}
def _singleton_patch(instance, serializer_class, request, audit_fields, secret_fields, invalidate_group):
before = _snapshot(instance, audit_fields)
serializer = serializer_class(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
changes = _audit_changes(before, _snapshot(instance, audit_fields), secret_fields)
if changes:
_write_audit(instance, "update", request.user, changes=changes)
transaction.on_commit(lambda: _invalidate_siem(invalidate_group))
return Response(serializer_class(instance).data)
class SiemSplunkConfigView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def get(self, request):
instance = SiemSplunkConfig.get_current()
if _singleton_view_context(request)["reveal_secrets"]:
_write_audit(instance, "reveal", request.user, metadata={"fields": ["password"]})
serializer = SiemSplunkConfigSerializer(instance, context=_singleton_view_context(request))
return Response(serializer.data)
@transaction.atomic
def patch(self, request):
return _singleton_patch(
SiemSplunkConfig.get_current(),
SiemSplunkConfigSerializer,
request,
SPLUNK_AUDIT_FIELDS,
{"password"},
"splunk",
)
class SiemSplunkTestView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def post(self, request):
instance = SiemSplunkConfig.get_current()
serializer = SiemSplunkConfigSerializer(instance, data=request.data or {}, partial=True)
serializer.is_valid(raise_exception=True)
config = _snapshot(instance, SPLUNK_AUDIT_FIELDS)
config.update(serializer.validated_data)
result = _run_config_test("settings.siem.splunk.test", test_splunk_config, config)
_write_audit(instance, "test", request.user, metadata={"success": result["success"]})
return Response(result, status=status.HTTP_200_OK)
class SiemElkConfigView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def get(self, request):
instance = SiemElkConfig.get_current()
if _singleton_view_context(request)["reveal_secrets"]:
_write_audit(instance, "reveal", request.user, metadata={"fields": ["api_key"]})
serializer = SiemElkConfigSerializer(instance, context=_singleton_view_context(request))
return Response(serializer.data)
@transaction.atomic
def patch(self, request):
return _singleton_patch(
SiemElkConfig.get_current(),
SiemElkConfigSerializer,
request,
ELK_AUDIT_FIELDS,
{"api_key"},
"elk",
)
class SiemElkTestView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def post(self, request):
instance = SiemElkConfig.get_current()
serializer = SiemElkConfigSerializer(instance, data=request.data or {}, partial=True)
serializer.is_valid(raise_exception=True)
config = _snapshot(instance, ELK_AUDIT_FIELDS)
config.update(serializer.validated_data)
result = _run_config_test("settings.siem.elk.test", test_elk_config, config)
_write_audit(instance, "test", request.user, metadata={"success": result["success"]})
return Response(result, status=status.HTTP_200_OK)
class LdapConfigView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def get(self, request):
instance = LdapConfig.get_current()
if _singleton_view_context(request)["reveal_secrets"]:
_write_audit(instance, "reveal", request.user, metadata={"fields": ["bind_password"]})
serializer = LdapConfigSerializer(instance, context=_singleton_view_context(request))
return Response(serializer.data)
@transaction.atomic
def patch(self, request):
instance = LdapConfig.get_current()
before = _snapshot(instance, LDAP_AUDIT_FIELDS)
serializer = LdapConfigSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
changes = _audit_changes(before, _snapshot(instance, LDAP_AUDIT_FIELDS), {"bind_password"})
if changes:
_write_audit(instance, "update", request.user, changes=changes)
transaction.on_commit(lambda: invalidate("ldap"))
return Response(LdapConfigSerializer(instance).data)
class LdapTestView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def post(self, request):
from apps.accounts.ldap import test_ldap_config
instance = LdapConfig.get_current()
serializer = LdapConfigSerializer(instance, data=request.data or {}, partial=True)
serializer.is_valid(raise_exception=True)
config = _snapshot(instance, LDAP_AUDIT_FIELDS)
config.update(serializer.validated_data)
test_username = str(request.data.get("test_username") or "")
test_password = str(request.data.get("test_password") or "")
result = _run_config_test(
"settings.ldap.test",
lambda data: test_ldap_config(
data,
test_username=test_username,
test_password=test_password,
),
config,
)
_write_audit(instance, "test", request.user, metadata={"success": result["success"]})
return Response(result, status=status.HTTP_200_OK)
class RuntimeConfigView(views.APIView):
permission_classes = [permissions.IsAuthenticated, IsAdmin]
def get(self, request):
instance = RuntimeConfig.get_current()
return Response(RuntimeConfigSerializer(instance).data)
@transaction.atomic
def patch(self, request):
instance = RuntimeConfig.get_current()
before = _snapshot(instance, RUNTIME_AUDIT_FIELDS)
serializer = RuntimeConfigSerializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
changes = _audit_changes(before, _snapshot(instance, RUNTIME_AUDIT_FIELDS), set())
if changes:
_write_audit(instance, "update", request.user, changes=changes)
transaction.on_commit(lambda: invalidate("runtime"))
return Response(RuntimeConfigSerializer(instance).data)