Where session 7 sits on the ladder
Six sessions in, Ironwood's bench reads three paths to the customers table, none of them undetected, and twelve of twelve tables recoverable. The two detections from session 5 did the undetected work: query logging and egress alerting take that number from three to zero while the path count stays at three. What they do not do is watch the edges that lead up to a read. A service key used from a new address, a role granted on a Friday, a DROP against the backup schema: none of those is a SELECT on customers, so none of them appears in the session 5 review table. Tonight fills those gaps with three more detections and puts all five in one place.
★ What you walk out with tonight. A method that turns any tactic on the ATT&CK list into a query over the logs you already have, in five steps. Five detections over login history, grant history and query history, each with a threshold tuned so it fires less than once a week without a real cause. One review table all five write to, with a tactic, an object, an actor, the evidence and a named reviewer on every row. And an inventory file where every remaining edge names the log that sees it.
LiveWhy the finished design is only defensible because these queries exist3 min▶
The finished Ironwood design has one open path: the support agent's own legitimate session reading customer records. No control removes it without stopping the work. The bench accepts that path as the honest remainder for one reason only: it crosses a logged edge, and somebody reads the log. Turn detection off and keep every prevention control on, and the bench reads one path and one undetected. Same door, same room, and now it is dark.
So the queries written tonight are not a monitoring nicety layered on top of a secure estate. They are the thing that makes it acceptable to leave the last door open. A leader who signs off the design is signing off the assumption that these detections run, that their thresholds are sane, and that a named human reads the table every day. The engineering that follows is about making that assumption true.
Ironwood's Friday admin grant was found by the session 5 unusual-read detection, six weeks after it was made, the first time the analyst used the role against customers. Grant history had recorded the grant itself within a second of it happening. Nobody had written the query that reads grant history, so the earliest possible signal sat unread for six weeks while the later signal did the work. Detection engineering is the practice of writing the earlier query.
Self-studyThe vocabulary for tonight3 min▶
- Tactic - ATT&CK's word for the attacker's goal at one stage. Version 19.2 lists fifteen for the enterprise matrix; tonight uses five of them: Credential Access, Privilege Escalation, Collection, Exfiltration and Impact.
- Hypothesis - a sentence of the form "if an attacker were doing this tactic here, the log would show that". Every detection starts as one, and a detection that cannot be stated as one is a query looking for a purpose.
- Login history - the warehouse's record of every authentication: who, when, from which address and client, whether it succeeded. Distinct from query history, which starts after the login.
- Grant history - the record of every role granted or revoked, to whom, by whom, when. Most platforms expose it as a system view or an account-usage table; some only as an audit log export.
- Change window - the agreed hours in which privilege changes are expected, with a ticket. A grant outside one is not wrong; it is a row.
- Precision - of the rows a detection writes, the share that had a real cause. The discipline is to raise it by tuning against the detection's own history, never by making the query blind.
- Review table - the one table all detections write to. Its reviewer and outcome columns are the control; everything upstream is plumbing.
A detection is a hypothesis about a tactic, written as a query
9 min liveThe mistake most detection programmes make is to start from the log and ask what could be queried. Start from the attacker instead. Pick a tactic, say what it would look like in your own logs, then write the query. The tactic list is finite and public; the logs are yours; the hypotheses in between are the engineering.
LiveThe method, in five steps3 min▶
- Pick a tactic. Use the ATT&CK v19.2 names, because they are the vocabulary the rest of the industry uses when it describes an incident, and because the list is finite. Fifteen tactics; a warehouse estate needs detections against roughly a third of them.
- State the hypothesis in your own logs' terms. "If someone were doing Credential Access with Ironwood's ETL key, login history would show that service account authenticating from a client or address it has never used." If you cannot finish that sentence for a tactic, you either lack the log or the tactic does not apply, and both are findings worth writing down.
- Write the query. The hypothesis becomes a WHERE clause and usually a join to a baseline or an allowlist. Keep it read-only over the log until the final step.
- Decide the threshold, then tune it against history. Run the query over the last 90 days before scheduling it. Count the rows. Each one either had a real cause or is a reason to sharpen the query.
- Name the reviewer. A person, not a channel. A detection with no named reader is a detection that will be read by the incident responder, afterwards.
LiveThe five detections that finish Ironwood's graph3 min▶
| Tactic (v19.2) | Hypothesis in Ironwood's logs | Log | Detection |
|---|---|---|---|
| Credential Access | A service account authenticates from a client application or address it has not used in 30 days. The leaked ETL key, used from anywhere but the runner. | login_history | new_client_login |
| Privilege Escalation | A role is granted to a human account outside an agreed change window, with no ticket. The Friday admin grant, the day it happens. | grant history | out_of_window_grant |
| Collection | A role reads customers when it has not in 30 days, above ten times its median rows, or at an hour it never works. Session 5, unchanged. | query_history | unusual_read |
| Exfiltration | A COPY or unload of customers names a destination that is not on the owned allowlist. Session 5, unchanged. | query_history | unlisted_copy |
| Impact | A DROP, TRUNCATE or purge statement names customers or anything in the backup schema. The hour before the ransom note. | query_history | drop_or_purge |
Two of the five already exist from session 5. The point of writing the table anyway is the first column: each detection now says which tactic it is a hypothesis about, which is the sentence an incident report needs and the sentence a new engineer needs to know whether the set is complete.
Self-studyWhy these five and not the other ten4 min▶
ATT&CK v19.2 lists fifteen enterprise tactics. Five have detections tonight because five correspond to edges on Ironwood's graph that lead to or through the crown jewels. The rest are not ignored; they are either closed by prevention, outside the warehouse, or someone else's log.
- Initial Access at Ironwood is phishing and a public repo. MFA and key rotation closed those edges in session 2; the residual signal is the new-client login, which is why Credential Access is watched rather than Initial Access.
- Execution and Persistence live on the CI runner and the laptops, whose logs belong to the platform and endpoint teams. The warehouse sees their consequence, which is a login or a query, and that is where tonight's detections sit.
- Stealth and Defense Impairment, the two tactics v19 split out of the old Defense Evasion, would appear in a warehouse as somebody disabling the logging or the scheduled job itself. The right control is that the review table notices when it stops receiving rows; the homework asks for that check.
- Discovery and Lateral Movement show up in query history as SHOW and DESCRIBE statements from a role that never issues them, and as a login hopping between roles. Both are reasonable sixth and seventh detections once the first five are quiet.
- Reconnaissance, Resource Development and Command and Control happen outside anything the warehouse records. Naming them here is what stops a future engineer from believing the five are the whole picture.
Precision, review, and the detection nobody reads
8 min liveFive queries that run are not five detections. A detection exists when it fires seldom enough to be believed, writes to a place one person reads every day, and is honest about what its evidence can and cannot prove. Most detection programmes fail on the first point, quietly, within a quarter.
LiveThe precision discipline: less than once a week without a real cause3 min▶
Data observability learned the false-page rule the expensive way: a monitor that pages on every late load is muted within a month, and it is muted the month it matters. Security detection inherits the rule with a sharper edge, because the person who stops opening the review table is the person the whole design assumes is there.
- Tune against the detection's own history before it goes live. Run each query over 90 days. Every row it returns is either a real cause you can name or a defect in the query. Fix the defects: a service account whose "new client" is the runner's second address, a change window nobody wrote down, a backup schema whose nightly rotation issues a legitimate DROP.
- The target is less than one row a week without a real cause. Not zero. A detection that never fires is either perfect or blind, and the tuning history is how you tell them apart.
- Tune the join, never the eyes. Add the runner's second address to the service account's expected clients. Add the rotation job's role to an exception list with an owner and a review date. Do not raise a threshold globally to silence one role; that blinds the detection for every other role.
- Record every tuning decision with a reason and an owner. An exception list without owners is the next public bucket.
LiveOne review table, a named reviewer per row, read daily2 min▶
Session 5's table took two detections. Tonight's takes five, and the reason for keeping one table rather than five is the reader. One table, one morning read, one place where "reviewed by" and "outcome" get filled in. Five tables become five habits, and habits are what a detection programme actually runs on.
Each row carries the tactic it is evidence for, the object it concerns, the actor as the log recorded them, the evidence itself, and the name of the person who owns the answer. The reviewer is chosen per detection, not per row: the platform lead owns new-client logins and drops, the access owner owns out-of-window grants, the data owner of customers owns unusual reads and unlisted copies. Their names are in the job, so a row is born with a reader.
The response is a question to the actor's owner, never an accusation. "Your service account authenticated from a new address at 04:10; was that a planned change?" resolves most rows in one reply. The one that does not resolve is the incident, and the row is its first exhibit.
Self-studyThe honest limits: attribution to a role set, and the bucket outside all five4 min▶
Two limits are worth writing next to the detections, because the review table will otherwise be read as saying more than it can.
- Query history attributes a read to a user and the roles active in the session, not to the grant that permitted it. When the analyst read customers with the admin role active, the row says user and role. It does not say whether the read was permitted by the Friday grant, by an older grant to a role the admin role inherits from, or by a direct grant on the table. Answering that needs a join to grant history as of the query's timestamp, and on many platforms the reconstruction is approximate. The review table records what the log recorded; the reviewer's question is what recovers the grant.
- The export bucket path is outside all five. A read of the nightly extract is a request to the object store. It is not a login to the warehouse, not a grant, not a SELECT, not a COPY, not a DROP. Session 3 closed that door with a private bucket; the only log that would ever see it being opened again is the object store's own access log, and it needs its own detection with its own reviewer, or an honest blank in the inventory's "seen by" column.
Write both limits into the runbook next to the table. The incident that tests them will not be the moment to discover them.
Self-studyDetection as evidence: the timeline only these tables hold3 min▶
When an incident is confirmed, the first document anyone asks for is the timeline: when did the credential first authenticate from the new address, when was the role granted, when was the first unusual read, when did the copy leave, when was the backup schema touched. Every one of those timestamps lives in a row one of tonight's detections wrote, or in the underlying history the detection reads.
- Retain the review table for longer than the log. Platform query history is often retained for weeks or a year; the review table is yours and can be kept for as long as the postmortem process needs it.
- Keep the query text and query identifier on the row. The evidence column that says "above 10x typical" is a summary; the identifier is what lets the responder pull the exact statement later.
- The outcome column is part of the record. "Expected, confirmed by owner on the day" is what turns a row from noise into a documented decision, and a stack of those is what lets a postmortem say the detections were working before the incident, not just after.
What a confirmed exfiltration obliges the company to tell regulators and customers is the governance course; what the first hour looks like and what the report funds is leader session 6. Tonight's contribution to both is that the timeline exists.
Five detections, one review table
★ 20 min · everyone buildsEverything runs against your own warehouse's history views, read-only until the last two steps, which create tables and a scheduled job in a schema you own. The views are written here as login_history, role_grant_history and query_history with generic column names; every platform ships these under its own names, with its own columns for client address, application and statement type, so substitute yours. The session 5 objects sec.egress_allowlist and the unusual-read query are reused as they were written.
Write the new-client login detection for service accounts (Credential Access). Start by listing your service accounts in a table with an owner, because "service account" is a business fact the login history does not know. Then flag any successful login by one of them from a client application and address pair it has not used in the last 30 days. The left join against the baseline does the work; a pair with no baseline row is new.
Write the out-of-window role grant detection (Privilege Escalation). Change windows are a table too, with a ticket per window. A grant to a human account, meaning any account not in the service account list, that falls in no window is a row. The Friday afternoon grant at Ironwood matches on the day it is made rather than six weeks later.
Reuse the session 5 unusual-read detection (Collection). Nothing changes in the query: a role new to customers in 30 days, above ten times its median rows per query, or at an hour it has never read the table in. What changes is the row it writes, which now carries the tactic name and a reviewer. Keep the query in one place and reference it from the job rather than pasting a second copy that will drift.
Write the unlisted-external-location COPY detection against the allowlist (Exfiltration). Also from session 5, and also reused; the allowlist table is the one that already has owners and review dates on every row. The two additions worth making tonight are to match on the destination the platform recorded where it records one separately from the statement text, and to include shares alongside COPY and unload, because a share is a copy that never shows up as bytes leaving.
Write the DROP-or-purge detection against customers and its backups (Impact). Statement type is more reliable than text where your platform records it. Name the crown-jewel table and the whole backup schema, because an attacker who drops the schema does not name the tables. Expect the session 6 rotation job to appear on the first run; it goes into an exception list with an owner, not into a wider blind spot.
The statement-type names in the IN list are placeholders for whatever your platform records; keep the text matches as the fallback so a rename in the platform's own vocabulary does not silently blind the detection.
Union all five into one scheduled job writing to one review table. The table has a tactic, an object, an actor, the evidence and a reviewer on every row, plus the two columns a human fills in. The reviewer is set per detection in the job. The scheduler object is a task, a job or a DAG depending on your platform; hourly is fine, the morning read is what matters.
Update the inventory file so every remaining edge has a log. Name the log and the detection on each row. The bench counts a path as seen when it crosses a logged edge; the file is where you prove which query does the seeing, and the bucket row is where you record the honest blank if the object store's log is not wired yet.
Run all five over 90 days before you schedule anything. The first pass tells you which service account has a second legitimate address, which change window nobody wrote down, and which rotation job issues a nightly DROP. Fix each with a row in the right exception table, owned and dated, and only then hand the review table to its readers. A detection handed over while it is loud will be muted, and muted is worse than never written, because the inventory file now says the edge is seen.
Before session 8
30 min- Run steps 1, 2 and 5 over the last 90 days of your own history and count the rows per detection. Anything above roughly a dozen per detection is tuning work, not a finding. Record each tuning decision with an owner and a reason.
- Fill the service account table and the change window table with what actually exists. Ask each service account's owner to confirm the client and address pairs the baseline found. At least one pair will be news to them.
- Write a sixth detection with no query: a check that the review table received at least one row from the job in the last 24 hours, and a row when it did not. A job that stops silently is Stealth or Defense Impairment in v19.2 terms, and it is the detection most often missing.
- Take the one remaining path on your own graph, the one no control can close, and write down the detection, the reviewer and the retention of the review table that make leaving it open defensible. Bring that sentence to session 8; it is what the bench scores.
Three questions
The review table shows the analyst read customers with the admin role active. What does query history establish about which grant permitted the read?
The log attributes a read to a user and their active roles, not to a specific grant. The admin role may hold the permission through an inherited role or a direct table grant. Reconstructing which one needs grant history at the query's timestamp, and the reviewer's question to the owner is what usually settles it.
Every prevention control on Ironwood's bench is on and every detection is off. What does the bench read, and why does it matter?
All prevention with no detection leaves one path and one undetected. The finished design leaves the same one path with zero undetected. The path is identical; the difference is whether anyone would know it was used, and that difference is the five detections.
The drop-or-purge detection fires every night at 02:00 on the session 6 backup rotation job. What is the right fix?
Tune the join, never the eyes. Narrowing the detection to exclude the backup schema or the hour blinds it to exactly the attack it exists for; an exception on the one role, with an owner and a review date, keeps it precise and keeps the decision recorded.