public/Invoke-ZtAssessment.ps1

<#
.SYNOPSIS
Runs the Zero Trust Assessment against the signed in tenant and generates a report of the findings.

.DESCRIPTION
This function runs the Zero Trust Assessment against the signed in tenant and generates a report of the findings.
The assessment can be configured using command-line parameters, a configuration file, or through interactive prompts.

.PARAMETER Path
The path to the folder to output the report to. If not specified, the report will be output to the current directory.

.PARAMETER Days
Optional. Number of days (between 1 and 30) to query sign-in logs. Defaults to 30 days.

.PARAMETER MaximumSignInLogQueryTime
Optional. The maximum time (in minutes) the assessment should spend on querying sign-in logs. Defaults to 60 minutes. Set to 0 for no limit.

.PARAMETER Resume
If specified, the previously exported data will be used to generate the report.

.PARAMETER ShowLog
If specified, the script will output a high level summary of log messages. Useful for debugging. Use -Verbose and -Debug for more detailed logs.

.PARAMETER ExportLog
If specified, writes the log to a file.

.PARAMETER DisableTelemetry
If specified, disables the collection of telemetry. The only telemetry collected is the tenant id. Defaults to false.

.PARAMETER Tests
The IDs of the specific test(s) to run. If not specified, all tests will be run.

.PARAMETER ConfigurationFile
Path to a configuration file. Parameters specified on the command line will override values from the configuration file.

.PARAMETER Interactive
If specified, prompts the user interactively for input values using a text-based user interface.

.EXAMPLE
Invoke-ZtAssessment

Run the Zero Trust Assessment against the signed in tenant and generates a report of the findings using default settings.

.EXAMPLE
Invoke-ZtAssessment -Path "C:\Reports\ZT" -Days 7 -ShowLog

Run the Zero Trust Assessment with a custom output path, querying 7 days of logs, and showing detailed logging.

.PARAMETER Pillar
The Zero Trust pillar to assess. Valid values are 'All', 'Identity', or 'Devices'. Defaults to 'All' which runs all tests.

.EXAMPLE
Invoke-ZtAssessment -ConfigurationFile "C:\Config\zt-config.json"

Run the Zero Trust Assessment using settings from a configuration file.

.EXAMPLE
Invoke-ZtAssessment -ConfigurationFile "C:\Config\zt-config.json" -Days 14 -ShowLog

Run the Zero Trust Assessment using settings from a configuration file, but override the Days parameter to 14 and enable ShowLog.

.EXAMPLE
Invoke-ZtAssessment -Interactive

Run the Zero Trust Assessment with an interactive text-based user interface to configure all parameters.

.EXAMPLE
Invoke-ZeroTrustAssessment -Pillar Identity

Run only the Identity pillar tests of the Zero Trust Assessment.

.EXAMPLE
Invoke-ZeroTrustAssessment -Pillar Devices

Run only the Devices pillar tests of the Zero Trust Assessment.
#>


