Files

123 lines
4.0 KiB
Python

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