Skip to main content
The UTMStack frontend is a sophisticated single-page application (SPA) built with Angular 7.2.0. It provides security analysts and administrators with an intuitive interface for threat detection, investigation, and response.

Technology Stack

Core Framework

  • Angular: 7.2.0
  • TypeScript: 3.2.2
  • RxJS: 6.3.3 (reactive programming)
  • Angular CLI: 7.3.6

UI Components & Styling

  • Bootstrap: 4.3.1 (responsive layout)
  • ng-bootstrap: 4.1.0 (Bootstrap components for Angular)
  • Font Awesome: 4.7.0 (icons)
  • Animate.css: 3.7.0 (animations)
  • Magic Check: 1.0.3 (custom checkboxes/radios)

Data Visualization

{
  "echarts": "4.4.0",
  "echarts-gl": "1.1.1",
  "echarts-leaflet": "1.1.0",
  "echarts-stat": "1.1.1",
  "echarts-wordcloud": "1.1.3",
  "ngx-echarts": "4.1.1",
  "leaflet": "1.6.0",
  "leaflet.heat": "0.2.0"
}

Additional Features

  • Monaco Editor: 0.20.0 (code editor for queries)
  • ngx-json-viewer: 2.4.0 (JSON visualization)
  • moment: 2.29.4 (date/time handling)
  • html2canvas: 1.0.0-rc.1 (screenshot capture)
  • jsPDF: 2.3.1 (PDF generation)

Application Architecture

Key Features

1. Interactive Dashboards

Dashboard Grid System:
import { GridsterConfig, GridsterItem } from 'angular-gridster2';

export class DashboardComponent implements OnInit {
  options: GridsterConfig;
  dashboard: Array<GridsterItem>;
  
  ngOnInit() {
    this.options = {
      gridType: GridType.Fit,
      displayGrid: DisplayGrid.OnDragAndResize,
      pushItems: true,
      draggable: {
        enabled: true
      },
      resizable: {
        enabled: true
      }
    };
    
    this.loadDashboardWidgets();
  }
  
  loadDashboardWidgets() {
    this.dashboardService.getUserDashboard().subscribe(
      (dashboard) => {
        this.dashboard = dashboard.widgets.map(widget => ({
          x: widget.x,
          y: widget.y,
          cols: widget.cols,
          rows: widget.rows,
          type: widget.type,
          config: widget.config
        }));
      }
    );
  }
}
ECharts Visualization:
import { EChartOption } from 'echarts';

export class AlertTrendWidget implements OnInit {
  chartOption: EChartOption;
  
  ngOnInit() {
    this.loadAlertTrends();
  }
  
  loadAlertTrends() {
    this.alertService.getAlertTrends('24h').subscribe(
      (data) => {
        this.chartOption = {
          title: { text: 'Alert Trends - Last 24 Hours' },
          tooltip: { trigger: 'axis' },
          xAxis: {
            type: 'category',
            data: data.timestamps
          },
          yAxis: {
            type: 'value'
          },
          series: [{
            name: 'High Severity',
            type: 'line',
            data: data.highSeverity,
            itemStyle: { color: '#d32f2f' }
          }, {
            name: 'Medium Severity',
            type: 'line',
            data: data.mediumSeverity,
            itemStyle: { color: '#f57c00' }
          }, {
            name: 'Low Severity',
            type: 'line',
            data: data.lowSeverity,
            itemStyle: { color: '#fbc02d' }
          }]
        };
      }
    );
  }
}
Search Component:
export class LogSearchComponent implements OnInit {
  searchForm: FormGroup;
  searchResults: LogEvent[] = [];
  loading = false;
  
  constructor(
    private fb: FormBuilder,
    private logService: LogSearchService
  ) {}
  
  ngOnInit() {
    this.searchForm = this.fb.group({
      query: [''],
      timeRange: ['last_24h'],
      startTime: [null],
      endTime: [null],
      sources: [[]],
      severity: [[]]
    });
  }
  
  search() {
    this.loading = true;
    const request = this.buildSearchRequest();
    
    this.logService.search(request).subscribe(
      (results) => {
        this.searchResults = results.events;
        this.loading = false;
      },
      (error) => {
        this.toastr.error('Search failed', 'Error');
        this.loading = false;
      }
    );
  }
  
