Spaces:
Runtime error
Runtime error
| /** | |
| * OSIRIS Intelligence Layer — osiris-intel | |
| * | |
| * Centralized ontology engine that ingests, indexes, and correlates entities | |
| * across open-source intelligence feeds. All other services query this one | |
| * brain via GET /resolve. | |
| * | |
| * Data sources: | |
| * - OpenSanctions (OFAC SDN) — bulk CSV, refreshed every 24h | |
| * - Wikidata SPARQL — on-demand with aggressive LRU cache | |
| * | |
| * Security: | |
| * - Outbound requests only to allowlisted domains | |
| * - SPARQL inputs sanitized against injection | |
| * - Rate-limited per client IP | |
| */ | |
| const express = require('express'); | |
| const app = express(); | |
| const PORT = process.env.INTEL_PORT || 4000; | |
| // ════════════════════════════════════════════════════ | |
| // §1 — CONFIGURATION | |
| // ════════════════════════════════════════════════════ | |
| const SDN_CSV_URL = 'https://data.opensanctions.org/datasets/latest/us_ofac_sdn/targets.simple.csv'; | |
| const WIKIDATA_ENDPOINT = 'https://query.wikidata.org/sparql'; | |
| const WIKIDATA_UA = 'OSIRIS-Intel/1.0 (https://osirisai.live; ontology engine)'; | |
| const SDN_REFRESH_MS = 24 * 60 * 60 * 1000; // 24h | |
| const WIKIDATA_CACHE_TTL = 24 * 60 * 60 * 1000; // 24h | |
| const WIKIDATA_CACHE_MAX = 10_000; | |
| const ALLOWED_DOMAINS = new Set(['query.wikidata.org', 'data.opensanctions.org', 'www.wikidata.org', 'ip-api.com', 'stat.ripe.net']); | |
| // ════════════════════════════════════════════════════ | |
| // §2 — SANCTIONS INDEX (in-memory graph) | |
| // ════════════════════════════════════════════════════ | |
| let sanctionsIndex = { | |
| entries: [], | |
| byNorm: new Map(), // normalised name/alias → [entry] | |
| fetchedAt: 0, | |
| }; | |
| function normName(s) { | |
| return s.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, ' ').replace(/\s+/g, ' ').trim(); | |
| } | |
| function parseCsv(text) { | |
| const rows = []; | |
| let field = '', row = [], inQ = false; | |
| for (let i = 0; i < text.length; i++) { | |
| const c = text[i]; | |
| if (inQ) { | |
| if (c === '"') { if (text[i + 1] === '"') { field += '"'; i++; } else inQ = false; } | |
| else field += c; | |
| } else if (c === '"') inQ = true; | |
| else if (c === ',') { row.push(field); field = ''; } | |
| else if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; } | |
| else if (c !== '\r') field += c; | |
| } | |
| if (field.length || row.length) { row.push(field); rows.push(row); } | |
| return rows; | |
| } | |
| async function loadSanctions() { | |
| console.log('[INTEL] Loading OpenSanctions OFAC SDN...'); | |
| try { | |
| const res = await fetch(SDN_CSV_URL, { | |
| signal: AbortSignal.timeout(30000), | |
| headers: { Accept: 'text/csv' }, | |
| }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| const text = await res.text(); | |
| const rows = parseCsv(text); | |
| if (rows.length < 2) throw new Error('CSV empty'); | |
| const headers = rows[0]; | |
| const idx = (col) => headers.indexOf(col); | |
| const i = { | |
| id: idx('id'), schema: idx('schema'), name: idx('name'), | |
| aliases: idx('aliases'), countries: idx('countries'), | |
| programs: idx('program_ids'), sanctions: idx('sanctions'), | |
| first_seen: idx('first_seen'), last_seen: idx('last_seen'), | |
| }; | |
| const entries = []; | |
| const byNorm = new Map(); | |
| for (let r = 1; r < rows.length; r++) { | |
| const row = rows[r]; | |
| if (!row[i.name]) continue; | |
| const entry = { | |
| id: row[i.id] || '', | |
| schema: row[i.schema] || 'LegalEntity', | |
| name: row[i.name], | |
| aliases: (row[i.aliases] || '').split(';').map(s => s.trim()).filter(Boolean), | |
| countries: (row[i.countries] || '').split(';').map(s => s.trim()).filter(Boolean), | |
| programs: (row[i.programs] || '').split(';').map(s => s.trim()).filter(Boolean), | |
| sanctions: row[i.sanctions] || '', | |
| first_seen: i.first_seen >= 0 ? row[i.first_seen] : undefined, | |
| }; | |
| entries.push(entry); | |
| const keys = new Set([entry.name, ...entry.aliases].map(normName)); | |
| for (const key of keys) { | |
| if (!key) continue; | |
| if (!byNorm.has(key)) byNorm.set(key, []); | |
| byNorm.get(key).push(entry); | |
| } | |
| } | |
| sanctionsIndex = { entries, byNorm, fetchedAt: Date.now() }; | |
| console.log(`[INTEL] Sanctions index loaded: ${entries.length} entities, ${byNorm.size} name keys`); | |
| } catch (e) { | |
| console.error('[INTEL] Sanctions load failed:', e.message); | |
| if (sanctionsIndex.entries.length > 0) { | |
| console.log('[INTEL] Keeping stale index'); | |
| } | |
| } | |
| } | |
| function sanctionsSearch(query, limit = 5) { | |
| if (!query || query.length < 3) return []; | |
| const q = normName(query); | |
| const exact = sanctionsIndex.byNorm.get(q) || []; | |
| if (exact.length > 0) return exact.slice(0, limit); | |
| const results = []; | |
| const seen = new Set(); | |
| for (const entry of sanctionsIndex.entries) { | |
| if (results.length >= limit) break; | |
| if (seen.has(entry.id)) continue; | |
| const n = normName(entry.name); | |
| if (n.includes(q) || entry.aliases.some(a => normName(a).includes(q))) { | |
| seen.add(entry.id); | |
| results.push(entry); | |
| } | |
| } | |
| return results; | |
| } | |
| // ════════════════════════════════════════════════════ | |
| // §3 — WIKIDATA LRU CACHE | |
| // ════════════════════════════════════════════════════ | |
| const wdCache = new Map(); // key → { data, ts } | |
| function wdCacheGet(key) { | |
| const entry = wdCache.get(key); | |
| if (!entry) return null; | |
| if (Date.now() - entry.ts > WIKIDATA_CACHE_TTL) { wdCache.delete(key); return null; } | |
| // Move to end (LRU) | |
| wdCache.delete(key); | |
| wdCache.set(key, entry); | |
| return entry.data; | |
| } | |
| function wdCacheSet(key, data) { | |
| if (wdCache.size >= WIKIDATA_CACHE_MAX) { | |
| const oldest = wdCache.keys().next().value; | |
| wdCache.delete(oldest); | |
| } | |
| wdCache.set(key, { data, ts: Date.now() }); | |
| } | |
| // ════════════════════════════════════════════════════ | |
| // §4 — WIKIDATA SPARQL (safe) | |
| // ════════════════════════════════════════════════════ | |
| function sanitizeId(id) { | |
| return id.replace(/[^a-zA-Z0-9 \-._]/g, '').trim(); | |
| } | |
| async function sparql(query) { | |
| const url = `${WIKIDATA_ENDPOINT}?query=${encodeURIComponent(query)}&format=json`; | |
| const parsed = new URL(url); | |
| if (!ALLOWED_DOMAINS.has(parsed.hostname)) { | |
| throw new Error(`Blocked domain: ${parsed.hostname}`); | |
| } | |
| const res = await fetch(url, { | |
| headers: { 'User-Agent': WIKIDATA_UA, Accept: 'application/sparql-results+json' }, | |
| signal: AbortSignal.timeout(10000), | |
| }); | |
| if (!res.ok) return []; | |
| const json = await res.json(); | |
| return json.results?.bindings || []; | |
| } | |
| // Search Wikidata for an entity by name, returns QID or null | |
| async function wdSearch(query, type = 'item') { | |
| const url = `https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${encodeURIComponent(query)}&language=en&limit=1&format=json`; | |
| const parsed = new URL(url); | |
| if (!ALLOWED_DOMAINS.has(parsed.hostname)) return null; | |
| try { | |
| const res = await fetch(url, { | |
| headers: { 'User-Agent': WIKIDATA_UA }, | |
| signal: AbortSignal.timeout(5000), | |
| }); | |
| if (!res.ok) return null; | |
| const json = await res.json(); | |
| return json.search?.[0]?.id || null; | |
| } catch { return null; } | |
| } | |
| // ════════════════════════════════════════════════════ | |
| // §5 — RESOLVERS (the intelligence) | |
| // ════════════════════════════════════════════════════ | |
| function addSanctionsToGraph(query, rootId, nodes, links) { | |
| const matches = sanctionsSearch(query); | |
| for (const m of matches) { | |
| const sid = `sanction:${m.id}`; | |
| nodes.push({ | |
| id: sid, label: `⚠ ${m.name}`, type: 'sanction', | |
| properties: { | |
| schema: m.schema, countries: m.countries.join(', '), | |
| programs: m.programs.join(', '), sanctions: m.sanctions, | |
| aliases: m.aliases.slice(0, 5).join('; '), | |
| first_seen: m.first_seen, sanctioned: true, | |
| }, | |
| }); | |
| links.push({ source: rootId, target: sid, label: 'SANCTIONS MATCH' }); | |
| } | |
| } | |
| function dedup(nodes, links) { | |
| const seen = new Set(); | |
| const uNodes = []; | |
| for (const n of nodes) { if (!seen.has(n.id)) { seen.add(n.id); uNodes.push(n); } } | |
| const lSeen = new Set(); | |
| const uLinks = []; | |
| for (const l of links) { | |
| const k = `${l.source}→${l.target}→${l.label}`; | |
| if (!lSeen.has(k)) { lSeen.add(k); uLinks.push(l); } | |
| } | |
| return { nodes: uNodes, links: uLinks }; | |
| } | |
| async function resolveAircraft(id, properties = {}) { | |
| const rootId = `aircraft:${id}`; | |
| const nodes = [], links = []; | |
| const cacheKey = `aircraft:${id}:${properties.registration || ''}`; | |
| const cached = wdCacheGet(cacheKey); | |
| if (cached) return { ...cached }; | |
| const callsign = id.toUpperCase().trim(); | |
| const registration = (properties.registration || '').toUpperCase().trim(); | |
| const model = properties.model || ''; | |
| // Step 1: Decode ICAO airline prefix from callsign (e.g. TRK → Turkish Airlines) | |
| // The prefix is the alphabetic portion of the callsign | |
| const airlinePrefix = callsign.replace(/[0-9]+$/, ''); | |
| let airlineName = null; | |
| if (airlinePrefix && airlinePrefix.length >= 2) { | |
| // Search Wikidata for the ICAO airline code | |
| try { | |
| const results = await sparql(` | |
| SELECT ?item ?itemLabel ?countryLabel ?ceoLabel ?parentLabel WHERE { | |
| ?item wdt:P230 "${airlinePrefix}" . | |
| OPTIONAL { ?item wdt:P17 ?country . } | |
| OPTIONAL { ?item wdt:P169 ?ceo . } | |
| OPTIONAL { ?item wdt:P749 ?parent . } | |
| SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . } | |
| } LIMIT 5`); | |
| for (const r of results) { | |
| if (r.itemLabel?.value) { | |
| airlineName = r.itemLabel.value; | |
| const airId = `company:${airlineName}`; | |
| nodes.push({ id: airId, label: airlineName, type: 'company', properties: { icao_code: airlinePrefix, source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: airId, label: 'OPERATED BY' }); | |
| if (r.countryLabel?.value) { | |
| const cid = `country:${r.countryLabel.value}`; | |
| nodes.push({ id: cid, label: r.countryLabel.value, type: 'country', properties: { source: 'Wikidata' } }); | |
| links.push({ source: airId, target: cid, label: 'HEADQUARTERED' }); | |
| } | |
| if (r.ceoLabel?.value) { | |
| const pid = `person:${r.ceoLabel.value}`; | |
| nodes.push({ id: pid, label: r.ceoLabel.value, type: 'person', properties: { role: 'CEO', source: 'Wikidata' } }); | |
| links.push({ source: airId, target: pid, label: 'CEO' }); | |
| } | |
| if (r.parentLabel?.value) { | |
| const pid = `company:${r.parentLabel.value}`; | |
| nodes.push({ id: pid, label: r.parentLabel.value, type: 'company', properties: { source: 'Wikidata' } }); | |
| links.push({ source: airId, target: pid, label: 'PARENT ORG' }); | |
| } | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] Airline ICAO lookup error:', e.message); } | |
| } | |
| // Step 2: Decode registration prefix → country (e.g. TC → Turkey, N → USA, G → UK) | |
| const REG_PREFIXES = { | |
| 'N':'United States','G':'United Kingdom','F':'France','D':'Germany','I':'Italy', | |
| 'JA':'Japan','HL':'South Korea','B':'China','VT':'India','TC':'Turkey', | |
| 'SU':'Russia','RA':'Russia','UR':'Ukraine','A6':'UAE','A7':'Qatar','9V':'Singapore', | |
| 'VH':'Australia','C':'Canada','PP':'Brazil','PR':'Brazil','PT':'Brazil', | |
| 'EC':'Spain','PH':'Philippines','HS':'Thailand','9M':'Malaysia','PK':'Pakistan', | |
| 'EP':'Iran','YI':'Iraq','HZ':'Saudi Arabia','4X':'Israel','SX':'Greece', | |
| 'OE':'Austria','HB':'Switzerland','SE':'Sweden','OH':'Finland','LN':'Norway', | |
| 'OY':'Denmark','PH':'Netherlands','OO':'Belgium','CS':'Portugal','SP':'Poland', | |
| 'OK':'Czech Republic','HA':'Hungary','YR':'Romania','LZ':'Bulgaria', | |
| 'EI':'Ireland','EW':'Belarus','ES':'Estonia','YL':'Latvia','LY':'Lithuania', | |
| }; | |
| if (registration) { | |
| let regCountry = null; | |
| // Try 2-char prefix first, then 1-char | |
| if (REG_PREFIXES[registration.substring(0, 2)]) regCountry = REG_PREFIXES[registration.substring(0, 2)]; | |
| else if (REG_PREFIXES[registration.substring(0, 1)]) regCountry = REG_PREFIXES[registration.substring(0, 1)]; | |
| if (regCountry) { | |
| const cid = `country:${regCountry}`; | |
| nodes.push({ id: cid, label: regCountry, type: 'country', properties: { source: 'Registration prefix' } }); | |
| links.push({ source: rootId, target: cid, label: 'REGISTERED IN' }); | |
| } | |
| } | |
| // Step 3: Add aircraft model info | |
| if (model) { | |
| const mid = `aircraft:model:${model}`; | |
| nodes.push({ id: mid, label: model, type: 'aircraft', properties: { type: 'model', source: 'ADS-B' } }); | |
| links.push({ source: rootId, target: mid, label: 'AIRCRAFT TYPE' }); | |
| } | |
| // Step 4: Cross-ref sanctions on airline name + callsign | |
| addSanctionsToGraph(callsign, rootId, nodes, links); | |
| if (airlineName) addSanctionsToGraph(airlineName, rootId, nodes, links); | |
| if (registration) addSanctionsToGraph(registration, rootId, nodes, links); | |
| const result = dedup(nodes, links); | |
| wdCacheSet(cacheKey, result); | |
| return result; | |
| } | |
| async function resolveVessel(id) { | |
| const rootId = `vessel:${id}`; | |
| const nodes = [], links = []; | |
| const cached = wdCacheGet(`vessel:${id}`); | |
| if (cached) return { ...cached }; | |
| try { | |
| const results = await sparql(` | |
| SELECT ?item ?itemLabel ?ownerLabel ?countryLabel ?operatorLabel ?flagLabel WHERE { | |
| { ?item wdt:P458 "${id}" . } | |
| UNION { ?item rdfs:label "${id}"@en . ?item wdt:P31/wdt:P279* wd:Q11446 . } | |
| OPTIONAL { ?item wdt:P127 ?owner . } | |
| OPTIONAL { ?item wdt:P17 ?country . } | |
| OPTIONAL { ?item wdt:P137 ?operator . } | |
| OPTIONAL { ?item wdt:P8047 ?flag . } | |
| SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . } | |
| } LIMIT 10`); | |
| for (const r of results) { | |
| if (r.ownerLabel?.value) { | |
| const oid = `company:${r.ownerLabel.value}`; | |
| nodes.push({ id: oid, label: r.ownerLabel.value, type: 'company', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: oid, label: 'OWNED BY' }); | |
| } | |
| const flag = r.flagLabel?.value || r.countryLabel?.value; | |
| if (flag) { | |
| const cid = `country:${flag}`; | |
| nodes.push({ id: cid, label: flag, type: 'country', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: cid, label: 'FLAG STATE' }); | |
| } | |
| if (r.operatorLabel?.value) { | |
| const oid = `company:${r.operatorLabel.value}`; | |
| nodes.push({ id: oid, label: r.operatorLabel.value, type: 'company', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: oid, label: 'OPERATED BY' }); | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] Wikidata vessel error:', e.message); } | |
| addSanctionsToGraph(id, rootId, nodes, links); | |
| const result = dedup(nodes, links); | |
| wdCacheSet(`vessel:${id}`, result); | |
| return result; | |
| } | |
| async function resolveCompany(id) { | |
| const rootId = `company:${id}`; | |
| const nodes = [], links = []; | |
| const cached = wdCacheGet(`company:${id}`); | |
| if (cached) return { ...cached }; | |
| try { | |
| // Use Wikidata search to find the QID first, then resolve by QID | |
| const qid = await wdSearch(id); | |
| const filter = qid | |
| ? `VALUES ?item { wd:${qid} }` | |
| : `?item rdfs:label "${id}"@en . { ?item wdt:P31/wdt:P279* wd:Q4830453 . } UNION { ?item wdt:P31/wdt:P279* wd:Q43229 . }`; | |
| const results = await sparql(` | |
| SELECT ?item ?itemLabel ?countryLabel ?parentLabel ?ceoLabel ?industryLabel WHERE { | |
| ${filter} | |
| OPTIONAL { ?item wdt:P17 ?country . } | |
| OPTIONAL { ?item wdt:P749 ?parent . } | |
| OPTIONAL { ?item wdt:P169 ?ceo . } | |
| OPTIONAL { ?item wdt:P452 ?industry . } | |
| SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . } | |
| } LIMIT 10`); | |
| for (const r of results) { | |
| if (r.countryLabel?.value) { | |
| const cid = `country:${r.countryLabel.value}`; | |
| nodes.push({ id: cid, label: r.countryLabel.value, type: 'country', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: cid, label: 'HEADQUARTERED' }); | |
| } | |
| if (r.parentLabel?.value) { | |
| const pid = `company:${r.parentLabel.value}`; | |
| nodes.push({ id: pid, label: r.parentLabel.value, type: 'company', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: pid, label: 'PARENT ORG' }); | |
| } | |
| if (r.ceoLabel?.value) { | |
| const pid = `person:${r.ceoLabel.value}`; | |
| nodes.push({ id: pid, label: r.ceoLabel.value, type: 'person', properties: { role: 'CEO', source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: pid, label: 'CEO' }); | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] Wikidata company error:', e.message); } | |
| addSanctionsToGraph(id, rootId, nodes, links); | |
| const result = dedup(nodes, links); | |
| wdCacheSet(`company:${id}`, result); | |
| return result; | |
| } | |
| async function resolvePerson(id) { | |
| const rootId = `person:${id}`; | |
| const nodes = [], links = []; | |
| const cached = wdCacheGet(`person:${id}`); | |
| if (cached) return { ...cached }; | |
| try { | |
| const qid = await wdSearch(id); | |
| const filter = qid | |
| ? `VALUES ?item { wd:${qid} }` | |
| : `?item rdfs:label "${id}"@en . ?item wdt:P31 wd:Q5 .`; | |
| const results = await sparql(` | |
| SELECT ?item ?itemLabel ?nationalityLabel ?employerLabel ?positionLabel WHERE { | |
| ${filter} | |
| OPTIONAL { ?item wdt:P27 ?nationality . } | |
| OPTIONAL { ?item wdt:P108 ?employer . } | |
| OPTIONAL { ?item wdt:P39 ?position . } | |
| SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . } | |
| } LIMIT 10`); | |
| for (const r of results) { | |
| if (r.nationalityLabel?.value) { | |
| const cid = `country:${r.nationalityLabel.value}`; | |
| nodes.push({ id: cid, label: r.nationalityLabel.value, type: 'country', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: cid, label: 'NATIONALITY' }); | |
| } | |
| if (r.employerLabel?.value) { | |
| const eid = `company:${r.employerLabel.value}`; | |
| nodes.push({ id: eid, label: r.employerLabel.value, type: 'company', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: eid, label: 'EMPLOYER' }); | |
| } | |
| if (r.positionLabel?.value) { | |
| const pid = `event:${r.positionLabel.value}`; | |
| nodes.push({ id: pid, label: r.positionLabel.value, type: 'event', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: pid, label: 'POSITION HELD' }); | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] Wikidata person error:', e.message); } | |
| addSanctionsToGraph(id, rootId, nodes, links); | |
| const result = dedup(nodes, links); | |
| wdCacheSet(`person:${id}`, result); | |
| return result; | |
| } | |
| async function resolveIP(id) { | |
| const rootId = `ip:${id}`; | |
| const nodes = [], links = []; | |
| const cached = wdCacheGet(`ip:${id}`); | |
| if (cached) return { ...cached }; | |
| // Step 1: ip-api.com — geolocation, ISP, ASN, proxy/hosting detection | |
| try { | |
| const ipApiUrl = `http://ip-api.com/json/${encodeURIComponent(id)}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,org,as,asname,mobile,proxy,hosting`; | |
| const parsed = new URL(ipApiUrl); | |
| if (!ALLOWED_DOMAINS.has(parsed.hostname)) throw new Error(`Blocked domain: ${parsed.hostname}`); | |
| const res = await fetch(ipApiUrl, { signal: AbortSignal.timeout(8000) }); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| // ISP node | |
| if (data.isp) { | |
| const ispId = `company:${data.isp}`; | |
| nodes.push({ id: ispId, label: data.isp, type: 'company', properties: { role: 'ISP', org: data.org || '', source: 'ip-api.com' } }); | |
| links.push({ source: rootId, target: ispId, label: 'HOSTED_BY' }); | |
| addSanctionsToGraph(data.isp, rootId, nodes, links); | |
| } | |
| // ASN node | |
| if (data.as) { | |
| const asLabel = data.asname || data.as; | |
| const asId = `company:${data.as}`; | |
| nodes.push({ id: asId, label: asLabel, type: 'company', properties: { as_number: data.as, source: 'ip-api.com' } }); | |
| links.push({ source: rootId, target: asId, label: 'ASN' }); | |
| } | |
| // Country node | |
| if (data.country) { | |
| const cid = `country:${data.country}`; | |
| nodes.push({ id: cid, label: data.country, type: 'country', properties: { code: data.countryCode || '', source: 'ip-api.com' } }); | |
| links.push({ source: rootId, target: cid, label: 'LOCATED_IN' }); | |
| addSanctionsToGraph(data.country, rootId, nodes, links); | |
| } | |
| // City node (as event type with lat/lng) | |
| if (data.city) { | |
| const cityId = `event:${data.city}`; | |
| nodes.push({ | |
| id: cityId, label: data.city, type: 'event', | |
| properties: { | |
| lat: data.lat, lon: data.lon, region: data.regionName || '', | |
| zip: data.zip || '', timezone: data.timezone || '', source: 'ip-api.com', | |
| }, | |
| }); | |
| links.push({ source: rootId, target: cityId, label: 'GEOLOCATED' }); | |
| } | |
| // Tag proxy/hosting/mobile flags on the root IP node | |
| nodes.push({ | |
| id: rootId, label: id, type: 'ip', | |
| properties: { | |
| proxy: !!data.proxy, hosting: !!data.hosting, mobile: !!data.mobile, | |
| source: 'ip-api.com', | |
| }, | |
| }); | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] ip-api.com error:', e.message); } | |
| // Step 2: RIPEstat WHOIS | |
| try { | |
| const whoisUrl = `https://stat.ripe.net/data/whois/data.json?resource=${encodeURIComponent(id)}`; | |
| const parsed = new URL(whoisUrl); | |
| if (!ALLOWED_DOMAINS.has(parsed.hostname)) throw new Error(`Blocked domain: ${parsed.hostname}`); | |
| const res = await fetch(whoisUrl, { signal: AbortSignal.timeout(8000) }); | |
| if (res.ok) { | |
| const json = await res.json(); | |
| const records = json.data?.records || []; | |
| for (const record of records) { | |
| for (const field of record) { | |
| if (field.key === 'netname' || field.key === 'NetName') { | |
| const netId = `company:${field.value}`; | |
| nodes.push({ id: netId, label: field.value, type: 'company', properties: { role: 'Network', source: 'RIPEstat WHOIS' } }); | |
| links.push({ source: rootId, target: netId, label: 'HOSTED_BY' }); | |
| } | |
| } | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] RIPEstat WHOIS error:', e.message); } | |
| // Step 3: RIPEstat Abuse Contact | |
| try { | |
| const abuseUrl = `https://stat.ripe.net/data/abuse-contact-finder/data.json?resource=${encodeURIComponent(id)}`; | |
| const parsed = new URL(abuseUrl); | |
| if (!ALLOWED_DOMAINS.has(parsed.hostname)) throw new Error(`Blocked domain: ${parsed.hostname}`); | |
| const res = await fetch(abuseUrl, { signal: AbortSignal.timeout(8000) }); | |
| if (res.ok) { | |
| const json = await res.json(); | |
| const contacts = json.data?.abuse_contacts || []; | |
| for (const email of contacts) { | |
| if (email) { | |
| const eid = `person:${email}`; | |
| nodes.push({ id: eid, label: email, type: 'person', properties: { role: 'Abuse Contact', source: 'RIPEstat' } }); | |
| links.push({ source: rootId, target: eid, label: 'ABUSE CONTACT' }); | |
| } | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] RIPEstat abuse-contact error:', e.message); } | |
| // Step 4: RIPEstat Network Info | |
| try { | |
| const netUrl = `https://stat.ripe.net/data/network-info/data.json?resource=${encodeURIComponent(id)}`; | |
| const parsed = new URL(netUrl); | |
| if (!ALLOWED_DOMAINS.has(parsed.hostname)) throw new Error(`Blocked domain: ${parsed.hostname}`); | |
| const res = await fetch(netUrl, { signal: AbortSignal.timeout(8000) }); | |
| if (res.ok) { | |
| const json = await res.json(); | |
| const prefix = json.data?.prefix; | |
| const asns = json.data?.asns || []; | |
| if (prefix) { | |
| const prefId = `ip:${prefix}`; | |
| nodes.push({ id: prefId, label: prefix, type: 'ip', properties: { role: 'Prefix', source: 'RIPEstat' } }); | |
| links.push({ source: rootId, target: prefId, label: 'PREFIX' }); | |
| } | |
| for (const asn of asns) { | |
| const asnId = `company:AS${asn}`; | |
| nodes.push({ id: asnId, label: `AS${asn}`, type: 'company', properties: { as_number: `AS${asn}`, source: 'RIPEstat' } }); | |
| links.push({ source: rootId, target: asnId, label: 'ASN' }); | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] RIPEstat network-info error:', e.message); } | |
| const result = dedup(nodes, links); | |
| wdCacheSet(`ip:${id}`, result); | |
| return result; | |
| } | |
| async function resolveCountry(id) { | |
| const rootId = `country:${id}`; | |
| const nodes = [], links = []; | |
| const cached = wdCacheGet(`country:${id}`); | |
| if (cached) return { ...cached }; | |
| try { | |
| const qid = await wdSearch(id); | |
| const filter = qid | |
| ? `VALUES ?item { wd:${qid} }` | |
| : `?item rdfs:label "${id}"@en . ?item wdt:P31 wd:Q6256 .`; | |
| const results = await sparql(` | |
| SELECT ?item ?itemLabel ?headLabel ?capitalLabel ?population ?gdp | |
| ?tld ?callingCode ?memberOfLabel ?neighborLabel WHERE { | |
| ${filter} | |
| OPTIONAL { ?item wdt:P35 ?head . } | |
| OPTIONAL { ?item wdt:P36 ?capital . } | |
| OPTIONAL { ?item wdt:P1082 ?population . } | |
| OPTIONAL { ?item wdt:P2131 ?gdp . } | |
| OPTIONAL { ?item wdt:P78 ?tld . } | |
| OPTIONAL { ?item wdt:P474 ?callingCode . } | |
| OPTIONAL { ?item wdt:P463 ?memberOf . } | |
| OPTIONAL { ?item wdt:P47 ?neighbor . } | |
| SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . } | |
| } LIMIT 50`); | |
| const seenHeads = new Set(); | |
| const seenMembers = new Set(); | |
| const seenNeighbors = new Set(); | |
| let propsSet = false; | |
| for (const r of results) { | |
| // Head of state/government | |
| if (r.headLabel?.value && !seenHeads.has(r.headLabel.value)) { | |
| seenHeads.add(r.headLabel.value); | |
| const hid = `person:${r.headLabel.value}`; | |
| nodes.push({ id: hid, label: r.headLabel.value, type: 'person', properties: { role: 'Head of State', source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: hid, label: 'HEAD OF STATE' }); | |
| } | |
| // Capital city | |
| if (r.capitalLabel?.value && !propsSet) { | |
| const capId = `event:${r.capitalLabel.value}`; | |
| nodes.push({ id: capId, label: r.capitalLabel.value, type: 'event', properties: { role: 'Capital', source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: capId, label: 'CAPITAL' }); | |
| } | |
| // Country properties (population, GDP, TLD, calling code) | |
| if (!propsSet) { | |
| const props = { source: 'Wikidata' }; | |
| if (r.population?.value) props.population = r.population.value; | |
| if (r.gdp?.value) props.gdp = r.gdp.value; | |
| if (r.tld?.value) props.tld = r.tld.value; | |
| if (r.callingCode?.value) props.calling_code = r.callingCode.value; | |
| nodes.push({ id: rootId, label: id, type: 'country', properties: props }); | |
| propsSet = true; | |
| } | |
| // Member of (UN, NATO, EU, etc.) | |
| if (r.memberOfLabel?.value && !seenMembers.has(r.memberOfLabel.value)) { | |
| seenMembers.add(r.memberOfLabel.value); | |
| const mid = `company:${r.memberOfLabel.value}`; | |
| nodes.push({ id: mid, label: r.memberOfLabel.value, type: 'company', properties: { role: 'Organization', source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: mid, label: 'MEMBER OF' }); | |
| } | |
| // Neighboring countries | |
| if (r.neighborLabel?.value && !seenNeighbors.has(r.neighborLabel.value)) { | |
| seenNeighbors.add(r.neighborLabel.value); | |
| const nid = `country:${r.neighborLabel.value}`; | |
| nodes.push({ id: nid, label: r.neighborLabel.value, type: 'country', properties: { source: 'Wikidata' } }); | |
| links.push({ source: rootId, target: nid, label: 'NEIGHBOR' }); | |
| } | |
| } | |
| } catch (e) { console.warn('[INTEL] Wikidata country error:', e.message); } | |
| addSanctionsToGraph(id, rootId, nodes, links); | |
| const result = dedup(nodes, links); | |
| wdCacheSet(`country:${id}`, result); | |
| return result; | |
| } | |
| const RESOLVERS = { aircraft: resolveAircraft, vessel: resolveVessel, company: resolveCompany, person: resolvePerson, ip: resolveIP, country: resolveCountry }; | |
| const ALLOWED_TYPES = new Set(Object.keys(RESOLVERS)); | |
| // ════════════════════════════════════════════════════ | |
| // §6 — RATE LIMITER | |
| // ════════════════════════════════════════════════════ | |
| const rateMap = new Map(); | |
| function isRateLimited(ip, limit = 30, windowMs = 60000) { | |
| const now = Date.now(); | |
| for (const [k, v] of rateMap) { if (now > v.resetAt) rateMap.delete(k); } | |
| const entry = rateMap.get(ip); | |
| if (!entry || now > entry.resetAt) { rateMap.set(ip, { count: 1, resetAt: now + windowMs }); return false; } | |
| entry.count++; | |
| return entry.count > limit; | |
| } | |
| // ════════════════════════════════════════════════════ | |
| // §7 — EXPRESS ROUTES | |
| // ════════════════════════════════════════════════════ | |
| app.get('/health', (_req, res) => { | |
| res.json({ | |
| status: 'ok', | |
| sanctions_entries: sanctionsIndex.entries.length, | |
| sanctions_loaded_at: sanctionsIndex.fetchedAt ? new Date(sanctionsIndex.fetchedAt).toISOString() : null, | |
| wikidata_cache_size: wdCache.size, | |
| uptime_seconds: Math.floor(process.uptime()), | |
| }); | |
| }); | |
| app.get('/resolve', async (req, res) => { | |
| const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip || 'unknown'; | |
| if (isRateLimited(ip)) return res.status(429).json({ error: 'Rate limit exceeded' }); | |
| const type = (req.query.type || '').toLowerCase().trim(); | |
| const rawId = (req.query.id || '').trim(); | |
| if (!type || !ALLOWED_TYPES.has(type)) { | |
| return res.status(400).json({ error: `Invalid type. Allowed: ${[...ALLOWED_TYPES].join(', ')}` }); | |
| } | |
| if (!rawId || rawId.length < 2 || rawId.length > 200) { | |
| return res.status(400).json({ error: 'Invalid id (2-200 chars)' }); | |
| } | |
| const id = sanitizeId(rawId); | |
| if (id.length < 2) return res.status(400).json({ error: 'ID contains too many invalid characters' }); | |
| try { | |
| const resolver = RESOLVERS[type]; | |
| // Pass extra properties for aircraft resolution (registration, model, etc.) | |
| const props = {}; | |
| if (req.query.registration) props.registration = sanitizeId(req.query.registration); | |
| if (req.query.model) props.model = sanitizeId(req.query.model); | |
| if (req.query.icao24) props.icao24 = sanitizeId(req.query.icao24); | |
| const result = await resolver(id, props); | |
| res.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=7200'); | |
| res.json({ | |
| nodes: result.nodes, | |
| links: result.links, | |
| entity: { type, id }, | |
| source: 'OSIRIS Intelligence Layer', | |
| sanctions_index_size: sanctionsIndex.entries.length, | |
| wikidata_cache_hits: wdCache.size, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| } catch (e) { | |
| console.error('[INTEL] Resolve error:', e); | |
| res.status(500).json({ error: 'Resolution failed', nodes: [], links: [] }); | |
| } | |
| }); | |
| // ════════════════════════════════════════════════════ | |
| // §8 — STARTUP | |
| // ════════════════════════════════════════════════════ | |
| async function boot() { | |
| console.log('[INTEL] OSIRIS Intelligence Layer starting...'); | |
| await loadSanctions(); | |
| // Refresh sanctions every 24h | |
| setInterval(() => loadSanctions(), SDN_REFRESH_MS); | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(`[INTEL] Intelligence Layer ready on port ${PORT}`); | |
| console.log(`[INTEL] Sanctions: ${sanctionsIndex.entries.length} entities indexed`); | |
| console.log(`[INTEL] Resolve endpoint: GET /resolve?type=<type>&id=<id>`); | |
| }); | |
| } | |
| boot().catch(e => { console.error('[INTEL] Fatal:', e); process.exit(1); }); | |