Inspiration

What it does

How we built it

Challenges we ran into

Accomplishments that we're proud of

What we learned

About the project (copy everything below):

# SentinelShift

**A background security duty officer — not another app your team has to open and manage.**

SentinelShift runs autonomously. It ingests security alerts, gathers evidence through a multi-agent Strands graph, applies a deterministic policy gate, and closes safe routine work on its own. It only surfaces when there is a real decision to make — and when it does, one human tap (console, CLI, Slack, Telegram, Discord, or webhook) produces a fully audited, evidence-backed outcome.

Built for small financial institutions. GitHub secret-exposure is mission #1, but dependency CVEs and anomalous deploys run through the same pipeline. New missions plug in without touching the policy gate.

**The LLM recommends. `policy.py` enforces.**

**Stack:** Python · Strands Agents SDK · Groq (`llama-3.3-70b-versatile`) via LiteLLM · FastAPI · HTMX · SQLite · Docker

---

## Inspiration

A lean security team at a small bank or SACCO is the after-hours duty officer for more than one class of alert. A GitHub secret leaks. A critical CVE lands. A production deploy looks wrong. For each event someone still has to:

1. Notice the alert.
2. Open the repo, inventory, deploys, ownership, tickets, and the runbook.
3. Decide if it is real and active.
4. Find the owner.
5. Ask for approval.
6. Follow up and document closure.

That is repetitive work. Production action itself is still too risky to hand to a chatbot.

I did not want an “AI SOC copilot” you babysit in a tab. I wanted a **background duty officer**: gather the evidence packet, apply a hard policy gate, auto-close the boring safe stuff, and only wake a human when something can actually hurt. One tap. Same audit trail whether you hit Approve in the quiet inbox, the CLI, Slack, Telegram, Discord, or a webhook.

The constraint I forced into the architecture:

> The model may recommend. Policy must authorize. Containment is token-gated and one-shot.

---

## What it does

```text
Alert → mission framing → dedup → evidence packet →
deterministic policy → one human decision → token-gated action →
verified closure

Missions (router picks the Strands graph from alert.type):

  • github.secret_exposure (default)
  • vuln.dependency
  • deploy.anomalous

Each graph is Triage → Evidence → Runbook → Risk. New missions register in missions.py with a graph_id. They change framing, runbooks, and read-only tools — never policy.py.

Policy outcomes you can actually demo:

Fixture What happens
AL-001 Production credential used nine minutes ago → approval requested → token-gated containment
AL-004 Test-fixture secret → ignored as false positive, quietly closed
AL-006 / AL-021 Staging / low severity → auto-resolved
AL-011 / AL-027 Prompt injection in alert text → approval still requested; containment tools never exposed to the model
AL-025 Critical CVE in production → approval requested
AL-026 / AL-030 Low CVE in dev/sandbox → auto-resolved
AL-028 Anomalous production deploy → approval requested
AL-029 Staging deploy in a known change window → auto-resolved

Operator surface: setup wizard (/wizard), quiet inbox with SSE, headless CLI, messaging gateway (Slack / Telegram / Discord / signed webhooks), outbound event subscriptions (sentinelshift.event/v1).

Proof on this build: 91 tests green · 30/30 eval scenarios · 0% false-positive error on explicit fixtures · 0 containment calls on 24 blocked injection attempts · python scripts/verify.py reproduces it.


How I built it

Shape of the system

Webhook / fixture  →  async queue  →  SQLite dedup  →  graph router
        │
        ├─ SecretExposureGraph
        ├─ VulnDependencyGraph
        └─ AnomalousDeployGraph
                    │
                    v
           DeterministicPolicy
           ├─ safe + reversible  → automatic closure
           └─ production / ambiguous → quiet inbox
                    │
                    v
           one human Approve/Reject
                    │
                    v
           token-gated containment + immutable audit

Two ideas did most of the work:

  1. A multi-agent Strands graph per mission, not one mega-prompt. Role-scoped tool allow-lists. Evidence collection is the graph’s job; authorization is not.
  2. A deterministic policy gate outside the model. Operator-preference memory after approve/reject/auto-resolve is advisory only and cannot bypass policy.

Durable actions (approve / reject) always do the same five things, from any channel:

  1. Resolve the decision row once (PENDING → APPROVED/REJECTED). A second resolution fails.
  2. Append immutable audit events.
  3. Write evidence JSON under evidence/{job_id}/.
  4. Consume the one-time approval token so containment cannot be replayed.
  5. Optionally notify connected channels that the outcome stuck.

Local adapters (SQLite, in-process queue, local evidence, APScheduler) sit behind the same contracts production AWS adapters would implement. No AWS account required to reproduce.


Challenges I ran into

1. Not building a SOC chatbot.
The first instinct is a chat window that “helps investigate.” Operators at small FIs do not need another app. They need silence until a real decision exists. Designing a quiet inbox (and making auto-resolve feel trustworthy) was harder than wiring Groq.

2. Keeping the model away from the kill switch.
If the LLM can call revoke/rotate, prompt injection in an alert body is game over. Injection fixtures (AL-011, AL-027) forced a product decision: containment tools are not on the model’s allow-list. Approval is a durable action with a one-time token. Measured result: 0 containment calls on blocked injection attempts.

3. One policy gate, many missions.
Secret-exposure, CVEs, and weird deploys wanted different runbooks without forking authorization. Mission specs + a router, with policy.py frozen, was the discipline. Tempting to special-case production secrets inside the graph. That would have been the end of the trust story.

4. Approvals that are real from Slack and the CLI.
Outbound packets are easy. Making Slack/Telegram/Discord/webhook Approve hit the same path as the console (audit, evidence, token consume, already_resolved on replay) took more plumbing than the agents. HMAC signatures, public URL constraints, and 409-on-replay were non-negotiable.

5. Dedup races and “did we drop that alert?”
At-least-once webhooks plus a restarting worker will double-fire or lose work if you enqueue before persist. Durable intake, SQLite fingerprints, and startup recovery were unglamorous and load-bearing. Same for eval: 30 fixed scenarios so judges (and I) can rerun verify.py instead of vibes.

6. Demo vs production honesty.
Simulated containment, loopback bind, optional console tokens — called out in the README. Shipping a fake “we rotated prod” without the dry-run/sim boundary would have been a worse demo than a honest local adapter.


What I learned

  • Background agents beat copilots for after-hours security work. The product is the packet and the one tap, not the conversation.
  • Policy-as-code is the product. Graphs can be creative; authorization cannot. Mean deterministic policy time on the recorded eval run was (2.553\,\mathrm{ms}) — fast enough that the gate is not the bottleneck.
  • Tool allow-lists are a security control, not a DX nicety. Injection containment is measurable (0 calls) only if the model physically cannot see revoke.
  • Channel-agnostic durable actions are how you get “approve from Telegram” without forking audit. If two paths can approve, you will get a split brain.
  • Missions should be data. Registering a graph_id without touching policy.py is how a fleet grows without rotting the trust boundary.
  • Reproducibility is a judging feature. 91 tests + 30/30 eval + Docker + verify.py mattered as much as the wizard.

Next: real GitHub/containment adapters behind the existing contracts, SSO-backed approver identity, more missions (IAM, backups, SIEM triage), and audit export with retention — same gate, bigger surface.


SentinelShift: evidence in, one human decision out — the model recommends, policy decides.

Built With

Share this project:

Updates

Submission history