Inspiration

It was 2:47 AM. The auth service was down. Users were authenticating with revoked credentials. Someone had cached auth tokens in Redis — and password resets weren't invalidating the cache.

Three weeks earlier, SENTINEL would have flagged this exact pattern in a Merge Request. The warning was there. It was dismissed. And then it was forgotten.

That's the moment I realized the problem isn't that teams don't have security tools. Every team has Copilot, SonarQube, Snyk. The problem is that dismissed warnings have no memory. They live in MR threads nobody reopens, in comment threads that scroll off the screen. When the pipeline fails at 2:47 AM, nobody connects it back to the warning that was ignored three weeks ago.

I built SENTINEL to be that memory.

What it does

SENTINEL is a two-agent security system that watches your GitLab project and connects dismissed warnings to future production incidents.

Prevention Mode — MR Scanner When a developer opens a Merge Request, SENTINEL automatically scans the code diff using Gemini 2.5 Flash on Vertex AI. It reasons about vulnerabilities the way a senior security engineer would — not pattern matching, not rules, but actual AI reasoning about what the code does and why it's dangerous. It posts a structured warning comment directly on the MR with severity levels, affected files, and remediation guidance.

Response Mode — Incident Responder When a pipeline fails, SENTINEL fetches the job logs, searches every past warning it has ever posted across all MRs in the project, and correlates the failure to the dismissed warning. It automatically creates a GitLab incident issue that says: here is the warning, here is the MR where it was dismissed, here is the line of code that broke production.

Both agents run automatically — triggered by GitLab webhooks, hosted on Cloud Run, with zero developer intervention required.

How we built it

Agents: Two Python agents — mr_scanner.py and incident_responder.py — each with a single responsibility. The MR scanner calls the GitLab API to fetch the diff, sends it to Gemini 2.5 Flash with a security-focused prompt, and parses the JSON findings back into a formatted GitLab comment. The incident responder fetches failed job logs, searches all MR notes for past SENTINEL warnings, runs a correlation algorithm to match log keywords to warning patterns, and creates a GitLab incident issue with the full chain.

AI: Gemini 2.5 Flash on Vertex AI via the google-genai SDK with Application Default Credentials. The model returns structured JSON findings — severity, title, detail, file, code snippet — which SENTINEL formats into a readable MR comment.

Infrastructure: FastAPI webhook receiver routes GitLab events to the correct agent using background tasks, so GitLab never times out. Deployed on Google Cloud Run with all secrets passed as environment variables. The GitLab webhook is registered programmatically against the demo project.

GitLab MCP Tools used: get_merge_request_diffs, create_merge_request_note, get_pipeline_jobs, create_issue, list_merge_request_notes

[Screenshot: Cloud Run service showing sentinel-agent live with health endpoint returning {"status": "SENTINEL is running"}]

Stack: Python, FastAPI, Google Cloud Run, Vertex AI, Gemini 2.5 Flash, GitLab API, httpx, python-dotenv

Challenges we ran into

Gemini response parsing on Cloud Run Locally, gemini-2.5-flash returned structured JSON perfectly. On Cloud Run, the same call returned None for response.text. The issue was that response_mime_type="application/json" in the config was incompatible with this model version in the Vertex AI environment. I fixed it by removing the MIME type constraint and instead parsing the raw text response — stripping markdown code fences when the model wrapped the JSON in them.

Token accidentally exposed Early in the project I accidentally typed my GitLab token directly into a terminal prompt instead of as a variable value. The token appeared in the terminal output. I caught it immediately, revoked it, and rotated to a new token — but it was a sharp reminder that secrets in terminal history are a real risk.

GitHub push blocked by secret scanner When pushing to GitHub, the secret scanner blocked the commit because scripts/create_mr2.py contained a fake Stripe key I wrote as demo vulnerable code. The irony of SENTINEL's own demo data triggering a security scanner wasn't lost on me. Fixed by replacing it with a clearly labelled placeholder.

GCP model naming gemini-2.0-flash-001 was listed in the Vertex AI model catalog but returned 404 on actual calls. It took listing available models programmatically to discover that gemini-2.5-flash was the correct callable name — and it turned out to be a better model anyway.

Accomplishments that we're proud of

The correlation actually works. When the pipeline failed because of the auth token caching bug, SENTINEL traced it back to the dismissed warning on MR !1 — automatically. The incident issue it created contained the full chain: warning text, MR link, job log excerpt, and remediation steps. That end-to-end moment, with no human intervention, is exactly what I set out to build.

Gemini found issues regex couldn't. On MR !2 with the payment service code, the pattern matcher found 2 issues. Gemini found 5 — including a HIPAA-relevant observation about storing sensitive financial data in plaintext that I hadn't explicitly programmed for. That's the difference between rules and reasoning.

It's genuinely live. This isn't a prototype with mocked responses. SENTINEL is deployed on Cloud Run, connected to a real GitLab project, receiving real webhooks, calling real Gemini APIs. The health endpoint is publicly reachable right now.

What we learned

Background tasks are non-negotiable for webhooks. GitLab expects a webhook response within a few seconds. The first version of the receiver ran agents synchronously — GitLab would time out and retry, causing duplicate scans. Moving to FastAPI BackgroundTasks fixed this immediately.

Gemini is better at security reasoning than I expected. I built the pattern-matching fallback assuming Gemini would be unreliable. It wasn't — it caught more issues, explained them more clearly, and reasoned about domain implications (HIPAA, financial data exposure) that no regex could ever express.

The story matters as much as the code. The technical implementation is only half the project. The 2:47 AM incident framing, the before/after comparison, the README narrative — these are what make someone understand why this exists in 30 seconds. A tool nobody understands doesn't get used.

What's next for SENTINEL

Google ADK integration — Rebuild the agents using Google Agent Development Kit for proper multi-agent orchestration, memory management, and tool calling. The current implementation is functional but the ADK would give SENTINEL proper agent lifecycle management.

Merge blocking — Use GitLab's merge request approval API to actually block merges when SENTINEL finds CRITICAL severity issues. Right now it warns. Next it should enforce.

Policy-as-code — Move security patterns and severity thresholds to a policies.yaml file checked into each project. Teams customize what SENTINEL watches for without touching Python.

Trend dashboard — A GitLab issue auto-generated weekly with a Mermaid chart showing warnings posted vs. warnings dismissed vs. incidents correlated. Make the memory visible.

Slack/Teams integration — Post SENTINEL warnings to the team channel when a CRITICAL issue is found, so dismissing it requires a conscious team decision rather than a single developer click.

Multi-project support — One SENTINEL deployment watching an entire GitLab group, with cross-project memory to detect when the same vulnerability pattern appears in multiple repos.

Built With

  • fastapi
  • gemini
  • gitlab
  • gitlab-mcp-server
  • google-cloud-run
  • google-genai
  • httpx
  • python
  • python-dotenv
  • uvicorn
  • vertex-ai
Share this project:

Updates