"""REST endpoints exposed by IoT PreTester 4.6 Demo.""" import json import re from pathlib import Path from uuid import uuid4 from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.files.storage import FileSystemStorage from django.http import HttpResponse from django.db.models.deletion import ProtectedError from django.shortcuts import get_object_or_404 from rest_framework import serializers, status from rest_framework.permissions import AllowAny, IsAdminUser from rest_framework.response import Response from rest_framework.views import APIView from .analyze.security_analysis import collect_cloud_results from .db_create import ( create_capture, create_device_type, create_manufacturer, create_new_device, ) from .db_read import ( get_all_device_types, get_all_devices, get_all_manufacturers, get_device_by_id, get_files_by_device, ) from .models import ( DeepAnalysisJob, Device, DeviceType, File, FindingReport, Manufacturer, ) from .user_funcs import ( delete_user, list_users, login_user, logout_user, register_user, set_password, ) def _validation_error(exc: ValidationError) -> Response: messages = getattr(exc, "messages", [str(exc)]) return Response({"error": messages[0]}, status=status.HTTP_400_BAD_REQUEST) class HealthView(APIView): permission_classes = [AllowAny] def get(self, request): return Response({"status": "ok", "service": "IoT PreTester 4.6 Demo"}) class LoginView(APIView): permission_classes = [AllowAny] def post(self, request): username = request.data.get("username") password = request.data.get("password") if not username or not password: return Response( [False, {"message": "Username and password are required."}], status=status.HTTP_400_BAD_REQUEST, ) return Response(login_user(request, username, password)) class LogoutView(APIView): def post(self, request): return Response(logout_user(request)) class UserSerializer(serializers.Serializer): username = serializers.CharField(max_length=150) password = serializers.CharField(write_only=True) role = serializers.ChoiceField(choices=["admin", "user"]) class RegisterUserView(APIView): permission_classes = [IsAdminUser] def post(self, request): serializer = UserSerializer(data=request.data) serializer.is_valid(raise_exception=True) try: result = register_user(**serializer.validated_data) except ValidationError as exc: return _validation_error(exc) return Response(result, status=status.HTTP_201_CREATED) class ListUsersView(APIView): permission_classes = [IsAdminUser] def get(self, request): return Response(list_users()) class DeleteUserView(APIView): permission_classes = [IsAdminUser] def post(self, request): try: user_id = int(request.data.get("id")) return Response(delete_user(request, user_id)) except (TypeError, ValueError): return Response({"error": "A valid user ID is required."}, status=400) except ObjectDoesNotExist: return Response({"error": "The user does not exist."}, status=404) except ValidationError as exc: return _validation_error(exc) class ResetPasswordView(APIView): permission_classes = [IsAdminUser] def post(self, request): user_id = request.data.get("user_id") password = request.data.get("new_password") if not isinstance(password, str) or not password: return Response({"error": "A new password is required."}, status=400) try: return Response(set_password(int(user_id), password)) except (TypeError, ValueError): return Response({"error": "A valid user ID is required."}, status=400) except ObjectDoesNotExist: return Response({"error": "The user does not exist."}, status=404) except ValidationError as exc: return _validation_error(exc) class CreateManufacturerView(APIView): permission_classes = [IsAdminUser] def post(self, request): try: item = create_manufacturer(request.data.get("name")) return Response(item.to_json(), status=status.HTTP_201_CREATED) except ValidationError as exc: return _validation_error(exc) class CreateDeviceTypeView(APIView): permission_classes = [IsAdminUser] def post(self, request): try: item = create_device_type(request.data.get("device_type")) return Response(item.to_json(), status=status.HTTP_201_CREATED) except ValidationError as exc: return _validation_error(exc) class DeviceSerializer(serializers.Serializer): device_type_id = serializers.IntegerField() device_name = serializers.CharField(max_length=100) serial = serializers.CharField(max_length=100) manufacturer_id = serializers.IntegerField() comment = serializers.CharField(max_length=255, required=False, allow_blank=True) class CreateDeviceView(APIView): permission_classes = [IsAdminUser] def post(self, request): serializer = DeviceSerializer(data=request.data) serializer.is_valid(raise_exception=True) try: device = create_new_device(**serializer.validated_data) return Response(device.to_json(), status=status.HTTP_201_CREATED) except ObjectDoesNotExist: return Response({"error": "The manufacturer or device type does not exist."}, status=400) except ValidationError as exc: return _validation_error(exc) class CaptureSerializer(serializers.Serializer): capture = serializers.FileField(write_only=True) device_id = serializers.IntegerField() firmware_version = serializers.CharField(max_length=100) ip_address = serializers.IPAddressField() def validate_capture(self, capture): if capture.size <= 0: raise serializers.ValidationError("The capture is empty.") if capture.size > 512 * 1024 * 1024: raise serializers.ValidationError("The capture exceeds the 512 MB demo limit.") if Path(capture.name).suffix.lower() not in {".pcap", ".pcapng"}: raise serializers.ValidationError("Only PCAP and PCAPNG captures are supported.") return capture class CreateCaptureView(APIView): def post(self, request): serializer = CaptureSerializer(data=request.data) serializer.is_valid(raise_exception=True) values = dict(serializer.validated_data) uploaded_capture = values.pop("capture") suffix = Path(uploaded_capture.name).suffix.lower() stem = re.sub(r"[^A-Za-z0-9._-]+", "_", Path(uploaded_capture.name).stem)[:80] stored_name = f"{stem or 'capture'}-{uuid4().hex[:12]}{suffix}" storage = FileSystemStorage(location=settings.CAPTURE_UPLOAD_DIR) stored_name = storage.save(stored_name, uploaded_capture) try: capture = create_capture(filename=stored_name, **values) except ObjectDoesNotExist: storage.delete(stored_name) return Response({"error": "The selected device does not exist."}, status=400) except ValidationError as exc: storage.delete(stored_name) return _validation_error(exc) except Exception: storage.delete(stored_name) raise return Response( {"status": "success", "data": capture.to_json()}, status=status.HTTP_201_CREATED, ) class ManufacturersView(APIView): def get(self, request): return Response(get_all_manufacturers()) class DeviceTypesView(APIView): def get(self, request): return Response(get_all_device_types()) class DevicesView(APIView): def get(self, request): return Response(get_all_devices()) class DeviceDetailView(APIView): def get(self, request, device_id): get_object_or_404(Device, id=device_id) return Response(get_device_by_id(device_id)) class DeviceCapturesView(APIView): def get(self, request, device_id): get_object_or_404(Device, id=device_id) return Response(get_files_by_device(device_id)) class DeleteDeviceView(APIView): permission_classes = [IsAdminUser] def delete(self, request, device_id): device = get_object_or_404(Device, id=device_id) device.delete() return Response({"status": "success"}) class DeleteManufacturerView(APIView): permission_classes = [IsAdminUser] def delete(self, request, manufacturer_id): item = get_object_or_404(Manufacturer, id=manufacturer_id) try: item.delete() except ProtectedError: return Response( {"error": "The manufacturer is still used by a product."}, status=status.HTTP_409_CONFLICT, ) return Response({"status": "success"}) class DeleteDeviceTypeView(APIView): permission_classes = [IsAdminUser] def delete(self, request, device_type_id): item = get_object_or_404(DeviceType, id=device_type_id) try: item.delete() except ProtectedError: return Response( {"error": "The device type is still used by a product."}, status=status.HTTP_409_CONFLICT, ) return Response({"status": "success"}) class FindingsView(APIView): def get(self, request, file_id): get_object_or_404(File, id=file_id) report = FindingReport.objects.filter(file_id=file_id).first() if report is None: return Response( { "status": "empty", "message": "No security analysis is available for this capture.", "summary": { "total": 0, "by_severity": {}, "by_category": {}, "highlights": [], "top": [], }, "findings": [], } ) return Response(json.loads(report.result_json)) class CloudResultsView(APIView): def get(self, request, file_id): get_object_or_404(File, id=file_id) response = Response(collect_cloud_results(file_id)) response["Cache-Control"] = "no-store" return response class StartAnalysisView(APIView): def post(self, request, file_id): get_object_or_404(File, id=file_id) try: url_ids = [int(value) for value in (request.data.get("url_ids") or [])] hash_ids = [int(value) for value in (request.data.get("hash_ids") or [])] except (TypeError, ValueError): return Response({"error": "Artifact IDs must be integers."}, status=400) if not url_ids and not hash_ids: return Response({"error": "Select at least one artifact."}, status=400) from .analyze.deep_analysis import start_deep_analysis start_deep_analysis(file_id, url_ids=url_ids, hash_ids=hash_ids) return Response( {"status": "started", "url_count": len(url_ids), "hash_count": len(hash_ids)}, status=status.HTTP_202_ACCEPTED, ) class AnalysisStatusView(APIView): def get(self, request, file_id): get_object_or_404(File, id=file_id) job = DeepAnalysisJob.objects.filter(file_id=file_id).first() data = job.to_json() if job else { "state": "pending", "url_total": 0, "url_done": 0, "hash_total": 0, "hash_done": 0, "cracked": 0, "progress_done": 0, "progress_total": 0, "percent": 0, "message": "Not started.", } response = Response(data) response["Cache-Control"] = "no-store" return response class ReportView(APIView): def get(self, request, file_id, fmt): from analyzer import FORMATS, render, result_from_dict fmt = fmt.lower() if fmt not in FORMATS: return Response({"error": f"Allowed formats: {', '.join(FORMATS)}"}, status=400) report = get_object_or_404(FindingReport, file_id=file_id) result = result_from_dict(json.loads(report.result_json)) content = render(result, fmt, collect_cloud_results(file_id)) content_types = {"md": "text/markdown", "txt": "text/plain", "json": "application/json"} response = HttpResponse(content, content_type=content_types[fmt]) response["Content-Disposition"] = ( f'attachment; filename="iot-pretester-4.6-demo-report-{file_id}.{fmt}"' ) return response