Skip to main content

Microsoft Defender

Fluency collects Microsoft Defender XDR incidents and alerts, and a Defender device and posture roster. Defender correlates the alerts raised by its own components — Defender for Endpoint, for Identity, for Office 365, for Cloud Apps — into incidents; this integration brings the correlated incidents, the individual alerts, and the device inventory and vulnerability posture those detections landed on, onto the platform, where they are scored and investigated alongside the rest of your telemetry.

Authentication is app-only. You register one application in Microsoft Entra ID, grant it six read-only permissions, and give its credentials to Fluency. No user account is involved, no agent is installed, and nothing is pushed from Microsoft — Fluency polls.

The six permissions are not all in the same place. Two are Microsoft Graph permissions and four are Defender for Endpoint permissions, which Entra lists under a separate API called WindowsDefenderATP. One application, one client secret, two trips through the permission picker — and, behind the scenes, two access tokens for two different audiences. That split is the single most common thing to get wrong here, and it is the reason step 2 and step 3 are separate steps rather than one long list.

The setup has two halves:

  1. In Microsoft Entra ID — register an application, add two Microsoft Graph permissions and four Defender for Endpoint permissions, grant admin consent, and create a client secret. This produces three values: a Tenant ID, a Client ID and a Client Secret.
  2. In Fluency — install the Microsoft Defender application template and paste those three values into its form.
There are two Defender tiles in the catalog — this page covers the current one
TemplateTemplate IDData lake and index
Microsoft Defender (this page)MSDefenderChosen on the form, under Advanced Configurations
Microsoft Defender LegacyDefenderATPLegacyFixed by the template

They take the same three credentials and carry the same description — Microsoft Defender XDR Incidents via Entra Application — so the tile name is the only thing separating them in the catalog. The one-second test is the form itself: expand it, and the template with an Advanced Configurations row is the current one.

Microsoft Defender Legacy is the template that shipped as Microsoft Defender in earlier releases, kept under a new display name so existing deployments keep working. Install the current one unless you are matching an existing deployment; everything in Part 1 of this page applies to either.

If you registered this application before August 2026, it is short four permissions

Earlier versions of this guide asked for the two Microsoft Graph permissions — SecurityIncident.Read.All and SecurityAlert.Read.All — and nothing else. Those two are still exactly right, and an application built from them still collects incidents and alerts perfectly. What it does not hold is any of the four Defender for Endpoint permissions, so it produces an empty MS Defender card on SIEM → Resources — and that raises no error anywhere, because the half that works is the half being watched.

Open the existing app registration, add the four permissions in step 3, and grant consent again. There is no need to create a new secret or reinstall the Fluency application — permissions attach to the app registration, not to the credential.

What arrives

StreamAPI and endpointPermissionContents
IncidentsMicrosoft Graph — GET /security/incidentsSecurityIncident.Read.AllCorrelated attacks — severity, classification, determination, status, assignee, priority score, and the Defender portal link.
AlertsMicrosoft Graph — GET /security/alerts_v2SecurityAlert.Read.AllIndividual detections — MITRE technique IDs, recommended actions, the detecting service, and an evidence array.
Device inventoryDefender for Endpoint — GET /api/machines, GET /api/deviceavinfoMachine.Read.AllThe Machines and AV Health record sets — hostname, OS, last-seen, risk and exposure level, onboarding status, tags, and per-device antivirus engine, platform and signature state.
VulnerabilitiesDefender for Endpoint — GET /api/vulnerabilities, GET /api/machines/SecureConfigurationsAssessmentByMachineVulnerability.Read.AllThe Vulnerabilities and Secure Config record sets — CVEs with CVSS, EPSS and exploit status, and per-device configuration compliance.
Security recommendationsDefender for Endpoint — GET /api/recommendationsSecurityRecommendation.Read.AllThe Recommendations record set — the prioritised remediation list, with exposed-device counts and exposure impact.
Device secure scoreDefender for Endpoint — GET /api/configurationScoreScore.Read.AllThe Secure Score record set — Microsoft Secure Score for Devices as a tenant-level figure over time.

Alerts carry the richest event payload: evidence is where device names, user SIDs, process command lines, file hashes and IP addresses live. Every permission above is read-only — Fluency never writes back to Defender.

Two feeds, and only one of them is an event stream

The install produces an event index and a resource roster, and they behave nothing alike.

The first two rows of the table are events: they arrive when something happens, and a quiet tenant legitimately produces none. The last four rows are not events at all. They are record sets — six of them, holding the current state of your estate — and they appear as an MS Defender card on SIEM → Resources:

MS Defender card on the Resources page with buttons for Machines, AV Health, Recommendations, Secure Config, Secure Score and Vulnerabilities