  buildSearchRequest(): LogSearchRequest {
    const formValue = this.searchForm.value;
    return {
      query: formValue.query,
      startTime: this.calculateStartTime(formValue.timeRange),
      endTime: new Date().toISOString(),
      filters: {
        sources: formValue.sources,
        severity: formValue.severity
      },
      offset: 0,
      limit: 100
    };
  }
}
Monaco Editor Integration:
import { Component } from '@angular/core';

@Component({
  selector: 'app-query-editor',
  template: `
    <ngx-monaco-editor
      [options]="editorOptions"
      [(ngModel)]="query"
      (ngModelChange)="onQueryChange($event)">
    </ngx-monaco-editor>
  `
})
export class QueryEditorComponent {
  query = '';
  
  editorOptions = {
    theme: 'vs-dark',
    language: 'lucene',
    minimap: { enabled: false },
    automaticLayout: true,
    suggest: {
      showFields: true,
      showKeywords: true
    }
  };
  
  onQueryChange(query: string) {
    // Validate and emit query
    this.queryChange.emit(query);
  }
}

3. Real-Time Alert Updates

WebSocket Service:
import { Injectable } from '@angular/core';
import * as SockJS from 'sockjs-client';
import * as Stomp from 'stompjs';
import { Observable, Subject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class WebSocketService {
  private stompClient: any;
  private alertSubject = new Subject<Alert>();
  
  connect(): void {
    const socket = new SockJS('/websocket');
    this.stompClient = Stomp.over(socket);
    
    this.stompClient.connect({}, () => {
      this.stompClient.subscribe('/topic/alerts', (message) => {
        const alert = JSON.parse(message.body);
        this.alertSubject.next(alert);
      });
    });
  }
  
  getAlertStream(): Observable<Alert> {
    return this.alertSubject.asObservable();
  }
  
  disconnect(): void {
    if (this.stompClient) {
      this.stompClient.disconnect();
    }
  }
}
Alert Dashboard Component:
export class AlertDashboardComponent implements OnInit, OnDestroy {
  alerts: Alert[] = [];
  private subscription: Subscription;
  
  constructor(
    private websocketService: WebSocketService,
    private toastr: ToastrService
  ) {}
  
  ngOnInit() {
    this.loadInitialAlerts();
    this.subscribeToAlerts();
  }
  
  subscribeToAlerts() {
    this.websocketService.connect();
    
    this.subscription = this.websocketService.getAlertStream().subscribe(
      (alert) => {
        this.alerts.unshift(alert);
        this.showAlertNotification(alert);
      }
    );
  }
  
  showAlertNotification(alert: Alert) {
    this.toastr.warning(
      alert.description,
      `New ${alert.severity} Alert`,
      {
        timeOut: 5000,
        progressBar: true,
        closeButton: true
      }
    );
  }
  
  ngOnDestroy() {
    this.subscription?.unsubscribe();
    this.websocketService.disconnect();
  }
}

4. Geographic Visualization

Leaflet Map Integration:
import * as L from 'leaflet';
import 'leaflet.heat';

export class ThreatMapComponent implements OnInit {
  private map: L.Map;
  private heatLayer: any;
  
  ngOnInit() {
    this.initializeMap();
    this.loadThreatData();
  }
  
  initializeMap() {
    this.map = L.map('threat-map').setView([20, 0], 2);
    
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors'
    }).addTo(this.map);
  }
  
  loadThreatData() {
    this.threatService.getThreatLocations().subscribe(
      (threats) => {
        const heatData = threats.map(t => [
          t.latitude,
          t.longitude,
          t.severity // Intensity
        ]);
        
        if (this.heatLayer) {
          this.map.removeLayer(this.heatLayer);
        }
        
        this.heatLayer = (L as any).heatLayer(heatData, {
          radius: 25,
          blur: 15,
          maxZoom: 10,
          gradient: {
            0.0: 'green',
            0.5: 'yellow',
            1.0: 'red'
          }
        }).addTo(this.map);
      }
    );
  }
}

State Management

