[D 7.3.1] Build Sheet: From Saved Query to Custom Detection

A saved query is a note to yourself. This takes one through the whole lifecycle to a governed detector with mapped entities, a defensible schedule, a name that survives you, and a benign end-to-end test you can actually run in a production tenant.


By the end of this you have a working custom detection built from a query, with entities mapped so the alert means something, a schedule chosen for a reason you can state, a name that will still be legible in two years, and an end-to-end test you can run on a production tenant without pretending to be an attacker. You also have the programmatic equivalent in the current interface shape rather than the one that stops working in October, and an honest list of the three things that shape does not yet document.


Prerequisites

The permission you want is Detection tuning (manage) in the unified model, which exists precisely so that detection authorship can be delegated without granting the whole settings permission. It covers custom detections, alert tuning and threat indicators together. Note that several current Microsoft pages, including the custom detections article itself, still name the older Security settings (manage) permission, which was renamed to Core security settings; that permission also works and is broader than you need. In Entra ID, Security Administrator works. Security Operator works only where Defender for Endpoint role based access control is switched off, and otherwise additionally needs the endpoint manage security settings permission. If you built roles in the activation build sheet, this is the permission to add to the detection author role rather than to the analyst roles.

There are further gates on the workload side. You need permissions for every workload whose tables your query touches, which is stricter than it sounds: the identity logon table carries authentication data from both the cloud apps product and the identity product, so managing a detection over it requires manage rights on both. And you need permissions on every device group in the rule’s scope, so a rule scoped to all devices can only be created and edited by someone who holds all of them. On licensing, the rule surface needs a Defender for Endpoint Plan 2 entitlement or an equivalent qualifying licence in the suite, and tables from workloads you have not licensed will validate and return nothing, so a rule can be created against a table your tenant will never populate. For the programmatic path in step 8 you additionally need admin consent for the custom detection scopes and a tenant in the global commercial cloud. For the end-to-end test you need one onboarded test device you can run a command on.

Step 1: Write the query, and obey the column rules

The argument for why any of this is worth owning is in the schema article; this starts one step later. Author and iterate in advanced hunting until the result set is what you want, then create the rule from the query editor with Create detection rule. Building it there rather than from the rules page means you have seen the results before the rule exists, which is the difference between a detection and a hypothesis.

Four column rules govern whether the resulting alert is useful. Microsoft words them as recommendations rather than requirements, which is technically accurate and practically misleading, because breaking them produces alerts that are missing their timeline, their scope or their entities.

SettingValueWhy
Timestamp columnTimestamp, or TimeGenerated for Sentinel dataSets the timestamp on the generated alert. Without it the alert’s first and last event times are derived from the lookback window instead of from the event, which makes a timeline useless.
Endpoint tablesProject DeviceId or DeviceNameTags the alert with the correct device group scope and lets the process tree render. A rule without it produces alerts your scoped analysts may not see.
All other Defender tablesProject Timestamp and ReportId from the same eventThese two together identify the original event. Taking them from different events, which an unwary summarize will do, breaks the enrichment silently.
Strong identifierAt least one of the documented entity columnsWithout one, the wizard cannot map an impacted asset automatically and the alert arrives attached to nothing.
Time filteringDo not filter on TimestampThe service prefilters by the lookback period and evaluates ingestion_time() to allow for ingestion delay. A time filter in the query fights that and narrows your results without saying so.

The documented strong identifier columns are worth having to hand rather than guessing at. For a device: DeviceId, DeviceName or RemoteDeviceName. For an account: AccountObjectId, AccountSid, AccountUpn, InitiatingProcessAccountSid or InitiatingProcessAccountUpn. For a mailbox: RecipientEmailAddress, SenderFromAddress, SenderMailFromAddress, SenderObjectId or RecipientObjectId.

One inconsistency to be aware of rather than to resolve. The documentation states in one section that evaluation uses ingestion time rather than event timestamps, and in another that rule frequency is based on the event timestamp and not the ingestion time. Both sentences are on the same current page. Treat scheduling as approximate, which Microsoft also says outright, and do not build a detection whose correctness depends on exact run timing.

