feat: Add OSINT features (v0.1.2)

- MAC vendor lookup (IEEE OUI database)
- BLE company_id to manufacturer mapping
- Device profile enrichment in API responses
- Export endpoints: devices.csv, devices.json, alerts.csv, probes.csv
- Auto-populate vendor on device creation
- CLI command: flask download-oui
- Makefile target: make oui

13 tests passing
This commit is contained in:
user
2026-02-05 21:14:27 +01:00
parent 924d28aab0
commit 3ad39cfaeb
16 changed files with 365 additions and 7 deletions

View File

@@ -3,4 +3,4 @@ from flask import Blueprint
bp = Blueprint('api', __name__)
from . import sensors, devices, alerts, events, probes, stats # noqa: E402, F401
from . import sensors, devices, alerts, events, probes, stats, export # noqa: E402, F401

View File

@@ -3,6 +3,7 @@ from flask import request
from . import bp
from ..models import Device, Sighting
from ..extensions import db
from ..services.device_service import enrich_device
@bp.route('/devices')
@@ -18,7 +19,7 @@ def list_devices():
query = query.limit(limit).offset(offset)
devices = db.session.scalars(query).all()
return {'devices': [d.to_dict() for d in devices], 'limit': limit, 'offset': offset}
return {'devices': [enrich_device(d) for d in devices], 'limit': limit, 'offset': offset}
@bp.route('/devices/<mac>')
@@ -37,6 +38,6 @@ def get_device(mac):
.limit(20)
).all()
result = device.to_dict()
result = enrich_device(device)
result['sightings'] = [s.to_dict() for s in sightings]
return result

100
src/esp32_web/api/export.py Normal file
View File

@@ -0,0 +1,100 @@
"""Export endpoints."""
import csv
import io
import json
from flask import Response, request
from . import bp
from ..models import Device, Alert, Probe
from ..extensions import db
from ..services.device_service import enrich_device
@bp.route('/export/devices.csv')
def export_devices_csv():
"""Export devices as CSV."""
devices = db.session.scalars(db.select(Device).order_by(Device.last_seen.desc())).all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['mac', 'type', 'vendor', 'name', 'company_id', 'first_seen', 'last_seen'])
for d in devices:
writer.writerow([
d.mac, d.device_type, d.vendor or '', d.name or '',
d.company_id or '', d.first_seen.isoformat(), d.last_seen.isoformat()
])
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': 'attachment; filename=devices.csv'}
)
@bp.route('/export/devices.json')
def export_devices_json():
"""Export devices as JSON."""
devices = db.session.scalars(db.select(Device).order_by(Device.last_seen.desc())).all()
data = [enrich_device(d) for d in devices]
return Response(
json.dumps(data, indent=2),
mimetype='application/json',
headers={'Content-Disposition': 'attachment; filename=devices.json'}
)
@bp.route('/export/alerts.csv')
def export_alerts_csv():
"""Export alerts as CSV."""
hours = request.args.get('hours', 24, type=int)
from datetime import datetime, timedelta, UTC
since = datetime.now(UTC) - timedelta(hours=hours)
alerts = db.session.scalars(
db.select(Alert).where(Alert.timestamp >= since).order_by(Alert.timestamp.desc())
).all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['timestamp', 'sensor_id', 'type', 'source_mac', 'target_mac', 'rssi'])
for a in alerts:
writer.writerow([
a.timestamp.isoformat(), a.sensor_id, a.alert_type,
a.source_mac or '', a.target_mac or '', a.rssi or ''
])
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': 'attachment; filename=alerts.csv'}
)
@bp.route('/export/probes.csv')
def export_probes_csv():
"""Export probe requests as CSV."""
hours = request.args.get('hours', 24, type=int)
from datetime import datetime, timedelta, UTC
since = datetime.now(UTC) - timedelta(hours=hours)
probes = db.session.scalars(
db.select(Probe).where(Probe.timestamp >= since).order_by(Probe.timestamp.desc())
).all()
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['timestamp', 'sensor_id', 'device_id', 'ssid', 'rssi', 'channel'])
for p in probes:
writer.writerow([
p.timestamp.isoformat(), p.sensor_id, p.device_id,
p.ssid, p.rssi, p.channel
])
return Response(
output.getvalue(),
mimetype='text/csv',
headers={'Content-Disposition': 'attachment; filename=probes.csv'}
)