--- phase: 07-user-access-audit plan: 06 type: execute wave: 2 depends_on: ["07-01"] files_modified: - SharepointToolbox/Services/Export/UserAccessCsvExportService.cs - SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs autonomous: true requirements: - UACC-02 must_haves: truths: - "CSV export produces one file per audited user with summary section at top and flat data rows" - "CSV filenames include user email and date (e.g. audit_alice@contoso.com_2026-04-07.csv)" - "HTML export produces a single self-contained report with collapsible groups, sortable columns, search filter" - "HTML report has both group-by-user and group-by-site views togglable via tab/button in header" - "HTML report shows per-user summary stats and risk highlights (high-privilege, external users)" - "Both exports follow established patterns: UTF-8+BOM for CSV, inline CSS/JS for HTML" artifacts: - path: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs" provides: "CSV export for user access audit results" contains: "class UserAccessCsvExportService" - path: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs" provides: "HTML export for user access audit results" contains: "class UserAccessHtmlExportService" key_links: - from: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs" to: "SharepointToolbox/Core/Models/UserAccessEntry.cs" via: "Takes IReadOnlyList as input" pattern: "UserAccessEntry" - from: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs" to: "SharepointToolbox/Core/Models/UserAccessEntry.cs" via: "Takes IReadOnlyList as input" pattern: "UserAccessEntry" --- Implement the two export services for User Access Audit: per-user CSV files with summary headers, and a single interactive HTML report with dual-view toggle, collapsible groups, and risk highlighting. Purpose: Audit results must be exportable for compliance documentation and sharing with stakeholders. Output: UserAccessCsvExportService.cs, UserAccessHtmlExportService.cs @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/07-user-access-audit/07-CONTEXT.md @.planning/phases/07-user-access-audit/07-01-SUMMARY.md From SharepointToolbox/Core/Models/UserAccessEntry.cs: ```csharp public enum AccessType { Direct, Group, Inherited } public record UserAccessEntry( string UserDisplayName, string UserLogin, string SiteUrl, string SiteTitle, string ObjectType, string ObjectTitle, string ObjectUrl, string PermissionLevel, AccessType AccessType, string GrantedThrough, bool IsHighPrivilege, bool IsExternalUser); ``` From SharepointToolbox/Services/Export/CsvExportService.cs: ```csharp public class CsvExportService { public string BuildCsv(IReadOnlyList entries) { ... } public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct) { var csv = BuildCsv(entries); await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); } private static string Csv(string value) { /* RFC 4180 escaping */ } } ``` From SharepointToolbox/Services/Export/HtmlExportService.cs: ```csharp public class HtmlExportService { public string BuildHtml(IReadOnlyList entries) { ... } public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct) { var html = BuildHtml(entries); await File.WriteAllTextAsync(filePath, html, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct); } } // Pattern: stats cards, filter input, table, inline JS for filter, inline CSS, badges, user pills ``` Task 1: Implement UserAccessCsvExportService SharepointToolbox/Services/Export/UserAccessCsvExportService.cs Create `SharepointToolbox/Services/Export/UserAccessCsvExportService.cs`: ```csharp using System.IO; using System.Text; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services.Export; /// /// Exports user access audit results to CSV format. /// Produces one CSV file per audited user with a summary section at the top. /// public class UserAccessCsvExportService { private const string DataHeader = "\"Site\",\"Object Type\",\"Object\",\"URL\",\"Permission Level\",\"Access Type\",\"Granted Through\""; /// /// Builds a CSV string for a single user's access entries. /// Includes a summary section at the top followed by data rows. /// public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList entries) { var sb = new StringBuilder(); // Summary section var sitesCount = entries.Select(e => e.SiteUrl).Distinct().Count(); var highPrivCount = entries.Count(e => e.IsHighPrivilege); sb.AppendLine($"\"User Access Audit Report\""); sb.AppendLine($"\"User\",\"{Csv(userDisplayName)} ({Csv(userLogin)})\""); sb.AppendLine($"\"Total Accesses\",\"{entries.Count}\""); sb.AppendLine($"\"Sites\",\"{sitesCount}\""); sb.AppendLine($"\"High Privilege\",\"{highPrivCount}\""); sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\""); sb.AppendLine(); // Blank line separating summary from data // Data rows sb.AppendLine(DataHeader); foreach (var entry in entries) { sb.AppendLine(string.Join(",", new[] { Csv(entry.SiteTitle), Csv(entry.ObjectType), Csv(entry.ObjectTitle), Csv(entry.ObjectUrl), Csv(entry.PermissionLevel), Csv(entry.AccessType.ToString()), Csv(entry.GrantedThrough) })); } return sb.ToString(); } /// /// Writes one CSV file per user to the specified directory. /// File names: audit_{email}_{date}.csv /// public async Task WriteAsync( IReadOnlyList allEntries, string directoryPath, CancellationToken ct) { Directory.CreateDirectory(directoryPath); var dateStr = DateTime.Now.ToString("yyyy-MM-dd"); // Group by user var byUser = allEntries.GroupBy(e => e.UserLogin); foreach (var group in byUser) { ct.ThrowIfCancellationRequested(); var userLogin = group.Key; var displayName = group.First().UserDisplayName; var entries = group.ToList(); // Sanitize email for filename (replace @ and other invalid chars) var safeLogin = SanitizeFileName(userLogin); var fileName = $"audit_{safeLogin}_{dateStr}.csv"; var filePath = Path.Combine(directoryPath, fileName); var csv = BuildCsv(displayName, userLogin, entries); await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); } } /// /// Writes all entries to a single CSV file (alternative for single-file export). /// Used when the ViewModel export command picks a single file path. /// public async Task WriteSingleFileAsync( IReadOnlyList entries, string filePath, CancellationToken ct) { var sb = new StringBuilder(); var fullHeader = "\"User\",\"User Login\"," + DataHeader; // Summary var users = entries.Select(e => e.UserLogin).Distinct().ToList(); sb.AppendLine($"\"User Access Audit Report\""); sb.AppendLine($"\"Users Audited\",\"{users.Count}\""); sb.AppendLine($"\"Total Accesses\",\"{entries.Count}\""); sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\""); sb.AppendLine(); sb.AppendLine(fullHeader); foreach (var entry in entries) { sb.AppendLine(string.Join(",", new[] { Csv(entry.UserDisplayName), Csv(entry.UserLogin), Csv(entry.SiteTitle), Csv(entry.ObjectType), Csv(entry.ObjectTitle), Csv(entry.ObjectUrl), Csv(entry.PermissionLevel), Csv(entry.AccessType.ToString()), Csv(entry.GrantedThrough) })); } await File.WriteAllTextAsync(filePath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); } private static string Csv(string value) { if (string.IsNullOrEmpty(value)) return "\"\""; return $"\"{value.Replace("\"", "\"\"")}\""; } private static string SanitizeFileName(string name) { var invalid = Path.GetInvalidFileNameChars(); var sb = new StringBuilder(name.Length); foreach (var c in name) sb.Append(invalid.Contains(c) ? '_' : c); return sb.ToString(); } } ``` Design notes: - Two write modes: WriteAsync (per-user files to directory) and WriteSingleFileAsync (all in one file) - The ViewModel will use WriteSingleFileAsync for the SaveFileDialog export (simpler UX) - WriteAsync with per-user files available for batch export scenarios - Summary section at top of each file per CONTEXT.md decision - RFC 4180 CSV escaping following existing CsvExportService.Csv() pattern - UTF-8 with BOM for Excel compatibility (same as existing exports) cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 UserAccessCsvExportService.cs compiles, has BuildCsv for per-user CSV, WriteAsync for per-user files, WriteSingleFileAsync for combined export, RFC 4180 escaping, UTF-8+BOM encoding. Task 2: Implement UserAccessHtmlExportService SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs Create `SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs`. Follow the HtmlExportService pattern (self-contained HTML with inline CSS/JS, stats cards, filter, table). The HTML report must include: 1. **Title**: "User Access Audit Report" 2. **Stats cards** row: Total Accesses, Users Audited, Sites Scanned, High Privilege Count, External Users Count 3. **Per-user summary section**: For each user, show a card with their name, total accesses, sites count, high-privilege count. Highlight if user has Site Collection Admin access. 4. **View toggle**: Two buttons "By User" / "By Site" that show/hide the corresponding grouped table (JavaScript toggle, no page reload) 5. **Filter input**: Text filter that searches across all visible rows 6. **Table (By User view)**: Grouped by user (collapsible sections). Each group header shows user name + count. Rows: Site, Object Type, Object, Permission Level, Access Type badge, Granted Through 7. **Table (By Site view)**: Grouped by site (collapsible sections). Each group header shows site title + count. Rows: User, Object Type, Object, Permission Level, Access Type badge, Granted Through 8. **Access Type badges**: Colored badges — Direct (blue), Group (green), Inherited (gray) 9. **High-privilege rows**: Warning icon + bold text 10. **External user badge**: Orange "Guest" pill next to user name 11. **Inline JS**: - `toggleView(view)`: Shows "by-user" or "by-site" div, updates active button state - `filterTable()`: Filters visible rows in the active view - `toggleGroup(id)`: Collapses/expands a group section - `sortTable(col)`: Sorts rows within groups by column The HTML should be ~300-400 lines of generated content. Use StringBuilder like the existing HtmlExportService. Follow the exact same CSS style as HtmlExportService (same font-family, stat-card styles, table styles, badge styles) with additions for: - `.access-direct { background: #dbeafe; color: #1e40af; }` (blue) - `.access-group { background: #dcfce7; color: #166534; }` (green) - `.access-inherited { background: #f3f4f6; color: #374151; }` (gray) - `.high-priv { font-weight: 700; }` + warning icon - `.guest-badge { background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }` (reuse external-user style) - `.view-toggle button.active { background: #1a1a2e; color: #fff; }` - `.group-header { cursor: pointer; background: #f0f0f0; padding: 10px; font-weight: 600; }` The service should have: - `BuildHtml(IReadOnlyList entries)` — returns full HTML string - `WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct)` — writes to file (UTF-8 without BOM, same as HtmlExportService) cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 UserAccessHtmlExportService.cs compiles, produces self-contained HTML with: stats cards, per-user summary, dual-view toggle (by-user/by-site), collapsible groups, filter input, sortable columns, color-coded access type badges, high-privilege warnings, external user badges, inline CSS/JS. - `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors - UserAccessCsvExportService has BuildCsv + WriteAsync + WriteSingleFileAsync - UserAccessHtmlExportService has BuildHtml + WriteAsync - HTML output contains inline CSS and JS (no external dependencies) - CSV uses RFC 4180 escaping and UTF-8+BOM Both export services compile and follow established patterns. CSV produces per-user files with summary headers. HTML produces an interactive report with dual-view toggle, collapsible groups, color-coded badges, and risk highlighting. Ready for ViewModel export commands in 07-04. After completion, create `.planning/phases/07-user-access-audit/07-06-SUMMARY.md`