Get-Snapshot.ps1

# Syskit Discovery - Script Runner
# This script runs Syskit Discovery: M365 Security Assessment via the M365Snapshot module
# Usage examples:
# .\Get-Snapshot.ps1 -TenantId "contoso.onmicrosoft.com" -ClientId "existing-app-id"
# .\Get-Snapshot.ps1 -TenantId "contoso.onmicrosoft.com"
# .\Get-Snapshot.ps1 -TenantId "contoso.onmicrosoft.com" -RegisterClient
# .\Get-Snapshot.ps1 -TenantId "contoso.onmicrosoft.com" -Scopes Applications,Exchange

[CmdletBinding(PositionalBinding=$false)]
param(
    [Parameter(Mandatory=$true)]
    [string]$TenantId,

    [Parameter(Mandatory=$false)]
    [string]$ClientId,

    [Parameter(Mandatory=$false)]
    [switch]$RegisterClient,

    [Parameter(Mandatory=$false)]
    [switch]$GenerateHTMLReport = $true,

    [Parameter(Mandatory=$false)]
    [string]$ReportPath,

    [Parameter(Mandatory=$false)]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$MaxUsers = 10000,

    [Parameter(Mandatory=$false)]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$MaxGroups = 10000,

    [Parameter(Mandatory=$false)]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$MaxSites = 1000,

    [Parameter(Mandatory=$false)]
    [switch]$LoadAllUsers,

    [Parameter(Mandatory=$false)]
    [switch]$LoadAllGroups,

    [Parameter(Mandatory=$false)]
    [switch]$LoadAllSites,

    [Parameter(Mandatory=$false)]
    [ValidateSet('All', 'Entra', 'Exchange', 'Applications', 'SharePoint')]
    [string[]]$Scopes = @('All'),

    [Parameter(Mandatory=$false)]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$MaxAppRegistrations = 1000,

    [Parameter(Mandatory=$false)]
    [switch]$LoadAllAppRegistrations,

    [Parameter(Mandatory=$false)]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$MaxSharedMailboxes = 100,

    [Parameter(Mandatory=$false)]
    [ValidateRange(1, [int]::MaxValue)]
    [int]$MaxSecurityDistributionGroups = 100,

    [Parameter(Mandatory=$false)]
    [string[]]$ConfidentialSensitivityLabels = @(),

    [Parameter(Mandatory=$false)]
    [string]$ConfidentialSensitivityLabelsFile,

    [Parameter(Mandatory=$false)]
    [switch]$NoRelaunch
)

$script:AppName = "Syskit Discovery"
$script:AppStorageName = ($script:AppName -replace "[^a-zA-Z0-9]", "").Trim()
if ([string]::IsNullOrWhiteSpace($script:AppStorageName)) {
    $script:AppStorageName = "App"
}

$global:SyskitDiscoveryTranscriptActive = $false

function Start-RunTranscript {
    param(
        [string]$StorageName,
        [switch]$Quiet
    )

    try {
        $logRoot = Join-Path $env:LOCALAPPDATA $StorageName
        $logDir = Join-Path $logRoot "Logs"
        if (-not (Test-Path $logDir)) {
            New-Item -Path $logDir -ItemType Directory -Force | Out-Null
        }

        $timestamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
        $logPath = Join-Path $logDir ("Snapshot_{0}_{1}.log" -f $timestamp, $PID)
        Start-Transcript -Path $logPath -Force | Out-Null
        $global:SyskitDiscoveryTranscriptActive = $true

        if (-not $Quiet) {
            Write-Host "[INFO] Run log: $logPath" -ForegroundColor DarkGray
        }
    }
    catch {
        if (-not $Quiet) {
            Write-Host "[INFO] Unable to start run transcript logging: $($_.Exception.Message)" -ForegroundColor DarkGray
        }
    }
}

if (-not (Get-EventSubscriber -ErrorAction SilentlyContinue | Where-Object { $_.SourceIdentifier -eq 'SyskitDiscovery.TranscriptStop' })) {
    Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
        if ($global:SyskitDiscoveryTranscriptActive) {
            try { Stop-Transcript | Out-Null } catch {}
            $global:SyskitDiscoveryTranscriptActive = $false
        }
    } -SupportEvent -ErrorAction SilentlyContinue | Out-Null
}

if ($NoRelaunch) {
    Start-RunTranscript -StorageName $script:AppStorageName
}

if ([string]::IsNullOrWhiteSpace($ReportPath)) {
    $ReportPath = "$($script:AppStorageName)_Report.html"
}

if (-not [string]::IsNullOrWhiteSpace($ConfidentialSensitivityLabelsFile) -and (Test-Path $ConfidentialSensitivityLabelsFile)) {
    try {
        $labelsFromFile = Get-Content -Path $ConfidentialSensitivityLabelsFile -Raw | ConvertFrom-Json
        if ($labelsFromFile -is [System.Array]) {
            $ConfidentialSensitivityLabels = @($labelsFromFile | ForEach-Object { [string]$_ })
        }
        elseif ($null -ne $labelsFromFile) {
            $ConfidentialSensitivityLabels = @([string]$labelsFromFile)
        }
    }
    catch {
        Write-Host "WARNING: Failed to load ConfidentialSensitivityLabels from file '$ConfidentialSensitivityLabelsFile'." -ForegroundColor Yellow
    }
    finally {
        try {
            Remove-Item -Path $ConfidentialSensitivityLabelsFile -Force -ErrorAction SilentlyContinue
        }
        catch { }
    }
}

