MantraBlockSenders.psm1

#
# MantraBlockSenders PowerShell Module
# Simplified onboarding for Mantra Threats Block multi-tenant deployment
#
# Copyright (c) 2026 Mantra. All rights reserved.
#

$script:GuidPattern = '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$'
$script:BlockingRole = 'Tenant AllowBlockList Manager'

function Test-MantraTenantMatch {
    param(
        [Parameter(Mandatory = $true)][string]$Organization,
        [string]$TenantId,
        [AllowEmptyCollection()][string[]]$AcceptedDomains = @()
    )

    if ($Organization -match $script:GuidPattern) {
        return $TenantId -eq $Organization
    }
    return @($AcceptedDomains) -contains $Organization
}

function Test-MantraNotFoundError {
    param(
        [Parameter(Mandatory = $true)][System.Management.Automation.ErrorRecord]$ErrorRecord,
        [Parameter(Mandatory = $true)][string]$Identity
    )

    if ($ErrorRecord.Exception -is [System.Management.Automation.CommandNotFoundException]) {
        return $false
    }
    if ($ErrorRecord.CategoryInfo.Category -eq [System.Management.Automation.ErrorCategory]::ObjectNotFound) {
        return $true
    }
    if ([string]$ErrorRecord.FullyQualifiedErrorId -like '*ManagementObjectNotFoundException*') {
        return $true
    }
    $escapedIdentity = [System.Management.Automation.WildcardPattern]::Escape($Identity)
    return $ErrorRecord.Exception.Message -like "*object '$escapedIdentity' couldn't be found*"
}

function Get-MantraServicePrincipalProblem {
    param(
        [Parameter(Mandatory = $true)]$ServicePrincipal,
        [Parameter(Mandatory = $true)][string]$AppId
    )

    if ([string]$ServicePrincipal.AppId -eq $AppId) {
        return $null
    }
    return ("The Object ID belongs to another application (App ID $($ServicePrincipal.AppId)), " +
        "not to Mantra (App ID $AppId). Use the Object ID of the Mantra enterprise application " +
        "(Microsoft Entra admin center > Enterprise applications), then run the command again.")
}

function Get-MantraRoleGroupProblem {
    param(
        [Parameter(Mandatory = $true)]$RoleGroup
    )

    $roles = @($RoleGroup.Roles | ForEach-Object {
            if ($_ -is [string]) { $_ } else { [string]$_.Name }
        })
    if ($roles -contains $script:BlockingRole) {
        return $null
    }
    return ("The existing role group '$($RoleGroup.Name)' does not have the '$script:BlockingRole' role " +
        "(it has: $($roles -join ', ')). Add that role to the group, or delete the group, then run the command again.")
}

