GraphEmailApp.psm1
#Region '.\Private\Get-AppSecret.ps1' 0 function Get-AppSecret { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The application name.")] [string]$AppName, [Parameter(Mandatory = $true, HelpMessage = "The app registration object.")] [PSObject]$AppRegistration, [Parameter(Mandatory = $true, HelpMessage = "The certificate thumbprint.")] [string]$CertThumbprint, [Parameter(Mandatory = $true, HelpMessage = "The context object.")] [PSObject]$Context, [Parameter(Mandatory = $true, HelpMessage = "The user object.")] [PSObject]$User, [Parameter(Mandatory = $true, HelpMessage = "The mail enabled sending group.")] [string]$MailEnabledSendingGroup ) # Begin Logging if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } if (!(Get-SecretVault -Name GraphEmailAppLocalStore)) { try { Write-AuditLog -Message "Registering CredMan Secret Vault" Register-SecretVault -Name GraphEmailAppLocalStore -ModuleName "SecretManagement.JustinGrote.CredMan" -ErrorAction Stop Write-AuditLog -Message "Secret Vault: GraphEmailAppLocalStore registered." } catch { throw $_.Exception } } elseif ((Get-SecretInfo -Name "CN=$AppName" -Vault GraphEmailAppLocalStore) ) { Write-AuditLog -Message "Secret found! Would you like to delete the previous configuration for `"CN=$AppName.`"?" -Severity Warning try { Remove-Secret -Name "CN=$AppName" -Vault GraphEmailAppLocalStore -Confirm:$false -ErrorAction Stop Write-AuditLog -Message "Previous secret CN=$AppName removed." } catch { throw $_.Exception } } $output = [PSCustomObject] @{ AppId = $AppRegistration.AppId CertThumbprint = $CertThumbprint TenantID = $Context.TenantId CertExpires = ($Cert.NotAfter).ToString("yyyy-MM-dd HH:mm:ss") SendAsUser = $($User.UserPrincipalName.Split("@")[0]) AppRestrictedSendGroup = $MailEnabledSendingGroup Appname = "CN=$AppName" } $delimiter = '|' $joinedString = ($output.PSObject.Properties.Value) -join $delimiter try { Set-Secret -Name "CN=$AppName" -Secret $joinedString -Vault GraphEmailAppLocalStore -ErrorAction Stop } catch { throw $_.Exception } Write-AuditLog -Message "Returning output. Save the AppName $("CN=$AppName"). The AppName will be needed to retreive the secret containing authentication info." Write-Host "You can use the following values as input into the email function!" -ForegroundColor Green Write-AuditLog -EndFunction $output | ForEach-Object { $hashTable = @{} $_.psobject.properties | ForEach-Object { $hashTable[$_.Name] = $_.Value } # Convert hashtable to script text $splatScript = "`$params = @{`n" $hashTable.Keys | ForEach-Object { $value = $hashTable[$_] if ($value -is [string]) { $value = "`"$value`"" } $splatScript += " $_ = $value`n" } $splatScript += "}" Write-Output $splatScript } } #EndRegion '.\Private\Get-AppSecret.ps1' 96 #Region '.\Private\Get-GraphEmailAppConfig.ps1' 0 function Get-GraphEmailAppConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The App Registration object.")] $AppRegistration, [Parameter(Mandatory = $true, HelpMessage = "The Graph Service Principal Id.")] [string]$GraphServicePrincipalId, [Parameter(Mandatory = $true, HelpMessage = "The Azure context.")] $Context, [Parameter(Mandatory = $true, HelpMessage = "The Certificate.")] [string]$CertThumbPrint ) begin { if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } Write-AuditLog "###############################################" Write-AuditLog "Creating service principal for app with AppId $($AppRegistration.AppId)" } process { try { # Create a Service Principal for the app. New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{} # Get the client Service Principal for the created app. $ClientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'" if (!($ClientSp)) { Write-AuditLog "Client service Principal not found for $($AppRegistration.AppId)" -Error throw "Unable to find Client Service Principal." } # Build the parameters for the New-MgOauth2PermissionGrant and create the grant. $Params = @{ "ClientId" = $ClientSp.Id "ConsentType" = "AllPrincipals" "ResourceId" = $GraphServicePrincipalId "Scope" = "Mail.Send" } New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false # Create the admin consent url: $adminConsentUrl = "https://login.microsoftonline.com/" + $Context.TenantId + "/adminconsent?client_id=" + $AppRegistration.AppId Write-Output "Please go to the following URL in your browser to provide admin consent" Write-Output $adminConsentUrl Write-Output "After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:" # Generate graph command that can be used to connect later that can be copied and saved. $connectGraph = "Connect-MgGraph -ClientId """ + $AppRegistration.AppId + """ -TenantId """` + $Context.TenantId + """ -CertificateName """ + $Cert.SubjectName.Name + """" Write-Output $connectGraph } catch { throw $_.Exception } Write-AuditLog -EndFunction } } #EndRegion '.\Private\Get-GraphEmailAppConfig.ps1' 67 #Region '.\Private\Initialize-GraphEmailApp.ps1' 0 function Initialize-GraphEmailApp { [OutputType([pscustomobject])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The 2 to 4 character long prefix ID of the app, files and certs that are created.")] [ValidatePattern('^[A-Z]{2,4}$')] [string]$Prefix, [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] [String] $UserId ) process { # Begin Logging Check if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } Write-AuditLog "###############################################" # Step 5: # Get the MGContext $context = Get-MgContext # Step 6: # Instantiate the user variable. $user = Get-MgUser -Filter "Mail eq '$UserId'" # Step 7: # Define the application Name and Encrypted File Paths. $AppName = "$($Prefix)-AuditGraphEmail-$($env:USERDNSDOMAIN)-As-$(($user.UserPrincipalName).Split("@")[0])" $graphServicePrincipal = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" $graphResourceId = $graphServicePrincipal.AppId Write-AuditLog "Microsoft Graph Service Principal AppId is $graphResourceId" # Step 9: # Build resource requirements variable using Find-MgGraphCommand -Command New-MgApplication | Select -First 1 -ExpandProperty Permissions # Find-MgGraphPermission -PermissionType Application -All | ? {$_.name -eq "Mail.Send"} $resId = (Find-MgGraphPermission -PermissionType Application -All | Where-Object { $_.name -eq "Mail.Send" }).Id return @{ "GraphDisplayname" = $graphServicePrincipal.DisplayName "Context" = $context "User" = $user "AppName" = $AppName "GraphServicePrincipal" = $graphServicePrincipal "GraphResourceId" = $graphResourceId "ResId" = $resId } Write-AuditLog -EndFunction } } #EndRegion '.\Private\Initialize-GraphEmailApp.ps1' 54 #Region '.\Private\Initialize-ModuleEnv.ps1' 0 function Initialize-ModuleEnv { <# .SYNOPSIS Initializes the environment by installing required PowerShell modules. .DESCRIPTION This function installs PowerShell modules required by the script. It can install public or pre-release versions of the module, and it supports installation for all users or current user. .PARAMETER PublicModuleNames An array of module names to be installed. Required when using the Public parameter set. .PARAMETER PublicRequiredVersions An array of required module versions to be installed. Required when using the Public parameter set. .PARAMETER PrereleaseModuleNames An array of pre-release module names to be installed. Required when using the Prerelease parameter set. .PARAMETER PrereleaseRequiredVersions An array of required pre-release module versions to be installed. Required when using the Prerelease parameter set. .PARAMETER Scope The scope of the module installation. Possible values are "AllUsers" and "CurrentUser". This determines the installation scope of the module. .PARAMETER ImportModuleNames The specific modules you'd like to import from the installed package to streamline imports. This is used when you want to import only specific modules from a package, rather than all of them. .EXAMPLE Initialize-ModuleEnv -PublicModuleNames "PSnmap", "Microsoft.Graph" -PublicRequiredVersions "1.3.1","1.23.0" -Scope AllUsers This example installs the PSnmap and Microsoft.Graph modules in the AllUsers scope with the specified versions. .EXAMPLE $params1 = @{ PublicModuleNames = "PSnmap","Microsoft.Graph" PublicRequiredVersions = "1.3.1","1.23.0" ImportModuleNames = "Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.SignIns" Scope = "CurrentUser" } Initialize-ModuleEnv @params1 This example installs Microsoft.Graph and Pester Modules in the CurrentUser scope with the specified versions. It will attempt to only import Microsoft.Graph Modules matching the names in the "ImportModulesNames" array. .EXAMPLE $params2 = @{ PrereleaseModuleNames = "Sampler", "Pester" PrereleaseRequiredVersions = "2.1.5", "4.10.1" Scope = "CurrentUser" } Initialize-ModuleEnv @params2 This example installs the PreRelease Sampler and Pester Modules in the CurrentUser scope with the specified versions. Double check https://www.powershellgallery.com/packages/<ModuleName>/<ModuleVersionNumber> to verify if the "-PreRelease" switch is needed. .INPUTS None .OUTPUTS None .NOTES Author: DrIOSx This function makes extensive use of the Write-AuditLog function for logging actions, warnings, and errors. It also uses a script-scope variable $script:VerbosePreference for controlling verbose output. #> [CmdletBinding(DefaultParameterSetName = "Public")] param ( [Parameter(ParameterSetName = "Public", Mandatory)] [string[]]$PublicModuleNames, [Parameter(ParameterSetName = "Public", Mandatory)] [string[]]$PublicRequiredVersions, [Parameter(ParameterSetName = "Prerelease", Mandatory)] [string[]]$PrereleaseModuleNames, [Parameter(ParameterSetName = "Prerelease", Mandatory)] [string[]]$PrereleaseRequiredVersions, [ValidateSet( "AllUsers", "CurrentUser" )] [string]$Scope, [string[]]$ImportModuleNames = $null ) # Start logging function execution if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } # Function limit needs to be set higher if installing graph module and if powershell is version 5.1. # The Microsoft.Graph module requires an increased function limit. # If we're installing this module, set the function limit to 8192. if ($PublicModuleNames -match 'Microsoft.Graph' -or $PrereleaseModuleNames -match "Microsoft.Graph") { if ($script:MaximumFunctionCount -lt 8192) { $script:MaximumFunctionCount = 8192 } } # Check and install PowerShellGet. # PowerShellGet is required for module management in PowerShell. ### https://learn.microsoft.com/en-us/powershell/scripting/gallery/installing-psget?view=powershell-7.3 # Get all available versions of PowerShellGet $PSGetVer = Get-Module -Name PowerShellGet -ListAvailable # Initialize flag to false $notOneFlag = $false # For each module version foreach ($module in $PSGetVer) { # Check if version is different from "1.0.0.1" if ($module.Version -ne "1.0.0.1") { $notOneFlag = $true break } } # If any version is different from "1.0.0.1", import the latest one if ($notOneFlag) { # Sort by version in descending order and select the first one (the latest) $latestModule = $PSGetVer | Sort-Object Version -Descending | Select-Object -First 1 # Import the latest version Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version } else { switch (Test-IsAdmin) { $false { Write-AuditLog "PowerShellGet is version 1.0.0.1. Please run this once as an administrator, to update PowershellGet." -Severity Error throw "Elevation required to update PowerShellGet!" } Default { Write-AuditLog "You have sufficient privileges to install to the PowershellGet" } } try { Write-AuditLog "Install the latest version of PowershellGet from the PSGallery?" -Severity Warning [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 Install-Module PowerShellGet -AllowClobber -Force -ErrorAction Stop Write-AuditLog "PowerShellGet was installed successfully!" $PSGetVer = Get-Module -Name PowerShellGet -ListAvailable $latestModule = $PSGetVer | Sort-Object Version -Descending | Select-Object -First 1 Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop } catch { throw $_.Exception } } # End Region PowershellGet Install if ($Scope -eq "AllUsers") { switch (Test-IsAdmin) { $false { Write-AuditLog "You must be an administrator to install in the `'AllUsers`' scope." -Severity Error Write-AuditLog "If you intended to install the module only for this user, select the `'CurrentUser`' scope." -Severity Error throw "Elevation required for `'AllUsers`' scope" } Default { Write-AuditLog "You have sufficient privileges to install to the `'AllUsers`' scope." } } } if ($PSCmdlet.ParameterSetName -eq "Public") { $modules = $PublicModuleNames $versions = $PublicRequiredVersions } elseif ($PSCmdlet.ParameterSetName -eq "Prerelease") { $modules = $PrereleaseModuleNames $versions = $PrereleaseRequiredVersions $prerelease = $true } foreach ($module in $modules) { $name = $module $version = $versions[$modules.IndexOf($module)] $installedModule = Get-Module -Name $name -ListAvailable switch (($null -eq $ImportModuleNames)) { $false { $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name } Write-AuditLog "Attempting to selecively install module/s:" } Default { $SelectiveImports = $null Write-AuditLog "Selective imports were not specified. All functions and commands will be imported." } } # Get Module Object switch ($prerelease) { $true { $message = "The PreRelease module $name version $version is not installed. Would you like to install it?" $throwmsg = "You must install the PreRelease module $name version $version to continue" } Default { $message = "The $name module version $version is not installed. Would you like to install it?" $throwmsg = "You must install the $name module version $version to continue." } } if (!$installedModule) { # Install Required Module Write-AuditLog $message -Severity Warning try { Write-AuditLog "Installing $name module/s version $version -AllowPrerelease:$prerelease." $SaveVerbosePreference = $script:VerbosePreference Install-Module $name -Scope $Scope -RequiredVersion $version -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference Write-AuditLog "$name module successfully installed!" if ($SelectiveImports) { foreach ($Mod in $SelectiveImports) { $name = $Mod Write-AuditLog "Selectively importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference Write-AuditLog "Successfully imported the $name module." } } else { Write-AuditLog "Importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference Write-AuditLog "Successfully imported the $name module." } } catch { Write-AuditLog $throwmsg -Severity Error throw $_.Exception } } else { try { if ($SelectiveImports) { foreach ($Mod in $SelectiveImports) { $name = $Mod Write-AuditLog "The $name module was found to be installed." Write-AuditLog "Selectively importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference Write-AuditLog "Successfully imported the $name module." Write-AuditLog -EndFunction } } else { Write-AuditLog "The $name module was found to be installed." Write-AuditLog "Importing the $name module." $SaveVerbosePreference = $script:VerbosePreference Import-Module $name -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference Write-AuditLog "Successfully imported the $name module." write-auditlog -EndFunction } } catch { Write-AuditLog $throwmsg -Severity Error throw $_.Exception } } } } #EndRegion '.\Private\Initialize-ModuleEnv.ps1' 242 #Region '.\Private\New-ExchangeEmailAppPolicy.ps1' 0 function New-ExchangeEmailAppPolicy { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The application registration object.")] [PSObject]$AppRegistration, [Parameter(Mandatory = $true, HelpMessage = "The Mail Enabled Sending Group.")] [string]$MailEnabledSendingGroup ) # Begin Logging if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } try { Write-AuditLog -Message "Creating Exchange Application policy for $($MailEnabledSendingGroup) for AppId $($AppRegistration.AppId)." New-ApplicationAccessPolicy -AppId $AppRegistration.AppId ` -PolicyScopeGroupId $MailEnabledSendingGroup -AccessRight RestrictAccess ` -Description "Limit MSG application to only send emails as a group of users" -ErrorAction Stop Write-AuditLog -Message "Created Exchange Application policy for $($MailEnabledSendingGroup)." } catch { throw $_.Exception } Write-AuditLog -EndFunction } #EndRegion '.\Private\New-ExchangeEmailAppPolicy.ps1' 29 #Region '.\Private\Register-GraphApp.ps1' 0 function Register-GraphApp { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The name of the application.")] [string]$AppName, [Parameter(Mandatory = $true, HelpMessage = "The Graph Resource Id.")] [string]$GraphResourceId, [Parameter(Mandatory = $true, HelpMessage = "The Resource Id.")] [string]$ResID, [Parameter(Mandatory = $true, HelpMessage = "The Certificate.")] [string]$CertThumbPrint ) begin { # Begin Logging if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } Write-AuditLog "###############################################" # Install and import the Microsoft.Graph module. Tested: 1.22.0 } process { try { Write-AuditLog "Creating app registration..." $RequiredResourceAccess = New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess $RequiredResourceAccess.ResourceAppId = $GraphResourceId $RequiredResourceAccess.ResourceAccess += @{ Id = $ResID; Type = "Role" } $AppPermissions = New-Object -TypeName System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $AppPermissions.Add($RequiredResourceAccess) Write-AuditLog "App permissions are: $AppPermissions" $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } $AppRegistration = New-MgApplication -DisplayName $AppName -SignInAudience "AzureADMyOrg" ` -Web @{ RedirectUris = "http://localhost"; } ` -RequiredResourceAccess $RequiredResourceAccess ` -AdditionalProperties @{} ` -KeyCredentials @(@{ Type = "AsymmetricX509Cert"; Usage = "Verify"; Key = $Cert.RawData }) if (!($AppRegistration)) { throw "The app creation failed for $($AppName)." } Write-AuditLog "App registration created with app ID $($AppRegistration.AppId)" Start-Sleep 1 } catch { throw $_.Exception } return $AppRegistration } end { Write-AuditLog -EndFunction } } #EndRegion '.\Private\Register-GraphApp.ps1' 60 #Region '.\Private\Test-IsAdmin.ps1' 0 function Test-IsAdmin { <# .SYNOPSIS Checks if the current user is an administrator on the machine. .DESCRIPTION This private function returns a Boolean value indicating whether the current user has administrator privileges on the machine. It does this by creating a new WindowsPrincipal object, passing in a WindowsIdentity object representing the current user, and then checking if that principal is in the Administrator role. .INPUTS None. .OUTPUTS Boolean. Returns True if the current user is an administrator, and False otherwise. .EXAMPLE PS C:\> Test-IsAdmin True #> # Create a new WindowsPrincipal object for the current user and check if it is in the Administrator role (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } #EndRegion '.\Private\Test-IsAdmin.ps1' 23 #Region '.\Private\Write-AuditLog.ps1' 0 function Write-AuditLog { <# .SYNOPSIS Writes log messages to the console and updates the script-wide log variable. .DESCRIPTION The Write-AuditLog function writes log messages to the console based on the severity (Verbose, Warning, or Error) and updates the script-wide log variable ($script:LogString) with the log entry. You can use the Start, End, and EndFunction switches to manage the lifecycle of the logging. .INPUTS System.String You can pipe a string to the Write-AuditLog function as the Message parameter. You can also pipe an object with a Severity property as the Severity parameter. .OUTPUTS None The Write-AuditLog function doesn't output any objects to the pipeline. It writes messages to the console and updates the script-wide log variable ($script:LogString). .PARAMETER BeginFunction Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable. .PARAMETER Message The message string to log. .PARAMETER Severity The severity of the log message. Accepted values are 'Information', 'Warning', and 'Error'. Defaults to 'Information'. .PARAMETER Start Initializes the script-wide log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function. .PARAMETER End Sets the message to "End Log" and exports the log to a CSV file if the OutputPath parameter is provided. .PARAMETER EndFunction Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable. .PARAMETER OutputPath The file path for exporting the log to a CSV file when using the End switch. .EXAMPLE Write-AuditLog -Message "This is a test message." Writes a test message with the default severity (Information) to the console and adds it to the log variable. .EXAMPLE Write-AuditLog -Message "This is a warning message." -Severity "Warning" Writes a warning message to the console and adds it to the log variable. .EXAMPLE Write-AuditLog -Start Initializes the log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function. .EXAMPLE Write-AuditLog -BeginFunction Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable. .EXAMPLE Write-AuditLog -EndFunction Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable. .EXAMPLE Write-AuditLog -End -OutputPath "C:\Logs\auditlog.csv" Sets the message to "End Log", adds it to the log variable, and exports the log to a CSV file. .NOTES Author: DrIOSx #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( ### [Parameter( Mandatory = $false, HelpMessage = 'Input a Message string.', Position = 0, ParameterSetName = 'Default', ValueFromPipeline = $true )] [ValidateNotNullOrEmpty()] [string]$Message, ### [Parameter( Mandatory = $false, HelpMessage = 'Information, Warning or Error.', Position = 1, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true )] [ValidateNotNullOrEmpty()] [ValidateSet('Information', 'Warning', 'Error')] [string]$Severity = 'Information', ### [Parameter( Mandatory = $false, ParameterSetName = 'End' )] [switch]$End, ### [Parameter( Mandatory = $false, ParameterSetName = 'BeginFunction' )] [switch]$BeginFunction, [Parameter( Mandatory = $false, ParameterSetName = 'EndFunction' )] [switch]$EndFunction, ### [Parameter( Mandatory = $false, ParameterSetName = 'Start' )] [switch]$Start, ### [Parameter( Mandatory = $false, ParameterSetName = 'End' )] [string]$OutputPath ) begin { $ErrorActionPreference = "SilentlyContinue" # Define variables to hold information about the command that was invoked. $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*' $FuncName = (Get-PSCallStack)[1].Command $ModuleVer = $MyInvocation.MyCommand.Version.ToString() # Set the error action preference to continue. $ErrorActionPreference = "Continue" } process { try { $Function = $($FuncName + '.v' + $ModuleVer) if ($Start) { $script:LogString = @() $Message = '+++ Begin Log | ' + $Function + ' |' } elseif ($BeginFunction) { $Message = '>>> Begin Function Log | ' + $Function + ' |' } $logEntry = [pscustomobject]@{ Time = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss')) Module = $ModuleName PSVersion = ($PSVersionTable.PSVersion).ToString() PSEdition = ($PSVersionTable.PSEdition).ToString() IsAdmin = $(Test-IsAdmin) User = "$Env:USERDOMAIN\$Env:USERNAME" HostName = $Env:COMPUTERNAME InvokedBy = $Function Severity = $Severity Message = $Message RunID = -1 } if ($BeginFunction) { $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Measure-Object -Property RunID -Maximum).Maximum if ($null -eq $maxRunID) { $maxRunID = -1 } $logEntry.RunID = $maxRunID + 1 } else { $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Select-Object -Last 1).RunID if ($null -eq $lastRunID) { $lastRunID = 0 } $logEntry.RunID = $lastRunID } if ($EndFunction) { $FunctionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $Function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)" $startTime = ([DateTime]::ParseExact("$FunctionStart", 'yyyy-MM-dd hh:mmTss', $null)) $endTime = Get-Date $timeTaken = $endTime - $startTime $Message = '<<< End Function Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" $logEntry.Message = $Message } elseif ($End) { $startTime = ([DateTime]::ParseExact($($script:LogString[0].Time), 'yyyy-MM-dd hh:mmTss', $null)) $endTime = Get-Date $timeTaken = $endTime - $startTime $Message = '--- End Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" $logEntry.Message = $Message } $script:LogString += $logEntry switch ($Severity) { 'Warning' { Write-Warning ('[WARNING] ! ' + $Message) $UserInput = Read-Host "Warning encountered! Do you want to continue? (Y/N)" if ($UserInput -eq 'N') { Write-Output "Script execution stopped by user!" exit } } 'Error' { Write-Error ('[ERROR] X - ' + $FuncName + ' ' + $Message) -ErrorAction Continue } 'Verbose' { Write-Verbose ('[VERBOSE] ~ ' + $Message) } Default { Write-Information ('[INFORMATION] * ' + $Message) -InformationAction Continue} } } catch { throw "Write-AuditLog encountered an error (process block): $($_.Exception.Message)" } } end { try { if ($End) { if (-not [string]::IsNullOrEmpty($OutputPath)) { $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8 Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)" } else { throw "OutputPath is not specified for End action." } } } catch { throw "Error in Write-AuditLog (end block): $($_.Exception.Message)" } } } #EndRegion '.\Private\Write-AuditLog.ps1' 205 #Region '.\Public\Connect-toMgGraph.ps1' 0 <# .SYNOPSIS Connects to Microsoft Graph and Exchange Online using defined permission scopes. .DESCRIPTION The Connect-ToMGGraph function is designed to facilitate a connection to Microsoft Graph and Exchange Online. It uses modern authentication pop-up, requesting the user to grant permissions. It logs the process of connection, including any errors that might occur. The function operates on three permission scopes for Microsoft Graph: - Application.ReadWrite.All - DelegatedPermissionGrant.ReadWrite.All - Directory.ReadWrite.All Note: It is necessary to press Enter at each prompt to proceed with the connection or you can cancel by pressing ctrl+c. .PARAMETERS The function does not take any parameters. .EXAMPLE Connect-ToMGGraph Executes the function, initiating the connection process to Microsoft Graph and Exchange Online. .INPUTS None. You cannot pipe inputs to this function. .OUTPUTS None. This function does not return any output. .NOTES Logging details are handled by the Write-AuditLog function, which needs to be available in the scope. If any error occurs during the connection process, the function will throw the corresponding exception. #> function Connect-ToMGGraph { # Begin Logging if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } Write-AuditLog "###############################################" # Step 4: Read-Host "Press Enter to connect to Microsoft Graph scopes Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, and Directory.ReadWrite.All, or press ctrl+c to cancel " -ErrorAction Stop # Connect to MSGraph with the appropriate permission scopes and then Exchange. Write-AuditLog "Connecting to MgGraph and ExchangeOnline using modern authentication pop-up." try { Write-AuditLog "Connecting to MgGraph with scopes Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, and Directory.ReadWrite.All." Connect-MgGraph -Scopes "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All", "Directory.ReadWrite.All" Write-AuditLog "Connected to MgGraph" Read-Host "Press Enter to connect to ExchangeOnline" -ErrorAction Stop Connect-ExchangeOnline -ErrorAction Stop Write-AuditLog "Connected to ExchangeOnline." Read-Host "Press Enter to continue" -ErrorAction Stop } catch { Write-AuditLog -Severity Error -Message "Error connecting to MgGraph or ExchangeOnline. Error: $($_.Exception.Message)" throw $_.Exception } Write-AuditLog -EndFunction } #EndRegion '.\Public\Connect-toMgGraph.ps1' 57 #Region '.\Public\Deploy-GraphEmailApp.ps1' 0 <# .SYNOPSIS Deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication. .DESCRIPTION This cmdlet deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication. It requires an AppPrefix for the app, an optional CertThumbprint, an AuthorizedSenderUserName, and a MailEnabledSendingGroup. .PARAMETER AppPrefix A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for grouping purposes. .PARAMETER CertThumbprint An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated. .PARAMETER AuthorizedSenderUserName The username of the authorized sender. .PARAMETER MailEnabledSendingGroup The mail-enabled group to which the sender belongs. This will be used to assign app policy restrictions. .EXAMPLE PS C:\> Deploy-GraphEmailApp -AppPrefix "ABC" -AuthorizedSenderUserName "jdoe@example.com" -MailEnabledSendingGroup "GraphAPIMailGroup@example.com" -CertThumbprint "AABBCCDDEEFF11223344556677889900" .INPUTS None .OUTPUTS Returns a pscustomobject containing the AppId, CertThumbprint, TenantID, and CertExpires. .NOTES This cmdlet requires that the user running the cmdlet have the necessary permissions to create the app and connect to Exchange Online. In addition, a mail-enabled security group must already exist in Exchange Online for the MailEnabledSendingGroup parameter. #> function Deploy-GraphEmailApp { [CmdletBinding()] param( [Parameter(Mandatory = $true, HelpMessage = "The prefix used to initialize the Graph Email App.")] [string]$AppPrefix, [Parameter(Mandatory = $false, HelpMessage = "The thumbprint of the certificate to be retrieved.")] [string]$CertThumbprint, [Parameter(Mandatory = $true, HelpMessage = "The username of the authorized sender.")] [string]$AuthorizedSenderUserName, [Parameter(Mandatory = $true, HelpMessage = "The Mail Enabled Sending Group.")] [string]$MailEnabledSendingGroup ) $PublicMods = ` "Microsoft.Graph", "ExchangeOnlineManagement", ` "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan" $PublicVers = ` "1.22.0", "3.1.0", ` "1.1.2", "1.0.0" $ImportMods = ` "Microsoft.Graph.Authentication", ` "Microsoft.Graph.Applications", ` "Microsoft.Graph.Identity.SignIns", ` "Microsoft.Graph.Users" $params1 = @{ PublicModuleNames = $PublicMods PublicRequiredVersions = $PublicVers ImportModuleNames = $ImportMods Scope = "CurrentUser" } if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } Write-AuditLog "###############################################" Initialize-ModuleEnv @params1 Connect-ToMGGraph $AppSettings = Initialize-GraphEmailApp -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" $CertDetails = Get-GraphEmailAppCert -AppName $AppSettings.AppName -CertThumbprint $CertThumbprint $appRegistration = Register-GraphApp -AppName $AppSettings.AppName -GraphResourceId $AppSettings.graphResourceId -ResID $AppSettings.ResId -CertThumbprint $CertDetails.CertThumbprint Get-GraphEmailAppConfig -AppRegistration $appRegistration -GraphServicePrincipalId $AppSettings.GraphServicePrincipal.Id -Context $AppSettings.Context -CertThumbprint $CertDetails.CertThumbprint Read-Host "Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue." # Call to New-ExchangeEmailAppPolicy [void](New-ExchangeEmailAppPolicy -AppRegistration $appRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup) $output = Get-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User -MailEnabledSendingGroup $MailEnabledSendingGroup return $output #> } #EndRegion '.\Public\Deploy-GraphEmailApp.ps1' 85 #Region '.\Public\Get-GraphEmailAppCert.ps1' 0 <# .SYNOPSIS Retrieves or creates a new certificate for the Microsoft Graph Email app. .DESCRIPTION The Get-GraphEmailAppCert function retrieves a certificate for the specified app from the CurrentUser's certificate store based on the provided thumbprint. If a thumbprint is not provided, it will generate a new self-signed certificate. .PARAMETER CertThumbprint The thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated. .PARAMETER AppName The name of the Graph Email App. .EXAMPLE PS C:\> Get-GraphEmailAppCert -AppName "MyApp" -CertThumbprint "9B8B40C5F148B710AD5C0E5CC8D0B71B5A30DB0C" .EXAMPLE PS C:\> Get-GraphEmailAppCert -AppName "MyApp" .INPUTS None .OUTPUTS A custom PowerShell object containing the certificate's thumbprint, expiration date, and the associated app's name. .NOTES The cmdlet requires that the user running the cmdlet have the necessary permissions to create or retrieve certificates from the certificate store. The certificate's expiration date is formatted as "yyyy-MM-dd HH:mm:ss". #> function Get-GraphEmailAppCert { param ( [string]$CertThumbprint, [string]$AppName ) if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } Write-AuditLog "###############################################" # Step 10: # Create or retrieve certificate from the store. try { if (!$CertThumbprint) { # Create a self-signed certificate for the app. $Cert = New-SelfSignedCertificate -Subject "CN=$AppName" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 $CertThumbprint = $Cert.Thumbprint $CertExpirationDate = $Cert.NotAfter $output = [PSCustomObject] @{ CertThumbprint = $CertThumbprint CertExpires = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss") AppName = $AppName } } else { # Retrieve the certificate from the CurrentUser's certificate store. $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } if (!($Cert)) { throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store." } $CertThumbprint = $Cert.Thumbprint $CertExpirationDate = $Cert.NotAfter $output = [PSCustomObject] @{ CertThumbprint = $CertThumbprint CertExpires = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss") AppName = $AppName } } return $output } catch { # If there is an error, throw an exception with the error message. throw $_.Exception } write-auditlog "Certificate with thumbprint $CertThumbprint created or retrieved from the CurrentUser's certificate store." Write-AuditLog -EndFunction } #EndRegion '.\Public\Get-GraphEmailAppCert.ps1' 73 #Region '.\Public\Send-GraphAppEmail.ps1' 0 function Send-GraphAppEmail { <# .SYNOPSIS Sends an email using the Microsoft Graph API. .DESCRIPTION The Send-GraphAppEmail function uses the Microsoft Graph API to send an email to a specified recipient. The function requires the Microsoft Graph API to be set up and requires a pre-created Microsoft Graph API app to send the email. The AppName can be passed in as a parameter and the function will retrieve the associated authentication details from the Credential Manager. .PARAMETER AppName The pre-created Microsoft Graph API app name used to send the email. .PARAMETER To The email address of the recipient. .PARAMETER FromAddress The email address of the sender who is a member of the Security Enabled Group allowed to send email that was configured using the Register-GraphEmailApp. .PARAMETER Subject The subject line of the email. .PARAMETER EmailBody The body text of the email. .PARAMETER AttachmentPath An array of file paths for any attachments to include in the email. .EXAMPLE Send-GraphAppEmail -AppName "GraphEmailApp" -To "recipient@example.com" -FromAddress "sender@example.com" -Subject "Test Email" -EmailBody "This is a test email." .NOTES The function requires the Microsoft.Graph and MSAL.PS modules to be installed and imported. #> [CmdletBinding()] param ( [Parameter(HelpMessage = "The Pre-created Register-GraphEmailApp Name for sending the email.")] [ValidateNotNullOrEmpty()] [string]$AppName, [Parameter(Mandatory = $true, HelpMessage = "The email address of the recipient.")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] [string]$To, [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] [string]$FromAddress, [Parameter(Mandatory = $true, HelpMessage = "The subject line of the email.")] [ValidateNotNullOrEmpty()] [string]$Subject, [Parameter(Mandatory = $true, HelpMessage = "The body text of the email.")] [ValidateNotNullOrEmpty()] [string]$EmailBody, [Parameter(Mandatory = $false, HelpMessage = "An array of file paths for any attachments to include in the email.")] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path $_ -PathType 'Leaf' })] [string[]]$AttachmentPath ) begin { if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } Write-AuditLog "Begin Log" Write-AuditLog "###############################################" # Install and import the Microsoft.Graph module. Tested: 1.22.0 $PublicMods = ` "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan", "MSAL.PS" $PublicVers = ` "1.1.2", "1.0.0", "4.37.0.0" $params1 = @{ PublicModuleNames = $PublicMods PublicRequiredVersions = $PublicVers Scope = "CurrentUser" } Initialize-ModuleEnv @params1 # If a GraphEmailApp object was not passed in, attempt to retrieve it from the local machine if ($AppName) { try { # Step 7: # Define the application Name and Encrypted File Paths. $Auth = Get-Secret -Name "$AppName" -Vault GraphEmailAppLocalStore -AsPlainText -ErrorAction Stop $delimiter = "|" $values = $Auth.Split($delimiter) # Create a new PSCustomObject using the values $authobj = [PSCustomObject] @{ AppId = $values[0] CertThumbprint = $values[1] TenantID = $values[2] CertExpires = $values[3] SendAsUser = $values[4] AppRestrictedSendGroup = $values[5] AppName = $values[6] } $GraphEmailApp = $authobj } catch { Write-Error $_.Exception.Message } } # End Region If if (!$GraphEmailApp) { throw "GraphEmailApp object not found. Please specify the GraphEmailApp object or provide the AppName and RedirectUri parameters." } # End Region If # Instatiate the required variables for retreiving the token. $AppId = $GraphEmailApp.AppId $CertThumbprint = $GraphEmailApp.CertThumbprint $Tenant = $GraphEmailApp.TenantID $Expiration = $GraphEmailApp.CertExpires Write-AuditLog "The Certificate $CertThumbprint will expire on $Expiration" # Retrieve the self-signed certificate from the CurrentUser's certificate store $cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } if (!($cert)) { throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store" } # End Region If Write-AuditLog -Message "Retrieved Certificate with thumbprint $CertThumbprint." } # End Region Begin Process { # Authenticate with Azure AD and obtain an access token for the Microsoft Graph API using the certificate $MSToken = Get-MsalToken -ClientCertificate $Cert -ClientId $AppId -Authority "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token" -ErrorAction Stop # Set up the request headers $authheader = @{Authorization = "Bearer $($MSToken.AccessToken)" } # Set up the request URL $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail" # Build the message body # Add a "from" field to the message object in $Message $FromField = @{ emailAddress = @{ address = "$($FromAddress)" } } $Message = @{ message = @{ subject = "$Subject" body = @{ contentType = "text" content = "$EmailBody" } toRecipients = @( @{ emailAddress = @{ address = "$To" } } ) from = $FromField } } if ($AttachmentPath) { Write-AuditLog -Message "Attachments found. Processing..." $Message.message.attachments = @() foreach ($Path in $AttachmentPath) { $attachmentName = (Split-Path -Path $Path -Leaf) $attachmentBytes = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Path)) $attachment = @{ "@odata.type" = "#microsoft.graph.fileAttachment" "Name" = $attachmentName "ContentBytes" = $attachmentBytes } $Message.message.attachments += $attachment } } $jsonMessage = $message | ConvertTo-Json -Depth 4 $body = $jsonMessage Write-AuditLog -Message "Processed message body. Ready to send email." } End { try { # Send the email message using the Invoke-RestMethod cmdlet Write-AuditLog "Sending email via Microsoft Graph." Invoke-RestMethod -Headers $authHeader -Uri $url -Body $body -Method POST -ContentType 'application/json' Write-AuditLog "Message sent to $To from $FromAddress with $(($Message.message.attachments).Count) attachments." Write-AuditLog -EndFunction } catch { throw $_.Exception } } # End Region End } #EndRegion '.\Public\Send-GraphAppEmail.ps1' 179 |