Agentic phishing triage and threat hunting with Sublime Security
Sakil (Founding Security Engineer)
A phishing report arrives and the clock starts. Somebody has to decide whether the message was a real attempt, who else received it, and what happens to the copies still sitting in mailboxes.
The first decision is reading, and it is what triage is built around: the headers, the authentication results, where the links actually point, how old the sending domain is.
The other two are a different kind of work. Finding the rest of the campaign means describing the attack rather than the message, and searching mail nobody flagged. Turning what you find into a detection means testing that description, checking it against the rules you already run, and knowing what else it would fire on. Neither is complicated. Both took us longer than the triage that prompted them, and both need somebody who can write queries against mail.
That gap is what this post is about: the threat hunt and the detection rule that come after the verdict, and what it takes to hand those two jobs to an agent without getting back something that reads well and is wrong.
Any mail platform you can query will do for this. We use Sublime Security. It parses inbound mail into a structured model, runs detection rules over it, and gives you a query language called MQL to search that model yourself. Its free Core tier covers all three, which is enough to read a message, hunt for the others and author a rule. Containment actions like quarantine are available for enterprise users.
This post is how Tracecat uses Tracecat to triage malicious emails with agents, starting with phishing on Sublime's free tier. By the end, you'll be able to build an agentic system that:
- Opens one case per flagged or user-reported message group, with the evidence Sublime parsed
- Scopes the campaign by indicator, then hunts the attacker's technique across every mailbox
- Surfaces mail that matches the technique and that the platform took no action on
- Tracks removal of mail as a case task for a human, because the free tier cannot quarantine
- Drafts a candidate detection rule, tests it, and creates it in Sublime only after somebody presses an approve button in Slack
All of it runs on one workflow with three lanes, two agent presets and the skills attached to them.
This automation contains no containment action. Sublime Core cannot quarantine or remove a message, and the automation does not try to. Removal stays a human action in the Google Workspace admin console, tracked as a case task so it can be ticked off.
Automations-as-code
Everything in this post is open source: https://github.com/TracecatHQ/tracecat
What you need
Hover or tap an icon for details
Platform
Data
Actions
Tracecat workspace
A Tracecat workspace with the Tracecat MCP server connected to your coding agent. The agent uses it to build everything else on this list.
Deploy it
Click your coding agent to get a prompt. It reads the workflow, the presets and the skills from the repository, connects to Tracecat over MCP, builds in dependency order, checks each step before moving on, and keeps the schedule off until you have seen a case you believe.
Click to deploy through prompts
How the workflow runs
One workflow, three lanes.
The three lanes write to the same case and the same tables, and they use the same two presets. You test and publish one workflow.
A schedule starts the poll lane. It runs three Sublime queries, drops duplicates, and then takes each message group on its own. Socky investigates it, writes the case, posts the card, and hunts for the other copies.
The three queries ask for different things: the mail Sublime scored as malicious or suspicious, the mail that fired one of its high or critical rules, and the mail somebody reported. They overlap, so a fourth step drops the duplicates and one message group gets one case.
None of that needs judgement, so none of it is given to an agent. It is the same four steps on every run, and we would rather the step that decides what gets a case did not vary between them.
A Slack button starts the click lane. The workflow reads which button was pressed and does that one thing: set a verdict and push it to Sublime, create and enable an approved rule, discard a proposal, scan the links, or acknowledge.
Generate detection starts the detection lane. Detectee drafts a rule, tests it, writes the proposal on the case, and posts it to Slack behind an Approve button.
The two presets
- Socky runs the investigation, the hunt and the Slack card updates. Its tools are read-only against Sublime and the enrichment services, plus the Tracecat case and Slack tools it needs to write its own output.
- Detectee is the detection engineering agent. It reads the rules you already run, writes a candidate, and tests it. It cannot deploy one: nothing in its allowlist creates a rule or turns one on. That step is a workflow action behind a human click.
The skills behind the agents
Skills are loaded on demand, not all at once, so a preset can carry a large library and a run only pays for what it opens. These are the skills this automation uses.
Socky
sublime-phishing-case-lifecycle. The path from one message group to a triaged case and a published card. The case description contract and the Slack delivery contract.email-evidence-core. Which Sublime and Google Workspace reads are permitted, how a mailbox resolves to a person, what each indicator does and does not prove, and the hunt job contract.tracecat-business-context. Deployments, schedules, benign patterns, the employee list.hypothesis-driven-triage. The investigative method and the hypothesis library.case-output. How a case description, brief or evidence table is laid out.case-writing-style. How the case should read, built on ASD-STE100 Simplified Technical English, and the evidential qualifiers a case must keep.slack-case-threads. Mentions, thread replies and button payloads.tracecat-org-chart. Who reports to whom, for naming an owner.
Detectee
email-evidence-core. Which Sublime and Google Workspace reads are permitted, how a mailbox resolves to a person, what each indicator does and does not prove, and the hunt job contract.sublime-detection-engineering. Authoring MQL: the dialect, the required tests, what a backtest can and cannot claim.tracecat-business-context. Deployments, schedules, benign patterns, the employee list.case-writing-style. How the case should read, built on ASD-STE100 Simplified Technical English, and the evidential qualifiers a case must keep.
Agentic threat hunting
Two of those skills do the hunting. email-evidence-core holds the hunt job contract, and sublime-detection-engineering holds the tests a candidate rule has to pass.
Agentic threat hunting means the agent writes the query. Not a saved search with the sender address slotted into it, and not a template somebody wrote for a different campaign. Socky reads the case, works out what the attacker actually did, and writes that as MQL.
That buys two things. The search reaches mail nobody flagged, because it runs over the whole retention window instead of the alert queue. And it survives the attacker changing their infrastructure, because a method described in a query matches the next sender and the next subject as well as this one.
It also gives the detection rule a head start. A query that describes a method and has already been tested against real mail is close to a rule already, which is why the detection step starts from the hunt rather than from the message.
What we found harder was checking the result.
When the agent reads a message, the evidence sits in front of it, goes into the case, and the next person to open that case can see what it used. A hunt is less visible. The query runs across every mailbox and comes back as a list, and that list looks much the same whether the query was right or wrong. Too broad and the message you wanted is somewhere under ordinary notification mail. Too narrow and it returns the message you started with, where a table with one row can read as "nobody else got this".
A detection rule is harder still, because it judges mail that has not arrived yet. Too broad and people start seeing ordinary mail flagged. Too narrow and nothing happens at all, which looks exactly like a quiet week, until the next one gets through.
So the agent does not get to hand us a result and have us take its word for it. Three things have to hold before a hunt result reaches a case:
- The query has to match the phish it was written from. Before it searches anything else, Socky runs the query against the one message already known to be malicious. If it cannot match that, it is broken, and nothing it returns afterwards means anything.
- It cannot narrow its way to a clean answer. A narrower query returns a shorter list, and a shorter list is easier to read and more likely to be missing somebody. So what the query may drop is fixed in advance: our own domains, and senders Sublime already trusts. It may not drop a different sender, a different recipient, or a subject with
Re:in front of it. - The report says what Sublime searched, not what was asked for. Retention can be shorter than the window in the request, and the job returns the shorter window without flagging it. Reading the window back from the finished job keeps a period nobody searched out of the case.
From message to detection
The rest of this post follows one flagged message through five steps, from the investigation that opens its case to a detection rule somebody has approved.
1. Investigate the message group
Socky runs the same investigation on every flagged email and each one generates a Tracecat case and slack alert message.
- Read what the platform parsed. Socky uses the Message Data Model, not the raw file: the sender and the display name, the authentication results, the links and where they point against what they display, the headers, the recipients, and whether each message was read, replied to or forwarded. It resolves the mailbox to a person through the directory, and checks the business context for a benign pattern that would explain the mail.
- Enrich the few indicators worth enriching. Domain registration age, the reputation of the sending IP, and the link and sender domains. Registration age is the signal that carries a lure sent from a domain the attacker has just bought.
- Reach a verdict, and say what it rests on. The case records the verdict, the evidence under it, and a named list of what could not be checked. The case writes "no link click is recorded", so a reader does not take that for "nobody clicked the link".
- Scope who else got this message. Socky searches the sender address, the sender domain, the link domain and the attachment hash.
- Post the slack message. The verdict, the deviation that earned it, the recipients, the case link, and the buttons that carry a decision back into the workflow.
The case is written and the card is posted. Step 4 looked past this one message only as far as the groups Sublime had already flagged.
2. Hunt the technique across every mailbox
Socky keeps going from there. If the same lure arrived from a different domain, under a different subject, and no rule fired on it, nothing in the run so far would have surfaced it. Whether any such mail exists is the question the hunt answers.
It answers it by running MQL over every message in the retention window, flagged or not, and by matching the shape of the lure rather than the indicators this message carried. The scope count is a floor. The hunt is whatever sits past that floor.
The sender address, the sender domain, the link domain and the attachment hash are cheap for an attacker to change between sends. The method behind them usually is not. The workflow runs both. Scoping keeps the copies of one delivery. Hunting adds the deliveries that share the method, including the ones no rule fired on.
| Step | Indicator scoping | Technique hunt |
|---|---|---|
| Searches | Flagged message groups | Every message in the retention window |
| Matched on | Sender address, sender domain, link domain, attachment hash | The shape of the lure |
| Answers | Who else got this message | Who else got this attack |
| Costs | A filtered read | One batch job |
Writing the query. Socky takes the attacker's method from the case, and deliberately not the indicators the case has already searched. The method is whatever the attack needs in order to work: a reply-to that points somewhere other than the sender, a link whose visible text and destination disagree, a subject that borrows an internal name, an attachment that does not match what the sender normally sends. The query names that, and leaves out everything the attacker can change for free.
Two habits keep the query durable. It refers to the lists Sublime maintains for the organisation, your own domains and the senders it already trusts, instead of writing names and allowlists into the query text, so it keeps working as staff and suppliers change. And it matches on shape rather than exact strings, so a prefix, a truncation or a reordering does not slip past it.
A hunt that has not finished looks like a hunt that found nothing. The job runs in the background and takes a few minutes, so the agent has to check back to find out whether it is done. On its own it will fire those checks off in seconds, be told every time that the job is still running, run out of attempts, and report an empty hunt.
So the step makes it wait between checks: ninety seconds, then one check, up to four times. Six minutes at most, and whatever comes back has actually finished.
The hunt returns two kinds of row that matter. The first is mail the platform never flagged. It matches the method, no rule fired on it, and it is still sitting in a mailbox, so nobody knows it is there. That is the gap the hunt exists to find.
The second is mail the platform did flag, where nobody ever opened the alert. The detection worked and the response did not. Socky lists the two separately, because what they need is different: the first needs a rule written for it, the second needs somebody to work the queue.
A big total is not a reason to narrow the query. Plenty of ordinary mail shares a surface with an attack, so most of what comes back is legitimate. The number worth reading is how many of those rows nobody flagged.
The report quotes the window Sublime actually searched, taken from the job's response rather than the request. Retention can be shorter than what was asked for, and days nobody searched should not be reported as clear.
The step posts that result into the card's thread and writes the same table onto the case, so the finding lands where the decision gets made.
3. Read what the hunt returned
Each returned group carries more than the fact that it matched. These fields are the platform's record of what happened in each mailbox, not the agent's reading of it, and they are what the containment tasks get built from.
| Field | What it tells you |
|---|---|
read_at | The recipient opened the message, and when |
replied_at | The recipient replied, which changes the case from attempted to engaged |
forwarded_at | The message was forwarded, so the lure spread inside the company |
forward_recipients | Which mailboxes it was forwarded to, so they can get their own removal task |
flagged_rules: null | The platform took no action on this message |
4. Author a detection, behind a human gate
The hunt has just listed mail that reached a mailbox with no rule firing on it. That is a detection gap: the method works, and nothing currently running describes it closely enough to catch it.
A rule closes that gap. Writing it for the method rather than for this one message is what keeps it closed after the sender and the subject change.
Detectee writes that rule. It does not add it. Creating a rule and turning it on change what happens to everybody's mail, so both wait for a human.
Pressing Generate detection acknowledges in the thread and starts the work in the background. Detectee then works through a fixed sequence:
- Read what made the message malicious. From the case and the parsed model, not from the case title.
- Check the existing rules. It compares MQL, not titles. Two rules with different names can match the same mail, and a rule named for one campaign often already covers the next one.
- Author a candidate rule. One technique per rule, with the MQL written to be read by a person.
- Validate the syntax. Through Sublime's rule validation, before anything is saved.
- Prove it matches the source message. A rule that does not match the message it was written for is wrong, whatever its syntax says.
- Prove it stays quiet on something legitimate. The candidate is run against ordinary mail that shares a surface feature with the phish, such as a calendar invitation carrying a colleague's name, and the proposal reports whether it stayed quiet.
- Measure the volume where it can. A hunt job runs the candidate MQL over the retention window to show what it would have caught.
That last step is a backtest: the candidate rule is run over mail that has already arrived, to show how often it would have fired. A rule that would have fired on hundreds of ordinary messages is not shippable, however good its intent, and the backtest is the only way to find that out before it goes live.
When the backtest matches nothing, the proposal says the volume is unknown rather than calling the rule clean. An empty result has two explanations that look identical from the outside: the technique really is rare, or the rule does not work. The same applies when the hunt could not run at all. The proposal still arrives, and it says which of its checks it was unable to complete.
The proposal is written to the case and posted to Slack with the full MQL, the tests, the hunt result and the window it actually covered, behind Approve and Discard buttons.
The button press is the authorisation. Access to the channel is the control here: whoever can see the card can approve it, and the case records who pressed.
If that is too open, the press already carries the Slack user ID of whoever made it, so restricting approval is a condition on the action that creates the rule. Keep the allowed IDs wherever suits you, a Tracecat table you can edit without touching the workflow, or a trigger input, and compare the two before the create call runs. Our Google OAuth workflow does this for its revoke button, and replies privately to anyone else who presses. Either way it is a change to the workflow and not to the agent. Who may approve is set by the automation, and Detectee has no say in it.
Nothing reaches Sublime until somebody presses Approve. On approval, the workflow reads the proposed rule back from the stored proposal and creates it from that, so the rule that exists is exactly the MQL the approver read. It is created first and enabled second, as two calls, so a failure between them leaves a rule that is off rather than a rule that is on and unreviewed.
Discard records who discarded it and that nothing was created.
Neither of those two calls is available to the agent. An agent that can write a rule and also switch it on can change what happens to everybody's mail without anyone having read the rule first. Keeping both calls in the workflow means the only route to a live rule runs through a person who has seen the MQL.
5. What ends up on the case
The case opens with the verdict in a sentence, and a row of links out to the places a reader will want to check for themselves: the message in Sublime, Sublime's remediation queue, and the mail log search in the Google Workspace admin console. Somebody skimming gets the answer and a way to verify it without reading any further.
Below that, the layout is fixed:
- Evidence tables. The rules that flagged the message, the authentication results, the sender domain's registration, the links and attachments, and what could not be checked.
- Scoping. What the indicator search returned, labelled as a search of the flagged set.
- The hunt table. Every matched group, with the mailbox, the sender, the interaction fields and whether the platform flagged it.
- Case tasks. One task per mailbox for removal, one to confirm it is gone, and one for the detection rule if somebody asked for one.
Tasks are how the case carries the containment work a person still has to do. Sublime Core cannot remove mail, so every removal is a human action, and each one gets its own task with its own open or closed state.
Make it your own
Every part of this is a separate action, so the pieces that depend on a particular vendor can be swapped without touching the logic around them. The mail platform is one action and a skill. The place approvals land is another, whether that is a chat tool or your service desk. The list the query checks employee names against can come from the mail platform, your identity provider, or a table you maintain yourself.
Two parts carry over even if you replace Sublime. The first is the separation between scoping by indicator and hunting by technique, which applies to any platform that lets you query mail it did not flag. The second is the shape of the detection gate: an agent that authors and proves, a human who approves, and a workflow that creates the rule from stored state rather than from the agent's last message.
Beyond that, five directions we did not build and would:
Containment, where your tier allows it. Removal is a manual step here only because Core cannot quarantine. On a tier that can, or through the mail platform's admin console, the removal tasks become actions, and the detection gate is the pattern to copy: the agent proposes, a person approves, and the workflow carries out the action.
How that gets triggered is open. It could be a button on the card, the way Approve already works. It could be a case task, so marking the removal task started is what kicks it off. Either one can run as another lane in this same workflow, or as a detached call like the detection lane, which is just a lane the workflow starts and does not wait for.
The part to keep whichever way you build it is where the list comes from. The mailboxes to act on are derived by the workflow from the hunt result, not read back out of the agent's summary, and shown to a person before anything is deleted. A wrong hunt query is a noisy case today. Give an agent the ability to delete mail and the same mistake removes somebody's messages.
Feed the approved rule into detection as code. If you already keep Sublime rules in a repository, the approval can go there instead of straight to the platform. The workflow would open a pull request carrying the same MQL, the tests it passed and the backtest volume, and your existing review and deployment path takes it from there. The proposal already holds everything a rule file needs, and the case keeps the evidence for why the rule exists.
User outreach. The case already knows which mailboxes received the message and which recipients read, replied to or forwarded it. That is enough to send each one a short, specific note, rather than a company-wide warning nobody reads. A reply from the recipient is also evidence: "I did reply with my number" changes the case from attempted to engaged, and hearing that from them is quicker than working it out from the record.
Raise an incident, not just a case. For a confirmed campaign, open a PagerDuty or incident.io incident from the verdict lane and keep the case as its evidence record. The two systems want different things: the incident wants a responder, a severity and a timeline; the case wants the hunt table and the authentication results. Let each hold what it is good at, and link them.
More enrichment at the sender and link layer. The automation checks registration age on the sender and link domains, and the reputation of the sending IP. There is more available at that layer: a threat intel platform, a passive DNS source, or any feed that tracks infrastructure reuse.
The hunt is where we would expect that to add most. A hunt can return unflagged mail from several senders with nothing obvious in common, and whether those senders share hosting is a question none of the checks here can answer.
Not all enrichment is silent, and it is worth knowing which kind you are adding. Some services answer out of data they already collected: you ask about a domain, they check their own records, and nothing of yours reaches the attacker. Others go and fetch the thing you asked about. Submitting a domain or a URL to one of those tells whoever owns it that somebody is looking, and on most of them the submission stays visible to anyone else who searches that service later. Against infrastructure still in use, that can be enough for them to move it, burn the link, or go quiet before the scoping is finished. It is the reason link scanning sits behind its own button here rather than running on every case, the card says the submission is public before anyone presses, and only registered domains go out rather than full URLs carrying a token.
What next?
What we learned
- Every flagged or reported message gets a case. The poll runs on a schedule and opens the case within minutes of delivery, rather than when somebody reaches the queue.
- Facts belong in the workflow, judgement in the agent. The queries, the deduplication, the fan-out, the verdict write and the read-back are typed actions with fixed arguments. The agents read the evidence and decide; they do not carry the plumbing.
- The flagged set is a floor, not a total. Scoping by indicator finds the copies of one delivery. Hunting by technique finds the mail that shares its method, including the mail no rule fired on.
- A proposal is a decision, not a deployment. Detectee drafts and tests. The workflow creates the rule from stored state, after a named person presses Approve, and records who that was.
- The record keeps itself up to date. The case, the removal tasks and the Slack thread are written as the run goes, so the evidence, the decision and the person who made it stay in one place.
What we did not cover
This automation takes no containment action at all. It decides, records and proposes, and then stops.
- The mail is not removed. Core cannot quarantine, so every copy stays where it is until somebody removes it in the admin console. The case carries that as a task per mailbox.
- The sender is not blocked. Nothing here adds a domain or an address to a block list.
- No account is touched. No session is revoked and no password is reset, even where somebody replied.
- The recipients are not warned. Nobody is told their mailbox received it unless a person tells them.
- Attachments are not detonated. Analysis stops at what the platform already parsed. There is no sandbox, and nothing is opened or run to see what it does.
The workflow, the presets and the skills are on GitHub and in the Tracecat docs.
We would like to hear how you scope a phishing campaign today, and whether you check the mail nobody flagged.