function Connect-MantraBlockSenders {
    <#
    .SYNOPSIS
        Configure Mantra Threats Block on your Microsoft 365 tenant

    .DESCRIPTION
        Automatically configures the Exchange Online permissions required
        for Mantra Threats Block (sender/domain blocking).

        This function:
        1. Installs ExchangeOnlineManagement module if needed
        2. Connects to Exchange Online (interactive login) and checks that it
           is the tenant given in -Organization
        3. Checks any existing Service Principal and role group, and stops
           before changing anything if one of them is wrong
        4. Creates the Service Principal and the limited RBAC role group
           (blocking only) when missing
        5. Assigns the Service Principal to the role group

    .PARAMETER AppId
        Mantra application (client) ID
        This is the same for all clients and is provided on the authorization page

    .PARAMETER ObjectId
        Service Principal Object ID from your tenant
        Retrieved automatically during the authorization process

    .PARAMETER Organization
        Your tenant identifier. Accepts either:
        - Domain name: "client.onmicrosoft.com" (primary verified domain)
        - Tenant ID: "12345678-1234-1234-1234-123456789012" (tenant GUID)

    .EXAMPLE
        Connect-MantraBlockSenders `
            -AppId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
            -ObjectId "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" `
            -Organization "client.onmicrosoft.com"

        Configures Mantra Threats Block using domain name.

    .EXAMPLE
        Connect-MantraBlockSenders `
            -AppId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
            -ObjectId "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" `
            -Organization "12345678-1234-1234-1234-123456789012"

        Configures Mantra Threats Block using tenant ID (GUID).

    .NOTES
        Requirements:
        - Global Administrator or Exchange Administrator role
        - Internet connection for initial setup
        - PowerShell 5.1 or later

        Estimated execution time: 30-60 seconds

    .LINK
        https://app.mantra.ms/docs
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')]
        [string]$AppId,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')]
        [string]$ObjectId,

        [Parameter(Mandatory = $true, Position = 2)]
        [ValidatePattern('^([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}|.*\.onmicrosoft\.com)$')]
        [string]$Organization
    )

    $ErrorActionPreference = "Stop"

    Write-Host ""
    Write-Host "============================================" -ForegroundColor Cyan
    Write-Host " Mantra Threats Block - Configuration " -ForegroundColor Cyan
    Write-Host "============================================" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Organization : $Organization" -ForegroundColor Gray
    Write-Host "App ID : $AppId" -ForegroundColor Gray
    Write-Host "Object ID : $ObjectId" -ForegroundColor Gray
    Write-Host ""

    $AppName = "MantraBlockSendersBlock"
    $RoleName = "Mantra Blocage"

    try {
        # [1/5] Check/Install Exchange module
        Write-Host "[1/5] Checking Exchange module..." -ForegroundColor Yellow
        $module = Get-Module -ListAvailable -Name ExchangeOnlineManagement
        if (-not $module) {
            Write-Host " Installing ExchangeOnlineManagement..." -ForegroundColor Gray
            Install-Module -Name ExchangeOnlineManagement -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop
            Write-Host " Installed [OK]" -ForegroundColor Green
        } else {
            Write-Host " OK (v$($module.Version)) [OK]" -ForegroundColor Green
        }

        Import-Module ExchangeOnlineManagement -ErrorAction Stop

        # [2/5] Connect to Exchange Online
        Write-Host "[2/5] Connecting to Exchange Online..." -ForegroundColor Yellow
        Write-Host " (Login window will open)" -ForegroundColor Gray
        Write-Host ""
        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
        Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
        $connection = Get-ConnectionInformation | Select-Object -Last 1
        $acceptedDomains = @()
        if ($Organization -notmatch $script:GuidPattern) {
            $acceptedDomains = @(Get-AcceptedDomain | ForEach-Object { [string]$_.DomainName })
        }
        if (-not (Test-MantraTenantMatch -Organization $Organization -TenantId ([string]$connection.TenantID) -AcceptedDomains $acceptedDomains)) {
            throw ("You are signed in to another tenant (tenant ID $($connection.TenantID)) than the one Mantra " +
                "expects ($Organization). Sign in with an administrator account of $Organization, then run the command again.")
        }
        Write-Host " Connected to $Organization [OK]" -ForegroundColor Green

        # [3/5] Check the existing configuration before changing anything
        Write-Host "[3/5] Checking existing configuration..." -ForegroundColor Yellow
        # A non-existent -Identity returns every object, so keep only the requested one.
        # Only a "not found" error means the object is missing; any other failure stops the setup.
        try {
            $existingSP = Get-ServicePrincipal -Identity $ObjectId -ErrorAction Stop |
                Where-Object { [string]$_.ObjectId -eq $ObjectId } | Select-Object -First 1
        } catch {
            if (-not (Test-MantraNotFoundError -ErrorRecord $_ -Identity $ObjectId)) {
                throw
            }
            $existingSP = $null
        }
        if ($existingSP) {
            $spProblem = Get-MantraServicePrincipalProblem -ServicePrincipal $existingSP -AppId $AppId
            if ($spProblem) {
                throw $spProblem
            }
        }
        try {
            $existingRole = Get-RoleGroup -Identity $RoleName -ErrorAction Stop |
                Where-Object { [string]$_.Name -eq $RoleName } | Select-Object -First 1
        } catch {
            if (-not (Test-MantraNotFoundError -ErrorRecord $_ -Identity $RoleName)) {
                throw
            }
            $existingRole = $null
        }
        if ($existingRole) {
            $roleProblem = Get-MantraRoleGroupProblem -RoleGroup $existingRole
            if ($roleProblem) {
                throw $roleProblem
            }
        }
        Write-Host " OK [OK]" -ForegroundColor Green

        # [4/5] Create the Service Principal and the role group
        Write-Host "[4/5] Configuring Service Principal and role group..." -ForegroundColor Yellow
        if ($existingSP) {
            Write-Host " Service Principal already exists [OK]" -ForegroundColor Green
        } else {
            New-ServicePrincipal -AppId $AppId -ServiceId $ObjectId -DisplayName $AppName -ErrorAction Stop | Out-Null
            Write-Host " Service Principal created [OK]" -ForegroundColor Green
        }
        if ($existingRole) {
            Write-Host " Role group already exists [OK]" -ForegroundColor Green
        } else {
            New-RoleGroup -Name $RoleName `
                -Roles @($script:BlockingRole) `
                -Description "Mantra Threats Block - Sender/domain blocking only" `
                -ErrorAction Stop | Out-Null
            Write-Host " Role group created [OK]" -ForegroundColor Green
        }

        # [5/5] Assign role
        Write-Host "[5/5] Assigning permissions..." -ForegroundColor Yellow
        try {
            Add-RoleGroupMember -Identity $RoleName -Member $ObjectId -ErrorAction Stop
            Write-Host " Assigned [OK]" -ForegroundColor Green
        } catch {
            if ($_.Exception.Message -like "*already a member*") {
                Write-Host " Already member [OK]" -ForegroundColor Green
            } else {
                throw
            }
        }

        # Disconnect
        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue

        # Success message
        Write-Host ""
        Write-Host "============================================" -ForegroundColor Green
        Write-Host " CONFIGURATION COMPLETED! " -ForegroundColor Green
        Write-Host "============================================" -ForegroundColor Green
        Write-Host ""
        Write-Host "Mantra Threats Block can now block malicious" -ForegroundColor Cyan
        Write-Host "senders and domains on your tenant." -ForegroundColor Cyan
        Write-Host ""
        Write-Host "You can close this window." -ForegroundColor Gray
        Write-Host ""

    } catch {
        Write-Host ""
        Write-Host "[X] Configuration failed" -ForegroundColor Red
        Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host ""
        Write-Host "Troubleshooting:" -ForegroundColor Yellow
        Write-Host " - Verify you have Global Admin or Exchange Admin role" -ForegroundColor Gray
        Write-Host " - Check that all parameters are correct" -ForegroundColor Gray
        Write-Host " - Ensure ExchangeOnlineManagement module can be installed" -ForegroundColor Gray
        Write-Host ""
        Write-Host "Contact support: support@mantra.ms" -ForegroundColor Cyan
        Write-Host ""

        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue

        throw
    }
}

function Test-MantraBlockSendersConnection {
    <#
    .SYNOPSIS
        Test Mantra Threats Block configuration

    .DESCRIPTION
        Verifies that Mantra Threats Block is correctly configured on your tenant
        by attempting to connect to Exchange Online and checking RBAC permissions.

    .EXAMPLE
        Test-MantraBlockSendersConnection

    .OUTPUTS
        System.Boolean
        Returns $true if configuration is valid, $false otherwise.
    #>

    [CmdletBinding()]
    param()

    Write-Host ""
    Write-Host "Testing Mantra Threats Block configuration..." -ForegroundColor Cyan
    Write-Host ""

    try {
        Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
        Write-Host " [OK] Exchange Online connection: OK" -ForegroundColor Green

        $testList = Get-TenantAllowBlockListItems -ListType Sender -ErrorAction Stop | Select-Object -First 1
        Write-Host " [OK] Blocking permissions: OK" -ForegroundColor Green

        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue

        Write-Host ""
        Write-Host "[OK] Configuration is valid!" -ForegroundColor Green
        Write-Host ""

        return $true

    } catch {
        Write-Host " [X] Configuration check failed" -ForegroundColor Red
        Write-Host ""
        Write-Host "[X] Configuration incomplete" -ForegroundColor Red
        Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host ""

        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue

        return $false
    }
}

function Get-MantraBlockSendersHelp {
    <#
    .SYNOPSIS
        Display help information

    .DESCRIPTION
        Shows available commands and support links
    #>


    Write-Host ""
    Write-Host "=================================================================" -ForegroundColor Cyan
    Write-Host "= Mantra Threats Block - Help =" -ForegroundColor Cyan
    Write-Host "=================================================================" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Available commands:" -ForegroundColor White
    Write-Host ""
    Write-Host " Connect-MantraBlockSenders" -ForegroundColor Yellow
    Write-Host " -AppId <guid> -ObjectId <guid> -Organization <tenant-guid|domain>" -ForegroundColor Gray
    Write-Host ""
    Write-Host " Test-MantraBlockSendersConnection" -ForegroundColor Yellow
    Write-Host " Verify configuration" -ForegroundColor Gray
    Write-Host ""
    Write-Host "Documentation: https://app.mantra.ms/docs" -ForegroundColor Cyan
    Write-Host "Support: support@mantra.ms" -ForegroundColor Cyan
    Write-Host ""
}

# Aliases
New-Alias -Name "Setup-MantraBlockSenders" -Value "Connect-MantraBlockSenders" -ErrorAction SilentlyContinue
New-Alias -Name "Test-MantraBlockSenders" -Value "Test-MantraBlockSendersConnection" -ErrorAction SilentlyContinue

# Exports
Export-ModuleMember -Function Connect-MantraBlockSenders, Test-MantraBlockSendersConnection, Get-MantraBlockSendersHelp
Export-ModuleMember -Alias Setup-MantraBlockSenders, Test-MantraBlockSenders