Collector teardown — Policy Assignments (the "who is this assigned to" collector)¶
"Which policies target which groups, and who's actually in those groups?" is one of the hardest questions to answer honestly in Intune — the data is scattered across four policy types, three targeting shapes, and two kinds of group. This collector gathers all of it read-only and, crucially, shapes it so an AI agent can answer without guessing from a fragment.
Script: scripts/Collect-PolicyAssignments.ps1.
Read-only, Managed-Identity. It emits a family of CSVs — full (with IDs) to root/ for Power
BI, slim (no IDs, size-gated) to agent-data/ for the index.
Beta endpoint. Policy lists use
/beta; group resolution uses/v1.0. Re-verify after Graph updates.
The four sweeps, and why they differ¶
Policy assignments don't come from one place. The collector drives a small config table so each policy type is retrieved the right way:
| Policy type | Endpoint | How assignments come back |
|---|---|---|
| Compliance | deviceCompliancePolicies?$expand=assignments |
inline (expanded) |
| Configuration profiles | deviceConfigurations?$expand=assignments |
inline (expanded) |
| Settings catalog | configurationPolicies?$expand=assignments |
inline (expanded) |
| Endpoint security | intents + intents/{id}/assignments |
a separate call per policy |
That last row is the gotcha: endpoint-security intents don't support $expand=assignments,
so their assignments need a per-policy follow-up call. Encoding the retrieval mode
(ExpandedOnly vs PerPolicy) in the table keeps the sweep loop uniform.
Each assignment's @odata.type is decoded into a target: All Devices, All Users, or a
specific Group, and into an intent — Include or Exclude (exclusion group types map to
Exclude). Every distinct group id is collected into a HashSet so it's resolved only once.
Groups: static vs dynamic, and the ones that vanished¶
Resolving a group is where the honesty lives.
- Static groups get their
transitiveMembersexpanded (users and devices), so you can answer "who is actually targeted." - Dynamic groups are not expanded — their membership is a rule, not a list. The
collector captures the
membershipRuleand its processing state instead. This is the honest move: enumerating a snapshot of dynamic members would imply a certainty the rule doesn't give you. The rule is the truth; the members are derived. - Deleted / unresolvable groups are detected and written to their own
DeletedGroupAssignments.csv. An assignment pointing at a group that no longer exists is a real hygiene problem — policies quietly targeting nothing — and surfacing it is exactly the kind of finding this pattern exists to make visible.
The key idea: one complete document per policy¶
This is the collector's most important output and the clearest RAG-design lesson in the repo.
A naive design indexes the flat PolicyAssignments.csv — one row per (policy, target). But an
AI agent retrieving over that data might pull three of a policy's five assignment rows and
confidently answer "this policy targets these three groups" — wrong, because it never saw the
other two. The fragment looked complete.
PolicyAssignments_Summary.csv fixes this by emitting one row per policy carrying the
complete include and exclude target lists (semicolon-joined, capped at a sane length). Now a
single retrieved document contains the whole answer for that policy. The agent can't assemble a
partial picture because the picture isn't split up.
The rule generalizes: if a correct answer requires all of an entity's rows, give the agent one document that already contains all of them. Pre-joining for completeness is as important as pre-aggregating for counts.
Keeping index documents inside the limits¶
Two mechanisms keep the agent-data/ files ingestible:
- Slim copies, no IDs. The agent-data versions drop GUIDs entirely and keep names — smaller files, and nothing the agent doesn't need to reason in plain language.
- Chunking + a manifest. Group membership can be huge. When it exceeds the row threshold,
it's split into
_Part001,_Part002, … files, each under the ~12 MB gate (the AI Search extraction ceiling is ~16 MB), and a_Manifest.csvrecords that it was chunked and how many rows total — so a consumer knows it's looking at parts of a whole, not the whole.
Permissions¶
Read-only, on the Managed Identity:
DeviceManagementConfiguration.Read.All— policies and endpoint-security intentsGroup.Read.All,Directory.Read.All— group metadata andtransitiveMembersStorage Blob Data Contributor— to write the CSVs
Every call is a GET. Nothing is written to the tenant.
What we still don't know (honest gaps)¶
- Dynamic membership is a rule, not a roster. "Who is in this dynamic group right now" isn't in the snapshot by design — answering it means evaluating the rule against current objects, which is a different (and heavier) operation.
- The summary truncates. Include/exclude target lists are capped (default 1,500 chars);
a policy targeting an enormous number of groups will show
...(truncated). Rare, but stated in the data rather than hidden. - Beta drift. The policy endpoints ride
/beta; a schema change could alter a type or an assignment shape.
Microsoft, Intune, Entra, Microsoft Graph, and Azure are trademarks of the Microsoft group of companies. Independent content; not endorsed by Microsoft.