if (-not $NoRelaunch) {
    $pwshCmd = Get-Command -Name pwsh -ErrorAction SilentlyContinue
    if ($pwshCmd) {
        Write-Host "Starting clean PowerShell session for dependency isolation..." -ForegroundColor DarkGray

        $childArgs = @(
            "-NoProfile",
            "-ExecutionPolicy", "Bypass",
            "-File", $PSCommandPath,
            "-TenantId", $TenantId,
            "-NoRelaunch"
        )

        if (-not [string]::IsNullOrWhiteSpace($ClientId)) {
            $childArgs += @("-ClientId", $ClientId)
        }
        if ($RegisterClient) {
            $childArgs += "-RegisterClient"
        }
        $childArgs += "-GenerateHTMLReport:$GenerateHTMLReport"
        if ($PSBoundParameters.ContainsKey("ReportPath") -and -not [string]::IsNullOrWhiteSpace($ReportPath)) {
            $childArgs += @("-ReportPath", $ReportPath)
        }
        if ($PSBoundParameters.ContainsKey("MaxUsers")) {
            $childArgs += @("-MaxUsers", $MaxUsers)
        }
        if ($PSBoundParameters.ContainsKey("MaxGroups")) {
            $childArgs += @("-MaxGroups", $MaxGroups)
        }
        if ($PSBoundParameters.ContainsKey("MaxSites")) {
            $childArgs += @("-MaxSites", $MaxSites)
        }
        if ($LoadAllUsers) {
            $childArgs += "-LoadAllUsers"
        }
        if ($LoadAllGroups) {
            $childArgs += "-LoadAllGroups"
        }
        if ($LoadAllSites) {
            $childArgs += "-LoadAllSites"
        }
        if ($PSBoundParameters.ContainsKey("Scopes")) {
            foreach ($scopeName in @($Scopes)) {
                $childArgs += @("-Scopes", $scopeName)
            }
        }
        if ($PSBoundParameters.ContainsKey("MaxAppRegistrations")) {
            $childArgs += @("-MaxAppRegistrations", $MaxAppRegistrations)
        }
        if ($LoadAllAppRegistrations) {
            $childArgs += "-LoadAllAppRegistrations"
        }
        if ($PSBoundParameters.ContainsKey("MaxSharedMailboxes")) {
            $childArgs += @("-MaxSharedMailboxes", $MaxSharedMailboxes)
        }
        if ($PSBoundParameters.ContainsKey("MaxSecurityDistributionGroups")) {
            $childArgs += @("-MaxSecurityDistributionGroups", $MaxSecurityDistributionGroups)
        }
        if ($ConfidentialSensitivityLabels.Count -gt 0) {
            $nonEmptyLabels = @($ConfidentialSensitivityLabels | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
            if ($nonEmptyLabels.Count -gt 0) {
                $labelsFile = Join-Path ([System.IO.Path]::GetTempPath()) ("{0}_{1}.json" -f $script:AppStorageName, ([guid]::NewGuid().ToString("N")))
                $nonEmptyLabels | ConvertTo-Json -Depth 3 | Set-Content -Path $labelsFile -Encoding UTF8
                $childArgs += @("-ConfidentialSensitivityLabelsFile", $labelsFile)
            }
        }

        & $pwshCmd.Source @childArgs
        exit $LASTEXITCODE
    }

    Start-RunTranscript -StorageName $script:AppStorageName
}

# Import the module
$modulePath = Join-Path $PSScriptRoot "Syskit.Discovery.psd1"

if (-not (Test-Path $modulePath)) {
    Write-Host "ERROR: $($script:AppName) module manifest not found." -ForegroundColor Red
    Write-Host "Expected: $modulePath" -ForegroundColor Yellow
    Write-Host "Please ensure Syskit.Discovery.psd1 and M365Snapshot.psm1 are in the same directory." -ForegroundColor Yellow
    exit 1
}

try {
    Import-Module $modulePath -Force -ErrorAction Stop
}
catch {
    Write-Host "ERROR: Failed to import module from '$modulePath'." -ForegroundColor Red
    Write-Host "Details: $($_.Exception.Message)" -ForegroundColor Yellow
    Write-Host "Ensure prerequisite modules are installed: MSAL.PS, PnP.PowerShell, Microsoft.Graph.Authentication, Microsoft.Graph.Applications, and Microsoft.Graph.Identity.SignIns." -ForegroundColor Yellow
    exit 1
}

# Call the module function
$snapshotParams = @{
    TenantId = $TenantId
    ClientId = $ClientId
    RegisterClient = $RegisterClient
    Scopes = $Scopes
    MaxUsers = $MaxUsers
    MaxGroups = $MaxGroups
    MaxSites = $MaxSites
    MaxAppRegistrations = $MaxAppRegistrations
    MaxSharedMailboxes = $MaxSharedMailboxes
    MaxSecurityDistributionGroups = $MaxSecurityDistributionGroups
    GenerateHTMLReport = $GenerateHTMLReport
}

if ($ConfidentialSensitivityLabels.Count -gt 0) {
    $snapshotParams["ConfidentialSensitivityLabels"] = $ConfidentialSensitivityLabels
}

if ($LoadAllUsers) {
    $snapshotParams["LoadAllUsers"] = $true
}
if ($LoadAllGroups) {
    $snapshotParams["LoadAllGroups"] = $true
}
if ($LoadAllSites) {
    $snapshotParams["LoadAllSites"] = $true
}
if ($LoadAllAppRegistrations) {
    $snapshotParams["LoadAllAppRegistrations"] = $true
}

if ($GenerateHTMLReport -and $PSBoundParameters.ContainsKey('ReportPath')) {
    $snapshotParams["ReportPath"] = $ReportPath
}

Get-M365Snapshot @snapshotParams