尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证GuardRail: a mini SIEM security monitoring dashboard (Next.js, TypeScript, Postgres) for HomeHaat and Oppora
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗A mini SIEM (security monitoring dashboard) for my two live websites, HomeHaat and Oppora. They send HMAC-signed security events; GuardRail runs six detection rules mapped to MITRE ATT&CK, raises alerts, emails me about the serious ones and shows everything live.
Live: https://guardrail-rasmitha.vercel.app · Try the read-only demo: https://guardrail-rasmitha.vercel.app/demo
The dashboard itself has exactly one account (mine, with 2FA). The demo shows the same pages with made-up data, clearly labelled, and can't change anything.
▶ Watch on YouTube (or download the MP4)
A one-minute explainer: the attacks live sites face, how signed events become alerts, the six MITRE ATT&CK rules, the
two real bugs GuardRail found, privacy, and what it's built with. Drawn on a canvas and encoded in Chrome
(docs/demo/maker/), with original background music.
All screenshots use the demo's made-up data (IP addresses from the ranges reserved for documentation, masked
example.com accounts).
| Overview: events per hour (stacked by site), busiest IPs, live feed, open alerts | Incident: what to do, the evidence, MITRE ATT&CK link, timeline with notes |
| Alerts: one alert per attack, severity, status | Log upload: an nginx/Apache or sshd log checked with the same rules, never stored |
| Home page with the 3D globe | What it catches: each card turns over to show when the rule fires |
Collects signed events from HomeHaat (Django on Render) and Oppora (Django on Render): failed and successful
sign-ins, admin sign-ins, rate-limit hits, scanner paths (/.env, /wp-admin…), forged payment webhooks, refunds,
403s. Each site signs every batch with its own secret; forged or replayed requests are refused.
Detects attacks with six rules, each a MITRE ATT&CK technique:
| Rule | MITRE | Fires when |
|---|---|---|
| Brute force | T1110 | 10+ wrong passwords for one account in 10 minutes |
| Credential stuffing | T1110.004 | one IP fails on 5+ different accounts in 10 minutes |
| Forged payment webhook | T1565 | any payment notification with a wrong signature |
| Scanning | T1595 | requests for scanner paths (medium at 5+ in 5 minutes) |
| Request flood | T1499 | one IP hits the rate limit 50 times in a minute |
| Sign-in from a new IP | T1078 | a sign-in from an IP the account hasn't used in 30 days (high for admins) |
Raises one alert per attack (not one per event), with evidence, plain advice, a status flow (open → acknowledged → resolved), notes and an incident timeline. High alerts send one email (Resend).
Shows it live: Server-Sent Events push new events to the dashboard within ~2 seconds, on free serverless hosting.
Analyses uploaded logs (nginx/Apache access logs, Linux auth.log) with the same rules, entirely in memory.
A public home page with a 3D globe of attack origins (countries only) and a public read-only demo.
components/views/) renders
both the real dashboard and the demo.Last-Event-ID, so nothing is missed or doubled. One
connection per tab, a polling fallback, and it pauses while the tab is hidden.
(app/api/stream/route.ts, components/live/LiveProvider.tsx)202 first; the rules run in Next.js
after(), so the sites' 2-second timeout never includes detection or email.
(lib/detection/engine.ts)components/landing/)components/charts/EventsChart.tsx)apiFetch is the
only place the browser calls the API; Drizzle ORM gives typed SQL.timestamp:nonce:raw body with a per-site secret, compared in
constant time; a ±5-minute window; each nonce is a primary key, so a captured request works exactly once, even
under concurrency. Every failure gives the same 401. (lib/ingest/signature.ts)UNIQUE (source, uid) stores it once. Found by testing: a slow first answer made HomeHaat retry a saved batch.UNIQUE (group_key) WHERE status <> 'resolved') plus ON CONFLICT DO NOTHING and SELECT … FOR UPDATE. (lib/alerts/store.ts)'strict-dynamic'), an Origin check on every POST (CSRF),
httpOnly __Host- cookies, HSTS, no framing.meta allow-list, raw IPs wiped after 30 days by a daily cron while a
keyed hash keeps the statistics working, alert emails without IPs, uploaded logs never stored, and the public
globe sees only country totals with at least 3 events per country.localhost (checked by parsing the URL, so
[email protected] is refused) and uses documentation-only IP ranges; tests that delete data refuse any database
except the local throwaway one.A race condition in Oppora's rate limiter. My attack simulator sent 75 sign-in requests to a local Oppora, 8 at
a time, against a limit of 20 a minute. 70 got through. Django REST Framework's throttle reads the request
history from Redis and writes it back in two separate steps, so requests arriving together all read the same old
history (check-then-act). The fix is an atomic counter (Redis ADD + INCR): now exactly 20 pass and 55 are
blocked, with a regression test that fires the requests in parallel.
Every Oppora visitor looked like Cloudflare. After going live, an alert showed the IP 172.69.131.168, a
Cloudflare address. On Render each request passes through Cloudflare and Render's relay (and Vercel, from the
website), and Oppora counted the wrong number of hops back in X-Forwarded-For. So visitors behind the same
Cloudflare data centre shared one sign-in limit. I measured the real chain with fake documentation addresses in
the header, then fixed it: the website passes the visitor's IP with a shared secret (Django trusts it only with the
secret), and requests straight to Render count back to the address Cloudflare itself wrote, which can't be faked.
HomeHaat got the same fix for requests that skip its website.
flowchart LR
subgraph Sites["The two sites (Render, Singapore)"]
HH["HomeHaat API<br/>Django"]
OP["Oppora API<br/>Django"]
end
HH -- "signed batch<br/>HMAC-SHA256, 2 s timeout<br/>(outbox + retry if down)" --> ING
OP -- "signed batch" --> ING
subgraph GR["GuardRail (Vercel, Singapore)"]
ING["POST /api/ingest<br/>signature → nonce → validate"]
DET["after(): 6 MITRE rules<br/>→ one alert per attack"]
SSE["GET /api/stream<br/>Server-Sent Events"]
UI["Dashboard (sign-in + 2FA)<br/>Demo · Home page globe"]
CRON["Daily cron<br/>wipe IPs after 30 days"]
end
ING --> DB[("Neon Postgres<br/>Singapore")]
ING --> DET --> DB
DET -- "high alerts" --> MAIL["Resend email"]
DB --> SSE --> UI
CRON --> DB
| Part | Choice |
|---|---|
| App | Next.js 16 (App Router, proxy.ts, after()), React 19, TypeScript strict |
| Styling | Tailwind CSS v4 with design tokens; Chakra Petch, IBM Plex Sans / Mono |
| 3D | Three.js + React Three Fiber |
| Database | PostgreSQL 17 (Neon in production, Docker locally), Drizzle ORM + migrations |
| Validation | Zod (requests, events, settings) |
| Auth | Argon2id (@node-rs/argon2), own TOTP (RFC 6238), AES-256-GCM, qrcode |
| Resend HTTP API (no SDK) | |
| Tests | Vitest (147 unit tests), Playwright (35 end-to-end tests) |
| Hosting | Vercel Hobby (functions in Singapore, daily cron), Neon free plan |
| The senders | Python in each Django site: a background-thread client with an outbox table |
You need Docker Desktop and Node.js 22+ on Windows.
Start GuardRail.bat (or run .\start-guardrail.ps1). It starts PostgreSQL in Docker, creates
.env with random local keys, installs packages, runs migrations and opens the app on http://localhost:3100.npm run admin:create to make your account, then sign in and set up 2FA.npm run connect:homehaat (or connect:oppora) once, then
npm run simulate (attacks your local copy only) and watch /dashboard.Tests: npm run check (lint + types + unit tests) and npm run test:e2e (Playwright on a separate throwaway
database). Putting it online for free: docs/deploy.md.
The plain-English explanation of every key piece ("How it works", with interview notes) is in CLAUDE.md.
I built GuardRail with Claude Code as my AI pair-programmer: I set the brief, the design direction and the rules (free plans only, real data only in the real dashboard, never paste secrets into chat, tests for every key piece of logic, never slow down or break the live sites), made the decisions at each stage, did the dashboard setup myself, and reviewed and tested the results.
<dialog>aria-live