Files
intelligence-terminal/apis/sources/ofac.mjs
MrSphay 62d9c680b7
All checks were successful
Codex Template Compliance / template-compliance (pull_request) Successful in 7s
Build / test-and-image (pull_request) Successful in 53s
fix: harden degraded source handling
2026-07-07 21:16:24 +02:00

178 lines
5.5 KiB
JavaScript

// OFAC — US Treasury Office of Foreign Assets Control Sanctions
// No auth required. Monitors the Specially Designated Nationals (SDN) list
// and consolidated sanctions list for changes.
const EXPORTS_BASE = 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports';
// SDN list endpoints
const SDN_XML_URL = `${EXPORTS_BASE}/SDN.XML`;
const SDN_ADVANCED_URL = `${EXPORTS_BASE}/SDN_ADVANCED.XML`;
const CONS_ADVANCED_URL = `${EXPORTS_BASE}/CONS_ADVANCED.XML`;
// Parse basic info from SDN XML (publish date, entry count)
function parseSDNMetadata(xml) {
if (!xml || xml.error) return { error: xml?.error || 'No data returned' };
const raw = xml.rawText || '';
// Extract publish date
const publishDate = raw.match(/<Publish_Date>(.*?)<\/Publish_Date>/)?.[1]
|| raw.match(/<publish_date>(.*?)<\/publish_date>/i)?.[1]
|| null;
// Count SDN entries
const entryMatches = raw.match(/<sdnEntry>/gi);
const entryCount = entryMatches ? entryMatches.length : null;
// Extract record count if present
const recordCount = raw.match(/<Record_Count>(.*?)<\/Record_Count>/)?.[1]
|| raw.match(/<records_count>(.*?)<\/records_count>/i)?.[1]
|| null;
return {
publishDate,
entryCount,
recordCount: recordCount ? parseInt(recordCount, 10) : null,
hasData: raw.length > 0,
dataSize: raw.length,
};
}
// Fetch SDN list metadata (smaller initial chunk via timeout)
export async function getSDNMetadata() {
const data = await fetchXmlSnippet(SDN_XML_URL);
return parseSDNMetadata(data);
}
// Fetch advanced SDN data (includes more structured info)
export async function getSDNAdvanced() {
const data = await fetchXmlSnippet(SDN_ADVANCED_URL);
return parseSDNMetadata(data);
}
// Fetch consolidated list metadata
export async function getConsolidatedMetadata() {
const data = await fetchXmlSnippet(CONS_ADVANCED_URL);
return parseSDNMetadata(data);
}
// Parse recent SDN entries from XML snippet
function parseRecentEntries(xml) {
if (!xml || xml.error) return [];
const raw = xml.rawText || '';
const entries = [];
const entryRegex = /<sdnEntry>([\s\S]*?)<\/sdnEntry>/gi;
let match;
let count = 0;
while ((match = entryRegex.exec(raw)) !== null && count < 20) {
const content = match[1];
const uid = content.match(/<uid>(.*?)<\/uid>/i)?.[1];
const lastName = content.match(/<lastName>(.*?)<\/lastName>/i)?.[1];
const firstName = content.match(/<firstName>(.*?)<\/firstName>/i)?.[1];
const sdnType = content.match(/<sdnType>(.*?)<\/sdnType>/i)?.[1];
// Extract programs
const programs = [];
const progRegex = /<program>(.*?)<\/program>/gi;
let progMatch;
while ((progMatch = progRegex.exec(content)) !== null) {
programs.push(progMatch[1]);
}
if (uid || lastName) {
entries.push({
uid,
name: [firstName, lastName].filter(Boolean).join(' '),
type: sdnType,
programs,
});
count++;
}
}
return entries;
}
// Briefing — report on sanctions list status and metadata
export async function briefing() {
const [sdnMeta, advancedMeta, advancedSnippet] = await Promise.all([
getSDNMetadata(),
getSDNAdvanced(),
fetchXmlSnippet(SDN_ADVANCED_URL, { maxBytes: 256_000 }),
]);
const sampleEntries = parseRecentEntries(advancedSnippet);
return {
source: 'OFAC Sanctions',
timestamp: new Date().toISOString(),
lastUpdated: sdnMeta.publishDate || advancedMeta.publishDate || 'unknown',
sdnList: {
publishDate: sdnMeta.publishDate,
entryCount: sdnMeta.entryCount,
recordCount: sdnMeta.recordCount,
dataAvailable: sdnMeta.hasData,
},
advancedList: {
publishDate: advancedMeta.publishDate,
entryCount: advancedMeta.entryCount,
recordCount: advancedMeta.recordCount,
dataAvailable: advancedMeta.hasData,
},
sampleEntries: sampleEntries.slice(0, 10),
endpoints: {
sdnXml: SDN_XML_URL,
sdnAdvanced: SDN_ADVANCED_URL,
consolidatedAdvanced: CONS_ADVANCED_URL,
},
};
}
async function fetchXmlSnippet(url, opts = {}) {
const timeout = opts.timeout || 9000;
const maxBytes = opts.maxBytes || 128_000;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, {
headers: {
'User-Agent': 'Crucix/2.0',
'Range': `bytes=0-${maxBytes - 1}`,
},
signal: controller.signal,
});
if (!res.ok && res.status !== 206) {
const body = await res.text().catch(() => '');
return { error: `HTTP ${res.status}: ${body.slice(0, 160)}` };
}
if (!res.body?.getReader) {
const text = await res.text();
return { rawText: text.slice(0, maxBytes) };
}
const reader = res.body.getReader();
const chunks = [];
let bytes = 0;
while (bytes < maxBytes) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
bytes += value.byteLength;
}
await reader.cancel().catch(() => {});
const buffer = Buffer.concat(chunks.map(chunk => Buffer.from(chunk)), bytes);
return { rawText: buffer.toString('utf8', 0, Math.min(bytes, maxBytes)) };
} catch (error) {
return { error: error.name === 'AbortError' ? `OFAC snippet request timed out after ${timeout}ms` : error.message };
} finally {
clearTimeout(timer);
}
}
// Run standalone
if (process.argv[1]?.endsWith('ofac.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}