123 lines
4.0 KiB
Python
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
|