feat(scans): persistencia de escaneos de red + POST /api/scan
Tabla network_scans (migración 005, schema main, lleva note_path) que otras herramientas pueblan vía HTTP con escaneos de reconocimiento (whois/rdap/dns/ nmap/traceroute/ping). Endpoint POST /api/scan: id determinista <target_slug>:<scan_type>:<YYYYMMDD-HHMM> derivado de scan_ts, idempotente por id (duckdb_upsert ON CONFLICT DO UPDATE) bajo el lock single-writer del service. summary (dict) se serializa a JSON. network_scans no se deriva de notas: ni ingest_vault ni ingest_dav la tocan, así que un re-ingest del vault no la trunca (test lo verifica). Tests: inserción + id derivado, idempotencia mismo-minuto, validación de campos requeridos (422), y no-truncado por ingest del vault. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,13 @@ from server import writes # noqa: E402
|
||||
RENDER_MAX_ROWS = 200
|
||||
|
||||
|
||||
def _utcnow_iso() -> str:
|
||||
"""Timestamp ISO 8601 en UTC (default de scan_ts si la herramienta no lo da)."""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
return datetime.now(tz=timezone.utc).isoformat()
|
||||
|
||||
|
||||
class QueryBody(BaseModel):
|
||||
"""Body de POST /api/query."""
|
||||
|
||||
@@ -154,6 +161,23 @@ class CalendarBody(BaseModel):
|
||||
color: str | None = None
|
||||
|
||||
|
||||
class ScanBody(BaseModel):
|
||||
"""Body de POST /api/scan: escaneo de red registrado por otra herramienta.
|
||||
|
||||
target/target_slug/scan_type/note_path son obligatorios; tool y summary son
|
||||
opcionales. scan_ts es un timestamp ISO del momento del escaneo (default: el
|
||||
ahora del service si la herramienta no lo aporta) del que se deriva el id.
|
||||
"""
|
||||
|
||||
target: str
|
||||
target_slug: str
|
||||
scan_type: str
|
||||
tool: str | None = None
|
||||
note_path: str
|
||||
summary: dict = Field(default_factory=dict)
|
||||
scan_ts: str | None = None
|
||||
|
||||
|
||||
def create_app(cfg: Config) -> FastAPI:
|
||||
"""Construye la app FastAPI con la configuración dada (inyectable en tests)."""
|
||||
app = FastAPI(title="osint_db", docs_url=None, redoc_url=None)
|
||||
@@ -393,6 +417,17 @@ def create_app(cfg: Config) -> FastAPI:
|
||||
# last-write-wins por etag (incremental, distinto de ingest_dav).
|
||||
return _guard(lambda: writes.pull_dav(cfg))
|
||||
|
||||
@app.post("/api/scan")
|
||||
def record_scan(body: ScanBody) -> dict:
|
||||
# Escaneo de red (whois/rdap/dns/nmap/...) registrado por otra
|
||||
# herramienta. La escritura va bajo el lock single-writer vía
|
||||
# writes.record_scan (duckdb_upsert idempotente por id). Si la
|
||||
# herramienta no aporta scan_ts, usamos el ahora del service en ISO.
|
||||
fields = body.model_dump()
|
||||
if not fields.get("scan_ts"):
|
||||
fields["scan_ts"] = _utcnow_iso()
|
||||
return _guard(lambda: writes.record_scan(cfg, fields))
|
||||
|
||||
@app.post("/api/org/render-contacts")
|
||||
def org_render_contacts() -> dict:
|
||||
# Materializa en la ficha .md de cada organización la tabla de sus
|
||||
|
||||
Reference in New Issue
Block a user