2323 " Get-AzureStorageContainerAcl" = " Get-AzureStorageContainer" ;
2424 " Start-CopyAzureStorageBlob" = " Start-AzureStorageBlobCopy" ;
2525 " 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