Skip to content

Commit 4aecb13

Browse files
committed
Export a summary JSON instead of TXT
1 parent 33fc21f commit 4aecb13

1 file changed

Lines changed: 307 additions & 21 deletions

File tree

modules/export_Summary.psm1

Lines changed: 307 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)