File size: 4,415 Bytes
9d2d895 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | import { getRpcBaseUrl } from '@/services/rpc-client';
import type { CyberThreat as ProtoCyberThreat, ListCyberThreatsResponse } from '@/generated/client/worldmonitor/cyber/v1/service_client';
import type {
CyberThreat,
CyberThreatType,
CyberThreatSource,
CyberThreatSeverity,
CyberThreatIndicatorType,
} from '@/types';
import { createCircuitBreaker } from '@/utils';
import { ensureHydrated } from '@/services/bootstrap';
import { CyberServiceClient } from '@/services/generated-rpc-clients';
// ---- Client + Circuit Breaker ----
const client = new CyberServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) });
const breaker = createCircuitBreaker<ListCyberThreatsResponse>({ name: 'Cyber Threats', cacheTtlMs: 10 * 60 * 1000, persistCache: true });
const emptyFallback: ListCyberThreatsResponse = { threats: [], pagination: undefined };
// ---- Proto enum -> legacy string adapters ----
const THREAT_TYPE_REVERSE: Record<string, CyberThreatType> = {
CYBER_THREAT_TYPE_C2_SERVER: 'c2_server',
CYBER_THREAT_TYPE_MALWARE_HOST: 'malware_host',
CYBER_THREAT_TYPE_PHISHING: 'phishing',
CYBER_THREAT_TYPE_MALICIOUS_URL: 'malicious_url',
};
const SOURCE_REVERSE: Record<string, CyberThreatSource> = {
CYBER_THREAT_SOURCE_FEODO: 'feodo',
CYBER_THREAT_SOURCE_URLHAUS: 'urlhaus',
CYBER_THREAT_SOURCE_C2INTEL: 'c2intel',
CYBER_THREAT_SOURCE_OTX: 'otx',
CYBER_THREAT_SOURCE_ABUSEIPDB: 'abuseipdb',
};
const INDICATOR_TYPE_REVERSE: Record<string, CyberThreatIndicatorType> = {
CYBER_THREAT_INDICATOR_TYPE_IP: 'ip',
CYBER_THREAT_INDICATOR_TYPE_DOMAIN: 'domain',
CYBER_THREAT_INDICATOR_TYPE_URL: 'url',
};
const SEVERITY_REVERSE: Record<string, CyberThreatSeverity> = {
CRITICALITY_LEVEL_LOW: 'low',
CRITICALITY_LEVEL_MEDIUM: 'medium',
CRITICALITY_LEVEL_HIGH: 'high',
CRITICALITY_LEVEL_CRITICAL: 'critical',
};
// ---- Adapter: proto CyberThreat -> legacy CyberThreat ----
function toCyberThreat(proto: ProtoCyberThreat): CyberThreat {
return {
id: proto.id,
type: THREAT_TYPE_REVERSE[proto.type] || 'malicious_url',
source: SOURCE_REVERSE[proto.source] || 'feodo',
indicator: proto.indicator,
indicatorType: INDICATOR_TYPE_REVERSE[proto.indicatorType] || 'ip',
lat: proto.location?.latitude ?? 0,
lon: proto.location?.longitude ?? 0,
country: proto.country || undefined,
severity: SEVERITY_REVERSE[proto.severity] || 'low',
malwareFamily: proto.malwareFamily || undefined,
tags: proto.tags,
firstSeen: proto.firstSeenAt ? new Date(proto.firstSeenAt).toISOString() : undefined,
lastSeen: proto.lastSeenAt ? new Date(proto.lastSeenAt).toISOString() : undefined,
};
}
// ---- Exported Functions ----
const DEFAULT_LIMIT = 500;
const MAX_LIMIT = 1000;
const DEFAULT_DAYS = 14;
const MAX_DAYS = 90;
function clampInt(rawValue: number | undefined, fallback: number, min: number, max: number): number {
if (!Number.isFinite(rawValue)) return fallback;
return Math.max(min, Math.min(max, Math.floor(rawValue as number)));
}
export async function fetchCyberThreats(options: { limit?: number; days?: number } = {}): Promise<CyberThreat[]> {
// `cyberThreats` is an on-demand bootstrap key (#5300): it no longer rides in
// the slow tier, because loadCyberThreats is gated on the cyber layer being ON
// and that layer is off by default in every variant — so the tier was shipping
// 364 KB to every visitor for data the default visitor never read. Callers that
// reach here have already passed that gate, so fetch it now, through its own
// CDN-shielded per-key URL. Falls through to the RPC below if that fetch fails.
const hydrated = (await ensureHydrated('cyberThreats')) as { threats?: ProtoCyberThreat[] } | undefined;
if (hydrated?.threats?.length) return hydrated.threats.map(toCyberThreat);
const limit = clampInt(options.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
const days = clampInt(options.days, DEFAULT_DAYS, 1, MAX_DAYS);
const now = Date.now();
const resp = await breaker.execute(async () => {
return client.listCyberThreats({
start: now - days * 24 * 60 * 60 * 1000,
end: now,
pageSize: limit,
cursor: '',
type: 'CYBER_THREAT_TYPE_UNSPECIFIED',
source: 'CYBER_THREAT_SOURCE_UNSPECIFIED',
minSeverity: 'CRITICALITY_LEVEL_UNSPECIFIED',
});
}, emptyFallback);
return resp.threats.map(toCyberThreat);
}
|