- 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)
274 lines
8.0 KiB
JavaScript
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,
|
|
};
|