Turn any attack into an alert, in plain English
Write alert rules the way you'd describe the attack: SecureNow picks an instant or a periodic rule, writes it from your app's real data and checks it against that data before you save. Seven rules from one request to a baseline, each attacked live, and one written by a coding agent.
The full written walkthrough is right below — everything in the episode, step by step.
What you'll learn
- ✓Instant rules (every request, as it arrives) versus periodic rules (a query on a schedule), and how to choose
- ✓How a rule drafted from English is checked against your data before you save it, and why every new rule starts in test mode
- ✓Seven detections from one request to a baseline: threshold, distinct count, ratio, sequence and baseline, each attacked live
- ✓How a coding agent turns a hypothesis into a rule over MCP, and checks its own drafts
Turn any attack into an alert, in plain English
An alert is a question your telemetry asks every minute, before anyone has to complain. Here we write SecureNow alert rules the way you'd describe the attack, check each one against real data, then attack our own lab and watch it fire. Everything below is from a real run on tutorial/ep11-alerting, with securenow@8.16.
Two kinds of rule
| Instant | Periodic | |
|---|---|---|
| Runs | on every request, as it arrives | on a schedule (every minute here), over a recent window |
| Sees | one request: path, method, user agent, status, body, headers, client address, host | requests, security events (sign-ins, permission checks, account changes) and your app's own log lines |
| Alerts | within seconds | on its next run |
| Good for | one request that is enough evidence | counts, distinct counts, ratios, sequences, baselines |
One question decides: is a single request enough evidence? Yes: instant. No: periodic.
From English to a checked rule
Describe what to catch, in the dashboard (Alert Rules → Describe in English) or the terminal:
npx securenow alerts rules from-text "One IP address failing 10 or more sign-ins within 5 minutes" \
--app <app-key> --env staging
SecureNow first reads your app's real data: its security event types, their fields and the values they take, your JSON log events and your busiest routes. Then it picks the kind, writes the rule (conditions for an instant rule, a query for a periodic one) and checks it against your data before anything is saved: an instant rule is replayed on your recent requests, a periodic rule is run once. You see what it would alert on today, and you create it only if that's right.
Every new rule starts in test mode: it alerts, and blocks nothing, until you promote it. In test mode the AI review can recommend a block, but it waits for a person.
The lab runs ordinary traffic the whole time (node ep11/attack.mjs normal): five colleagues, Dana, and a backup integration that exports notes every minute.
Rung 1 — One request is enough (instant)
Alert the moment anyone requests a database dump or backup file, like backup.sql, db.sql.gz, or anything ending in .bak
The draft: an instant rule for staging, three conditions (path contains backup.sql, path contains db.sql.gz, path ends_with .bak). The replay over the last 367 requests already found one match, a probe from a few minutes earlier. We created it, then:
node ep11/attack.mjs scan # 203.0.113.51 asks for /backup.sql, /db.sql.gz, /dump.sql, /site.bak
The alert was in the Threat Inbox 6 seconds later, with the three requests that matched. /dump.sql isn't there: the rule matches exactly what its conditions say, so read them before you save.
Rung 2 — Count it (threshold)
One IP address failing 10 or more sign-ins within 5 minutes
Counting needs a periodic rule: every minute, a five-minute window, on the failed-sign-in events, with the threshold in the query:
SELECT attributes_string['http.client_ip'] AS ip, count() AS failed_signins, …
FROM signoz_logs.distributed_logs_v2
WHERE … AND attributes_string['event.type'] = 'auth.login.failure'
GROUP BY ip
HAVING ip != '' AND count() >= 10
Twelve wrong passwords from 192.0.2.77 (node ep11/attack.mjs brute): alert 25 seconds after the last one.
Rung 3 — Spread thin (distinct count)
Credential stuffing from 40 addresses, 3 tries each, one browser string, 6 accounts (node ep11/attack.mjs stuffing). The rung 2 rule never sees it: run right after the attack, it returned 0 rows. No address got past three.
Failed sign-ins that share one browser user agent, hitting 5 or more different accounts from 10 or more IP addresses within 10 minutes
… GROUP BY user_agent
HAVING uniqExact(account_ref) >= 5 AND uniqExact(client_ip) >= 10
It fired while the wave was still running (6 accounts, 33 addresses, 99 failures at that run). Because the rule is in test mode, the AI review recommended a block and waited for approval.
Rung 4 — Mostly not found (ratio)
One signed-in user whose note reads in 10 minutes touch 15 or more different notes, counting both granted and denied permission checks, with at least half of those reads denied as not found
The draft had to learn something about this app: a granted permission check names the note object_id, a denied one names it target_ref. It counted both:
if(attributes_string['event.type'] = 'authz.granted',
attributes_string['object_id'], attributes_string['target_ref']) AS object_ref
…
HAVING uniqExact(object_ref) >= 15
AND countIf(event_type = 'authz.denied' AND reason = 'not_found') * 2 >= count()
Eve walks 30 notes from 203.0.113.60 (node ep11/attack.mjs idor), every one not found: alert 18 seconds later.
Rung 5 — First this, then that (sequence)
A successful sign-in from an IP address the account has not used in the previous 7 days, followed within 5 minutes by a password change on the same account
The query keeps each step visible: the last sign-in and the last password change per account, the change after the sign-in and within 5 minutes (pw_ns - login_ns <= 5 * 60000000000, nanoseconds), and the address checked against the account's 7-day history ((user, ip) NOT IN (…)). The takeover (node ep11/attack.mjs takeover): a new address, Dana's real password, a new password 13 seconds later. Alert on the rule's next run.
Rung 6 — Normal for whom? (baseline)
An account exporting its notes 5 or more times within 10 minutes
Read the check before saving: it would alert right now, on the backup integration, which exports every minute, all day. That's normal for it. So say what you mean:
An account exporting its notes at least 5 times in the last 10 minutes, and at least 5 times its own average per 10 minutes over the 2 hours before that. Check every minute.
Our first, vaguer wording produced a draft that divided the two-hour history by minutes instead of 10-minute periods, and its check showed the integration alerting again. We'd created it with --yes before reading it, so we disabled that rule: read the check first. The precise draft compares recent_exports with prior_exports / 12.0, and its check stayed quiet. Then Dana's account exported 8 times in under two minutes (node ep11/attack.mjs exfil): the alert named Dana, and only Dana (8 exports against a prior average of 0.08 per 10 minutes).
Rung 7 — Hand an agent a hypothesis
An attack nobody had a rule for: after a takeover, the attacker changes the password, creates an organization and invites an outside address as an admin, a way back in that a password reset doesn't close.
claude mcp add securenow -- npx securenow mcp
bash ep11/agent-run.sh <app-key> build
Claude Code used securenow_alert_rule_draft five times and turned four drafts down, each with its reason: an event this app never sends, a nanosecond gap compared against 3600, a query that failed, a path read from http_url instead of http.target. It created the fifth with securenow_alert_rule_create (test mode; the tool can't set any other mode) and said which step it wasn't sure of.
node ep11/attack.mjs persistence
bash ep11/agent-run.sh <app-key> verify
Four minutes after the invite, the rule fired. The agent checked it: Dana's account, the attacker's address, every step in order, and a reminder that blocking the address won't remove the pending invite.
Running it for real
- Read every draft. The check is there so you can: it shows what would alert today.
- Keep new rules in test mode until they've proved themselves on real traffic, then promote them (
npx securenow alerts rules promote <id>) so automations can block, rate-limit or challenge. - An alert is skipped for an address that's already blocked, so an older armed rule can fire first and keep a new one quiet.
What you just did
Seven rules, from one request to a baseline, each written in a sentence, checked against real data and caught live, and one more written by an agent. Write down the attack you're worried about, in one sentence. Then make it an alert.
Try it on your own app
Start the threat model, then plug SecureNow in — the whole series works on your project today.