23
23
" Get-AzureStorageContainerAcl" = " Get-AzureStorageContainer" ;
24
24
" Start-CopyAzureStorageBlob" = " Start-AzureStorageBlobCopy" ;
25
25
" Stop-CopyAzureStorageBlob" = " Stop-AzureStorageBlobCopy" ;
26
- }.GetEnumerator() | Select @ {Name = ' Name' ; Expression = {$_.Key }}, @ {Name = ' Value' ; Expression = {$_.Value }} | New-Alias - Description " AzureAlias"
26
+ }.GetEnumerator() | Select @ {Name = ' Name' ; Expression = {$_.Key }}, @ {Name = ' Value' ; Expression = {$_.Value }} | New-Alias - Description " AzureAlias"
27
+
28
+
29
+ # Authorization script commandlet that builds on top of existing Insights comandlets.
30
+ # This commandlet gets all events for the "Microsoft.Authorization" resource provider by calling the "Get-AzureResourceProviderLog" commandlet
31
+
32
+ function Get-AzureAuthorizationChangeLog {
33
+ [CmdletBinding ()]
34
+ param (
35
+ [parameter (Mandatory = $false , ValueFromPipelineByPropertyName = $true , HelpMessage = " The start time. Optional
36
+ If both StartTime and EndTime are not provided, defaults to querying for the past 1 hour. Maximum allowed difference in StartTime and EndTime is 15 days" )]
37
+ [DateTime ] $StartTime ,
38
+
39
+ [parameter (Mandatory = $false , ValueFromPipelineByPropertyName = $true , HelpMessage = " The end time. Optional.
40
+ If both StartTime and EndTime are not provided, defaults to querying for the past 1 hour. Maximum allowed difference in StartTime and EndTime is 15 days" )]
41
+ [DateTime ] $EndTime
42
+ )
43
+ PROCESS {
44
+ # Get all events for the "Microsoft.Authorization" provider by calling the Insights commandlet
45
+ $events = Get-AzureResourceProviderLog - ResourceProvider " Microsoft.Authorization" - DetailedOutput - StartTime $StartTime - EndTime $EndTime
46
+
47
+ $startEvents = @ {}
48
+ $endEvents = @ {}
49
+ $offlineEvents = @ ()
50
+
51
+ # StartEvents and EndEvents will contain matching pairs of logs for when role assignments (and definitions) were created or deleted.
52
+ # i.e. A PUT on roleassignments will have a Start-End event combination and a DELETE on roleassignments will have another Start-End event combination
53
+ $startEvents = $events | ? { $_.httpRequest -and $_.Status -ieq " Started" }
54
+ $events | ? { $_.httpRequest -and $_.Status -ne " Started" } | % { $endEvents [$_.OperationId ] = $_ }
55
+ # This filters non-RBAC events like classic administrator write or delete
56
+ $events | ? { $_.httpRequest -eq $null } | % { $offlineEvents += $_ }
57
+
58
+ $output = @ ()
59
+
60
+ # Get all role definitions once from the service and cache to use for all 'startevents'
61
+ $azureRoleDefinitionCache = @ {}
62
+ Get-AzureRoleDefinition | % { $azureRoleDefinitionCache [$_.Id ] = $_ }
63
+
64
+ $principalDetailsCache = @ {}
65
+
66
+ # Process StartEvents
67
+ # Find matching EndEvents that succeeded and relating to role assignments only
68
+ $startEvents | ? { $endEvents.ContainsKey ($_.OperationId ) `
69
+ -and $endEvents [$_.OperationId ] -ne $null `
70
+ -and $endevents [$_.OperationId ].OperationName.StartsWith(" Microsoft.Authorization/roleAssignments" , [System.StringComparison ]::OrdinalIgnoreCase) `
71
+ -and $endEvents [$_.OperationId ].Status -ieq " Succeeded" } | % {
72
+
73
+ $endEvent = $endEvents [$_.OperationId ];
74
+
75
+ # Create the output structure
76
+ $out = " " | select Timestamp, Caller, Action, PrincipalId, PrincipalName, PrincipalType, Scope, ScopeName, ScopeType, RoleDefinitionId, RoleName
77
+ $out.Timestamp = $endEvent.EventTimestamp
78
+ $out.Caller = $_.Caller
79
+ if ($_.HttpRequest.Method -ieq " PUT" ) {
80
+ $out.Action = " Granted"
81
+ if ($_.Properties.Content.ContainsKey (" requestbody" )) {
82
+ $messageBody = ConvertFrom-Json $_.Properties.Content [" requestbody" ]
83
+ }
84
+
85
+ $out.Scope = $_.Authorization.Scope
86
+ }
87
+ elseif ($_.HttpRequest.Method -ieq " DELETE" ) {
88
+ $out.Action = " Revoked"
89
+ if ($endEvent.Properties.Content.ContainsKey (" responseBody" )) {
90
+ $messageBody = ConvertFrom-Json $endEvent.Properties.Content [" responseBody" ]
91
+ }
92
+ }
93
+
94
+ if ($messageBody ) {
95
+
96
+ $out.PrincipalId = $messageBody.properties.principalId
97
+ if ($out.PrincipalId -ne $null ) {
98
+ $principalDetails = Get-PrincipalDetails $out.PrincipalId ([REF ]$principalDetailsCache )
99
+ $out.PrincipalName = $principalDetails.Name
100
+ $out.PrincipalType = $principalDetails.Type
101
+ }
102
+
103
+ if ([string ]::IsNullOrEmpty($out.Scope )) { $out.Scope = $messageBody.properties.Scope }
104
+ if ($out.Scope -ne $null ) {
105
+ $resourceDetails = Get-ResourceDetails $out.Scope
106
+ $out.ScopeName = $resourceDetails.Name
107
+ $out.ScopeType = $resourceDetails.Type
108
+ }
109
+
110
+ $out.RoleDefinitionId = $messageBody.properties.roleDefinitionId
111
+ if ($out.RoleDefinitionId -ne $null ) {
112
+ if ($azureRoleDefinitionCache [$out.RoleDefinitionId ]) {
113
+ $out.RoleName = $azureRoleDefinitionCache [$out.RoleDefinitionId ].Name
114
+ } else {
115
+ $out.RoleName = " "
116
+ }
117
+ }
118
+ }
119
+ $output += $out
120
+ } # start event processing complete
121
+
122
+ # Filter classic admins events
123
+ $offlineEvents | % {
124
+ if ($_.Status -ne $null -and $_.Status -ieq " Succeeded" -and $_.OperationName -ne $null -and $_.operationName.StartsWith (" Microsoft.Authorization/ClassicAdministrators" , [System.StringComparison ]::OrdinalIgnoreCase)) {
125
+
126
+ $out = " " | select Timestamp, Caller, Action, PrincipalId, PrincipalName, PrincipalType, Scope, ScopeName, ScopeType, RoleDefinitionId, RoleName
127
+ $out.Timestamp = $_.EventTimestamp
128
+ $out.Caller = " Subscription Admin"
129
+
130
+ if ($_.operationName -ieq " Microsoft.Authorization/ClassicAdministrators/write" ){
131
+ $out.Action = " Granted"
132
+ }
133
+ elseif ($_.operationName -ieq " Microsoft.Authorization/ClassicAdministrators/delete" ){
134
+ $out.Action = " Revoked"
135
+ }
136
+
137
+ $out.RoleDefinitionId = $null
138
+ $out.PrincipalId = $null
139
+ $out.PrincipalType = " User"
140
+ $out.Scope = " /subscriptions/" + $_.SubscriptionId
141
+ $out.ScopeType = " Subscription"
142
+ $out.ScopeName = $_.SubscriptionId
143
+
144
+ if ($_.Properties -ne $null ){
145
+ $out.PrincipalName = $_.Properties.Content [" adminEmail" ]
146
+ $out.RoleName = " Classic " + $_.Properties.Content [" adminType" ]
147
+ }
148
+
149
+ $output += $out
150
+ }
151
+ } # end offline events
152
+
153
+ $output | Sort Timestamp
154
+ }
155
+ } # End commandlet
156
+
157
+ # Helper functions
158
+ # Resolve a principal. If the principal's object id was encountered in the principals resolved so far, return principalDetails from the cache.
159
+ # Else make a Grpah call and add that principal to cache of known principals
160
+ function Get-PrincipalDetails ($principalId , [REF ]$principalDetailsCache )
161
+ {
162
+ if ($principalDetailsCache.Value.ContainsKey ($principalId )) {
163
+ return $principalDetailsCache.Value [$principalId ]
164
+ }
165
+
166
+ $principalDetails = " " | select Name, Type
167
+ $user = Get-AzureADUser - ObjectId $principalId
168
+ if ($user ) {
169
+ $principalDetails.Name = $user.DisplayName
170
+ $principalDetails.Type = " User"
171
+ } else {
172
+ $group = Get-AzureADGroup - ObjectId $principalId
173
+ if ($group ) {
174
+ $principalDetails.Name = $group.DisplayName
175
+ $principalDetails.Type = " Group"
176
+ } else {
177
+ $servicePrincipal = Get-AzureADServicePrincipal - objectId $principalId
178
+ if ($servicePrincipal ) {
179
+ $principalDetails.Name = $servicePrincipal.DisplayName
180
+ $principalDetails.Type = " Service Principal"
181
+ }
182
+ }
183
+ }
184
+
185
+ $principalDetailsCache.Value.Add ($principalId , $principalDetails );
186
+
187
+ $principalDetails
188
+ }
189
+
190
+ # Get resource details from scope
191
+ function Get-ResourceDetails ($scope )
192
+ {
193
+ $resourceDetails = " " | select Name, Type
194
+ $scopeParts = $scope.Split (' /' , [System.StringSplitOptions ]::RemoveEmptyEntries)
195
+ $len = $scopeParts.Length
196
+
197
+ if ($len -gt 0 -and $len -le 2 -and $scope.ToLower ().Contains(" subscriptions" )) {
198
+ $resourceDetails.Type = " Subscription"
199
+ $resourceDetails.Name = $scopeParts [1 ]
200
+ }
201
+ elseif ($len -gt 0 -and $len -le 4 -and $scope.ToLower ().Contains(" resourcegroups" )) {
202
+ $resourceDetails.Type = " Resource Group"
203
+ $resourceDetails.Name = $scopeParts [3 ]
204
+ }
205
+ elseif ($len -ge 6 -and $scope.ToLower ().Contains(" providers" )) {
206
+ $resourceDetails.Type = " Resource"
207
+ $resourceDetails.Name = $scopeParts [$len -1 ]
208
+ }
209
+
210
+ $resourceDetails
211
+ }
212
+
0 commit comments