The worked example for this sheet is a canary. It is inert, it fires only when somebody deliberately types a marker string, it satisfies every column rule above, and it is eligible for continuous evaluation because it queries a single supported table with no joins, no unions and no comments. That combination is what makes it safe to run in production, which is the whole point of using it to prove the pipeline.

DeviceProcessEvents
| where FileName in~ ("powershell.exe", "pwsh.exe", "cmd.exe")
| where ProcessCommandLine has "CD-CANARY-9F2A"
| project Timestamp, ReportId, DeviceId, DeviceName, FileName,
          ProcessCommandLine, AccountName, AccountSid,
          InitiatingProcessAccountSid, InitiatingProcessAccountUpn

Expected result before you create anything: zero rows, because nobody has run the marker yet. That empty result is the correct starting state and it is also the failure mode to watch for later, since a detection that never fires and a detection that is broken look identical from the outside. The marker string is deliberately high entropy so that it cannot collide with real command lines, and it should be different in your tenant from the one printed here.

Step 2: Choose the frequency deliberately

Frequency and lookback are paired, and for Defender data the lookback is fixed by the frequency rather than chosen. Choose by how quickly the behaviour needs to be caught, not by how important it feels.

SettingValueWhy
FrequencyEvery 24 hoursLookback 30 days. Correct for slow, low-signal patterns such as a dormant account waking, where a day of latency costs nothing.
FrequencyEvery 12 hoursLookback 48 hours.
FrequencyEvery 3 hoursLookback 12 hours.
FrequencyEvery hourLookback 4 hours. The default I reach for when a rule takes no response action.
FrequencyContinuous (NRT)The right answer for anything with a response action attached, because the gap between detection and containment is the whole value. Constrained, see below.
FrequencyCustom5 minutes to 14 days, available only where the rule uses Microsoft Sentinel data exclusively. Selecting it means Defender fetches from the workspace, and scoping stops applying because Sentinel does not support it.

Continuous evaluation has five hard constraints and one soft one. The table must be on Microsoft’s published list of tables that support it, which is roughly twenty Defender tables and a separate Sentinel set, and a query over a table outside that list cannot run continuously however well it obeys everything else. Then the query must reference exactly one table, must not use joins, unions or the external data operator, must contain no comments, and must use operators from the Azure Monitor supported subset, where a regular expression has to be written as an escaped string literal. The soft constraint is that only generally available columns are supported, which quietly excludes anything you were using from a preview table. There is a bulk migration control on the rules page which finds every existing rule whose query already qualifies and switches it, and running that once after building a library is worth the two minutes.

The lookback and frequency interact in a way that is useful rather than annoying. Where the lookback is longer than the frequency, the same event is seen on consecutive runs, and the service deduplicates on the combination of entities and details rather than raising the same alert repeatedly. That is why an hourly rule with a four hour lookback is not four times as noisy as it sounds, and it is also why changing the entity mapping on a live rule can suddenly produce duplicates of things you thought were resolved.

Step 3: Fill in the alert details, and know which two fields are undocumented

The alert details page is where a detection becomes something a human can act on, and it is routinely rushed. The fields, in order, are detection name, frequency, lookback where the rule is Sentinel-only, alert title, severity, category, then three separate MITRE fields for tactic, techniques and sub-techniques, then an optional link to a threat analytics report, a description, and recommended actions.

SettingValueWhy
FrequencyEvery hourFour hour lookback, which is ample for a marker somebody typed minutes ago, and it matches the programmatic form in step 8 so the portal build and the code build agree.
Detection nameCD-EP-Info-Canary marker process executionFollows the convention in step 5. This is the name in the rules list, not the alert title, and the two are separate fields for a reason.
Alert titleCanary marker process executed on {{DeviceName}}Plain text only. Strings are sanitised, so markup does not render and any URL must be percent-encoded. The substitution is what makes a queue of these alerts readable at a glance.
SeverityInformationalFor the canary, the point is to prove the pipeline rather than to interrupt anyone. Reserve high for detections that genuinely warrant waking somebody.
CategoryChoose in the tenantMicrosoft documents no list of the available values anywhere. Pick from what the dropdown offers and record your choice in the rule register, because it cannot be looked up.
Tactic, Techniques, Sub-techniquesThe closest honest mapping, or noneThree separate fields now, where older guidance describes one picker. A wrong technique is worse than an empty one, because it feeds reporting that somebody will present.
Recommended actionsWhat the responder should do firstThe field most often left blank and the one that decides whether a three in the morning responder acts correctly. Write the first triage step, not a policy statement.

