DEV Community

Cover image for ARGUS - Autonomous Threat Intelligence Pipeline
Ahmed El euch
Ahmed El euch

Posted on

ARGUS - Autonomous Threat Intelligence Pipeline

[!IMPORTANT]
About this Manual: This document is the definitive master guide for the ARGUS platform. It aggregates all architectural deep-dives, API specifications, multi-agent pipeline designs, and frontend systems into a single navigable resource.

πŸ“‘ Table of Contents


Part 1: Project Overview & Vision


Extracted from: README.md

ARGUS - Autonomous Threat Intelligence Pipeline πŸ“„

Agentic Reasoning for Global Unified Security Intelligence

ARGUS is a multi-agent autonomous threat intelligence pipeline that ingests Indicators of Compromise (IOCs) from free threat feeds, enriches them via APIs (VirusTotal, OTX, Shodan), correlates IOCs into campaign clusters using vector similarity (Qdrant) + graph analysis (Neo4j), maps adversary behavior to MITRE ATT&CK techniques using LLMs, predicts the adversary's next kill-chain phase, and self-validates those predictions.

Architecture

Feeds -> Ingestor -> Enrichment -> Correlation -> MITRE Mapping -> Narrative -> Feedback
  |          |            |             |               |              |           |
  +---- Redis Streams (inter-agent messaging) -------------------------------+
  |
  +---- PostgreSQL (IOCs, Campaigns, Reports, Agent State)
  +---- Neo4j (Campaign Graph)
  +---- Qdrant (Vector Embeddings)
Enter fullscreen mode Exit fullscreen mode

Quickstart

Prerequisites

  • Docker & Docker Compose
  • API keys: VirusTotal, OTX, Groq (Groq or Ollama for AI features)

Setup

## 1. Copy and configure environment πŸ“„
cp .env.example .env
## Edit .env with your API keys πŸ“„

## 2. Generate a secure API key (for dashboard access) πŸ“„
python -c "import secrets; print(secrets.token_urlsafe(32))"
## Set ARGUS_API_KEY in .env πŸ“„

## 3. Start the full stack πŸ“„
make up

## 4. Access the dashboard πŸ“„
open http://localhost:5173
Enter fullscreen mode Exit fullscreen mode

Getting your API key

After starting ARGUS, open the dashboard and click Settings (gear icon, bottom-left) β†’ Your API Key β†’ Generate. You'll get a key in argus_<hex> format.

Then use it from any code:

curl -H "X-API-Key: argus_..." http://localhost:8000/iocs
Enter fullscreen mode Exit fullscreen mode

See SETUP.md for Python, Node.js, and PowerShell examples.

Key hierarchy

Key Where Purpose
Personal key Dashboard Settings β†’ Generate Programmatic access, CI/CD
Account key Account β†’ API Keys Per-user keys with tracking (requires sign-in)
Master key .env ARGUS_API_KEY Server admin, first-time setup
JWT token Login response Dashboard session

Makefile Targets

Command Description
make up Start full stack with Docker Compose
make up-dev Start API + workers + frontend only
make up-low-mem Start with low-memory overrides
make up-prod Start with production overrides
make down Stop all services
make setup Install Python + Node dependencies
make test Run all tests
make lint Run linter
make typecheck Run mypy type checker
make backup Run database backup
make ui Start frontend dev server

Services

Service Port Description
Frontend (React) 5173 Dashboard UI
API (FastAPI) 8000 REST + WebSocket API
Redis 6379 Message broker + cache
PostgreSQL 5432 Relational data
Qdrant 6333/6334 Vector store
Neo4j 7474/7687 Graph database
Prometheus 9090 Metrics collection
Grafana 3000 Dashboards

API Endpoints

Method Path Description
GET /health Service health check
GET /iocs List IOCs (paginated)
GET /iocs/{id} Get IOC by ID
GET /campaigns List campaigns
GET /campaigns/{id} Get campaign + IOCs + prediction
GET /campaigns/{id}/graph Campaign graph data
GET /reports List threat reports
GET /reports/{id}/pdf Download report as PDF
GET /mitre/heatmap MITRE ATT&CK heatmap
GET /agents/health Agent health status
GET /metrics/summary Dashboard summary metrics
GET /feeds List threat feeds
POST /feeds/trigger/{name} Trigger feed ingestion
POST /feeds/pause/{name} Pause a feed
POST /feeds/resume/{name} Resume a feed
WS /ws/live-feed Live IOC feed stream

Authentication: set ARGUS_API_KEY in .env and pass it as X-API-Key header or Authorization: Bearer <key>.

Production Deployment

For production, use the production override:

## 1. Generate strong passwords πŸ“„
REDIS_PASSWORD=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
POSTGRES_PASSWORD=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
ARGUS_API_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")

## 2. Export and start πŸ“„
export REDIS_PASSWORD POSTGRES_PASSWORD GRAFANA_ADMIN_PASSWORD NEO4J_AUTH ARGUS_API_KEY
make up-prod
Enter fullscreen mode Exit fullscreen mode

The production override:

  • Closes all database ports (not exposed to host)
  • Removes --reload and live volume mounts
  • Sets ENVIRONMENT=production
  • Use Traefik or Nginx with Let's Encrypt for TLS termination

Project Structure

argus/
  agents/         # 7 autonomous Celery agents
  api/            # FastAPI application + routers
  config.py       # Pydantic settings (env vars)
  correlation/    # Qdrant vector indexer
  enrichment/     # API client wrappers
  feeds/          # Threat feed parsers
  frontend/       # React SPA
  llm/            # LLM router + prompt templates
  models/         # SQLAlchemy ORM models
  monitoring/     # Prometheus metrics
  normaliser/     # STIX 2.1 normalization
  reporting/      # PDF report generation
  schemas/        # Pydantic schemas
  storage/        # DB clients (Redis, Postgres, Neo4j)

tests/            # Unit + integration tests
scripts/          # Utility scripts
data/             # Grafana dashboards, Prometheus config
Enter fullscreen mode Exit fullscreen mode

License

MIT

Extracted from: PROJECT.md

PROJECT: ARGUS β€” Agentic Threat Intelligence Platform πŸ“„

Overview

ARGUS is a fully autonomous, AI-powered threat intelligence pipeline that ingests indicators of compromise (IOCs) from multiple open-source threat feeds, enriches them with external intelligence, correlates them into campaigns, maps them to the MITRE ATT&CK framework using LLMs, generates human-readable threat reports, and serves a real-time operational dashboard.

The system is designed to run as a self-contained Docker deployment with no external dependencies beyond free API keys (VirusTotal Community, OTX, Groq, etc.).


Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Threat     │───▢│  Enrichment  │───▢│  Correlation │───▢│   MITRE     │───▢│  Narrative │───▢│  Threat  β”‚
β”‚  Feeds      β”‚    β”‚  (VT, OTX,   β”‚    β”‚  (Qdrant +   β”‚    β”‚  Mapping   β”‚    β”‚  (LLM)     β”‚    β”‚  Reports β”‚
β”‚  (7 feeds)  β”‚    β”‚   Shodan...) β”‚    β”‚   Neo4j)     β”‚    β”‚  (LLM)     β”‚    β”‚            β”‚    β”‚          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β–Ό                  β–Ό                   β–Ό                  β–Ό                β–Ό                β–Ό
  stream:raw         stream:            stream:             stream:           stream:           stream:
  _iocs              enriched_iocs      correlation_        mitre_            threat_reports    community_
                                        hypotheses          annotated                           review
Enter fullscreen mode Exit fullscreen mode

Pipeline Stages

  1. Ingestion (ingestor.py) β€” Polls 7 external threat feeds every 60 seconds, normalizes IOCs to STIX 2.1, deduplicates, and publishes to Redis stream.

  2. Enrichment (enrichment.py) β€” Consumes raw IOCs, queries external APIs in parallel (VirusTotal, OTX, Shodan, URLhaus, MalwareBazaar, CIRCL CVE), computes confidence scores, geo-locates IPs.

  3. Vector Indexing (vector_indexer.py) β€” Creates 384-dimension sentence embeddings via all-MiniLM-L6-v2 and indexes IOCs in Qdrant for semantic similarity search.

  4. Correlation (correlation.py) β€” Consumes enriched IOCs, queries Qdrant for similar IOCs, creates campaigns or merges IOCs into existing campaigns. Links IOCs to campaigns in PostgreSQL, Qdrant, and Neo4j. Updates campaign aggregate fields (ioc_count, confidence, timestamps, malware family).

  5. MITRE Mapping (mitre_mapping.py) β€” Consumes correlation hypotheses, fetches campaign IOCs, builds a prompt with the full MITRE ATT&CK v15 reference, calls Groq LLM to map observed behaviors to ATT&CK techniques, validates techniques, updates campaign with MITRE data.

  6. Threat Narrative (threat_narrative.py) β€” Consumes MITRE annotations, builds a kill-chain timeline from Neo4j, calls Groq LLM to generate executive summaries and technical narratives, creates threat reports with STIX 2.1 format, generates predictions about future attack phases.

  7. Publication (publication_worker.py) β€” Publishes ready reports and IOCs every 5 minutes.


AI Agents

Agent ID Name Role LLM Used
ingestor_agent IngestorAgent Feed polling & IOC normalization None
enrichment_agent EnrichmentAgent Multi-source enrichment & confidence scoring None
correlation_agent CorrelationAgent Semantic similarity clustering & campaign creation Sentence Transformers (MiniLM)
mitre_mapping_agent MITREMappingAgent ATT&CK v15 technique identification Groq (LLaMA-3.1 8B/70B)
threat_narrative_agent ThreatNarrativeAgent Report generation & kill-chain analysis Groq (LLaMA-3.1 8B/70B)
feedback_learning_agent FeedbackLearningAgent Self-improvement from prediction outcomes Groq (LLaMA-3.1 70B)
orchestrator_agent OrchestratorAgent Health monitoring, auto-scaling, A/B testing None

Agents report health via periodic heartbeat to the agent_state table. The orchestrator monitors stream depths, feed failures, token budgets, and community review queues.


Data Stores

Service Role Port
PostgreSQL 16 IOCs, campaigns, reports, agents, users, API keys 5432
Redis 7 Stream bus (Celery), dedup cache, rate limiting 6379
Qdrant 1.9 Vector similarity search (384-dim embeddings) 6333
Neo4j 5 IOC-campaign graph, infrastructure relationships 7687
Prometheus Metrics collection 9090
Grafana Operational dashboards 3000

Threat Feeds

Feed Type URL
OTX (AlienVault) REST otx.alienvault.com
ThreatFox (abuse.ch) REST threatfox-api.abuse.ch
URLhaus (abuse.ch) REST urlhaus-api.abuse.ch
Feodo Tracker (abuse.ch) REST feodotracker.abuse.ch
CISA KEV REST cisa.gov
MISP CIRCL REST circl.lu
MalwareBazaar (abuse.ch) REST mb-api.abuse.ch

Frontend Dashboard

A React + Vite SPA served via nginx. Features:

  • Threat Globe: 3D geographic visualization of IOC origins
  • Live Feed: Real-time WebSocket stream of incoming IOCs
  • Metrics Dashboard: IOC counts, confidence trends, malware top-list, type breakdowns, time-series charts
  • Campaigns: AI-generated campaign clusters with MITRE technique heatmaps
  • Reports: LLM-generated threat intelligence reports with kill-chain timelines, predictions, and mitigations
  • Command Palette: Quick navigation (Ctrl+K)
  • Settings: API key generation/management, account settings
  • Onboarding: Guided setup wizard

The dashboard authenticates via X-API-Key header or JWT Bearer token.


API

FastAPI backend on port 8000. Key endpoints:

Method Path Description
GET /health Service health check
GET /iocs List/filter IOCs
GET /iocs/{id} Single IOC detail
GET /campaigns List campaigns with MITRE data
GET /campaigns/{id} Campaign detail + graph
GET /reports List threat reports
GET /reports/{id}/pdf Download report as PDF
GET /metrics/summary Dashboard top-level stats
GET /metrics/timeseries Time-series IOC data
GET /metrics/breakdown IOC type breakdown
GET /metrics/confidence-trend Confidence trend chart
GET /metrics/malware-top Top 10 malware families
GET /geo/heatmap Geographic IOC distribution
GET /mitre/heatmap Campaign MITRE technique heatmap
GET /agents/health Agent fleet status & stream depths
POST /feeds/trigger/{name} Manually trigger a feed
POST /feeds/pause/{name} Pause a feed
POST /feeds/resume/{name} Resume a feed
POST /auth/register Create user account
POST /auth/login Get JWT token
GET /account/api-keys List/manage API keys

Configuration

All configuration via .env file. Key settings:

ARGUS_API_KEY=...          # Master API key for dashboard auth
GROQ_API_KEY=...           # LLM inference (free tier: ~30 req/min)
VIRUSTOTAL_API_KEY=...     # VirusTotal Community (500 req/day)
OTX_API_KEY=...            # AlienVault OTX
THREATFOX_API_KEY=...      # abuse.ch (shared across ThreatFox/URLhaus/MalwareBazaar)
CAMPAIGN_MERGE_SIMILARITY_THRESHOLD=0.55  # Cosine similarity for campaign merging
CONFIDENCE_ESCALATION_THRESHOLD=0.65      # Below this β†’ community review
GROQ_TOKEN_BUDGET_PER_MINUTE=12000        # LLM token budget cap
Enter fullscreen mode Exit fullscreen mode

Deployment

## Copy env template and configure API keys πŸ“„
cp .env.example .env
## Build and launch all 11 services πŸ“„
docker compose up -d --build
## Check everything is healthy πŸ“„
curl http://localhost:8000/health
## Dashboard at http://localhost:5173 πŸ“„
Enter fullscreen mode Exit fullscreen mode

Services start in dependency order: Redis β†’ Postgres β†’ Qdrant β†’ Neo4j β†’ API β†’ Workers β†’ Beat β†’ Frontend.

Extracted from: docs/PROJECT_VISION_AND_PROBLEM_SOLVED.md

ARGUS: Project Vision, Problem Statement & Solution Overview πŸ“„

1. Project Philosophy & Vision

ARGUS (Agentic Reasoning for Global Unified Security Intelligence) is an autonomous, multi-agent cyber threat intelligence (CTI) pipeline designed to bridge the gap between raw threat indicators and actionable executive security strategy.

Named after the mythical hundred-eyed giant watchman of Greek mythology, ARGUS maintains continuous surveillance over the open threat landscape. It transforms vast streams of unrefined Indicators of Compromise (IOCs)β€”IP addresses, file hashes, domains, URLs, and vulnerability IDsβ€”into high-confidence, contextualized threat intelligence campaigns, automated kill-chain phase predictions, and executive-ready reports without human intervention.


2. The SOC & SecOps Crisis: Problems Solved by ARGUS

Traditional Security Operations Centers (SOCs) and Threat Intelligence Teams face systemic operational challenges that slow down incident response and increase organization vulnerability:

1. Alert Fatigue & Noise

  • The Challenge: Modern SOCs ingest tens of thousands of raw IOCs daily from open-source intelligence (OSINT), commercial feeds, and internal sensors. Over 80% of these alerts are noisy, duplicate, or stale.
  • ARGUS Solution: Autonomous real-time deduplication using Redis key-space hashes combined with STIX 2.1 normalization. ARGUS filters out noise before downstream processing.

2. Manual & Fragmented Enrichment

  • The Challenge: Analysts must manually query multiple external enrichment platforms (VirusTotal, AlienVault OTX, Shodan, URLhaus, CIRCL CVE) to evaluate an indicator's severity. This consumes critical time during active attacks.
  • ARGUS Solution: EnrichmentAgent executes parallel, asynchronous API queries across 6+ OSINT sources, computes dynamic confidence scores, and geo-locates IP targets automatically within milliseconds.

3. Disconnected Indicators (Lack of Campaign Context)

  • The Challenge: Viewing IOCs as isolated data points prevents security teams from identifying coordinated threat actor campaigns or persistent threats across infrastructure.
  • ARGUS Solution: CorrelationAgent uses Qdrant vector database with 384-dimensional sentence transformer embeddings (all-MiniLM-L6-v2) and Neo4j graph databases to cluster related IOCs into unified threat campaign clusters based on semantic and behavioral similarity.

4. Delayed Behavior Identification & MITRE Mapping

  • The Challenge: Mapping adversary techniques to the MITRE ATT&CK framework requires deep manual domain expertise and hours of documentation analysis.
  • ARGUS Solution: MITREMappingAgent utilizes Groq LLaMA-3.1 LLMs with full ATT&CK v15 matrix reference prompts to automatically extract observed TTPs (Tactics, Techniques, and Procedures) and tag campaigns.

5. Reactive Security Posture (Lack of Predictive Analysis)

  • The Challenge: Traditional CTI tools report what happened, but cannot anticipate what an adversary will do next in the cyber kill chain.
  • ARGUS Solution: ThreatNarrativeAgent analyzes campaign graph timelines in Neo4j to generate predictive intelligenceβ€”hypothesizing the adversary's next likely move (e.g., initial access -> lateral movement -> data exfiltration) and providing proactive mitigations.

6. Static Knowledge & Zero Self-Improvement

  • The Challenge: Threat intelligence software does not learn from past false positives or incorrect threat predictions.
  • ARGUS Solution: FeedbackLearningAgent and PredictionValidator monitor actual observed security events against previous AI predictions, self-validating outcome accuracy and dynamically tuning confidence scoring and LLM prompt weights over time.

3. High-Level System Architecture & Flow

ARGUS operates as an event-driven reactive pipeline powered by a fleet of 7 specialized autonomous agents communicating over Redis Streams:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                   ARGUS PIPELINE                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

 [ 7 OSINT Feeds ] ──▢ IngestorAgent ──▢ Redis Stream: raw_iocs
                             β”‚
                             β–Ό
                    EnrichmentAgent ──▢ Parallel API Queries (VT, OTX, Shodan, etc.)
                             β”‚
                             β–Ό
                    CorrelationAgent ──▢ Qdrant Vector Store + Neo4j Graph DB
                             β”‚
                             β–Ό
                  MITREMappingAgent ──▢ Groq LLaMA 3.1 ATT&CK v15 Reasoning
                             β”‚
                             β–Ό
               ThreatNarrativeAgent ──▢ ReportLab PDF + Kill-Chain Prediction
                             β”‚
                             β–Ό
             FeedbackLearningAgent ──▢ Ground-Truth Self-Validation & Model Tuning
Enter fullscreen mode Exit fullscreen mode

4. Key Value Proposition & Capabilities

Feature Legacy SOC / CTI ARGUS Platform
Ingestion Manual/Single feed polling Autonomous polling across 7 feeds every 60s
Data Format Proprietary CSV/JSON formats Native STIX 2.1 JSON standardization
Enrichment Analyst copy-pasting into VT Automated async multi-source querying & scoring
Clustering Static IP subnet matching 384-dim Qdrant Vector Embeddings + Neo4j Graph
MITRE ATT&CK Manual analyst lookup LLM-driven ATT&CK v15 automated mapping
Reporting Hours of manual drafting Instant AI executive summaries & downloadable PDFs
Foresight Post-incident post-mortems Next kill-chain phase predictive intelligence
Validation Static rules Ground-truth AI self-critique & feedback loops
Visualization Flat tables Interactive 3D Threat Globe & MITRE heatmaps

5. Summary

ARGUS transforms threat intelligence from a passive repository of static indicators into an active, self-learning autonomous engine. It frees SOC teams from repetitive manual lookup tasks, allowing defenders to operate at machine speed against sophisticated cyber threat actors.

Part 2: Core Architecture & Pipeline


Extracted from: docs/architecture/01_PIPELINE_AND_MULTI_AGENT_SYSTEM.md

Technical Architecture 01: Multi-Agent System & Reactive Pipeline πŸ“„

1. Executive Summary

The ARGUS core engine is designed as a multi-agent, event-driven reactive pipeline. Rather than relying on monolithic execution or monolithic cron scripts, ARGUS breaks threat intelligence processing down into specialized, autonomous AI agents managed via Celery worker pools and Redis Streams.


2. Event-Driven Messaging Architecture (Redis Streams)

Inter-agent communication relies on Redis Streams (redis-py), which act as a durable, asynchronous message bus. This decouples agents from one another, enabling independent scaling, backpressure management, and stream replay capabilities.

Redis Stream Topology

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚   IngestorAgent    β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚ Stream: raw_iocs
                            β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  EnrichmentAgent   β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚ Stream: enriched_iocs
                            β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  CorrelationAgent  β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚ Stream: correlation_hypotheses
                            β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚ MITREMappingAgent  β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚ Stream: mitre_annotated
                            β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚ThreatNarrativeAgentβ”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚ Stream: threat_reports
                            β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚ PublicationWorker  β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Stream Definitions & Message Payloads

| Stream Name | Producer Agent | Consumer Agent | Payload Description |
|-------------|────────────────|────────────────|─────────────────────|
| stream:raw_iocs | IngestorAgent | EnrichmentAgent | Raw normalized STIX 2.1 IOC objects |
| stream:enriched_iocs | EnrichmentAgent | CorrelationAgent | IOCs enriched with VT, OTX, Shodan data & score |
| stream:correlation_hypotheses | CorrelationAgent | MITREMappingAgent | Created/Updated campaign cluster metadata |
| stream:mitre_annotated | MITREMappingAgent | ThreatNarrativeAgent | Campaigns mapped with MITRE ATT&CK v15 techniques |
| stream:threat_reports | ThreatNarrativeAgent | PublicationWorker | Generated PDF reports, summaries & predictions |


3. Autonomous Agent Fleet Overview

ARGUS deploys 7 primary Celery agents defined under argus/agents/:

argus/agents/
β”œβ”€β”€ ingestor.py             # IngestorAgent
β”œβ”€β”€ enrichment.py           # EnrichmentAgent
β”œβ”€β”€ correlation.py          # CorrelationAgent
β”œβ”€β”€ mitre_mapping.py        # MITREMappingAgent
β”œβ”€β”€ threat_narrative.py     # ThreatNarrativeAgent
β”œβ”€β”€ feedback_learning.py    # FeedbackLearningAgent
β”œβ”€β”€ prediction_validator.py # PredictionValidator
β”œβ”€β”€ orchestrator.py         # OrchestratorAgent
β”œβ”€β”€ campaign_cleaner.py     # CampaignCleaner
β”œβ”€β”€ ioc_decay.py            # IOCDecayWorker
└── publication_worker.py   # PublicationWorker
Enter fullscreen mode Exit fullscreen mode

Agent Roles & Specifications

  1. IngestorAgent (ingestor.py):

    • Periodically polls 7 OSINT threat feeds every 60 seconds.
    • Normalizes feed data into STIX 2.1 JSON schema.
    • Deduplicates using Redis key-space hashes (ioc:dedup:<hash>).
    • Pushes non-duplicate IOCs to stream:raw_iocs.
  2. EnrichmentAgent (enrichment.py):

    • Consumes messages from stream:raw_iocs.
    • Executes async API calls to VirusTotal, AlienVault OTX, Shodan, URLhaus, MalwareBazaar, and CIRCL CVE.
    • Resolves IP geolocation via MaxMind / IP-API.
    • Computes composite confidence scores (0.0 to 1.0) and publishes to stream:enriched_iocs.
  3. CorrelationAgent (correlation.py):

    • Consumes stream:enriched_iocs.
    • Generates 384-dimensional vector embeddings using all-MiniLM-L6-v2.
    • Performs cosine similarity queries against Qdrant vector database.
    • Merges IOC into existing campaigns if similarity exceeds threshold (CAMPAIGN_MERGE_SIMILARITY_THRESHOLD = 0.55) or creates a new campaign.
    • Writes relationships into Neo4j graph DB and emits to stream:correlation_hypotheses.
  4. MITREMappingAgent (mitre_mapping.py):

    • Consumes stream:correlation_hypotheses.
    • Formulates prompts containing observed campaign behaviors and full MITRE ATT&CK v15 matrix reference.
    • Calls Groq LLM (LLaMA-3.1-8B/70B) to identify sub-techniques.
    • Escalates low-confidence mappings (< 0.65) to community review queue; publishes high-confidence mappings to stream:mitre_annotated.
  5. ThreatNarrativeAgent (threat_narrative.py):

    • Consumes stream:mitre_annotated.
    • Extracts kill-chain timeline graphs from Neo4j.
    • Invokes Groq LLM to draft executive summaries, technical narratives, and predictive next-phase attack hypotheses.
    • Generates downloadable PDF reports via ReportLab and publishes to stream:threat_reports.
  6. FeedbackLearningAgent & PredictionValidator (feedback_learning.py, prediction_validator.py):

    • Audits actual incoming threat events against past predictive hypotheses.
    • Evaluates prediction accuracy (Precision, Recall, True Positives).
    • Adjusts confidence weightings and system parameters dynamically over time.
  7. OrchestratorAgent (orchestrator.py):

    • System supervisor monitoring Redis stream backlog depths, agent heartbeats, API token budgets, and worker health.
    • Dynamically scales Celery worker allocation and triggers maintenance cycles.

4. Agent Health & Heartbeat Architecture

Each agent reports status every 15 seconds to the PostgreSQL agent_state table:

CREATE TABLE agent_state (
    agent_id VARCHAR(64) PRIMARY KEY,
    agent_name VARCHAR(128) NOT NULL,
    status VARCHAR(32) NOT NULL, -- "running", "degraded", "stopped"
    last_heartbeat TIMESTAMP WITH TIME ZONE NOT NULL,
    metrics JSONB DEFAULT '{}'::jsonb
);
Enter fullscreen mode Exit fullscreen mode

If last_heartbeat exceeds 60 seconds, OrchestratorAgent flags the agent as degraded or dead and triggers alerting endpoints in the API (/agents/health).


5. Resilience, Fault Tolerance & Retries

  • Redis Stream Consumer Groups: Every agent runs within a dedicated consumer group. Unacknowledged messages (XPENDING) are automatically re-claimed if a worker process crashes mid-execution.
  • Backpressure Handling: If stream backlog depth exceeds 10,000 items, OrchestratorAgent pauses non-critical feeds and increases worker concurrency.
  • Graceful Error Handling: API key rate limits and external service timeouts trigger exponential backoff retry loops without blocking downstream pipeline operations.

Extracted from: docs/architecture/02_INGESTION_AND_STIX_NORMALIZATION.md

Technical Architecture 02: Ingestion & STIX 2.1 Normalization πŸ“„

1. Executive Summary

Data ingestion forms the foundational entry point of the ARGUS threat intelligence pipeline. The IngestorAgent autonomously polls 7 public threat intelligence feeds every 60 seconds, parses diverse raw data formats (JSON, CSV, Plaintext), normalizes indicators into standard STIX 2.1 (Structured Threat Information eXpression) objects, deduplicates them, and streams them downstream.


2. Ingested Threat Intelligence Feeds

ARGUS integrates 7 free open-source threat intelligence feeds without requiring expensive enterprise subscriptions:

Feed Name Source Provider Data Format Target Threat Types
AlienVault OTX AlienVault REST JSON Malicious IPs, Hashes, Domains, URLs
ThreatFox abuse.ch REST JSON Hashes, Malicious URLs, Botnet IPs
URLhaus abuse.ch CSV / JSON Malicious Phishing & Malware URLs
Feodo Tracker abuse.ch JSON Dridex, TrickBot, QakBot C2 Botnet IPs
CISA KEV CISA JSON Known Exploited Vulnerabilities (CVEs)
MISP CIRCL CIRCL REST JSON Threat Actor OSINT & Malware IOCs
MalwareBazaar abuse.ch REST JSON Fresh Malware Samples & SHA-256 Hashes

