@@ -143,6 +143,97 @@ return @"
143143 }
144144 }
145145
146+ function Get-DurationSeconds {
147+ param (
148+ [string ]$Start ,
149+ [string ]$End
150+ )
151+
152+ if ([string ]::IsNullOrWhiteSpace($Start ) -or [string ]::IsNullOrWhiteSpace($End )) { return $null }
153+
154+ try {
155+ $culture = [System.Globalization.CultureInfo ]::InvariantCulture
156+ $styles = [System.Globalization.DateTimeStyles ]::AssumeLocal
157+ $startTime = [datetime ]::ParseExact($Start , " yyyyMMdd HH:mm" , $culture , $styles )
158+ $endTime = [datetime ]::ParseExact($End , " yyyyMMdd HH:mm" , $culture , $styles )
159+ $duration = $endTime - $startTime
160+ if ($duration.TotalSeconds -lt 0 ) { return $null }
161+ return [int ][math ]::Round($duration.TotalSeconds )
162+ } catch {
163+ return $null
164+ }
165+ }
166+
167+ function Get-IsoTimestamp {
168+ param (
169+ [string ]$Value
170+ )
171+
172+ if ([string ]::IsNullOrWhiteSpace($Value )) { return $null }
173+
174+ try {
175+ $culture = [System.Globalization.CultureInfo ]::InvariantCulture
176+ $styles = [System.Globalization.DateTimeStyles ]::AssumeLocal
177+ $parsedTime = [datetime ]::ParseExact($Value , " yyyyMMdd HH:mm" , $culture , $styles )
178+ return $parsedTime.ToString (" o" )
179+ } catch {
180+ return $null
181+ }
182+ }
183+
184+ function Get-DomainUserCountLookup {
185+ param (
186+ [hashtable ]$Users = @ {}
187+ )
188+
189+ $domainUserCount = @ {}
190+ foreach ($userObj in $Users.Values ) {
191+ if ($userObj.UPN ) {
192+ $at = $userObj.UPN.IndexOf (' @' )
193+ if ($at -ge 0 ) {
194+ $upnDomain = $userObj.UPN.Substring ($at + 1 ).ToLowerInvariant()
195+ if ($domainUserCount.ContainsKey ($upnDomain )) {
196+ $domainUserCount [$upnDomain ]++
197+ } else {
198+ $domainUserCount [$upnDomain ] = 1
199+ }
200+ }
201+ }
202+ }
203+
204+ return $domainUserCount
205+ }
206+
207+ function Get-CoverageWarnings {
208+ param (
209+ [bool ]$SubscriptionCountIncomplete
210+ )
211+
212+ $warnings = New-Object System.Collections.Generic.List[string ]
213+
214+ if (-not [bool ]$GLOBALAzurePsChecks ) {
215+ if ($GlobalAuditSummary.ManagedIdentities.Count -ge 1 ) {
216+ $warnings.Add (" Azure IAM not collected: no subscription visible or accessible, but managed identities exist." )
217+ } else {
218+ $warnings.Add (" Azure IAM not collected: no subscriptions exist or access is unavailable." )
219+ }
220+ }
221+ if (-not [bool ]$GLOBALGraphExtendedChecks ) {
222+ $warnings.Add (" PIM role data not collected." )
223+ }
224+ if (-not [bool ]$GLOBALPimForGroupsChecked ) {
225+ $warnings.Add (" PIM group data not collected." )
226+ }
227+ if (-not [bool ]$GlobalAuditSummary.EnterpriseApps.IncludeMsApps ) {
228+ $warnings.Add (" Default Microsoft enterprise applications not collected." )
229+ }
230+ if ($SubscriptionCountIncomplete ) {
231+ $warnings.Add (" Subscription count is incomplete because Azure subscription access was unavailable." )
232+ }
233+
234+ return @ ($warnings )
235+ }
236+
146237 function New-GeneralSection {
147238 param (
148239 [string ]$TenantName ,
@@ -208,21 +299,7 @@ return @"
208299
209300 if (-not $Domains -or $Domains.Count -eq 0 ) { return " " }
210301
211- # Build domain -> user count lookup (single pass, no regex)
212- $domainUserCount = @ {}
213- foreach ($userObj in $Users.Values ) {
214- if ($userObj.UPN ) {
215- $at = $userObj.UPN.IndexOf (' @' )
216- if ($at -ge 0 ) {
217- $upnDomain = $userObj.UPN.Substring ($at + 1 ).ToLower()
218- if ($domainUserCount.ContainsKey ($upnDomain )) {
219- $domainUserCount [$upnDomain ]++
220- } else {
221- $domainUserCount [$upnDomain ] = 1
222- }
223- }
224- }
225- }
302+ $domainUserCount = Get-DomainUserCountLookup - Users $Users
226303 $escapedTenantName = [System.Uri ]::EscapeDataString($CurrentTenant.DisplayName )
227304 $userReportBase = " Users_$ ( $StartTimestamp ) _$ ( $escapedTenantName ) .html"
228305
@@ -375,15 +452,29 @@ return @"
375452 " False (default)"
376453 }
377454
455+ $subscriptionCountNumeric = [int ]$GlobalAuditSummary.Subscriptions.Count
456+ $subscriptionCollected = [bool ]$GLOBALAzurePsChecks
457+ $subscriptionCountIncomplete = (-not $subscriptionCollected -and $subscriptionCountNumeric -eq 0 -and $GlobalAuditSummary.ManagedIdentities.Count -ge 1 )
458+ $subscriptionReason = $null
459+ if ($subscriptionCountNumeric -eq 0 ) {
460+ if ($subscriptionCountIncomplete ) {
461+ $subscriptionReason = " no subscription visible or accessible, but managed identities exist"
462+ } elseif (-not $subscriptionCollected ) {
463+ $subscriptionReason = " no subscriptions or no access"
464+ } else {
465+ $subscriptionReason = " no subscriptions"
466+ }
467+ }
468+
378469 # Check whether there are subscriptions
379- if ($ ( $GlobalAuditSummary .Subscriptions.Count ) -eq 0 ) {
470+ if ($subscriptionCountNumeric -eq 0 ) {
380471 if ($ ($GlobalAuditSummary.ManagedIdentities.Count ) -ge 1 ) {
381472 $SubscriptionCount = " ? (no access - but there are managed identities!)"
382473 } else {
383474 $SubscriptionCount = " 0 (no subscriptions or no access)"
384475 }
385476 } else {
386- $SubscriptionCount = $ ( $GlobalAuditSummary .Subscriptions.Count )
477+ $SubscriptionCount = $subscriptionCountNumeric
387478 }
388479
389480 $securityFindingsSummary = $GlobalAuditSummary.SecurityFindings
@@ -395,6 +486,19 @@ return @"
395486 $hostOs = Get-EntraFalconHostOs
396487 $powerShellDisplay = " V$ ( $PSVersionTable.PSVersion.ToString ()) ($hostOs )"
397488 $durationDisplay = Get-DurationDisplay - Start $GlobalAuditSummary.Time.Start - End $GlobalAuditSummary.Time.End
489+ $durationSeconds = Get-DurationSeconds - Start $GlobalAuditSummary.Time.Start - End $GlobalAuditSummary.Time.End
490+ $executionStartIso = Get-IsoTimestamp - Value $GlobalAuditSummary.Time.Start
491+ $executionEndIso = Get-IsoTimestamp - Value $GlobalAuditSummary.Time.End
492+ $domainUserCount = Get-DomainUserCountLookup - Users $Users
493+ $defaultDomain = @ ($TenantDomains | Where-Object { $_.IsDefault } | Select-Object - First 1 - ExpandProperty Id)
494+ if ($defaultDomain.Count -eq 0 ) {
495+ $defaultDomain = $null
496+ } else {
497+ $defaultDomain = $defaultDomain [0 ]
498+ }
499+ $coverageWarnings = Get-CoverageWarnings - SubscriptionCountIncomplete $subscriptionCountIncomplete
500+ $summaryReportKey = " Summary"
501+ $summaryReportName = " EntraFalcon Enumeration Summary"
398502
399503 $azureIamBadge = if ([bool ]$GLOBALAzurePsChecks ) {
400504 New-GeneralStatusBadge - Text " Collected" - Tone " success"
@@ -1632,7 +1736,189 @@ Enumeration Results:
16321736"@
16331737
16341738 # Set generic information hich gets injected into the HTML
1635- Set-GlobalReportManifest - CurrentReportKey ' Summary' - CurrentReportName ' EntraFalcon Enumeration Summary'
1739+ Set-GlobalReportManifest - CurrentReportKey $summaryReportKey - CurrentReportName $summaryReportName
1740+
1741+ $summaryJson = [ordered ]@ {
1742+ schemaVersion = 1
1743+ generatedAt = (Get-Date ).ToString(" o" )
1744+ tenant = [ordered ]@ {
1745+ id = $GlobalAuditSummary.Tenant.ID
1746+ name = $GlobalAuditSummary.Tenant.Name
1747+ defaultDomain = $defaultDomain
1748+ license = [ordered ]@ {
1749+ name = $GlobalAuditSummary.TenantLicense.Name
1750+ level = $GlobalAuditSummary.TenantLicense.Level
1751+ }
1752+ }
1753+ execution = [ordered ]@ {
1754+ start = $executionStartIso
1755+ end = $executionEndIso
1756+ durationSeconds = $durationSeconds
1757+ entraFalconVersion = $GlobalAuditSummary.EntraFalcon.Version
1758+ powershellVersion = $PSVersionTable.PSVersion.ToString ()
1759+ hostOs = $hostOs
1760+ userAgent = $GlobalAuditSummary.UserAgent.Name
1761+ }
1762+ coverage = [ordered ]@ {
1763+ azureIamCollected = [bool ]$GLOBALAzurePsChecks
1764+ pimRoleDataCollected = [bool ]$GLOBALGraphExtendedChecks
1765+ pimGroupDataCollected = [bool ]$GLOBALPimForGroupsChecked
1766+ defaultMicrosoftSpCollected = [bool ]$GlobalAuditSummary.EnterpriseApps.IncludeMsApps
1767+ coverageWarnings = @ ($coverageWarnings )
1768+ }
1769+ counts = [ordered ]@ {
1770+ users = [ordered ]@ {
1771+ count = $GlobalAuditSummary.Users.Count
1772+ guests = $GlobalAuditSummary.Users.Guests
1773+ inactive = $GlobalAuditSummary.Users.Inactive
1774+ enabled = $GlobalAuditSummary.Users.Enabled
1775+ onPrem = $GlobalAuditSummary.Users.OnPrem
1776+ mfaCapable = $GlobalAuditSummary.Users.MfaCapable
1777+ }
1778+ groups = [ordered ]@ {
1779+ count = $GlobalAuditSummary.Groups.Count
1780+ m365 = $GlobalAuditSummary.Groups.M365
1781+ publicM365 = $GlobalAuditSummary.Groups.PublicM365
1782+ pimOnboarded = $GlobalAuditSummary.Groups.PimOnboarded
1783+ onPrem = $GlobalAuditSummary.Groups.OnPrem
1784+ }
1785+ appRegistrations = [ordered ]@ {
1786+ count = $GlobalAuditSummary.AppRegistrations.Count
1787+ appLock = $GlobalAuditSummary.AppRegistrations.AppLock
1788+ credentials = [ordered ]@ {
1789+ appsSecrets = $GlobalAuditSummary.AppRegistrations.Credentials.AppsSecrets
1790+ appsCerts = $GlobalAuditSummary.AppRegistrations.Credentials.AppsCerts
1791+ appsFederatedCreds = $GlobalAuditSummary.AppRegistrations.Credentials.AppsFederatedCreds
1792+ appsNoCreds = $GlobalAuditSummary.AppRegistrations.Credentials.AppsNoCreds
1793+ }
1794+ }
1795+ enterpriseApps = [ordered ]@ {
1796+ count = $GlobalAuditSummary.EnterpriseApps.Count
1797+ foreign = $GlobalAuditSummary.EnterpriseApps.Foreign
1798+ credentials = $GlobalAuditSummary.EnterpriseApps.Credentials
1799+ includeMsApps = $GlobalAuditSummary.EnterpriseApps.IncludeMsApps
1800+ }
1801+ managedIdentities = [ordered ]@ {
1802+ count = $GlobalAuditSummary.ManagedIdentities.Count
1803+ isExplicit = $GlobalAuditSummary.ManagedIdentities.IsExplicit
1804+ }
1805+ agentIdentities = [ordered ]@ {
1806+ count = $GlobalAuditSummary.AgentIdentities.Count
1807+ foreign = $GlobalAuditSummary.AgentIdentities.Foreign
1808+ inactive = $GlobalAuditSummary.AgentIdentities.Inactive
1809+ totalAgentUsers = $GlobalAuditSummary.AgentIdentities.TotalAgentUsers
1810+ }
1811+ agentIdentityBlueprintsPrincipals = [ordered ]@ {
1812+ count = $GlobalAuditSummary.AgentIdentityBlueprintsPrincipals.Count
1813+ foreign = $GlobalAuditSummary.AgentIdentityBlueprintsPrincipals.Foreign
1814+ }
1815+ agentIdentityBlueprints = [ordered ]@ {
1816+ count = $GlobalAuditSummary.AgentIdentityBlueprints.Count
1817+ credentials = [ordered ]@ {
1818+ secrets = $GlobalAuditSummary.AgentIdentityBlueprints.Credentials.Secrets
1819+ certificates = $GlobalAuditSummary.AgentIdentityBlueprints.Credentials.Certificates
1820+ federatedCredentials = $GlobalAuditSummary.AgentIdentityBlueprints.Credentials .' Federated Credentials'
1821+ none = $GlobalAuditSummary.AgentIdentityBlueprints.Credentials.None
1822+ }
1823+ }
1824+ administrativeUnits = [ordered ]@ {
1825+ count = $GlobalAuditSummary.AdministrativeUnits.Count
1826+ }
1827+ conditionalAccess = [ordered ]@ {
1828+ count = $GlobalAuditSummary.ConditionalAccess.Count
1829+ enabled = $GlobalAuditSummary.ConditionalAccess.Enabled
1830+ }
1831+ domains = [ordered ]@ {
1832+ count = $GlobalAuditSummary.Domains.Count
1833+ federated = $GlobalAuditSummary.Domains.Federated
1834+ verified = $GlobalAuditSummary.Domains.Verified
1835+ default = $GlobalAuditSummary.Domains.Default
1836+ adminManaged = $GlobalAuditSummary.Domains.AdminManaged
1837+ }
1838+ subscriptions = [ordered ]@ {
1839+ count = $subscriptionCountNumeric
1840+ collected = $subscriptionCollected
1841+ incomplete = $subscriptionCountIncomplete
1842+ reason = $subscriptionReason
1843+ }
1844+ entraRoleAssignments = [ordered ]@ {
1845+ count = $GlobalAuditSummary.EntraRoleAssignments.Count
1846+ eligible = $GlobalAuditSummary.EntraRoleAssignments.Eligible
1847+ builtIn = $GlobalAuditSummary.EntraRoleAssignments.BuiltIn
1848+ principalType = [ordered ]@ {
1849+ user = $GlobalAuditSummary.EntraRoleAssignments.PrincipalType.User
1850+ group = $GlobalAuditSummary.EntraRoleAssignments.PrincipalType.Group
1851+ app = $GlobalAuditSummary.EntraRoleAssignments.PrincipalType.App
1852+ mi = $GlobalAuditSummary.EntraRoleAssignments.PrincipalType.MI
1853+ agentIdentity = $GlobalAuditSummary.EntraRoleAssignments.PrincipalType.AgentIdentity
1854+ blueprintPrincipal = $GlobalAuditSummary.EntraRoleAssignments.PrincipalType.BlueprintPrincipal
1855+ unknown = $GlobalAuditSummary.EntraRoleAssignments.PrincipalType.Unknown
1856+ }
1857+ tiers = [ordered ]@ {
1858+ tier0 = $GlobalAuditSummary.EntraRoleAssignments.Tiers .' Tier-0'
1859+ tier1 = $GlobalAuditSummary.EntraRoleAssignments.Tiers .' Tier-1'
1860+ tier2 = $GlobalAuditSummary.EntraRoleAssignments.Tiers .' Tier-2'
1861+ uncategorized = $GlobalAuditSummary.EntraRoleAssignments.Tiers.Uncategorized
1862+ }
1863+ }
1864+ azureRoleAssignments = [ordered ]@ {
1865+ count = $GlobalAuditSummary.AzureRoleAssignments.Count
1866+ eligible = $GlobalAuditSummary.AzureRoleAssignments.Eligible
1867+ builtIn = $GlobalAuditSummary.AzureRoleAssignments.BuiltIn
1868+ principalType = [ordered ]@ {
1869+ user = $GlobalAuditSummary.AzureRoleAssignments.PrincipalType.User
1870+ group = $GlobalAuditSummary.AzureRoleAssignments.PrincipalType.Group
1871+ sp = $GlobalAuditSummary.AzureRoleAssignments.PrincipalType.SP
1872+ mi = $GlobalAuditSummary.AzureRoleAssignments.PrincipalType.MI
1873+ agentIdentity = $GlobalAuditSummary.AzureRoleAssignments.PrincipalType.AgentIdentity
1874+ blueprintPrincipal = $GlobalAuditSummary.AzureRoleAssignments.PrincipalType.BlueprintPrincipal
1875+ unknown = $GlobalAuditSummary.AzureRoleAssignments.PrincipalType.Unknown
1876+ }
1877+ tiers = [ordered ]@ {
1878+ tier0 = $GlobalAuditSummary.AzureRoleAssignments.Tiers .' Tier-0'
1879+ tier1 = $GlobalAuditSummary.AzureRoleAssignments.Tiers .' Tier-1'
1880+ tier2 = $GlobalAuditSummary.AzureRoleAssignments.Tiers .' Tier-2'
1881+ tier3 = $GlobalAuditSummary.AzureRoleAssignments.Tiers .' Tier-3'
1882+ uncategorized = $GlobalAuditSummary.AzureRoleAssignments.Tiers.Uncategorized
1883+ }
1884+ }
1885+ pimSettings = [ordered ]@ {
1886+ count = $GlobalAuditSummary.PimSettings.Count
1887+ }
1888+ securityFindings = [ordered ]@ {
1889+ vulnerable = $securityFindingsSummary.Vulnerable
1890+ notVulnerable = $securityFindingsSummary.NotVulnerable
1891+ skipped = $securityFindingsSummary.Skipped
1892+ total = $securityFindingsSummary.Total
1893+ }
1894+ }
1895+ domains = @ (
1896+ @ ($TenantDomains | ForEach-Object {
1897+ $domainKey = $_.Id.ToLowerInvariant ()
1898+ [ordered ]@ {
1899+ id = $_.Id
1900+ authenticationType = $_.AuthenticationType
1901+ isAdminManaged = $_.IsAdminManaged
1902+ isDefault = $_.IsDefault
1903+ isVerified = $_.IsVerified
1904+ supportedServices = @ ($_.SupportedServices )
1905+ federatedIdpMfaBehavior = if ([string ]::IsNullOrWhiteSpace($_.FederatedIdpMfaBehavior )) { $null } else { $_.FederatedIdpMfaBehavior }
1906+ userCount = if ($domainUserCount.ContainsKey ($domainKey )) { $domainUserCount [$domainKey ] } else { 0 }
1907+ }
1908+ })
1909+ )
1910+ subscriptionDetails = @ (
1911+ @ ($GlobalAuditSummary.Subscriptions.Details | ForEach-Object {
1912+ [ordered ]@ {
1913+ id = $_.Id
1914+ displayName = $_.DisplayName
1915+ state = $_.State
1916+ managedByTenants = $_.ManagedByTenants
1917+ resources = $_.Resources
1918+ }
1919+ })
1920+ )
1921+ }
16361922
16371923 # Build header section
16381924 $headerHTML = " <div id=`" loadingOverlay`" ><div class=`" spinner`" ></div><div class=`" loading-text`" >Loading data...</div></div>$generalSectionHtml "
@@ -1642,15 +1928,15 @@ Enumeration Results:
16421928 $CssCombined = " <title>EF - Summary</title>`n " + $GLOBALcss + $CustomCss + $global :GLOBALReportManifestScript
16431929 $Report = ConvertTo-HTML - Body " $headerHTML $kpiSectionHtml $mainTableRuntimeHtml " - Head $CssCombined - PostContent $PostContentCombined
16441930 $summaryHtmlPath = " $outputFolder \_EntraFalconEnumerationSummary_$ ( $StartTimestamp ) _$ ( $CurrentTenant.DisplayName ) .html"
1645- $summaryTxtPath = " $outputFolder \_EntraFalconEnumerationSummary_$ ( $StartTimestamp ) _$ ( $CurrentTenant.DisplayName ) .txt "
1931+ $summaryJsonPath = " $outputFolder \_EntraFalconEnumerationSummary_$ ( $StartTimestamp ) _$ ( $CurrentTenant.DisplayName ) .json "
16461932 $Report | Out-File $summaryHtmlPath
1647- $OutputCLI | Out-File - Width 512 - FilePath $summaryTxtPath
1933+ $summaryJson | ConvertTo-Json - Depth 10 | Out-File - FilePath $summaryJsonPath - Encoding utf8
16481934
16491935 # Print to console
16501936 Write-Host " `n`n ========================================= Summary =========================================" - ForegroundColor Cyan
16511937 Write-Host $OutputCLI
16521938 Write-Host " ===========================================================================================" - ForegroundColor Cyan
16531939 write-host " [+] Enumeration summary stored at: $summaryHtmlPath "
1654- write-host " [+] Enumeration summary (txt ) stored at: $summaryTxtPath "
1940+ write-host " [+] Enumeration summary (json ) stored at: $summaryJsonPath "
16551941}
16561942
0 commit comments