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/CHANGELOG.md b/CHANGELOG.md index 761fac6..f43e831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,25 @@ 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 + +- 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. +- Consolidated functions to reduce complexity. +- Minor Change to README.md. + +## [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 433b0f4..42d71bb 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,36 @@ -# GraphEmailApp +# GraphEmailApp Module Functions -A module for creating a graphemail app +## Connect-ToMGGraph -## Make it yours +Connects to Microsoft Graph and Exchange Online. ---- -Generated with Plaster and the SampleModule template +- **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 -This is a sample Readme +Deploys Microsoft Graph Email app with app-only authentication. -## Make it yours +- **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. +- **Outputs**: Email sent, no direct output. diff --git a/source/Private/ConvertTo-ParameterSplat.ps1 b/source/Private/ConvertTo-ParameterSplat.ps1 new file mode 100644 index 0000000..698a50b --- /dev/null +++ b/source/Private/ConvertTo-ParameterSplat.ps1 @@ -0,0 +1,20 @@ +function ConvertTo-ParameterSplat { + [CmdletBinding()] + [OutputType([string])] + 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 deleted file mode 100644 index b492c28..0000000 --- a/source/Private/Get-AppSecret.ps1 +++ /dev/null @@ -1,95 +0,0 @@ -function Get-AppSecret { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true, HelpMessage = "The application name.")] - [string]$AppName, - - [Parameter(Mandatory = $true, HelpMessage = "The app registration object.")] - [PSObject]$AppRegistration, - - [Parameter(Mandatory = $true, HelpMessage = "The certificate thumbprint.")] - [string]$CertThumbprint, - - [Parameter(Mandatory = $true, HelpMessage = "The context object.")] - [PSObject]$Context, - - [Parameter(Mandatory = $true, HelpMessage = "The user object.")] - [PSObject]$User, - - [Parameter(Mandatory = $true, HelpMessage = "The mail enabled sending group.")] - [string]$MailEnabledSendingGroup - ) - - # Begin Logging - if (!($script:LogString)) { - Write-AuditLog -Start - } - else { - Write-AuditLog -BeginFunction - } - $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } - if (!(Get-SecretVault -Name GraphEmailAppLocalStore)) { - try { - Write-AuditLog -Message "Registering CredMan Secret Vault" - Register-SecretVault -Name GraphEmailAppLocalStore -ModuleName "SecretManagement.JustinGrote.CredMan" -ErrorAction Stop - Write-AuditLog -Message "Secret Vault: GraphEmailAppLocalStore registered." - } - catch { - throw $_.Exception - } - } - elseif ((Get-SecretInfo -Name "CN=$AppName" -Vault GraphEmailAppLocalStore) ) { - Write-AuditLog -Message "Secret found! Would you like to delete the previous configuration for `"CN=$AppName.`"?" -Severity Warning - try { - Remove-Secret -Name "CN=$AppName" -Vault GraphEmailAppLocalStore -Confirm:$false -ErrorAction Stop - Write-AuditLog -Message "Previous secret CN=$AppName removed." - } - catch { - throw $_.Exception - } - } - - $output = [PSCustomObject] @{ - AppId = $AppRegistration.AppId - CertThumbprint = $CertThumbprint - TenantID = $Context.TenantId - CertExpires = ($Cert.NotAfter).ToString("yyyy-MM-dd HH:mm:ss") - SendAsUser = $($User.UserPrincipalName.Split("@")[0]) - AppRestrictedSendGroup = $MailEnabledSendingGroup - Appname = "CN=$AppName" - } - - $delimiter = '|' - $joinedString = ($output.PSObject.Properties.Value) -join $delimiter - - try { - Set-Secret -Name "CN=$AppName" -Secret $joinedString -Vault GraphEmailAppLocalStore -ErrorAction Stop - } - catch { - throw $_.Exception - } - - Write-AuditLog -Message "Returning output. Save the AppName $("CN=$AppName"). The AppName will be needed to retreive the secret containing authentication info." - - Write-Host "You can use the following values as input into the email function!" -ForegroundColor Green - Write-AuditLog -EndFunction - $output | ForEach-Object { - $hashTable = @{} - $_.psobject.properties | ForEach-Object { - $hashTable[$_.Name] = $_.Value - } - - # Convert hashtable to script text - $splatScript = "`$params = @{`n" - $hashTable.Keys | ForEach-Object { - $value = $hashTable[$_] - if ($value -is [string]) { - $value = "`"$value`"" - } - $splatScript += " $_ = $value`n" - } - $splatScript += "}" - - Write-Output $splatScript - } -} diff --git a/source/Private/Get-GraphEmailAppConfig.ps1 b/source/Private/Get-GraphEmailAppConfig.ps1 deleted file mode 100644 index 3ff2e52..0000000 --- a/source/Private/Get-GraphEmailAppConfig.ps1 +++ /dev/null @@ -1,66 +0,0 @@ -function Get-GraphEmailAppConfig { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true, HelpMessage = "The App Registration object.")] - $AppRegistration, - - [Parameter(Mandatory = $true, HelpMessage = "The Graph Service Principal Id.")] - [string]$GraphServicePrincipalId, - - [Parameter(Mandatory = $true, HelpMessage = "The Azure context.")] - $Context, - - [Parameter(Mandatory = $true, HelpMessage = "The Certificate.")] - [string]$CertThumbPrint - ) - - begin { - if (!($script:LogString)) { - Write-AuditLog -Start - } - else { - Write-AuditLog -BeginFunction - } - $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } - Write-AuditLog "###############################################" - Write-AuditLog "Creating service principal for app with AppId $($AppRegistration.AppId)" - } - - process { - try { - # Create a Service Principal for the app. - New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{} - - # Get the client Service Principal for the created app. - $ClientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'" - if (!($ClientSp)) { - Write-AuditLog "Client service Principal not found for $($AppRegistration.AppId)" -Error - throw "Unable to find Client Service Principal." - } - - # Build the parameters for the New-MgOauth2PermissionGrant and create the grant. - $Params = @{ - "ClientId" = $ClientSp.Id - "ConsentType" = "AllPrincipals" - "ResourceId" = $GraphServicePrincipalId - "Scope" = "Mail.Send" - } - New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false - - # Create the admin consent url: - $adminConsentUrl = "https://login.microsoftonline.com/" + $Context.TenantId + "/adminconsent?client_id=" + $AppRegistration.AppId - Write-Output "Please go to the following URL in your browser to provide admin consent" - Write-Output $adminConsentUrl - Write-Output "After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:" - - # Generate graph command that can be used to connect later that can be copied and saved. - $connectGraph = "Connect-MgGraph -ClientId """ + $AppRegistration.AppId + """ -TenantId """` - + $Context.TenantId + """ -CertificateName """ + $Cert.SubjectName.Name + """" - Write-Output $connectGraph - } - catch { - throw $_.Exception - } - Write-AuditLog -EndFunction - } -} diff --git a/source/Private/Initialize-GraphAppRegistration.ps1 b/source/Private/Initialize-GraphAppRegistration.ps1 new file mode 100644 index 0000000..a11fdfc --- /dev/null +++ b/source/Private/Initialize-GraphAppRegistration.ps1 @@ -0,0 +1,104 @@ +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. 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 + '"' + 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 {} +} diff --git a/source/Private/Initialize-GraphEmailApp.ps1 b/source/Private/Initialize-GraphEmailApp.ps1 deleted file mode 100644 index c8e98e5..0000000 --- a/source/Private/Initialize-GraphEmailApp.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -function Initialize-GraphEmailApp { - [OutputType([pscustomobject])] - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true, HelpMessage = "The 2 to 4 character long prefix ID of the app, files and certs that are created.")] - [ValidatePattern('^[A-Z]{2,4}$')] - [string]$Prefix, - - [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")] - [ValidateNotNullOrEmpty()] - [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")] - [String] $UserId - ) - - process { - # Begin Logging Check - if (!($script:LogString)) { - Write-AuditLog -Start - } - else { - Write-AuditLog -BeginFunction - } - Write-AuditLog "###############################################" - - # Step 5: - # Get the MGContext - $context = Get-MgContext - # Step 6: - # Instantiate the user variable. - $user = Get-MgUser -Filter "Mail eq '$UserId'" - # Step 7: - # Define the application Name and Encrypted File Paths. - $AppName = "$($Prefix)-AuditGraphEmail-$($env:USERDNSDOMAIN)-As-$(($user.UserPrincipalName).Split("@")[0])" - $graphServicePrincipal = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" - $graphResourceId = $graphServicePrincipal.AppId - Write-AuditLog "Microsoft Graph Service Principal AppId is $graphResourceId" - # Step 9: - # Build resource requirements variable using Find-MgGraphCommand -Command New-MgApplication | Select -First 1 -ExpandProperty Permissions - # Find-MgGraphPermission -PermissionType Application -All | ? {$_.name -eq "Mail.Send"} - $resId = (Find-MgGraphPermission -PermissionType Application -All | Where-Object { $_.name -eq "Mail.Send" }).Id - - return @{ - "GraphDisplayname" = $graphServicePrincipal.DisplayName - "Context" = $context - "User" = $user - "AppName" = $AppName - "GraphServicePrincipal" = $graphServicePrincipal - "GraphResourceId" = $graphResourceId - "ResId" = $resId - } - Write-AuditLog -EndFunction - } -} diff --git a/source/Private/Initialize-ModuleEnv.ps1 b/source/Private/Initialize-ModuleEnv.ps1 index 74e01cf..158f735 100644 --- a/source/Private/Initialize-ModuleEnv.ps1 +++ b/source/Private/Initialize-ModuleEnv.ps1 @@ -1,26 +1,41 @@ -function Initialize-ModuleEnv { - <# - .SYNOPSIS - Initializes the environment by installing required PowerShell modules. - .DESCRIPTION - This function installs PowerShell modules required by the script. It can install public or pre-release versions of the module, and it supports installation for all users or current user. - .PARAMETER PublicModuleNames - An array of module names to be installed. Required when using the Public parameter set. - .PARAMETER PublicRequiredVersions - An array of required module versions to be installed. Required when using the Public parameter set. - .PARAMETER PrereleaseModuleNames - An array of pre-release module names to be installed. Required when using the Prerelease parameter set. - .PARAMETER PrereleaseRequiredVersions - An array of required pre-release module versions to be installed. Required when using the Prerelease parameter set. - .PARAMETER Scope - The scope of the module installation. Possible values are "AllUsers" and "CurrentUser". This determines the installation scope of the module. - .PARAMETER ImportModuleNames - The specific modules you'd like to import from the installed package to streamline imports. This is used when you want to import only specific modules from a package, rather than all of them. - .EXAMPLE - Initialize-ModuleEnv -PublicModuleNames "PSnmap", "Microsoft.Graph" -PublicRequiredVersions "1.3.1","1.23.0" -Scope AllUsers +<# + .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'. - This example installs the PSnmap and Microsoft.Graph modules in the AllUsers scope with the specified versions. - .EXAMPLE + .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. + + .EXAMPLE $params1 = @{ PublicModuleNames = "PSnmap","Microsoft.Graph" PublicRequiredVersions = "1.3.1","1.23.0" @@ -28,214 +43,154 @@ function Initialize-ModuleEnv { 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. - #> - [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 + 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)] + [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 + ) + 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 } } - # 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 + # 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 } } - - # 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 + if($hasNonDefaultVer){ # Import the latest version - Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version + $latestModule=$psGetModules|Sort-Object Version -Descending|Select-Object -First 1 + 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" - } + 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!' } - try { - Write-AuditLog "Install the latest version of PowershellGet from the PSGallery?" -Severity Warning - [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + else{ + Write-AuditLog 'Updating PowerShellGet...' + [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 + $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 } - catch { - throw $_.Exception - } } - # End Region PowershellGet Install - if ($Scope -eq "AllUsers") { - switch (Test-IsAdmin) { - $false { - Write-AuditLog "You must be an administrator to install in the `'AllUsers`' scope." -Severity Error - Write-AuditLog "If you intended to install the module only for this user, select the `'CurrentUser`' scope." -Severity Error - throw "Elevation required for `'AllUsers`' scope" - } - Default { - Write-AuditLog "You have sufficient privileges to install to the `'AllUsers`' scope." - } + # 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." + } + else{ + Write-AuditLog "Installing modules for 'AllUsers' scope." } } - if ($PSCmdlet.ParameterSetName -eq "Public") { - $modules = $PublicModuleNames - $versions = $PublicRequiredVersions + # 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 + elseif($PSCmdlet.ParameterSetName -eq 'Prerelease'){ + $modules=$PrereleaseModuleNames + $versions=$PrereleaseRequiredVersions + $prerelease=$true } - foreach ($module in $modules) { - $name = $module - $version = $versions[$modules.IndexOf($module)] - $installedModule = Get-Module -Name $name -ListAvailable - switch (($null -eq $ImportModuleNames)) { - $false { - $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name } - Write-AuditLog "Attempting to selecively install module/s:" - } - Default { - $SelectiveImports = $null - Write-AuditLog "Selective imports were not specified. All functions and commands will be imported." - } - } - # Get Module Object - switch ($prerelease) { - $true { - $message = "The PreRelease module $name version $version is not installed. Would you like to install it?" - $throwmsg = "You must install the PreRelease module $name version $version to continue" - } - Default { - $message = "The $name module version $version is not installed. Would you like to install it?" - $throwmsg = "You must install the $name module version $version to continue." - } + # 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 (!$installedModule) { - # Install Required Module - Write-AuditLog $message -Severity Warning - try { - Write-AuditLog "Installing $name module/s version $version -AllowPrerelease:$prerelease." - $SaveVerbosePreference = $script:VerbosePreference - Install-Module $name -Scope $Scope -RequiredVersion $version -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "$name module successfully installed!" - if ($SelectiveImports) { - foreach ($Mod in $SelectiveImports) { - $name = $Mod - Write-AuditLog "Selectively importing the $name module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." - } - } - else { - Write-AuditLog "Importing the $name module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." + 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." } } - catch { - Write-AuditLog $throwmsg -Severity Error - throw $_.Exception + else{ + Write-AuditLog "Importing the $m module." + Import-Module $m -ErrorAction Stop + Write-AuditLog "Successfully imported the $m module." } } - else { - try { - if ($SelectiveImports) { - foreach ($Mod in $SelectiveImports) { - $name = $Mod - Write-AuditLog "The $name module was found to be installed." - Write-AuditLog "Selectively importing the $name module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." - Write-AuditLog -EndFunction - } - } - else { - Write-AuditLog "The $name module was found to be installed." - Write-AuditLog "Importing the $name module." - $SaveVerbosePreference = $script:VerbosePreference - Import-Module $name -ErrorAction Stop -Verbose:$false - $script:VerbosePreference = $SaveVerbosePreference - Write-AuditLog "Successfully imported the $name module." - write-auditlog -EndFunction + 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." } } - catch { - Write-AuditLog $throwmsg -Severity Error - throw $_.Exception + else{ + Write-AuditLog "Importing the $m module." + Import-Module $m -ErrorAction Stop + Write-AuditLog "Successfully imported the $m module." } } } - } \ No newline at end of file + } + 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/Private/New-EnterpriseAppRegistration.ps1 b/source/Private/New-EnterpriseAppRegistration.ps1 new file mode 100644 index 0000000..82d3571 --- /dev/null +++ b/source/Private/New-EnterpriseAppRegistration.ps1 @@ -0,0 +1,110 @@ +<# + .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 ( + [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/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/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-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/Register-GraphApp.ps1 b/source/Private/Register-GraphApp.ps1 deleted file mode 100644 index 886213c..0000000 --- a/source/Private/Register-GraphApp.ps1 +++ /dev/null @@ -1,59 +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 { - throw $_.Exception - } - return $AppRegistration - } - end { - Write-AuditLog -EndFunction - } -} 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/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..15a8178 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,16 +109,26 @@ [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 + $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" + $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 = @() @@ -169,19 +179,18 @@ 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!" - 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} + 'Error' { Write-Error ('[ERROR] X - ' + $FuncName + ' ' + $Message) -ErrorAction Continue } + 'Verbose' { Write-Verbose ('[VERBOSE] ~ ' + $Message) } + 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): $($_)" } } @@ -189,11 +198,11 @@ 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 { - throw "OutputPath is not specified for End action." + throw 'OutputPath is not specified for End action.' } } } @@ -201,4 +210,4 @@ throw "Error in Write-AuditLog (end block): $($_.Exception.Message)" } } -} +} \ No newline at end of file diff --git a/source/Public/Connect-ToMsService.ps1 b/source/Public/Connect-ToMsService.ps1 new file mode 100644 index 0000000..e7dfacf --- /dev/null +++ b/source/Public/Connect-ToMsService.ps1 @@ -0,0 +1,106 @@ +<# + .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 { + Disconnect-ExchangeOnline -Confirm:$false + 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 -ShowBanner:$false -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/Deploy-GraphEmailApp.ps1 b/source/Public/Deploy-GraphEmailApp.ps1 deleted file mode 100644 index 1cb3f3e..0000000 --- a/source/Public/Deploy-GraphEmailApp.ps1 +++ /dev/null @@ -1,84 +0,0 @@ -<# - .SYNOPSIS - Deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication. - .DESCRIPTION - This cmdlet deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication. - It requires an AppPrefix for the app, an optional CertThumbprint, an AuthorizedSenderUserName, and a MailEnabledSendingGroup. - .PARAMETER AppPrefix - A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for grouping purposes. - .PARAMETER CertThumbprint - An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated. - .PARAMETER AuthorizedSenderUserName - The username of the authorized sender. - .PARAMETER MailEnabledSendingGroup - The mail-enabled group to which the sender belongs. This will be used to assign app policy restrictions. - .EXAMPLE - PS C:\> Deploy-GraphEmailApp -AppPrefix "ABC" -AuthorizedSenderUserName "jdoe@example.com" -MailEnabledSendingGroup "GraphAPIMailGroup@example.com" -CertThumbprint "AABBCCDDEEFF11223344556677889900" - .INPUTS - None - .OUTPUTS - Returns a pscustomobject containing the AppId, CertThumbprint, TenantID, and CertExpires. - .NOTES - This cmdlet requires that the user running the cmdlet have the necessary permissions - to create the app and connect to Exchange Online. In addition, a mail-enabled security - group must already exist in Exchange Online for the MailEnabledSendingGroup parameter. -#> -function Deploy-GraphEmailApp { - [CmdletBinding()] - param( - - [Parameter(Mandatory = $true, HelpMessage = "The prefix used to initialize the Graph Email App.")] - [string]$AppPrefix, - - [Parameter(Mandatory = $false, HelpMessage = "The thumbprint of the certificate to be retrieved.")] - [string]$CertThumbprint, - - [Parameter(Mandatory = $true, HelpMessage = "The username of the authorized sender.")] - [string]$AuthorizedSenderUserName, - - [Parameter(Mandatory = $true, HelpMessage = "The Mail Enabled Sending Group.")] - [string]$MailEnabledSendingGroup - ) - - $PublicMods = ` - "Microsoft.Graph", "ExchangeOnlineManagement", ` - "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan" - $PublicVers = ` - "1.22.0", "3.1.0", ` - "1.1.2", "1.0.0" - $ImportMods = ` - "Microsoft.Graph.Authentication", ` - "Microsoft.Graph.Applications", ` - "Microsoft.Graph.Identity.SignIns", ` - "Microsoft.Graph.Users" - $params1 = @{ - PublicModuleNames = $PublicMods - PublicRequiredVersions = $PublicVers - ImportModuleNames = $ImportMods - Scope = "CurrentUser" - } - if (!($script:LogString)) { - Write-AuditLog -Start - } - else { - Write-AuditLog -BeginFunction - } - Write-AuditLog "###############################################" - Initialize-ModuleEnv @params1 - Connect-ToMGGraph - $AppSettings = Initialize-GraphEmailApp -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName" - - $CertDetails = Get-GraphEmailAppCert -AppName $AppSettings.AppName -CertThumbprint $CertThumbprint - - $appRegistration = Register-GraphApp -AppName $AppSettings.AppName -GraphResourceId $AppSettings.graphResourceId -ResID $AppSettings.ResId -CertThumbprint $CertDetails.CertThumbprint - - - Get-GraphEmailAppConfig -AppRegistration $appRegistration -GraphServicePrincipalId $AppSettings.GraphServicePrincipal.Id -Context $AppSettings.Context -CertThumbprint $CertDetails.CertThumbprint - Read-Host "Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue." - # Call to New-ExchangeEmailAppPolicy - - [void](New-ExchangeEmailAppPolicy -AppRegistration $appRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup) - $output = Get-AppSecret -AppName $AppSettings.AppName -AppRegistration $appRegistration -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User -MailEnabledSendingGroup $MailEnabledSendingGroup - return $output - #> -} \ No newline at end of file diff --git a/source/Public/Get-GraphEmailAppCert.ps1 b/source/Public/Get-GraphEmailAppCert.ps1 deleted file mode 100644 index e586b79..0000000 --- a/source/Public/Get-GraphEmailAppCert.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -<# - .SYNOPSIS - Retrieves or creates a new certificate for the Microsoft Graph Email app. - .DESCRIPTION - The Get-GraphEmailAppCert function retrieves a certificate for the specified app from the CurrentUser's certificate store based on the provided thumbprint. - If a thumbprint is not provided, it will generate a new self-signed certificate. - .PARAMETER CertThumbprint - The thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated. - .PARAMETER AppName - The name of the Graph Email App. - .EXAMPLE - PS C:\> Get-GraphEmailAppCert -AppName "MyApp" -CertThumbprint "9B8B40C5F148B710AD5C0E5CC8D0B71B5A30DB0C" - .EXAMPLE - PS C:\> Get-GraphEmailAppCert -AppName "MyApp" - .INPUTS - None - .OUTPUTS - A custom PowerShell object containing the certificate's thumbprint, expiration date, and the associated app's name. - .NOTES - The cmdlet requires that the user running the cmdlet have the necessary permissions to create or retrieve certificates from the certificate store. - The certificate's expiration date is formatted as "yyyy-MM-dd HH:mm:ss". -#> -function Get-GraphEmailAppCert { - param ( - [string]$CertThumbprint, - [string]$AppName - ) - if (!($script:LogString)) { - Write-AuditLog -Start - } - else { - Write-AuditLog -BeginFunction - } - Write-AuditLog "###############################################" - # Step 10: - # Create or retrieve certificate from the store. - try { - if (!$CertThumbprint) { - # Create a self-signed certificate for the app. - $Cert = New-SelfSignedCertificate -Subject "CN=$AppName" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 - $CertThumbprint = $Cert.Thumbprint - $CertExpirationDate = $Cert.NotAfter - $output = [PSCustomObject] @{ - CertThumbprint = $CertThumbprint - CertExpires = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss") - AppName = $AppName - } - } - else { - # Retrieve the certificate from the CurrentUser's certificate store. - $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } - if (!($Cert)) { - throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store." - } - $CertThumbprint = $Cert.Thumbprint - $CertExpirationDate = $Cert.NotAfter - $output = [PSCustomObject] @{ - CertThumbprint = $CertThumbprint - CertExpires = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss") - AppName = $AppName - } - } - return $output - } - catch { - # If there is an error, throw an exception with the error message. - throw $_.Exception - } - write-auditlog "Certificate with thumbprint $CertThumbprint created or retrieved from the CurrentUser's certificate store." - Write-AuditLog -EndFunction -} - 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/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1 new file mode 100644 index 0000000..9e94e58 --- /dev/null +++ b/source/Public/New-MailEnabledSendingGroup.ps1 @@ -0,0 +1,68 @@ +function New-MailEnabledSendingGroup { + [CmdletBinding(DefaultParameterSetName = 'CustomDomain')] + param ( + [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.')] + [string]$Alias, + [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.')] + [string]$DefaultDomain + ) + if (!($script:LogString)) { + Write-AuditLog -Start + } + else { + Write-AuditLog -BeginFunction + } + try { + Connect-ToMsService -ExchangeOnline + if (-not $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) { + # 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 + } + # Create the distribution group + $groupParams = @{ + Name = $Name + Alias = $Alias + PrimarySmtpAddress = $PrimarySmtpAddress + Type = 'security' + } + 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 + } + 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/Publish-GraphEmailApp.ps1 b/source/Public/Publish-GraphEmailApp.ps1 new file mode 100644 index 0000000..f569735 --- /dev/null +++ b/source/Public/Publish-GraphEmailApp.ps1 @@ -0,0 +1,168 @@ +<# + .SYNOPSIS + Deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication. + .DESCRIPTION + This cmdlet deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication. + It requires an AppPrefix for the app, an optional CertThumbprint, an AuthorizedSenderUserName, and a MailEnabledSendingGroup. + .PARAMETER AppPrefix + A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for grouping purposes. + .PARAMETER CertThumbprint + An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated. + .PARAMETER AuthorizedSenderUserName + The username of the authorized sender. + .PARAMETER MailEnabledSendingGroup + The mail-enabled group to which the sender belongs. This will be used to assign app policy restrictions. + .EXAMPLE + PS C:\> Publish-GraphEmailApp -AppPrefix "ABC" -AuthorizedSenderUserName "jdoe@example.com" -MailEnabledSendingGroup "GraphAPIMailGroup@example.com" -CertThumbprint "AABBCCDDEEFF11223344556677889900" + .INPUTS + None + .OUTPUTS + Returns a pscustomobject containing the AppId, CertThumbprint, TenantID, and CertExpires. + .NOTES + This cmdlet requires that the user running the cmdlet have the necessary permissions + to create the app and connect to Exchange Online. In addition, a mail-enabled security + group must already exist in Exchange Online for the MailEnabledSendingGroup parameter. +#> +function 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}$')] + [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 = '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) { + 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 + # 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 ` + -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 + ) + } + } + process { + try { + # Register App + $appRegistration = New-EnterpriseAppRegistration ` + -DisplayName $AppSettings.AppName ` + -CertThumbprint $CertDetails.CertThumbprint ` + -ResourceAppId $AppSettings.GraphResourceId ` + -PermissionIds $AppSettings.ResId -SignInAudience 'AzureADMyOrg' + # Set App Config + Initialize-GraphAppRegistration ` + -AppRegistration $appRegistration ` + -GraphServicePrincipalId $AppSettings.GraphServicePrincipal.Id ` + -Context $AppSettings.Context ` + -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]@{ + 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 $VaultName -Overwrite + Write-AuditLog "Secret '$name' saved to vault '$VaultName'." + } + 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 { + # Return output + if ($DoNotReturnParamSplat) { + return $output + } + else { + Write-Output ($output | ConvertTo-ParameterSplat) + } + } +} diff --git a/source/Public/Publish-MemPolicyManagerApp.ps1 b/source/Public/Publish-MemPolicyManagerApp.ps1 new file mode 100644 index 0000000..027bc62 --- /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 = 'MemPolicyManagerLocalStore', + [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 + } +} diff --git a/source/Public/Send-GraphAppEmail.ps1 b/source/Public/Send-GraphAppEmail.ps1 index c83ddff..b77fd10 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,17 @@ 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 - $authobj = [PSCustomObject] @{ - AppId = $values[0] - CertThumbprint = $values[1] - TenantID = $values[2] - CertExpires = $values[3] - SendAsUser = $values[4] - AppRestrictedSendGroup = $values[5] - AppName = $values[6] - } - $GraphEmailApp = $authobj + $authObj = $Auth | ConvertFrom-Json + $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 +102,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 +116,7 @@ function Send-GraphAppEmail { message = @{ subject = "$Subject" body = @{ - contentType = "text" + contentType = 'text' content = "$EmailBody" } toRecipients = @( @@ -146,33 +130,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 }