Service-Based State

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
  private preferencesSubject = new BehaviorSubject<UserPreferences>(null);
  public preferences$ = this.preferencesSubject.asObservable();
  
  loadPreferences(): void {
    const stored = localStorage.getItem('userPreferences');
    if (stored) {
      this.preferencesSubject.next(JSON.parse(stored));
    } else {
      this.loadFromServer();
    }
  }
  
  updatePreferences(preferences: Partial<UserPreferences>): void {
    const current = this.preferencesSubject.value;
    const updated = { ...current, ...preferences };
    
    localStorage.setItem('userPreferences', JSON.stringify(updated));
    this.preferencesSubject.next(updated);
    
    // Sync with server
    this.api.updatePreferences(updated).subscribe();
  }
  
  private loadFromServer(): void {
    this.api.getPreferences().subscribe(
      (preferences) => {
        this.preferencesSubject.next(preferences);
        localStorage.setItem('userPreferences', JSON.stringify(preferences));
      }
    );
  }
}

Authentication & Security

Auth Guard

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}
  
  canActivate(route: ActivatedRouteSnapshot): boolean {
    if (!this.authService.isAuthenticated()) {
      this.router.navigate(['/login']);
      return false;
    }
    
    const requiredRoles = route.data.roles as string[];
    if (requiredRoles && !this.authService.hasAnyRole(requiredRoles)) {
      this.router.navigate(['/forbidden']);
      return false;
    }
    
    return true;
  }
}

HTTP Interceptor

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.authService.getToken();
    
    if (token) {
      const cloned = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      });
      return next.handle(cloned);
    }
    
    return next.handle(req);
  }
}

Responsive Design

Mobile-First Approach

// Mobile first
.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  
  // Tablet
  @media (min-width: 768px) {
    grid-template-columns: repeat(2, 1fr);
  }
  
  // Desktop
  @media (min-width: 1200px) {
    grid-template-columns: repeat(3, 1fr);
  }
}

// Alert card
.alert-card {
  padding: 1rem;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  
  &.severity-high {
    border-left: 4px solid #d32f2f;
  }
  
  &.severity-medium {
    border-left: 4px solid #f57c00;
  }
  
  &.severity-low {
    border-left: 4px solid #fbc02d;
  }
}

Performance Optimization

Lazy Loading Modules

const routes: Routes = [
  {
    path: 'dashboards',
    loadChildren: () => import('./dashboards/dashboards.module')
      .then(m => m.DashboardsModule),
    canActivate: [AuthGuard]
  },
  {
    path: 'alerts',
    loadChildren: () => import('./alerts/alerts.module')
      .then(m => m.AlertsModule),
    canActivate: [AuthGuard]
  },
  {
    path: 'logs',
    loadChildren: () => import('./logs/logs.module')
      .then(m => m.LogsModule),
    canActivate: [AuthGuard],
    data: { roles: ['ROLE_ANALYST', 'ROLE_ADMIN'] }
  }
];

OnPush Change Detection

import { Component, ChangeDetectionStrategy, Input } from '@angular/core';

@Component({
  selector: 'app-alert-card',
  templateUrl: './alert-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AlertCardComponent {
  @Input() alert: Alert;
  
  // Component only updates when input reference changes
}

Virtual Scrolling

import { Component } from '@angular/core';
import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  selector: 'app-log-list',
  template: `
    <cdk-virtual-scroll-viewport itemSize="50" class="log-viewport">
      <div *cdkVirtualFor="let log of logs" class="log-item">
        {{ log.timestamp }} - {{ log.message }}
      </div>
    </cdk-virtual-scroll-viewport>
  `
})
export class LogListComponent {
  logs: LogEvent[] = [];
}

Internationalization

import { TranslateService } from '@ngx-translate/core';

export class AppComponent implements OnInit {
  constructor(private translate: TranslateService) {
    translate.setDefaultLang('en');
    translate.use(localStorage.getItem('language') || 'en');
  }
  
  switchLanguage(lang: string) {
    this.translate.use(lang);
    localStorage.setItem('language', lang);
  }
}

Next Steps

Backend API

Understand the API consumed by the frontend

Agent System

Learn about data collection agents

Data Flow

See how data flows to the UI

System Architecture

View the complete system design

Build docs developers (and LLMs) love