This is where least privilege quietly costs you something. Each of those six buttons is backed by a different endpoint, and the endpoints are gated by four different permissions:

Record setEndpoint behind itPermission that buys it
MachinesGET /api/machinesMachine.Read.All
AV HealthGET /api/deviceavinfoMachine.Read.All
RecommendationsGET /api/recommendationsSecurityRecommendation.Read.All
Secure ConfigGET /api/machines/SecureConfigurationsAssessmentByMachineVulnerability.Read.All
Secure ScoreGET /api/configurationScoreScore.Read.All
VulnerabilitiesGET /api/vulnerabilitiesVulnerability.Read.All

Skip one permission and you lose the record sets it backs — not with a permission error, but with an empty list. An application granted only the two Microsoft Graph permissions collects incidents and alerts perfectly and shows no MS Defender card at all, and nothing about that reads as a failure, because the events are arriving. It stays looking healthy until the first time someone needs to know which devices a CVE is actually on.

Grant all four. The narrower sets are only worth considering if a policy forbids them, and the checkbox you skip is the record set you will not have.

The two halves are also meant to be read together. Alert evidence identifies its host by Defender's device ID — the 40-character hex string that is also id on a Machines record. Without the roster, a detection can tell you that 1e5bc9d7e413… was involved; with it, the same detection can name the machine, its owner, its risk level and the CVEs open on it.

What was verified, and what was not

The permission-to-endpoint mapping above is taken from Microsoft's API reference and is accurate for the Defender for Endpoint API. What could not be verified from outside the product is which of the several variants of each endpoint the template calls — Microsoft offers both a paginated JSON form and a bulk file-export form for the assessment and antivirus-health data, and they do not always take the same permission — nor the sync cadence for the roster.

To see what your install actually created, open the application's details panel with the blue eye button and read its Actions list, which records every object the install produced.

Before you begin

You need four things.

A Defender XDR licence that provisions incidents. Any of Microsoft 365 E5/A5, Microsoft 365 E3 with the Defender Suite add-on, Office 365 E5, Defender for Endpoint, Defender for Business, or Microsoft 365 Business Premium will do; Microsoft's Defender XDR prerequisites has the complete list. The practical test is simpler: if https://security.microsoft.com shows you an Incidents queue, you are covered for the first two streams.

Defender for Endpoint, provisioned and licensed for vulnerability management. The four Defender for Endpoint permissions reach capabilities licensed separately from incidents and alerts, and a tenant can be fully entitled to one and not the other. Two checks answer it, and each one takes a second:

  • Assets → Devices in the Defender portal shows a Device inventory. If it does, Machine.Read.All will return data.

    Defender portal Device inventory listing onboarded devices with risk level, exposure level, onboarding status and sensor health columns

  • Exposure management → Recommendations (or, on tenants not yet moved, Endpoints → Vulnerability management → Recommendations) shows a populated recommendations list. If it does, the three vulnerability-management permissions will return data.

    Defender portal security recommendations list showing recommendation name, weaknesses, exposed devices, remediation type and impact columns

A plan gate sits behind the second check. Microsoft Defender Vulnerability Management "is available as an add-on for Microsoft Defender for Endpoint Plan 2 or as a standalone subscription", though "Defender for Endpoint Plan 2 includes core vulnerability management capabilities" on its own. Four of the six record sets — Vulnerabilities, Secure Config, Recommendations and Secure Score — depend on it.

The gate does not block the install. A tenant without the entitlement installs the application successfully, collects incidents and alerts, populates Machines and AV Health, and leaves the other four record sets empty — so settle this before you install, rather than diagnosing an empty card a month later.

An Entra identity that can register an application and grant tenant-wide admin consent. That means Global Administrator, or Privileged Role Administrator combined with Application Administrator or Cloud Application Administrator. Consent rights are the part people miss — without them you can create the application, but its permissions stay unconsented and no data ever flows.

Access to the Fluency portal with permission to install applications.

Part 1 — Microsoft Entra ID

Work through this half in the Microsoft Entra admin center, or in the Azure portal under Microsoft Entra ID. If you would rather script it, skip to Automated alternative — Azure CLI; the result is identical.

1. Register the application

Go to App registrations → New registration.

  • Name — anything recognisable. ingext-defender is the name Fluency's own tooling uses, and is a good default.
  • Supported account typesAccounts in this organizational directory only (single tenant).
  • Redirect URI — leave blank. This application never signs a user in.

Click Register.

2. Add the Microsoft Graph permissions

On the new application, go to API permissions → Add a permission. On the Microsoft APIs tab, choose Microsoft Graph.

Request API permissions dialog with the Microsoft APIs tab open and Microsoft Graph selected

Choose Application permissions"Your application runs as a background service or daemon without a signed-in user." This is the app-only path; Delegated permissions would require a signed-in user and will not work here.

