Files
IoT_PreTester_V46_Demo/django_project/IoTAnalyzer/views.py
T
2026-08-19 08:04:59 +02:00

383 lines
13 KiB
Python

"""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