diff --git a/.env.example b/.env.example index 688e698..627883a 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,8 @@ AISSTREAM_API_KEY= ACLED_EMAIL= # OAuth2 password grant (API keys deprecated Sept 2025) ACLED_PASSWORD= +# Cloudflare Radar internet outages & traffic anomalies (free: dash.cloudflare.com/profile/api-tokens, Account Analytics Read) +CLOUDFLARE_API_TOKEN= # === Server Configuration === diff --git a/apis/briefing.mjs b/apis/briefing.mjs index 94e4173..1ae2cff 100644 --- a/apis/briefing.mjs +++ b/apis/briefing.mjs @@ -43,6 +43,10 @@ import { briefing as space } from './sources/space.mjs'; // === Tier 5: Live Market Data === import { briefing as yfinance } from './sources/yfinance.mjs'; +// === Tier 6: Cyber & Infrastructure === +import { briefing as cisaKev } from './sources/cisa-kev.mjs'; +import { briefing as cloudflareRadar } from './sources/cloudflare-radar.mjs'; + const SOURCE_TIMEOUT_MS = 30_000; // 30s max per individual source export async function runSource(name, fn, ...args) { @@ -63,7 +67,7 @@ export async function runSource(name, fn, ...args) { } export async function fullBriefing() { - console.error('[Crucix] Starting intelligence sweep — 27 sources...'); + console.error('[Crucix] Starting intelligence sweep — 29 sources...'); const start = Date.now(); const allPromises = [ @@ -103,6 +107,10 @@ export async function fullBriefing() { // Tier 5: Live Market Data runSource('YFinance', yfinance), + + // Tier 6: Cyber & Infrastructure + runSource('CISA-KEV', cisaKev), + runSource('Cloudflare-Radar', cloudflareRadar), ]; // Each runSource has its own 30s timeout, so allSettled will resolve diff --git a/apis/sources/cisa-kev.mjs b/apis/sources/cisa-kev.mjs new file mode 100644 index 0000000..dcac626 --- /dev/null +++ b/apis/sources/cisa-kev.mjs @@ -0,0 +1,144 @@ +// CISA KEV — Known Exploited Vulnerabilities Catalog +// No auth required. Tracks CVEs actively exploited in the wild. +// Federal agencies must patch these within due dates — useful signal +// for cybersecurity posture and active threat landscape. + +import { safeFetch } from '../utils/fetch.mjs'; + +const KEV_URL = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json'; + +function summarizeVulnerabilities(vulns) { + if (!vulns.length) return {}; + + // Recent additions (last 30 days) + const thirtyDaysAgo = new Date(Date.now() - 30 * 86400_000); + const recent = vulns.filter(v => { + const added = new Date(v.dateAdded); + return !isNaN(added) && added >= thirtyDaysAgo; + }); + + // Group by vendor + const byVendor = {}; + for (const v of vulns) { + const vendor = v.vendorProject || 'Unknown'; + byVendor[vendor] = (byVendor[vendor] || 0) + 1; + } + + // Top vendors sorted by count + const topVendors = Object.entries(byVendor) + .sort((a, b) => b[1] - a[1]) + .slice(0, 15) + .map(([vendor, count]) => ({ vendor, count })); + + // Ransomware-linked + const ransomwareLinked = vulns.filter(v => v.knownRansomwareCampaignUse === 'Known'); + + // Overdue (due date has passed) + const now = new Date(); + const overdue = vulns.filter(v => { + const due = new Date(v.dueDate); + return !isNaN(due) && due < now; + }); + + // Group recent by product for signal detection + const recentByProduct = {}; + for (const v of recent) { + const key = `${v.vendorProject} ${v.product}`; + if (!recentByProduct[key]) recentByProduct[key] = []; + recentByProduct[key].push(v); + } + + const hotProducts = Object.entries(recentByProduct) + .filter(([, vs]) => vs.length >= 2) + .sort((a, b) => b[1].length - a[1].length) + .slice(0, 10) + .map(([product, vs]) => ({ + product, + count: vs.length, + cves: vs.map(v => v.cveID) + })); + + return { + totalInCatalog: vulns.length, + recentAdditions: recent.length, + ransomwareLinked: ransomwareLinked.length, + overdueCount: overdue.length, + topVendors, + hotProducts, + }; +} + +export async function briefing() { + const data = await safeFetch(KEV_URL, { timeout: 20000 }); + + if (data.error) { + return { + source: 'CISA-KEV', + timestamp: new Date().toISOString(), + error: data.error, + }; + } + + const vulns = data.vulnerabilities || []; + const catalogVersion = data.catalogVersion || null; + const dateReleased = data.dateReleased || null; + + const summary = summarizeVulnerabilities(vulns); + + // Get the 20 most recently added + const sorted = [...vulns] + .sort((a, b) => new Date(b.dateAdded) - new Date(a.dateAdded)); + + const recentEntries = sorted.slice(0, 20).map(v => ({ + cveID: v.cveID, + vendorProject: v.vendorProject, + product: v.product, + vulnerabilityName: v.vulnerabilityName, + dateAdded: v.dateAdded, + dueDate: v.dueDate, + shortDescription: (v.shortDescription || '').substring(0, 300), + knownRansomwareCampaignUse: v.knownRansomwareCampaignUse, + })); + + // Signals — actionable intelligence + const signals = []; + + if (summary.recentAdditions > 5) { + signals.push({ + severity: 'high', + signal: `${summary.recentAdditions} new KEV entries in last 30 days — elevated exploit activity`, + }); + } + + if (summary.hotProducts?.length > 0) { + const top = summary.hotProducts[0]; + signals.push({ + severity: 'medium', + signal: `${top.product} has ${top.count} actively exploited CVEs recently added`, + }); + } + + const ransomwareRecent = recentEntries.filter(v => v.knownRansomwareCampaignUse === 'Known'); + if (ransomwareRecent.length > 0) { + signals.push({ + severity: 'critical', + signal: `${ransomwareRecent.length} recently added CVEs linked to ransomware campaigns`, + }); + } + + return { + source: 'CISA-KEV', + timestamp: new Date().toISOString(), + catalogVersion, + dateReleased, + summary, + vulnerabilities: recentEntries, + signals, + }; +} + +// Run standalone +if (process.argv[1]?.endsWith('cisa-kev.mjs')) { + const data = await briefing(); + console.log(JSON.stringify(data, null, 2)); +} diff --git a/apis/sources/cloudflare-radar.mjs b/apis/sources/cloudflare-radar.mjs new file mode 100644 index 0000000..f03453a --- /dev/null +++ b/apis/sources/cloudflare-radar.mjs @@ -0,0 +1,224 @@ +// Cloudflare Radar — Internet traffic anomalies and outages +// Requires a free Cloudflare API token (CLOUDFLARE_API_TOKEN). +// Get one at: https://dash.cloudflare.com/profile/api-tokens +// Create a custom token with Account → Account Analytics → Read permission. +// +// Monitors internet outages, traffic anomalies, and attack trends +// that correlate with conflict, censorship, and infrastructure disruption. + +import { safeFetch } from '../utils/fetch.mjs'; +import '../utils/env.mjs'; + +const RADAR_BASE = 'https://api.cloudflare.com/client/v4/radar'; + +// Countries of intelligence interest for internet monitoring +const WATCHLIST_COUNTRIES = [ + 'RU', 'UA', 'CN', 'IR', 'KP', 'SY', 'MM', 'ET', 'SD', + 'YE', 'AF', 'IQ', 'LB', 'PS', 'TW', 'BY', 'VE', 'CU' +]; + +function getAuthHeaders() { + const token = process.env.CLOUDFLARE_API_TOKEN; + if (!token) return null; + return { Authorization: `Bearer ${token}` }; +} + +async function fetchAnnotations() { + const headers = getAuthHeaders(); + if (!headers) return { error: 'no_credentials' }; + + // Cloudflare Radar Annotations — internet outages and government shutdowns + const url = `${RADAR_BASE}/annotations/outages?dateRange=30d&format=json`; + const data = await safeFetch(url, { timeout: 15000, headers }); + + if (data.error) return { error: data.error }; + + const annotations = data.result?.annotations || []; + return annotations.map(a => ({ + id: a.id, + description: (a.description || '').substring(0, 500), + startDate: a.startDate, + endDate: a.endDate, + linkedUrl: a.linkedUrl || null, + scope: a.scope || null, + asns: a.asns || [], + locations: a.locations || [], + eventType: a.eventType || 'outage', + })); +} + +async function fetchAttackSummary() { + const headers = getAuthHeaders(); + if (!headers) return { error: 'no_credentials' }; + + // Layer 3 DDoS attack summaries by protocol and vector + // API requires a dimension: /summary/{dimension} + const [byProtocol, byVector] = await Promise.all([ + safeFetch(`${RADAR_BASE}/attacks/layer3/summary/protocol?dateRange=7d&format=json`, { timeout: 15000, headers }), + safeFetch(`${RADAR_BASE}/attacks/layer3/summary/vector?dateRange=7d&format=json`, { timeout: 15000, headers }), + ]); + + const result = {}; + + if (!byProtocol.error && byProtocol.result) { + result.byProtocol = byProtocol.result.summary_0 || byProtocol.result; + } + if (!byVector.error && byVector.result) { + result.byVector = byVector.result.summary_0 || byVector.result; + } + + if (!result.byProtocol && !result.byVector) { + return { error: byProtocol.error || byVector.error || 'No attack data returned' }; + } + + return result; +} + +async function fetchTrafficAnomalies() { + const headers = getAuthHeaders(); + if (!headers) return { error: 'no_credentials' }; + + // Traffic anomalies — significant deviations from normal patterns + const url = `${RADAR_BASE}/traffic_anomalies?dateRange=7d&format=json&limit=50`; + const data = await safeFetch(url, { timeout: 15000, headers }); + + if (data.error) return { error: data.error }; + + const anomalies = data.result?.trafficAnomalies || []; + return anomalies.map(a => ({ + startDate: a.startDate, + endDate: a.endDate, + type: a.type || 'unknown', + status: a.status, + asnDetails: a.asnDetails || null, + locationDetails: a.locationDetails || null, + visibleInAllDataSources: a.visibleInAllDataSources || false, + })); +} + +function buildSignals(outages, anomalies) { + const signals = []; + + if (!Array.isArray(outages)) return signals; + + // Check for outages in watchlist countries + const watchlistOutages = outages.filter(o => { + const locations = o.locations || []; + return locations.some(l => WATCHLIST_COUNTRIES.includes(l)); + }); + + if (watchlistOutages.length > 0) { + const countries = [...new Set(watchlistOutages.flatMap(o => o.locations))].filter(l => WATCHLIST_COUNTRIES.includes(l)); + signals.push({ + severity: 'high', + signal: `Internet outages detected in ${countries.join(', ')} — possible government shutdown or infrastructure attack`, + }); + } + + // Multiple outages in same country = sustained disruption + const locationCounts = {}; + for (const o of outages) { + for (const loc of (o.locations || [])) { + locationCounts[loc] = (locationCounts[loc] || 0) + 1; + } + } + + const repeated = Object.entries(locationCounts) + .filter(([, count]) => count >= 3) + .map(([loc]) => loc); + + if (repeated.length > 0) { + signals.push({ + severity: 'medium', + signal: `Sustained internet disruptions in ${repeated.join(', ')} — ${repeated.length} locations with 3+ outage events in 30 days`, + }); + } + + // Traffic anomalies + if (Array.isArray(anomalies) && anomalies.length > 10) { + signals.push({ + severity: 'medium', + signal: `${anomalies.length} traffic anomalies detected globally in last 7 days — elevated internet instability`, + }); + } + + return signals; +} + +export async function briefing() { + if (!process.env.CLOUDFLARE_API_TOKEN) { + return { + source: 'Cloudflare-Radar', + timestamp: new Date().toISOString(), + status: 'no_credentials', + message: 'Set CLOUDFLARE_API_TOKEN in .env. Get a free token at https://dash.cloudflare.com/profile/api-tokens with Account → Account Analytics → Read permission.', + }; + } + + const [outages, attacks, anomalies] = await Promise.all([ + fetchAnnotations(), + fetchAttackSummary(), + fetchTrafficAnomalies(), + ]); + + // Handle complete failure + if (outages?.error && attacks?.error && anomalies?.error) { + return { + source: 'Cloudflare-Radar', + timestamp: new Date().toISOString(), + error: outages.error || attacks.error || anomalies.error, + }; + } + + const outageList = Array.isArray(outages) ? outages : []; + const anomalyList = Array.isArray(anomalies) ? anomalies : []; + + // Separate active vs resolved outages + const now = new Date(); + const activeOutages = outageList.filter(o => !o.endDate || new Date(o.endDate) > now); + const recentResolved = outageList.filter(o => o.endDate && new Date(o.endDate) <= now).slice(0, 10); + + // Group outages by location + const outagesByLocation = {}; + for (const o of outageList) { + for (const loc of (o.locations || ['unknown'])) { + if (!outagesByLocation[loc]) outagesByLocation[loc] = []; + outagesByLocation[loc].push(o); + } + } + + const topAffectedLocations = Object.entries(outagesByLocation) + .sort((a, b) => b[1].length - a[1].length) + .slice(0, 15) + .map(([location, events]) => ({ + location, + eventCount: events.length, + activeCount: events.filter(e => !e.endDate || new Date(e.endDate) > now).length, + })); + + const signals = buildSignals(outageList, anomalyList); + + return { + source: 'Cloudflare-Radar', + timestamp: new Date().toISOString(), + outages: { + total: outageList.length, + active: activeOutages.length, + activeEvents: activeOutages.slice(0, 20), + recentResolved: recentResolved, + topAffectedLocations, + }, + anomalies: { + total: anomalyList.length, + events: anomalyList.slice(0, 20), + }, + attacks: attacks?.error ? { error: attacks.error } : attacks, + signals, + }; +} + +// Run standalone +if (process.argv[1]?.endsWith('cloudflare-radar.mjs')) { + const data = await briefing(); + console.log(JSON.stringify(data, null, 2)); +}