From dcc23ff13df2d5f65083202fa2b80e45ae90679b Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:02:32 -0600 Subject: [PATCH 01/31] fix: pre=Refactor mod --- README.md | 33 ++++++++---- source/Public/Deploy-GraphEmailApp.ps1 | 10 ---- source/Public/New-MailEnabledSendingGroup.ps1 | 51 +++++++++++++++++++ 3 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 source/Public/New-MailEnabledSendingGroup.ps1 diff --git a/README.md b/README.md index 433b0f4..64006cb 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,28 @@ -# GraphEmailApp +# GraphEmailApp Module Functions -A module for creating a graphemail app +## Connect-ToMGGraph +Connects to Microsoft Graph and Exchange Online. +- **Permissions**: Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, Directory.ReadWrite.All. +- **Modules**: Microsoft.Graph, ExchangeOnlineManagement, SecretManagement modules. +- **User Interaction**: Requires key press prompts. +- **Outputs**: Connection established, no direct output. -## Make it yours +## Deploy-GraphEmailApp +Deploys Microsoft Graph Email app with app-only authentication. +- **Parameters**: AppPrefix, CertThumbprint (optional), AuthorizedSenderUserName, MailEnabledSendingGroup. +- **Permissions**: Administrator-level for app and Exchange Online access. +- **Requirements**: Internet connectivity, mail-enabled security group in Exchange Online. +- **Outputs**: Custom object with AppId, CertThumbprint, TenantID, CertExpires. ---- -Generated with Plaster and the SampleModule template +## Get-GraphEmailAppCert +Retrieves or creates a new certificate. +- **Parameters**: CertThumbprint (optional), AppName. +- **Permissions**: Certificate store access. +- **Outputs**: Custom object with certificate details. - -This is a sample Readme - -## Make it yours +## Send-GraphAppEmail +Sends an email via Microsoft Graph API. +- **Parameters**: AppName, To, FromAddress, Subject, EmailBody, AttachmentPath (optional). +- **Modules**: Microsoft.Graph, MSAL.PS. +- **Requirements**: AppName with necessary permissions and configurations. +- **Outputs**: Email sent, no direct output. diff --git a/source/Public/Deploy-GraphEmailApp.ps1 b/source/Public/Deploy-GraphEmailApp.ps1 index 1cb3f3e..e62472b 100644 --- a/source/Public/Deploy-GraphEmailApp.ps1 +++ b/source/Public/Deploy-GraphEmailApp.ps1 @@ -26,20 +26,15 @@ 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" @@ -67,16 +62,11 @@ function Deploy-GraphEmailApp { 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 diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 new file mode 100644 index 0000000..1ddb885 --- /dev/null +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -0,0 +1,51 @@ +function New-MailEnabledSendingGroup { + [CmdletBinding(DefaultParameterSetName = 'CustomDomain')] + param ( + [Parameter(Mandatory = $true)] + [string]$Name, + + [Parameter(Mandatory = $true)] + [string]$Alias, + + [Parameter(Mandatory = $true, ParameterSetName = 'CustomDomain')] + [string]$PrimarySmtpAddress, + + [Parameter(Mandatory = $true, ParameterSetName = 'DefaultDomain')] + [string]$DefaultDomain + ) + + # Begin Logging + if (!($script:LogString)) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + + try { + if ($PSCmdlet.ParameterSetName -eq 'DefaultDomain') { + $PrimarySmtpAddress = "$Alias@$DefaultDomain" + } + + # Create the distribution group + $groupParams = @{ + Name = $Name + Alias = $Alias + PrimarySmtpAddress = $PrimarySmtpAddress + Type = "security" + } + # Using EXO. + Write-AuditLog -Message "Creating distribution group with parameters: $($groupParams | Out-String)" + $group = New-DistributionGroup @groupParams + + Write-AuditLog -Message "Distribution group created: $($group | Out-String)" + return $group + } + catch { + Write-AuditLog -Severity Error -Message $_.Exception.Message + throw $_.Exception + } + finally { + Write-AuditLog -EndFunction + } +} From 7226b69f39471dc55316d12dc5ff8d51800c906b Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:32:20 -0600 Subject: [PATCH 02/31] Fix: Mail group creation function --- source/Public/New-MailEnabledSendingGroup.ps1 | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 index 1ddb885..b6c4d82 100644 --- a/source/Public/New-MailEnabledSendingGroup.ps1 +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -3,17 +3,13 @@ function New-MailEnabledSendingGroup { param ( [Parameter(Mandatory = $true)] [string]$Name, - - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [string]$Alias, - [Parameter(Mandatory = $true, ParameterSetName = 'CustomDomain')] [string]$PrimarySmtpAddress, - [Parameter(Mandatory = $true, ParameterSetName = 'DefaultDomain')] [string]$DefaultDomain ) - # Begin Logging if (!($script:LogString)) { Write-AuditLog -Start @@ -21,12 +17,19 @@ function New-MailEnabledSendingGroup { else { Write-AuditLog -BeginFunction } - try { + if (!($Alias)) { + $Alias = $Name + } if ($PSCmdlet.ParameterSetName -eq 'DefaultDomain') { $PrimarySmtpAddress = "$Alias@$DefaultDomain" } - + # Check if the distribution group already exists + $existingGroup = Get-DistributionGroup -Identity $Name -ErrorAction SilentlyContinue + if ($existingGroup) { + Write-AuditLog -Message "Distribution group '$Name' already exists. Returning existing group." + return $existingGroup + } # Create the distribution group $groupParams = @{ Name = $Name @@ -34,10 +37,8 @@ function New-MailEnabledSendingGroup { PrimarySmtpAddress = $PrimarySmtpAddress Type = "security" } - # Using EXO. Write-AuditLog -Message "Creating distribution group with parameters: $($groupParams | Out-String)" $group = New-DistributionGroup @groupParams - Write-AuditLog -Message "Distribution group created: $($group | Out-String)" return $group } From 920b366ecbc2a767e3f16460fce751cd60472b0e Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:39:00 -0600 Subject: [PATCH 03/31] format: fix formatting --- source/Private/Initialize-ModuleEnv.ps1 | 5 +++-- source/Public/New-MailEnabledSendingGroup.ps1 | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/source/Private/Initialize-ModuleEnv.ps1 b/source/Private/Initialize-ModuleEnv.ps1 index 74e01cf..e6591df 100644 --- a/source/Private/Initialize-ModuleEnv.ps1 +++ b/source/Private/Initialize-ModuleEnv.ps1 @@ -1,4 +1,3 @@ -function Initialize-ModuleEnv { <# .SYNOPSIS Initializes the environment by installing required PowerShell modules. @@ -49,6 +48,8 @@ function Initialize-ModuleEnv { 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. #> +function Initialize-ModuleEnv { + [CmdletBinding(DefaultParameterSetName = "Public")] param ( [Parameter(ParameterSetName = "Public", Mandatory)] @@ -238,4 +239,4 @@ function Initialize-ModuleEnv { } } } - } \ No newline at end of file +} \ No newline at end of file diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 index b6c4d82..0f8145e 100644 --- a/source/Public/New-MailEnabledSendingGroup.ps1 +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -1,13 +1,13 @@ function New-MailEnabledSendingGroup { [CmdletBinding(DefaultParameterSetName = 'CustomDomain')] param ( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, HelpMessage = "Specifies the name of the mail enabled sending group.")] [string]$Name, - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $false, HelpMessage = "Optional alias for the group. If not provided, the group name will be used.")] [string]$Alias, - [Parameter(Mandatory = $true, ParameterSetName = 'CustomDomain')] + [Parameter(Mandatory = $true, ParameterSetName = 'CustomDomain', HelpMessage = "Specifies the primary SMTP address for the group when using a custom domain.")] [string]$PrimarySmtpAddress, - [Parameter(Mandatory = $true, ParameterSetName = 'DefaultDomain')] + [Parameter(Mandatory = $true, ParameterSetName = 'DefaultDomain', HelpMessage = "Specifies the default domain to construct the primary SMTP address (alias@DefaultDomain) for the group.")] [string]$DefaultDomain ) # Begin Logging @@ -18,7 +18,7 @@ function New-MailEnabledSendingGroup { Write-AuditLog -BeginFunction } try { - if (!($Alias)) { + if (-not $Alias) { $Alias = $Name } if ($PSCmdlet.ParameterSetName -eq 'DefaultDomain') { From 389eafdd483f7beae1b698674ac324ac31351922 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 15:19:22 -0600 Subject: [PATCH 04/31] update: Initialize module to continue on newer module versions --- .gitignore | 5 +- source/Private/Initialize-ModuleEnv.ps1 | 76 ++++++++++++++----------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 911723a..801613c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ markdownissues.txt node_modules package-lock.json func.ps1 -GetMGApplicationCertificateAndSecretExpiration.ps1 \ No newline at end of file +GetMGApplicationCertificateAndSecretExpiration.ps1 +test1.ps1 +testitem.txt +testitem.zip \ No newline at end of file diff --git a/source/Private/Initialize-ModuleEnv.ps1 b/source/Private/Initialize-ModuleEnv.ps1 index e6591df..92830c1 100644 --- a/source/Private/Initialize-ModuleEnv.ps1 +++ b/source/Private/Initialize-ModuleEnv.ps1 @@ -48,7 +48,7 @@ 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. #> -function Initialize-ModuleEnv { + function Initialize-ModuleEnv { [CmdletBinding(DefaultParameterSetName = "Public")] param ( @@ -60,13 +60,11 @@ function Initialize-ModuleEnv { [string[]]$PrereleaseModuleNames, [Parameter(ParameterSetName = "Prerelease", Mandatory)] [string[]]$PrereleaseRequiredVersions, - [ValidateSet( - "AllUsers", - "CurrentUser" - )] + [ValidateSet("AllUsers", "CurrentUser")] [string]$Scope, [string[]]$ImportModuleNames = $null ) + # Start logging function execution if (!($script:LogString)) { Write-AuditLog -Start @@ -74,7 +72,8 @@ function Initialize-ModuleEnv { else { Write-AuditLog -BeginFunction } - # Function limit needs to be set higher if installing graph module and if powershell is version 5.1. + + # 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") { @@ -82,6 +81,7 @@ function Initialize-ModuleEnv { $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 @@ -110,15 +110,15 @@ function Initialize-ModuleEnv { 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 + 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" + 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 + 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!" @@ -130,7 +130,8 @@ function Initialize-ModuleEnv { throw $_.Exception } } - # End Region PowershellGet Install + # End Region PowerShellGet Install + if ($Scope -eq "AllUsers") { switch (Test-IsAdmin) { $false { @@ -143,6 +144,7 @@ function Initialize-ModuleEnv { } } } + if ($PSCmdlet.ParameterSetName -eq "Public") { $modules = $PublicModuleNames $versions = $PublicRequiredVersions @@ -152,48 +154,57 @@ function Initialize-ModuleEnv { $versions = $PrereleaseRequiredVersions $prerelease = $true } + else { + $prerelease = $false + } + foreach ($module in $modules) { $name = $module - $version = $versions[$modules.IndexOf($module)] - $installedModule = Get-Module -Name $name -ListAvailable + $requiredVersion = $versions[$modules.IndexOf($module)] + + # Filter installed modules for one with a version equal or higher than required. + $installedModule = Get-Module -Name $name -ListAvailable | + Where-Object { [version]$_.Version -ge [version]$requiredVersion } | + Sort-Object Version -Descending | + Select-Object -First 1 + switch (($null -eq $ImportModuleNames)) { $false { $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name } - Write-AuditLog "Attempting to selecively install module/s:" + Write-AuditLog "Attempting to selectively install module/s:" } Default { $SelectiveImports = $null Write-AuditLog "Selective imports were not specified. All functions and commands will be imported." } } - # Get Module Object + # Set messages based on whether this is a prerelease module or not. 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" + $message = "The PreRelease module $name version $requiredVersion (or higher) is not installed. Would you like to install it?" + $throwmsg = "You must install the PreRelease module $name version $requiredVersion (or higher) 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." + $message = "The $name module version $requiredVersion (or higher) is not installed. Would you like to install it?" + $throwmsg = "You must install the $name module version $requiredVersion (or higher) to continue." } } - if (!$installedModule) { + if (-not $installedModule) { # Install Required Module Write-AuditLog $message -Severity Warning try { - Write-AuditLog "Installing $name module/s version $version -AllowPrerelease:$prerelease." + Write-AuditLog "Installing $name module/s version $requiredVersion -AllowPrerelease:$prerelease." $SaveVerbosePreference = $script:VerbosePreference - Install-Module $name -Scope $Scope -RequiredVersion $version -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false + Install-Module $name -Scope $Scope -RequiredVersion $requiredVersion -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." + Write-AuditLog "Selectively importing the $Mod module." $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false + Import-Module $Mod -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." + Write-AuditLog "Successfully imported the $Mod module." } } else { @@ -213,24 +224,23 @@ function Initialize-ModuleEnv { 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." + Write-AuditLog "The $Mod module was found installed with version $($installedModule.Version)." + Write-AuditLog "Selectively importing the $Mod module." $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false + Import-Module $Mod -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." + Write-AuditLog "Successfully imported the $Mod module." Write-AuditLog -EndFunction } } else { - Write-AuditLog "The $name module was found to be installed." + Write-AuditLog "The $name module was found installed with version $($installedModule.Version)." 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 + Write-AuditLog -EndFunction } } catch { @@ -239,4 +249,4 @@ function Initialize-ModuleEnv { } } } -} \ No newline at end of file + } From bf9f4fb19abdac048d59eef85839d089a6184a33 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 15:47:12 -0600 Subject: [PATCH 05/31] docs: Update changelog --- CHANGELOG.md | 11 +++++++++++ README.md | 2 +- ...oy-GraphEmailApp.ps1 => Publish-GraphEmailApp.ps1} | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) rename source/Public/{Deploy-GraphEmailApp.ps1 => Publish-GraphEmailApp.ps1} (94%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 761fac6..4b9532f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add support for newer versions of each module. + +### Fixed + +- Fixed approved verb for main public function. + +## [0.1.0] - 2023-07-15 + +### Added + - Add support for multiple attachments - Release to public. + ## [0.1.0-preview0001] - 2023-07-15 ### Added diff --git a/README.md b/README.md index 64006cb..ce3028d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Connects to Microsoft Graph and Exchange Online. - **User Interaction**: Requires key press prompts. - **Outputs**: Connection established, no direct output. -## Deploy-GraphEmailApp +## Publish-GraphEmailApp Deploys Microsoft Graph Email app with app-only authentication. - **Parameters**: AppPrefix, CertThumbprint (optional), AuthorizedSenderUserName, MailEnabledSendingGroup. - **Permissions**: Administrator-level for app and Exchange Online access. diff --git a/source/Public/Deploy-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 similarity index 94% rename from source/Public/Deploy-GraphEmailApp.ps1 rename to source/Public/Publish-GraphEmailApp.ps1 index e62472b..3d9a939 100644 --- a/source/Public/Deploy-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -13,7 +13,7 @@ .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" + PS C:\> Publish-GraphEmailApp -AppPrefix "ABC" -AuthorizedSenderUserName "jdoe@example.com" -MailEnabledSendingGroup "GraphAPIMailGroup@example.com" -CertThumbprint "AABBCCDDEEFF11223344556677889900" .INPUTS None .OUTPUTS @@ -23,7 +23,7 @@ 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 { +function Publish-GraphEmailApp { [CmdletBinding()] param( [Parameter(Mandatory = $true, HelpMessage = "The prefix used to initialize the Graph Email App.")] From 266a57fad26e52e5f8b95613a4acb0d971f178c2 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:21:52 -0600 Subject: [PATCH 06/31] fix: pattern validation for company to include numbers --- source/Private/Initialize-GraphEmailApp.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Private/Initialize-GraphEmailApp.ps1 b/source/Private/Initialize-GraphEmailApp.ps1 index c8e98e5..19e35c0 100644 --- a/source/Private/Initialize-GraphEmailApp.ps1 +++ b/source/Private/Initialize-GraphEmailApp.ps1 @@ -3,7 +3,7 @@ function Initialize-GraphEmailApp { [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}$')] + [ValidatePattern("^[A-Z0-9]{2,4}$")] [string]$Prefix, [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] From 2f68af92d4a39ee2597f08ad6d2345dad06381f1 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:23:26 -0600 Subject: [PATCH 07/31] fix: pattern validation for company to include numbers --- source/Public/Publish-GraphEmailApp.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 3d9a939..91b732d 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -26,7 +26,8 @@ function Publish-GraphEmailApp { [CmdletBinding()] param( - [Parameter(Mandatory = $true, HelpMessage = "The prefix used to initialize the Graph Email App.")] + [Parameter(Mandatory = $true, HelpMessage = "The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.")] + [ValidatePattern("^[A-Z0-9]{2,4}$")] [string]$AppPrefix, [Parameter(Mandatory = $false, HelpMessage = "The thumbprint of the certificate to be retrieved.")] [string]$CertThumbprint, From ff7b98517cf279dcc86ae4397363d679b24243ae Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:23:32 -0600 Subject: [PATCH 08/31] docs: Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9532f..a3a3195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add support for newer versions of each module. +- Add support for numbers in pattern for app prefix. ### Fixed From fee048dea7360a84e5a736c74835e930af947cca Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:26:35 -0600 Subject: [PATCH 09/31] fix: formatting --- source/Public/Publish-GraphEmailApp.ps1 | 35 ++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 91b732d..076e211 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -26,32 +26,32 @@ function Publish-GraphEmailApp { [CmdletBinding()] param( - [Parameter(Mandatory = $true, HelpMessage = "The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.")] - [ValidatePattern("^[A-Z0-9]{2,4}$")] + [Parameter(Mandatory = $true, HelpMessage = 'The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.')] + [ValidatePattern('^[A-Z0-9]{2,4}$')] [string]$AppPrefix, - [Parameter(Mandatory = $false, HelpMessage = "The thumbprint of the certificate to be retrieved.")] + [Parameter(Mandatory = $false, HelpMessage = 'The thumbprint of the certificate to be retrieved.')] [string]$CertThumbprint, - [Parameter(Mandatory = $true, HelpMessage = "The username of the authorized sender.")] + [Parameter(Mandatory = $true, HelpMessage = 'The username of the authorized sender.')] [string]$AuthorizedSenderUserName, - [Parameter(Mandatory = $true, HelpMessage = "The Mail Enabled Sending Group.")] + [Parameter(Mandatory = $true, HelpMessage = 'The Mail Enabled Sending Group.')] [string]$MailEnabledSendingGroup ) $PublicMods = ` - "Microsoft.Graph", "ExchangeOnlineManagement", ` - "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan" + 'Microsoft.Graph', 'ExchangeOnlineManagement', ` + 'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan' $PublicVers = ` - "1.22.0", "3.1.0", ` - "1.1.2", "1.0.0" + '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" + 'Microsoft.Graph.Authentication', ` + 'Microsoft.Graph.Applications', ` + 'Microsoft.Graph.Identity.SignIns', ` + 'Microsoft.Graph.Users' $params1 = @{ PublicModuleNames = $PublicMods PublicRequiredVersions = $PublicVers ImportModuleNames = $ImportMods - Scope = "CurrentUser" + Scope = 'CurrentUser' } if (!($script:LogString)) { Write-AuditLog -Start @@ -59,17 +59,16 @@ function Publish-GraphEmailApp { else { Write-AuditLog -BeginFunction } - Write-AuditLog "###############################################" + 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." + 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 + $output = Get-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User -MailEnabledSendingGroup $MailEnabledSendingGroup return $output - #> } \ No newline at end of file From 713a6bcc26745f9e8a5e77f882004bcd7d91c0bf Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:27:11 -0600 Subject: [PATCH 10/31] add: connect-exchangeonline to group creation function --- source/Public/New-MailEnabledSendingGroup.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 index 0f8145e..8430173 100644 --- a/source/Public/New-MailEnabledSendingGroup.ps1 +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -18,6 +18,7 @@ function New-MailEnabledSendingGroup { Write-AuditLog -BeginFunction } try { + Connect-ExchangeOnline if (-not $Alias) { $Alias = $Name } From 7224777f12e0f8f039f436ea136063f2f4e455f9 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:47:13 -0600 Subject: [PATCH 11/31] format: fix output formatting --- source/Private/Get-GraphEmailAppConfig.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Private/Get-GraphEmailAppConfig.ps1 b/source/Private/Get-GraphEmailAppConfig.ps1 index 3ff2e52..80c0a01 100644 --- a/source/Private/Get-GraphEmailAppConfig.ps1 +++ b/source/Private/Get-GraphEmailAppConfig.ps1 @@ -49,14 +49,14 @@ function Get-GraphEmailAppConfig { # 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-Information "Please go to the following URL in your browser to provide admin consent" -InformationAction Continue 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 + Write-Information $connectGraph -InformationAction Continue } catch { throw $_.Exception From 25c94b9780c893d7ea53b7f9c2e1c47e05eb059a Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Fri, 28 Feb 2025 17:13:06 -0600 Subject: [PATCH 12/31] Still need connection info formatting --- source/Private/Get-AppSecret.ps1 | 34 ++++++++++++---------- source/Private/Get-GraphEmailAppConfig.ps1 | 8 ++--- source/Public/Publish-GraphEmailApp.ps1 | 4 ++- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/source/Private/Get-AppSecret.ps1 b/source/Private/Get-AppSecret.ps1 index b492c28..008ea5b 100644 --- a/source/Private/Get-AppSecret.ps1 +++ b/source/Private/Get-AppSecret.ps1 @@ -1,23 +1,26 @@ function Get-AppSecret { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, HelpMessage = "The application name.")] + [Parameter(Mandatory = $true, HelpMessage = 'The application name.')] [string]$AppName, - [Parameter(Mandatory = $true, HelpMessage = "The app registration object.")] + [Parameter(Mandatory = $true, HelpMessage = 'The app registration object.')] [PSObject]$AppRegistration, - [Parameter(Mandatory = $true, HelpMessage = "The certificate thumbprint.")] + [Parameter(Mandatory = $true, HelpMessage = 'The certificate thumbprint.')] [string]$CertThumbprint, - [Parameter(Mandatory = $true, HelpMessage = "The context object.")] + [Parameter(Mandatory = $true, HelpMessage = 'The context object.')] [PSObject]$Context, - [Parameter(Mandatory = $true, HelpMessage = "The user object.")] + [Parameter(Mandatory = $true, HelpMessage = 'The user object.')] [PSObject]$User, - [Parameter(Mandatory = $true, HelpMessage = "The mail enabled sending group.")] - [string]$MailEnabledSendingGroup + [Parameter(Mandatory = $true, HelpMessage = 'The mail enabled sending group.')] + [string]$MailEnabledSendingGroup, + + [Parameter(Mandatory = $true, HelpMessage = 'The Default Domain')] + [string]$DefaultDomain ) # Begin Logging @@ -30,9 +33,9 @@ function Get-AppSecret { $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." + 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 @@ -53,10 +56,11 @@ function Get-AppSecret { AppId = $AppRegistration.AppId CertThumbprint = $CertThumbprint TenantID = $Context.TenantId - CertExpires = ($Cert.NotAfter).ToString("yyyy-MM-dd HH:mm:ss") - SendAsUser = $($User.UserPrincipalName.Split("@")[0]) + CertExpires = ($Cert.NotAfter).ToString('yyyy-MM-dd HH:mm:ss') + SendAsUser = $($User.UserPrincipalName.Split('@')[0]) AppRestrictedSendGroup = $MailEnabledSendingGroup - Appname = "CN=$AppName" + Appname = "CN=$AppName" + DefaultDomain = $DefaultDomain } $delimiter = '|' @@ -71,7 +75,7 @@ function Get-AppSecret { 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-Host 'You can use the following values as input into the email function!' -ForegroundColor Green Write-AuditLog -EndFunction $output | ForEach-Object { $hashTable = @{} @@ -88,7 +92,7 @@ function Get-AppSecret { } $splatScript += " $_ = $value`n" } - $splatScript += "}" + $splatScript += '}' Write-Output $splatScript } diff --git a/source/Private/Get-GraphEmailAppConfig.ps1 b/source/Private/Get-GraphEmailAppConfig.ps1 index 80c0a01..d2bb7e4 100644 --- a/source/Private/Get-GraphEmailAppConfig.ps1 +++ b/source/Private/Get-GraphEmailAppConfig.ps1 @@ -49,14 +49,14 @@ function Get-GraphEmailAppConfig { # Create the admin consent url: $adminConsentUrl = "https://login.microsoftonline.com/" + $Context.TenantId + "/adminconsent?client_id=" + $AppRegistration.AppId - Write-Information "Please go to the following URL in your browser to provide admin consent" -InformationAction Continue - Write-Output $adminConsentUrl - Write-Output "After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:" + Write-Verbose "Please go to the following URL in your browser to provide admin consent" -Verbose + Write-Host $adminConsentUrl -ForegroundColor DarkGray + Write-Verbose "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-Information $connectGraph -InformationAction Continue + Write-Host $connectGraph -DarkGray } catch { throw $_.Exception diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 076e211..65f53b0 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -69,6 +69,8 @@ function Publish-GraphEmailApp { 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 + $output = Get-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration ` + -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User ` + -MailEnabledSendingGroup $MailEnabledSendingGroup -DefaultDomain $MailEnabledSendingGroup.Split('@')[1] return $output } \ No newline at end of file From 386d6e63026e5e125f70b6cac65878d5e3829e70 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 1 Mar 2025 13:59:46 -0600 Subject: [PATCH 13/31] fix: formatting and error handling --- source/Private/ConvertTo-ParameterSplat.ps1 | 19 + source/Private/Get-AppSecret.ps1 | 131 ++++--- source/Private/Get-GraphEmailAppConfig.ps1 | 57 +-- source/Private/Initialize-GraphEmailApp.ps1 | 71 ++-- source/Private/Initialize-ModuleEnv.ps1 | 338 +++++++++--------- source/Private/New-ExchangeEmailAppPolicy.ps1 | 29 +- source/Private/Register-GraphApp.ps1 | 40 ++- source/Private/Test-IsAdmin.ps1 | 1 - source/Private/Write-AuditLog.ps1 | 23 +- source/Public/Connect-ToMsService.ps1 | 105 ++++++ source/Public/Connect-toMgGraph.ps1 | 56 --- source/Public/Get-GraphEmailAppCert.ps1 | 16 +- source/Public/New-MailEnabledSendingGroup.ps1 | 16 +- source/Public/Publish-GraphEmailApp.ps1 | 36 +- source/Public/Send-GraphAppEmail.ps1 | 105 +++--- 15 files changed, 559 insertions(+), 484 deletions(-) create mode 100644 source/Private/ConvertTo-ParameterSplat.ps1 create mode 100644 source/Public/Connect-ToMsService.ps1 delete mode 100644 source/Public/Connect-toMgGraph.ps1 diff --git a/source/Private/ConvertTo-ParameterSplat.ps1 b/source/Private/ConvertTo-ParameterSplat.ps1 new file mode 100644 index 0000000..66c4a49 --- /dev/null +++ b/source/Private/ConvertTo-ParameterSplat.ps1 @@ -0,0 +1,19 @@ +function ConvertTo-ParameterSplat { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [PSObject]$InputObject + ) + process { + $splatScript = "`$params = @{`n" + $InputObject.psobject.Properties | ForEach-Object { + $value = $_.Value + if ($value -is [string]) { + $value = "`"$value`"" + } + $splatScript += " $($_.Name) = $value`n" + } + $splatScript += "}" + Write-Output $splatScript + } +} diff --git a/source/Private/Get-AppSecret.ps1 b/source/Private/Get-AppSecret.ps1 index 008ea5b..8d37479 100644 --- a/source/Private/Get-AppSecret.ps1 +++ b/source/Private/Get-AppSecret.ps1 @@ -1,28 +1,42 @@ function Get-AppSecret { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, HelpMessage = 'The application name.')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The application name.' + )] [string]$AppName, - - [Parameter(Mandatory = $true, HelpMessage = 'The app registration object.')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The app registration object.' + )] [PSObject]$AppRegistration, - - [Parameter(Mandatory = $true, HelpMessage = 'The certificate thumbprint.')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The certificate thumbprint.' + )] [string]$CertThumbprint, - - [Parameter(Mandatory = $true, HelpMessage = 'The context object.')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The context object.' + )] [PSObject]$Context, - - [Parameter(Mandatory = $true, HelpMessage = 'The user object.')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The user object.' + )] [PSObject]$User, - - [Parameter(Mandatory = $true, HelpMessage = 'The mail enabled sending group.')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The mail enabled sending group.' + )] [string]$MailEnabledSendingGroup, - - [Parameter(Mandatory = $true, HelpMessage = 'The Default Domain')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The Default Domain' + )] [string]$DefaultDomain ) - # Begin Logging if (!($script:LogString)) { Write-AuditLog -Start @@ -30,70 +44,47 @@ function Get-AppSecret { else { Write-AuditLog -BeginFunction } - $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } - if (!(Get-SecretVault -Name GraphEmailAppLocalStore)) { - try { + try { + $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } + if (!(Get-SecretVault -Name GraphEmailAppLocalStore)) { 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." + 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 + } } - catch { - throw $_.Exception + $output = [PSCustomObject]@{ + AppId = $AppRegistration.AppId + AppName = "CN=$AppName" + AppRestrictedSendGroup = $MailEnabledSendingGroup + CertExpires = ($Cert.NotAfter).ToString('yyyy-MM-dd HH:mm:ss') + CertThumbprint = $CertThumbprint + DefaultDomain = $DefaultDomain + SendAsUser = ($User.UserPrincipalName.Split('@')[0]) + SendAsUserEmail = $User.UserPrincipalName + TenantID = $Context.TenantId } - } - - $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" - DefaultDomain = $DefaultDomain - } - - $delimiter = '|' - $joinedString = ($output.PSObject.Properties.Value) -join $delimiter - - try { + $delimiter = '|' + $joinedString = ($output.PSObject.Properties.Value) -join $delimiter Set-Secret -Name "CN=$AppName" -Secret $joinedString -Vault GraphEmailAppLocalStore -ErrorAction Stop + Write-AuditLog -Message "Returning output. Save the AppName $("CN=$AppName"). The AppName will be needed to retrieve the secret containing authentication info." + Write-Host 'You can use the following values as input into the email function!' -ForegroundColor Green + Write-AuditLog -EndFunction + # Now simply call ConvertTo-ParameterSplat passing in the output PSObject + Write-Output ($output | ConvertTo-ParameterSplat) } 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 + Write-AuditLog -Severity Error -Message $_.Exception.Message + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } } diff --git a/source/Private/Get-GraphEmailAppConfig.ps1 b/source/Private/Get-GraphEmailAppConfig.ps1 index d2bb7e4..a8def8c 100644 --- a/source/Private/Get-GraphEmailAppConfig.ps1 +++ b/source/Private/Get-GraphEmailAppConfig.ps1 @@ -1,19 +1,25 @@ function Get-GraphEmailAppConfig { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, HelpMessage = "The App Registration object.")] + [Parameter( + Mandatory = $true, + HelpMessage = 'The App Registration object.' + )] $AppRegistration, - - [Parameter(Mandatory = $true, HelpMessage = "The Graph Service Principal Id.")] + [Parameter( + Mandatory = $true, + HelpMessage = 'The Graph Service Principal Id.' + )] [string]$GraphServicePrincipalId, - - [Parameter(Mandatory = $true, HelpMessage = "The Azure context.")] + [Parameter(Mandatory = $true, + HelpMessage = 'The Azure context.' + )] $Context, - - [Parameter(Mandatory = $true, HelpMessage = "The Certificate.")] + [Parameter(Mandatory = $true, + HelpMessage = 'The Certificate.' + )] [string]$CertThumbPrint ) - begin { if (!($script:LogString)) { Write-AuditLog -Start @@ -22,45 +28,44 @@ function Get-GraphEmailAppConfig { Write-AuditLog -BeginFunction } $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } - Write-AuditLog "###############################################" + 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." + 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" + '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-Verbose "Please go to the following URL in your browser to provide admin consent" -Verbose + $adminConsentUrl = 'https://login.microsoftonline.com/' + $Context.TenantId + '/adminconsent?client_id=' + $AppRegistration.AppId + Write-Verbose 'Please go to the following URL in your browser to provide admin consent' -Verbose Write-Host $adminConsentUrl -ForegroundColor DarkGray - Write-Verbose "After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:" - + Write-Verbose '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-Host $connectGraph -DarkGray + $connectGraph = 'Connect-MgGraph -ClientId "' + $AppRegistration.AppId + '" -TenantId "'` + + $Context.TenantId + '" -CertificateName "' + $Cert.SubjectName.Name + '"' + Write-Host $connectGraph -ForegroundColor DarkGray } catch { - throw $_.Exception + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } Write-AuditLog -EndFunction } } + + diff --git a/source/Private/Initialize-GraphEmailApp.ps1 b/source/Private/Initialize-GraphEmailApp.ps1 index 19e35c0..9c1652a 100644 --- a/source/Private/Initialize-GraphEmailApp.ps1 +++ b/source/Private/Initialize-GraphEmailApp.ps1 @@ -2,16 +2,14 @@ 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-Z0-9]{2,4}$")] + [Parameter(Mandatory = $true, HelpMessage = 'The 2 to 4 character long prefix ID of the app, files and certs that are created.')] + [ValidatePattern('^[A-Z0-9]{2,4}$')] [string]$Prefix, - - [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] + [Parameter(Mandatory = $true, HelpMessage = 'The email address of the sender.')] [ValidateNotNullOrEmpty()] - [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] + [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] [String] $UserId ) - process { # Begin Logging Check if (!($script:LogString)) { @@ -20,34 +18,39 @@ function Initialize-GraphEmailApp { 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 + try { + 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 + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } - Write-AuditLog -EndFunction } } diff --git a/source/Private/Initialize-ModuleEnv.ps1 b/source/Private/Initialize-ModuleEnv.ps1 index 92830c1..398879f 100644 --- a/source/Private/Initialize-ModuleEnv.ps1 +++ b/source/Private/Initialize-ModuleEnv.ps1 @@ -1,4 +1,4 @@ - <# +<# .SYNOPSIS Initializes the environment by installing required PowerShell modules. .DESCRIPTION @@ -16,7 +16,7 @@ .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 + 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 @@ -48,205 +48,193 @@ 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. #> - function Initialize-ModuleEnv { - - [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 +function Initialize-ModuleEnv { + [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 } - else { - Write-AuditLog -BeginFunction + } + # 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 } - - # 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 + } + # 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!' } - } - - # 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 + Default { + Write-AuditLog 'You have sufficient privileges to install to the PowerShellGet' } } - - # 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) + 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 the latest version - Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version + Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop } - 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" } - catch { - throw $_.Exception + Default { + Write-AuditLog "You have sufficient privileges to install to the `'AllUsers`' scope." } } - # 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 + } + else { + $prerelease = $false + } + foreach ($module in $modules) { + $name = $module + $requiredVersion = $versions[$modules.IndexOf($module)] + # Filter installed modules for one with a version equal or higher than required. + $installedModule = Get-Module -Name $name -ListAvailable | + Where-Object { [version]$_.Version -ge [version]$requiredVersion } | + Sort-Object Version -Descending | + Select-Object -First 1 + switch (($null -eq $ImportModuleNames)) { + $false { + $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name } + Write-AuditLog 'Attempting to selectively install module/s:' + } + Default { + $SelectiveImports = $null + Write-AuditLog 'Selective imports were not specified. All functions and commands will be imported.' } } - - if ($PSCmdlet.ParameterSetName -eq "Public") { - $modules = $PublicModuleNames - $versions = $PublicRequiredVersions - } - elseif ($PSCmdlet.ParameterSetName -eq "Prerelease") { - $modules = $PrereleaseModuleNames - $versions = $PrereleaseRequiredVersions - $prerelease = $true - } - else { - $prerelease = $false - } - - foreach ($module in $modules) { - $name = $module - $requiredVersion = $versions[$modules.IndexOf($module)] - - # Filter installed modules for one with a version equal or higher than required. - $installedModule = Get-Module -Name $name -ListAvailable | - Where-Object { [version]$_.Version -ge [version]$requiredVersion } | - Sort-Object Version -Descending | - Select-Object -First 1 - - switch (($null -eq $ImportModuleNames)) { - $false { - $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name } - Write-AuditLog "Attempting to selectively install module/s:" - } - Default { - $SelectiveImports = $null - Write-AuditLog "Selective imports were not specified. All functions and commands will be imported." - } + # Set messages based on whether this is a prerelease module or not. + switch ($prerelease) { + $true { + $message = "The PreRelease module $name version $requiredVersion (or higher) is not installed. Would you like to install it?" + $throwMsg = "You must install the PreRelease module $name version $requiredVersion (or higher) to continue." } - # Set messages based on whether this is a prerelease module or not. - switch ($prerelease) { - $true { - $message = "The PreRelease module $name version $requiredVersion (or higher) is not installed. Would you like to install it?" - $throwmsg = "You must install the PreRelease module $name version $requiredVersion (or higher) to continue." - } - Default { - $message = "The $name module version $requiredVersion (or higher) is not installed. Would you like to install it?" - $throwmsg = "You must install the $name module version $requiredVersion (or higher) to continue." - } + Default { + $message = "The $name module version $requiredVersion (or higher) is not installed. Would you like to install it?" + $throwMsg = "You must install the $name module version $requiredVersion (or higher) to continue." } - if (-not $installedModule) { - # Install Required Module - Write-AuditLog $message -Severity Warning - try { - Write-AuditLog "Installing $name module/s version $requiredVersion -AllowPrerelease:$prerelease." - $SaveVerbosePreference = $script:VerbosePreference - Install-Module $name -Scope $Scope -RequiredVersion $requiredVersion -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "$name module successfully installed!" - if ($SelectiveImports) { - foreach ($Mod in $SelectiveImports) { - Write-AuditLog "Selectively importing the $Mod module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $Mod -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $Mod module." - } - } - else { - Write-AuditLog "Importing the $name module." + } + if (-not $installedModule) { + # Install Required Module + Write-AuditLog $message -Severity Warning + try { + Write-AuditLog "Installing $name module/s version $requiredVersion -AllowPrerelease:$prerelease." + $SaveVerbosePreference = $script:VerbosePreference + Install-Module $name -Scope $Scope -RequiredVersion $requiredVersion -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false + $script:VerbosePreference = $SaveVerbosePreference + Write-AuditLog "$name module successfully installed!" + if ($SelectiveImports) { + foreach ($Mod in $SelectiveImports) { + Write-AuditLog "Selectively importing the $Mod module." $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false + Import-Module $Mod -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." + Write-AuditLog "Successfully imported the $Mod module." } } - catch { - Write-AuditLog $throwmsg -Severity Error - throw $_.Exception + 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." } } - else { - try { - if ($SelectiveImports) { - foreach ($Mod in $SelectiveImports) { - Write-AuditLog "The $Mod module was found installed with version $($installedModule.Version)." - Write-AuditLog "Selectively importing the $Mod module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $Mod -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $Mod module." - Write-AuditLog -EndFunction - } - } - else { - Write-AuditLog "The $name module was found installed with version $($installedModule.Version)." - Write-AuditLog "Importing the $name module." + catch { + Write-AuditLog $throwMsg -Severity Error + throw $_.Exception + } + } + else { + try { + if ($SelectiveImports) { + foreach ($Mod in $SelectiveImports) { + Write-AuditLog "The $Mod module was found installed with version $($installedModule.Version)." + Write-AuditLog "Selectively importing the $Mod module." $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false + Import-Module $Mod -ErrorAction Stop -Verbose:$false $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." + Write-AuditLog "Successfully imported the $Mod module." Write-AuditLog -EndFunction } } - catch { - Write-AuditLog $throwmsg -Severity Error - throw $_.Exception + else { + Write-AuditLog "The $name module was found installed with version $($installedModule.Version)." + 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 + } } } +} diff --git a/source/Private/New-ExchangeEmailAppPolicy.ps1 b/source/Private/New-ExchangeEmailAppPolicy.ps1 index d69ca46..88eaf29 100644 --- a/source/Private/New-ExchangeEmailAppPolicy.ps1 +++ b/source/Private/New-ExchangeEmailAppPolicy.ps1 @@ -1,28 +1,33 @@ function New-ExchangeEmailAppPolicy { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, HelpMessage = "The application registration object.")] + [Parameter(Mandatory = $true, + HelpMessage = 'The application registration object.' + )] [PSObject]$AppRegistration, - - [Parameter(Mandatory = $true, HelpMessage = "The Mail Enabled Sending Group.")] + [Parameter(Mandatory = $true, + HelpMessage = 'The Mail Enabled Sending Group.' + )] [string]$MailEnabledSendingGroup ) - # Begin Logging - if (!($script:LogString)) { - Write-AuditLog -Start - } - else { - Write-AuditLog -BeginFunction - } + # 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 + -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 + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } Write-AuditLog -EndFunction } diff --git a/source/Private/Register-GraphApp.ps1 b/source/Private/Register-GraphApp.ps1 index 886213c..44e8871 100644 --- a/source/Private/Register-GraphApp.ps1 +++ b/source/Private/Register-GraphApp.ps1 @@ -1,16 +1,23 @@ function Register-GraphApp { [CmdletBinding()] param ( - [Parameter(Mandatory = $true, HelpMessage = "The name of the application.")] + [Parameter(Mandatory = $true, + HelpMessage = 'The name of the application.' + )] [string]$AppName, - - [Parameter(Mandatory = $true, HelpMessage = "The Graph Resource Id.")] + [Parameter(Mandatory = $true, + HelpMessage = 'The Graph Resource Id.' + )] [string]$GraphResourceId, - - [Parameter(Mandatory = $true, HelpMessage = "The Resource Id.")] + [Parameter( + Mandatory = $true, + HelpMessage = 'The Resource Id.' + )] [string]$ResID, - - [Parameter(Mandatory = $true, HelpMessage = "The Certificate.")] + [Parameter( + Mandatory = $true, + HelpMessage = 'The Certificate.' + )] [string]$CertThumbPrint ) begin { @@ -21,27 +28,24 @@ function Register-GraphApp { else { Write-AuditLog -BeginFunction } - Write-AuditLog "###############################################" + Write-AuditLog '###############################################' # Install and import the Microsoft.Graph module. Tested: 1.22.0 } process { try { - Write-AuditLog "Creating app registration..." + Write-AuditLog 'Creating app registration...' $RequiredResourceAccess = New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess $RequiredResourceAccess.ResourceAppId = $GraphResourceId - $RequiredResourceAccess.ResourceAccess += @{ Id = $ResID; Type = "Role" } - + $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"; } ` + $AppRegistration = New-MgApplication -DisplayName $AppName -SignInAudience 'AzureADMyOrg' ` + -Web @{ RedirectUris = 'http://localhost'; } ` -RequiredResourceAccess $RequiredResourceAccess ` -AdditionalProperties @{} ` - -KeyCredentials @(@{ Type = "AsymmetricX509Cert"; Usage = "Verify"; Key = $Cert.RawData }) - + -KeyCredentials @(@{ Type = 'AsymmetricX509Cert'; Usage = 'Verify'; Key = $Cert.RawData }) if (!($AppRegistration)) { throw "The app creation failed for $($AppName)." } @@ -49,7 +53,9 @@ function Register-GraphApp { Start-Sleep 1 } catch { - throw $_.Exception + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } return $AppRegistration } diff --git a/source/Private/Test-IsAdmin.ps1 b/source/Private/Test-IsAdmin.ps1 index 64c47dc..ddc1573 100644 --- a/source/Private/Test-IsAdmin.ps1 +++ b/source/Private/Test-IsAdmin.ps1 @@ -16,7 +16,6 @@ function Test-IsAdmin { 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) } \ No newline at end of file diff --git a/source/Private/Write-AuditLog.ps1 b/source/Private/Write-AuditLog.ps1 index 701e42e..0fe29a9 100644 --- a/source/Private/Write-AuditLog.ps1 +++ b/source/Private/Write-AuditLog.ps1 @@ -1,5 +1,4 @@ -function Write-AuditLog { - <# +<# .SYNOPSIS Writes log messages to the console and updates the script-wide log variable. .DESCRIPTION @@ -49,12 +48,13 @@ 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" + 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 #> +function Write-AuditLog { [CmdletBinding(DefaultParameterSetName = 'Default')] param( ### @@ -109,13 +109,13 @@ [string]$OutputPath ) begin { - $ErrorActionPreference = "SilentlyContinue" + $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" + $ErrorActionPreference = 'Continue' } process { try { @@ -169,21 +169,20 @@ switch ($Severity) { 'Warning' { Write-Warning ('[WARNING] ! ' + $Message) - $UserInput = Read-Host "Warning encountered! Do you want to continue? (Y/N)" + $UserInput = Read-Host 'Warning encountered! Do you want to continue? (Y/N)' if ($UserInput -eq 'N') { - Write-Output "Script execution stopped by user!" + 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} + '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 { @@ -193,7 +192,7 @@ Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)" } else { - throw "OutputPath is not specified for End action." + throw 'OutputPath is not specified for End action.' } } } diff --git a/source/Public/Connect-ToMsService.ps1 b/source/Public/Connect-ToMsService.ps1 new file mode 100644 index 0000000..aba0141 --- /dev/null +++ b/source/Public/Connect-ToMsService.ps1 @@ -0,0 +1,105 @@ +<# + .SYNOPSIS + Connects to Microsoft Graph and/or Exchange Online using defined permission scopes. + .DESCRIPTION + The Connect-ToServices function is designed to facilitate a connection to Microsoft Graph and Exchange Online. + It uses modern authentication pop-ups to request the necessary permissions and logs the connection process, + including any errors encountered. You can choose to connect to Microsoft Graph, Exchange Online, or both via + the provided switch parameters. + + For Microsoft Graph, the following permission scopes are used: + - Application.ReadWrite.All + - DelegatedPermissionGrant.ReadWrite.All + - Directory.ReadWrite.All + + The function supports ShouldProcess for WhatIf support and additional confirmations as needed. + .PARAMETER MgGraph + Indicates that the function should connect to Microsoft Graph. This switch defaults to $true. + .PARAMETER ExchangeOnline + Indicates that the function should connect to Exchange Online. This switch defaults to $true. + .EXAMPLE + Connect-ToServices + Executes the function, connecting to both Microsoft Graph and Exchange Online. + .EXAMPLE + Connect-ToServices -MgGraph:$false + Connects only to Exchange Online. + .EXAMPLE + Connect-ToServices -ExchangeOnline:$false + Connects only to Microsoft Graph. + .INPUTS + None. You cannot pipe inputs to this function. + .OUTPUTS + None. This function does not return any output. + .NOTES + Logging is handled by the Write-AuditLog function, which must be available in the scope. + If an error occurs during the connection process, the function will throw the corresponding exception. +#> +function Connect-ToMsService { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [Parameter(HelpMessage = 'Connect to Microsoft Graph.')] + [Switch]$MgGraph, + [Parameter(HelpMessage = 'Connect to Exchange Online.')] + [Switch]$ExchangeOnline + ) + # Begin Logging + if (-not $script:LogString) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } + Write-AuditLog "###############################################" + # Connect to Microsoft Graph if selected. + if ($MgGraph) { + if ($PSCmdlet.ShouldProcess("Microsoft Graph", "Connecting with scopes Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, Directory.ReadWrite.All")) { + try { + $mgContext = Get-MgContext -ErrorAction SilentlyContinue + if ($mgContext) { + Write-Host "An active Microsoft Graph session is detected:`n$mgContext" + $useExisting = Read-Host "Do you want to use the existing Microsoft Graph session? (Y/N)" + if ($useExisting -match '^[Yy]') { Write-AuditLog "Using existing Microsoft Graph session." } + else { + Write-AuditLog "Creating new Microsoft Graph session." + Connect-MgGraph -Scopes "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All", "Directory.ReadWrite.All" -ErrorAction Stop + Write-AuditLog "Connected to Microsoft Graph." + } + } + else { + Write-AuditLog "No existing Microsoft Graph session found. Connecting..." + Connect-MgGraph -Scopes "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All", "Directory.ReadWrite.All" -ErrorAction Stop + Write-AuditLog "Connected to Microsoft Graph." + } + } + catch { + Write-AuditLog -Severity Error -Message "Error connecting to Microsoft Graph. Error: $($_.Exception.Message)" + throw $_.Exception + } + } + } + # Connect to Exchange Online if selected. + if ($ExchangeOnline) { + if ($PSCmdlet.ShouldProcess("Exchange Online", "Connecting to ExchangeOnline using modern authentication pop-up.")) { + try { + $exoSession = Get-PSSession | Where-Object { $_.Application -like "*ExchangeOnline*" } + if ($exoSession) { + Write-Host "An active Exchange Online session is detected:" + $exoSession | Format-Table -AutoSize + $useExisting = Read-Host "Do you want to use the existing Exchange Online session? (Y/N)" + if ($useExisting -match '^[Yy]') { Write-AuditLog "Using existing Exchange Online session." } + else { + Write-AuditLog "Creating new Exchange Online session." + Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop + Write-AuditLog "Connected to Exchange Online." + } + } + else { + Write-AuditLog "No existing Exchange Online session found. Connecting..." + Connect-ExchangeOnline -ErrorAction Stop + Write-AuditLog "Connected to Exchange Online." + } + } + catch { + Write-AuditLog -Severity Error -Message "Error connecting to Exchange Online. Error: $($_.Exception.Message)" + throw $_.Exception + } + } + } + Write-AuditLog -EndFunction +} + diff --git a/source/Public/Connect-toMgGraph.ps1 b/source/Public/Connect-toMgGraph.ps1 deleted file mode 100644 index b55fdd7..0000000 --- a/source/Public/Connect-toMgGraph.ps1 +++ /dev/null @@ -1,56 +0,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 -} \ No newline at end of file diff --git a/source/Public/Get-GraphEmailAppCert.ps1 b/source/Public/Get-GraphEmailAppCert.ps1 index e586b79..bc2ae53 100644 --- a/source/Public/Get-GraphEmailAppCert.ps1 +++ b/source/Public/Get-GraphEmailAppCert.ps1 @@ -2,7 +2,7 @@ .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. + 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. @@ -31,18 +31,18 @@ function Get-GraphEmailAppCert { else { Write-AuditLog -BeginFunction } - Write-AuditLog "###############################################" + 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 + $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") + CertExpires = $certExpirationDate.ToString('yyyy-MM-dd HH:mm:ss') AppName = $AppName } } @@ -56,7 +56,7 @@ function Get-GraphEmailAppCert { $CertExpirationDate = $Cert.NotAfter $output = [PSCustomObject] @{ CertThumbprint = $CertThumbprint - CertExpires = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss") + CertExpires = $certExpirationDate.ToString('yyyy-MM-dd HH:mm:ss') AppName = $AppName } } @@ -64,9 +64,11 @@ function Get-GraphEmailAppCert { } catch { # If there is an error, throw an exception with the error message. - throw $_.Exception + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } - write-auditlog "Certificate with thumbprint $CertThumbprint created or retrieved from the CurrentUser's certificate store." + write-AuditLog "Certificate with thumbprint $CertThumbprint created or retrieved from the CurrentUser's certificate store." Write-AuditLog -EndFunction } diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 index 8430173..bef98d7 100644 --- a/source/Public/New-MailEnabledSendingGroup.ps1 +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -1,13 +1,13 @@ function New-MailEnabledSendingGroup { [CmdletBinding(DefaultParameterSetName = 'CustomDomain')] param ( - [Parameter(Mandatory = $true, HelpMessage = "Specifies the name of the mail enabled sending group.")] + [Parameter(Mandatory = $true, HelpMessage = 'Specifies the name of the mail enabled sending group.')] [string]$Name, - [Parameter(Mandatory = $false, HelpMessage = "Optional alias for the group. If not provided, the group name will be used.")] + [Parameter(Mandatory = $false, HelpMessage = 'Optional alias for the group. If not provided, the group name will be used.')] [string]$Alias, - [Parameter(Mandatory = $true, ParameterSetName = 'CustomDomain', HelpMessage = "Specifies the primary SMTP address for the group when using a custom domain.")] + [Parameter(Mandatory = $true, ParameterSetName = 'CustomDomain', HelpMessage = 'Specifies the primary SMTP address for the group when using a custom domain.')] [string]$PrimarySmtpAddress, - [Parameter(Mandatory = $true, ParameterSetName = 'DefaultDomain', HelpMessage = "Specifies the default domain to construct the primary SMTP address (alias@DefaultDomain) for the group.")] + [Parameter(Mandatory = $true, ParameterSetName = 'DefaultDomain', HelpMessage = 'Specifies the default domain to construct the primary SMTP address (alias@DefaultDomain) for the group.')] [string]$DefaultDomain ) # Begin Logging @@ -18,7 +18,7 @@ function New-MailEnabledSendingGroup { Write-AuditLog -BeginFunction } try { - Connect-ExchangeOnline + Connect-ToMsService -ExchangeOnline if (-not $Alias) { $Alias = $Name } @@ -36,7 +36,7 @@ function New-MailEnabledSendingGroup { Name = $Name Alias = $Alias PrimarySmtpAddress = $PrimarySmtpAddress - Type = "security" + Type = 'security' } Write-AuditLog -Message "Creating distribution group with parameters: $($groupParams | Out-String)" $group = New-DistributionGroup @groupParams @@ -45,7 +45,9 @@ function New-MailEnabledSendingGroup { } catch { Write-AuditLog -Severity Error -Message $_.Exception.Message - throw $_.Exception + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } finally { Write-AuditLog -EndFunction diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 65f53b0..69b999d 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -59,18 +59,26 @@ function Publish-GraphEmailApp { 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 -DefaultDomain $MailEnabledSendingGroup.Split('@')[1] - return $output + try { + Write-AuditLog '###############################################' + Initialize-ModuleEnv @params1 + Connect-ToMsService -MgGraph -ExchangeOnline + $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 -DefaultDomain $MailEnabledSendingGroup.Split('@')[1] + return $output + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) + } + } \ No newline at end of file diff --git a/source/Public/Send-GraphAppEmail.ps1 b/source/Public/Send-GraphAppEmail.ps1 index c83ddff..f4178cc 100644 --- a/source/Public/Send-GraphAppEmail.ps1 +++ b/source/Public/Send-GraphAppEmail.ps1 @@ -1,55 +1,50 @@ -function Send-GraphAppEmail { <# .SYNOPSIS - Sends an email using the Microsoft Graph API. + 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. + 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. + The pre-created Microsoft Graph API app name used to send the email. .PARAMETER To - The email address of the recipient. + 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. + 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. + The subject line of the email. .PARAMETER EmailBody - The body text of the email. + The body text of the email. .PARAMETER AttachmentPath - An array of file paths for any attachments to include in the email. + 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." + 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. + The function requires the Microsoft.Graph and MSAL.PS modules to be installed and imported. #> +function Send-GraphAppEmail { [CmdletBinding()] param ( - [Parameter(HelpMessage = "The Pre-created Register-GraphEmailApp Name for sending the email.")] + [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.")] + [Parameter(Mandatory = $true, HelpMessage = 'The email address of the recipient.')] [ValidateNotNullOrEmpty()] - [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] + [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] [string]$To, - - [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] + [Parameter(Mandatory = $true, HelpMessage = 'The email address of the sender.')] [ValidateNotNullOrEmpty()] - [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] + [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] [string]$FromAddress, - - [Parameter(Mandatory = $true, HelpMessage = "The subject line of the email.")] + [Parameter(Mandatory = $true, HelpMessage = 'The subject line of the email.')] [ValidateNotNullOrEmpty()] [string]$Subject, - - [Parameter(Mandatory = $true, HelpMessage = "The body text of the email.")] + [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.")] + [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 @@ -61,17 +56,17 @@ function Send-GraphAppEmail { else { Write-AuditLog -BeginFunction } - Write-AuditLog "Begin Log" - Write-AuditLog "###############################################" + 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" + 'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan', 'MSAL.PS' $PublicVers = ` - "1.1.2", "1.0.0", "4.37.0.0" + '1.1.2', '1.0.0', '4.37.0.0' $params1 = @{ PublicModuleNames = $PublicMods PublicRequiredVersions = $PublicVers - Scope = "CurrentUser" + Scope = 'CurrentUser' } Initialize-ModuleEnv @params1 # If a GraphEmailApp object was not passed in, attempt to retrieve it from the local machine @@ -80,28 +75,30 @@ function Send-GraphAppEmail { # Step 7: # Define the application Name and Encrypted File Paths. $Auth = Get-Secret -Name "$AppName" -Vault GraphEmailAppLocalStore -AsPlainText -ErrorAction Stop - $delimiter = "|" + $delimiter = '|' $values = $Auth.Split($delimiter) - # Create a new PSCustomObject using the values - $authobj = [PSCustomObject] @{ + # Create a new PSCustomObject using the values in the alphabetized order. + $authObj = [PSCustomObject]@{ AppId = $values[0] - CertThumbprint = $values[1] - TenantID = $values[2] + AppName = $values[1] + AppRestrictedSendGroup = $values[2] CertExpires = $values[3] - SendAsUser = $values[4] - AppRestrictedSendGroup = $values[5] - AppName = $values[6] + CertThumbprint = $values[4] + DefaultDomain = $values[5] + SendAsUser = $values[6] + SendAsUserEmail = $values[7] + TenantID = $values[8] } - $GraphEmailApp = $authobj + $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." + 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. + # Instantiate the required variables for retrieving the token. $AppId = $GraphEmailApp.AppId $CertThumbprint = $GraphEmailApp.CertThumbprint $Tenant = $GraphEmailApp.TenantID @@ -118,7 +115,7 @@ function Send-GraphAppEmail { # 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)" } + $authHeader = @{Authorization = "Bearer $($MSToken.AccessToken)" } # Set up the request URL $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail" # Build the message body @@ -132,7 +129,7 @@ function Send-GraphAppEmail { message = @{ subject = "$Subject" body = @{ - contentType = "text" + contentType = 'text' content = "$EmailBody" } toRecipients = @( @@ -146,33 +143,35 @@ function Send-GraphAppEmail { } } if ($AttachmentPath) { - Write-AuditLog -Message "Attachments found. Processing..." + 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 + '@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." + 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." + 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 + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } } # End Region End } From 6634ef39ee63487242fac89db4097ecaa3078aa5 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:00:02 -0600 Subject: [PATCH 14/31] docs: Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a3195..d749a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed approved verb for main public function. +- Some Error handling improvements. ## [0.1.0] - 2023-07-15 From 1443c43282fd1b7b2daa22f728f4d578a17f0d95 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:15:35 -0600 Subject: [PATCH 15/31] format: format output and voids --- source/Private/Get-GraphEmailAppConfig.ps1 | 10 ++++++---- source/Public/Connect-ToMsService.ps1 | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/source/Private/Get-GraphEmailAppConfig.ps1 b/source/Private/Get-GraphEmailAppConfig.ps1 index a8def8c..a68e47e 100644 --- a/source/Private/Get-GraphEmailAppConfig.ps1 +++ b/source/Private/Get-GraphEmailAppConfig.ps1 @@ -27,6 +27,7 @@ function Get-GraphEmailAppConfig { else { Write-AuditLog -BeginFunction } + # TODO Find out if I can change the scope where cert is located and how that affects who and what can access it. $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)" @@ -34,7 +35,7 @@ function Get-GraphEmailAppConfig { process { try { # Create a Service Principal for the app. - New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{} + [void](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)) { @@ -48,16 +49,17 @@ function Get-GraphEmailAppConfig { 'ResourceId' = $GraphServicePrincipalId 'Scope' = 'Mail.Send' } - New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false + Write-AuditLog "Creating OAuth2 Permission Grant for $($ClientSp.DisplayName)" + [void](New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false) # Create the admin consent url: $adminConsentUrl = 'https://login.microsoftonline.com/' + $Context.TenantId + '/adminconsent?client_id=' + $AppRegistration.AppId Write-Verbose 'Please go to the following URL in your browser to provide admin consent' -Verbose Write-Host $adminConsentUrl -ForegroundColor DarkGray - Write-Verbose 'After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:' + Write-Verbose 'After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:' -Verbose # 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-Host $connectGraph -ForegroundColor DarkGray + Write-Host $connectGraph -ForegroundColor DarkMagenta } catch { $line = $_.InvocationInfo.Line diff --git a/source/Public/Connect-ToMsService.ps1 b/source/Public/Connect-ToMsService.ps1 index aba0141..e7dfacf 100644 --- a/source/Public/Connect-ToMsService.ps1 +++ b/source/Public/Connect-ToMsService.ps1 @@ -83,6 +83,7 @@ function Connect-ToMsService { $useExisting = Read-Host "Do you want to use the existing Exchange Online session? (Y/N)" if ($useExisting -match '^[Yy]') { Write-AuditLog "Using existing Exchange Online session." } else { + Disconnect-ExchangeOnline -Confirm:$false Write-AuditLog "Creating new Exchange Online session." Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop Write-AuditLog "Connected to Exchange Online." @@ -90,7 +91,7 @@ function Connect-ToMsService { } else { Write-AuditLog "No existing Exchange Online session found. Connecting..." - Connect-ExchangeOnline -ErrorAction Stop + Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop Write-AuditLog "Connected to Exchange Online." } } From e40b3ed716200ff3e26ae874f7dd3cfcdc68e0c1 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:18:14 -0600 Subject: [PATCH 16/31] fix: Function name --- ...{Get-GraphEmailAppConfig.ps1 => Set-GraphEmailAppConfig.ps1} | 2 +- source/Public/Publish-GraphEmailApp.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename source/Private/{Get-GraphEmailAppConfig.ps1 => Set-GraphEmailAppConfig.ps1} (98%) diff --git a/source/Private/Get-GraphEmailAppConfig.ps1 b/source/Private/Set-GraphEmailAppConfig.ps1 similarity index 98% rename from source/Private/Get-GraphEmailAppConfig.ps1 rename to source/Private/Set-GraphEmailAppConfig.ps1 index a68e47e..e02bff5 100644 --- a/source/Private/Get-GraphEmailAppConfig.ps1 +++ b/source/Private/Set-GraphEmailAppConfig.ps1 @@ -1,4 +1,4 @@ -function Get-GraphEmailAppConfig { +function Set-GraphEmailAppConfig { [CmdletBinding()] param ( [Parameter( diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 69b999d..734df76 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -66,7 +66,7 @@ function Publish-GraphEmailApp { $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 + Set-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) From 7785c0279fd747d579c8dc46a65a15865a9248fc Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:24:41 -0600 Subject: [PATCH 17/31] format: Output formatting --- source/Private/Set-GraphEmailAppConfig.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Private/Set-GraphEmailAppConfig.ps1 b/source/Private/Set-GraphEmailAppConfig.ps1 index e02bff5..b78d0a9 100644 --- a/source/Private/Set-GraphEmailAppConfig.ps1 +++ b/source/Private/Set-GraphEmailAppConfig.ps1 @@ -53,13 +53,13 @@ function Set-GraphEmailAppConfig { [void](New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false) # Create the admin consent url: $adminConsentUrl = 'https://login.microsoftonline.com/' + $Context.TenantId + '/adminconsent?client_id=' + $AppRegistration.AppId - Write-Verbose 'Please go to the following URL in your browser to provide admin consent' -Verbose + Write-Verbose 'Please go to the following URL in your browser to provide admin consent:' -Verbose Write-Host $adminConsentUrl -ForegroundColor DarkGray Write-Verbose 'After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:' -Verbose # 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-Host $connectGraph -ForegroundColor DarkMagenta + Write-Host $connectGraph -ForegroundColor DarkGreen } catch { $line = $_.InvocationInfo.Line From 16629e341cfce591d9c2c5c77ff336d2f94ad9d1 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 11:46:12 -0600 Subject: [PATCH 18/31] fix: function names --- README.md | 2 +- ...hEmailApp.ps1 => New-GraphEmailAppContext.ps1} | 2 +- .../{Get-AppSecret.ps1 => Set-AppSecret.ps1} | 8 ++++---- source/Private/Set-GraphEmailAppConfig.ps1 | 2 +- ...pCert.ps1 => Initialize-GraphEmailAppCert.ps1} | 8 ++++---- source/Public/New-MailEnabledSendingGroup.ps1 | 2 +- source/Public/Publish-GraphEmailApp.ps1 | 6 +++--- source/Public/Send-GraphAppEmail.ps1 | 15 +-------------- 8 files changed, 16 insertions(+), 29 deletions(-) rename source/Private/{Initialize-GraphEmailApp.ps1 => New-GraphEmailAppContext.ps1} (98%) rename source/Private/{Get-AppSecret.ps1 => Set-AppSecret.ps1} (93%) rename source/Public/{Get-GraphEmailAppCert.ps1 => Initialize-GraphEmailAppCert.ps1} (88%) diff --git a/README.md b/README.md index ce3028d..51ad409 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Deploys Microsoft Graph Email app with app-only authentication. - **Requirements**: Internet connectivity, mail-enabled security group in Exchange Online. - **Outputs**: Custom object with AppId, CertThumbprint, TenantID, CertExpires. -## Get-GraphEmailAppCert +## Initialize-GraphEmailAppCert Retrieves or creates a new certificate. - **Parameters**: CertThumbprint (optional), AppName. - **Permissions**: Certificate store access. diff --git a/source/Private/Initialize-GraphEmailApp.ps1 b/source/Private/New-GraphEmailAppContext.ps1 similarity index 98% rename from source/Private/Initialize-GraphEmailApp.ps1 rename to source/Private/New-GraphEmailAppContext.ps1 index 9c1652a..1aea9c9 100644 --- a/source/Private/Initialize-GraphEmailApp.ps1 +++ b/source/Private/New-GraphEmailAppContext.ps1 @@ -1,4 +1,4 @@ -function Initialize-GraphEmailApp { +function New-GraphEmailAppContext { [OutputType([pscustomobject])] [CmdletBinding()] param ( diff --git a/source/Private/Get-AppSecret.ps1 b/source/Private/Set-AppSecret.ps1 similarity index 93% rename from source/Private/Get-AppSecret.ps1 rename to source/Private/Set-AppSecret.ps1 index 8d37479..c6dd361 100644 --- a/source/Private/Get-AppSecret.ps1 +++ b/source/Private/Set-AppSecret.ps1 @@ -1,4 +1,4 @@ -function Get-AppSecret { +function Set-AppSecret { [CmdletBinding()] param ( [Parameter( @@ -61,6 +61,7 @@ function Get-AppSecret { throw $_.Exception } } + $output = [PSCustomObject]@{ AppId = $AppRegistration.AppId AppName = "CN=$AppName" @@ -72,9 +73,8 @@ function Get-AppSecret { SendAsUserEmail = $User.UserPrincipalName TenantID = $Context.TenantId } - $delimiter = '|' - $joinedString = ($output.PSObject.Properties.Value) -join $delimiter - Set-Secret -Name "CN=$AppName" -Secret $joinedString -Vault GraphEmailAppLocalStore -ErrorAction Stop + $SecretJson = $output | ConvertTo-Json -Compress + Set-Secret -Name "CN=$AppName" -Secret $SecretJson -Vault GraphEmailAppLocalStore -ErrorAction Stop Write-AuditLog -Message "Returning output. Save the AppName $("CN=$AppName"). The AppName will be needed to retrieve the secret containing authentication info." Write-Host 'You can use the following values as input into the email function!' -ForegroundColor Green Write-AuditLog -EndFunction diff --git a/source/Private/Set-GraphEmailAppConfig.ps1 b/source/Private/Set-GraphEmailAppConfig.ps1 index b78d0a9..68c8a24 100644 --- a/source/Private/Set-GraphEmailAppConfig.ps1 +++ b/source/Private/Set-GraphEmailAppConfig.ps1 @@ -59,7 +59,7 @@ function Set-GraphEmailAppConfig { # 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-Host $connectGraph -ForegroundColor DarkGreen + Write-Host "`n$connectGraph`n" -ForegroundColor DarkGreen } catch { $line = $_.InvocationInfo.Line diff --git a/source/Public/Get-GraphEmailAppCert.ps1 b/source/Public/Initialize-GraphEmailAppCert.ps1 similarity index 88% rename from source/Public/Get-GraphEmailAppCert.ps1 rename to source/Public/Initialize-GraphEmailAppCert.ps1 index bc2ae53..76ea96d 100644 --- a/source/Public/Get-GraphEmailAppCert.ps1 +++ b/source/Public/Initialize-GraphEmailAppCert.ps1 @@ -2,16 +2,16 @@ .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. + The Initialize-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" + PS C:\> Initialize-GraphEmailAppCert -AppName "MyApp" -CertThumbprint "9B8B40C5F148B710AD5C0E5CC8D0B71B5A30DB0C" .EXAMPLE - PS C:\> Get-GraphEmailAppCert -AppName "MyApp" + PS C:\> Initialize-GraphEmailAppCert -AppName "MyApp" .INPUTS None .OUTPUTS @@ -20,7 +20,7 @@ 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 { +function Initialize-GraphEmailAppCert { param ( [string]$CertThumbprint, [string]$AppName diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 index bef98d7..4b31b96 100644 --- a/source/Public/New-MailEnabledSendingGroup.ps1 +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -38,7 +38,7 @@ function New-MailEnabledSendingGroup { PrimarySmtpAddress = $PrimarySmtpAddress Type = 'security' } - Write-AuditLog -Message "Creating distribution group with parameters: $($groupParams | Out-String)" + Write-AuditLog -Message "Creating distribution group with parameters: `n$($groupParams | Out-String)" $group = New-DistributionGroup @groupParams Write-AuditLog -Message "Distribution group created: $($group | Out-String)" return $group diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 734df76..808caf8 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -63,14 +63,14 @@ function Publish-GraphEmailApp { Write-AuditLog '###############################################' Initialize-ModuleEnv @params1 Connect-ToMsService -MgGraph -ExchangeOnline - $AppSettings = Initialize-GraphEmailApp -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" - $CertDetails = Get-GraphEmailAppCert -AppName $AppSettings.AppName -CertThumbprint $CertThumbprint + $AppSettings = New-GraphEmailAppContext -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" + $CertDetails = Initialize-GraphEmailAppCert -AppName $AppSettings.AppName -CertThumbprint $CertThumbprint $appRegistration = Register-GraphApp -AppName $AppSettings.AppName -GraphResourceId $AppSettings.graphResourceId -ResID $AppSettings.ResId -CertThumbprint $CertDetails.CertThumbprint Set-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 ` + $output = Set-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration ` -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User ` -MailEnabledSendingGroup $MailEnabledSendingGroup -DefaultDomain $MailEnabledSendingGroup.Split('@')[1] return $output diff --git a/source/Public/Send-GraphAppEmail.ps1 b/source/Public/Send-GraphAppEmail.ps1 index f4178cc..b77fd10 100644 --- a/source/Public/Send-GraphAppEmail.ps1 +++ b/source/Public/Send-GraphAppEmail.ps1 @@ -75,20 +75,7 @@ function Send-GraphAppEmail { # 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 in the alphabetized order. - $authObj = [PSCustomObject]@{ - AppId = $values[0] - AppName = $values[1] - AppRestrictedSendGroup = $values[2] - CertExpires = $values[3] - CertThumbprint = $values[4] - DefaultDomain = $values[5] - SendAsUser = $values[6] - SendAsUserEmail = $values[7] - TenantID = $values[8] - } + $authObj = $Auth | ConvertFrom-Json $GraphEmailApp = $authObj } catch { From 80bc268a0ce5e8fb1948c585b35bd0cbcb55a1a9 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 11:47:28 -0600 Subject: [PATCH 19/31] docs: format docs --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 51ad409..42d71bb 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,35 @@ # GraphEmailApp Module Functions ## Connect-ToMGGraph + Connects to Microsoft Graph and Exchange Online. + - **Permissions**: Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, Directory.ReadWrite.All. - **Modules**: Microsoft.Graph, ExchangeOnlineManagement, SecretManagement modules. - **User Interaction**: Requires key press prompts. - **Outputs**: Connection established, no direct output. ## Publish-GraphEmailApp + Deploys Microsoft Graph Email app with app-only authentication. + - **Parameters**: AppPrefix, CertThumbprint (optional), AuthorizedSenderUserName, MailEnabledSendingGroup. - **Permissions**: Administrator-level for app and Exchange Online access. - **Requirements**: Internet connectivity, mail-enabled security group in Exchange Online. - **Outputs**: Custom object with AppId, CertThumbprint, TenantID, CertExpires. ## Initialize-GraphEmailAppCert + Retrieves or creates a new certificate. + - **Parameters**: CertThumbprint (optional), AppName. - **Permissions**: Certificate store access. - **Outputs**: Custom object with certificate details. ## Send-GraphAppEmail + Sends an email via Microsoft Graph API. + - **Parameters**: AppName, To, FromAddress, Subject, EmailBody, AttachmentPath (optional). - **Modules**: Microsoft.Graph, MSAL.PS. - **Requirements**: AppName with necessary permissions and configurations. From 57ce7f3c594a2a1c931227c8e83e2d31223891c8 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 11:50:04 -0600 Subject: [PATCH 20/31] add: output type to private function --- source/Private/ConvertTo-ParameterSplat.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/source/Private/ConvertTo-ParameterSplat.ps1 b/source/Private/ConvertTo-ParameterSplat.ps1 index 66c4a49..698a50b 100644 --- a/source/Private/ConvertTo-ParameterSplat.ps1 +++ b/source/Private/ConvertTo-ParameterSplat.ps1 @@ -1,5 +1,6 @@ function ConvertTo-ParameterSplat { [CmdletBinding()] + [OutputType([string])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [PSObject]$InputObject From 70201b1eec72f864743d38627b6a33dedda32345 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 11:50:22 -0600 Subject: [PATCH 21/31] docs: update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d749a05..437d85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed approved verb for main public function. - Some Error handling improvements. +- Function names are now more consistent with approved verbs. +- Refactored code to improve readability. ## [0.1.0] - 2023-07-15 From 0037eaa46c618fa5c4be3c1b2ff36ae3048796e3 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:29:56 -0600 Subject: [PATCH 22/31] add: refactor app registration function --- .../Private/New-EnterpriseAppRegistration.ps1 | 77 +++++++++++++++++++ source/Private/Register-GraphApp.ps1 | 65 ---------------- source/Public/Publish-GraphEmailApp.ps1 | 8 +- 3 files changed, 83 insertions(+), 67 deletions(-) create mode 100644 source/Private/New-EnterpriseAppRegistration.ps1 delete mode 100644 source/Private/Register-GraphApp.ps1 diff --git a/source/Private/New-EnterpriseAppRegistration.ps1 b/source/Private/New-EnterpriseAppRegistration.ps1 new file mode 100644 index 0000000..24fdc5a --- /dev/null +++ b/source/Private/New-EnterpriseAppRegistration.ps1 @@ -0,0 +1,77 @@ +function New-EnterpriseAppRegistration { + [CmdletBinding()] + param ( + [Parameter( + Mandatory = $true, + HelpMessage = 'The display name for the new app registration.' + )] + [string]$DisplayName, + [Parameter( + Mandatory = $true, + HelpMessage = 'The thumbprint of the certificate used to secure this app.' + )] + [string] + $CertThumbprint, + [Parameter( + Mandatory = $true, + HelpMessage = 'The Azure AD resource (e.g., Microsoft Graph AppId).' + )] + [string]$ResourceAppId, + [Parameter( + Mandatory = $true, + HelpMessage = 'One or more permission IDs you want to grant. For example, "Mail.Send".' + )] + [string[]]$PermissionIds, + [Parameter( + HelpMessage = 'The sign-in audience for the app registration.' + )] + [ValidateSet('AzureADMyOrg', 'AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount')] + [string]$SignInAudience = 'AzureADMyOrg' + ) + # Begin Logging + if (-not $script:LogString) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } + Write-AuditLog '###############################################' + try { + Write-AuditLog "Creating new enterprise app registration for '$DisplayName'." + # Retrieve the certificate from the CurrentUser store for the app registration + $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } + if (-not $Cert) { + throw "Certificate with thumbprint $CertThumbprint not found in Cert:\CurrentUser\My." + } + # Build the required resource access object + $requiredResourceAccess = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new() + $requiredResourceAccess.ResourceAppId = $ResourceAppId + foreach ($permId in $PermissionIds) { + # Type = 'Role' for Application permissions + $requiredResourceAccess.ResourceAccess += @{ Id = $permId; Type = 'Role' } + } + # Create the new app registration + $AppRegistration = New-MgApplication -DisplayName $DisplayName ` + -SignInAudience $SignInAudience ` + -RequiredResourceAccess $requiredResourceAccess ` + -AdditionalProperties @{} ` + -KeyCredentials @( + @{ + Type = 'AsymmetricX509Cert' + Usage = 'Verify' + Key = $Cert.RawData + } + ) + if (-not $AppRegistration) { + throw "The app creation failed for '$DisplayName'." + } + Write-AuditLog "App registration created with app ID $($AppRegistration.AppId)." + return $AppRegistration + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } + finally { + Write-AuditLog -EndFunction + } +} diff --git a/source/Private/Register-GraphApp.ps1 b/source/Private/Register-GraphApp.ps1 deleted file mode 100644 index 44e8871..0000000 --- a/source/Private/Register-GraphApp.ps1 +++ /dev/null @@ -1,65 +0,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 { - $line = $_.InvocationInfo.Line - $lineNum = $_.InvocationInfo.ScriptLineNumber - throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) - } - return $AppRegistration - } - end { - Write-AuditLog -EndFunction - } -} diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 808caf8..89f4800 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -65,7 +65,12 @@ function Publish-GraphEmailApp { Connect-ToMsService -MgGraph -ExchangeOnline $AppSettings = New-GraphEmailAppContext -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" $CertDetails = Initialize-GraphEmailAppCert -AppName $AppSettings.AppName -CertThumbprint $CertThumbprint - $appRegistration = Register-GraphApp -AppName $AppSettings.AppName -GraphResourceId $AppSettings.graphResourceId -ResID $AppSettings.ResId -CertThumbprint $CertDetails.CertThumbprint + $appRegistration = New-EnterpriseAppRegistration ` + -DisplayName $AppSettings.AppName ` + -CertThumbprint $CertDetails.CertThumbprint ` + -ResourceAppId $AppSettings.GraphResourceId ` + -PermissionIds $AppSettings.ResId -SignInAudience 'AzureADMyOrg' # e.g. 'Mail.Send' + # $appRegistration = Register-GraphApp -AppName $AppSettings.AppName -GraphResourceId $AppSettings.graphResourceId -ResID $AppSettings.ResId -CertThumbprint $CertDetails.CertThumbprint Set-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 @@ -80,5 +85,4 @@ function Publish-GraphEmailApp { $lineNum = $_.InvocationInfo.ScriptLineNumber throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) } - } \ No newline at end of file From 318490ee757ca3011793e6e1eca7ae0c72bd7d7a Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:50:37 -0600 Subject: [PATCH 23/31] docs: add comment block --- .../Private/New-EnterpriseAppRegistration.ps1 | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/source/Private/New-EnterpriseAppRegistration.ps1 b/source/Private/New-EnterpriseAppRegistration.ps1 index 24fdc5a..82d3571 100644 --- a/source/Private/New-EnterpriseAppRegistration.ps1 +++ b/source/Private/New-EnterpriseAppRegistration.ps1 @@ -1,3 +1,36 @@ +<# + .SYNOPSIS + Creates a new enterprise application registration in Azure AD with a specified certificate. + .DESCRIPTION + The New-EnterpriseAppRegistration function creates a new Azure AD application registration (sometimes called + an enterprise app) using Microsoft Graph. It sets the sign-in audience, attaches a certificate for authentication, + and configures one or more application permission IDs for the specified resource (e.g., Microsoft Graph). + Logging is handled by the Write-AuditLog function, and the newly created application object is returned. + .PARAMETER DisplayName + The display name for the new app registration. + .PARAMETER CertThumbprint + The thumbprint of the certificate used to secure this app, located in the CurrentUser certificate store. + .PARAMETER ResourceAppId + The Azure AD resource (for example, the Microsoft Graph app ID: 00000003-0000-0000-c000-000000000000). + .PARAMETER PermissionIds + One or more permission IDs (application permissions) to grant for the resource. For example, "Mail.Send". + .PARAMETER SignInAudience + The sign-in audience for the app registration. Valid values are "AzureADMyOrg", "AzureADMultipleOrgs", + and "AzureADandPersonalMicrosoftAccount". Defaults to "AzureADMyOrg". + .EXAMPLE + PS C:\> New-EnterpriseAppRegistration -DisplayName "MyEnterpriseApp" -CertThumbprint "AABBCCDDEEFF1122" -ResourceAppId "00000003-0000-0000-c000-000000000000" -PermissionIds "Mail.Send" + Creates a new Azure AD application named "MyEnterpriseApp", attaches the specified certificate, targets the Microsoft Graph + resource (AppId 00000003-0000-0000-c000-000000000000), and grants the "Mail.Send" permission. + .INPUTS + None. You cannot pipe input to this function. + .OUTPUTS + Microsoft.Graph.PowerShell.Models.MicrosoftGraphApplication + Returns the newly created Azure AD application registration object. + .NOTES + Author: DrIOSx + Requires: Microsoft.Graph PowerShell module, Write-AuditLog function + The user must have permissions in Azure AD to create and manage applications. +#> function New-EnterpriseAppRegistration { [CmdletBinding()] param ( From 5f9902a18060823f7e3b55cab76611a457b35e42 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:34:16 -0600 Subject: [PATCH 24/31] fix: refactor functions --- source/Private/Initialize-ModuleEnv.ps1 | 342 ++++++++---------- source/Public/Initialize-Certificate.ps1 | 106 ++++++ .../Public/Initialize-GraphEmailAppCert.ps1 | 74 ---- source/Public/Publish-GraphEmailApp.ps1 | 108 +++--- 4 files changed, 317 insertions(+), 313 deletions(-) create mode 100644 source/Public/Initialize-Certificate.ps1 delete mode 100644 source/Public/Initialize-GraphEmailAppCert.ps1 diff --git a/source/Private/Initialize-ModuleEnv.ps1 b/source/Private/Initialize-ModuleEnv.ps1 index 398879f..158f735 100644 --- a/source/Private/Initialize-ModuleEnv.ps1 +++ b/source/Private/Initialize-ModuleEnv.ps1 @@ -1,25 +1,41 @@ <# - .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 + .SYNOPSIS + Installs or updates required PowerShell modules, with support for stable or pre-release versions. + + .DESCRIPTION + The Initialize-ModuleEnv function handles module installation and importing in a flexible manner. + It checks for PowerShellGet (and updates it if needed), adjusts the function limit if the Microsoft.Graph + module is included, and can install modules for either the CurrentUser or AllUsers scope. It supports + both stable (Public) and pre-release modules, and optionally imports specific modules by name. + + Logging is handled via Write-AuditLog, and administrative privileges are required for certain operations + (e.g., installing modules for AllUsers). + + .PARAMETER PublicModuleNames + An array of stable module names to install when using the 'Public' parameter set. + + .PARAMETER PublicRequiredVersions + An array of required stable module versions corresponding to each name in PublicModuleNames. + + .PARAMETER PrereleaseModuleNames + An array of pre-release module names to install when using the 'Prerelease' parameter set. + + .PARAMETER PrereleaseRequiredVersions + An array of required pre-release module versions corresponding to each name in PrereleaseModuleNames. + + .PARAMETER Scope + Specifies whether to install the modules for the CurrentUser or AllUsers. + Accepts 'CurrentUser' or 'AllUsers'. Requires administrative privileges for 'AllUsers'. + + .PARAMETER ImportModuleNames + An optional list of modules to selectively import after installation. If not specified, all installed modules + are imported. + + .EXAMPLE Initialize-ModuleEnv -PublicModuleNames "PsNmap", "Microsoft.Graph" -PublicRequiredVersions "1.3.1","1.23.0" -Scope AllUsers + Installs PsNmap and Microsoft.Graph in the AllUsers scope with the specified versions. - This example installs the PSnmap and Microsoft.Graph modules in the AllUsers scope with the specified versions. - .EXAMPLE + .EXAMPLE $params1 = @{ PublicModuleNames = "PSnmap","Microsoft.Graph" PublicRequiredVersions = "1.3.1","1.23.0" @@ -27,214 +43,154 @@ Scope = "CurrentUser" } Initialize-ModuleEnv @params1 + Installs and imports specific modules for Microsoft.Graph. - 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 + .EXAMPLE $params2 = @{ PrereleaseModuleNames = "Sampler", "Pester" PrereleaseRequiredVersions = "2.1.5", "4.10.1" - Scope = "CurrentUser" + 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// - to verify if the "-PreRelease" switch is needed. - .INPUTS - None - .OUTPUTS - None - .NOTES + Installs the pre-release versions of Sampler and Pester in the CurrentUser scope. + + .INPUTS + None. You cannot pipe input into this function. + + .OUTPUTS + None. This function does not return objects to the pipeline. + + .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. - #> + Requires: Write-AuditLog, Test-IsAdmin + - This function checks for and updates PowerShellGet if needed. + - It sets the function limit to 8192 if the Microsoft.Graph module is included and PowerShell is 5.1. + - If the user lacks administrative privileges but tries to install to AllUsers, it throws an error. +#> function Initialize-ModuleEnv { - [CmdletBinding(DefaultParameterSetName = 'Public')] - param ( - [Parameter(ParameterSetName = 'Public', Mandatory)] + [CmdletBinding(DefaultParameterSetName='Public')] + param( + [Parameter(ParameterSetName='Public',Mandatory)] [string[]]$PublicModuleNames, - [Parameter(ParameterSetName = 'Public', Mandatory)] + [Parameter(ParameterSetName='Public',Mandatory)] [string[]]$PublicRequiredVersions, - [Parameter(ParameterSetName = 'Prerelease', Mandatory)] + [Parameter(ParameterSetName='Prerelease',Mandatory)] [string[]]$PrereleaseModuleNames, - [Parameter(ParameterSetName = 'Prerelease', Mandatory)] + [Parameter(ParameterSetName='Prerelease',Mandatory)] [string[]]$PrereleaseRequiredVersions, - [ValidateSet('AllUsers', 'CurrentUser')] + [ValidateSet('AllUsers','CurrentUser')] [string]$Scope, - [string[]]$ImportModuleNames = $null + [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!' + if(-not $script:LogString){Write-AuditLog -Start}else{Write-AuditLog -BeginFunction} + Write-AuditLog '###############################################' + try{ + # If Microsoft.Graph is being installed, raise function limit if < 8192. + if(($PublicModuleNames -match 'Microsoft.Graph') -or ($PrereleaseModuleNames -match 'Microsoft.Graph')){ + if($script:MaximumFunctionCount -lt 8192){ + $script:MaximumFunctionCount=8192 } - Default { - Write-AuditLog 'You have sufficient privileges to install to the PowerShellGet' + } + # Step 1: Check/Update PowerShellGet if needed + $psGetModules=Get-Module -Name PowerShellGet -ListAvailable + $hasNonDefaultVer=$false + foreach($mod in $psGetModules){ + if($mod.Version -ne '1.0.0.1'){ + $hasNonDefaultVer=$true + break } } - 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 + if($hasNonDefaultVer){ + # Import the latest version + $latestModule=$psGetModules|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" + else{ + if(-not(Test-IsAdmin)){ + Write-AuditLog 'PowerShellGet is version 1.0.0.1. Please run once as admin to update PowerShellGet.' -Severity Error + throw 'Elevation required to update PowerShellGet!' } - Default { - Write-AuditLog "You have sufficient privileges to install to the `'AllUsers`' scope." + else{ + Write-AuditLog 'Updating PowerShellGet...' + [Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + Install-Module PowerShellGet -AllowClobber -Force -ErrorAction Stop + $psGetModules=Get-Module -Name PowerShellGet -ListAvailable + $latestModule=$psGetModules|Sort-Object Version -Descending|Select-Object -First 1 + Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop } } - } - if ($PSCmdlet.ParameterSetName -eq 'Public') { - $modules = $PublicModuleNames - $versions = $PublicRequiredVersions - } - elseif ($PSCmdlet.ParameterSetName -eq 'Prerelease') { - $modules = $PrereleaseModuleNames - $versions = $PrereleaseRequiredVersions - $prerelease = $true - } - else { - $prerelease = $false - } - foreach ($module in $modules) { - $name = $module - $requiredVersion = $versions[$modules.IndexOf($module)] - # Filter installed modules for one with a version equal or higher than required. - $installedModule = Get-Module -Name $name -ListAvailable | - Where-Object { [version]$_.Version -ge [version]$requiredVersion } | - Sort-Object Version -Descending | - Select-Object -First 1 - switch (($null -eq $ImportModuleNames)) { - $false { - $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name } - Write-AuditLog 'Attempting to selectively install module/s:' + # Step 2: Validate scope + if($Scope -eq 'AllUsers'){ + if(-not(Test-IsAdmin)){ + Write-AuditLog "You must be an administrator to install in 'AllUsers' scope." -Severity Error + throw "Elevation required for 'AllUsers' scope." } - Default { - $SelectiveImports = $null - Write-AuditLog 'Selective imports were not specified. All functions and commands will be imported.' + else{ + Write-AuditLog "Installing modules for 'AllUsers' scope." } } - # Set messages based on whether this is a prerelease module or not. - switch ($prerelease) { - $true { - $message = "The PreRelease module $name version $requiredVersion (or higher) is not installed. Would you like to install it?" - $throwMsg = "You must install the PreRelease module $name version $requiredVersion (or higher) to continue." - } - Default { - $message = "The $name module version $requiredVersion (or higher) is not installed. Would you like to install it?" - $throwMsg = "You must install the $name module version $requiredVersion (or higher) to continue." - } + # Step 3: Determine module set + $prerelease=$false + if($PSCmdlet.ParameterSetName -eq 'Public'){ + $modules=$PublicModuleNames + $versions=$PublicRequiredVersions + } + elseif($PSCmdlet.ParameterSetName -eq 'Prerelease'){ + $modules=$PrereleaseModuleNames + $versions=$PrereleaseRequiredVersions + $prerelease=$true } - if (-not $installedModule) { - # Install Required Module - Write-AuditLog $message -Severity Warning - try { - Write-AuditLog "Installing $name module/s version $requiredVersion -AllowPrerelease:$prerelease." - $SaveVerbosePreference = $script:VerbosePreference - Install-Module $name -Scope $Scope -RequiredVersion $requiredVersion -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "$name module successfully installed!" - if ($SelectiveImports) { - foreach ($Mod in $SelectiveImports) { - Write-AuditLog "Selectively importing the $Mod module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $Mod -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $Mod module." + # Step 4: Install/Import each module + foreach($m in $modules){ + $requiredVersion=$versions[$modules.IndexOf($m)] + $installed=Get-Module -Name $m -ListAvailable|Where-Object{[version]$_.Version -ge [version]$requiredVersion}|Sort-Object Version -Descending|Select-Object -First 1 + $SelectiveImports=$null + if($ImportModuleNames){ + $SelectiveImports=$ImportModuleNames|Where-Object{$_ -match $m} + } + if(-not $installed){ + $msgPrefix=if($prerelease){'PreRelease'}else{'stable'} + Write-AuditLog "The $msgPrefix module $m version $requiredVersion (or higher) is not installed." -Severity Warning + Write-AuditLog "Installing $m version $requiredVersion -AllowPrerelease:$prerelease." + Install-Module $m -Scope $Scope -RequiredVersion $requiredVersion -AllowPrerelease:$prerelease -ErrorAction Stop + Write-AuditLog "$m module successfully installed!" + if($SelectiveImports){ + foreach($ModName in $SelectiveImports){ + Write-AuditLog "Selectively importing $ModName." + Import-Module $ModName -ErrorAction Stop + Write-AuditLog "Successfully imported $ModName." } } - 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." + else{ + Write-AuditLog "Importing the $m module." + Import-Module $m -ErrorAction Stop + Write-AuditLog "Successfully imported the $m module." } } - catch { - Write-AuditLog $throwMsg -Severity Error - throw $_.Exception - } - } - else { - try { - if ($SelectiveImports) { - foreach ($Mod in $SelectiveImports) { - Write-AuditLog "The $Mod module was found installed with version $($installedModule.Version)." - Write-AuditLog "Selectively importing the $Mod module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $Mod -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $Mod module." - Write-AuditLog -EndFunction + else{ + Write-AuditLog "Found $m version $($installed.Version) installed." + if($SelectiveImports){ + foreach($ModName in $SelectiveImports){ + Write-AuditLog "Selectively importing $ModName." + Import-Module $ModName -ErrorAction Stop + Write-AuditLog "Successfully imported $ModName." } } - else { - Write-AuditLog "The $name module was found installed with version $($installedModule.Version)." - 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 + else{ + Write-AuditLog "Importing the $m module." + Import-Module $m -ErrorAction Stop + Write-AuditLog "Successfully imported the $m module." } } - catch { - Write-AuditLog $throwMsg -Severity Error - throw $_.Exception - } } } + catch{ + Write-AuditLog -Severity Error -Message $_.Exception.Message + $line=$_.InvocationInfo.Line + $lineNum=$_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)",$_.Exception) + } + finally{ + Write-AuditLog -EndFunction + } } diff --git a/source/Public/Initialize-Certificate.ps1 b/source/Public/Initialize-Certificate.ps1 new file mode 100644 index 0000000..f16e4c3 --- /dev/null +++ b/source/Public/Initialize-Certificate.ps1 @@ -0,0 +1,106 @@ +<# + .SYNOPSIS + Retrieves or creates a self-signed certificate in the specified store. + .DESCRIPTION + The Initialize-Certificate function either retrieves a certificate by thumbprint from + the specified store or creates a new self-signed certificate if no thumbprint is provided. + It returns a PSCustomObject containing the certificate's thumbprint, expiration date, + and an optional AppName (to maintain compatibility with existing usage). + .PARAMETER Thumbprint + The thumbprint of the certificate to retrieve. If omitted, a new self-signed certificate + is created. + .PARAMETER AppName + An optional name for the application or usage context of this certificate. + This is used to populate the "AppName" property in the returned object if needed. + .PARAMETER Subject + The certificate subject, for example: "CN=MyNewAppCert". Defaults to "CN=DefaultSelfSignedCert" + if no thumbprint is provided. + .PARAMETER CertStoreLocation + The certificate store path (e.g., "Cert:\CurrentUser\My" or "Cert:\LocalMachine\My"). + Defaults to "Cert:\CurrentUser\My". + .EXAMPLE + # Retrieve an existing cert by thumbprint + Initialize-Certificate -Thumbprint "9B8B40C5F148B710AD5C0E5CC8D0B71B5A30DB0C" + .EXAMPLE + # Create a new self-signed cert for a specific application name + Initialize-Certificate -AppName "MyGraphApp" -Subject "CN=MyGraphAppCert" + Returns an object containing AppName, CertThumbprint, and expiration info. + .OUTPUTS + PSCustomObject with: + - CertThumbprint + - CertExpires + - AppName (if provided) + Preserving compatibility with your existing usage pattern. + .NOTES + Author: DrIOSx + Requires: Write-AuditLog + The user must have permission to create or retrieve certificates from the specified store. +#> +function Initialize-Certificate { + [CmdletBinding()] + param( + [Parameter( + Mandatory = $false, + HelpMessage = 'The thumbprint of the certificate to retrieve. If omitted, a new self-signed certificate is created.' + )] + [string]$Thumbprint, + [Parameter( + Mandatory = $false, + HelpMessage = 'An optional name to store in the output object (e.g., the associated app name).' + )] + [string]$AppName, + [Parameter( + Mandatory = $false, + HelpMessage = 'The subject name for the new certificate if no thumbprint is provided.' + )] + [string]$Subject = 'CN=DefaultSelfSignedCert', + [Parameter( + Mandatory = $false, + HelpMessage = 'The certificate store location (e.g., "Cert:\CurrentUser\My").' + )] + [string]$CertStoreLocation = 'Cert:\CurrentUser\My' + ) + if (-not $script:LogString) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + Write-AuditLog '###############################################' + try { + if ($Thumbprint) { + # Attempt to retrieve an existing certificate + $Cert = Get-ChildItem -Path $CertStoreLocation | Where-Object { $_.Thumbprint -eq $Thumbprint } + if (-not $Cert) { + throw "Certificate with thumbprint $Thumbprint not found in $CertStoreLocation." + } + Write-AuditLog "Retrieved certificate with thumbprint $Thumbprint from $CertStoreLocation." + } + else { + # Create a new self-signed certificate + $Cert = New-SelfSignedCertificate -Subject $Subject -CertStoreLocation $CertStoreLocation ` + -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 + Write-AuditLog "Created new self-signed certificate with subject '$Subject' in $CertStoreLocation." + } + $output = [PSCustomObject]@{ + CertThumbprint = $Cert.Thumbprint + CertExpires = $Cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss') + } + # Only include AppName if provided (maintaining your original usage pattern) + if ($AppName) { + $output | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $AppName + } + return $output + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } + finally { + Write-AuditLog -EndFunction + } +} diff --git a/source/Public/Initialize-GraphEmailAppCert.ps1 b/source/Public/Initialize-GraphEmailAppCert.ps1 deleted file mode 100644 index 76ea96d..0000000 --- a/source/Public/Initialize-GraphEmailAppCert.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -<# - .SYNOPSIS - Retrieves or creates a new certificate for the Microsoft Graph Email app. - .DESCRIPTION - The Initialize-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:\> Initialize-GraphEmailAppCert -AppName "MyApp" -CertThumbprint "9B8B40C5F148B710AD5C0E5CC8D0B71B5A30DB0C" - .EXAMPLE - PS C:\> Initialize-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 Initialize-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. - $line = $_.InvocationInfo.Line - $lineNum = $_.InvocationInfo.ScriptLineNumber - throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) - } - write-AuditLog "Certificate with thumbprint $CertThumbprint created or retrieved from the CurrentUser's certificate store." - Write-AuditLog -EndFunction -} - diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 89f4800..5f6d465 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -36,53 +36,69 @@ function Publish-GraphEmailApp { [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' + begin { + if (-not $script:LogString) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + try { + Write-AuditLog '###############################################' + $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' + $ModParams = @{ + PublicModuleNames = $PublicMods + PublicRequiredVersions = $PublicVers + ImportModuleNames = $ImportMods + Scope = 'CurrentUser' + } + Initialize-ModuleEnv @ModParams + Connect-ToMsService -MgGraph -ExchangeOnline + $AppSettings = New-GraphEmailAppContext -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" + $CertDetails = Initialize-Certificate ` + -AppName $AppSettings.AppName ` + -Thumbprint $CertThumbprint ` + -Subject "CN=$($AppSettings.AppName)" + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } } - if (!($script:LogString)) { - Write-AuditLog -Start + process { + try { + # Register App + $appRegistration = New-EnterpriseAppRegistration ` + -DisplayName $AppSettings.AppName ` + -CertThumbprint $CertDetails.CertThumbprint ` + -ResourceAppId $AppSettings.GraphResourceId ` + -PermissionIds $AppSettings.ResId -SignInAudience 'AzureADMyOrg' + # Set App Config + Set-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.' + [void](New-ExchangeEmailAppPolicy -AppRegistration $appRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup) + # Set App Secret + $output = Set-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration ` + -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User ` + -MailEnabledSendingGroup $MailEnabledSendingGroup -DefaultDomain $MailEnabledSendingGroup.Split('@')[1] + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } } - else { - Write-AuditLog -BeginFunction - } - try { - Write-AuditLog '###############################################' - Initialize-ModuleEnv @params1 - Connect-ToMsService -MgGraph -ExchangeOnline - $AppSettings = New-GraphEmailAppContext -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" - $CertDetails = Initialize-GraphEmailAppCert -AppName $AppSettings.AppName -CertThumbprint $CertThumbprint - $appRegistration = New-EnterpriseAppRegistration ` - -DisplayName $AppSettings.AppName ` - -CertThumbprint $CertDetails.CertThumbprint ` - -ResourceAppId $AppSettings.GraphResourceId ` - -PermissionIds $AppSettings.ResId -SignInAudience 'AzureADMyOrg' # e.g. 'Mail.Send' - # $appRegistration = Register-GraphApp -AppName $AppSettings.AppName -GraphResourceId $AppSettings.graphResourceId -ResID $AppSettings.ResId -CertThumbprint $CertDetails.CertThumbprint - Set-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 = Set-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration ` - -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User ` - -MailEnabledSendingGroup $MailEnabledSendingGroup -DefaultDomain $MailEnabledSendingGroup.Split('@')[1] + end { + # Return output return $output } - catch { - $line = $_.InvocationInfo.Line - $lineNum = $_.InvocationInfo.ScriptLineNumber - throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) - } -} \ No newline at end of file +} From 12efeefae3e7e428943fefb5203b93474873f472 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 14:50:13 -0600 Subject: [PATCH 25/31] fix: refactor functions --- source/Private/Set-AppSecret.ps1 | 90 ------------------------- source/Private/Set-JsonSecret.ps1 | 51 ++++++++++++++ source/Public/Publish-GraphEmailApp.ps1 | 34 ++++++++-- 3 files changed, 79 insertions(+), 96 deletions(-) delete mode 100644 source/Private/Set-AppSecret.ps1 create mode 100644 source/Private/Set-JsonSecret.ps1 diff --git a/source/Private/Set-AppSecret.ps1 b/source/Private/Set-AppSecret.ps1 deleted file mode 100644 index c6dd361..0000000 --- a/source/Private/Set-AppSecret.ps1 +++ /dev/null @@ -1,90 +0,0 @@ -function Set-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, - [Parameter( - Mandatory = $true, - HelpMessage = 'The Default Domain' - )] - [string]$DefaultDomain - ) - # Begin Logging - if (!($script:LogString)) { - Write-AuditLog -Start - } - else { - Write-AuditLog -BeginFunction - } - try { - $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } - if (!(Get-SecretVault -Name GraphEmailAppLocalStore)) { - Write-AuditLog -Message 'Registering CredMan Secret Vault' - Register-SecretVault -Name GraphEmailAppLocalStore -ModuleName 'SecretManagement.JustinGrote.CredMan' -ErrorAction Stop - Write-AuditLog -Message 'Secret Vault: GraphEmailAppLocalStore registered.' - } - 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 - AppName = "CN=$AppName" - AppRestrictedSendGroup = $MailEnabledSendingGroup - CertExpires = ($Cert.NotAfter).ToString('yyyy-MM-dd HH:mm:ss') - CertThumbprint = $CertThumbprint - DefaultDomain = $DefaultDomain - SendAsUser = ($User.UserPrincipalName.Split('@')[0]) - SendAsUserEmail = $User.UserPrincipalName - TenantID = $Context.TenantId - } - $SecretJson = $output | ConvertTo-Json -Compress - Set-Secret -Name "CN=$AppName" -Secret $SecretJson -Vault GraphEmailAppLocalStore -ErrorAction Stop - Write-AuditLog -Message "Returning output. Save the AppName $("CN=$AppName"). The AppName will be needed to retrieve the secret containing authentication info." - Write-Host 'You can use the following values as input into the email function!' -ForegroundColor Green - Write-AuditLog -EndFunction - # Now simply call ConvertTo-ParameterSplat passing in the output PSObject - Write-Output ($output | ConvertTo-ParameterSplat) - } - catch { - Write-AuditLog -Severity Error -Message $_.Exception.Message - $line = $_.InvocationInfo.Line - $lineNum = $_.InvocationInfo.ScriptLineNumber - throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) - } -} diff --git a/source/Private/Set-JsonSecret.ps1 b/source/Private/Set-JsonSecret.ps1 new file mode 100644 index 0000000..c8403e4 --- /dev/null +++ b/source/Private/Set-JsonSecret.ps1 @@ -0,0 +1,51 @@ +function Set-JsonSecret { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true,HelpMessage='The name under which to store the secret.')] + [string]$Name, + [Parameter(Mandatory=$true,HelpMessage='The object to convert to JSON and store.')] + [PSObject]$InputObject, + [Parameter(Mandatory=$false,HelpMessage='Name of the vault. Defaults to GraphEmailAppLocalStore.')] + [string]$VaultName='GraphEmailAppLocalStore', + [Parameter(Mandatory=$false,HelpMessage='Name of the vault module to use if auto-registering. Defaults to SecretManagement.JustinGrote.CredMan.')] + [string]$VaultModuleName='SecretManagement.JustinGrote.CredMan', + [Parameter(Mandatory=$false,HelpMessage='Overwrite existing secret of the same name without prompting.')] + [switch]$Overwrite + ) + if(!($script:LogString)){Write-AuditLog -Start}else{Write-AuditLog -BeginFunction} + try{ + Write-AuditLog "###############################################" + # Auto-register vault if missing + if(!(Get-SecretVault -Name $VaultName -ErrorAction SilentlyContinue)){ + Write-AuditLog -Message "Registering $VaultName using $VaultModuleName" + Register-SecretVault -Name $VaultName -ModuleName $VaultModuleName -ErrorAction Stop + Write-AuditLog -Message "Vault '$VaultName' registered." + } + else{ + Write-AuditLog "Vault '$VaultName' is already registered." + } + # Check if secret already exists + $secretExists=(Get-SecretInfo -Name $Name -Vault $VaultName -ErrorAction SilentlyContinue) + if($secretExists){ + if($Overwrite){ + Write-AuditLog -Message "Overwriting existing secret '$Name' in vault '$VaultName'." + Remove-Secret -Name $Name -Vault $VaultName -Confirm:$false -ErrorAction Stop + } + else{ + Write-AuditLog -Message "Secret '$Name' already exists. Remove it or specify -Overwrite to overwrite." -Severity Warning + return + } + } + $json=($InputObject | ConvertTo-Json -Compress) + Set-Secret -Name $Name -Secret $json -Vault $VaultName -ErrorAction Stop + Write-AuditLog -Message "Secret '$Name' saved to vault '$VaultName'." + Write-AuditLog -EndFunction + return $Name + } + catch{ + Write-AuditLog -Severity Error -Message $_.Exception.Message + $line=$_.InvocationInfo.Line + $lineNum=$_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)",$_.Exception) + } +} diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 5f6d465..b1f9aa9 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -34,7 +34,9 @@ function Publish-GraphEmailApp { [Parameter(Mandatory = $true, HelpMessage = 'The username of the authorized sender.')] [string]$AuthorizedSenderUserName, [Parameter(Mandatory = $true, HelpMessage = 'The Mail Enabled Sending Group.')] - [string]$MailEnabledSendingGroup + [string]$MailEnabledSendingGroup, + [Parameter(Mandatory = $false, HelpMessage = 'Return the parameter splat for use in other functions.')] + [switch]$DoNotReturnParamSplat ) begin { if (-not $script:LogString) { @@ -80,13 +82,28 @@ function Publish-GraphEmailApp { -ResourceAppId $AppSettings.GraphResourceId ` -PermissionIds $AppSettings.ResId -SignInAudience 'AzureADMyOrg' # Set App Config - Set-GraphEmailAppConfig -AppRegistration $appRegistration -GraphServicePrincipalId $AppSettings.GraphServicePrincipal.Id -Context $AppSettings.Context -CertThumbprint $CertDetails.CertThumbprint + Set-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.' [void](New-ExchangeEmailAppPolicy -AppRegistration $appRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup) # Set App Secret - $output = Set-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration ` - -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User ` - -MailEnabledSendingGroup $MailEnabledSendingGroup -DefaultDomain $MailEnabledSendingGroup.Split('@')[1] + $output = [PSCustomObject]@{ + AppId = $appRegistration.AppId + AppName = "CN=$($AppSettings.AppName)" + AppRestrictedSendGroup = $MailEnabledSendingGroup + CertExpires = ($CertDetails.CertExpires) + CertThumbprint = $CertDetails.CertThumbprint + DefaultDomain = $MailEnabledSendingGroup.Split('@')[1] + SendAsUser = ($AppSettings.User.UserPrincipalName.Split('@')[0]) + SendAsUserEmail = $AppSettings.User.UserPrincipalName + TenantID = $AppSettings.Context.TenantId + } + # Store it as JSON in the vault + $name = Set-JsonSecret -Name "CN=$($AppSettings.AppName)" -InputObject $output -VaultName 'GraphEmailAppLocalStore' -Overwrite + Write-AuditLog "Secret '$name' saved to vault 'GraphEmailAppLocalStore'." } catch { $line = $_.InvocationInfo.Line @@ -99,6 +116,11 @@ function Publish-GraphEmailApp { } end { # Return output - return $output + if ($DoNotReturnParamSplat) { + return $output + } + else { + Write-Output ($output | ConvertTo-ParameterSplat) + } } } From 84abb8a733dc5586bf238e5fcbba7c40fc74e9ee Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 15:24:47 -0600 Subject: [PATCH 26/31] fix: update audit function --- source/Private/Write-AuditLog.ps1 | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/source/Private/Write-AuditLog.ps1 b/source/Private/Write-AuditLog.ps1 index 0fe29a9..15a8178 100644 --- a/source/Private/Write-AuditLog.ps1 +++ b/source/Private/Write-AuditLog.ps1 @@ -112,13 +112,23 @@ function Write-AuditLog { $ErrorActionPreference = 'SilentlyContinue' # Define variables to hold information about the command that was invoked. $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*' - $FuncName = (Get-PSCallStack)[1].Command + $callStack = Get-PSCallStack + if ($callStack.Count -gt 1) { + $FuncName = $callStack[1].Command + } + else { + $FuncName = 'DirectCall' # Or any other default name you prefer + } + #Write-Verbose "Funcname Name is $FuncName!" -Verbose $ModuleVer = $MyInvocation.MyCommand.Version.ToString() # Set the error action preference to continue. $ErrorActionPreference = 'Continue' } process { try { + if (-not $Start -and -not (Test-Path variable:script:LogString)) { + throw "The logging variable is not initialized. Please call Write-AuditLog with the -Start switch or ensure $script:LogString is set." + } $Function = $($FuncName + '.v' + $ModuleVer) if ($Start) { $script:LogString = @() @@ -171,24 +181,24 @@ function Write-AuditLog { 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 + throw 'Script execution stopped by user.' } } 'Error' { Write-Error ('[ERROR] X - ' + $FuncName + ' ' + $Message) -ErrorAction Continue } 'Verbose' { Write-Verbose ('[VERBOSE] ~ ' + $Message) } - Default { Write-Information ('[INFORMATION] * ' + $Message) -InformationAction Continue } + Default { Write-Information ('[INFO] * ' + $Message) -InformationAction Continue } } } catch { - throw "Write-AuditLog encountered an error (process block): $($_.Exception.Message)" + throw "Write-AuditLog encountered an error (process block): $($_)" } + } end { try { if ($End) { if (-not [string]::IsNullOrEmpty($OutputPath)) { - $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8 + $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)" } else { @@ -200,4 +210,4 @@ function Write-AuditLog { throw "Error in Write-AuditLog (end block): $($_.Exception.Message)" } } -} +} \ No newline at end of file From 546fd23d98a68f39d4f528033d40893ec374d949 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:00:56 -0600 Subject: [PATCH 27/31] fix: refactor rc prior to additional scenarios --- .../Initialize-GraphAppRegistration.ps1 | 105 ++++++++++++++++++ source/Private/New-GraphAppName.ps1 | 52 +++++++++ source/Private/New-GraphEmailAppContext.ps1 | 56 ---------- source/Private/New-MgGraphContextObject.ps1 | 65 +++++++++++ source/Private/Set-GraphEmailAppConfig.ps1 | 73 ------------ source/Public/New-MailEnabledSendingGroup.ps1 | 25 ++++- source/Public/Publish-GraphEmailApp.ps1 | 22 +++- 7 files changed, 260 insertions(+), 138 deletions(-) create mode 100644 source/Private/Initialize-GraphAppRegistration.ps1 create mode 100644 source/Private/New-GraphAppName.ps1 delete mode 100644 source/Private/New-GraphEmailAppContext.ps1 create mode 100644 source/Private/New-MgGraphContextObject.ps1 delete mode 100644 source/Private/Set-GraphEmailAppConfig.ps1 diff --git a/source/Private/Initialize-GraphAppRegistration.ps1 b/source/Private/Initialize-GraphAppRegistration.ps1 new file mode 100644 index 0000000..7b9830c --- /dev/null +++ b/source/Private/Initialize-GraphAppRegistration.ps1 @@ -0,0 +1,105 @@ +function Initialize-GraphAppRegistration { + [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 = $false, + HelpMessage = 'One or more OAuth2 scopes to grant. Defaults to Mail.Send.' + )] + [string[]]$Scopes = @('Mail.Send'), + [Parameter( + Mandatory = $false, + HelpMessage = 'Auth method (placeholder). Currently only "Certificate" is used.' + )] + [ValidateSet('Certificate','ClientSecret','ManagedIdentity','None')] + [string]$AuthMethod = 'Certificate', + [Parameter( + Mandatory = $false, + HelpMessage = 'Certificate thumbprint if using Certificate-based auth.' + )] + [string]$CertThumbprint + ) + begin { + if (-not $script:LogString) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + Write-AuditLog '###############################################' + if ($AuthMethod -eq 'Certificate' -and -not $CertThumbprint) { + throw "CertThumbprint is required when AuthMethod is 'Certificate'." + } + } + process { + try { + # 1. If using certificate auth, retrieve the certificate + $Cert = $null + if ($AuthMethod -eq 'Certificate') { + Write-AuditLog "Retrieving certificate with thumbprint $CertThumbprint." + $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } + if (-not $Cert) { + throw "Certificate with thumbprint $CertThumbprint not found in Cert:\CurrentUser\My." + } + } + # 2. Create a Service Principal for the app (if not existing). + Write-AuditLog "Creating service principal for app with AppId $($AppRegistration.AppId)." + [void](New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{}) + # 3. Get the client Service Principal for the created app. + $ClientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'" + if (-not $ClientSp) { + Write-AuditLog "Client service principal not found for $($AppRegistration.AppId)." -Severity Error + throw "Unable to find client service principal." + } + # 4. Grant each scope in $Scopes + foreach ($scope in $Scopes) { + Write-AuditLog "Granting '$scope' to Service Principal $($ClientSp.DisplayName)." + $Params = @{ + 'ClientId' = $ClientSp.Id + 'ConsentType' = 'AllPrincipals' + 'ResourceId' = $GraphServicePrincipalId + 'Scope' = $scope + } + [void](New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false) + } + # 5. Build the admin consent URL + $adminConsentUrl = 'https://login.microsoftonline.com/' + $Context.TenantId + '/adminconsent?client_id=' + $AppRegistration.AppId + Write-Verbose 'Please go to the following URL in your browser to provide admin consent:' -Verbose + Write-Host $adminConsentUrl -ForegroundColor DarkGray + Write-Verbose 'After providing admin consent, you can use the following command for certificate-based auth:' -Verbose + if ($AuthMethod -eq 'Certificate') { + $connectGraph = 'Connect-MgGraph -ClientId "' + $AppRegistration.AppId + '" -TenantId "'` + + $Context.TenantId + '" -CertificateName "' + $Cert.SubjectName.Name + '"' + Write-Host "`n$connectGraph`n" -ForegroundColor DarkGreen + } + else { + # Placeholder for other auth methods + Write-Host "Future logic for $AuthMethod auth can go here." + } + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } + Write-AuditLog -EndFunction + } + end {} +} \ No newline at end of file diff --git a/source/Private/New-GraphAppName.ps1 b/source/Private/New-GraphAppName.ps1 new file mode 100644 index 0000000..fa1aaa8 --- /dev/null +++ b/source/Private/New-GraphAppName.ps1 @@ -0,0 +1,52 @@ +function New-GraphAppName { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true, + HelpMessage='A short prefix for your app name (2-4 alphanumeric chars).')] + [ValidatePattern('^[A-Z0-9]{2,4}$')] + [string]$Prefix, + [Parameter(Mandatory=$false, + HelpMessage='Optional scenario name (e.g. AuditGraphEmail, MemPolicy, etc.).')] + [string]$ScenarioName = "GraphApp", + [Parameter(Mandatory=$false, + HelpMessage='Optional user email to append "As-[username]" suffix.')] + [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] + [string]$UserId + ) + begin { + if (-not $script:LogString) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } + } + process { + try { + Write-AuditLog "Building app name..." + # Build a user suffix if $UserId is provided + $userSuffix = "" + if ($UserId) { + # e.g. "helpdesk@mydomain.com" -> "As-helpDesk" + $userPrefix = ($UserId.Split('@')[0]) + $userSuffix = "-As-$userPrefix" + } + # Example final: "CORP-AuditGraphEmail-AD.MYDOMAIN.COM-As-helpDesk" + # But you can do anything you want with $env:USERDNSDOMAIN, etc. + $domainSuffix = $env:USERDNSDOMAIN + if (-not $domainSuffix) { + # fallback if not set + $domainSuffix = "MyDomain" + } + $appName = "$Prefix-$ScenarioName-$domainSuffix$userSuffix" + Write-AuditLog "Returning app name: $appName" + return $appName + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } + finally { + Write-AuditLog -EndFunction + } + } +} diff --git a/source/Private/New-GraphEmailAppContext.ps1 b/source/Private/New-GraphEmailAppContext.ps1 deleted file mode 100644 index 1aea9c9..0000000 --- a/source/Private/New-GraphEmailAppContext.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -function New-GraphEmailAppContext { - [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-Z0-9]{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 - } - try { - 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 - } - catch { - $line = $_.InvocationInfo.Line - $lineNum = $_.InvocationInfo.ScriptLineNumber - throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) - } - } -} diff --git a/source/Private/New-MgGraphContextObject.ps1 b/source/Private/New-MgGraphContextObject.ps1 new file mode 100644 index 0000000..1b8a936 --- /dev/null +++ b/source/Private/New-MgGraphContextObject.ps1 @@ -0,0 +1,65 @@ +function New-MgGraphContextObject { + [OutputType([pscustomobject])] + [CmdletBinding()] + param ( + [Parameter( + Mandatory = $false, + HelpMessage = 'An array of Graph permission names. Defaults to "Mail.Send".' + )] + [string[]]$Permissions = @("Mail.Send") + ) + process { + if (-not $script:LogString) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + try { + Write-AuditLog '###############################################' + Write-AuditLog "Retrieving current MgContext..." + $context = Get-MgContext + Write-AuditLog "Looking up Microsoft Graph service principal..." + $graphServicePrincipal = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" + if (-not $graphServicePrincipal) { + throw "Microsoft Graph Service Principal not found!" + } + $graphResourceId = $graphServicePrincipal.AppId + Write-AuditLog "Microsoft Graph Service Principal AppId is $graphResourceId." + # Collect all found permission IDs + $resIds = @() + foreach ($permName in $Permissions) { + Write-AuditLog "Searching for application permission '$permName'..." + $foundPerm = Find-MgGraphPermission -PermissionType Application -All | Where-Object { $_.Name -eq $permName } + if ($foundPerm) { + $resIds += $foundPerm.Id + Write-AuditLog "Found permission ID for '$permName': $($foundPerm.Id)" + } + else { + Write-AuditLog -Severity Warning -Message "Permission '$permName' not found!" + } + } + # Build final object + $result = [PSCustomObject]@{ + GraphDisplayName = $graphServicePrincipal.DisplayName + Context = $context + GraphServicePrincipal = $graphServicePrincipal + GraphResourceId = $graphResourceId + ResId = $resIds + } + Write-AuditLog "Returning Graph context object." + return $result + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } + finally { + Write-AuditLog -EndFunction + } + } +} diff --git a/source/Private/Set-GraphEmailAppConfig.ps1 b/source/Private/Set-GraphEmailAppConfig.ps1 deleted file mode 100644 index 68c8a24..0000000 --- a/source/Private/Set-GraphEmailAppConfig.ps1 +++ /dev/null @@ -1,73 +0,0 @@ -function Set-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 - } - # TODO Find out if I can change the scope where cert is located and how that affects who and what can access it. - $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. - [void](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' - } - Write-AuditLog "Creating OAuth2 Permission Grant for $($ClientSp.DisplayName)" - [void](New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false) - # Create the admin consent url: - $adminConsentUrl = 'https://login.microsoftonline.com/' + $Context.TenantId + '/adminconsent?client_id=' + $AppRegistration.AppId - Write-Verbose 'Please go to the following URL in your browser to provide admin consent:' -Verbose - Write-Host $adminConsentUrl -ForegroundColor DarkGray - Write-Verbose 'After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:' -Verbose - # 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-Host "`n$connectGraph`n" -ForegroundColor DarkGreen - } - catch { - $line = $_.InvocationInfo.Line - $lineNum = $_.InvocationInfo.ScriptLineNumber - throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) - } - Write-AuditLog -EndFunction - } -} - - diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 index 4b31b96..9e94e58 100644 --- a/source/Public/New-MailEnabledSendingGroup.ps1 +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -1,16 +1,21 @@ function New-MailEnabledSendingGroup { [CmdletBinding(DefaultParameterSetName = 'CustomDomain')] param ( - [Parameter(Mandatory = $true, HelpMessage = 'Specifies the name of the mail enabled sending group.')] + [Parameter(Mandatory = $true, + HelpMessage = 'Specifies the name of the mail enabled sending group.')] [string]$Name, - [Parameter(Mandatory = $false, HelpMessage = 'Optional alias for the group. If not provided, the group name will be used.')] + [Parameter(Mandatory = $false, + HelpMessage = 'Optional alias for the group. If not provided, the group name will be used.')] [string]$Alias, - [Parameter(Mandatory = $true, ParameterSetName = 'CustomDomain', HelpMessage = 'Specifies the primary SMTP address for the group when using a custom domain.')] + [Parameter(Mandatory = $true, + ParameterSetName = 'CustomDomain', + HelpMessage = 'Specifies the primary SMTP address for the group when using a custom domain.')] [string]$PrimarySmtpAddress, - [Parameter(Mandatory = $true, ParameterSetName = 'DefaultDomain', HelpMessage = 'Specifies the default domain to construct the primary SMTP address (alias@DefaultDomain) for the group.')] + [Parameter(Mandatory = $true, + ParameterSetName = 'DefaultDomain', + HelpMessage = 'Specifies the default domain to construct the primary SMTP address (alias@DefaultDomain) for the group.')] [string]$DefaultDomain ) - # Begin Logging if (!($script:LogString)) { Write-AuditLog -Start } @@ -28,6 +33,11 @@ function New-MailEnabledSendingGroup { # Check if the distribution group already exists $existingGroup = Get-DistributionGroup -Identity $Name -ErrorAction SilentlyContinue if ($existingGroup) { + # Confirm the group is security-enabled + # $existingGroup.GroupType might be something like "Universal, SecurityEnabled" + if ($existingGroup.GroupType -notmatch 'SecurityEnabled') { + throw "Group '$Name' exists but is not SecurityEnabled. Please provide a mail-enabled security group." + } Write-AuditLog -Message "Distribution group '$Name' already exists. Returning existing group." return $existingGroup } @@ -47,7 +57,10 @@ function New-MailEnabledSendingGroup { Write-AuditLog -Severity Error -Message $_.Exception.Message $line = $_.InvocationInfo.Line $lineNum = $_.InvocationInfo.ScriptLineNumber - throw [System.Management.Automation.RuntimeException]::new("Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", $_.Exception) + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) } finally { Write-AuditLog -EndFunction diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index b1f9aa9..9ff7fc5 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -30,10 +30,13 @@ function Publish-GraphEmailApp { [ValidatePattern('^[A-Z0-9]{2,4}$')] [string]$AppPrefix, [Parameter(Mandatory = $false, HelpMessage = 'The thumbprint of the certificate to be retrieved.')] + [ValidatePattern('^[A-Fa-f0-9]{40}$')] [string]$CertThumbprint, [Parameter(Mandatory = $true, HelpMessage = 'The username of the authorized sender.')] + [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] [string]$AuthorizedSenderUserName, [Parameter(Mandatory = $true, HelpMessage = 'The Mail Enabled Sending Group.')] + [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] [string]$MailEnabledSendingGroup, [Parameter(Mandatory = $false, HelpMessage = 'Return the parameter splat for use in other functions.')] [switch]$DoNotReturnParamSplat @@ -58,7 +61,17 @@ function Publish-GraphEmailApp { } Initialize-ModuleEnv @ModParams Connect-ToMsService -MgGraph -ExchangeOnline - $AppSettings = New-GraphEmailAppContext -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" + # Verify if user exists and store object + $user = Get-MgUser -Filter "Mail eq '$AuthorizedSenderUserName'" + if (-not $user) { + throw "User '$AuthorizedSenderUserName' not found in the tenant." + } + $AppSettings = New-MgGraphContextObject -Permissions 'Mail.Send' + $appName = New-GraphAppName -Prefix $AppPrefix ` + -ScenarioName 'AuditGraphEmail' ` + -UserId $AuthorizedSenderUserName + $AppSettings | Add-Member -NotePropertyName 'User' -NotePropertyValue $user + $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName $CertDetails = Initialize-Certificate ` -AppName $AppSettings.AppName ` -Thumbprint $CertThumbprint ` @@ -82,12 +95,15 @@ function Publish-GraphEmailApp { -ResourceAppId $AppSettings.GraphResourceId ` -PermissionIds $AppSettings.ResId -SignInAudience 'AzureADMyOrg' # Set App Config - Set-GraphEmailAppConfig ` + Initialize-GraphAppRegistration ` -AppRegistration $appRegistration ` -GraphServicePrincipalId $AppSettings.GraphServicePrincipal.Id ` -Context $AppSettings.Context ` - -CertThumbprint $CertDetails.CertThumbprint + -AuthMethod 'Certificate' ` + -CertThumbprint $CertDetails.CertThumbprint ` + -Scopes 'Mail.Send' Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.' + # Exchange Online App Policy [void](New-ExchangeEmailAppPolicy -AppRegistration $appRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup) # Set App Secret $output = [PSCustomObject]@{ From 5c29408bf30616b9e46286d3cd9c7aee825c477f Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:18:28 -0600 Subject: [PATCH 28/31] fix: consolidate functions --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 437d85c..07ebf55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Some Error handling improvements. - Function names are now more consistent with approved verbs. - Refactored code to improve readability. +- Consolidated functions to reduce complexity. ## [0.1.0] - 2023-07-15 From 5d79a3f610d20fd5674e69420dda6fe82cd323d2 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 19:18:56 -0600 Subject: [PATCH 29/31] fix: consolidate functions --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ebf55..f43e831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Function names are now more consistent with approved verbs. - Refactored code to improve readability. - Consolidated functions to reduce complexity. +- Minor Change to README.md. ## [0.1.0] - 2023-07-15 From 2bcb37c47fc2cedc0924f17e98eade2898822763 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Sun, 2 Mar 2025 21:07:46 -0600 Subject: [PATCH 30/31] add: MEMPolicyManager scenario --- .../Initialize-GraphAppRegistration.ps1 | 25 ++- source/Public/Publish-GraphEmailApp.ps1 | 50 ++++-- source/Public/Publish-MemPolicyManagerApp.ps1 | 148 ++++++++++++++++++ 3 files changed, 198 insertions(+), 25 deletions(-) create mode 100644 source/Public/Publish-MemPolicyManagerApp.ps1 diff --git a/source/Private/Initialize-GraphAppRegistration.ps1 b/source/Private/Initialize-GraphAppRegistration.ps1 index 7b9830c..a11fdfc 100644 --- a/source/Private/Initialize-GraphAppRegistration.ps1 +++ b/source/Private/Initialize-GraphAppRegistration.ps1 @@ -65,25 +65,24 @@ function Initialize-GraphAppRegistration { Write-AuditLog "Client service principal not found for $($AppRegistration.AppId)." -Severity Error throw "Unable to find client service principal." } - # 4. Grant each scope in $Scopes - foreach ($scope in $Scopes) { - Write-AuditLog "Granting '$scope' to Service Principal $($ClientSp.DisplayName)." - $Params = @{ - 'ClientId' = $ClientSp.Id - 'ConsentType' = 'AllPrincipals' - 'ResourceId' = $GraphServicePrincipalId - 'Scope' = $scope - } - [void](New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false) + # 4. Combine all scopes into a single space-delimited string + $combinedScopes = $Scopes -join ' ' + Write-AuditLog "Granting the following scope(s) to Service Principal $($ClientSp.DisplayName): $combinedScopes" + $Params = @{ + ClientId = $ClientSp.Id + ConsentType = 'AllPrincipals' + ResourceId = $GraphServicePrincipalId + Scope = $combinedScopes } + [void](New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false) # 5. Build the admin consent URL $adminConsentUrl = 'https://login.microsoftonline.com/' + $Context.TenantId + '/adminconsent?client_id=' + $AppRegistration.AppId Write-Verbose 'Please go to the following URL in your browser to provide admin consent:' -Verbose Write-Host $adminConsentUrl -ForegroundColor DarkGray Write-Verbose 'After providing admin consent, you can use the following command for certificate-based auth:' -Verbose if ($AuthMethod -eq 'Certificate') { - $connectGraph = 'Connect-MgGraph -ClientId "' + $AppRegistration.AppId + '" -TenantId "'` - + $Context.TenantId + '" -CertificateName "' + $Cert.SubjectName.Name + '"' + $connectGraph = 'Connect-MgGraph -ClientId "' + $AppRegistration.AppId + '" -TenantId "' + + $Context.TenantId + '" -CertificateName "' + $Cert.SubjectName.Name + '"' Write-Host "`n$connectGraph`n" -ForegroundColor DarkGreen } else { @@ -102,4 +101,4 @@ function Initialize-GraphAppRegistration { Write-AuditLog -EndFunction } end {} -} \ No newline at end of file +} diff --git a/source/Public/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 index 9ff7fc5..f569735 100644 --- a/source/Public/Publish-GraphEmailApp.ps1 +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -26,20 +26,46 @@ function Publish-GraphEmailApp { [CmdletBinding()] param( - [Parameter(Mandatory = $true, HelpMessage = 'The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.')] + [Parameter( + Mandatory = $true, + HelpMessage = 'The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.' + )] [ValidatePattern('^[A-Z0-9]{2,4}$')] - [string]$AppPrefix, - [Parameter(Mandatory = $false, HelpMessage = 'The thumbprint of the certificate to be retrieved.')] + [string] + $AppPrefix, + [Parameter( + Mandatory = $false, + HelpMessage = 'The thumbprint of the certificate to be retrieved.' + )] [ValidatePattern('^[A-Fa-f0-9]{40}$')] - [string]$CertThumbprint, - [Parameter(Mandatory = $true, HelpMessage = 'The username of the authorized sender.')] + [string] + $CertThumbprint, + [Parameter( + Mandatory = $true, + HelpMessage = 'The username of the authorized sender.' + )] [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] - [string]$AuthorizedSenderUserName, - [Parameter(Mandatory = $true, HelpMessage = 'The Mail Enabled Sending Group.')] + [string] + $AuthorizedSenderUserName, + [Parameter( + Mandatory = $true, + HelpMessage = 'The Mail Enabled Sending Group.' + )] [ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')] - [string]$MailEnabledSendingGroup, - [Parameter(Mandatory = $false, HelpMessage = 'Return the parameter splat for use in other functions.')] - [switch]$DoNotReturnParamSplat + [string] + $MailEnabledSendingGroup, + [Parameter( + Mandatory = $false, + HelpMessage = 'If specified, use a custom vault name. Otherwise, use the default.' + )] + [string] + $VaultName = 'GraphEmailAppLocalStore', + [Parameter( + Mandatory = $false, + HelpMessage = 'Return the parameter splat for use in other functions.' + )] + [switch] + $DoNotReturnParamSplat ) begin { if (-not $script:LogString) { @@ -118,8 +144,8 @@ function Publish-GraphEmailApp { TenantID = $AppSettings.Context.TenantId } # Store it as JSON in the vault - $name = Set-JsonSecret -Name "CN=$($AppSettings.AppName)" -InputObject $output -VaultName 'GraphEmailAppLocalStore' -Overwrite - Write-AuditLog "Secret '$name' saved to vault 'GraphEmailAppLocalStore'." + $name = Set-JsonSecret -Name "CN=$($AppSettings.AppName)" -InputObject $output -VaultName $VaultName -Overwrite + Write-AuditLog "Secret '$name' saved to vault '$VaultName'." } catch { $line = $_.InvocationInfo.Line diff --git a/source/Public/Publish-MemPolicyManagerApp.ps1 b/source/Public/Publish-MemPolicyManagerApp.ps1 new file mode 100644 index 0000000..36dc820 --- /dev/null +++ b/source/Public/Publish-MemPolicyManagerApp.ps1 @@ -0,0 +1,148 @@ +function Publish-MemPolicyManagerApp { + [CmdletBinding()] + param( + [Parameter( + Mandatory = $true, + HelpMessage = '2-4 character prefix used for the App Name (e.g. MSN, CORP, etc.)' + )] + [ValidatePattern('^[A-Z0-9]{2,4}$')] + [string]$Prefix, + [Parameter( + Mandatory = $false, + HelpMessage = 'Thumbprint of the certificate. If omitted, a self-signed cert is created.' + )] + [ValidatePattern('^[A-Fa-f0-9]{40}$')] + [string]$CertThumbprint, + [Parameter( + Mandatory = $false, + HelpMessage = 'If specified, use a custom vault name. Otherwise, use the default.' + )] + # TODO Change default vault name to 'MemPolicyManagerLocalStore' + [string]$VaultName = 'GraphEmailAppLocalStore', + [Parameter( + Mandatory = $false, + HelpMessage = 'If specified, overwrite the vault secret if it already exists.' + )] + [switch]$OverwriteVaultSecret, + [Parameter( + HelpMessage = 'If specified, grant ReadWrite perms. Otherwise, read-only perms.' + )] + [switch]$ReadWrite, + [Parameter( + Mandatory = $false, + HelpMessage = 'Return the param splat for use in other functions.' + )] + [switch]$DoNotReturnParamSplat + ) + begin { + if (-not $script:LogString) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + try { + Write-AuditLog '###############################################' + $PublicMods = 'Microsoft.Graph', 'Microsoft.PowerShell.SecretManagement', 'SecretManagement.JustinGrote.CredMan' + $PublicVers = '1.22.0', '1.1.2', '1.0.0' + $ImportMods = 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.Identity.SignIns', 'Microsoft.Graph.Users' + $ModParams = @{ + PublicModuleNames = $PublicMods + PublicRequiredVersions = $PublicVers + ImportModuleNames = $ImportMods + Scope = 'CurrentUser' + } + Initialize-ModuleEnv @ModParams + # Only connect to Graph + Connect-ToMsService -MgGraph + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } + } + process { + try { + # 1) Determine the correct set of MEM permissions + # (We can expand or tweak these as needed) + $readWritePerms = @( + 'DeviceManagementConfiguration.ReadWrite.All', + 'DeviceManagementApps.ReadWrite.All', + 'DeviceManagementManagedDevices.ReadWrite.All', + 'Policy.ReadWrite.ConditionalAccess', + 'Policy.Read.All' + ) + $readOnlyPerms = @( + 'DeviceManagementConfiguration.Read.All', + 'DeviceManagementApps.Read.All', + 'DeviceManagementManagedDevices.Read.All', + 'Policy.Read.ConditionalAccess' + 'Policy.Read.All' + ) + $permissions = if ($ReadWrite) { $readWritePerms } else { $readOnlyPerms } + Write-AuditLog "Using the following MEM permissions: $($permissions -join ', ')" + # 2) Build a Graph context object that looks up these permission IDs + $AppSettings = New-MgGraphContextObject -Permissions $permissions + # 3) Build an app name for scenario "MemPolicyManager" + $appName = New-GraphAppName -Prefix $Prefix -ScenarioName 'MemPolicyManager' + # 4) Add TenantId & AppName to the object so we can store them in the final JSON + $AppSettings | Add-Member -NotePropertyName 'TenantId' -NotePropertyValue $AppSettings.Context.TenantId + $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName + # 5) Create or retrieve the certificate + $CertDetails = Initialize-Certificate ` + -AppName $AppSettings.AppName ` + -Thumbprint $CertThumbprint ` + -Subject "CN=$($AppSettings.AppName)" + # 6) Register the application (with the cert) + $appRegistration = New-EnterpriseAppRegistration ` + -DisplayName $AppSettings.AppName ` + -CertThumbprint $CertDetails.CertThumbprint ` + -ResourceAppId $AppSettings.GraphResourceId ` + -PermissionIds $AppSettings.ResId ` + -SignInAudience 'AzureADMyOrg' + # 7) Create the Service Principal & grant the permissions (Initialize-GraphAppRegistration) + Initialize-GraphAppRegistration ` + -AppRegistration $appRegistration ` + -GraphServicePrincipalId $AppSettings.GraphServicePrincipal.Id ` + -Context $AppSettings.Context ` + -AuthMethod 'Certificate' ` + -CertThumbprint $CertDetails.CertThumbprint ` + -Scopes $permissions + # 8) Build a final PSCustomObject to store in the secret vault + $output = [PSCustomObject]@{ + AppId = $appRegistration.AppId + TenantId = $AppSettings.Context.TenantId + CertThumbprint = $CertDetails.CertThumbprint + AppName = "CN=$($AppSettings.AppName)" + Permissions = if ($ReadWrite) { 'ReadWrite' } else { 'ReadOnly' } + ClientId = $appRegistration.AppId + } + # 9) Store as JSON secret + $secretName = "CN=$($AppSettings.AppName)" + $savedName = Set-JsonSecret -Name $secretName -InputObject $output -VaultName $VaultName -Overwrite:$OverwriteVaultSecret + Write-AuditLog "Secret '$savedName' saved to vault '$VaultName'." + # Return the final object (param-splat or normal) + if ($DoNotReturnParamSplat) { + $output + } + else { + Write-Output ($output | ConvertTo-ParameterSplat) + } + } + catch { + $line = $_.InvocationInfo.Line + $lineNum = $_.InvocationInfo.ScriptLineNumber + throw [System.Management.Automation.RuntimeException]::new( + "Error in $($MyInvocation.MyCommand.Name) at line $lineNum`:`n'$line' - $($_.Exception.Message)", + $_.Exception + ) + } + } + end { + Write-AuditLog -EndFunction + } +} From 336f7868eb6fd0a34af6dfad78a5bca6c9bd84f7 Mon Sep 17 00:00:00 2001 From: DrIOS <58635327+DrIOSX@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:46:59 -0600 Subject: [PATCH 31/31] fix: formatting --- source/Public/Publish-MemPolicyManagerApp.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Public/Publish-MemPolicyManagerApp.ps1 b/source/Public/Publish-MemPolicyManagerApp.ps1 index 36dc820..027bc62 100644 --- a/source/Public/Publish-MemPolicyManagerApp.ps1 +++ b/source/Public/Publish-MemPolicyManagerApp.ps1 @@ -18,7 +18,7 @@ function Publish-MemPolicyManagerApp { HelpMessage = 'If specified, use a custom vault name. Otherwise, use the default.' )] # TODO Change default vault name to 'MemPolicyManagerLocalStore' - [string]$VaultName = 'GraphEmailAppLocalStore', + [string]$VaultName = 'MemPolicyManagerLocalStore', [Parameter( Mandatory = $false, HelpMessage = 'If specified, overwrite the vault secret if it already exists.'