Private/ReportGeneration/New-AuthContextHtmlReport.ps1
|
function New-AuthContextHtmlReport { <# .SYNOPSIS Builds a static HTML report consolidating all Authentication Context usage artifacts. .DESCRIPTION Accepts pre-collected dataset objects (contexts, labels, sites, groups, CA policies, protected actions, and PIM outputs) and produces a self-contained HTML (no interactive JS) summarizing KPIs plus tabular sections. Sanitizes auth context identifier columns to remove stray quotes introduced by serialization edge cases. .PARAMETER AuthContexts Authentication context objects (Id, DisplayName,...). .PARAMETER Labels Sensitivity labels with potential Authentication Context bindings. .PARAMETER Sites SharePoint site objects carrying auth context references. .PARAMETER Groups Unified / security group objects (already filtered where relevant). .PARAMETER CA Conditional Access policy summary rows from Get-ConditionalAccessPoliciesWithAuthContext. .PARAMETER ProtectedActions Action rows from Get-ProtectedActionsWithAuthContext. .PARAMETER PIMPoliciesEntra Normalized Entra (directory) PIM policy rows. .PARAMETER PIMPoliciesGroups Normalized group-scope PIM policy rows. .PARAMETER PIMPoliciesAzureResources Normalized Azure resource PIM policy rows. .PARAMETER Path Destination file path (HTML). If omitted, output object is returned and no file written. .PARAMETER QuietMode Suppresses non-error host output. .OUTPUTS String (path) when -Path supplied; otherwise PSCustomObject representing report metadata/content. .NOTES Designed for portability; tables rely on basic HTML/CSS only. .EXAMPLE New-AuthContextHtmlReport -AuthContexts $ac -Labels $lbl -Sites $sites -Groups $grps -CA $ca -ProtectedActions $pa -PIMPoliciesEntra $pimE -PIMPoliciesGroups $pimG -PIMPoliciesAzureResources $pimAz -Path report.html #> [CmdletBinding()] param( [Parameter(Mandatory)][object]$AuthContexts, [Parameter(Mandatory)][object]$Labels, [Parameter(Mandatory)][object]$Sites, [Parameter(Mandatory)][object]$Groups, [object]$CA, [object]$ProtectedActions, [object]$PIMPoliciesEntra, [object]$PIMPoliciesGroups, [object]$PIMPoliciesAzureResources, [string]$Path, [ValidateSet('Classic', 'Dark')][string]$Style = 'Classic', [ValidateSet('Classic', 'Tabbed', 'TabbedOverview', 'Sidebar', 'Masonry', 'Dashboard', 'Layoutv2')][string]$Layout = 'TabbedOverview', [switch]$GenerateAllThemesForLayout, [switch]$QuietMode ) $Sites = SanitizeAuthContextText $Sites $Groups = SanitizeAuthContextText $Groups $CA = SanitizeAuthContextText $CA $ProtectedActions = SanitizeAuthContextText $ProtectedActions $PIMPoliciesEntra = SanitizeAuthContextText $PIMPoliciesEntra $PIMPoliciesGroups = SanitizeAuthContextText $PIMPoliciesGroups $PIMPoliciesAzureResources = SanitizeAuthContextText $PIMPoliciesAzureResources $kpi = [pscustomobject]@{ 'Authentication Contexts' = ($AuthContexts | Measure-Object).Count 'Sensitivity Labels' = ($Labels | Measure-Object).Count 'SharePoint Sites' = ($Sites | Measure-Object).Count 'Security Groups' = ($Groups | Measure-Object).Count 'Conditional Access Policies' = ($CA | Measure-Object).Count 'Protected Actions' = ($ProtectedActions | Measure-Object).Count 'PIM Policies for Entra' = ($PIMPoliciesEntra | Measure-Object).Count 'PIM Policies for Groups' = ($PIMPoliciesGroups | Measure-Object).Count 'PIM Policies for Azure Resources' = ($PIMPoliciesAzureResources | Measure-Object).Count } $ts = Get-Date $sections = @( @{Title = 'Authentication Contexts'; Data = $AuthContexts }, @{Title = 'Sensitivity Labels'; Data = $Labels }, @{Title = 'SharePoint Sites'; Data = $Sites }, @{Title = 'Security Groups'; Data = $Groups }, @{Title = 'Conditional Access Policies'; Data = $CA }, @{Title = 'Protected Actions'; Data = $ProtectedActions }, @{Title = 'PIM Policies for Entra'; Data = $PIMPoliciesEntra }, @{Title = 'PIM Policies for Groups'; Data = $PIMPoliciesGroups }, @{Title = 'PIM Policies for Azure Resources'; Data = $PIMPoliciesAzureResources } ) # Dual-mode CSS (Classic light & Dark) via CSS variables $css = 'body{font-family:Segoe UI,Arial,sans-serif;margin:0;--bg:#f5f7fa;--text:#222;--panel:#fff;--panel-border:#e2e7ef;--accent:#0f4c81;--muted:#555;--hover:#f9fcff;--kpi-bg:#fff;--footer:#666;background:var(--bg);color:var(--text)}body.dark-mode{--bg:#1e1f24;--text:#e6e6e6;--panel:#26282e;--panel-border:#3b3d44;--accent:#4da3ff;--muted:#9aa4b1;--hover:#31343a;--kpi-bg:#2b2d33;--footer:#9aa4b1}header{background:var(--accent);color:#fff;padding:18px 28px;display:flex;align-items:center;gap:16px}header h1{margin:0;font-size:22px;flex:1}button.theme-toggle{background:#fff;color:var(--accent);border:1px solid var(--accent);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600}body.dark-mode button.theme-toggle{background:#2b2d33;color:var(--accent);border-color:#2b2d33}.kpis{display:flex;flex-wrap:wrap;margin:20px}.kpi{background:var(--kpi-bg);border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,.08);padding:14px 18px;margin:8px;flex:1 1 160px;min-width:160px}.kpi h3{margin:0 0 6px;font-size:12px;font-weight:600;text-transform:uppercase;color:var(--muted)}.kpi .val{font-size:26px;font-weight:600;color:var(--accent)}section{margin:25px 28px;background:var(--panel);padding:18px 22px 28px;border-radius:10px;box-shadow:0 2px 6px rgba(0,0,0,.08)}section h2{margin:0 0 10px;font-size:18px;color:var(--accent);display:flex;align-items:center;cursor:pointer}section h2 .chevron{margin-left:8px;transition:transform .25s}section.collapsed .chevron{transform:rotate(-90deg)}table{border-collapse:collapse;width:100%;margin-top:12px;font-size:13px}th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--panel-border)}th{background:color-mix(in srgb,var(--accent) 15%,#fff);cursor:pointer;position:relative}body.dark-mode th{background:#30333a;color:#cdd3db}th.sort-asc::after,th.sort-desc::after{content:"";border:5px solid transparent;position:absolute;right:8px;top:50%;transform:translateY(-50%)}th.sort-asc::after{border-bottom-color:var(--accent);margin-top:-6px}th.sort-desc::after{border-top-color:var(--accent);margin-top:4px}tr:hover{background:var(--hover)}footer{margin:40px 0 0;padding:20px 28px;font-size:11px;color:var(--footer);text-align:center}body.dark-mode .tab-button{color:#e6e6e6}body.dark-mode .tab-button.active{color:#fff}body.dark-mode .tab-pane h2{color:var(--accent)}body.dark-mode .overview-stack h3{color:#cdd3db}' # Build JS bundle depending on layout $jsCommon = @() $jsCommon += 'function sortTable(t,idx){const tbody=t.tBodies[0];const rows=[...tbody.querySelectorAll("tr")];const th=t.tHead.rows[0].cells[idx];const asc=!th.classList.contains("sort-asc");[...t.tHead.rows[0].cells].forEach(h=>h.classList.remove("sort-asc","sort-desc"));th.classList.add(asc?"sort-asc":"sort-desc");rows.sort((a,b)=>{const ta=a.cells[idx].innerText||"";const tb=b.cells[idx].innerText||"";const na=parseFloat(ta.replace(/[^0-9.-]/g,""));const nb=parseFloat(tb.replace(/[^0-9.-]/g,""));if(!isNaN(na)&&!isNaN(nb)){return asc?na-nb:nb-na}return asc?ta.localeCompare(tb):tb.localeCompare(ta);});rows.forEach(r=>tbody.appendChild(r));}' switch ($Layout) { 'Classic' { $jsCommon += 'document.querySelectorAll("th").forEach((th,i)=>th.addEventListener("click",()=>sortTable(th.closest("table"),i)));document.querySelectorAll("section h2").forEach(h=>h.addEventListener("click",()=>h.parentElement.classList.toggle("collapsed")));' } 'Tabbed' { $jsCommon += 'document.querySelectorAll("th").forEach((th,i)=>th.addEventListener("click",()=>sortTable(th.closest("table"),i)));const tabs=[...document.querySelectorAll(".tab-button")];const panes=[...document.querySelectorAll(".tab-pane")];tabs.forEach(tb=>tb.addEventListener("click",()=>{tabs.forEach(t=>t.classList.remove("active"));tb.classList.add("active");panes.forEach(p=>p.classList.toggle("active",p.dataset.pid===tb.dataset.pid));}));if(tabs.length>0){tabs[0].click();}' } 'TabbedOverview' { $jsCommon += 'document.querySelectorAll("th").forEach((th,i)=>th.addEventListener("click",()=>sortTable(th.closest("table"),i)));const tabs=[...document.querySelectorAll(".tab-button")];const panes=[...document.querySelectorAll(".tab-pane")];tabs.forEach(tb=>tb.addEventListener("click",()=>{tabs.forEach(t=>t.classList.remove("active"));tb.classList.add("active");panes.forEach(p=>p.classList.toggle("active",p.dataset.pid===tb.dataset.pid));}));if(tabs.length>0){tabs[0].click();}' } 'Sidebar' { $jsCommon += 'document.querySelectorAll("th").forEach((th,i)=>th.addEventListener("click",()=>sortTable(th.closest("table"),i)));document.querySelectorAll("section h2").forEach(h=>h.addEventListener("click",()=>h.parentElement.classList.toggle("collapsed")));' } 'Masonry' { $jsCommon += 'document.querySelectorAll("th").forEach((th,i)=>th.addEventListener("click",()=>sortTable(th.closest("table"),i)));document.querySelectorAll("section h2").forEach(h=>h.addEventListener("click",()=>h.parentElement.classList.toggle("collapsed")));' } 'Dashboard' { $jsCommon += 'document.querySelectorAll(".detail-open").forEach(btn=>btn.addEventListener("click",()=>{const id=btn.dataset.target;document.getElementById(id).classList.add("show");document.body.classList.add("modal-open");}));document.querySelectorAll(".modal .close").forEach(btn=>btn.addEventListener("click",()=>{btn.closest(".modal").classList.remove("show");document.body.classList.remove("modal-open");}));document.querySelectorAll("th").forEach((th,i)=>th.addEventListener("click",()=>sortTable(th.closest("table"),i)));' } 'Layoutv2' { $jsCommon += 'document.querySelectorAll("th").forEach((th,i)=>th.addEventListener("click",()=>sortTable(th.closest("table"),i)));const g=document.getElementById("globalSearch");if(g){g.addEventListener("input",()=>{const q=g.value.toLowerCase();document.querySelectorAll("table tbody tr").forEach(r=>{const t=r.innerText.toLowerCase();r.style.display=t.indexOf(q)>-1?"":"none";});});}document.querySelectorAll(".collapse-btn").forEach(btn=>btn.addEventListener("click",()=>{const card=btn.closest(".lv2-card");card.classList.toggle("collapsed");btn.textContent=card.classList.contains("collapsed")?"+":"−";}));' } } $jsBundleCore = ($jsCommon -join '') $jsBundle = 'document.addEventListener("DOMContentLoaded",()=>{' + $jsBundleCore + 'const tg=document.getElementById("themeToggle");if(tg){tg.addEventListener("click",()=>{document.body.classList.toggle("dark-mode");tg.textContent=document.body.classList.contains("dark-mode")?"Light Mode":"Dark Mode";});if("' + $Style + '"==="Dark"){document.body.classList.add("dark-mode");tg.textContent="Light Mode";}}});' $scriptTag = '<script>' + $jsBundle + '</script>' # Layout-specific CSS extension $layoutCss = switch ($Layout) { 'Classic' { '' } 'Tabbed' { '.tabs{margin:0 40px}.tab-list{display:flex;flex-wrap:wrap;gap:8px;margin:20px 0}.tab-button{background:var(--panel);border:1px solid var(--panel-border);border-radius:20px;padding:8px 14px;font-size:13px;cursor:pointer;transition:.2s}.tab-button.active{background:var(--accent);color:#fff}.tab-pane{display:none}.tab-pane.active{display:block}' } 'TabbedOverview' { '.tabs{margin:0 40px}.tab-list{display:flex;flex-wrap:wrap;gap:8px;margin:20px 0}.tab-button{background:var(--panel);border:1px solid var(--panel-border);border-radius:20px;padding:8px 14px;font-size:13px;cursor:pointer;transition:.2s}.tab-button.active{background:var(--accent);color:#fff}.tab-pane{display:none}.tab-pane.active{display:block}.overview-stack .ov-section{margin:22px 0;padding:14px 18px;border:1px solid var(--panel-border);border-radius:10px;background:var(--panel)}' } 'Sidebar' { 'body{display:flex}aside.nav{width:240px;background:#103c64;color:#fff;min-height:100vh;padding:28px 20px;box-shadow:2px 0 6px rgba(0,0,0,.15)}aside.nav h2{margin:0 0 16px;font-size:18px}aside.nav a{display:block;color:#fff;text-decoration:none;padding:6px 10px;border-radius:6px;margin-bottom:4px;font-size:13px}aside.nav a:hover{background:#1d5795}main.report{flex:1}main.report .kpis{margin:26px 40px}main.report section{margin:26px 40px}' } 'Masonry' { '.masonry{columns:3;column-gap:18px;margin:0 30px 40px}@media(max-width:1300px){.masonry{columns:2}}@media(max-width:900px){.masonry{columns:1}}.masonry section{display:inline-block;width:100%;margin:0 0 18px;break-inside:avoid}' } 'Dashboard' { '.grid-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:18px;margin:30px 40px}.card{background:#fff;border-radius:14px;padding:20px 22px;box-shadow:0 4px 12px rgba(0,0,0,.08);position:relative;overflow:hidden}.card h3{margin:0 0 10px;font-size:14px;text-transform:uppercase;color:#536b85}.card .num{font-size:38px;font-weight:700;color:#103c64;margin:0 0 14px}.card button{background:#103c64;color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:12px}.card button:hover{background:#0d3350}.modal{position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,.55);display:none;align-items:flex-start;overflow:auto;padding:60px 40px 40px;z-index:1000}.modal.show{display:flex}.modal .content{background:#fff;border-radius:16px;padding:30px 34px;max-width:1200px;width:100%;box-shadow:0 6px 18px rgba(0,0,0,.25)}.modal .close{float:right;font-size:16px;font-weight:600;background:#eee;border:none;padding:4px 10px;border-radius:6px;cursor:pointer;margin:-8px -8px 10px 0}body.modal-open{overflow:hidden}' } 'Layoutv2' { 'body{font-family:Segoe UI,Arial,sans-serif;margin:0;background:linear-gradient(120deg,#eef2f7,#dde7f3,#f4f7fa);color:#1d2935}header{backdrop-filter:blur(6px);background:rgba(16,60,100,.85);color:#fff;padding:26px 42px;position:sticky;top:0;z-index:500;box-shadow:0 4px 16px rgba(0,0,0,.25)}header h1{margin:0;font-size:26px;letter-spacing:.5px}.lv2-kpi-bar{display:flex;flex-wrap:wrap;gap:14px;margin:26px 42px}.lv2-kpi{flex:1 1 180px;background:rgba(255,255,255,.75);backdrop-filter:blur(4px);border-radius:14px;padding:14px 18px;box-shadow:0 2px 6px rgba(0,0,0,.12)}.lv2-kpi .kpi-label{display:block;font-size:11px;font-weight:600;text-transform:uppercase;color:#4e6075}.lv2-kpi .kpi-value{font-size:28px;font-weight:600;color:#103c64}.lv2-container{margin:10px 42px 60px}.lv2-search{margin:0 0 24px;display:flex;justify-content:flex-end}.lv2-search input{width:320px;padding:10px 14px;border:1px solid #b8c7d6;border-radius:10px;font-size:14px;box-shadow:0 1px 3px rgba(0,0,0,.1)}.lv2-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:22px}.lv2-card{background:rgba(255,255,255,.7);backdrop-filter:blur(4px);border-radius:18px;box-shadow:0 8px 24px -6px rgba(0,0,0,.18);display:flex;flex-direction:column;overflow:hidden;transition:.25s}.lv2-card:hover{transform:translateY(-4px);box-shadow:0 12px 32px -6px rgba(0,0,0,.22)}.lv2-card-head{display:flex;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid #d6e1ec;background:linear-gradient(90deg,#ffffff,#f5f9fc)}.lv2-card-head h2{flex:1;margin:0;font-size:18px;color:#103c64}.count-badge{background:#103c64;color:#fff;padding:4px 10px;border-radius:20px;font-size:12px;font-weight:600}.collapse-btn{background:#103c64;color:#fff;border:none;padding:6px 10px;border-radius:8px;cursor:pointer;font-size:12px}.collapse-btn:hover{background:#0d3350}.lv2-card-body{padding:16px 20px;max-height:640px;overflow:auto}.lv2-card.collapsed .lv2-card-body{display:none}.lv2-card.collapsed .collapse-btn{background:#607a94}.lv2-card table{font-size:12px}footer{margin:60px 0 0;padding:30px 42px;font-size:12px;color:#455869;text-align:center}.sticky-search-shadow{box-shadow:0 4px 12px -3px rgba(0,0,0,.28)}' } } $css = $css + $layoutCss # Pre-render section contents $sectionHtmlBlocks = foreach ($reportSection in $sections) { Convert-DataTableHtml -Data $reportSection.Data -Title $reportSection.Title } $htmlHeader = '<!DOCTYPE html><html><head><meta charset="utf-8" /><title>Authentication Context Inventory</title><meta name="viewport" content="width=device-width,initial-scale=1" /><style>' + $css + '</style>' + $scriptTag + '</head><body>' $headerBlock = '<header><h1>Authentication Context Inventory</h1><button id="themeToggle" class="theme-toggle" type="button">' + ($Style -eq 'Dark' ? 'Light Mode' : 'Dark Mode') + '</button></header>' $kpiBlock = '<div class="kpis">' + (($kpi.PSObject.Properties | ForEach-Object { "<div class='kpi'><h3>$($_.Name)</h3><div class='val'>$($_.Value)</div></div>" }) -join '') + '</div>' switch ($Layout) { 'Classic' { $bodyBlock = ($sections | ForEach-Object -Begin { $acc = @() } -Process { $acc += "<section><h2>$($_.Title)<span class='chevron'>▶</span></h2>$($sectionHtmlBlocks[$acc.Count])</section>" } -End { $acc -join '' }) } 'Tabbed' { $tabButtons = ($sections | ForEach-Object -Begin { $i = 0; $btns = @() } -Process { $btns += "<button class='tab-button' data-pid='pane$i'>$($_.Title)</button>"; $i++ } -End { $btns -join '' }) $tabPanes = ($sections | ForEach-Object -Begin { $j = 0; $panes = @() } -Process { $panes += "<div class='tab-pane' data-pid='pane$j'><h2>$($_.Title)</h2>$($sectionHtmlBlocks[$j])</div>"; $j++ } -End { $panes -join '' }) $bodyBlock = "<div class='tabs'><div class='tab-list'>$tabButtons</div>$tabPanes</div>" } 'TabbedOverview' { $overviewContent = ($sections | ForEach-Object -Begin { $o = @() } -Process { $o += "<div class='ov-section'><h3>$($_.Title)</h3>$($sectionHtmlBlocks[$o.Count])</div>" } -End { $o -join '' }) $tabButtons = '<button class="tab-button" data-pid="paneOverview">Overview</button>' + ($sections | ForEach-Object -Begin { $i = 0; $btns = @() } -Process { $btns += "<button class='tab-button' data-pid='pane$i'>$($_.Title)</button>"; $i++ } -End { $btns -join '' }) $tabPanesOverview = "<div class='tab-pane overview-pane' data-pid='paneOverview'><h2>Overview</h2>$kpiBlock<div class='overview-stack'>$overviewContent</div></div>" $tabPanesSections = ($sections | ForEach-Object -Begin { $j = 0; $panes = @() } -Process { $panes += "<div class='tab-pane' data-pid='pane$j'><h2>$($_.Title)</h2>$($sectionHtmlBlocks[$j])</div>"; $j++ } -End { $panes -join '' }) $bodyBlock = "<div class='tabs'><div class='tab-list'>$tabButtons</div>$tabPanesOverview$tabPanesSections</div>" $kpiBlock = '' } 'Sidebar' { $navLinks = ($sections | ForEach-Object -Begin { $x = 0; $links = @() } -Process { $links += "<a href='#sec$x'>$($_.Title)</a>"; $x++ } -End { $links -join '' }) $contentSections = ($sections | ForEach-Object -Begin { $y = 0; $secs = @() } -Process { $secs += "<section id='sec$y'><h2>$($_.Title)<span class='chevron'>▶</span></h2>$($sectionHtmlBlocks[$y])</section>"; $y++ } -End { $secs -join '' }) $bodyBlock = "<aside class='nav'><h2>Sections</h2>$navLinks</aside><main class='report'>$kpiBlock$contentSections</main>"; $kpiBlock = '' } 'Masonry' { $masonrySections = ($sections | ForEach-Object -Begin { $z = 0; $cards = @() } -Process { $cards += "<section><h2>$($_.Title)<span class='chevron'>▶</span></h2>$($sectionHtmlBlocks[$z])</section>"; $z++ } -End { $cards -join '' }) $bodyBlock = "<div class='masonry'>$masonrySections</div>" } 'Dashboard' { $cards = ($sections | ForEach-Object -Begin { $d = 0; $c = @() } -Process { $countVal = ($sections[$d].Data | Measure-Object).Count; $modalId = "modal$d"; $c += "<div class='card'><h3>$($_.Title)</h3><div class='num'>$countVal</div><button class='detail-open' data-target='$modalId'>Details</button></div>"; $d++ } -End { $c -join '' }) $modals = ($sections | ForEach-Object -Begin { $e = 0; $m = @() } -Process { $m += "<div class='modal' id='modal$e'><div class='content'><button class='close' aria-label='Close'>×</button><h2>$($_.Title)</h2>$($sectionHtmlBlocks[$e])</div></div>"; $e++ } -End { $m -join '' }) $bodyBlock = "<div class='grid-cards'>$cards</div>$modals"; $kpiBlock = '' } 'Layoutv2' { $kpiBlock = '<div class="lv2-kpi-bar">' + (($kpi.PSObject.Properties | ForEach-Object { "<div class='lv2-kpi'><span class='kpi-label'>$($_.Name)</span><span class='kpi-value'>$($_.Value)</span></div>" }) -join '') + '</div>' $searchBar = '<div class="lv2-search"><input type="text" id="globalSearch" placeholder="Search all tables..." aria-label="Global search" /></div>' $sectionBlocks = ($sections | ForEach-Object -Begin { $i2 = 0; $b = @() } -Process { $countVal = ($sections[$i2].Data | Measure-Object).Count; $b += "<div class='lv2-card' data-card-index='$i2'><div class='lv2-card-head'><h2>$($_.Title)</h2><span class='count-badge'>$countVal</span><button class='collapse-btn' aria-label='Toggle section'>−</button></div><div class='lv2-card-body'>$($sectionHtmlBlocks[$i2])</div></div>"; $i2++ } -End { $b -join '' }) $bodyBlock = "<div class='lv2-container'>$searchBar<div class='lv2-grid'>$sectionBlocks</div></div>" } } $footerBlock = "<footer>Generated $($ts.ToString('dd-MM-yyyy')) by Authentication Context Inventory Script v$script:ToolVersion<br>By Sebastian Flæng Markdanner @Chanceofsecurity.com — Style: $Style — Layout: $Layout</footer>" $finalHtml = $htmlHeader + $headerBlock + $kpiBlock + $bodyBlock + $footerBlock + '</body></html>' if ($GenerateAllThemesForLayout) { $base = [System.IO.Path]::GetFileNameWithoutExtension($Path) $dir = [System.IO.Path]::GetDirectoryName($Path) foreach ($theme in 'Default', 'Minimal', 'Dark', 'Compact', 'Accessible', 'Cards') { $themePath = Join-Path $dir ($base + '_' + $Layout + '_' + $theme + '.html') if (-not $QuietMode) { Write-Host "[Report] Generating variant: Layout=$Layout Theme=$theme -> $themePath" -ForegroundColor DarkCyan } New-AuthContextHtmlReport -AuthContexts $AuthContexts -Labels $Labels -Sites $Sites -Groups $Groups -CA $CA -ProtectedActions $ProtectedActions -PIMPoliciesEntra $PIMPoliciesEntra -PIMPoliciesGroups $PIMPoliciesGroups -PIMPoliciesAzureResources $PIMPoliciesAzureResources -Path $themePath -Style $theme -Layout $Layout -QuietMode:$QuietMode | Out-Null } } Set-Content -Path $Path -Value $finalHtml -Encoding UTF8 return $Path } |