The documentation is thin in two places here and neither is worth working around. The four severity values are not enumerated on the custom detections page at all; they are documented in the incident interface schema as informational, low, medium and high, which is a defensible cross-reference and not a statement about this dropdown. And the category values are documented nowhere on Learn, unchanged for as long as I have been checking. Both are cosmetic in effect and both mean your rule register has to record what you chose.

Step 4: Enrich and map entities

Dynamic substitution into the title and description uses the column name in double curly brackets, with a maximum of three columns per field. Custom details are key and column pairs that surface as named fields on the alert, capped at twenty pairs and four kilobytes combined, and the overflow behaviour is worth knowing because it is unforgiving: exceeding the size limit drops the entire custom details array from the alert rather than truncating it.

Entity mapping splits into impacted assets and related evidence, and the split is not cosmetic. Impacted assets are account, device, mailbox, cloud application, and Azure, Amazon and Google resources. Related evidence covers process, file, registry value, IP, OAuth application, DNS, security group, URL, mail cluster and mail message. Only assets can be mapped as impacted entities, which Microsoft states explicitly, so a detection whose subject is genuinely a file still has to name a device or an account as its impacted asset if you want it to scope and correlate properly.

SettingValueWhy
Impacted asset, deviceDeviceIdOne column per entity type, and it must be a column your query actually returns. Defender data maps automatically; Sentinel data has to be mapped by hand.
Impacted asset, accountAccountSidChosen over the account name because the identifier is what response actions later require. Mapping the friendly name instead is the mistake that surfaces only when you try to add an action.
Custom detailsCommandLine mapped to ProcessCommandLinePuts the triggering evidence on the face of the alert so a responder does not have to run the query to know what happened.
Sentinel-scoped tenantsProject SentinelScope_CFRequired if you have configured scoping. Omit it and the resulting alerts are unscoped and invisible to scoped analysts, which looks exactly like a detection that never fires.

Step 5: Actions, with restraint, and one dated warning

A rule using Defender data can act automatically on devices, files, users and mail. The device actions are isolate, collect an investigation package, run an antivirus scan, initiate an investigation and restrict app execution, all keyed on the device identifier column. Files can be quarantined from any of the four hash columns; allowing and blocking is a different grant, needing remediate permissions for files and a file identifier such as a hash, and it applies to a device group rather than to named devices. Mail can be moved to junk, inbox or deleted items, or soft or hard deleted, which requires the message identifier and recipient columns to be present.

The user actions have the strictest column requirements and the most consequence. Marking a user as compromised sets their risk to high in Entra ID and takes effect through whatever identity protection policies you have, and it needs an object identifier column. Disabling a user and resetting authentication both require a security identifier column, specifically one of the account, initiating process account, request account or on-premises identifier columns. For Entra identities the account object identifier is needed for all of these actions. Get the column wrong and the action is simply unavailable in the wizard, which is a better failure than the alternative.

SettingValueWhy
Quarantine file, mark user as compromisedDefensible defaultsBoth are reversible, both are proportionate, and both leave the human decision intact. If a rule is going to act at all, start here.
Isolate deviceRequires written justificationCuts a machine off the network on the strength of a query somebody wrote. Acceptable for a narrow, high-confidence detection with a named owner; not acceptable as a default posture.
Disable user, reset authenticationRarely, and never on a broad queryThe blast radius of a false positive is a person unable to work. A query that matches more than it should here produces an outage rather than an alert.
Initiate investigationDo not attach it nowAutomated investigation stops running for Defender for Endpoint on 1 September 2026. The option is still present in the wizard with no deprecation marker, on a page updated in July 2026, and Microsoft has published nothing about what happens to rules that use it. See below.

