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:
@@ -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
|
||||
|
||||
@@ -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
100
src/esp32_web/api/export.py
Normal 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'}
|
||||
)
|
||||
Reference in New Issue
Block a user