From e7bd25c02bd9faeb437387d0d17edcccf44554a7 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Tue, 11 Mar 2025 18:58:13 -0500
Subject: [PATCH 01/38] add: existing email app parameter set
---
.gitignore | 1 +
CHANGELOG.md | 13 +-
source/Private/Connect-TkMsService.ps1 | 36 +-
.../Initialize-TkAppSpRegistration.ps1 | 3 +-
source/Public/Publish-TkEmailApp.ps1 | 396 +++++++++++++-----
source/Public/Publish-TkM365AuditApp.ps1 | 11 +-
.../Public/Publish-TkMemPolicyManagerApp.ps1 | 9 +-
7 files changed, 329 insertions(+), 140 deletions(-)
diff --git a/.gitignore b/.gitignore
index 30c5f6d..53dfda4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ markdownissues.txt
node_modules
package-lock.json
ZZBuild-Help.ps1
+test1.ps1
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e83f926..1b82a3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Added cert options to the GraphAppToolkit send email.
+- Updated auth methods to invoke needed permissions only.
+
+### Fixed
+
+- Fixed formatting.
+
+## [0.1.2] - 2025-03-11
+
+### Added
+
- Added class definitions for GraphAppToolkit
## [0.1.1] - 2025-03-10
@@ -27,4 +38,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
-- Initial release of GraphAppToolkit
\ No newline at end of file
+- Initial release of GraphAppToolkit
diff --git a/source/Private/Connect-TkMsService.ps1 b/source/Private/Connect-TkMsService.ps1
index 3b12da2..201972c 100644
--- a/source/Private/Connect-TkMsService.ps1
+++ b/source/Private/Connect-TkMsService.ps1
@@ -6,6 +6,11 @@ function Connect-TkMsService {
)]
[Switch]
$MgGraph,
+ [Parameter(
+ HelpMessage = 'Graph Scopes.'
+ )]
+ [String[]]
+ $GraphAuthScopes,
[Parameter(
HelpMessage = 'Connect to Exchange Online.'
)]
@@ -36,12 +41,15 @@ function Connect-TkMsService {
Get-MgUser -Top 1 -ErrorAction Stop | Out-Null
$ContextMg = Get-MgContext -ErrorAction Stop
# Check required scopes
- $scopesNeeded = @(
- 'Application.ReadWrite.All',
- 'DelegatedPermissionGrant.ReadWrite.All',
- 'Directory.ReadWrite.All',
- 'RoleManagement.ReadWrite.Directory'
- )
+ <#
+ $scopesNeeded = @(
+ 'Application.ReadWrite.All',
+ 'DelegatedPermissionGrant.ReadWrite.All',
+ 'Directory.ReadWrite.All',
+ 'RoleManagement.ReadWrite.Directory'
+ )
+ #>
+ $scopesNeeded = $GraphAuthScopes
$missing = $scopesNeeded | Where-Object { $ContextMg.Scopes -notcontains $_ }
if ($missing) {
Write-AuditLog "The following needed scopes are missing: $($missing -join ', ')"
@@ -65,24 +73,16 @@ function Connect-TkMsService {
# Remove the old context so we can connect fresh
Remove-MgContext -ErrorAction SilentlyContinue
Write-AuditLog 'Creating a new Microsoft Graph session.'
- Connect-MgGraph -Scopes `
- 'Application.ReadWrite.All', `
- 'DelegatedPermissionGrant.ReadWrite.All', `
- 'Directory.ReadWrite.All', `
- 'RoleManagement.ReadWrite.Directory' `
- -ErrorAction Stop
+ Connect-MgGraph -Scopes $scopesNeeded `
+ -ErrorAction Stop -Confirm
Write-AuditLog 'Connected to Microsoft Graph.'
}
}
else {
# No valid session, so just connect
Write-AuditLog 'No valid Microsoft Graph session found. Connecting...'
- Connect-MgGraph -Scopes `
- 'Application.ReadWrite.All', `
- 'DelegatedPermissionGrant.ReadWrite.All', `
- 'Directory.ReadWrite.All', `
- 'RoleManagement.ReadWrite.Directory' `
- -ErrorAction Stop
+ Connect-MgGraph -Scopes $scopesNeeded `
+ -ErrorAction Stop -Confirm
Write-AuditLog 'Connected to Microsoft Graph.'
}
}
diff --git a/source/Private/Initialize-TkAppSpRegistration.ps1 b/source/Private/Initialize-TkAppSpRegistration.ps1
index c7d8079..5a189b5 100644
--- a/source/Private/Initialize-TkAppSpRegistration.ps1
+++ b/source/Private/Initialize-TkAppSpRegistration.ps1
@@ -20,7 +20,8 @@ function Initialize-TkAppSpRegistration {
Mandatory = $false,
HelpMessage = 'One or more OAuth2 scopes to grant. Defaults to Mail.Send.'
)]
- [psobject[]]$Scopes = [PSCustomObject]@{
+ [psobject[]]
+ $Scopes = [PSCustomObject]@{
Graph = @('Mail.Send')
},
[Parameter(
diff --git a/source/Public/Publish-TkEmailApp.ps1 b/source/Public/Publish-TkEmailApp.ps1
index 2947b59..6c6f7ba 100644
--- a/source/Public/Publish-TkEmailApp.ps1
+++ b/source/Public/Publish-TkEmailApp.ps1
@@ -50,17 +50,20 @@
#>
function Publish-TkEmailApp {
- [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
+ [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'CreateNewApp')]
param(
+ # REGION: CREATE NEW APP param set
[Parameter(
- Mandatory = $true,
+ Mandatory = $false,
+ ParameterSetName = 'CreateNewApp',
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,
+ $AppPrefix = 'Gtk',
[Parameter(
Mandatory = $true,
+ ParameterSetName = 'CreateNewApp',
HelpMessage = 'The username of the authorized sender.'
)]
[ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
@@ -68,11 +71,34 @@ function Publish-TkEmailApp {
$AuthorizedSenderUserName,
[Parameter(
Mandatory = $true,
+ ParameterSetName = 'CreateNewApp',
HelpMessage = 'The Mail Enabled Sending Group.'
)]
[ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
[string]
$MailEnabledSendingGroup,
+ # REGION: USE EXISTING APP param set
+ [Parameter(
+ Mandatory = $true,
+ ParameterSetName = 'UseExistingApp',
+ HelpMessage = 'The AppId of the existing App Registration to which you want to attach a certificate.'
+ )]
+ [ValidatePattern('^[0-9a-fA-F-]{36}$')]
+ [string]
+ $ExistingAppObjectId,
+ [Parameter(
+ Mandatory = $true,
+ ParameterSetName = 'UseExistingApp',
+ HelpMessage = 'Prefix to add to certificate subject for existing app.'
+ )]
+ [Parameter(
+ Mandatory = $false,
+ ParameterSetName = 'CreateNewApp',
+ HelpMessage = 'Prefix to add to certificate subject for existing app.'
+ )]
+ [string]
+ $CertPrefix,
+ # REGION: Shared parameters
[Parameter(
Mandatory = $false,
HelpMessage = 'The thumbprint of the certificate to be retrieved.'
@@ -126,134 +152,274 @@ function Publish-TkEmailApp {
Scope = 'CurrentUser'
}
Initialize-TkModuleEnv @ModParams
- # 2) Connect to both Graph and Exchange
- Connect-TkMsService -MgGraph -ExchangeOnline
- # 3) Verify if the user (authorized sender) exists
- $user = Get-MgUser -Filter "Mail eq '$AuthorizedSenderUserName'"
- if (-not $user) {
- throw "User '$AuthorizedSenderUserName' not found in the tenant."
- }
- $Context = Get-MgContext -ErrorAction Stop
- # 4) Build the app context (Mail.Send permission, etc.)
- $AppSettings = New-TkRequiredResourcePermissionObject -GraphPermissions 'Mail.Send'
- $appName = New-TkAppName `
- -Prefix $AppPrefix `
- -ScenarioName 'AuditGraphEmail' `
- -UserId $AuthorizedSenderUserName
- # Add relevant properties to $AppSettings
- $AppSettings | Add-Member -NotePropertyName 'User' -NotePropertyValue $user
- $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName
- # 5) Create or retrieve the certificate
- $CertDetails = Initialize-TkAppAuthCertificate `
- -AppName $AppSettings.AppName `
- -Thumbprint $CertThumbprint `
- -Subject "CN=$($AppSettings.AppName)" `
- -KeyExportPolicy $KeyExportPolicy `
- -ErrorAction Stop
+ $scopesNeeded = @(
+ 'Application.ReadWrite.All',
+ 'DelegatedPermissionGrant.ReadWrite.All',
+ 'Directory.ReadWrite.All'
+ )
}
catch {
throw
}
}
process {
- $proposedObject = [PSCustomObject]@{
- ProposedAppName = $AppSettings.AppName
- CertificateThumbprintUsed = $CertDetails.CertThumbprint
- CertExpires = $CertDetails.CertExpires
- UserPrincipalName = $user.UserPrincipalName
- TenantID = $Context.TenantId
- Permissions = 'Mail.Send'
- PermissionType = 'Application'
- ConsentType = 'AllPrincipals'
- ExchangePolicyRestrictedToGroup = $MailEnabledSendingGroup
- }
- Write-AuditLog 'The following object will be created (or configured) in Azure AD:'
- Write-AuditLog "`n$($proposedObject | Format-List)`n"
- $permissionsObject = [PSCustomObject]@{
- Graph = 'Mail.Send'
- }
- if ($PSCmdlet.ShouldProcess(
- "GraphEmailApp '$($AppSettings.AppName)'",
- 'Creating & configuring a new Graph Email App in Azure AD'
- )) {
- try {
- $Notes = @"
-Graph Email App for: $AuthorizedSenderUserName
-Restricted to group: '$MailEnabledSendingGroup'.
-Certificate Thumbprint: $($CertDetails.CertThumbprint)
-Certificate Expires: $($CertDetails.CertExpires)
-Tenant ID: $($Context.TenantId)
-App Permissions: $($permissionsObject.Graph)
-Authorized Client IP: $((Invoke-WebRequest ifconfig.me/ip).Content.Trim())
-Client Hostname: $env:COMPUTERNAME
-"@
- # 6) Register the new enterprise app for Graph
- $appRegistration = New-TkAppRegistration `
- -DisplayName $AppSettings.AppName `
- -CertThumbprint $CertDetails.CertThumbprint `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -SignInAudience 'AzureADMyOrg' `
- -Notes $Notes `
- -ErrorAction Stop
- # 7) Configure the service principal, permissions, etc.
- $ConsentUrl = Initialize-TkAppSpRegistration `
- -AppRegistration $appRegistration `
- -Context $Context `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -Scopes $permissionsObject `
- -AuthMethod 'Certificate' `
- -CertThumbprint $CertDetails.CertThumbprint `
+ switch ($PSCmdlet.ParameterSetName) {
+ # ------------------------------------------------------
+ # ============== SCENARIO 1: CREATE NEW APP =============
+ # ------------------------------------------------------
+ 'CreateNewApp' {
+ # 2) Connect to both Graph and Exchange
+ Connect-TkMsService `
+ -MgGraph `
+ -ExchangeOnline `
+ -GraphAuthScopes $scopesNeeded
+ # 3) Grab MgContext for tenant info
+ $Context = Get-MgContext -ErrorAction Stop
+ # 1) Validate the user (AuthorizedSenderUserName) is in tenant
+ $user = Get-MgUser -Filter "Mail eq '$AuthorizedSenderUserName'"
+ if (-not $user) {
+ throw "User '$AuthorizedSenderUserName' not found in the tenant."
+ }
+ # 2) Build the app context (Mail.Send permission, etc.)
+ $AppSettings = New-TkRequiredResourcePermissionObject `
+ -GraphPermissions 'Mail.Send'
+ $appName = New-TkAppName `
+ -Prefix $AppPrefix `
+ -ScenarioName 'AuditGraphEmail' `
+ -UserId $AuthorizedSenderUserName
+ # Add relevant properties
+ $AppSettings | Add-Member -NotePropertyName 'User' -NotePropertyValue $user
+ $AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName
+ if ($CertPrefix) {
+ $updatedString = $appName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix"
+ $CertName = "CN=$updatedString"
+ $ClientCertPrefix = "$certPrefix"
+ }
+ else {
+ $CertName = "CN=$appName"
+ $ClientCertPrefix = "$AppPrefix"
+ }
+ # 3) Create or retrieve the certificate
+ $CertDetails = Initialize-TkAppAuthCertificate `
+ -AppName $AppSettings.AppName `
+ -Thumbprint $CertThumbprint `
+ -Subject $CertName `
+ -KeyExportPolicy $KeyExportPolicy `
-ErrorAction Stop
- [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
- # 8) Create the Exchange Online policy restricting send
- [void](New-TkExchangeEmailAppPolicy -AppRegistration $appRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup)
- # 9) Build final output object
- $output = [PSCustomObject]@{
- AppId = $appRegistration.AppId
- Id = $appRegistration.Id
- AppName = "CN=$($AppSettings.AppName)"
- AppRestrictedSendGroup = $MailEnabledSendingGroup
- CertExpires = $CertDetails.CertExpires
- CertThumbprint = $CertDetails.CertThumbprint
- ConsentUrl = $ConsentUrl
- DefaultDomain = $MailEnabledSendingGroup.Split('@')[1]
- SendAsUser = ($AppSettings.User.UserPrincipalName.Split('@')[0])
- SendAsUserEmail = $AppSettings.User.UserPrincipalName
- TenantID = $Context.TenantId
+ # 4) Show the proposed object
+ $proposedObject = [PSCustomObject]@{
+ ProposedAppName = $AppSettings.AppName
+ CertificateThumbprintUsed = $CertDetails.CertThumbprint
+ CertExpires = $CertDetails.CertExpires
+ UserPrincipalName = $user.UserPrincipalName
+ TenantID = $Context.TenantId
+ Permissions = 'Mail.Send'
+ PermissionType = 'Application'
+ ConsentType = 'AllPrincipals'
+ ExchangePolicyRestrictedToGroup = $MailEnabledSendingGroup
+ }
+ Write-AuditLog 'The following object will be created (or configured) in Azure AD:'
+ Write-AuditLog "`n$($proposedObject | Format-List)`n"
+ # 5) Only proceed if ShouldProcess is allowed
+ if ($PSCmdlet.ShouldProcess("GraphEmailApp '$($AppSettings.AppName)'",
+ 'Creating & configuring a new Graph Email App in Azure AD')) {
+ try {
+ # Build a hashtable (or PSCustomObject) of the fields you want:
+ $notesHash = [ordered]@{
+ GraphEmailAppFor = $AuthorizedSenderUserName
+ RestrictedToGroup = $MailEnabledSendingGroup
+ AppPermissions = 'Mail.Send'
+ ($ClientCertPrefix + '_ClientIP') = (Invoke-RestMethod ifconfig.me/ip)
+ ($ClientCertPrefix + '_Host') = $env:COMPUTERNAME
+ }
+ # Convert that hashtable to a JSON string:
+ $Notes = $notesHash | ConvertTo-Json #-Compress
+ # 6) Register the new enterprise app for Graph
+ $appRegistration = New-TkAppRegistration `
+ -DisplayName $AppSettings.AppName `
+ -CertThumbprint $CertDetails.CertThumbprint `
+ -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
+ -SignInAudience 'AzureADMyOrg' `
+ -Notes $Notes `
+ -ErrorAction Stop
+ # 7) Initialize the service principal, permissions, etc.
+ $ConsentUrl = Initialize-TkAppSpRegistration `
+ -AppRegistration $appRegistration `
+ -Context $Context `
+ -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
+ -AuthMethod 'Certificate' `
+ -CertThumbprint $CertDetails.CertThumbprint `
+ -ErrorAction Stop
+ [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
+ # 8) Create the Exchange Online policy restricting send
+ New-TkExchangeEmailAppPolicy `
+ -AppRegistration $appRegistration `
+ -MailEnabledSendingGroup $MailEnabledSendingGroup | Out-Null
+ # 9) Build final output object
+ $output = [PSCustomObject]@{
+ AppId = $appRegistration.AppId
+ Id = $appRegistration.Id
+ AppName = "CN=$($AppSettings.AppName)"
+ AppRestrictedSendGroup = $MailEnabledSendingGroup
+ CertExpires = $CertDetails.CertExpires
+ CertThumbprint = $CertDetails.CertThumbprint
+ ConsentUrl = $ConsentUrl
+ DefaultDomain = $MailEnabledSendingGroup.Split('@')[1]
+ SendAsUser = ($AppSettings.User.UserPrincipalName.Split('@')[0])
+ SendAsUserEmail = $AppSettings.User.UserPrincipalName
+ TenantID = $Context.TenantId
+ }
+ # Create a typed object if needed
+ $graphEmailApp = [TkEmailAppParams]::new(
+ $output.AppId,
+ $output.Id,
+ $output.AppName,
+ $output.AppRestrictedSendGroup,
+ $output.CertExpires,
+ $output.CertThumbprint,
+ $output.ConsentUrl,
+ $output.DefaultDomain,
+ $output.SendAsUser,
+ $output.SendAsUserEmail,
+ $output.TenantID
+ )
+ # 10) Store it as JSON in the vault
+ $secretName = "CN=$($AppSettings.AppName)"
+ $savedSecretName = Set-TkJsonSecret `
+ -Name $secretName `
+ -InputObject $output `
+ -VaultName $VaultName -Overwrite:$OverwriteVaultSecret
+ Write-AuditLog "Secret '$savedSecretName' saved to vault '$VaultName'."
+ }
+ catch {
+ throw
+ }
+ }
+ else {
+ Write-AuditLog 'User elected not to create or configure the Graph Email App. (ShouldProcess => false).'
}
- $graphEmailApp = [TkEmailAppParams]::new(
- $output.AppId,
- $output.Id,
- $output.AppName,
- $output.AppRestrictedSendGroup,
- $output.CertExpires,
- $output.CertThumbprint,
- $output.ConsentUrl,
- $output.DefaultDomain,
- $output.SendAsUser,
- $output.SendAsUserEmail,
- $output.TenantID
- )
- # 10) Store it as JSON in the vault
- $secretName = "CN=$($AppSettings.AppName)"
- $savedSecretName = Set-TkJsonSecret -Name $secretName -InputObject $output -VaultName $VaultName -Overwrite:$OverwriteVaultSecret
- Write-AuditLog "Secret '$savedSecretName' saved to vault '$VaultName'."
}
- catch {
- throw
+ # ---------------------------------------------------------
+ # ============ SCENARIO 2: USE EXISTING APP ===============
+ # ---------------------------------------------------------
+ 'UseExistingApp' {
+ # Grab MgContext for tenant info
+ Connect-TkMsService `
+ -MgGraph `
+ -GraphAuthScopes $scopesNeeded
+ $Context = Get-MgContext -ErrorAction Stop
+ $ClientCertPrefix = "$CertPrefix"
+ # Retrieve the existing app registration by AppId
+ Write-AuditLog "Looking up existing app with ObjectId: $ExistingAppObjectId"
+ # Get-MgApplication uses the application object id, not the app id
+ $existingApp = Get-MgApplication -ApplicationId $ExistingAppObjectId -ErrorAction Stop
+ if (!($existingApp | Where-Object { $_.DisplayName -like 'GraphToolKit-*' })) {
+ throw "The existing app with AppId '$ExistingAppObjectId' is not a GraphToolKit app."
+ }
+ if (-not $existingApp) {
+ throw "Could not find an existing application with AppId '$ExistingAppObjectId'."
+ }
+ $updatedString = $existingApp.DisplayName -replace '(GraphToolKit-)[A-Za-z0-9]{2,4}(?=-)', "`$1$CertPrefix"
+ # Retrieve or create the certificate
+ $certDetails = Initialize-TkAppAuthCertificate `
+ -AppName $updatedString `
+ -Thumbprint $CertThumbprint `
+ -Subject ("CN=$updatedString") `
+ -KeyExportPolicy $KeyExportPolicy `
+ -ErrorAction Stop
+ Write-AuditLog "Attaching certificate (Thumbprint: $($certDetails.CertThumbprint)) to existing app '$($existingApp.DisplayName)'."
+ # Merge or append the new certificate to the existing KeyCredentials
+ $currentKeys = $existingApp.KeyCredentials
+ $newCert = @{
+ Type = 'AsymmetricX509Cert'
+ Usage = 'Verify'
+ Key = (Get-ChildItem -Path Cert:\CurrentUser\My |
+ Where-Object { $_.Thumbprint -eq $certDetails.CertThumbprint }).RawData
+ DisplayName = "CN=$updatedString"
+ }
+ # If you want to specify start/end date, you can do so as well:
+ # $newCert.StartDateTime = (Get-Date)
+ # $newCert.EndDateTime = (Get-Date).AddYears(1)
+ # Append the new cert to existing
+ $mergedKeys = $currentKeys + $newCert
+ $existingNotesRaw = $existingApp.Notes
+ if (-not [string]::IsNullOrEmpty($existingNotesRaw)) {
+ try {
+ $notesObject = $existingNotesRaw | ConvertFrom-Json -ErrorAction Stop
+ }
+ catch {
+ Write-AuditLog 'Existing .Notes was not valid JSON; ignoring it.'
+ $notesObject = [ordered]@{}
+ }
+ }
+ else {
+ $notesObject = [ordered]@{}
+ }
+ # Add your new properties each time the function runs
+ $notesObject | Add-Member -NotePropertyName ($ClientCertPrefix + '_ClientIP') -NotePropertyValue (Invoke-RestMethod ifconfig.me/ip)
+ $notesObject | Add-Member -NotePropertyName ($ClientCertPrefix + '_Host') -NotePropertyValue $env:COMPUTERNAME
+ $updatedNotes = $notesObject | ConvertTo-Json #-Compress
+ if (($updatedNotes.length -gt 1024)) {
+ throw 'The Notes object is too large. Please reduce the size of the Notes object.'
+ }
+ if ($PSCmdlet.ShouldProcess("AppId '$ExistingAppObjectId'",
+ "Adding a new certificate to existing App '$($existingApp.DisplayName)'")) {
+ try {
+ # Update the application with the new KeyCredentials array
+ Update-MgApplication `
+ -ApplicationId $existingApp.Id `
+ -KeyCredentials $mergedKeys `
+ -Notes $updatedNotes `
+ -ErrorAction Stop | Out-Null
+ # Build an output object similar to "new" scenario
+ $output = [PSCustomObject]@{
+ AppId = $existingApp.AppId
+ Id = $existingApp.Id
+ AppName = "CN=$updatedString"
+ CertExpires = $certDetails.CertExpires
+ CertThumbprint = $certDetails.CertThumbprint
+ TenantID = $Context.TenantId
+ }
+ $graphEmailApp = [TkEmailAppParams]::new(
+ $output.AppId,
+ $output.Id,
+ $output.AppName,
+ $notesObject.RestrictedToGroup, # AppRestrictedSendGroup
+ $output.CertExpires,
+ $output.CertThumbprint,
+ $null, # ConsentUrl (Made as nullable string)
+ ($notesObject.GraphEmailAppFor.Split('@')[1]), # DefaultDomain
+ ($notesObject.GraphEmailAppFor.Split('@')[0]), # SendAsUser
+ $notesObject.GraphEmailAppFor, # SendAsUserEmail
+ $output.TenantID
+ )
+ # Store updated info in the vault
+ $secretName = "CN=$updatedString"
+ $savedSecretName = Set-TkJsonSecret `
+ -Name $secretName `
+ -InputObject $graphEmailApp `
+ -VaultName $VaultName `
+ -Overwrite:$OverwriteVaultSecret
+ Write-AuditLog "Secret for existing app saved as '$secretName' in vault '$VaultName'."
+ }
+ catch {
+ throw
+ }
+ }
+ else {
+ Write-AuditLog "User canceled updating existing app '$($existingApp.DisplayName)'."
+ }
}
- }
- else {
- Write-AuditLog 'User elected not to create or configure the Graph Email App. (ShouldProcess => false).'
- }
+ } # end switch
}
end {
- if ($ReturnParamSplat) {
+ if ($ReturnParamSplat -and $graphEmailApp) {
return ($graphEmailApp | ConvertTo-ParameterSplat)
}
- else {
+ elseif ($graphEmailApp) {
return $graphEmailApp
}
Write-AuditLog -EndFunction
}
}
+
diff --git a/source/Public/Publish-TkM365AuditApp.ps1 b/source/Public/Publish-TkM365AuditApp.ps1
index a59340d..7edbfe3 100644
--- a/source/Public/Publish-TkM365AuditApp.ps1
+++ b/source/Public/Publish-TkM365AuditApp.ps1
@@ -106,9 +106,14 @@ function Publish-TkM365AuditApp {
}
Write-AuditLog '###############################################'
Write-AuditLog 'Initializing M365 Audit App publication process...'
-
+ $scopesNeeded = @(
+ 'Application.ReadWrite.All',
+ 'DelegatedPermissionGrant.ReadWrite.All',
+ 'Directory.ReadWrite.All',
+ 'RoleManagement.ReadWrite.Directory'
+ )
# 1) Connect to Graph so we can query permissions & create the app
- Connect-TkMsService -MgGraph
+ Connect-TkMsService -MgGraph -GraphAuthScopes $scopesNeeded
}
process {
try {
@@ -197,7 +202,7 @@ Graph App Permissions: $($graphPerms -join ', ')
SharePoint App Permissions: $($sharePointPerms -join ', ')
Exchange App Permissions: $($exchangePerms -join ', ')
Roles Assigned: 'Exchange Administrator', 'Global Reader'
-Authorized Client IP: $((Invoke-WebRequest ifconfig.me/ip).Content.Trim())
+Authorized Client IP: $((Invoke-RestMethod ifconfig.me/ip))
Client Hostname: $env:COMPUTERNAME
"@
if ($PSCmdlet.ShouldProcess($appName, 'Create and configure M365 Audit App in EntraAD')) {
diff --git a/source/Public/Publish-TkMemPolicyManagerApp.ps1 b/source/Public/Publish-TkMemPolicyManagerApp.ps1
index 7426cca..4211562 100644
--- a/source/Public/Publish-TkMemPolicyManagerApp.ps1
+++ b/source/Public/Publish-TkMemPolicyManagerApp.ps1
@@ -118,7 +118,12 @@ function Publish-TkMemPolicyManagerApp {
}
Initialize-TkModuleEnv @ModParams
# Only connect to Graph
- Connect-TkMsService -MgGraph
+ $scopesNeeded = @(
+ 'Application.ReadWrite.All',
+ 'DelegatedPermissionGrant.ReadWrite.All',
+ 'Directory.ReadWrite.All'
+ )
+ Connect-TkMsService -MgGraph -GraphAuthScopes $scopesNeeded
$Context = Get-MgContext -ErrorAction Stop
}
catch {
@@ -184,7 +189,7 @@ Certificate Expires: $($CertDetails.CertExpires)
Tenant ID: $($Context.TenantId)
Graph App Permissions: $($permissions -join ', ')
Read-Write Permissions: $(if ($ReadWrite) { 'ReadWrite' } else { 'Read-Only' })
-Authorized Client IP: $((Invoke-WebRequest ifconfig.me/ip).Content.Trim())
+Authorized Client IP: $((Invoke-RestMethod ifconfig.me/ip))
Client Hostname: $env:COMPUTERNAME
"@
if ($PSCmdlet.ShouldProcess("MemPolicyManager App '$($AppSettings.AppName)'",
From 55942d374f28e7222f3d1b86a894ffe04f4be8ce Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Tue, 11 Mar 2025 19:26:09 -0500
Subject: [PATCH 02/38] fix: formatting
---
source/Public/Publish-TkM365AuditApp.ps1 | 125 ++++++++++-------------
1 file changed, 55 insertions(+), 70 deletions(-)
diff --git a/source/Public/Publish-TkM365AuditApp.ps1 b/source/Public/Publish-TkM365AuditApp.ps1
index 7edbfe3..e68c10c 100644
--- a/source/Public/Publish-TkM365AuditApp.ps1
+++ b/source/Public/Publish-TkM365AuditApp.ps1
@@ -51,15 +51,15 @@ function Publish-TkM365AuditApp {
[Parameter(
Mandatory = $false,
HelpMessage = `
- 'Prefix for the new M365 Audit app name (2-4 alphanumeric characters).'
+ 'Prefix for the new M365 Audit app name (2-4 alphanumeric characters).'
)]
[ValidatePattern('^[A-Z0-9]{2,4}$')]
[string]
- $AppPrefix = "Gtk",
+ $AppPrefix = 'Gtk',
[Parameter(
Mandatory = $false,
HelpMessage = `
- 'Thumbprint of an existing certificate to use. If not provided, a self-signed cert will be created.'
+ 'Thumbprint of an existing certificate to use. If not provided, a self-signed cert will be created.'
)]
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
[string]
@@ -74,30 +74,25 @@ function Publish-TkM365AuditApp {
[Parameter(
Mandatory = $false,
HelpMessage = `
- 'Name of the SecretManagement vault to store app credentials.'
+ 'Name of the SecretManagement vault to store app credentials.'
)]
[string]
$VaultName = 'M365AuditAppLocalStore',
[Parameter(
Mandatory = $false,
HelpMessage = `
- 'If specified, overwrite the vault secret if it already exists.'
+ 'If specified, overwrite the vault secret if it already exists.'
)]
[switch]
$OverwriteVaultSecret,
[Parameter(
Mandatory = $false,
HelpMessage = `
- 'Return output as a parameter splat string for use in other functions.'
+ 'Return output as a parameter splat string for use in other functions.'
)]
[switch]$ReturnParamSplat
)
begin {
- <#
- $uniqueSuffix = [System.Guid]::NewGuid().ToString('N').Substring(0, 4)
- $TwoToFourLetterCompanyAbbreviation = "CS$($uniqueSuffix.Substring(0,2))"
- Publish-TkM365AuditApp -AppPrefix $TwoToFourLetterCompanyAbbreviation -ReturnParamSplat
- #>
if (-not $script:LogString) {
Write-AuditLog -Start
}
@@ -118,43 +113,25 @@ function Publish-TkM365AuditApp {
process {
try {
# 2) Define read-only vs. read-write sets
- $graphReadOnly = @(
+ $graph = @(
'AppCatalog.ReadWrite.All',
- #'AuditLog.Read.All',
'Channel.Delete.All',
'ChannelMember.ReadWrite.All',
'ChannelSettings.ReadWrite.All',
- #'DeviceManagementApps.Read.All',
- #'DeviceManagementApps.ReadWrite.All',
- #'DeviceManagementConfiguration.Read.All',
- #'DeviceManagementConfiguration.ReadWrite.All',
- #'DeviceManagementManagedDevices.Read.All',
- #'DeviceManagementManagedDevices.ReadWrite.All',
'Directory.Read.All',
- #'Group.Read.All',
'Group.ReadWrite.All',
'Organization.Read.All',
'Policy.Read.All',
'Domain.Read.All'
- #'Policy.Read.ConditionalAccess',
- #'RoleManagement.Read.Directory',
'TeamSettings.ReadWrite.All'
- #'TeamSettings.Read.All',
- #'UserAuthenticationMethod.Read.All',
'User.Read.All'
)
- #$graphReadWrite = @('Directory.ReadWrite.All') # add more if needed
- # For SharePoint, only 'Sites.Read.All' for read-only,
- # 'Sites.FullControl.All' for read-write
- $sharePointReadOnly = @('Sites.Read.All')
- $sharePointReadWrite = @('Sites.FullControl.All')
- # For Exchange, typically 'Exchange.ManageAsApp' suffices in read-only mode
- # Add more if you need read-write Exchange perms
- $exchangeReadOnly = @('Exchange.ManageAsApp')
+ $sharePoint = @('Sites.Read.All', 'Sites.FullControl.All')
+ $exchange = @('Exchange.ManageAsApp')
# Decide which sets to use
- $graphPerms = $graphReadOnly #if ($ReadWrite) { $graphReadOnly + $graphReadWrite } else { $graphReadOnly }
- $sharePointPerms = $sharePointReadOnly + $sharePointReadWrite
- $exchangePerms = $exchangeReadOnly
+ $graphPerms = $graph
+ $sharePointPerms = $sharePoint
+ $exchangePerms = $exchange
$permissionsObject = [PSCustomObject]@{
Graph = $graphPerms
SharePoint = $sharePointPerms
@@ -164,23 +141,24 @@ function Publish-TkM365AuditApp {
Write-AuditLog "SharePoint Perms: $($sharePointPerms -join ', ')"
Write-AuditLog "Exchange Perms: $($exchangePerms -join ', ')"
$Context = Get-MgContext -ErrorAction Stop
- # 3) Gather the resource access objects (GUIDs) for all these perms
+ # Gather the resource access objects (GUIDs) for all these perms
$AppSettings = New-TkRequiredResourcePermissionObject `
-GraphPermissions $graphPerms `
-Scenario '365Audit' `
-ErrorAction Stop
- # This returns an object with .RequiredResourceAccessList (the array
- # of MicrosoftGraphRequiredResourceAccess objects) plus .TenantId, etc.
- # 4) Generate the app name
- $appName = New-TkAppName -Prefix $AppPrefix -ScenarioName 'M365Audit' -ErrorAction Stop
+ # Generate the app name
+ $appName = New-TkAppName `
+ -Prefix $AppPrefix `
+ -ScenarioName 'M365Audit' `
+ -ErrorAction Stop
Write-AuditLog "Proposed new M365 Audit App name: $appName"
- # 5) Retrieve or create the certificate
+ # Retrieve or create the certificate
$CertDetails = Initialize-TkAppAuthCertificate `
- -AppName $appName `
- -Thumbprint $CertThumbprint `
- -Subject "CN=$appName" `
- -KeyExportPolicy $KeyExportPolicy `
- -ErrorAction Stop
+ -AppName $appName `
+ -Thumbprint $CertThumbprint `
+ -Subject "CN=$appName" `
+ -KeyExportPolicy $KeyExportPolicy `
+ -ErrorAction Stop
Write-AuditLog "Certificate Thumbprint: $($CertDetails.CertThumbprint); Expires: $($CertDetails.CertExpires)."
# Show user proposed config
$proposed = [PSCustomObject]@{
@@ -193,18 +171,19 @@ function Publish-TkM365AuditApp {
}
Write-AuditLog 'Proposed creation of a new M365 Audit App with the following properties:'
Write-AuditLog "$($proposed | Format-List)"
- # 6) Create the app in one pass with all resources
- $Notes = @"
-Certificate Thumbprint: $($CertDetails.CertThumbprint)
-Certificate Expires: $($CertDetails.CertExpires)
-Tenant ID: $($Context.TenantId)
-Graph App Permissions: $($graphPerms -join ', ')
-SharePoint App Permissions: $($sharePointPerms -join ', ')
-Exchange App Permissions: $($exchangePerms -join ', ')
-Roles Assigned: 'Exchange Administrator', 'Global Reader'
-Authorized Client IP: $((Invoke-RestMethod ifconfig.me/ip))
-Client Hostname: $env:COMPUTERNAME
-"@
+ # Create the app in one pass with all resources
+ $notesHash = [ordered]@{
+ 'Certificate Thumbprint' = $($CertDetails.CertThumbprint)
+ 'Certificate Expires' = $($CertDetails.CertExpires)
+ 'GraphAppPermissions' = $($graphPerms -join ', ')
+ 'SharePointAppPermissions' = $($sharePointPerms -join ', ')
+ 'ExchangeAppPermissions' = $($exchangePerms -join ', ')
+ 'RolesAssigned' = @('Exchange Administrator', 'Global Reader')
+ 'AuthorizedClient IP' = $((Invoke-RestMethod ifconfig.me/ip))
+ 'ClientOrUserHostname' = if ($env:COMPUTERNAME) { $env:COMPUTERNAME } else { $env:USERNAME }
+ }
+ # Convert that hashtable to a JSON string:
+ $Notes = $notesHash | ConvertTo-Json #-Compress
if ($PSCmdlet.ShouldProcess($appName, 'Create and configure M365 Audit App in EntraAD')) {
Write-AuditLog 'Creating new EntraAD application with all resource permissions...'
$appRegistration = New-TkAppRegistration `
@@ -212,9 +191,10 @@ Client Hostname: $env:COMPUTERNAME
-CertThumbprint $CertDetails.CertThumbprint `
-RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
-Notes $Notes `
- -SignInAudience 'AzureADMyOrg'
+ -SignInAudience 'AzureADMyOrg' `
+ -ErrorAction Stop
Write-AuditLog "App registered. Object ID = $($appRegistration.Id), ClientId = $($appRegistration.AppId)."
- # 7) Grant the oauth2 permissions to service principal
+ # Grant the oauth2 permissions to service principal
$ConsentUrl = Initialize-TkAppSpRegistration `
-AppRegistration $appRegistration `
-Context $Context `
@@ -223,9 +203,9 @@ Client Hostname: $env:COMPUTERNAME
-AuthMethod 'Certificate' `
-CertThumbprint $CertDetails.CertThumbprint `
-ErrorAction Stop
- [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
+ [void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
Write-AuditLog 'Appending Exchange Administrator role to the app.'
- $exoAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Exchange Administrator'"
+ $exoAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Exchange Administrator'" -ErrorAction Stop
# Get the service principal object ID of the app
$sp = Get-MgServicePrincipal -Filter "appId eq '$($appRegistration.appid)'" -ErrorAction Stop
$spObjectId = $sp.Id
@@ -234,16 +214,17 @@ Client Hostname: $env:COMPUTERNAME
}
New-MgDirectoryRoleMemberByRef `
-DirectoryRoleId $exoAdminRole.Id `
- -BodyParameter $body
+ -BodyParameter $body `
+ -ErrorAction Stop
Write-AuditLog 'Appending Global Reader role to the app.'
- $globalReaderRole = Get-MgDirectoryRole -Filter "displayName eq 'Global Reader'"
- $body = @{
- '@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$spObjectId"
- }
+ $globalReaderRole = Get-MgDirectoryRole `
+ -Filter "displayName eq 'Global Reader'" `
+ -ErrorAction Stop
New-MgDirectoryRoleMemberByRef `
-DirectoryRoleId $globalReaderRole.Id `
- -BodyParameter $body
- # 8) Store final app info in the vault
+ -BodyParameter $body `
+ -ErrorAction Stop
+ # Store final app info in the vault
$output = [PSCustomObject]@{
AppName = $("CN=$appName")
AppId = $appRegistration.AppId
@@ -269,7 +250,11 @@ Client Hostname: $env:COMPUTERNAME
$output.ExchangePermissions
)
# Save to vault
- Set-TkJsonSecret -Name "CN=$appName" -InputObject $output -VaultName $VaultName -Overwrite:$OverwriteVaultSecret
+ Set-TkJsonSecret `
+ -Name "CN=$appName" `
+ -InputObject $output `
+ -VaultName $VaultName `
+ -Overwrite:$OverwriteVaultSecret
Write-AuditLog "Saved app credentials to vault '$VaultName'."
# Return as either param splat or plain object
if ($ReturnParamSplat) {
From 90b9cb4203ec5e5e65454ca7e7b91e678e4e24be Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Tue, 11 Mar 2025 20:34:53 -0500
Subject: [PATCH 03/38] fix: refactor calls
---
.../Classes/TkMemPolicyManagerAppParams .ps1 | 6 +-
source/Private/New-TkEmailAppParams.ps1 | 28 +++
source/Private/New-TkM365AuditAppParams.ps1 | 26 +++
.../New-TkMemPolicyManagerAppParams.ps1 | 22 +++
source/Public/Publish-TkEmailApp.ps1 | 165 +++++++++---------
source/Public/Publish-TkM365AuditApp.ps1 | 115 ++++++------
.../Public/Publish-TkMemPolicyManagerApp.ps1 | 116 ++++++------
7 files changed, 279 insertions(+), 199 deletions(-)
create mode 100644 source/Private/New-TkEmailAppParams.ps1
create mode 100644 source/Private/New-TkM365AuditAppParams.ps1
create mode 100644 source/Private/New-TkMemPolicyManagerAppParams.ps1
diff --git a/source/Classes/TkMemPolicyManagerAppParams .ps1 b/source/Classes/TkMemPolicyManagerAppParams .ps1
index c7ee3c4..5133a24 100644
--- a/source/Classes/TkMemPolicyManagerAppParams .ps1
+++ b/source/Classes/TkMemPolicyManagerAppParams .ps1
@@ -2,7 +2,7 @@ class TkMemPolicyManagerAppParams {
[string]$AppId
[string]$AppName
[string]$CertThumbprint
- [string]$ClientId
+ [string]$ObjectId
[string]$ConsentUrl
[string]$PermissionSet
[string]$Permissions
@@ -12,7 +12,7 @@ class TkMemPolicyManagerAppParams {
[string]$AppId,
[string]$AppName,
[string]$CertThumbprint,
- [string]$ClientId,
+ [string]$ObjectId,
[string]$ConsentUrl,
[string]$PermissionSet,
[string]$Permissions,
@@ -21,7 +21,7 @@ class TkMemPolicyManagerAppParams {
$this.AppId = $AppId
$this.AppName = $AppName
$this.CertThumbprint = $CertThumbprint
- $this.ClientId = $ClientId
+ $this.ObjectId = $ObjectId
$this.ConsentUrl = $ConsentUrl
$this.PermissionSet = $PermissionSet
$this.Permissions = $Permissions
diff --git a/source/Private/New-TkEmailAppParams.ps1 b/source/Private/New-TkEmailAppParams.ps1
new file mode 100644
index 0000000..fc58698
--- /dev/null
+++ b/source/Private/New-TkEmailAppParams.ps1
@@ -0,0 +1,28 @@
+function New-TkEmailAppParams {
+ param (
+ [string]$AppId,
+ [string]$Id,
+ [string]$AppName,
+ [string]$AppRestrictedSendGroup,
+ [string]$CertExpires,
+ [string]$CertThumbprint,
+ [string]$ConsentUrl,
+ [string]$DefaultDomain,
+ [string]$SendAsUser,
+ [string]$SendAsUserEmail,
+ [string]$TenantID
+ )
+ return [TkEmailAppParams]::new(
+ $AppId,
+ $Id,
+ $AppName,
+ $AppRestrictedSendGroup,
+ $CertExpires,
+ $CertThumbprint,
+ $ConsentUrl,
+ $DefaultDomain,
+ $SendAsUser,
+ $SendAsUserEmail,
+ $TenantID
+ )
+}
\ No newline at end of file
diff --git a/source/Private/New-TkM365AuditAppParams.ps1 b/source/Private/New-TkM365AuditAppParams.ps1
new file mode 100644
index 0000000..41b2ec5
--- /dev/null
+++ b/source/Private/New-TkM365AuditAppParams.ps1
@@ -0,0 +1,26 @@
+function New-TkM365AuditAppParams {
+ param (
+ [string]$AppName,
+ [string]$AppId,
+ [string]$ObjectId,
+ [string]$TenantId,
+ [string]$CertThumbprint,
+ [string]$CertExpires,
+ [string]$ConsentUrl,
+ [string]$MgGraphPermissions,
+ [string]$SharePointPermissions,
+ [string]$ExchangePermissions
+ )
+ return [TkM365AuditAppParams]::new(
+ $AppName,
+ $AppId,
+ $ObjectId,
+ $TenantId,
+ $CertThumbprint,
+ $CertExpires,
+ $ConsentUrl,
+ $MgGraphPermissions,
+ $SharePointPermissions,
+ $ExchangePermissions
+ )
+}
\ No newline at end of file
diff --git a/source/Private/New-TkMemPolicyManagerAppParams.ps1 b/source/Private/New-TkMemPolicyManagerAppParams.ps1
new file mode 100644
index 0000000..6266acb
--- /dev/null
+++ b/source/Private/New-TkMemPolicyManagerAppParams.ps1
@@ -0,0 +1,22 @@
+function New-TkMemPolicyManagerAppParams {
+ param (
+ [string]$AppId,
+ [string]$AppName,
+ [string]$CertThumbprint,
+ [string]$ObjectId,
+ [string]$ConsentUrl,
+ [string]$PermissionSet,
+ [string]$Permissions,
+ [string]$TenantId
+ )
+ return [TkMemPolicyManagerAppParams]::new(
+ $AppId,
+ $AppName,
+ $CertThumbprint,
+ $ObjectId,
+ $ConsentUrl,
+ $PermissionSet,
+ $Permissions,
+ $TenantId
+ )
+}
\ No newline at end of file
diff --git a/source/Public/Publish-TkEmailApp.ps1 b/source/Public/Publish-TkEmailApp.ps1
index 6c6f7ba..d8ab841 100644
--- a/source/Public/Publish-TkEmailApp.ps1
+++ b/source/Public/Publish-TkEmailApp.ps1
@@ -56,7 +56,8 @@ function Publish-TkEmailApp {
[Parameter(
Mandatory = $false,
ParameterSetName = 'CreateNewApp',
- HelpMessage = 'The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.'
+ HelpMessage = `
+ 'The prefix used to initialize the Graph Email App. 2-4 characters letters and numbers only.'
)]
[ValidatePattern('^[A-Z0-9]{2,4}$')]
[string]
@@ -64,7 +65,8 @@ function Publish-TkEmailApp {
[Parameter(
Mandatory = $true,
ParameterSetName = 'CreateNewApp',
- HelpMessage = 'The username of the authorized sender.'
+ HelpMessage = `
+ 'The username of the authorized sender.'
)]
[ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
[string]
@@ -72,7 +74,8 @@ function Publish-TkEmailApp {
[Parameter(
Mandatory = $true,
ParameterSetName = 'CreateNewApp',
- HelpMessage = 'The Mail Enabled Sending Group.'
+ HelpMessage = `
+ 'The Mail Enabled Sending Group.'
)]
[ValidatePattern('^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')]
[string]
@@ -81,7 +84,8 @@ function Publish-TkEmailApp {
[Parameter(
Mandatory = $true,
ParameterSetName = 'UseExistingApp',
- HelpMessage = 'The AppId of the existing App Registration to which you want to attach a certificate.'
+ HelpMessage = `
+ 'The AppId of the existing App Registration to which you want to attach a certificate.'
)]
[ValidatePattern('^[0-9a-fA-F-]{36}$')]
[string]
@@ -89,45 +93,52 @@ function Publish-TkEmailApp {
[Parameter(
Mandatory = $true,
ParameterSetName = 'UseExistingApp',
- HelpMessage = 'Prefix to add to certificate subject for existing app.'
+ HelpMessage = `
+ 'Prefix to add to certificate subject for existing app.'
)]
[Parameter(
Mandatory = $false,
ParameterSetName = 'CreateNewApp',
- HelpMessage = 'Prefix to add to certificate subject for existing app.'
+ HelpMessage = `
+ 'Prefix to add to certificate subject for existing app.'
)]
[string]
$CertPrefix,
# REGION: Shared parameters
[Parameter(
Mandatory = $false,
- HelpMessage = 'The thumbprint of the certificate to be retrieved.'
+ HelpMessage = `
+ 'The thumbprint of the certificate to be retrieved.'
)]
[ValidatePattern('^[A-Fa-f0-9]{40}$')]
[string]
$CertThumbprint,
[Parameter(
Mandatory = $false,
- HelpMessage = 'Key export policy for the certificate.'
+ HelpMessage = `
+ 'Key export policy for the certificate.'
)]
[ValidateSet('Exportable', 'NonExportable')]
[string]
$KeyExportPolicy = 'NonExportable',
[Parameter(
Mandatory = $false,
- HelpMessage = 'If specified, use a custom vault name. Otherwise, use the default.'
+ HelpMessage = `
+ 'If specified, use a custom vault name. Otherwise, use the default.'
)]
[string]
$VaultName = 'GraphEmailAppLocalStore',
[Parameter(
Mandatory = $false,
- HelpMessage = 'If specified, overwrite the vault secret if it already exists.'
+ HelpMessage = `
+ 'If specified, overwrite the vault secret if it already exists.'
)]
[switch]
$OverwriteVaultSecret,
[Parameter(
Mandatory = $false,
- HelpMessage = 'Return the parameter splat for use in other functions.'
+ HelpMessage = `
+ 'Return the parameter splat for use in other functions.'
)]
[switch]
$ReturnParamSplat
@@ -200,12 +211,14 @@ function Publish-TkEmailApp {
$ClientCertPrefix = "$AppPrefix"
}
# 3) Create or retrieve the certificate
- $CertDetails = Initialize-TkAppAuthCertificate `
- -AppName $AppSettings.AppName `
- -Thumbprint $CertThumbprint `
- -Subject $CertName `
- -KeyExportPolicy $KeyExportPolicy `
- -ErrorAction Stop
+ $AppAuthCertificateParams = @{
+ AppName = $AppSettings.AppName
+ Thumbprint = $CertThumbprint
+ Subject = $CertName
+ KeyExportPolicy = $KeyExportPolicy
+ ErrorAction = 'Stop'
+ }
+ $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams
# 4) Show the proposed object
$proposedObject = [PSCustomObject]@{
ProposedAppName = $AppSettings.AppName
@@ -235,28 +248,33 @@ function Publish-TkEmailApp {
# Convert that hashtable to a JSON string:
$Notes = $notesHash | ConvertTo-Json #-Compress
# 6) Register the new enterprise app for Graph
- $appRegistration = New-TkAppRegistration `
- -DisplayName $AppSettings.AppName `
- -CertThumbprint $CertDetails.CertThumbprint `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -SignInAudience 'AzureADMyOrg' `
- -Notes $Notes `
- -ErrorAction Stop
+ $AppRegistrationParams = @{
+ DisplayName = $AppSettings.AppName
+ CertThumbprint = $CertDetails.CertThumbprint
+ RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
+ SignInAudience = 'AzureADMyOrg'
+ Notes = $Notes
+ ErrorAction = 'Stop'
+ }
+ $appRegistration = New-TkAppRegistration @AppRegistrationParams
# 7) Initialize the service principal, permissions, etc.
- $ConsentUrl = Initialize-TkAppSpRegistration `
- -AppRegistration $appRegistration `
- -Context $Context `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -AuthMethod 'Certificate' `
- -CertThumbprint $CertDetails.CertThumbprint `
- -ErrorAction Stop
+ $AppSpRegistrationParams = @{
+ AppRegistration = $appRegistration
+ Context = $Context
+ RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
+ Scopes = $permissionsObject
+ AuthMethod = 'Certificate'
+ CertThumbprint = $CertDetails.CertThumbprint
+ ErrorAction = 'Stop'
+ }
+ $ConsentUrl = Initialize-TkAppSpRegistration @AppSpRegistrationParams
[void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
# 8) Create the Exchange Online policy restricting send
New-TkExchangeEmailAppPolicy `
-AppRegistration $appRegistration `
-MailEnabledSendingGroup $MailEnabledSendingGroup | Out-Null
# 9) Build final output object
- $output = [PSCustomObject]@{
+ $EmailAppParams = @{
AppId = $appRegistration.AppId
Id = $appRegistration.Id
AppName = "CN=$($AppSettings.AppName)"
@@ -265,30 +283,20 @@ function Publish-TkEmailApp {
CertThumbprint = $CertDetails.CertThumbprint
ConsentUrl = $ConsentUrl
DefaultDomain = $MailEnabledSendingGroup.Split('@')[1]
- SendAsUser = ($AppSettings.User.UserPrincipalName.Split('@')[0])
+ SendAsUser = $AppSettings.User.UserPrincipalName.Split('@')[0]
SendAsUserEmail = $AppSettings.User.UserPrincipalName
TenantID = $Context.TenantId
}
- # Create a typed object if needed
- $graphEmailApp = [TkEmailAppParams]::new(
- $output.AppId,
- $output.Id,
- $output.AppName,
- $output.AppRestrictedSendGroup,
- $output.CertExpires,
- $output.CertThumbprint,
- $output.ConsentUrl,
- $output.DefaultDomain,
- $output.SendAsUser,
- $output.SendAsUserEmail,
- $output.TenantID
- )
+ [TkEmailAppParams]$graphEmailApp = New-TkEmailAppParams @EmailAppParams
# 10) Store it as JSON in the vault
- $secretName = "CN=$($AppSettings.AppName)"
- $savedSecretName = Set-TkJsonSecret `
- -Name $secretName `
- -InputObject $output `
- -VaultName $VaultName -Overwrite:$OverwriteVaultSecret
+ $JsonSecretParams = @{
+ Name = "CN=$($AppSettings.AppName)"
+ InputObject = $graphEmailApp
+ VaultName = $VaultName
+ Overwrite = $OverwriteVaultSecret
+ ErrorAction = 'Stop'
+ }
+ $savedSecretName = Set-TkJsonSecret @JsonSecretParams
Write-AuditLog "Secret '$savedSecretName' saved to vault '$VaultName'."
}
catch {
@@ -305,8 +313,8 @@ function Publish-TkEmailApp {
'UseExistingApp' {
# Grab MgContext for tenant info
Connect-TkMsService `
- -MgGraph `
- -GraphAuthScopes $scopesNeeded
+ -MgGraph `
+ -GraphAuthScopes $scopesNeeded
$Context = Get-MgContext -ErrorAction Stop
$ClientCertPrefix = "$CertPrefix"
# Retrieve the existing app registration by AppId
@@ -372,35 +380,30 @@ function Publish-TkEmailApp {
-Notes $updatedNotes `
-ErrorAction Stop | Out-Null
# Build an output object similar to "new" scenario
- $output = [PSCustomObject]@{
- AppId = $existingApp.AppId
- Id = $existingApp.Id
- AppName = "CN=$updatedString"
- CertExpires = $certDetails.CertExpires
- CertThumbprint = $certDetails.CertThumbprint
- TenantID = $Context.TenantId
+ $EmailAppParams = @{
+ AppId = $appRegistration.AppId
+ Id = $appRegistration.Id
+ AppName = "CN=$updatedString"
+ AppRestrictedSendGroup = $MailEnabledSendingGroup
+ CertExpires = $CertDetails.CertExpires
+ CertThumbprint = $CertDetails.CertThumbprint
+ ConsentUrl = $null
+ DefaultDomain = ($notesObject.GraphEmailAppFor.Split('@')[1])
+ SendAsUser = ($notesObject.GraphEmailAppFor.Split('@')[0])
+ SendAsUserEmail = $notesObject.GraphEmailAppFor
+ TenantID = $output.TenantID
}
- $graphEmailApp = [TkEmailAppParams]::new(
- $output.AppId,
- $output.Id,
- $output.AppName,
- $notesObject.RestrictedToGroup, # AppRestrictedSendGroup
- $output.CertExpires,
- $output.CertThumbprint,
- $null, # ConsentUrl (Made as nullable string)
- ($notesObject.GraphEmailAppFor.Split('@')[1]), # DefaultDomain
- ($notesObject.GraphEmailAppFor.Split('@')[0]), # SendAsUser
- $notesObject.GraphEmailAppFor, # SendAsUserEmail
- $output.TenantID
- )
+ [TkEmailAppParams]$graphEmailApp = New-TkEmailAppParams @EmailAppParams
# Store updated info in the vault
- $secretName = "CN=$updatedString"
- $savedSecretName = Set-TkJsonSecret `
- -Name $secretName `
- -InputObject $graphEmailApp `
- -VaultName $VaultName `
- -Overwrite:$OverwriteVaultSecret
- Write-AuditLog "Secret for existing app saved as '$secretName' in vault '$VaultName'."
+ $JsonSecretParams = @{
+ Name = "CN=$updatedString"
+ InputObject = $graphEmailApp
+ VaultName = $VaultName
+ Overwrite = $OverwriteVaultSecret
+ ErrorAction = 'Stop'
+ }
+ $savedSecretName = Set-TkJsonSecret @JsonSecretParams
+ Write-AuditLog "Secret for existing app saved as '$savedSecretName' in vault '$VaultName'."
}
catch {
throw
diff --git a/source/Public/Publish-TkM365AuditApp.ps1 b/source/Public/Publish-TkM365AuditApp.ps1
index e68c10c..ef40e9d 100644
--- a/source/Public/Publish-TkM365AuditApp.ps1
+++ b/source/Public/Publish-TkM365AuditApp.ps1
@@ -129,21 +129,18 @@ function Publish-TkM365AuditApp {
$sharePoint = @('Sites.Read.All', 'Sites.FullControl.All')
$exchange = @('Exchange.ManageAsApp')
# Decide which sets to use
- $graphPerms = $graph
- $sharePointPerms = $sharePoint
- $exchangePerms = $exchange
$permissionsObject = [PSCustomObject]@{
- Graph = $graphPerms
- SharePoint = $sharePointPerms
- Exchange = $exchangePerms
+ Graph = $graph
+ SharePoint = $sharePoint
+ Exchange = $exchange
}
- Write-AuditLog "Graph Perms: $($graphPerms -join ', ')"
- Write-AuditLog "SharePoint Perms: $($sharePointPerms -join ', ')"
- Write-AuditLog "Exchange Perms: $($exchangePerms -join ', ')"
+ Write-AuditLog "Graph Perms: $($graph -join ', ')"
+ Write-AuditLog "SharePoint Perms: $($sharePoint -join ', ')"
+ Write-AuditLog "Exchange Perms: $($exchange -join ', ')"
$Context = Get-MgContext -ErrorAction Stop
# Gather the resource access objects (GUIDs) for all these perms
$AppSettings = New-TkRequiredResourcePermissionObject `
- -GraphPermissions $graphPerms `
+ -GraphPermissions $graph `
-Scenario '365Audit' `
-ErrorAction Stop
# Generate the app name
@@ -153,21 +150,23 @@ function Publish-TkM365AuditApp {
-ErrorAction Stop
Write-AuditLog "Proposed new M365 Audit App name: $appName"
# Retrieve or create the certificate
- $CertDetails = Initialize-TkAppAuthCertificate `
- -AppName $appName `
- -Thumbprint $CertThumbprint `
- -Subject "CN=$appName" `
- -KeyExportPolicy $KeyExportPolicy `
- -ErrorAction Stop
+ $AppAuthCertificateParams = @{
+ AppName = $appName
+ Thumbprint = $CertThumbprint
+ Subject = "CN=$appName"
+ KeyExportPolicy = $KeyExportPolicy
+ ErrorAction = 'Stop'
+ }
+ $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams
Write-AuditLog "Certificate Thumbprint: $($CertDetails.CertThumbprint); Expires: $($CertDetails.CertExpires)."
# Show user proposed config
$proposed = [PSCustomObject]@{
ProposedAppName = $appName
CertificateThumbprint = $CertDetails.CertThumbprint
CertExpires = $CertDetails.CertExpires
- GraphPermissions = $graphPerms -join ', '
- SharePointPermissions = $sharePointPerms -join ', '
- ExchangePermissions = $exchangePerms -join ', '
+ GraphPermissions = $graph -join ', '
+ SharePointPermissions = $sharePoint -join ', '
+ ExchangePermissions = $exchange -join ', '
}
Write-AuditLog 'Proposed creation of a new M365 Audit App with the following properties:'
Write-AuditLog "$($proposed | Format-List)"
@@ -175,9 +174,9 @@ function Publish-TkM365AuditApp {
$notesHash = [ordered]@{
'Certificate Thumbprint' = $($CertDetails.CertThumbprint)
'Certificate Expires' = $($CertDetails.CertExpires)
- 'GraphAppPermissions' = $($graphPerms -join ', ')
- 'SharePointAppPermissions' = $($sharePointPerms -join ', ')
- 'ExchangeAppPermissions' = $($exchangePerms -join ', ')
+ 'GraphAppPermissions' = $($graph -join ', ')
+ 'SharePointAppPermissions' = $($sharePoint -join ', ')
+ 'ExchangeAppPermissions' = $($exchange -join ', ')
'RolesAssigned' = @('Exchange Administrator', 'Global Reader')
'AuthorizedClient IP' = $((Invoke-RestMethod ifconfig.me/ip))
'ClientOrUserHostname' = if ($env:COMPUTERNAME) { $env:COMPUTERNAME } else { $env:USERNAME }
@@ -186,23 +185,27 @@ function Publish-TkM365AuditApp {
$Notes = $notesHash | ConvertTo-Json #-Compress
if ($PSCmdlet.ShouldProcess($appName, 'Create and configure M365 Audit App in EntraAD')) {
Write-AuditLog 'Creating new EntraAD application with all resource permissions...'
- $appRegistration = New-TkAppRegistration `
- -DisplayName $appName `
- -CertThumbprint $CertDetails.CertThumbprint `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -Notes $Notes `
- -SignInAudience 'AzureADMyOrg' `
- -ErrorAction Stop
+ $AppRegistrationParams = @{
+ DisplayName = $appName
+ CertThumbprint = $CertDetails.CertThumbprint
+ RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
+ SignInAudience = 'AzureADMyOrg'
+ Notes = $Notes
+ ErrorAction = 'Stop'
+ }
+ $appRegistration = New-TkAppRegistration @AppRegistrationParams
Write-AuditLog "App registered. Object ID = $($appRegistration.Id), ClientId = $($appRegistration.AppId)."
# Grant the oauth2 permissions to service principal
- $ConsentUrl = Initialize-TkAppSpRegistration `
- -AppRegistration $appRegistration `
- -Context $Context `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -Scopes $permissionsObject `
- -AuthMethod 'Certificate' `
- -CertThumbprint $CertDetails.CertThumbprint `
- -ErrorAction Stop
+ $AppSpRegistrationParams = @{
+ AppRegistration = $appRegistration
+ Context = $Context
+ RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
+ Scopes = $permissionsObject
+ AuthMethod = 'Certificate'
+ CertThumbprint = $CertDetails.CertThumbprint
+ ErrorAction = 'Stop'
+ }
+ $ConsentUrl = Initialize-TkAppSpRegistration @AppSpRegistrationParams
[void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
Write-AuditLog 'Appending Exchange Administrator role to the app.'
$exoAdminRole = Get-MgDirectoryRole -Filter "displayName eq 'Exchange Administrator'" -ErrorAction Stop
@@ -225,37 +228,29 @@ function Publish-TkM365AuditApp {
-BodyParameter $body `
-ErrorAction Stop
# Store final app info in the vault
- $output = [PSCustomObject]@{
- AppName = $("CN=$appName")
+ $M365AuditAppParams = @{
+ AppName = "CN=$appName"
AppId = $appRegistration.AppId
ObjectId = $appRegistration.Id
TenantId = $context.TenantId
CertThumbprint = $CertDetails.CertThumbprint
CertExpires = $CertDetails.CertExpires
ConsentUrl = $ConsentUrl
- MgGraphPermissions = "$($graphPerms)"
- SharePointPermissions = "$($sharePointPerms)"
- ExchangePermissions = "$($exchangePerms)"
+ MgGraphPermissions = "$graph"
+ SharePointPermissions = "$sharePoint"
+ ExchangePermissions = "$exchange"
}
- $m365AuditApp = [TkM365AuditAppParams]::new(
- $output.AppName,
- $output.AppId,
- $output.ObjectId,
- $output.TenantId,
- $output.CertThumbprint,
- $output.CertExpires,
- $output.ConsentUrl,
- $output.MgGraphPermissions,
- $output.SharePointPermissions,
- $output.ExchangePermissions
- )
+ [TkM365AuditAppParams]$m365AuditApp = New-TkM365AuditAppParams @M365AuditAppParams
# Save to vault
- Set-TkJsonSecret `
- -Name "CN=$appName" `
- -InputObject $output `
- -VaultName $VaultName `
- -Overwrite:$OverwriteVaultSecret
- Write-AuditLog "Saved app credentials to vault '$VaultName'."
+ $JsonSecretParams = @{
+ Name = "CN=$appName"
+ InputObject = $m365AuditApp
+ VaultName = $VaultName
+ Overwrite = $OverwriteVaultSecret
+ ErrorAction = 'Stop'
+ }
+ $savedName = Set-TkJsonSecret @JsonSecretParams
+ Write-AuditLog "Secret '$savedName' saved to vault '$VaultName'."
# Return as either param splat or plain object
if ($ReturnParamSplat) {
return $m365AuditApp | ConvertTo-ParameterSplat
diff --git a/source/Public/Publish-TkMemPolicyManagerApp.ps1 b/source/Public/Publish-TkMemPolicyManagerApp.ps1
index 4211562..9448a68 100644
--- a/source/Public/Publish-TkMemPolicyManagerApp.ps1
+++ b/source/Public/Publish-TkMemPolicyManagerApp.ps1
@@ -66,7 +66,8 @@ function Publish-TkMemPolicyManagerApp {
$CertThumbprint,
[Parameter(
Mandatory = $false,
- HelpMessage = 'Key export policy for the certificate.'
+ HelpMessage = `
+ 'Key export policy for the certificate.'
)]
[ValidateSet('Exportable', 'NonExportable')]
[string]
@@ -123,16 +124,14 @@ function Publish-TkMemPolicyManagerApp {
'DelegatedPermissionGrant.ReadWrite.All',
'Directory.ReadWrite.All'
)
- Connect-TkMsService -MgGraph -GraphAuthScopes $scopesNeeded
+ Connect-TkMsService `
+ -MgGraph `
+ -GraphAuthScopes $scopesNeeded `
+ -ErrorAction Stop
$Context = Get-MgContext -ErrorAction Stop
}
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
- )
+ throw
}
}
process {
@@ -158,19 +157,25 @@ function Publish-TkMemPolicyManagerApp {
}
Write-AuditLog "Using the following MEM permissions: $($permissions -join ', ')"
# 2) Build a Graph context object that looks up these permission IDs
- $AppSettings = New-TkRequiredResourcePermissionObject -GraphPermissions $permissions
+ $AppSettings = New-TkRequiredResourcePermissionObject `
+ -GraphPermissions $permissions
# 3) Build an app name for scenario "MemPolicyManager"
- $appName = New-TkAppName -Prefix $AppPrefix -ScenarioName 'MemPolicyManager'
+ $appName = New-TkAppName `
+ -Prefix $AppPrefix `
+ -ScenarioName 'MemPolicyManager' `
+ -ErrorAction Stop
# 4) Add TenantId & AppName to the object so we can store them in the final JSON
$AppSettings | Add-Member -NotePropertyName 'TenantId' -NotePropertyValue $Context.TenantId
$AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName
# 5) Create or retrieve the certificate
- $CertDetails = Initialize-TkAppAuthCertificate `
- -AppName $AppSettings.AppName `
- -Thumbprint $CertThumbprint `
- -Subject "CN=$($AppSettings.AppName)" `
- -KeyExportPolicy $KeyExportPolicy `
- -ErrorAction Stop
+ $AppAuthCertificateParams = @{
+ AppName = $AppSettings.AppName
+ Thumbprint = $CertThumbprint
+ Subject = "CN=$($AppSettings.AppName)"
+ KeyExportPolicy = $KeyExportPolicy
+ ErrorAction = 'Stop'
+ }
+ $CertDetails = Initialize-TkAppAuthCertificate @AppAuthCertificateParams
# Build a “proposed” object so the user sees what’s about to happen
$proposedObject = [PSCustomObject]@{
ProposedAppName = $AppSettings.AppName
@@ -182,60 +187,61 @@ function Publish-TkMemPolicyManagerApp {
}
Write-AuditLog 'Proposed creation of a new MEM Policy Manager App with the following properties:'
Write-AuditLog "$($proposedObject | Format-List)"
- # The big If: confirm with ShouldProcess
- $Notes = @"
-Certificate Thumbprint: $($CertDetails.CertThumbprint)
-Certificate Expires: $($CertDetails.CertExpires)
-Tenant ID: $($Context.TenantId)
-Graph App Permissions: $($permissions -join ', ')
-Read-Write Permissions: $(if ($ReadWrite) { 'ReadWrite' } else { 'Read-Only' })
-Authorized Client IP: $((Invoke-RestMethod ifconfig.me/ip))
-Client Hostname: $env:COMPUTERNAME
-"@
+ $notesHash = [ordered]@{
+ 'Certificate Thumbprint' = $($CertDetails.CertThumbprint)
+ 'Certificate Expires' = $($CertDetails.CertExpires)
+ 'GraphAppPermissions' = $($permissions -join ', ')
+ 'Read-Write Permissions' = $(if ($ReadWrite) { 'ReadWrite' } else { 'Read-Only' })
+ 'AuthorizedClient IP' = $((Invoke-RestMethod ifconfig.me/ip))
+ 'ClientOrUserHostname' = if ($env:COMPUTERNAME) { $env:COMPUTERNAME } else { $env:USERNAME }
+ }
+ # Convert that hashtable to a JSON string:
+ $Notes = $notesHash | ConvertTo-Json #-Compress
if ($PSCmdlet.ShouldProcess("MemPolicyManager App '$($AppSettings.AppName)'",
'Create and configure a new MEM Policy Manager app in Azure AD?')) {
# 6) Register the application (with the cert)
- $appRegistration = New-TkAppRegistration `
- -DisplayName $AppSettings.AppName `
- -CertThumbprint $CertDetails.CertThumbprint `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -SignInAudience 'AzureADMyOrg' `
- -Notes $Notes `
- -ErrorAction Stop
+ $AppRegistrationParams = @{
+ DisplayName = $AppSettings.AppName
+ CertThumbprint = $CertDetails.CertThumbprint
+ RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
+ SignInAudience = 'AzureADMyOrg'
+ Notes = $Notes
+ ErrorAction = 'Stop'
+ }
+ $appRegistration = New-TkAppRegistration @AppRegistrationParams
# 7) Create the Service Principal & grant the permissions
- $ConsentUrl = Initialize-TkAppSpRegistration `
- -AppRegistration $appRegistration `
- -Context $Context `
- -RequiredResourceAccessList $AppSettings.RequiredResourceAccessList `
- -Scopes $permissionsObject `
- -AuthMethod 'Certificate' `
- -CertThumbprint $CertDetails.CertThumbprint `
- -ErrorAction Stop
+ $AppSpRegistrationParams = @{
+ AppRegistration = $appRegistration
+ Context = $Context
+ RequiredResourceAccessList = $AppSettings.RequiredResourceAccessList
+ Scopes = $permissionsObject
+ AuthMethod = 'Certificate'
+ CertThumbprint = $CertDetails.CertThumbprint
+ ErrorAction = 'Stop'
+ }
+ $ConsentUrl = Initialize-TkAppSpRegistration @AppSpRegistrationParams
[void](Read-Host 'Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue.')
# 8) Build a final PSCustomObject to store in the secret vault
- $output = [PSCustomObject]@{
+ $TkMemPolicyManagerAppParams = @{
AppId = $appRegistration.AppId
AppName = "CN=$($AppSettings.AppName)"
CertThumbprint = $CertDetails.CertThumbprint
- ClientId = $appRegistration.AppId
+ ObjectId = $appRegistration.Id
ConsentUrl = $ConsentUrl
PermissionSet = if ($ReadWrite) { 'ReadWrite' } else { 'ReadOnly' }
Permissions = $permissions
TenantId = $Context.TenantId
}
- $auditObj = [TkMemPolicyManagerAppParams]::new(
- $output.AppId,
- $output.AppName,
- $output.CertThumbprint,
- $output.ClientId,
- $output.ConsentUrl,
- $output.PermissionSet,
- $output.Permissions,
- $output.TenantId
- )
+ [TkMemPolicyManagerAppParams]$auditObj = New-TkMemPolicyManagerAppParams @TkMemPolicyManagerAppParams
# 9) Store as JSON secret
- $secretName = "CN=$($AppSettings.AppName)"
- $savedName = Set-TkJsonSecret -Name $secretName -InputObject $output -VaultName $VaultName -Overwrite:$OverwriteVaultSecret
+ $JsonSecretParams = @{
+ Name = "CN=$($AppSettings.AppName)"
+ InputObject = $auditObj
+ VaultName = $VaultName
+ Overwrite = $OverwriteVaultSecret
+ ErrorAction = 'Stop'
+ }
+ $savedName = Set-TkJsonSecret @JsonSecretParams
Write-AuditLog "Secret '$savedName' saved to vault '$VaultName'."
# Return the final object (param-splat or normal)
if ($ReturnParamSplat) {
From ff5a7206acef0277b4202a46bc74f54fae5d09b6 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 08:58:02 -0500
Subject: [PATCH 04/38] fix: remove -confirm from connect function
---
source/Private/Connect-TkMsService.ps1 | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/source/Private/Connect-TkMsService.ps1 b/source/Private/Connect-TkMsService.ps1
index 201972c..884a2a4 100644
--- a/source/Private/Connect-TkMsService.ps1
+++ b/source/Private/Connect-TkMsService.ps1
@@ -74,7 +74,7 @@ function Connect-TkMsService {
Remove-MgContext -ErrorAction SilentlyContinue
Write-AuditLog 'Creating a new Microsoft Graph session.'
Connect-MgGraph -Scopes $scopesNeeded `
- -ErrorAction Stop -Confirm
+ -ErrorAction Stop
Write-AuditLog 'Connected to Microsoft Graph.'
}
}
@@ -82,7 +82,7 @@ function Connect-TkMsService {
# No valid session, so just connect
Write-AuditLog 'No valid Microsoft Graph session found. Connecting...'
Connect-MgGraph -Scopes $scopesNeeded `
- -ErrorAction Stop -Confirm
+ -ErrorAction Stop
Write-AuditLog 'Connected to Microsoft Graph.'
}
}
From 33d33e6853089741b90bab9ee4cf5b450530a273 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 08:58:20 -0500
Subject: [PATCH 05/38] add: Todo
---
source/Public/New-MailEnabledSendingGroup.ps1 | 1 +
1 file changed, 1 insertion(+)
diff --git a/source/Public/New-MailEnabledSendingGroup.ps1 b/source/Public/New-MailEnabledSendingGroup.ps1
index 31ec70d..e42f626 100644
--- a/source/Public/New-MailEnabledSendingGroup.ps1
+++ b/source/Public/New-MailEnabledSendingGroup.ps1
@@ -77,6 +77,7 @@ function New-MailEnabledSendingGroup {
Write-AuditLog -BeginFunction
}
try {
+ # TODO Add confirmation prompt
Connect-TkMsService -ExchangeOnline
if (-not $Alias) {
$Alias = $Name
From 309527ed994d5c8f1990fa356062ed9f137b56d9 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 08:58:51 -0500
Subject: [PATCH 06/38] fix: Existing app output params
---
source/Public/Publish-TkEmailApp.ps1 | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/source/Public/Publish-TkEmailApp.ps1 b/source/Public/Publish-TkEmailApp.ps1
index d8ab841..26aa5b5 100644
--- a/source/Public/Publish-TkEmailApp.ps1
+++ b/source/Public/Publish-TkEmailApp.ps1
@@ -381,17 +381,17 @@ function Publish-TkEmailApp {
-ErrorAction Stop | Out-Null
# Build an output object similar to "new" scenario
$EmailAppParams = @{
- AppId = $appRegistration.AppId
- Id = $appRegistration.Id
+ AppId = $existingApp.AppId
+ Id = $existingApp.Id
AppName = "CN=$updatedString"
- AppRestrictedSendGroup = $MailEnabledSendingGroup
+ AppRestrictedSendGroup = $notesObject.RestrictedToGroup
CertExpires = $CertDetails.CertExpires
CertThumbprint = $CertDetails.CertThumbprint
ConsentUrl = $null
DefaultDomain = ($notesObject.GraphEmailAppFor.Split('@')[1])
SendAsUser = ($notesObject.GraphEmailAppFor.Split('@')[0])
SendAsUserEmail = $notesObject.GraphEmailAppFor
- TenantID = $output.TenantID
+ TenantID = $Context.TenantID
}
[TkEmailAppParams]$graphEmailApp = New-TkEmailAppParams @EmailAppParams
# Store updated info in the vault
From c92f2b3c8878e57b15dabc1977f41480d87e215d Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 10:45:00 -0500
Subject: [PATCH 07/38] add: Helper file to gitignore
---
.gitignore | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 53dfda4..75bad52 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,5 @@ markdownissues.txt
node_modules
package-lock.json
ZZBuild-Help.ps1
-test1.ps1
\ No newline at end of file
+test1.ps1
+helpdoc.ps1
\ No newline at end of file
From 6a2a82a1cdfb2db74efd79c06b417ec5f406d845 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 10:45:18 -0500
Subject: [PATCH 08/38] add: confirm to high for connect function
---
source/Private/Connect-TkMsService.ps1 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/source/Private/Connect-TkMsService.ps1 b/source/Private/Connect-TkMsService.ps1
index 884a2a4..697e73c 100644
--- a/source/Private/Connect-TkMsService.ps1
+++ b/source/Private/Connect-TkMsService.ps1
@@ -1,5 +1,5 @@
function Connect-TkMsService {
- [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
+ [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param (
[Parameter(
HelpMessage = 'Connect to Microsoft Graph.'
From aaf03f3f2eff23a18c727b5a90fcfcd8407fb9a6 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 10:46:32 -0500
Subject: [PATCH 09/38] fix: manual app call for sending email
---
source/Public/Send-TkEmailAppMessage.ps1 | 2 ++
1 file changed, 2 insertions(+)
diff --git a/source/Public/Send-TkEmailAppMessage.ps1 b/source/Public/Send-TkEmailAppMessage.ps1
index a601c8b..2d41365 100644
--- a/source/Public/Send-TkEmailAppMessage.ps1
+++ b/source/Public/Send-TkEmailAppMessage.ps1
@@ -187,12 +187,14 @@ function Send-TkEmailAppMessage {
# If manual parameter set:
if ($PSCmdlet.ParameterSetName -eq 'Manual') {
$cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint } -ErrorAction Stop
+ # TODO Confirm this object is not needed elsewhere
$GraphEmailApp = @{
AppId = $AppId
CertThumbprint = $CertThumbprint
TenantID = $TenantId
CertExpires = $cert.NotAfter
}
+ $Tenant = $TenantId
}
elseif ($PSCmdlet.ParameterSetName -eq 'Vault') {
# If a GraphEmailApp object was not passed in, attempt to retrieve it from the local machine
From 2cfa118e35d3d9f718faa8e3c37917cb8d595445 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 10:47:27 -0500
Subject: [PATCH 10/38] docs: Update Changelog
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b82a3b..907b1bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed formatting.
+- Manual app call for sending email.
+- Confirm to high for connect function.
## [0.1.2] - 2025-03-11
From 54f1d925edde7739bf63410e2d8979c1733ca888 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Wed, 12 Mar 2025 16:38:28 -0500
Subject: [PATCH 11/38] add: private functions and updated docs
---
CHANGELOG.md | 1 +
source/Private/Get-TkExistingCert.ps1 | 40 +++++++++++
source/Private/Get-TkExistingSecret.ps1 | 33 +++++++++
source/Public/Publish-TkEmailApp.ps1 | 94 +++++++++++++++----------
4 files changed, 130 insertions(+), 38 deletions(-)
create mode 100644 source/Private/Get-TkExistingCert.ps1
create mode 100644 source/Private/Get-TkExistingSecret.ps1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 907b1bf..d4bb657 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added cert options to the GraphAppToolkit send email.
- Updated auth methods to invoke needed permissions only.
+- Added private functions to handle existing certs and secrets.
### Fixed
diff --git a/source/Private/Get-TkExistingCert.ps1 b/source/Private/Get-TkExistingCert.ps1
new file mode 100644
index 0000000..47f247f
--- /dev/null
+++ b/source/Private/Get-TkExistingCert.ps1
@@ -0,0 +1,40 @@
+<#
+.SYNOPSIS
+ Retrieves an existing certificate from the current user's certificate store based on the provided certificate name.
+.DESCRIPTION
+ The Get-TkExistingCert function searches for a certificate in the current user's "My" certificate store with a subject that matches the provided certificate name.
+ If the certificate is found, it logs audit messages and provides instructions for removing the certificate if needed.
+ If the certificate is not found, it logs an audit message indicating that the certificate does not exist.
+.PARAMETER CertName
+ The subject name of the certificate to search for in the current user's certificate store.
+.EXAMPLE
+ PS C:\> Get-TkExistingCert -CertName "CN=example.com"
+ This command searches for a certificate with the subject "CN=example.com" in the current user's certificate store.
+.NOTES
+ Author: DrIOSx
+ Date: 2025-03-12
+ Version: 1.0
+#>
+function Get-TkExistingCert {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]$CertName
+ )
+ $ExistingCert = Get-ChildItem -Path Cert:\CurrentUser\My -ErrorAction SilentlyContinue |
+ Where-Object { $_.Subject -eq $CertName } -ErrorAction SilentlyContinue
+ if ( $ExistingCert) {
+ $VerbosePreference = 'Continue'
+ Write-AuditLog "Certificate with subject '$CertName' already exists in the certificate store."
+ Write-AuditLog 'You can remove the old certificate if no longer needed with the following commands:'
+ Write-AuditLog '1. Verify if more than one cert already exists:'
+ Write-AuditLog "Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { `$_.Subject -eq '$CertName' }"
+ Write-AuditLog '2. If you are comfortable removing the old certificate, and any duplicates, run the following command:'
+ Write-AuditLog "Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { `$_.Subject -eq '$CertName' } | Remove-Item"
+ $VerbosePreference = 'SilentlyContinue'
+ throw "Certificate with subject '$CertName' already exists in the certificate store."
+ }
+ else {
+ Write-AuditLog "Certificate with subject '$CertName' does not exist in the certificate store. Continuing..."
+ }
+}
\ No newline at end of file
diff --git a/source/Private/Get-TkExistingSecret.ps1 b/source/Private/Get-TkExistingSecret.ps1
new file mode 100644
index 0000000..e51ec4a
--- /dev/null
+++ b/source/Private/Get-TkExistingSecret.ps1
@@ -0,0 +1,33 @@
+<#
+ .SYNOPSIS
+ Checks if a secret exists in the specified secret vault.
+ .PARAMETER AppName
+ The name of the application for which the secret is being checked.
+ .PARAMETER VaultName
+ The name of the secret vault where the secret is stored. Defaults to 'GraphEmailAppLocalStore'.
+ .Outputs
+ [bool] $true if the secret exists, $false otherwise.
+ .EXAMPLE
+ $secretExists = Get-TkExistingSecret -AppName 'MyApp'
+ if ($secretExists) {
+ Write-Output "Secret exists."
+ } else {
+ Write-Output "Secret does not exist."
+ }
+ .NOTES
+ This function uses the Get-Secret cmdlet to check for the existence of a secret in the specified vault.
+#>
+function Get-TkExistingSecret {
+ param (
+ [string]$AppName,
+ [string]$VaultName = 'GraphEmailAppLocalStore'
+ )
+ $ExistingSecret = Get-Secret -Name "$AppName" -Vault $VaultName -ErrorAction SilentlyContinue
+ if ($ExistingSecret) {
+ return $true
+ }
+ else {
+ return $false
+ }
+}
+
diff --git a/source/Public/Publish-TkEmailApp.ps1 b/source/Public/Publish-TkEmailApp.ps1
index 26aa5b5..04125a5 100644
--- a/source/Public/Publish-TkEmailApp.ps1
+++ b/source/Public/Publish-TkEmailApp.ps1
@@ -1,54 +1,47 @@
<#
.SYNOPSIS
- Deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication.
+ Publishes a new or existing Graph Email App with specified configurations.
.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. Additionally, you can specify a
- KeyExportPolicy for the certificate, control how secrets are stored via VaultName and OverwriteVaultSecret,
- and optionally return a parameter splat instead of a PSCustomObject.
+ The Publish-TkEmailApp function creates or configures a Graph Email App in Azure AD. It supports two scenarios:
+ 1. Creating a new app with specified parameters.
+ 2. Using an existing app and attaching a certificate to it.
.PARAMETER AppPrefix
- A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for
- grouping purposes (2-4 alphanumeric characters).
+ The prefix used to initialize the Graph Email App. Must be 2-4 characters, letters, and numbers only. Default is 'Gtk'.
.PARAMETER AuthorizedSenderUserName
- The username of the authorized sender.
+ The username of the authorized sender. Must be a valid email address.
.PARAMETER MailEnabledSendingGroup
- The mail-enabled group to which the sender belongs. This will be used to assign
- app policy restrictions.
+ The mail-enabled security group. Must be a valid email address.
+ .PARAMETER ExistingAppObjectId
+ The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID.
+ .PARAMETER CertPrefix
+ Prefix to add to the certificate subject for the existing app.
.PARAMETER CertThumbprint
- An optional parameter indicating the thumbprint of the certificate to be retrieved. If not
- specified, a self-signed certificate will be generated.
+ The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string.
.PARAMETER KeyExportPolicy
- Specifies the key export policy for the newly created certificate. Valid values are
- 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'.
+ Key export policy for the certificate. Valid values are 'Exportable' and 'NonExportable'. Default is 'NonExportable'.
.PARAMETER VaultName
- If specified, the name of the vault to store the app's credentials. Otherwise,
- defaults to 'GraphEmailAppLocalStore'.
+ If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'.
.PARAMETER OverwriteVaultSecret
- If specified, the function overwrites an existing secret in the vault if it
- already exists.
+ If specified, overwrite the vault secret if it already exists.
.PARAMETER ReturnParamSplat
- If specified, returns the parameter splat for use in other functions instead
- of the PSCustomObject.
+ If specified, return the parameter splat for use in other functions.
.EXAMPLE
- PS C:\> Publish-TkEmailApp -AppPrefix "ABC" -AuthorizedSenderUserName "jdoe@example.com" -MailEnabledSendingGroup "GraphAPIMailGroup@example.com" -CertThumbprint "AABBCCDDEEFF11223344556677889900"
- .INPUTS
- None
- .OUTPUTS
- By default, returns a PSCustomObject containing details such as AppId, CertThumbprint,
- TenantID, and CertExpires. If -ReturnParamSplat is specified, returns the parameter
- splat instead.
+ Publish-TkEmailApp -AppPrefix 'Gtk' -AuthorizedSenderUserName 'user@example.com' -MailEnabledSendingGroup 'group@example.com'
+
+ Creates a new Graph Email App with the specified parameters.
+ .EXAMPLE
+ Publish-TkEmailApp -ExistingAppObjectId '12345678-1234-1234-1234-1234567890ab' -CertPrefix 'Cert'
+
+ Uses an existing app and attaches a certificate with the specified prefix.
.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.
+ This cmdlet requires that the user running the cmdlet have the necessary permissions to create the app and connect to Exchange Online.
Permissions required:
- 'Application.ReadWrite.All',
- 'DelegatedPermissionGrant.ReadWrite.All',
- 'Directory.ReadWrite.All',
- 'RoleManagement.ReadWrite.Directory'
-#>
+ - 'Application.ReadWrite.All'
+ - 'DelegatedPermissionGrant.ReadWrite.All'
+ - 'Directory.ReadWrite.All'
+ - 'RoleManagement.ReadWrite.Directory'
+#>
function Publish-TkEmailApp {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'CreateNewApp')]
param(
@@ -144,6 +137,16 @@ function Publish-TkEmailApp {
$ReturnParamSplat
)
begin {
+ <#
+ 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.
+ Permissions required:
+ 'Application.ReadWrite.All',
+ 'DelegatedPermissionGrant.ReadWrite.All',
+ 'Directory.ReadWrite.All',
+ 'RoleManagement.ReadWrite.Directory'
+ #>
if (-not $script:LogString) {
Write-AuditLog -Start
}
@@ -168,6 +171,7 @@ function Publish-TkEmailApp {
'DelegatedPermissionGrant.ReadWrite.All',
'Directory.ReadWrite.All'
)
+
}
catch {
throw
@@ -185,7 +189,10 @@ function Publish-TkEmailApp {
-ExchangeOnline `
-GraphAuthScopes $scopesNeeded
# 3) Grab MgContext for tenant info
- $Context = Get-MgContext -ErrorAction Stop
+ $Context = Get-MgContext
+ if (!$Context) {
+ throw 'Could not retrieve the context for the tenant.'
+ }
# 1) Validate the user (AuthorizedSenderUserName) is in tenant
$user = Get-MgUser -Filter "Mail eq '$AuthorizedSenderUserName'"
if (-not $user) {
@@ -198,6 +205,14 @@ function Publish-TkEmailApp {
-Prefix $AppPrefix `
-ScenarioName 'AuditGraphEmail' `
-UserId $AuthorizedSenderUserName
+ # Verify if the secret already exists in the vault
+ $existingSecret = Get-TkExistingSecret `
+ -AppName $appName `
+ -VaultName $VaultName `
+ -ErrorAction SilentlyContinue
+ if ($ExistingSecret -and -not $OverwriteVaultSecret) {
+ throw "Secret '$AppName' already exists in vault '$VaultName'. Use the -OverwriteVaultSecret switch to overwrite it."
+ }
# Add relevant properties
$AppSettings | Add-Member -NotePropertyName 'User' -NotePropertyValue $user
$AppSettings | Add-Member -NotePropertyName 'AppName' -NotePropertyValue $appName
@@ -315,7 +330,10 @@ function Publish-TkEmailApp {
Connect-TkMsService `
-MgGraph `
-GraphAuthScopes $scopesNeeded
- $Context = Get-MgContext -ErrorAction Stop
+ $Context = Get-MgContext
+ if (!$Context) {
+ throw 'Could not retrieve the context for the tenant.'
+ }
$ClientCertPrefix = "$CertPrefix"
# Retrieve the existing app registration by AppId
Write-AuditLog "Looking up existing app with ObjectId: $ExistingAppObjectId"
From a129e53fc260422ea80440dea2a6495eda86be0f Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:23:03 -0500
Subject: [PATCH 12/38] Update Help XML for Cmdlets
---
source/en-US/GraphAppToolkit-help.xml | 461 ++++++++++++++++----------
1 file changed, 291 insertions(+), 170 deletions(-)
diff --git a/source/en-US/GraphAppToolkit-help.xml b/source/en-US/GraphAppToolkit-help.xml
index cf72fef..3813b27 100644
--- a/source/en-US/GraphAppToolkit-help.xml
+++ b/source/en-US/GraphAppToolkit-help.xml
@@ -51,6 +51,28 @@
None
+
+ WhatIf
+
+ Shows what would happen if the cmdlet runs. The cmdlet is not run.
+
+
+ SwitchParameter
+
+
+ False
+
+
+ Confirm
+
+ Prompts you for confirmation before running the cmdlet.
+
+
+ SwitchParameter
+
+
+ False
+
ProgressAction
@@ -102,6 +124,28 @@
None
+
+ WhatIf
+
+ Shows what would happen if the cmdlet runs. The cmdlet is not run.
+
+
+ SwitchParameter
+
+
+ False
+
+
+ Confirm
+
+ Prompts you for confirmation before running the cmdlet.
+
+
+ SwitchParameter
+
+
+ False
+
ProgressAction
@@ -165,6 +209,30 @@
None
+
+ WhatIf
+
+ Shows what would happen if the cmdlet runs. The cmdlet is not run.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
+
+ Confirm
+
+ Prompts you for confirmation before running the cmdlet.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
ProgressAction
@@ -237,7 +305,7 @@
Publish-TkEmailApp
-
+
AppPrefix
A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for grouping purposes (2-4 alphanumeric characters).
@@ -249,7 +317,7 @@
None
-
+
AuthorizedSenderUserName
The username of the authorized sender.
@@ -261,7 +329,7 @@
None
-
+
MailEnabledSendingGroup
The mail-enabled group to which the sender belongs. This will be used to assign app policy restrictions.
@@ -273,7 +341,19 @@
None
-
+
+ CertPrefix
+
+ Prefix to add to the certificate subject for the existing app.
+
+ String
+
+ String
+
+
+ None
+
+
CertThumbprint
An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated.
@@ -285,7 +365,7 @@
None
-
+
KeyExportPolicy
Specifies the key export policy for the newly created certificate. Valid values are 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'.
@@ -297,7 +377,7 @@
NonExportable
-
+
VaultName
If specified, the name of the vault to store the app's credentials. Otherwise, defaults to 'GraphEmailAppLocalStore'.
@@ -331,10 +411,85 @@
False
-
- WhatIf
+
+ ProgressAction
- Shows what would happen if the cmdlet runs. The cmdlet is not run.
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+ Publish-TkEmailApp
+
+ ExistingAppObjectId
+
+ The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID.
+
+ String
+
+ String
+
+
+ None
+
+
+ CertPrefix
+
+ Prefix to add to the certificate subject for the existing app.
+
+ String
+
+ String
+
+
+ None
+
+
+ CertThumbprint
+
+ An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated.
+
+ String
+
+ String
+
+
+ None
+
+
+ KeyExportPolicy
+
+ Specifies the key export policy for the newly created certificate. Valid values are 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'.
+
+ String
+
+ String
+
+
+ NonExportable
+
+
+ VaultName
+
+ If specified, the name of the vault to store the app's credentials. Otherwise, defaults to 'GraphEmailAppLocalStore'.
+
+ String
+
+ String
+
+
+ GraphEmailAppLocalStore
+
+
+ OverwriteVaultSecret
+
+ If specified, the function overwrites an existing secret in the vault if it already exists.
SwitchParameter
@@ -342,10 +497,10 @@
False
-
- Confirm
+
+ ReturnParamSplat
- Prompts you for confirmation before running the cmdlet.
+ If specified, returns the parameter splat for use in other functions instead of the PSCustomObject.
SwitchParameter
@@ -368,7 +523,7 @@
-
+
AppPrefix
A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for grouping purposes (2-4 alphanumeric characters).
@@ -380,7 +535,7 @@
None
-
+
AuthorizedSenderUserName
The username of the authorized sender.
@@ -392,7 +547,7 @@
None
-
+
MailEnabledSendingGroup
The mail-enabled group to which the sender belongs. This will be used to assign app policy restrictions.
@@ -404,10 +559,10 @@
None
-
- CertThumbprint
+
+ ExistingAppObjectId
- An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated.
+ The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID.
String
@@ -416,58 +571,58 @@
None
-
- KeyExportPolicy
+
+ CertPrefix
- Specifies the key export policy for the newly created certificate. Valid values are 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'.
+ Prefix to add to the certificate subject for the existing app.
String
String
- NonExportable
+ None
-
- VaultName
+
+ CertThumbprint
- If specified, the name of the vault to store the app's credentials. Otherwise, defaults to 'GraphEmailAppLocalStore'.
+ An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated.
String
String
- GraphEmailAppLocalStore
+ None
- OverwriteVaultSecret
+ KeyExportPolicy
- If specified, the function overwrites an existing secret in the vault if it already exists.
+ Specifies the key export policy for the newly created certificate. Valid values are 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'.
- SwitchParameter
+ String
- SwitchParameter
+ String
- False
+ NonExportable
- ReturnParamSplat
+ VaultName
- If specified, returns the parameter splat for use in other functions instead of the PSCustomObject.
+ If specified, the name of the vault to store the app's credentials. Otherwise, defaults to 'GraphEmailAppLocalStore'.
- SwitchParameter
+ String
- SwitchParameter
+ String
- False
+ GraphEmailAppLocalStore
-
- WhatIf
+
+ OverwriteVaultSecret
- Shows what would happen if the cmdlet runs. The cmdlet is not run.
+ If specified, the function overwrites an existing secret in the vault if it already exists.
SwitchParameter
@@ -476,10 +631,10 @@
False
-
- Confirm
+
+ ReturnParamSplat
- Prompts you for confirmation before running the cmdlet.
+ If specified, returns the parameter splat for use in other functions instead of the PSCustomObject.
SwitchParameter
@@ -501,42 +656,8 @@
None
-
-
-
- None
-
-
-
-
-
-
-
-
-
- By default, returns a PSCustomObject containing details such as AppId, CertThumbprint,
-
-
-
-
-
-
-
- TenantID, and CertExpires. If -ReturnParamSplat is specified, returns the parameter
-
-
-
-
-
-
-
- splat instead.
-
-
-
-
-
-
+
+
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.
@@ -639,28 +760,6 @@
False
-
- WhatIf
-
- Shows what would happen if the cmdlet runs. The cmdlet is not run.
-
-
- SwitchParameter
-
-
- False
-
-
- Confirm
-
- Prompts you for confirmation before running the cmdlet.
-
-
- SwitchParameter
-
-
- False
-
ProgressAction
@@ -748,30 +847,6 @@
False
-
- WhatIf
-
- Shows what would happen if the cmdlet runs. The cmdlet is not run.
-
- SwitchParameter
-
- SwitchParameter
-
-
- False
-
-
- Confirm
-
- Prompts you for confirmation before running the cmdlet.
-
- SwitchParameter
-
- SwitchParameter
-
-
- False
-
ProgressAction
@@ -936,28 +1011,6 @@ the credentials in the default vault.
False
-
- WhatIf
-
- Shows what would happen if the cmdlet runs. The cmdlet is not run.
-
-
- SwitchParameter
-
-
- False
-
-
- Confirm
-
- Prompts you for confirmation before running the cmdlet.
-
-
- SwitchParameter
-
-
- False
-
ProgressAction
@@ -1057,30 +1110,6 @@ the credentials in the default vault.
False
-
- WhatIf
-
- Shows what would happen if the cmdlet runs. The cmdlet is not run.
-
- SwitchParameter
-
- SwitchParameter
-
-
- False
-
-
- Confirm
-
- Prompts you for confirmation before running the cmdlet.
-
- SwitchParameter
-
- SwitchParameter
-
-
- False
-
ProgressAction
@@ -1235,6 +1264,40 @@ the credentials in the default vault.
None
+
+ VaultName
+
+ [Vault Parameter Set Only] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'.
+
+ String
+
+ String
+
+
+ GraphEmailAppLocalStore
+
+
+ WhatIf
+
+ Shows what would happen if the cmdlet runs. The cmdlet is not run.
+
+
+ SwitchParameter
+
+
+ False
+
+
+ Confirm
+
+ Prompts you for confirmation before running the cmdlet.
+
+
+ SwitchParameter
+
+
+ False
+
ProgressAction
@@ -1346,6 +1409,28 @@ the credentials in the default vault.
None
+
+ WhatIf
+
+ Shows what would happen if the cmdlet runs. The cmdlet is not run.
+
+
+ SwitchParameter
+
+
+ False
+
+
+ Confirm
+
+ Prompts you for confirmation before running the cmdlet.
+
+
+ SwitchParameter
+
+
+ False
+
ProgressAction
@@ -1469,6 +1554,42 @@ the credentials in the default vault.
None
+
+ VaultName
+
+ [Vault Parameter Set Only] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'.
+
+ String
+
+ String
+
+
+ GraphEmailAppLocalStore
+
+
+ WhatIf
+
+ Shows what would happen if the cmdlet runs. The cmdlet is not run.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
+
+ Confirm
+
+ Prompts you for confirmation before running the cmdlet.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
ProgressAction
From 9564a0950098afbd73ee7cc37ae7df4f24050428 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:05 -0500
Subject: [PATCH 13/38] Refactor Connect-TkMsService Unit Tests
---
.../Private/Connect-TkMsService.tests.ps1 | 83 ++++++++++++++++---
1 file changed, 73 insertions(+), 10 deletions(-)
diff --git a/tests/Unit/Private/Connect-TkMsService.tests.ps1 b/tests/Unit/Private/Connect-TkMsService.tests.ps1
index 4a2aa69..28e6fba 100644
--- a/tests/Unit/Private/Connect-TkMsService.tests.ps1
+++ b/tests/Unit/Private/Connect-TkMsService.tests.ps1
@@ -4,24 +4,87 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
$(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
}).BaseName
-
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe 'Connect-TkMsService' {
+
+ # -- Mock your own function from your module:
+ Mock -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit'
+
+ # -- Microsoft.Graph commands:
+ Mock -CommandName 'Get-MgUser' -ModuleName 'Microsoft.Graph'
+ Mock -CommandName 'Get-MgContext' -ModuleName 'Microsoft.Graph'
+ Mock -CommandName 'Connect-MgGraph' -ModuleName 'Microsoft.Graph'
+ Mock -CommandName 'Remove-MgContext' -ModuleName 'Microsoft.Graph'
+ Mock -CommandName 'Get-MgOrganization' -ModuleName 'Microsoft.Graph'
+
+ # -- ExchangeOnlineManagement commands:
+ Mock -CommandName 'Get-OrganizationConfig' -ModuleName 'ExchangeOnlineManagement'
+ Mock -CommandName 'Connect-ExchangeOnline' -ModuleName 'ExchangeOnlineManagement'
+ Mock -CommandName 'Disconnect-ExchangeOnline' -ModuleName 'ExchangeOnlineManagement'
+
+ Context 'When connecting to Microsoft Graph' {
+
+ It 'Should connect to Microsoft Graph with specified scopes' {
+ $params = @{
+ MgGraph = $true
+ GraphAuthScopes = @('User.Read', 'Mail.Read')
+ Confirm = $false
+ }
+ Connect-TkMsService @params
+
+ # Notice the -ModuleName arguments below, matching the mocks
+ Assert-MockCalled -CommandName 'Connect-MgGraph' -ModuleName 'Microsoft.Graph' -Times 1
+ Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 `
+ -ParameterFilter { $_ -eq 'Connected to Microsoft Graph.' }
}
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
+ It 'Should reuse existing Microsoft Graph session if valid' {
+ Mock -CommandName 'Get-MgUser' -ModuleName 'Microsoft.Graph' -MockWith { $null }
+ Mock -CommandName 'Get-MgContext' -ModuleName 'Microsoft.Graph' -MockWith { @{ Scopes = @('User.Read', 'Mail.Read') } }
+
+ $params = @{
+ MgGraph = $true
+ GraphAuthScopes = @('User.Read', 'Mail.Read')
+ Confirm = $false
+ }
+ Connect-TkMsService @params
+
+ Assert-MockCalled -CommandName 'Get-MgUser' -ModuleName 'Microsoft.Graph' -Times 1
+ Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 `
+ -ParameterFilter { $_ -eq 'An active Microsoft Graph session is detected and all required scopes are present.' }
}
+ }
+
+ Context 'When connecting to Exchange Online' {
+
+ It 'Should connect to Exchange Online' {
+ $params = @{
+ ExchangeOnline = $true
+ Confirm = $false
+ }
+ Connect-TkMsService @params
+
+ Assert-MockCalled -CommandName 'Connect-ExchangeOnline' -ModuleName 'ExchangeOnlineManagement' -Times 1
+ Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 `
+ -ParameterFilter { $_ -eq 'Connected to Exchange Online.' }
+ }
+
+ It 'Should reuse existing Exchange Online session if valid' {
+ # Provide a mock for Get-OrganizationConfig from ExchangeOnlineManagement
+ Mock -CommandName 'Get-OrganizationConfig' -ModuleName 'ExchangeOnlineManagement' -MockWith { @{ Identity = 'TestOrg' } }
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ $params = @{
+ ExchangeOnline = $true
+ Confirm = $false
+ }
+ Connect-TkMsService @params
+
+ Assert-MockCalled -CommandName 'Get-OrganizationConfig' -ModuleName 'ExchangeOnlineManagement' -Times 1
+ Assert-MockCalled -CommandName 'Write-AuditLog' -ModuleName 'GraphAppToolkit' -Times 1 `
+ -ParameterFilter { $_ -eq 'An active Exchange Online session is detected.' }
}
}
}
}
-
From f90bea2530b37ff72d50916791512dd1d68f5982 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:05 -0500
Subject: [PATCH 14/38] Refactor ConvertTo-ParameterSplat Unit Tests
---
.../ConvertTo-ParameterSplat.tests.ps1 | 67 +++++++++++++++----
1 file changed, 54 insertions(+), 13 deletions(-)
diff --git a/tests/Unit/Private/ConvertTo-ParameterSplat.tests.ps1 b/tests/Unit/Private/ConvertTo-ParameterSplat.tests.ps1
index 4a2aa69..b6f073a 100644
--- a/tests/Unit/Private/ConvertTo-ParameterSplat.tests.ps1
+++ b/tests/Unit/Private/ConvertTo-ParameterSplat.tests.ps1
@@ -8,19 +8,60 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
- }
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
- }
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
- }
+ Describe 'ConvertTo-ParameterSplat' {
+ It 'should convert object properties to a parameter splatting hashtable script' {
+ $obj = [PSCustomObject]@{ Name = 'John'; Age = 30 }
+ $expected = @'
+$params = @{
+ Name = "John"
+ Age = 30
+}
+'@
+ $result = $obj | ConvertTo-ParameterSplat
+ $result | Should -BeExactly $expected
+ }
+ It 'should handle string properties correctly' {
+ $obj = [PSCustomObject]@{ City = 'New York'; Country = 'USA' }
+ $expected = @'
+$params = @{
+ City = "New York"
+ Country = "USA"
+}
+'@
+ $result = $obj | ConvertTo-ParameterSplat
+ $result | Should -BeExactly $expected
+ }
+ It 'should handle numeric properties correctly' {
+ $obj = [PSCustomObject]@{ Width = 1920; Height = 1080 }
+ $expected = @'
+$params = @{
+ Width = 1920
+ Height = 1080
+}
+'@
+ $result = $obj | ConvertTo-ParameterSplat
+ $result | Should -BeExactly $expected
+ }
+ It 'should handle mixed property types correctly' {
+ $obj = [PSCustomObject]@{ Name = 'Alice'; Age = 25; IsActive = $true }
+ $expected = @'
+$params = @{
+ Name = "Alice"
+ Age = 25
+ IsActive = True
+}
+'@
+ $result = $obj | ConvertTo-ParameterSplat
+ $result | Should -BeExactly $expected
+ }
+ It 'should handle empty objects correctly' {
+ $obj = [PSCustomObject]@{}
+ $expected = @'
+$params = @{
+}
+'@
+ $result = $obj | ConvertTo-ParameterSplat
+ $result | Should -BeExactly $expected
}
}
}
From 2fd4a4fe57c8ae1afdc936dc111dd2539636018b Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:05 -0500
Subject: [PATCH 15/38] Update Initialize-TkAppAuthCertificate Unit Tests
---
.../Initialize-TkAppAuthCertificate.tests.ps1 | 89 +++++++++++++++++--
1 file changed, 81 insertions(+), 8 deletions(-)
diff --git a/tests/Unit/Private/Initialize-TkAppAuthCertificate.tests.ps1 b/tests/Unit/Private/Initialize-TkAppAuthCertificate.tests.ps1
index 4a2aa69..a24bb74 100644
--- a/tests/Unit/Private/Initialize-TkAppAuthCertificate.tests.ps1
+++ b/tests/Unit/Private/Initialize-TkAppAuthCertificate.tests.ps1
@@ -8,18 +8,91 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe 'Initialize-TkAppAuthCertificate' {
+ Context 'When retrieving an existing certificate by thumbprint' {
+ It 'Should retrieve the certificate if it exists' {
+ # Arrange
+ $thumbprint = 'ABC123DEF456'
+ $cert = New-Object PSCustomObject -Property @{
+ Thumbprint = $thumbprint
+ NotAfter = (Get-Date).AddYears(1)
+ }
+ Mock -CommandName Get-ChildItem -MockWith {
+ $cert
+ }
+
+ # Act
+ $result = Initialize-TkAppAuthCertificate -Thumbprint $thumbprint -Confirm:$false
+
+ # Assert
+ $result.CertThumbprint | Should -Be $thumbprint
+ $result.CertExpires | Should -Be $cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss')
}
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
+ It 'Should throw an error if the certificate does not exist' {
+ # Arrange
+ $thumbprint = 'NONEXISTENT'
+ Mock -CommandName Get-ChildItem -MockWith {
+ $null
+ }
+
+ # Act & Assert
+ { Initialize-TkAppAuthCertificate -Thumbprint $thumbprint -Confirm:$false } | Should -Throw "Certificate with thumbprint $thumbprint not found in Cert:\CurrentUser\My."
}
+ }
+
+ Context 'When creating a new self-signed certificate' {
+ It 'Should create a new certificate if no thumbprint is provided' {
+ # Arrange
+ $subject = 'CN=MyAppCert'
+ $cert = New-Object PSCustomObject -Property @{
+ Thumbprint = 'NEWCERT123'
+ NotAfter = (Get-Date).AddYears(1)
+ }
+ Mock -CommandName New-SelfSignedCertificate -MockWith {
+ $cert
+ }
+ Mock -CommandName Get-TkExistingCert -MockWith {}
+
+ # Act
+ $result = Initialize-TkAppAuthCertificate -Subject $subject -Confirm:$false
+
+ # Assert
+ $result.CertThumbprint | Should -Be $cert.Thumbprint
+ $result.CertExpires | Should -Be $cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss')
+ }
+
+ It 'Should include AppName in the output if provided' {
+ # Arrange
+ $subject = 'CN=MyAppCert'
+ $appName = 'MyApp'
+ $cert = New-Object PSCustomObject -Property @{
+ Thumbprint = 'NEWCERT123'
+ NotAfter = (Get-Date).AddYears(1)
+ }
+ Mock -CommandName New-SelfSignedCertificate -MockWith {
+ $cert
+ }
+ Mock -CommandName Get-TkExistingCert -MockWith {}
+
+ # Act
+ $result = Initialize-TkAppAuthCertificate -Subject $subject -AppName $appName -Confirm:$false
+
+ # Assert
+ $result.CertThumbprint | Should -Be $cert.Thumbprint
+ $result.CertExpires | Should -Be $cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss')
+ $result.AppName | Should -Be $appName
+ }
+
+ It 'Should throw an error if certificate creation is skipped by user confirmation' {
+ # Arrange
+ Mock -CommandName New-SelfSignedCertificate -MockWith {
+ throw 'Certificate creation was skipped by user confirmation.'
+ }
+ Mock -CommandName Get-TkExistingCert -MockWith {}
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ # Act & Assert
+ { Initialize-TkAppAuthCertificate -Subject 'CN=MyAppCert' -Confirm:$false } | Should -Throw 'Certificate creation was skipped by user confirmation.'
}
}
}
From a219171a3c3d702af5838c00e29c68261715cc09 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:05 -0500
Subject: [PATCH 16/38] Refactor Initialize-TkAppSpRegistration Unit Tests
---
.../Initialize-TkAppSpRegistration.tests.ps1 | 72 ++++++++++++++++---
1 file changed, 62 insertions(+), 10 deletions(-)
diff --git a/tests/Unit/Private/Initialize-TkAppSpRegistration.tests.ps1 b/tests/Unit/Private/Initialize-TkAppSpRegistration.tests.ps1
index 4a2aa69..a43beae 100644
--- a/tests/Unit/Private/Initialize-TkAppSpRegistration.tests.ps1
+++ b/tests/Unit/Private/Initialize-TkAppSpRegistration.tests.ps1
@@ -8,18 +8,70 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe 'Initialize-TkAppSpRegistration' {
+ Mock -CommandName Write-AuditLog
+ Mock -CommandName Get-ChildItem
+ Mock -CommandName New-MgServicePrincipal
+ Mock -CommandName Get-MgServicePrincipal
+ Mock -CommandName New-MgOauth2PermissionGrant
+ Context 'When AuthMethod is Certificate and CertThumbprint is not provided' {
+ It 'Throws an error' {
+ $AppRegistration = [PSCustomObject]@{ AppId = 'test-app-id' }
+ $RequiredResourceAccessList = @()
+ $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' }
+ { Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' } | Should -Throw "CertThumbprint is required when AuthMethod is 'Certificate'."
}
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
+ }
+ Context 'When AuthMethod is Certificate and CertThumbprint is provided' {
+ It 'Retrieves the certificate and creates a service principal' {
+ $AppRegistration = [PSCustomObject]@{ AppId = 'test-app-id' }
+ $RequiredResourceAccessList = @()
+ $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' }
+ $CertThumbprint = 'test-thumbprint'
+ $Cert = [PSCustomObject]@{ Thumbprint = $CertThumbprint; SubjectName = [PSCustomObject]@{ Name = 'test-cert' } }
+ Mock -CommandName Get-ChildItem -MockWith { $Cert }
+ Mock -CommandName Get-MgServicePrincipal -MockWith { [PSCustomObject]@{ Id = 'test-sp-id'; DisplayName = 'test-sp' } }
+ Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' -CertThumbprint $CertThumbprint
+ Assert-MockCalled -CommandName Get-ChildItem -Times 1
+ Assert-MockCalled -CommandName New-MgServicePrincipal -Times 1
+ Assert-MockCalled -CommandName Get-MgServicePrincipal -Times 1
}
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ }
+ Context 'When AuthMethod is not Certificate' {
+ It 'Throws an error for unimplemented auth methods' {
+ $AppRegistration = [PSCustomObject]@{ AppId = 'test-app-id' }
+ $RequiredResourceAccessList = @()
+ $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' }
+ { Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'ClientSecret' } | Should -Throw "AuthMethod ClientSecret is not yet implemented."
+ }
+ }
+ Context 'When RequiredResourceAccessList has too many resources' {
+ It 'Throws an error' {
+ $AppRegistration = [PSCustomObject]@{ AppId = 'test-app-id' }
+ $RequiredResourceAccessList = @(
+ [PSCustomObject]@{ ResourceAppId = 'resource1'; ResourceAccess = @() },
+ [PSCustomObject]@{ ResourceAppId = 'resource2'; ResourceAccess = @() },
+ [PSCustomObject]@{ ResourceAppId = 'resource3'; ResourceAccess = @() }
+ )
+ $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' }
+ { Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context } | Should -Throw 'Too many resources in RequiredResourceAccessList.'
+ }
+ }
+ Context 'When RequiredResourceAccessList is valid' {
+ It 'Grants the required scopes and returns the admin consent URL' {
+ $AppRegistration = [PSCustomObject]@{ AppId = 'test-app-id' }
+ $RequiredResourceAccessList = @(
+ [PSCustomObject]@{ ResourceAppId = 'resource1'; ResourceAccess = @() }
+ )
+ $Context = [PSCustomObject]@{ TenantId = 'test-tenant-id' }
+ $CertThumbprint = 'test-thumbprint'
+ $Cert = [PSCustomObject]@{ Thumbprint = $CertThumbprint; SubjectName = [PSCustomObject]@{ Name = 'test-cert' } }
+ Mock -CommandName Get-ChildItem -MockWith { $Cert }
+ Mock -CommandName Get-MgServicePrincipal -MockWith { [PSCustomObject]@{ Id = 'test-sp-id'; DisplayName = 'test-sp' } }
+ Mock -CommandName New-MgOauth2PermissionGrant
+ $result = Initialize-TkAppSpRegistration -AppRegistration $AppRegistration -RequiredResourceAccessList $RequiredResourceAccessList -Context $Context -AuthMethod 'Certificate' -CertThumbprint $CertThumbprint
+ Assert-MockCalled -CommandName New-MgOauth2PermissionGrant -Times 1
+ $result | Should -Be "https://login.microsoftonline.com/test-tenant-id/adminconsent?client_id=test-app-id"
}
}
}
From 2a4159287fc408832012bb2abfb7f27ba9e9594a Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:05 -0500
Subject: [PATCH 17/38] Update Initialize-TkModuleEnv Unit Tests
---
.../Private/Initialize-TkModuleEnv.tests.ps1 | 78 +++++++++++++++++--
1 file changed, 70 insertions(+), 8 deletions(-)
diff --git a/tests/Unit/Private/Initialize-TkModuleEnv.tests.ps1 b/tests/Unit/Private/Initialize-TkModuleEnv.tests.ps1
index 4a2aa69..5713b23 100644
--- a/tests/Unit/Private/Initialize-TkModuleEnv.tests.ps1
+++ b/tests/Unit/Private/Initialize-TkModuleEnv.tests.ps1
@@ -8,18 +8,80 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe "Initialize-TkModuleEnv" {
+ Context "When installing public modules" {
+ It "Should install and import specified public modules" {
+ $params = @{
+ PublicModuleNames = "PSnmap","Microsoft.Graph"
+ PublicRequiredVersions = "1.3.1","1.23.0"
+ ImportModuleNames = "Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.SignIns"
+ Scope = "CurrentUser"
+ }
+
+ Mock -CommandName Install-Module -MockWith { }
+ Mock -CommandName Import-Module -MockWith { }
+ Mock -CommandName Write-AuditLog -MockWith { }
+
+ Initialize-TkModuleEnv @params
+
+ Assert-MockCalled -CommandName Install-Module -Times 2
+ Assert-MockCalled -CommandName Import-Module -Times 4
+ }
+ }
+
+ Context "When installing pre-release modules" {
+ It "Should install and import specified pre-release modules" {
+ $params = @{
+ PrereleaseModuleNames = "Sampler", "Pester"
+ PrereleaseRequiredVersions = "2.1.5", "4.10.1"
+ Scope = "CurrentUser"
+ }
+
+ Mock -CommandName Install-Module -MockWith { }
+ Mock -CommandName Import-Module -MockWith { }
+ Mock -CommandName Write-AuditLog -MockWith { }
+
+ Initialize-TkModuleEnv @params
+
+ Assert-MockCalled -CommandName Install-Module -Times 2
+ Assert-MockCalled -CommandName Import-Module -Times 2
}
+ }
+
+ Context "When PowerShellGet needs to be updated" {
+ It "Should update PowerShellGet if required" {
+ $params = @{
+ PublicModuleNames = "PSnmap"
+ PublicRequiredVersions = "1.3.1"
+ Scope = "CurrentUser"
+ }
+
+ Mock -CommandName Get-Module -MockWith {
+ return [pscustomobject]@{ Name = "PowerShellGet"; Version = [version]"1.0.0.1" }
+ }
+ Mock -CommandName Install-Module -MockWith { }
+ Mock -CommandName Import-Module -MockWith { }
+ Mock -CommandName Write-AuditLog -MockWith { }
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
+ Initialize-TkModuleEnv @params
+
+ Assert-MockCalled -CommandName Install-Module -Times 1
+ Assert-MockCalled -CommandName Import-Module -Times 1
}
+ }
+
+ Context "When installing modules for AllUsers scope" {
+ It "Should require elevation for AllUsers scope" {
+ $params = @{
+ PublicModuleNames = "PSnmap"
+ PublicRequiredVersions = "1.3.1"
+ Scope = "AllUsers"
+ }
+
+ Mock -CommandName Test-IsAdmin -MockWith { return $false }
+ Mock -CommandName Write-AuditLog -MockWith { }
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ { Initialize-TkModuleEnv @params } | Should -Throw "Elevation required for 'AllUsers' scope."
}
}
}
From 1f3cf64ab555b2fe21589f4a08e30314d17637e9 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:06 -0500
Subject: [PATCH 18/38] Remove Redundant Tests for New-TkAppName and
TkRequiredResourcePermission
---
tests/Unit/Private/New-TkAppName.tests.ps1 | 27 -------------------
...RequiredResourcePermissionObject.tests.ps1 | 27 -------------------
2 files changed, 54 deletions(-)
delete mode 100644 tests/Unit/Private/New-TkAppName.tests.ps1
delete mode 100644 tests/Unit/Private/New-TkRequiredResourcePermissionObject.tests.ps1
diff --git a/tests/Unit/Private/New-TkAppName.tests.ps1 b/tests/Unit/Private/New-TkAppName.tests.ps1
deleted file mode 100644
index 4a2aa69..0000000
--- a/tests/Unit/Private/New-TkAppName.tests.ps1
+++ /dev/null
@@ -1,27 +0,0 @@
-$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
-$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
- ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
- $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
- }).BaseName
-
-
-Import-Module $ProjectName
-
-InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
- }
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
- }
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
- }
- }
- }
-}
-
diff --git a/tests/Unit/Private/New-TkRequiredResourcePermissionObject.tests.ps1 b/tests/Unit/Private/New-TkRequiredResourcePermissionObject.tests.ps1
deleted file mode 100644
index 4a2aa69..0000000
--- a/tests/Unit/Private/New-TkRequiredResourcePermissionObject.tests.ps1
+++ /dev/null
@@ -1,27 +0,0 @@
-$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
-$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
- ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
- $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
- }).BaseName
-
-
-Import-Module $ProjectName
-
-InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
- }
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
- }
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
- }
- }
- }
-}
-
From 9c153f8e397e4a328fe51c05c58140a7e8e6e47a Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:06 -0500
Subject: [PATCH 19/38] Add and Update New-TkAppRegistration and
New-TkExchangeEmailAppPolicy Tests
---
.../Private/New-TkAppRegistration.tests.ps1 | 45 ++++++++++++++----
.../New-TkExchangeEmailAppPolicy.tests.ps1 | 47 +++++++++++++++----
2 files changed, 72 insertions(+), 20 deletions(-)
diff --git a/tests/Unit/Private/New-TkAppRegistration.tests.ps1 b/tests/Unit/Private/New-TkAppRegistration.tests.ps1
index 4a2aa69..c3912c9 100644
--- a/tests/Unit/Private/New-TkAppRegistration.tests.ps1
+++ b/tests/Unit/Private/New-TkAppRegistration.tests.ps1
@@ -8,18 +8,43 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe "New-TkAppRegistration" {
+ Mock -CommandName Get-ChildItem -MockWith {
+ param ($Path)
+ return @{
+ Thumbprint = "ABC123"
+ RawData = "MockedRawData"
}
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
+ }
+ Mock -CommandName New-MgApplication -MockWith {
+ param ($Params)
+ return @{
+ Id = "MockedAppId"
}
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ }
+ Mock -CommandName Write-AuditLog
+ Context "When creating a new app registration" {
+ It "Should create a new app registration with valid parameters" {
+ $DisplayName = "MyApp"
+ $CertThumbprint = "ABC123"
+ $Notes = "This is a sample app."
+ $AppRegistration = New-TkAppRegistration -DisplayName $DisplayName -CertThumbprint $CertThumbprint -Notes $Notes
+ $AppRegistration.Id | Should -Be "MockedAppId"
+ Assert-MockCalled -CommandName Get-ChildItem -Exactly 1 -Scope It
+ Assert-MockCalled -CommandName New-MgApplication -Exactly 1 -Scope It
+ }
+ It "Should throw an error if the certificate is not found" {
+ Mock -CommandName Get-ChildItem -MockWith {
+ param ($Path)
+ return $null
+ }
+ $DisplayName = "MyApp"
+ $CertThumbprint = "INVALID"
+ { New-TkAppRegistration -DisplayName $DisplayName -CertThumbprint $CertThumbprint } | Should -Throw "Certificate with thumbprint INVALID not found in Cert:\CurrentUser\My."
+ }
+ It "Should throw an error if CertThumbprint is not provided" {
+ $DisplayName = "MyApp"
+ { New-TkAppRegistration -DisplayName $DisplayName } | Should -Throw "CertThumbprint is required to create an app registration. No other methods are supported yet."
}
}
}
diff --git a/tests/Unit/Private/New-TkExchangeEmailAppPolicy.tests.ps1 b/tests/Unit/Private/New-TkExchangeEmailAppPolicy.tests.ps1
index 4a2aa69..a7aecef 100644
--- a/tests/Unit/Private/New-TkExchangeEmailAppPolicy.tests.ps1
+++ b/tests/Unit/Private/New-TkExchangeEmailAppPolicy.tests.ps1
@@ -8,18 +8,45 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe "New-TkExchangeEmailAppPolicy Tests" {
+ Mock Write-AuditLog
+ Mock Add-DistributionGroupMember
+ Mock New-ApplicationAccessPolicy
+ Context "When AuthorizedSenderUserName is provided" {
+ It "Should add the user to the mail-enabled sending group and create a new application access policy" {
+ $AppRegistration = [PSCustomObject]@{ AppId = "test-app-id" }
+ $MailEnabledSendingGroup = "TestGroup"
+ $AuthorizedSenderUserName = "TestUser"
+ New-TkExchangeEmailAppPolicy -AppRegistration $AppRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup -AuthorizedSenderUserName $AuthorizedSenderUserName
+ Assert-MockCalled -CommandName Write-AuditLog -Exactly 4 -Scope It
+ Assert-MockCalled -CommandName Add-DistributionGroupMember -Exactly 1 -Scope It -ParameterFilter {
+ $Identity -eq $MailEnabledSendingGroup -and $Member -eq $AuthorizedSenderUserName
+ }
+ Assert-MockCalled -CommandName New-ApplicationAccessPolicy -Exactly 1 -Scope It -ParameterFilter {
+ $AppId -eq $AppRegistration.AppId -and $PolicyScopeGroupId -eq $MailEnabledSendingGroup
+ }
}
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
+ }
+ Context "When AuthorizedSenderUserName is not provided" {
+ It "Should create a new application access policy without adding any user to the group" {
+ $AppRegistration = [PSCustomObject]@{ AppId = "test-app-id" }
+ $MailEnabledSendingGroup = "TestGroup"
+ New-TkExchangeEmailAppPolicy -AppRegistration $AppRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup
+ Assert-MockCalled -CommandName Write-AuditLog -Exactly 3 -Scope It
+ Assert-MockCalled -CommandName Add-DistributionGroupMember -Exactly 0 -Scope It
+ Assert-MockCalled -CommandName New-ApplicationAccessPolicy -Exactly 1 -Scope It -ParameterFilter {
+ $AppId -eq $AppRegistration.AppId -and $PolicyScopeGroupId -eq $MailEnabledSendingGroup
+ }
}
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ }
+ Context "When an error occurs" {
+ It "Should log the error and throw" {
+ $AppRegistration = [PSCustomObject]@{ AppId = "test-app-id" }
+ $MailEnabledSendingGroup = "TestGroup"
+ $AuthorizedSenderUserName = "TestUser"
+ Mock Add-DistributionGroupMember { throw "Test error" }
+ { New-TkExchangeEmailAppPolicy -AppRegistration $AppRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup -AuthorizedSenderUserName $AuthorizedSenderUserName } | Should -Throw
+ Assert-MockCalled -CommandName Write-AuditLog -ParameterFilter { $Message -like "Error creating Exchange Application policy: *" } -Exactly 1 -Scope It
}
}
}
From fe8575502a3b2f8fda49d107317f858147b7b6eb Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:06 -0500
Subject: [PATCH 20/38] Update Set-TkJsonSecret Unit Tests
---
tests/Unit/Private/Set-TkJsonSecret.tests.ps1 | 46 +++++++++++++++----
1 file changed, 36 insertions(+), 10 deletions(-)
diff --git a/tests/Unit/Private/Set-TkJsonSecret.tests.ps1 b/tests/Unit/Private/Set-TkJsonSecret.tests.ps1
index 4a2aa69..65bd379 100644
--- a/tests/Unit/Private/Set-TkJsonSecret.tests.ps1
+++ b/tests/Unit/Private/Set-TkJsonSecret.tests.ps1
@@ -8,18 +8,44 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe 'Set-TkJsonSecret' {
+ Mock -CommandName Get-SecretVault -MockWith { return @() }
+ Mock -CommandName Register-SecretVault
+ Mock -CommandName Get-SecretInfo -MockWith { return $null }
+ Mock -CommandName Remove-Secret
+ Mock -CommandName Set-Secret
+ Mock -CommandName Write-AuditLog
+ Context 'When the vault is not registered' {
+ It 'Should register the vault' {
+ Set-TkJsonSecret -Name 'TestSecret' -InputObject @{ Key = 'Value' } -Confirm:$false
+ Assert-MockCalled -CommandName Register-SecretVault -Exactly 1 -Scope It
}
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
+ }
+ Context 'When the vault is already registered' {
+ Mock -CommandName Get-SecretVault -MockWith { return @{ Name = 'GraphEmailAppLocalStore' } }
+ It 'Should not register the vault again' {
+ Set-TkJsonSecret -Name 'TestSecret' -InputObject @{ Key = 'Value' } -Confirm:$false
+ Assert-MockCalled -CommandName Register-SecretVault -Exactly 0 -Scope It
}
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ }
+ Context 'When the secret does not exist' {
+ It 'Should store the secret' {
+ Set-TkJsonSecret -Name 'TestSecret' -InputObject @{ Key = 'Value' } -Confirm:$false
+ Assert-MockCalled -CommandName Set-Secret -Exactly 1 -Scope It
+ }
+ }
+ Context 'When the secret already exists and Overwrite is not specified' {
+ Mock -CommandName Get-SecretInfo -MockWith { return @{ Name = 'TestSecret' } }
+ It 'Should throw an error' {
+ { Set-TkJsonSecret -Name 'TestSecret' -InputObject @{ Key = 'Value' } -Confirm:$false } | Should -Throw
+ }
+ }
+ Context 'When the secret already exists and Overwrite is specified' {
+ Mock -CommandName Get-SecretInfo -MockWith { return @{ Name = 'TestSecret' } }
+ It 'Should overwrite the secret' {
+ Set-TkJsonSecret -Name 'TestSecret' -InputObject @{ Key = 'Value' } -Overwrite -Confirm:$false
+ Assert-MockCalled -CommandName Remove-Secret -Exactly 1 -Scope It
+ Assert-MockCalled -CommandName Set-Secret -Exactly 1 -Scope It
}
}
}
From 337946a9866e1c798ecb23ed6688413a1f7ecd6c Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:06 -0500
Subject: [PATCH 21/38] Add/Update Test-IsAdmin and Write-AuditLog Unit Tests
---
tests/Unit/Private/Test-IsAdmin.tests.ps1 | 41 +++++++++----
tests/Unit/Private/Write-AuditLog.tests.ps1 | 67 +++++++++++++++++----
2 files changed, 84 insertions(+), 24 deletions(-)
diff --git a/tests/Unit/Private/Test-IsAdmin.tests.ps1 b/tests/Unit/Private/Test-IsAdmin.tests.ps1
index 4a2aa69..9b0e541 100644
--- a/tests/Unit/Private/Test-IsAdmin.tests.ps1
+++ b/tests/Unit/Private/Test-IsAdmin.tests.ps1
@@ -8,18 +8,37 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
+ Describe "Test-IsAdmin" {
+ Context "When the user is an administrator" {
+ It "Should return True" {
+ # Mock the WindowsPrincipal and WindowsIdentity classes
+ Mock -CommandName 'Security.Principal.WindowsPrincipal' -MockWith {
+ return @{
+ IsInRole = { param($role) return $role -eq [Security.Principal.WindowsBuiltinRole]::Administrator }
+ }
+ }
+ Mock -CommandName 'Security.Principal.WindowsIdentity::GetCurrent' -MockWith {
+ return $null
+ }
+ # Call the function and assert the result
+ $result = Test-IsAdmin
+ $result | Should -Be $true
}
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
- }
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
+ }
+ Context "When the user is not an administrator" {
+ It "Should return False" {
+ # Mock the WindowsPrincipal and WindowsIdentity classes
+ Mock -CommandName 'Security.Principal.WindowsPrincipal' -MockWith {
+ return @{
+ IsInRole = { param($role) return $false }
+ }
+ }
+ Mock -CommandName 'Security.Principal.WindowsIdentity::GetCurrent' -MockWith {
+ return $null
+ }
+ # Call the function and assert the result
+ $result = Test-IsAdmin
+ $result | Should -Be $false
}
}
}
diff --git a/tests/Unit/Private/Write-AuditLog.tests.ps1 b/tests/Unit/Private/Write-AuditLog.tests.ps1
index 4a2aa69..e82ddf5 100644
--- a/tests/Unit/Private/Write-AuditLog.tests.ps1
+++ b/tests/Unit/Private/Write-AuditLog.tests.ps1
@@ -8,19 +8,60 @@ $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
Import-Module $ProjectName
InModuleScope $ProjectName {
- Describe Get-PrivateFunction {
- Context 'Default' {
- BeforeEach {
- $return = Get-PrivateFunction -PrivateData 'string'
- }
-
- It 'Returns a single object' {
- ($return | Measure-Object).Count | Should -Be 1
- }
-
- It 'Returns a string based on the parameter PrivateData' {
- $return | Should -Be 'string'
- }
+ Describe "Write-AuditLog Tests" {
+ It "Should initialize log with Start switch" {
+ $script:LogString = $null
+ Write-AuditLog -Start
+ $script:LogString | Should -Not -BeNullOrEmpty
+ $script:LogString[1].Message | Should -Match 'Begin Log'
+ }
+ It "Should log a message with default severity" {
+ Write-AuditLog -Start
+ Write-AuditLog -Message "This is a test message."
+ $script:LogString | Should -Contain { $_.Message -eq "This is a test message." }
+ $script:LogString[1].Severity | Should -Be "Verbose"
+ }
+ It "Should log a warning message" {
+ Write-AuditLog -Start
+ Write-AuditLog -Message "This is a warning message." -Severity "Warning"
+ $script:LogString | Should -Contain { $_.Message -eq "This is a warning message." }
+ $script:LogString[1].Severity | Should -Be "Warning"
+ }
+ It "Should log an error message" {
+ Write-AuditLog -Start
+ Write-AuditLog -Message "This is an error message." -Severity "Error"
+ $script:LogString | Should -Contain { $_.Message -eq "This is an error message." }
+ $script:LogString[1].Severity | Should -Be "Error"
+ }
+ It "Should log a verbose message" {
+ Write-AuditLog -Start
+ Write-AuditLog -Message "This is a verbose message." -Severity "Verbose"
+ $script:LogString | Should -Contain { $_.Message -eq "This is a verbose message." }
+ $script:LogString[1].Severity | Should -Be "Verbose"
+ }
+ It "Should log the beginning of a function" {
+ Write-AuditLog -Start
+ Write-AuditLog -BeginFunction
+ $script:LogString | Should -Contain { $_.Message -Match 'Begin Function Log' }
+ }
+ It "Should log the end of a function" {
+ Write-AuditLog -Start
+ Write-AuditLog -BeginFunction
+ Write-AuditLog -EndFunction
+ $script:LogString | Should -Contain { $_.Message -Match 'End Function Log' }
+ }
+ It "Should log the end of the log and export to CSV" {
+ $testPath = "TestDrive:\test.csv"
+ $outputPath = $testPath
+ Write-AuditLog -Start
+ Write-AuditLog -End -OutputPath $outputPath
+ $script:LogString | Should -Contain { $_.Message -Match 'End Log' }
+ Test-Path $outputPath | Should -Be $true
+ Remove-Item $outputPath
+ }
+ AfterEach {
+ # Clean up the script-wide log variable
+ Remove-Variable -Name script:LogString -ErrorAction SilentlyContinue
}
}
}
From c9bd19f8646ac956e463198b1a3651b4a6be1cf6 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:24:07 -0500
Subject: [PATCH 22/38] Add New Private Functions & Their Tests
---
source/Private/Initialize-TkAppName.ps1 | 85 +++++++++++++
.../Initialize-TkEmailAppParamsObject.ps1 | 63 ++++++++++
.../Initialize-TkM365AuditAppParamsObject.ps1 | 57 +++++++++
...lize-TkMemPolicyManagerAppParamsObject.ps1 | 48 +++++++
...ize-TkRequiredResourcePermissionObject.ps1 | 118 ++++++++++++++++++
.../Private/Initialize-TkAppName.tests.ps1 | 57 +++++++++
...nitialize-TkEmailAppParamsObject.tests.ps1 | 61 +++++++++
...alize-TkM365AuditAppParamsObject.tests.ps1 | 59 +++++++++
...kMemPolicyManagerAppParamsObject.tests.ps1 | 53 ++++++++
...RequiredResourcePermissionObject.tests.ps1 | 63 ++++++++++
10 files changed, 664 insertions(+)
create mode 100644 source/Private/Initialize-TkAppName.ps1
create mode 100644 source/Private/Initialize-TkEmailAppParamsObject.ps1
create mode 100644 source/Private/Initialize-TkM365AuditAppParamsObject.ps1
create mode 100644 source/Private/Initialize-TkMemPolicyManagerAppParamsObject.ps1
create mode 100644 source/Private/Initialize-TkRequiredResourcePermissionObject.ps1
create mode 100644 tests/Unit/Private/Initialize-TkAppName.tests.ps1
create mode 100644 tests/Unit/Private/Initialize-TkEmailAppParamsObject.tests.ps1
create mode 100644 tests/Unit/Private/Initialize-TkM365AuditAppParamsObject.tests.ps1
create mode 100644 tests/Unit/Private/Initialize-TkMemPolicyManagerAppParamsObject.tests.ps1
create mode 100644 tests/Unit/Private/Initialize-TkRequiredResourcePermissionObject.tests.ps1
diff --git a/source/Private/Initialize-TkAppName.ps1 b/source/Private/Initialize-TkAppName.ps1
new file mode 100644
index 0000000..a92fc08
--- /dev/null
+++ b/source/Private/Initialize-TkAppName.ps1
@@ -0,0 +1,85 @@
+<#
+ .SYNOPSIS
+ Generates a new application name based on provided prefix, scenario name, and user email.
+ .DESCRIPTION
+ The Initialize-TkAppName function constructs an application name using a specified prefix, an optional scenario name,
+ and an optional user email. The generated name includes a domain suffix derived from the environment variable USERDNSDOMAIN.
+ .PARAMETER Prefix
+ A short prefix for your app name (2-4 alphanumeric characters). This parameter is mandatory.
+ .PARAMETER ScenarioName
+ An optional scenario name to include in the app name (e.g., AuditGraphEmail, MemPolicy, etc.). Defaults to "GraphApp".
+ .PARAMETER UserId
+ An optional user email to append an "As-[username]" suffix to the app name. The email must be in a valid format.
+ .EXAMPLE
+ PS> Initialize-TkAppName -Prefix "MSN"
+ Generates an app name with the prefix "MSN" and default scenario name "GraphApp".
+ .EXAMPLE
+ PS> Initialize-TkAppName -Prefix "MSN" -ScenarioName "AuditGraphEmail"
+ Generates an app name with the prefix "MSN" and scenario name "AuditGraphEmail".
+ .EXAMPLE
+ PS> Initialize-TkAppName -Prefix "MSN" -UserId "helpdesk@mydomain.com"
+ Generates an app name with the prefix "MSN" and appends the user suffix derived from the email "helpdesk@mydomain.com".
+ .NOTES
+ The function logs the process of building the app name and handles errors by logging and throwing them.
+#>
+function Initialize-TkAppName {
+ [CmdletBinding()]
+ [OutputType([string])]
+ 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,}$')]
+ [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: GraphToolKit-MSN-GraphApp-MyDomain-As-helpDesk
+ $domainSuffix = $env:USERDNSDOMAIN
+ if (-not $domainSuffix) {
+ # fallback if not set
+ $domainSuffix = "MyDomain"
+ }
+ $appName = "GraphToolKit-$Prefix"
+ Write-AuditLog "Returning app name: $appName (Prefix: $Prefix, Scenario: $ScenarioName, User Suffix: $userSuffix)"
+ $appName += "-$domainSuffix"
+ $appName += "$userSuffix"
+ Write-AuditLog "Returning app name: $appName"
+ return $appName
+ }
+ catch {
+ $errorMessage = "An error occurred while building the app name: $_"
+ Write-AuditLog $errorMessage
+ throw $errorMessage
+ }
+ finally {
+ Write-AuditLog -EndFunction
+ }
+ }
+}
diff --git a/source/Private/Initialize-TkEmailAppParamsObject.ps1 b/source/Private/Initialize-TkEmailAppParamsObject.ps1
new file mode 100644
index 0000000..5c853a6
--- /dev/null
+++ b/source/Private/Initialize-TkEmailAppParamsObject.ps1
@@ -0,0 +1,63 @@
+<#
+ .SYNOPSIS
+ Initializes a TkEmailAppParams object with the provided parameters.
+ .DESCRIPTION
+ The Initialize-TkEmailAppParamsObject function creates and returns a new instance of the TkEmailAppParams class using the provided parameters. This function ensures that all necessary parameters are provided and initializes the object accordingly.
+ .PARAMETER AppId
+ The application ID used to identify the email application.
+ .PARAMETER Id
+ The unique identifier for the email application instance.
+ .PARAMETER AppName
+ The name of the email application being initialized.
+ .PARAMETER AppRestrictedSendGroup
+ The group that is restricted from sending emails within the application.
+ .PARAMETER CertExpires
+ The expiration date of the certificate used by the email application.
+ .PARAMETER CertThumbprint
+ The thumbprint of the certificate used for authentication.
+ .PARAMETER ConsentUrl
+ The URL where users can provide consent for the email application.
+ .PARAMETER DefaultDomain
+ The default domain used by the email application for sending emails.
+ .PARAMETER SendAsUser
+ The user who will send emails on behalf of the email application.
+ .PARAMETER SendAsUserEmail
+ The email address of the user who will send emails on behalf of the application.
+ .PARAMETER TenantID
+ The tenant ID associated with the email application.
+ .OUTPUTS
+ [TkEmailAppParams]
+ Returns a new instance of the TkEmailAppParams class initialized with the provided parameters.
+ .EXAMPLE
+ $tkEmailAppParams = Initialize-TkEmailAppParamsObject -AppId "12345" -Id "67890" -AppName "MyEmailApp" -AppRestrictedSendGroup "RestrictedGroup" -CertExpires "2023-12-31" -CertThumbprint "ABCDEF123456" -ConsentUrl "https://consent.url" -DefaultDomain "example.com" -SendAsUser "user@example.com" -SendAsUserEmail "user@example.com" -TenantID "tenant123"
+
+ This example initializes a TkEmailAppParams object with the specified parameters.
+#>
+function Initialize-TkEmailAppParamsObject {
+ param (
+ [string]$AppId,
+ [string]$Id,
+ [string]$AppName,
+ [string]$AppRestrictedSendGroup,
+ [string]$CertExpires,
+ [string]$CertThumbprint,
+ [string]$ConsentUrl,
+ [string]$DefaultDomain,
+ [string]$SendAsUser,
+ [string]$SendAsUserEmail,
+ [string]$TenantID
+ )
+ return [TkEmailAppParams]::new(
+ $AppId,
+ $Id,
+ $AppName,
+ $AppRestrictedSendGroup,
+ $CertExpires,
+ $CertThumbprint,
+ $ConsentUrl,
+ $DefaultDomain,
+ $SendAsUser,
+ $SendAsUserEmail,
+ $TenantID
+ )
+}
\ No newline at end of file
diff --git a/source/Private/Initialize-TkM365AuditAppParamsObject.ps1 b/source/Private/Initialize-TkM365AuditAppParamsObject.ps1
new file mode 100644
index 0000000..78c051f
--- /dev/null
+++ b/source/Private/Initialize-TkM365AuditAppParamsObject.ps1
@@ -0,0 +1,57 @@
+<#
+ .SYNOPSIS
+ Initializes a TkM365AuditAppParams object with the provided parameters.
+ .DESCRIPTION
+ This function initializes a TkM365AuditAppParams object using the parameters provided by the user. It sets up the application name, application ID, object ID, tenant ID, certificate thumbprint, certificate expiration date, consent URL, and various permissions for Microsoft Graph, SharePoint, and Exchange. This allows for the configuration and management of the TkM365AuditAppParams object within the application.
+ .PARAMETER AppName
+ The name of the application.
+ .PARAMETER AppId
+ The unique identifier for the application.
+ .PARAMETER ObjectId
+ The unique identifier for the object.
+ .PARAMETER TenantId
+ The unique identifier for the tenant.
+ .PARAMETER CertThumbprint
+ The thumbprint of the certificate used.
+ .PARAMETER CertExpires
+ The expiration date of the certificate.
+ .PARAMETER ConsentUrl
+ The URL used for consent.
+ .PARAMETER MgGraphPermissions
+ An array of permissions for Microsoft Graph.
+ .PARAMETER SharePointPermissions
+ An array of permissions for SharePoint.
+ .PARAMETER ExchangePermissions
+ An array of permissions for Exchange.
+ .OUTPUTS
+ TkM365AuditAppParams
+ A new instance of the TkM365AuditAppParams object initialized with the provided parameters.
+ .EXAMPLE
+ $Params = Initialize-TkM365AuditAppParamsObject -AppName "MyApp" -AppId "12345" -ObjectId "67890" -TenantId "tenant123" -CertThumbprint "ABCDEF" -CertExpires "2023-12-31" -ConsentUrl "https://consent.url" -MgGraphPermissions @("Permission1", "Permission2") -SharePointPermissions @("Permission1") -ExchangePermissions @("Permission1", "Permission2")
+#>
+function Initialize-TkM365AuditAppParamsObject {
+ param (
+ [string]$AppName,
+ [string]$AppId,
+ [string]$ObjectId,
+ [string]$TenantId,
+ [string]$CertThumbprint,
+ [string]$CertExpires,
+ [string]$ConsentUrl,
+ [string[]]$MgGraphPermissions,
+ [string[]]$SharePointPermissions,
+ [string[]]$ExchangePermissions
+ )
+ return [TkM365AuditAppParams]::new(
+ $AppName,
+ $AppId,
+ $ObjectId,
+ $TenantId,
+ $CertThumbprint,
+ $CertExpires,
+ $ConsentUrl,
+ $MgGraphPermissions,
+ $SharePointPermissions,
+ $ExchangePermissions
+ )
+}
\ No newline at end of file
diff --git a/source/Private/Initialize-TkMemPolicyManagerAppParamsObject.ps1 b/source/Private/Initialize-TkMemPolicyManagerAppParamsObject.ps1
new file mode 100644
index 0000000..1fbf861
--- /dev/null
+++ b/source/Private/Initialize-TkMemPolicyManagerAppParamsObject.ps1
@@ -0,0 +1,48 @@
+<#
+ .SYNOPSIS
+ Initializes a TkMemPolicyManagerAppParams object with the provided parameters.
+ .DESCRIPTION
+ This function creates and returns a new instance of the TkMemPolicyManagerAppParams class using the provided parameters.
+ .PARAMETER AppId
+ The unique identifier for the application.
+ .PARAMETER AppName
+ The name of the application to be initialized.
+ .PARAMETER CertThumbprint
+ The thumbprint of the certificate used for authentication.
+ .PARAMETER ObjectId
+ The unique identifier for the object.
+ .PARAMETER ConsentUrl
+ The URL where consent can be granted for the application.
+ .PARAMETER PermissionSet
+ The set of permissions required by the application.
+ .PARAMETER Permissions
+ The specific permissions granted to the application.
+ .PARAMETER TenantId
+ The unique identifier for the tenant.
+ .OUTPUTS
+ [TkMemPolicyManagerAppParams] The initialized TkMemPolicyManagerAppParams object.
+ .EXAMPLE
+ $AppParams = Initialize-TkMemPolicyManagerAppParamsObject -AppId "12345" -AppName "MyApp" -CertThumbprint "ABCDEF" -ObjectId "67890" -ConsentUrl "https://consent.url" -PermissionSet "ReadWrite" -Permissions "All" -TenantId "Tenant123"
+#>
+function Initialize-TkMemPolicyManagerAppParamsObject {
+ param (
+ [string]$AppId,
+ [string]$AppName,
+ [string]$CertThumbprint,
+ [string]$ObjectId,
+ [string]$ConsentUrl,
+ [string]$PermissionSet,
+ [string]$Permissions,
+ [string]$TenantId
+ )
+ return [TkMemPolicyManagerAppParams]::new(
+ $AppId,
+ $AppName,
+ $CertThumbprint,
+ $ObjectId,
+ $ConsentUrl,
+ $PermissionSet,
+ $Permissions,
+ $TenantId
+ )
+}
\ No newline at end of file
diff --git a/source/Private/Initialize-TkRequiredResourcePermissionObject.ps1 b/source/Private/Initialize-TkRequiredResourcePermissionObject.ps1
new file mode 100644
index 0000000..cc72fd1
--- /dev/null
+++ b/source/Private/Initialize-TkRequiredResourcePermissionObject.ps1
@@ -0,0 +1,118 @@
+<#
+ .SYNOPSIS
+ Creates a new required resource permission object for Microsoft Graph and specific scenarios.
+ .DESCRIPTION
+ The Initialize-TkRequiredResourcePermissionObject function creates a new required resource permission object for Microsoft Graph and specific scenarios. It retrieves service principals by display name, builds an array of MicrosoftGraphRequiredResourceAccess objects, and processes application permissions and scenario-specific permissions.
+ .PARAMETER GraphPermissions
+ An array of application (app-only) permissions for Microsoft Graph. Default is 'Mail.Send'.
+ .PARAMETER Scenario
+ The scenario app version. Currently supports '365Audit'.
+ .EXAMPLE
+ PS C:\> Initialize-TkRequiredResourcePermissionObject -GraphPermissions 'User.Read', 'Mail.Send'
+
+ Creates a required resource permission object with the specified Graph permissions.
+ .EXAMPLE
+ PS C:\> Initialize-TkRequiredResourcePermissionObject -Scenario '365Audit'
+
+ Creates a required resource permission object for the '365Audit' scenario, including specific SharePoint and Exchange permissions.
+ .NOTES
+ This function requires the Microsoft.Graph PowerShell module.
+#>
+function Initialize-TkRequiredResourcePermissionObject {
+ [CmdletBinding(DefaultParameterSetName = 'Default')]
+ param (
+ [Parameter(
+ Mandatory = $false,
+ HelpMessage = 'Application (app-only) permissions for Microsoft Graph.'
+ )]
+ [string[]]
+ $GraphPermissions = @('Mail.Send'),
+ [Parameter(
+ Mandatory = $false,
+ HelpMessage = 'Scenario app version.',
+ ParameterSetName = 'Scenario'
+ )]
+ [ValidateSet('365Audit')]
+ [string]
+ $Scenario
+ )
+ process {
+ if (-not $script:LogString) {
+ Write-AuditLog -Start
+ }
+ else {
+ Write-AuditLog -BeginFunction
+ }
+ try {
+ Write-AuditLog '###############################################'
+ ## 1) Retrieve service principals by DisplayName
+ Write-AuditLog 'Looking up service principals by display name...'
+ $spGraph = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'" -ErrorAction Stop
+ # 2) Build an array of [MicrosoftGraphRequiredResourceAccess] objects
+ $requiredResourceAccessList = @()
+ # Retrieve all application permissions
+ $permissionList = Find-MgGraphPermission -PermissionType Application -All
+ # region Graph perms
+ [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $graphRra = $null
+ # If GraphPermissions is not null or empty, process them
+ if ($GraphPermissions -and $GraphPermissions.Count -gt 0) {
+ if (-not $spGraph) {
+ throw 'Microsoft Graph Service Principal not found (by display name).'
+ }
+ Write-AuditLog "Gathering permissions: $($GraphPermissions -join ', ')"
+ $graphRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new()
+ $graphRra.ResourceAppId = $spGraph.AppId
+ foreach ($permName in $GraphPermissions) {
+ $foundPerm = $permissionList | Where-Object { $_.Name -eq $permName } #Find-MgGraphPermission -PermissionType Application -All |
+ if ($foundPerm) {
+ # If multiple matches, pick the first
+ $graphRra.ResourceAccess += @{ Id = $foundPerm.Id; Type = 'Role' }
+ Write-AuditLog "Found Graph permission ID for '$permName': $($foundPerm[0].Id)"
+ }
+ else {
+ Write-AuditLog -Severity Warning -Message "Graph Permission '$permName' not found!"
+ }
+ }
+ if ($graphRra.ResourceAccess) {
+ $requiredResourceAccessList += $graphRra
+ }
+ else {
+ throw "No Graph permissions found for '$($GraphPermissions -join ', ')'. Check the permission names and try again."
+ }
+ }
+ # endregion
+ # region Scenario-specific permissions
+ # Scenario 365Audit
+ if ($Scenario -eq '365Audit') {
+ # region SharePoint perms
+ [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $spRra = $null
+ $spRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new()
+ $spRra.ResourceAppId = "00000003-0000-0ff1-ce00-000000000000" # SharePoint Online
+ $spRra.ResourceAccess += @{ Id = 'd13f72ca-a275-4b96-b789-48ebcc4da984'; Type = 'Role' }
+ $spRra.ResourceAccess += @{ Id = '678536fe-1083-478a-9c59-b99265e6b0d3'; Type = 'Role' }
+ $requiredResourceAccessList += $spRra
+ # endregion
+ # region Exchange perms
+ [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] $spRra = $null
+ $exRra = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]::new()
+ $exRra.ResourceAppId = "00000002-0000-0ff1-ce00-000000000000" # Exchange Online
+ $exRra.ResourceAccess += @{ Id = 'dc50a0fb-09a3-484d-be87-e023b12c6440'; Type = 'Role' }
+ $requiredResourceAccessList += $exRra
+ } # endregion Scenario 365Audit
+ # endregion Scenario-specific permissions
+ # 3) Build final result object
+ $result = [PSCustomObject]@{
+ RequiredResourceAccessList = $requiredResourceAccessList
+ }
+ # }
+ Write-AuditLog 'Returning context object.'
+ return $result
+ }
+ catch {
+ throw
+ }
+ finally {
+ Write-AuditLog -EndFunction
+ }
+ }
+}
diff --git a/tests/Unit/Private/Initialize-TkAppName.tests.ps1 b/tests/Unit/Private/Initialize-TkAppName.tests.ps1
new file mode 100644
index 0000000..b186005
--- /dev/null
+++ b/tests/Unit/Private/Initialize-TkAppName.tests.ps1
@@ -0,0 +1,57 @@
+$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
+$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
+ ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
+ $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
+ }).BaseName
+
+
+Import-Module $ProjectName
+
+InModuleScope $ProjectName {
+ Describe "Initialize-TkAppName" {
+ Context "When generating app name with mandatory parameters" {
+ It "should generate app name with prefix only" {
+ $env:USERDNSDOMAIN = "MyDomain"
+ $result = Initialize-TkAppName -Prefix "MSN"
+ $result | Should -Be "GraphToolKit-MSN-MyDomain"
+ }
+ }
+
+ Context "When generating app name with optional scenario name" {
+ It "should generate app name with prefix and scenario name" {
+ $env:USERDNSDOMAIN = "MyDomain"
+ $result = Initialize-TkAppName -Prefix "MSN" -ScenarioName "AuditGraphEmail"
+ $result | Should -Be "GraphToolKit-MSN-MyDomain"
+ }
+ }
+
+ Context "When generating app name with optional user email" {
+ It "should generate app name with prefix and user suffix" {
+ $env:USERDNSDOMAIN = "MyDomain"
+ $result = Initialize-TkAppName -Prefix "MSN" -UserId "helpdesk@mydomain.com"
+ $result | Should -Be "GraphToolKit-MSN-MyDomain-As-helpdesk"
+ }
+ }
+
+ Context "When USERDNSDOMAIN environment variable is not set" {
+ It "should fallback to default domain suffix" {
+ $env:USERDNSDOMAIN = $null
+ $result = Initialize-TkAppName -Prefix "MSN"
+ $result | Should -Be "GraphToolKit-MSN-MyDomain"
+ }
+ }
+
+ Context "When invalid prefix is provided" {
+ It "should throw a validation error" {
+ { Initialize-TkAppName -Prefix "INVALID" } | Should -Throw
+ }
+ }
+
+ Context "When invalid email is provided" {
+ It "should throw a validation error" {
+ { Initialize-TkAppName -Prefix "MSN" -UserId "invalid-email" } | Should -Throw
+ }
+ }
+ }
+}
+
diff --git a/tests/Unit/Private/Initialize-TkEmailAppParamsObject.tests.ps1 b/tests/Unit/Private/Initialize-TkEmailAppParamsObject.tests.ps1
new file mode 100644
index 0000000..d87e963
--- /dev/null
+++ b/tests/Unit/Private/Initialize-TkEmailAppParamsObject.tests.ps1
@@ -0,0 +1,61 @@
+$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
+$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
+ ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
+ $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
+ }).BaseName
+
+
+Import-Module $ProjectName
+
+InModuleScope $ProjectName {
+ Describe "Initialize-TkEmailAppParamsObject Tests" {
+ It "Should create a TkEmailAppParams object with the specified parameters" {
+ # Arrange
+ $AppId = "12345"
+ $Id = "67890"
+ $AppName = "MyEmailApp"
+ $AppRestrictedSendGroup = "RestrictedGroup"
+ $CertExpires = "2023-12-31"
+ $CertThumbprint = "ABCDEF123456"
+ $ConsentUrl = "https://consent.url"
+ $DefaultDomain = "example.com"
+ $SendAsUser = "user1"
+ $SendAsUserEmail = "user1@example.com"
+ $TenantID = "tenant123"
+
+ # Act
+ $result = Initialize-TkEmailAppParamsObject `
+ -AppId $AppId `
+ -Id $Id `
+ -AppName $AppName `
+ -AppRestrictedSendGroup $AppRestrictedSendGroup `
+ -CertExpires $CertExpires `
+ -CertThumbprint $CertThumbprint `
+ -ConsentUrl $ConsentUrl `
+ -DefaultDomain $DefaultDomain `
+ -SendAsUser $SendAsUser `
+ -SendAsUserEmail $SendAsUserEmail `
+ -TenantID $TenantID
+
+ # Assert
+ $result | Should -BeOfType "TkEmailAppParams"
+ $result.AppId | Should -Be $AppId
+ $result.Id | Should -Be $Id
+ $result.AppName | Should -Be $AppName
+ $result.AppRestrictedSendGroup | Should -Be $AppRestrictedSendGroup
+ $result.CertExpires | Should -Be $CertExpires
+ $result.CertThumbprint | Should -Be $CertThumbprint
+ $result.ConsentUrl | Should -Be $ConsentUrl
+ $result.DefaultDomain | Should -Be $DefaultDomain
+ $result.SendAsUser | Should -Be $SendAsUser
+ $result.SendAsUserEmail | Should -Be $SendAsUserEmail
+ $result.TenantID | Should -Be $TenantID
+ }
+ }
+}
+
+
+
+
+
+
diff --git a/tests/Unit/Private/Initialize-TkM365AuditAppParamsObject.tests.ps1 b/tests/Unit/Private/Initialize-TkM365AuditAppParamsObject.tests.ps1
new file mode 100644
index 0000000..ff27c5e
--- /dev/null
+++ b/tests/Unit/Private/Initialize-TkM365AuditAppParamsObject.tests.ps1
@@ -0,0 +1,59 @@
+$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
+$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
+ ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
+ $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
+ }).BaseName
+
+
+Import-Module $ProjectName
+
+InModuleScope $ProjectName {
+ Describe "Initialize-TkM365AuditAppParamsObject Tests" {
+ It "Should initialize TkM365AuditAppParams object with valid parameters" {
+ # Arrange
+ $AppName = "MyApp"
+ $AppId = "12345"
+ $ObjectId = "67890"
+ $TenantId = "tenant123"
+ $CertThumbprint = "ABCDEF"
+ $CertExpires = "2023-12-31"
+ $ConsentUrl = "https://consent.url"
+ $MgGraphPermissions = @("Permission1", "Permission2")
+ $SharePointPermissions = @("Permission1")
+ $ExchangePermissions = @("Permission1", "Permission2")
+
+ # Act
+ $result = Initialize-TkM365AuditAppParamsObject -AppName $AppName -AppId $AppId -ObjectId $ObjectId -TenantId $TenantId -CertThumbprint $CertThumbprint -CertExpires $CertExpires -ConsentUrl $ConsentUrl -MgGraphPermissions $MgGraphPermissions -SharePointPermissions $SharePointPermissions -ExchangePermissions $ExchangePermissions
+
+ # Assert
+ $result | Should -BeOfType "TkM365AuditAppParams"
+ $result.AppName | Should -Be $AppName
+ $result.AppId | Should -Be $AppId
+ $result.ObjectId | Should -Be $ObjectId
+ $result.TenantId | Should -Be $TenantId
+ $result.CertThumbprint | Should -Be $CertThumbprint
+ $result.CertExpires | Should -Be $CertExpires
+ $result.ConsentUrl | Should -Be $ConsentUrl
+ $result.MgGraphPermissions | Should -Be $MgGraphPermissions
+ $result.SharePointPermissions | Should -Be $SharePointPermissions
+ $result.ExchangePermissions | Should -Be $ExchangePermissions
+ }
+
+ It "Should throw an error when required parameters are missing" {
+ # Arrange
+ $AppName = "MyApp"
+ $AppId = "12345"
+ $ObjectId = "67890"
+ $TenantId = "tenant123"
+ $CertThumbprint = "ABCDEF"
+ $CertExpires = "2023-12-31"
+ $ConsentUrl = "https://consent.url"
+ $MgGraphPermissions = @("Permission1", "Permission2")
+ $SharePointPermissions = @("Permission1")
+ $ExchangePermissions = @("Permission1", "Permission2")
+
+ # Act & Assert
+ { Initialize-TkM365AuditAppParamsObject -AppId $AppId -ObjectId $ObjectId -TenantId $TenantId -CertThumbprint $CertThumbprint -CertExpires $CertExpires -ConsentUrl $ConsentUrl -MgGraphPermissions $MgGraphPermissions -SharePointPermissions $SharePointPermissions -ExchangePermissions $ExchangePermissions } | Should -Throw
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Unit/Private/Initialize-TkMemPolicyManagerAppParamsObject.tests.ps1 b/tests/Unit/Private/Initialize-TkMemPolicyManagerAppParamsObject.tests.ps1
new file mode 100644
index 0000000..93e5ba1
--- /dev/null
+++ b/tests/Unit/Private/Initialize-TkMemPolicyManagerAppParamsObject.tests.ps1
@@ -0,0 +1,53 @@
+$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
+$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
+ ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
+ $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
+ }).BaseName
+
+
+Import-Module $ProjectName
+
+InModuleScope $ProjectName {
+ Describe "Initialize-TkMemPolicyManagerAppParamsObject" {
+ Context "When called with valid parameters" {
+ It "should return a TkMemPolicyManagerAppParams object with the correct properties" {
+ # Arrange
+ $AppId = "12345"
+ $AppName = "MyApp"
+ $CertThumbprint = "ABCDEF"
+ $ObjectId = "67890"
+ $ConsentUrl = "https://consent.url"
+ $PermissionSet = "ReadWrite"
+ $Permissions = "All"
+ $TenantId = "Tenant123"
+ # Act
+ $result = Initialize-TkMemPolicyManagerAppParamsObject -AppId $AppId -AppName $AppName -CertThumbprint $CertThumbprint -ObjectId $ObjectId -ConsentUrl $ConsentUrl -PermissionSet $PermissionSet -Permissions $Permissions -TenantId $TenantId
+ # Assert
+ $result | Should -BeOfType "TkMemPolicyManagerAppParams"
+ $result.AppId | Should -Be $AppId
+ $result.AppName | Should -Be $AppName
+ $result.CertThumbprint | Should -Be $CertThumbprint
+ $result.ObjectId | Should -Be $ObjectId
+ $result.ConsentUrl | Should -Be $ConsentUrl
+ $result.PermissionSet | Should -Be $PermissionSet
+ $result.Permissions | Should -Be $Permissions
+ $result.TenantId | Should -Be $TenantId
+ }
+ }
+ Context "When called with missing parameters" {
+ It "should throw an error" {
+ # Arrange
+ $AppId = "12345"
+ $AppName = "MyApp"
+ $CertThumbprint = "ABCDEF"
+ $ObjectId = "67890"
+ $ConsentUrl = "https://consent.url"
+ $PermissionSet = "ReadWrite"
+ $Permissions = "All"
+ $TenantId = "Tenant123"
+ # Act & Assert
+ { Initialize-TkMemPolicyManagerAppParamsObject -AppId $AppId -AppName $AppName -CertThumbprint $CertThumbprint -ObjectId $ObjectId -ConsentUrl $ConsentUrl -PermissionSet $PermissionSet -Permissions $Permissions } | Should -Throw
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Unit/Private/Initialize-TkRequiredResourcePermissionObject.tests.ps1 b/tests/Unit/Private/Initialize-TkRequiredResourcePermissionObject.tests.ps1
new file mode 100644
index 0000000..b510265
--- /dev/null
+++ b/tests/Unit/Private/Initialize-TkRequiredResourcePermissionObject.tests.ps1
@@ -0,0 +1,63 @@
+$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
+$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{
+ ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
+ $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } )
+ }).BaseName
+
+
+Import-Module $ProjectName
+
+InModuleScope $ProjectName {
+ Describe 'Initialize-TkRequiredResourcePermissionObject' {
+ BeforeAll {
+ # Mock the necessary cmdlets
+ Mock -CommandName Get-MgServicePrincipal -MockWith {
+ @{
+ AppId = '00000003-0000-0ff1-ce00-000000000000'
+ }
+ }
+ Mock -CommandName Find-MgGraphPermission -MockWith {
+ @(
+ @{ Name = 'Mail.Send'; Id = '12345' },
+ @{ Name = 'User.Read'; Id = '67890' }
+ )
+ }
+ Mock -CommandName Write-AuditLog
+ }
+ Context 'When called with default parameters' {
+ It 'should return a required resource permission object with Mail.Send permission' {
+ $result = Initialize-TkRequiredResourcePermissionObject
+ $result.RequiredResourceAccessList | Should -HaveCount 1
+ $result.RequiredResourceAccessList[0].ResourceAccess | Should -Contain @{ Id = '12345'; Type = 'Role' }
+ }
+ }
+ Context 'When called with specific GraphPermissions' {
+ It 'should return a required resource permission object with specified permissions' {
+ $result = Initialize-TkRequiredResourcePermissionObject -GraphPermissions 'User.Read', 'Mail.Send'
+ $result.RequiredResourceAccessList | Should -HaveCount 1
+ $result.RequiredResourceAccessList[0].ResourceAccess | Should -Contain @{ Id = '12345'; Type = 'Role' }
+ $result.RequiredResourceAccessList[0].ResourceAccess | Should -Contain @{ Id = '67890'; Type = 'Role' }
+ }
+ }
+ Context 'When called with Scenario 365Audit' {
+ It 'should return a required resource permission object with SharePoint and Exchange permissions' {
+ $result = Initialize-TkRequiredResourcePermissionObject -Scenario '365Audit'
+ $result.RequiredResourceAccessList | Should -HaveCount 3
+ $result.RequiredResourceAccessList[1].ResourceAccess | Should -Contain @{ Id = 'd13f72ca-a275-4b96-b789-48ebcc4da984'; Type = 'Role' }
+ $result.RequiredResourceAccessList[1].ResourceAccess | Should -Contain @{ Id = '678536fe-1083-478a-9c59-b99265e6b0d3'; Type = 'Role' }
+ $result.RequiredResourceAccessList[2].ResourceAccess | Should -Contain @{ Id = 'dc50a0fb-09a3-484d-be87-e023b12c6440'; Type = 'Role' }
+ }
+ }
+ Context 'When GraphPermissions are not found' {
+ BeforeAll {
+ Mock -CommandName Find-MgGraphPermission -MockWith {
+ @()
+ }
+ }
+ It 'should throw an error' {
+ { Initialize-TkRequiredResourcePermissionObject -GraphPermissions 'Invalid.Permission' } | Should -Throw
+ }
+ }
+ }
+}
+
From 8ea1c844898a2c65b57ec9dfde4959d700892200 Mon Sep 17 00:00:00 2001
From: DrIOS <58635327+DrIOSX@users.noreply.github.com>
Date: Thu, 13 Mar 2025 21:37:32 -0500
Subject: [PATCH 23/38] fix: aligned and refactored formatting
---
.gitignore | 2 +-
README2.md | 73 +++---
docs/index.html | 200 ++++++++-------
help/New-MailEnabledSendingGroup.md | 34 ++-
help/Publish-TkEmailApp.md | 112 +++++----
help/Publish-TkM365AuditApp.md | 33 +--
help/Publish-TkMemPolicyManagerApp.md | 33 +--
help/Send-TkEmailAppMessage.md | 51 +++-
source/Private/Connect-TkMsService.ps1 | 55 ++++-
source/Private/ConvertTo-ParameterSplat.ps1 | 21 ++
source/Private/Get-TkExistingSecret.ps1 | 38 +--
.../Initialize-TkAppAuthCertificate.ps1 | 48 ++--
.../Initialize-TkAppSpRegistration.ps1 | 150 ++++++++----
source/Private/Initialize-TkModuleEnv.ps1 | 75 +++---
source/Private/New-TkAppName.ps1 | 56 -----
source/Private/New-TkAppRegistration.ps1 | 79 +++---
source/Private/New-TkEmailAppParams.ps1 | 28 ---
.../Private/New-TkExchangeEmailAppPolicy.ps1 | 60 ++++-
source/Private/New-TkM365AuditAppParams.ps1 | 26 --
.../New-TkMemPolicyManagerAppParams.ps1 | 22 --
...New-TkRequiredResourcePermissionObject.ps1 | 98 --------
source/Private/Set-TkJsonSecret.ps1 | 91 +++++--
source/Private/Write-AuditLog.ps1 | 4 -
source/Public/New-MailEnabledSendingGroup.ps1 | 13 +-
source/Public/Publish-TkEmailApp.ps1 | 230 +++++++++---------
source/Public/Publish-TkM365AuditApp.ps1 | 157 ++++++------
.../Public/Publish-TkMemPolicyManagerApp.ps1 | 111 ++++-----
source/Public/Send-TkEmailAppMessage.ps1 | 30 ++-
.../Unit/Private/Get-TkExistingCert.tests.ps1 | 37 +++
.../Private/Get-TkExistingSecret.tests.ps1 | 60 +++++
30 files changed, 1050 insertions(+), 977 deletions(-)
delete mode 100644 source/Private/New-TkAppName.ps1
delete mode 100644 source/Private/New-TkEmailAppParams.ps1
delete mode 100644 source/Private/New-TkM365AuditAppParams.ps1
delete mode 100644 source/Private/New-TkMemPolicyManagerAppParams.ps1
delete mode 100644 source/Private/New-TkRequiredResourcePermissionObject.ps1
create mode 100644 tests/Unit/Private/Get-TkExistingCert.tests.ps1
create mode 100644 tests/Unit/Private/Get-TkExistingSecret.tests.ps1
diff --git a/.gitignore b/.gitignore
index 75bad52..23fea0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@ output/
*.local.*
!**/README.md
.kitchen/
-
+/scripts/
*.nupkg
*.suo
*.user
diff --git a/README2.md b/README2.md
index 3f0664c..f09c8c4 100644
--- a/README2.md
+++ b/README2.md
@@ -5,9 +5,9 @@ Creates or retrieves a mail-enabled security group with a custom or default doma
### Syntax
```powershell
-New-MailEnabledSendingGroup -Name [-Alias ] -PrimarySmtpAddress []
+New-MailEnabledSendingGroup -Name [-Alias ] -PrimarySmtpAddress [-WhatIf] [-Confirm] []
-New-MailEnabledSendingGroup -Name [-Alias ] -DefaultDomain []
+New-MailEnabledSendingGroup -Name [-Alias ] -DefaultDomain [-WhatIf] [-Confirm] []
@@ -20,6 +20,8 @@ New-MailEnabledSendingGroup -Name [-Alias ] -DefaultDomain Alias | | An optional alias for the group. If omitted, the group name is used as the alias. | false | false | |
| PrimarySmtpAddress | | \(CustomDomain parameter set\) The full SMTP address for the group \(e.g. "MyGroup@contoso.com"\). This parameter is mandatory when using the 'CustomDomain' parameter set. | true | false | |
| DefaultDomain | | \(DefaultDomain parameter set\) The domain portion to be appended to the group alias \(e.g. "Alias@DefaultDomain"\). This parameter is mandatory when using the 'DefaultDomain' parameter set. | true | false | |
+| WhatIf | wi | | false | false | |
+| Confirm | cf | | false | false | |
### Inputs
- None. This function does not accept pipeline input.
@@ -48,11 +50,13 @@ and a primary SMTP address of Senders@customdomain.org.
## Publish-TkEmailApp
### Synopsis
-Deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication.
+Publishes a new or existing Graph Email App with specified configurations.
### Syntax
```powershell
-Publish-TkEmailApp [-AppPrefix] [-AuthorizedSenderUserName] [-MailEnabledSendingGroup] [[-CertThumbprint] ] [[-KeyExportPolicy] ] [[-VaultName] ] [-OverwriteVaultSecret] [-ReturnParamSplat] [-WhatIf] [-Confirm] []
+Publish-TkEmailApp [-AppPrefix ] -AuthorizedSenderUserName -MailEnabledSendingGroup [-CertPrefix ] [-CertThumbprint ] [-KeyExportPolicy ] [-VaultName ] [-OverwriteVaultSecret] [-ReturnParamSplat] []
+
+Publish-TkEmailApp -ExistingAppObjectId -CertPrefix [-CertThumbprint ] [-KeyExportPolicy ] [-VaultName ] [-OverwriteVaultSecret] [-ReturnParamSplat] []
@@ -61,31 +65,31 @@ Publish-TkEmailApp [-AppPrefix] [-AuthorizedSenderUserName] [-
### Parameters
| Name | Alias | Description | Required? | Pipeline Input | Default Value |
| - | - | - | - | - | - |
-| AppPrefix | | A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for grouping purposes \(2-4 alphanumeric characters\). | true | false | |
-| AuthorizedSenderUserName | | The username of the authorized sender. | true | false | |
-| MailEnabledSendingGroup | | The mail-enabled group to which the sender belongs. This will be used to assign app policy restrictions. | true | false | |
-| CertThumbprint | | An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated. | false | false | |
-| KeyExportPolicy | | Specifies the key export policy for the newly created certificate. Valid values are 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'. | false | false | NonExportable |
-| VaultName | | If specified, the name of the vault to store the app's credentials. Otherwise, defaults to 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore |
-| OverwriteVaultSecret | | If specified, the function overwrites an existing secret in the vault if it already exists. | false | false | False |
-| ReturnParamSplat | | If specified, returns the parameter splat for use in other functions instead of the PSCustomObject. | false | false | False |
-| WhatIf | wi | | false | false | |
-| Confirm | cf | | false | false | |
-### Inputs
- - None
-
-### Outputs
- - By default, returns a PSCustomObject containing details such as AppId, CertThumbprint, TenantID, and CertExpires. If -ReturnParamSplat is specified, returns the parameter splat instead.
-
+| AppPrefix | | The prefix used to initialize the Graph Email App. Must be 2-4 characters, letters, and numbers only. Default is 'Gtk'. | false | false | Gtk |
+| AuthorizedSenderUserName | | The username of the authorized sender. Must be a valid email address. | true | false | |
+| MailEnabledSendingGroup | | The mail-enabled security group. Must be a valid email address. | true | false | |
+| ExistingAppObjectId | | The AppId of the existing App Registration to which you want to attach a certificate. Must be a valid GUID. | true | false | |
+| CertPrefix | | Prefix to add to the certificate subject for the existing app. | false | false | |
+| CertThumbprint | | The thumbprint of the certificate to be retrieved. Must be a valid 40-character hexadecimal string. | false | false | |
+| KeyExportPolicy | | Key export policy for the certificate. Valid values are 'Exportable' and 'NonExportable'. Default is 'NonExportable'. | false | false | NonExportable |
+| VaultName | | If specified, use a custom vault name. Otherwise, use the default 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore |
+| OverwriteVaultSecret | | If specified, overwrite the vault secret if it already exists. | false | false | False |
+| ReturnParamSplat | | If specified, return the parameter splat for use in other functions. | false | false | False |
### Note
-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. Permissions required: 'Application.ReadWrite.All', 'DelegatedPermissionGrant.ReadWrite.All', 'Directory.ReadWrite.All', 'RoleManagement.ReadWrite.Directory'
+This cmdlet requires that the user running the cmdlet have the necessary permissions to create the app and connect to Exchange Online. Permissions required: - 'Application.ReadWrite.All' - 'DelegatedPermissionGrant.ReadWrite.All' - 'Directory.ReadWrite.All' - 'RoleManagement.ReadWrite.Directory'
### Examples
**EXAMPLE 1**
```powershell
-Publish-TkEmailApp -AppPrefix "ABC" -AuthorizedSenderUserName "jdoe@example.com" -MailEnabledSendingGroup "GraphAPIMailGroup@example.com" -CertThumbprint "AABBCCDDEEFF11223344556677889900"
+Publish-TkEmailApp -AppPrefix 'Gtk' -AuthorizedSenderUserName 'user@example.com' -MailEnabledSendingGroup 'group@example.com'
```
+Creates a new Graph Email App with the specified parameters.
+**EXAMPLE 2**
+```powershell
+Publish-TkEmailApp -ExistingAppObjectId '12345678-1234-1234-1234-1234567890ab' -CertPrefix 'Cert'
+```
+Uses an existing app and attaches a certificate with the specified prefix.
## Publish-TkM365AuditApp
### Synopsis
@@ -93,7 +97,7 @@ Publishes \(creates\) a new M365 Audit App registration in Entra ID \(Azure AD\)
### Syntax
```powershell
-Publish-TkM365AuditApp [[-AppPrefix] ] [[-CertThumbprint] ] [[-KeyExportPolicy] ] [[-VaultName] ] [-OverwriteVaultSecret] [-ReturnParamSplat] [-WhatIf] [-Confirm] []
+Publish-TkM365AuditApp [[-AppPrefix] ] [[-CertThumbprint] ] [[-KeyExportPolicy] ] [[-VaultName] ] [-OverwriteVaultSecret] [-ReturnParamSplat] []
@@ -102,14 +106,12 @@ Publish-TkM365AuditApp [[-AppPrefix] ] [[-CertThumbprint] ] [[-K
### Parameters
| Name | Alias | Description | Required? | Pipeline Input | Default Value |
| - | - | - | - | - | - |
-| AppPrefix | | A short prefix \(2-4 alphanumeric characters\) used to build the app name. Defaults to "Gtk" if not specified. | false | false | Gtk |
+| AppPrefix | | A short prefix \(2-4 alphanumeric characters\) used to build the app name. Defaults to "Gtk" if not specified. Example app name: GraphToolKit-MSN-GraphApp-MyDomain-As-helpDesk | false | false | Gtk |
| CertThumbprint | | The thumbprint of an existing certificate in the current user's certificate store. If not provided, a new self-signed certificate is created. | false | false | |
| KeyExportPolicy | | Specifies whether the newly created certificate \(if no thumbprint is provided\) is 'Exportable' or 'NonExportable'. Defaults to 'NonExportable'. | false | false | NonExportable |
| VaultName | | The SecretManagement vault name in which to store the app credentials. Defaults to "M365AuditAppLocalStore" if not specified. | false | false | M365AuditAppLocalStore |
| OverwriteVaultSecret | | If specified, overwrites an existing secret in the specified vault if it already exists. | false | false | False |
| ReturnParamSplat | | If specified, returns a parameter splat string for use in other functions, instead of the default PSCustomObject containing the app details. | false | false | False |
-| WhatIf | wi | | false | false | |
-| Confirm | cf | | false | false | |
### Inputs
- None. This function does not accept pipeline input.
@@ -134,7 +136,7 @@ Publishes a new MEM \(Intune\) Policy Manager App in Azure AD with read-only or
### Syntax
```powershell
-Publish-TkMemPolicyManagerApp [-AppPrefix] [[-CertThumbprint] ] [[-KeyExportPolicy] ] [[-VaultName] ] [-OverwriteVaultSecret] [-ReadWrite] [-ReturnParamSplat] [-WhatIf] [-Confirm] []
+Publish-TkMemPolicyManagerApp [-AppPrefix] [[-CertThumbprint] ] [[-KeyExportPolicy] ] [[-VaultName] ] [-OverwriteVaultSecret] [-ReadWrite] [-ReturnParamSplat] []
@@ -150,8 +152,6 @@ Publish-TkMemPolicyManagerApp [-AppPrefix] [[-CertThumbprint] ]
| OverwriteVaultSecret | | If specified, overwrites any existing secret of the same name in the vault. | false | false | False |
| ReadWrite | | If specified, grants read-write MEM/Intune permissions. Otherwise, read-only permissions are granted. | false | false | False |
| ReturnParamSplat | | If specified, returns a parameter splat string for use in other functions. Otherwise, returns a PSCustomObject containing the app details. | false | false | False |
-| WhatIf | wi | | false | false | |
-| Confirm | cf | | false | false | |
### Inputs
- None. This function does not accept pipeline input.
@@ -176,9 +176,9 @@ Sends an email using the Microsoft Graph API, either by retrieving app credentia
### Syntax
```powershell
-Send-TkEmailAppMessage -AppName -To -FromAddress -Subject -EmailBody [-AttachmentPath ] []
+Send-TkEmailAppMessage -AppName -To -FromAddress -Subject -EmailBody [-AttachmentPath ] [-VaultName ] [-WhatIf] [-Confirm] []
-Send-TkEmailAppMessage -AppId -TenantId -CertThumbprint -To -FromAddress -Subject -EmailBody [-AttachmentPath ] []
+Send-TkEmailAppMessage -AppId -TenantId -CertThumbprint -To -FromAddress -Subject -EmailBody [-AttachmentPath ] [-WhatIf] [-Confirm] []
@@ -187,15 +187,18 @@ Send-TkEmailAppMessage -AppId -TenantId -CertThumbprint AppName | | \[Vault Parameter Set Only\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | |
-| AppId | | \[Manual Parameter Set Only\] The Azure AD application \(client\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | |
-| TenantId | | \[Manual Parameter Set Only\] The Azure AD tenant ID \(GUID or domain name\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | |
-| CertThumbprint | | \[Manual Parameter Set Only\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | |
+| AppName | | \\[Vault Parameter Set Only\\] The name of the pre-created Microsoft Graph Email App \(stored in GraphEmailAppLocalStore\). Used only if the 'Vault' parameter set is chosen. The function retrieves the AppId, TenantId, and certificate thumbprint from the vault entry. | true | false | |
+| AppId | | \\[Manual Parameter Set Only\\] The Azure AD application \(client\) ID to use for sending the email. Must be used together with TenantId and CertThumbprint in the 'Manual' parameter set. | true | false | |
+| TenantId | | \\[Manual Parameter Set Only\\] The Azure AD tenant ID \(GUID or domain name\). Must be used together with AppId and CertThumbprint in the 'Manual' parameter set. | true | false | |
+| CertThumbprint | | \\[Manual Parameter Set Only\\] The certificate thumbprint \(in Cert:\\CurrentUser\\My\) used for authenticating as the Azure AD app. Must be used together with AppId and TenantId in the 'Manual' parameter set. | true | false | |
| To | | The email address of the recipient. | true | false | |
| FromAddress | | The email address of the sender who is authorized to send email as configured in the Graph Email App. | true | false | |
| Subject | | The subject line of the email. | true | false | |
| EmailBody | | The body text of the email. | true | false | |
| AttachmentPath | | An array of file paths for any attachments to include in the email. Each path must exist as a leaf file. | false | false | |
+| VaultName | | \\[Vault Parameter Set Only\\] The name of the vault to retrieve the GraphEmailApp object. Default is 'GraphEmailAppLocalStore'. | false | false | GraphEmailAppLocalStore |
+| WhatIf | wi | | false | false | |
+| Confirm | cf | | false | false | |
### Note
- This function requires the Microsoft.Graph, SecretManagement, SecretManagement.JustinGrote.CredMan, and MSAL.PS modules to be installed \(handled automatically via Initialize-TkModuleEnv\). - For the 'Vault' parameter set, the local vault secret must store JSON properties including AppId, TenantID, and CertThumbprint. - Refer to https://learn.microsoft.com/en-us/graph/outlook-send-mail for details on sending mail via Microsoft Graph.
diff --git a/docs/index.html b/docs/index.html
index 0852b81..d37531e 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -2,7 +2,7 @@