CyberGuard AI
An agentic RAG platform for security advisory intelligence: natural-language answers over GitHub Security Advisories, grounded in sources and running fully local.
What I built
CyberGuard AI is a question-answering platform for security advisories. It ingests GitHub Security Advisories (GHSA records with CVE IDs, CVSS scores, affected packages and ecosystems), indexes them, and lets you ask questions in plain English (“what are the most critical vulnerabilities this week?”), getting a sourced answer with citations you can click through.
It goes beyond the usual RAG demo: there’s an agentic mode where a LangGraph agent decides how to answer, advisory-scoped chat (click any advisory and the conversation is pinned to its chunks), PDF analysis over your own documents parsed into the same index, user accounts with OAuth and JWT, conversations, analytics, and an admin surface. The UI offers standard vs agentic answering and a Hybrid / BM25 / Vector search-mode selector, so you can feel the difference between retrieval strategies yourself.
Why I built it
Security advisories are dense, scattered and hard to search. The information you need usually exists (spread across vendor bulletins, CVE records and patches), but getting an answer means reading dozens of documents and stitching facts together yourself.
A general-purpose LLM is of limited help here: it will happily answer from stale training data, which is exactly what you can’t accept when the answer must reflect the current advisory corpus. That failure mode is why retrieval, grading and citations are the core of this system, not an afterthought. And because security data is sensitive, I wanted inference to run locally end to end: the default model is a 1B-parameter Llama served by Ollama, so nothing leaves the machine.
It started as an educational RAG codebase, then grew into a security-focused, multi-service platform: the agent graph, ingestion pipeline, gateway and UI all got rebuilt around real advisory data.
How the corpus gets in
- A .NET Hangfire recurring job runs daily at 06:00 UTC, fetching advisories modified in the last 7 days from the GitHub advisories REST API, filtered to high and critical severity across npm, pip, NuGet and Maven.
- Advisories are section-aware chunked (600-word chunks, 100-word overlap), embedded with Jina embeddings v3 (1024-dim, separate retrieval.passage / retrieval.query tasks), and bulk-indexed into a single OpenSearch index; your uploaded PDFs land in the same index, discriminated by a source_type field.
- The vector field is HNSW with cosine similarity, and every chunk carries denormalized metadata (GHSA ID, CVE ID, CVSS score, affected ecosystems and packages), so retrieval results are filterable and displayable without extra lookups.
How a question becomes an answer
- Guardrail: an LLM scored 0–100 decides if the question is in scope (threshold 40), with a regex fast-path: a well-formed CVE or GHSA ID routes instantly with no model call at all.
- Retrieve: a hybrid query hits OpenSearch: BM25 multi-match and kNN vector search run in parallel and are fused by OpenSearch’s native Reciprocal Rank Fusion (rank constant 60). If embedding fails, it degrades to BM25-only instead of erroring.
- Grade: the agent binary-grades each retrieved chunk for relevance before using it. The yes/no is parsed from plain text deliberately: forced JSON-schema output triples latency on a 1B model.
- Rewrite or answer: if grading says the evidence is weak, the query is rewritten and retrieval runs again; otherwise the answer is generated from the graded context, with conversation memory (the last 6 turns) carried through a per-thread LangGraph checkpointer.
- Citations: sources are extracted from the agent’s retrieval step and streamed to the UI first. The .NET gateway emits a Server-Sent Events stream that opens with the sources, then re-emits the answer word by word so the page feels live.
How it’s bundled and hosted
The whole system ships as a Docker Compose stack: a .NET 8 API gateway, five FastAPI services sharing one Python core (advisory ingestion, embeddings, search, agentic RAG, PDF parsing), PostgreSQL, Redis, OpenSearch, Ollama, and a self-hosted Langfuse stack for tracing every agent step.
In production the frontend deploys to Cloudflare Pages, and the API sits behind Caddy on my server. Deploys are a zero-downtime rolling script that builds all images in parallel, recreates one service at a time against health checks, and if anything fails it re-tags the previous images and rolls back automatically, then posts the outcome to Slack. CI connects over Tailscale to run that same script on push to main.
The stack: who does what
- Next.js 14
- The dashboard: chat with streaming answers, a collapsible sources panel, advisory-scoped mode, analytics and admin pages.
- .NET 8
- The API gateway: JWT + OAuth auth, Hangfire job scheduling for daily ingestion, conversations and analytics, and the SSE bridge that streams answers to the UI.
- LangGraph
- The agent: a state graph of guardrail, retrieve, grade, rewrite and generate nodes with a per-thread memory checkpointer.
- FastAPI
- The Python core behind five services: advisory fetching, chunking/indexing, unified search, the agent itself and PDF parsing.
- OpenSearch
- Retrieval: one hybrid index (BM25 + HNSW kNN) fused with a native RRF search pipeline.
- Jina embeddings v3
- 1024-dimension embeddings with task-specific passage and query modes, batched and retried with backoff.
- Ollama
- Local inference: a 1B Llama model for grading, guardrail and generation, so no data leaves the server.
- PostgreSQL + Redis
- Advisory storage, identity, Hangfire jobs, and a shared 6-hour response cache.
- Docker
- Packaging: a multi-service Compose stack deployed by a rolling script with automatic rollback and Slack notifications.