← All projects End-to-end walkthrough

From identity attack to investigated alert

Okta · Elastic Security · Python · Claude API · Slack

What this demonstrates: one alert followed through every stage I built: identity telemetry, detection, AI-assisted triage with personal data kept out, and a clear line between what the agent may do and what a person decides. Six of the seven steps ran live in one test; the last is designed but not built, and this page says so.

Homelab project: a fake company, real systems, my own test traffic.

The test

On October 28 I played the attacker against my own lab and its company, Bionic Footwear (fictional). From one private browser window I tried to sign in once with a made-up outside email address, then entered a wrong password five times for a fake employee. Then I watched what each part of the lab did with it, without touching anything until the verdict was in Slack.

Everything below is rebuilt from that run. Names, addresses and IDs are replaced with made-up values; times, counts and event names are real.

StepStatus
1. Suspicious sign-ins in OktaRan live
2. Events reach the SIEMRan live
3. Detection firesRan live
4. The AI agent investigatesRan live
5. Personal data replacedRan live
6. Verdict posted to SlackRan live
7. A person decides; approved actions runDesigned, not built. Today the agent can only recommend.
Walkthrough Times are Pacific, 2026-10-28. Made-up names and IDs.
  1. 12:58–12:59 PM · step 1 of 7 · ran live

    Password guessing against a fake employee

    One sign-in with a made-up outside address, then five wrong passwords for user-a, all from the same browser and address within 71 seconds. Okta logs each one as a failed user.session.start.

    12:58:12  user.session.start  FAILURE  someone@outside.example
    12:59     user.session.start  FAILURE  user-a@example-company   VERIFICATION_ERROR
      ...     (5 for user-a in about six seconds, the last at 12:59:23)

    Limit: the target account was never activated, so this is guessing at an account that can't sign in. No account takeover was simulated.

    ← Back
  2. by 1:05 PM · step 2 of 7 · ran live

    The events reach the SIEM

    Elastic Agent reads Okta's audit log every minute, signing in as its own read-only OAuth app. All six failures were in Elastic, with one source address between them. An hourly check matches every Okta event ID against Elastic, so a lost event would alert on its own.

    logs-okta.system-*   user.session.start   failure   6
      user-a: 5     distinct source addresses: 1
  3. 1:00–1:03 PM run · step 3 of 7 · ran live

    The detection fires

    The rule counts failed sign-ins per user: 5 or more in one run fires. It runs every 5 minutes, looks back 6 minutes, and counts by ingest time so late events still count. It fired once for user-a.

    ALERT  Okta: repeated failed sign-ins for one user
           user.name: user-a@example-company   count: 5
  4. 1:05 PM · step 4 of 7 · ran live

    The AI agent investigates

    A timer checks for new alerts every 5 minutes and claims each alert once. The agent can call only read-only tools, at most 15 per run, with a token cap. Here it looked up the alert, the user's sign-ins, everything else from that address, the account and its groups.

    tick: 1 open alerts in 24h, 1 not yet run, running 1
    totals: 6 of 15 tool calls, 1 model request,
            9,515 of 150,000 tokens, 0 strings that looked like injection
    posted to Slack (verdict)
    exit=0 verdict

    The "0 strings that looked like injection" is a pattern count for this one run's tool output. It shows nothing suspicious came back this time; it doesn't prove the agent resists injection. That's covered by tests that assume the model obeys injected text and check that code rejects what it asks for.

    The risk score is computed in code, not by the model. The model's answer has to pass checks before it's posted: every piece of evidence must cite events the tools returned.

  5. 1:05 PM · step 5 of 7 · ran live

    Personal data replaced before the model sees it

    The lab's sign-ins come from my own devices, and anyone can type anything into a sign-in page. So addresses, places and outside emails leave the tools as stand-ins. The fake employee's address stays, because the verdict has to say who the alert is about.

    source address    203.0.113.x          →  [masked-ip-1]
    location          (city, country)      →  withheld
    outside username  someone@outside...   →  [email-1]
    fake employee     user-a@example-company  (kept)

    Afterwards I scanned the run's 18 audit records: no outside address in any of them. Those records keep tool arguments, not tool output, so the post itself is where the stand-in shows.

  6. 1:05 PM · step 6 of 7 · ran live

    The verdict in Slack

    Bionic Footwear triage: suspicious, risk 40/100
    Needs a person: yes
    User: user-a
    Summary: Five failed sign-ins for user-a came from [masked-ip-1]
    within about six seconds. The same address also tried one other
    account, [email-1], once. The account has never been activated
    and none of the sign-ins succeeded. Worth watching; nothing shows
    a compromise.
    Recommended (advice only, nothing was done):
    - watch_user
    - ask_user: should this account see sign-in attempts at all?
    Not checked: reputation of [masked-ip-1], country (withheld)

    It also flagged that the alert's time window (1:00–1:03) didn't match the events (12:59). That's expected: the rule counts by ingest time, and the window is the run's. I'd rather it raise a small mismatch than smooth it over.

  7. designed · step 7 of 7 · not built

    A person decides

    Today the agent has no tools that change anything. It recommends from a fixed list, and I act by hand. For this alert there was nothing to do: it was my own test, against an account that can't sign in.

    The design for approvals, written and reviewed but not built:

    suspend_user or revoke_sessions recommended
      → Slack post gets Approve / Deny, for me only
      → the button carries a one-time ID, not the target
      → expires after 15 minutes; at most 3 actions an hour
      → protected accounts (admins, service accounts) get no buttons
      → a separate process with a narrow Okta role acts
      → it checks the change is visible, then records it
    unsuspend stays manual

What I'd point out to a reviewer

The pieces, in more detail