build 4.6 Demo

This commit is contained in:
DaStra3003
2026-08-19 08:04:59 +02:00
commit ff8f1ca989
123 changed files with 17696 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
"""Django admin registrations for demo operations."""
from django.contrib import admin
from .models import (
AnalyzedDataset,
CipherSuite,
DeepAnalysisJob,
Device,
DeviceType,
File,
FindingReport,
Firmware,
FirmwareDevices,
HashAnalyzed,
HashRaw,
HashType,
Manufacturer,
Port,
Product,
RawDataset,
TlsVersion,
Tool,
UrlAnalyzed,
UrlRaw,
)
admin.site.register(
[
Manufacturer,
DeviceType,
Product,
Device,
Firmware,
FirmwareDevices,
File,
RawDataset,
HashRaw,
UrlRaw,
Tool,
Port,
TlsVersion,
CipherSuite,
HashType,
AnalyzedDataset,
UrlAnalyzed,
HashAnalyzed,
FindingReport,
DeepAnalysisJob,
]
)
@@ -0,0 +1,168 @@
"""Optional hash cracking and endpoint probing with progress persistence."""
from __future__ import annotations
import logging
import threading
from django.db import connection
logger = logging.getLogger(__name__)
def start_deep_analysis(file_id: int, url_ids=None, hash_ids=None,
synchronous: bool = False) -> None:
"""Start enrichment for all artifacts or the explicitly selected raw IDs."""
if synchronous:
_run(file_id, url_ids, hash_ids, close_conn=False)
return
thread = threading.Thread(target=_run, args=(file_id, url_ids, hash_ids), daemon=True,
name=f"deep-analysis-{file_id}")
thread.start()
def _analyzed_dataset_for(raw_obj):
"""Return the single analyzed dataset associated with a capture."""
from IoTAnalyzer.models import AnalyzedDataset
file_obj = raw_obj.raw_dataset.file
ds, _ = AnalyzedDataset.objects.get_or_create(file=file_obj)
return ds
def crack_raw_hash(raw_hash) -> None:
"""Crack one raw hash and persist the result idempotently."""
from django.db import transaction
from IoTAnalyzer.models import Tool, HashType, HashAnalyzed
from analyzer.artifacts import HashArtifact
from analyzer.enrich import crack_hash
try:
res = crack_hash(HashArtifact(value=raw_hash.hash_value))
cracked = bool(res.cracked and res.plaintext)
logger.info("Hash id=%s mode=%s state=%s cracked=%s error=%s",
raw_hash.id, res.hashcat_mode, res.state, cracked, res.error)
ds = _analyzed_dataset_for(raw_hash)
tool, _ = Tool.objects.get_or_create(tool_name="Hashcat", defaults={"version": "6.x.x"})
htype, _ = HashType.objects.get_or_create(hash_type=(res.hash_format or "unknown"))
crack_time = f"{res.duration_seconds}s" if res.duration_seconds is not None else None
with transaction.atomic():
ha, created = HashAnalyzed.objects.get_or_create(
analyzed_dataset=ds, hash_value=res.value,
defaults={"tool": tool, "cracked": cracked,
"cracked_value": res.plaintext if cracked else None,
"crack_time": crack_time},
)
if not created:
ha.tool = tool
ha.cracked = cracked
ha.cracked_value = res.plaintext if cracked else None
ha.crack_time = crack_time
ha.save()
ha.hash_type.add(htype)
logger.info("Stored hash result id=%s (created=%s, cracked=%s)", ha.id, created, cracked)
except Exception as exc:
logger.exception("Hash enrichment failed (id=%s): %s", raw_hash.id, exc)
def probe_raw_url(raw_url) -> None:
"""Probe one raw endpoint and persist DNS, port, and TLS results."""
from django.db import transaction
from IoTAnalyzer.models import Tool, UrlAnalyzed, Port, TlsVersion, CipherSuite
from analyzer.enrich import probe_url
try:
probe = probe_url(raw_url.url, active=True)
logger.info("URL id=%s ports=%s tls=%s error=%s",
raw_url.id, probe.open_ports, probe.tls_versions, probe.error)
ds = _analyzed_dataset_for(raw_url)
tool, _ = Tool.objects.get_or_create(tool_name="DNS Analyzer", defaults={"version": "1.0"})
with transaction.atomic():
ua, created = UrlAnalyzed.objects.get_or_create(
analyzed_dataset=ds, url=raw_url.url, defaults={"tool": tool})
for p in probe.open_ports:
port, _ = Port.objects.get_or_create(port=p)
ua.ports.add(port)
for v in (probe.tls_versions or ["N/A"]):
tls, _ = TlsVersion.objects.get_or_create(tls_version=v)
ua.tls_versions.add(tls)
for c in (probe.cipher_suites or ["N/A"]):
cs, _ = CipherSuite.objects.get_or_create(cipher_suite=c)
ua.cipher_suites.add(cs)
logger.info("Stored endpoint result id=%s (created=%s)", ua.id, created)
except Exception as exc:
logger.exception("Endpoint enrichment failed (id=%s): %s", raw_url.id, exc)
def _run(file_id: int, url_ids=None, hash_ids=None, close_conn: bool = True) -> None:
from IoTAnalyzer.models import UrlRaw, HashRaw, HashAnalyzed, DeepAnalysisJob
job = None
try:
url_qs = UrlRaw.objects.filter(raw_dataset__file_id=file_id)
hash_qs = HashRaw.objects.filter(raw_dataset__file_id=file_id)
# None selects all records; an empty list intentionally selects none.
if url_ids is not None:
url_qs = url_qs.filter(id__in=url_ids)
if hash_ids is not None:
hash_qs = hash_qs.filter(id__in=hash_ids)
urls = list(url_qs)
hashes = list(hash_qs)
DeepAnalysisJob.objects.filter(file_id=file_id).delete()
job = DeepAnalysisJob.objects.create(
file_id=file_id, state='running',
url_total=len(urls), hash_total=len(hashes),
message='Enrichment started',
)
logger.info("Enrichment started: file=%s urls=%s hashes=%s",
file_id, len(urls), len(hashes))
# Crack hashes first because they usually complete quickly.
for raw_hash in hashes:
try:
crack_raw_hash(raw_hash)
except Exception as exc:
logger.exception("Hash cracking failed (id=%s): %s", raw_hash.id, exc)
job.hash_done += 1
job.message = f"Hash-Cracking {job.hash_done}/{job.hash_total}"
job.save(update_fields=['hash_done', 'message', 'updated'])
# Endpoint probing may perform DNS, port, and TLS checks.
for raw_url in urls:
try:
probe_raw_url(raw_url)
except Exception as exc:
logger.exception("Endpoint probing failed (id=%s): %s", raw_url.id, exc)
job.url_done += 1
job.message = f"Endpoint probing {job.url_done}/{job.url_total}"
job.save(update_fields=['url_done', 'message', 'updated'])
try:
job.cracked = HashAnalyzed.objects.filter(
analyzed_dataset__file_id=file_id, cracked=True
).count()
except Exception:
pass
job.state = 'done'
job.message = 'Enrichment completed'
job.save()
logger.info("Enrichment completed: file=%s cracked=%s", file_id, job.cracked)
except Exception as exc:
logger.exception("Enrichment failed for file=%s: %s", file_id, exc)
if job is not None:
job.state = 'error'
job.message = str(exc)[:500]
try:
job.save()
except Exception:
pass
finally:
# Close only the worker thread's connection, never the request connection.
if close_conn:
connection.close()
@@ -0,0 +1,146 @@
"""Persist results and artifacts produced by the shared analysis context."""
from __future__ import annotations
import json
import logging
import os
from django.conf import settings
from analyzer import AnalysisEngine, extract_urls, extract_hashes
from analyzer.extract_pyshark import build_context_from_pcap
from analyzer.findings import Severity
from IoTAnalyzer.models import FindingReport, UrlRaw, HashRaw
logger = logging.getLogger(__name__)
_CANDIDATE_DIRS = [
str(settings.CAPTURE_UPLOAD_DIR),
]
def resolve_capture_path(filename: str) -> str | None:
"""Resolve a stored capture name against supported upload locations."""
if not filename:
return None
if os.path.isabs(filename) and os.path.exists(filename):
return filename
for base in _CANDIDATE_DIRS:
candidate = os.path.join(base, filename)
if os.path.exists(candidate):
return candidate
return None
def _max_severity(result) -> str:
worst = Severity.INFO
for f in result.findings:
if f.severity.rank > worst.rank:
worst = f.severity
return worst.value
def collect_cloud_results(file_id: int) -> dict:
"""Return deduplicated capture artifacts and any enrichment results."""
from IoTAnalyzer.models import UrlRaw, HashRaw, UrlAnalyzed, HashAnalyzed
# Retain raw IDs so the frontend can request a selected enrichment subset.
raw_urls = list(UrlRaw.objects.filter(raw_dataset__file_id=file_id)
.values_list('id', 'url'))
raw_hashes = list(HashRaw.objects.filter(raw_dataset__file_id=file_id)
.values_list('id', 'hash_value'))
url_map = {}
for ua in (UrlAnalyzed.objects.filter(analyzed_dataset__file_id=file_id)
.prefetch_related('ports', 'tls_versions', 'cipher_suites')):
url_map[ua.url] = {
"ports": sorted({p.port for p in ua.ports.all()}),
"tls_versions": [t.tls_version for t in ua.tls_versions.all()],
"cipher_suites": [c.cipher_suite for c in ua.cipher_suites.all()],
}
hash_map = {}
for ha in (HashAnalyzed.objects.filter(analyzed_dataset__file_id=file_id)
.prefetch_related('hash_type')):
entry = {
"cracked": ha.cracked,
"cracked_value": ha.cracked_value,
"crack_time": ha.crack_time,
"hash_type": [h.hash_type for h in ha.hash_type.all()],
}
existing = hash_map.get(ha.hash_value)
if existing is None or (ha.cracked and not existing.get("cracked")):
hash_map[ha.hash_value] = entry
urls, seen = [], set()
for raw_id, u in raw_urls:
if not u or u in seen:
continue
seen.add(u)
a = url_map.get(u)
urls.append({"id": raw_id, "url": u, "analyzed": a is not None, **(a or {})})
hashes, seen_h = [], set()
for raw_id, h in raw_hashes:
if not h or h in seen_h:
continue
seen_h.add(h)
a = hash_map.get(h)
hashes.append({"id": raw_id, "hash_value": h, "analyzed": a is not None,
**(a or {"cracked": False, "cracked_value": None,
"crack_time": None, "hash_type": []})})
return {"urls": urls, "hashes": hashes}
def _persist_artifacts(ctx, raw_dataset_id: int) -> None:
"""Persist URL and hash artifacts extracted during the analysis pass."""
if raw_dataset_id is None:
return
try:
for u in extract_urls(ctx):
UrlRaw.objects.get_or_create(raw_dataset_id=raw_dataset_id, url=u.value[:200])
except Exception as exc:
logger.exception("URL persistence failed: %s", exc)
try:
for h in extract_hashes(ctx):
HashRaw.objects.get_or_create(raw_dataset_id=raw_dataset_id, hash_value=h.value)
except Exception as exc:
logger.exception("Hash persistence failed: %s", exc)
def analyze_capture(filename: str, file_id: int, raw_dataset_id: int | None = None):
"""Analyze a capture once and persist findings plus extracted artifacts."""
path = resolve_capture_path(filename)
if not path:
logger.warning("Capture file not found: %s", filename)
return None
try:
ctx = build_context_from_pcap(path)
result = AnalysisEngine().analyze_context(ctx)
except Exception as exc:
logger.exception("Security analysis failed for %s: %s", filename, exc)
return None
try:
# Reanalysis replaces the previous report atomically at the file level.
FindingReport.objects.filter(file_id=file_id).delete()
FindingReport.objects.create(
file_id=file_id,
result_json=json.dumps(result.to_dict()),
finding_count=len(result.findings),
max_severity=_max_severity(result),
)
logger.info("Stored finding report: file_id=%s, findings=%s",
file_id, len(result.findings))
except Exception as exc:
logger.exception("Finding report persistence failed: %s", exc)
_persist_artifacts(ctx, raw_dataset_id)
return result
def run_security_analysis(filename: str, file_id: int):
return analyze_capture(filename, file_id, raw_dataset_id=None)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class IotanalyzerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'IoTAnalyzer'
@@ -0,0 +1,13 @@
"""Session authentication adapted to the localhost-only demo API.
State-changing requests remain protected by authenticated session cookies and a
strict CORS allow-list. The separate CSRF check is disabled because the frontend
and API use different localhost ports.
"""
from rest_framework.authentication import SessionAuthentication
class DemoSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
return None
+122
View File
@@ -0,0 +1,122 @@
"""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, analyze the stored file, and update its status."""
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 analyze_capture
result = analyze_capture(filename, file_entry.id, raw_dataset.id)
file_entry.status = "analyzed" if result is not None else "analysis_failed"
file_entry.save(update_fields=["status"])
logger.info("Capture %s completed with status %s", file_entry.id, file_entry.status)
return file_entry
+51
View File
@@ -0,0 +1,51 @@
"""Read projections used by the demo API."""
from .models import Device, DeviceType, File, Manufacturer
def get_all_manufacturers() -> list[dict]:
return [item.to_json() for item in Manufacturer.objects.all()]
def get_all_device_types() -> list[dict]:
return [item.to_json() for item in DeviceType.objects.all()]
def get_all_devices() -> list[dict]:
devices = Device.objects.select_related(
"product", "product__manufacturer", "product__device_type"
)
return [
{
"id": device.id,
"manufacturer_name": device.product.manufacturer.name,
"device_type": device.product.device_type.device_type,
"device_name": device.product.product_name,
"serial": device.serial,
"comment": device.comment,
"created": device.created.isoformat(),
}
for device in devices
]
def get_device_by_id(device_id: int) -> dict:
device = Device.objects.select_related(
"product", "product__manufacturer", "product__device_type"
).get(id=device_id)
return device.to_json()
def get_files_by_device(device_id: int) -> list[dict]:
files = File.objects.filter(device_id=device_id).select_related("firmware")
return [
{
"id": file.id,
"filename": file.filename,
"ip": file.ip_address,
"state": file.status,
"upload": file.upload_time.isoformat(),
"firmware_version": file.firmware.version,
}
for file in files
]
@@ -0,0 +1,333 @@
# Generated by Django 4.2.20 on 2026-07-17 12:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='AnalyzedDataset',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_analyzed_dataset',
},
),
migrations.CreateModel(
name='CipherSuite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cipher_suite', models.CharField(max_length=100, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_cipher_suites',
},
),
migrations.CreateModel(
name='Device',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('serial', models.CharField(max_length=100)),
('comment', models.CharField(blank=True, default='', max_length=255)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_device',
},
),
migrations.CreateModel(
name='DeviceType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('device_type', models.CharField(max_length=50, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_device_type',
'ordering': ['device_type'],
},
),
migrations.CreateModel(
name='File',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filename', models.CharField(max_length=255)),
('ip_address', models.GenericIPAddressField()),
('status', models.CharField(default='pending', max_length=50)),
('upload_time', models.DateTimeField(auto_now_add=True)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.device')),
],
options={
'db_table': 'iot_files',
'ordering': ['-upload_time'],
},
),
migrations.CreateModel(
name='Firmware',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('version', models.CharField(max_length=100)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_firmware',
},
),
migrations.CreateModel(
name='HashType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hash_type', models.CharField(max_length=100, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_hash_types',
},
),
migrations.CreateModel(
name='Manufacturer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_manufacturer',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Port',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('port', models.PositiveIntegerField(unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_ports',
},
),
migrations.CreateModel(
name='RawDataset',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('file', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.file')),
],
options={
'db_table': 'iot_raw_dataset',
},
),
migrations.CreateModel(
name='TlsVersion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tls_version', models.CharField(max_length=50, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_tls_versions',
},
),
migrations.CreateModel(
name='Tool',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tool_name', models.CharField(max_length=100, unique=True)),
('version', models.CharField(max_length=50)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'iot_tool',
},
),
migrations.CreateModel(
name='UrlRaw',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.CharField(max_length=500)),
('created', models.DateTimeField(auto_now_add=True)),
('raw_dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.rawdataset')),
],
options={
'db_table': 'iot_raw_url',
},
),
migrations.CreateModel(
name='UrlAnalyzed',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('url', models.CharField(max_length=500)),
('created', models.DateTimeField(auto_now_add=True)),
('analyzed_dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.analyzeddataset')),
('cipher_suites', models.ManyToManyField(to='IoTAnalyzer.ciphersuite')),
('ports', models.ManyToManyField(to='IoTAnalyzer.port')),
('tls_versions', models.ManyToManyField(to='IoTAnalyzer.tlsversion')),
('tool', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='IoTAnalyzer.tool')),
],
options={
'db_table': 'iot_analyzed_url',
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_name', models.CharField(max_length=100)),
('created', models.DateTimeField(auto_now_add=True)),
('device_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='IoTAnalyzer.devicetype')),
('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='IoTAnalyzer.manufacturer')),
],
options={
'db_table': 'iot_product',
'ordering': ['product_name'],
},
),
migrations.CreateModel(
name='HashRaw',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hash_value', models.TextField()),
('created', models.DateTimeField(auto_now_add=True)),
('raw_dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.rawdataset')),
],
options={
'db_table': 'iot_raw_hash',
},
),
migrations.CreateModel(
name='HashAnalyzed',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hash_value', models.TextField()),
('cracked', models.BooleanField(default=False)),
('cracked_value', models.CharField(blank=True, max_length=255, null=True)),
('crack_time', models.CharField(blank=True, max_length=50, null=True)),
('created', models.DateTimeField(auto_now_add=True)),
('analyzed_dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.analyzeddataset')),
('hash_type', models.ManyToManyField(to='IoTAnalyzer.hashtype')),
('tool', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='IoTAnalyzer.tool')),
],
options={
'db_table': 'iot_analyzed_hash',
},
),
migrations.CreateModel(
name='FirmwareDevices',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.device')),
('firmware', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.firmware')),
],
options={
'db_table': 'iot_firmware_devices',
},
),
migrations.AddField(
model_name='firmware',
name='devices',
field=models.ManyToManyField(through='IoTAnalyzer.FirmwareDevices', to='IoTAnalyzer.device'),
),
migrations.AddField(
model_name='firmware',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='IoTAnalyzer.product'),
),
migrations.CreateModel(
name='FindingReport',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('result_json', models.TextField()),
('finding_count', models.IntegerField(default=0)),
('max_severity', models.CharField(default='info', max_length=20)),
('created', models.DateTimeField(auto_now_add=True)),
('file', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.file')),
],
options={
'db_table': 'iot_finding_report',
'ordering': ['-created'],
},
),
migrations.AddField(
model_name='file',
name='firmware',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='IoTAnalyzer.firmware'),
),
migrations.AddField(
model_name='device',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='IoTAnalyzer.product'),
),
migrations.CreateModel(
name='DeepAnalysisJob',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('state', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('done', 'Done'), ('error', 'Error')], default='pending', max_length=20)),
('url_total', models.IntegerField(default=0)),
('url_done', models.IntegerField(default=0)),
('hash_total', models.IntegerField(default=0)),
('hash_done', models.IntegerField(default=0)),
('cracked', models.IntegerField(default=0)),
('message', models.TextField(blank=True, default='')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('file', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.file')),
],
options={
'db_table': 'iot_deep_analysis_job',
'ordering': ['-created'],
},
),
migrations.AddField(
model_name='analyzeddataset',
name='file',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='IoTAnalyzer.file'),
),
migrations.AddConstraint(
model_name='urlraw',
constraint=models.UniqueConstraint(fields=('raw_dataset', 'url'), name='unique_raw_url'),
),
migrations.AddConstraint(
model_name='urlanalyzed',
constraint=models.UniqueConstraint(fields=('analyzed_dataset', 'url'), name='unique_analyzed_url'),
),
migrations.AddConstraint(
model_name='product',
constraint=models.UniqueConstraint(fields=('manufacturer', 'device_type', 'product_name'), name='unique_demo_product'),
),
migrations.AddConstraint(
model_name='hashraw',
constraint=models.UniqueConstraint(fields=('raw_dataset', 'hash_value'), name='unique_raw_hash'),
),
migrations.AddConstraint(
model_name='hashanalyzed',
constraint=models.UniqueConstraint(fields=('analyzed_dataset', 'hash_value'), name='unique_analyzed_hash'),
),
migrations.AddConstraint(
model_name='firmwaredevices',
constraint=models.UniqueConstraint(fields=('firmware', 'device'), name='unique_device_firmware'),
),
migrations.AddConstraint(
model_name='firmware',
constraint=models.UniqueConstraint(fields=('product', 'version'), name='unique_product_firmware'),
),
migrations.AddConstraint(
model_name='file',
constraint=models.UniqueConstraint(fields=('device', 'firmware', 'filename', 'ip_address'), name='unique_device_capture'),
),
migrations.AddConstraint(
model_name='device',
constraint=models.UniqueConstraint(fields=('product', 'serial'), name='unique_demo_device'),
),
]
+337
View File
@@ -0,0 +1,337 @@
"""Persistence models for IoT PreTester 4.6 Demo."""
import json
from django.db import models
class Manufacturer(models.Model):
name = models.CharField(max_length=50, unique=True)
created = models.DateTimeField(auto_now_add=True)
def to_json(self):
return {"id": self.id, "name": self.name, "created": self.created.isoformat()}
class Meta:
db_table = "iot_manufacturer"
ordering = ["name"]
class DeviceType(models.Model):
device_type = models.CharField(max_length=50, unique=True)
created = models.DateTimeField(auto_now_add=True)
def to_json(self):
return {
"id": self.id,
"device_type": self.device_type,
"created": self.created.isoformat(),
}
class Meta:
db_table = "iot_device_type"
ordering = ["device_type"]
class Product(models.Model):
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.PROTECT)
device_type = models.ForeignKey(DeviceType, on_delete=models.PROTECT)
product_name = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
def to_json(self):
return {
"id": self.id,
"manufacturer": self.manufacturer.to_json(),
"product_name": self.product_name,
"device_type": self.device_type.to_json(),
"created": self.created.isoformat(),
}
class Meta:
db_table = "iot_product"
ordering = ["product_name"]
constraints = [
models.UniqueConstraint(
fields=["manufacturer", "device_type", "product_name"],
name="unique_demo_product",
)
]
class Device(models.Model):
product = models.ForeignKey(Product, on_delete=models.PROTECT)
serial = models.CharField(max_length=100)
comment = models.CharField(max_length=255, blank=True, default="")
created = models.DateTimeField(auto_now_add=True)
def to_json(self):
return {
"id": self.id,
"product": self.product.to_json(),
"serial": self.serial,
"comment": self.comment,
"created": self.created.isoformat(),
}
class Meta:
db_table = "iot_device"
constraints = [
models.UniqueConstraint(
fields=["product", "serial"], name="unique_demo_device"
)
]
class Firmware(models.Model):
"""Firmware metadata associated with a capture; binaries are not analyzed."""
version = models.CharField(max_length=100)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
devices = models.ManyToManyField(Device, through="FirmwareDevices")
created = models.DateTimeField(auto_now_add=True)
def to_json(self):
return {
"id": self.id,
"version": self.version,
"product": self.product_id,
"created": self.created.isoformat(),
}
class Meta:
db_table = "iot_firmware"
constraints = [
models.UniqueConstraint(
fields=["product", "version"], name="unique_product_firmware"
)
]
class FirmwareDevices(models.Model):
firmware = models.ForeignKey(Firmware, on_delete=models.CASCADE)
device = models.ForeignKey(Device, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_firmware_devices"
constraints = [
models.UniqueConstraint(
fields=["firmware", "device"], name="unique_device_firmware"
)
]
class File(models.Model):
firmware = models.ForeignKey(Firmware, on_delete=models.PROTECT)
device = models.ForeignKey(Device, on_delete=models.CASCADE)
filename = models.CharField(max_length=255)
ip_address = models.GenericIPAddressField()
status = models.CharField(max_length=50, default="pending")
upload_time = models.DateTimeField(auto_now_add=True)
def to_json(self):
return {
"id": self.id,
"filename": self.filename,
"ip_address": self.ip_address,
"firmware_version": self.firmware.version,
"status": self.status,
"upload_time": self.upload_time.isoformat(),
}
class Meta:
db_table = "iot_files"
ordering = ["-upload_time"]
constraints = [
models.UniqueConstraint(
fields=["device", "firmware", "filename", "ip_address"],
name="unique_device_capture",
)
]
class RawDataset(models.Model):
file = models.OneToOneField(File, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_raw_dataset"
class HashRaw(models.Model):
raw_dataset = models.ForeignKey(RawDataset, on_delete=models.CASCADE)
hash_value = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_raw_hash"
constraints = [
models.UniqueConstraint(
fields=["raw_dataset", "hash_value"], name="unique_raw_hash"
)
]
class UrlRaw(models.Model):
raw_dataset = models.ForeignKey(RawDataset, on_delete=models.CASCADE)
url = models.CharField(max_length=500)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_raw_url"
constraints = [
models.UniqueConstraint(
fields=["raw_dataset", "url"], name="unique_raw_url"
)
]
class Tool(models.Model):
tool_name = models.CharField(max_length=100, unique=True)
version = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_tool"
class Port(models.Model):
port = models.PositiveIntegerField(unique=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_ports"
class TlsVersion(models.Model):
tls_version = models.CharField(max_length=50, unique=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_tls_versions"
class CipherSuite(models.Model):
cipher_suite = models.CharField(max_length=100, unique=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_cipher_suites"
class HashType(models.Model):
hash_type = models.CharField(max_length=100, unique=True)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_hash_types"
class AnalyzedDataset(models.Model):
file = models.OneToOneField(File, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_analyzed_dataset"
class UrlAnalyzed(models.Model):
analyzed_dataset = models.ForeignKey(AnalyzedDataset, on_delete=models.CASCADE)
url = models.CharField(max_length=500)
tool = models.ForeignKey(Tool, on_delete=models.PROTECT)
ports = models.ManyToManyField(Port)
tls_versions = models.ManyToManyField(TlsVersion)
cipher_suites = models.ManyToManyField(CipherSuite)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_analyzed_url"
constraints = [
models.UniqueConstraint(
fields=["analyzed_dataset", "url"], name="unique_analyzed_url"
)
]
class HashAnalyzed(models.Model):
analyzed_dataset = models.ForeignKey(AnalyzedDataset, on_delete=models.CASCADE)
hash_type = models.ManyToManyField(HashType)
hash_value = models.TextField()
cracked = models.BooleanField(default=False)
cracked_value = models.CharField(max_length=255, blank=True, null=True)
crack_time = models.CharField(max_length=50, blank=True, null=True)
tool = models.ForeignKey(Tool, on_delete=models.PROTECT)
created = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "iot_analyzed_hash"
constraints = [
models.UniqueConstraint(
fields=["analyzed_dataset", "hash_value"], name="unique_analyzed_hash"
)
]
class FindingReport(models.Model):
file = models.OneToOneField(File, on_delete=models.CASCADE)
result_json = models.TextField()
finding_count = models.IntegerField(default=0)
max_severity = models.CharField(max_length=20, default="info")
created = models.DateTimeField(auto_now_add=True)
def to_json(self):
return {
"id": self.id,
"file_id": self.file_id,
"finding_count": self.finding_count,
"max_severity": self.max_severity,
"created": self.created.isoformat(),
"result": json.loads(self.result_json),
}
class Meta:
db_table = "iot_finding_report"
ordering = ["-created"]
class DeepAnalysisJob(models.Model):
STATE_CHOICES = [
("pending", "Pending"),
("running", "Running"),
("done", "Done"),
("error", "Error"),
]
file = models.OneToOneField(File, on_delete=models.CASCADE)
state = models.CharField(max_length=20, choices=STATE_CHOICES, default="pending")
url_total = models.IntegerField(default=0)
url_done = models.IntegerField(default=0)
hash_total = models.IntegerField(default=0)
hash_done = models.IntegerField(default=0)
cracked = models.IntegerField(default=0)
message = models.TextField(blank=True, default="")
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def to_json(self):
done = self.url_done + self.hash_done
total = self.url_total + self.hash_total
return {
"file_id": self.file_id,
"state": self.state,
"url_total": self.url_total,
"url_done": self.url_done,
"hash_total": self.hash_total,
"hash_done": self.hash_done,
"cracked": self.cracked,
"progress_done": done,
"progress_total": total,
"percent": int(done * 100 / total) if total else (100 if self.state == "done" else 0),
"message": self.message,
"updated": self.updated.isoformat(),
}
class Meta:
db_table = "iot_deep_analysis_job"
ordering = ["-created"]
+32
View File
@@ -0,0 +1,32 @@
"""URL routes for the IoT PreTester 4.6 Demo API."""
from django.urls import path
from . import views
urlpatterns = [
path("api/health", views.HealthView.as_view(), name="health"),
path("api/login", views.LoginView.as_view(), name="login"),
path("api/logout", views.LogoutView.as_view(), name="logout"),
path("api/register", views.RegisterUserView.as_view(), name="register_user"),
path("api/list-users", views.ListUsersView.as_view(), name="list_users"),
path("api/delete-user", views.DeleteUserView.as_view(), name="delete_user"),
path("api/reset-password", views.ResetPasswordView.as_view(), name="reset_password"),
path("api/create_manufact", views.CreateManufacturerView.as_view(), name="create_manufacturer"),
path("api/create_device_type", views.CreateDeviceTypeView.as_view(), name="create_device_type"),
path("api/create_device", views.CreateDeviceView.as_view(), name="create_device"),
path("api/captures", views.CreateCaptureView.as_view(), name="create_capture"),
path("api/get_all_manufacturers", views.ManufacturersView.as_view(), name="manufacturers"),
path("api/get_all_devicetypes", views.DeviceTypesView.as_view(), name="device_types"),
path("api/get_all_devices", views.DevicesView.as_view(), name="devices"),
path("api/get_device_by_id/<int:device_id>", views.DeviceDetailView.as_view(), name="device_detail"),
path("api/get_file_by_device/<int:device_id>", views.DeviceCapturesView.as_view(), name="device_captures"),
path("api/delete_device/<int:device_id>", views.DeleteDeviceView.as_view(), name="delete_device"),
path("api/delete_manufacturer/<int:manufacturer_id>", views.DeleteManufacturerView.as_view(), name="delete_manufacturer"),
path("api/delete_device_type/<int:device_type_id>", views.DeleteDeviceTypeView.as_view(), name="delete_device_type"),
path("api/get_findings/<int:file_id>", views.FindingsView.as_view(), name="findings"),
path("api/get_cloud_results/<int:file_id>", views.CloudResultsView.as_view(), name="cloud_results"),
path("api/start_analysis/<int:file_id>", views.StartAnalysisView.as_view(), name="start_analysis"),
path("api/get_analysis_status/<int:file_id>", views.AnalysisStatusView.as_view(), name="analysis_status"),
path("api/get_report/<int:file_id>/<str:fmt>", views.ReportView.as_view(), name="report"),
]
+80
View File
@@ -0,0 +1,80 @@
"""User-account services for the controlled demo environment."""
from django.contrib.auth import authenticate, get_user_model, login, logout
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.db import transaction
User = get_user_model()
@transaction.atomic
def register_user(username: str, password: str, role: str) -> dict:
username = (username or "").strip()
if not username:
raise ValidationError("Username is required.")
if role not in {"admin", "user"}:
raise ValidationError("Role must be admin or user.")
if User.objects.filter(username=username).exists():
raise ValidationError("This username already exists.")
validate_password(password)
is_admin = role == "admin"
User.objects.create_user(
username=username,
password=password,
is_superuser=is_admin,
is_staff=is_admin,
)
return {"status": "success", "message": "User created successfully."}
@transaction.atomic
def delete_user(request, user_id: int) -> dict:
if request.user.id == user_id:
raise ValidationError("You cannot delete your own account.")
user = User.objects.get(id=user_id)
user.delete()
return {"status": "success", "message": "User deleted successfully."}
@transaction.atomic
def set_password(user_id: int, new_password: str) -> dict:
user = User.objects.get(id=user_id)
validate_password(new_password, user)
user.set_password(new_password)
user.save(update_fields=["password"])
return {"status": "success", "message": "Password reset successfully."}
def login_user(request, username: str, password: str):
user = authenticate(request, username=(username or "").strip(), password=password)
if user is None:
return False, {"message": "Invalid username or password."}
login(request, user)
role = "admin" if user.is_superuser else "user"
return True, {
"message": "Login successful.",
"user": {"id": user.id, "username": user.username, "role": role},
}
def logout_user(request) -> dict:
logout(request)
return {"status": "success", "message": "Logout successful."}
def list_users() -> dict:
return {
"status": "success",
"data": [
{
"id": user.id,
"username": user.username,
"email": user.email,
"role": "admin" if user.is_superuser else "user",
}
for user in User.objects.order_by("username")
],
}
+382
View File
@@ -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