Menu

Change language

GitHub repo

Leaflet Threat Map Radar

blocksecurity

Live threat radar on free OpenStreetMap/Esri dark tiles — no API key. Attack arcs hit edge PoPs under a rotating sweep; click an origin to isolate its vector.

Installation

$npx uipkge-ng@latest add leaflet-threat-map-radar
Manifest:https://uipkge.dev/r/angular/leaflet-threat-map-radar.jsonInstalls to:src/app/components/blocks/leaflet-threat-map-radar/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ThreatOrigin
interface ThreatOrigin {
  id: string
  country: string
  countryCode: string
  volume: string
  vectorType: AttackVectorType
  mitigation: MitigationAction
  /** [lng, lat] */
  lngLat: [number, number]
  targetEdgeId: string
}
EdgePoP
interface EdgePoP {
  id: string
  code: string
  city: string
  /** [lng, lat] */
  lngLat: [number, number]
}
AttackArc
interface AttackArc {
  id: string
  originId: string
  edgeId: string
  vectorType: AttackVectorType
  /** Sampled arc path as [lng, lat][]. */
  path: [number, number][]
}

Files installed (2)

  • src/app/components/blocks/leaflet-threat-map-radar/leaflet-threat-map-radar.component.ts11.4 kB
    // Live threat radar on free OpenStreetMap/Esri dark tiles — no API key. Attack arcs hit
    // edge PoPs under a rotating sweep; click an origin to isolate its vector. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiLeafletMapComponent,
      UiLeafletMarkerComponent,
      UiLeafletPolylineComponent,
    } from '@/ui/leaflet-map/leaflet-map.component'
    
    export type AttackVectorType = 'volumetric' | 'l7' | 'botnet'
    export type MitigationAction = 'DROP' | 'CHALLENGE' | 'RATE_LIMIT'
    
    export interface ThreatOrigin {
      id: string
      country: string
      countryCode: string
      volume: string
      vectorType: AttackVectorType
      mitigation: MitigationAction
      /** [lng, lat] */
      lngLat: [number, number]
      targetEdgeId: string
    }
    
    export interface EdgePoP {
      id: string
      code: string
      city: string
      /** [lng, lat] */
      lngLat: [number, number]
    }
    
    export interface AttackArc {
      id: string
      originId: string
      edgeId: string
      vectorType: AttackVectorType
      /** Sampled arc path as [lng, lat][]. */
      path: [number, number][]
    }
    
    export const defaultOrigins: ThreatOrigin[] = [
      {
        id: 'origin-1',
        country: 'China',
        countryCode: 'CN',
        volume: '642.5K/s',
        vectorType: 'l7',
        mitigation: 'DROP',
        lngLat: [104.2, 35.9],
        targetEdgeId: 'edge-nrt',
      },
      {
        id: 'origin-2',
        country: 'Russia',
        countryCode: 'RU',
        volume: '418.2K/s',
        vectorType: 'l7',
        mitigation: 'CHALLENGE',
        lngLat: [37.6, 55.7],
        targetEdgeId: 'edge-fra',
      },
      {
        id: 'origin-3',
        country: 'Brazil',
        countryCode: 'BR',
        volume: '285.4K/s',
        vectorType: 'botnet',
        mitigation: 'RATE_LIMIT',
        lngLat: [-51.9, -14.2],
        targetEdgeId: 'edge-iad',
      },
      {
        id: 'origin-4',
        country: 'Vietnam',
        countryCode: 'VN',
        volume: '194.0K/s',
        vectorType: 'volumetric',
        mitigation: 'DROP',
        lngLat: [105.8, 21.0],
        targetEdgeId: 'edge-sin',
      },
    ]
    
    export const edgeNodes: EdgePoP[] = [
      { id: 'edge-iad', code: 'IAD', city: 'Ashburn', lngLat: [-77.45, 39.04] },
      { id: 'edge-fra', code: 'FRA', city: 'Frankfurt', lngLat: [8.68, 50.11] },
      { id: 'edge-nrt', code: 'NRT', city: 'Tokyo', lngLat: [139.69, 35.69] },
      { id: 'edge-sin', code: 'SIN', city: 'Singapore', lngLat: [103.85, 1.29] },
      { id: 'edge-gru', code: 'GRU', city: 'São Paulo', lngLat: [-46.63, -23.55] },
    ]
    
    // Curved attack arc: sample a quadratic bezier whose control point bows
    // perpendicular to the route (biased northward) for a great-circle feel.
    export function arcPath(from: [number, number], to: [number, number], bend = 0.22): [number, number][] {
      const dx = to[0] - from[0]
      const dy = to[1] - from[1]
      const dist = Math.hypot(dx, dy) || 1
      let nx = -dy / dist
      let ny = dx / dist
      if (ny < 0) {
        nx = -nx
        ny = -ny
      }
      const cx = (from[0] + to[0]) / 2 + nx * dist * bend
      const cy = (from[1] + to[1]) / 2 + ny * dist * bend
      const points: [number, number][] = []
      const steps = 32
      for (let i = 0; i <= steps; i++) {
        const t = i / steps
        const u = 1 - t
        points.push([u * u * from[0] + 2 * u * t * cx + t * t * to[0], u * u * from[1] + 2 * u * t * cy + t * t * to[1]])
      }
      return points
    }
    
    export const attackArcs: AttackArc[] = [
      {
        id: 'arc-1',
        originId: 'origin-1',
        edgeId: 'edge-nrt',
        vectorType: 'l7',
        path: arcPath([104.2, 35.9], [139.69, 35.69], 0.3),
      },
      {
        id: 'arc-2',
        originId: 'origin-1',
        edgeId: 'edge-iad',
        vectorType: 'l7',
        path: arcPath([104.2, 35.9], [-77.45, 39.04], 0.22),
      },
      {
        id: 'arc-3',
        originId: 'origin-2',
        edgeId: 'edge-fra',
        vectorType: 'l7',
        path: arcPath([37.6, 55.7], [8.68, 50.11], 0.35),
      },
      {
        id: 'arc-4',
        originId: 'origin-3',
        edgeId: 'edge-iad',
        vectorType: 'botnet',
        path: arcPath([-51.9, -14.2], [-77.45, 39.04], 0.25),
      },
      {
        id: 'arc-5',
        originId: 'origin-4',
        edgeId: 'edge-sin',
        vectorType: 'volumetric',
        path: arcPath([105.8, 21.0], [103.85, 1.29], 0.35),
      },
    ]
    
    // `hex` feeds Leaflet path options; `dot`/`ping` feed the div-icon markers.
    export const VECTOR_STYLES: Record<AttackVectorType, { hex: string; dot: string; ping: string }> = {
      volumetric: { hex: '#f43f5e', dot: 'bg-destructive', ping: 'bg-destructive' },
      l7: { hex: '#f59e0b', dot: 'bg-warning', ping: 'bg-warning' },
      botnet: { hex: '#0ea5e9', dot: 'bg-info', ping: 'bg-info' },
    }
    
    // Module-level map camera: stable reference so the map center input never re-fires.
    const MAP_CENTER: [number, number] = [10, 30]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-leaflet-threat-map-radar, [ui-leaflet-threat-map-radar]',
      standalone: true,
      host: { '[attr.class]': '"contents"' },
      imports: [
        UiBadgeComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiLeafletMapComponent,
        UiLeafletMarkerComponent,
        UiLeafletPolylineComponent,
      ],
      template: `
        <div data-slot="leaflet-threat-map-radar" [class]="rootClass">
          <div class="flex flex-wrap items-center justify-between gap-3">
            <div class="flex flex-wrap items-center gap-2">
              <h2 class="text-foreground text-lg font-semibold tracking-tight">Threat Radar</h2>
              <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium">
                Live
              </span>
            </div>
            <p class="text-muted-foreground text-xs">{{ origins().length }} origins · {{ nodes.length }} edge PoPs</p>
          </div>
    
          <div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12">
            <div ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-8">
              <div class="relative min-h-[360px] flex-1 overflow-hidden bg-zinc-950">
                <ui-leaflet-map
                  variant="dark"
                  [center]="mapCenter"
                  [zoom]="2"
                  [minZoom]="2"
                  [navigation]="false"
                  [scrollWheelZoom]="false"
                  class="pointer-events-none absolute inset-0 size-full"
                >
                  <!-- Radar sweep: a rotating conic-gradient wedge layered over the tiles, plus two static range rings. -->
                  <div class="pointer-events-none absolute inset-0 z-[500] flex items-center justify-center">
                    <div class="border-success/15 absolute size-[340px] rounded-full border"></div>
                    <div class="border-success/15 absolute size-[220px] rounded-full border"></div>
                    <div
                      class="size-[340px] animate-[spin_9s_linear_infinite] rounded-full bg-[conic-gradient(from_0deg,rgba(16,185,129,0.35),transparent_75deg)]"
                    ></div>
                  </div>
    
                  @for (arc of filteredArcs(); track arc.id) {
                    <ui-leaflet-polyline
                      [lngLatPath]="arc.path"
                      [color]="arcColor(arc)"
                      [weight]="2"
                      dashArray="6 4"
                      lineCap="round"
                      [opacity]="0.9"
                    />
                  }
    
                  @for (edge of nodes; track edge.id) {
                    <ui-leaflet-marker [lngLat]="edge.lngLat" anchor="center">
                      <div class="flex flex-col items-center gap-1">
                        <span class="bg-success block size-2.5 rotate-45 border border-white/70 shadow-xs"></span>
                        <span class="text-success font-mono text-xs font-bold">{{ edge.code }}</span>
                      </div>
                    </ui-leaflet-marker>
                  }
    
                  @for (origin of origins(); track origin.id) {
                    <ui-leaflet-marker [lngLat]="origin.lngLat" anchor="center">
                      <button
                        type="button"
                        class="pointer-events-auto relative flex flex-col items-center gap-1"
                        [attr.aria-label]="origin.country"
                        (click)="selectOrigin(origin.id)"
                      >
                        <span class="font-mono text-xs font-bold text-white">{{ origin.countryCode }}</span>
                        <span class="relative flex size-3 items-center justify-center">
                          <span [class]="pingClass(origin)"></span>
                          <span [class]="dotClass(origin)"></span>
                        </span>
                      </button>
                    </ui-leaflet-marker>
                  }
                </ui-leaflet-map>
              </div>
            </div>
    
            <div ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4">
              <div ui-card-header class="border-border border-b p-4">
                <h3 ui-card-title class="text-sm font-semibold">Origins</h3>
              </div>
              <div ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0">
                @for (origin of origins(); track origin.id) {
                  <button type="button" [class]="rowClass(origin)" (click)="selectOrigin(origin.id)">
                    <div class="min-w-0">
                      <p class="text-foreground truncate text-sm font-medium">
                        {{ origin.countryCode }} · {{ origin.country }}
                      </p>
                      <p class="text-muted-foreground font-mono text-xs tabular-nums">{{ origin.volume }}</p>
                    </div>
                    <span ui-badge [variant]="mitigationVariant(origin.mitigation)" class="shrink-0 text-xs uppercase">
                      {{ origin.mitigation.replace('_', ' ') }}
                    </span>
                  </button>
                }
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiLeafletThreatMapRadarComponent {
      @Input('origins') set originsInput(v: ThreatOrigin[] | undefined) {
        this.origins.set(v ?? defaultOrigins.map((o) => ({ ...o, lngLat: [...o.lngLat] as [number, number] })))
      }
      @Input('class') className?: string
    
      readonly origins = signal<ThreatOrigin[]>(defaultOrigins.map((o) => ({ ...o })))
      readonly selectedId = signal<string | null>(null)
      protected readonly nodes = edgeNodes
      protected readonly mapCenter = MAP_CENTER
    
      readonly filteredArcs = computed(() => {
        const sel = this.selectedId()
        if (!sel) return attackArcs
        return attackArcs.filter((arc) => arc.originId === sel)
      })
    
      get rootClass(): string {
        return cn('flex h-full min-h-0 flex-col gap-4', this.className)
      }
    
      selectOrigin(id: string): void {
        this.selectedId.update((cur) => (cur === id ? null : id))
      }
    
      mitigationVariant(action: MitigationAction): 'destructive' | 'warning' | 'info' {
        if (action === 'DROP') return 'destructive'
        if (action === 'CHALLENGE') return 'warning'
        return 'info'
      }
    
      arcColor(arc: AttackArc): string {
        return VECTOR_STYLES[arc.vectorType].hex
      }
    
      pingClass(origin: ThreatOrigin): string {
        return cn('absolute size-3 animate-ping rounded-full opacity-70', VECTOR_STYLES[origin.vectorType].ping)
      }
    
      dotClass(origin: ThreatOrigin): string {
        return cn('relative size-3 rounded-full border-2 border-white/80', VECTOR_STYLES[origin.vectorType].dot)
      }
    
      rowClass(origin: ThreatOrigin): string {
        return cn(
          'hover:bg-accent/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors',
          this.selectedId() === origin.id && 'bg-accent/60',
        )
      }
    }
    
  • src/app/components/blocks/leaflet-threat-map-radar/index.ts0.3 kB

Raw manifest:https://uipkge.dev/r/angular/leaflet-threat-map-radar.json