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.
- 4writes a removed user could still make, until I found and fixed the race
- 5detection rules, also written as Sigma, SPL and KQL and run in real query engines
- 0events lost or duplicated when I stopped the Wazuh collector mid-run
- 48attack and benign scenarios scored, with misses and false alerts counted
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
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.
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.
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.
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.
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.
| Problem | Before the fix | Now |
|---|---|---|
| Write race | A user just removed from a workspace could still make 4 case writes | Permissions re-checked inside the transaction; 13 regression tests |
| Sign-in blocked | A keyboard-only browser test showed the console's security policy blocked the redirect to Keycloak | Policy fixed, with its own regression test |
| Generated Splunk query | pySigma's Splunk output for one rule compiled, but Splunk refused to run it | Hand-written SPL with rolling windows; generated and hand-written queries labeled |
| Default lab password | Scanner triage flagged a default database password and open backend ports | All 8 database credentials rotated, ports closed, memory and CPU capped |
| Wazuh log limit | The collector stopped at 10.6 hours of a planned 24-hour run | Log-folder limit fixed |
| Accessibility | An axe scan found six kinds of issue, including a nearly invisible link | 0 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.
| Run | Result | Record |
|---|---|---|
| Test apps with a planted access bug | 23 events processed; the bug opened one case | Run |
| Keycloak sign-in, password and TOTP | 28 protocol checks and 5 browser checks passed | Run |
| Wazuh collection | 8 alerts, each raised once; nothing lost after a collector stop | Run, recovery |
| OWASP ZAP | 1 finding on the faulty build, none after the fix | Run |
| Finding to verified fix | Self-review refused; an independent reviewer approved | Run |
| Shuffle SOAR workflow | Replayed, changed, wrong-app and stale requests refused | Run |
| PostgreSQL races and a killed worker | 14 checks passed, no partial cases | Run |
| Continuous run | 27,118 events delivered over 13.5 hours | Run |
| Prometheus and Grafana | Backlog alerts fired and cleared; 24 checks passed | Run |
| Detection evaluation, 48 scenarios | 71% precision (15 of 21), 65% recall (15 of 23) | Run |
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.
| Engine | Matched the Python rules | Why not all 48 |
|---|---|---|
| KQL in Microsoft's Kusto emulator | 48 of 48 | — |
| Hand-written SPL in Splunk Free | 45 of 48 | Splunk drops events exactly on a window edge |
| Compiled Sigma in SQLite | 45 of 48 | Sigma can't express a re-granted permission |
| pySigma's generated SPL | 40 of 48 | Splunk refused one query; fixed time bins missed two |
Sigma rules and engine results · Splunk run · Kusto run · Replay report
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.
Receiver design · Dry run · Round 1 · Round 2
Limits
- Synthetic data in an isolated lab on one PC; not a production deployment.
- I wrote the 48 evaluation scenarios knowing the rules, so the scores catch regressions rather than measure real-world accuracy.
- The planned 24-hour run stopped at 10.6 hours of Wazuh capture, and I didn't repeat it.
- ZAP ran one passive rule on captured traffic, not an active scan.
- The KQL ran in the Kusto emulator, not Microsoft Sentinel.
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.

