28 KiB
Extension Challenges
You've built the base project. Now make it yours by extending it with new features.
These challenges are ordered by difficulty. Start with the easier ones to build confidence, then tackle the harder ones when you want to dive deeper.
Easy Challenges
Challenge 1: Add a New Detection Rule
What to build:
Add a detection rule for XML External Entity (XXE) injection attacks. XXE payloads contain strings like <!ENTITY, <!DOCTYPE, SYSTEM "file:///, and xmlns:xi in URL parameters or paths.
Why it's useful: XXE (CWE-611) was in the OWASP Top 10 until 2021 when it was merged into A05:2021 Security Misconfiguration. The 2014 Facebook XXE vulnerability allowed reading arbitrary files from their servers. Any application that parses XML input from users is potentially vulnerable.
What you'll learn:
- How the rule engine pattern matching works
- Writing regex patterns that catch evasion variants
- Calibrating rule scores relative to existing rules
Hints:
- Add a compiled regex to
patterns.pycovering common XXE payloads - Add a
_PatternRuleentry to_PATTERN_RULESinrules.pywith an appropriate score (think about where XXE falls between FILE_INCLUSION at 0.75 and COMMAND_INJECTION at 0.90) - Test with the dev-log simulator: modify
simulate.pyto generate XXE payloads
Test it works:
curl "http://localhost:8000/api/test?xml=%3C!DOCTYPE%20foo%20%5B%3C!ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2Fetc%2Fpasswd%22%3E%5D%3E"
Check the dashboard for a new threat event with your rule name in the matched_rules field.
Challenge 2: Add Request Method Anomaly Detection
What to build: Add a threshold rule that fires when an IP sends an unusual mix of HTTP methods. Normal users predominantly send GET requests with occasional POSTs. An IP sending lots of PUT, DELETE, PATCH, or OPTIONS requests within 5 minutes is likely probing the API for misconfigured endpoints.
Why it's useful: API enumeration attacks often use uncommon HTTP methods to discover endpoints that respond differently (returning 200 instead of 405). The 2019 First American Financial breach was partially enabled by direct object reference vulnerabilities discovered through API probing.
What you'll learn:
- How threshold rules use windowed features
- Working with the
WindowAggregatorRedis data - The difference between pattern rules (regex on URI) and threshold rules (numeric conditions on features)
Hints:
method_entropy_5mis already computed by the aggregator. An entropy above 1.5 (more than 3 distinct methods with roughly equal frequency) is suspicious for most web applications- Add a
_ThresholdRuleinrules.pytargetingmethod_entropy_5m - Score it around 0.25-0.35 since this is a behavioral signal with potential for false positives on legitimate API clients
Test it works: Use the dev-log simulator to send a mix of GET/POST/PUT/DELETE/OPTIONS from a single IP and verify the rule fires.
Challenge 3: Add a Country Blocklist Feature
What to build: Add a configurable list of country codes that automatically boost the threat score of requests originating from those countries. Many organizations restrict traffic from regions where they have no customers.
Why it's useful:
Geo-blocking is a common defense layer. The country_code feature already exists in the pipeline. This challenge extends it from a passive feature (used by ML models) to an active rule (directly influencing scores).
What you'll learn:
- How GeoIP enrichment feeds into the detection pipeline
- Adding configurable parameters via Pydantic Settings
- The tension between geographic blocking and false positives (VPNs, CDNs, traveling employees)
Hints:
- Add a
blocked_countriessetting inconfig.pyas a comma-separated string, parsed into aset[str] - Add a check in
RuleEngine.score_requestthat matches thecountry_codefeature against the blocklist - Score it around 0.20-0.30 since geographic origin alone isn't a strong indicator
Test it works:
Set BLOCKED_COUNTRIES=CN,RU in .env, restart, and verify that requests from those country codes get a score boost.
Intermediate Challenges
Challenge 4: Build an Active Learning Feedback Loop
What to build: Add API endpoints for analysts to mark threat events as true positives or false positives, and use those labels to automatically trigger retraining when enough new labels accumulate.
Implementation approach:
-
Add review endpoints to handle analyst feedback
PATCH /threats/{id}/reviewwith body{"label": "true_positive"}or{"label": "false_positive"}- Store the label in
ThreatEvent.review_labeland setreviewed = True - Files to modify:
threats.py,threat_service.py
-
Add automatic retrain trigger
- Track the count of new labels since last training
- When the count exceeds a threshold (e.g., 50 new labels), trigger retraining automatically
- Files to create: a background task that periodically checks label counts
-
Use labels as training data
- Modify the retrain endpoint to use
review_labelas ground truth instead of score-based labeling true_positiveevents become attack samples,false_positiveevents become normal samples- Files to modify:
models_api.py
- Modify the retrain endpoint to use
What you'll learn:
- Active learning: using human feedback to iteratively improve ML models
- The cold start problem: initial labels are scarce, so the system must work with weak labels (score thresholds) until analysts provide enough real labels
- How false positive reduction directly impacts analyst trust and alert fatigue
Hints:
- The
reviewedandreview_labelfields already exist onThreatEvent. The infrastructure is there, you just need to wire it up - Consider adding a
labels_since_last_traincounter toModelMetadata - Be careful with class balance: if analysts only label obvious true positives, the retraining data will be biased
Extra credit: Build a simple "review queue" page in the frontend that shows unreviewed MEDIUM-severity events (the most ambiguous ones) and lets analysts click "True Positive" or "False Positive" buttons.
Challenge 5: Add Request Body Analysis via Error Logs
What to build: Extend the tailer to also watch nginx error logs, which contain request body information for failed requests. Parse the error log format and extract features from POST bodies.
Real world application: Many injection attacks (SQL injection, XXE, deserialization) are delivered in POST request bodies. The access log only shows the method and path. Error logs often include the actual payload that caused the error, giving the detection system visibility into the attack content.
What you'll learn:
- nginx error log format (different from access log combined format)
- Extending the tailer to watch multiple files
- Feature extraction from unstructured text (request bodies contain arbitrary content)
- How the 2017 Equifax breach (CVE-2017-5638) used a Content-Type header payload that would have been visible in error logs but not access logs
Implementation approach:
-
Extend the tailer
- Add a second
_LogHandlerinstance watching the nginx error log - Both handlers push to the same
raw_queuewith a line prefix indicating the source ([access]vs[error])
- Add a second
-
Add an error log parser
- Parse the nginx error log format to extract timestamp, error level, client IP, and the error message (which often contains the request body)
- Create a new
ParsedErrorEntrydataclass
-
Extract body features
- Compute entropy, length, and attack pattern matches on the extracted body text
- Add these as additional features (expanding the vector beyond 35 dimensions, which means retraining)
Hints:
- nginx error log format:
2026/03/15 09:22:31 [error] 7#7: *1234 upstream prematurely closed connection while reading response header from upstream, client: 93.184.216.34 - The tricky part is that error logs are much less structured than access logs. You'll need robust parsing for various error message formats
- Consider keeping access log features and error log features as separate feature sets that get concatenated
Extra credit: Add a feature that correlates access log entries with error log entries by timestamp and IP, linking the full request context (from access log) with the error detail (from error log).
Challenge 6: Add Prometheus Metrics Export
What to build:
Add a /metrics endpoint that exports pipeline statistics, detection metrics, and system health in Prometheus exposition format.
Why it's useful: Production threat detection systems need monitoring. You want to know: How many requests per second is the pipeline processing? What's the detection rate by severity? Are any queues backing up? How fast is inference?
What you'll learn:
- Prometheus metric types (counters, gauges, histograms)
- Instrumenting async Python code without adding latency to the hot path
- The difference between monitoring the monitored system (threat metrics) and monitoring the monitoring system (pipeline health)
Implementation approach:
- Add prometheus-client dependency
- Instrument the pipeline with counters and histograms
vigil_requests_processed_total(counter by stage)vigil_threats_detected_total(counter by severity)vigil_inference_duration_seconds(histogram)vigil_queue_depth(gauge per queue)
- Add the
/metricsendpoint usinggenerate_latest() - Add a Grafana dashboard JSON in
infra/grafana/
Hints:
- Use
time.perf_counter()around the ONNX inference call for latency measurement - Queue depths can be sampled periodically (every 5 seconds) via a background task rather than instrumenting every put/get
- The pipeline
statsdict already tracks processed/error counts. Expose these as Prometheus counters
Advanced Challenges
Challenge 7: Add a 4th Model (Transformer)
What to build: Add a small transformer encoder model to the ensemble that processes sequences of requests from the same IP (not just individual requests). This model sees the temporal ordering of requests, which the other three models don't.
Why this is hard: The current models process one request at a time. A transformer needs a sequence of recent requests from the same IP as input. This means buffering recent requests per-IP, padding/truncating to a fixed sequence length, and computing attention across the sequence. The training data pipeline also needs to generate sequences, not individual samples.
What you'll learn:
- Transformer architecture for sequential anomaly detection
- Attention mechanisms and what they learn about request sequences
- Integrating a sequence model with per-request models in an ensemble
- How to handle variable-length sequences (padding, masking)
Architecture changes needed:
┌───────────────────┐
│ Existing Models │
│ (per-request) │
Feature Vector ────────►│ AE + RF + IF ├───┐
└───────────────────┘ │
│
┌───────────────────┐ ├──► Ensemble
Recent N requests ─────►│ Transformer │───┘ Fusion
(from same IP) │ (per-sequence) │
└───────────────────┘
Implementation steps:
-
Buffer recent requests per-IP in Redis or in-memory
- Store the last 16 feature vectors per IP
- Pad with zeros if fewer than 16 are available
-
Build the transformer model
- Small encoder-only model: 2 layers, 4 attention heads, 64 embedding dim
- Input: (batch, seq_len=16, features=35)
- Output: anomaly score (single float)
- Train on sequences of normal traffic, flag sequences that contain anomalies
-
Integrate with the ensemble
- Add a 4th weight (e.g., AE 0.30, RF 0.30, IF 0.15, Transformer 0.25)
- Export to ONNX alongside the other models
-
Update the training pipeline
- Generate sequence samples from the training data
- Handle the cold start (fewer than 16 requests from a new IP)
Gotchas:
- The transformer's input shape is (batch, seq, features), not (batch, features). ONNX export handles this but the batch inference in the pipeline needs updating
- Attention can be expensive. Profile the inference latency carefully. A 2-layer transformer with 16 sequence length should be manageable (< 5ms per batch of 32)
Resources:
- Vaswani et al., "Attention Is All You Need" (2017), Section 3 for the encoder architecture
- PyTorch
nn.TransformerEncoderdocumentation for implementation details
Challenge 8: Build a Distributed Pipeline with Kafka
What to build:
Replace the asyncio.Queue-based pipeline with Apache Kafka topics. Each pipeline stage becomes a consumer group that reads from one topic and produces to the next. This removes the single-process throughput ceiling.
Why this is hard: Kafka changes the concurrency model fundamentally. Currently, one Python process handles everything sequentially. With Kafka, you can run multiple parse workers, feature workers, and detection workers independently. But this introduces challenges: message ordering, exactly-once semantics, partition assignment, consumer lag monitoring, and schema evolution.
What you'll learn:
- Kafka producer/consumer patterns with aiokafka
- Partitioning strategies (partition by source IP for windowed features)
- Consumer groups and rebalancing
- Schema registry for evolving message formats
- How production systems like Cloudflare's WAF pipeline handle millions of requests per second
Implementation phases:
Phase 1: Topic Design
vigil.raw- Raw log lines (partitioned by hash of source IP)vigil.parsed- Parsed log entries (same partition key)vigil.enriched- Enriched feature vectorsvigil.scored- Final scored results for dispatch
Phase 2: Replace Queues with Kafka
- Swap
asyncio.Queue.put/getwith Kafka produce/consume - Use Avro or JSON Schema for message serialization
- Maintain partition affinity by source IP so windowed features stay consistent
Phase 3: Scale Out
- Run 4 parse workers, 2 feature workers, 2 detection workers
- Monitor consumer lag to detect bottlenecks
- Add auto-scaling based on lag thresholds
Phase 4: Observability
- Add Kafka metrics to the Prometheus exporter
- Monitor consumer lag, produce latency, and partition distribution
- Build a Grafana dashboard showing pipeline throughput per stage
Known challenges:
- Windowed features require partition affinity - All requests from the same IP must go to the same feature worker so the Redis sorted sets are consistent. Use source IP as the Kafka partition key.
- Exactly-once semantics - If a detection worker crashes mid-processing, the message should be reprocessed, not lost. Use Kafka consumer offsets with manual commit after successful processing.
Success criteria:
- Pipeline handles 5000+ req/s sustained (10x current ceiling)
- Adding/removing workers doesn't cause data loss
- Consumer lag stays under 1000 messages during normal operation
- Partition affinity ensures windowed features are correct
Expert Challenges
Challenge 9: Build a Full SOAR Integration
What to build: Build a Security Orchestration, Automation, and Response (SOAR) layer that automatically responds to detected threats. HIGH severity events should trigger automated responses: add the source IP to an nginx blocklist, send a Slack/Discord notification, create a ticket in a tracking system, and enrich the alert with threat intelligence from public feeds.
Prerequisites: You should have completed Challenge 4 (active learning) and Challenge 6 (Prometheus metrics) first because this builds on analyst feedback workflows and monitoring infrastructure.
What you'll learn:
- SOAR automation patterns used in enterprise SOCs
- Playbook-driven incident response (if-this-then-that automation)
- Threat intelligence integration (AbuseIPDB, VirusTotal, Shodan)
- The risks of automated blocking (false positives lock out real users)
- How tools like Splunk SOAR and Palo Alto XSOAR implement these patterns
Planning this feature:
Before you code, think through:
- What actions should be fully automated vs. requiring human approval?
- How do you handle false positive automated blocks? (Answer: auto-unblock after a TTL, require analyst confirmation for permanent blocks)
- What's the blast radius if the ML model goes wrong and generates mass false positives?
High level architecture:
Scored Request (HIGH severity)
│
├──► Playbook Engine
│ │
│ ├──► Block IP (nginx blocklist update)
│ ├──► Notify (Slack webhook)
│ ├──► Enrich (AbuseIPDB lookup)
│ └──► Ticket (create incident)
│
└──► Audit Log (all actions recorded)
Implementation phases:
Phase 1: Playbook Engine
- Define playbooks as YAML files describing trigger conditions and actions
- Build a simple rule evaluator that matches scored requests to playbooks
- Log all automated actions for audit
Phase 2: Response Actions
- nginx blocklist: Write blocked IPs to a file that nginx includes via
denydirective, then send SIGHUP to reload - Slack notification: POST to a webhook URL with threat details
- AbuseIPDB enrichment: GET the IP's abuse confidence score and history
Phase 3: Safety Rails
- Rate limit automated blocks (max 10 IPs per hour)
- Auto-unblock after a configurable TTL (default 1 hour)
- Require analyst confirmation for blocks that affect more than N requests
- Kill switch: disable all automated responses via an API endpoint
Phase 4: Dashboard Integration
- Show automated response status on the threat detail page
- Display enrichment data (AbuseIPDB score, VirusTotal results)
- "Undo Block" button for analysts
Success criteria:
- HIGH severity events trigger automated Slack notifications within 5 seconds
- Blocked IPs are automatically unblocked after the configured TTL
- All automated actions are logged with full context
- The kill switch disables all automation within one API call
- False positive automated blocks can be reversed by analysts from the dashboard
Mix and Match
Combine features for bigger projects:
Project Idea 1: Full SOC Stack
- Combine Challenge 4 (active learning) + Challenge 6 (Prometheus) + Challenge 9 (SOAR)
- Add a case management system for tracking incidents from detection to resolution
- Result: A miniature Security Operations Center in a Docker Compose stack
Project Idea 2: Multi-Source Detection
- Combine Challenge 5 (error logs) + Challenge 7 (transformer) + Challenge 8 (Kafka)
- Ingest from multiple log sources, use the transformer to detect multi-stage attacks that span request sequences
- Result: A distributed, multi-source threat detection platform
Real World Integration Challenges
Integrate with Elasticsearch and Kibana
The goal: Ship all scored threat events to Elasticsearch for long term storage and build Kibana dashboards for investigation.
What you'll need:
- Elasticsearch 8.x cluster (can run locally via Docker)
- Kibana for visualization
- Understanding of Elasticsearch index mappings and ILM policies
Implementation plan:
- Add an Elasticsearch output to the
AlertDispatcher(parallel to PostgreSQL, not replacing it) - Define an index mapping that includes GeoIP fields as
geo_pointtype for map visualizations - Build a Kibana dashboard with:
- Threat map showing source IPs on a world map
- Timeline of threat scores over time
- Top attack types by rule name
- Drill-down from severity to individual events
Watch out for:
- Elasticsearch bulk indexing latency can slow down the dispatch stage. Use async bulk operations with a buffer
- Index lifecycle management (ILM) to prevent unbounded storage growth. Roll indices daily and delete after 30 days
Deploy to Kubernetes
The goal: Convert the Docker Compose stack to Kubernetes manifests and deploy to a cluster.
What you'll learn:
- Writing Kubernetes Deployments, Services, ConfigMaps, and Secrets
- Persistent volume claims for PostgreSQL and model data
- Health probes (liveness, readiness, startup) mapped from Docker healthchecks
- Horizontal Pod Autoscaler for the backend based on queue depth metrics
Steps:
- Convert each Docker Compose service to a Kubernetes Deployment
- Create Services for internal communication (postgres, redis, backend)
- Use a ConfigMap for non-secret configuration and Secrets for credentials
- Create PersistentVolumeClaims for postgres data and model artifacts
- Add an Ingress resource for frontend traffic
- Set up HPA for the backend based on Prometheus metrics (Challenge 6)
Production checklist:
- All secrets are stored in Kubernetes Secrets, not ConfigMaps
- PostgreSQL uses a StatefulSet with persistent storage
- Backend readiness probe gates traffic until models are loaded
- Resource requests and limits are set for all pods
- Network policies restrict access to PostgreSQL and Redis
Performance Challenges
Challenge: Handle 5000 Requests Per Second
The goal: Make the single-process pipeline handle 5000 req/s without dropping logs.
Current bottleneck: ML inference at ~640 req/s with batch size 32. The detection stage is the ceiling.
Optimization approaches:
Approach 1: Increase batch size
- How: Set
BATCH_SIZE=128andBATCH_TIMEOUT_MS=100 - Gain: Larger batches amortize ONNX session overhead. Expected 2-3x throughput
- Tradeoff: Higher latency (up to 100ms wait for a full batch vs. 50ms)
Approach 2: Quantize the ONNX models
- How: Use ONNX Runtime's quantization tools to convert float32 models to int8
- Gain: 2-4x inference speedup on CPU with minimal accuracy loss
- Tradeoff: Small accuracy degradation, especially for the autoencoder. Validate with the quality gates
Approach 3: Skip ML inference for clearly benign traffic
- How: If the rule engine scores 0.0 (no rules matched), skip ML inference and output the rule score directly
- Gain: 90-95% of traffic is benign, so this skips inference for most requests
- Tradeoff: You lose ML-only detections (anomalies that don't match any rule). Acceptable if rule coverage is good
Benchmark it:
just devlog-simulate normal 10000 -d 0.0001
Target metrics:
- Throughput: 5000+ req/s sustained
- Queue depths: raw_queue never full
- p99 latency: < 200ms end-to-end
Challenge: Reduce Memory Footprint
The goal: Run the entire stack on a 2GB RAM machine (currently needs 4GB+).
Profile first:
docker stats --no-stream
Common optimization areas:
- ONNX models: 50-200MB. Quantization (int8) reduces this by 2-4x
- GeoIP database: ~60MB mmap'd. Switch to the smaller GeoLite2-Country (~5MB) if city-level precision isn't needed
- PostgreSQL: Reduce
shared_buffersandwork_meminpostgresql.conf - Redis: Set
maxmemory 64mbwithallkeys-lrueviction to cap memory usage
Security Challenges
Challenge: Add TLS Mutual Authentication for the API
What to implement: Require client certificates for API access instead of (or in addition to) the X-API-Key header.
Threat model: This protects against:
- API key theft (the key is a static string that could be leaked in logs, config files, or commit history)
- Replay attacks (a stolen API key can be used from any network location)
Implementation:
- Generate a CA certificate, server certificate, and client certificate using
openssl - Configure uvicorn to require client certificates (or put nginx in front of the backend with
ssl_verify_client on) - Validate the client certificate's subject CN against an allowlist
Testing the security:
- Try to hit the API without a client certificate (should get 403)
- Try with a certificate signed by a different CA (should get 403)
- Verify that certificate rotation works (issue new certs, old ones still work until revoked)
Challenge: Pass CIS Docker Benchmark
The goal: Make the Docker Compose stack compliant with the CIS Docker Benchmark.
Current gaps:
- Containers run as root (add
USERdirectives to Dockerfiles) - No resource limits (add
mem_limitandcpusto compose.yml) - No read-only root filesystem (add
read_only: truewhere possible) - No security options (add
security_opt: [no-new-privileges:true])
Remediation:
Run docker-bench-security against the stack and address each finding. Focus on the host configuration, daemon configuration, and container runtime sections.
Contribution Ideas
Finished a challenge? Share it back:
- Fork the repo
- Implement your extension in a new branch
- Document it in this learn folder
- Submit a PR with:
- Your implementation
- Tests
- Updated learn documentation
- Example output showing it works
Good extensions might get merged into the main project.
Challenge Yourself Further
Build Something New
Use the concepts you learned here to build:
- A network traffic anomaly detector that analyzes pcap files instead of HTTP logs, using the same ensemble approach but with network-layer features (packet sizes, protocol distribution, connection patterns)
- A DNS threat detector that monitors DNS query logs for tunneling, DGA domains, and anomalous resolution patterns
- A container runtime anomaly detector that watches syscall traces from Falco and flags unusual process behavior
Study Real Implementations
Compare your implementation to production tools:
- Suricata - Open source IDS/IPS. Look at how their rule engine handles signature matching at wire speed. Their multi-threaded architecture handles 10 Gbps+
- Elastic ML - Elasticsearch's anomaly detection module. Compare their time-series decomposition approach with our autoencoder approach
- AWS GuardDuty - Amazon's managed threat detection. Read their documentation on how they combine ML, threat intelligence, and behavioral analysis
Read their code (Suricata is open source), understand their tradeoffs, steal their good ideas.
Write About It
Document your extension:
- Blog post explaining what you built and what you learned about threat detection
- Comparison between rule-based and ML-based detection approaches using concrete examples from your experience with this project
- Analysis of false positive rates at different ensemble weight configurations
Teaching others is the best way to verify you understand it.
Getting Help
Stuck on a challenge?
-
Debug systematically
- What did you expect to happen?
- What actually happened?
- What's the smallest test case that reproduces it?
-
Read the existing code
- The rule engine in
rules.pyshows the pattern for adding detection logic - The pipeline in
pipeline.pyshows how stages communicate - The orchestrator in
orchestrator.pyshows the full training flow
- The rule engine in
-
Search for similar problems
- FastAPI async patterns: fastapi.tiangolo.com
- ONNX Runtime issues: github.com/microsoft/onnxruntime
- Redis sorted set patterns: redis.io/docs/data-types/sorted-sets
-
Ask for help
- Post in GitHub Discussions
- Include: what you tried, what happened, what you expected
- Show the relevant code and error messages, not just the stack trace
Challenge Completion
Track your progress:
- Easy 1: Add XXE detection rule
- Easy 2: Add method anomaly detection
- Easy 3: Add country blocklist
- Intermediate 4: Build active learning feedback loop
- Intermediate 5: Add error log analysis
- Intermediate 6: Add Prometheus metrics
- Advanced 7: Add transformer model
- Advanced 8: Build Kafka distributed pipeline
- Expert 9: Build SOAR integration
Completed all of them? You've mastered this project. Time to build something new or contribute back to the community.