PeerCortex/magatama-s2ten-bgp-enrichment.js
Rene Fichtmueller 5554c1a53e feat: BGP Hijack Alerting + Webhooks (Feature 1)
- Deterministic Classification: MOAS/HIJACK/LEAK type detection
- Severity scoring: CRITICAL/HIGH/MEDIUM/LOW based on prefix length
- Optional Ollama enrichment (qwen2.5:3b) for CRITICAL only (5s timeout)
- PostgreSQL backend: hijack_events, webhook_subscriptions, webhook_deliveries
- HMAC-SHA256 webhook signing with exponential backoff retry
- Retry scheduler: node-cron job every 5 minutes
- 6 API endpoints: POST/GET/DELETE webhooks, test delivery, list/resolve hijacks
- 22 comprehensive tests (80%+ coverage)
- Zero external API costs (deterministic + local Ollama only)
2026-04-29 07:45:15 +02:00

274 lines
8.0 KiB
JavaScript

/**
* MAGATAMA S2 TEN BGP Enrichment Task
*
* Enriches MAGATAMA S2 TEN (Cloud) findings with:
* 1. BGP status (is the IP/prefix announced?)
* 2. RPKI validity (is the announcing ASN authorized?)
* 3. Threat intelligence (known malicious IP or ASN?)
*
* This bridges PeerCortex local intelligence to MAGATAMA security findings.
* Called when S2 TEN detects anomalous external traffic.
*/
const pg = require('pg');
// Initialize PostgreSQL connection pool
const pool = new pg.Pool({
host: process.env.DB_HOST || '192.168.178.82',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'llm_gateway',
user: process.env.DB_USER || 'llm',
password: process.env.DB_PASSWORD || 'llm_secure_2026',
max: 10,
idleTimeoutMillis: 10000,
connectionTimeoutMillis: 5000,
});
/**
* Get BGP status for an IP address
*/
async function getBgpStatus(ipAddress) {
const client = await pool.connect();
try {
const query = `
SELECT
prefix,
origin_asn,
ARRAY_AGG(DISTINCT origin_asn) as origin_asns,
visibility_percent,
last_seen
FROM bgp_routes
WHERE prefix >> $1::inet
ORDER BY prefix DESC
LIMIT 1
`;
const result = await client.query(query, [ipAddress]);
return result.rows.length > 0 ? result.rows[0] : null;
} finally {
client.release();
}
}
/**
* Validate RPKI for a prefix + origin ASN pair
*/
async function validateRpki(prefix, originAsn) {
const client = await pool.connect();
try {
const query = `
SELECT
status,
details
FROM rpki_roas
WHERE prefix >> $1::inet
AND origin_asn = $2
LIMIT 1
`;
const result = await client.query(query, [prefix, originAsn]);
if (result.rows.length > 0) {
return {
status: result.rows[0].status || 'valid',
details: result.rows[0].details,
};
}
// No ROA found = not-found
return { status: 'not-found' };
} finally {
client.release();
}
}
/**
* Get threat intelligence for an IP address
*/
async function getThreatIntel(ipAddress) {
const client = await pool.connect();
try {
const query = `
SELECT
ip_address,
threat_level,
confidence_score,
source,
cached_at
FROM threat_intel
WHERE ip_address = $1::inet
LIMIT 1
`;
const result = await client.query(query, [ipAddress]);
return result.rows.length > 0 ? result.rows[0] : null;
} finally {
client.release();
}
}
/**
* Determine kill-chain phase based on BGP + RPKI + Threat signals
*/
function determineKillChainPhase(enriched) {
const flags = enriched.security_flags || [];
if (flags.some(f => f.flag === 'RPKI_INVALID')) {
return 'WEAPONIZATION'; // BGP hijack = active attack infrastructure setup
}
if (flags.some(f => f.flag === 'MALICIOUS_IP')) {
return 'DELIVERY'; // Known malicious IP attempting connection
}
if (enriched.bgp_intel.announced === false) {
return 'EXPLOITATION'; // Unknown IP not in routing table = potentially spoofed
}
return 'RECONNAISSANCE'; // Announced legitimate IP but with anomalous traffic pattern
}
/**
* Map to MITRE ATT&CK framework
*/
function mapToMitreAttack(enriched) {
const mapping = [];
if (enriched.bgp_intel.rpki_valid === false) {
mapping.push({
technique: 'T1040', // Network Sniffing (implicit in BGP hijack)
tactic: 'Credential Access / Collection',
description: 'BGP RPKI validation failed - possible route hijack',
});
}
if (enriched.threat_intel?.threat_level === 'CRITICAL') {
mapping.push({
technique: 'T1566', // Phishing (network variant)
tactic: 'Initial Access',
description: 'Connection from known malicious IP',
});
}
return mapping;
}
/**
* Enrich a finding with BGP + RPKI + Threat data
* @param {Object} finding - MAGATAMA finding { ip_address, port, protocol, ... }
* @returns {Object} Enriched finding with bgp_status, rpki_valid, threat_level
*/
async function enrichFindingWithBGPIntel(finding) {
const enriched = { ...finding, bgp_intel: {} };
const ipAddress = finding.ip_address;
if (!ipAddress) {
console.log('[S2TEN Enrichment] No IP address in finding, skipping enrichment');
return enriched;
}
try {
// ---- Step 1: BGP Status Lookup ----
// Check if this IP is part of an announced prefix in our local BGP routes
const bgpStatus = await getBgpStatus(ipAddress);
if (bgpStatus) {
enriched.bgp_intel.announced = true;
enriched.bgp_intel.prefix = bgpStatus.prefix;
enriched.bgp_intel.origin_asn = bgpStatus.origin_asns ? bgpStatus.origin_asns[0] : null;
enriched.bgp_intel.origin_asns = bgpStatus.origin_asns || [];
enriched.bgp_intel.visibility_percent = bgpStatus.visibility_percent;
enriched.bgp_intel.last_seen = bgpStatus.last_seen;
// ---- Step 2: RPKI Validity Check ----
// If we know the origin ASN, validate RPKI
if (enriched.bgp_intel.origin_asn) {
try {
const rpkiResult = await validateRpki(
bgpStatus.prefix || ipAddress,
enriched.bgp_intel.origin_asn
);
enriched.bgp_intel.rpki_status = rpkiResult.status;
enriched.bgp_intel.rpki_valid = rpkiResult.status === 'valid';
// Alert if RPKI is INVALID (potential hijack!)
if (rpkiResult.status === 'invalid') {
enriched.security_flags = enriched.security_flags || [];
enriched.security_flags.push({
flag: 'RPKI_INVALID',
severity: 'CRITICAL',
message: `RPKI validation failed: AS${enriched.bgp_intel.origin_asn} is not authorized to announce this prefix`,
});
}
} catch (e) {
console.error(`[S2TEN] RPKI check failed for ${ipAddress}:`, e.message);
enriched.bgp_intel.rpki_status = 'unknown';
}
}
} else {
enriched.bgp_intel.announced = false;
enriched.bgp_intel.message = 'IP not found in BGP routing table';
}
// ---- Step 3: Threat Intelligence Lookup ----
// Check if this IP or its origin ASN is in threat_intel table
const threatIntel = await getThreatIntel(ipAddress);
if (threatIntel) {
enriched.threat_intel = {
ip_address: threatIntel.ip_address,
threat_level: threatIntel.threat_level,
confidence_score: threatIntel.confidence_score,
source: threatIntel.source,
cached_at: threatIntel.cached_at,
};
// Alert if threat level is HIGH or CRITICAL
if (['HIGH', 'CRITICAL'].includes(threatIntel.threat_level)) {
enriched.security_flags = enriched.security_flags || [];
enriched.security_flags.push({
flag: 'MALICIOUS_IP',
severity: threatIntel.threat_level,
message: `Known malicious IP: threat level ${threatIntel.threat_level} (confidence: ${threatIntel.confidence_score}%)`,
source: threatIntel.source,
});
}
}
// ---- Step 4: MAGATAMA Kill-Chain Correlation ----
// Map BGP + RPKI + Threat findings to MITRE ATT&CK kill chain
enriched.kill_chain_phase = determineKillChainPhase(enriched);
enriched.mitre_attack_mapping = mapToMitreAttack(enriched);
return enriched;
} catch (e) {
console.error(`[S2TEN Enrichment] Fatal error enriching ${ipAddress}:`, e.message);
enriched.enrichment_error = e.message;
return enriched;
}
}
/**
* Batch enrich multiple findings
*/
async function enrichFindings(findings) {
console.log(`[S2TEN Enrichment] Enriching ${findings.length} findings...`);
const enriched = [];
for (const finding of findings) {
try {
const result = await enrichFindingWithBGPIntel(finding);
enriched.push(result);
} catch (e) {
console.error(`[S2TEN Enrichment] Error enriching finding:`, e.message);
enriched.push({ ...finding, enrichment_error: e.message });
}
}
return enriched;
}
module.exports = {
enrichFindingWithBGPIntel,
enrichFindings,
determineKillChainPhase,
mapToMitreAttack,
pool,
getBgpStatus,
validateRpki,
getThreatIntel,
};