function Invoke-ZtAssessment {
    [Alias('Invoke-ZeroTrustAssessment')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        # The path to the folder folder to output the report to. If not specified, the report will be output to the current directory.
        [Parameter(ParameterSetName = 'Default')]
        [string]
        $Path = "./ZeroTrustReport",

        # Optional. Number of days (between 1 and 30) to query sign-in logs. Defaults to last two days.
        [Parameter(ParameterSetName = 'Default')]
        [ValidateScript({
                $_ -ge 1 -and $_ -le 30
            },
            ErrorMessage = "Logs are only available for 30 days. Please enter a number between 1 and 30.")]
        [int]
        $Days = 30,

        # Optional. The maximum time (in minutes) the assessment should spend on querying sign-in logs. Defaults to collecting sign logs for 60 minutes. Set to 0 for no limit.
        [Parameter(ParameterSetName = 'Default')]
        [int]
        $MaximumSignInLogQueryTime = 60,

        # If specified, the previously exported data will be used to generate the report.
        [Parameter(ParameterSetName = 'Default')]
        [switch]
        $Resume,

        # If specified, the script will output a high level summary of log messages. Useful for debugging. Use -Verbose and -Debug for more detailed logs.
        [Parameter(ParameterSetName = 'Default')]
        [switch]
        $ShowLog,

        # If specified, writes the log to a file.
        [Parameter(ParameterSetName = 'Default')]
        [switch]
        $ExportLog,

        # If specified, disables the collection of telemetry. The only telemetry collected is the tenant id. Defaults to true.
        [Parameter(ParameterSetName = 'Default')]
        [switch]
        $DisableTelemetry = $false,

        # The IDs of the specific test(s) to run. If not specified, all tests will be run.
        [Parameter(ParameterSetName = 'Default')]
        [string[]]
        $Tests,

        # Path to a configuration file. Parameters specified on the command line will override values from the configuration file.
        [Parameter(ParameterSetName = 'Default')]
        [ValidateScript({
                if (Test-Path $_ -PathType Leaf) {
                    $true
                } else {
                    throw "Configuration file '$_' does not exist."
                }
            })]
        [string]
        $ConfigurationFile,

        # If specified, prompts the user interactively for input values.
        [Parameter(ParameterSetName = 'Interactive', Mandatory)]
        [switch]
        $Interactive,

        # The Zero Trust pillar to assess. Defaults to All.
        [ValidateSet('All', 'Identity', 'Devices')]
        [string]
        $Pillar = 'All'
    )

    $banner = @"
╔═════════════════════════════════════════════════════════════════════════════╗
║ 🛡️ Microsoft Zero Trust Assessment v2 ║
║ ║
║ Comprehensive security posture evaluation for your Microsoft 365 tenant ║
╚═════════════════════════════════════════════════════════════════════════════╝
"@


    Write-Host $banner -ForegroundColor Cyan
    Write-Host
    Write-Host "🚀 " -NoNewline -ForegroundColor Green
    Write-Host "Starting Zero Trust Assessment..." -ForegroundColor White
    Write-Host

    # Handle configuration file parameter
    if ($ConfigurationFile) {
        try {
            Write-Host "📄 " -NoNewline -ForegroundColor Blue
            Write-Host "Loading configuration from file: " -NoNewline -ForegroundColor White
            Write-Host $ConfigurationFile -ForegroundColor Cyan
            $configContent = Get-Content -Path $ConfigurationFile -Raw | ConvertFrom-Json

            # Define parameters that can be configured
            $configurableParameters = @('Path', 'Days', 'MaximumSignInLogQueryTime', 'ShowLog', 'ExportLog', 'DisableTelemetry', 'Resume', 'Tests')

            # Apply configuration values only if parameters weren't explicitly provided
            foreach ($paramName in $configurableParameters) {
                # Skip if parameter was explicitly provided or config doesn't contain the property
                if ($PSBoundParameters.ContainsKey($paramName) -or
                    $configContent.PSObject.Properties.Name -notcontains $paramName) {
                    continue
                }

                # Special handling for Tests array to ensure it has items
                if ($paramName -eq 'Tests') {
                    if ($configContent.$paramName -and $configContent.$paramName.Count -gt 0) {
                        Set-Variable -Name $paramName -Value $configContent.$paramName
                    }
                } else {
                    Set-Variable -Name $paramName -Value $configContent.$paramName
                }
            }

            Write-Host "✅ " -NoNewline -ForegroundColor Green
            Write-Host "Configuration loaded successfully. Command line parameters will override configuration file values." -ForegroundColor White
            Write-Host
        }
        catch {
            Write-Host "❌ " -NoNewline -ForegroundColor Red
            Write-Host "Failed to load configuration from file '$ConfigurationFile': $($_.Exception.Message)" -ForegroundColor Red
            return
        }
    }

    # Handle interactive parameter collection
    if ($Interactive) {
        try {
            Write-Host "🎮 " -NoNewline -ForegroundColor Magenta
            Write-Host "Starting interactive parameter collection..." -ForegroundColor White
            Write-Host
            $tempConfigFile = New-ZtInteractiveConfig

            # Check if user cancelled the configuration creation
            if ($null -eq $tempConfigFile -or -not $tempConfigFile) {
                Write-Host "⏹️ " -NoNewline -ForegroundColor Yellow
                Write-Host "Interactive configuration cancelled by user. Exiting." -ForegroundColor Yellow
                return
            }

            # Verify the file exists before trying to read it (only if tempConfigFile is not null)
            if (-not (Test-Path $tempConfigFile.FullName -ErrorAction SilentlyContinue)) {
                Write-Host "❌ " -NoNewline -ForegroundColor Red
                Write-Host "Configuration file was not created. Exiting." -ForegroundColor Red
                return
            }

            # Import the configuration data from JSON
            $config = Get-Content -Path $tempConfigFile.FullName | ConvertFrom-Json

            # Assign interactive parameter values to variables
            $Path = $config.Path
            $Days = $config.Days
            $MaximumSignInLogQueryTime = $config.MaximumSignInLogQueryTime
            $ShowLog = $config.ShowLog
            $ExportLog = $config.ExportLog
            $DisableTelemetry = $config.DisableTelemetry
            $Resume = $config.Resume

            if ($config.PSObject.Properties.Name -contains 'Tests' -and $config.Tests.Count -gt 0) {
                $Tests = $config.Tests
            }

            Write-Host "✅ " -NoNewline -ForegroundColor Green
            Write-Host "Interactive configuration complete. Starting assessment..." -ForegroundColor Green
            Write-Host
        }
        catch {
            Write-Host "❌ " -NoNewline -ForegroundColor Red
            Write-Host "Failed to collect interactive parameters: $($_.Exception.Message)" -ForegroundColor Red
            return
        }
    }

    #$ExportLog = $true # Always create support package during public preview TODO: Remove this line after public preview

    if ($ShowLog) {
        $null = New-PSFMessageLevelModifier -Name ZeroTrustAssessmentV2.VeryVerbose -Modifier -1 -IncludeModuleName ZeroTrustAssessmentV2
    }
    else {
        Get-PSFMessageLevelModifier -Name ZeroTrustAssessmentV2.VeryVerbose | Remove-PSFMessageLevelModifier
    }

    if(-not (Test-DatabaseAssembly)) {
        return
    }

    if (-not (Test-ZtContext)) {
        return
    }

    $exportPath = Join-Path $Path "zt-export"

    # Stop if folder has items inside it
    if (!$Resume.IsPresent -and (Test-Path $Path)) {
        if ((Get-ChildItem $Path).Count -gt 0) {
            Write-Host
            Write-Host "⚠️ " -NoNewline -ForegroundColor Yellow
            Write-Host "Output folder is not empty" -ForegroundColor Yellow
            Write-Host "📁 Path: " -NoNewline -ForegroundColor White
            Write-Host $Path -ForegroundColor Cyan
            Write-Host
            Write-Host "To generate a new report, the existing contents need to be removed." -ForegroundColor White
            Write-Host "Do you want to delete the contents and continue? " -NoNewline -ForegroundColor White
            Write-Host "[y/n]" -NoNewline -ForegroundColor Yellow
            $deleteFolder = Read-Host " "

            if ($deleteFolder -eq "y") {
                Write-Host "🗑️ " -NoNewline -ForegroundColor Red
                Write-Host "Cleaning up existing files..." -ForegroundColor White
                Remove-Item -Path $Path -Recurse -Force -ErrorAction Stop | Out-Null
                Write-Host "✅ " -NoNewline -ForegroundColor Green
                Write-Host "Folder cleaned successfully" -ForegroundColor Green
                Write-Host
            }
            else {
                Write-Host "❌ " -NoNewline -ForegroundColor Red
                Write-Host "Assessment cancelled. Please provide a path to an empty folder or use -Resume to continue from existing data." -ForegroundColor Red
                return
            }
        }
    }

    # Create the export path if it doesn't exist
    if (!(Test-Path $exportPath)) {
        New-Item -ItemType Directory -Path $exportPath -Force -ErrorAction Stop | Out-Null
    }


    # Send telemetry if not disabled
    if (!$DisableTelemetry) {
        try {
            $tenantId = (Get-MgContext).TenantId
            if ($tenantId) {
                Send-ZtAppInsightsTelemetry -EventName "ZTv2TenantId" -Properties @{ TenantId = $tenantId }
            }
        }
        catch {
            # Silently continue if sending telemetry fails
            Write-PSFMessage -Level Debug -Message "Failed to send telemetry: $_"
        }
    }

    Clear-ZtModuleVariable # Reset the graph cache and urls to avoid stale data

    Write-PSFMessage 'Creating report folder $Path'
    New-Item -ItemType Directory -Path $Path -Force -ErrorAction Stop | Out-Null

    # Move the interactive configuration file to the report directory if it exists
    if ($Interactive -and $tempConfigFile) {
        try {
            $finalConfigPath = Join-Path $Path "zt-interactive-config.json"
            Move-Item -Path $tempConfigFile.FullName -Destination $finalConfigPath -Force
            Write-Host "Configuration file moved to report directory: $finalConfigPath" -ForegroundColor Green
        }
        catch {
            Write-PSFMessage -Level Warning -Message "Failed to move configuration file to report directory: $_"
        }
    }

    # Collect data
    Export-TenantData -ExportPath $exportPath -Days $Days -MaximumSignInLogQueryTime $MaximumSignInLogQueryTime -Pillar $Pillar
    $db = Export-Database -ExportPath $exportPath -Pillar $Pillar

    # Run the tests
    Invoke-ZtTests -Database $db -Tests $Tests -Pillar $Pillar
    Invoke-ZtTenantInfo -Database $db -Pillar $Pillar

    $assessmentResults = Get-ZtAssessmentResults

    Disconnect-Database -Db $db

    $assessmentResultsJson = $assessmentResults | ConvertTo-Json -Depth 10
    $resultsJsonPath = Join-Path $exportPath "ZeroTrustAssessmentReport.json"
    $assessmentResultsJson | Out-File -FilePath $resultsJsonPath -Force

    Write-ZtProgress -Activity "Creating html report"
    $htmlReportPath = Join-Path $Path "ZeroTrustAssessmentReport.html"
    $output = Get-HtmlReport -AssessmentResults $assessmentResultsJson -Path $Path
    $output | Out-File -FilePath $htmlReportPath -Encoding UTF8

    Write-Host
    Write-Host "🛡️ Zero Trust Assessment report generated at $htmlReportPath" -ForegroundColor Green
    Show-ZtSecurityWarning -ExportPath $exportPath
    Write-Host "▶▶▶ ✨ Your feedback matters! Help us improve 👉 https://aka.ms/ztassess/feedback ◀◀◀" -ForegroundColor Yellow
    Write-Host
    Write-Host
    Invoke-Item $htmlReportPath | Out-Null

    if ($ExportLog) {
        Write-ZtProgress -Activity "Creating support package"
        $logPath = Join-Path $Path "log"
        if(!(Test-Path $logPath)) {
            New-Item -ItemType Directory -Path $logPath -Force -ErrorAction Stop | Out-Null
        }
        New-PSFSupportPackage -Path $logPath
    }
}

function Show-ZtSecurityWarning {
    [CmdletBinding()]
    param (
        [string]
        $ExportPath
    )

    Write-Host
    Write-Host "⚠️ SECURITY REMINDER: The report and export folder contain sensitive tenant information." -ForegroundColor Yellow
    Write-Host "Please delete the export folder and restrict access to the report." -ForegroundColor Yellow
    Write-Host "Export folder: $ExportPath" -ForegroundColor Yellow
    Write-Host "Share the report only with authorized personnel in your organization." -ForegroundColor Yellow
    Write-Host
}