That last row is the one dated warning in this sheet and it deserves stating plainly. The action is documented, unmarked, and selectable today. The capability it invokes is documented as ceasing to run for endpoint in a matter of weeks. Microsoft has said nothing about whether affected rules keep running with the action dropped, fail validation when next edited, or get migrated, and I have looked hard for a statement rather than inferring one. A rule you ship this week with that action attached has an undefined behaviour shortly afterwards. Attach nothing, or attach a different action, and put a note in the rule register for anything in your existing library that already uses it. The reasoning behind that change is in the response fabric article.

For the canary, attach no actions at all. A rule whose purpose is to prove the pipeline should change nothing when it fires.

Step 6: Name it, scope it, and register it

Naming is the cheapest governance available and the first thing to decay. The convention this sheet uses is CD-<Workload>-<Severity>-<Behaviour>, giving CD-EP-Info-Canary marker process execution, with a matching lower-case hyphenated identifier for the programmatic form, cd-ep-info-canary-marker-process-execution. Two properties make it worth adopting. The workload token lets a reviewer filter the list to the detections a departing team owned without opening any of them, and the severity token makes an inconsistency visible where a rule named informational carries a response action.

Scope is all devices or specific device groups, and it affects only the device-oriented parts of a rule; a detection over mailboxes or accounts is unaffected by it. Remember that you cannot create or edit a rule scoped beyond the device groups you personally hold, which is the authority model from earlier in this block asserting itself at the moment of authorship.

Then register the rule outside the platform: name, owner, purpose in one sentence, the category and MITRE values you picked, whether it takes an action, and a review date. The register exists because two of the fields you just filled in cannot be looked up in documentation, because ownership is the thing that decays first, and because the rules page tells you a rule exists and never why.

On review, the rule runs immediately when created and its first run sweeps the previous thirty days of data. For a canary that means nothing. For a real detection over historical behaviour it means the first run can generate a burst of alerts for things that happened weeks ago, capped at a hundred and fifty alerts per run, and it is worth warning the queue before you press create.

Step 7: Validation, then the end-to-end test

Validate the rule object before you validate the pipeline. Open it from the custom detection rules page and check four things against what you entered: the frequency and lookback pair are what step 2 chose, the scope names a device group your test device is actually in, the entity mapping lists both a device and an account column, and the status is enabled with a next run time in the future. A rule created from the query editor captured the query at the moment you pressed create, so if you carried on editing in the hunting page afterwards, the rule is still running the older one.

Then the test, which is the part that makes the canary worth building. Run the marker on your onboarded test device, force the rule, and follow the alert all the way through to a resolved incident. It exercises the query, the schedule mechanism, entity mapping, alert generation and correlation, and it does so without simulating an attack.

powershell.exe -NoProfile -Command "Write-Output 'CD-CANARY-9F2A'"

Expected result: the command prints the marker and exits, and within a few minutes a matching row carries it in the command line column. Two failure modes. Nothing lands in the table at all, which is usually an unhealthy or un-onboarded sensor rather than a failed command, so confirm the device reports in the inventory before touching the rule. Or the marker appears on the initiating process columns instead, which happens when a management tool launched the shell; run it from an interactive session on the device.

Wait for the event to appear in the hunting results, which is usually a few minutes, then open the rule from the custom detection rules page and select Run. That triggers an immediate evaluation and resets the interval to the next scheduled run, and it is the only supported way to make a rule fire on demand.

// Confirm the alert exists and carries the right entities.
AlertInfo
| where Timestamp > ago(1d)
| where Title has "Canary marker process executed"
| join kind=inner (AlertEvidence | where Timestamp > ago(1d)) on AlertId
| project Timestamp, AlertId, Title, Severity, EntityType, DeviceName, AccountName

Expected result: one alert, informational severity, with a device entity and an account entity attached. Its identifier begins with ea for a platform custom detection or ed for a Defender for Endpoint one, and either is a pass here: a rule querying only device tables is an endpoint rule until it touches an identity or mail table, so this canary can legitimately produce the second form. An incident should have been created around it. Open the alert, confirm the dynamic title resolved to the actual device name rather than printing the placeholder, confirm your custom detail carries the command line, then classify and resolve the incident to close the loop.

