Skip to content

Commit 77b56f5

Browse files
authored
Add files via upload
1 parent 73fe4ff commit 77b56f5

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<#
2+
Group-aware disabled-user license exporter.
3+
Run in the SAME PowerShell session where you already ran Connect-MgGraph.
4+
Something like: Connect-MgGraph -Scopes "User.Read.All","Directory.Read.All" -UseDeviceAuthentication
5+
works great for me.
6+
Usage: pwsh -NoProfile .\Get-DisabledUsers-Licenses-GroupAware.ps1 -ExportCsv .\disabled_licenses.csv
7+
#>
8+
9+
param(
10+
[string]$ExportCsv = ".\disabled_licenses.csv"
11+
)
12+
13+
function Info { param($m) Write-Host $m -ForegroundColor Cyan }
14+
function Warn { param($m) Write-Host $m -ForegroundColor Yellow }
15+
function Err { param($m) Write-Host $m -ForegroundColor Red }
16+
17+
# Require existing Graph context (reuse same session)
18+
if (-not (Get-MgContext -ErrorAction SilentlyContinue)) {
19+
Err "No Graph context found in this session. Run Connect-MgGraph -Scopes <scopes> first in this window."
20+
return
21+
}
22+
23+
Info "Building SKU maps (SkuId/AccountSkuId -> SkuPartNumber)..."
24+
$guidToPart = @{}
25+
$acctToPart = @{}
26+
try {
27+
$skus = Get-MgSubscribedSku -All -ErrorAction Stop
28+
foreach ($s in $skus) {
29+
if ($s.SkuId) { $guidToPart[$s.SkuId.Guid.ToString().Trim('{}')] = $s.SkuPartNumber }
30+
if ($s.AccountSkuId) { $acctToPart[$s.AccountSkuId] = $s.SkuPartNumber }
31+
if ($s.SkuPartNumber) {
32+
$guidToPart[$s.SkuPartNumber] = $s.SkuPartNumber
33+
$acctToPart[$s.SkuPartNumber] = $s.SkuPartNumber
34+
}
35+
}
36+
Info ("Subscribed SKUs loaded: {0}" -f $skus.Count)
37+
} catch {
38+
Warn "Could not enumerate subscribed SKUs: $($_.Exception.Message). Name resolution may be limited."
39+
}
40+
41+
Info "Enumerating disabled users..."
42+
$users = Get-MgUser -Filter "accountEnabled eq false" -Property "displayName,userPrincipalName,id" -All -ErrorAction Stop
43+
Info ("Total disabled users found: {0}" -f $users.Count)
44+
45+
$results = @()
46+
$counter = 0
47+
48+
foreach ($u in $users) {
49+
$counter++
50+
if ($counter % 200 -eq 0) { Info "Processed $counter users..." }
51+
52+
$licenseNames = @()
53+
$source = ""
54+
55+
# 1) Try licenseDetails (effective licenses)
56+
try {
57+
$ld = Get-MgUserLicenseDetail -UserId $u.Id -ErrorAction Stop
58+
} catch {
59+
$ld = @()
60+
}
61+
62+
if ($ld -and $ld.Count -gt 0) {
63+
$licenseNames = $ld | ForEach-Object { $_.SkuPartNumber } | Where-Object { $_ } | Select-Object -Unique
64+
$source = "licenseDetails"
65+
} else {
66+
# 2) Fallback: check group-based licensing
67+
# Get groups the user is a member of (transitive membership)
68+
try {
69+
$memberOf = Get-MgUserMemberOf -UserId $u.Id -All -ErrorAction Stop
70+
} catch {
71+
$memberOf = @()
72+
}
73+
74+
if ($memberOf -and $memberOf.Count -gt 0) {
75+
# For each group object, if it's a group, get group's assignedLicenses
76+
foreach ($m in $memberOf) {
77+
# memberOf returns directoryObject shapes; check objectType or @odata.type
78+
$isGroup = $false
79+
if ($m.AdditionalProperties.ContainsKey('@odata.type')) {
80+
$odata = $m.AdditionalProperties['@odata.type']
81+
if ($odata -like '*group*') { $isGroup = $true }
82+
}
83+
# fallback: check if object has 'displayName' and 'group' like properties
84+
if (-not $isGroup -and $m.PSObject.Properties.Match('DisplayName').Count -gt 0 -and $m.PSObject.Properties.Match('Id').Count -gt 0) {
85+
# treat as group candidate
86+
$isGroup = $true
87+
}
88+
89+
if ($isGroup) {
90+
$groupId = $m.Id
91+
if (-not $groupId) { continue }
92+
try {
93+
$g = Get-MgGroup -GroupId $groupId -Property "displayName,assignedLicenses" -ErrorAction Stop
94+
} catch {
95+
continue
96+
}
97+
$gAssigned = $null
98+
if ($g.AdditionalProperties.ContainsKey('assignedLicenses')) { $gAssigned = $g.AdditionalProperties['assignedLicenses'] }
99+
if ($gAssigned -and $gAssigned.Count -gt 0) {
100+
foreach ($gal in $gAssigned) {
101+
$resolved = $null
102+
if ($null -ne $gal.skuId) {
103+
$gid = $gal.skuId.ToString().Trim('{}')
104+
if ($guidToPart.ContainsKey($gid)) { $resolved = $guidToPart[$gid] }
105+
}
106+
if (-not $resolved -and $null -ne $gal.skuPartNumber) { $resolved = $gal.skuPartNumber }
107+
if (-not $resolved -and $null -ne $gal.accountSkuId) {
108+
if ($acctToPart.ContainsKey($gal.accountSkuId)) { $resolved = $acctToPart[$gal.accountSkuId] }
109+
else { $resolved = ($gal.accountSkuId -split ':')[-1] }
110+
}
111+
if (-not $resolved) { $resolved = ($gal | ConvertTo-Json -Compress) }
112+
if ($resolved) { $licenseNames += $resolved }
113+
}
114+
}
115+
}
116+
} # end foreach memberOf
117+
118+
if ($licenseNames.Count -gt 0) {
119+
$licenseNames = $licenseNames | Select-Object -Unique
120+
$source = "groupAssigned (memberOf)"
121+
}
122+
} # end if memberOf
123+
} # end fallback
124+
125+
if ($licenseNames.Count -gt 0) {
126+
$results += [PSCustomObject]@{
127+
DisplayName = $u.DisplayName
128+
UserPrincipalName = $u.UserPrincipalName
129+
LicenseNames = ,$licenseNames
130+
Source = $source
131+
}
132+
}
133+
}
134+
135+
Info ("Total disabled users with discovered licenses: {0}" -f $results.Count)
136+
137+
if ($results.Count -eq 0) {
138+
Warn "No licenses discovered via licenseDetails or group assignedLicenses. If the original script still shows users, run it now and capture one UPN it reports; then re-run this script for that UPN specifically so we can compare."
139+
return
140+
}
141+
142+
# Build columns License1..N
143+
$max = ($results | ForEach-Object { $_.LicenseNames.Count } | Measure-Object -Maximum).Maximum
144+
if (-not $max) { $max = 0 }
145+
146+
$output = foreach ($r in $results) {
147+
$props = @{
148+
DisplayName = $r.DisplayName
149+
UserPrincipalName = $r.UserPrincipalName
150+
Source = $r.Source
151+
}
152+
for ($i = 0; $i -lt $max; $i++) {
153+
$col = "License$([int]($i+1))"
154+
$props[$col] = if ($i -lt $r.LicenseNames.Count) { $r.LicenseNames[$i] } else { "" }
155+
}
156+
[PSCustomObject]$props
157+
}
158+
159+
# Print sample and export CSV
160+
$output | Select-Object -First 20 | Format-Table -AutoSize
161+
162+
if ($ExportCsv) {
163+
try {
164+
$output | Export-Csv -Path $ExportCsv -NoTypeInformation -Force
165+
Info "`nExported results to $ExportCsv"
166+
} catch {
167+
Warn "Failed to export CSV: $($_.Exception.Message)"
168+
}
169+
}
170+
171+
Info "`nDone."

0 commit comments

Comments
 (0)