Private/Format-TLDocumentation.ps1
|
function Format-TLDocumentation { <# .SYNOPSIS Builds the human-readable Markdown tenant documentation from a snapshot. .DESCRIPTION The Conditional Access section renders one prose block per policy with resolved display names - the part consultants otherwise write by hand. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object]$Snapshot ) function Test-TLArea { param([object]$Snapshot, [string]$Name) return ($null -ne $Snapshot.PSObject.Properties[$Name] -and $null -ne $Snapshot.$Name) } function Get-TLListText { param([object]$Values, [string]$EmptyText = 'none') $list = @($Values | Where-Object { $null -ne $_ -and "$_" -ne '' }) if ($list.Count -eq 0) { return $EmptyText } return ($list -join ', ') } $md = [System.Text.StringBuilder]::new() $tenantLabel = 'tenant' $generated = [DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm') + ' UTC' if ($Snapshot.PSObject.Properties['_Manifest'] -and $Snapshot._Manifest) { $manifest = $Snapshot._Manifest if ($manifest.PSObject.Properties['tenant'] -and $manifest.tenant -and $manifest.tenant.PSObject.Properties['label']) { $tenantLabel = [string]$manifest.tenant.label } if ($manifest.PSObject.Properties['generatedUtc'] -and $manifest.generatedUtc) { $generated = [string]$manifest.generatedUtc } } [void]$md.AppendLine("# Tenant documentation - $tenantLabel") [void]$md.AppendLine() [void]$md.AppendLine("*Generated $generated by TenantLens $($script:TLVersion). Source snapshot: ``$(Split-Path -Path $Snapshot._Path -Parent | Split-Path -Leaf)``.*") [void]$md.AppendLine() # --- Tenant overview ----------------------------------------------------- if (Test-TLArea -Snapshot $Snapshot -Name 'TenantBasics') { $basics = $Snapshot.TenantBasics [void]$md.AppendLine('## Tenant overview') [void]$md.AppendLine() if ($basics.PSObject.Properties['organization'] -and $basics.organization) { $org = $basics.organization [void]$md.AppendLine("- **Organization:** $($org.displayName)") [void]$md.AppendLine("- **Tenant id:** ``$($org.id)``") if ($org.PSObject.Properties['countryLetterCode'] -and $org.countryLetterCode) { [void]$md.AppendLine("- **Country:** $($org.countryLetterCode)") } } [void]$md.AppendLine() if ($basics.PSObject.Properties['domains'] -and @($basics.domains).Count -gt 0) { [void]$md.AppendLine('### Verified domains') [void]$md.AppendLine() [void]$md.AppendLine('| Domain | Verified | Default | Initial |') [void]$md.AppendLine('| --- | --- | --- | --- |') foreach ($domain in @($basics.domains)) { [void]$md.AppendLine(('| {0} | {1} | {2} | {3} |' -f $domain.id, $(if ($domain.isVerified) { 'yes' } else { 'no' }), $(if ($domain.isDefault) { 'yes' } else { '' }), $(if ($domain.isInitial) { 'yes' } else { '' }))) } [void]$md.AppendLine() } if ($basics.PSObject.Properties['skus'] -and @($basics.skus).Count -gt 0) { [void]$md.AppendLine('### Licensing') [void]$md.AppendLine() [void]$md.AppendLine('| SKU | Prepaid | Consumed |') [void]$md.AppendLine('| --- | --- | --- |') foreach ($sku in @($basics.skus)) { $prepaid = '' if ($sku.PSObject.Properties['prepaidUnits'] -and $sku.prepaidUnits) { $prepaid = $sku.prepaidUnits.enabled } [void]$md.AppendLine(('| {0} | {1} | {2} |' -f $sku.skuPartNumber, $prepaid, $sku.consumedUnits)) } [void]$md.AppendLine() } } # --- Conditional Access -------------------------------------------------- if (Test-TLArea -Snapshot $Snapshot -Name 'ConditionalAccess') { $policies = @($Snapshot.ConditionalAccess) [void]$md.AppendLine("## Conditional Access policies ($($policies.Count))") [void]$md.AppendLine() foreach ($policy in ($policies | Sort-Object -Property displayName)) { $stateLabel = switch ([string]$policy.state) { 'enabled' { 'Enabled' } 'disabled' { 'Disabled' } 'enabledForReportingButNotEnforced' { 'Report-only' } default { [string]$policy.state } } $resolved = $null if ($policy.PSObject.Properties['_tlResolved']) { $resolved = $policy._tlResolved } [void]$md.AppendLine("### $($policy.displayName) ($stateLabel)") [void]$md.AppendLine() $conditions = $policy.conditions $users = $null if ($conditions -and $conditions.PSObject.Properties['users']) { $users = $conditions.users } $userInclude = @(); $userExclude = @() if ($resolved) { $userInclude = @($resolved.usersInclude) + @($resolved.groupsInclude | ForEach-Object { "group: $_" }) + @($resolved.rolesInclude | ForEach-Object { "role: $_" }) $userExclude = @($resolved.usersExclude) + @($resolved.groupsExclude | ForEach-Object { "group: $_" }) + @($resolved.rolesExclude | ForEach-Object { "role: $_" }) } elseif ($users) { $userInclude = @($users.includeUsers) + @($users.includeGroups) + @($users.includeRoles) $userExclude = @($users.excludeUsers) + @($users.excludeGroups) + @($users.excludeRoles) } [void]$md.AppendLine("- **Users:** include: $(Get-TLListText -Values $userInclude); exclude: $(Get-TLListText -Values $userExclude)") $appInclude = @(); $appExclude = @() if ($resolved) { $appInclude = @($resolved.appsInclude); $appExclude = @($resolved.appsExclude) } elseif ($conditions -and $conditions.PSObject.Properties['applications'] -and $conditions.applications) { $appInclude = @($conditions.applications.includeApplications) $appExclude = @($conditions.applications.excludeApplications) } [void]$md.AppendLine("- **Applications:** include: $(Get-TLListText -Values $appInclude); exclude: $(Get-TLListText -Values $appExclude)") $conditionParts = [System.Collections.Generic.List[string]]::new() if ($conditions) { if ($conditions.PSObject.Properties['clientAppTypes'] -and @($conditions.clientAppTypes).Count -gt 0) { $conditionParts.Add("client apps: $(Get-TLListText -Values $conditions.clientAppTypes)") } if ($conditions.PSObject.Properties['platforms'] -and $conditions.platforms) { $conditionParts.Add("platforms: include $(Get-TLListText -Values $conditions.platforms.includePlatforms -EmptyText 'all')") } $locInclude = @(); $locExclude = @() if ($resolved -and $resolved.PSObject.Properties['locationsInclude']) { $locInclude = @($resolved.locationsInclude); $locExclude = @($resolved.locationsExclude) } elseif ($conditions.PSObject.Properties['locations'] -and $conditions.locations) { $locInclude = @($conditions.locations.includeLocations); $locExclude = @($conditions.locations.excludeLocations) } if (@($locInclude).Count -gt 0 -or @($locExclude).Count -gt 0) { $conditionParts.Add("locations: include $(Get-TLListText -Values $locInclude -EmptyText 'all'); exclude $(Get-TLListText -Values $locExclude)") } if ($conditions.PSObject.Properties['signInRiskLevels'] -and @($conditions.signInRiskLevels).Count -gt 0) { $conditionParts.Add("sign-in risk: $(Get-TLListText -Values $conditions.signInRiskLevels)") } if ($conditions.PSObject.Properties['userRiskLevels'] -and @($conditions.userRiskLevels).Count -gt 0) { $conditionParts.Add("user risk: $(Get-TLListText -Values $conditions.userRiskLevels)") } } if ($conditionParts.Count -eq 0) { $conditionParts.Add('no additional conditions') } [void]$md.AppendLine("- **Conditions:** $($conditionParts -join '; ')") $grantText = 'none' if ($policy.PSObject.Properties['grantControls'] -and $policy.grantControls) { $grant = $policy.grantControls $controls = @($grant.builtInControls) if ($grant.PSObject.Properties['authenticationStrength'] -and $grant.authenticationStrength -and $grant.authenticationStrength.PSObject.Properties['displayName'] -and $grant.authenticationStrength.displayName) { $controls += "authentication strength: $($grant.authenticationStrength.displayName)" } if (@($controls).Count -gt 0) { $operator = '' if ($grant.PSObject.Properties['operator'] -and $grant.operator -and @($controls).Count -gt 1) { $operator = " ($($grant.operator))" } $grantText = "$(Get-TLListText -Values $controls)$operator" } } [void]$md.AppendLine("- **Grant:** $grantText") if ($policy.PSObject.Properties['sessionControls'] -and $policy.sessionControls) { $sessionParts = @($policy.sessionControls.PSObject.Properties | Where-Object { $null -ne $_.Value -and $_.Name -notlike '@*' } | ForEach-Object { $_.Name }) if ($sessionParts.Count -gt 0) { [void]$md.AppendLine("- **Session:** $($sessionParts -join ', ')") } } [void]$md.AppendLine() } } # --- Named locations ----------------------------------------------------- if (Test-TLArea -Snapshot $Snapshot -Name 'NamedLocations') { $locations = @($Snapshot.NamedLocations) [void]$md.AppendLine("## Named locations ($($locations.Count))") [void]$md.AppendLine() foreach ($location in $locations) { $detail = '' $type = ([string]$location.'@odata.type') -replace '^#microsoft\.graph\.', '' if ($type -eq 'ipNamedLocation') { $ranges = @($location.ipRanges | ForEach-Object { $_.cidrAddress }) $trusted = $(if ($location.isTrusted) { 'trusted' } else { 'not trusted' }) $detail = "IP ($trusted): $(Get-TLListText -Values $ranges)" } elseif ($type -eq 'countryNamedLocation') { $detail = "Countries: $(Get-TLListText -Values $location.countriesAndRegions)" } [void]$md.AppendLine("- **$($location.displayName)** - $detail") } [void]$md.AppendLine() } # --- Directory roles ----------------------------------------------------- if (Test-TLArea -Snapshot $Snapshot -Name 'RoleAssignments') { $roleData = $Snapshot.RoleAssignments [void]$md.AppendLine('## Privileged directory roles') [void]$md.AppendLine() $assignments = @() $eligibility = @() if ($roleData.PSObject.Properties['assignments']) { $assignments = @($roleData.assignments) } if ($roleData.PSObject.Properties['eligibility']) { $eligibility = @($roleData.eligibility) } $byRole = $assignments | Group-Object -Property _tlRoleName | Sort-Object -Property Name foreach ($group in $byRole) { $members = @($group.Group | ForEach-Object { $(if ($_.PSObject.Properties['_tlPrincipalName'] -and $_._tlPrincipalName) { $_._tlPrincipalName } else { $_.principalId }) }) [void]$md.AppendLine("- **$($group.Name)** (active): $(Get-TLListText -Values $members)") } $eligibleByRole = $eligibility | Group-Object -Property _tlRoleName | Sort-Object -Property Name foreach ($group in $eligibleByRole) { $members = @($group.Group | ForEach-Object { $(if ($_.PSObject.Properties['_tlPrincipalName'] -and $_._tlPrincipalName) { $_._tlPrincipalName } else { $_.principalId }) }) [void]$md.AppendLine("- **$($group.Name)** (PIM eligible): $(Get-TLListText -Values $members)") } [void]$md.AppendLine() } # --- Authorization policy ------------------------------------------------ if (Test-TLArea -Snapshot $Snapshot -Name 'AuthorizationPolicy') { $authArea = $Snapshot.AuthorizationPolicy [void]$md.AppendLine('## Authorization and security defaults') [void]$md.AppendLine() if ($authArea.PSObject.Properties['securityDefaults'] -and $authArea.securityDefaults) { $sdState = $(if ($authArea.securityDefaults.isEnabled) { 'enabled' } else { 'disabled' }) [void]$md.AppendLine("- **Security defaults:** $sdState") } if ($authArea.PSObject.Properties['authorizationPolicy'] -and $authArea.authorizationPolicy) { $authPolicy = $authArea.authorizationPolicy if ($authPolicy.PSObject.Properties['allowInvitesFrom'] -and $authPolicy.allowInvitesFrom) { [void]$md.AppendLine("- **Guest invites allowed from:** $($authPolicy.allowInvitesFrom)") } if ($authPolicy.PSObject.Properties['guestUserRoleId'] -and $authPolicy.guestUserRoleId) { $guestRole = switch ([string]$authPolicy.guestUserRoleId) { 'a0b1b346-4d3e-4e8b-98f8-753987be4970' { 'same as member users' } '10dae51f-b6af-4016-8d66-8c2a99b929b3' { 'limited access (default)' } '2af84b1e-32c8-42b7-82bc-daa82404023b' { 'restricted access' } default { [string]$authPolicy.guestUserRoleId } } [void]$md.AppendLine("- **Guest user permissions:** $guestRole") } if ($authPolicy.PSObject.Properties['defaultUserRolePermissions'] -and $authPolicy.defaultUserRolePermissions) { $userPermissions = $authPolicy.defaultUserRolePermissions [void]$md.AppendLine("- **Users can register applications:** $([bool]$userPermissions.allowedToCreateApps)") if ($userPermissions.PSObject.Properties['permissionGrantPoliciesAssigned']) { [void]$md.AppendLine("- **User consent policy:** $(Get-TLListText -Values $userPermissions.permissionGrantPoliciesAssigned -EmptyText 'consent disabled')") } } } [void]$md.AppendLine() } # --- Authentication methods ---------------------------------------------- if (Test-TLArea -Snapshot $Snapshot -Name 'AuthMethods') { $methodPolicy = $Snapshot.AuthMethods if ($methodPolicy.PSObject.Properties['authenticationMethodConfigurations'] -and $methodPolicy.authenticationMethodConfigurations) { [void]$md.AppendLine('## Authentication methods') [void]$md.AppendLine() foreach ($method in @($methodPolicy.authenticationMethodConfigurations)) { [void]$md.AppendLine("- **$($method.id):** $($method.state)") } [void]$md.AppendLine() } } # --- E-mail authentication ----------------------------------------------- if (Test-TLArea -Snapshot $Snapshot -Name 'EmailAuthDns') { $mailDomains = @($Snapshot.EmailAuthDns) [void]$md.AppendLine('## E-mail authentication (SPF / DKIM / DMARC)') [void]$md.AppendLine() [void]$md.AppendLine('| Domain | SPF | DKIM | DMARC |') [void]$md.AppendLine('| --- | --- | --- | --- |') foreach ($entry in $mailDomains) { $spfText = 'missing' if ($entry.spf.present) { $spfText = "present ($($entry.spf.qualifier))" } $dkimText = 'missing' if ($entry.dkim.selector1 -and $entry.dkim.selector2) { $dkimText = 'both selectors' } elseif ($entry.dkim.selector1 -or $entry.dkim.selector2) { $dkimText = 'one selector only' } $dmarcText = 'missing' if ($entry.dmarc.present) { $dmarcText = "p=$($entry.dmarc.policy)" } [void]$md.AppendLine(('| {0} | {1} | {2} | {3} |' -f $entry.domain, $spfText, $dkimText, $dmarcText)) } [void]$md.AppendLine() } # --- Coverage note ------------------------------------------------------- if ($Snapshot.PSObject.Properties['_Areas'] -and $Snapshot._Areas) { $skippedAreas = @($Snapshot._Areas.PSObject.Properties | Where-Object { $_.Value.Skipped }) if ($skippedAreas.Count -gt 0) { [void]$md.AppendLine('## Not collected') [void]$md.AppendLine() foreach ($areaProperty in $skippedAreas) { [void]$md.AppendLine("- **$($areaProperty.Name):** $($areaProperty.Value.SkipReason)") } [void]$md.AppendLine() } } return $md.ToString() } |