Permission type selection showing Delegated permissions and Application permissions

Tick two permissions. The search box takes a fragment of the name:

Search forTickListed as
securityincidentSecurityIncident.Read.AllRead all security incidents
securityalertSecurityAlert.Read.AllRead all security alerts

Be careful to pick SecurityAlert.Read.All, not SecurityAlert.Create.All or SecurityAlert.ReadWrite.All. Admin consent required reads Yes for both — that is step 4.

Permission picker filtered to securityincident with SecurityIncident.Read.All ticked

Permission picker filtered to securityalert with SecurityAlert.Read.All ticked

Click Add permissions.

Register against Microsoft Graph, not Microsoft Threat Protection

Microsoft still publishes an older Defender XDR walkthrough, Create an app to access Microsoft Defender XDR without a user. Do not follow it for this integration. It grants Incident.Read.All and Alert.Read.All on the legacy Microsoft Threat Protection resource (api.security.microsoft.com as an audience), which are different permissions on a different API. Fluency calls Microsoft Graph for incidents and alerts, so those two permissions must come from the Microsoft Graph tile shown above.

Note that this is not the same thing as the WindowsDefenderATP tile you are about to use — Microsoft Threat Protection and WindowsDefenderATP are two different resources with two different application IDs. See Troubleshooting for what a wrongly-registered app looks like.

3. Add the Defender for Endpoint permissions

Still on API permissions, click Add a permission again. This time switch to the APIs my organization uses tab, type WindowsDefenderATP, and select it. The application ID should read fc780465-2017-40d4-a0c5-307022471b92.

Request API permissions dialog on the APIs my organization uses tab, filtered to WindowsDefenderATP, showing one result with application ID fc780465-2017-40d4-a0c5-307022471b92

WindowsDefenderATP is Entra's name for the Microsoft Defender for Endpoint API. It is not on the Microsoft APIs tab, which is why the first tab is the wrong place to look for it.

Choose Application permissions again — the same reason as before, and the same mistake is available.

Permission type selection under WindowsDefenderATP showing Delegated permissions and Application permissions

Tick four permissions:

PermissionListed asWhat it buys
Machine.Read.AllRead all machine profilesMachines and AV Health
Vulnerability.Read.AllRead Threat and Vulnerability Management vulnerability informationVulnerabilities and Secure Config
SecurityRecommendation.Read.AllRead Threat and Vulnerability Management security recommendation informationRecommendations
Score.Read.AllRead Threat and Vulnerability Management scoreSecure Score

"Threat and Vulnerability Management" is Microsoft's former name for Defender Vulnerability Management. The permission strings never changed, so the picker still uses the old wording while the portal uses the new one.

Click Add permissions.

If WindowsDefenderATP does not appear on the list

The APIs my organization uses tab shows only APIs that exist as service principals in your directory — "Apps in your directory that expose APIs are shown below." If WindowsDefenderATP returns nothing, Defender for Endpoint has not been provisioned in this tenant yet. Sign in to https://security.microsoft.com, complete Defender for Endpoint setup so the service principal is created, then come back to this step. The two Microsoft Graph permissions from step 2 are unaffected and do not need to be re-added.

Application permissions do nothing until an administrator consents on the tenant's behalf. On the same API permissions page, click Grant admin consent for <tenant>.

Grant admin consent confirmation dialog

Confirm with Yes. All six rows must then show a green Granted for <tenant>, under two group headings — Microsoft Graph and WindowsDefenderATP (4):

Configured permissions listing Microsoft Graph with SecurityAlert.Read.All and SecurityIncident.Read.All, and WindowsDefenderATP with Machine.Read.All, Score.Read.All, SecurityRecommendation.Read.All and Vulnerability.Read.All, all showing Granted for the tenant, with one further Microsoft Graph row greyed out

Before consent, the same rows read Not granted in the Status column. Two group headings is the thing to check — a reader who missed the second tab will see only the Microsoft Graph group here, and that page looks perfectly healthy.

If the button is greyed out, the account you are signed in as does not hold consent rights. The application and its permissions are already saved — ask an administrator who does to open the same page and click it.

5. Create a client secret

Go to Certificates & secrets → Client secrets → New client secret. Give it a description you will recognise in two years' time, and choose an expiry.

Add a client secret panel with a description and expiry drop-down

The drop-down opens on Recommended: 180 days (6 months). Microsoft caps the portal's maximum at 24 months; pick whatever your credential-rotation policy allows, and write the date down. When the secret expires the integration stops collecting, and it fails quietly from Defender's side — see Rotating the client secret.

Click Add. The new secret appears in the list with its Value shown in full, exactly once.

