from rest_framework import serializers from .models import ( LdapConfig, LLMProviderConfig, RuntimeConfig, SiemElkConfig, SiemSplunkConfig, ThreatIntelAlienVaultOTXConfig, ThreatIntelOpenCTIConfig, ) class LLMProviderConfigSerializer(serializers.ModelSerializer): api_key_configured = serializers.SerializerMethodField() class Meta: model = LLMProviderConfig fields = ( "id", "name", "base_url", "model", "api_key", "api_key_configured", "proxy", "tags", "enabled", "priority", "created_at", "updated_at", ) read_only_fields = ("id", "api_key_configured", "created_at", "updated_at") extra_kwargs = { "api_key": {"required": False, "allow_blank": True, "trim_whitespace": False}, "proxy": {"required": False, "allow_blank": True}, "tags": {"required": False}, } def get_api_key_configured(self, obj): return bool(obj.api_key) def validate_tags(self, value): if value in (None, ""): return [] if not isinstance(value, list): raise serializers.ValidationError("Tags must be a list.") tags = [] for item in value: tag = str(item).strip() if tag and tag not in tags: tags.append(tag) return tags def validate(self, attrs): attrs = super().validate(attrs) tags = attrs.get("tags") if tags is None and self.instance is not None: tags = self.instance.tags if not tags: raise serializers.ValidationError({"tags": "Select at least one tag."}) return attrs def validate_proxy(self, value): proxy = (value or "").strip() if proxy and not proxy.startswith(("http://", "https://", "socks4://", "socks5://")): raise serializers.ValidationError("Proxy must start with http://, https://, socks4://, or socks5://.") return proxy def to_representation(self, instance): data = super().to_representation(instance) if not self.context.get("reveal_secrets"): data["api_key"] = "" return data class ThreatIntelAlienVaultOTXConfigSerializer(serializers.ModelSerializer): api_key_configured = serializers.SerializerMethodField() class Meta: model = ThreatIntelAlienVaultOTXConfig fields = ( "enabled", "api_key", "api_key_configured", "base_url", "proxy", "updated_at", ) read_only_fields = ("api_key_configured", "updated_at") extra_kwargs = { "api_key": {"required": True, "allow_blank": False, "trim_whitespace": False}, "proxy": {"required": False, "allow_blank": True}, } def get_api_key_configured(self, obj): return bool(obj.api_key) def validate_proxy(self, value): proxy = (value or "").strip() if proxy and not proxy.startswith(("http://", "https://", "socks4://", "socks5://")): raise serializers.ValidationError("Proxy must start with http://, https://, socks4://, or socks5://.") return proxy def validate(self, attrs): attrs = super().validate(attrs) api_key = attrs.get("api_key") if api_key is None and self.instance is not None: api_key = self.instance.api_key if not str(api_key or "").strip(): raise serializers.ValidationError({"api_key": "API key is required."}) return attrs def to_representation(self, instance): data = super().to_representation(instance) if not self.context.get("reveal_secrets"): data["api_key"] = "" return data class ThreatIntelOpenCTIConfigSerializer(serializers.ModelSerializer): token_configured = serializers.SerializerMethodField() class Meta: model = ThreatIntelOpenCTIConfig fields = ( "enabled", "url", "token", "token_configured", "ssl_verify", "proxy", "updated_at", ) read_only_fields = ("token_configured", "updated_at") extra_kwargs = { "url": {"required": True, "allow_blank": False}, "token": {"required": True, "allow_blank": False, "trim_whitespace": False}, "proxy": {"required": False, "allow_blank": True}, } def get_token_configured(self, obj): return bool(obj.token) def validate_proxy(self, value): proxy = (value or "").strip() if proxy and not proxy.startswith(("http://", "https://", "socks4://", "socks5://")): raise serializers.ValidationError("Proxy must start with http://, https://, socks4://, or socks5://.") return proxy def validate(self, attrs): attrs = super().validate(attrs) token = attrs.get("token") if token is None and self.instance is not None: token = self.instance.token if not str(token or "").strip(): raise serializers.ValidationError({"token": "API token is required."}) return attrs def to_representation(self, instance): data = super().to_representation(instance) if not self.context.get("reveal_secrets"): data["token"] = "" return data class SiemSplunkConfigSerializer(serializers.ModelSerializer): password_configured = serializers.SerializerMethodField() class Meta: model = SiemSplunkConfig fields = ( "host", "port", "username", "password", "password_configured", "scheme", "verify", "updated_at", ) read_only_fields = ("password_configured", "updated_at") extra_kwargs = { "host": {"required": True, "allow_blank": False}, "username": {"required": True, "allow_blank": False}, "password": {"required": True, "allow_blank": False, "trim_whitespace": False}, } def get_password_configured(self, obj): return bool(obj.password) def validate_port(self, value): if value <= 0 or value > 65535: raise serializers.ValidationError("Port must be between 1 and 65535.") return value def validate_scheme(self, value): scheme = (value or "").strip().lower() if scheme not in {"http", "https"}: raise serializers.ValidationError("Scheme must be http or https.") return scheme def validate(self, attrs): attrs = super().validate(attrs) for field in ("host", "username", "password"): value = attrs.get(field) if value is None and self.instance is not None: value = getattr(self.instance, field) if not str(value or "").strip(): raise serializers.ValidationError({field: f"{field.replace('_', ' ').title()} is required."}) return attrs def to_representation(self, instance): data = super().to_representation(instance) if not self.context.get("reveal_secrets"): data["password"] = "" return data class SiemElkConfigSerializer(serializers.ModelSerializer): api_key_configured = serializers.SerializerMethodField() class Meta: model = SiemElkConfig fields = ( "host", "api_key", "api_key_configured", "verify_certs", "process_alert_from_index_enabled", "action_index", "action_poll_interval_seconds", "action_size", "updated_at", ) read_only_fields = ("api_key_configured", "updated_at") extra_kwargs = { "host": {"required": True, "allow_blank": False}, "api_key": {"required": True, "allow_blank": False, "trim_whitespace": False}, "action_index": {"required": False, "allow_blank": True}, "action_poll_interval_seconds": {"required": False, "allow_null": True}, "action_size": {"required": False, "allow_null": True}, } def get_api_key_configured(self, obj): return bool(obj.api_key) def validate_action_poll_interval_seconds(self, value): if value is None: return value if value <= 0: raise serializers.ValidationError("Action poll interval must be greater than 0.") return value def validate_action_size(self, value): if value is None: return value if value <= 0: raise serializers.ValidationError("Action size must be greater than 0.") return value def validate(self, attrs): attrs = super().validate(attrs) for field in ("host", "api_key"): value = attrs.get(field) if value is None and self.instance is not None: value = getattr(self.instance, field) if not str(value or "").strip(): raise serializers.ValidationError({field: f"{field.replace('_', ' ').title()} is required."}) process_enabled = attrs.get("process_alert_from_index_enabled") if process_enabled is None and self.instance is not None: process_enabled = self.instance.process_alert_from_index_enabled if process_enabled: action_index = attrs.get("action_index") if action_index is None and self.instance is not None: action_index = self.instance.action_index if not str(action_index or "").strip(): raise serializers.ValidationError({"action_index": "Action index is required when alert processing is enabled."}) poll_interval = attrs.get("action_poll_interval_seconds") if "action_poll_interval_seconds" in attrs and poll_interval is None: raise serializers.ValidationError({ "action_poll_interval_seconds": "Action poll interval is required when alert processing is enabled." }) if poll_interval is None and self.instance is not None: poll_interval = self.instance.action_poll_interval_seconds if poll_interval is None: raise serializers.ValidationError({ "action_poll_interval_seconds": "Action poll interval is required when alert processing is enabled." }) action_size = attrs.get("action_size") if "action_size" in attrs and action_size is None: raise serializers.ValidationError({ "action_size": "Action size is required when alert processing is enabled." }) if action_size is None and self.instance is not None: action_size = self.instance.action_size if action_size is None: raise serializers.ValidationError({"action_size": "Action size is required when alert processing is enabled."}) elif attrs.get("action_poll_interval_seconds") is None: attrs.pop("action_poll_interval_seconds", None) if not process_enabled and attrs.get("action_size") is None: attrs.pop("action_size", None) return attrs def to_representation(self, instance): data = super().to_representation(instance) if not self.context.get("reveal_secrets"): data["api_key"] = "" return data class LdapConfigSerializer(serializers.ModelSerializer): bind_password_configured = serializers.SerializerMethodField() class Meta: model = LdapConfig fields = ( "enabled", "server_uri", "domain", "bind_dn", "bind_password", "bind_password_configured", "user_search_base_dn", "user_login_attr", "updated_at", ) read_only_fields = ("bind_password_configured", "updated_at") extra_kwargs = { "server_uri": {"required": False, "allow_blank": True}, "domain": {"required": False, "allow_blank": True}, "bind_dn": {"required": False, "allow_blank": True}, "bind_password": {"required": False, "allow_blank": True, "trim_whitespace": False}, "user_search_base_dn": {"required": False, "allow_blank": True}, "user_login_attr": {"required": False, "allow_blank": True}, } def get_bind_password_configured(self, obj): return bool(obj.bind_password) def validate_server_uri(self, value): server_uri = (value or "").strip() if server_uri and not server_uri.startswith(("ldap://", "ldaps://")): raise serializers.ValidationError("Server URI must start with ldap:// or ldaps://.") return server_uri def validate(self, attrs): attrs = super().validate(attrs) enabled = attrs.get("enabled") if enabled is None and self.instance is not None: enabled = self.instance.enabled server_uri = attrs.get("server_uri") if server_uri is None and self.instance is not None: server_uri = self.instance.server_uri if enabled and not str(server_uri or "").strip(): raise serializers.ValidationError({"server_uri": "Server URI is required when LDAP is enabled."}) bind_dn = attrs.get("bind_dn") if bind_dn is None and self.instance is not None: bind_dn = self.instance.bind_dn bind_password = attrs.get("bind_password") if bind_password is None and self.instance is not None: bind_password = self.instance.bind_password if str(bind_dn or "").strip() and not str(bind_password or "").strip(): raise serializers.ValidationError({"bind_password": "Bind password is required when bind DN is configured."}) user_login_attr = attrs.get("user_login_attr") if user_login_attr is None and self.instance is not None: user_login_attr = self.instance.user_login_attr if not str(user_login_attr or "").strip(): raise serializers.ValidationError({"user_login_attr": "User login attribute is required."}) return attrs def to_representation(self, instance): data = super().to_representation(instance) if not self.context.get("reveal_secrets"): data["bind_password"] = "" return data class RuntimeConfigSerializer(serializers.ModelSerializer): DASHBOARD_REFRESH_INTERVALS = {300, 900, 1800, 3600} class Meta: model = RuntimeConfig fields = ( "prompt_language", "stream_maxlen", "dashboard_refresh_interval_seconds", "updated_at", ) read_only_fields = ("updated_at",) def validate_prompt_language(self, value): language = (value or "").strip().lower() if language not in {"en", "zh"}: raise serializers.ValidationError("Prompt language must be en or zh.") return language def validate_stream_maxlen(self, value): if value <= 0: raise serializers.ValidationError("Stream maxlen must be greater than 0.") return value def validate_dashboard_refresh_interval_seconds(self, value): if value not in self.DASHBOARD_REFRESH_INTERVALS: raise serializers.ValidationError("Dashboard refresh interval must be 300, 900, 1800, or 3600 seconds.") return value