New Defender Plugin#55
Conversation
| { | ||
| "name": "microsoft-defender", | ||
| "displayName": "Microsoft Defender for Endpoint", | ||
| "version": "1.0.0", | ||
| "author": { | ||
| "name": "SquaredUp Labs", | ||
| "type": "community" | ||
| }, |
There was a problem hiding this comment.
🔴 Missing docs/README.md and links array in metadata.json. The PR checklist explicitly leaves '[ ] README added including configuration guidance' unchecked, and the README is shown in-product during plugin configuration — without it users have no setup guidance for the OAuth Client Credentials flow (Entra app registration, required Graph API permissions like SecurityAlert.Read.All, ThreatHunting.Read.All, etc.). Also, per repo guidelines, metadata.json should typically include a links array with a source link to the GitHub repo and a documentation link to the README — all other plugins (Phare, UniFi, Rootly, DigiCert, GoogleSheets, UptimeRobot, …) follow this convention.
Extended reasoning...
What is missing
This new community plugin for Microsoft Defender for Endpoint ships without two pieces of metadata that the repo guidelines require:
-
No
docs/README.md— theplugins/MicrosoftDefender/v1/tree has nodocs/folder at all. The PR description itself confirms this in the checklist:- [ ] README added including configuration guidance -
No
linksarray inmetadata.json— the newmetadata.jsonhasname,displayName,description,base, etc., but no top-levellinksfield. The repository review guidelines say:links— Should typically contain two links, one withcategory: sourcelinking to the GitHub repository, and another withcategory: documentationlinking to the markdown documentation in the repository.
Why this matters
The README is rendered in-product during plugin configuration, so its absence is a real UX gap for first-time users — not just a docs nicety. The Microsoft Defender plugin uses OAuth 2.0 Client Credentials against the Microsoft Graph API (see metadata.json lines 13–41), which means the user has to perform a non-trivial setup before the plugin will work:
- Register an application in Microsoft Entra ID (Azure AD).
- Generate a client secret.
- Grant application (not delegated) Graph API permissions — at minimum the ones implied by the data streams in this PR:
SecurityAlert.Read.All(foralerts.json)SecurityIncident.Read.All(forincidents.json)ThreatHunting.Read.All(forrunHuntingQueryused byadvancedHuntingQuery,Vulnerabilities,devices,recommendations,listDevices)SecurityEvents.Read.All/SecureScore.Read.All(forsecureScoreHistory.json)
- Grant admin consent on those permissions.
- Note the tenant ID, client ID, and client secret to enter into
ui.json's three required fields.
Without a README, none of this is discoverable from inside SquaredUp; the validation step (configValidation.json) will simply return "Cannot access the Endpoint API - check your client ID & secret. Do you have the correct permissions set?" with no further guidance.
Step-by-step proof
ls plugins/MicrosoftDefender/v1/→ nodocs/directory; noREADME.mdanywhere in the plugin.jq '.links' plugins/MicrosoftDefender/v1/metadata.json→null(the key does not exist).- Compare to e.g.
plugins/Phare/v1/metadata.json, which has"links": [ { "category": "source", "url": "…" }, { "category": "documentation", "url": "…" } ]. The same is true of UniFi, Rootly, DigiCert, GoogleSheets, UptimeRobot, AutoTask, DattoRMM, FantasyPremierLeague, Postcoder — i.e. essentially every other plugin in this repo. - The PR description checklist explicitly leaves the README item unchecked, confirming the author is aware this is outstanding work.
How to fix
-
Add
plugins/MicrosoftDefender/v1/docs/README.mdwith at least: overview, prerequisites (Entra app registration walkthrough), required Graph API application permissions, how to fill in the three UI fields (tenant ID, client ID, client secret), and a short description of each dashboard/data stream. -
Add a
linksarray tometadata.json, e.g.:"links": [ { "category": "source", "label": "GitHub", "url": "https://github.com/squaredup/community-plugins/tree/main/plugins/MicrosoftDefender/v1" }, { "category": "documentation", "label": "Documentation", "url": "https://github.com/squaredup/community-plugins/blob/main/plugins/MicrosoftDefender/v1/docs/README.md" } ]
-
Tick the README checkbox in the PR description once added.
| "name": "DeviceName", | ||
| "type": { | ||
| "value": "Defender Device" |
There was a problem hiding this comment.
🔴 The sourceType is set to "Defender Device" in indexDefinitions/default.json and custom_types.json, which prefixes it with the plugin name. Per the repository naming guidelines, source types should be named after the upstream product/API (e.g. device or machine — matching Microsoft Graph / Defender terminology) — the guideline literally calls out NinjaOne Device as the anti-pattern to avoid, and Defender Device is structurally identical. Move the friendly display name Defender Device into custom_types.json's name field and use the upstream term for sourceType; this also requires updating defaultContent/scopes.json and the matches clauses in devices.json, recommendations.json, and Vulnerabilities.json (plus the gremlin query in cockpit.dash.json and recommendations.dash.json).
Extended reasoning...
What the bug is
The SquaredUp plugin naming guidelines explicitly state:
Name source types after how they are referred to in the upstream product or API (e.g. agent, device). Do not prefix them with the plugin name (e.g. avoid NinjaOne Device). A separate friendly display name can be configured if needed (via custom_types.json).
This PR sets the sourceType to "Defender Device" in two places — exactly the <plugin-name> Device anti-pattern the guideline calls out.
Where it manifests in the code
plugins/MicrosoftDefender/v1/indexDefinitions/default.json(L11–13) —objectMapping.type.valueis set to"Defender Device", which stamps every imported device with thatsourceType.plugins/MicrosoftDefender/v1/custom_types.json— bothnameandsourceTypeare"Defender Device", whereasnameshould be the friendly display label andsourceTypeshould be the API-native key.
The mismatch propagates to several other files that filter on the type:
defaultContent/scopes.json— the "Defender Devices" scope matchessourceTypeoneOf: ["Defender Device"].dataStreams/devices.json,dataStreams/recommendations.json,dataStreams/Vulnerabilities.json— each has amatchesclause keyed offsourceType == "Defender Device".defaultContent/cockpit.dash.jsonanddefaultContent/recommendations.dash.json— the gremlin scope queries contain__.has("sourceType", "Defender Device").
Why existing code doesn't prevent it
sourceType is a free-form string identifier — there's no schema-level validator that rejects plugin-name prefixes. The validation step in configValidation.json only checks API connectivity, not naming conventions. Nothing in the harness catches this; it's purely a convention defined in the contributor guidelines.
Impact
The string "Defender Device" becomes part of the durable identity of every imported object. Once a customer installs this plugin and ingests devices, that sourceType is baked into the data graph, dashboards, scopes, and any user-built content. Changing it after release is a breaking change that orphans existing scopes, breaks user dashboards filtered by that sourceType, and forces a re-import of every device. Fixing it now — before release — is virtually free; fixing it later is painful or impractical, which is why this convention is enforced pre-merge.
How to fix
Use the upstream Microsoft Graph / Defender term — device (or machine, depending on which API surface is canonical for the contributor) — as the sourceType, and keep Defender Device as the friendly display name only in custom_types.json:
[
{
"name": "Defender Device",
"sourceType": "device",
"icon": "server",
"singular": "Device",
"plural": "Devices"
}
]Then update every other reference to the same key:
indexDefinitions/default.json:type.value→"device"defaultContent/scopes.json:oneOf: ["device"]dataStreams/devices.json,recommendations.json,Vulnerabilities.json:matches.sourceType.value→"device"dataStreams/devices.jsonandVulnerabilities.jsonmetadata:sourceType→"device"cockpit.dash.jsonandrecommendations.dash.json:__.has("sourceType", "device")
Step-by-step proof
- Open
plugins/MicrosoftDefender/v1/indexDefinitions/default.json. Line 11–13:Every device imported gets"type": { "value": "Defender Device" }
sourceType = "Defender Device". - Open
plugins/MicrosoftDefender/v1/custom_types.json. ThesourceTypefield is also"Defender Device"— so the type registration matches what the index writes. Both halves carry the plugin-prefixed string. - Compare with the guideline example: the guideline says "avoid
NinjaOne Device". Substitute plugin name →Defender Deviceis the same anti-pattern, byte for byte by shape. - The Microsoft Graph / Defender API itself refers to these as
device(e.g./security/devices, theDeviceInfo/DeviceIdcolumns in advanced hunting) andmachinein the legacy Defender for Endpoint API. Either of those — notDefender Device— is the upstream-native term the guideline asks for. - The friendly display name
Defender Deviceis exactly whatcustom_types.json'sname,singular,pluralfields are designed to surface — so renamingsourceTypetodevicekeeps the UI label identical for end users.
| "name": "SquaredUp Labs", | ||
| "type": "community" | ||
| }, | ||
| "description": "Visualize Defender insights including advanced hunting, exposure, devices, and vulnerabilities.", |
There was a problem hiding this comment.
🟡 The category field in metadata.json is set to "User Defined", the generic fallback shown when no real category is selected. Repository guidelines state: "category - Mandatory. Reuse an existing category from other plugins where possible." Since this is a Microsoft Defender for Endpoint (EDR) plugin, "Security" would be a much more natural fit — and it is already an established category used by the Phare plugin.
Extended reasoning...
What the bug is\n\nIn plugins/MicrosoftDefender/v1/metadata.json line 9, the category field is set to:\n\njson\n"category": "User Defined"\n\n\n"User Defined" is effectively the no-category fallback — the placeholder shown when an author has not selected a real category. For a plugin whose entire purpose is to surface security/EDR data (alerts, incidents, vulnerabilities, recommendations, secure score, antivirus detections, exploit guard events), this is a missed categorization.\n\n### Why it matters\n\nThe repository's plugin-authoring guidelines explicitly state:\n\n> category – Mandatory. Reuse an existing category from other plugins where possible.\n\nA quick sweep of the existing plugins shows the categories already in use across plugins/*/v*/metadata.json include Security, Monitoring, Utility, Network, Database, Fun, etc. Security is already established (used by Phare) and is the obvious fit for a Microsoft Defender for Endpoint plugin. Only one other plugin in the repo currently uses "User Defined", so this is genuinely an outlier.\n\n### Impact\n\nThe plugin still functions correctly — this is not a runtime bug. The impact is on discoverability and consistency: when users browse plugins in-product by category, a Defender plugin filed under "User Defined" will not show up alongside other security tooling, and the catalog ends up with the generic fallback category accumulating mismatched plugins.\n\n### How to fix\n\nChange line 9 of plugins/MicrosoftDefender/v1/metadata.json from:\n\njson\n"category": "User Defined",\n\n\nto:\n\njson\n"category": "Security",\n\n\n("Monitoring" would also be defensible given the dashboards and secure-score-history aspects, but "Security" is the more precise fit for an EDR/Defender plugin.)\n\n### Step-by-step proof\n\n1. Open plugins/MicrosoftDefender/v1/metadata.json — line 9 reads "category": "User Defined".\n2. The repo's contribution guidelines require reusing an existing category.\n3. grep across plugins/*/v*/metadata.json confirms "Security" is already an established category (used by Phare), and "Monitoring" is used by multiple plugins.\n4. The plugin's PR description states its audience is "IT Administrators / Security Teams & Specialists" and its purpose is "access security (defender for endpoint) based data" — confirming "Security" is the natural category.\n5. Therefore the current value violates the guideline by selecting the generic fallback when a fitting existing category exists.
🧩 Plugin PR Summary📦 Modified Plugins
📋 Results
🔍 Validation Details✅
|
🔌 Plugin overview
🖼️ Plugin screenshots
Plugin configuration
Default dashboards
🧪 Testing
We originally developed this plugin for the Microsoft Defender for Endpoint API. However we found that a lot of data streams we wanted to include had been moved to the Security portion of the Microsoft Graph API. Upon weighing up both API's, we decided to go with the latter.
I have tried to clone the dashboards from the old plugin as best as I could. Some data streams (e.g Recommendations, Vulnerabilities), do not have direct endpoints in the graph API so I have fectched similar data using Advanced Hunting. There are of course some missing pieces:
📚 Checklist