Three failure modes and what each means. No rows in the hunting query means the event has not landed, so wait rather than debugging the rule. Rows in hunting but no alert after a manual run usually means a scope mismatch, because the test device sits in a device group the rule does not cover. An alert with no entities attached means the mapping did not take, and the usual cause is that the column you mapped was not in the projected output. If your tenant uses Sentinel scoping and the alert exists but nobody scoped can see it, you did not project the scope column.

Step 8: The programmatic path, in the shape that survives October

State the boundaries first, because three of them will decide whether this is available to you at all. The interface is Microsoft Graph beta only, with no version one surface for these resources. It is restricted to the global commercial cloud, so government and sovereign tenants have no programmatic path and portal authoring is the only option. And the resource shape changed: the old property set is deprecated with a hard removal on 1 October 2026, roughly nine weeks after this publishes, so anything written now must use the new shape and anything you already have must be migrated.

The substitutions are worth listing because the old names are what every existing example uses. The enabled flag becomes a status with values for enabled, disabled and automatically disabled. The schedule period becomes a frequency expressed as a duration. The category and technique fields collapse into a nested tactics structure. Impacted assets become entity mappings, in the same place on the alert template rather than moving. Response actions become automated actions, keyed by action name. And the identifier is now supplied by the client and required, which is what makes round-tripping a rule between source control and a tenant possible at all.

POST https://graph.microsoft.com/beta/security/rules/detectionRules
Content-Type: application/json

{
  "@odata.type": "#microsoft.graph.security.detectionRule",
  "id": "cd-ep-info-canary-marker-process-execution",
  "displayName": "CD-EP-Info-Canary marker process execution",
  "description": "Fires when a deliberately planted marker string is executed on an onboarded device. Used to validate the custom detection pipeline end to end.",
  "status": "enabled",
  "queryCondition": {
    "queryText": "DeviceProcessEvents | where FileName in~ ('powershell.exe','pwsh.exe','cmd.exe') | where ProcessCommandLine has 'CD-CANARY-9F2A' | project Timestamp, ReportId, DeviceId, DeviceName, FileName, ProcessCommandLine, AccountName, AccountSid"
  },
  "schedule": {
    "frequency": "PT1H"
  },
  "detectionAction": {
    "alertTemplate": {
      "title": "Canary marker process executed",
      "description": "A canary marker string was executed on an onboarded device.",
      "severity": "informational",
      "recommendedActions": "Confirm the execution was a deliberate pipeline test before treating it as an incident.",
      "entityMappings": {
        "hosts": [
          { "deviceIdColumn": "DeviceId", "nameColumn": "DeviceName" }
        ],
        "accounts": [
          { "sidColumn": "AccountSid", "nameColumn": "AccountName" }
        ]
      }
    }
  }
}

Expected result: a created response whose location header addresses the identifier you supplied, so the rule is immediately retrievable at a path you chose rather than at a generated one. The write permission is CustomDetection.ReadWrite.All and reading needs CustomDetection.Read.All, both delegated or application and both requiring admin consent. A delegated caller also needs Detection tuning (manage), Security Administrator, or Security Operator on the terms in the prerequisites. Failure mode to watch for: omitting the identifier fails, because unlike almost every other resource in this interface it is required on creation rather than generated.

The new shape has gaps, and I am not going to paper over any of them with a plausible value. First, the encoding for continuous evaluation under the new frequency property is not documented anywhere. The old property had an explicit value for it; the replacement gives duration examples for hourly and daily and says nothing about near real time. If you need it programmatically, create one rule in the portal at continuous frequency and read it back to see what the service returns. Second, the structure of custom details in the new shape is described in prose as key and column pairs but appears in no example, so its wire format is unverified and this sheet omits it. Third, the incident task action from the old shape has no successor in the new one, so anyone using it today has no documented migration target.

