383 lines
13 KiB
Python
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
|