Merge pull request #69 from calesthio/feat/cisa-kev-cloudflare-radar

Add CISA-KEV and Cloudflare Radar source adapters
This commit is contained in:
Calesthio
2026-03-21 14:58:39 -07:00
committed by GitHub
4 changed files with 379 additions and 1 deletions

View File

@@ -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 ===

View File

@@ -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

144
apis/sources/cisa-kev.mjs Normal file
View File

@@ -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));
}

View File

@@ -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));
}