[Medium] Intel sanctions refresh replaces a populated index with an unvalidated empty/malformed CSV result
Severity: Medium (data integrity / silent capability loss) Confidence: Confirmed by an executed zero-network reproduction of the actual refresh function. Audited commit: bd4057567de8ee18d49a8b2744c9746bcd67f3e0 (master, 2026-09-13 audit).
The intelligence service's sanctions-list refresh treats any HTTP 200 response with two or more CSV rows as a successful load, even if the CSV has none of the expected columns. A schema-drifted or damaged upstream feed silently wipes the previously loaded, working sanctions index and replaces it with an empty one — reported as healthy — until the next successful refresh (every 24 hours) or a restart.
Evidence and mechanism
- intel/server.js:75-85: only the physical row count is validated; a missing expected header yields column index
-1with no rejection. - Lines 90-113: rows lacking the expected name column are dropped one by one, and the resulting (possibly empty) index unconditionally replaces the live one.
- Lines 115-119: the code path that would retain the previous good index only runs on a thrown exception, not on a "successful" but structurally invalid response.
- Line 708-715:
/healthcontinues to reportstatus: 'ok'regardless.
Reproduction and observed result
Executed the actual loadSanctions()/sanctionsSearch() functions in a zero-network harness:
- Load a valid fixture row;
sanctionsSearch('Example Sanctioned Company')returns one match. - Simulate the next scheduled refresh returning HTTP 200 with
unexpected,columns\nx,y(noid/namecolumns). - The same lookup now returns zero entries; the log states the index "loaded: 0 entities";
/healthstill reportsok.
{"case":"malformed CSV replaces loaded index","entries":0,"health":"ok","loggedAsLoaded":true}Impact
A single malformed upstream response — schema drift, a CDN error page served with HTTP 200, a truncated download — silently erases sanctions-screening capability for up to 24 hours, with no operational signal that anything degraded.
Smallest correction and acceptance criteria
- Validate that required columns/records are present and reject the refresh (retaining the previous index) before the atomic index-assignment step, the same way the existing exception-driven retention path already works.
- Represent initial-load failure and stale-refresh state explicitly in availability/freshness metadata; do not report an unqualified "ok" over a rejected or degraded refresh.
- Acceptance: a refresh missing
id/namecolumns, or otherwise structurally invalid, must leave the previously loaded entries and successful-fetch timestamp intact and must not be logged as a successful load.
Prior-issue check: #33/#151 (closed) address the unrelated Next.js /api/health endpoint; #39 addresses data provenance elsewhere. No existing issue covers loss of the intel service's sanctions index on malformed CSV.
Source: simplifaisoul/osiris