In this guide, we’ll explore how you can leverage Azure Key Vault to securely manage secrets in your single-tenant or multi-tenant PowerShell scripts. This will enable you to securely access authentication tokens and other sensitive information needed to interact with downstream customer environments. Let’s dive in!

Understanding the Secure Application Model

Many of us are familiar with the secure application model, even if we don’t realize it. Tools like CIPP leverage this architecture to obtain access tokens for downstream customer environments through established GDAP relationships. For example, I wrote a multi-tenant script that loops through all my customers in partner center and inspects user mailboxes for inbox rules, specifically those related to forwarding, as a part of regular security audits.

I have written multiple blog post on how to set up the secure application model:

The Importance of Securely Managing Secrets

When running scripts that require sensitive information such as app registration details or refresh tokens, it’s crucial to manage these secrets securely. Azure Key Vault is an excellent solution for this. It allows you to store and access these sensitive artifacts securely, avoiding the risks associated with storing them locally or manually copying and pasting them into your scripts. 

Doing so also allows you to set up headless automation so you can run these scripts on a schedule behind the scenes as well. 

Setting Up Azure Key Vault

While this can certainly be done manually, I made a script for the Secure Application model where you can easily create and load your secrets into a Key Vault: 

 #PARAMETERS
param ( 
    [Parameter(Mandatory=$false)]
    [string] $AppId, 
    [Parameter(Mandatory=$false)]
    [string] $AppSecret, 
    [Parameter(Mandatory=$false)]
    [string] $TenantId,
    [Parameter(Mandatory=$false)]
    [string] $refreshToken,
    [Parameter(Mandatory=$false)]
    [string] $PartnerRefreshToken,
    [Parameter(Mandatory=$false)]
    [string] $AppDisplayName
)


$ErrorActionPreference = "SilentlyContinue"

#See if Az.Accounts and Az.KeyVault Powershell module is installed and if not install it 
Write-Host "Checking for Az module"
$AzModule = Get-Module -Name Az.Accounts -ListAvailable
if ($null -eq $AzModule) {
    Write-Host "Az module not found, installing now"
    Install-Module -Name Az.Accounts -Force -AllowClobber
}
else {
    Write-Host "Az module found"
}

$AzModule = Get-Module -Name Az.KeyVault -ListAvailable
if ($null -eq $AzModule) {
    Write-Host "Az.KeyVault module not found, installing now"
    Install-Module -Name Az.KeyVault -Force -AllowClobber
}
else {
    Write-Host "Az.KeyVault module found"
}

#Connect to Azure, sign in with Global Admin Credentials
Connect-AzAccount

#Set values for Key Vault Name and Resource Group and Location 
$KeyVaultName = Read-Host -Prompt "Enter the name you want the key vault to be called. Make this unique"
$ResourceGroupName = Read-Host -Prompt "Enter the name of the resource group you want the key vault to be in"
$Location = Read-Host -Prompt "Enter the location you want the key vault to be in, i.e. East US"

#Create the Key Vault