Client secrets list with the new secret&#39;s value visible in the Value column

Copy the Value now, and copy the Value — not the Secret ID

The Value column is readable only until you navigate away; on reload it is masked forever and the secret cannot be recovered. The Secret ID in the next column is not the secret — it is an identifier for the credential record. Pasting the Secret ID into Fluency is the single most common cause of a Defender application that installs and then immediately aborts.

6. Record the Tenant ID and Client ID

Go to the application's Overview page. Under Essentials, copy:

  • Application (client) ID → Fluency's Client ID
  • Directory (tenant) ID → Fluency's Tenant ID

App registration Overview page with the Application (client) ID and Directory (tenant) ID fields

You should now be holding all three values:

ValueWhere it came from
Tenant IDOverview → Directory (tenant) ID
Client IDOverview → Application (client) ID
Client SecretCertificates & secrets → the secret's Value

Unlike the client secret, the tenant and client IDs are not sensitive and can be read from the portal at any time.

Automated alternative — Azure CLI

The same registration as a script, in the same seven steps. Useful when onboarding several tenants, and the path Fluency's own onboarding tooling takes. Requires Azure CLI and the same consent rights as the portal walkthrough.

# Sign in to the target tenant, then confirm which tenant you are in
az login
az account show --query '{tenant:tenantId, account:user.name}' -o table

# 1. Register the application (single tenant, no redirect URI)
APP_ID=$(az ad app create \
--display-name ingext-defender \
--sign-in-audience AzureADMyOrg \
--query appId -o tsv)

# 2. The two resources. GRAPH is a well-known constant; MDE is WindowsDefenderATP.
GRAPH=00000003-0000-0000-c000-000000000000
MDE=fc780465-2017-40d4-a0c5-307022471b92

# 3. Resolve the application-role IDs at runtime — no hardcoded GUIDs
role() { az ad sp show --id "$1" --query "appRoles[?value=='$2'].id | [0]" -o tsv; }

INCIDENT=$(role $GRAPH SecurityIncident.Read.All)
ALERT=$(role $GRAPH SecurityAlert.Read.All)

MACHINE=$(role $MDE Machine.Read.All)
VULN=$(role $MDE Vulnerability.Read.All)
RECO=$(role $MDE SecurityRecommendation.Read.All)
SCORE=$(role $MDE Score.Read.All)

# 4. Request them as Application permissions (=Role, as opposed to =Scope for delegated).
# One call per API — --api takes a single resource.
az ad app permission add --id $APP_ID --api $GRAPH \
--api-permissions $INCIDENT=Role $ALERT=Role
az ad app permission add --id $APP_ID --api $MDE \
--api-permissions $MACHINE=Role $VULN=Role $RECO=Role $SCORE=Role

# 5. Create the service principal, then grant tenant-wide admin consent
az ad sp create --id $APP_ID
az ad app permission admin-consent --id $APP_ID

# 6. Append a client secret valid for two years, and print it
az ad app credential reset --id $APP_ID \
--display-name ingext-defender --years 2 --append \
--query password -o tsv

# 7. Print the tenant and client IDs
az account show --query tenantId -o tsv
echo $APP_ID
--append is not optional

Without --append, az ad app credential reset "clears all passwords and keys" before creating the new one. On a fresh application that is harmless; run it against an application already in service and you will revoke the credential that is currently collecting data.

Step 3 fails if WindowsDefenderATP has no service principal in the tenant — az ad sp show --id $MDE returns a not-found error, and the four role $MDE ... variables come back empty. That means Defender for Endpoint has never been provisioned here; see step 3.

Step 5 can fail with a replication delay immediately after step 1 — if admin-consent reports that the service principal was not found, wait a few seconds and run it again. The commands are otherwise safe to re-run: az ad app create will create a second application of the same name, so on a retry substitute the existing appId rather than re-running step 1.

Confirm consent landed by counting the app-role assignments — six is correct:

az rest --method GET \
--url "https://graph.microsoft.com/v1.0/servicePrincipals(appId='$APP_ID')/appRoleAssignments" \
--query "length(value)"

To see the split by resource, which is what actually catches a missed second tab:

az rest --method GET \
--url "https://graph.microsoft.com/v1.0/servicePrincipals(appId='$APP_ID')/appRoleAssignments" \
--query "value[].resourceDisplayName" -o tsv | sort | uniq -c

Expect 2 Microsoft Graph and 4 WindowsDefenderATP.

Verify before you leave Microsoft

The credential is now doing two jobs against two different token audiences, and only one of them can be exercised with a Graph call. Ten minutes here is worth an afternoon later.

TENANT=<tenantId>; CLIENT=<clientId>; SECRET=<clientSecret>

