/** * Local Database Client for PeerCortex * Replaces external API calls with local PostgreSQL queries * BGP + RPKI + Threat Intel + RDAP caching */ const { Pool } = require('pg'); const pool = new Pool({ user: process.env.DB_USER || 'llm', password: process.env.DB_PASSWORD || 'llm_secure_2026', host: process.env.DB_HOST || '192.168.178.82', port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME || 'llm_gateway', }); // RDAP Cache (in-memory for this session) const rdapCache = new Map(); const RDAP_CACHE_TTL = 3600000; // 1 hour // ══════════════════════════════════════════════════════════════ // BGP FUNCTIONS // ══════════════════════════════════════════════════════════════ async function getBgpStatus(prefix) { try { const result = await pool.query( `SELECT DISTINCT origin_asn, MAX(visibility_percent) as visibility_percent, MAX(last_seen) as last_seen FROM bgp_routes WHERE prefix = $1::cidr GROUP BY origin_asn`, [prefix] ); if (result.rows.length === 0) { return { announced: false, origin_asns: [], visibility_percent: 0, last_seen: new Date().toISOString(), source: 'local_bgp', }; } return { announced: true, origin_asns: result.rows.map(r => r.origin_asn), visibility_percent: Math.max(...result.rows.map(r => parseFloat(r.visibility_percent) || 0)), last_seen: result.rows[0].last_seen || new Date().toISOString(), source: 'local_bgp', }; } catch (error) { console.error('[Local DB] BGP Status Error:', error.message); return null; } } async function getAnnouncedPrefixes(asn) { try { const result = await pool.query( `SELECT prefix, origin_asn, visibility_percent, last_seen FROM bgp_routes WHERE origin_asn = $1 ORDER BY visibility_percent DESC LIMIT 100`, [asn] ); return result.rows; } catch (error) { console.error('[Local DB] Announced Prefixes Error:', error.message); return []; } } async function checkBgpHijack(prefix) { try { const result = await pool.query( `SELECT DISTINCT origin_asn FROM bgp_routes WHERE prefix = $1::cidr`, [prefix] ); return result.rows.length > 1 ? result.rows.map(r => r.origin_asn) : []; } catch (error) { console.error('[Local DB] Hijack Check Error:', error.message); return []; } } // ══════════════════════════════════════════════════════════════ // RPKI FUNCTIONS // ══════════════════════════════════════════════════════════════ async function validateRpki(prefix, originAsn) { try { const prefixParts = prefix.split('/'); if (prefixParts.length !== 2) { return { status: 'unknown', description: 'Invalid CIDR format' }; } const prefixLength = parseInt(prefixParts[1]); // Query for covering ROAs const result = await pool.query( `SELECT * FROM rpki_roas WHERE $1::cidr << (prefix || '/' || max_length)::cidr AND origin_asn = $2 AND expires > NOW() LIMIT 10`, [prefix, originAsn] ); if (result.rows.length === 0) { const anyRoa = await pool.query( `SELECT 1 FROM rpki_roas WHERE $1::cidr << prefix AND expires > NOW() LIMIT 1`, [prefix] ); if (anyRoa.rows.length > 0) { return { status: 'invalid', prefix, asn: originAsn, description: `RPKI INVALID: ROAs exist but origin ASN ${originAsn} not authorized`, }; } return { status: 'not-found', prefix, asn: originAsn, description: 'No matching ROA found (unprotected)', }; } const roa = result.rows[0]; if (prefixLength > roa.max_length) { return { status: 'invalid', prefix, asn: originAsn, max_length: roa.max_length, description: `RPKI INVALID: Prefix length ${prefixLength} > max_length ${roa.max_length}`, }; } return { status: 'valid', prefix, asn: originAsn, max_length: roa.max_length, expires: roa.expires, description: `RPKI VALID: Origin ASN ${originAsn} authorized`, }; } catch (error) { console.error('[Local DB] RPKI Validation Error:', error.message); return { status: 'unknown', description: 'RPKI validation error' }; } } async function getRoasForAsn(asn) { try { const result = await pool.query( `SELECT prefix, max_length, expires FROM rpki_roas WHERE origin_asn = $1 AND expires > NOW() ORDER BY prefix`, [asn] ); return result.rows; } catch (error) { console.error('[Local DB] ROAs for ASN Error:', error.message); return []; } } // ══════════════════════════════════════════════════════════════ // THREAT INTEL FUNCTIONS // ══════════════════════════════════════════════════════════════ async function getThreatIntel(ip) { try { const result = await pool.query( `SELECT ip_address, threat_level, confidence_score, source, details, cached_at FROM threat_intel WHERE ip_address = $1::inet AND expires_at > NOW() LIMIT 1`, [ip] ); return result.rows.length > 0 ? result.rows[0] : null; } catch (error) { console.error('[Local DB] Threat Intel Error:', error.message); return null; } } async function isMaliciousIp(ip) { try { const result = await pool.query( `SELECT 1 FROM threat_intel WHERE ip_address = $1::inet AND threat_level IN ('CRITICAL', 'HIGH') AND expires_at > NOW() LIMIT 1`, [ip] ); return result.rows.length > 0; } catch (error) { console.error('[Local DB] Malicious IP Check Error:', error.message); return false; } } // ══════════════════════════════════════════════════════════════ // RDAP CACHING (in-memory) // ══════════════════════════════════════════════════════════════ function getRdapCached(resource) { const cached = rdapCache.get(resource); if (cached && Date.now() - cached.timestamp < RDAP_CACHE_TTL) { console.log(`[RDAP Cache] HIT: ${resource}`); return cached.data; } if (cached) rdapCache.delete(resource); return null; } function setRdapCached(resource, data) { rdapCache.set(resource, { data, timestamp: Date.now() }); console.log(`[RDAP Cache] SET: ${resource} (TTL: 1h)`); } // ══════════════════════════════════════════════════════════════ // STATS & HEALTH CHECK // ══════════════════════════════════════════════════════════════ async function getLocalDbStats() { try { const bgp = await pool.query(`SELECT COUNT(*) as count FROM bgp_routes`); const rpki = await pool.query(`SELECT COUNT(*) as count FROM rpki_roas WHERE expires > NOW()`); const threat = await pool.query(`SELECT COUNT(*) as count FROM threat_intel WHERE expires_at > NOW()`); return { bgp_routes: parseInt(bgp.rows[0].count), rpki_roas: parseInt(rpki.rows[0].count), threat_intel: parseInt(threat.rows[0].count), rdap_cache_entries: rdapCache.size, }; } catch (error) { console.error('[Local DB] Stats Error:', error.message); return null; } } async function cleanup() { await pool.end(); } // ══════════════════════════════════════════════════════════════ // EXPORTS // ══════════════════════════════════════════════════════════════ module.exports = { // BGP getBgpStatus, getAnnouncedPrefixes, checkBgpHijack, // RPKI validateRpki, getRoasForAsn, // Threat Intel getThreatIntel, isMaliciousIp, // RDAP Cache getRdapCached, setRdapCached, // Health getLocalDbStats, cleanup, };