Public/Invoke-Raidiness.ps1
|
function Invoke-Raidiness { <# .SYNOPSIS Collects read-only evidence from the signed-in tenant into JSON files — Microsoft Graph by default, optional modules on request. .DESCRIPTION -Modules picks what a run covers: MsGraph the core Graph collection (default; needs Connect-Raidiness) ExchangeOnline Exchange Online & Purview — Connect-ExchangeOnline + Connect-IPPSSession Teams Microsoft Teams — Connect-MicrosoftTeams SharePoint SharePoint Online admin — Connect-SPOService / PnP PowerPlatform Power Platform & Copilot Studio — Add-PowerAppsAccount All all of the above, one after the other Every module signs in on its own, interactively, and only ever reads. Before each sign-in the run says what will be read and which role it takes and waits for Enter (S skips; -NoPrompt runs straight through). A module that cannot run (missing PowerShell module, declined sign-in, missing role) is skipped with a note — the rest of the run continues and the report marks its checks "not measured". -Path collects into a new run folder named for the moment it started, so runs pile up next to each other instead of overwriting; -OutputPath collects into one directory you name, which is what earlier versions did and what a scripted pipeline may still want. The core Graph evidence is written as <data>/<collector.key>.json in exactly the shape the assessment engine's measure() functions are unit-tested against (paged value-merges, single objects, CSV rows); module results land as <data>/raidiness-<module>.json. Whatever could not be collected, and why, is written next to them as raidiness-run.json and repeated in the run log. .PARAMETER Modules Which modules to run (default: MsGraph). Repeatable, or All. .PARAMETER TenantId Your tenant id or default domain — the optional modules sign in with it; taken from the Graph session when omitted. Give the <name>.onmicrosoft.com form when you include SharePoint: its admin URL needs the tenant name, which is otherwise looked up once from /organization. .PARAMETER Path Where the run folders live. A new <timestamp> folder is created inside it for this run, with the evidence in its data/ subfolder. Default: Raidiness in the current user's Documents folder. This avoids requiring administrator rights when PowerShell starts in a protected working directory such as C:\Windows\System32. .PARAMETER OutputPath One directory for the evidence files, chosen by you and reused on every run. The form earlier versions used. Default: ./raidiness-data .PARAMETER IncludeSlow Also run Graph collectors marked slow in the manifest. .PARAMETER Collector Collect only the named Graph collector keys (repeatable), e.g. a re-try. .PARAMETER MaxPage Stop a paged Graph source after this many pages and record it as truncated (default 200), instead of following @odata.nextLink forever. .PARAMETER GraphRequestTimeoutSeconds Maximum time, in seconds, for each Microsoft Graph request. A timed-out collector is recorded as skipped and collection continues (default 100). .PARAMETER NoPrompt Do not pause before each module sign-in (for an unattended run). .PARAMETER Quiet No progress bars and no per-collector lines; warnings and the log stay. .PARAMETER HashUpns Let the module scripts replace user names and e-mail addresses with hashes in their results (where a script supports it). The Graph evidence is not affected. .EXAMPLE Connect-Raidiness Invoke-Raidiness -Modules All -TenantId contoso.onmicrosoft.com Export-RaidinessReport -Customer 'Contoso Ltd' #> [CmdletBinding(DefaultParameterSetName = 'RunFolder')] param( [ValidateSet('All', 'MsGraph', 'ExchangeOnline', 'Teams', 'SharePoint', 'PowerPlatform')] [string[]] $Modules = @('MsGraph'), [string] $TenantId, [Parameter(ParameterSetName = 'RunFolder')] [string] $Path = (Get-RaidinessDefaultRunPath), [Parameter(ParameterSetName = 'Legacy', Mandatory = $true)] [string] $OutputPath, [switch] $IncludeSlow, [string[]] $Collector, [int] $MaxPage = 200, [ValidateRange(1, 2147483647)] [int] $GraphRequestTimeoutSeconds = 100, [switch] $NoPrompt, [switch] $Quiet, [switch] $HashUpns ) $ErrorActionPreference = 'Stop' $selected = Get-RaidinessModuleCatalog -Modules $Modules $context = Get-MgContext if (($selected.Name -contains 'MsGraph') -and -not $context) { throw 'Not connected to Microsoft Graph. Run Connect-Raidiness first (or leave MsGraph out of -Modules).' } # The optional modules sign in with the tenant; resolve it once, up front, # so a run that cannot sign in anywhere fails before the first prompt. $tenant = $null if (@($selected | Where-Object { $_.Script }).Count -gt 0) { $tenant = Resolve-RaidinessTenant -TenantId $TenantId } $run = $null if ($PSCmdlet.ParameterSetName -eq 'RunFolder') { $run = Initialize-RaidinessRunFolder -Path $Path -TenantName ($tenant ? $tenant.TenantName : $null) $dataPath = $run.Data Open-RaidinessLog -Path $run.Root | Out-Null } else { $dataPath = $OutputPath } Write-Host "Raidiness — read-only collection: $($selected.Title -join ', ')." -ForegroundColor Cyan Write-Host 'Each module signs in on its own and only reads. Nothing in your tenant will be changed.' -ForegroundColor Cyan $summary = [System.Collections.Generic.List[object]]::new() $skipped = [System.Collections.Generic.List[object]]::new() $notes = [System.Collections.Generic.List[string]]::new() $step = 0 try { foreach ($module in $selected) { $step++ Write-RaidinessProgress -Id 1 -Quiet:$Quiet -Activity 'Raidiness checkup' ` -Status "$($module.Title) ($step of $($selected.Count))" ` -PercentComplete ([int](100 * ($step - 1) / [math]::Max($selected.Count, 1))) Write-Host '' Write-Host "[$step/$($selected.Count)] $($module.Title)" -ForegroundColor Cyan Write-Host " reads: $($module.Reads)" Write-Host " sign-in: $($module.SignIn)" Write-Host " role: $($module.Role)" $missing = @($module.RequiredModules | Where-Object { -not (Get-Module -ListAvailable -Name $_ | Select-Object -First 1) }) if ($missing.Count -gt 0) { $reason = "PowerShell module(s) not installed: $($missing -join ', ') (Install-Module $($missing[0]) -Scope CurrentUser)" Write-Warning "Skipped $($module.Title): $reason" Write-RaidinessLog -Level warn -Phase 'module' -Source $module.Name -Message "skipped: $reason" $summary.Add([pscustomobject]@{ Module = $module.Name; Status = 'skipped'; Note = $reason }) $skipped.Add([pscustomobject]@{ Key = $module.Name; Kind = 'module'; Reason = $reason }) continue } if ($module.Name -eq 'SharePoint' -and -not $tenant.TenantName) { $reason = 'tenant name unknown — the SharePoint admin URL is <name>-admin.sharepoint.com; pass -TenantId <name>.onmicrosoft.com' Write-Warning "Skipped $($module.Title): $reason" Write-RaidinessLog -Level warn -Phase 'module' -Source $module.Name -Message "skipped: $reason" $summary.Add([pscustomobject]@{ Module = $module.Name; Status = 'skipped'; Note = $reason }) $skipped.Add([pscustomobject]@{ Key = $module.Name; Kind = 'module'; Reason = $reason }) continue } if ($module.Script -and -not $NoPrompt) { $answer = Read-Host 'Press Enter to continue, or S to skip' if ("$answer".Trim() -like 's*') { Write-Host " skipped $($module.Title) at your request." -ForegroundColor Yellow Write-RaidinessLog -Phase 'module' -Source $module.Name -Message 'skipped: declined at the prompt' $summary.Add([pscustomobject]@{ Module = $module.Name; Status = 'skipped'; Note = 'declined at the prompt' }) $skipped.Add([pscustomobject]@{ Key = $module.Name; Kind = 'module'; Reason = 'declined at the prompt' }) continue } } try { if ($module.Name -eq 'MsGraph') { $graphResult = Invoke-RaidinessGraphCollect -OutputPath $dataPath -IncludeSlow:$IncludeSlow -Collector $Collector -MaxPage $MaxPage -GraphRequestTimeoutSeconds $GraphRequestTimeoutSeconds -Quiet:$Quiet $note = "$($graphResult.Collected) source(s) collected" + ($graphResult.Skipped.Count -gt 0 ? ", $($graphResult.Skipped.Count) skipped" : '') $summary.Add([pscustomobject]@{ Module = $module.Name; Status = 'collected'; Note = $note; Skipped = $graphResult.Skipped }) foreach ($row in $graphResult.Skipped) { $skipped.Add($row) } foreach ($row in $graphResult.Notes) { $notes.Add($row) } } else { $logPath = $run ? $run.Log : $null Invoke-RaidinessModuleScript -Name $module.Script -TenantId $tenant.TenantId -TenantName $tenant.TenantName -OutputPath $dataPath -HashUpns:$HashUpns -LogPath $logPath $summary.Add([pscustomobject]@{ Module = $module.Name; Status = 'collected'; Note = "raidiness-$($module.Script).json" }) Write-RaidinessLog -Phase 'module' -Source $module.Name -Message 'collected' } } catch { # One declined sign-in or missing role must not cost the rest of the # run; the report degrades this module's checks to "not measured". Write-Warning "Skipped $($module.Title): $($_.Exception.Message)" Write-RaidinessLog -Level error -Phase 'module' -Source $module.Name -Message "skipped: $($_.Exception.Message)" $summary.Add([pscustomobject]@{ Module = $module.Name; Status = 'skipped'; Note = $_.Exception.Message }) $skipped.Add([pscustomobject]@{ Key = $module.Name; Kind = 'module'; Reason = $_.Exception.Message }) } } } finally { Write-RaidinessProgress -Id 1 -Quiet:$Quiet -Activity 'Raidiness checkup' -Completed } # What could not be collected travels with the evidence, so the report can # say why a check is "not measured" instead of leaving the reader guessing. # Out-Null, or the path it returns joins the summary this cmdlet emits. Write-RaidinessRunContext -Path $dataPath -TenantId ($tenant ? $tenant.TenantId : $null) -Skipped $skipped.ToArray() -Notes $notes.ToArray() | Out-Null Write-Host '' Write-Host 'Collection summary:' -ForegroundColor Cyan foreach ($row in $summary) { $color = $row.Status -eq 'collected' ? 'Green' : 'Yellow' Write-Host (" {0,-15} {1,-10} {2}" -f $row.Module, $row.Status, $row.Note) -ForegroundColor $color } if ($skipped.Count -gt 0) { Write-Host " $($skipped.Count) source(s) not collected — see the reasons in the run log or raidiness-run.json." -ForegroundColor Yellow } $resolvedOutput = Resolve-Path -Path $dataPath -ErrorAction SilentlyContinue $dataFolder = $resolvedOutput ? $resolvedOutput.Path : $dataPath Write-Host "Data folder: $dataFolder" if ($run) { Write-Host "Run folder: $($run.Root)" Write-Host "Next: Export-RaidinessReport -RunPath '$($run.Root)' (add -LLM for an LLM-ready markdown bundle)." Close-RaidinessLog | Out-Null } else { Write-Host 'Next: Export-RaidinessReport (add -LLM for an LLM-ready markdown bundle).' } Write-Host "Portal CSV exports (SAM DAG, apps inventory, MSCommerce) can be dropped into $dataFolder" return $summary.ToArray() } |