# 1. Microsoft Graph — incidents and alerts
GRAPH_TOKEN=$(curl -s -X POST "https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token" \
-d "client_id=$CLIENT" -d "client_secret=$SECRET" \
-d "scope=https://graph.microsoft.com/.default" \
-d "grant_type=client_credentials" | jq -r .access_token)

curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $GRAPH_TOKEN" \
"https://graph.microsoft.com/v1.0/security/incidents?\$top=1"

# 2. Defender for Endpoint — the roster. Note the different scope.
MDE_TOKEN=$(curl -s -X POST "https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token" \
-d "client_id=$CLIENT" -d "client_secret=$SECRET" \
-d "scope=https://api.securitycenter.microsoft.com/.default" \
-d "grant_type=client_credentials" | jq -r .access_token)

curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $MDE_TOKEN" \
"https://api.security.microsoft.com/api/machines?\$top=1"

Two 200s means both halves work. Otherwise, match the error against the table below — the AADSTS… strings are Microsoft's standard identity-platform codes, quoted from its error reference rather than observed on a particular tenant.

ResponseWhat it means
AADSTS7000215: Invalid client secret providedThe Secret ID was pasted instead of the secret Value, or the secret has expired. Back to step 5.
AADSTS700016: Application with identifier … was not foundWrong Client ID, or the right one against the wrong tenant. Back to step 6.
AADSTS500011: The resource principal named https://api.securitycenter.microsoft.com was not found in the tenantDefender for Endpoint has never been provisioned in this tenant, so there is no resource to issue a token for. Back to step 3.
403 Forbidden on both callsAdmin consent was never granted. Back to step 4.
403 Forbidden on the Defender call while the Graph call returns 200Either the four WindowsDefenderATP permissions are missing, or the token was requested with the wrong scope. Check the scope on the second token first — it is https://api.securitycenter.microsoft.com/.default, and it is not the host the request goes to.
404 Not Found on /api/machinesAuthorised, but no devices in the tenant's retention window. This is not a URL error — Microsoft documents 404 as the empty response for this endpoint.

To check what the application is actually authorised for rather than inferring it, decode either token at https://jwt.ms. The Graph token's roles claim should hold SecurityIncident.Read.All and SecurityAlert.Read.All with aud of https://graph.microsoft.com; the Defender token's should hold Machine.Read.All, Vulnerability.Read.All, SecurityRecommendation.Read.All and Score.Read.All with aud of https://api.securitycenter.microsoft.com.

Part 2 — Fluency

Install the Application

Go to Platform → Applications → Install Application From Template and choose Microsoft Defender from the Endpoint Management category.

ParameterNotes
Tenant IDThe directory the application is registered in — Entra's Directory (tenant) ID.
Client IDThe registered application — Entra's Application (client) ID.
Client SecretThe secret Value, not the Secret ID. Masked once the application is installed.
datalakePre-filled managed.
datalake index namePre-filled MSDefender. Two applications writing to the same index name in the same lake will collide, and the second one aborts — so a second tenant needs its own index name as well as its own Name.

The last two sit in the collapsed Advanced Configurations row below the parameters — expand it to reach them.

Leave Name and Display Name at default / Default unless you are installing a second instance — collecting from two Defender tenants, for example — in which case give each one a name you will recognise on the applications list.

Setup New Microsoft Defender Application panel

Three credentials and a destination is the whole form, and it did not grow when the permission set did. Nothing here selects which streams to collect: the template asks the credential for everything, and what arrives is decided entirely by what you granted in Part 1. There is no Fluency-side setting that can compensate for a missing permission, and no Fluency-side symptom when one is missing.

Press Install. The application then appears in the Installed Applications view, where its badge reads Running once the pipeline is up.

Confirm it is running

The application appears under Installed Applications as Microsoft Defender: Default. Its badge should read Running in green within a few seconds; Aborted in red means the pipeline stopped on an error, and the details panel will carry the message.

The blue eye button on the card opens the details panel, titled Viewing MSDefender Application. Details repeats the template name, the internal Name, the Display Name and the Status badge. Parameters shows the three values back with the client secret masked. Actions lists, one line per object, the pipeline objects the install created.

Viewing MSDefender Application panel showing details, parameters and the list of pipeline actions

This capture predates the resource sync, and shows the five objects an events-only install produced — an integration, a router, the two data sinks EW-MSDefender-<name> and Behavior-MSDefender-<name>, and a pipe to the router. Read your own Actions list rather than this one; it is the authoritative record of what your install built.

Every field in this panel is read-only. Changing a credential means uninstalling the application with the red Uninstall button and installing it again.

Check the roster, not the event count. First collection of events reaches back 24 hours and the two event streams then poll independently every five minutes, so a quiet tenant can legitimately produce nothing for a while — which makes an empty index useless as a health signal. The MS Defender card on SIEM → Resources is the stricter check, because a device roster is never legitimately empty on a tenant that has any devices at all:

