build 4.6 Demo
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user