Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
1d36d4e
feat(MicrosoftDefender): work without advanced hunting (PLUG-4744)
claude Oct 28, 2026
356e29a
docs(MicrosoftDefender): scope app permissions and advanced hunting l…
claude Oct 30, 2026
276c96e
feat(MicrosoftDefender): vulnerability exposures without advanced hun…
claude Oct 30, 2026
ae06cee
fix(MicrosoftDefender): issue both API tokens from the pre-request sc…
claude Oct 30, 2026
b658786
fix(MicrosoftDefender): request tokens from login.microsoft.com
claude Oct 30, 2026
424e21b
fix(MicrosoftDefender): valid alert status filter and clear vulnerabi…
claude Oct 30, 2026
f1f10ad
fix(MicrosoftDefender): scope the Vulnerability Exposure dashboard to…
claude Oct 30, 2026
e944a71
feat(MicrosoftDefender): say what each stream needs, and explain Grap…
claude Nov 2, 2026
b00b609
revert(MicrosoftDefender): hold back vulnerability data without advan…
claude Nov 2, 2026
2779940
fix(MicrosoftDefender): name the missing table and both causes of a 403
claude Nov 6, 2026
21c022a
fix(MicrosoftDefender): count all active alerts on Overview
claude Nov 6, 2026
4bf96c3
fix(MicrosoftDefender): add credentials hint to the advanced hunting …
claude Nov 9, 2026
0cb8a79
fix(MicrosoftDefender): scope Overview and Incidents dashboards to hi…
claude Nov 9, 2026
3aaefca
fix(MicrosoftDefender): count incidents by status and show top incide…
claude Nov 9, 2026
43458fd
fix(MicrosoftDefender): read-only permission hint and full device exp…
claude Nov 10, 2026
414479c
fix(MicrosoftDefender): label devices with no exposure level
claude Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions plugins/MicrosoftDefender/v1/configValidation.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@
"displayName": "Alerts access",
"dataStream": { "name": "validationAlerts" },
"success": "Successfully read alerts.",
"error": "Cannot read alerts. Grant the 'SecurityAlert.Read.All' application permission in Microsoft Entra ID, or check your client ID and secret.",
"required": true
"error": "Cannot read alerts, so alert tiles will not work. Grant the 'SecurityAlert.Read.All' application permission in Microsoft Entra ID. If every check fails, check your tenant ID, client ID and secret.",
"required": false
},
{
"displayName": "Incidents access",
"dataStream": { "name": "validationIncidents" },
"success": "Successfully read incidents.",
"error": "Cannot read incidents. Grant the 'SecurityIncident.Read.All' application permission in Microsoft Entra ID, or check your client ID and secret."
"error": "Cannot read incidents, so incident tiles will not work. Grant the 'SecurityIncident.Read.All' application permission in Microsoft Entra ID. If every check fails, check your tenant ID, client ID and secret.",
"required": false
},
{
"displayName": "Advanced hunting access",
"dataStream": { "name": "validationHunting" },
"success": "Successfully ran an advanced hunting query.",
"error": "Cannot run advanced hunting queries. Grant the 'ThreatHunting.Read.All' application permission in Microsoft Entra ID, or check your client ID and secret.",
"required": true
"error": "Cannot run advanced hunting queries on device data, so devices will not be imported and the Advanced Hunting dashboards will not work. This needs the 'ThreatHunting.Read.All' application permission and a licence that includes advanced hunting on devices, such as Microsoft Defender for Endpoint Plan 2. Alerts, incidents and Secure Score work without it. If every check fails, check your tenant ID, client ID and secret.",
"required": false
},
{
"displayName": "Secure score access",
"displayName": "Secure Score access",
"dataStream": { "name": "validationSecureScore" },
"success": "Successfully read secure scores.",
"error": "Cannot read secure scores. Grant the 'SecurityEvents.Read.All' application permission in Microsoft Entra ID, or check your client ID and secret."
"success": "Successfully read Secure Score.",
"error": "Cannot read Secure Score, so Secure Score tiles will not work. Grant the 'SecurityEvents.Read.All' application permission in Microsoft Entra ID. If every check fails, check your tenant ID, client ID and secret.",
"required": false
}
]
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "Vulnerabilities",
"displayName": "Vulnerabilities",
"description": "Returns vulnerabilities data for the specified device",
"description": "Returns vulnerabilities for the selected device (needs advanced hunting on device data)",
"baseDataSourceName": "httpRequestScoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "advancedHuntingQuery",
"displayName": "Advanced Hunting Query",
"description": "Queries a specified set of data supported by Defender to proactively look for specific threats in your environment",
"description": "Runs a KQL query to look for threats in your environment (needs advanced hunting, such as Defender for Endpoint Plan 2 or Defender for Office 365 Plan 2)",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
15 changes: 11 additions & 4 deletions plugins/MicrosoftDefender/v1/dataStreams/alerts.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "alerts",
"displayName": "Alerts",
"description": "Returns a list of alert resources created to track suspicious activities in an organization",
"description": "Returns alerts created to track suspicious activity in the organization (needs SecurityAlert.Read.All)",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "nextUrl",
Expand All @@ -30,7 +31,7 @@
"getArgs": [
{
"key": "$filter",
"value": "{{ status && status.length > 0 ? \"(status eq \" + status.map((m) => { return `'${m}'` }).join(\" or status eq \") + \") and \" : \"\" }}{{ severity && severity.length > 0 ? \"(severity eq \" + severity.map((m) => { return `'${m}'` }).join(\" or severity eq \") + \") and \" : \"\" }}{{timeframe.enum !== \"none\" ? timeframeCol + \" ge \" + timeframe.start + \" and \" + timeframeCol + \" le \" + timeframe.end : \"1 eq 1\"}}"
"value": "{{ status && status.length > 0 ? \"(status eq \" + status.map((m) => { return `'${m === \"newAlert\" ? \"new\" : m}'` }).join(\" or status eq \") + \") and \" : \"\" }}{{ severity && severity.length > 0 ? \"(severity eq \" + severity.map((m) => { return `'${m}'` }).join(\" or severity eq \") + \") and \" : \"\" }}{{timeframe.enum !== \"none\" ? timeframeCol + \" ge \" + timeframe.start + \" and \" + timeframeCol + \" le \" + timeframe.end : \"1 eq 1\"}}"
}
],
"headers": []
Expand Down Expand Up @@ -66,6 +67,12 @@
"shape": "string",
"role": "label"
},
{
"name": "serviceSource",
"displayName": "Service Source",
"shape": "string",
"role": "label"
},
{
"name": "detectionSource",
"displayName": "Detection Source",
Expand Down Expand Up @@ -164,7 +171,7 @@
"source": "fixed",
"values": [
{
"value": "newAlert",
"value": "new",
"label": "New"
},
{
Expand Down
5 changes: 3 additions & 2 deletions plugins/MicrosoftDefender/v1/dataStreams/devices.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "devices",
"displayName": "Devices",
"description": "Returns detailed attributes and properties for the specified device",
"description": "Returns attributes and properties for the selected device (needs advanced hunting on device data)",
"baseDataSourceName": "httpRequestScoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
5 changes: 3 additions & 2 deletions plugins/MicrosoftDefender/v1/dataStreams/incidents.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "incidents",
"displayName": "Incidents",
"description": "Returns a list of incident objects that Microsoft Defender created to track attacks in an organization",
"description": "Returns incidents Microsoft Defender created to track attacks in the organization (needs SecurityIncident.Read.All)",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "nextUrl",
Expand Down
3 changes: 2 additions & 1 deletion plugins/MicrosoftDefender/v1/dataStreams/listDevices.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "recommendations",
"displayName": "Recommendations",
"description": "Returns recommendations data for the specified device",
"description": "Returns security configuration recommendations for the selected device (needs advanced hunting on device data)",
"baseDataSourceName": "httpRequestScoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Errors from streams that call Microsoft Graph. The common failures are licence and permission
// gaps rather than faults, so name what is missing instead of passing on the raw Graph error.
const error = (data && data.error) || {};
const apiMessage = typeof error.message === "string" ? error.message : "";

// Advanced hunting runs, but the tenant's licences don't include the table the query reads.
// Device tables (DeviceInfo, DeviceTvm...) need Defender for Endpoint Plan 2 or equivalent.
const missingTable = apiMessage.match(/Failed to resolve table or column expression named '([^']+)'/);

if (missingTable && missingTable[1].startsWith("Device")) {
// Name the table: a typo in a custom query fails the same way as a missing licence.
result =
"The advanced hunting table '" +
missingTable[1] +
"' isn't available. If the name is right, your Microsoft Defender licence doesn't include " +
"advanced hunting on device data, such as Microsoft Defender for Endpoint Plan 2. " +
"The Overview and Incidents dashboards work without it.";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
} else if (missingTable) {
result =
"The advanced hunting table '" +
missingTable[1] +
"' isn't available in this tenant. The tables you can query depend on your Microsoft Defender licences.";
} else if (response.status === 401) {
result =
"Microsoft Graph rejected the credentials (HTTP 401). Check the tenant ID, client ID and client secret, " +
"and that the secret has not expired.";
} else if (response.status === 403) {
// Graph names the missing role in its message, for example "API required roles: ThreatHunting.Read.All".
result =
"Microsoft Graph refused access to this data (HTTP 403). Grant the missing application permission " +
"in Microsoft Entra ID with admin consent, and check the tenant has a licence that includes this data." +
(apiMessage ? " " + apiMessage : "") +
// Always list the read-only roles: Secure Score's 403 names none, and Graph lists read and
// write roles side by side for alerts and incidents, where read-only is enough.
" Read-only permissions are enough: alerts need SecurityAlert.Read.All, incidents " +
"SecurityIncident.Read.All, Secure Score SecurityEvents.Read.All, and advanced hunting ThreatHunting.Read.All.";
} else {
result = apiMessage || "Microsoft Graph returned HTTP " + response.status;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "secureScoreHistory",
"displayName": "Secure Score History",
"description": "Retrieves the current tenant's Secure Score data from the past 90 days",
"description": "Returns the tenant's Secure Score over the past 90 days (needs SecurityEvents.Read.All)",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "offset",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"config": {
"httpMethod": "get",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"config": {
"httpMethod": "get",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"config": {
"httpMethod": "get",
"errorHandling": {
"type": "default"
"type": "script",
"script": "errorHandling/microsoftGraphApi.js"
},
"paging": {
"mode": "none"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,8 @@
"version": 29,
"columns": 4
},
"folderPath": ["Devices"],
"folderPath": ["Advanced Hunting"],
"variables": [
"{{variables.Device}}"
]
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"name": "Devices Health",
"schemaVersion": "1.5",
"folderPath": [
"Advanced Hunting"
],
"dashboard": {
"_type": "layout/grid",
"contents": [
Expand Down Expand Up @@ -484,9 +487,87 @@
}
}
}
},
{
"static": false,
"w": 4,
"moved": false,
"h": 2,
"x": 0,
"y": 5,
"i": "635e0de1-af29-4660-aa68-ddc5dd58e0c4",
"z": 0,
"config": {
"dataStream": {
"name": "advancedHuntingQuery",
"dataSourceConfig": {
"query": "DeviceInfo\n| summarize arg_max(Timestamp, *) by DeviceId\n| extend ExposureLevel = iff(isempty(ExposureLevel), \"No data\", ExposureLevel)\n| summarize Devices = count() by ExposureLevel"
},
"id": "{{dataStreams.advancedHuntingQuery}}",
"pluginConfigId": "{{configId}}"
},
"_type": "tile/data-stream",
"description": "Devices by their latest exposure level",
"activePluginConfigIds": [
"{{configId}}"
],
"title": "Device Exposure State",
"visualisation": {
"type": "data-stream-bar-chart",
"config": {
"data-stream-bar-chart": {
"color": {
"type": "custom",
"customColors": [
{
"color": "#88898C",
"expression": "label == \"No data\""
},
{
"color": "#259A51",
"expression": "label == \"None\""
},
{
"color": "#0075FF",
"expression": "label == \"Low\""
},
{
"color": "#E18700",
"expression": "label == \"Medium\""
},
{
"color": "#DE0038",
"expression": "label == \"High\""
}
]
},
"xAxisGroup": "none",
"showLegend": false,
"range": {
"type": "auto"
},
"showGrid": true,
"grouping": false,
"displayMode": "actual",
"xAxisData": "ExposureLevel",
"showTotals": false,
"yAxisLabel": "",
"horizontalLayout": "vertical",
"showValue": true,
"yAxisData": [
"Devices"
],
"showYAxisLabel": false,
"xAxisLabel": "",
"legendPosition": "bottom",
"showXAxisLabel": false
}
}
}
}
}
],
"version": 74,
"columns": 4
}
}
}
Loading
Loading