Resources page showing an Office 365 card and an MS Defender card with buttons for Machines, AV Health, Recommendations, Secure Config, Secure Score and Vulnerabilities

Click through each of the six buttons. A record set that has loaded shows a Total Records count and populated facets; one that has loaded and is genuinely empty shows Total Records 0 and No data found; one that was never loaded at all shows Resource Load Failed instead of a list. Those three states mean different things — see Resources — and the difference between the last two is usually the difference between a licence gate and a missing permission.

See Confirm data is arriving for how to check that events are landing.

Maintenance

Rotating the client secret

Client secrets expire, and Defender gives no warning on the Fluency side when one does — collection simply stops. Rotate before the expiry date you recorded in step 5:

  1. In Entra, go to the application's Certificates & secrets and add a New client secret. Leave the old one in place for now; an application can hold several.
  2. Copy the new Value.
  3. In Fluency, uninstall the Microsoft Defender application and install it again with the new secret. The parameters are read-only after install, so reinstalling is the supported way to change a credential.
  4. Once data is flowing again, delete the old secret in Entra.

Rotation does not touch the permission set. The six permissions live on the app registration and survive any number of secrets.

Reviewing the permission set

When the template gains a stream — as it did in August 2026 — the app registration needs the new permission, and nothing on the Fluency side reports that it is missing. The symptom is an empty record set on a card that is otherwise full, which reads as "we have no vulnerabilities" rather than as a fault.

Re-read the API permissions page against the table in step 3 when a stream you expect is empty, and after any Fluency upgrade that mentions Defender. The appRoleAssignments count in the CLI section answers the same question in one command.

Removing the integration

Uninstall the application in Fluency first — the red Uninstall button at the foot of its details panel — so the pipeline stops cleanly before its credentials stop working.

To revoke access on the Microsoft side, delete the app registration, or revoke just the consent from Microsoft Entra ID → Enterprise applications, where the application appears as a service principal. Deleting it there removes the tenant's consent without deleting the registration.

Enterprise applications list filtered by name

Troubleshooting

403 Forbidden is doing double duty on this integration. It is the honest answer when consent was never granted, and it is also what Defender for Endpoint returns to a perfectly authorised application that asked for a token against the wrong audience — Microsoft's own note says requests fail with 403 Forbidden "even if the API endpoint uses https://api.security.microsoft.com" when the token audience does not match. If Graph calls succeed and Defender for Endpoint calls do not, it is the audience, not the consent.

SymptomLikely causeFix
The application shows Aborted immediately after installThe Secret ID was pasted instead of the secret Value, or the secret has expiredUninstall, then reinstall with the secret's Value. If it was never captured, create a new secret — the old value is unrecoverable.
Every call returns 403 ForbiddenAdmin consent was never grantedEntra → App registrations → the app → API permissionsGrant admin consent. All six rows must read Granted for <tenant>.
Incidents and alerts arrive; the MS Defender card on Resources is missing entirelyNone of the four WindowsDefenderATP permissions were added — usually because only the Microsoft APIs tab was usedAdd them from the APIs my organization uses tab (step 3) and consent again. The event feed is unaffected either way, which is why this fails quietly.
The card is there, but one record set is empty while the others are fullThe permission behind that one record set is missingMatch the empty set against the mapping table and add the permission it names.
Machines and AV Health are full; Vulnerabilities, Secure Config, Recommendations and Secure Score are all emptyDefender Vulnerability Management is not licensed on this tenant, so the permissions are granted over data that does not existConfirm in the Defender portal under Exposure management → Recommendations. If that page is empty too, this is a licence gate rather than a fault.
WindowsDefenderATP returns nothing on the APIs my organization uses tabDefender for Endpoint has never been provisioned in the tenant, so its service principal does not existComplete Defender for Endpoint setup at https://security.microsoft.com, then add the permissions.
A direct call to /api/machines returns 404 Not FoundNo devices in the retention window — this is the documented empty response, not a bad URLCheck Assets → Devices in the Defender portal. If it is empty there, nothing is onboarded yet.
Incidents arrive but alerts do not, or the reverseOnly one of the two Graph permissions was added or consentedCheck that both SecurityIncident.Read.All and SecurityAlert.Read.All are listed and green.
Permissions are listed under Microsoft Threat Protection rather than Microsoft GraphThe app was registered by following Microsoft's legacy Defender XDR walkthroughAdd the two Microsoft Graph application permissions (step 2) and grant consent again. The legacy permissions can be removed. Microsoft Threat Protection is a different API from WindowsDefenderATP; the four permissions in step 3 belong on the latter.
Collection worked, then stopped around the six-month markThe client secret expired — 180 days is the portal's defaultRotate the secret.
The application is Running but no Defender events appearThe tenant genuinely has no incidents or alerts in the polled windowConfirm at https://security.microsoft.com that the Incidents queue is non-empty, and check the Resources card instead — it is the health signal that does not depend on event volume.

