Public/Export-MacRosettaAuditReport.ps1
|
function Export-MacRosettaAuditReport { <# .SYNOPSIS Exports MacRosettaAudit results to HTML, CSV, and/or JSON reports. .DESCRIPTION Takes pipeline output from Get-MacRosettaAudit and generates one or more report files. The HTML report is fully self-contained (inline CSS and JavaScript) and works completely offline. It includes a summary dashboard, filterable/sortable table, and a detail side-panel for each entry. Sorting only applies to the main result table, not to the detail inner tables. .PARAMETER InputObject Audit records from Get-MacRosettaAudit. Accepts pipeline input. .PARAMETER OutputPath Directory where report files are written. Defaults to ~/Desktop/MacRosettaAudit. .PARAMETER Format Report format to generate. Accepted values: Html, Csv, Json, All. Defaults to Html. .PARAMETER OpenReport Opens the generated HTML report in the default browser after export. Only applicable when Format is Html or All. .EXAMPLE Get-MacRosettaAudit | Export-MacRosettaAuditReport Full scan and export to HTML on the Desktop. .EXAMPLE Get-MacRosettaAudit | Export-MacRosettaAuditReport -Format All -OutputPath ~/audit Export HTML, CSV, and JSON to ~/audit. .EXAMPLE Get-MacRosettaAudit -IncludeDependencies | Export-MacRosettaAuditReport -Format Html -OpenReport Scan with dependency analysis, export HTML, and open it immediately. .NOTES Requires macOS with PowerShell 7+. The HTML report is fully offline-capable — no external CDN or internet connection required. #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)] [pscustomobject[]]$InputObject, [string]$OutputPath = (Join-Path $HOME 'Desktop/MacRosettaAudit'), [ValidateSet('Html', 'Csv', 'Json', 'All')] [string]$Format = 'Html', [switch]$OpenReport ) begin { $allRecords = [System.Collections.Generic.List[object]]::new() } process { foreach ($item in $InputObject) { $allRecords.Add($item) } } end { if ($allRecords.Count -eq 0) { Write-Warning "No audit records received. Pipe Get-MacRosettaAudit output into Export-MacRosettaAuditReport." return } New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null $deduped = $allRecords | Sort-Object Category, DisplayName, ExecutablePath, ProcessId -Unique $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $hostname = $env:COMPUTERNAME $csvPath = Join-Path $OutputPath "rosetta-audit.csv" $jsonPath = Join-Path $OutputPath "rosetta-audit.json" $htmlPath = Join-Path $OutputPath "rosetta-audit.html" if ($Format -in 'Csv', 'All') { $deduped | Select-Object ` Category, DisplayName, Vendor, Version, BundleDisplayName, BundleName, BundleId, BundleExecutable, BundleShortVersion, BundleVersion, Status, RosettaNeeded, CurrentlyUsingRosetta, ProcessArchitecture, RosettaRuntimeReason, Architectures, Reason, Signed, SignatureId, TeamIdentifier, Authority, DependencyCount, IntelDepCount, ExecutablePath, SourcePath, BundlePath, PlistPath, LaunchLabel, ProcessId, ProcessUser | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8 Write-Host "CSV : $csvPath" } if ($Format -in 'Json', 'All') { $deduped | ConvertTo-Json -Depth 8 | Out-File -FilePath $jsonPath -Encoding UTF8 Write-Host "JSON: $jsonPath" } if ($Format -in 'Html', 'All') { $total = $deduped.Count $rosettaCount = @($deduped | Where-Object { $_.RosettaNeeded }).Count $runtimeRosettaCount = @($deduped | Where-Object { $_.CurrentlyUsingRosetta }).Count $nativeCount = @($deduped | Where-Object { $_.Status -eq "native" }).Count $universalCount = @($deduped | Where-Object { $_.Status -eq "universal" }).Count $unknownCount = @($deduped | Where-Object { $_.Status -eq "unknown" }).Count $intelDepCount = @($deduped | Where-Object { $_.IntelDepCount -gt 0 }).Count $rows = "" foreach ($r in $deduped) { $statusLabel = switch ($r.Status) { "rosetta" { "Rosetta erforderlich" } "native" { "Native" } "universal" { "Universal" } "missing" { "Fehlt" } default { "Unbekannt" } } $runtimeLabel = if ($r.CurrentlyUsingRosetta) { "Ja" } else { "Nein" } $runtimeClass = if ($r.CurrentlyUsingRosetta) { "runtimeYes" } else { "runtimeNo" } $runtimeData = if ($r.CurrentlyUsingRosetta) { "true" } else { "false" } $depHtml = if ($r.Dependencies -and $r.Dependencies.Count -gt 0) { ($r.Dependencies | ForEach-Object { "<li><code>$(ConvertTo-HtmlSafe $_)</code></li>" }) -join "`n" } else { "<li>Keine Abhängigkeiten ausgewertet oder gefunden.</li>" } $intelDepHtml = if ($r.IntelDependencies -and $r.IntelDependencies.Count -gt 0) { ($r.IntelDependencies | ForEach-Object { "<li><code>$(ConvertTo-HtmlSafe $_)</code></li>" }) -join "`n" } else { "<li>Keine Intel-only Dependencies gefunden.</li>" } $rows += @" <tr class="$($r.Status)" data-status="$($r.Status)" data-runtime="$runtimeData" data-category="$(ConvertTo-HtmlSafe $r.Category)"> <td><span class="badge $($r.Status)">$(ConvertTo-HtmlSafe $statusLabel)</span></td> <td><strong>$(ConvertTo-HtmlSafe $r.DisplayName)</strong><br><small>$(ConvertTo-HtmlSafe $r.BundleId)</small></td> <td>$(ConvertTo-HtmlSafe $r.Vendor)</td> <td>$(ConvertTo-HtmlSafe $r.Version)</td> <td>$(ConvertTo-HtmlSafe $r.Category)</td> <td><code>$(ConvertTo-HtmlSafe $r.Architectures)</code></td> <td><span class="badge $runtimeClass">$runtimeLabel</span></td> <td><code>$(ConvertTo-HtmlSafe $r.ProcessArchitecture)</code></td> <td>$($r.DependencyCount)</td> <td>$($r.IntelDepCount)</td> <td> <button class="detailButton" onclick="openDetails(this)">Details</button> <template class="detailTemplate"> <div class="drawerContent"> <h3>$(ConvertTo-HtmlSafe $r.DisplayName)</h3> <p class="drawerSubline">$(ConvertTo-HtmlSafe $r.Vendor) · $(ConvertTo-HtmlSafe $r.Version) · $(ConvertTo-HtmlSafe $statusLabel)</p> <h4>Allgemein</h4> <table class="inner"> <tr><th>DisplayName</th><td>$(ConvertTo-HtmlSafe $r.DisplayName)</td></tr> <tr><th>Vendor</th><td>$(ConvertTo-HtmlSafe $r.Vendor)</td></tr> <tr><th>Version</th><td>$(ConvertTo-HtmlSafe $r.Version)</td></tr> <tr><th>Bundle Display Name</th><td>$(ConvertTo-HtmlSafe $r.BundleDisplayName)</td></tr> <tr><th>Bundle Name</th><td>$(ConvertTo-HtmlSafe $r.BundleName)</td></tr> <tr><th>Bundle ID</th><td><code>$(ConvertTo-HtmlSafe $r.BundleId)</code></td></tr> <tr><th>Bundle Executable</th><td><code>$(ConvertTo-HtmlSafe $r.BundleExecutable)</code></td></tr> <tr><th>Bundle Version</th><td>$(ConvertTo-HtmlSafe $r.BundleShortVersion) / $(ConvertTo-HtmlSafe $r.BundleVersion)</td></tr> <tr><th>Status</th><td>$(ConvertTo-HtmlSafe $statusLabel)</td></tr> <tr><th>Grund</th><td>$(ConvertTo-HtmlSafe $r.Reason)</td></tr> </table> <h4>Aktuelle Rosetta-Nutzung</h4> <table class="inner"> <tr><th>Aktuell Rosetta</th><td>$(ConvertTo-HtmlSafe $runtimeLabel)</td></tr> <tr><th>Prozessarchitektur</th><td><code>$(ConvertTo-HtmlSafe $r.ProcessArchitecture)</code></td></tr> <tr><th>Prozess-ID</th><td>$(ConvertTo-HtmlSafe $r.ProcessId)</td></tr> <tr><th>Prozess-User</th><td>$(ConvertTo-HtmlSafe $r.ProcessUser)</td></tr> <tr><th>Bewertung</th><td>$(ConvertTo-HtmlSafe $r.RosettaRuntimeReason)</td></tr> </table> <h4>Pfade</h4> <table class="inner"> <tr><th>Executable</th><td><code>$(ConvertTo-HtmlSafe $r.ExecutablePath)</code></td></tr> <tr><th>Quelle</th><td><code>$(ConvertTo-HtmlSafe $r.SourcePath)</code></td></tr> <tr><th>Bundle</th><td><code>$(ConvertTo-HtmlSafe $r.BundlePath)</code></td></tr> <tr><th>Plist</th><td><code>$(ConvertTo-HtmlSafe $r.PlistPath)</code></td></tr> <tr><th>Launch Label</th><td><code>$(ConvertTo-HtmlSafe $r.LaunchLabel)</code></td></tr> </table> <h4>Signatur</h4> <table class="inner"> <tr><th>Signiert</th><td>$(ConvertTo-HtmlSafe $r.Signed)</td></tr> <tr><th>Identifier</th><td><code>$(ConvertTo-HtmlSafe $r.SignatureId)</code></td></tr> <tr><th>Team ID</th><td><code>$(ConvertTo-HtmlSafe $r.TeamIdentifier)</code></td></tr> <tr><th>Authority</th><td>$(ConvertTo-HtmlSafe $r.Authority)</td></tr> </table> <h4>Intel-only Dependencies</h4> <ul>$intelDepHtml</ul> <h4>Alle Dependencies</h4> <ul>$depHtml</ul> </div> </template> </td> </tr> "@ } $html = @" <!doctype html> <html lang="de"> <head> <meta charset="utf-8"> <title>MacRosettaAudit Report</title> <style> :root { --bg: #f5f7fb; --card: #ffffff; --text: #1f2937; --muted: #6b7280; --line: #e5e7eb; --bad: #fee2e2; --badText: #991b1b; --good: #dcfce7; --goodText: #166534; --warn: #fef3c7; --warnText: #92400e; --blue: #dbeafe; --blueText: #1e40af; --purple: #ede9fe; --purpleText: #5b21b6; } body { margin: 0; background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } header { padding: 28px 36px; background: #111827; color: white; } header h1 { margin: 0 0 8px 0; font-size: 28px; } header p { margin: 0; color: #d1d5db; } main { padding: 24px 36px; } .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 22px; } .card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px; box-shadow: 0 3px 10px rgba(0,0,0,.04); } .card .number { font-size: 28px; font-weight: 700; } .card .label { color: var(--muted); font-size: 13px; } .toolbar { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; align-items: center; } input, select, button { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; background: white; font-size: 14px; } input { min-width: 420px; } button { cursor: pointer; } table { width: 100%; border-collapse: collapse; background: white; border-radius: 14px; overflow: hidden; } th { text-align: left; background: #f3f4f6; color: #374151; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; padding: 10px; border-bottom: 1px solid var(--line); cursor: pointer; } td { padding: 10px; border-bottom: 1px solid var(--line); vertical-align: top; font-size: 13px; } tr:hover { background: #f9fafb; } code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 12px; word-break: break-all; } .badge { display: inline-block; padding: 4px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; } .badge.rosetta { background: var(--bad); color: var(--badText); } .badge.native { background: var(--good); color: var(--goodText); } .badge.universal { background: var(--blue); color: var(--blueText); } .badge.unknown, .badge.missing { background: var(--warn); color: var(--warnText); } .badge.runtimeYes { background: var(--purple); color: var(--purpleText); } .badge.runtimeNo { background: #f3f4f6; color: #374151; } .detailButton { white-space: nowrap; font-weight: 600; } .inner { margin: 8px 0 18px 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; } .inner th { width: 190px; text-transform: none; letter-spacing: 0; cursor: default; background: #f9fafb; pointer-events: none; } .inner td, .inner th { font-size: 12px; padding: 8px; vertical-align: top; } .hidden { display: none; } .footer { margin-top: 18px; color: var(--muted); font-size: 12px; } #drawerOverlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 1000; } #drawerOverlay.open { display: block; } #detailDrawer { position: fixed; top: 0; right: -760px; width: 720px; max-width: 92vw; height: 100vh; background: white; z-index: 1001; box-shadow: -10px 0 30px rgba(0,0,0,.18); transition: right .22s ease; overflow-y: auto; } #detailDrawer.open { right: 0; } .drawerHeader { position: sticky; top: 0; background: #111827; color: white; padding: 16px 18px; display: flex; justify-content: space-between; align-items: center; z-index: 1002; } .drawerHeader h2 { margin: 0; font-size: 20px; } .drawerHeader button { background: white; color: #111827; border: 0; font-weight: 700; } .drawerContent { padding: 18px; text-align: left; } .drawerContent h3 { margin: 0 0 4px 0; font-size: 22px; } .drawerContent h4 { margin: 20px 0 8px 0; font-size: 15px; } .drawerSubline { margin: 0 0 16px 0; color: var(--muted); } .drawerContent ul { padding-left: 20px; } .drawerContent li { margin-bottom: 6px; } </style> </head> <body> <header> <h1>MacRosettaAudit Report</h1> <p>Erstellt am $timestamp · Host: $hostname</p> </header> <main> <section class="cards"> <div class="card"><div class="number">$total</div><div class="label">Gesamt</div></div> <div class="card"><div class="number">$runtimeRosettaCount</div><div class="label">Nutzen Rosetta aktuell</div></div> <div class="card"><div class="number">$rosettaCount</div><div class="label">Rosetta erforderlich</div></div> <div class="card"><div class="number">$nativeCount</div><div class="label">Native</div></div> <div class="card"><div class="number">$universalCount</div><div class="label">Universal</div></div> <div class="card"><div class="number">$intelDepCount</div><div class="label">Intel-only Dependencies</div></div> <div class="card"><div class="number">$unknownCount</div><div class="label">Unbekannt</div></div> </section> <section class="toolbar"> <input id="search" placeholder="Suche nach DisplayName, Vendor, Version, Bundle ID, Pfad, Team ID..." /> <select id="statusFilter"> <option value="all">Alle Status</option> <option value="rosetta">Nur Rosetta erforderlich</option> <option value="native">Nur Native</option> <option value="universal">Nur Universal</option> <option value="unknown">Nur Unbekannt</option> <option value="missing">Nur Fehlend</option> </select> <select id="runtimeFilter"> <option value="all">Aktuelle Rosetta-Nutzung: alle</option> <option value="true">Nur aktuell Rosetta</option> <option value="false">Nicht aktuell Rosetta</option> </select> <select id="categoryFilter"> <option value="all">Alle Kategorien</option> <option value="Application">Applications</option> <option value="LaunchItem">LaunchItems</option> <option value="RunningProcess">RunningProcesses</option> </select> </section> <table id="resultTable"> <thead> <tr> <th>Status</th> <th>DisplayName</th> <th>Vendor</th> <th>Version</th> <th>Kategorie</th> <th>Architektur</th> <th>Aktuell Rosetta</th> <th>Prozess-Arch</th> <th>Deps</th> <th>Intel Deps</th> <th>Details</th> </tr> </thead> <tbody> $rows </tbody> </table> <div class="footer"> Hinweis: „Rosetta erforderlich" bedeutet Intel-only Binary. „Aktuell Rosetta" bedeutet, dass ein laufender Prozess aktuell als x86_64 erkannt wurde oder die laufende Binary Intel-only ist. </div> </main> <div id="drawerOverlay" onclick="closeDetails()"></div> <aside id="detailDrawer"> <div class="drawerHeader"> <h2>Details</h2> <button onclick="closeDetails()">Schließen</button> </div> <div id="drawerBody"></div> </aside> <script> const search = document.getElementById("search"); const statusFilter = document.getElementById("statusFilter"); const runtimeFilter = document.getElementById("runtimeFilter"); const categoryFilter = document.getElementById("categoryFilter"); const tableRows = Array.from(document.querySelectorAll("#resultTable > tbody > tr")); function applyFilters() { const q = search.value.toLowerCase(); const status = statusFilter.value; const runtime = runtimeFilter.value; const category = categoryFilter.value; tableRows.forEach(row => { const text = row.innerText.toLowerCase(); const rowStatus = row.dataset.status || ""; const rowRuntime = (row.dataset.runtime || "false").toLowerCase(); const rowCategory = row.dataset.category || ""; let visible = true; if (q && !text.includes(q)) visible = false; if (status !== "all" && rowStatus !== status) visible = false; if (runtime !== "all" && rowRuntime !== runtime) visible = false; if (category !== "all" && rowCategory !== category) visible = false; row.classList.toggle("hidden", !visible); }); } function openDetails(button) { const template = button.parentElement.querySelector(".detailTemplate"); const drawer = document.getElementById("detailDrawer"); const overlay = document.getElementById("drawerOverlay"); const body = document.getElementById("drawerBody"); body.innerHTML = template.innerHTML; drawer.classList.add("open"); overlay.classList.add("open"); } function closeDetails() { document.getElementById("detailDrawer").classList.remove("open"); document.getElementById("drawerOverlay").classList.remove("open"); document.getElementById("drawerBody").innerHTML = ""; } search.addEventListener("input", applyFilters); statusFilter.addEventListener("change", applyFilters); runtimeFilter.addEventListener("change", applyFilters); categoryFilter.addEventListener("change", applyFilters); document.querySelectorAll("#resultTable > thead > tr > th").forEach((th, index) => { th.addEventListener("click", () => { const tbody = document.querySelector("#resultTable > tbody"); const sorted = tableRows.sort((a, b) => { const av = (a.children[index]?.innerText || "").trim().toLowerCase(); const bv = (b.children[index]?.innerText || "").trim().toLowerCase(); return av.localeCompare(bv, "de", { numeric: true }); }); sorted.forEach(row => tbody.appendChild(row)); applyFilters(); }); }); document.addEventListener("keydown", event => { if (event.key === "Escape") closeDetails(); }); </script> </body> </html> "@ $html | Out-File -FilePath $htmlPath -Encoding UTF8 Write-Host "HTML: $htmlPath" } Write-Host "" Write-Host "Fertig. Ausgabeverzeichnis: $OutputPath" -ForegroundColor Green if ($OpenReport -and ($Format -in 'Html', 'All')) { /usr/bin/open "$htmlPath" } } } |