Skip to content

Instantly share code, notes, and snippets.

@blakedrumm
Last active April 30, 2024 21:40
Show Gist options
  • Save blakedrumm/8f73e82f78b675bea2968117b70fd83e to your computer and use it in GitHub Desktop.
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…
<#
.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 'admin@example.com' -EmailPassword (ConvertTo-SecureString 'Secure123' -AsPlainText -Force) -SMTPServer 'smtp.example.com' -From 'noreply@example.com' -To 'user1@example.com','user2@example.com' -Cc 'manager@example.com' -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 (blakedrumm@microsoft.com)
.CREATED
April 23rd, 2024
.MODIFIED
April 30th, 2024
.LINK
Azure Automation Personal Blog: https://blakedrumm.com/
#>
param
(
[System.String]$EmailUsername = 'admin@example.com',
[System.Security.SecureString]$EmailPassword,
[System.String]$SMTPServer = 'smtp.example.com',
[System.String]$From = 'from@example.com',
[System.String[]]$To = 'user1@example.com',
[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 = "admin@example.com"
$EmailPassword = ConvertTo-SecureString "PlainTextPassword" -AsPlainText -Force
$From = "from@example.com"
$To = "destination@example.com"
$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