A wrongly-registered application looks like this — note Microsoft Threat Protection as the API and Incident.Read.All as the permission. Consent is granted and the portal shows no error, which is what makes this one hard to spot:

Configured permissions showing the legacy Microsoft Threat Protection API with Incident.Read.All

Under the hood

Engineering reference — collection behaviour, the two token audiences, output format and API limits

This section documents the plugin's implementation and the APIs it calls, for Fluency engineering. Administrators connecting Defender do not need it.

Two audiences, one credential

The single client-credentials pair is exchanged for two different tokens, because the six permissions are split across two resources:

Microsoft GraphDefender for Endpoint
Entra API nameMicrosoft GraphWindowsDefenderATP
Application ID00000003-0000-0000-c000-000000000000fc780465-2017-40d4-a0c5-307022471b92
Token scopehttps://graph.microsoft.com/.defaulthttps://api.securitycenter.microsoft.com/.default
Request hosthttps://graph.microsoft.com/v1.0https://api.security.microsoft.com/api
PermissionsSecurityIncident.Read.All, SecurityAlert.Read.AllMachine.Read.All, Vulnerability.Read.All, SecurityRecommendation.Read.All, Score.Read.All

The scope and the host do not match on the Defender for Endpoint side, and that is deliberate on Microsoft's part rather than a documentation error. Microsoft states it twice in its own API reference:

Some Microsoft Defender for Endpoint APIs continue to require access tokens issued for the legacy resource https://api.securitycenter.microsoft.com. If the token audience doesn't match the resource expected by the API, requests fail with 403 Forbidden, even if the API endpoint uses https://api.security.microsoft.com.

If permission is defined under WindowsDefenderATP, the scope must be set to: https://api.securitycenter.microsoft.com/.default

Both hosts answer, and both are documented, so a wrong-audience token produces an authorisation failure rather than a routing one. There is no combination of hostnames that turns this into a clearer error.

Collection behaviour

The two original event streams poll on a five-minute cadence:

  • Incidents — GET https://graph.microsoft.com/v1.0/security/incidents
  • Alerts — GET https://graph.microsoft.com/v1.0/security/alerts_v2

Each maintains an independent watermark (lastPoll for incidents, lastAlertPoll for alerts). On each poll, the filter lastUpdateDateTime ge <watermark> returns everything updated since the last successful run; pagination follows @odata.nextLink. On first run with no state, both watermarks default to 24 hours back.

Auth is OAuth2 client credentials.

POST https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
scope=https://graph.microsoft.com/.default # incidents and alerts
scope=https://api.securitycenter.microsoft.com/.default # machines, TVM
grant_type=client_credentials

Retry and backoff handle 429/503/504, with Retry-After honoured.

API limits worth knowing

APILimit
GET /api/machines100 calls/min, 1,500/hour; $top max 10,000; devices are returned per the tenant's configured retention period; 404 when there are none
GET /api/vulnerabilities$top max 8,000
GET /api/recommendations$top max 10,000
GET /api/deviceavinfo30 calls/min, 1,000/hour; max page size 200,000; default page size 20
GET /api/machines/SecureConfigurationsAssessmentByMachine30 calls/min, 1,000/hour; max page size 200,000

The record sets are state, not events, which matters to anything written against them. A vulnerability record carries a status of RemediationRequired, NoActionRequired, UnderException or PartialException, and a recommendation carries Active, Full exception or Partial exception — so a rule that fires on the arrival of a record rather than on its status will fire on the ones Microsoft has already ruled out. Deduplicate vulnerabilities on id (the CVE) and recommendations on id (va-_-microsoft-_-edge_chromium-based and similar); device-scoped rows key on deviceId plus the finding's own identifier.

Two further properties of the vulnerability-management data are worth carrying into detection content. It is a current-state snapshot with no history — Microsoft: "Data that is collected (using either JSON response or via files) is the current snapshot of the current state, and does not contain historic data" — so trend analysis has to come from successive collections stored on our side. And its device population is scoped differently from the device inventory's: "Vulnerability management shows devices that were in use within the last 30 days. This is different from device status in Defender for Endpoint, where if a device has Inactive status if it doesn't communicate with the service for more than seven days." A device count taken from Machines and one taken from Vulnerabilities will not agree, and neither is wrong.

Output

