Component · for humans & their agents
Signin Spray Shape
verified · first-partyactively maintained$0 during beta (was $69)
Password spray is horizontal, and per-account thresholds structurally cannot see it. Reads a sign-in log and reports the attack shapes in it — spray, brute force, enumeration, and the successful login that follows them.
by saltyhash · Code Recycle admin
23 tests · 3/3 deliberate defects caught. Zero runtime dependencies.
The blind spot
The obvious detector counts failures per account:
if (failuresFor(user) >= 8) alert(user);That catches a brute-force against one account — the attack people picture, not the attack people get. Spray is deliberately horizontal: three attempts against each of four hundred accounts, using the season-and-year passwords that always work somewhere. No account approaches eight failures, so the detector reports nothing. Lockout policy does not fire either, for the same reason. Both stay silent precisely because the attack is being run competently.
A test pins this rather than asserting it in prose — the same twelve-account dataset that triggers password_spray has a worst-case-per-account count of 1.
detectSprayPatterns(events);
// { kind: "password_spray", severity: "critical",
// detail: "12 accounts saw credential failures within 60m, at most 1 each —
// the horizontal shape a per-account threshold never trips." }The window slides rather than bucketing by clock hour, so a burst straddling a boundary cannot land under the threshold in two adjacent buckets.
The second error, which inflates everything
errorCode !== 0 is not "a failed password". Directory logs record MFA challenges, interrupted flows and policy blocks with non-zero codes too, and those are the most common non-zero codes in a healthy tenant. Counting them does two things:
1. Buries real credential failures under an order of magnitude of noise — and the spray
detector above runs on those counts, so its thresholds stop meaning anything.
2. **Misreads the most important event in the log.** mfa_required means the password was
**correct** and only the second factor stood in the way. That is not a failed attack, it is a
nearly-successful one. It deserves to be louder than a wrong password, not averaged in with it. classifySignIn(50126); // "invalid_credentials" → counted
classifySignIn(50076); // "mfa_required" → NOT counted, escalated separately
classifySignIn(50058); // "interrupted" → not counted
classifySignIn(53003); // "policy_blocked" → not countedWhat it reports
| kind | severity | meaning | |---|---|---| | successful_after_failures | critical | a guess landed. Stop reading, start responding | | password_spray | critical | many accounts, few attempts each, one window | | mfa_wall | high | password accepted, MFA held. Reset these — do not rely on MFA holding | | brute_force | high | one account hammered | | enumeration | medium | attempts against accounts that do not exist |
Findings, not a boolean, because these are different problems with different responses.
Scope and honesty
Error codes are Microsoft Entra's (ENTRA_CODES, exported and extensible). The detection logic is provider-neutral — map your provider's codes to the same Outcome union and everything downstream works. Nothing here is Entra-specific except the table.
What it does not do:
| not included | why | |---|---| | Fetching sign-in logs | you fetch; this analyses | | Impossible-travel / geo-velocity | needs a geo-IP database and a defensible speed model; a wrong one accuses travelling staff | | Blocking or locking accounts | analysis only — auto-lockout on a spray detector is a self-inflicted outage | | Deduplicating repeat alerts | findings-delta | | Mailbox rules the attacker left behind | inbox-rule-tripwire |
Thresholds are defaults (minDistinctUsers: 8, maxAttemptsPerUser: 5, burstThreshold: 8, windowMinutes: 60), not calibrated truth. A tenant of 30 people and a tenant of 30,000 want different numbers, and every one of them is an argument.
Timestamps are parsed with Date.parse, so feed it ISO 8601. Anything else is at the mercy of implementation-defined parsing, which fails silently rather than loudly.
01Capabilities
Does
- + Authentication
- + Security monitoring
- + Bot and crawler verification
Doesn’t
- No exclusions declared
02Requirements & stack
Depends on
No declared dependencies
Credentials needed
None declared
Stack
03Community
No endorsements yetNo verified confirmations yet — be the first.
Confirmations come from verified purchasers, installers, vetted reviewers, or an installation outcome your org reported through the agent tools. They grade quality — security is verified separately, and community votes can never override the security gate.
Sign in to confirm — weight comes from verified usage, not vote count.
Issues 0
Open an issueNobody has reported anything yet — a success counts as a report too.
04Trust Passport
Full passport →0/0 automated components pass. An automated score is never a security guarantee.
- publisher identity Publisher status verified; 1 verification(s) on file
- malicious pattern scan No known malicious-behavior patterns across 7 source file(s) plus listing text
- capability contract All 0 observed capability reference(s) match the declared manifest
- agent safety scan No injection patterns in agent-readable content
- provenance No release signature or provenance attestation
- behavioral sandbox Not performed in this environment — requires the production isolated runner (docs/sandbox-requirements.md). No untrusted code is ever executed on the application host.
Every listing must pass this review before it can be sold, and it is re-run on every release. Verification describes what we checked — it is not a guarantee that the software is safe.
05Versions
Full history →| Version | Channel | Released | Notes |
|---|---|---|---|
| 0.1.0 | stable | Aug 12, 2026 | Initial extraction. |