Frontend Repo: https://github.com/12458/conway-technical-fe
Real-time anomaly detection and alerting system for GitHub public events using RRCF (Robust Random Cut Forest) and AI-powered summarization.
Click to play on YouTube
I also found a malicious crypto bot scam user while I was at it.
- What: Real-time anomaly monitoring for GitHub public events with AI summaries.
- Novelty (OOD): RRCF over vectorized GitHub events + rule-based patterns and bot-aware filtering for higher-precision signals.
- Flow: Poll REST /events (ETag + backoff) → Detect (RRCF: 50 trees × 256) → Store (Postgres) → Enrich (GitHub GraphQL) → Summarize (GPT-5 mini) → Stream (SSE) → UI.
- Frontend: Live incident feed (cards + expand for raw JSON). Repo: https://github.com/12458/conway-fe
- APIs: GET /summary (query summaries), GET /stream (SSE), GET /health, GET /stats.
- Infra: Python 3.12+, FastAPI, Redis RQ, Postgres. Caching, jittered backoff, idempotent writes.
- Enrichment: Actor/repo/CI context, commit verification; cached with TTL to reduce GraphQL load.
- Output: Structured severity (low/medium/high/critical) with Root Cause / Impact / Next Steps.
A complete background polling service for GitHub Events API with:
- RRCF-based streaming anomaly detection (50 trees, 256 samples each)
- Adaptive percentile-based threshold (98th percentile = top 2% flagged)
- GraphQL enrichment pipeline for enhanced context
- AI-powered summarization using GPT 5 mini
- Real-time SSE streaming for instant UI updates
- Redis Queue for scalable async job processing
- RRCF for GitHub Events: First application of Robust Random Cut Forest to GitHub event streams with custom feature engineering optimized for tree-based anomaly detection
- Adaptive Threshold System: Percentile-based dynamic thresholding that maintains balanced anomaly rate (top 2%) regardless of data distribution, preventing both alert storms and silent periods
- Hybrid Detection Pipeline: Combines statistical anomaly detection (RRCF) with rule-based pattern matching for higher accuracy
- GraphQL Enrichment: Post-detection enrichment with actor profiles, repository context, workflow status, and commit verification for risk-based severity escalation
- Bot-Aware Processing: Intelligent filtering of 60+ known bot accounts to reduce noise by ~40%
- Streaming Architecture: True online anomaly detection with minimal memory footprint via lazy decay and LRU eviction
- Python 3.12+
- Redis URL
- OpenAI API key
- GitHub token
uv synccp .env.example .envpython main.pyOpen a new terminal:
# Check health
curl http://localhost:8000/health
# Get stats
curl http://localhost:8000/stats
# Stream events (keep running)
curl http://localhost:8000/stream- Poller starts fetching GitHub events every 60 seconds (or longer as indicated by
X-Poll-Interval) - Anomaly detector analyzes each event using RRCF
- Anomalies are saved to database and enqueued
- GraphQL enrichment adds actor/repo/workflow context
- Workers process jobs and generate AI summaries
- SSE stream broadcasts summaries to connected clients
For anomalty detection, I used Robust Random Cut Forest Algorithm for anomaly detection by Guha et al. (2016).
My modified implementation of the reference implementation can be found here.
graph TD
A[GitHub Events API] -->|"Poll every 60s<br/>(with ETag caching)"| C["
<strong>Background Poller</strong><br/>
• Exponential backoff<br/>
• Rate limit handling<br/>
• Event deduplication
"];
C --> D["
<strong>Anomaly Detector (RRCF)</strong><br/>
• Feature extraction<br/>
• 50 trees, size 256<br/>
• CoDisp scoring<br/>
• Rule-based patterns<br/>
• Bot filtering
"];
D --> E["
<strong>Database (PostgreSQL)</strong><br/>
• GitHubEvent (all events)<br/>
• AnomalySummary (anomalies)<br/>
• Enrichment cache (4 tables)
"];
E -->|"Anomalies only"| F["
<strong>Redis Queue (RQ)</strong><br/>
• Async job processing<br/>
• Scalable workers
"];
F --> G["
<strong>GraphQL Enrichment Service</strong><br/>
• Actor profiles<br/>
• Repository context<br/>
• Workflow/CI status<br/>
• Commit verification<br/>
• DB caching (TTL-based)
"];
B["GitHub GraphQL API<br/>(Enrichment Data)"] --> G;
G --> H["
<strong>AI Summarization Workers</strong><br/>
• GPT 5 Mini<br/>
• Enriched context<br/>
• Risk-based severity<br/>
• Structured summaries
"];
H --> I["
<strong>FastAPI Server</strong><br/>
• GET /summary (query)<br/>
• GET /stream (SSE)<br/>
• GET /health, /stats
"];
I -->|"SSE"| J["
<strong>Frontend UI</strong><br/>
• Real-time incident feed<br/>
• Filterable cards
"];
%% Styling
classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px,font-family:arial;
- Pydantic-based settings management
- Environment variable support with
SERVICE_prefix - Redis URL, GitHub token, OpenAI API keys
- Configurable thresholds and parameters
- SQLAlchemy async ORM models
- GitHubEvent: Stores all processed events with anomaly scores
- AnomalySummary: Stores AI-generated incident reports
- Enrichment cache tables: Actor, repo, workflow, commit data
- Support for PostgreSQL
- RQ (Redis Queue) integration
- Enqueue anomaly summarization jobs
- Worker-based async processing
- Job statistics and monitoring
- RRCF-based streaming detector
- Forest of 50 Robust Random Cut Trees (size 256)
- Custom feature engineering (299-dimensional vectors)
- CoDisp (collusive displacement) scoring
- Hybrid approach: Statistical + rule-based patterns
- Bot filtering (60+ known bots)
- Polls GitHub Events API every 60 seconds
- ETag caching for efficient polling (304 Not Modified)
- Exponential backoff with jitter for rate limiting
- Handles 403/429/503 errors gracefully
- Deduplicates events using database
- Flow: Poll → Detect → Save → Enqueue
- Post-detection enrichment of anomalies
- Actor profiles, repo context, workflow status, commit verification
- SQLite caching with TTL (24h/1h/5min)
- Conservative rate limiting and exponential backoff
- Risk-based severity escalation
- RQ worker job:
summarize_anomaly_job() - Supports Anthropic Claude and OpenAI GPT
- Always uses enriched context from GraphQL data
- Generates structured summaries with risk-based severity
- Saves to database and broadcasts via SSE
- GET /summary: Query summaries (filtering by time, severity, limit)
- GET /stream: Server-Sent Events for real-time updates
- GET /health: Service health and statistics
- GET /stats: Detailed component stats
- CORS enabled for frontend integration
- Background tasks: poller + summary broadcaster
- Robust Random Cut Forest with 50 trees (size 256 each)
- Proper feature encoding for tree-based models:
- One-hot hashing for categorical features (event type, actor, repo, org)
- Character n-gram hashing for text (commits, titles, bodies)
- Exponential decay for temporal patterns
- Z-score normalization using Welford's algorithm
- CoDisp (collusive displacement) scoring
- Adaptive threshold (default: 98th percentile = top 2% of events flagged)
- Static threshold fallback (default: 60.0)
The system uses percentile-based adaptive thresholding to maintain a balanced anomaly detection rate:
How It Works:
- Rolling Window: Tracks last 1000 CoDisp scores across all events
- Dynamic Threshold: Automatically sets threshold to 98th percentile of recent scores
- Warm-Up Period: Uses static threshold (60.0) for first 1000 events while building score history
- Self-Adjusting: Adapts to different data regimes (quiet periods, busy periods, distribution shifts)
- Per-Forest Tracking: Multi-forest mode maintains separate percentiles per event type group
Benefits:
- Never goes silent: Always flags top 2% of events (guaranteed alerts)
- No alert storms: Caps maximum anomaly rate at 2% (prevents overwhelming)
- Distribution-agnostic: Works across different event patterns and volumes
- Interpretable: "Top 2%" is clearer than "score > 60"
Important: Anomaly scores are used ONLY for detection triggering, NOT for severity assessment. Severity is determined by enrichment data (actor trust, repo criticality, action type, security signals) via AI analysis.
- RRCF statistical anomaly detection
- Rule-based pattern matching (force pushes, permission changes, etc.)
- Combined scoring for high accuracy
- Bot filtering (removes 60+ known bot accounts)
Post-detection enrichment of anomalies with GitHub GraphQL API for enhanced context:
Actor Profile Enrichment:
- Account creation date and age
- Total contributions (commits, PRs, issues)
- Follower/following counts
- Organization memberships
- Site admin status
- Company affiliation
Repository Context Enrichment:
- Popularity metrics (stars, forks, watchers)
- Branch protection status
- Required code review settings
- Vulnerability alerts enabled
- Security policy presence
- Primary language and topics
Workflow/CI Status Enrichment:
- Check suite conclusions
- Failed/successful job counts
- Individual check run details
- Overall CI/CD health
Commit Verification Enrichment:
- GPG signature validation
- Signer identity
- Lines added/deleted
- Files changed
- Author information
Enrichment data is cached in SQLite with TTL-based expiration:
| Data Type | TTL | Rationale |
|---|---|---|
| Actor Profiles | 24 hours | User metadata changes infrequently |
| Repository Context | 1 hour | Repo settings may change during incidents |
| Workflow Status | 5 minutes | CI status updates frequently |
| Commit Verification | Permanent | Commits are immutable |
Cache hit rates typically exceed 70% after warm-up, significantly reducing GraphQL API calls.
Always choose the highest level whose criteria are met. If none apply, default to Low. Primary factors (in order): repo criticality/protection → actor trust/age → action type → CI/security signals → commit entropy/size → popularity/consumption → anomaly score.
-
Critical
- Destructive or confirmed-compromise on critical/protected assets:
- Force-push/branch deletion on protected branch of a critical repo (e.g., >5k★ or widely consumed)
- Verified credential compromise, secret leakage, or confirmed malware/backdoor
- Release/package tampering pushed to users
- Very high entropy (>7.0) with corroborating indicators (e.g., malicious patterns, bypassed checks)
- Destructive or confirmed-compromise on critical/protected assets:
-
High
- Strongly suspicious with elevated blast radius:
- New/untrusted account (<30 days) performing sensitive actions on important repos
- Unsigned commit to a protected branch or bypass of required checks
- Failing security checks (SAST/secret scan) tied to the change
- Elevated entropy (6.0–7.0) plus another suspicious signal (privilege change, unusual permission edit)
- Very large change (>1000 LOC) from untrusted sources on important repos
- Strongly suspicious with elevated blast radius:
-
Medium
- Policy violations or unusual patterns with moderate risk:
- Unsigned commits to default branch on repos of moderate/unknown criticality
- Moderate anomaly score with failed CI/workflows (non-security)
- Activity from inactive/recently reactivated accounts on standard repos
- Large changes (>500 LOC) without other red flags
- Spammy usernames/repo names within org scope
- Policy violations or unusual patterns with moderate risk:
-
Low
- Benign/low-impact unusual activity with strong mitigations:
- Maintenance/admin tasks by established maintainers
- Unsigned commit to default branch on low-criticality repo by trusted actor, small diff and normal entropy (<6.0), no corroborating anomalies
- No CI configured and no failing security checks; low popularity/consumption
- Low anomaly score with no concerning enrichment factors
- Benign/low-impact unusual activity with strong mitigations:
GraphQL enrichment is mandatory and requires a GitHub token with GraphQL access.
- Streaming architecture with minimal latency
- ETag caching for efficient polling
- Background job queue for AI processing
- Server-Sent Events for instant UI updates
Generates structured summaries with:
- Title (200 chars max)
- Severity (low/medium/high/critical)
- Root Cause (3-5 bullets)
- Impact (3-5 bullets)
- Next Steps (3-5 bullets)
- Tags for categorization
Query anomaly summaries from database.
Query Parameters:
since: ISO timestamp (e.g.,2024-01-01T00:00:00Z)limit: Max results (1-500, default: 50)severity: Filter bylow,medium,high,critical
Example:
curl "http://localhost:8000/summary?limit=10&severity=high"Response:
{
"count": 2,
"summaries": [
{
"id": 1,
"event_id": "12345",
"title": "Force push to main branch on critical-repo",
"severity": "high",
"root_cause": [
"Actor john-doe performed force push to main branch",
"Anomaly score: 85.3 (threshold: 40.0)",
"Pattern: Force push detected"
],
"impact": [
"Potential code history rewrite",
"May affect other contributors",
"Could indicate compromised account"
],
"next_steps": [
"Investigate john-doe's recent activity",
"Review branch protection rules",
"Contact repository maintainers"
],
"suspicious_patterns": ["Force push detected"],
"anomaly_score": 85.3,
"event_type": "PushEvent",
"actor_login": "john-doe",
"repo_name": "org/critical-repo",
"event_timestamp": "2024-01-15T10:30:00Z",
"created_at": "2024-01-15T10:30:15Z",
"tags": ["pushevent", "high", "code-change"]
}
]
}Server-Sent Events stream for real-time anomaly notifications.
Example:
curl http://localhost:8000/streamConnection Flow:
- Client connects → receives
connectedmessage immediately - New anomalies → receives
anomalymessages as they're detected - Connection persists indefinitely (until client disconnects)
Message Types:
- Connected Message - Initial connection acknowledgment
{
"type": "connected",
"message": "Stream connected"
}- Anomaly Message - Real-time anomaly notification
{
"type": "anomaly",
"data": {
"id": 42,
"title": "Suspicious Force Push to Main Branch by New Account",
"severity": "high",
"root_cause": [...],
"impact": [...],
"next_steps": [...],
"anomaly_score": 12.3,
"event_type": "PushEvent",
"actor_login": "suspicious-user-123",
"repo_name": "acme/critical-infrastructure",
"raw_event": {...},
"tags": ["security", "malicious-activity", "risk:high"]
}
}Frontend Integration:
const eventSource = new EventSource('http://localhost:8000/stream');
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'connected') {
console.log('Connected to anomaly stream');
} else if (message.type === 'anomaly') {
displayIncident(message.data);
}
};Service health and component status.
Example:
curl http://localhost:8000/healthReturns: Poller status, detector statistics, queue statistics, SSE client count
Detailed statistics for all components.
Example:
curl http://localhost:8000/statsThe project includes a robust, type-safe Python client for accessing GitHub's public events REST API.
- Async/Await Support: Built on
httpxfor high-performance async HTTP requests - Type Safety: Full Pydantic v2 models for request/response validation
- ETag Caching: Implements GitHub's ETag caching mechanism to optimize polling
- Poll Interval Awareness: Automatically parses and respects
X-Poll-Intervalheaders - Comprehensive Error Handling: Custom exceptions for different API error scenarios
- OOP Design: Clean class-based architecture with context manager support
- Pagination Support: Easy pagination through event results
import asyncio
from github_client import GitHubEventsClient
async def main():
async with GitHubEventsClient() as client:
response = await client.list_public_events(per_page=10)
print(f"Fetched {len(response.events)} events")
for event in response.events:
print(f"{event.type} by {event.actor.login} on {event.repo.name}")
asyncio.run(main())async with GitHubEventsClient() as client:
# First request
response = await client.list_public_events(per_page=5)
etag = response.etag
# Second request with ETag
response2 = await client.list_public_events(etag=etag, per_page=5)
if not response2.events:
print("No new events (304 Not Modified)")
# Your rate limit was not affected!The client supports loading and analyzing historical GitHub event data from GH Archive.
from github_client import GHArchiveLoader
# Load events from a local archive file
loader = GHArchiveLoader("data/2015-01-01-15.json.gz")
# Iterate through events (memory-efficient)
for event in loader.iter_events():
print(f"{event.type} by {event.actor.login} on {event.repo.name}")
# Get processing statistics
stats = loader.get_stats()
print(f"Processed {stats.valid_events} valid events out of {stats.total_events} total")from github_client import (
GitHubAPIError,
ForbiddenError,
ServiceUnavailableError,
RateLimitError,
)
async with GitHubEventsClient() as client:
try:
response = await client.list_public_events()
except ForbiddenError:
print("Rate limit exceeded")
except ServiceUnavailableError:
print("GitHub API is down")
except GitHubAPIError as e:
print(f"API error: {e.message} (status: {e.status_code})")All settings can be configured via environment variables with the SERVICE_ prefix:
SERVICE_GITHUB_TOKEN: Personal access token (optional, increases rate limit from 60 to 5000 req/hr)SERVICE_GITHUB_GRAPHQL_TOKEN: REQUIRED - Token for GraphQL enrichment (requires: repo, read:org, read:user)
SERVICE_REDIS_URL: Redis connection URL (default:redis://localhost:6379)
SERVICE_DATABASE_URL: SQLAlchemy connection string- SQLite:
sqlite+aiosqlite:///./github_events.db - PostgreSQL:
postgresql+asyncpg://user:pass@host/db
- SQLite:
SERVICE_POLLING_INTERVAL: Default poll interval in seconds (default: 60)SERVICE_MAX_BACKOFF_SECONDS: Max backoff for rate limiting (default: 300)SERVICE_MAX_EVENTS_PER_FETCH: Events per API call (default: 100)
SERVICE_ANOMALY_THRESHOLD: Static CoDisp threshold fallback (default: 60.0)SERVICE_TREE_SIZE: RRCF tree size (default: 256)SERVICE_NUM_TREES: Number of trees in forest (default: 50)SERVICE_ENABLE_BOT_FILTERING: Filter known bots (default: true)
SERVICE_ENABLE_ADAPTIVE_THRESHOLD: Enable adaptive percentile-based threshold (default: true)SERVICE_ADAPTIVE_PERCENTILE: Percentile for threshold (default: 98.0 = top 2%)SERVICE_ADAPTIVE_WINDOW_SIZE: Rolling window size for score tracking (default: 1000)SERVICE_MIN_SAMPLES_FOR_ADAPTIVE: Warm-up period before adaptive mode activates (default: 1000)
SERVICE_ENRICHMENT_BATCH_SIZE: Batch size for queries (default: 10)SERVICE_ENRICHMENT_TIMEOUT_MS: Query timeout (default: 5000)
SERVICE_AI_PROVIDER:anthropicoropenai(default: anthropic)SERVICE_AI_MODEL: Model name (default: claude-3-5-sonnet-20241022)SERVICE_ANTHROPIC_API_KEY: Anthropic API keySERVICE_OPENAI_API_KEY: OpenAI API key
SERVICE_WORKER_COUNT: Number of RQ workers (default: 2)SERVICE_QUEUE_NAME: RQ queue name (default: github-anomalies)
Stores all processed GitHub events:
id: GitHub event ID (primary key)event_type: Event type (PushEvent, IssuesEvent, etc.)actor_login,actor_id: Actor informationrepo_name,repo_id: Repository informationorg_login: Organization (if applicable)created_at: Event timestamppayload: Full event payload (JSON)processed: Processing flagis_anomaly: Anomaly detection resultanomaly_score: CoDisp score
Stores AI-generated summaries for anomalies:
id: Auto-increment primary keyevent_id: Reference to GitHubEventtitle: Short incident descriptionseverity: low, medium, high, criticalroot_cause: Array of 3-5 bullet pointsimpact: Array of 3-5 bullet pointsnext_steps: Array of 3-5 bullet pointssuspicious_patterns: Detected patternsanomaly_score: CoDisp scoreevent_type,actor_login,repo_name: Event contextraw_event: Full event data (JSON)event_timestamp,created_at: Timestampstags: Array of tags
actor_profile_cache: Actor profile data (TTL: 24h)repository_context_cache: Repository context (TTL: 1h)workflow_status_cache: Workflow/CI status (TTL: 5min)commit_verification_cache: Commit verification (permanent)
# Run all tests
pytest
# Run only unit tests (fast, mocked)
pytest -m unit
# Run only integration tests (slow, real API)
pytest -m integration
# Skip integration tests
pytest -m "not integration"
# Run with coverage report
pytest --cov=github_client --cov=service --cov-report=html
# Run tests verbosely
pytest -vUnit Tests (github_client/tests/unit/, tests/unit/):
test_models.py: Tests for Pydantic modelstest_config.py: Tests for configuration settingstest_client.py: Tests for client with mocked HTTPtest_sse_models.py: Tests for SSE message modelstest_multiforest.py: Tests for multi-forest detectortest_velocity_fix.py: Tests for velocity tracking
Integration Tests (github_client/tests/integration/, tests/integration/):
test_github_api.py: Tests hitting real GitHub APItest_service.py: Service integration teststest_event_parsing.py: Event parsing tests
# In .env
# Flag more events (top 5% instead of top 2%)
SERVICE_ADAPTIVE_PERCENTILE=95.0
# Flag fewer events (top 1% - very selective)
SERVICE_ADAPTIVE_PERCENTILE=99.0
# Disable adaptive threshold and use static threshold
SERVICE_ENABLE_ADAPTIVE_THRESHOLD=false
SERVICE_ANOMALY_THRESHOLD=20.0python run_service.py --workers 4python run_service.py --port 8080# Run everything (API server + workers)
python run_service.py
# Run with custom worker count
python run_service.py --workers 4
# Run on custom host/port
python run_service.py --host 0.0.0.0 --port 8080
# Run only workers (no API)
python run_service.py --workers-only
# Run only API (no workers)
python run_service.py --api-onlyconway/
├── service/
│ ├── __init__.py
│ ├── config.py # Configuration management
│ ├── database.py # SQLAlchemy models
│ ├── queue.py # RQ integration
│ ├── anomaly_detector.py # RRCF detector
│ ├── poller.py # Background polling
│ ├── enrichment.py # GraphQL enrichment
│ ├── summarizer.py # AI worker
│ ├── app.py # FastAPI endpoints
│ ├── worker.py # RQ worker entry
│ └── sse_models.py # SSE message models
├── github_client/ # GitHub client library
│ ├── client.py # GitHubEventsClient
│ ├── models.py # Pydantic models
│ ├── feature_extractor.py # Feature engineering
│ ├── gharchive.py # GH Archive loader
│ └── tests/ # Client tests
├── tests/ # Service tests
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests
├── run_service.py # Main runner
├── test_service.py # Setup verification
├── .env.example # Config template
├── pyproject.toml # Dependencies
└── README.md # This file
- Background polling with ETag caching
- Exponential backoff (60s → 300s max)
- RRCF streaming algorithm
- Adaptive percentile-based thresholding (98th percentile, self-adjusting)
- Proper feature encoding (one-hot hashing, n-grams, exponential decay)
- 299-dimensional feature vectors
- Bot filtering reduces noise
- GraphQL enrichment pipeline
- Database persistence with deduplication
- Redis-backed RQ for job processing
- RESTful query endpoint
- SSE streaming for live updates
- Health checks and metrics
- AI summarization (Claude/GPT)
- Structured JSON output with enriched context
- Environment-based configuration
- Graceful shutdown
- Error handling and retries
- Multiprocessing support
This repo is licensed under AGPL-3.0.