Event output goes to gzip-JSONL files under the plugin's home directory, named msdefender_incident_<ts>.log.gz and msdefender_alert_<ts>.log.gz. Each line is {"@fields": <event>, "@customer": <name>}. Incidents are emitted with Sourcetype / Importsource = microsoftDefender; alerts with microsoftDefenderAlert.

The plugin reads three values from its integration config/secret: tenantId, clientId, clientSecret.

What your own install records

The August 2026 permission expansion is documented here from the Entra capture of the granted permission set, the Fluency Resources card, and Microsoft's API reference. Three things vary with the release you are running, and the Actions list on the installed application — reached with the blue eye button on its details panel — is the authoritative record of all three:

  • Which endpoint variant backs each record set. Microsoft offers both a paginated JSON form and a bulk file-export form for the secure-configuration and antivirus-health data, and they take different permissions (Vulnerability.Read.All for the export form, Machine.Read.All for the JSON form of deviceavinfo).
  • The sync cadence and watermarking for the six record sets. They are snapshots rather than event streams, so a watermark may not apply.
  • Whether the install creates exactly the five pipeline objects in the capture above, or an MSDefender resource group alongside them.

Files

cmd/plugin_msdefender/ Long-running plugin binary (managed by pluginMgr)
cmd/test_msdefender/ Standalone CLI for one-shot testing outside the manager
internal/msdefender/ Graph API client: auth, fetch, retry, pagination
defender-atp-poll.py DEPRECATED legacy reference (legacy api.security.microsoft.com)

Standalone testing

Use the test CLI to exercise auth and fetch without the plugin manager:

cat > /tmp/msdef.json <<EOF
{
"tenantID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"clientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
EOF

go run ./cmd/test_msdefender -config /tmp/msdef.json -hours 24 -log debug

It fetches both incidents and alerts for the requested window, writes one gzip-JSONL file per kind under -home (default /tmp/msdefender_test), and prints up to three sample records of each.

Build and verify

go build ./...
go vet ./...

Verification checklist after deploying a new build:

  1. Run the standalone tester against a real tenant. Confirm log lines fetched incidents count=N and fetched alerts count=M.
  2. Inspect the JSONL output. Each record should carry Graph field names — id, displayName, createdDateTime, lastUpdateDateTime — plus severity, classification, and (alerts) evidence and mitreTechniques. Incidents also carry a synthesised alertKey of the form displayName (id).
  3. Pagination: pick a tenant with more than one page of incidents (or temporarily lower $top for testing). Confirm the loop follows @odata.nextLink and returns the full set.
  4. Watermark: run twice in succession; the second run should fetch zero or few new events.
  5. Auth failure: temporarily revoke SecurityIncident.Read.All and confirm the plugin surfaces an error notification with a clear message rather than crashing.
  6. Audience: acquire a token with scope=https://graph.microsoft.com/.default and call /api/machines with it. Confirm the failure is surfaced as an authorisation error naming the resource, not as a generic 403 — this is the mistake most likely to reach production.

Migration notes — legacy API to Graph (April 2026)

This plugin previously polled https://api.security.microsoft.com/api/incidents and /api/alerts with the legacy Threat Protection scope. Microsoft has marked those endpoints as deprecated in favour of the unified Microsoft Graph security API. The plugin was migrated to Graph in April 2026.

LegacyMicrosoft Graph
Base URLhttps://api.security.microsoft.comhttps://graph.microsoft.com/v1.0
Incidents path/api/incidents/security/incidents
Alerts path/api/alerts/security/alerts_v2
Token scopehttps://api.security.microsoft.com/.defaulthttps://graph.microsoft.com/.default
Filter fieldlastUpdateTimelastUpdateDateTime
App permissionsIncident.Read.All, Alert.Read.All (Threat Protection)SecurityIncident.Read.All, SecurityAlert.Read.All (Graph)
Pagination@odata.nextLink@odata.nextLink (unchanged)

Schema also changed — incidents use id / displayName / createdDateTime / lastUpdateDateTime instead of incidentId / incidentName / createdTime / lastUpdateTime. Alerts gained richer evidence (evidence[], mitreTechniques, priorityScore).

Design decisions

  • Pass through Graph schema as-is. No legacy-name remapping or aliasing — id, displayName, createdDateTime, lastUpdateDateTime, evidence and mitreTechniques are emitted verbatim.
  • Sourcetype/importsource names unchanged (microsoftDefender, microsoftDefenderAlert). Downstream parsers must be updated to handle the new schema under the same sourcetype.
  • No $expand=alerts on incidents. The two-stream design — independent watermarks, separate dump files — is preserved.
  • defender-atp-poll.py is kept as a historical reference with a deprecation header; it is no longer a functional path for new pipelines.

References

Microsoft — Graph security API

Microsoft — Defender for Endpoint API

Microsoft — licensing and prerequisites

Fluency