From e782af66e4346c466ae4dbd03ffe4c96699d04e0 Mon Sep 17 00:00:00 2001 From: ghifiardi Date: Tue, 24 Feb 2026 11:10:12 +0700 Subject: [PATCH 1/2] Add GATRA integration layer with connector, panel, and map layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a unified GATRA SOC integration layer for the World Monitor cyber variant: - src/gatra/connector.ts: Centralized connector that fetches all GATRA data sources (ADA alerts, TAA analyses, CRA actions, agent health, correlations) in parallel with pub/sub notifications - src/panels/gatra-soc-panel.ts: Enhanced dashboard panel with 6 sections โ€” agent status, incident stats, alert feed, TAA threat analysis (actor/campaign/kill-chain), CRA response actions, and dynamic World Monitor correlation insights - src/layers/gatra-alerts-layer.ts: Standalone deck.gl layer factory with severity-colored markers and pulsing rings for critical/high alerts - Extends src/services/gatra.ts with TAA analysis, correlation, and typed CRA action mock data using Indonesian locations and IOH infrastructure references - Wires createGatraAlertsLayers() in DeckGLMap.ts and adds gatraAlerts to all variant MapLayers definitions Co-Authored-By: Claude Opus 4.6 --- src/components/DeckGLMap.ts | 31 +++ src/components/GatraSOCPanel.ts | 179 +++++++++++++ src/config/panels.ts | 119 ++++++++- src/config/variants/cyber.ts | 205 ++++++++++++++ src/config/variants/finance.ts | 2 + src/config/variants/full.ts | 2 + src/config/variants/tech.ts | 2 + src/e2e/map-harness.ts | 2 + src/e2e/mobile-map-integration-harness.ts | 1 + src/gatra/connector.ts | 127 +++++++++ src/layers/gatra-alerts-layer.ts | 123 +++++++++ src/panels/gatra-soc-panel.ts | 312 ++++++++++++++++++++++ src/services/gatra.ts | 290 ++++++++++++++++++++ src/types/index.ts | 88 ++++++ 14 files changed, 1480 insertions(+), 3 deletions(-) create mode 100644 src/components/GatraSOCPanel.ts create mode 100644 src/config/variants/cyber.ts create mode 100644 src/gatra/connector.ts create mode 100644 src/layers/gatra-alerts-layer.ts create mode 100644 src/panels/gatra-soc-panel.ts create mode 100644 src/services/gatra.ts diff --git a/src/components/DeckGLMap.ts b/src/components/DeckGLMap.ts index d5626cea..f8550543 100644 --- a/src/components/DeckGLMap.ts +++ b/src/components/DeckGLMap.ts @@ -33,6 +33,7 @@ import type { MapDatacenterCluster, CyberThreat, CableHealthRecord, + GatraAlert, } from '@/types'; import type { AirportDelayAlert } from '@/services/aviation'; import type { DisplacementFlow } from '@/services/displacement'; @@ -40,6 +41,7 @@ import type { Earthquake } from '@/services/earthquakes'; import type { ClimateAnomaly } from '@/services/climate'; import { ArcLayer } from '@deck.gl/layers'; import { HeatmapLayer } from '@deck.gl/aggregation-layers'; +import { createGatraAlertsLayers as buildGatraLayers } from '@/layers/gatra-alerts-layer'; import type { WeatherAlert } from '@/services/weather'; import { escapeHtml } from '@/utils/sanitize'; import { t } from '@/services/i18n'; @@ -268,6 +270,7 @@ export class DeckGLMap { private ucdpEvents: UcdpGeoEvent[] = []; private displacementFlows: DisplacementFlow[] = []; private climateAnomalies: ClimateAnomaly[] = []; + private gatraAlerts: GatraAlert[] = []; // Country highlight state private countryGeoJsonLoaded = false; @@ -1140,6 +1143,11 @@ export class DeckGLMap { layers.push(this.createGulfInvestmentsLayer()); } + // GATRA SOC alerts layer + if (mapLayers.gatraAlerts && this.gatraAlerts.length > 0) { + layers.push(...this.createGatraAlertsLayers()); + } + // News geo-locations (always shown if data exists) if (this.newsLocations.length > 0) { layers.push(...this.createNewsLocationsLayer()); @@ -2449,6 +2457,8 @@ export class DeckGLMap { return { html: `
${text(obj.asn || t('components.deckgl.tooltip.internetOutage'))}
${text(obj.country)}
` }; case 'cyber-threats-layer': return { html: `
${t('popups.cyberThreat.title')}
${text(obj.severity || t('components.deckgl.tooltip.medium'))} ยท ${text(obj.country || t('popups.unknown'))}
` }; + case 'gatra-alerts-layer': + return { html: `
GATRA ${text(obj.severity?.toUpperCase() || 'ALERT')}
${text(obj.mitreId || '')} ${text(obj.mitreName || '')}
${text(obj.locationName || '')} ยท ${text(obj.infrastructure || '')}
${obj.confidence != null ? obj.confidence + '% confidence' : ''}
` }; case 'news-locations-layer': return { html: `
๐Ÿ“ฐ ${t('components.deckgl.tooltip.news')}
${text(obj.title?.slice(0, 80) || '')}
` }; case 'gulf-investments-layer': { @@ -2786,6 +2796,18 @@ export class DeckGLMap { { key: 'natural', label: t('components.deckgl.layers.naturalEvents'), icon: '🌋' }, { key: 'cyberThreats', label: t('components.deckgl.layers.cyberThreats'), icon: '🛡' }, ] + : SITE_VARIANT === 'cyber' + ? [ + { key: 'gatraAlerts', label: t('components.deckgl.layers.gatraAlerts'), icon: '🛡' }, + { key: 'cyberThreats', label: t('components.deckgl.layers.cyberThreats'), icon: '🔒' }, + { key: 'conflicts', label: t('components.deckgl.layers.conflictZones'), icon: '⚔' }, + { key: 'cables', label: t('components.deckgl.layers.underseaCables'), icon: '🔌' }, + { key: 'datacenters', label: t('components.deckgl.layers.aiDataCenters'), icon: '🖥' }, + { key: 'military', label: t('components.deckgl.layers.militaryActivity'), icon: '✈' }, + { key: 'outages', label: t('components.deckgl.layers.internetOutages'), icon: '📡' }, + { key: 'natural', label: t('components.deckgl.layers.naturalEvents'), icon: '🌋' }, + { key: 'fires', label: t('components.deckgl.layers.fires'), icon: '🔥' }, + ] : [ { key: 'hotspots', label: t('components.deckgl.layers.intelHotspots'), icon: '🎯' }, { key: 'conflicts', label: t('components.deckgl.layers.conflictZones'), icon: '⚔' }, @@ -3257,6 +3279,10 @@ export class DeckGLMap { }); } + private createGatraAlertsLayers(): Layer[] { + return buildGatraLayers(this.gatraAlerts, this.pulseTime || Date.now()); + } + // Data setters - all use render() for debouncing public setEarthquakes(earthquakes: Earthquake[]): void { this.earthquakes = earthquakes; @@ -3280,6 +3306,11 @@ export class DeckGLMap { this.render(); } + public setGatraAlerts(alerts: GatraAlert[]): void { + this.gatraAlerts = alerts; + this.render(); + } + public setAisData(disruptions: AisDisruptionEvent[], density: AisDensityZone[]): void { this.aisDisruptions = disruptions; this.aisDensity = density; diff --git a/src/components/GatraSOCPanel.ts b/src/components/GatraSOCPanel.ts new file mode 100644 index 00000000..af34ffee --- /dev/null +++ b/src/components/GatraSOCPanel.ts @@ -0,0 +1,179 @@ +/** + * GatraSOCPanel โ€” GATRA SOC integration dashboard panel. + * + * Self-contained pull pattern: App calls refresh() on a 60s interval. + * Displays agent status, incident stats, alert feed, and + * correlation insights linking WorldMonitor geopolitical events to GATRA alerts. + */ + +import { Panel } from './Panel'; +import { escapeHtml } from '@/utils/sanitize'; +import { + fetchGatraAlerts, + fetchGatraAgentStatus, + fetchGatraIncidentSummary, +} from '@/services/gatra'; +import type { + GatraAlert, + GatraAgentStatus, + GatraIncidentSummary, +} from '@/types'; + +export class GatraSOCPanel extends Panel { + private alerts: GatraAlert[] = []; + private agentStatus: GatraAgentStatus[] = []; + private summary: GatraIncidentSummary | null = null; + private loading = false; + + constructor() { + super({ + id: 'gatra-soc', + title: 'GATRA SOC', + showCount: true, + }); + } + + /** Called by App on 60s interval. */ + public async refresh(): Promise { + if (this.loading) return; + this.loading = true; + + try { + const [alerts, agents, summary] = await Promise.all([ + fetchGatraAlerts(), + fetchGatraAgentStatus(), + fetchGatraIncidentSummary(), + ]); + + this.alerts = alerts; + this.agentStatus = agents; + this.summary = summary; + + this.setCount(alerts.length); + this.render(); + } catch (err) { + console.error('[GatraSOCPanel] refresh error:', err); + this.showError('Failed to load GATRA data'); + } finally { + this.loading = false; + } + } + + /** Expose alerts for the map layer. */ + public getAlerts(): GatraAlert[] { + return this.alerts; + } + + // โ”€โ”€ Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private render(): void { + const html = [ + this.renderAgentStatusBar(), + this.renderStatsRow(), + this.renderAlertFeed(), + this.renderCorrelation(), + ].join(''); + + this.setContent(html); + } + + private renderAgentStatusBar(): string { + if (this.agentStatus.length === 0) return ''; + + const dots = this.agentStatus + .map((a) => { + const color = + a.status === 'online' ? '#22c55e' : + a.status === 'processing' ? '#eab308' : + '#ef4444'; + const title = escapeHtml(`${a.fullName} โ€” ${a.status}`); + return ` + + ${escapeHtml(a.name)} + `; + }) + .join(''); + + return `
+ Agents + ${dots} +
`; + } + + private renderStatsRow(): string { + if (!this.summary) return ''; + const s = this.summary; + const stat = (label: string, value: string | number) => + `
+
${value}
+
${label}
+
`; + + return `
+ ${stat('Active', s.activeIncidents)} + ${stat('MTTR', s.mttrMinutes + 'm')} + ${stat('24h Alerts', s.alerts24h)} + ${stat('24h Resp', s.responses24h)} +
`; + } + + private renderAlertFeed(): string { + if (this.alerts.length === 0) return '
No alerts
'; + + const rows = this.alerts.slice(0, 20).map((a) => { + const sevColor = + a.severity === 'critical' ? '#ef4444' : + a.severity === 'high' ? '#f97316' : + a.severity === 'medium' ? '#eab308' : + '#3b82f6'; + const ts = this.timeAgo(a.timestamp); + + return `
+ ${a.severity.toUpperCase()} +
+
+ ${escapeHtml(a.mitreId)} โ€” ${escapeHtml(a.mitreName)} + ${ts} +
+
${escapeHtml(a.description)}
+
${escapeHtml(a.locationName)} ยท ${escapeHtml(a.infrastructure)} ยท ${a.confidence}%
+
+
`; + }).join(''); + + return `
+
Alert Feed
+ ${rows} +
`; + } + + private renderCorrelation(): string { + const insights = [ + 'Phishing campaign (T1566) targeting Jakarta NOC operators correlates with regional APT activity detected by WorldMonitor threat intel layer.', + 'Elevated brute-force attempts on Surabaya edge gateways coincide with increased nation-state cyber activity in Southeast Asia.', + 'GATRA CRA automated 12 containment actions in 24h โ€” MTTR improved 35% vs. manual SOC baseline.', + ]; + + const rows = insights + .map( + (text) => + `
+ ${escapeHtml(text)} +
` + ) + .join(''); + + return `
+
Correlation Insights
+ ${rows} +
`; + } + + private timeAgo(date: Date): string { + const ms = Date.now() - date.getTime(); + if (ms < 60_000) return 'just now'; + if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`; + if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ago`; + return `${Math.floor(ms / 86_400_000)}d ago`; + } +} diff --git a/src/config/panels.ts b/src/config/panels.ts index 32f43fa0..cc4068b7 100644 --- a/src/config/panels.ts +++ b/src/config/panels.ts @@ -86,6 +86,7 @@ const FULL_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; const FULL_MOBILE_MAP_LAYERS: MapLayers = { @@ -127,6 +128,7 @@ const FULL_MOBILE_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; // ============================================ @@ -208,6 +210,7 @@ const TECH_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; const TECH_MOBILE_MAP_LAYERS: MapLayers = { @@ -249,6 +252,7 @@ const TECH_MOBILE_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; // ============================================ @@ -325,6 +329,7 @@ const FINANCE_MAP_LAYERS: MapLayers = { centralBanks: true, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; const FINANCE_MOBILE_MAP_LAYERS: MapLayers = { @@ -366,14 +371,121 @@ const FINANCE_MOBILE_MAP_LAYERS: MapLayers = { centralBanks: true, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, +}; + +// ============================================ +// CYBER VARIANT (Cybersecurity/GATRA) +// ============================================ +const CYBER_PANELS: Record = { + map: { name: 'Global Cyber Map', enabled: true, priority: 1 }, + 'live-news': { name: 'Cyber Headlines', enabled: true, priority: 1 }, + insights: { name: 'AI Insights', enabled: true, priority: 1 }, + security: { name: 'Cybersecurity News', enabled: true, priority: 1 }, + indonesia: { name: 'Indonesia Cyber (BSSN)', enabled: true, priority: 1 }, + threats: { name: 'Threat Intelligence', enabled: true, priority: 1 }, + malware: { name: 'Ransomware & Malware', enabled: true, priority: 1 }, + infrastructure: { name: 'Infrastructure Security', enabled: true, priority: 1 }, + geoCyber: { name: 'Nation-State Cyber', enabled: true, priority: 1 }, + research: { name: 'Security Research', enabled: true, priority: 1 }, + policy: { name: 'Cyber Policy', enabled: true, priority: 2 }, + aiSecurity: { name: 'AI & Security', enabled: true, priority: 2 }, + 'gatra-soc': { name: 'GATRA SOC', enabled: true, priority: 1 }, + monitors: { name: 'My Monitors', enabled: true, priority: 2 }, +}; + +const CYBER_MAP_LAYERS: MapLayers = { + conflicts: true, + bases: false, + cables: true, + pipelines: false, + hotspots: false, + ais: false, + nuclear: false, + irradiators: false, + sanctions: false, + weather: false, + economic: false, + waterways: false, + outages: true, + cyberThreats: true, + datacenters: true, + protests: false, + flights: false, + military: true, + natural: true, + spaceports: false, + minerals: false, + fires: true, + // Data source layers + ucdpEvents: false, + displacement: false, + climate: false, + // Tech layers (disabled in cyber variant) + startupHubs: false, + cloudRegions: false, + accelerators: false, + techHQs: false, + techEvents: false, + // Finance layers (disabled in cyber variant) + stockExchanges: false, + financialCenters: false, + centralBanks: false, + commodityHubs: false, + gulfInvestments: false, + // GATRA SOC layer (enabled in cyber variant) + gatraAlerts: true, +}; + +const CYBER_MOBILE_MAP_LAYERS: MapLayers = { + conflicts: true, + bases: false, + cables: true, + pipelines: false, + hotspots: false, + ais: false, + nuclear: false, + irradiators: false, + sanctions: false, + weather: false, + economic: false, + waterways: false, + outages: true, + cyberThreats: true, + datacenters: false, + protests: false, + flights: false, + military: false, + natural: true, + spaceports: false, + minerals: false, + fires: true, + // Data source layers + ucdpEvents: false, + displacement: false, + climate: false, + // Tech layers + startupHubs: false, + cloudRegions: false, + accelerators: false, + techHQs: false, + techEvents: false, + // Finance layers + stockExchanges: false, + financialCenters: false, + centralBanks: false, + commodityHubs: false, + gulfInvestments: false, + // GATRA SOC layer (enabled in cyber variant) + gatraAlerts: true, }; // ============================================ // VARIANT-AWARE EXPORTS // ============================================ -export const DEFAULT_PANELS = SITE_VARIANT === 'tech' ? TECH_PANELS : SITE_VARIANT === 'finance' ? FINANCE_PANELS : FULL_PANELS; -export const DEFAULT_MAP_LAYERS = SITE_VARIANT === 'tech' ? TECH_MAP_LAYERS : SITE_VARIANT === 'finance' ? FINANCE_MAP_LAYERS : FULL_MAP_LAYERS; -export const MOBILE_DEFAULT_MAP_LAYERS = SITE_VARIANT === 'tech' ? TECH_MOBILE_MAP_LAYERS : SITE_VARIANT === 'finance' ? FINANCE_MOBILE_MAP_LAYERS : FULL_MOBILE_MAP_LAYERS; +export const DEFAULT_PANELS = SITE_VARIANT === 'tech' ? TECH_PANELS : SITE_VARIANT === 'finance' ? FINANCE_PANELS : SITE_VARIANT === 'cyber' ? CYBER_PANELS : FULL_PANELS; +export const DEFAULT_MAP_LAYERS = SITE_VARIANT === 'tech' ? TECH_MAP_LAYERS : SITE_VARIANT === 'finance' ? FINANCE_MAP_LAYERS : SITE_VARIANT === 'cyber' ? CYBER_MAP_LAYERS : FULL_MAP_LAYERS; +export const MOBILE_DEFAULT_MAP_LAYERS = SITE_VARIANT === 'tech' ? TECH_MOBILE_MAP_LAYERS : SITE_VARIANT === 'finance' ? FINANCE_MOBILE_MAP_LAYERS : SITE_VARIANT === 'cyber' ? CYBER_MOBILE_MAP_LAYERS : FULL_MOBILE_MAP_LAYERS; /** Maps map-layer toggle keys to their data-freshness source IDs (single source of truth). */ export const LAYER_TO_SOURCE: Partial> = { @@ -387,6 +499,7 @@ export const LAYER_TO_SOURCE: Partial> = ucdpEvents: ['ucdp_events'], displacement: ['unhcr'], climate: ['climate'], + gatraAlerts: ['gatra'], }; // Monitor palette โ€” fixed category colors persisted to localStorage (not theme-dependent) diff --git a/src/config/variants/cyber.ts b/src/config/variants/cyber.ts new file mode 100644 index 00000000..2324879a --- /dev/null +++ b/src/config/variants/cyber.ts @@ -0,0 +1,205 @@ +// Cyber/GATRA variant - cybersecurity intelligence dashboard +// +// Includes the GATRA integration layer: +// - GatraSOCDashboardPanel (src/panels/gatra-soc-panel.ts) +// - GATRA alerts map layer (src/layers/gatra-alerts-layer.ts) +// - GATRA connector service (src/gatra/connector.ts) +import type { PanelConfig, MapLayers } from '@/types'; +import type { VariantConfig } from './base'; + +// Re-export base config +export * from './base'; + +// Cyber-focused feeds configuration +import type { Feed } from '@/types'; + +const rss = (url: string) => `/api/rss-proxy?url=${encodeURIComponent(url)}`; + +export const FEEDS: Record = { + // Core Cybersecurity News + security: [ + { name: 'Krebs on Security', url: rss('https://krebsonsecurity.com/feed/') }, + { name: 'Bleeping Computer', url: rss('https://www.bleepingcomputer.com/feed/') }, + { name: 'The Hacker News', url: rss('https://feeds.feedburner.com/TheHackersNews') }, + { name: 'Dark Reading', url: rss('https://www.darkreading.com/rss.xml') }, + { name: 'Schneier on Security', url: rss('https://www.schneier.com/feed/') }, + { name: 'SecurityWeek', url: rss('https://www.securityweek.com/feed/') }, + { name: 'The Record', url: rss('https://therecord.media/feed') }, + { name: 'CSO Online', url: rss('https://www.csoonline.com/feed/') }, + ], + + // Indonesian Cyber Sources + indonesia: [ + { name: 'BSSN News', url: rss('https://www.bssn.go.id/feed/') }, + { name: 'Indonesia Cyber', url: rss('https://news.google.com/rss/search?q=(BSSN+OR+"Badan+Siber"+OR+Indonesia+cybersecurity+OR+Indonesia+cyber+attack)+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'APJII News', url: rss('https://news.google.com/rss/search?q=APJII+OR+"internet+Indonesia"+OR+"digital+Indonesia"+when:7d&hl=en-US&gl=US&ceid=US:en') }, + ], + + // Threat Intelligence + threats: [ + { name: 'CISA Advisories', url: rss('https://www.cisa.gov/cybersecurity-advisories/all.xml') }, + { name: 'US-CERT Alerts', url: rss('https://www.cisa.gov/uscert/ncas/alerts.xml') }, + { name: 'NIST CVE', url: rss('https://news.google.com/rss/search?q=(CVE+OR+"zero+day"+OR+"critical+vulnerability")+when:3d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'Exploit DB', url: rss('https://news.google.com/rss/search?q=("exploit"+OR+"proof+of+concept"+OR+"CVE")+cybersecurity+when:3d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'Cyber Incidents', url: rss('https://news.google.com/rss/search?q=(cyber+attack+OR+data+breach+OR+ransomware+OR+hacking)+when:3d&hl=en-US&gl=US&ceid=US:en') }, + ], + + // Ransomware & Malware + malware: [ + { name: 'Ransomware News', url: rss('https://news.google.com/rss/search?q=ransomware+attack+OR+ransomware+gang+when:3d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'Malware Analysis', url: rss('https://news.google.com/rss/search?q=malware+analysis+OR+malware+campaign+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'APT Groups', url: rss('https://news.google.com/rss/search?q=(APT+OR+"advanced+persistent+threat"+OR+"state+sponsored")+cyber+when:7d&hl=en-US&gl=US&ceid=US:en') }, + ], + + // Infrastructure & ICS/OT Security + infrastructure: [ + { name: 'ICS Security', url: rss('https://news.google.com/rss/search?q=(ICS+OR+SCADA+OR+"operational+technology"+OR+OT)+cybersecurity+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'Critical Infrastructure', url: rss('https://news.google.com/rss/search?q="critical+infrastructure"+cyber+OR+attack+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'Submarine Cable News', url: rss('https://news.google.com/rss/search?q="submarine+cable"+OR+"undersea+cable"+sabotage+OR+damage+OR+security+when:7d&hl=en-US&gl=US&ceid=US:en') }, + ], + + // Nation-State & Geopolitical Cyber + geoCyber: [ + { name: 'Cyber Warfare', url: rss('https://news.google.com/rss/search?q="cyber+warfare"+OR+"cyber+espionage"+OR+"nation+state"+hacking+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'China Cyber', url: rss('https://news.google.com/rss/search?q=China+cyber+espionage+OR+China+hacking+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'Russia Cyber', url: rss('https://news.google.com/rss/search?q=Russia+cyber+attack+OR+Russia+hacking+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'North Korea Cyber', url: rss('https://news.google.com/rss/search?q="North+Korea"+cyber+OR+Lazarus+Group+when:14d&hl=en-US&gl=US&ceid=US:en') }, + ], + + // Security Research & Vendor Blogs + research: [ + { name: 'Google Project Zero', url: rss('https://googleprojectzero.blogspot.com/feeds/posts/default?alt=rss') }, + { name: 'Microsoft Security', url: rss('https://www.microsoft.com/en-us/security/blog/feed/') }, + { name: 'Google TAG', url: rss('https://blog.google/threat-analysis-group/rss/') }, + { name: 'Mandiant Blog', url: rss('https://www.mandiant.com/resources/blog/rss.xml') }, + { name: 'CrowdStrike Blog', url: rss('https://www.crowdstrike.com/blog/feed/') }, + { name: 'Palo Alto Unit 42', url: rss('https://unit42.paloaltonetworks.com/feed/') }, + ], + + // Policy & Regulation + policy: [ + { name: 'Cyber Policy', url: rss('https://news.google.com/rss/search?q=cybersecurity+regulation+OR+cybersecurity+policy+OR+cybersecurity+law+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'NIST Framework', url: rss('https://news.google.com/rss/search?q=NIST+cybersecurity+framework+when:14d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'EU Cyber', url: rss('https://news.google.com/rss/search?q=(NIS2+OR+ENISA+OR+"EU+cybersecurity")+when:14d&hl=en-US&gl=US&ceid=US:en') }, + ], + + // AI & Security + aiSecurity: [ + { name: 'AI Security', url: rss('https://news.google.com/rss/search?q=("AI+security"+OR+"machine+learning"+cybersecurity+OR+"adversarial+AI")+when:7d&hl=en-US&gl=US&ceid=US:en') }, + { name: 'AI for SOC', url: rss('https://news.google.com/rss/search?q=("AI+SOC"+OR+"AI+threat+detection"+OR+"automated+security")+when:7d&hl=en-US&gl=US&ceid=US:en') }, + ], +}; + +// Panel configuration for cyber/GATRA dashboard +export const DEFAULT_PANELS: Record = { + map: { name: 'Global Cyber Map', enabled: true, priority: 1 }, + 'live-news': { name: 'Cyber Headlines', enabled: true, priority: 1 }, + insights: { name: 'AI Insights', enabled: true, priority: 1 }, + 'gatra-soc': { name: 'GATRA SOC', enabled: true, priority: 1 }, + security: { name: 'Cybersecurity News', enabled: true, priority: 1 }, + indonesia: { name: 'Indonesia Cyber (BSSN)', enabled: true, priority: 1 }, + threats: { name: 'Threat Intelligence', enabled: true, priority: 1 }, + malware: { name: 'Ransomware & Malware', enabled: true, priority: 1 }, + infrastructure: { name: 'Infrastructure Security', enabled: true, priority: 1 }, + geoCyber: { name: 'Nation-State Cyber', enabled: true, priority: 1 }, + research: { name: 'Security Research', enabled: true, priority: 1 }, + policy: { name: 'Cyber Policy', enabled: true, priority: 2 }, + aiSecurity: { name: 'AI & Security', enabled: true, priority: 2 }, + monitors: { name: 'My Monitors', enabled: true, priority: 2 }, +}; + +// Cyber-focused map layers +export const DEFAULT_MAP_LAYERS: MapLayers = { + conflicts: true, + bases: false, + cables: true, + pipelines: false, + hotspots: false, + ais: false, + nuclear: false, + irradiators: false, + sanctions: false, + weather: false, + economic: false, + waterways: false, + outages: true, + cyberThreats: true, + datacenters: true, + protests: false, + flights: false, + military: true, + natural: true, + spaceports: false, + minerals: false, + fires: true, + // Data source layers + ucdpEvents: false, + displacement: false, + climate: false, + // Tech layers + startupHubs: false, + cloudRegions: false, + accelerators: false, + techHQs: false, + techEvents: false, + // Finance layers + stockExchanges: false, + financialCenters: false, + centralBanks: false, + commodityHubs: false, + gulfInvestments: false, + // GATRA SOC layer โ€” enabled by default in cyber variant + gatraAlerts: true, +}; + +// Mobile defaults for cyber variant +export const MOBILE_DEFAULT_MAP_LAYERS: MapLayers = { + conflicts: true, + bases: false, + cables: true, + pipelines: false, + hotspots: false, + ais: false, + nuclear: false, + irradiators: false, + sanctions: false, + weather: false, + economic: false, + waterways: false, + outages: true, + cyberThreats: true, + datacenters: false, + protests: false, + flights: false, + military: false, + natural: true, + spaceports: false, + minerals: false, + fires: true, + // Data source layers + ucdpEvents: false, + displacement: false, + climate: false, + // Tech layers + startupHubs: false, + cloudRegions: false, + accelerators: false, + techHQs: false, + techEvents: false, + // Finance layers + stockExchanges: false, + financialCenters: false, + centralBanks: false, + commodityHubs: false, + gulfInvestments: false, + // GATRA SOC layer โ€” enabled on mobile too + gatraAlerts: true, +}; + +export const VARIANT_CONFIG: VariantConfig = { + name: 'cyber', + description: 'Cybersecurity & GATRA threat intelligence dashboard', + panels: DEFAULT_PANELS, + mapLayers: DEFAULT_MAP_LAYERS, + mobileMapLayers: MOBILE_DEFAULT_MAP_LAYERS, +}; diff --git a/src/config/variants/finance.ts b/src/config/variants/finance.ts index ccb2db20..9c9857c3 100644 --- a/src/config/variants/finance.ts +++ b/src/config/variants/finance.ts @@ -209,6 +209,7 @@ export const DEFAULT_MAP_LAYERS: MapLayers = { centralBanks: true, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; // Mobile defaults for finance variant @@ -250,6 +251,7 @@ export const MOBILE_DEFAULT_MAP_LAYERS: MapLayers = { centralBanks: true, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; export const VARIANT_CONFIG: VariantConfig = { diff --git a/src/config/variants/full.ts b/src/config/variants/full.ts index 02bc89e0..4f4c3165 100644 --- a/src/config/variants/full.ts +++ b/src/config/variants/full.ts @@ -87,6 +87,7 @@ export const DEFAULT_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; // Mobile-specific defaults for geopolitical @@ -128,6 +129,7 @@ export const MOBILE_DEFAULT_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; export const VARIANT_CONFIG: VariantConfig = { diff --git a/src/config/variants/tech.ts b/src/config/variants/tech.ts index 0925b0e6..311aff81 100644 --- a/src/config/variants/tech.ts +++ b/src/config/variants/tech.ts @@ -238,6 +238,7 @@ export const DEFAULT_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; // Mobile defaults for tech variant @@ -279,6 +280,7 @@ export const MOBILE_DEFAULT_MAP_LAYERS: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; export const VARIANT_CONFIG: VariantConfig = { diff --git a/src/e2e/map-harness.ts b/src/e2e/map-harness.ts index 58910989..4461aded 100644 --- a/src/e2e/map-harness.ts +++ b/src/e2e/map-harness.ts @@ -171,6 +171,7 @@ const allLayersEnabled: MapLayers = { centralBanks: true, commodityHubs: true, gulfInvestments: true, + gatraAlerts: true, }; const allLayersDisabled: MapLayers = { @@ -209,6 +210,7 @@ const allLayersDisabled: MapLayers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; const SEEDED_NEWS_LOCATIONS: Array<{ diff --git a/src/e2e/mobile-map-integration-harness.ts b/src/e2e/mobile-map-integration-harness.ts index 4b25351c..7b24b73e 100644 --- a/src/e2e/mobile-map-integration-harness.ts +++ b/src/e2e/mobile-map-integration-harness.ts @@ -120,6 +120,7 @@ const layers = { centralBanks: false, commodityHubs: false, gulfInvestments: false, + gatraAlerts: false, }; await initI18n(); diff --git a/src/gatra/connector.ts b/src/gatra/connector.ts new file mode 100644 index 00000000..aff6079f --- /dev/null +++ b/src/gatra/connector.ts @@ -0,0 +1,127 @@ +/** + * GATRA SOC Connector โ€” unified integration layer + * + * Exposes GATRA's 5-agent pipeline data via the same panel system + * World Monitor uses. Current implementation uses mock data from + * `@/services/gatra`; the mock calls will be replaced by real GATRA + * API feeds via Pub/Sub once the production connector is ready. + * + * Data exposed: + * ADA alerts โ€” anomaly detections with MITRE ATT&CK mapping + * TAA analyses โ€” threat investigation with actor/campaign/kill-chain + * CRA actions โ€” automated containment responses with status + * Agent health โ€” ADA/TAA/CRA/CLA/RVA heartbeat & state + * Correlations โ€” links between World Monitor events and GATRA alerts + */ + +import { + fetchGatraAlerts, + fetchGatraAgentStatus, + fetchGatraIncidentSummary, + fetchGatraCRAActions, + fetchGatraTAAAnalyses, + fetchGatraCorrelations, +} from '@/services/gatra'; + +import type { + GatraAlert, + GatraAgentStatus, + GatraIncidentSummary, + GatraCRAAction, + GatraTAAAnalysis, + GatraCorrelation, + GatraConnectorSnapshot, +} from '@/types'; + +// โ”€โ”€ Connector state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +let _snapshot: GatraConnectorSnapshot | null = null; +let _refreshing = false; +const _listeners: Set<(snap: GatraConnectorSnapshot) => void> = new Set(); + +// โ”€โ”€ Public API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** + * Fetch all GATRA data sources in parallel and cache the result. + * Returns a unified snapshot that panels, layers, and other consumers + * can read without issuing their own requests. + */ +export async function refreshGatraData(): Promise { + if (_refreshing && _snapshot) return _snapshot; + _refreshing = true; + + try { + const [alerts, agents, summary, craActions] = await Promise.all([ + fetchGatraAlerts(), + fetchGatraAgentStatus(), + fetchGatraIncidentSummary(), + fetchGatraCRAActions(), + ]); + + // TAA and correlations depend on alerts + const [taaAnalyses, correlations] = await Promise.all([ + fetchGatraTAAAnalyses(alerts), + fetchGatraCorrelations(alerts), + ]); + + _snapshot = { + alerts, + agents, + summary, + craActions, + taaAnalyses, + correlations, + lastRefresh: new Date(), + }; + + // Notify subscribers + for (const fn of _listeners) { + try { fn(_snapshot); } catch (e) { console.error('[GatraConnector] listener error:', e); } + } + + return _snapshot; + } catch (err) { + console.error('[GatraConnector] refresh failed:', err); + if (_snapshot) return _snapshot; + throw err; + } finally { + _refreshing = false; + } +} + +/** Return the last cached snapshot (may be null before first refresh). */ +export function getGatraSnapshot(): GatraConnectorSnapshot | null { + return _snapshot; +} + +/** Subscribe to snapshot updates. Returns an unsubscribe function. */ +export function onGatraUpdate(fn: (snap: GatraConnectorSnapshot) => void): () => void { + _listeners.add(fn); + return () => { _listeners.delete(fn); }; +} + +// โ”€โ”€ Convenience accessors โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export function getAlerts(): GatraAlert[] { + return _snapshot?.alerts ?? []; +} + +export function getAgentStatus(): GatraAgentStatus[] { + return _snapshot?.agents ?? []; +} + +export function getIncidentSummary(): GatraIncidentSummary | null { + return _snapshot?.summary ?? null; +} + +export function getCRAActions(): GatraCRAAction[] { + return _snapshot?.craActions ?? []; +} + +export function getTAAAnalyses(): GatraTAAAnalysis[] { + return _snapshot?.taaAnalyses ?? []; +} + +export function getCorrelations(): GatraCorrelation[] { + return _snapshot?.correlations ?? []; +} diff --git a/src/layers/gatra-alerts-layer.ts b/src/layers/gatra-alerts-layer.ts new file mode 100644 index 00000000..77d8575f --- /dev/null +++ b/src/layers/gatra-alerts-layer.ts @@ -0,0 +1,123 @@ +/** + * GATRA Alerts Map Layer โ€” deck.gl layer factory + * + * Plots GATRA alert locations on the World Monitor map using deck.gl: + * - Red pulsing markers for critical alerts + * - Orange markers for high severity + * - Yellow for medium, blue for low + * + * Returns an array of Layer instances that DeckGLMap can spread into + * its layer list. The pulse ring layer uses a time-based radius scale + * so critical alerts visually pulse on the map. + */ + +import { ScatterplotLayer, TextLayer } from '@deck.gl/layers'; +import type { Layer } from '@deck.gl/core'; +import type { GatraAlert, GatraAlertSeverity } from '@/types'; + +// โ”€โ”€ Color palette โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +type RGBA = [number, number, number, number]; + +const SEVERITY_FILL: Record = { + critical: [255, 50, 50, 220], + high: [255, 150, 0, 220], + medium: [255, 220, 0, 200], + low: [100, 150, 255, 180], +}; + +const SEVERITY_RADIUS: Record = { + critical: 18000, + high: 14000, + medium: 10000, + low: 8000, +}; + +// โ”€โ”€ Public factory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** + * Build the deck.gl layers for GATRA alerts. + * + * @param alerts Current GATRA alert list + * @param pulseTime Monotonic timestamp used for pulse animation + * (pass `Date.now()` or the shared pulse clock from DeckGLMap) + */ +export function createGatraAlertsLayers( + alerts: GatraAlert[], + pulseTime: number = Date.now(), +): Layer[] { + if (alerts.length === 0) return []; + + const layers: Layer[] = []; + + // 1. Base scatterplot โ€” all alerts + layers.push( + new ScatterplotLayer({ + id: 'gatra-alerts-layer', + data: alerts, + getPosition: (d) => [d.lon, d.lat], + getRadius: (d) => SEVERITY_RADIUS[d.severity], + getFillColor: (d) => SEVERITY_FILL[d.severity], + radiusMinPixels: 4, + radiusMaxPixels: 16, + pickable: true, + stroked: true, + getLineColor: [255, 255, 255, 100] as RGBA, + lineWidthMinPixels: 1, + }), + ); + + // 2. Pulse ring โ€” critical and high alerts only + const pulsable = alerts.filter( + (a) => a.severity === 'critical' || a.severity === 'high', + ); + + if (pulsable.length > 0) { + const pulseScale = 1.0 + 0.9 * (0.5 + 0.5 * Math.sin(pulseTime / 350)); + + layers.push( + new ScatterplotLayer({ + id: 'gatra-alerts-pulse', + data: pulsable, + getPosition: (d) => [d.lon, d.lat], + getRadius: (d) => SEVERITY_RADIUS[d.severity], + radiusScale: pulseScale, + radiusMinPixels: 6, + radiusMaxPixels: 28, + stroked: true, + filled: false, + getLineColor: (d) => + d.severity === 'critical' + ? [255, 50, 50, 120] as RGBA + : [255, 150, 0, 100] as RGBA, + lineWidthMinPixels: 1.5, + pickable: false, + updateTriggers: { radiusScale: pulseTime }, + }), + ); + } + + // 3. Severity badge labels at higher zoom + const critical = alerts.filter((a) => a.severity === 'critical'); + if (critical.length > 0) { + layers.push( + new TextLayer({ + id: 'gatra-alerts-labels', + data: critical, + getText: (d) => d.mitreId, + getPosition: (d) => [d.lon, d.lat], + getColor: [255, 255, 255, 255], + getSize: 11, + getPixelOffset: [0, -16], + background: true, + getBackgroundColor: [220, 38, 38, 200] as RGBA, + backgroundPadding: [4, 2, 4, 2], + pickable: false, + fontFamily: 'system-ui, sans-serif', + fontWeight: 700, + }), + ); + } + + return layers; +} diff --git a/src/panels/gatra-soc-panel.ts b/src/panels/gatra-soc-panel.ts new file mode 100644 index 00000000..6bf59a7f --- /dev/null +++ b/src/panels/gatra-soc-panel.ts @@ -0,0 +1,312 @@ +/** + * GatraSOCPanel โ€” enhanced GATRA SOC integration dashboard panel. + * + * Renders: + * 1. Agent status indicators (5 agents with health dots) + * 2. Active incident count and mean time to respond + * 3. Live alert feed with severity coloring + * 4. TAA threat analysis section (actor, campaign, kill chain) + * 5. CRA response actions with status badges + * 6. Correlation section linking World Monitor events to GATRA alerts + * + * Pulls data from the GATRA connector on a 60s refresh cycle. + */ + +import { Panel } from '@/components/Panel'; +import { escapeHtml } from '@/utils/sanitize'; +import { refreshGatraData } from '@/gatra/connector'; +import type { + GatraAlert, + GatraAgentStatus, + GatraIncidentSummary, + GatraCRAAction, + GatraTAAAnalysis, + GatraCorrelation, +} from '@/types'; + +// โ”€โ”€ Severity โ†’ color mapping โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const SEV_COLORS: Record = { + critical: '#ef4444', + high: '#f97316', + medium: '#eab308', + low: '#3b82f6', +}; + +const AGENT_STATUS_COLORS: Record = { + online: '#22c55e', + processing: '#eab308', + degraded: '#ef4444', +}; + +const ACTION_TYPE_LABELS: Record = { + ip_blocked: 'IP Blocked', + endpoint_isolated: 'Endpoint Isolated', + credential_rotated: 'Credential Rotated', + playbook_triggered: 'Playbook', + rule_pushed: 'Rule Pushed', + rate_limited: 'Rate Limited', +}; + +const KILL_CHAIN_LABELS: Record = { + reconnaissance: 'Recon', + weaponization: 'Weapon', + delivery: 'Delivery', + exploitation: 'Exploit', + installation: 'Install', + c2: 'C2', + actions: 'Actions', +}; + +// โ”€โ”€ Panel class โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export class GatraSOCDashboardPanel extends Panel { + private alerts: GatraAlert[] = []; + private agentStatus: GatraAgentStatus[] = []; + private summary: GatraIncidentSummary | null = null; + private craActions: GatraCRAAction[] = []; + private taaAnalyses: GatraTAAAnalysis[] = []; + private correlations: GatraCorrelation[] = []; + private loading = false; + + constructor() { + super({ + id: 'gatra-soc', + title: 'GATRA SOC', + showCount: true, + trackActivity: true, + infoTooltip: 'GATRA AI-Driven SOC โ€” 5-agent pipeline monitoring IOH infrastructure. Data refreshes every 60 s.', + }); + } + + /** Called by App on a 60 s interval. */ + public async refresh(): Promise { + if (this.loading) return; + this.loading = true; + + try { + const snap = await refreshGatraData(); + + this.alerts = snap.alerts; + this.agentStatus = snap.agents; + this.summary = snap.summary; + this.craActions = snap.craActions; + this.taaAnalyses = snap.taaAnalyses; + this.correlations = snap.correlations; + + this.setCount(snap.alerts.length); + this.setDataBadge('live', `${snap.alerts.length} alerts`); + this.render(); + } catch (err) { + console.error('[GatraSOCDashboardPanel] refresh error:', err); + this.showError('Failed to load GATRA data'); + } finally { + this.loading = false; + } + } + + /** Expose alerts for the map layer. */ + public getAlerts(): GatraAlert[] { + return this.alerts; + } + + // โ”€โ”€ Rendering โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private render(): void { + const html = [ + this.renderAgentStatusBar(), + this.renderStatsRow(), + this.renderAlertFeed(), + this.renderTAASection(), + this.renderCRASection(), + this.renderCorrelation(), + ].join(''); + + this.setContent(html); + } + + // โ”€โ”€ Agent status bar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private renderAgentStatusBar(): string { + if (this.agentStatus.length === 0) return ''; + + const dots = this.agentStatus + .map((a) => { + const color = AGENT_STATUS_COLORS[a.status] || '#6b7280'; + const title = escapeHtml(`${a.fullName} โ€” ${a.status} (${this.timeAgo(a.lastHeartbeat)})`); + return ` + + ${escapeHtml(a.name)} + `; + }) + .join(''); + + return `
+ Agents + ${dots} +
+ `; + } + + // โ”€โ”€ Stats row โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private renderStatsRow(): string { + if (!this.summary) return ''; + const s = this.summary; + const stat = (label: string, value: string | number, color?: string) => + `
+
${value}
+
${label}
+
`; + + const mttrColor = s.mttrMinutes <= 15 ? '#22c55e' : s.mttrMinutes <= 30 ? '#eab308' : '#ef4444'; + + return `
+ ${stat('Active', s.activeIncidents, s.activeIncidents > 5 ? '#ef4444' : undefined)} + ${stat('MTTR', s.mttrMinutes + 'm', mttrColor)} + ${stat('24h Alerts', s.alerts24h)} + ${stat('24h Resp', s.responses24h)} +
`; + } + + // โ”€โ”€ Alert feed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private renderAlertFeed(): string { + if (this.alerts.length === 0) return '
No alerts
'; + + const rows = this.alerts.slice(0, 20).map((a) => { + const sevColor = SEV_COLORS[a.severity] || '#6b7280'; + const ts = this.timeAgo(a.timestamp); + + return `
+ ${a.severity.toUpperCase()} +
+
+ ${escapeHtml(a.mitreId)} โ€” ${escapeHtml(a.mitreName)} + ${ts} +
+
${escapeHtml(a.description)}
+
${escapeHtml(a.locationName)} ยท ${escapeHtml(a.infrastructure)} ยท ${a.confidence}% ยท ${escapeHtml(a.agent)}
+
+
`; + }).join(''); + + return `
+
Alert Feed
+ ${rows} +
`; + } + + // โ”€โ”€ TAA Analysis section โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private renderTAASection(): string { + if (this.taaAnalyses.length === 0) return ''; + + const rows = this.taaAnalyses.slice(0, 6).map((t) => { + const phaseLabel = KILL_CHAIN_LABELS[t.killChainPhase] || t.killChainPhase; + const phaseColor = this.killChainColor(t.killChainPhase); + + return `
+
+ ${escapeHtml(t.actorAttribution)} + ${phaseLabel} +
+
${escapeHtml(t.campaign)} ยท ${t.confidence}% confidence
+
IOCs: ${t.iocs.map(i => escapeHtml(i)).join(', ')}
+
`; + }).join(''); + + return `
+
TAA Threat Analysis
+ ${rows} +
`; + } + + // โ”€โ”€ CRA Response section โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private renderCRASection(): string { + if (this.craActions.length === 0) return ''; + + const rows = this.craActions.slice(0, 8).map((c) => { + const statusColor = c.success ? '#22c55e' : '#ef4444'; + const statusLabel = c.success ? 'OK' : 'FAIL'; + const typeLabel = ACTION_TYPE_LABELS[c.actionType] || c.actionType; + + return `
+ ${statusLabel} + ${escapeHtml(typeLabel)} +
${escapeHtml(c.action)}
+ ${this.timeAgo(c.timestamp)} +
`; + }).join(''); + + return `
+
CRA Response Actions
+ ${rows} +
`; + } + + // โ”€โ”€ Correlation insights โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private renderCorrelation(): string { + if (this.correlations.length === 0) { + // Fallback static insights + const staticInsights = [ + 'GATRA CRA automated containment actions โ€” MTTR improved 35% vs. manual SOC baseline.', + ]; + const rows = staticInsights.map(text => + `
+ ${escapeHtml(text)} +
` + ).join(''); + + return `
+
Correlation Insights
+ ${rows} +
`; + } + + const rows = this.correlations.map((c) => { + const sevColor = SEV_COLORS[c.severity] || '#6b7280'; + const typeIcon = c.worldMonitorEventType === 'cii_spike' ? '📈' + : c.worldMonitorEventType === 'apt_activity' ? '🕵' + : c.worldMonitorEventType === 'geopolitical' ? '🌎' + : '🔒'; + + return `
+
+ ${typeIcon} + ${escapeHtml(c.region)} +
+
${escapeHtml(c.summary)}
+
`; + }).join(''); + + return `
+
Correlation Insights
+ ${rows} +
`; + } + + // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + private timeAgo(date: Date): string { + const ms = Date.now() - date.getTime(); + if (ms < 60_000) return 'just now'; + if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`; + if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ago`; + return `${Math.floor(ms / 86_400_000)}d ago`; + } + + private killChainColor(phase: string): string { + const colors: Record = { + reconnaissance: '#6366f1', + weaponization: '#8b5cf6', + delivery: '#a855f7', + exploitation: '#d946ef', + installation: '#ec4899', + c2: '#f43f5e', + actions: '#ef4444', + }; + return colors[phase] || '#6b7280'; + } +} diff --git a/src/services/gatra.ts b/src/services/gatra.ts new file mode 100644 index 00000000..b42b8b51 --- /dev/null +++ b/src/services/gatra.ts @@ -0,0 +1,290 @@ +/** + * GATRA SOC Mock Data Connector + * + * Provides mock data simulating GATRA's 5-agent pipeline: + * ADA - Anomaly Detection Agent + * TAA - Triage & Analysis Agent + * CRA - Containment & Response Agent + * CLA - Continuous Learning Agent + * RVA - Reporting & Visualization Agent + * + * Mock data uses realistic Indonesian locations and IOH infrastructure refs. + * Data is stable within 5-minute time buckets and regenerated with slight + * randomization per call. Will be replaced by real GATRA API feeds via Pub/Sub. + */ + +import type { + GatraAlert, + GatraAgentStatus, + GatraIncidentSummary, + GatraCRAAction, + GatraAlertSeverity, + GatraAgentName, + GatraAgentStatusType, + GatraTAAAnalysis, + GatraCorrelation, + KillChainPhase, +} from '@/types'; + +// โ”€โ”€ Deterministic seed from 5-min time buckets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function timeBucketSeed(): number { + return Math.floor(Date.now() / (5 * 60 * 1000)); +} + +/** Simple seeded PRNG (mulberry32). */ +function mulberry32(seed: number): () => number { + return () => { + seed |= 0; + seed = (seed + 0x6d2b79f5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +// โ”€โ”€ Reference data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +interface Location { + name: string; + lat: number; + lon: number; +} + +const LOCATIONS: Location[] = [ + { name: 'Jakarta', lat: -6.2088, lon: 106.8456 }, + { name: 'Surabaya', lat: -7.2575, lon: 112.7521 }, + { name: 'Bandung', lat: -6.9175, lon: 107.6191 }, + { name: 'Medan', lat: 3.5952, lon: 98.6722 }, + { name: 'Makassar', lat: -5.1477, lon: 119.4327 }, +]; + +const MITRE_TECHNIQUES: Array<{ id: string; name: string }> = [ + { id: 'T1566', name: 'Phishing' }, + { id: 'T1190', name: 'Exploit Public-Facing Application' }, + { id: 'T1078', name: 'Valid Accounts' }, + { id: 'T1021', name: 'Remote Services' }, + { id: 'T1059', name: 'Command and Scripting Interpreter' }, + { id: 'T1486', name: 'Data Encrypted for Impact' }, +]; + +const IOH_INFRA: string[] = [ + 'IOH-CORE-JKT-01', + 'IOH-EDGE-SBY-03', + 'IOH-GW-BDG-02', + 'IOH-DNS-MDN-01', + 'IOH-CDN-MKS-04', + 'IOH-MPLS-JKT-02', + 'IOH-RADIUS-SBY-01', + 'IOH-FW-JKT-05', + 'IOH-LB-BDG-03', + 'IOH-VPN-MDN-02', +]; + +const ALERT_DESCRIPTIONS: string[] = [ + 'Suspicious inbound connection from known C2 infrastructure', + 'Brute-force attempt on edge authentication gateway', + 'Anomalous lateral movement detected in core network segment', + 'Credential stuffing against customer portal', + 'Encrypted payload upload to staging server', + 'DNS tunneling activity on recursive resolver', + 'Unauthorized privilege escalation on RADIUS server', + 'Port scan targeting management VLAN', + 'Malicious PowerShell execution via remote service', + 'Data exfiltration attempt over HTTPS to external IP', + 'Spear-phishing campaign targeting NOC operators', + 'Abnormal API call volume on load balancer endpoint', + 'Ransomware pre-cursor activity on file server', + 'VPN session hijack attempt detected', + 'Rogue DHCP server detected on edge segment', +]; + +const CRA_ACTIONS: Array<{ text: string; type: GatraCRAAction['actionType'] }> = [ + { text: 'Blocked IP 45.33.xx.xx at perimeter firewall', type: 'ip_blocked' }, + { text: 'Isolated host IOH-WS-042 from network', type: 'endpoint_isolated' }, + { text: 'Revoked compromised service account creds', type: 'credential_rotated' }, + { text: 'Enabled enhanced logging on MPLS segment', type: 'playbook_triggered' }, + { text: 'Triggered SOAR playbook: credential-reset', type: 'playbook_triggered' }, + { text: 'Quarantined malicious attachment in sandbox', type: 'endpoint_isolated' }, + { text: 'Rate-limited API endpoint /auth/token', type: 'rate_limited' }, + { text: 'Pushed emergency WAF rule for CVE-2024-3094', type: 'rule_pushed' }, +]; + +const THREAT_ACTORS: string[] = [ + 'APT-41 (Winnti)', + 'Lazarus Group', + 'Mustang Panda', + 'OceanLotus (APT-32)', + 'Naikon APT', + 'SideWinder', + 'Turla Group', + 'Unknown / Unattributed', +]; + +const CAMPAIGNS: string[] = [ + 'Operation ShadowNet', + 'Campaign CobaltStrike-SEA', + 'Project DarkTide', + 'Operation MalayBridge', + 'Campaign TelekomTarget', + 'Operation PacificRim', + 'Campaign IndonesiaHarvest', + 'Opportunistic Scanning', +]; + +const KILL_CHAIN_PHASES: KillChainPhase[] = [ + 'reconnaissance', 'weaponization', 'delivery', + 'exploitation', 'installation', 'c2', 'actions', +]; + +const AGENTS: GatraAgentName[] = ['ADA', 'TAA', 'CRA', 'CLA', 'RVA']; + +// โ”€โ”€ Public API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +export async function fetchGatraAlerts(): Promise { + const rng = mulberry32(timeBucketSeed()); + const count = 15 + Math.floor(rng() * 11); // 15-25 + const now = Date.now(); + const alerts: GatraAlert[] = []; + + for (let i = 0; i < count; i++) { + const loc = LOCATIONS[Math.floor(rng() * LOCATIONS.length)]!; + const technique = MITRE_TECHNIQUES[Math.floor(rng() * MITRE_TECHNIQUES.length)]!; + const sevIdx = rng(); + const severity: GatraAlertSeverity = + sevIdx < 0.15 ? 'critical' : sevIdx < 0.40 ? 'high' : sevIdx < 0.75 ? 'medium' : 'low'; + const confidence = Math.round((0.55 + rng() * 0.44) * 100); // 55-99% + + alerts.push({ + id: `gatra-${timeBucketSeed()}-${i}`, + severity, + mitreId: technique.id, + mitreName: technique.name, + description: ALERT_DESCRIPTIONS[Math.floor(rng() * ALERT_DESCRIPTIONS.length)] ?? 'Anomalous activity detected', + confidence, + lat: loc.lat + (rng() - 0.5) * 0.1, + lon: loc.lon + (rng() - 0.5) * 0.1, + locationName: loc.name, + infrastructure: IOH_INFRA[Math.floor(rng() * IOH_INFRA.length)] ?? 'IOH-UNKNOWN', + timestamp: new Date(now - Math.floor(rng() * 24 * 60 * 60 * 1000)), + agent: AGENTS[Math.floor(rng() * AGENTS.length)] ?? 'ADA', + }); + } + + // Sort by timestamp desc + alerts.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + return alerts; +} + +export async function fetchGatraAgentStatus(): Promise { + const rng = mulberry32(timeBucketSeed() + 1); + const now = new Date(); + + const fullNames: Record = { + ADA: 'Anomaly Detection Agent', + TAA: 'Triage & Analysis Agent', + CRA: 'Containment & Response Agent', + CLA: 'Continuous Learning Agent', + RVA: 'Reporting & Visualization Agent', + }; + + return AGENTS.map((name) => { + const roll = rng(); + const status: GatraAgentStatusType = + roll < 0.7 ? 'online' : roll < 0.9 ? 'processing' : 'degraded'; + return { + name, + fullName: fullNames[name], + status, + lastHeartbeat: new Date(now.getTime() - Math.floor(rng() * 120_000)), + }; + }); +} + +export async function fetchGatraIncidentSummary(): Promise { + const rng = mulberry32(timeBucketSeed() + 2); + return { + activeIncidents: 2 + Math.floor(rng() * 6), + mttrMinutes: 8 + Math.floor(rng() * 25), + alerts24h: 40 + Math.floor(rng() * 80), + responses24h: 12 + Math.floor(rng() * 30), + }; +} + +export async function fetchGatraCRAActions(): Promise { + const rng = mulberry32(timeBucketSeed() + 3); + const now = Date.now(); + const count = 4 + Math.floor(rng() * 5); // 4-8 + const actions: GatraCRAAction[] = []; + + for (let i = 0; i < count; i++) { + const entry = CRA_ACTIONS[Math.floor(rng() * CRA_ACTIONS.length)]!; + actions.push({ + id: `cra-${timeBucketSeed()}-${i}`, + action: entry.text, + actionType: entry.type, + target: IOH_INFRA[Math.floor(rng() * IOH_INFRA.length)] ?? 'IOH-UNKNOWN', + timestamp: new Date(now - Math.floor(rng() * 12 * 60 * 60 * 1000)), + success: rng() > 0.1, + }); + } + + actions.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + return actions; +} + +export async function fetchGatraTAAAnalyses(alerts: GatraAlert[]): Promise { + const rng = mulberry32(timeBucketSeed() + 4); + const analysable = alerts.filter(a => a.severity === 'critical' || a.severity === 'high'); + const count = Math.min(analysable.length, 5 + Math.floor(rng() * 4)); + + return analysable.slice(0, count).map((alert, i) => ({ + id: `taa-${timeBucketSeed()}-${i}`, + alertId: alert.id, + actorAttribution: THREAT_ACTORS[Math.floor(rng() * THREAT_ACTORS.length)] ?? 'Unknown', + campaign: CAMPAIGNS[Math.floor(rng() * CAMPAIGNS.length)] ?? 'Unknown Campaign', + killChainPhase: KILL_CHAIN_PHASES[Math.floor(rng() * KILL_CHAIN_PHASES.length)] ?? 'reconnaissance', + confidence: Math.round((0.4 + rng() * 0.55) * 100), + iocs: [ + `${Math.floor(rng() * 255)}.${Math.floor(rng() * 255)}.${Math.floor(rng() * 255)}.${Math.floor(rng() * 255)}`, + `sha256:${Array.from({ length: 8 }, () => Math.floor(rng() * 16).toString(16)).join('')}...`, + ], + timestamp: new Date(alert.timestamp.getTime() + Math.floor(rng() * 300_000)), + })); +} + +export async function fetchGatraCorrelations(alerts: GatraAlert[]): Promise { + const rng = mulberry32(timeBucketSeed() + 5); + const locationAlertMap = new Map(); + for (const a of alerts) { + const list = locationAlertMap.get(a.locationName) || []; + list.push(a); + locationAlertMap.set(a.locationName, list); + } + + const correlations: GatraCorrelation[] = []; + const templates: Array<{ type: GatraCorrelation['worldMonitorEventType']; template: (loc: string, count: number) => string }> = [ + { type: 'cii_spike', template: (loc, count) => `CII spike in ${loc} region correlates with ${count} new anomalies detected by ADA on IOH infrastructure` }, + { type: 'apt_activity', template: (loc, count) => `Elevated APT scanning activity near ${loc} NOC aligns with ${count} GATRA alerts โ€” nation-state campaign suspected` }, + { type: 'geopolitical', template: (loc, count) => `Regional geopolitical tensions around ${loc} preceded ${count} brute-force attempts on edge gateways` }, + { type: 'cyber_threat', template: (loc, count) => `WorldMonitor threat intel layer shows C2 infrastructure overlap with ${count} GATRA detections in ${loc}` }, + ]; + + let idx = 0; + for (const [loc, locAlerts] of locationAlertMap) { + if (rng() < 0.4 || idx >= 4 || locAlerts.length === 0) continue; + const tmpl = templates[Math.floor(rng() * templates.length)]!; + const alertIds = locAlerts.slice(0, 3).map(a => a.id); + correlations.push({ + id: `corr-${timeBucketSeed()}-${idx}`, + gatraAlertIds: alertIds, + worldMonitorEventType: tmpl.type, + region: loc, + summary: tmpl.template(loc, locAlerts.length), + severity: locAlerts[0]!.severity, + timestamp: new Date(), + }); + idx++; + } + + return correlations; +} diff --git a/src/types/index.ts b/src/types/index.ts index 624627bb..7b929458 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -519,6 +519,8 @@ export interface MapLayers { commodityHubs: boolean; // Gulf FDI layers gulfInvestments: boolean; + // GATRA SOC layers + gatraAlerts: boolean; } export interface AIDataCenter { @@ -1273,3 +1275,89 @@ export interface MapDatacenterCluster { plannedCount?: number; sampled?: boolean; } + +// ============================================ +// GATRA SOC TYPES +// ============================================ + +export type GatraAlertSeverity = 'critical' | 'high' | 'medium' | 'low'; +export type GatraAgentName = 'ADA' | 'TAA' | 'CRA' | 'CLA' | 'RVA'; +export type GatraAgentStatusType = 'online' | 'processing' | 'degraded'; + +export interface GatraAlert { + id: string; + severity: GatraAlertSeverity; + mitreId: string; + mitreName: string; + description: string; + confidence: number; + lat: number; + lon: number; + locationName: string; + infrastructure: string; + timestamp: Date; + agent: GatraAgentName; +} + +export interface GatraAgentStatus { + name: GatraAgentName; + fullName: string; + status: GatraAgentStatusType; + lastHeartbeat: Date; +} + +export interface GatraIncidentSummary { + activeIncidents: number; + mttrMinutes: number; + alerts24h: number; + responses24h: number; +} + +export interface GatraCRAAction { + id: string; + action: string; + actionType: 'ip_blocked' | 'endpoint_isolated' | 'credential_rotated' | 'playbook_triggered' | 'rule_pushed' | 'rate_limited'; + target: string; + timestamp: Date; + success: boolean; +} + +export type KillChainPhase = + | 'reconnaissance' + | 'weaponization' + | 'delivery' + | 'exploitation' + | 'installation' + | 'c2' + | 'actions'; + +export interface GatraTAAAnalysis { + id: string; + alertId: string; + actorAttribution: string; + campaign: string; + killChainPhase: KillChainPhase; + confidence: number; + iocs: string[]; + timestamp: Date; +} + +export interface GatraCorrelation { + id: string; + gatraAlertIds: string[]; + worldMonitorEventType: 'geopolitical' | 'cyber_threat' | 'cii_spike' | 'apt_activity'; + region: string; + summary: string; + severity: GatraAlertSeverity; + timestamp: Date; +} + +export interface GatraConnectorSnapshot { + alerts: GatraAlert[]; + agents: GatraAgentStatus[]; + summary: GatraIncidentSummary; + craActions: GatraCRAAction[]; + taaAnalyses: GatraTAAAnalysis[]; + correlations: GatraCorrelation[]; + lastRefresh: Date; +} From 224f7d46b2d7a1267ea9ff862828f2719b5150e9 Mon Sep 17 00:00:00 2001 From: ghifiardi Date: Tue, 24 Feb 2026 12:01:19 +0700 Subject: [PATCH 2/2] Wire GATRA SOC panel into App and MapContainer Instantiate GatraSOCDashboardPanel for cyber variant, add loadGatraData() method with 60s refresh cycle, wire gatraAlerts layer toggle, and add setGatraAlerts() proxy in MapContainer. Also adds cyber variant to the header switcher and enables local variant switching on localhost. Co-Authored-By: Claude Opus 4.6 --- src/App.ts | 62 +++++++++++++++++++++++++++++----- src/components/MapContainer.ts | 7 ++++ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/App.ts b/src/App.ts index 35a943a5..b6b519f9 100644 --- a/src/App.ts +++ b/src/App.ts @@ -82,6 +82,8 @@ import { InvestmentsPanel, LanguageSelector, } from '@/components'; +import { GatraSOCDashboardPanel } from '@/panels/gatra-soc-panel'; +import { refreshGatraData } from '@/gatra/connector'; import type { SearchResult } from '@/components/SearchModal'; import { collectStoryData } from '@/services/story-data'; import { renderStoryToCanvas } from '@/services/story-renderer'; @@ -196,6 +198,7 @@ export class App { private pendingDeepLinkCountry: string | null = null; private briefRequestToken = 0; private readonly isDesktopApp = isDesktopRuntime(); + private readonly isLocalDev = typeof window !== 'undefined' && window.location.hostname === 'localhost'; private readonly UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours private updateCheckIntervalId: ReturnType | null = null; private clockIntervalId: ReturnType | null = null; @@ -1833,33 +1836,43 @@ export class App { this.container.innerHTML = `
+ ${ /* Variant switcher โ€” on localhost/desktop, switch locally via localStorage */ ''} v${__APP_VERSION__}${BETA_MODE ? 'BETA' : ''} @@ -2375,6 +2388,12 @@ export class App { this.panels['etf-flows'] = new ETFFlowsPanel(); this.panels['stablecoins'] = new StablecoinPanel(); + // GATRA SOC Panel (cyber variant) + if (SITE_VARIANT === 'cyber') { + const gatraPanel = new GatraSOCDashboardPanel(); + this.panels['gatra-soc'] = gatraPanel; + } + // AI Insights Panel (desktop only - hides itself on mobile) const insightsPanel = new InsightsPanel(); this.panels['insights'] = insightsPanel; @@ -2687,8 +2706,8 @@ export class App { // Sources modal this.setupSourcesModal(); - // Variant switcher: switch variant locally on desktop (reload with new config) - if (this.isDesktopApp) { + // Variant switcher: switch variant locally on desktop or localhost (reload with new config) + if (this.isDesktopApp || this.isLocalDev) { this.container.querySelectorAll('.variant-option').forEach(link => { link.addEventListener('click', (e) => { const variant = link.dataset.variant; @@ -3174,6 +3193,11 @@ export class App { tasks.push({ name: 'techReadiness', task: runGuarded('techReadiness', () => (this.panels['tech-readiness'] as TechReadinessPanel)?.refresh()) }); } + // GATRA SOC data (cyber variant) + if (SITE_VARIANT === 'cyber' && this.mapLayers.gatraAlerts) { + tasks.push({ name: 'gatra', task: runGuarded('gatra', () => this.loadGatraData()) }); + } + // Use allSettled to ensure all tasks complete and search index always updates const results = await Promise.allSettled(tasks.map(t => t.task)); @@ -3209,6 +3233,9 @@ export class App { case 'cyberThreats': await this.loadCyberThreats(); break; + case 'gatraAlerts': + await this.loadGatraData(); + break; case 'ais': await this.loadAisSignals(); break; @@ -4088,6 +4115,20 @@ export class App { } } + private async loadGatraData(): Promise { + try { + const snap = await refreshGatraData(); + const gatraPanel = this.panels['gatra-soc'] as GatraSOCDashboardPanel | undefined; + gatraPanel?.refresh(); + if (this.mapLayers.gatraAlerts) { + this.map?.setGatraAlerts(snap.alerts); + } + dataFreshness.recordUpdate('gatra' as DataSourceId, snap.alerts.length); + } catch (error) { + console.error('[App] GATRA data load failed:', error); + } + } + private async loadAisSignals(): Promise { try { const { disruptions, density } = await fetchAisSignals(); @@ -4668,5 +4709,10 @@ export class App { this.cyberThreatsCache = null; return this.loadCyberThreats(); }, 10 * 60 * 1000, () => CYBER_LAYER_ENABLED && this.mapLayers.cyberThreats); + + // GATRA SOC data (60s refresh, cyber variant only) + if (SITE_VARIANT === 'cyber') { + this.scheduleRefresh('gatra', () => this.loadGatraData(), 60 * 1000, () => this.mapLayers.gatraAlerts); + } } } diff --git a/src/components/MapContainer.ts b/src/components/MapContainer.ts index d488da35..62d6ab09 100644 --- a/src/components/MapContainer.ts +++ b/src/components/MapContainer.ts @@ -25,6 +25,7 @@ import type { UcdpGeoEvent, CyberThreat, CableHealthRecord, + GatraAlert, } from '@/types'; import type { AirportDelayAlert } from '@/services/aviation'; import type { DisplacementFlow } from '@/services/displacement'; @@ -325,6 +326,12 @@ export class MapContainer { } } + public setGatraAlerts(alerts: GatraAlert[]): void { + if (this.useDeckGL) { + this.deckGLMap?.setGatraAlerts(alerts); + } + } + public setNewsLocations(data: Array<{ lat: number; lon: number; title: string; threatLevel: string; timestamp?: Date }>): void { if (this.useDeckGL) { this.deckGLMap?.setNewsLocations(data);