using System.IO; using System.Text; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services.Export; /// /// Exports user access audit results to a self-contained interactive HTML report. /// Produces a single HTML file with dual-view toggle (by-user / by-site), /// collapsible groups, sortable columns, filter input, and risk highlighting. /// No external CSS/JS dependencies — everything is inline. /// public class UserAccessHtmlExportService { /// /// Builds a self-contained HTML string from the supplied user access entries. /// public string BuildHtml(IReadOnlyList entries, ReportBranding? branding = null) { // Compute stats var totalAccesses = entries.Count; var usersAudited = entries.Select(e => e.UserLogin).Distinct().Count(); var sitesScanned = entries.Select(e => e.SiteUrl).Distinct().Count(); var highPrivCount = entries.Count(e => e.IsHighPrivilege); var externalCount = entries.Count(e => e.IsExternalUser); var sb = new StringBuilder(); // ── HTML HEAD ────────────────────────────────────────────────────────── sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("User Access Audit Report"); sb.AppendLine(""); sb.AppendLine(""); // ── BODY ─────────────────────────────────────────────────────────────── sb.AppendLine(""); sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); sb.AppendLine("

User Access Audit Report

"); // Stats cards sb.AppendLine("
"); sb.AppendLine($"
{totalAccesses}
Total Accesses
"); sb.AppendLine($"
{usersAudited}
Users Audited
"); sb.AppendLine($"
{sitesScanned}
Sites Scanned
"); sb.AppendLine($"
{highPrivCount}
High Privilege
"); sb.AppendLine($"
{externalCount}
External Users
"); sb.AppendLine("
"); // Per-user summary cards sb.AppendLine("
"); var userGroups = entries.GroupBy(e => e.UserLogin).OrderBy(g => g.Key).ToList(); foreach (var ug in userGroups) { var uName = HtmlEncode(ug.First().UserDisplayName); var uLogin = HtmlEncode(ug.Key); var uTotal = ug.Count(); var uSites = ug.Select(e => e.SiteUrl).Distinct().Count(); var uHighPriv = ug.Count(e => e.IsHighPrivilege); var uIsExt = ug.First().IsExternalUser; var cardClass = uHighPriv > 0 ? "user-card has-high-priv" : "user-card"; sb.AppendLine($"
"); sb.AppendLine($"
{uName}{(uIsExt ? " Guest" : "")}
"); sb.AppendLine($"
{uLogin}
"); sb.AppendLine($"
{uTotal} accesses • {uSites} site(s){(uHighPriv > 0 ? $" • {uHighPriv} high-priv" : "")}
"); sb.AppendLine("
"); } sb.AppendLine("
"); // View toggle buttons sb.AppendLine("
"); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine("
"); // Filter input sb.AppendLine("
"); sb.AppendLine(" "); sb.AppendLine("
"); // ── BY-USER VIEW ─────────────────────────────────────────────────────── sb.AppendLine("
"); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(""); sb.AppendLine(""); int userGroupIdx = 0; foreach (var ug in userGroups) { var groupId = $"ugrp{userGroupIdx++}"; var uName = HtmlEncode(ug.First().UserDisplayName); var uIsExt = ug.First().IsExternalUser; var uCount = ug.Count(); var guestBadge = uIsExt ? " Guest" : ""; sb.AppendLine($""); sb.AppendLine($" "); sb.AppendLine(""); foreach (var entry in ug) { var rowClass = entry.IsHighPrivilege ? " class=\"high-priv\"" : ""; var accessBadge = AccessTypeBadge(entry.AccessType); var highIcon = entry.IsHighPrivilege ? " ⚠" : ""; sb.AppendLine($""); sb.AppendLine($" {HtmlEncode(entry.SiteTitle)}"); sb.AppendLine($" "); sb.AppendLine($" "); sb.AppendLine($" {HtmlEncode(entry.PermissionLevel)}{highIcon}"); sb.AppendLine($" "); sb.AppendLine($" "); sb.AppendLine(""); } } sb.AppendLine(""); sb.AppendLine("
SiteObject TypeObjectPermission LevelAccess TypeGranted Through
{uName}{guestBadge} — {uCount} access(es)
{HtmlEncode(entry.ObjectType)}{HtmlEncode(entry.ObjectTitle)}{accessBadge}{HtmlEncode(entry.GrantedThrough)}
"); sb.AppendLine("
"); // ── BY-SITE VIEW ─────────────────────────────────────────────────────── sb.AppendLine("
"); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(""); sb.AppendLine(""); var siteGroups = entries.GroupBy(e => e.SiteUrl).OrderBy(g => g.Key).ToList(); int siteGroupIdx = 0; foreach (var sg in siteGroups) { var groupId = $"sgrp{siteGroupIdx++}"; var siteTitle = HtmlEncode(sg.First().SiteTitle); var sCount = sg.Count(); sb.AppendLine($""); sb.AppendLine($" "); sb.AppendLine(""); foreach (var entry in sg) { var rowClass = entry.IsHighPrivilege ? " class=\"high-priv\"" : ""; var accessBadge = AccessTypeBadge(entry.AccessType); var highIcon = entry.IsHighPrivilege ? " ⚠" : ""; var guestBadge = entry.IsExternalUser ? " Guest" : ""; sb.AppendLine($""); sb.AppendLine($" "); sb.AppendLine($" "); sb.AppendLine($" "); sb.AppendLine($" {HtmlEncode(entry.PermissionLevel)}{highIcon}"); sb.AppendLine($" "); sb.AppendLine($" "); sb.AppendLine(""); } } sb.AppendLine(""); sb.AppendLine("
UserObject TypeObjectPermission LevelAccess TypeGranted Through
{siteTitle} — {sCount} access(es)
{HtmlEncode(entry.UserDisplayName)}{guestBadge}{HtmlEncode(entry.ObjectType)}{HtmlEncode(entry.ObjectTitle)}{accessBadge}{HtmlEncode(entry.GrantedThrough)}
"); sb.AppendLine("
"); // ── INLINE JS ───────────────────────────────────────────────────────── sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); return sb.ToString(); } /// /// Writes the HTML report to the specified file path using UTF-8 without BOM. /// public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct, ReportBranding? branding = null) { var html = BuildHtml(entries, branding); await File.WriteAllTextAsync(filePath, html, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct); } /// Returns a colored badge span for the given access type. private static string AccessTypeBadge(AccessType accessType) => accessType switch { AccessType.Direct => "Direct", AccessType.Group => "Group", AccessType.Inherited => "Inherited", _ => $"{HtmlEncode(accessType.ToString())}" }; /// Minimal HTML encoding for text content and attribute values. private static string HtmlEncode(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; return value .Replace("&", "&") .Replace("<", "<") .Replace(">", ">") .Replace("\"", """) .Replace("'", "'"); } }