One more inconsistency, in Microsoft’s own examples rather than in the reference. The tactics structure documents a technique field holding a technique and a separate sub-techniques collection, while the worked example puts a sub-technique identifier into the technique field. Both are first-party. Mirror the example if you want the thing to work today, and expect this to be tidied.

If detections as code is where you are heading rather than scripted creation, there is now a first-party path in preview: custom detection rules can be managed from a GitHub or Azure DevOps repository using Microsoft’s security infrastructure-as-code extension, deployed through the Sentinel repositories mechanism for automatic synchronisation or through the command line for a custom pipeline. It is preview, it is the only supported as-code route, and the client-supplied identifier described above is what makes it coherent. If you already run policy as code, this is the model to aim at rather than a folder of scripts.

Step 9: The governance loop

Two reviews, both monthly, both short. The first is the custom detection rules page, which lists every rule with its last run, last run status, next run and status, and which also lists Sentinel analytics rules where a workspace is onboarded. Look for rules that have not fired in ninety days, rules whose last run failed, and rules whose owner has left. Two things about disabled rules. The service can disable a custom detection on its own, which is why the programmatic status property carries an automatically disabled value alongside enabled and disabled; Microsoft does not publish the failure threshold that triggers it. And any rule in that list whose name has acquired an automatically disabled prefix is a Sentinel analytics rule rather than a custom detection, because the name-prefixing behaviour is documented only for analytics rules.

The second is the query resources report, reached from the advanced hunting page or from the reports page, which shows thirty days of compute consumption per query with an interface column distinguishing portal work from custom detections from programmatic calls, a usage rating, and a state that includes throttled. This is the report that identifies the rule making everyone else’s queries slow. Seeing every user’s queries requires the Entra Security Reader role or higher; without it a reviewer sees only their own.

Do not expect Microsoft’s built-in tuning to help you here: it states outright that built-in alert tuning rules do not apply to alerts from custom detection rules. Noise control for a detection you wrote lives in the query and in the entity design, and the honest remedy for a noisy rule is to fix it or retire it.

Rollback

Turning a rule off stops it running and leaves it and its history intact, which is the right first move for anything misbehaving. Deleting it turns it off and removes it permanently, and the alerts it already raised remain. Editing a rule takes effect at the next scheduled run rather than immediately, so an urgent fix wants an explicit run after the edit. Alerts and incidents already created are not withdrawn by any of this, and neither are any response actions the rule took, which are reversed through the Action center like any other action.

For the canary specifically, leave it running. It costs nothing, it fires only on a string nobody types by accident, and it is the cheapest available proof that the detection pipeline still works after a schema change, a permissions change or a tenant migration. Re-running its end-to-end test is a two minute regression check, and I have used it more than once to establish that a reported problem was somewhere else entirely.

Completion checklist

The query returns the timestamp and report identifier from the same event, projects a strong identifier column, and contains no time filter. The frequency is chosen for a stated reason and continuous evaluation has been considered for anything that acts. The alert title uses dynamic substitution and the recommended actions field is filled in rather than blank. Entities are mapped to identifier columns rather than to friendly names, and the scope column is projected if your tenant uses Sentinel scoping. Response actions are proportionate, and nothing new carries the investigation action. The rule is named on the convention and registered outside the platform with an owner and a review date. The end-to-end test has been run and produced one alert with both entities attached and an incident around it. The programmatic form, if you use one, is written in the new shape. And both monthly reviews have an owner and a place in the calendar.

Put these in the calendar rather than on the checklist. Migrating any existing programmatic rules to the new shape before 1 October 2026, which is a hard removal rather than a deprecation warning. And a check of the existing library for the investigation action, which becomes undefined in early September and about which nothing has been published.

You now own a detection plane on top of a schema you did not have to build, with a governance loop attached to it. What decides how much of your history that plane can reach, and whether anything that is not Microsoft’s ever enters it, is a workspace, and that decision has acquired a deadline. That is the Sentinel decision.


Defender XDR
‹ Previous: [D 7.3] The Schema Is the Product: Advanced Hunting and Custom Detections
Next: [D 7.4] The Sentinel Decision: What E5 Buys and What the Workspace Adds