-
Notifications
You must be signed in to change notification settings - Fork 574
/
Find-UnderusedCopilotLicenses.PS1
263 lines (243 loc) · 12.9 KB
/
Find-UnderusedCopilotLicenses.PS1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# Find-UnderusedCopilotLicenses.PS1
# a script to check users with Microsfot 365 Copilot licenses who might not be using the features as they should
# V1.0 5-Nov-2024
# GitHub link:
# Connect to Microsoft Graph
If (!(Get-MgContext).Account) {
Write-Host "Connecting to Microsoft Graph..."
Connect-MgGraph -NoWelcome -Scopes Reports.Read.All, ReportSettings.ReadWrite.All, User.Read.All
}
# Define the score that marks a user as underusing Microsoft 365 Copilot
[double]$MicrosoftCopilotScore = 30
# Sku Id for the Microsoft 365 Copilot license
[guid]$CopilotSKUId = "639dec6b-bb19-468b-871c-c5c441c4b0cb"
Write-Host "Scanning for user accounts with Microsoft 365 Copilot licenses..."
[array]$Users = Get-MgUser -Filter "usertype eq 'Member' and assignedLicenses/any(s:s/skuId eq $CopilotSkuId)" `
-ConsistencyLevel Eventual -CountVariable Licenses -All -Sort 'displayName' `
-Property Id, displayName, signInActivity, userPrincipalName -PageSize 999
If (!$Users) {
Write-Host "No users with Microsoft 365 Copilot licenses found"
Break
} Else {
Write-Host ("{0} users with Microsoft 365 Copilot licenses found" -f $Users.Count)
}
# Make sure that we can fetch usage data that isn't obfuscated
If ((Get-MgAdminReportSetting).DisplayConcealedNames -eq $true) {
$Parameters = @{ displayConcealedNames = $false }
Update-MgAdminReportSetting -BodyParameter $Parameters
$ConcealedNames = $false
}
# Fetch usage data for Copilot
Write-Host "Fetching Microsoft 365 Copilot usage data..."
$Uri = "https://graph.microsoft.com/beta/reports/getMicrosoft365CopilotUsageUserDetail(period='D90')"
[array]$UsageData = Invoke-GraphRequest -Uri $Uri -Method Get | Select-Object -ExpandProperty Value
If (!($UsageData)) {
Write-Host "No usage data found for Microsoft 365 Copilot"
Break
}
$CopilotReport = [System.Collections.Generic.List[Object]]::new()
ForEach ($User in $Users) {
$LastSignIn = $null; $ScoreApps = 7
[array]$UserData = $UsageData | Where-Object {$_.UserPrincipalName -eq $User.UserPrincipalName}
If (!($UserData)) {
# can't assess a user if we don't have usage data
Write-Host ("No Microsoft 365 Copilot usage data found for {0}" -f $User.DisplayName)
Continue
}
If ($User.SignInActivity.LastSuccessfulSignInDateTime) {
$LastSignIn = $User.SignInActivity.LastSuccessfulSignInDateTime
} Else {
$LastSignIn = $User.SignInactivity.LastSignInDateTime
}
If ($null -eq $LastSignIn) {
$LastSignIn = "Never"
$DaysSinceSignIn = "N/A"
} Else {
# Is it more than 30 days since a sign-in?
$LastSignIn = Get-Date $LastSignIn -format 'dd-MMM-yyyy HH:mm:ss'
$DaysSinceSignIn = (New-TimeSpan ($LastSignIn)).Days
}
# Check dates of use for the various Copilot features
# OneNote
If (-not ([string]::IsNullOrEmpty($UserData.oneNoteCopilotLastActivityDate))) {
$OneNoteDate = Get-Date $UserData.oneNoteCopilotLastActivityDate -format 'dd-MMM-yyyy'
$OneNoteDays = (New-TimeSpan $OneNoteDate).Days
} Else {
$OneNoteDate = 'Not used'
$OneNoteDays = 0
$ScoreApps = $ScoreApps -1
}
#Teams
If (-not ([string]::IsNullOrEmpty($UserData.microsoftTeamsCopilotLastActivityDate))) {
$TeamsDate = Get-Date $UserData.microsoftTeamsCopilotLastActivityDate -format 'dd-MMM-yyyy'
$TeamsDays = (New-TimeSpan $TeamsDate).Days
} Else {
$TeamsDate = 'Not used'
$TeamsDays = 0
$ScoreApps = $ScoreApps -1
}
#Outlook
If (-not ([string]::IsNullOrEmpty($UserData.outlookCopilotLastActivityDate))) {
$OutlookDate = Get-Date $UserData.outlookCopilotLastActivityDate -format 'dd-MMM-yyyy'
$OutlookDays = (New-TimeSpan $OutlookDate).Days
} Else {
$OutlookDate = 'Not used'
$OutlookDays = 0
$ScoreApps = $ScoreApps -1
}
# Word
If (-not ([string]::IsNullOrEmpty($UserData.wordCopilotLastActivityDate))) {
$WordDate = Get-Date $UserData.wordCopilotLastActivityDate -format 'dd-MMM-yyyy'
$WordDays = (New-TimeSpan $WordDate).Days
} Else {
$WordDate = 'Not used'
$WordDays = 0
$ScoreApps = $ScoreApps -1
}
# Microsoft 365 Chat
If (-not ([string]::IsNullOrEmpty($UserData.copilotChatLastActivityDate))) {
$ChatDate = Get-Date $UserData.copilotChatLastActivityDate -format 'dd-MMM-yyyy'
$ChatDays = (New-TimeSpan $ChatDate).Days
} Else {
$ChatDate = 'Not used'
$ChatDays = 0
$ScoreApps = $ScoreApps -1
}
# Excel
If (-not ([string]::IsNullOrEmpty($UserData.excelCopilotLastActivityDate))) {
$ExcelDate = Get-Date $UserData.excelCopilotLastActivityDate -format 'dd-MMM-yyyy'
$ExcelDays = (New-TimeSpan $ExcelDate).Days
} Else {
$ExcelDate = 'Not used'
$ExcelDays = 0
$ScoreApps = $ScoreApps -1
}
# PowerPoint
If (-not ([string]::IsNullOrEmpty($UserData.powerPointCopilotLastActivityDate))) {
$PowerPointDate = Get-Date $UserData.powerPointCopilotLastActivityDate -format 'dd-MMM-yyyy'
$PowerPointDays = (New-TimeSpan $PowerPointDate).Days
} Else {
$PowerPointDate = 'Not used'
$PowerPointDays = 0
$ScoreApps = $ScoreApps -1
}
# Compute a score for the user
$Score = $OutlookDays + $TeamsDays + $OneNoteDays + $ExcelDays + $WordDays + $ChatDays + $PowerPointDays
[double]$UserScore = ($Score / $ScoreApps)
$ReportLine = [PSCustomObject][Ordered]@{
UserPrincipalName = $User.UserPrincipalName
User = $User.DisplayName
'Last sign in' = $LastSignIn
'Days since sign in' = $DaysSinceSignIn
'Copilot data from' = Get-Date $UserData.reportRefreshDate -format 'dd-MMM-yyyy'
'Copilot in Teams' = $TeamsDate
'Days since Teams' = $TeamsDays
'Copilot in Outlook' = $OutlookDate
'Days since Outlook' = $OutlookDays
'Copilot in Word' = $WordDate
'Days since Word' = $WordDays
'Copilot in Chat' = $ChatDate
'Days since Chat' = $ChatDays
'Copilot in Excel' = $ExcelDate
'Days since Excel' = $ExcelDays
'Copilot in PowerPoint' = $PowerPointDate
'Days since PowerPoint' = $PowerPointDays
'Copilot in OneNote' = $OneNoteDate
'Days since OneNote' = $OneNoteDays
'Number active apps' = $ScoreApps
'Overall Score' = $UserScore
}
$CopilotReport.Add($ReportLine)
}
# Extract the set of users who should be considered as underusing Copilot
[array]$UnderusedCopilot = $CopilotReport | Where-Object {$_.'Overall Score' -gt $MicrosoftCopilotScore}
# If there are no underused Copilot users, say so - and if we have, give the administrator the chance to remove the licenses
If (!($UnderusedCopilot)) {
Write-Host "No users found to be underusing an assigned Microsoft 365 Copilot license"
} Else {
$LicenseReport = [System.Collections.Generic.List[Object]]::new()
Write-Host ("The folllowing {0} users are underusing their assigned Microsoft 365 Copilot license" -f $UnderusedCopilot.Count)
$UnderusedCopilot | Sort-Object {$_.'Overall Score' -as [double]} | Select-Object User, UserPrincipalName, 'Number active apps', 'Overall Score' | Format-Table -AutoSize
[string]$Decision = Read-Host "Do you want to remove the licenses from these users"
If ($Decision.Substring(0,1).toUpper() -eq "Y") {
ForEach ($User in $UnderusedCopilot) {
# Check that the user still has a Copilot license...
$UserLicenseData = $User = Get-MgUser -Userid $User.UserPrincipalName -Property Id, displayName, userPrincipalName, assignedLicenses, licenseAssignmentStates
If ($CopilotSKUId -notin $UserLicenseData.assignedLicenses.skuId) {
Write-Host ("The {0} account does not have a Microsoft 365 Copilot license" -f $UserLicenseData.displayName)
Continue
}
# Direct assigned license or group-assigned license?
[array]$CopilotLicense = $User.LicenseAssignmentStates | Where-Object {$_.skuId -eq $CopilotSkuId}
If ($null -eq $CopilotLicense[0].assignedByGroup) {
# Process the removal of a direct-assigned license
Try {
Write-Host ("Removing direct-assigned Microsoft 365 Copilot license from {0}" -f $UserLicenseData.displayName) -ForegroundColor Yellow
Set-MgUserLicense -UserId $UserLicenseData.Id -AddLicenses @{} -RemoveLicenses @($CopilotSKUId) -ErrorAction Stop | Out-Null
$LicenseReportLine = = [PSCustomObject][Ordered]@{
UserPrincipalName = $UserLicenseData.UserPrincipalName
User = $UserLicenseData.displayName
Action = "Removed direct assigned Copilot license"
SkuId = $CopilotSKUId
Timestamp = Get-Date -format s
}
$LicenseReport.Add($LicenseReportLine)
} Catch {
Write-Host ("Failed to remove Microsoft 365 Copilot license from {0}: {1}" -f $UserLicenseData.displayName, $_.Exception.Message) -ForegroundColor Red
}
} Else {
# Process the removal of a group-assigned license
Write-Host ("Removing group-assigned Microsoft 365 Copilot license from {0}" -f $UserLicenseData.displayName) -ForegroundColor Yellow
$GroupId = $CopilotLicense[0].assignedByGroup
Try {
Remove-MgGroupMemberDirectoryObjectByRef -DirectoryObjectId $UserLicenseData.Id -GroupId $GroupId -ErrorAction Stop
$LicenseReportLine = [PSCustomObject][Ordered]@{
UserPrincipalName = $UserLicenseData.UserPrincipalName
User = $UserLicenseData.displayName
Action = ("Removed group assigned Copilot license from {0}" -f $GroupId)
SkuId = $CopilotSKUId
Timestamp = Get-Date -format s
}
$LicenseReport.Add($LicenseReportLine)
} Catch {
Write-Host ("Failed to remove Microsoft 365 Copilot license for {0} from group {1}: {2}" -f $UserLicenseData.displayName, $GroupId, $_.Exception.Message) -ForegroundColor Red
}
}
}
Write-Host ("{0} licenses removed" -f $LicemseReport.Count)
} Else {
Write-Host "No licenses removed"
}
}
If ($LicenseReport) {
Write-Host ""
Write-Host "License removal report"
$LicenseReport | Select-Object Timestamp, User, UserPrincipalName, Action | Sort-Object Timestamp | Format-Table -AutoSize
}
Write-Host "Generating report..."
If (Get-Module ImportExcel -ListAvailable) {
$ExcelGenerated = $True
Import-Module ImportExcel -ErrorAction SilentlyContinue
$ExcelOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\Underused Copilot Licenses.xlsx"
If (Test-Path $ExcelOutputFile) {
Remove-Item $ExcelOutputFile -ErrorAction SilentlyContinue
}
$UnderusedCopilot | Export-Excel -Path $ExcelOutputFile -WorksheetName "Copilot License Report" -Title ("Underused Copilot License Report {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "UnderusedCopilot"
} Else {
$CSVOutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\Underused Copilot License.CSV"
$UnderusedCopilot | Export-Csv -Path $CSVOutputFile -NoTypeInformation -Encoding Utf8
}
If ($ExcelGenerated) {
Write-Host ("An Excel report of underused Microsoft 365 Copilot licenses is available in {0}" -f $ExcelOutputFile)
} Else {
Write-Host ("A CSV report of underused Microsoft 365 Copilot licenses is available in {0}" -f $CSVOutputFile)
}
# Reset tenant obfuscation settings to True if we switched the setting earlier
If ((Get-MgAdminReportSetting).DisplayConcealedNames -eq $false -and $ConcealedNames) {
$Parameters = @{ displayConcealedNames = $True }
Update-MgAdminReportSetting -BodyParameter $Parameters
}
# An example script used to illustrate a concept. More information about the topic can be found in the Office 365 for IT Pros eBook https://gum.co/O365IT/
# and/or a relevant article on https://office365itpros.com or https://www.practical365.com. See our post about the Office 365 for IT Pros repository # https://office365itpros.com/office-365-github-repository/ for information about the scripts we write.
# Do not use our scripts in production until you are satisfied that the code meets the need of your organization. Never run any code downloaded from the Internet without
# first validating the code in a non-production environment.