Last active
April 30, 2024 21:40
-
-
Save blakedrumm/8f73e82f78b675bea2968117b70fd83e to your computer and use it in GitHub Desktop.
This PowerShell script generates a report on Azure subscription user roles, groups, and their memberships, and then emails this report as an attachment. It logs into Azure using a managed identity, fetches role assignments for given subscriptions, compiles them into a report, and mails this report to specified recipients. The script uses the .NE…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Sends detailed reports on Azure Users, Groups, and Roles via email. | |
.DESCRIPTION | |
This PowerShell script generates a report on Azure subscription user roles, groups, and their memberships, and then emails this report as an attachment. It logs into Azure | |
using a managed identity, fetches role assignments for given subscriptions, compiles them into a report, and mails this report to specified recipients. The script uses | |
the .NET Mail API for secure email transmission. | |
.PARAMETER EmailUsername | |
The username used to authenticate with the SMTP server. | |
.PARAMETER EmailPassword | |
The secure password used for SMTP authentication. | |
.PARAMETER From | |
The email address from which the report will be sent. | |
.PARAMETER To | |
An array of recipient email addresses to whom the report will be sent. | |
.PARAMETER Cc | |
An array of CC recipient email addresses. | |
.PARAMETER Subject | |
The subject line of the email. | |
.PARAMETER Body | |
The body text of the email, describing the contents of the report. This can be either HTML or plain text. | |
.PARAMETER SMTPServer | |
The SMTP server used for sending the email. | |
.PARAMETER SubscriptionIds | |
Array of Azure subscription IDs to be included in the report. | |
.PARAMETER WhatIf | |
A switch to simulate the script execution for testing purposes without performing any actual operations. | |
.EXAMPLE | |
PS C:\> .\Get-AzRoleAssignmentReport.ps1 -EmailUsername '[email protected]' -EmailPassword (ConvertTo-SecureString 'Secure123' -AsPlainText -Force) -SMTPServer 'smtp.example.com' -From '[email protected]' -To '[email protected]','[email protected]' -Cc '[email protected]' -Subject 'Monthly Azure Report' -Body 'Attached is the monthly Azure usage report.' -SubscriptionIds 'sub1','sub2' | |
Sends an email with an Azure roles report for specified subscriptions. | |
.NOTES | |
Ensure that proper Azure permissions are configured for accessing subscription details and role assignments. This script requires the Azure PowerShell module and an SMTP server that supports SSL. | |
.AUTHOR | |
Blake Drumm ([email protected]) | |
.CREATED | |
April 23rd, 2024 | |
.MODIFIED | |
April 30th, 2024 | |
.LINK | |
Azure Automation Personal Blog: https://blakedrumm.com/ | |
#> | |
param | |
( | |
[System.String]$EmailUsername = '[email protected]', | |
[System.Security.SecureString]$EmailPassword, | |
[System.String]$SMTPServer = 'smtp.example.com', | |
[System.String]$From = '[email protected]', | |
[System.String[]]$To = '[email protected]', | |
[System.String[]]$Cc, | |
[System.String]$Subject = "Azure Users, Groups & Roles Report", | |
[System.String]$Body, | |
[System.String[]]$SubscriptionIds, | |
[boolean]$WhatIf | |
) | |
# Disable automatic saving of Azure context to disk within the current process. | |
# This prevents using outdated context data and improves script performance by reducing disk operations. | |
# The output is directed to Out-Null to suppress any console output from this command. | |
Disable-AzContextAutosave -Scope Process | Out-Null | |
# Check if the variable $Body is not set or is empty. | |
# If $Body is null or empty, the following block of code will execute to set a default message for the email body. | |
if (-NOT $Body) | |
{ | |
$Body = @" | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<style> | |
body { | |
font-family: 'Arial', sans-serif; | |
color: #333; | |
margin: 0; | |
padding: 20px; | |
} | |
.container { | |
padding: 20px; | |
border-radius: 5px; | |
box-shadow: 0 0 10px rgba(0,0,0,0.1); | |
} | |
h1 { | |
color: #0078D7; | |
} | |
p { | |
font-size: 16px; | |
} | |
.footer { | |
margin-top: 20px; | |
font-size: 14px; | |
text-align: center; | |
color: #667; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Hello Team,</h1> | |
<p>Please see the attached Azure report that contains information about all the users, groups, and roles across all supported subscriptions.</p> | |
<p>Thank you,</p> | |
<p><strong>Enterprise System Infrastructure Team</strong></p> | |
</div> | |
<div class="footer"> | |
This is an automated message. Please do not reply directly to this email. | |
</div> | |
</body> | |
</html> | |
"@ | |
} | |
# Check if the $EmailPassword variable is not set or is empty. | |
# If $EmailPassword is null or empty, fetch the password from an automation tool's variable storage, | |
# convert it into a SecureString, and assign it to $EmailPassword. | |
# This ensures that the script has a password to use for authentication that is handled securely. | |
if (-NOT $EmailPassword) | |
{ | |
[System.Security.SecureString]$EmailPassword = ConvertTo-SecureString $(Get-AutomationVariable -Name 'EmailPassword') -AsPlainText -Force | |
} | |
try | |
{ | |
Write-Output "Logging in to Azure..." | |
# Connect to Azure with user-assigned managed identity (for System Assigned, just remove the -AccountId portion below) | |
$AzureContext = (Connect-AzAccount -Identity -AccountId "8a0858a7-3051-4655-b78f-b23b0c5998d1").Context | |
} | |
catch | |
{ | |
Write-Warning $_ | |
exit 1 | |
} | |
######################################################################################################################### | |
#Email Function | |
function Send-EmailNotification | |
{ | |
param | |
( | |
[System.String]$EmailUsername, | |
[System.Security.SecureString]$EmailPassword, | |
[System.Management.Automation.PSCredential]$Credential, | |
#Either utilize $Credential or $EmailUsername and $EmailPassword. | |
[System.String]$From, | |
[System.String[]]$To, | |
[System.String[]]$Cc, | |
[System.String]$Subject, | |
[System.String]$Body, | |
[System.String]$SMTPServer, | |
[System.String]$SMTPPort = '587', | |
[System.String]$Attachment, | |
[boolean]$IsBodyHtml | |
) | |
function Test-TCPConnection { | |
[CmdletBinding()] | |
param ( | |
[string]$IPAddress, | |
[int]$Port, | |
[int]$Timeout = 1000, | |
[int]$RetryCount = 3 | |
) | |
$attempt = 0 | |
while ($attempt -lt $RetryCount) { | |
try { | |
$tcpclient = New-Object System.Net.Sockets.TcpClient | |
$connect = $tcpclient.BeginConnect($IPAddress, $Port, $null, $null) | |
$wait = $connect.AsyncWaitHandle.WaitOne($Timeout, $false) | |
if (!$wait) { | |
throw "Connection timeout" | |
} | |
$tcpclient.EndConnect($connect) | |
$tcpclient.Close() | |
return $true | |
} | |
catch { | |
$tcpclient.Close() | |
if ($attempt -eq $RetryCount - 1) { | |
if ($ErrorActionPreference -ne 'SilentlyContinue' -and $ErrorActionPreference -ne 'Ignore') { | |
# If it's the last attempt and error action is not to ignore or silently continue, throw an exception | |
throw "Failed to connect to $IPAddress on port $Port after $RetryCount attempts. Error: $_" | |
} | |
} | |
Start-Sleep -Seconds 1 # Optional: sleep 1 second between retries | |
} | |
$attempt++ | |
} | |
return $false | |
} | |
try | |
{ | |
# Start progress | |
$progressParams = @{ | |
Activity = "Sending Email" | |
Status = "Preparing to send email" | |
PercentComplete = 0 | |
} | |
Write-Progress @progressParams | |
# Create a new MailMessage object | |
$MailMessage = New-Object System.Net.Mail.MailMessage | |
$MailMessage.From = $From | |
$To.ForEach({ $MailMessage.To.Add($_) }) | |
$Cc.ForEach({ $MailMessage.CC.Add($_) }) | |
$MailMessage.Subject = $Subject | |
$MailMessage.Body = $Body | |
$MailMessage.IsBodyHtml = $IsBodyHtml | |
# Handle attachment if specified | |
if ($Attachment -ne $null -and $Attachment -ne '') | |
{ | |
# Update progress | |
$progressParams.Status = "Adding attachments" | |
$progressParams.PercentComplete = 20 | |
Write-Progress @progressParams | |
$MailMessage.Attachments.Add((New-Object System.Net.Mail.Attachment($Attachment))) | |
} | |
else | |
{ | |
# Update progress | |
$progressParams.Status = "Not adding any attachments" | |
$progressParams.PercentComplete = 20 | |
Write-Progress @progressParams | |
} | |
# Update progress | |
$progressParams.Status = "Setting up SMTP client to: $SMTPServer`:$SMTPPort" | |
$progressParams.PercentComplete = 40 | |
Write-Progress @progressParams | |
# Example usage | |
Test-TCPConnection -IPAddress $SMTPServer -Port $SMTPPort -ErrorAction Stop | Out-Null | |
# Create SMTP client | |
$SmtpClient = New-Object System.Net.Mail.SmtpClient($SMTPServer, $SMTPPort) | |
$SmtpClient.EnableSsl = $true | |
if ($Credential) | |
{ | |
$SmtpClient.Credentials = $Credential | |
} | |
else | |
{ | |
$SmtpClient.Credentials = New-Object System.Net.NetworkCredential($EmailUsername, $EmailPassword) | |
} | |
# Update progress | |
$progressParams.Status = "Sending email" | |
$progressParams.PercentComplete = 60 | |
Write-Progress @progressParams | |
# Send the email | |
$SmtpClient.Send($MailMessage) | |
# Final progress update | |
$progressParams.Status = "Email sent successfully!" | |
$progressParams.PercentComplete = 100 | |
Write-Progress @progressParams | |
Write-Output "Email sent successfully!" | |
} | |
catch | |
{ | |
Write-Warning @" | |
Exception while sending email notification. $_ | |
"@ | |
} | |
finally | |
{ | |
if ($MailMessage) | |
{ | |
$MailMessage.Dispose() | |
} | |
if ($SmtpClient) | |
{ | |
$SmtpClient.Dispose() | |
} | |
Write-Progress -Activity "Sending Email" -Status "Completed" -Completed | |
} | |
} | |
# Get list of subscriptions | |
if ($SubscriptionIds) | |
{ | |
$subscriptions = @() | |
foreach ($subscriptionId in $SubscriptionIds) | |
{ | |
$subscriptions += Get-AzSubscription -SubscriptionId $subscriptionId | |
} | |
} | |
else | |
{ | |
$subscriptions = Get-AzSubscription | |
} | |
#Define results array | |
$UserGroupRolesMemberships = @() | |
$i = 0 | |
foreach ($subscription in $subscriptions) | |
{ | |
$i++ | |
Write-Output "Subscription $i out of $($subscriptions.Count)" | |
$SubscriptionContext = Set-AzContext -SubscriptionId $subscription.Id | |
$sub = $SubscriptionContext.Subscription | |
Write-Output " Working on: $($sub.Name) (Id: $($sub.Id))" | |
# Get list of users and their roles in the subscription | |
$users = Get-AzRoleAssignment -Scope "/subscriptions/$($sub.Id)" | Select-Object DisplayName, RoleDefinitionName, ObjectType, Description | |
Write-Output " - User count: $($users.Count)" | |
foreach ($user in $users) | |
{ | |
if ($user.ObjectType -ne "Unknown") | |
{ | |
$UserGroupRolesMemberships += [PSCustomObject]@{ | |
SubscriptionId = $sub.Id | |
SubscriptionName = $sub.Name | |
UserName = $user.DisplayName | |
Role = $user.RoleDefinitionName | |
ObjectType = $user.ObjectType | |
Description = $user.Description | |
} | |
} | |
} | |
if ($UserGroupRolesMemberships) | |
{ | |
$UserGroupRolesMemberships | Sort-Object -Property "SubscriptionName" -Descending | Export-Csv "$env:TEMP\UserGroupRolesMemberships.csv" -NoTypeInformation -Encoding ASCII -Force | |
} | |
} | |
<# | |
#Set SMTP Details | |
$SMTPServer = "smtp.example.com" | |
$EmailUsername = "[email protected]" | |
$EmailPassword = ConvertTo-SecureString "PlainTextPassword" -AsPlainText -Force | |
$From = "[email protected]" | |
$To = "[email protected]" | |
$Subject = "Azure Users, Groups & Roles Report" | |
$Body = @" | |
Hello Team, | |
Please see the attached Azure report that contains information about all the users, groups and roles across all supported subscriptions. | |
Thanks, | |
Enterprise System | |
Infrastructure Team | |
"@ | |
#> | |
# Check if the script is not in What-If mode or the WhatIf parameter is explicitly set to false. | |
if ((-NOT $WhatIf) -or ($WhatIf -eq $false)) | |
{ | |
# Output a message indicating that the script is sending an email notification. | |
Write-Output "Sending email notification" | |
# Check if the CSV attachment file exists. | |
if (Test-Path -Path "$env:TEMP\UserGroupRolesMemberships.csv") | |
{ | |
# Check if the body contains HTML tags by using a regular expression match. | |
if ($Body -match '(<\s*(html|body|div|span|p|a|h[1-6]|ul|ol|li|table|tr|td|th|thead|tbody|tfoot|b|i|strong|em)\b)') | |
{ | |
# If HTML tags are detected in the body, set IsBodyHtml parameter to $true | |
# and send the email notification with HTML formatting. | |
Send-EmailNotification -IsBodyHtml:$true -EmailUsername $EmailUsername -EmailPassword $EmailPassword -From $From -To $To -Cc $Cc -Subject $Subject -Body $Body -SmtpServer $SMTPServer -Attachment "$env:TEMP\UserGroupRolesMemberships.csv" | |
} | |
else | |
{ | |
# If no HTML tags are detected in the body, set IsBodyHtml parameter to $false | |
# and send the email notification without HTML formatting. | |
Send-EmailNotification -IsBodyHtml:$false -EmailUsername $EmailUsername -EmailPassword $EmailPassword -From $From -To $To -Cc $Cc -Subject $Subject -Body $Body -SmtpServer $SMTPServer -Attachment "$env:TEMP\UserGroupRolesMemberships.csv" | |
} | |
} | |
else | |
{ | |
# If the CSV attachment file does not exist, output a warning message. | |
Write-Warning "Unable to locate the file attachment: $env:TEMP\UserGroupRolesMemberships.csv" | |
Write-Warning "Cannot send email without the attachment!" | |
} | |
} | |
else | |
{ | |
# Output a formatted message indicating that the script is in What-If mode and would send an email notification. | |
Write-Output @" | |
WhatIf: Sending email notification | |
------------------------------------------- | |
SMTP Server: $SMTPServer | |
Email Username: $EmailUsername | |
------------------------------------------- | |
From: $From | |
To: $To | |
Cc: $Cc | |
Subject: $Subject | |
$Body | |
------------------------------------------- | |
Attachment output: | |
$(Import-Csv -Path "$env:TEMP\UserGroupRolesMemberships.csv" | Sort-Object -Property "PropertyCount" -Descending | Format-Table -AutoSize | Out-String -Width 2048) | |
"@ | |
} | |
<# | |
Copyright (c) Microsoft Corporation. MIT License | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
#> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment