Private/Collect/Get-TcEntraIdCollector.ps1
|
function Get-TcEntraIdCollector { <# .SYNOPSIS Lists the Entra ID collectors, in the order they run. #> [CmdletBinding()] [OutputType([hashtable])] param() @( @{ Id = 'ENT-01' Workload = 'Entra ID' Title = 'Organization, domains and sync' Service = 'Graph' Collect = { $o = @((Get-TcProperty (Get-TcGraphObject 'v1.0/organization') 'value')) | Select-Object -First 1 Add-TcTable 'organizationSettings' @( [ordered]@{ 'Setting' = 'Name'; 'Value' = (Get-TcProperty $o 'displayName') }, [ordered]@{ 'Setting' = 'Tenant ID'; 'Value' = (Get-TcProperty $o 'id') }, [ordered]@{ 'Setting' = 'Created on'; 'Value' = (Format-TcValue (Get-TcProperty $o 'createdDateTime')) }, [ordered]@{ 'Setting' = 'Country'; 'Value' = (Get-TcProperty $o 'countryLetterCode') }, [ordered]@{ 'Setting' = 'Language'; 'Value' = (Get-TcProperty $o 'preferredLanguage') }, [ordered]@{ 'Setting' = 'AD sync enabled'; 'Value' = [bool](Get-TcProperty $o 'onPremisesSyncEnabled') }, [ordered]@{ 'Setting' = 'Last AD sync'; 'Value' = (Format-TcValue (Get-TcProperty $o 'onPremisesLastSyncDateTime')) }, [ordered]@{ 'Setting' = 'Days since last sync'; 'Value' = (Get-TcDaysSince (Get-TcProperty $o 'onPremisesLastSyncDateTime')) }, [ordered]@{ 'Setting' = 'Technical notification emails'; 'Value' = (Format-TcValue (Get-TcProperty $o 'technicalNotificationMails')) }) Add-TcTable 'domains' @(foreach ($d in @((Get-TcProperty (Get-TcGraphObject 'v1.0/domains') 'value'))) { [ordered]@{ 'Domain' = (Get-TcProperty $d 'id') 'Verified' = [bool](Get-TcProperty $d 'isVerified') 'Default' = [bool](Get-TcProperty $d 'isDefault') 'Initial' = [bool](Get-TcProperty $d 'isInitial') 'Authentication' = (Get-TcProperty $d 'authenticationType') 'Services' = (Format-TcValue (Get-TcProperty $d 'supportedServices')) } }) Invoke-TcCollectorPart 'On-premises sync' { $s = @(Get-TcGraphList 'v1.0/directory/onPremisesSynchronization') foreach ($x in $s) { Add-TcTable 'organizationSettings' -Append @(ConvertTo-TcSettingRow (Get-TcProperty $x 'features') 'Sync') } } Invoke-TcCollectorPart 'Administrative units' { Add-TcTable 'administrativeUnits' @(foreach ($u in Get-TcGraphList 'v1.0/directory/administrativeUnits') { [ordered]@{ 'Name' = (Get-TcProperty $u 'displayName') 'Description' = (Get-TcProperty $u 'description') 'Membership' = (Get-TcProperty $u 'membershipType') } }) } } } @{ Id = 'ENT-02' Workload = 'Entra ID' Title = 'Licenses and subscriptions' Service = 'Graph' Collect = { Add-TcTable 'licenses' @(foreach ($s in @((Get-TcProperty (Get-TcGraphObject 'v1.0/subscribedSkus') 'value'))) { $purchased = [int](Get-TcProperty (Get-TcProperty $s 'prepaidUnits') 'enabled' 0) [ordered]@{ 'SKU' = (Get-TcProperty $s 'skuPartNumber') 'SkuId' = (Get-TcProperty $s 'skuId') 'State' = (Get-TcProperty $s 'capabilityStatus') 'Purchased' = $purchased 'Consumed' = [int](Get-TcProperty $s 'consumedUnits') 'Available' = $purchased - [int](Get-TcProperty $s 'consumedUnits') 'Suspended' = [int](Get-TcProperty (Get-TcProperty $s 'prepaidUnits') 'suspended' 0) 'In warning' = [int](Get-TcProperty (Get-TcProperty $s 'prepaidUnits') 'warning' 0) 'Plans' = (@((Get-TcProperty $s 'servicePlans') | ForEach-Object { (Get-TcProperty $_ 'servicePlanName') }) -join ', ') } }) } } @{ Id = 'ENT-03' Workload = 'Entra ID' Title = 'Users, guests and last sign-ins' Service = 'Graph' Collect = { $skus = @{} foreach ($l in Get-TcTable 'licenses') { $skus[[string](Get-TcProperty $l 'SkuId')] = (Get-TcProperty $l 'SKU') } if (-not $skus.Count) { try { foreach ($s in @((Get-TcProperty (Get-TcGraphObject 'v1.0/subscribedSkus') 'value'))) { $skus[[string](Get-TcProperty $s 'skuId')] = (Get-TcProperty $s 'skuPartNumber') } } catch { Add-TcCollectorWarning 'License names' $_ } } $fields = 'id,userPrincipalName,displayName,givenName,surname,userType,accountEnabled,createdDateTime,assignedLicenses,licenseAssignmentStates,onPremisesSyncEnabled,onPremisesImmutableId,onPremisesSamAccountName,onPremisesProvisioningErrors,employeeId,mail,mobilePhone,businessPhones,officeLocation,city,department,jobTitle,companyName,usageLocation,externalUserState,creationType,proxyAddresses' $hasSignIn = $true $u = try { Get-TcGraphList "v1.0/users?`$select=$fields,signInActivity&`$top=999" } catch { $hasSignIn = $false; Add-TcCollectorWarning 'Last sign-ins (needs an Entra ID P1 license and AuditLog.Read.All)' $_; Get-TcGraphList "v1.0/users?`$select=$fields&`$top=999" } Add-TcTable 'users' @(foreach ($x in $u) { $signIn = Get-TcProperty $x 'signInActivity' $dates = @((Get-TcProperty $signIn 'lastSignInDateTime'), (Get-TcProperty $signIn 'lastNonInteractiveSignInDateTime'), (Get-TcProperty $signIn 'lastSuccessfulSignInDateTime')) | Where-Object { $_ } $days = @($dates | ForEach-Object { Get-TcDaysSince $_ } | Where-Object { $null -ne $_ } | Sort-Object | Select-Object -First 1) $licenses = @((Get-TcProperty $x 'assignedLicenses') | ForEach-Object { $k = [string](Get-TcProperty $_ 'skuId'); if ($skus[$k]) { $skus[$k] } else { $k } }) [ordered]@{ 'Id' = (Get-TcProperty $x 'id') 'UPN' = (Get-TcProperty $x 'userPrincipalName') 'Name' = (Get-TcProperty $x 'displayName') 'Type' = (Get-TcProperty $x 'userType') 'Enabled' = [bool](Get-TcProperty $x 'accountEnabled') 'Created' = (Format-TcValue (Get-TcProperty $x 'createdDateTime')) 'Days since creation' = (Get-TcDaysSince (Get-TcProperty $x 'createdDateTime')) 'Last sign-in' = $(if ($dates) { Format-TcValue ($dates | Sort-Object -Descending | Select-Object -First 1) } else { $null }) 'Days inactive' = $(if (-not $hasSignIn) { $null } elseif ($days.Count) { $days[0] } else { -1 }) 'Licenses' = ($licenses -join ', ') 'License count' = $licenses.Count 'Synced' = [bool](Get-TcProperty $x 'onPremisesSyncEnabled') 'Email' = (Get-TcProperty $x 'mail') 'Department' = (Get-TcProperty $x 'department') 'Job title' = (Get-TcProperty $x 'jobTitle') 'Company' = (Get-TcProperty $x 'companyName') 'Country' = (Get-TcProperty $x 'usageLocation') 'Invitation state' = (Get-TcProperty $x 'externalUserState') 'Creation type' = (Get-TcProperty $x 'creationType') 'License errors' = @((Get-TcProperty $x 'licenseAssignmentStates') | Where-Object { (Get-TcProperty $_ 'state') -eq 'Error' }).Count 'Address count' = @((Get-TcProperty $x 'proxyAddresses')).Count 'First name' = (Get-TcProperty $x 'givenName') 'Last name' = (Get-TcProperty $x 'surname') 'Employee ID' = (Get-TcProperty $x 'employeeId') 'AD account' = (Get-TcProperty $x 'onPremisesSamAccountName') 'AD anchor' = [bool](Get-TcProperty $x 'onPremisesImmutableId') 'Sync errors' = @((Get-TcProperty $x 'onPremisesProvisioningErrors')).Count 'Mobile' = (Get-TcProperty $x 'mobilePhone') 'Phone' = (Format-TcValue (Get-TcProperty $x 'businessPhones')) 'Office' = (Get-TcProperty $x 'officeLocation') 'City' = (Get-TcProperty $x 'city') 'Addresses' = (Format-TcValue (@((Get-TcProperty $x 'proxyAddresses') | Where-Object { $_ -match '^smtp:' }) | ForEach-Object { $_ -replace '^smtp:', '' })) } }) if (-not $hasSignIn) { Add-TcCollectorNote 'Last sign-ins are not available: ''Days inactive'' is empty.' } } } @{ Id = 'ENT-04' Workload = 'Entra ID' Title = 'Groups' Service = 'Graph' Collect = { $fields = 'id,displayName,mail,mailNickname,groupTypes,securityEnabled,mailEnabled,membershipRule,visibility,resourceProvisioningOptions,createdDateTime,onPremisesSyncEnabled,assignedLicenses' $hasOwners = $true $g = try { Get-TcGraphList "v1.0/groups?`$select=$fields&`$expand=owners(`$select=id)&`$top=100" } catch { $hasOwners = $false; Add-TcCollectorWarning 'Group owners' $_; Get-TcGraphList "v1.0/groups?`$select=$fields&`$top=999" } Add-TcTable 'groups' @(foreach ($x in $g) { $type = if (@((Get-TcProperty $x 'groupTypes')) -contains 'Unified') { 'Microsoft 365' } elseif ((Get-TcProperty $x 'mailEnabled') -and (Get-TcProperty $x 'securityEnabled')) { 'Mail-enabled security' } elseif ((Get-TcProperty $x 'mailEnabled')) { 'Distribution list' } else { 'Security' } [ordered]@{ 'Id' = (Get-TcProperty $x 'id') 'Name' = (Get-TcProperty $x 'displayName') 'Type' = $type 'Team' = (@((Get-TcProperty $x 'resourceProvisioningOptions')) -contains 'Team') 'Dynamic' = [bool](Get-TcProperty $x 'membershipRule') 'Visibility' = (Get-TcProperty $x 'visibility') 'Synced' = [bool](Get-TcProperty $x 'onPremisesSyncEnabled') 'Group licenses' = @((Get-TcProperty $x 'assignedLicenses')).Count 'Owners' = $(if ($hasOwners) { @((Get-TcProperty $x 'owners')).Count } else { $null }) 'Email' = (Get-TcProperty $x 'mail') 'Alias' = (Get-TcProperty $x 'mailNickname') 'Created' = (Format-TcValue (Get-TcProperty $x 'createdDateTime')) 'Members' = $null } }) if ($script:TcDepth -eq 'Complete') { $n = 0 foreach ($l in @(Limit-TcCollectorRow 'groups' @(Get-TcTable 'groups') ([int](Get-TcCollectionSetting 'maxGroupMemberCounts')))) { try { $l['Members'] = [int](Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups/$(Get-TcProperty $l 'Id')/members/`$count" -Headers @{ ConsistencyLevel = 'eventual' } -ErrorAction Stop) } catch { $n++; if ($n -le 3) { Add-TcCollectorWarning "Member count of $(Get-TcProperty $l 'Name')" $_ } } } if ($n) { Add-TcCollectorNote "Member count not read for $n groups." } } Invoke-TcCollectorPart 'Group settings' { foreach ($s in @((Get-TcProperty (Get-TcGraphObject 'v1.0/groupSettings') 'value'))) { Add-TcTable 'groupSettings' -Append @(foreach ($v in @($s.values)) { [ordered]@{ 'Setting' = "$(Get-TcProperty $s 'displayName').$(Get-TcProperty $v 'name')"; 'Value' = (Get-TcProperty $v 'value') } }) } foreach ($p in @((Get-TcProperty (Get-TcGraphObject 'v1.0/groupLifecyclePolicies') 'value'))) { Add-TcTable 'groupSettings' -Append @(ConvertTo-TcSettingRow $p 'Expiration') } if (-not @(Get-TcTable 'groupSettings').Count) { Add-TcTable 'groupSettings' @([ordered]@{ 'Setting' = 'Group.Unified'; 'Value' = 'Default settings (anyone can create groups)' }) } } } } @{ Id = 'ENT-05' Workload = 'Entra ID' Title = 'Admin roles (active and eligible)' Service = 'Graph' Collect = { $roleNames = @{} foreach ($d in Get-TcGraphList "v1.0/roleManagement/directory/roleDefinitions?`$select=id,displayName") { $roleNames[[string](Get-TcProperty $d 'id')] = (Get-TcProperty $d 'displayName') } $buildRow = { param($a, $type) $p = (Get-TcProperty $a 'principal'); [ordered]@{ 'Role' = $roleNames[[string](Get-TcProperty $a 'roleDefinitionId')] 'Principal' = $(if ((Get-TcProperty $p 'userPrincipalName')) { (Get-TcProperty $p 'userPrincipalName') } else { (Get-TcProperty $p 'displayName') }) 'Principal type' = ([string](Get-TcProperty $p '@odata.type') -replace '#microsoft\.graph\.', '') 'Assignment' = $type 'Scope' = (Get-TcProperty $a 'directoryScopeId') } } Add-TcTable 'adminRoles' @(foreach ($a in Get-TcGraphList "v1.0/roleManagement/directory/roleAssignments?`$expand=principal") { & $buildRow $a 'Active' }) Invoke-TcCollectorPart 'Eligible roles (PIM, Entra ID P2 license)' { Add-TcTable 'adminRoles' -Append @(foreach ($a in Get-TcGraphList "v1.0/roleManagement/directory/roleEligibilitySchedules?`$expand=principal") { & $buildRow $a 'Eligible' }) } } } @{ Id = 'ENT-06' Workload = 'Entra ID' Title = 'Conditional Access, authentication and authorization' Service = 'Graph' Collect = { Invoke-TcCollectorPart 'Conditional Access policies (Entra ID P1 license)' { Add-TcTable 'conditionalAccess' @(foreach ($p in @((Get-TcProperty (Get-TcGraphObject 'v1.0/identity/conditionalAccess/policies') 'value'))) { $c = (Get-TcProperty $p 'conditions'); $u = (Get-TcProperty $c 'users'); $grantControls = (Get-TcProperty $p 'grantControls') $controls = @((Get-TcProperty $grantControls 'builtInControls')) + @($(if ((Get-TcProperty $grantControls 'authenticationStrength')) { "strength:$(Get-TcProperty (Get-TcProperty $grantControls 'authenticationStrength') 'displayName')" })) [ordered]@{ 'Name' = (Get-TcProperty $p 'displayName') 'State' = (Get-TcProperty $p 'state') 'Users' = (Format-TcValue (@((Get-TcProperty $u 'includeUsers')) + @(@((Get-TcProperty $u 'includeGroups')) | ForEach-Object { "group:$_" }) + @(@((Get-TcProperty $u 'includeRoles')) | ForEach-Object { "role:$_" }) + @($(if ((Get-TcProperty $u 'includeGuestsOrExternalUsers')) { 'guests' })))) 'Exclusions' = (@((Get-TcProperty $u 'excludeUsers')).Count + @((Get-TcProperty $u 'excludeGroups')).Count + @((Get-TcProperty $u 'excludeRoles')).Count) 'Applications' = (Format-TcValue (Get-TcProperty (Get-TcProperty $c 'applications') 'includeApplications')) 'Client apps' = (Format-TcValue (Get-TcProperty $c 'clientAppTypes')) 'Platforms' = (Format-TcValue (Get-TcProperty (Get-TcProperty $c 'platforms') 'includePlatforms')) 'Locations' = (Format-TcValue (Get-TcProperty (Get-TcProperty $c 'locations') 'includeLocations')) 'Controls' = ("{0} ({1})" -f ($controls -join ', '), (Get-TcProperty $grantControls 'operator')) 'Session' = (Format-TcValue (Get-TcProperty $p 'sessionControls')) 'MFA for all' = ((Get-TcProperty $p 'state') -eq 'enabled' -and @((Get-TcProperty $u 'includeUsers')) -contains 'All' -and (@((Get-TcProperty $grantControls 'builtInControls')) -contains 'mfa' -or [bool](Get-TcProperty $grantControls 'authenticationStrength'))) 'Blocks legacy authentication' = ((Get-TcProperty $p 'state') -eq 'enabled' -and @((Get-TcProperty $grantControls 'builtInControls')) -contains 'block' -and (@((Get-TcProperty $c 'clientAppTypes')) -contains 'exchangeActiveSync' -or @((Get-TcProperty $c 'clientAppTypes')) -contains 'other')) } }) Add-TcTable 'namedLocations' @(foreach ($l in @((Get-TcProperty (Get-TcGraphObject 'v1.0/identity/conditionalAccess/namedLocations') 'value'))) { [ordered]@{ 'Name' = (Get-TcProperty $l 'displayName') 'Type' = ([string](Get-TcProperty $l '@odata.type') -replace '#microsoft\.graph\.', '') 'Trusted' = [bool](Get-TcProperty $l 'isTrusted') 'Ranges or countries' = (Format-TcValue $(if ((Get-TcProperty $l 'ipRanges')) { @((Get-TcProperty $l 'ipRanges') | ForEach-Object { (Get-TcProperty $_ 'cidrAddress') }) } else { (Get-TcProperty $l 'countriesAndRegions') })) } }) } Invoke-TcCollectorPart 'Security defaults' { $d = Get-TcGraphObject 'v1.0/policies/identitySecurityDefaultsEnforcementPolicy'; Add-TcTable 'securitySettings' -Append @([ordered]@{ 'Setting' = 'Security defaults enabled'; 'Value' = [bool](Get-TcProperty $d 'isEnabled') }) } Invoke-TcCollectorPart 'Authorization policy' { $a = Get-TcGraphObject 'v1.0/policies/authorizationPolicy' $guestAccess = @{ 'a0b1b346-4d3e-4e8b-98f8-753987be4970' = 'Same as members'; '10dae51f-b6af-4016-8d66-8c2a99b929b3' = 'Limited (default)'; '2af84b1e-32c8-42b7-82bc-daa82404023b' = 'Restricted' } Add-TcTable 'securitySettings' -Append @( [ordered]@{ 'Setting' = 'Invitations allowed from'; 'Value' = (Get-TcProperty $a 'allowInvitesFrom') }, [ordered]@{ 'Setting' = 'Guest directory access'; 'Value' = $(if ($guestAccess[[string](Get-TcProperty $a 'guestUserRoleId')]) { $guestAccess[[string](Get-TcProperty $a 'guestUserRoleId')] } else { (Get-TcProperty $a 'guestUserRoleId') }) }, [ordered]@{ 'Setting' = 'Email-based self-service sign-up'; 'Value' = [bool](Get-TcProperty $a 'allowedToSignUpEmailBasedSubscriptions') }, [ordered]@{ 'Setting' = 'Email-verified users can join'; 'Value' = [bool](Get-TcProperty $a 'allowEmailVerifiedUsersToJoinOrganization') }) Add-TcTable 'securitySettings' -Append @(ConvertTo-TcSettingRow (Get-TcProperty $a 'defaultUserRolePermissions') 'User permissions') } Invoke-TcCollectorPart 'Authentication methods' { $m = Get-TcGraphObject 'v1.0/policies/authenticationMethodsPolicy' Add-TcTable 'securitySettings' -Append @(foreach ($x in @((Get-TcProperty $m 'authenticationMethodConfigurations'))) { [ordered]@{ 'Setting' = "Method $(Get-TcProperty $x 'id')"; 'Value' = (Get-TcProperty $x 'state') } }) } } } @{ Id = 'ENT-07' Workload = 'Entra ID' Title = 'Cross-tenant access, B2B and multitenant organization' Service = 'Graph' Collect = { Add-TcTable 'crossTenantSettings' @(ConvertTo-TcSettingRow (Get-TcGraphObject 'v1.0/policies/crossTenantAccessPolicy/default') 'Default') $partners = try { Get-TcGraphList "v1.0/policies/crossTenantAccessPolicy/partners?`$expand=identitySynchronization" } catch { Add-TcCollectorWarning 'Identity synchronization of the partners' $_; Get-TcGraphList 'v1.0/policies/crossTenantAccessPolicy/partners' } Add-TcTable 'crossTenantPartners' @(foreach ($p in $partners) { $displayName = $null; $defaultDomain = $null try { $i = Get-TcGraphObject ("v1.0/tenantRelationships/findTenantInformationByTenantId(tenantId='{0}')" -f (Get-TcProperty $p 'tenantId')); $displayName = (Get-TcProperty $i 'displayName'); $defaultDomain = (Get-TcProperty $i 'defaultDomainName') } catch { Add-TcCollectorWarning "Name of partner $(Get-TcProperty $p 'tenantId')" $_ } [ordered]@{ 'Tenant' = (Get-TcProperty $p 'tenantId') 'Name' = $displayName 'Domain' = $defaultDomain 'MFA trusted' = (Get-TcProperty (Get-TcProperty $p 'inboundTrust') 'isMfaAccepted') 'Compliant device trusted' = (Get-TcProperty (Get-TcProperty $p 'inboundTrust') 'isCompliantDeviceAccepted') 'Inbound collaboration' = (Get-TcProperty (Get-TcProperty (Get-TcProperty $p 'b2bCollaborationInbound') 'usersAndGroups') 'accessType') 'Outbound collaboration' = (Get-TcProperty (Get-TcProperty (Get-TcProperty $p 'b2bCollaborationOutbound') 'usersAndGroups') 'accessType') 'Inbound direct connect' = (Get-TcProperty (Get-TcProperty (Get-TcProperty $p 'b2bDirectConnectInbound') 'usersAndGroups') 'accessType') 'Outbound direct connect' = (Get-TcProperty (Get-TcProperty (Get-TcProperty $p 'b2bDirectConnectOutbound') 'usersAndGroups') 'accessType') 'Inbound auto-redeem' = (Get-TcProperty (Get-TcProperty $p 'automaticUserConsentSettings') 'inboundAllowed') 'Outbound auto-redeem' = (Get-TcProperty (Get-TcProperty $p 'automaticUserConsentSettings') 'outboundAllowed') 'Inbound sync' = (Get-TcProperty (Get-TcProperty (Get-TcProperty $p 'identitySynchronization') 'userSyncInbound') 'isSyncAllowed') } }) Invoke-TcCollectorPart 'Multitenant organization' { $m = Get-TcGraphObject 'v1.0/tenantRelationships/multiTenantOrganization' Add-TcTable 'crossTenantSettings' -Append @([ordered]@{ 'Setting' = 'Multitenant organization'; 'Value' = ('{0} ({1})' -f (Get-TcProperty $m 'displayName'), (Get-TcProperty $m 'state')) }) } } } @{ Id = 'ENT-08' Workload = 'Entra ID' Title = 'Applications, service principals and consents' Service = 'Graph' Collect = { param($Role) $tenantId = (Get-TcProperty $script:TcTenants[$Role] 'tenantId') Add-TcTable 'applications' @(foreach ($a in Get-TcGraphList "v1.0/applications?`$select=id,appId,displayName,createdDateTime,signInAudience,passwordCredentials,keyCredentials&`$top=999") { $expiries = @(@((Get-TcProperty $a 'passwordCredentials')) + @((Get-TcProperty $a 'keyCredentials')) | ForEach-Object { Get-TcDaysSince (Get-TcProperty $_ 'endDateTime') } | Where-Object { $null -ne $_ }) [ordered]@{ 'Name' = (Get-TcProperty $a 'displayName') 'AppId' = (Get-TcProperty $a 'appId') 'Created' = (Format-TcValue (Get-TcProperty $a 'createdDateTime')) 'Audience' = (Get-TcProperty $a 'signInAudience') 'Secrets' = @((Get-TcProperty $a 'passwordCredentials')).Count 'Certificates' = @((Get-TcProperty $a 'keyCredentials')).Count 'Days to expiry' = $(if (@($expiries | Where-Object { $_ -le 0 }).Count) { -1 * (Get-TcProperty (@($expiries | Where-Object { $_ -le 0 }) | Measure-Object -Maximum) 'Maximum') } else { $null }) 'Expired' = @($expiries | Where-Object { $_ -gt 0 }).Count } }) $principals = Get-TcGraphList "v1.0/servicePrincipals?`$select=id,appId,displayName,servicePrincipalType,appOwnerOrganizationId,accountEnabled,publisherName&`$top=999" $principalNames = @{}; foreach ($s in $principals) { $principalNames[[string](Get-TcProperty $s 'id')] = (Get-TcProperty $s 'displayName') } Add-TcTable 'servicePrincipals' @(foreach ($s in $principals | Where-Object { (Get-TcProperty $_ 'servicePrincipalType') -ne 'ManagedIdentity' }) { $owner = [string](Get-TcProperty $s 'appOwnerOrganizationId') $origin = if ($owner -in $script:TcMicrosoftTenantIds) { 'Microsoft' } elseif ($owner -eq $tenantId) { 'Internal' } else { 'Third party' } [ordered]@{ 'Name' = (Get-TcProperty $s 'displayName') 'AppId' = (Get-TcProperty $s 'appId') 'Publisher' = (Get-TcProperty $s 'publisherName') 'Origin' = $origin 'Enabled' = [bool](Get-TcProperty $s 'accountEnabled') 'Type' = (Get-TcProperty $s 'servicePrincipalType') 'Id' = (Get-TcProperty $s 'id') } }) $upn = @{}; foreach ($u in Get-TcTable 'users') { $upn[[string](Get-TcProperty $u 'Id')] = (Get-TcProperty $u 'UPN') } Invoke-TcCollectorPart 'Delegated consents' { Add-TcTable 'delegatedConsents' @(foreach ($g in Get-TcGraphList "v1.0/oauth2PermissionGrants?`$top=999") { [ordered]@{ 'Application' = $principalNames[[string](Get-TcProperty $g 'clientId')] 'Consent type' = $(if ((Get-TcProperty $g 'consentType') -eq 'AllPrincipals') { 'Administrator (all)' } else { 'User' }) 'Scopes' = ([string](Get-TcProperty $g 'scope')).Trim() 'User' = $(if ((Get-TcProperty $g 'principalId')) { $upn[[string](Get-TcProperty $g 'principalId')] }) } }) } } } @{ Id = 'ENT-09' Workload = 'Entra ID' Title = 'Devices registered in Entra ID' Service = 'Graph' Collect = { Add-TcTable 'devices' @(foreach ($d in Get-TcGraphList "v1.0/devices?`$select=displayName,operatingSystem,operatingSystemVersion,trustType,approximateLastSignInDateTime,isCompliant,isManaged,accountEnabled,registrationDateTime&`$top=999") { [ordered]@{ 'Name' = (Get-TcProperty $d 'displayName') 'OS' = (Get-TcProperty $d 'operatingSystem') 'Version' = (Get-TcProperty $d 'operatingSystemVersion') 'Join type' = @{ AzureAd = 'Entra ID'; ServerAd = 'Hybrid'; Workplace = 'Registered' }[[string](Get-TcProperty $d 'trustType')] 'Compliant' = (Get-TcProperty $d 'isCompliant') 'Managed' = (Get-TcProperty $d 'isManaged') 'Enabled' = [bool](Get-TcProperty $d 'accountEnabled') 'Last sign-in' = (Format-TcValue (Get-TcProperty $d 'approximateLastSignInDateTime')) 'Days inactive' = (Get-TcDaysSince (Get-TcProperty $d 'approximateLastSignInDateTime')) } }) } } @{ Id = 'ENT-10' Workload = 'Entra ID' Title = 'MFA registration and methods' Service = 'Graph' Collect = { Add-TcTable 'mfaRegistrations' @(foreach ($r in Get-TcGraphList "v1.0/reports/authenticationMethods/userRegistrationDetails?`$top=999") { [ordered]@{ 'UPN' = (Get-TcProperty $r 'userPrincipalName') 'Type' = (Get-TcProperty $r 'userType') 'MFA registered' = [bool](Get-TcProperty $r 'isMfaRegistered') 'MFA capable' = [bool](Get-TcProperty $r 'isMfaCapable') 'Passwordless' = [bool](Get-TcProperty $r 'isPasswordlessCapable') 'Methods' = (Format-TcValue (Get-TcProperty $r 'methodsRegistered')) 'Default method' = (Get-TcProperty $r 'defaultMfaMethod') 'Admin' = [bool](Get-TcProperty $r 'isAdmin') } }) } } @{ Id = 'ENT-11' Workload = 'Entra ID' Title = 'Recent sign-ins: client apps, legacy authentication, countries' Service = 'Graph' Collect = { $days = [int](Get-TcCollectionSetting 'signInDays'); $pageSize = 999; $maxPages = [int](Get-TcCollectionSetting 'maxSignInPages') $buildUri = { "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=createdDateTime ge {0}&`$top={1}" -f (Get-Date).ToUniversalTime().AddDays(- $days).ToString('yyyy-MM-ddTHH:mm:ssZ'), $pageSize } $uri = & $buildUri $aggregates = @{}; $legacy = @{}; $pages = 0; $total = 0; $reduced = $false; $truncated = $false $modernClients = '^(Browser|Mobile Apps and Desktop clients)$' while ($uri -and $pages -lt $maxPages) { $r = $null for ($attempt = 1; $attempt -le 4 -and $null -eq $r; $attempt++) { try { $r = Invoke-MgGraphRequest -Method GET -Uri $uri -OutputType Hashtable -ErrorAction Stop } catch { $t = Get-TcErrorText $_ if ($attempt -ge 4 -or $t -notmatch 'Timeout|timed out|canceled|429|503|504|TooManyRequests') { if ($pages) { break }; throw } if ($pages -eq 0 -and $t -match 'Timeout|timed out|canceled|504') { # A large log: the first page exceeds the 300 second limit of the Graph client. $pageSize = 100; $days = [math]::Max(1, [math]::Floor($days / 3)); $maxPages *= 5; $uri = & $buildUri; $reduced = $true } else { Start-Sleep -Seconds (10 * $attempt) } } } if ($null -eq $r) { $truncated = $true; break } foreach ($s in @((Get-TcProperty $r 'value'))) { $total++ $outcome = if ([int](Get-TcProperty (Get-TcProperty $s 'status') 'errorCode') -eq 0) { 'Success' } else { 'Failure' } foreach ($dimension in @(@('Client app', [string](Get-TcProperty $s 'clientAppUsed')), @('Application', [string](Get-TcProperty $s 'appDisplayName')), @('Country', [string](Get-TcProperty (Get-TcProperty $s 'location') 'countryOrRegion')), @('Result', $outcome))) { $k = "$($dimension[0])|$($dimension[1])" if (-not $aggregates.ContainsKey($k)) { $aggregates[$k] = @{ n = 0; u = @{} } } $aggregates[$k]['n']++; $aggregates[$k]['u'][[string](Get-TcProperty $s 'userPrincipalName')] = 1 } if ([string](Get-TcProperty $s 'clientAppUsed') -and [string](Get-TcProperty $s 'clientAppUsed') -notmatch $modernClients) { $k = "$(Get-TcProperty $s 'userPrincipalName')|$(Get-TcProperty $s 'clientAppUsed')" if (-not $legacy.ContainsKey($k)) { $legacy[$k] = @{ upn = (Get-TcProperty $s 'userPrincipalName'); client = (Get-TcProperty $s 'clientAppUsed'); n = 0; d = (Get-TcProperty $s 'createdDateTime') } } $legacy[$k]['n']++ } } $uri = (Get-TcProperty $r '@odata.nextLink'); $pages++ } if ($reduced) { Add-TcCollectorNote ('Large sign-in log (the 300 s limit was exceeded): read over {0} days, in pages of 100.' -f $days) } if ($truncated) { Add-TcCollectorLimit ('reading stopped after {0} pages (timeouts): sample of {1} sign-ins.' -f $pages, $total) } elseif ($uri) { Add-TcCollectorLimit ('sample of {0} sign-ins from the last {1} days ({2} pages).' -f $total, $days, $maxPages) } Add-TcTable 'signInSummary' @(foreach ($k in $aggregates.Keys | Sort-Object) { $p = $k -split '\|', 2; [ordered]@{ 'Dimension' = $p[0] 'Value' = $(if ($p[1]) { $p[1] } else { '(empty)' }) 'Sign-ins' = (Get-TcProperty $aggregates[$k] 'n') 'Users' = (Get-TcProperty $aggregates[$k] 'u').Count } }) Add-TcTable 'legacySignIns' @(foreach ($h in $legacy.Values) { [ordered]@{ 'UPN' = (Get-TcProperty $h 'upn') 'Client' = (Get-TcProperty $h 'client') 'Sign-ins' = (Get-TcProperty $h 'n') 'Last' = (Format-TcValue (Get-TcProperty $h 'd')) } }) } } @{ Id = 'ENT-12' Workload = 'Entra ID' Title = 'Application permissions granted to third-party and internal apps' Service = 'Graph' Requires = @('servicePrincipals') Collect = { $principals = @(Limit-TcCollectorRow 'third-party and internal applications' @(Get-TcTable 'servicePrincipals' | Where-Object { (Get-TcProperty $_ 'Origin') -in 'Third party', 'Internal' -and (Get-TcProperty $_ 'Id') }) ([int](Get-TcCollectionSetting 'maxPermissionApps'))) $roles = @{} Add-TcTable 'applicationPermissions' @(foreach ($sp in $principals) { try { foreach ($a in Get-TcGraphList "v1.0/servicePrincipals/$(Get-TcProperty $sp 'Id')/appRoleAssignments") { $resourceId = [string](Get-TcProperty $a 'resourceId') if (-not $roles.ContainsKey($resourceId)) { $roles[$resourceId] = @{}; try { foreach ($x in @((Get-TcProperty (Get-TcGraphObject "v1.0/servicePrincipals/$resourceId`?`$select=appRoles") 'appRoles'))) { $roles[$resourceId][[string](Get-TcProperty $x 'id')] = (Get-TcProperty $x 'value') } } catch { Add-TcCollectorWarning "Permission names of $(Get-TcProperty $a 'resourceDisplayName')" $_ } } [ordered]@{ 'Application' = (Get-TcProperty $sp 'Name') 'Origin' = (Get-TcProperty $sp 'Origin') 'Resource' = (Get-TcProperty $a 'resourceDisplayName') 'Permission' = $(if ($roles[$resourceId][[string](Get-TcProperty $a 'appRoleId')]) { $roles[$resourceId][[string](Get-TcProperty $a 'appRoleId')] } else { [string](Get-TcProperty $a 'appRoleId') }) 'Granted on' = (Format-TcValue (Get-TcProperty $a 'createdDateTime')) } } } catch { Add-TcCollectorWarning "Permissions of $(Get-TcProperty $sp 'Name')" $_ } }) } } @{ Id = 'ENT-13' Workload = 'Entra ID' Title = 'Risky users (Identity Protection)' Service = 'Graph' Collect = { Add-TcTable 'riskyUsers' @(foreach ($u in Get-TcGraphList "v1.0/identityProtection/riskyUsers?`$filter=riskState eq 'atRisk' or riskState eq 'confirmedCompromised'") { [ordered]@{ 'UPN' = (Get-TcProperty $u 'userPrincipalName') 'Level' = (Get-TcProperty $u 'riskLevel') 'State' = (Get-TcProperty $u 'riskState') 'Detail' = (Get-TcProperty $u 'riskDetail') 'Updated' = (Format-TcValue (Get-TcProperty $u 'riskLastUpdatedDateTime')) } }) } } ) } |