3. Data Processing & Pipeline Stages

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ External Feeds  │───▢ β”‚ Parsers (feeds/)    │───▢ β”‚ STIX 2.1 Normalizer   β”‚
β”‚ (7 Sources)     β”‚     β”‚ Extract Raw Tokens  β”‚     β”‚ (normaliser/stix.py)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                               β”‚
                                                               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Redis Stream    β”‚ ◀───│ Redis Key-Space     β”‚ ◀───│ Schema Validation     β”‚
β”‚ (stream:raw_iocs)β”‚     β”‚ Dedup Check         β”‚     β”‚ (schemas/ioc.py)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

4. Feed Parser Architecture

Feeds are defined under argus/feeds/. Each parser inherits from an abstract base class BaseFeedParser:

class BaseFeedParser(ABC):
    @abstractmethod
    async def fetch(self) -> List[Dict[str, Any]]:
        """Fetch raw payload from feed HTTP endpoint"""
        pass

    @abstractmethod
    def parse(self, raw_data: List[Dict[str, Any]]) -> List[RawIOC]:
        """Parse raw feed payload into intermediate RawIOC schema"""
        pass
Enter fullscreen mode Exit fullscreen mode

Supported Indicator Types (IOCType Enum)

  • ipv4 / ipv6 (IP Addresses)
  • domain (Domain Names)
  • url (Uniform Resource Locators)
  • md5 / sha1 / sha256 (Cryptographic File Hashes)
  • cve (Common Vulnerabilities and Exposures)

5. STIX 2.1 Normalization Standard

To eliminate data format fragmentation, ARGUS converts every ingested indicator into official STIX 2.1 JSON specifications (argus/normaliser/stix.py).

STIX 2.1 Domain Object Mapping

