Private/New-HtmlDashboard.ps1
|
function New-HtmlDashboard { <# .SYNOPSIS Generates a dark-themed HTML dashboard for a user lookup report. .DESCRIPTION Takes consolidated user data from Get-UserEverything and renders an HTML dashboard with sections for AD details, M365 licensing, devices, and sign-in history. Uses an orange (#f0883e) accent color. .PARAMETER UserData The consolidated PSCustomObject returned by Get-UserEverything. .PARAMETER OutputPath Full file path for the generated HTML report. .OUTPUTS System.IO.FileInfo for the generated HTML file. .EXAMPLE New-HtmlDashboard -UserData $userData -OutputPath "C:\Reports\jsmith.html" #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$UserData, [Parameter(Mandatory)] [string]$OutputPath ) $ad = $UserData.ADAccount $m365 = $UserData.M365 $devices = $UserData.Devices $signIns = $UserData.SignIns $displayName = if ($ad) { $ad.DisplayName } else { $UserData.Identity } $title = if ($ad) { $ad.Title } else { 'N/A' } $department = if ($ad) { $ad.Department } else { 'N/A' } $manager = if ($ad) { $ad.Manager } else { 'N/A' } $email = if ($ad) { $ad.Email } else { 'N/A' } $upn = if ($ad) { $ad.UPN } else { 'N/A' } $sam = if ($ad) { $ad.SAMAccountName } else { 'N/A' } $office = if ($ad) { $ad.Office } else { 'N/A' } # Status badges $enabledBadge = if ($ad -and $ad.Enabled) { '<span class="badge badge-good">Enabled</span>' } else { '<span class="badge badge-bad">Disabled</span>' } $lockedBadge = if ($ad -and $ad.LockedOut) { '<span class="badge badge-bad">Locked Out</span>' } elseif ($ad) { '<span class="badge badge-good">Not Locked</span>' } else { '' } $licenseBadge = if ($m365 -and $m365.LicenseAssignments -and $m365.LicenseAssignments.Count -gt 0) { '<span class="badge badge-good">Licensed</span>' } elseif ($m365) { '<span class="badge badge-bad">Unlicensed</span>' } else { '<span class="badge badge-warn">M365 Unavailable</span>' } $mfaBadge = if ($m365 -and $m365.MFAStatus -eq 'Enabled') { '<span class="badge badge-good">MFA On</span>' } elseif ($m365) { '<span class="badge badge-bad">MFA Off</span>' } else { '' } # Build AD details table $adSection = if ($ad) { $pwdLastSet = if ($ad.PasswordLastSet) { $ad.PasswordLastSet.ToString('yyyy-MM-dd HH:mm') } else { 'Never' } $lastLogon = if ($ad.LastLogonDate) { $ad.LastLogonDate.ToString('yyyy-MM-dd HH:mm') } else { 'Never' } $created = if ($ad.Created) { $ad.Created.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' } $expiration = if ($ad.AccountExpirationDate) { $ad.AccountExpirationDate.ToString('yyyy-MM-dd') } else { 'Never' } $groups = if ($ad.MemberOf) { ($ad.MemberOf | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None</li>' } $reports = if ($ad.DirectReports) { ($ad.DirectReports | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None</li>' } @" <div class="card"> <h2>Active Directory Details</h2> <div class="detail-grid"> <div class="detail-row"><span class="label">SAMAccountName</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($ad.SAMAccountName))</span></div> <div class="detail-row"><span class="label">UPN</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($ad.UPN))</span></div> <div class="detail-row"><span class="label">Distinguished Name</span><span class="value" style="font-size:0.85em;">$([System.Web.HttpUtility]::HtmlEncode($ad.DistinguishedName))</span></div> <div class="detail-row"><span class="label">Description</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($ad.Description))</span></div> <div class="detail-row"><span class="label">Account Status</span><span class="value">$enabledBadge $lockedBadge</span></div> <div class="detail-row"><span class="label">Password Last Set</span><span class="value">$pwdLastSet</span></div> <div class="detail-row"><span class="label">Password Expired</span><span class="value">$($ad.PasswordExpired)</span></div> <div class="detail-row"><span class="label">Password Never Expires</span><span class="value">$($ad.PasswordNeverExpires)</span></div> <div class="detail-row"><span class="label">Last Logon</span><span class="value">$lastLogon</span></div> <div class="detail-row"><span class="label">Account Created</span><span class="value">$created</span></div> <div class="detail-row"><span class="label">Account Expires</span><span class="value">$expiration</span></div> </div> <h3>Group Memberships</h3> <ul class="item-list">$groups</ul> <h3>Direct Reports</h3> <ul class="item-list">$reports</ul> </div> "@ } else { '<div class="card"><h2>Active Directory Details</h2><p class="unavailable">AD data unavailable.</p></div>' } # Build M365 section $m365Section = if ($m365) { $licenses = if ($m365.LicenseAssignments -and $m365.LicenseAssignments.Count -gt 0) { ($m365.LicenseAssignments | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None assigned</li>' } $mfaMethods = if ($m365.MFAMethods -and $m365.MFAMethods.Count -gt 0) { ($m365.MFAMethods | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None configured</li>' } $caPolicies = if ($m365.ConditionalAccessPolicies -and $m365.ConditionalAccessPolicies.Count -gt 0) { ($m365.ConditionalAccessPolicies | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None detected</li>' } $sharedMbx = if ($m365.SharedMailboxAccess -and $m365.SharedMailboxAccess.Count -gt 0) { ($m365.SharedMailboxAccess | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" } else { '<li>None</li>' } @" <div class="card"> <h2>Microsoft 365 Details $licenseBadge $mfaBadge</h2> <div class="detail-grid"> <div class="detail-row"><span class="label">Mailbox Size</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.MailboxSize))</span></div> <div class="detail-row"><span class="label">Mailbox Item Count</span><span class="value">$($m365.MailboxItemCount)</span></div> <div class="detail-row"><span class="label">OneDrive Usage</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.OneDriveUsage))</span></div> <div class="detail-row"><span class="label">Teams Last Activity</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.TeamsActivity))</span></div> <div class="detail-row"><span class="label">SharePoint Sites</span><span class="value">$($m365.SharePointSites)</span></div> <div class="detail-row"><span class="label">MFA Status</span><span class="value">$([System.Web.HttpUtility]::HtmlEncode($m365.MFAStatus))</span></div> </div> <h3>License Assignments</h3> <ul class="item-list">$licenses</ul> <h3>MFA Methods</h3> <ul class="item-list">$mfaMethods</ul> <h3>Conditional Access Policies</h3> <ul class="item-list">$caPolicies</ul> <h3>Shared Mailbox Access</h3> <ul class="item-list">$sharedMbx</ul> </div> "@ } else { '<div class="card"><h2>Microsoft 365 Details</h2><p class="unavailable">M365 data unavailable. Ensure Microsoft.Graph modules are installed and connected.</p></div>' } # Build devices section $devicesSection = if ($devices -and $devices.Count -gt 0) { $deviceRows = foreach ($d in $devices) { $complianceBadge = switch ($d.ComplianceState) { 'Compliant' { '<span class="badge badge-good">Compliant</span>' } 'NonCompliant' { '<span class="badge badge-bad">Non-Compliant</span>' } default { '<span class="badge badge-warn">' + $([System.Web.HttpUtility]::HtmlEncode($d.ComplianceState)) + '</span>' } } $lastCheck = if ($d.LastCheckIn) { $d.LastCheckIn.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' } $enrolled = if ($d.EnrollmentDate) { $d.EnrollmentDate.ToString('yyyy-MM-dd') } else { 'N/A' } @" <tr> <td>$([System.Web.HttpUtility]::HtmlEncode($d.DeviceName))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($d.OS)) $([System.Web.HttpUtility]::HtmlEncode($d.OSVersion))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($d.Manufacturer)) $([System.Web.HttpUtility]::HtmlEncode($d.Model))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($d.SerialNumber))</td> <td>$complianceBadge</td> <td>$lastCheck</td> <td>$enrolled</td> <td>$($d.IsManaged)</td> <td>$([System.Web.HttpUtility]::HtmlEncode($d.EncryptionStatus))</td> </tr> "@ } @" <div class="card"> <h2>Devices ($($devices.Count))</h2> <div class="table-wrap"> <table> <thead> <tr> <th>Device Name</th><th>OS</th><th>Make / Model</th><th>Serial</th> <th>Compliance</th><th>Last Check-In</th><th>Enrolled</th><th>Managed</th><th>Encryption</th> </tr> </thead> <tbody> $($deviceRows -join "`n") </tbody> </table> </div> </div> "@ } else { '<div class="card"><h2>Devices</h2><p class="unavailable">No devices found or Intune data unavailable.</p></div>' } # Build sign-in history section $signInSection = if ($signIns) { $lastSuccess = if ($signIns.LastSuccessfulSignIn) { $signIns.LastSuccessfulSignIn.ToString('yyyy-MM-dd HH:mm') } else { 'None' } $lastFailed = if ($signIns.LastFailedSignIn) { $signIns.LastFailedSignIn.ToString('yyyy-MM-dd HH:mm') } else { 'None' } $signInRows = if ($signIns.RecentSignIns -and $signIns.RecentSignIns.Count -gt 0) { foreach ($s in $signIns.RecentSignIns) { $statusBadge = if ($s.Status -eq 'Success') { '<span class="badge badge-good">Success</span>' } else { '<span class="badge badge-bad">' + $([System.Web.HttpUtility]::HtmlEncode($s.Status)) + '</span>' } $dt = if ($s.Date) { $s.Date.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' } @" <tr> <td>$dt</td> <td>$([System.Web.HttpUtility]::HtmlEncode($s.App))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($s.IPAddress))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($s.Location))</td> <td>$statusBadge</td> <td>$([System.Web.HttpUtility]::HtmlEncode($s.MFAResult))</td> </tr> "@ } } $riskRows = if ($signIns.RiskDetections -and $signIns.RiskDetections.Count -gt 0) { foreach ($r in $signIns.RiskDetections) { $riskDate = if ($r.DetectedDateTime) { $r.DetectedDateTime.ToString('yyyy-MM-dd HH:mm') } else { 'N/A' } @" <tr> <td>$riskDate</td> <td>$([System.Web.HttpUtility]::HtmlEncode($r.RiskType))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($r.RiskLevel))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($r.RiskState))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($r.IPAddress))</td> <td>$([System.Web.HttpUtility]::HtmlEncode($r.Location))</td> </tr> "@ } } $unusualHtml = if ($signIns.UnusualLocations -and $signIns.UnusualLocations.Count -gt 0) { $items = ($signIns.UnusualLocations | ForEach-Object { "<li>$([System.Web.HttpUtility]::HtmlEncode($_))</li>" }) -join "`n" "<h3>Unusual Locations</h3><ul class='item-list'>$items</ul>" } else { '' } @" <div class="card"> <h2>Sign-In History</h2> <div class="detail-grid"> <div class="detail-row"><span class="label">Last Successful Sign-In</span><span class="value">$lastSuccess</span></div> <div class="detail-row"><span class="label">Last Failed Sign-In</span><span class="value">$lastFailed</span></div> <div class="detail-row"><span class="label">Failed Attempts (period)</span><span class="value">$($signIns.FailedAttempts.Count)</span></div> </div> $(if ($signInRows) { @" <h3>Recent Sign-Ins</h3> <div class="table-wrap"> <table> <thead><tr><th>Date</th><th>Application</th><th>IP</th><th>Location</th><th>Status</th><th>MFA</th></tr></thead> <tbody>$($signInRows -join "`n")</tbody> </table> </div> "@ }) $(if ($riskRows) { @" <h3>Risk Detections</h3> <div class="table-wrap"> <table> <thead><tr><th>Date</th><th>Risk Type</th><th>Level</th><th>State</th><th>IP</th><th>Location</th></tr></thead> <tbody>$($riskRows -join "`n")</tbody> </table> </div> "@ }) $unusualHtml </div> "@ } else { '<div class="card"><h2>Sign-In History</h2><p class="unavailable">Sign-in data was not requested. Use -IncludeSignInHistory to include this section.</p></div>' } $reportDate = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') $initials = if ($displayName -and $displayName -match '(\S)\S*\s+(\S)') { ($Matches[1] + $Matches[2]).ToUpper() } else { '??' } $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>User Lookup: $([System.Web.HttpUtility]::HtmlEncode($displayName))</title> <style> :root { --accent: #f0883e; --accent-dim: rgba(240, 136, 62, 0.15); --bg: #0d1117; --surface: #161b22; --surface-hover: #1c2129; --border: #30363d; --text: #e6edf3; --text-muted: #8b949e; --good: #3fb950; --bad: #f85149; --warn: #d29922; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; padding: 2rem; } .header { display: flex; align-items: center; gap: 1.5rem; margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border); } .avatar { width: 80px; height: 80px; border-radius: 50%; background: var(--accent-dim); border: 2px solid var(--accent); display: flex; align-items: center; justify-content: center; font-size: 1.8rem; font-weight: 700; color: var(--accent); flex-shrink: 0; } .header-info h1 { font-size: 1.8rem; font-weight: 600; color: var(--text); } .header-info p { color: var(--text-muted); font-size: 0.95rem; } .header-info .meta { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 0.5rem; } .badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; } .badge-good { background: rgba(63,185,80,0.15); color: var(--good); border: 1px solid rgba(63,185,80,0.3); } .badge-bad { background: rgba(248,81,73,0.15); color: var(--bad); border: 1px solid rgba(248,81,73,0.3); } .badge-warn { background: rgba(210,153,34,0.15); color: var(--warn); border: 1px solid rgba(210,153,34,0.3); } .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem; } .card h2 { font-size: 1.2rem; color: var(--accent); margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); } .card h3 { font-size: 1rem; color: var(--text-muted); margin: 1rem 0 0.5rem 0; } .detail-grid { display: grid; grid-template-columns: 1fr; gap: 0; } .detail-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid rgba(48,54,61,0.5); } .detail-row:last-child { border-bottom: none; } .label { color: var(--text-muted); font-size: 0.9rem; min-width: 200px; } .value { color: var(--text); font-size: 0.9rem; text-align: right; word-break: break-all; } .item-list { list-style: none; padding: 0; } .item-list li { padding: 4px 8px; margin: 2px 0; background: var(--bg); border-radius: 4px; font-size: 0.85rem; color: var(--text-muted); } .table-wrap { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } th { text-align: left; padding: 8px 12px; background: var(--bg); color: var(--text-muted); font-weight: 600; border-bottom: 2px solid var(--border); white-space: nowrap; } td { padding: 8px 12px; border-bottom: 1px solid var(--border); color: var(--text); } tr:hover td { background: var(--surface-hover); } .unavailable { color: var(--text-muted); font-style: italic; padding: 1rem 0; } .footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--text-muted); font-size: 0.8rem; text-align: center; } @media (max-width: 768px) { body { padding: 1rem; } .detail-row { flex-direction: column; } .value { text-align: left; } } </style> </head> <body> <div class="header"> <div class="avatar">$initials</div> <div class="header-info"> <h1>$([System.Web.HttpUtility]::HtmlEncode($displayName))</h1> <p>$([System.Web.HttpUtility]::HtmlEncode($title)) — $([System.Web.HttpUtility]::HtmlEncode($department))</p> <p>Manager: $([System.Web.HttpUtility]::HtmlEncode($manager)) • Office: $([System.Web.HttpUtility]::HtmlEncode($office))</p> <p>$([System.Web.HttpUtility]::HtmlEncode($email))</p> <div class="meta"> $enabledBadge $lockedBadge $licenseBadge $mfaBadge </div> </div> </div> $adSection $m365Section $devicesSection $signInSection <div class="footer"> Generated by Admin-UserLookup v1.0.0 on $reportDate </div> </body> </html> "@ $parentDir = Split-Path -Path $OutputPath -Parent if ($parentDir -and -not (Test-Path $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null } $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force Write-Verbose "HTML dashboard saved to $OutputPath" Get-Item -Path $OutputPath } |