"""Focused creation services used by the demo API.""" import ipaddress import logging import os from django.core.exceptions import ValidationError from django.db import transaction from .models import ( Device, DeviceType, File, Firmware, FirmwareDevices, Manufacturer, Product, RawDataset, ) logger = logging.getLogger(__name__) CAPTURE_EXTENSIONS = {".pcap", ".pcapng"} def _required_text(value, label: str, max_length: int) -> str: if not isinstance(value, str) or not value.strip(): raise ValidationError(f"{label} is required.") value = value.strip() if len(value) > max_length: raise ValidationError(f"{label} must not exceed {max_length} characters.") return value @transaction.atomic def create_manufacturer(name: str) -> Manufacturer: name = _required_text(name, "Manufacturer name", 50) manufacturer, created = Manufacturer.objects.get_or_create(name=name) if not created: raise ValidationError("This manufacturer already exists.") return manufacturer @transaction.atomic def create_device_type(device_type: str) -> DeviceType: device_type = _required_text(device_type, "Device type", 50) instance, created = DeviceType.objects.get_or_create(device_type=device_type) if not created: raise ValidationError("This device type already exists.") return instance @transaction.atomic def create_new_device( manufacturer_id: int, device_type_id: int, device_name: str, serial: str, comment: str = "", ) -> Device: manufacturer = Manufacturer.objects.get(id=manufacturer_id) device_type = DeviceType.objects.get(id=device_type_id) device_name = _required_text(device_name, "Device name", 100) serial = _required_text(serial, "Serial number", 100) comment = (comment or "").strip()[:255] product, _ = Product.objects.get_or_create( manufacturer=manufacturer, device_type=device_type, product_name=device_name, ) device, created = Device.objects.get_or_create( product=product, serial=serial, defaults={"comment": comment}, ) if not created: raise ValidationError("A device with this product and serial number already exists.") return device def create_capture( device_id: int, firmware_version: str, filename: str, ip_address: str, ) -> File: """Create capture metadata and hand the analysis to a background worker.""" firmware_version = _required_text(firmware_version, "Firmware version", 100) filename = os.path.basename(_required_text(filename, "Capture filename", 255)) if os.path.splitext(filename)[1].lower() not in CAPTURE_EXTENSIONS: raise ValidationError("Only .pcap and .pcapng captures are supported.") try: normalized_ip = str(ipaddress.ip_address(ip_address)) except ValueError as exc: raise ValidationError("A valid device IP address is required.") from exc with transaction.atomic(): device = Device.objects.select_related("product").get(id=device_id) firmware, _ = Firmware.objects.get_or_create( product=device.product, version=firmware_version, ) FirmwareDevices.objects.get_or_create(device=device, firmware=firmware) file_entry, created = File.objects.get_or_create( device=device, firmware=firmware, filename=filename, ip_address=normalized_ip, defaults={"status": "analyzing"}, ) if not created: raise ValidationError("This capture is already registered for the device.") raw_dataset = RawDataset.objects.create(file=file_entry) from .analyze.security_analysis import start_capture_analysis # The analysis reads the whole capture with tshark and can run for many # minutes, so it must not block the upload response. start_capture_analysis(file_entry.id, filename, raw_dataset.id) logger.info("Capture %s registered, analysis queued", file_entry.id) return file_entry