{
  "type": "indicator",
  "spec_version": "2.1",
  "id": "indicator--8e2e2d80-162d-4ba2-bc57-08138b61b36d",
  "created": "2026-07-23T14:00:00.000Z",
  "modified": "2026-07-23T14:00:00.000Z",
  "name": "Malicious IP 192.0.2.1",
  "indicator_types": ["malicious-activity"],
  "pattern": "[ipv4-addr:value = '192.0.2.1']",
  "pattern_type": "stix",
  "valid_from": "2026-07-23T14:00:00.000Z",
  "confidence": 50,
  "external_references": [
    {
      "source_name": "Feodo Tracker",
      "url": "https://feodotracker.abuse.ch"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

6. High-Performance Deduplication Mechanism

Threat feeds frequently contain duplicate indicators. To avoid processing redundant indicators through resource-heavy enrichment and LLM modules, ARGUS employs a dual-stage deduplication protocol:

1. Redis Key-Space Deduplication

Before adding an IOC to the pipeline, IngestorAgent hashes the canonical indicator representation (sha256(type:value)):

dedup_key = f"ioc:dedup:{hash_val}"
is_new = await redis_client.set(dedup_key, "1", nx=True, ex=86400) # 24hr TTL
Enter fullscreen mode Exit fullscreen mode

If set(..., nx=True) returns None, the IOC was already processed in the last 24 hours and is instantly skipped.

2. Database Constraint Check

In PostgreSQL, the iocs table enforces a unique compound index on (type, value). If a duplicate bypasses Redis cache expiry, PostgreSQL handles conflict resolution via ON CONFLICT DO UPDATE to bump the indicator's last_seen timestamp and sighting_count.


7. Fault Tolerance & Feed Controls

  • Circuit Breakers: If a feed fails 3 consecutive HTTP calls (e.g., 500 Server Error), the parser enters a 15-minute cooldown state to prevent resource starvation.
  • Feed Management APIs: Feed execution can be paused, resumed, or manually triggered via API endpoints:
    • POST /feeds/trigger/{name}
    • POST /feeds/pause/{name}
    • POST /feeds/resume/{name}

Extracted from: docs/architecture/03_MULTI_SOURCE_ENRICHMENT_AND_SCORING.md

Technical Architecture 03: Multi-Source Threat Enrichment & Confidence Scoring πŸ“„

1. Executive Summary

Raw indicators (e.g., an IP address or file hash) lack actionable security context. The EnrichmentAgent automatically transforms unrefined IOCs into enriched threat intelligence objects by querying external APIs in parallel, identifying geolocation metadata, and calculating a composite confidence score.


2. Enrichment Services & Clients

Enrichment services reside under argus/enrichment/. Each integration operates as an asynchronous client wrapper:

                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                      β”‚    EnrichmentAgent    β”‚
                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚ Async Task Gathering
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό                         β–Ό                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  VirusTotal   β”‚         β”‚ AlienVault OTXβ”‚         β”‚    Shodan     β”‚
β”‚ Client        β”‚         β”‚ Client        β”‚         β”‚ Client        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                         β”‚                         β”‚
        β–Ό                         β–Ό                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   URLhaus     β”‚         β”‚ MalwareBazaar β”‚         β”‚   CIRCL CVE   β”‚
β”‚ Client        β”‚         β”‚ Client        β”‚         β”‚ Client        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Integration Matrix

Service Client File Data Extracted Target IOC Types
VirusTotal Community virustotal_client.py Detection ratios, engine tags, malicious classification Hashes, IPs, Domains, URLs
AlienVault OTX otx_client.py Pulse count, related malware families, adversary tags All IOC Types
Shodan shodan_client.py Open ports, running banners, ASN, vulnerabilities IPs
URLhaus urlhaus_client.py URL status, hosting payload, threat tags URLs, Domains
MalwareBazaar malwarebazaar_client.py Signature name, file type, YARA matches, delivery method Hashes
CIRCL CVE circl_cve_client.py CVSS score, summary, attack vector, CWE classification CVEs
GeoIP Engine geoip.py Latitude, Longitude, Country Code, City, ISP IPs

3. Asynchronous Multi-Query Engine

Enrichment queries are executed using asyncio.gather(*tasks) over httpx.AsyncClient. This architecture guarantees that querying 6 external APIs takes no longer than the slowest single API response (typically < 1.2 seconds), avoiding sequential bottleneck delays.

async def enrich_ioc(self, ioc: IOC) -> Dict[str, Any]:
    tasks = [
        self.vt_client.query(ioc),
        self.otx_client.query(ioc),
        self.shodan_client.query(ioc),
        self.geoip_engine.locate(ioc)
    ]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    return self.aggregate_enrichment(ioc, results)
Enter fullscreen mode Exit fullscreen mode

4. Geolocation Engine (geoip.py)

For IP indicators (ipv4, ipv6), geoip.py determines physical geographic origin:

  1. Primary Lookup: Local MaxMind GeoLite2 binary database (.mmdb) for offline zero-latency resolution.
  2. Fallback Lookup: HTTP REST queries to ip-api.com or ipinfo.io if local database files are unmounted.
  3. Data Emitted: Latitude, longitude, country code (ISO-2), country name, city, and Autonomous System Number (ASN).

This data directly feeds the 3D Threat Globe visualizer on the frontend dashboard.


5. Rate Limiting & Token Budget Management

External CTI APIs enforce strict request quotas (e.g., VirusTotal free tier is capped at 4 requests/min, 500 requests/day). ARGUS manages these constraints using:

  • Redis Token Bucket Limiter (rate_limiter.py): Enforces per-minute call limits using Redis sliding window algorithms.
  • Global Budget Pool (budget_pool.py): Tracks daily token consumption per API key. If a budget exhausts, queries to that provider are gracefully disabled without failing the enrichment pipeline.

6. Composite Confidence Scoring Algorithm

The EnrichmentAgent computes a standardized Composite Confidence Score ((S \in [0.0, 1.0])) for each indicator based on weighted signals:

[
S = \min\left(1.0, \sum_{i=1}^{n} w_i \cdot s_i\right)
]

Scoring Weights Breakdown

Signal Source Weight ((w_i)) Condition / Formula
VirusTotal 0.35 (\frac{\text{malicious_engines}}{\text{total_engines}})
AlienVault OTX 0.25 (\min\left(1.0, \frac{\text{pulse_count}}{5}\right))
MalwareBazaar 0.20 1.0 if signature matched else 0.0
Shodan / URLhaus 0.10 1.0 if open C2 ports or active host status
Feed Reputation 0.10 Base reliability weight of source feed

Score Classification

  • High Confidence ((S \ge 0.75)): Automatically escalated to campaign correlation.
  • Medium Confidence ((0.40 \le S < 0.75)): Normal processing pipeline.
  • Low Confidence ((S < 0.40)): Marked for observation; low priority.

Extracted from: docs/architecture/04_VECTOR_INDEXING_AND_CAMPAIGN_CLUSTERING.md

Technical Architecture 04: Vector Indexing & Semantic Campaign Clustering πŸ“„

1. Executive Summary

Traditional SIEMs cluster threat indicators using basic static matching (e.g., exact IP subnets or matching domain strings). ARGUS leverages semantic vector embeddings and high-dimensional similarity search via Qdrant Vector Database to automatically identify related IOCs and cluster them into unified threat campaign entities.


2. Embedding Generation (vector_indexer.py)

The VectorIndexer (argus/correlation/vector_indexer.py) converts structured enriched IOC metadata into high-dimensional vector representations using PyTorch and Sentence-Transformers.

Model Specs

  • Model: sentence-transformers/all-MiniLM-L6-v2
  • Embedding Dimensionality: 384 dimensions
  • Distance Metric: Cosine Similarity ((\cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| |\mathbf{B}|}))

Textual Serialization Strategy

To capture both structural and contextual properties, each IOC is serialized into a rich text prompt prior to vector encoding:

text_representation = f"""
Type: {ioc.type}
Value: {ioc.value}
Malware Family: {ioc.malware_family or 'unknown'}
Tags: {', '.join(ioc.tags)}
ASN: {ioc.asn or 'unknown'}
CVSS: {ioc.cvss_score or 'N/A'}
VirusTotal Tags: {', '.join(ioc.vt_tags)}
"""
vector = embedding_model.encode(text_representation).tolist()
Enter fullscreen mode Exit fullscreen mode

3. Qdrant Vector Database Integration

ARGUS maintains a vector collection named argus_iocs in Qdrant (port 6333):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   CorrelationAgent    β”‚ ──────▢ β”‚   Qdrant Vector Store   β”‚
β”‚   (VectorIndexer)     β”‚         β”‚   Collection: argus_iocsβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                β”‚
                                                β–Ό
                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                   β”‚  384-Dim Vector Index  β”‚
                                   β”‚  HNSW Payload Indexes  β”‚
                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Vector Payload Schema

In addition to the 384-dim vector, each Qdrant point stores payload metadata for fast filtering without SQL joins:

  • ioc_id: PostgreSQL UUID string
  • type: ipv4, domain, hash, etc.
  • campaign_id: Associated campaign UUID (if already assigned)
  • confidence: Composite confidence score
  • created_at: ISO timestamp

4. Autonomous Campaign Clustering & Merging Protocol

When CorrelationAgent receives an enriched IOC from stream:enriched_iocs:

                       Enriched IOC
                            β”‚
                            β–Ό
               Generate 384-dim Embedding
                            β”‚
                            β–Ό
              Qdrant Cosine Similarity Search
                            β”‚
             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β–Ό                             β–Ό
   Max Similarity >= 0.55        Max Similarity < 0.55
             β”‚                             β”‚
             β–Ό                             β–Ό
  Merge into Existing Campaign     Create New Threat Campaign
Enter fullscreen mode Exit fullscreen mode

Algorithm Breakdown

  1. Similarity Query: Query Qdrant for top-5 nearest neighbor IOC vectors.
  2. Threshold Evaluation:
    • Environment setting: CAMPAIGN_MERGE_SIMILARITY_THRESHOLD = 0.55.
    • If similarity >= 0.55: The incoming IOC is merged into the existing target campaign.
    • If similarity < 0.55: A new campaign cluster is created in PostgreSQL and Neo4j.
  3. Campaign Aggregation Update:
    • Recalculate campaign total ioc_count.
    • Update campaign last_activity timestamp.
    • Re-evaluate dominant malware_family tag across cluster members.
    • Recalculate campaign aggregate confidence score.

5. Indicator Time Decay Engine (ioc_decay.py)

Threat indicators deteriorate in relevance over time (e.g., command-and-control IP addresses are abandoned by attackers). ARGUS implements an automated decay engine (ioc_decay.py):

Half-Life Exponential Decay Formula

[
C(t) = C_0 \cdot e^{-\lambda t}
]

  • (C_0): Initial confidence score.
  • (t): Time elapsed in days since last sighting.
  • (\lambda): Decay constant ((\lambda = \frac{\ln(2)}{T_{half}})). Default half-life (T_{half} = 14) days.

If an indicator's decayed score drops below (0.20), it is marked as inactive in PostgreSQL and purged from active Qdrant search indices to maintain high precision.

Extracted from: docs/architecture/05_GRAPH_DATABASE_AND_INFRASTRUCTURE_MAPPING.md

Technical Architecture 05: Graph Database Architecture & Infrastructure Mapping πŸ“„

1. Executive Summary

While relational databases excel at transactional records and vector stores at similarity matching, understanding complex multi-hop adversary infrastructure requires a Graph Database. ARGUS integrates Neo4j 5 (neo4j_client.py) to map relationships between IOCs, Threat Campaigns, Malware Families, and MITRE ATT&CK Techniques.


2. Neo4j Graph Model Schema

ARGUS structures its threat graph around 5 primary Node labels and 5 Relationship types:

 (ThreatActor) ──[SPONSORS]──▢ (Campaign) ──[USES_TECHNIQUE]──▢ (AttackTechnique)
                                   β”‚
                              [HAS_IOC]
                                   β”‚
                                   β–Ό
  (IOC:Domain) ──[RESOLVES_TO]──▢ (IOC:IP) ──[HOSTS_PAYLOAD]──▢ (Malware)
Enter fullscreen mode Exit fullscreen mode

Node Labels & Attributes

Node Label Attributes Description
:IOC id, type, value, confidence, first_seen Individual threat indicators (IP, Hash, Domain)
:Campaign id, name, malware_family, ioc_count Aggregated threat campaign entity
:ThreatActor id, name, aliases, country_origin Known APT groups or adversary collectives
:AttackTechnique id, name, t_code, tactics MITRE ATT&CK technique (e.g., T1566.001)
:Malware id, name, family, type Identified malware payload or strain

Relationship Types

  • (:IOC)-[:BELONGS_TO]->(:Campaign)
  • (:IOC)-[:RESOLVES_TO]->(:IOC)
  • (:Campaign)-[:USES_TECHNIQUE]->(:AttackTechnique)
  • (:Campaign)-[:ASSOCIATED_WITH]->(:ThreatActor)
  • (:IOC)-[:HOSTS_MALWARE]->(:Malware)

3. Cypher Query Patterns & Execution

The Neo4j client wrapper (neo4j_client.py) encapsulates graph transaction queries using the async Python driver neo4j.AsyncGraphDatabase.

1. Upserting IOC & Campaign Link

MERGE (c:Campaign {id: $campaign_id})
ON CREATE SET c.name = $campaign_name, c.created_at = datetime()
MERGE (i:IOC {id: $ioc_id})
ON CREATE SET i.type = $ioc_type, i.value = $ioc_value, i.confidence = $confidence
MERGE (i)-[r:BELONGS_TO]->(c)
ON CREATE SET r.created_at = datetime()
Enter fullscreen mode Exit fullscreen mode

2. Traversing Campaign Graph for API & Dashboard

MATCH (c:Campaign {id: $campaign_id})<-[:BELONGS_TO]-(i:IOC)
OPTIONAL MATCH (c)-[:USES_TECHNIQUE]->(t:AttackTechnique)
OPTIONAL MATCH (i)-[:HOSTS_MALWARE]->(m:Malware)
RETURN c, collect(DISTINCT i) as iocs, collect(DISTINCT t) as techniques, collect(DISTINCT m) as malware
Enter fullscreen mode Exit fullscreen mode

4. Kill-Chain Timeline Extraction

The ThreatNarrativeAgent uses graph path traversal to reconstruct chronological attack timelines:

MATCH path = (c:Campaign {id: $campaign_id})<-[:BELONGS_TO]-(i:IOC)
RETURN i.first_seen AS timestamp, i.type AS ioc_type, i.value AS ioc_value
ORDER BY i.first_seen ASC
Enter fullscreen mode Exit fullscreen mode

By ordering graph nodes along their first_seen timestamps, ARGUS converts unordered raw events into a step-by-step kill-chain sequence (e.g., Domain Registration (\rightarrow) Phishing URL Deployment (\rightarrow) C2 IP Callback (\rightarrow) Malware Hash Execution).


5. Visualizing Graphs in Dashboard

The FastAPI backend exposes graph data via GET /campaigns/{id}/graph, returning a JSON structure compatible with graph rendering libraries:

{
  "nodes": [
    {"id": "camp-123", "label": "Campaign", "name": "Operation Cobalt"},
    {"id": "ioc-456", "label": "IOC", "value": "198.51.100.45", "type": "ipv4"},
    {"id": "tech-789", "label": "AttackTechnique", "name": "Spearphishing Attachment", "t_code": "T1566.001"}
  ],
  "links": [
    {"source": "ioc-456", "target": "camp-123", "type": "BELONGS_TO"},
    {"source": "camp-123", "target": "tech-789", "type": "USES_TECHNIQUE"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

This response directly powers the interactive graph visualizer on the campaign detail page.

Extracted from: docs/architecture/06_LLM_MITRE_ATTACK_MAPPING_ENGINE.md

Technical Architecture 06: LLM-Powered MITRE ATT&CK Mapping Engine πŸ“„

1. Executive Summary

Mapping technical indicators to the MITRE ATT&CK Framework (v15) requires semantic reasoning over observed threat behavior. The MITREMappingAgent leverages Large Language Models (LLMs) via Groq (LLaMA-3.1 8B/70B) or local Ollama backends to automatically analyze campaign patterns, identify sub-techniques, and generate confidence-weighted annotations.


2. LLM Router Architecture (argus/llm/)

ARGUS abstracts LLM interactions through a provider router pattern (argus/llm/router.py):

                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                       β”‚       LLMRouter         β”‚
                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚ Model Routing
             β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β–Ό                                             β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚     Groq Client     β”‚                       β”‚    Ollama Client    β”‚
  β”‚ (LLaMA-3.1-8B/70B)  β”‚                       β”‚   (Local Fallback)  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Supported Inference Providers

  1. Groq Cloud API (groq_client.py):
    • Primary provider for production inference.
    • Models: llama-3.1-70b-versatile (deep reasoning) and llama-3.1-8b-instant (high throughput).
    • High speed (~300 tokens/sec) via Groq LPU hardware acceleration.
  2. Ollama Engine (ollama_client.py):
    • Air-gapped / offline fallback client querying local instances (http://localhost:11434).

3. Prompt Engineering & Jinja2 Templates

Prompts are structured as external Jinja2 templates under argus/llm/prompts/:

  • mitre_mapping.j2: Main prompt template instructing the LLM to analyze IOCs and return structured JSON mappings against the MITRE ATT&CK v15 matrix.
  • self_critique.j2: Secondary prompt for reflection and verification of proposed technique matches.

Template Injection Flow

template = jinja_env.get_template("mitre_mapping.j2")
prompt = template.render(
    campaign_name=campaign.name,
    malware_family=campaign.malware_family,
    iocs=formatted_iocs,
    mitre_reference=mitre_v15_summary
)
Enter fullscreen mode Exit fullscreen mode

4. Structured JSON Output Enforcement

To ensure programmatic parsability without crashing downstream JSON parsers, LLM calls enforce strict JSON formatting instructions:

{
  "techniques": [
    {
      "technique_id": "T1566.001",
      "technique_name": "Spearphishing Attachment",
      "tactic": "Initial Access",
      "confidence": 0.88,
      "reasoning": "Observed malicious email attachments hosting executable payloads matching URLhaus signatures."
    },
    {
      "technique_id": "T1071.001",
      "technique_name": "Web Protocols",
      "tactic": "Command and Control",
      "confidence": 0.92,
      "reasoning": "C2 communications routed over HTTP/HTTPS to known malware distribution domains."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

5. Self-Critique & Validation Loop

Before committing techniques to the database, the agent executes a Self-Critique Cycle (self_critique.j2):

  1. Initial Generation: LLM proposes a list of MITRE technique candidate IDs.
  2. Critique Prompt: The proposal is re-submitted to the LLM with instructions: > "Review these technique mappings against the provided evidence. Are any mappings based on weak assumptions? Remove false positives."
  3. Filtering: Mappings failing the self-critique phase are pruned automatically.

6. Community Review Escalation Queue

ARGUS enforces a safety threshold (CONFIDENCE_ESCALATION_THRESHOLD = 0.65):

  • Mappings with confidence score (\ge 0.65) are approved and written to Neo4j and PostgreSQL.
  • Mappings with confidence score (< 0.65) are flagged as pending_review and routed to the Community Review Queue (/reviews) for human analyst verification.

Analysts can approve, reject, or edit proposed technique mappings directly from the dashboard UI.

Extracted from: docs/architecture/07_NARRATIVE_GENERATION_PREDICTION_AND_FEEDBACK.md

Technical Architecture 07: Narrative Generation, Predictive Intelligence & Self-Validation Loop πŸ“„

1. Executive Summary

Threat intelligence must be actionable for decision-makers as well as technical defenders. The ThreatNarrativeAgent, FeedbackLearningAgent, and PredictionValidator collaborate to generate executive-ready threat reports, predict an adversary's next attack move, and continuously evaluate prediction accuracy against observed real-world telemetry.


2. Threat Narrative Generation (threat_narrative.py)

When MITRE ATT&CK techniques are mapped to a campaign, ThreatNarrativeAgent (argus/agents/threat_narrative.py) compiles campaign context, graph timelines, and enriched IOC lists into a structured prompt using threat_narrative.j2.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Campaign Metadata         β”‚
β”‚ Graph Timeline (Neo4j)    β”‚ ──▢ ThreatNarrativeAgent ──▢ Groq LLaMA-3.1 70B
β”‚ MITRE ATT&CK Annotations  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                                  β”‚
                                                                  β–Ό
                                                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                      β”‚ Executive Summary       β”‚
                                                      β”‚ Technical Deep-Dive     β”‚
                                                      β”‚ Recommended Mitigations β”‚
                                                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

PDF Report Compilation Engine (reporting/)

ARGUS generates publication-ready PDF threat reports using ReportLab and Jinja2 HTML templates (report_pdf.html.j2):

  • Header Section: Executive summary, risk score, primary malware family, first/last seen timestamps.
  • Infrastructure Breakdown: Table of associated IP addresses, domains, and cryptographic hashes with confidence metrics.
  • MITRE Matrix Heatmap Table: Mapped tactics and technique codes.
  • Recommended Mitigations & Actionable Defense Rules: Automatically generated Sigma rules (sigma_rule.yaml.j2) and Snort signatures.

Reports are accessible via API: GET /reports/{id}/pdf.


3. Predictive Threat Intelligence Engine (prediction.j2)

Traditional security systems are purely reactive. ARGUS introduces Predictive Cyber Kill-Chain Analysis by analyzing an active campaign's progress along the 7 stages of the Cyber Kill Chain:

[1. Reconnaissance] ──▢ [2. Weaponization] ──▢ [3. Delivery] ──▢ [4. Exploitation] ──▢ [5. Installation] ──▢ [6. C2] ──▢ [7. Exfiltration]
Enter fullscreen mode Exit fullscreen mode

Prediction Prompt (prediction.j2)

The LLM evaluates observed techniques and infrastructure to predict the adversary's next probable phase:

{
  "prediction": {
    "current_stage": "Command and Control",
    "predicted_next_stage": "Actions on Objectives / Exfiltration",
    "target_asset_types": ["Database Server", "Domain Controller"],
    "predicted_techniques": ["T1041 - Exfiltration Over C2 Channel"],
    "confidence": 0.82,
    "recommended_proactive_controls": [
      "Isolate internal subnet 10.0.4.0/24",
      "Block outgoing traffic on non-standard ports to target C2 IPs"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Ground-Truth Self-Validation Loop

To prevent AI hallucination and ensure long-term reliability, predictions are submitted to an autonomous Ground-Truth Self-Validation Loop:

 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚  AI Next-Phase         β”‚
 β”‚  Attack Prediction     β”‚
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚ Saved to DB
             β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       Real-Time Incoming telemetry
 β”‚  PredictionValidator   β”‚ ◀───────────────────────────────────
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚ Evaluates Outcome (30-day window)
             β–Ό
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚ FeedbackLearningAgent  β”‚ ──▢ Adjust Confidence Weights & Prompts
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Validation Workflow

  1. Storage: When a prediction is generated, it is stored in predictions table with status pending.
  2. Monitoring: PredictionValidator (prediction_validator.py) monitors new incoming IOCs over a 30-day window.
  3. Evaluation: If incoming indicators for the campaign match predicted techniques or infrastructure types:
    • Status updated to validated_true (True Positive).
    • If 30 days elapse without matching activity, status updated to validated_false (False Positive).

5. Feedback Learning Agent (feedback_learning.py)

The FeedbackLearningAgent (feedback_learning.py) analyzes historical validation metrics:

[
\text{Precision} = \frac{\text{True Positives}}{\text{True Positives} + \text{False Positives}}
]

Dynamic Adjustments

  • High Precision (> 0.85): Increases confidence weights for matching LLM prompt configurations.
  • Low Precision (< 0.60): Automatically tightens similarity thresholds and adjusts temperature parameters for Groq LLM queries.
  • Audit Logs: Generates performance metric breakdowns displayed in the Leaderboard dashboard (/leaderboard).

Part 3: Infrastructure & Backend


Extracted from: docs/architecture/08_FASTAPI_BACKEND_AND_REALTIME_STREAMING.md

Technical Architecture 08: FastAPI Backend & Real-Time Streaming πŸ“„

1. Executive Summary

The ARGUS API server (argus/api/main.py) is built on FastAPI and Python 3.11+, providing a high-performance, asynchronous RESTful interface alongside a real-time WebSocket live stream. It acts as the central control gateway for the web dashboard, third-party SIEM integrations, and administrative tools.


2. API Architecture & Modular Routers

The API layer uses FastAPI APIRouter modularization under argus/api/routers/:

argus/api/
β”œβ”€β”€ main.py                # FastAPI Application Entrypoint & Middleware
β”œβ”€β”€ auth.py                # Authentication Logic & Key Validation
β”œβ”€β”€ rate_limiter.py        # API Rate Limiter
└── routers/
    β”œβ”€β”€ api_v1.py          # Primary IOC & Campaign Data Endpoints
    β”œβ”€β”€ auth.py            # User Login & JWT Token Management
    β”œβ”€β”€ account.py         # Account Management & User API Keys
    β”œβ”€β”€ settings.py        # System Configuration & API Key Generation
    β”œβ”€β”€ geo.py             # Geolocation Heatmap Endpoints
    β”œβ”€β”€ metrics.py         # Summary & Time-Series Analytics
    β”œβ”€β”€ community.py       # Community Review & Feedback Submission
    β”œβ”€β”€ reviews.py         # MITRE Annotation Review Management
    β”œβ”€β”€ sigma.py           # Automated Sigma Rule Generation
    β”œβ”€β”€ export.py          # STIX 2.1 & PDF Download Handlers
    β”œβ”€β”€ leaderboard.py     # AI Model Performance Metrics
    └── demo.py            # Live Pipeline Simulation Data Generator
Enter fullscreen mode Exit fullscreen mode

3. Database Persistence & Async SQLAlchemy

Data persistence is managed using PostgreSQL 16 with asyncpg drivers and SQLAlchemy 2.0 ORM models (argus/models/):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ FastAPI Request     β”‚ ──────▢ β”‚ Async SQLAlchemy 2.0    β”‚
β”‚ Handler             β”‚         β”‚ Connection Pool         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                             β”‚
                                             β–Ό
                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                β”‚ PostgreSQL 16 DB        β”‚
                                β”‚ (Tables: iocs,          β”‚
                                β”‚  campaigns, reports...) β”‚
                                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Key Database Models

  • IOCModel: Core indicator record with confidence scores, sighting counts, and JSONB enrichment payloads.
  • CampaignModel: Threat campaign cluster aggregates.
  • ReportModel: Generated executive narratives and prediction records.
  • AgentStateModel: Heartbeat and telemetry tracker for all 7 Celery agents.
  • User / APIKey: Multi-tenant user accounts and hashed API keys.

4. Multi-Tier Authentication System

ARGUS implements a robust multi-tiered authentication security architecture (auth.py):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        ARGUS AUTHENTICATION HIERARCHY                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

 1. Master API Key        Passed via `ARGUS_API_KEY` in `.env`
                          Full system admin permissions.

 2. Account API Keys      Generated in UI (`argus_<hex32>`)
                          Stored as SHA-256 hashes in DB.
                          Per-user tracking for CI/CD & integrations.

 3. JWT Bearer Tokens     Issued upon `/auth/login` HTTP POST.
                          Used for React Dashboard sessions (12hr expiry).
Enter fullscreen mode Exit fullscreen mode

Authentication Header Formats

Clients pass credentials using standard headers:

  • X-API-Key: argus_a1b2c3d4...
  • Authorization: Bearer <jwt_token_string>

5. Real-Time WebSockets (/ws/live-feed)

To stream incoming IOCs directly to the frontend without polling overhead, FastAPI manages a WebSocket connection pool:

@app.websocket("/ws/live-feed")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            # Broadcast incoming enriched IOCs from Redis Stream
            data = await redis_pubsub.get_message()
            if data:
                await websocket.send_json(data)
    except WebSocketDisconnect:
        manager.disconnect(websocket)
Enter fullscreen mode Exit fullscreen mode

6. Observability: Prometheus & Grafana

ARGUS exposes operational metrics via Prometheus format at GET /metrics (monitoring/):

  • argus_iocs_ingested_total{feed="..."}: Counter of ingested indicators per source feed.
  • argus_campaigns_created_total: Counter of generated threat campaigns.
  • argus_enrichment_latency_seconds: Histogram of external enrichment API response times.
  • argus_agent_heartbeat_timestamp{agent="..."}: Gauge of agent heartbeat timestamps.

Prometheus scrapes these metrics every 15 seconds, driving pre-configured Grafana operational dashboards (port 3000).

Extracted from: docs/architecture/10_DEVOPS_DOCKER_AND_INFRASTRUCTURE.md

Technical Architecture 10: DevOps, Docker Deployment & Operational Readiness πŸ“„

1. Executive Summary

ARGUS is built to be a self-contained, enterprise-grade cloud platform. It can be deployed in under 2 minutes using Docker Compose across local development, resource-constrained environments, or production Kubernetes/VM deployments.


2. Docker Service Topology

The ARGUS stack consists of 11 interconnected containers coordinated via docker-compose.yml:

                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                               β”‚  Frontend Container  β”‚
                               β”‚  (Nginx SPA - :5173) β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚ HTTP / WS
                                          β–Ό
                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                               β”‚    API Container     β”‚
                               β”‚   (FastAPI - :8000)  β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                  β–Ό               β–Ό               β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Redis 7     β”‚   β”‚ Postgres 16  β”‚β”‚  Qdrant 1.9  β”‚β”‚   Neo4j 5    β”‚  β”‚ Prometheus   β”‚
β”‚  Broker/Cacheβ”‚   β”‚ Relational   β”‚β”‚  Vector Storeβ”‚β”‚   Graph DB   β”‚  β”‚ Metrics      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                  β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Celery Workerβ”‚   β”‚ Celery Beat  β”‚   β”‚   Grafana    β”‚
β”‚ Agent Fleet  β”‚   β”‚ Scheduler    β”‚   β”‚  Dashboards  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Complete Service Specification

Container Service Base Image Exposed Port Internal Role
frontend node:20-alpine / nginx:alpine 5173 React SPA static web server
api python:3.11-slim 8000 FastAPI REST & WebSocket server
redis redis:7-alpine 6379 Message stream broker & dedup cache
postgres postgres:16-alpine 5432 Primary transactional relational storage
qdrant qdrant/qdrant:v1.9.0 6333 / 6334 384-dimensional vector similarity store
neo4j neo4j:5-community 7474 / 7687 Cypher threat graph database
celery_worker python:3.11-slim N/A Multi-agent task execution pool
celery_beat python:3.11-slim N/A Periodic cron task scheduler
prometheus prom/prometheus:latest 9090 Metrics scraping collector
grafana grafana/grafana:latest 3000 Operational monitoring dashboards

3. Compose Deployment Configurations

ARGUS provides specialized Docker Compose override files for diverse deployment requirements:

## Standard Local Development πŸ“„
docker compose up -d

## 1. Low-Memory Overrides (Low-spec VPS / 4GB RAM hosts) πŸ“„
docker compose -f docker-compose.yml -f docker-compose.low-memory.yml up -d

## 2. Production Overrides (Closed ports, hardened secrets, no reload) πŸ“„
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

## 3. TLS / HTTPS Overrides (Traefik / Nginx reverse proxy) πŸ“„
docker compose -f docker-compose.yml -f docker-compose.tls.yml up -d
Enter fullscreen mode Exit fullscreen mode

Low-Memory Profile Features (docker-compose.low-memory.yml)

  • Caps Java heap memory for Neo4j (NEO4J_dbms_memory_heap_initial__size=256m).
  • Disables Prometheus/Grafana monitoring containers.
  • Reduces Celery worker concurrency from 8 to 2 processes.

4. Environment Variables & Secret Hierarchy (.env)

System parameters are configured through standard environment files (.env.example):

## Master Authentication Key πŸ“„
ARGUS_API_KEY=argus_master_key_secure_random_string

## External Intelligence API Keys πŸ“„
GROQ_API_KEY=gsk_...
VIRUSTOTAL_API_KEY=...
OTX_API_KEY=...
THREATFOX_API_KEY=...

## Operational Thresholds πŸ“„
CAMPAIGN_MERGE_SIMILARITY_THRESHOLD=0.55
CONFIDENCE_ESCALATION_THRESHOLD=0.65
GROQ_TOKEN_BUDGET_PER_MINUTE=12000

## Database Passwords & Credentials πŸ“„
POSTGRES_USER=argus
POSTGRES_PASSWORD=secure_postgres_pass
POSTGRES_DB=argus_db
REDIS_PASSWORD=secure_redis_pass
NEO4J_AUTH=neo4j/secure_neo4j_pass
Enter fullscreen mode Exit fullscreen mode

5. Database Migration Management (Alembic)

Database schema evolution is managed via Alembic under alembic/:

## Generate a new migration script after changing SQLAlchemy models πŸ“„
alembic revision --autogenerate -m "add_threat_actor_table"

## Upgrade database to latest schema version πŸ“„
alembic upgrade head
Enter fullscreen mode Exit fullscreen mode

Container startup scripts (startup.sh) automatically execute alembic upgrade head before booting the API server to guarantee schema synchronization.


6. Automation & Maintenance Targets (Makefile)

The root Makefile provides standard short-hand commands:

up:
    docker compose up -d --build

up-low-mem:
    docker compose -f docker-compose.yml -f docker-compose.low-memory.yml up -d

up-prod:
    docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

down:
    docker compose down

test:
    pytest tests/ -v --cov=argus

lint:
    ruff check argus/ tests/

typecheck:
    mypy argus/

backup:
    ./scripts/backup_db.sh
Enter fullscreen mode Exit fullscreen mode

7. Production Hardening Checklist

  • [x] Database ports (5432, 6379, 6333, 7687) closed to public interface in docker-compose.prod.yml.
  • [x] API key passwords generated using CSPRNG (secrets.token_urlsafe(32)).
  • [x] Reverse Proxy / TLS termination configured via Nginx / Traefik.
  • [x] Read-only filesystem volumes mounted for static application code.
  • [x] Stream rate limiters active to prevent external DDoS vectors.

Extracted from: docs/12_BACKEND_EXTENSIONS.md

ARGUS Backend Extensions πŸ“„

API endpoints, WebSockets, and SQL required for the React Frontend


2.1 β€” WebSocket Implementation for Live Feed

The React frontend requires a real-time stream of IOCs as they are ingested by the pipeline. This is implemented via FastAPI WebSockets.

File: argus/api/websockets.py

from fastapi import APIRouter, WebSocket, WebSocketDisconnect
import asyncio
import json
import logging
from typing import Set

log = logging.getLogger(__name__)
ws_router = APIRouter()

class ConnectionManager:
    def __init__(self):
        self.active_connections: Set[WebSocket] = set()

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.add(websocket)
        log.info(f"Client connected. Total clients: {len(self.active_connections)}")

    def disconnect(self, websocket: WebSocket):
        self.active_connections.discard(websocket)
        log.info(f"Client disconnected. Total clients: {len(self.active_connections)}")

    async def broadcast(self, message: str):
        if not self.active_connections:
            return

        disconnected = set()
        for connection in self.active_connections:
            try:
                await connection.send_text(message)
            except RuntimeError:
                # Connection was closed
                disconnected.add(connection)

        for conn in disconnected:
            self.disconnect(conn)

manager = ConnectionManager()

@ws_router.websocket("/ws/live-feed")
async def live_feed_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            # We just need to keep the connection open and respond to pings
            # The actual data broadcasting is handled by the Redis stream listener
            data = await websocket.receive_text()
            if data == "ping":
                await websocket.send_text(json.dumps({"event": "pong"}))
    except WebSocketDisconnect:
        manager.disconnect(websocket)
Enter fullscreen mode Exit fullscreen mode

Redis Stream Listener Background Task

To broadcast new IOCs, we need a background task that listens to stream:enriched_iocs (or stream:raw_iocs if we want them faster, before enrichment. The frontend spec asks for malware_family and confidence, which are only available after enrichment. Therefore, listen to stream:enriched_iocs).

Add this to argus/api/main.py lifespan:

import asyncio
from argus.api.websockets import manager
from argus.config import settings
import redis.asyncio as redis

async def listen_for_iocs():
    """Background task to listen to Redis and broadcast to WebSockets"""
    client = redis.from_url(settings.redis_url)
    last_id = "$" # Only get new messages from now on

    while True:
        try:
            # Block for up to 1 second waiting for new messages
            messages = await client.xread(
                {"stream:enriched_iocs": last_id}, 
                count=10, 
                block=1000
            )

            for stream, stream_messages in messages:
                for message_id, message_data in stream_messages:
                    last_id = message_id

                    # Ensure we have clients before parsing
                    if not manager.active_connections:
                        continue

                    payload = json.loads(message_data.get(b"payload", b"{}").decode("utf-8"))

                    # Format for the frontend
                    ws_message = {
                        "event": "new_ioc",
                        "data": {
                            "ioc_id": payload.get("ioc_id"),
                            "ioc_type": payload.get("ioc_type"),
                            "value": payload.get("value"),
                            "source_feed": payload.get("source_feed"),
                            "confidence_score": payload.get("confidence_score", 0.5),
                            "malware_family": payload.get("malware_family"),
                            "ingested_at": payload.get("ingested_at")
                        }
                    }

                    await manager.broadcast(json.dumps(ws_message))

        except asyncio.CancelledError:
            break
        except Exception as e:
            log.error(f"Error in websocket listener: {e}")
            await asyncio.sleep(5) # Backoff on error

    await client.aclose()
Enter fullscreen mode Exit fullscreen mode

2.2 β€” New REST Endpoints

1. GET /api/iocs

Purpose: Fetch recent IOCs for the Live Feed fallback mechanism.

@router.get("/iocs")
async def get_iocs(
    ioc_type: str = None,
    source_feed: str = None,
    since: datetime = None,
    limit: int = 50,
    offset: int = 0,
    db: AsyncSession = Depends(get_db)
):
    query = select(IOC).order_by(desc(IOC.ingested_at))

    if ioc_type:
        query = query.where(IOC.ioc_type == ioc_type)
    if source_feed:
        query = query.where(IOC.source_feed == source_feed)
    if since:
        query = query.where(IOC.ingested_at > since)

    # Get total count
    count_query = select(func.count()).select_from(query.subquery())
    total = await db.scalar(count_query)

    # Get paginated data
    query = query.limit(limit).offset(offset)
    result = await db.execute(query)
    iocs = result.scalars().all()

    return {
        "iocs": iocs,
        "total": total,
        "limit": limit,
        "offset": offset
    }
Enter fullscreen mode Exit fullscreen mode

2. GET /api/campaigns and GET /api/campaigns/{id}

Purpose: Campaign Explorer list and detail views.

@router.get("/campaigns")
async def get_campaigns(limit: int = 50, offset: int = 0, db: AsyncSession = Depends(get_db)):
    query = select(Campaign).order_by(desc(Campaign.last_seen))

    count_query = select(func.count(Campaign.campaign_id))
    total = await db.scalar(count_query)

    query = query.limit(limit).offset(offset)
    result = await db.execute(query)
    campaigns = result.scalars().all()

    return {"campaigns": campaigns, "total": total}

@router.get("/campaigns/{campaign_id}")
async def get_campaign(campaign_id: UUID, db: AsyncSession = Depends(get_db)):
    # Load campaign with its IOCs and the latest prediction
    query = select(Campaign).options(
        selectinload(Campaign.iocs)
    ).where(Campaign.campaign_id == campaign_id)

    result = await db.execute(query)
    campaign = result.scalar_one_or_none()

    if not campaign:
        raise HTTPException(status_code=404, detail="Campaign not found")

    # Get the latest prediction separately to avoid complex joins
    pred_query = select(Prediction).where(
        Prediction.campaign_id == campaign_id
    ).order_by(desc(Prediction.created_at)).limit(1)

    pred_result = await db.execute(pred_query)
    prediction = pred_result.scalar_one_or_none()

    campaign_dict = {
        **campaign.__dict__,
        "prediction": prediction
    }
    campaign_dict.pop("_sa_instance_state", None)

    return campaign_dict
Enter fullscreen mode Exit fullscreen mode

3. GET /api/campaigns/{id}/graph

Purpose: Graph Viewer data source.

@router.get("/campaigns/{campaign_id}/graph")
async def get_campaign_graph(campaign_id: UUID, db: AsyncSession = Depends(get_db)):
    # 1. Fetch the campaign
    campaign_result = await db.execute(select(Campaign).where(Campaign.campaign_id == campaign_id))
    campaign = campaign_result.scalar_one_or_none()

    if not campaign:
        raise HTTPException(status_code=404, detail="Campaign not found")

    # 2. Fetch all IOCs belonging to this campaign
    ioc_result = await db.execute(
        select(IOC).where(IOC.campaign_id == campaign_id)
    )
    iocs = ioc_result.scalars().all()

    nodes = []
    edges = []

    # 3. Create the central Campaign node
    camp_node_id = f"camp_{campaign_id}"
    nodes.append({
        "id": camp_node_id,
        "type": "campaignNode",
        "data": {
            "label": str(campaign_id)[:8],
            "ioc_count": campaign.ioc_count
        },
        "position": {"x": 0, "y": 0} # Handled by dagre client-side
    })

    # 4. Create IOC nodes and link them to the campaign
    for ioc in iocs:
        node_id = f"ioc_{ioc.ioc_id}"
        nodes.append({
            "id": node_id,
            "type": "iocNode",
            "data": {
                "label": ioc.value,
                "ioc_type": ioc.ioc_type,
                "confidence": ioc.confidence_score,
                "malware_family": ioc.malware_family,
                "asn_org": ioc.geo_asn_org
            },
            "position": {"x": 0, "y": 0}
        })

        edges.append({
            "id": f"edge_{camp_node_id}_{node_id}",
            "source": camp_node_id,
            "target": node_id,
            "label": "CONTAINS"
        })

    # Note: In a full implementation, you would query Neo4j here to get the actual
    # relationships between IOCs (e.g. SHARES_ASN, DELIVERS) and add those as edges.

    return {"nodes": nodes, "edges": edges}
Enter fullscreen mode Exit fullscreen mode

4. GET /api/mitre/heatmap

Purpose: MITRE ATT&CK Heatmap data source.

@router.get("/mitre/heatmap")
async def get_mitre_heatmap(days: int = 30, db: AsyncSession = Depends(get_db)):
    cutoff = datetime.now(timezone.utc) - timedelta(days=days)

    # This query requires unnesting the mitre_techniques JSONB array
    # We want to count how many campaigns contain each technique
    query = text("""
        SELECT 
            t->>'technique_id' as technique_id,
            t->>'technique_name' as technique_name,
            t->>'tactic' as tactic,
            COUNT(DISTINCT c.campaign_id) as campaign_count,
            AVG((t->>'confidence')::float) as avg_confidence,
            MAX(c.last_seen) as last_seen
        FROM 
            campaigns c,
            jsonb_array_elements(c.mitre_techniques) as t
        WHERE 
            c.last_seen >= :cutoff
            AND c.mitre_techniques != '[]'::jsonb
        GROUP BY 
            1, 2, 3
        ORDER BY 
            campaign_count DESC
    """)

    result = await db.execute(query, {"cutoff": cutoff})

    techniques = []
    total_observed = 0

    for row in result:
        techniques.append({
            "technique_id": row.technique_id,
            "technique_name": row.technique_name,
            "tactic": row.tactic,
            "count": row.campaign_count,
            "avg_confidence": row.avg_confidence,
            "last_seen": row.last_seen
        })
        total_observed += row.campaign_count

    return {
        "techniques": techniques,
        "total_techniques_observed": total_observed,
        "time_range_days": days
    }
Enter fullscreen mode Exit fullscreen mode

5. GET /api/metrics/summary

Purpose: Dashboard topbar and stat cards.

@router.get("/metrics/summary")
async def get_summary_metrics(db: AsyncSession = Depends(get_db)):
    one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)

    # 1. IOCs last 24h
    iocs_24h = await db.scalar(
        select(func.count(IOC.ioc_id)).where(IOC.ingested_at >= one_day_ago)
    )

    # 2. Total IOCs
    iocs_total = await db.scalar(select(func.count(IOC.ioc_id)))

    # 3. Active feeds
    active_feeds = await db.scalar(
        select(func.count(Feed.feed_id)).where(
            Feed.is_active == True,
            Feed.consecutive_failures < 3
        )
    )

    # 4. Pending reviews
    pending_reviews = await db.scalar(
        select(func.count(ThreatReport.report_id)).where(
            ThreatReport.escalation_required == True,
            # Need a status field or rely on lack of human review event
        )
    )

    # 5. Last ingested timestamp
    last_ioc = await db.execute(
        select(IOC.ingested_at).order_by(desc(IOC.ingested_at)).limit(1)
    )
    last_ts = last_ioc.scalar_one_or_none()

    return {
        "iocs_last_24h": iocs_24h,
        "iocs_total": iocs_total,
        "active_feeds": active_feeds,
        "pending_reviews": pending_reviews,
        "last_ioc_ingested_at": last_ts
    }
Enter fullscreen mode Exit fullscreen mode

6. GET /api/agents/health

Purpose: Agent Health Monitor page.

@router.get("/agents/health")
async def get_agent_health(db: AsyncSession = Depends(get_db)):
    # 1. Get agent states from DB
    result = await db.execute(select(AgentState))
    agent_states = {state.agent_id: state for state in result.scalars().all()}

    # 2. Get Redis stream depths
    redis_client = redis.from_url(settings.redis_url)
    streams_info = {}

    stream_names = [
        "stream:raw_iocs", "stream:enriched_iocs", 
        "stream:correlation_hypotheses", "stream:mitre_annotated",
        "stream:human_review", "stream:threat_reports", "stream:feedback"
    ]

    for stream in stream_names:
        try:
            depth = await redis_client.xlen(stream)

            # Get groups to check lag
            groups = await redis_client.xinfo_groups(stream)
            max_lag = 0
            if groups:
                # Calculate lag (approximate) based on last-delivered-id
                pass # Simplified for brevity

            streams_info[stream] = {
                "name": stream,
                "depth": depth,
                "lag": max_lag,
                "dlq": 0 # Would check stream:dlq:{stream}
            }
        except redis.exceptions.ResponseError:
            # Stream might not exist yet
            streams_info[stream] = {"name": stream, "depth": 0, "lag": 0, "dlq": 0}

    await redis_client.aclose()

    # 3. Format response
    agents = []
    expected_agents = [
        "IngestorAgent", "EnrichmentAgent", "CorrelationAgent", 
        "MITREMappingAgent", "ThreatNarrativeAgent", 
        "FeedbackLearningAgent", "OrchestratorAgent"
    ]

    for agent_name in expected_agents:
        # Map agent ID to snake_case if needed
        db_id = agent_name.replace("Agent", "").lower() + "_agent"
        state = agent_states.get(db_id)

        status = "healthy"
        # Determine status based on last_updated or other logic

        agents.append({
            "agent_id": db_id,
            "name": agent_name,
            "status": status,
            "last_active": state.last_updated if state else datetime.now(timezone.utc),
            "tasks_processed": getattr(state, "predictions_made", 0) if state else 0, # Placeholder
            "reputation": getattr(state, "reputation_score", None) if state else None,
            "queue_depth": 0 # Get from related stream
        })

    return {
        "agents": agents,
        "streams": list(streams_info.values()),
        "predictions_made": sum(a["tasks_processed"] for a in agents),
        "avg_reputation": sum(a["reputation"] for a in agents if a["reputation"]) / len([a for a in agents if a["reputation"]]) if any(a["reputation"] for a in agents) else 0.7
    }
Enter fullscreen mode Exit fullscreen mode

Part 4: Frontend & UI Systems


Extracted from: docs/architecture/09_FRONTEND_DASHBOARD_AND_VISUALIZATION.md

Technical Architecture 09: Frontend Dashboard & 3D Globe Visualization πŸ“„

1. Executive Summary

The ARGUS Frontend (argus/frontend/) is a modern single-page application (SPA) built with React 18, Vite, and Tailwind CSS / Custom Glassmorphism UI Components. Designed for high-density threat surveillance, it provides real-time WebSocket data feeds, 3D geographic attack visualizations, interactive MITRE ATT&CK heatmaps, and campaign graph rendering.


2. Technology Stack & Design System

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          FRONTEND TECH STACK                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  - Framework:       React 18 (Hooks, Context, Concurrent Rendering)
  - Build Tool:      Vite 5 (Hot Module Replacement, ESM Bundling)
  - Styling:         Tailwind CSS + Vanilla Glassmorphism Token Palette
  - 3D Visualizer:   Three.js + React Three Fiber / Globe.gl
  - Graph Rendering: Force-Graph / Cytoscape
  - Icons:           Lucide React
Enter fullscreen mode Exit fullscreen mode

Design Philosophy

  • Dark Mode First: Optimized for SOC environment screens (#0B0F19 background, high-contrast neon accents).
  • Glassmorphism: Backdrop blur overlays (backdrop-filter: blur(12px)), translucent panel borders.
  • Zero Heavy UI Libraries: Built using custom light-weight components to guarantee smooth 60 FPS performance.

3. Frontend Page Architecture

The user interface is organized into distinct functional viewports (argus/frontend/src/):

argus/frontend/src/
β”œβ”€β”€ App.jsx                # Root Application Container & Providers
β”œβ”€β”€ router.jsx             # React Router v6 Page Definitions
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ layout/            # AppShell, Navigation Sidebar, Header, Status Bar
β”‚   β”œβ”€β”€ globe/             # 3D Threat Globe Component (Three.js)
β”‚   β”œβ”€β”€ iocs/              # IOC Feed Tables, Filter Controls & Modal Details
β”‚   β”œβ”€β”€ campaigns/         # Campaign Cards, Vector Similarity Graph & Details
β”‚   β”œβ”€β”€ mitre/             # Interactive MITRE ATT&CK Heatmap Matrix
β”‚   β”œβ”€β”€ reports/           # PDF Viewer, Narrative Reader & Prediction Panel
β”‚   └── settings/          # Key Management, Feed Control Switches & Profile
Enter fullscreen mode Exit fullscreen mode

4. 3D Threat Globe Visualizer

The flagship visual component of ARGUS is the interactive 3D Threat Globe (ThreatGlobe.jsx):

                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                           β”‚   WebGL / Three.js Canvas β”‚
                           β”‚   - Rotated 3D Sphere     β”‚
                           β”‚   - Landmass Textures     β”‚
                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                   β–Ό                                           β–Ό
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚ Geographic Points     β”‚                   β”‚ Animated Attack Arcs  β”‚
       β”‚ (Latitude, Longitude) β”‚                   β”‚ (Origin -> Target)    β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Key Globe Features

  • Geographic Points: Displays latitude/longitude coordinates extracted by geoip.py for ingested IP indicators.
  • Color-Coded Severity:
    • Red: High Confidence Malicious ((S \ge 0.75))
    • Amber: Medium Confidence ((0.40 \le S < 0.75))
    • Blue: Low Confidence ((S < 0.40))
  • Animated Attack Arcs: Renders bezier 3D curves showing cyber attack vectors originating from global IOC locations toward protected infrastructure.

5. Real-Time State & WebSocket Synchronization

The dashboard maintains continuous live state using a custom WebSocket hook useWebSocket:

export function useLiveFeed() {
  const [iocs, setIocs] = useState([]);

  useEffect(() => {
    const ws = new WebSocket("ws://localhost:8000/ws/live-feed");
    ws.onmessage = (event) => {
      const newIoc = JSON.parse(event.data);
      setIocs((prev) => [newIoc, ...prev.slice(0, 99)]); // Maintain top 100
    };
    return () => ws.close();
  }, []);

  return iocs;
}
Enter fullscreen mode Exit fullscreen mode

This updates table feeds, ticker metrics, and globe points instantaneously without requiring manual page reloads.


6. Interactive MITRE ATT&CK Heatmap

The MITRE matrix component (MitreHeatmap.jsx) renders a grid of 14 tactic columns (Initial Access, Execution, Persistence, etc.) and associated technique cells:

  • Intensity Heatmapping: Cell background opacity scales dynamically based on the frequency of detected technique instances across active campaigns.
  • Technique Drill-Down: Clicking a technique cell opens a detailed modal listing matched IOCs, LLM reasoning excerpts, and confidence metrics.

7. Command Palette (Ctrl+K)

For rapid keyboard navigation during incident triage, pressing Ctrl+K (or Cmd+K) triggers an overlay Command Palette allowing analysts to search IOCs, filter campaigns, toggle feed statuses, or navigate to settings within milliseconds.

Extracted from: docs/10_FRONTEND_ARCHITECTURE.md

ARGUS Frontend Architecture πŸ“„

The complete specification for the React analyst & recruiter dashboard


1.1 β€” Overview and Philosophy

What the React frontend IS: The analyst and recruiter view β€” a rich, interactive dashboard where a threat analyst explores campaigns, reviews predictions, and approves escalated reports. It is also what a recruiter sees first when evaluating ARGUS as a portfolio project.

What Grafana IS: The ops view β€” live pipeline metrics, feed ingest rates, prediction accuracy trends. Grafana stays. It is not being replaced.

The architectural split in one sentence: Grafana shows the pipeline's health; React shows the pipeline's intelligence.

The "first impression" principle: When a recruiter opens http://localhost:5173, within 3 seconds they see: a dark, minimal dashboard with a pulsing green "LIVE" badge, animated IOC count climbing, and a stream of threat indicators sliding into view β€” each one tagged with a type badge and confidence bar. It looks like a CrowdStrike analyst console, not a homework assignment.


1.2 β€” Complete Project Structure

argus/frontend/
β”œβ”€β”€ Dockerfile                          # Multi-stage: Node build β†’ nginx serve
β”œβ”€β”€ nginx.conf                          # Static files + /api proxy + SPA routing
β”œβ”€β”€ package.json                        # All dependencies with pinned versions
β”œβ”€β”€ vite.config.js                      # React plugin, /api proxy, /ws proxy
β”œβ”€β”€ tailwind.config.js                  # Full ARGUS dark theme design system
β”œβ”€β”€ postcss.config.js                   # Tailwind + Autoprefixer
β”œβ”€β”€ index.html                          # Vite entry point, loads Google Fonts
└── src/
    β”œβ”€β”€ main.jsx                        # ReactDOM.createRoot, QueryClientProvider, RouterProvider
    β”œβ”€β”€ App.jsx                         # Root component with QueryClientProvider wrapper
    β”œβ”€β”€ router.jsx                      # createBrowserRouter with all 8 routes
    β”œβ”€β”€ api/                            # All API calls, one file per resource
    β”‚   β”œβ”€β”€ client.js                   # Shared fetch wrapper with base URL, error handling
    β”‚   β”œβ”€β”€ iocs.js                     # fetchIocs, fetchIoc, WS URL builder
    β”‚   β”œβ”€β”€ campaigns.js                # fetchCampaigns, fetchCampaign, fetchCampaignGraph
    β”‚   β”œβ”€β”€ reports.js                  # fetchReports, fetchReport, downloadReportPdf
    β”‚   β”œβ”€β”€ feeds.js                    # fetchFeeds, triggerFeed, pauseFeed, resumeFeed
    β”‚   β”œβ”€β”€ reviews.js                  # fetchPendingReviews, fetchReview, submitReview
    β”‚   β”œβ”€β”€ agents.js                   # fetchAgentHealth
    β”‚   β”œβ”€β”€ mitre.js                    # fetchMitreHeatmap
    β”‚   └── metrics.js                  # fetchSummaryMetrics
    β”œβ”€β”€ hooks/                          # Custom React hooks
    β”‚   β”œβ”€β”€ useWebSocket.js             # WebSocket with reconnection + exponential backoff
    β”‚   β”œβ”€β”€ usePollQuery.js             # Thin wrapper around useQuery with refetchInterval
    β”‚   β”œβ”€β”€ useCountUp.js               # Animated number counter using requestAnimationFrame
    β”‚   β”œβ”€β”€ useLiveFeed.js              # Orchestrates WS + REST fallback for Live Feed page
    β”‚   └── useReactFlow.js             # Fetches + transforms campaign graph for React Flow
    β”œβ”€β”€ components/
    β”‚   β”œβ”€β”€ shared/                     # Used on multiple pages
    β”‚   β”‚   β”œβ”€β”€ StatCard.jsx            # Animated counter card with label, value, trend
    β”‚   β”‚   β”œβ”€β”€ Badge.jsx               # Pill badge for IOC types, confidence, TLP, status
    β”‚   β”‚   β”œβ”€β”€ IOCValue.jsx            # Monospace IOC value with copy-to-clipboard
    β”‚   β”‚   β”œβ”€β”€ ConfidenceBar.jsx       # Horizontal bar with colour gradient 0–1
    β”‚   β”‚   β”œβ”€β”€ LoadingSkeleton.jsx     # Animated grey pulse placeholder
    β”‚   β”‚   β”œβ”€β”€ EmptyState.jsx          # Empty state with icon, message, optional action
    β”‚   β”‚   β”œβ”€β”€ ErrorBoundary.jsx       # Catches render errors, shows retry
    β”‚   β”‚   β”œβ”€β”€ TimeAgo.jsx             # "3 minutes ago", auto-updates every 10s
    β”‚   β”‚   └── TechniqueTag.jsx        # MITRE technique ID badge with tooltip
    β”‚   └── layout/                     # App shell, sidebar, topbar
    β”‚       β”œβ”€β”€ AppShell.jsx            # Root layout: sidebar + main content area
    β”‚       β”œβ”€β”€ Sidebar.jsx             # Nav links, logo, pipeline status indicator
    β”‚       β”œβ”€β”€ Topbar.jsx              # Page title, last updated, live indicator, review badge
    β”‚       └── PageWrapper.jsx         # Consistent padding, max-width, fade-in animation
    └── pages/                          # One folder per page/route
        β”œβ”€β”€ LiveFeed/
        β”‚   β”œβ”€β”€ index.jsx               # Main Live Feed page component
        β”‚   β”œβ”€β”€ IOCRow.jsx              # Single IOC row with highlight animation
        β”‚   β”œβ”€β”€ IOCTypeChart.jsx        # Mini bar chart of IOC types (Recharts)
        β”‚   └── FilterBar.jsx           # Client-side filters: type, source, confidence
        β”œβ”€β”€ Campaigns/
        β”‚   β”œβ”€β”€ index.jsx               # Campaign Explorer: list + detail split view
        β”‚   β”œβ”€β”€ CampaignList.jsx        # Scrollable campaign list (left panel)
        β”‚   β”œβ”€β”€ CampaignDetail.jsx      # Full campaign detail (right panel)
        β”‚   β”œβ”€β”€ CampaignOverview.jsx    # Overview tab content
        β”‚   β”œβ”€β”€ CampaignIOCList.jsx     # IOC list tab content
        β”‚   β”œβ”€β”€ CampaignMITRE.jsx       # MITRE techniques tab content
        β”‚   └── CampaignPrediction.jsx  # Prediction tab content
        β”œβ”€β”€ ThreatReports/
        β”‚   β”œβ”€β”€ index.jsx               # Report list + detail reader view
        β”‚   β”œβ”€β”€ ReportList.jsx          # Report cards (left panel)
        β”‚   └── ReportDetail.jsx        # Full report reader (right panel)
        β”œβ”€β”€ MitreHeatmap/
        β”‚   β”œβ”€β”€ index.jsx               # Full-screen MITRE ATT&CK heatmap
        β”‚   β”œβ”€β”€ HeatmapGrid.jsx         # The matrix grid of technique cells
        β”‚   └── TechniquePanel.jsx      # Slide-in panel for technique detail
        β”œβ”€β”€ GraphViewer/
        β”‚   β”œβ”€β”€ index.jsx               # React Flow canvas with campaign selector
        β”‚   β”œβ”€β”€ GraphCanvas.jsx         # React Flow instance with custom nodes
        β”‚   β”œβ”€β”€ IOCNode.jsx             # Custom React Flow node per IOC type
        β”‚   β”œβ”€β”€ CampaignNode.jsx        # Custom React Flow node for campaign center
        β”‚   β”œβ”€β”€ GraphLegend.jsx         # Node type β†’ colour mapping legend
        β”‚   └── NodeDetailPanel.jsx     # Slide-in panel for node enrichment data
        β”œβ”€β”€ ReviewQueue/
        β”‚   β”œβ”€β”€ index.jsx               # Human review queue: list + review panel
        β”‚   β”œβ”€β”€ ReviewList.jsx          # Pending review list (left panel)
        β”‚   β”œβ”€β”€ ReviewPanel.jsx         # Full review with action buttons (right panel)
        β”‚   └── OverrideForm.jsx        # Inline form for prediction override
        β”œβ”€β”€ AgentHealth/
        β”‚   β”œβ”€β”€ index.jsx               # Agent health monitor: stats + cards + streams
        β”‚   β”œβ”€β”€ AgentCard.jsx           # Individual agent status card
        β”‚   β”œβ”€β”€ ReputationGauge.jsx     # Circular progress for reputation score
        β”‚   β”œβ”€β”€ StreamHealthTable.jsx   # Redis stream depths table
        β”‚   └── GroqBudgetBar.jsx       # Token usage progress bar
        └── FeedManager/
            β”œβ”€β”€ index.jsx               # Feed manager: grid of feed cards
            └── FeedCard.jsx            # Individual feed control card
Enter fullscreen mode Exit fullscreen mode

1.3 β€” Tailwind Design System

File: argus/frontend/tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        argus: {
          // Background hierarchy (3 levels)
          bg: '#0a0a0f',           // Page background β€” deepest
          surface: '#111118',      // Card / panel background
          elevated: '#1a1a24',     // Elevated elements (dropdowns, modals, hover states)

          // Border
          border: '#1e1e2e',       // Default border colour
          'border-bright': '#2a2a3e', // Active/focused border

          // Accent colours
          red: '#e94560',          // Primary accent β€” ARGUS brand red
          'red-muted': '#e9456020', // Red background tint (for badges, highlights)
          'red-hover': '#ff5a75',  // Red hover state
          blue: '#3b82f6',         // Secondary accent β€” muted blue
          'blue-muted': '#3b82f620', // Blue background tint
          green: '#10b981',        // Success / healthy / confirmed
          'green-muted': '#10b98120', // Green background tint
          yellow: '#f59e0b',       // Warning / degraded / amber
          'yellow-muted': '#f59e0b20', // Yellow background tint
          orange: '#f97316',       // Hashes, medium confidence

          // Text hierarchy (4 levels)
          'text-primary': '#e4e4ef',   // Primary text β€” high contrast
          'text-secondary': '#a0a0b8', // Secondary text β€” labels, descriptions
          'text-muted': '#6b6b80',     // Muted text β€” timestamps, hints
          'text-disabled': '#404055',  // Disabled text
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
        mono: ['JetBrains Mono', 'Fira Code', 'Consolas', 'monospace'],
      },
      animation: {
        'pulse-dot': 'pulse-dot 2s ease-in-out infinite',
        'fade-in': 'fade-in 0.3s ease-out',
        'slide-up': 'slide-up 0.3s ease-out',
        'slide-in-right': 'slide-in-right 0.3s ease-out',
        'slide-out-left': 'slide-out-left 0.3s ease-out',
        'highlight-fade': 'highlight-fade 3s ease-out',
      },
      keyframes: {
        'pulse-dot': {
          '0%, 100%': { opacity: '1', transform: 'scale(1)' },
          '50%': { opacity: '0.5', transform: 'scale(1.3)' },
        },
        'fade-in': {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        'slide-up': {
          '0%': { opacity: '0', transform: 'translateY(10px)' },
          '100%': { opacity: '1', transform: 'translateY(0)' },
        },
        'slide-in-right': {
          '0%': { opacity: '0', transform: 'translateX(20px)' },
          '100%': { opacity: '1', transform: 'translateX(0)' },
        },
        'slide-out-left': {
          '0%': { opacity: '1', transform: 'translateX(0)' },
          '100%': { opacity: '0', transform: 'translateX(-20px)' },
        },
        'highlight-fade': {
          '0%': { backgroundColor: '#10b98115' },
          '100%': { backgroundColor: 'transparent' },
        },
      },
      spacing: {
        'sidebar': '240px',
      },
    },
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

File: argus/frontend/postcss.config.js

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
Enter fullscreen mode Exit fullscreen mode

1.4 β€” Package Dependencies

File: argus/frontend/package.json

{
  "name": "argus-frontend",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "^6.23.1",
    "@tanstack/react-query": "^5.45.0",
    "@xyflow/react": "^12.0.4",
    "@dagrejs/dagre": "^1.1.2",
    "recharts": "^2.12.7",
    "lucide-react": "^0.383.0",
    "clsx": "^2.1.1",
    "tailwind-merge": "^2.3.0",
    "date-fns": "^3.6.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.1",
    "vite": "^5.3.1",
    "tailwindcss": "^3.4.4",
    "autoprefixer": "^10.4.19",
    "postcss": "^8.4.38"
  }
}
Enter fullscreen mode Exit fullscreen mode

1.5 β€” Vite Configuration

File: argus/frontend/vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0',
    port: 5173,
    proxy: {
      '/api': {
        target: 'http://argus-api:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
      '/ws': {
        target: 'ws://argus-api:8000',
        ws: true,
        rewrite: (path) => path.replace(/^\/ws/, '/ws'),
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Note for local development outside Docker: Change target to http://localhost:8000 and ws://localhost:8000 respectively when running Vite on the host machine without Docker.


1.6 β€” Docker and Compose Integration

File: argus/frontend/Dockerfile

## Stage 1: Build πŸ“„
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY . .
RUN npm run build

## Stage 2: Serve πŸ“„
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

File: argus/frontend/nginx.conf

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # Serve static files directly
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Proxy API requests to FastAPI
    location /api/ {
        proxy_pass http://argus-api:8000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Proxy WebSocket requests
    location /ws/ {
        proxy_pass http://argus-api:8000/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400;
    }

    # SPA fallback: all other routes β†’ index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
}
Enter fullscreen mode Exit fullscreen mode

Docker Compose addition (add to root docker-compose.yml):

  frontend:
    build:
      context: ./argus/frontend
      dockerfile: Dockerfile
    ports: ["5173:80"]
    depends_on:
      - argus-api
    mem_limit: 128m
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Makefile additions (add to root Makefile):

## React Frontend πŸ“„
ui:
    cd argus/frontend && npm run dev

ui-dev:
    cd argus/frontend && npm install && npm run dev

ui-build:
    cd argus/frontend && npm ci && npm run build
Enter fullscreen mode Exit fullscreen mode

1.7 β€” App Shell (Layout Components)

src/components/layout/AppShell.jsx

The root layout wrapper. Renders a fixed sidebar on the left, and a scrollable main content area on the right with a topbar.

import { Outlet } from 'react-router-dom'
import Sidebar from './Sidebar'
import Topbar from './Topbar'

export default function AppShell() {
  return (
    <div className="flex h-screen bg-argus-bg text-argus-text-primary overflow-hidden">
      {/* Fixed sidebar */}
      <Sidebar />

      {/* Main content area */}
      <div className="flex-1 flex flex-col ml-[240px]">
        <Topbar />
        <main className="flex-1 overflow-y-auto">
          <Outlet />
        </main>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

src/components/layout/Sidebar.jsx

Fixed 240px left sidebar. Contains ARGUS logo, navigation links, and pipeline health indicator at bottom.

Navigation items (exact list with Lucide icon names):

Label Route Lucide Icon
Live Feed /live-feed Radio
Campaigns /campaigns Target
Reports /reports FileText
MITRE ATT&CK /mitre Grid3x3
Graph /graph Network
Review Queue /reviews ClipboardCheck
Agent Health /agents Activity
Feed Manager /feeds Rss

Active state: Left border border-l-2 border-argus-red, background bg-argus-elevated.

Logo: The text "ARGUS" in font-bold text-xl tracking-wider with the "A" in text-argus-red and the rest in text-argus-text-primary. Below the logo, a horizontal red bar h-0.5 w-12 bg-argus-red.

Pipeline status indicator (bottom of sidebar): A small row showing a coloured dot and "Pipeline: Healthy" / "Pipeline: Degraded" / "Pipeline: Failing". Fetches from GET /api/agents/health every 30 seconds via usePollQuery. Logic:

  • All agents have status === 'healthy' β†’ green dot + "Healthy"
  • Any agent has status === 'degraded' β†’ amber dot + "Degraded"
  • Any agent has status === 'failed' β†’ red dot + "Failing"

Skeleton structure:

<aside className="fixed left-0 top-0 h-screen w-[240px] bg-argus-surface border-r border-argus-border flex flex-col z-50">
  {/* Logo section */}
  <div className="p-6 border-b border-argus-border">
    <h1 className="text-xl font-bold tracking-wider">
      <span className="text-argus-red">A</span>RGUS
    </h1>
    <div className="h-0.5 w-12 bg-argus-red mt-2" />
  </div>

  {/* Navigation */}
  <nav className="flex-1 py-4 px-3 space-y-1">
    {/* NavLink items rendered here */}
  </nav>

  {/* Pipeline status */}
  <div className="p-4 border-t border-argus-border">
    {/* Status dot + label */}
  </div>
</aside>
Enter fullscreen mode Exit fullscreen mode

src/components/layout/Topbar.jsx

Horizontal bar above the main page content. Contains:

  • Left: Page title β€” dynamically derived from the current route using a lookup map:

    • /live-feed β†’ "Live Feed"
    • /campaigns β†’ "Campaign Explorer"
    • /reports β†’ "Threat Reports"
    • /mitre β†’ "MITRE ATT&CK Heatmap"
    • /graph β†’ "Graph Viewer"
    • /reviews β†’ "Review Queue"
    • /agents β†’ "Agent Health"
    • /feeds β†’ "Feed Manager"
  • Right side (flex row, gap-4, items-center):

    1. Last updated timestamp: Shows TimeAgo component with the most recent last_ioc_ingested_at from GET /api/metrics/summary. Styled: text-argus-text-muted text-sm.
    2. Live indicator dot: A w-2 h-2 rounded-full dot. Green (bg-argus-green animate-pulse-dot) when WebSocket is connected (on Live Feed page) or when data is fresh (last IOC < 60s ago). Grey (bg-argus-text-disabled) otherwise.
    3. Notification badge: Count of pending reviews from GET /api/metrics/summary field pending_reviews. Rendered as a bg-argus-red text-white text-xs rounded-full px-2 py-0.5 badge next to a Bell icon. Clicking navigates to /reviews. Hidden when count is 0.

Skeleton structure:

<header className="h-14 border-b border-argus-border bg-argus-surface flex items-center justify-between px-6 shrink-0">
  <h2 className="text-lg font-semibold">{pageTitle}</h2>
  <div className="flex items-center gap-4">
    {/* Last updated */}
    {/* Live dot */}
    {/* Review badge */}
  </div>
</header>
Enter fullscreen mode Exit fullscreen mode

src/components/layout/PageWrapper.jsx

Wraps every page component. Adds consistent padding, max-width constraint, and a fade-in animation.

export default function PageWrapper({ children, fullWidth = false }) {
  return (
    <div className={`animate-fade-in p-6 ${fullWidth ? '' : 'max-w-[1600px] mx-auto'}`}>
      {children}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Props:

  • children β€” page content
  • fullWidth β€” boolean, default false. When true, removes max-width constraint (used by Graph Viewer and MITRE Heatmap which need full screen).

1.8 β€” Shared Components

src/components/shared/StatCard.jsx

An animated counter card for displaying key metrics.

Props (JSDoc):

/**
 * @param {object} props
 * @param {string} props.label - The metric label (e.g. "IOCs Today")
 * @param {number} props.value - The numeric value to display
 * @param {string} [props.icon] - Lucide icon name to render
 * @param {'red'|'blue'|'green'|'yellow'|'default'} [props.variant='default'] - Colour variant
 * @param {string} [props.suffix] - Text after the number (e.g. "%")
 * @param {string} [props.trend] - Trend indicator: "up", "down", or null
 */
Enter fullscreen mode Exit fullscreen mode

Behaviour: Uses useCountUp hook to animate the value. The number counts up from the previous value to the new value over 600ms whenever value changes.

Layout: bg-argus-surface border border-argus-border rounded-lg p-4. Icon top-left in the variant colour. Label in text-argus-text-secondary text-sm. Value in text-2xl font-bold font-mono. Trend arrow (if provided) in green (up) or red (down).

src/components/shared/Badge.jsx

A pill-shaped badge for IOC types, status, confidence levels, TLP markings.

Props:

/**
 * @param {object} props
 * @param {string} props.children - Badge text
 * @param {'red'|'blue'|'green'|'yellow'|'orange'|'grey'|'default'} [props.variant='default']
 * @param {'sm'|'md'} [props.size='sm'] - sm = text-xs, md = text-sm
 */
Enter fullscreen mode Exit fullscreen mode

Colour map:

  • red β†’ bg-argus-red-muted text-argus-red
  • blue β†’ bg-argus-blue-muted text-argus-blue
  • green β†’ bg-argus-green-muted text-argus-green
  • yellow β†’ bg-argus-yellow-muted text-argus-yellow
  • orange β†’ bg-orange-500/20 text-orange-400
  • grey β†’ bg-argus-elevated text-argus-text-muted
  • default β†’ bg-argus-elevated text-argus-text-secondary

IOC type β†’ badge variant mapping (used throughout the app):

  • ipv4, ipv6 β†’ red
  • domain β†’ blue
  • url β†’ blue
  • sha256, sha1, md5 β†’ orange
  • cve β†’ yellow
  • email β†’ grey

src/components/shared/IOCValue.jsx

Renders an IOC value in monospace font with a copy button.

Props:

/**
 * @param {object} props
 * @param {string} props.value - The IOC value string
 * @param {number} [props.maxLength=40] - Truncate with ellipsis if longer
 */
Enter fullscreen mode Exit fullscreen mode

Layout: Inline-flex. Value in font-mono text-sm text-argus-text-primary. On hover, a Copy icon (from Lucide) appears to the right. Clicking copies value to clipboard and briefly shows a Check icon for 1.5 seconds.

src/components/shared/ConfidenceBar.jsx

Horizontal bar showing confidence 0–1.

Props:

/**
 * @param {object} props
 * @param {number} props.value - Confidence score 0–1
 * @param {boolean} [props.showLabel=true] - Show percentage label to the right
 */
Enter fullscreen mode Exit fullscreen mode

Layout: Container h-2 bg-argus-elevated rounded-full overflow-hidden. Inner bar width = value * 100%. Colour: value >= 0.7 β†’ bg-argus-green, value >= 0.4 β†’ bg-argus-yellow, else β†’ bg-argus-red.

src/components/shared/LoadingSkeleton.jsx

Animated skeleton placeholder.

Props:

/**
 * @param {object} props
 * @param {'card'|'row'|'text'|'chart'} [props.variant='text']
 * @param {number} [props.count=1] - Number of skeleton items to render
 */
Enter fullscreen mode Exit fullscreen mode

Layout: animate-pulse bg-argus-elevated rounded. Different dimensions per variant:

  • text β†’ h-4 w-full rounded (for text lines)
  • card β†’ h-32 w-full rounded-lg (for stat cards)
  • row β†’ h-12 w-full rounded (for table rows)
  • chart β†’ h-64 w-full rounded-lg (for chart areas)

src/components/shared/EmptyState.jsx

Shown when a page or panel has no data.

Props:

/**
 * @param {object} props
 * @param {string} props.icon - Lucide icon name
 * @param {string} props.title - Primary message (e.g. "No campaigns yet")
 * @param {string} [props.description] - Secondary explanation
 * @param {string} [props.actionLabel] - Button text (e.g. "Run Demo")
 * @param {function} [props.onAction] - Button click handler
 */
Enter fullscreen mode Exit fullscreen mode

Layout: Centred vertically and horizontally. Icon in text-argus-text-muted at 48px. Title in text-lg text-argus-text-secondary mt-4. Description in text-sm text-argus-text-muted mt-2. Action button (if provided) in mt-4 px-4 py-2 bg-argus-red text-white rounded-lg hover:bg-argus-red-hover transition-colors.

src/components/shared/ErrorBoundary.jsx

React error boundary. Catches render errors in child components.

Implementation: Class component (error boundaries require componentDidCatch). Displays: an AlertTriangle icon, "Something went wrong" heading, the error message in font-mono text-sm text-argus-text-muted, and a "Try Again" button that calls this.setState({ hasError: false }).

Layout: Centred in the content area. bg-argus-surface border border-argus-border rounded-lg p-8.

src/components/shared/TimeAgo.jsx

Renders a relative timestamp that auto-updates.

Props:

/**
 * @param {object} props
 * @param {string|Date} props.timestamp - ISO datetime string or Date object
 * @param {string} [props.className] - Additional CSS classes
 */
Enter fullscreen mode Exit fullscreen mode

Behaviour: Uses date-fns/formatDistanceToNow to render "3 minutes ago", "just now", etc. Sets a setInterval every 10 seconds to re-render. Clears interval on unmount.

src/components/shared/TechniqueTag.jsx

Renders a MITRE technique ID as a styled badge with tooltip.

Props:

/**
 * @param {object} props
 * @param {string} props.techniqueId - e.g. "T1566.001"
 * @param {string} [props.techniqueName] - e.g. "Spearphishing Attachment"
 * @param {number} [props.confidence] - 0–1, adds a subtle confidence indicator
 */
Enter fullscreen mode Exit fullscreen mode

Layout: inline-flex items-center gap-1 bg-argus-elevated border border-argus-border rounded px-2 py-0.5 text-xs font-mono. The technique ID in text-argus-text-primary. On hover, if techniqueName is provided, show a tooltip (CSS-only, using group + group-hover:visible pattern) with the full technique name.


1.9 β€” Custom Hooks

src/hooks/useWebSocket.js

Manages a WebSocket connection with automatic reconnection.

Parameters:

/**
 * @param {string} url - WebSocket URL (e.g. "/ws/live-feed")
 * @param {object} options
 * @param {function} options.onMessage - Called with parsed JSON data for each message
 * @param {function} [options.onOpen] - Called when connection opens
 * @param {function} [options.onClose] - Called when connection closes
 * @param {function} [options.onError] - Called on error
 * @param {number} [options.reconnectDelay=1000] - Initial reconnect delay in ms
 * @param {number} [options.maxReconnects=5] - Maximum reconnection attempts
 * @param {boolean} [options.enabled=true] - Whether to connect
 */
Enter fullscreen mode Exit fullscreen mode

Returns:

/**
 * @returns {{ status: 'connecting'|'connected'|'disconnected'|'error', send: function, disconnect: function }}
 */
Enter fullscreen mode Exit fullscreen mode

Implementation notes:

  • Store the WebSocket instance in a useRef (not useState β€” avoids stale closures in event handlers)
  • Store reconnect attempt count in a useRef
  • On close (if not intentional): wait reconnectDelay * 2^attempt ms (exponential backoff), cap at 30000ms, attempt reconnect
  • After maxReconnects failures, set status to 'error' and stop trying
  • On useEffect cleanup (component unmount): close WebSocket with code 1000 (normal closure) β€” this prevents reconnection attempts
  • Build the full WebSocket URL from window.location: const wsUrl = (window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host + url

src/hooks/usePollQuery.js

Thin wrapper around TanStack Query's useQuery for polling.

Parameters:

/**
 * @param {Array} queryKey - TanStack Query key array
 * @param {function} queryFn - Async function that fetches data
 * @param {number} intervalMs - Polling interval in milliseconds
 * @param {object} [options] - Additional useQuery options
 */
Enter fullscreen mode Exit fullscreen mode

Returns: The useQuery result object: { data, isLoading, isError, error, isFetching }.

Implementation:

import { useQuery } from '@tanstack/react-query'

export default function usePollQuery(queryKey, queryFn, intervalMs, options = {}) {
  return useQuery({
    queryKey,
    queryFn,
    refetchInterval: intervalMs,
    refetchIntervalInBackground: false, // Don't poll when tab is hidden
    staleTime: intervalMs - 1000,       // Consider data stale just before next poll
    ...options,
  })
}
Enter fullscreen mode Exit fullscreen mode

src/hooks/useCountUp.js

Animates a number from its previous value to its new value.

Parameters:

/**
 * @param {number} value - Target value to animate to
 * @param {number} [duration=600] - Animation duration in ms
 * @returns {number} - The currently displayed intermediate value
 */
Enter fullscreen mode Exit fullscreen mode

Implementation notes:

  • Store the previous value in a useRef
  • When value changes, start a requestAnimationFrame loop
  • Each frame: calculate progress as (now - startTime) / duration, clamp to [0, 1]
  • Apply ease-out: progress = 1 - Math.pow(1 - progress, 3) (cubic ease-out)
  • Current display value = Math.round(previousValue + (value - previousValue) * progress)
  • When progress reaches 1.0: stop the animation, update previousValue ref
  • On unmount: cancelAnimationFrame(rafId) to prevent memory leaks

src/hooks/useLiveFeed.js

Orchestrates the Live Feed page's real-time data.

Returns:

/**
 * @returns {{
 *   iocs: Array,              // Rolling buffer of the last 500 IOCs
 *   status: string,           // 'connected'|'connecting'|'disconnected'|'polling'|'error'
 *   isPaused: boolean,        // Whether the user has paused the feed
 *   setPaused: function,      // Toggle pause state
 *   filters: object,          // { iocType: string|null, sourceFeed: string|null, minConfidence: number }
 *   setFilters: function,     // Update filters
 *   filteredIocs: Array,      // IOCs after client-side filtering
 *   stats: object,            // { total: number, lastTimestamp: string|null }
 * }}
 */
Enter fullscreen mode Exit fullscreen mode

Implementation notes:

  • Primary path: Connect to WebSocket at /ws/live-feed via useWebSocket
  • Fallback path: If WebSocket status is 'error' (max reconnects exceeded), fall back to polling GET /api/iocs?limit=20&since={lastSeenTimestamp} every 5 seconds via usePollQuery. Set status to 'polling'.
  • IOC buffer: Maintain a useRef array of up to 500 IOCs. New IOCs are prepended (newest at index 0). When length exceeds 500, slice to 500.
  • On WebSocket message: Parse event field. If event === 'new_ioc', prepend data to the buffer (unless isPaused is true). If event === 'ping', ignore (it's a keepalive).
  • Initial load: On mount, fetch GET /api/iocs?limit=20 to pre-populate the buffer while the WebSocket connects. This satisfies the "3-second test" β€” the user sees data immediately.
  • Filters: Applied client-side to the buffer: filteredIocs = iocs.filter(...) based on iocType, sourceFeed, minConfidence.
  • Pause: When isPaused is true, new WebSocket messages are still received but NOT prepended to the display buffer. They are buffered in a separate useRef array. When unpaused, the buffered IOCs are merged in.

src/hooks/useReactFlow.js

Fetches and transforms campaign graph data for React Flow.

Parameters:

/**
 * @param {string|null} campaignId - Campaign UUID to fetch graph for
 * @returns {{ nodes: Array, edges: Array, isLoading: boolean, error: object|null }}
 */
Enter fullscreen mode Exit fullscreen mode

Implementation notes:

  • Fetches from GET /api/campaigns/{campaignId}/graph using useQuery (not polling β€” graphs don't change fast enough)
  • Enabled only when campaignId is not null
  • Node transformation: The backend returns position: { x: 0, y: 0 } for all nodes. Client-side layout uses @dagrejs/dagre:
  import dagre from '@dagrejs/dagre'

  function layoutGraph(nodes, edges) {
    const g = new dagre.graphlib.Graph()
    g.setDefaultEdgeLabel(() => ({}))
    g.setGraph({ rankdir: 'TB', nodesep: 100, ranksep: 80 })

    nodes.forEach((node) => {
      g.setNode(node.id, { width: 180, height: 60 })
    })
    edges.forEach((edge) => {
      g.setEdge(edge.source, edge.target)
    })

    dagre.layout(g)

    return nodes.map((node) => {
      const pos = g.node(node.id)
      return { ...node, position: { x: pos.x - 90, y: pos.y - 30 } }
    })
  }
Enter fullscreen mode Exit fullscreen mode
  • Node type assignment: Based on data.ioc_type:
    • ipv4, ipv6 β†’ type: 'iocNode', border colour: argus-red
    • domain β†’ type: 'iocNode', border colour: argus-blue
    • sha256, md5, sha1 β†’ type: 'iocNode', border colour: orange-400
    • campaign β†’ type: 'campaignNode', border colour: yellow-500
  • Edge properties: All edges get animated: true (dashed animation) and style: { stroke: '#6b6b80' }.

1.10 β€” API Layer

src/api/client.js

Shared fetch wrapper used by all API modules.

const API_BASE = '/api'

export async function apiFetch(path, options = {}) {
  const url = `${API_BASE}${path}`
  const response = await fetch(url, {
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
    ...options,
  })

  if (!response.ok) {
    const error = new Error(`API error: ${response.status} ${response.statusText}`)
    error.status = response.status
    try {
      error.body = await response.json()
    } catch {
      // No JSON body
    }
    throw error
  }

  // Handle blob responses (PDF downloads)
  if (options.responseType === 'blob') {
    return response.blob()
  }

  return response.json()
}

export function buildWsUrl(path) {
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
  return `${protocol}//${window.location.host}${path}`
}
Enter fullscreen mode Exit fullscreen mode

src/api/iocs.js

import { apiFetch, buildWsUrl } from './client'

/**
 * Fetch paginated IOC list
 * @param {object} params - { ioc_type?, source_feed?, limit?, offset?, since? }
 * @returns {Promise<{iocs: Array, total: number, limit: number, offset: number}>}
 */
export async function fetchIocs(params = {}) {
  const query = new URLSearchParams()
  if (params.ioc_type) query.set('ioc_type', params.ioc_type)
  if (params.source_feed) query.set('source_feed', params.source_feed)
  if (params.limit) query.set('limit', String(params.limit))
  if (params.offset) query.set('offset', String(params.offset))
  if (params.since) query.set('since', params.since)
  return apiFetch(`/iocs?${query.toString()}`)
}

/**
 * Fetch single IOC by ID
 * @param {string} iocId - UUID
 * @returns {Promise<object>} - EnrichedIOC
 */
export async function fetchIoc(iocId) {
  return apiFetch(`/iocs/${iocId}`)
}

/**
 * Get WebSocket URL for live feed
 * @returns {string}
 */
export function getLiveFeedWsUrl() {
  return buildWsUrl('/ws/live-feed')
}
Enter fullscreen mode Exit fullscreen mode

src/api/campaigns.js

import { apiFetch } from './client'

/**
 * Fetch paginated campaign list
 * @param {object} params - { limit?, offset? }
 * @returns {Promise<{campaigns: Array, total: number}>}
 */
export async function fetchCampaigns(params = {}) {
  const query = new URLSearchParams()
  if (params.limit) query.set('limit', String(params.limit))
  if (params.offset) query.set('offset', String(params.offset))
  return apiFetch(`/campaigns?${query.toString()}`)
}

/**
 * Fetch single campaign with all details
 * @param {string} campaignId - UUID
 * @returns {Promise<object>}
 */
export async function fetchCampaign(campaignId) {
  return apiFetch(`/campaigns/${campaignId}`)
}

/**
 * Fetch campaign graph data for React Flow
 * @param {string} campaignId - UUID
 * @returns {Promise<{nodes: Array, edges: Array}>}
 */
export async function fetchCampaignGraph(campaignId) {
  return apiFetch(`/campaigns/${campaignId}/graph`)
}
Enter fullscreen mode Exit fullscreen mode

src/api/reports.js

import { apiFetch } from './client'

/**
 * Fetch paginated report list
 * @param {object} params - { limit?, offset? }
 * @returns {Promise<{reports: Array, total: number}>}
 */
export async function fetchReports(params = {}) {
  const query = new URLSearchParams()
  if (params.limit) query.set('limit', String(params.limit))
  if (params.offset) query.set('offset', String(params.offset))
  return apiFetch(`/reports?${query.toString()}`)
}

/**
 * Fetch single report with full detail
 * @param {string} reportId - UUID
 * @returns {Promise<object>}
 */
export async function fetchReport(reportId) {
  return apiFetch(`/reports/${reportId}`)
}

/**
 * Download report as PDF blob
 * @param {string} reportId - UUID
 * @returns {Promise<Blob>}
 */
export async function downloadReportPdf(reportId) {
  return apiFetch(`/reports/${reportId}/pdf`, { responseType: 'blob' })
}
Enter fullscreen mode Exit fullscreen mode

src/api/feeds.js

import { apiFetch } from './client'

/**
 * Fetch all feed statuses
 * @returns {Promise<{feeds: Array}>}
 */
export async function fetchFeeds() {
  return apiFetch('/feeds/status')
}

/**
 * Trigger immediate feed poll
 * @param {string} feedName
 * @returns {Promise<{triggered: boolean, feed_name: string}>}
 */
export async function triggerFeed(feedName) {
  return apiFetch(`/feeds/trigger/${feedName}`, { method: 'POST' })
}

/**
 * Pause a feed
 * @param {string} feedName
 * @returns {Promise<{status: string}>}
 */
export async function pauseFeed(feedName) {
  return apiFetch(`/feeds/pause/${feedName}`, { method: 'POST' })
}

/**
 * Resume a feed
 * @param {string} feedName
 * @returns {Promise<{status: string}>}
 */
export async function resumeFeed(feedName) {
  return apiFetch(`/feeds/resume/${feedName}`, { method: 'POST' })
}
Enter fullscreen mode Exit fullscreen mode

src/api/reviews.js

import { apiFetch } from './client'

/**
 * Fetch all pending human reviews
 * @returns {Promise<{reviews: Array}>}
 */
export async function fetchPendingReviews() {
  return apiFetch('/reviews')
}

/**
 * Fetch single review detail
 * @param {string} reportId - UUID
 * @returns {Promise<object>}
 */
export async function fetchReview(reportId) {
  return apiFetch(`/reviews/${reportId}`)
}

/**
 * Submit a review decision
 * @param {string} reportId - UUID
 * @param {object} decision - { decision: 'approve'|'override'|'false_positive', analyst_notes?, override_prediction? }
 * @returns {Promise<{status: string, decision: string}>}
 */
export async function submitReview(reportId, decision) {
  return apiFetch(`/reviews/${reportId}/decision`, {
    method: 'POST',
    body: JSON.stringify(decision),
  })
}
Enter fullscreen mode Exit fullscreen mode

src/api/agents.js

import { apiFetch } from './client'

/**
 * Fetch all agent health data
 * @returns {Promise<object>}
 */
export async function fetchAgentHealth() {
  return apiFetch('/agents/health')
}
Enter fullscreen mode Exit fullscreen mode

src/api/mitre.js

import { apiFetch } from './client'

/**
 * Fetch MITRE ATT&CK heatmap data
 * @param {object} params - { days? } - time range in days
 * @returns {Promise<{techniques: Array, total_techniques_observed: number, time_range_days: number}>}
 */
export async function fetchMitreHeatmap(params = {}) {
  const query = new URLSearchParams()
  if (params.days) query.set('days', String(params.days))
  return apiFetch(`/mitre/heatmap?${query.toString()}`)
}
Enter fullscreen mode Exit fullscreen mode

src/api/metrics.js

import { apiFetch } from './client'

/**
 * Fetch aggregated summary metrics for dashboard header
 * @returns {Promise<object>}
 */
export async function fetchSummaryMetrics() {
  return apiFetch('/metrics/summary')
}
Enter fullscreen mode Exit fullscreen mode

1.11 β€” Router Configuration

File: src/router.jsx

import { createBrowserRouter } from 'react-router-dom'
import AppShell from './components/layout/AppShell'
import LiveFeed from './pages/LiveFeed'
import Campaigns from './pages/Campaigns'
import ThreatReports from './pages/ThreatReports'
import MitreHeatmap from './pages/MitreHeatmap'
import GraphViewer from './pages/GraphViewer'
import ReviewQueue from './pages/ReviewQueue'
import AgentHealth from './pages/AgentHealth'
import FeedManager from './pages/FeedManager'

const router = createBrowserRouter([
  {
    element: <AppShell />,
    children: [
      { index: true, element: <LiveFeed /> },
      { path: 'live-feed', element: <LiveFeed /> },
      { path: 'campaigns', element: <Campaigns /> },
      { path: 'reports', element: <ThreatReports /> },
      { path: 'mitre', element: <MitreHeatmap /> },
      { path: 'graph', element: <GraphViewer /> },
      { path: 'reviews', element: <ReviewQueue /> },
      { path: 'agents', element: <AgentHealth /> },
      { path: 'feeds', element: <FeedManager /> },
    ],
  },
])

export default router
Enter fullscreen mode Exit fullscreen mode

File: src/main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import router from './router'
import './index.css'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 2,
      refetchOnWindowFocus: false,
      staleTime: 5000,
    },
  },
})

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  </React.StrictMode>
)
Enter fullscreen mode Exit fullscreen mode

File: src/index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

/* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');

/* Reset and base styles */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Inter', system-ui, sans-serif;
  background-color: #0a0a0f;
  color: #e4e4ef;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Custom scrollbar for dark theme */
::-webkit-scrollbar {
  width: 6px;
}
::-webkit-scrollbar-track {
  background: #0a0a0f;
}
::-webkit-scrollbar-thumb {
  background: #1e1e2e;
  border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
  background: #2a2a3e;
}
Enter fullscreen mode Exit fullscreen mode

File: src/App.jsx

import { Outlet } from 'react-router-dom'

export default function App() {
  return <Outlet />
}
Enter fullscreen mode Exit fullscreen mode

File: argus/frontend/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="ARGUS β€” Agentic threat intelligence dashboard for real-time IOC monitoring, campaign analysis, and adversary trajectory prediction" />
    <title>ARGUS β€” Threat Intelligence Dashboard</title>
    <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>πŸ¦…</text></svg>" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Extracted from: docs/11_FRONTEND_PAGES.md

ARGUS Frontend Pages πŸ“„

Complete specification for all 8 React pages


PAGE 1 β€” Live Feed (/live-feed)

Purpose

The real-time IOC stream page β€” the centrepiece of the recruiter demo. It shows threat indicators arriving in real time, with animated counters, a live data stream, and type distribution charts.

Demo Moment

A recruiter watches IOC rows slide in from the top, each with a green highlight that fades over 3 seconds, while the "IOCs Today" counter animates upward. The pulsing red "LIVE" badge confirms this is real data, not a recording.

Data Sources

  • Primary: WebSocket /ws/live-feed for real-time IOC push
  • Fallback: GET /api/iocs?limit=20&since={ts} polled every 5s (when WS fails)
  • Stats: GET /api/metrics/summary polled every 10s (for stat cards)
  • Initial load: GET /api/iocs?limit=20 on mount (pre-populate while WS connects)

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      [LIVE ●]    β”‚
β”‚  β”‚ IOCs Today   β”‚  β”‚ Active Feeds β”‚  β”‚ Last IOC     β”‚      or          β”‚
β”‚  β”‚   1,247  ↑   β”‚  β”‚     8        β”‚  β”‚ 3s ago       β”‚      [PAUSED]    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      [RECONNECT] β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚  LIVE IOC STREAM                            β”‚  β”‚ IOCs by Type       β”‚β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚  β”‚ (last 10 min)      β”‚β”‚
β”‚  β”‚  β”‚ [ipv4] 192.0.2.1    feodo  β–ˆβ–ˆβ–‘β–‘ 82%  3sβ”‚ β”‚  β”‚                    β”‚β”‚
β”‚  β”‚  β”‚ [domain] evil.com   otx    β–ˆβ–ˆβ–ˆβ–‘ 91%  5sβ”‚ β”‚  β”‚  β”Œβ”€β”               β”‚β”‚
β”‚  β”‚  β”‚ [sha256] abc123...  vt     β–ˆβ–‘β–‘β–‘ 45% 12sβ”‚ β”‚  β”‚  β”‚ β”‚  β”Œβ”€β”          β”‚β”‚
β”‚  β”‚  β”‚ [ipv4] 10.0.0.1     feodo  β–ˆβ–ˆβ–‘β–‘ 78% 18sβ”‚ β”‚  β”‚  β”‚ β”‚  β”‚ β”‚ β”Œβ”€β”      β”‚β”‚
β”‚  β”‚  β”‚ ... (scrollable, max 500 rows)         β”‚ β”‚  β”‚  β”‚ β”‚  β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”  β”‚β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚  β”‚  IP  DOM URL HASH  β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Filter: [IOC Type β–Ό]  [Source Feed β–Ό]  [Min Confidence: β–ˆβ–ˆβ–ˆβ–ˆβ–‘ 0.5]    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

LiveFeed/index.jsx
β”œβ”€β”€ PageWrapper
β”‚   β”œβ”€β”€ <header> β€” Stats row
β”‚   β”‚   β”œβ”€β”€ StatCard { label: "IOCs Today", value: metrics.iocs_last_24h, icon: "Shield", variant: "red" }
β”‚   β”‚   β”œβ”€β”€ StatCard { label: "Active Feeds", value: metrics.active_feeds, icon: "Rss", variant: "blue" }
β”‚   β”‚   β”œβ”€β”€ StatCard { label: "Last IOC", value: <TimeAgo timestamp={metrics.last_ioc_ingested_at} />, icon: "Clock" }
β”‚   β”‚   └── LiveStatusBadge { status: feed.status } β€” inline component
β”‚   β”œβ”€β”€ <div className="flex gap-6"> β€” Main content row
β”‚   β”‚   β”œβ”€β”€ <div className="flex-1"> β€” IOC Stream
β”‚   β”‚   β”‚   └── <div className="overflow-y-auto max-h-[calc(100vh-320px)]">
β”‚   β”‚   β”‚       └── {filteredIocs.map(ioc => <IOCRow key={ioc.ioc_id} ioc={ioc} />)}
β”‚   β”‚   └── <div className="w-80"> β€” Sidebar chart
β”‚   β”‚       └── IOCTypeChart { iocs: filteredIocs }
β”‚   └── FilterBar { filters, setFilters }
Enter fullscreen mode Exit fullscreen mode

IOCRow.jsx β€” Single IOC row

Each row is a div with class flex items-center gap-3 px-4 py-3 border-b border-argus-border hover:bg-argus-elevated transition-colors. New rows that arrived in the last 3 seconds have the additional class animate-highlight-fade.

Row contents (left to right):

  1. Badge with IOC type, variant from the type→colour map
  2. IOCValue with the IOC value (monospace, copy button)
  3. <span className="text-sm text-argus-text-secondary"> with source_feed
  4. ConfidenceBar with value={ioc.confidence_score} (width: 80px)
  5. TimeAgo with timestamp={ioc.ingested_at}
  6. If ioc.malware_family: small Badge with variant yellow

IOCTypeChart.jsx β€” Mini bar chart

Uses Recharts <BarChart> with a <Bar> per IOC type. Data is computed client-side from the IOC buffer: count by ioc_type over the last 10 minutes. Refreshes every 5 seconds via useState + setInterval.

Chart config:

  • width={300} height={250}
  • Dark theme: <XAxis tick={{ fill: '#a0a0b8', fontSize: 12 }}>
  • Bar fills: use the IOC type colour map (red for IP, blue for domain, etc.)
  • No grid lines: <CartesianGrid strokeDasharray="3 3" stroke="#1e1e2e" />

FilterBar.jsx

Horizontal bar at the bottom of the page.

Three filters:

  1. IOC Type dropdown: <select> with options: "All Types", "ipv4", "domain", "url", "sha256", "md5", "cve". Styled: bg-argus-elevated border border-argus-border rounded-lg px-3 py-2 text-sm text-argus-text-primary
  2. Source Feed dropdown: <select> with dynamic options from the IOC buffer's unique source_feed values
  3. Min Confidence slider: <input type="range" min="0" max="1" step="0.1"> with current value displayed. Styled: accent-argus-red

Filters are applied client-side to filteredIocs in the useLiveFeed hook.

LiveStatusBadge β€” inline component

Renders based on status:

  • 'connected': <span className="flex items-center gap-1.5 bg-argus-red-muted text-argus-red px-3 py-1 rounded-full text-xs font-semibold"><span className="w-2 h-2 rounded-full bg-argus-red animate-pulse-dot" /> LIVE</span>
  • 'paused' (user paused): <span className="... bg-argus-yellow-muted text-argus-yellow ...">PAUSED</span>
  • 'connecting' or 'polling': <span className="... bg-argus-elevated text-argus-text-muted ..."><Loader2 className="animate-spin" size={12} /> RECONNECTING</span>
  • 'error': <span className="... bg-argus-elevated text-argus-text-muted ...">OFFLINE β€” Polling</span>

Interaction Spec

  • Scroll: The IOC stream scrolls vertically. If the user scrolls up (away from latest), auto-scroll pauses. A "↓ Scroll to latest" button appears at the bottom centre.
  • Pause: Clicking the LIVE badge toggles isPaused. When paused, new IOCs are buffered but not displayed. Badge changes to PAUSED (yellow).
  • Filter: Selecting a filter immediately filters the displayed IOC list (client-side).
  • Copy: Clicking the copy button on IOCValue copies the value to clipboard.
  • Click IOC row: Future enhancement β€” opens a detail panel. For MVP, rows are not clickable.

Empty State

Shows EmptyState with icon="Radio", title="Waiting for IOCs...", description="Start the pipeline with 'make demo' to see live threat indicators flowing in.".

Loading State

Three LoadingSkeleton variant="card" for stat cards. Multiple LoadingSkeleton variant="row" for the IOC stream area. A LoadingSkeleton variant="chart" for the chart.

Animation Spec

Animation Trigger What it does Duration
Count-up Stat card value changes Number animates from old β†’ new 600ms
Highlight fade New IOC row added Green background fades to transparent 3000ms
Pulse dot LIVE badge dot Continuous pulsing opacity + scale 2000ms loop
Fade-in Page mount Entire page fades in 300ms
Slide-up IOC row enters list Row slides up from 10px below 300ms

PAGE 2 β€” Campaign Explorer (/campaigns)

Purpose

Browse and inspect correlated IOC campaigns. The left panel lists campaigns sorted by recency; selecting one shows full detail in the right panel with tabs for Overview, IOCs, MITRE Techniques, and Prediction.

Demo Moment

After the APT29 demo runs, the recruiter sees a campaign card with "SUNBURST" malware family, a confidence gauge, and clicking it reveals an interconnected kill-chain timeline with predicted next phase.

Data Sources

  • Campaign list: GET /api/campaigns?limit=50 polled every 30s
  • Campaign detail: GET /api/campaigns/{id} fetched on selection
  • Campaign graph (for overview tab): GET /api/campaigns/{id}/graph fetched on selection

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  CAMPAIGNS (sorted by      β”‚  CAMPAIGN DETAIL                            β”‚
β”‚  last activity)            β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Campaign a0eebc99    β”‚β—„ β”‚  β”‚ Campaign: a0eebc99    Confidence: 78% β”‚  β”‚
β”‚  β”‚ IOCs: 12 | Emotet    β”‚  β”‚  β”‚ IOCs: 12 | First: Jan 10 | Last: Jan 15β”‚  β”‚
β”‚  β”‚ Confidence: β–ˆβ–ˆβ–ˆβ–‘ 78% β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”‚ 3 hours ago          β”‚  β”‚                                             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  [Overview] [IOC List] [MITRE] [Prediction] β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  ─────────────────────────────────────────── β”‚
β”‚  β”‚ Campaign 7c9e6679    β”‚  β”‚  (tab content rendered here)                β”‚
β”‚  β”‚ IOCs: 5 | Unknown    β”‚  β”‚                                             β”‚
β”‚  β”‚ Confidence: β–ˆβ–ˆβ–‘β–‘ 55% β”‚  β”‚                                             β”‚
β”‚  β”‚ 1 day ago            β”‚  β”‚                                             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚                                             β”‚
β”‚  ...                       β”‚                                             β”‚
β”‚  (scrollable)              β”‚                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

Campaigns/index.jsx
β”œβ”€β”€ PageWrapper
β”‚   └── <div className="flex gap-6 h-[calc(100vh-120px)]">
β”‚       β”œβ”€β”€ CampaignList { campaigns, selectedId, onSelect }
β”‚       β”‚   └── <div className="w-80 overflow-y-auto border-r border-argus-border pr-4">
β”‚       β”‚       └── {campaigns.map(c => <CampaignListItem ... />)}
β”‚       └── CampaignDetail { campaignId }
β”‚           β”œβ”€β”€ <header> β€” Campaign ID, confidence gauge, IOC count, date range
β”‚           β”œβ”€β”€ <nav> β€” Tab buttons: Overview | IOC List | MITRE | Prediction
β”‚           └── <div> β€” Active tab content
β”‚               β”œβ”€β”€ CampaignOverview (if tab === 'overview')
β”‚               β”œβ”€β”€ CampaignIOCList (if tab === 'iocs')
β”‚               β”œβ”€β”€ CampaignMITRE (if tab === 'mitre')
β”‚               └── CampaignPrediction (if tab === 'prediction')
Enter fullscreen mode Exit fullscreen mode

Tab Contents

Overview tab (CampaignOverview.jsx):

  • Kill-chain phase timeline: horizontal stepper showing observed phases (e.g., Reconnaissance β†’ Delivery β†’ C&C). Each phase is a circle connected by lines. Observed phases are filled with bg-argus-red, unobserved are bg-argus-elevated.
  • Top 5 IOCs: list showing type badge, value, confidence.
  • Enrichment coverage: a small stat showing "X of Y IOCs enriched" with a bar.

IOC List tab (CampaignIOCList.jsx):

  • Table with columns: Type (badge), Value (monospace), Confidence (bar), Source Feed, Malware Family, Ingested At.
  • Pagination: simple prev/next with 20 IOCs per page.
  • Uses data from the campaign detail endpoint's iocs array.

MITRE Techniques tab (CampaignMITRE.jsx):

  • List of mapped techniques. Each item: TechniqueTag with technique ID, technique name, tactic in text-argus-text-muted, ConfidenceBar, and rationale text in text-sm text-argus-text-secondary.

Prediction tab (CampaignPrediction.jsx):

  • Prediction box styled with border-2 border-argus-red rounded-lg p-6.
  • Header: "Adversary Trajectory Prediction" in text-argus-red font-semibold.
  • Predicted phase: large text (e.g., "Installation").
  • Predicted techniques: list of TechniqueTag + probability bar + rationale.
  • Confidence: ConfidenceBar for overall prediction confidence.
  • Validation status badge: Badge showing "Pending" (grey), "Confirmed" (green), or "Falsified" (red).

Interaction Spec

  • Click a campaign in the left panel β†’ right panel loads that campaign's detail
  • Tab switching is client-side state (no URL change)
  • Campaign list auto-refreshes every 30s; if the selected campaign is in the new list, detail stays open

Empty State

Left panel: EmptyState icon="Target" title="No campaigns detected" description="Campaigns appear when the pipeline correlates IOCs into clusters. Run 'make demo' to generate one."
Right panel (no campaign selected): centred text "Select a campaign to view details" in text-argus-text-muted.

Loading State

Left panel: 5 LoadingSkeleton variant="row". Right panel: LoadingSkeleton variant="card" + multiple LoadingSkeleton variant="text".

Animation Spec

Animation Trigger Duration
Fade-in Page mount 300ms
Slide-in-right Campaign detail loads 300ms
Count-up Confidence percentage changes 600ms

PAGE 3 β€” Threat Reports (/reports)

Purpose

Browse and read generated threat intelligence reports. Each report is rendered in a styled reader view (not raw JSON) with executive summary, technical narrative, kill-chain timeline, MITRE tags, prediction, and recommended mitigations.

Demo Moment

The recruiter opens a report and sees a Mandiant-style intelligence brief β€” executive summary in clean prose, a visual kill-chain timeline, technique tags, and a prominent prediction box. They can download it as a PDF.

Data Sources

  • Report list: GET /api/reports?limit=20 polled every 30s
  • Report detail: GET /api/reports/{id} fetched on selection
  • PDF download: GET /api/reports/{id}/pdf triggered on button click

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  REPORTS                β”‚  REPORT DETAIL                       [πŸ“₯ PDF] β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚                                               β”‚
β”‚  β”‚ Campaign a0eebc99 β”‚  β”‚  ═══════════════════════════════════════════  β”‚
β”‚  β”‚ Confidence: 78%   β”‚  β”‚  EXECUTIVE SUMMARY                            β”‚
β”‚  β”‚ Jan 15, 2024      β”‚  β”‚  A financially-motivated threat actor is...   β”‚
β”‚  β”‚ [NEEDS REVIEW] 🟠 β”‚  β”‚                                               β”‚
β”‚  β”‚ "A financially..." β”‚  β”‚  ═══════════════════════════════════════════  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  TECHNICAL NARRATIVE                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  The campaign was first detected on...         β”‚
β”‚  β”‚ Campaign 7c9e6679 β”‚  β”‚  (flowing paragraphs)                         β”‚
β”‚  β”‚ βœ“ AUTO-PROCESSED  β”‚  β”‚                                               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  ═══════════════════════════════════════════  β”‚
β”‚  ...                    β”‚  KILL-CHAIN TIMELINE                           β”‚
β”‚                         β”‚  Recon ──→ Delivery ──→ C2                     β”‚
β”‚                         β”‚                                               β”‚
β”‚                         β”‚  ═══════════════════════════════════════════  β”‚
β”‚                         β”‚  MITRE ATT&CK TECHNIQUES                      β”‚
β”‚                         β”‚  [T1566.001] [T1071.001] [T1053.005]          β”‚
β”‚                         β”‚                                               β”‚
β”‚                         β”‚  ═══════════════════════════════════════════  β”‚
β”‚                         β”‚  β”Œβ”€ PREDICTION ──────────────────────────┐    β”‚
β”‚                         β”‚  β”‚ Next Phase: Installation              β”‚    β”‚
β”‚                         β”‚  β”‚ Confidence: 72%                       β”‚    β”‚
β”‚                         β”‚  β”‚ Techniques: T1053.005 (78%)           β”‚    β”‚
β”‚                         β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                         β”‚                                               β”‚
β”‚                         β”‚  ═══════════════════════════════════════════  β”‚
β”‚                         β”‚  RECOMMENDED MITIGATIONS                      β”‚
β”‚                         β”‚  1. Block outbound HTTPS to...                β”‚
β”‚                         β”‚  2. Disable macro execution...                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

ThreatReports/index.jsx
β”œβ”€β”€ PageWrapper
β”‚   └── <div className="flex gap-6 h-[calc(100vh-120px)]">
β”‚       β”œβ”€β”€ ReportList { reports, selectedId, onSelect }
β”‚       β”‚   └── Report cards with escalation badge, summary snippet
β”‚       └── ReportDetail { reportId }
β”‚           β”œβ”€β”€ <header> β€” Report title + PDF download button
β”‚           β”œβ”€β”€ <section> β€” Executive Summary (clean prose paragraph)
β”‚           β”œβ”€β”€ <section> β€” Technical Narrative (flowing paragraphs, not JSON)
β”‚           β”œβ”€β”€ <section> β€” Kill-chain timeline (horizontal visual)
β”‚           β”œβ”€β”€ <section> β€” MITRE Techniques (TechniqueTag list)
β”‚           β”œβ”€β”€ <section> β€” Prediction box (red-bordered box)
β”‚           └── <section> β€” Recommended Mitigations (numbered list)
Enter fullscreen mode Exit fullscreen mode

Report List Item

Each report card: bg-argus-surface border border-argus-border rounded-lg p-4 cursor-pointer hover:border-argus-border-bright transition-colors. Contents:

  • Campaign ID (first 8 chars) in font-mono text-sm
  • Confidence percentage
  • Date
  • Escalation badge: Badge variant="yellow" with text "NEEDS REVIEW" if escalation_required === true, or Badge variant="green" with "AUTO-PROCESSED" if false
  • First 100 chars of executive summary in text-sm text-argus-text-muted line-clamp-2

PDF Download

The "Download PDF" button calls downloadReportPdf(reportId) which returns a Blob. Create a temporary <a> element with URL.createObjectURL(blob), set download filename, click, then revoke URL.

Interaction Spec

  • Click a report card β†’ detail panel loads
  • Click "Download PDF" β†’ browser downloads the PDF file
  • Report list auto-refreshes every 30s

Empty State

EmptyState icon="FileText" title="No reports generated" description="Reports are created when the ThreatNarrativeAgent processes a campaign with 2+ MITRE techniques."

Loading State

Left panel: 3 LoadingSkeleton variant="card". Right panel: multiple LoadingSkeleton variant="text".

Animation Spec

Animation Trigger Duration
Fade-in Page mount 300ms
Slide-in-right Report detail loads 300ms

PAGE 4 β€” MITRE ATT&CK Heatmap (/mitre)

Purpose

Full-screen MITRE ATT&CK enterprise matrix heatmap showing which techniques ARGUS has observed across all campaigns. Colour intensity indicates frequency. Clicking a technique reveals a detail panel.

Demo Moment

The recruiter sees a dark matrix of technique cells, with a cluster of cells glowing red around Initial Access and Command & Control β€” the APT29 demo campaign's techniques lighting up the matrix.

Data Sources

  • Heatmap data: GET /api/mitre/heatmap?days=30 polled every 60s
  • Time range options: 7 / 30 / all (parameter passed to endpoint)

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  [Last 7 days] [Last 30 days] [All time]        Total observed: 12     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Recon β”‚ Res Dev β”‚ Init Acc β”‚ Exec β”‚ Persist β”‚ Priv Esc β”‚ ... β”‚ Impact β”‚
β”‚  ──────┼─────────┼──────────┼──────┼─────────┼──────────┼─────┼────────│
β”‚        β”‚         β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚      β”‚         β”‚          β”‚     β”‚        β”‚
β”‚        β”‚         β”‚ T1566    β”‚      β”‚ T1053   β”‚          β”‚     β”‚        β”‚
β”‚        β”‚         β”‚ (12)     β”‚      β”‚ (3)     β”‚          β”‚     β”‚        β”‚
β”‚        β”‚         β”‚          β”‚      β”‚         β”‚          β”‚     β”‚        β”‚
β”‚        β”‚         β”‚ β–ˆβ–ˆβ–ˆβ–ˆ     β”‚      β”‚         β”‚          β”‚     β”‚        β”‚
β”‚        β”‚         β”‚ T1190    β”‚      β”‚         β”‚          β”‚     β”‚        β”‚
β”‚        β”‚         β”‚ (5)      β”‚      β”‚         β”‚          β”‚     β”‚        β”‚
β”‚        β”‚         β”‚          β”‚      β”‚         β”‚ β–ˆβ–ˆβ–ˆβ–ˆ     β”‚     β”‚        β”‚
β”‚        β”‚         β”‚          β”‚      β”‚         β”‚ T1055    β”‚     β”‚        β”‚
β”‚        β”‚         β”‚          β”‚      β”‚         β”‚ (2)      β”‚     β”‚        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                    β”‚  T1566.001 β€” Spearphishing          β”‚
                                    β”‚  Attachment                          β”‚
                                    β”‚  Tactic: Initial Access              β”‚
                                    β”‚  Observed in 4 campaigns             β”‚
                                    β”‚  Avg confidence: 81%                 β”‚
                                    β”‚  Last seen: 3 hours ago              β”‚
                                    β”‚                                      β”‚
                                    β”‚  Campaigns:                          β”‚
                                    β”‚  - a0eebc99 (conf: 88%)             β”‚
                                    β”‚  - 7c9e6679 (conf: 72%)             β”‚
                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

MitreHeatmap/index.jsx
β”œβ”€β”€ PageWrapper { fullWidth: true }
β”‚   β”œβ”€β”€ <header> β€” Time range buttons + total observed count
β”‚   β”œβ”€β”€ HeatmapGrid { techniques, onTechniqueClick }
β”‚   β”‚   └── <div className="grid grid-cols-[repeat(14,1fr)] gap-1">
β”‚   β”‚       β”œβ”€β”€ (14 tactic column headers)
β”‚   β”‚       └── (technique cells per tactic column)
β”‚   └── TechniquePanel { technique, isOpen, onClose }
β”‚       └── Slide-in from right with technique detail
Enter fullscreen mode Exit fullscreen mode

HeatmapGrid

Implementation approach: The MITRE ATT&CK enterprise matrix has 14 tactics. The grid is a CSS grid with 14 columns. Each column represents one tactic. The endpoint returns only observed techniques β€” unobserved techniques are not rendered.

Tactic columns (exact order):

  1. Reconnaissance
  2. Resource Development
  3. Initial Access
  4. Execution
  5. Persistence
  6. Privilege Escalation
  7. Defense Evasion
  8. Credential Access
  9. Discovery
  10. Lateral Movement
  11. Collection
  12. Command and Control
  13. Exfiltration
  14. Impact

Technique cell: Each observed technique is a div in the appropriate tactic column. Styled:

bg-opacity based on count:
  count >= 10: bg-argus-red (full intensity)
  count >= 5:  bg-argus-red/70
  count >= 2:  bg-argus-blue
  count == 1:  bg-argus-blue/50

Size: w-full rounded p-2
Text: technique_id in font-mono text-xs, technique_name truncated to 20 chars in text-xs
Count badge: (count) in text-xs font-bold
Enter fullscreen mode Exit fullscreen mode

Clicking a cell opens TechniquePanel.

TechniquePanel

Slide-in panel from the right (fixed right-0 top-0 h-full w-96 bg-argus-surface border-l border-argus-border p-6 animate-slide-in-right z-40). Shows:

  • Technique ID and full name
  • Tactic
  • Observation count
  • Number of campaigns
  • Average confidence with ConfidenceBar
  • Last seen with TimeAgo
  • Campaign list with confidence per campaign

Close button (X) in top-right corner. Clicking outside the panel also closes it.

Interaction Spec

  • Click time range button β†’ refetch heatmap with days parameter
  • Click technique cell β†’ detail panel slides in from right
  • Click X or outside panel β†’ panel closes

Empty State

Centred EmptyState icon="Grid3x3" title="No techniques observed" description="MITRE techniques are mapped when the MITREMappingAgent processes campaign correlations."

Loading State

A full-width LoadingSkeleton variant="chart" with 14 narrow columns.

Animation Spec

Animation Trigger Duration
Fade-in Page mount 300ms
Slide-in-right Technique panel opens 300ms

PAGE 5 β€” Graph Viewer (/graph)

Purpose

Interactive React Flow canvas showing the campaign graph: IOC nodes connected by relationship edges. Demonstrates ARGUS's autonomous correlation visually.

Demo Moment

The recruiter sees an interconnected cluster of IP, domain, and hash nodes radiating from a gold campaign centre node. Animated dashed edges pulse between nodes. They zoom in and see "SHARES_ASN" and "DELIVERS" labels on the edges.

Data Sources

  • Campaign list for selector: GET /api/campaigns?limit=50 on mount
  • Campaign graph: GET /api/campaigns/{id}/graph fetched on campaign selection via useReactFlow hook

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Campaign: [β–Ό Select a campaign...]                                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”                                                             β”‚
β”‚  β”‚ Legend β”‚                                                             β”‚
β”‚  β”‚ πŸ”΄ IP  β”‚          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                      β”‚
β”‚  β”‚ πŸ”΅ Domainβ”‚        β”‚ Campaign  β”‚ ← gold border                       β”‚
β”‚  β”‚ 🟠 Hash β”‚       β”Œβ”€β”€ a0eebc99 β”œβ”€β”€β”€β”€β”€β”                                β”‚
β”‚  β”‚ 🟑 Campaignβ”‚    β”‚  β”‚ 12 IOCs  β”‚     β”‚                                β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚                                β”‚
β”‚                   β”‚                    β”‚                                 β”‚
β”‚              β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”                          β”‚
β”‚              β”‚ 192.0.2.1β”‚         β”‚ evil.com β”‚                          β”‚
β”‚              β”‚ LeaseWeb β”‚         β”‚ registrarβ”‚                          β”‚
β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚                                                                         β”‚
β”‚                                              β”Œβ”€β”€β”€β”€β”€β”€β” Zoom: [+][-][⊑]  β”‚
β”‚                                              β”‚minimapβ”‚                  β”‚
β”‚                                              β””β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

GraphViewer/index.jsx
β”œβ”€β”€ PageWrapper { fullWidth: true }
β”‚   β”œβ”€β”€ <header> β€” Campaign selector dropdown
β”‚   β”œβ”€β”€ <div className="flex h-[calc(100vh-180px)]">
β”‚   β”‚   β”œβ”€β”€ GraphLegend β€” Fixed left panel (w-40)
β”‚   β”‚   └── GraphCanvas { nodes, edges, isLoading }
β”‚   β”‚       β”œβ”€β”€ <ReactFlow> instance with custom node types registered
β”‚   β”‚       β”œβ”€β”€ <MiniMap> bottom-right
β”‚   β”‚       └── <Controls> zoom buttons
β”‚   └── NodeDetailPanel { selectedNode, isOpen, onClose }
β”‚       └── Slide-in from right with full IOC enrichment data
Enter fullscreen mode Exit fullscreen mode

Custom Node Components

IOCNode.jsx β€” Custom React Flow node for IOC types.

// Receives data prop from React Flow: { label, ioc_type, confidence, malware_family, asn_org, registrar }

// Node container: w-[180px] bg-argus-surface border-2 rounded-lg p-3
// Border colour based on ioc_type:
//   ipv4/ipv6 β†’ border-argus-red
//   domain    β†’ border-argus-blue
//   hash      β†’ border-orange-400

// Contents:
// Line 1: Badge with ioc_type + label (value) in font-mono text-xs truncated
// Line 2: malware_family OR asn_org OR registrar in text-xs text-argus-text-muted
// Line 3: ConfidenceBar mini version
Enter fullscreen mode Exit fullscreen mode

CampaignNode.jsx β€” Custom React Flow node for campaign centre.

// Larger node: w-[200px] bg-argus-surface border-2 border-yellow-500 rounded-lg p-4
// Line 1: "Campaign" label in text-xs text-argus-text-muted
// Line 2: campaign_id (first 8 chars) in font-mono text-sm font-bold
// Line 3: IOC count in text-xs
Enter fullscreen mode Exit fullscreen mode

Edge styling

All edges: animated: true (creates dashed animation), style: { stroke: '#6b6b80', strokeWidth: 1.5 }, labelStyle: { fill: '#a0a0b8', fontSize: 10 }.

Interaction Spec

  • Select campaign from dropdown β†’ graph loads and auto-layouts via dagre
  • Click a node β†’ NodeDetailPanel slides in from right showing full IOC enrichment data
  • Zoom/pan via mouse wheel and drag (React Flow built-in)
  • Minimap in bottom-right corner for navigation

Empty State

Centred EmptyState icon="Network" title="Select a campaign" description="Choose a campaign from the dropdown to visualize its IOC graph."

Loading State

After campaign selection: a centred Loader2 spinner with "Loading graph..." text.

Animation Spec

Animation Trigger Duration
Fade-in Page mount 300ms
Edge dash animation Always on Continuous
Slide-in-right Node detail panel opens 300ms

PAGE 6 β€” Human Review Queue (/reviews)

Purpose

The analyst's workstation for reviewing escalated reports. Shows pending reviews with confidence breakdowns and action buttons for approve, override, or false positive decisions.

Demo Moment

The recruiter sees that ARGUS autonomously escalated a low-confidence report to a human β€” demonstrating the system knows when it's uncertain. The analyst can approve, override, or reject.

Data Sources

  • Pending reviews: GET /api/reviews polled every 15s
  • Review detail: GET /api/reviews/{report_id} fetched on selection
  • Submit decision: POST /api/reviews/{report_id}/decision

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Pending Reviews: 3 ●●●                                                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  REVIEW PANEL                              β”‚
β”‚  β”‚ Campaign a0eebc99    β”‚  β”‚                                             β”‚
β”‚  β”‚ Confidence: 58% 🟠   β”‚  β”‚  Executive Summary:                        β”‚
β”‚  β”‚ "A financially..."   β”‚  β”‚  "A financially-motivated threat actor..."  β”‚
β”‚  β”‚ In queue: 2 hours    β”‚  β”‚                                             β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  Prediction:                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  Next Phase: Installation                   β”‚
β”‚  β”‚ Campaign 7c9e6679    β”‚  β”‚  Techniques: T1053.005 (78%)                β”‚
β”‚  β”‚ Confidence: 42% πŸ”΄   β”‚  β”‚                                             β”‚
β”‚  β”‚ "An unknown actor..." β”‚  β”‚  Confidence Breakdown:                     β”‚
β”‚  β”‚ In queue: 45 min     β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚ MITRE confidence:  0.72 (Γ—0.35) β”‚       β”‚
β”‚                            β”‚  β”‚ Correlation:       0.65 (Γ—0.30) β”‚       β”‚
β”‚                            β”‚  β”‚ Enrichment:        0.80 (Γ—0.20) β”‚       β”‚
β”‚                            β”‚  β”‚ IOC count score:   0.35 (Γ—0.15) β”‚       β”‚
β”‚                            β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β”‚                            β”‚                                             β”‚
β”‚                            β”‚  [βœ“ Approve]  [✎ Override]  [βœ— False Pos]  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

ReviewQueue/index.jsx
β”œβ”€β”€ PageWrapper
β”‚   β”œβ”€β”€ <header> β€” "Pending Reviews: {count}" with pulsing amber badge if count > 0
β”‚   └── <div className="flex gap-6 h-[calc(100vh-160px)]">
β”‚       β”œβ”€β”€ ReviewList { reviews, selectedId, onSelect }
β”‚       └── ReviewPanel { reportId, onDecisionSubmitted }
β”‚           β”œβ”€β”€ Executive summary section
β”‚           β”œβ”€β”€ Prediction section (phase + techniques)
β”‚           β”œβ”€β”€ Confidence breakdown table
β”‚           β”œβ”€β”€ Action buttons row
β”‚           └── OverrideForm (conditionally rendered)
Enter fullscreen mode Exit fullscreen mode

Action Buttons

Three buttons in a row:

  1. Approve: bg-argus-green text-white px-4 py-2 rounded-lg hover:bg-green-600 with Check icon
  2. Override: bg-argus-blue text-white px-4 py-2 rounded-lg hover:bg-blue-600 with Pencil icon β€” clicking this toggles the OverrideForm below
  3. False Positive: bg-argus-red text-white px-4 py-2 rounded-lg hover:bg-red-600 with X icon β€” clicking this shows a confirmation dialog: "Are you sure? This will mark all IOCs in this campaign as false positives." with "Confirm" and "Cancel" buttons

OverrideForm.jsx

Inline form that appears below the action buttons when "Override" is clicked.

Fields:

  • Corrected Phase: <select> with options: Reconnaissance, Weaponisation, Delivery, Exploitation, Installation, Command & Control, Actions on Objectives
  • Corrected Techniques: <input type="text" placeholder="T1234.001, T1567.002"> (comma-separated)
  • Analyst Notes: <textarea> for free-text notes
  • Submit button: "Submit Override" β†’ calls submitReview(reportId, { decision: 'override', override_prediction: { predicted_phase, predicted_techniques }, analyst_notes })

Interaction Spec

  • Click a review item β†’ detail panel loads
  • Click "Approve" β†’ immediate API call, row animates out of list with animate-slide-out-left
  • Click "Override" β†’ form appears below buttons with animate-slide-up
  • Click "False Positive" β†’ confirmation dialog, then API call + row animates out
  • After any decision submission, invalidate the reviews query to refetch

Empty State

Centred EmptyState icon="ClipboardCheck" title="No pending reviews" description="All reports were processed with sufficient confidence. Reports with confidence below 65% are escalated here." with a green Check circle icon.

Loading State

Left: 3 LoadingSkeleton variant="row". Right: LoadingSkeleton variant="card" + LoadingSkeleton variant="text" count={5}.

Animation Spec

Animation Trigger Duration
Fade-in Page mount 300ms
Slide-out-left Decision submitted, row exits 300ms
Slide-up Override form appears 300ms
Pulse dot Pending count badge (amber) 2000ms loop

PAGE 7 β€” Agent Health Monitor (/agents)

Purpose

Pipeline operations page showing the health and performance of all 7 ARGUS agents, Redis stream depths, and Groq token budget.

Demo Moment

The recruiter sees 7 agent cards, all with green status dots, and the ThreatNarrativeAgent's reputation score displayed as a circular progress gauge at 0.78 β€” the most visually impressive element on this page.

Data Sources

  • Agent health: GET /api/agents/health polled every 15s
  • Summary metrics: GET /api/metrics/summary polled every 10s (for stat cards)

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ IOCs Processed β”‚  β”‚ Predictions    β”‚  β”‚ Accuracy       β”‚  β”‚ Rep.   β”‚β”‚
β”‚  β”‚   24,891       β”‚  β”‚    47          β”‚  β”‚    78%         β”‚  β”‚  0.78  β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  AGENT CARDS (7 cards, 2-column grid)                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ ● IngestorAgent             β”‚  β”‚ ● EnrichmentAgent                β”‚ β”‚
β”‚  β”‚ Last active: 3s ago         β”‚  β”‚ Last active: 5s ago              β”‚ β”‚
β”‚  β”‚ Tasks: 1,247                β”‚  β”‚ Tasks: 1,198                     β”‚ β”‚
β”‚  β”‚ Queue depth: 12 ▁▂▃▁▂▁▃▂▁▁│  β”‚ Queue depth: 5 ▁▁▁▁▂▁▁▁▁▁      β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ ● CorrelationAgent          β”‚  β”‚ ● MITREMappingAgent              β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ ● ThreatNarrativeAgent      β”‚  β”‚ ● FeedbackLearningAgent          β”‚ β”‚
β”‚  β”‚ Reputation: ◐ 0.78          β”‚  β”‚ ...                              β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                      β”‚
β”‚  β”‚ ● OrchestratorAgent         β”‚                                      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  STREAM HEALTH                                                          β”‚
β”‚  stream:raw_iocs        | depth: 12  | lag: 0  | DLQ: 0               β”‚
β”‚  stream:enriched_iocs   | depth: 5   | lag: 0  | DLQ: 0               β”‚
β”‚  ...                                                                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  GROQ TOKEN BUDGET:  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘  8,200 / 14,400 (57%)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

AgentHealth/index.jsx
β”œβ”€β”€ PageWrapper
β”‚   β”œβ”€β”€ <div className="grid grid-cols-4 gap-4 mb-6"> β€” Stat cards row
β”‚   β”‚   β”œβ”€β”€ StatCard { label: "IOCs Processed", value: metrics.iocs_total, icon: "Shield" }
β”‚   β”‚   β”œβ”€β”€ StatCard { label: "Predictions Made", value: agents.predictions_made, icon: "TrendingUp" }
β”‚   β”‚   β”œβ”€β”€ StatCard { label: "Prediction Accuracy", value: accuracy, suffix: "%", icon: "Target" }
β”‚   β”‚   └── StatCard { label: "Avg Reputation", value: agents.avg_reputation, icon: "Award" }
β”‚   β”œβ”€β”€ <div className="grid grid-cols-2 gap-4 mb-6"> β€” Agent cards
β”‚   β”‚   └── {agents.map(agent => <AgentCard key={agent.agent_id} agent={agent} />)}
β”‚   β”œβ”€β”€ StreamHealthTable { streams }
β”‚   └── GroqBudgetBar { tokensUsed, tokenBudget }
Enter fullscreen mode Exit fullscreen mode

AgentCard.jsx

Card for each agent. bg-argus-surface border border-argus-border rounded-lg p-4.

Contents:

  • Header row: Status dot (green/amber/red w-2.5 h-2.5 rounded-full) + agent name in font-semibold
  • Last active: TimeAgo component
  • Tasks processed: count in font-mono
  • Queue depth: current depth value + a sparkline of last 10 readings (rendered as a tiny inline SVG or a series of inline-block w-1 bg-argus-blue bars at varying heights)
  • Special for ThreatNarrativeAgent: ReputationGauge component replaces the queue depth

ReputationGauge.jsx

Circular progress indicator showing the reputation score (0–1).

Implementation: An SVG circle with a stroke-dasharray and stroke-dashoffset animation.

<svg width="80" height="80" className="transform -rotate-90">
  <circle cx="40" cy="40" r="32" stroke="#1e1e2e" strokeWidth="6" fill="none" />
  <circle cx="40" cy="40" r="32" stroke={colour} strokeWidth="6" fill="none"
          strokeDasharray={circumference}
          strokeDashoffset={circumference * (1 - score)}
          className="transition-all duration-1000" />
</svg>
Enter fullscreen mode Exit fullscreen mode

Score text centred inside: font-mono text-lg font-bold.
Colour: score >= 0.7 β†’ text-argus-green, score >= 0.5 β†’ text-argus-yellow, else β†’ text-argus-red.

StreamHealthTable.jsx

Table showing all Redis streams. Columns: Stream Name, Current Depth, Consumer Group Lag, DLQ Depth.

Styled: bg-argus-surface rounded-lg overflow-hidden. Header row: bg-argus-elevated. Row background: alternate bg-argus-surface and bg-argus-bg. DLQ depth column: red text if > 0.

GroqBudgetBar.jsx

A progress bar showing tokens_used / token_budget for the current minute.

Bar container: h-4 bg-argus-elevated rounded-full overflow-hidden. Inner bar width = percentage. Colour: < 60% β†’ bg-argus-green, 60-85% β†’ bg-argus-yellow, > 85% β†’ bg-argus-red. Label text to the right: "{used} / {budget} ({pct}%)" in font-mono text-sm.

Note: This bar auto-resets every 60 seconds as the Groq token counter resets. Show a small "Resets every 60s" note.

Interaction Spec

  • Page auto-refreshes every 15s
  • No click interactions (pure monitoring view)

Empty State

Shows stat cards at 0 and agent cards with grey status dots and "No data" labels.

Loading State

4 LoadingSkeleton variant="card" for stat row. 7 LoadingSkeleton variant="card" for agent grid.

Animation Spec

Animation Trigger Duration
Fade-in Page mount 300ms
Count-up Stat card values change 600ms
Reputation gauge fill Score loads/changes 1000ms transition

PAGE 8 β€” Feed Manager (/feeds)

Purpose

Control plane for threat intelligence feed sources. View status, trigger manual polls, pause/resume feeds.

Demo Moment

The recruiter sees a grid of feed cards β€” Feodo Tracker, URLhaus, OTX β€” all showing green "Active" badges with last poll times. They can click "Trigger Now" and watch the button show a spinner then a checkmark.

Data Sources

  • Feed status: GET /api/feeds/status polled every 15s
  • Trigger feed: POST /api/feeds/trigger/{name}
  • Pause feed: POST /api/feeds/pause/{name}
  • Resume feed: POST /api/feeds/resume/{name}

Layout (ASCII Wireframe)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Feed Manager                                          [Trigger All β–Ά]  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ πŸ”΅ Feodo Tracker                β”‚  β”‚ πŸ”΅ URLhaus                     β”‚β”‚
β”‚  β”‚ Status: ● Active                β”‚  β”‚ Status: ● Active               β”‚β”‚
β”‚  β”‚ Last poll: 3 minutes ago        β”‚  β”‚ Last poll: 5 minutes ago       β”‚β”‚
β”‚  β”‚ Interval: Every 5 min (= stable)β”‚  β”‚ Interval: Every 5 min (↑ fast) β”‚β”‚
β”‚  β”‚ Novel ratio: 12%                β”‚  β”‚ Novel ratio: 8%                β”‚β”‚
β”‚  β”‚ Total ingested: 4,521           β”‚  β”‚ Total ingested: 2,103          β”‚β”‚
β”‚  β”‚ Failures: 0                     β”‚  β”‚ Failures: 0                    β”‚β”‚
β”‚  β”‚                                 β”‚  β”‚                                 β”‚β”‚
β”‚  β”‚ [Trigger Now]  [⏸ Pause]       β”‚  β”‚ [Trigger Now]  [⏸ Pause]       β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ 🟒 AlienVault OTX              β”‚  β”‚ πŸ”΄ ThreatFox                   β”‚β”‚
β”‚  β”‚ Status: ● Active                β”‚  β”‚ Status: ● Failed               β”‚β”‚
β”‚  β”‚ ...                             β”‚  β”‚ Failures: 3                    β”‚β”‚
β”‚  β”‚ [Trigger Now]  [⏸ Pause]       β”‚  β”‚ [Trigger Now]  [β–Ά Resume]      β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚  ...                                                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Component Breakdown

FeedManager/index.jsx
β”œβ”€β”€ PageWrapper
β”‚   β”œβ”€β”€ <header className="flex justify-between items-center mb-6">
β”‚   β”‚   β”œβ”€β”€ <h3>Feed Manager</h3>
β”‚   β”‚   └── <button> "Trigger All" β€” triggers all active feeds sequentially
β”‚   └── <div className="grid grid-cols-2 gap-4">
β”‚       └── {feeds.map(feed => <FeedCard key={feed.feed_name} feed={feed} />)}
Enter fullscreen mode Exit fullscreen mode

FeedCard.jsx

Card for each feed. bg-argus-surface border border-argus-border rounded-lg p-5.

Props:

/**
 * @param {object} props
 * @param {object} props.feed - Feed object from API
 *   { feed_name, feed_url, feed_type, is_active, poll_interval_secs, last_polled,
 *     last_novel_ratio, consecutive_failures, total_iocs_ingested }
 */
Enter fullscreen mode Exit fullscreen mode

Contents:

  1. Header: Feed icon (coloured circle per feed type) + feed name in font-semibold text-lg
  2. Status badge: Badge β€” "Active" (green) if is_active && consecutive_failures < 3, "Paused" (yellow) if !is_active, "Failed" (red) if consecutive_failures >= 3
  3. Last polled: TimeAgo component β€” "Last poll: {timeAgo}"
  4. Poll interval: {interval} seconds + adaptive indicator:
    • Compare poll_interval_secs to default (300). If < 300: "↑ speeding up" in green. If > 300: "↓ slowing down" in yellow. If === 300: "= stable" in grey.
  5. Novel IOC ratio: {(last_novel_ratio * 100).toFixed(0)}% in a small Badge
  6. Total ingested: count in font-mono
  7. Consecutive failures: shown in red text if > 0
  8. Actions row:
    • "Trigger Now" button: bg-argus-elevated border border-argus-border text-argus-text-primary px-3 py-1.5 rounded-lg text-sm hover:bg-argus-border. On click, calls triggerFeed(feedName). Shows Loader2 className="animate-spin" while loading. Shows Check icon with "Triggered" text for 2 seconds after success.
    • "Pause" button (if active): text-argus-yellow hover:bg-argus-yellow-muted px-3 py-1.5 rounded-lg text-sm. On click, calls pauseFeed(feedName).
    • "Resume" button (if paused): text-argus-green hover:bg-argus-green-muted px-3 py-1.5 rounded-lg text-sm. On click, calls resumeFeed(feedName).

"Trigger All" Button

Top-right button. Iterates through all active feeds and calls triggerFeed for each. Shows a spinner while any are in progress. Shows "All Triggered βœ“" for 2 seconds when complete.

Interaction Spec

  • Click "Trigger Now" β†’ button shows spinner β†’ success checkmark for 2s β†’ reverts to normal
  • Click "Pause" β†’ calls API β†’ feed status updates on next poll (15s)
  • Click "Resume" β†’ calls API β†’ feed status updates on next poll
  • After any mutation, invalidate the feeds query to refetch immediately

Empty State

EmptyState icon="Rss" title="No feeds configured" description="Feed sources are seeded into the database on first startup."

Loading State

4 LoadingSkeleton variant="card" in a 2-column grid.

Animation Spec

Animation Trigger Duration
Fade-in Page mount 300ms
Spinner Trigger button loading While loading
Check flash Trigger success 2000ms display

Extracted from: docs/14_DESIGN_SYSTEM.md

ARGUS UI β€” Design System & Tokens πŸ“„

File: argus-context/14_DESIGN_SYSTEM.md

For AI coding agents β€” read this before writing a single CSS class


1. Brand Identity

ARGUS is a precision instrument, not a hacker tool. The visual language communicates authority, intelligence, and urgency without theatrical effects. Every design decision serves one question: does this make the data easier to trust?

North star references: Vercel dashboard, Linear app, Stripe dashboard β€” dark mode, typographic, data-dense, zero decoration.

What ARGUS is NOT: green-on-black terminal, glassmorphism blur effects, neon glow, particle systems, gradient meshes, 3D transforms on UI elements.


2. Colour Palette (Complete, Exhaustive)

These are the ONLY colours used anywhere in the application. No exceptions.

Core Palette

:root {
  /* ── BACKGROUNDS ─────────────────────────────────── */
  --bg-page:        #000000;   /* True black. Page canvas. */
  --bg-surface:     #0a0608;   /* Cards, panels, sidebar */
  --bg-elevated:    #110a0c;   /* Hover states, modals, dropdowns */
  --bg-subtle:      #1a0d0f;   /* Table row stripes, inactive tabs */

  /* ── BORDERS ─────────────────────────────────────── */
  --border-default: #1e1014;   /* Default card/panel borders */
  --border-strong:  #2a1518;   /* Focused/active borders */
  --border-accent:  #5a0d1a;   /* Accent borders (campaign card selected) */

  /* ── TEXT ────────────────────────────────────────── */
  --text-primary:   #ffffff;   /* Headlines, primary values */
  --text-secondary: #a0a0a0;   /* Labels, metadata */
  --text-muted:     #555555;   /* Placeholders, disabled, timestamps */
  --text-inverse:   #000000;   /* Text on red backgrounds */

  /* ── RED β€” The ARGUS accent. Use sparingly. ──────── */
  --red-900:        #2d0008;   /* Deepest red background */
  --red-800:        #5a0d1a;   /* Border on red surfaces */
  --red-700:        #8b1224;   /* Muted red for de-emphasized */
  --red-600:        #a8142a;   /* Secondary red actions */
  --red-500:        #c0162b;   /* PRIMARY RED β€” buttons, active states */
  --red-400:        #d63348;   /* Hover on red */
  --red-300:        #e05068;   /* Red text on dark bg */
  --red-200:        #f0909e;   /* Very muted red text */
  --red-100:        #fce8eb;   /* Red on white (unused in dark mode) */

  /* ── SEMANTIC COLOURS (not red, not white) ───────── */
  --green-500:      #16c784;   /* Live indicator, confirmed prediction, success */
  --green-bg:       rgba(22, 199, 132, 0.08); /* Green status background */
  --amber-500:      #f59e0b;   /* Warning, pending, medium confidence */
  --amber-bg:       rgba(245, 158, 11, 0.08);
  --blue-500:       #3b82f6;   /* Informational, domain nodes in graph */
  --blue-bg:        rgba(59, 130, 246, 0.08);
  --purple-500:     #8b5cf6;   /* Prediction phase, future state */
  --purple-bg:      rgba(139, 92, 246, 0.08);

  /* ── GRAPH NODE COLOURS (Neo4j / React Flow) ─────── */
  --node-ip:        #c0162b;   /* IPv4/IPv6 nodes */
  --node-domain:    #3b82f6;   /* Domain nodes */
  --node-hash:      #f59e0b;   /* Hash nodes (MD5/SHA) */
  --node-url:       #8b5cf6;   /* URL nodes */
  --node-campaign:  #ffffff;   /* Campaign hub node */
  --node-cve:       #16c784;   /* CVE nodes */
}
Enter fullscreen mode Exit fullscreen mode

Colour Usage Rules (follow exactly)

Use case Colour token Notes
Page background --bg-page Always #000 β€” never #0a0a0a or similar
Card / panel bg --bg-surface Subtle warmth (red undertone)
Hover / elevated --bg-elevated Only on interaction
Primary CTA button --red-500 bg, white text One per view max
Active nav item --red-500 left border (2px), --bg-elevated bg
IOC type badge (ipv4) --red-500 bg 10% opacity, --red-300 text
IOC type badge (domain) --blue-500 bg 10% opacity, --blue-500 text
IOC type badge (hash) --amber-500 bg 10% opacity, --amber-500 text
Live indicator --green-500 pulsing dot
Confidence: high (>0.7) --green-500
Confidence: medium (0.4–0.7) --amber-500
Confidence: low (<0.4) --red-300
Escalation / needs review --amber-500 badge
Confirmed prediction --green-500 badge
Falsified prediction --red-300 badge
Pending prediction --text-muted badge
MITRE technique tag --red-900 bg, --red-300 text, --red-800 border
Monospace IOC values --text-secondary in font-mono

The Red Rule

--red-500 (#c0162b) appears on exactly three things:

  1. The primary CTA button
  2. Active navigation state (left border)
  3. The highest-severity / most active data (top technique in heatmap, live IOC highlight)

Everywhere else, use the muted variants (--red-700, --red-300) or semantic colours (green for confirmed, amber for warning). Red loses meaning if it's everywhere.


3. Typography

Font Stack

--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
Enter fullscreen mode Exit fullscreen mode

Load via Google Fonts in index.html:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
Enter fullscreen mode Exit fullscreen mode

Type Scale

/* Use these classes / Tailwind equivalents β€” no other sizes */

.text-hero      { font-size: 56px; font-weight: 700; letter-spacing: -0.04em; line-height: 1.0; }
.text-display   { font-size: 36px; font-weight: 700; letter-spacing: -0.03em; line-height: 1.1; }
.text-title     { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.2; }
.text-heading   { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; }
.text-body      { font-size: 14px; font-weight: 400; letter-spacing: 0;       line-height: 1.6; }
.text-small     { font-size: 12px; font-weight: 400; letter-spacing: 0;       line-height: 1.5; }
.text-micro     { font-size: 11px; font-weight: 500; letter-spacing: 0.06em;  line-height: 1.4; text-transform: uppercase; }
.text-mono      { font-family: var(--font-mono); font-size: 13px; font-weight: 400; }
.text-mono-sm   { font-family: var(--font-mono); font-size: 11px; font-weight: 400; }
Enter fullscreen mode Exit fullscreen mode

Typography Rules

  • All IOC values, IPs, hashes, domains β†’ font-mono always. Never render threat data in sans-serif.
  • MITRE technique IDs (T1566.001) β†’ font-mono + red badge
  • Timestamps β†’ text-small + --text-muted
  • Section labels / table headers β†’ text-micro (uppercase, letter-spaced)
  • Stat numbers β†’ text-display with --text-primary, label below in text-micro + --text-muted
  • Never bold body copy β€” weight is reserved for headings and stat numbers only

4. Spacing System

Uses an 8px base grid. Only these values:

4px   β€” xs  (icon gap, badge padding)
8px   β€” sm  (component internal padding)
12px  β€” md  (card internal padding small)
16px  β€” lg  (card internal padding, row gap)
24px  β€” xl  (section gap, card gap)
32px  β€” 2xl (major section gap)
48px  β€” 3xl (page section gap)
64px  β€” 4xl (hero spacing)
Enter fullscreen mode Exit fullscreen mode

Tailwind equivalents: p-1 p-2 p-3 p-4 p-6 p-8 p-12 p-16


5. Border Radius

4px  β€” badges, tags, small pills         (rounded)
6px  β€” buttons, inputs                   (rounded-md)
8px  β€” cards, panels, dropdowns          (rounded-lg)
12px β€” large cards, modals               (rounded-xl)
50%  β€” avatar circles, status dots only  (rounded-full)
Enter fullscreen mode Exit fullscreen mode

Never use rounded-3xl or rounded-2xl β€” too soft for a security tool.


6. Shadow System

ARGUS uses no decorative shadows. The only shadow is the ambient glow on important interactive elements:

/* Red glow β€” only on active campaign nodes, primary buttons on hover */
--shadow-red: 0 0 20px rgba(192, 22, 43, 0.15), 0 0 60px rgba(192, 22, 43, 0.05);

/* Subtle elevation β€” modals, dropdowns */
--shadow-elevation: 0 4px 24px rgba(0, 0, 0, 0.8), 0 1px 4px rgba(0, 0, 0, 0.6);
Enter fullscreen mode Exit fullscreen mode

All card borders are done with border: 1px solid var(--border-default) β€” not shadows.


7. Animation Tokens

--duration-instant:  80ms;
--duration-fast:     150ms;
--duration-base:     250ms;
--duration-slow:     400ms;
--duration-enter:    600ms;

--ease-out:   cubic-bezier(0.0, 0.0, 0.2, 1);    /* Elements entering */
--ease-in:    cubic-bezier(0.4, 0.0, 1.0, 1);    /* Elements leaving */
--ease-inout: cubic-bezier(0.4, 0.0, 0.2, 1);    /* In-place transitions */
--ease-spring:cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Bounce β€” rare */
Enter fullscreen mode Exit fullscreen mode

Animation Inventory (every animation in the app, defined once here)

Name Duration Easing Trigger Description
page-enter 250ms ease-out Route change opacity: 0→1, translateY: 8px→0
card-hover 150ms ease-out Hover border-color transition to --border-strong
live-pulse 2s ease-in-out Always (live dot) opacity 1β†’0.3β†’1, scale 1β†’0.7β†’1
ioc-row-enter 400ms ease-out New IOC via WS opacity 0→1, translateX: -8px→0, bg flash --red-900→transparent
ioc-row-highlight 3s linear After enter Background fades from --red-900 to transparent over 3 seconds
count-up 600ms ease-out Data load Numeric counter from previous value to new value via requestAnimationFrame
skeleton-pulse 1.5s ease-in-out Loading opacity 0.4β†’0.8β†’0.4
slide-in-right 300ms ease-out Panel open translateX: 20px→0, opacity 0→1
slide-out-right 250ms ease-in Panel close translateX: 0β†’20px, opacity 1β†’0
review-exit 300ms ease-in After decision translateX: 0β†’-100%, opacity 1β†’0
orbit-r1 14s linear Always Orbital motion, ring 1 (hero page only)
orbit-r2 20s linear Always Orbital motion, ring 2 (hero page only)
orbit-r3 28s linear Always Orbital motion, ring 3 (hero page only)
confidence-fill 600ms ease-out Data load Width 0β†’N% on confidence bars
graph-node-enter 400ms ease-spring Node add scale 0β†’1 on React Flow node mount

Animation Rules

  • Never animate layout properties (width, height, top, left). Only transform and opacity.
  • Respect prefers-reduced-motion: All animations must check this and skip if set.
  • Orbital animations are CSS-only (no JS) for performance.
  • count-up uses requestAnimationFrame in the useCountUp hook β€” not CSS.
  • IOC row highlight: uses a CSS class toggled by JS, not inline styles.
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

8. Component Tokens (Tailwind Config)

Complete tailwind.config.js β€” copy this exactly:

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,jsx}'],
  theme: {
    extend: {
      colors: {
        // Page backgrounds
        'bg-page':     '#000000',
        'bg-surface':  '#0a0608',
        'bg-elevated': '#110a0c',
        'bg-subtle':   '#1a0d0f',

        // Borders
        'border-default': '#1e1014',
        'border-strong':  '#2a1518',
        'border-accent':  '#5a0d1a',

        // Text
        'text-primary':   '#ffffff',
        'text-secondary': '#a0a0a0',
        'text-muted':     '#555555',

        // ARGUS Red
        'red': {
          900: '#2d0008',
          800: '#5a0d1a',
          700: '#8b1224',
          600: '#a8142a',
          500: '#c0162b',
          400: '#d63348',
          300: '#e05068',
          200: '#f0909e',
        },

        // Semantic
        'green':  { 500: '#16c784', bg: 'rgba(22,199,132,0.08)' },
        'amber':  { 500: '#f59e0b', bg: 'rgba(245,158,11,0.08)' },
        'blue':   { 500: '#3b82f6', bg: 'rgba(59,130,246,0.08)' },
        'purple': { 500: '#8b5cf6', bg: 'rgba(139,92,246,0.08)' },

        // Graph nodes
        'node': {
          ip:       '#c0162b',
          domain:   '#3b82f6',
          hash:     '#f59e0b',
          url:      '#8b5cf6',
          campaign: '#ffffff',
          cve:      '#16c784',
        },
      },

      fontFamily: {
        sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
        mono: ['JetBrains Mono', 'Fira Code', 'Consolas', 'monospace'],
      },

      fontSize: {
        'hero':    ['56px', { lineHeight: '1.0',  letterSpacing: '-0.04em', fontWeight: '700' }],
        'display': ['36px', { lineHeight: '1.1',  letterSpacing: '-0.03em', fontWeight: '700' }],
        'title':   ['22px', { lineHeight: '1.2',  letterSpacing: '-0.02em', fontWeight: '600' }],
        'heading': ['16px', { lineHeight: '1.3',  letterSpacing: '-0.01em', fontWeight: '600' }],
        'body':    ['14px', { lineHeight: '1.6',  letterSpacing: '0' }],
        'small':   ['12px', { lineHeight: '1.5',  letterSpacing: '0' }],
        'micro':   ['11px', { lineHeight: '1.4',  letterSpacing: '0.06em', fontWeight: '500' }],
      },

      borderRadius: {
        'tag':    '4px',
        'btn':    '6px',
        'card':   '8px',
        'modal':  '12px',
      },

      animation: {
        'live-pulse':      'livePulse 2s ease-in-out infinite',
        'skeleton':        'skeleton 1.5s ease-in-out infinite',
        'page-enter':      'pageEnter 250ms ease-out forwards',
        'slide-in-right':  'slideInRight 300ms ease-out forwards',
        'slide-out-right': 'slideOutRight 250ms ease-in forwards',
        'count-up':        'none', // Handled in JS
        'orbit-r1':        'orbitR1 14s linear infinite',
        'orbit-r2':        'orbitR2 20s linear infinite',
        'orbit-r3':        'orbitR3 28s linear infinite',
        'ioc-highlight':   'iocHighlight 3s ease-out forwards',
        'graph-node':      'graphNode 400ms cubic-bezier(0.175,0.885,0.32,1.275) forwards',
      },

      keyframes: {
        livePulse: {
          '0%, 100%': { opacity: '1', transform: 'scale(1)' },
          '50%':      { opacity: '0.3', transform: 'scale(0.7)' },
        },
        skeleton: {
          '0%, 100%': { opacity: '0.4' },
          '50%':      { opacity: '0.8' },
        },
        pageEnter: {
          from: { opacity: '0', transform: 'translateY(8px)' },
          to:   { opacity: '1', transform: 'translateY(0)' },
        },
        slideInRight: {
          from: { opacity: '0', transform: 'translateX(20px)' },
          to:   { opacity: '1', transform: 'translateX(0)' },
        },
        slideOutRight: {
          from: { opacity: '1', transform: 'translateX(0)' },
          to:   { opacity: '0', transform: 'translateX(20px)' },
        },
        iocHighlight: {
          '0%':   { backgroundColor: 'rgba(192,22,43,0.12)' },
          '100%': { backgroundColor: 'transparent' },
        },
        graphNode: {
          from: { opacity: '0', transform: 'scale(0)' },
          to:   { opacity: '1', transform: 'scale(1)' },
        },
        orbitR1: {
          from: { transform: 'rotate(0deg)' },
          to:   { transform: 'rotate(360deg)' },
        },
        orbitR2: {
          from: { transform: 'rotate(0deg)' },
          to:   { transform: 'rotate(360deg)' },
        },
        orbitR3: {
          from: { transform: 'rotate(0deg)' },
          to:   { transform: 'rotate(360deg)' },
        },
      },

      boxShadow: {
        'red':       '0 0 20px rgba(192,22,43,0.15), 0 0 60px rgba(192,22,43,0.05)',
        'elevation': '0 4px 24px rgba(0,0,0,0.8), 0 1px 4px rgba(0,0,0,0.6)',
        'card':      'inset 0 0 0 1px rgba(255,255,255,0.04)',
      },
    },
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

9. Shared CSS Classes (globals.css)

/* argus/frontend/src/globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  * { box-sizing: border-box; }

  html { background: #000; color: #fff; }

  body {
    font-family: 'Inter', -apple-system, sans-serif;
    background: #000;
    color: #fff;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  /* Scrollbar styling */
  ::-webkit-scrollbar { width: 4px; height: 4px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb { background: #2a1518; border-radius: 2px; }
  ::-webkit-scrollbar-thumb:hover { background: #c0162b; }

  /* Selection */
  ::selection { background: rgba(192,22,43,0.3); color: #fff; }
}

@layer components {
  /* Card primitive */
  .argus-card {
    @apply bg-bg-surface border border-border-default rounded-card;
  }

  /* Badge primitives β€” one per IOC type */
  .badge-ipv4   { @apply bg-red-500/10 text-red-300 font-mono text-micro px-2 py-0.5 rounded-tag; }
  .badge-domain { @apply bg-blue-500/10 text-blue-500 font-mono text-micro px-2 py-0.5 rounded-tag; }
  .badge-hash   { @apply bg-amber-500/10 text-amber-500 font-mono text-micro px-2 py-0.5 rounded-tag; }
  .badge-url    { @apply bg-purple-500/10 text-purple-500 font-mono text-micro px-2 py-0.5 rounded-tag; }
  .badge-cve    { @apply bg-green-bg text-green-500 font-mono text-micro px-2 py-0.5 rounded-tag; }

  /* Status badges */
  .badge-live     { @apply bg-green-bg text-green-500 text-micro px-2 py-0.5 rounded-tag; }
  .badge-warning  { @apply bg-amber-bg text-amber-500 text-micro px-2 py-0.5 rounded-tag; }
  .badge-danger   { @apply bg-red-900/50 text-red-300 text-micro px-2 py-0.5 rounded-tag; }
  .badge-neutral  { @apply bg-bg-subtle text-text-muted text-micro px-2 py-0.5 rounded-tag; }

  /* MITRE technique tag */
  .mitre-tag {
    @apply font-mono text-micro bg-red-900/60 text-red-300 border border-red-800
           px-2 py-0.5 rounded-tag cursor-default;
  }

  /* Primary button */
  .btn-primary {
    @apply bg-red-500 hover:bg-red-400 text-white font-sans text-small font-semibold
           px-4 py-2 rounded-btn transition-colors duration-fast cursor-pointer
           border-0 outline-none;
  }

  /* Secondary button */
  .btn-secondary {
    @apply bg-transparent hover:bg-bg-elevated text-text-secondary hover:text-text-primary
           font-sans text-small font-medium px-4 py-2 rounded-btn
           border border-border-default hover:border-border-strong
           transition-all duration-fast cursor-pointer outline-none;
  }

  /* Ghost button */
  .btn-ghost {
    @apply bg-transparent hover:bg-bg-elevated text-text-muted hover:text-text-secondary
           font-sans text-small px-3 py-1.5 rounded-btn
           transition-all duration-fast cursor-pointer outline-none border-0;
  }

  /* Live dot */
  .live-dot {
    @apply w-1.5 h-1.5 rounded-full bg-green-500 inline-block animate-live-pulse;
  }

  /* Skeleton loader */
  .skeleton {
    @apply bg-bg-subtle rounded animate-skeleton;
  }

  /* Table styles */
  .argus-table th {
    @apply text-micro text-text-muted uppercase tracking-widest
           border-b border-border-default pb-2 text-left font-medium;
  }
  .argus-table td {
    @apply text-body text-text-secondary py-3
           border-b border-border-default/50;
  }
  .argus-table tr:hover td {
    @apply bg-bg-elevated;
  }

  /* Confidence bar */
  .confidence-bar-track {
    @apply w-full h-1 bg-bg-subtle rounded-full overflow-hidden;
  }
  .confidence-bar-fill {
    @apply h-full rounded-full transition-all duration-slow;
  }

  /* Monospace value (IOC, hash, IP) */
  .ioc-value {
    @apply font-mono text-mono text-text-secondary
           hover:text-text-primary transition-colors duration-fast
           cursor-default select-all;
  }
}
Enter fullscreen mode Exit fullscreen mode

Part 5: Technical Contracts


Extracted from: docs/05_DATA_CONTRACTS.md

ARGUS Data Contracts Reference πŸ“„

The canonical truth for all inter-agent message formats

This file is the single source of truth for every data shape that flows between agents.
When in doubt, check here. If a schema here conflicts with code, fix the code.


Redis Stream Map

External Feeds
     β”‚
     β–Ό STIX 2.1 Bundle (JSON)
stream:raw_iocs
     β”œβ”€β”€ consumer group: enrichment    β†’ EnrichmentAgent
     └── consumer group: audit         β†’ Audit logger

     β”‚
     β–Ό EnrichedIOC (JSON)
stream:enriched_iocs
     β”œβ”€β”€ consumer group: correlation   β†’ CorrelationAgent
     └── consumer group: vector_indexer β†’ Vector Indexer Worker

     β”‚
     β–Ό CorrelationHypothesis (JSON)
stream:correlation_hypotheses
     └── consumer group: mitre         β†’ MITREMappingAgent

     β”‚
     β–Ό ATTACKAnnotation (JSON)
stream:mitre_annotated
     └── consumer group: narrative     β†’ ThreatNarrativeAgent

     β”‚
     β”œβ”€ [escalation_required=True] β†’ stream:human_review
     β”‚       └── consumer group: orchestrator β†’ OrchestratorAgent
     β”‚
     └─ [escalation_required=False] β†’ stream:threat_reports
             └── consumer group: reporting β†’ PDF Generator

stream:feedback
     └── consumer group: learning      β†’ FeedbackLearningAgent

stream:dlq:*
     └── Dead-letter queues per agent (manual retry)
Enter fullscreen mode Exit fullscreen mode

All Stream Message Envelopes

Every message published to any Redis Stream MUST include these fields:

{
  "schema_version": "1.0",
  "trace_id": "uuid4-string",
  "created_at": "2024-01-15T10:23:00.000Z",
  "payload": { ... }
}
Enter fullscreen mode Exit fullscreen mode

The trace_id is generated by IngestorAgent and passed through every subsequent message.
This enables tracing a single IOC through the entire pipeline in logs.


Schema 1 β€” RawIOC (stream:raw_iocs)

Published by: IngestorAgent

Consumed by: EnrichmentAgent, Audit Logger

{
  "schema_version": "1.0",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-01-15T10:23:00.000Z",
  "ioc_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "ioc_type": "ipv4",
  "value": "192.0.2.1",
  "source_feed": "feodo",
  "source_feed_url": "https://feodotracker.abuse.ch/downloads/ipblocklist.json",
  "ingested_at": "2024-01-15T10:23:00.000Z",
  "is_refresh": false,
  "stix_object": {
    "type": "indicator",
    "spec_version": "2.1",
    "id": "indicator--6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "created": "2024-01-15T10:23:00.000Z",
    "modified": "2024-01-15T10:23:00.000Z",
    "name": "Malicious IPv4: 192.0.2.1",
    "indicator_types": ["malicious-activity"],
    "pattern": "[ipv4-addr:value = '192.0.2.1']",
    "pattern_type": "stix",
    "valid_from": "2024-01-15T10:23:00.000Z",
    "labels": ["botnet", "emotet"],
    "external_references": [
      {
        "source_name": "feodo_tracker",
        "url": "https://feodotracker.abuse.ch/browse/host/192.0.2.1/"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Valid ioc_type values: ipv4 | ipv6 | domain | url | md5 | sha1 | sha256 | cve | email


Schema 2 β€” EnrichedIOC (stream:enriched_iocs)

Published by: EnrichmentAgent

Consumed by: CorrelationAgent, Vector Indexer

{
  "schema_version": "1.0",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-01-15T10:23:45.000Z",
  "ioc_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "ioc_type": "ipv4",
  "value": "192.0.2.1",
  "source_feed": "feodo",
  "ingested_at": "2024-01-15T10:23:00.000Z",

  "vt_enriched": true,
  "vt_stats": {
    "malicious": 52,
    "suspicious": 3,
    "undetected": 15,
    "harmless": 5,
    "total_engines": 75
  },
  "vt_detection_ratio": 0.693,

  "otx_enriched": true,
  "otx_pulse_count": 12,
  "otx_tags": ["emotet", "botnet", "c2"],
  "otx_malware_families": ["Emotet", "QakBot"],

  "urlhaus_enriched": true,
  "urlhaus_status": "online",
  "urlhaus_threat": "malware_download",

  "circl_enriched": false,
  "circl_cvss_score": null,
  "circl_cwe": null,

  "shodan_enriched": true,
  "shodan_ports": [80, 443, 8080],
  "shodan_vulns": [],
  "shodan_tags": ["self-signed"],

  "geo": {
    "country": "Netherlands",
    "country_code": "NL",
    "asn": 60781,
    "asn_org": "LeaseWeb Netherlands B.V.",
    "city": "Amsterdam"
  },

  "freshness_score": 0.95,
  "confidence_score": 0.82,
  "source_count": 3,
  "malware_family": "Emotet",
  "tags": ["emotet", "botnet", "c2", "feodo"],
  "campaign_id": null,
  "inference_tier": null
}
Enter fullscreen mode Exit fullscreen mode

Schema 3 β€” CorrelationHypothesis (stream:correlation_hypotheses)

Published by: CorrelationAgent

Consumed by: MITREMappingAgent

{
  "schema_version": "1.0",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-01-15T10:24:00.000Z",
  "hypothesis_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "campaign_id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
  "campaign_is_new": false,
  "ioc_ids": [
    "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "6ba7b814-9dad-11d1-80b4-00c04fd430c8"
  ],
  "shared_signals": [
    {"type": "asn", "value": "AS60781", "weight": 0.6},
    {"type": "malware_family", "value": "Emotet", "weight": 0.9},
    {"type": "tag", "value": "c2", "weight": 0.5}
  ],
  "similarity_score": 0.87,
  "graph_edges": [
    {
      "src_node_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
      "src_node_type": "ip",
      "dst_node_id": "6ba7b814-9dad-11d1-80b4-00c04fd430c8",
      "dst_node_type": "ip",
      "relationship": "SHARES_ASN",
      "weight": 0.6,
      "evidence": "Both in AS60781 (LeaseWeb Netherlands)"
    }
  ],
  "confidence": 0.79
}
Enter fullscreen mode Exit fullscreen mode

Schema 4 β€” ATTACKAnnotation (stream:mitre_annotated)

Published by: MITREMappingAgent

Consumed by: ThreatNarrativeAgent

{
  "schema_version": "1.0",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-01-15T10:24:30.000Z",
  "campaign_id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
  "hypothesis_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "techniques": [
    {
      "technique_id": "T1071.001",
      "technique_name": "Application Layer Protocol: Web Protocols",
      "tactic": "command-and-control",
      "kill_chain_phase": "Command & Control",
      "confidence": 0.88,
      "rationale": "C2 traffic observed on port 443 matching HTTPS patterns in Emotet campaigns"
    },
    {
      "technique_id": "T1566.001",
      "technique_name": "Phishing: Spearphishing Attachment",
      "tactic": "initial-access",
      "kill_chain_phase": "Delivery",
      "confidence": 0.72,
      "rationale": "Malicious email attachments observed in associated URLhaus entries"
    }
  ],
  "kill_chain_phases_observed": ["Delivery", "Command & Control"],
  "navigator_layer_json": {
    "name": "ARGUS β€” Campaign a0eebc99",
    "versions": {"attack": "15", "navigator": "4.9", "layer": "4.5"},
    "domain": "enterprise-attack",
    "techniques": [...]
  },
  "inference_tier": "groq_70b",
  "technique_count": 2
}
Enter fullscreen mode Exit fullscreen mode

Schema 5 β€” ThreatReport (stream:threat_reports / stream:human_review)

Published by: ThreatNarrativeAgent

Consumed by: PDF Generator / OrchestratorAgent

{
  "schema_version": "1.0",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-01-15T10:25:00.000Z",
  "report_id": "b3c45d67-8901-23ef-gh45-678901234567",
  "campaign_id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
  "executive_summary": "A financially-motivated threat actor is operating...",
  "technical_narrative": "The campaign was first detected on...",
  "kill_chain_timeline": [
    {
      "timestamp": "2024-01-10T08:30:00Z",
      "ioc_type": "domain",
      "ioc_value": "evil-domain.com",
      "kill_chain_phase": "Delivery",
      "technique_id": "T1566.001",
      "technique_name": "Phishing: Spearphishing Attachment",
      "malware_family": "Emotet",
      "edge_type": null,
      "related_value": null
    }
  ],
  "prediction": {
    "predicted_phase": "Installation",
    "likely_techniques": [
      {
        "technique_id": "T1053.005",
        "technique_name": "Scheduled Task/Job: Scheduled Task",
        "probability": 0.78,
        "rationale": "Common Emotet persistence mechanism following C2 establishment"
      }
    ],
    "rationale": "Based on observed Delivery β†’ C2 progression...",
    "confidence": 0.71
  },
  "recommended_mitigations": [
    "Block outbound HTTPS to non-categorised domains at perimeter firewall (NIST CSF PR.AC-5)",
    "Disable macro execution in Office applications via Group Policy (NIST CSF PR.PT-3)"
  ],
  "overall_confidence": 0.74,
  "escalation_required": false,
  "stix_report_object": {
    "type": "report",
    "spec_version": "2.1",
    "id": "report--b3c45d67-8901-23ef-gh45-678901234567",
    "name": "ARGUS Intelligence Report: Campaign a0eebc99",
    "report_types": ["threat-actor", "campaign"],
    "published": "2024-01-15T10:25:00.000Z",
    "object_refs": ["indicator--...", "indicator--..."],
    "custom_properties": {
      "x_argus_confidence": 0.74,
      "x_argus_predicted_phase": "Installation",
      "x_argus_escalation_required": false
    }
  },
  "inference_tier": "groq_70b"
}
Enter fullscreen mode Exit fullscreen mode

Schema 6 β€” PredictionValidationEvent (stream:feedback)

Published by: PredictionValidator (part of CorrelationAgent)

Also published by: Human review endpoint (POST /reviews/{id}/decision)

Consumed by: FeedbackLearningAgent

{
  "schema_version": "1.0",
  "trace_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-01-16T14:30:00.000Z",
  "event_id": "c4d56e78-9012-34fg-hi56-789012345678",
  "event_type": "prediction_validated",
  "prediction_id": "d5e67f89-0123-45gh-ij67-890123456789",
  "campaign_id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
  "outcome": "confirmed",
  "matching_ioc_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "matching_technique_id": "T1053.005",
  "confidence_at_prediction": 0.71,
  "source": "automated"
}
Enter fullscreen mode Exit fullscreen mode

event_type values:

  • prediction_validated β€” new IOC matched prediction (automated)
  • prediction_falsified β€” campaign archived without prediction confirmed (automated)
  • analyst_override β€” analyst corrected the prediction (human)
  • analyst_false_positive β€” analyst flagged entire campaign as FP (human)
  • analyst_approve β€” analyst approved low-confidence report (human)

Redis Key Conventions

All Redis keys follow this pattern: argus:{namespace}:{identifier}

## Deduplication πŸ“„
argus:dedup:{sha256(ioc_type + ":" + value)}   TTL: 86400s (24h)

## Rate limiting πŸ“„
argus:ratelimit:{api_name}                      # Sorted set (score = epoch ms)
argus:daily_count:{api_name}:{YYYY-MM-DD}       TTL: 86400s

## Groq token budget πŸ“„
argus:groq:tokens:minute                        TTL: 60s (auto-resets each minute)
argus:groq:fallback_active                      TTL: 60s (set by OrchestratorAgent)

## Campaign tracking πŸ“„
argus:campaign:{campaign_id}:pending_predictions  # Set of prediction UUIDs

## Orchestrator state πŸ“„
argus:orchestrator:last_ab_check                # Timestamp float
argus:orchestrator:worker_count:{queue_name}    # Current worker count

## Health πŸ“„
argus:health:last_orchestrator_cycle            # Timestamp (dead-man's switch)
Enter fullscreen mode Exit fullscreen mode

Neo4j Node Labels and Properties

(:IOC)
  ioc_id: UUID (unique)
  value: string
  ioc_type: string
  confidence: float
  first_seen: datetime
  last_seen: datetime
  malware_family: string?
  campaign_id: UUID?
  source_feed: string
  subnet_24: string?     # For IPv4: first 3 octets (e.g. "192.0.2")
  registrar: string?     # For domains
  registered_date: date? # For domains
  imphash: string?       # For PE hashes

(:IP) extends (:IOC) β€” label applied to ipv4/ipv6 IOCs
(:Domain) extends (:IOC)
(:Hash) extends (:IOC)
(:URL) extends (:IOC)

(:Campaign)
  campaign_id: UUID (unique)
  ioc_count: int
  first_seen: datetime
  last_seen: datetime
  kill_chain_phases: list[string]
  top_malware_family: string?
  confidence_score: float
Enter fullscreen mode Exit fullscreen mode

Neo4j Relationship Types

(:IOC)-[:BELONGS_TO]->(:Campaign)
(:URL)-[:RESOLVES_TO]->(:Domain)
(:Domain)-[:RESOLVES_TO]->(:IP)
(:IP)-[:SHARES_SUBNET {weight: float}]->(:IP)
(:Domain)-[:SHARES_REGISTRAR {weight: float}]->(:Domain)
(:Hash)-[:SHARES_IMPHASH {weight: float}]->(:Hash)
(:IOC)-[:DELIVERS]->(:Hash)
(:Hash)-[:COMMUNICATES_WITH]->(:IP)
(:Campaign)-[:TARGETS]->(:Sector)   # future β€” when sector info available
Enter fullscreen mode Exit fullscreen mode

PostgreSQL Foreign Key Map

campaigns (campaign_id) ◄─── iocs (campaign_id)
campaigns (campaign_id) ◄─── threat_reports (campaign_id)
campaigns (campaign_id) ◄─── predictions (campaign_id)
threat_reports (report_id) ◄─── predictions (report_id)
agent_state (agent_id) ◄─── agent_prompt_versions (agent_id)
Enter fullscreen mode Exit fullscreen mode

STIX 2.1 Pattern Reference

Quick reference for writing correct STIX patterns:

IPv4:     [ipv4-addr:value = '1.2.3.4']
IPv6:     [ipv6-addr:value = '::1']
Domain:   [domain-name:value = 'evil.com']
URL:      [url:value = 'http://evil.com/payload']
MD5:      [file:hashes.MD5 = 'abc123def456']
SHA1:     [file:hashes.SHA-1 = 'abc123']
SHA256:   [file:hashes.SHA-256 = 'abc123']
CVE:      [vulnerability:name = 'CVE-2024-1234']
Email:    [email-message:from_ref.value = 'phish@evil.com']
Enter fullscreen mode Exit fullscreen mode

Compound patterns (AND):

[ipv4-addr:value = '1.2.3.4' AND network-traffic:dst_port = 443]
Enter fullscreen mode Exit fullscreen mode

Confidence Score Formula

overall_confidence = (
    mitre_technique_confidence_avg * 0.35 +
    correlation_hypothesis_confidence * 0.30 +
    enrichment_coverage_ratio * 0.20 +
    ioc_count_score * 0.15
)

Where:
  mitre_technique_confidence_avg = mean(technique.confidence for technique in techniques)
  enrichment_coverage_ratio = enriched_ioc_count / total_ioc_count
  ioc_count_score = min(1.0, log10(ioc_count + 1) / 2)
    # reaches 0.5 at 10 IOCs, 1.0 at 100 IOCs

Escalation threshold: overall_confidence < 0.65 β†’ route to human_review
Enter fullscreen mode Exit fullscreen mode

LLM Routing Decision Tree

Is task_complexity == "simple" AND Groq fallback NOT active?
  YES β†’ Use Groq llama-3.1-8b-instant (fast, lower cost)
  NO β†’
    Is task_complexity == "complex" AND Groq fallback NOT active?
      YES β†’ Use Groq llama-3.1-70b-versatile (best quality)
      NO β†’ Use Ollama llama3.1:8b-instruct-q4_K_M (local, no rate limit)
            Tag output with inference_tier: "ollama_local"

Groq fallback active when:
  argus:groq:fallback_active key exists in Redis (set by OrchestratorAgent)
  This key has TTL=60s β€” auto-resets each minute
  OrchestratorAgent sets it when groq:tokens:minute > GROQ_TOKEN_BUDGET_PER_MINUTE
Enter fullscreen mode Exit fullscreen mode

Extracted from: docs/07_IMPLEMENTATION_CONTRACTS.md

ARGUS Implementation Contracts πŸ“„

The precise specifications for undocumented implementations


Section A β€” SQLAlchemy ORM Models (Complete)

File: argus/storage/postgres.py

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import declarative_base, Mapped, mapped_column, relationship
from sqlalchemy import String, Integer, Float, Boolean, JSON, DateTime, ForeignKey, Index, ARRAY
from sqlalchemy.dialects.postgresql import UUID, JSONB
from datetime import datetime, timezone
import uuid
from argus.config import settings

engine = create_async_engine(
    settings.postgres_url,
    pool_size=10,
    max_overflow=5,
    pool_timeout=30,
    pool_pre_ping=True,
    prepared_statement_cache_size=0, # Prevents InvalidCachedStatementError on migrations
)

AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)

Base = declarative_base()

async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

class IOC(Base):
    __tablename__ = "iocs"
    ioc_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    ioc_type: Mapped[str] = mapped_column(String(20), nullable=False)
    value: Mapped[str] = mapped_column(String, nullable=False)
    value_hash: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
    source_feed: Mapped[str] = mapped_column(String(100), nullable=False)
    source_count: Mapped[int] = mapped_column(Integer, default=1)
    ingested_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
    last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
    freshness_score: Mapped[float] = mapped_column(Float, default=1.0)
    confidence_score: Mapped[float] = mapped_column(Float, default=0.5)
    enriched_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
    campaign_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("campaigns.campaign_id"), nullable=True)
    malware_family: Mapped[str] = mapped_column(String(200), nullable=True)
    tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
    stix_object: Mapped[dict] = mapped_column(JSONB, nullable=False)
    enrichment_data: Mapped[dict] = mapped_column(JSONB, default=dict)
    vt_detection_ratio: Mapped[float] = mapped_column(Float, nullable=True)
    vt_enriched: Mapped[bool] = mapped_column(Boolean, default=False)
    otx_pulse_count: Mapped[int] = mapped_column(Integer, default=0)
    shodan_ports: Mapped[list[int]] = mapped_column(ARRAY(Integer), default=list)
    geo_country: Mapped[str] = mapped_column(String(100), nullable=True)
    geo_asn_org: Mapped[str] = mapped_column(String(200), nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))

    # Relationships
    campaign = relationship("Campaign", back_populates="iocs")

class Feed(Base):
    __tablename__ = "feeds"
    feed_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    feed_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
    feed_url: Mapped[str] = mapped_column(String, nullable=False)
    feed_type: Mapped[str] = mapped_column(String(50), nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    poll_interval_secs: Mapped[int] = mapped_column(Integer, default=300)
    last_polled: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
    last_novel_ratio: Mapped[float] = mapped_column(Float, default=0.0)
    consecutive_failures: Mapped[int] = mapped_column(Integer, default=0)
    total_iocs_ingested: Mapped[int] = mapped_column(Integer, default=0)

class Campaign(Base):
    __tablename__ = "campaigns"
    campaign_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    ioc_count: Mapped[int] = mapped_column(Integer, default=0)
    first_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
    last_seen: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
    kill_chain_phases: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
    mitre_techniques: Mapped[dict] = mapped_column(JSONB, default=list)
    navigator_layer: Mapped[dict] = mapped_column(JSONB, nullable=True)
    top_malware_family: Mapped[str] = mapped_column(String(200), nullable=True)
    confidence_score: Mapped[float] = mapped_column(Float, default=0.5)

    iocs = relationship("IOC", back_populates="campaign")
    reports = relationship("ThreatReport", back_populates="campaign")

class ThreatReport(Base):
    __tablename__ = "threat_reports"
    report_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    campaign_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("campaigns.campaign_id"), nullable=False)
    executive_summary: Mapped[str] = mapped_column(String, nullable=True)
    technical_narrative: Mapped[str] = mapped_column(String, nullable=True)
    kill_chain_timeline: Mapped[dict] = mapped_column(JSONB, default=list)
    recommended_mitigations: Mapped[list[str]] = mapped_column(ARRAY(String), default=list)
    overall_confidence: Mapped[float] = mapped_column(Float, nullable=True)
    escalation_required: Mapped[bool] = mapped_column(Boolean, default=False)
    stix_report_object: Mapped[dict] = mapped_column(JSONB, nullable=True)
    inference_tier: Mapped[str] = mapped_column(String(50), nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))

    campaign = relationship("Campaign", back_populates="reports")

class Prediction(Base):
    __tablename__ = "predictions"
    prediction_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    report_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("threat_reports.report_id"), nullable=True)
    campaign_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("campaigns.campaign_id"), nullable=False)
    predicted_phase: Mapped[str] = mapped_column(String(100), nullable=False)
    predicted_techniques: Mapped[dict] = mapped_column(JSONB, default=list, nullable=False)
    rationale: Mapped[str] = mapped_column(String, nullable=True)
    confidence: Mapped[float] = mapped_column(Float, nullable=False)
    outcome: Mapped[str] = mapped_column(String(20), default="pending")
    matching_ioc_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=True)
    validated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))

class AgentState(Base):
    __tablename__ = "agent_state"
    agent_id: Mapped[str] = mapped_column(String(100), primary_key=True)
    prompt_hash: Mapped[str] = mapped_column(String(64), nullable=True)
    current_prompt: Mapped[str] = mapped_column(String, nullable=True)
    f1_score: Mapped[float] = mapped_column(Float, default=0.5)
    predictions_made: Mapped[int] = mapped_column(Integer, default=0)
    predictions_confirmed: Mapped[int] = mapped_column(Integer, default=0)
    predictions_falsified: Mapped[int] = mapped_column(Integer, default=0)
    reputation_score: Mapped[float] = mapped_column(Float, default=0.7)
    last_updated: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))

Enter fullscreen mode Exit fullscreen mode

Section B β€” Celery Task Signatures (Complete)

All agent tasks must use this decorator pattern to ensure idempotency and prevent duplicate processing:

from celery import shared_task
from argus.celery_app import app
import redis.asyncio as redis

## Example for EnrichmentAgent πŸ“„
@app.task(
    bind=True,
    name="argus.agents.enrichment.run_enrichment_worker",
    max_retries=3,
    default_retry_delay=60,
    acks_late=True,                 # Critical: Only ack when fully processed
    reject_on_worker_lost=True,     # Critical: Re-queue if worker OOMs
    time_limit=300,                 # 5 minutes hard limit
    soft_time_limit=240             # 4 minutes soft limit to handle graceful shutdown
)
def run_enrichment_worker(self, message_batch):
    # Tasks must manually handle DLQ routing on permanent failure
    try:
        # Processing logic...
        pass
    except Exception as exc:
        # PUSH to stream:dlq:enrichment
        self.retry(exc=exc)
Enter fullscreen mode Exit fullscreen mode

Note: We handle standard async loops via asgiref.sync.async_to_sync or manually driving the loop.


Section C β€” Startup/Shutdown Lifecycle (Complete)

FastAPI Lifespan (argus/api/main.py)

from contextlib import asynccontextmanager
from fastapi import FastAPI
from argus.storage.postgres import engine
import argus.storage.redis_client as redis_client

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    await redis_client.init_streams() # Idempotent stream/group creation
    # Initialize Neo4j driver constraints
    # Start Prometheus metrics server
    from prometheus_client import start_http_server
    start_http_server(9090)
    yield
    # Shutdown
    await engine.dispose()
    # await neo4j_driver.close()
Enter fullscreen mode Exit fullscreen mode

Celery Worker Lifecycle

from celery.signals import worker_process_init, worker_process_shutdown

@worker_process_init.connect
def init_worker(**kwargs):
    # Initialize DB engines specific to this process
    # Pre-load sentence-transformers model to GPU/CPU memory
    pass

@worker_process_shutdown.connect
def shutdown_worker(**kwargs):
    # Close connections gracefully
    pass
Enter fullscreen mode Exit fullscreen mode

Section D β€” Missing File Specifications

1. argus/schemas/messages.py

from pydantic import BaseModel, UUID4, Field
from datetime import datetime, timezone
import uuid

class StreamEnvelope(BaseModel):
    schema_version: str = "1.0"
    trace_id: UUID4 = Field(default_factory=uuid.uuid4)
    created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    payload: dict
Enter fullscreen mode Exit fullscreen mode

2. scripts/init_streams.py

import asyncio
import redis.asyncio as redis
from argus.config import settings

STREAMS = {
    "stream:raw_iocs": ["enrichment", "audit"],
    "stream:enriched_iocs": ["correlation", "vector_indexer"],
    "stream:correlation_hypotheses": ["mitre"],
    "stream:mitre_annotated": ["narrative"],
    "stream:human_review": ["orchestrator"],
    "stream:threat_reports": ["reporting"], # Fixed blocker
    "stream:feedback": ["learning"],
}

async def init_streams():
    client = redis.from_url(settings.redis_url)
    for stream_name, groups in STREAMS.items():
        for group in groups:
            try:
                await client.xgroup_create(stream_name, group, id="0", mkstream=True)
            except redis.exceptions.ResponseError as e:
                if "BUSYGROUP" not in str(e):
                    raise
    await client.aclose()

if __name__ == "__main__":
    asyncio.run(init_streams())
Enter fullscreen mode Exit fullscreen mode

3. scripts/health_check.py

import asyncio
import sys
import redis.asyncio as redis
from argus.config import settings

async def check():
    try:
        r = redis.from_url(settings.redis_url)
        await r.ping()
        await r.aclose()
        # Ping PG, Neo4j, Qdrant...
        sys.exit(0)
    except Exception as e:
        print(f"Healthcheck failed: {e}")
        sys.exit(1)

if __name__ == "__main__":
    asyncio.run(check())
Enter fullscreen mode Exit fullscreen mode

4. alembic/env.py (Async config)

import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from alembic import context
from argus.storage.postgres import Base
from argus.config import settings

target_metadata = Base.metadata

def do_run_migrations(connection):
    context.configure(connection=connection, target_metadata=target_metadata)
    with context.begin_transaction():
        context.run_migrations()

async def run_migrations_online():
    connectable = create_async_engine(settings.postgres_url)
    async with connectable.connect() as connection:
        await connection.run_sync(do_run_migrations)
    await connectable.dispose()

if context.is_offline_mode():
    pass # Add offline sync string generator
else:
    asyncio.run(run_migrations_online())
Enter fullscreen mode Exit fullscreen mode

5. data/grafana/datasources/prometheus.yaml

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://argus-api:9090
    isDefault: false
Enter fullscreen mode Exit fullscreen mode

Section E β€” Error Handling Contracts

Exception Type Source Handling Recovery
pydantic.ValidationError Any Stream Consumer Catch immediately before processing. Push raw payload to stream:dlq:{agent_name}. ACK message.
groq.RateLimitError LLM Router Catch in router.py. Set Redis argus:groq:fallback_active. Reroute seamlessly to Ollama local model.
aiohttp.ClientError Feed Ingestor Catch in fetch_raw(). Return []. Increment consecutive_failures in Postgres.
redis.ConnectionError Redis Client Unhandled (let crash). Docker Swarm/Compose will restart container.
qdrant_client.http.exceptions.UnexpectedResponse Correlation Agent Catch if dimensions mismatch. Drop/recreate collection and raise Exception to restart worker.

Extracted from: docs/08_WOW_ADDITIONS.md

ARGUS WOW Additions πŸ“„

Extraordinary features to elevate the ARGUS platform


WOW ADDITION 1 β€” Sigma Rule Auto-Generation

Description: ARGUS will automatically generate a deployable SIEM Sigma rule from the mapped MITRE ATT&CK techniques of a campaign. This allows Security Operations teams to instantly ingest ARGUS intelligence directly into their SOC rulebase.

Implementation Plan:

  1. Schema Definition: Define a Pydantic schema for Sigma rule output.
  2. Jinja2 Template (argus/reporting/templates/sigma_rule.yaml.j2):
title: "ARGUS Auto-Generated Rule: Campaign {{ campaign_id }}"
id: {{ rule_uuid }}
status: experimental
description: "Detects behaviour associated with Campaign {{ campaign_id }}. Auto-generated by ARGUS based on MITRE mappings."
author: ARGUS AI Agent
date: {{ current_date }}
tags:
{% for tech in techniques %}
  - attack.{{ tech.tactic }}
  - attack.t{{ tech.technique_id.replace('T', '') }}
{% endfor %}
logsource:
  category: network_connection
detection:
  selection:
    DestinationIp:
    {% for ip in mapped_ips %}
      - {{ ip }}
    {% endfor %}
  condition: selection
falsepositives:
  - Unknown
level: high
Enter fullscreen mode Exit fullscreen mode
  1. Endpoint (GET /campaigns/{campaign_id}/sigma): This endpoint dynamically queries the IOCs for the campaign, fills the Jinja2 template, and returns a raw application/x-yaml payload that can be piped into SIEM environments.

WOW ADDITION 2 β€” APT Group Attribution Matching

Description: Automatically match campaign techniques against known MITRE ATT&CK Intrusion Sets to suggest attribution (e.g. "This campaign exhibits 82% overlap with APT29 (Cozy Bear)").

Implementation Plan:

  1. MITRE Indexing: Extract intrusion-set objects from the local ATT&CK bundle. For each set, map its known attack-pattern (techniques) via relationship objects.
  2. Attribution Scoring (argus/agents/threat_narrative.py):
def compute_attribution(campaign_techniques, intrusion_sets):
    best_match = None
    highest_score = 0
    for apt, known_techniques in intrusion_sets.items():
        overlap = set([t.technique_id for t in campaign_techniques]).intersection(known_techniques)
        score = len(overlap) / len(campaign_techniques) if campaign_techniques else 0
        if score > highest_score and score > 0.5: # 50% overlap threshold
            highest_score = score
            best_match = apt
    return best_match, highest_score
Enter fullscreen mode Exit fullscreen mode
  1. Report Integration: Add "Attribution Hypothesis" to the Threat Report PDF.

WOW ADDITION 3 β€” IOC Decay and Archive System

Description: Implement principled, time-based decay of IOC confidence to prevent stale intelligence from falsely correlating new campaigns.

Implementation Plan:

  1. Decay Formulas (argus/storage/postgres.py background job):
    • IPs: Half-life of 7 days (IPs are recycled quickly).
    • Domains: Half-life of 30 days.
    • Hashes: Half-life of 180 days.
  2. Periodic Task: Add a Celery Beat task decay_ioc_confidence that runs daily.
    • Updates confidence_score = confidence_score * e^(-lambda * days_since_last_seen).
    • If confidence_score < 0.1, set status to archived and remove from Qdrant vector store.
  3. Grafana Panel: Add "IOC Decay Distribution" showing Active vs Archived over time.

WOW ADDITION 4 β€” TLP Traffic Light Protocol Classification

Description: Automatically classify Threat Reports using the TLP standard (WHITE/GREEN/AMBER/RED) based on source feed sensitivity and overall confidence.

Implementation Plan:

  1. Classification Rules (argus/agents/threat_narrative.py):
    • If any IOC originated from a proprietary/internal feed -> TLP:AMBER.
    • If overall_confidence > 0.85 and escalation not required -> TLP:GREEN.
    • If report contains high-confidence CVE exploits -> TLP:AMBER.
    • Default -> TLP:WHITE.
  2. STIX Integration: Inject the tlp marking definition into the stix_report_object.
  3. PDF Visuals: Update threat_report.html.j2 to dynamically change the cover classification badge color (e.g., Red background for TLP:RED).

WOW ADDITION 5 β€” Live Attack Surface Correlation

Description: Configure a .env variable with the user's infrastructure subnets. If ARGUS identifies an IOC matching this infrastructure, immediately trigger a critical alert.

Implementation Plan:

  1. Config (.env): USER_ASSET_RANGES="10.0.0.0/8,192.168.1.0/24"
  2. Correlation Logic (argus/agents/correlation.py):
    • During Graph Edge Detection, compare subnet_24 or raw IPs against USER_ASSET_RANGES using Python's ipaddress module.
    • If match found, create a [:TARGETS_USER_INFRASTRUCTURE] edge in Neo4j.
  3. Escalation: If this edge is created, immediately bypass confidence thresholds and set escalation_required = true with reason "YOUR ENVIRONMENT IS MENTIONED".
  4. Grafana Alert: Implement a prominent red alert panel driven by a query counting TARGETS_USER_INFRASTRUCTURE edges.

Top comments (0)