← All projects
Case study · Security operations · Detection engineering

SignalBridge

A detection lab that catches access that should have ended. Two test apps send signed events about who tried to read what; five detection rules flag access that continues after a permission was removed, and an analyst takes each finding to a verified fix. I ran it against real Keycloak, Wazuh, OWASP ZAP and Shuffle in an isolated lab.

Status
Public source, signed v0.1.0 release
Timeline
September–October 2026
Stack
Python, Django, PostgreSQL, Docker
Works with
Keycloak, Wazuh, OWASP ZAP, Shuffle SOAR, Splunk, Prometheus and Grafana
The SignalBridge evidence viewer showing lab runs from Wazuh, with each record compared against the rule that fired
The evidence viewer: each lab run with its record, failed runs included.

Key decisionRe-check permissions inside the write itself. A test showed that checking first let a just-removed user keep writing, so the check now runs in the same database transaction as the change.

What it shows

Detection engineering

Five versioned rules, rewritten as Sigma with MITRE ATT&CK tags, then run as SPL in Splunk and as KQL in a Kusto emulator.

Real security tools

Keycloak sign-in with MFA, a Wazuh SIEM, OWASP ZAP scans and a Shuffle SOAR workflow, all in an isolated lab.

From alert to fix

Each finding goes from case to analyst review to a retest, and nobody can approve their own fix.

Reliable event intake

Signed, de-duplicated events that survived a killed worker, a stopped collector and a 13.5-hour run.

How it works

SignalBridge started as a monitor for two of my own apps, BetTail and Netted. For the lab I built two test apps, one for documents and one for expenses, with a deliberate access bug, so a real failure could be caught from start to finish.

Everything runs on one PC: pinned images on an internal Docker network with no internet route, and Shuffle in a separate VM with no network adapter.

Follow an event Choose a stage

  1. 01App event
  2. 02Signed intake
  3. 03Detection rules
  4. 04Analyst review

Stage 01 / App event

What did the app see?

Each test app sends a small event: which account tried to read which record, and whether it was allowed. Accounts are pseudonyms, and the event has no fields for names, emails, IP addresses or record contents.

Boundary

A signature proves which app sent the event, not that the app reported correctly.

Stage 02 / Signed intake

Can the event be trusted?

Every event is signed with HMAC-SHA-256 and checked against a fixed schema. Events older than 5 minutes are rejected, an exact repeat is accepted once, and a reused ID with different content is refused.

Boundary

Each app can send 600 new events a minute; more are refused.

Stage 03 / Detection rules

Why did this open a case?

Five rules look for repeated failed reads, reads after a permission was removed, slow probing across many records and one record denied to several accounts. Each case keeps the events that triggered it.

Boundary

Activity below a rule's threshold isn't flagged; the evaluation counts those misses.

Stage 04 / Analyst review

Who can close it?

Viewer, analyst and reviewer roles are scoped to each app. Analysts assign the case, open a fix task and import a retest; a different reviewer approves the fix.

Boundary

The console is built for a local lab, not the public internet.

What testing found

Each of these came from a check that failed. The fix and the test that proves it are in the repository, and lessons learned covers the details.

Problems found by testing, what happened before the fix, and what happens now
ProblemBefore the fixNow
Write raceA user just removed from a workspace could still make 4 case writesPermissions re-checked inside the transaction; 13 regression tests
Sign-in blockedA keyboard-only browser test showed the console's security policy blocked the redirect to KeycloakPolicy fixed, with its own regression test
Generated Splunk querypySigma's Splunk output for one rule compiled, but Splunk refused to run itHand-written SPL with rolling windows; generated and hand-written queries labeled
Default lab passwordScanner triage flagged a default database password and open backend portsAll 8 database credentials rotated, ports closed, memory and CPU capped
Wazuh log limitThe collector stopped at 10.6 hours of a planned 24-hour runLog-folder limit fixed
AccessibilityAn axe scan found six kinds of issue, including a nearly invisible link0 violations on 19 pages, light and dark

Results

October 1–5, 2026, in an isolated lab with synthetic data. Each row links the record its run wrote.

Lab runs, their results and run records
RunResultRecord
Test apps with a planted access bug23 events processed; the bug opened one caseRun
Keycloak sign-in, password and TOTP28 protocol checks and 5 browser checks passedRun
Wazuh collection8 alerts, each raised once; nothing lost after a collector stopRun, recovery
OWASP ZAP1 finding on the faulty build, none after the fixRun
Finding to verified fixSelf-review refused; an independent reviewer approvedRun
Shuffle SOAR workflowReplayed, changed, wrong-app and stale requests refusedRun
PostgreSQL races and a killed worker14 checks passed, no partial casesRun
Continuous run27,118 events delivered over 13.5 hoursRun
Prometheus and GrafanaBacklog alerts fired and cleared; 24 checks passedRun
Detection evaluation, 48 scenarios71% precision (15 of 21), 65% recall (15 of 23)Run
Each field of an alert compared with the Wazuh rule that fired
Each alert field compared with the Wazuh rule that fired.
A delivered record matched to Wazuh's result by ID
A delivered record matched to Wazuh's result by ID.

Detection as code

I rewrote the five rules as Sigma with MITRE ATT&CK tags (T1213, plus T1078 for the two revocation rules), compiled them with pySigma and ran them in real engines against the same 48 scenarios. Running them found a problem compiling alone didn't: Splunk refused pySigma's query for one rule.

How often each engine matched the Python rules on 48 scenarios
EngineMatched the Python rulesWhy not all 48
KQL in Microsoft's Kusto emulator48 of 48—
Hand-written SPL in Splunk Free45 of 48Splunk drops events exactly on a window edge
Compiled Sigma in SQLite45 of 48Sigma can't express a re-granted permission
pySigma's generated SPL40 of 48Splunk refused one query; fixed time bins missed two

Access after departure, from AccessOps

AccessOps, my offboarding project, sends signed Shared Signals events (CAEP and RISC) when it cuts off a departing employee and when their account is used afterward. SignalBridge polls them over TLS, verifies each signature, issuer and audience, stores each event once and only then acknowledges it.

38 events verified, none refused

Two live rounds against the running AccessOps lab. Each real poll followed a dry run that verified without storing anything.

A critical case when it mattered

In round two, a contained test account was re-enabled and used, and SignalBridge opened a critical "access after departure" case.

Limits

Discuss SignalBridge

Ask about the write race, the query Splunk refused or how a leaver event from AccessOps becomes a case. Email me at aagarwalcollege@gmail.com.

Project screenshot

Open original ↗

View full size to read details. Scroll or swipe to move around the enlarged image.