[!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
- Part 2: Core Architecture & Pipeline
- Part 3: Infrastructure & Backend
- Part 4: Frontend & UI Systems
- Part 5: Technical Contracts
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)
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
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
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
The production override:
- Closes all database ports (not exposed to host)
- Removes
--reloadand 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
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
Pipeline Stages
Ingestion (
ingestor.py) β Polls 7 external threat feeds every 60 seconds, normalizes IOCs to STIX 2.1, deduplicates, and publishes to Redis stream.Enrichment (
enrichment.py) β Consumes raw IOCs, queries external APIs in parallel (VirusTotal, OTX, Shodan, URLhaus, MalwareBazaar, CIRCL CVE), computes confidence scores, geo-locates IPs.Vector Indexing (
vector_indexer.py) β Creates 384-dimension sentence embeddings viaall-MiniLM-L6-v2and indexes IOCs in Qdrant for semantic similarity search.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).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.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.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
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 π
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:
EnrichmentAgentexecutes 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:
CorrelationAgentuses 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:
MITREMappingAgentutilizes 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:
ThreatNarrativeAgentanalyzes 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:
FeedbackLearningAgentandPredictionValidatormonitor 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
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 β
ββββββββββββββββββββββ
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
Agent Roles & Specifications
-
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.
-
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.
- Consumes messages from
-
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.
- Consumes
-
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.
- Consumes
-
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.
- Consumes
-
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.
-
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
);
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,
OrchestratorAgentpauses 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) β
βββββββββββββββββββ βββββββββββββββββββββββ βββββββββββββββββββββββββ
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
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"
}
]
}
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
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 β
βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ
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)
4. Geolocation Engine (geoip.py)
For IP indicators (ipv4, ipv6), geoip.py determines physical geographic origin:
-
Primary Lookup: Local MaxMind GeoLite2 binary database (
.mmdb) for offline zero-latency resolution. -
Fallback Lookup: HTTP REST queries to
ip-api.comoripinfo.ioif local database files are unmounted. - 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()
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 β
ββββββββββββββββββββββββββ
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
Algorithm Breakdown
- Similarity Query: Query Qdrant for top-5 nearest neighbor IOC vectors.
-
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.
- Environment setting:
-
Campaign Aggregation Update:
- Recalculate campaign total
ioc_count. - Update campaign
last_activitytimestamp. - Re-evaluate dominant
malware_familytag across cluster members. - Recalculate campaign aggregate confidence score.
- Recalculate campaign total
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)
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()
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
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
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"}
]
}
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) β
βββββββββββββββββββββββ βββββββββββββββββββββββ
Supported Inference Providers
-
Groq Cloud API (groq_client.py):
- Primary provider for production inference.
- Models:
llama-3.1-70b-versatile(deep reasoning) andllama-3.1-8b-instant(high throughput). - High speed (~300 tokens/sec) via Groq LPU hardware acceleration.
-
Ollama Engine (ollama_client.py):
- Air-gapped / offline fallback client querying local instances (
http://localhost:11434).
- Air-gapped / offline fallback client querying local instances (
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
)
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."
}
]
}
5. Self-Critique & Validation Loop
Before committing techniques to the database, the agent executes a Self-Critique Cycle (self_critique.j2):
- Initial Generation: LLM proposes a list of MITRE technique candidate IDs.
- 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."
- 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_reviewand 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 β
βββββββββββββββββββββββββββ
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]
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"
]
}
}
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
ββββββββββββββββββββββββββ
Validation Workflow
-
Storage: When a prediction is generated, it is stored in
predictionstable with statuspending. -
Monitoring:
PredictionValidator(prediction_validator.py) monitors new incoming IOCs over a 30-day window. -
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).
- Status updated to
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
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...) β
βββββββββββββββββββββββββββ
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).
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)
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 β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
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
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
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
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
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)
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()
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
}
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
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}
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
}
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
}
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
}
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
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
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) β
βββββββββββββββββββββββββ βββββββββββββββββββββββββ
Key Globe Features
-
Geographic Points: Displays latitude/longitude coordinates extracted by
geoip.pyfor 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;
}
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
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: [],
}
File: argus/frontend/postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
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"
}
}
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'),
},
},
},
})
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;"]
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;
}
}
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
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
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>
)
}
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>
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):
-
Last updated timestamp: Shows
TimeAgocomponent with the most recentlast_ioc_ingested_atfromGET /api/metrics/summary. Styled:text-argus-text-muted text-sm. -
Live indicator dot: A
w-2 h-2 rounded-fulldot. 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. -
Notification badge: Count of pending reviews from
GET /api/metrics/summaryfieldpending_reviews. Rendered as abg-argus-red text-white text-xs rounded-full px-2 py-0.5badge next to aBellicon. Clicking navigates to/reviews. Hidden when count is 0.
-
Last updated timestamp: Shows
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>
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>
)
}
Props:
-
childrenβ page content -
fullWidthβ boolean, defaultfalse. Whentrue, 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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
Returns:
/**
* @returns {{ status: 'connecting'|'connected'|'disconnected'|'error', send: function, disconnect: function }}
*/
Implementation notes:
- Store the WebSocket instance in a
useRef(notuseStateβ avoids stale closures in event handlers) - Store reconnect attempt count in a
useRef - On close (if not intentional): wait
reconnectDelay * 2^attemptms (exponential backoff), cap at 30000ms, attempt reconnect - After
maxReconnectsfailures, set status to'error'and stop trying - On
useEffectcleanup (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
*/
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,
})
}
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
*/
Implementation notes:
- Store the previous value in a
useRef - When
valuechanges, start arequestAnimationFrameloop - 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 }
* }}
*/
Implementation notes:
-
Primary path: Connect to WebSocket at
/ws/live-feedviauseWebSocket -
Fallback path: If WebSocket status is
'error'(max reconnects exceeded), fall back to pollingGET /api/iocs?limit=20&since={lastSeenTimestamp}every 5 seconds viausePollQuery. Set status to'polling'. -
IOC buffer: Maintain a
useRefarray of up to 500 IOCs. New IOCs are prepended (newest at index 0). When length exceeds 500, slice to 500. -
On WebSocket message: Parse
eventfield. Ifevent === 'new_ioc', prependdatato the buffer (unlessisPausedis true). Ifevent === 'ping', ignore (it's a keepalive). -
Initial load: On mount, fetch
GET /api/iocs?limit=20to 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 oniocType,sourceFeed,minConfidence. -
Pause: When
isPausedis true, new WebSocket messages are still received but NOT prepended to the display buffer. They are buffered in a separateuseRefarray. 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 }}
*/
Implementation notes:
- Fetches from
GET /api/campaigns/{campaignId}/graphusinguseQuery(not polling β graphs don't change fast enough) - Enabled only when
campaignIdis 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 } }
})
}
-
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) andstyle: { 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}`
}
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')
}
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`)
}
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' })
}
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' })
}
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),
})
}
src/api/agents.js
import { apiFetch } from './client'
/**
* Fetch all agent health data
* @returns {Promise<object>}
*/
export async function fetchAgentHealth() {
return apiFetch('/agents/health')
}
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()}`)
}
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')
}
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
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>
)
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;
}
File: src/App.jsx
import { Outlet } from 'react-router-dom'
export default function App() {
return <Outlet />
}
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>
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-feedfor real-time IOC push -
Fallback:
GET /api/iocs?limit=20&since={ts}polled every 5s (when WS fails) -
Stats:
GET /api/metrics/summarypolled every 10s (for stat cards) -
Initial load:
GET /api/iocs?limit=20on 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] β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 }
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):
-
Badgewith IOC type, variant from the typeβcolour map -
IOCValuewith the IOC value (monospace, copy button) -
<span className="text-sm text-argus-text-secondary">withsource_feed -
ConfidenceBarwithvalue={ioc.confidence_score}(width: 80px) -
TimeAgowithtimestamp={ioc.ingested_at} - If
ioc.malware_family: smallBadgewith variantyellow
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:
-
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 -
Source Feed dropdown:
<select>with dynamic options from the IOC buffer's uniquesource_feedvalues -
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
IOCValuecopies 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=50polled every 30s -
Campaign detail:
GET /api/campaigns/{id}fetched on selection -
Campaign graph (for overview tab):
GET /api/campaigns/{id}/graphfetched 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) β β
ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
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')
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 arebg-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
iocsarray.
MITRE Techniques tab (CampaignMITRE.jsx):
- List of mapped techniques. Each item:
TechniqueTagwith technique ID, technique name, tactic intext-argus-text-muted,ConfidenceBar, and rationale text intext-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:
ConfidenceBarfor overall prediction confidence. - Validation status badge:
Badgeshowing "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=20polled every 30s -
Report detail:
GET /api/reports/{id}fetched on selection -
PDF download:
GET /api/reports/{id}/pdftriggered 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... β
βββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββ
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)
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" ifescalation_required === true, orBadge 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=30polled 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%) β
ββββββββββββββββββββββββββββββββββββββββ
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
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):
- Reconnaissance
- Resource Development
- Initial Access
- Execution
- Persistence
- Privilege Escalation
- Defense Evasion
- Credential Access
- Discovery
- Lateral Movement
- Collection
- Command and Control
- Exfiltration
- 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
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
daysparameter - 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=50on mount -
Campaign graph:
GET /api/campaigns/{id}/graphfetched on campaign selection viauseReactFlowhook
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β β
β ββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
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
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
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 β
NodeDetailPanelslides 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/reviewspolled 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] β
ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
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)
Action Buttons
Three buttons in a row:
-
Approve:
bg-argus-green text-white px-4 py-2 rounded-lg hover:bg-green-600withCheckicon -
Override:
bg-argus-blue text-white px-4 py-2 rounded-lg hover:bg-blue-600withPencilicon β clicking this toggles theOverrideFormbelow -
False Positive:
bg-argus-red text-white px-4 py-2 rounded-lg hover:bg-red-600withXicon β 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/healthpolled every 15s -
Summary metrics:
GET /api/metrics/summarypolled 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%) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 }
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 infont-semibold -
Last active:
TimeAgocomponent -
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-bluebars at varying heights) -
Special for ThreatNarrativeAgent:
ReputationGaugecomponent 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>
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/statuspolled 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] ββ
β ββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β ... β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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} />)}
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 }
*/
Contents:
-
Header: Feed icon (coloured circle per feed type) + feed name in
font-semibold text-lg -
Status badge:
Badgeβ "Active" (green) ifis_active && consecutive_failures < 3, "Paused" (yellow) if!is_active, "Failed" (red) ifconsecutive_failures >= 3 -
Last polled:
TimeAgocomponent β "Last poll: {timeAgo}" -
Poll interval:
{interval} seconds+ adaptive indicator:- Compare
poll_interval_secsto default (300). If < 300: "β speeding up" in green. If > 300: "β slowing down" in yellow. If === 300: "= stable" in grey.
- Compare
-
Novel IOC ratio:
{(last_novel_ratio * 100).toFixed(0)}%in a smallBadge -
Total ingested: count in
font-mono - Consecutive failures: shown in red text if > 0
-
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, callstriggerFeed(feedName). ShowsLoader2 className="animate-spin"while loading. ShowsCheckicon 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, callspauseFeed(feedName). - "Resume" button (if paused):
text-argus-green hover:bg-argus-green-muted px-3 py-1.5 rounded-lg text-sm. On click, callsresumeFeed(feedName).
- "Trigger Now" button:
"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 */
}
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:
- The primary CTA button
- Active navigation state (left border)
- 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;
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">
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; }
Typography Rules
-
All IOC values, IPs, hashes, domains β
font-monoalways. 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-displaywith--text-primary, label below intext-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)
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)
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);
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 */
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). Onlytransformandopacity. -
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
requestAnimationFramein theuseCountUphook β 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;
}
}
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: [],
}
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;
}
}
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)
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": { ... }
}
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/"
}
]
}
}
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
}
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
}
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
}
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"
}
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"
}
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)
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
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
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)
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']
Compound patterns (AND):
[ipv4-addr:value = '1.2.3.4' AND network-traffic:dst_port = 443]
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
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
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))
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)
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()
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
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
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())
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())
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())
5. data/grafana/datasources/prometheus.yaml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://argus-api:9090
isDefault: false
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:
- Schema Definition: Define a Pydantic schema for Sigma rule output.
-
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
-
Endpoint (
GET /campaigns/{campaign_id}/sigma): This endpoint dynamically queries the IOCs for the campaign, fills the Jinja2 template, and returns a rawapplication/x-yamlpayload 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:
-
MITRE Indexing: Extract
intrusion-setobjects from the local ATT&CK bundle. For each set, map its knownattack-pattern(techniques) via relationship objects. -
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
- 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:
-
Decay Formulas (
argus/storage/postgres.pybackground job):- IPs: Half-life of 7 days (IPs are recycled quickly).
- Domains: Half-life of 30 days.
- Hashes: Half-life of 180 days.
-
Periodic Task: Add a Celery Beat task
decay_ioc_confidencethat runs daily.- Updates
confidence_score = confidence_score * e^(-lambda * days_since_last_seen). - If
confidence_score < 0.1, set status toarchivedand remove from Qdrant vector store.
- Updates
- 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:
-
Classification Rules (
argus/agents/threat_narrative.py):- If any IOC originated from a proprietary/internal feed -> TLP:AMBER.
- If
overall_confidence > 0.85and escalation not required -> TLP:GREEN. - If report contains high-confidence CVE exploits -> TLP:AMBER.
- Default -> TLP:WHITE.
-
STIX Integration: Inject the
tlpmarking definition into thestix_report_object. -
PDF Visuals: Update
threat_report.html.j2to 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:
-
Config (
.env):USER_ASSET_RANGES="10.0.0.0/8,192.168.1.0/24" -
Correlation Logic (
argus/agents/correlation.py):- During Graph Edge Detection, compare
subnet_24or raw IPs againstUSER_ASSET_RANGESusing Python'sipaddressmodule. - If match found, create a
[:TARGETS_USER_INFRASTRUCTURE]edge in Neo4j.
- During Graph Edge Detection, compare
-
Escalation: If this edge is created, immediately bypass confidence thresholds and set
escalation_required = truewith reason "YOUR ENVIRONMENT IS MENTIONED". -
Grafana Alert: Implement a prominent red alert panel driven by a query counting
TARGETS_USER_INFRASTRUCTUREedges.
Top comments (0)