public/core/New-MtMaesterApp.ps1

function New-MtMaesterApp {
    <#
    .SYNOPSIS
    Creates a new Maester application in Entra ID with required permissions.

    .DESCRIPTION
    Creates a new application registration in Entra ID specifically configured for running
    Maester tests in a DevOps pipeline. The application will be granted the necessary Graph API
    permissions based on the specified parameters and tagged for easy identification.

    The user running this command must have a permissions to create applications and consent to Graph Permissions.
    This requires a minimum of being a Privileged Role Administrator (and Cloud Application Administrator if needed) or Global Administrator.

    .PARAMETER Name
    The display name for the application. If not specified, defaults to 'Maester DevOps Account'.

    .PARAMETER SendMail
    If specified, includes the Mail.Send permission scope.

    .PARAMETER SendTeamsMessage
    If specified, includes the ChannelMessage.Send permission scope.

    .PARAMETER Privileged
    If specified, includes privileged permission scopes for read-write operations.

    .PARAMETER Scopes
    Additional custom permission scopes to include beyond the default Maester scopes.

    .PARAMETER GitHubOrganization
    Your GitHub organization name or GitHub username (e.g. 'jasonf'). When supplied
    together with -GitHubRepository the cmdlet will also create a federated identity
    credential for GitHub Actions OIDC.

    .PARAMETER GitHubRepository
    Your GitHub repository name where the workflow lives (e.g. 'maester-tests').

    .PARAMETER GitHubActions
    Enable end-to-end GitHub Actions setup. Creates a federated identity credential
    after granting permissions, and auto-detects the GitHub organization/repository
    from the local git remote ('origin') when -GitHubOrganization/-GitHubRepository
    are not explicitly supplied. This is the recommended entry point for the GitHub
    flow.

    .PARAMETER SetGitHubSecrets
    Pushes AZURE_CLIENT_ID and AZURE_TENANT_ID to the target repository's Actions
    secrets via the GitHub CLI ('gh'). Falls back to printing manual instructions
    when 'gh' is unavailable or not authenticated. Passing -SetGitHubSecrets on its
    own implicitly enables the GitHub Actions flow, so -GitHubActions does not need
    to be specified alongside it.

    .EXAMPLE
    New-MtMaesterApp

    Creates a new Maester app with default permissions and name 'Maester DevOps Account'.

    .EXAMPLE
    New-MtMaesterApp -Name "My Maester Pipeline App" -SendMail

    Creates a new Maester app with mail sending capabilities.

    .EXAMPLE
    New-MtMaesterApp -Privileged -Scopes @("User.Read.All", "Group.Read.All")

    Creates a new Maester app with privileged scopes and additional custom scopes.

    .EXAMPLE
    New-MtMaesterApp -GitHubActions -SetGitHubSecrets

    Full zero-config GitHub Actions setup. Auto-detects the target repository from
    the current git remote, creates the app, grants permissions, adds the federated
    credential, and pushes the AZURE_CLIENT_ID / AZURE_TENANT_ID secrets via gh CLI.

    .LINK
    https://maester.dev/docs/commands/New-MtMaesterApp
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colors are beautiful')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'TODO: Implement ShouldProcess')]
    [CmdletBinding()]
    param(
        # The display name for the application
        [string] $Name,

        # Include Mail.Send permission scope
        [switch] $SendMail,

        # Include ChannelMessage.Send permission scope
        [switch] $SendTeamsMessage,

        # Include privileged permission scopes
        [switch] $Privileged,

        # Additional custom permission scopes
        [string[]] $Scopes = @(),

        # If specified adds federated credential for GitHub Actions
        # Your GitHub organization name or GitHub username. E.g. jasonf
        [string] $GitHubOrganization,

        # Your GitHub repository name where the GitHub Actions workflow is located. E.g. maester-tests
        [string] $GitHubRepository,

        # Enable end-to-end GitHub Actions setup (creates a federated identity credential).
        # Auto-detects -GitHubOrganization/-GitHubRepository from the local git remote when
        # they are not explicitly provided.
        [switch] $GitHubActions,

        # Together with -GitHubActions, push AZURE_CLIENT_ID/AZURE_TENANT_ID to the repo's
        # Actions secrets via the GitHub CLI ('gh').
        [switch] $SetGitHubSecrets
    )

    # We use the Azure module to create the app registration since it has pre-consented permissions to create apps
    # Maester is meant for read-only access, so we don't want users to consent to Application.ReadWrite.All or similar.
    # Instead, we create the app using the Az module context and then assign only the minimum required permissions.
    # This also avoids needing admin consent during Connect-MgGraph.
    if (-not (Test-MtAzContext)) {
        return
    }

    # Treat any GitHub-flow parameter as opting into the GitHub Actions path.
    $useGitHubFlow = $GitHubActions -or $SetGitHubSecrets -or $GitHubOrganization -or $GitHubRepository

    if ($useGitHubFlow) {
        # Auto-detect from local git remote only when BOTH are omitted. Mixing an explicit
        # value with auto-detection of the other is ambiguous (which repo did the caller
        # really mean?) so we require both-or-neither.
        if (-not $GitHubOrganization -and -not $GitHubRepository) {
            $detected = Get-MtGitHubRepoFromGit
            if ($detected) {
                $GitHubOrganization = $detected.Organization
                $GitHubRepository   = $detected.Repository
                Write-Host "Auto-detected GitHub repository from git remote: $GitHubOrganization/$GitHubRepository" -ForegroundColor Cyan
            }
        } elseif (-not $GitHubOrganization -or -not $GitHubRepository) {
            Write-Error "Specify both -GitHubOrganization and -GitHubRepository, or omit both to auto-detect from the local git remote."
            return
        }

        if (-not $GitHubOrganization -or -not $GitHubRepository) {
            Write-Error "Both GitHubOrganization and GitHubRepository must be specified to add a federated credential. They can be auto-detected when the current directory is a git working tree whose 'origin' remote points at GitHub."
            return
        }
    }

    if (-not $Name) {
        if($GitHubOrganization -and $GitHubRepository) {
            $Name = "Maester DevOps Account - $GitHubOrganization/$GitHubRepository"
        } else {
            $Name = "Maester DevOps Account"
        }
    }

    $existingApps = Get-MtMaesterApp -WarningAction SilentlyContinue
    $appCount = ($existingApps | Measure-Object).Count
    if ($appCount -gt 0) {
        Write-Warning "We found $appCount Maester application(s) in this tenant."
        $existingApps

        $confirmation = Read-Host "Create a new Maester application anyway? (y/N)"
        if ($confirmation -ne 'y' -and $confirmation -ne 'Y') {
            Write-Host "Update cancelled." -ForegroundColor Yellow
            return
        }
    }

    Write-Host "Creating new Maester application: $Name" -ForegroundColor Green

    # Create the application
    $appBody = @{
        displayName = $Name
        description = "Application created by Maester for running security assessments in DevOps pipelines"
        tags        = @('maester')
    } | ConvertTo-Json -Depth 3

    Write-Verbose "Creating application with body: $appBody"
    $app = Invoke-MtAzureRequest -RelativeUri 'applications' -Method POST -Payload $appBody -Graph

    Write-Host "✅ Application created successfully" -ForegroundColor Green
    Write-Host " Application ID: $($app.appId)" -ForegroundColor Cyan
    Write-Host " Object ID: $($app.id)" -ForegroundColor Cyan

    # Get the required scopes
    $scopeParams = @{}
    if ($SendMail) { $scopeParams['SendMail'] = $true }
    if ($SendTeamsMessage) { $scopeParams['SendTeamsMessage'] = $true }
    if ($Privileged) { $scopeParams['Privileged'] = $true }

    $requiredScopes = Get-MtGraphScope @scopeParams

    # Add any additional custom scopes
    if ($Scopes) {
        $requiredScopes += $Scopes
        $requiredScopes = $requiredScopes | Sort-Object -Unique
    }

    # Create a service principal for the app
    $spBody = @{
        appId = $app.appId
        tags  = @("maester")
    } | ConvertTo-Json

    Write-Host "Creating service principal..." -ForegroundColor Yellow
    $servicePrincipal = Invoke-MtAzureRequest -RelativeUri "servicePrincipals" -Method POST -Payload $spBody -Graph
    Write-Host "✅ Service principal created successfully" -ForegroundColor Green
    Write-Host " Service Principal ID: $($servicePrincipal.id)" -ForegroundColor Cyan

    # Set the permissions
    Write-Host "Configuring permissions..." -ForegroundColor Yellow
    Write-Verbose "Required scopes: $($requiredScopes -join ', ')"

    $permissionsGranted = $true
    try {
        Set-MaesterAppPermission -AppId $app.appId -Scopes $requiredScopes
    } catch {
        $permissionsGranted = $false
        Write-Host "❌ $($_.Exception.Message)" -ForegroundColor Red
    }

    $result = Get-MtMaesterApp -Id $app.id

    Write-Host ""
    if ($permissionsGranted) {
        Write-Host "🎉 Maester application created successfully!" -ForegroundColor Green
    } else {
        Write-Host "⚠️ Maester application was created but some permissions could not be granted." -ForegroundColor Red
        Write-Host " The application is in a non-functional state until all required permissions are consented." -ForegroundColor Yellow
        Write-Host " Ensure the account running New-MtMaesterApp has Privileged Role Administrator or Global Administrator rights." -ForegroundColor Yellow
    }

    if ($useGitHubFlow) {
        $ficParams = @{
            AppId               = $app.appId
            GitHubOrganization  = $GitHubOrganization
            GitHubRepository    = $GitHubRepository
        }
        if ($SetGitHubSecrets) { $ficParams['SetGitHubSecrets'] = $true }
        Add-MtMaesterAppFederatedCredential @ficParams
    } else {
        Write-Output $result
    }
}