try {
    $KeyVault = New-AzKeyVault -VaultName $KeyVaultName -ResourceGroupName $ResourceGroupName -Location $Location
    Write-Host "Key Vault created successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to create Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

#Create the secrets based on the params in the key vault

#AppID
try{
    $secretvalue = ConvertTo-SecureString $AppId -AsPlainText -Force
    $secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppID" -SecretValue $secretvalue
    Write-Host "AppID secret created successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to create AppID secret" -ForegroundColor Red
    Write-Host $_.Exception.Message
    return
}

#AppSecret
try{
    $secretvalue = ConvertTo-SecureString $AppSecret -AsPlainText -Force
    $secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppSecret" -SecretValue $secretvalue
    Write-Host "AppSecret secret created successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to create AppSecret secret" -ForegroundColor Red
    Write-Host $_.Exception.Message
    return
}

#TenantID
try{
    $secretvalue = ConvertTo-SecureString $TenantId -AsPlainText -Force
    $secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "TenantID" -SecretValue $secretvalue
    Write-Host "TenantID secret created successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to create TenantID secret" -ForegroundColor Red
    Write-Host $_.Exception.Message
    return
}

#RefreshToken
try{
    $secretvalue = ConvertTo-SecureString $refreshToken -AsPlainText -Force
    $secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "RefreshToken" -SecretValue $secretvalue
    Write-Host "RefreshToken secret created successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to create RefreshToken secret" -ForegroundColor Red
    Write-Host $_.Exception.Message
    return
}

#PartnerRefreshToken
try{
    $secretvalue = ConvertTo-SecureString $PartnerRefreshToken -AsPlainText -Force
    $secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "PartnerRefreshToken" -SecretValue $secretvalue
    Write-Host "PartnerRefreshToken secret created successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to create PartnerRefreshToken secret" -ForegroundColor Red
    Write-Host $_.Exception.Message
    return
}

#AppDisplayName
try{
    $secretvalue = ConvertTo-SecureString $AppDisplayName -AsPlainText -Force
    $secret = Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppDisplayName" -SecretValue $secretvalue
    Write-Host "AppDisplayName secret created successfully" -ForegroundColor Green
}
catch {
    Write-Host "Failed to create AppDisplayName secret" -ForegroundColor Red
    Write-Host $_.Exception.Message
    return
}

Ensure your key vault name is unique globally. Also ensure it is not too many characters, it needs to be 32 characters or less. Result of your actions: 

Accessing Secrets in PowerShell Scripts

Once your secrets are stored in Azure Key Vault, you can access them securely in your PowerShell scripts. Here’s how you can modify your script to retrieve these secrets:

#PARAMETERS
param ( 
    [Parameter(Mandatory=$false)]
    [string] $KeyVaultName
)



$ErrorActionPreference = "SilentlyContinue"

#See if Az.Accounts and Az.KeyVault Powershell module is installed and if not install it 
Write-Host "Checking for Az module"
$AzModule = Get-Module -Name Az.Accounts -ListAvailable
if ($null -eq $AzModule) {
    Write-Host "Az module not found, installing now"
    Install-Module -Name Az.Accounts -Force -AllowClobber
}
else {
    Write-Host "Az module found"
}

$AzModule = Get-Module -Name Az.KeyVault -ListAvailable
if ($null -eq $AzModule) {
    Write-Host "Az.KeyVault module not found, installing now"
    Install-Module -Name Az.KeyVault -Force -AllowClobber
}
else {
    Write-Host "Az.KeyVault module found"
}

#Connect to Azure, sign in with Global Admin Credentials
try{
    Connect-AzAccount
    Write-Host "Connected to Azure" -ForegroundColor Green
}
catch {
    Write-Host "Failed to connect to Azure" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break

}


try {
    $AppId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppID" -AsPlainText
    Write-Host "Got AppID from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get AppID from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

try {
    $AppSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppSecret" -AsPlainText
    Write-Host "Got AppSecret from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get AppSecret from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

try {
    $TenantId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "TenantId" -AsPlainText
    Write-Host "Got TenantId from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get TenantId from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

try {
    $refreshToken = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "RefreshToken" -AsPlainText
    Write-Host "Got RefreshToken from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get RefreshToken from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

This script will prompt you for your Azure credentials and authenticate securely using the stored secrets. Example of this in a script: 

Running Your Secure PowerShell Script

From here, we can just use these variables to get access tokens to run more multi-tenant scripts. Example functions: 

function Get-GraphAccessToken ($TenantId) {
    $Body = @{
        'tenant'        = $TenantId
        'client_id'     = $AppId
        'scope'         = 'https://graph.microsoft.com/.default'
        'client_secret' = $AppSecret
        'grant_type'    = 'refresh_token'
        'refresh_token' = $refreshToken
    }
    $Params = @{
        'Uri'         = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
        'Method'      = 'Post'
        'ContentType' = 'application/x-www-form-urlencoded'
        'Body'        = $Body
    }
    $AuthResponse = Invoke-RestMethod @Params
    Write-Host "Got accestoken for $AppID"
    return $AuthResponse.access_token
}

function Get-AppPermAccessToken ($TenantId) {
    $Body = @{
        'tenant'        = $TenantId
        'client_id'     = $AppId
        'scope'         = 'https://graph.microsoft.com/.default'
        'client_secret' = $AppSecret
        'grant_type'    = 'client_credentials'
    }
    $Params = @{
        'Uri'         = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
        'Method'      = 'Post'
        'ContentType' = 'application/x-www-form-urlencoded'
        'Body'        = $Body
    }
    $AuthResponse = Invoke-RestMethod @Params
    Write-Host "Got accestoken for $AppID"
    return $AuthResponse.access_token
}

Full Multi-tenant Inbox Rule Script:

#PARAMETERS
param ( 
    [Parameter(Mandatory=$false)]
    [string] $KeyVaultName
)



$ErrorActionPreference = "SilentlyContinue"

#See if Az.Accounts and Az.KeyVault Powershell module is installed and if not install it 
Write-Host "Checking for Az module"
$AzModule = Get-Module -Name Az.Accounts -ListAvailable
if ($null -eq $AzModule) {
    Write-Host "Az module not found, installing now"
    Install-Module -Name Az.Accounts -Force -AllowClobber
}
else {
    Write-Host "Az module found"
}

$AzModule = Get-Module -Name Az.KeyVault -ListAvailable
if ($null -eq $AzModule) {
    Write-Host "Az.KeyVault module not found, installing now"
    Install-Module -Name Az.KeyVault -Force -AllowClobber
}
else {
    Write-Host "Az.KeyVault module found"
}

#Connect to Azure, sign in with Global Admin Credentials
try{
    Connect-AzAccount
    Write-Host "Connected to Azure" -ForegroundColor Green
}
catch {
    Write-Host "Failed to connect to Azure" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break

}


try {
    $AppId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppID" -AsPlainText
    Write-Host "Got AppID from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get AppID from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

try {
    $AppSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "AppSecret" -AsPlainText
    Write-Host "Got AppSecret from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get AppSecret from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

try {
    $TenantId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "TenantId" -AsPlainText
    Write-Host "Got TenantId from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get TenantId from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}

try {
    $refreshToken = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "RefreshToken" -AsPlainText
    Write-Host "Got RefreshToken from Key Vault" -ForegroundColor Green
}
catch {
    Write-Host "Failed to get RefreshToken from Key Vault" -ForegroundColor Red
    Write-Host $_.Exception.Message
    break
}



# Function to get an access token for Microsoft Graph
function Get-GraphAccessToken ($TenantId) {
    $Body = @{
        'tenant'        = $TenantId
        'client_id'     = $AppId
        'scope'         = 'https://graph.microsoft.com/.default'
        'client_secret' = $AppSecret
        'grant_type'    = 'refresh_token'
        'refresh_token' = $refreshToken
    }
    $Params = @{
        'Uri'         = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
        'Method'      = 'Post'
        'ContentType' = 'application/x-www-form-urlencoded'
        'Body'        = $Body
    }
    $AuthResponse = Invoke-RestMethod @Params
    Write-Host "Got accestoken for $AppID"
    return $AuthResponse.access_token
}

function Get-AppPermAccessToken ($TenantId) {
    $Body = @{
        'tenant'        = $TenantId
        'client_id'     = $AppId
        'scope'         = 'https://graph.microsoft.com/.default'
        'client_secret' = $AppSecret
        'grant_type'    = 'client_credentials'
    }
    $Params = @{
        'Uri'         = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
        'Method'      = 'Post'
        'ContentType' = 'application/x-www-form-urlencoded'
        'Body'        = $Body
    }
    $AuthResponse = Invoke-RestMethod @Params
    Write-Host "Got accestoken for $AppID"
    return $AuthResponse.access_token
}

# Initialize a list to hold audit logs
$AuditLogs = [System.Collections.Generic.List[Object]]::new()


#function to record an audit log of events
function Write-Log {
    param (
        [string]$customerName,
        [string]$EventName,
        [string]$Status,
        [string]$Message
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $Result = @{

        'Customer Name' = $CustomerName
        'Timestamp' = $timestamp
        'Event' = $EventName
        'Status' = $Status
        'Message' = $Message
    }  
    $ReportLine = New-Object PSObject -Property $Result
    $AuditLogs.Add($ReportLine)
    

    # Optionally, you can also display this log message in the console
    Write-Host $logMessage
}


# Initialize a list to hold the results
$Report = [System.Collections.Generic.List[Object]]::new()


# Function for adding output to the list
Function ExportCSV {
    Param($MailBoxName, $UPN, $InboxRule, $CustomerName)

    if ($InboxRule.actions.ForwardTo.Count -gt 0) {
        # Collect email addresses into an array
        $emailAddresses = $InboxRule.actions.ForwardTo | ForEach-Object { $_.emailaddress.address }
    
         # Join email addresses with a comma
         $ForwardTo = $emailAddresses -join ','
    }
    else {
        $ForwardTo = $null
    }
    if ($InboxRule.actions.redirectTo.Count -gt 0) {
        #loop through the fw to addresses and join them with a comma
        $redirects = $InboxRule.actions.RedirectTo | ForEach-Object { $_.emailaddress.address }
        $RedirectTo = $redirects -join ','
    }
    else {
        $RedirectTo = $null
    }
    if ($InboxRule.actions.forwardAsAttachmentTo.Count -gt 0) {
        #loop through the fw to addresses and join them with a comma
        $forwardAsAttachmentTo = $InboxRule.actions.forwardAsAttachmentTo | ForEach-Object { $_.emailaddress.address }
        $forwardAsAttachmentTo = $forwardAsAttachmentTo -join ','
    }
    else {
        $forwardAsAttachmentTo = $null
    }

    If ($InboxRule.actions.MoveToFolder -ne $null) {
        $MoveToFolder = $true
    }
    else {
        $MoveToFolder = $null
    }

    $Result = @{

            'Customer Name' = $CustomerName
            'Mailbox Name' = $MailBoxName
            'UPN' = $UPN
            'Inbox Rule Name' = $InboxRule.displayName
            'Enabled' = $InboxRule.isEnabled
            'Forward To' = $ForwardTo
            'Redirect To' = $RedirectTo
            'Forward As Attachment To' = $forwardAsAttachmentTo
            'Move To Folder' = $MoveToFolder
            'Delete Message' = $InboxRule.actions.delete
            'Mark As Read' = $InboxRule.actions.markAsRead
    
    }  
    $ReportLine = New-Object PSObject -Property $Result
    $Report.Add($ReportLine)
}

# Get the access token
try {
    $AccessToken =  Get-GraphAccessToken -TenantId $TenantId
    Write-Host "Got partner access token for Microsoft Graph" -ForegroundColor Green
    Write-Log -customerName "Partner Tenant" -EventName "Get-GraphAccessToken" -Status "Success" -Message "Got partner access token for Microsoft Graph"

}
catch {
    Write-Host "Failed to partner access token for Microsoft Graph" -ForegroundColor Red
    $ErrorMessage = $_.Exception.Message
    Write-Log -customerName "Partner Tenant" -EventName "Get-GraphAccessToken" -Status "Fail" -Message $ErrorMessage
    break
}
# Define header with authorization token
$Headers = @{
    'Authorization' = "Bearer $AccessToken"
    'Content-Type'  = 'application/json'
}

# Get GDAP Customers
try{
    $GraphUrl = "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminCustomers"
    $Customers = Invoke-RestMethod -Uri $GraphUrl  -Headers $Headers -Method Get
    #Write-Host a list of the customer display Names
    Write-Host "Customers:" -ForegroundColor Green
    $Customers.value.displayName
    Write-Log -customerName "Partner Tenant" -EventName "Get-GDAPCustomers" -Status "Success" -Message "Got GDAP Customers"

}catch{
    Write-Host "Failed to get GDAP Customers" -ForegroundColor Red
    $ErrorMessage = $_.Exception.Message
    Write-Log -customerName "Partner Tenant" -EventName "Get-GDAPCustomers" -Status "Fail" -Message $ErrorMessage
    break

}

#Loop Through Each Customer, Connect to Exchange Online and Get Mailboxes

$Customers.value | ForEach-Object {
    $Customer = $_
    $CustomerName = $Customer.displayName
    $CustomerTenantId = $Customer.id

    # Display the customer name
    Write-Host "Processing Customer: $CustomerName" -ForegroundColor Green

    # Get the access token for the customer with Refresh token
    try {
        Write-Host "Getting Graph Token" -ForegroundColor Green
        $GraphAccessToken = Get-GraphAccessToken -TenantId $CustomerTenantId
        Write-Log -customerName $CustomerName -EventName "Get Graph Token for customer" -Status "Success" -Message "Got access token for Microsoft Graph"
    }
    catch {
        $ErrorMSg = $_.Exception.Message
        Write-Host "Failed to get Graph Token for $CustomerName - $ErrorMsg"
        Write-Log -customerName $CustomerName -EventName "Get Graph Token for customer" -Status "Fail" -Message $_.Exception.Message
        return
    }

    
    # Get the access token for the customer with client credentials
    try {
        Write-Host "Getting Application Permission Graph Token" -ForegroundColor Green
        $AppPermAccessToken = Get-AppPermAccessToken -TenantId $CustomerTenantId
        Write-Log -customerName $CustomerName -EventName "Get Application Permission Graph Token for customer" -Status "Success" -Message "Got access token for Microsoft Graph"
    }
    catch {
        $ErrorMSg = $_.Exception.Message
        Write-Host "Failed to get Application Permission Graph Token for $CustomerName - $ErrorMsg"
        Write-Log -customerName $CustomerName -EventName "Get Application Permission Graph Token for customer" -Status "Fail" -Message $_.Exception.Message
        return
    }


    try{
    # Use the graph access token to call the graph API to get all mailboxes

    $GraphUrl = "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application/json"
    $Headers = @{
        'Authorization' = "Bearer $GraphAccessToken"
        'Content-Type'  = 'application/json'
    }
    $Mailboxes = Invoke-RestMethod -Uri $GraphUrl  -Headers $Headers -Method Get
    Write-Log -customerName $CustomerName -EventName "Get Mailboxes" -Status "Success" -Message "Retrieved mailboxes"

    } catch {
        $ErrorMSg = $_.Exception.Message
        Write-Host "Failed to get mailboxes for $CustomerName - $ErrorMsg" -ForegroundColor Red
        Write-Log -customerName $CustomerName -EventName "Get Mailboxes" -Status "Fail" -Message $_.Exception.Message
        return
    }
    #Reset headers for the application permission token
    $Headers = @{
        'Authorization' = "Bearer $AppPermAccessToken"
        'Content-Type'  = 'application/json'
    }
    #loop thright the mailboxes to get the inbox rules
    $Mailboxes.value | ForEach-Object {
        Write-Host "Processing Mailbox: $($_.displayName)"
        $MailBoxName = $_.DisplayName
        $UPN = $_.UserPrincipalName
        try{
            $GraphInboxRuleURL = "https://graph.microsoft.com/v1.0/users/$UPN/mailFolders/inbox/messageRules"
            $InboxRules = (Invoke-RestMethod -Uri $GraphInboxRuleURL -Headers $Headers -Method Get).value
            #if there are any inbox rules, loop through them and export them to CSV
            if ($InboxRules.Count -gt 0) {
                $InboxRules | ForEach-Object {
                    Write-Host "Exporting Inbox Rule: $($_.displayName) for $UPN" -ForegroundColor Green
                    ExportCSV -MailBoxName $MailBoxName -UPN $UPN -InboxRule $_ -CustomerName $CustomerName
                }
                Write-Log -customerName $CustomerName -EventName "Get Inbox Rules-$UPN" -Status "Success" -Message "Retrieved inbox rules"
            } else{
                Write-Log -customerName $CustomerName -EventName "Get Inbox Rules-$UPN" -Status "Success" -Message "No Inbox rules for $UPN"
            }
        } catch {
            $ErrorMSg = $_.Exception.Message
            Write-Host "Failed to get inbox rules for $UPN - $ErrorMsg" -ForegroundColor Red
            Write-Log -customerName $CustomerName -EventName "Get Inbox Rules-$UPN" -Status "Fail" -Message $_.Exception.Message
            return
        }
    }

} 

# When displaying and exporting, select the properties in the desired order

$Report | Select-Object 'Customer Name', 'Mailbox Name', 'UPN', 'Inbox Rule Name', 'Enabled', 'Forward To', 'Redirect To', 'Forward As Attachment To', 'Move To Folder', 'Delete Message', 'Mark As Read' | Out-GridView
$path = "$((Get-Location).Path)\Inbox Rules $(Get-Date -Format 'yyyy-MM-dd').csv"
$Report | Select-Object 'Customer Name', 'Mailbox Name', 'UPN', 'Inbox Rule Name', 'Enabled', 'Forward To', 'Redirect To', 'Forward As Attachment To', 'Move To Folder', 'Delete Message', 'Mark As Read' | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
$AuditLogPath = "$((Get-Location).Path)\Inbox Rules $(Get-Date -Format 'yyyy-MM-dd') Audit Log.csv"
$AuditLogs | Select-Object 'Customer Name', 'Timestamp', 'Event', 'Status', 'Message' | Export-CSV -Path $AuditLogPath -NoTypeInformation 
Write-Host "Report saved to $Path" -ForegroundColor Cyan

Conclusion

Using Azure Key Vault in your PowerShell scripts not only enhances security by securely managing sensitive information but also streamlines your workflow by automating the retrieval of secrets. This ensures that your scripts run smoothly and securely, without the need for manual input of sensitive data.

I hope this guide helps you understand how to leverage Azure Key Vault for secure scripting. If you want me to help with this in a consulting capacity, check out my premium content page and fill out a consulting request. Happy scripting!

Share with the Community