Private/New-CGScorecardHtml.ps1
|
# Builds the branded Copilot Readiness Scorecard HTML from the gathered findings. # Self-contained (inline CSS) so the file can be shared or screenshotted as-is. function New-CGScorecardHtml { [CmdletBinding(SupportsShouldProcess)] param( [object[]]$Everyone, [object[]]$Shares, [hashtable]$Summary, [Parameter(Mandatory)][string]$Path ) # HTML-encode any value that came from SharePoint (site/library titles can # legitimately contain & < > and would otherwise break the markup). function Enc([object]$v) { [System.Net.WebUtility]::HtmlEncode([string]$v) } $accentBand = if ($Summary.Score -ge 80) { '#107c10' } # low risk - green elseif ($Summary.Score -ge 50) { '#f7630c' } # attention - orange else { '#d13438' } # high risk - red # per-site rollup $siteNames = @($Everyone.Site + $Shares.Site | Where-Object { $_ } | Select-Object -Unique | Sort-Object) $siteRows = foreach ($s in $siteNames) { $se = @($Everyone | Where-Object { $_.Site -eq $s }).Count $sa = @($Shares | Where-Object { $_.Site -eq $s -and $_.LinkClass -eq 'Anonymous' }).Count $so = @($Shares | Where-Object { $_.Site -eq $s -and $_.LinkClass -eq 'Organization' }).Count $risk = if ($sa -gt 0) { 'High' } elseif ($se -gt 0 -or $so -gt 0) { 'Medium' } else { 'Low' } "<tr><td class='u'>$(Enc $s)</td><td class='n'>$se</td><td class='n'>$sa</td><td class='n'>$so</td><td><span class='pill $($risk.ToLower())'>$risk</span></td></tr>" } if (-not $siteRows) { $siteRows = "<tr><td colspan='5' class='muted'>No sites with exposure found.</td></tr>" } $eeeuRows = foreach ($e in $Everyone) { "<tr><td class='u'>$(Enc $e.Site)</td><td>$(Enc $e.Claim)</td><td>$(Enc $e.Permission)</td><td>$(Enc $e.Scope)</td></tr>" } if (-not $eeeuRows) { $eeeuRows = "<tr><td colspan='4' class='muted'>None.</td></tr>" } $linkRows = foreach ($l in ($Shares | Sort-Object LinkClass, Site)) { "<tr><td class='u'>$(Enc $l.Site)</td><td><span class='pill $($l.LinkClass.ToLower())'>$(Enc $l.LinkClass)</span></td><td class='n'>$($l.SharedWithCount)</td><td class='mono'>$(Enc $l.LinkKind)</td></tr>" } if (-not $linkRows) { $linkRows = "<tr><td colspan='4' class='muted'>None.</td></tr>" } # Coverage strip - only when some sites could not be read (blind spots). $coverageWarn = '' if ($Summary.Skipped -and [int]$Summary.Skipped -gt 0) { $list = (@($Summary.SkippedSites) | ForEach-Object { Enc $_ }) -join ', ' $coverageWarn = "<div class=""warn""><strong>Partial coverage.</strong> $([int]$Summary.Skipped) site(s) could not be read and are <em>not</em> reflected in the score: $list</div>" } $coverageSub = if ($Summary.Total) { " · scanned $($Summary.Scanned) of $($Summary.Total) sites" } else { '' } $html = @" <!doctype html> <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> <title>Copilot Readiness Scorecard</title> <style> :root{--ink:#242424;--muted:#605e5c;--line:#e1dfdd;--card:#fff;--bg:#faf9f8;--accent:#0f6cbd;} *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:'Segoe UI',system-ui,Arial,sans-serif;line-height:1.45} .wrap{max-width:1080px;margin:0 auto;padding:28px 22px 60px} header{display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin-bottom:6px} h1{font-size:24px;margin:0;font-weight:600;letter-spacing:-.01em} .sub{color:var(--muted);font-size:13.5px;margin:2px 0 0} .score{margin-left:auto;text-align:center;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 22px;min-width:150px} .score .num{font-size:40px;font-weight:700;line-height:1;color:$accentBand} .score .band{font-size:12.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:$accentBand;margin-top:4px} .score .lbl{font-size:11px;color:var(--muted);margin-top:6px} .tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:22px 0} @media(max-width:680px){.tiles{grid-template-columns:repeat(2,1fr)}} .tile{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 18px} .tile .v{font-size:28px;font-weight:700}.tile .k{color:var(--muted);font-size:12.5px;margin-top:2px} .tile.crit .v{color:#d13438}.tile.warn .v{color:#f7630c}.tile.info .v{color:var(--accent)} h2{font-size:16px;margin:26px 0 10px;font-weight:600} table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden;font-size:13px} th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--line);vertical-align:top} th{background:#f3f2f1;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:var(--muted)} tr:last-child td{border-bottom:0} td.n{text-align:right;font-variant-numeric:tabular-nums} td.u{word-break:break-all;max-width:420px}.mono{font-family:Consolas,monospace;font-size:11.5px;color:var(--muted)} .muted{color:var(--muted)} .pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 9px;border-radius:100px} .pill.high,.pill.anonymous{background:#fdf3f4;color:#d13438} .pill.medium,.pill.organization{background:#fdf1e9;color:#b3560b} .pill.low,.pill.specific{background:#dff6dd;color:#107c10} footer{margin-top:30px;color:var(--muted);font-size:12.5px;border-top:1px solid var(--line);padding-top:16px} a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline} .note{background:#eff6fc;border:1px solid #cfe4f7;border-radius:10px;padding:12px 14px;font-size:13px;margin:14px 0} .warn{background:#fdf1e9;border:1px solid #f6d9c2;border-radius:10px;padding:12px 14px;font-size:13px;margin:14px 0;color:#8a4b0b;word-break:break-all} </style></head> <body><div class="wrap"> <header> <div> <h1>Copilot Readiness Scorecard</h1> <p class="sub">$(Enc $Summary.ScopeLabel)$coverageSub · generated $($Summary.Generated)</p> </div> <div class="score"><div class="num">$($Summary.Score)</div><div class="band">$($Summary.Band)</div><div class="lbl">readiness / 100</div></div> </header> <div class="note"><strong>What this measures.</strong> Microsoft 365 Copilot can surface anything a user can already reach. This scorecard flags the broad access that makes that a risk — Everyone/EEEU grants and Anonymous / Organization-wide sharing links — so you can fix it before turning Copilot on. Read-only; nothing was changed.</div> $coverageWarn <div class="tiles"> <div class="tile crit"><div class="v">$($Summary.Anon)</div><div class="k">Anonymous links</div></div> <div class="tile warn"><div class="v">$($Summary.Org)</div><div class="k">Organization-wide links</div></div> <div class="tile warn"><div class="v">$($Summary.Eeeu)</div><div class="k">Everyone / EEEU grants</div></div> <div class="tile info"><div class="v">$($Summary.SitesAtRisk)</div><div class="k">Sites with exposure</div></div> </div> <h2>By site</h2> <table><thead><tr><th>Site</th><th>EEEU/Everyone</th><th>Anon links</th><th>Org links</th><th>Risk</th></tr></thead> <tbody> $($siteRows -join "`n") </tbody></table> <h2>Everyone / Everyone-Except-External-Users grants</h2> <table><thead><tr><th>Site</th><th>Claim</th><th>Permission</th><th>Scope</th></tr></thead> <tbody> $($eeeuRows -join "`n") </tbody></table> <h2>Oversharing links</h2> <table><thead><tr><th>Site</th><th>Type</th><th>Shared with</th><th>Kind</th></tr></thead> <tbody> $($linkRows -join "`n") </tbody></table> <footer> Generated by <strong>CopilotGovernance</strong> — a free, read-only M365 oversharing toolkit. ★ <a href="https://github.com/gvijaikumar9/CopilotGovernance">Star on GitHub</a> · run <code>Send-CopilotGovernanceFeedback</code> to tell me how you used it. </footer> </div></body></html> "@ if ($PSCmdlet.ShouldProcess($Path, 'Write Copilot Readiness Scorecard')) { Set-Content -Path $Path -Value $html -Encoding UTF8 } } |