Public/Get-MacRosettaAudit.ps1
|
function Get-MacRosettaAudit { <# .SYNOPSIS Scans macOS for Intel-only apps, LaunchAgents/Daemons, and running processes to identify Rosetta 2 usage. .DESCRIPTION Get-MacRosettaAudit inspects application bundles, launch items, and running processes on an Apple Silicon Mac to determine which binaries require Rosetta 2 (Intel-only), are Universal, or run natively. Results include code signature information, version, vendor, and dependency data. Output objects are pipeline-compatible and can be piped directly into Export-MacRosettaAuditReport. .PARAMETER IncludeSystemPaths Also scans /System/Applications, /System/Library/CoreServices, and system-level launch item directories. Requires Full Disk Access for complete results. .PARAMETER IncludeDependencies Uses otool -L to resolve shared library dependencies for each binary and flags Intel-only dependencies. Significantly increases scan time. .PARAMETER IncludeRunningProcesses Scans currently running processes via ps and detects active Rosetta usage. Enabled by default. Set to $false to skip process scanning. .PARAMETER IncludeLaunchItems Scans /Library/LaunchAgents, /Library/LaunchDaemons, and ~/Library/LaunchAgents. Enabled by default. Set to $false to skip launch item scanning. .OUTPUTS [pscustomobject] with properties: Category, DisplayName, Vendor, Version, BundleId, Status, RosettaNeeded, CurrentlyUsingRosetta, Architectures, Signed, and more. .EXAMPLE Get-MacRosettaAudit Full scan: applications, launch items, and running processes. .EXAMPLE Get-MacRosettaAudit -IncludeSystemPaths -IncludeDependencies Full scan including system paths and shared library dependency analysis. .EXAMPLE Get-MacRosettaAudit | Where-Object { $_.RosettaNeeded } Show only Intel-only binaries. .EXAMPLE Get-MacRosettaAudit | Export-MacRosettaAuditReport -Format All -OpenReport Full scan, then export HTML, CSV, and JSON reports and open the HTML report. .EXAMPLE Get-MacRosettaAudit -IncludeRunningProcesses $false -IncludeLaunchItems $false Scan applications only. .NOTES Requires macOS with PowerShell 7+. Uses only macOS built-in tools: file, lipo, otool, codesign, mdls, PlistBuddy, ps, sysctl. Full Disk Access may be required to scan all application directories and launch items. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [switch]$IncludeSystemPaths, [switch]$IncludeDependencies, [bool]$IncludeRunningProcesses = $true, [bool]$IncludeLaunchItems = $true ) $ErrorActionPreference = 'SilentlyContinue' # Versions-Banner $moduleVersion = (Get-Module -Name MacRosettaAudit).Version.ToString() Write-Host "[MacRosettaAudit] v$moduleVersion — macOS Rosetta Audit" -ForegroundColor Cyan Write-Host "" # Privilege check — LaunchDaemons and system paths require root on macOS $isRoot = ((& /usr/bin/id -u) -eq '0') Write-Verbose "Apple Silicon detected: $(Test-IsAppleSilicon)" Write-Verbose "Running as root: $isRoot" Write-Verbose "IncludeSystemPaths: $($IncludeSystemPaths.IsPresent) IncludeDependencies: $($IncludeDependencies.IsPresent) IncludeLaunchItems: $IncludeLaunchItems IncludeRunningProcesses: $IncludeRunningProcesses" if (-not $isRoot) { Write-Host "[MacRosettaAudit] Laeuft ohne root-Rechte." -ForegroundColor Yellow Write-Host " LaunchDaemons koennen unvollstaendig sein — einige Plists sind nur als root lesbar." -ForegroundColor Yellow Write-Host " Tipp fuer vollstaendige Ergebnisse:" -ForegroundColor DarkYellow Write-Host " sudo pwsh -NoProfile -Command 'Import-Module /pfad/MacRosettaAudit.psd1; Get-MacRosettaAudit'" -ForegroundColor DarkYellow Write-Host "" } else { Write-Verbose "Privileged (root) — alle Pfade erreichbar." } $records = [System.Collections.Generic.List[object]]::new() $appRoots = @( "/Applications", "/Library/Application Support" ) if ($IncludeSystemPaths) { $appRoots += @( "/System/Applications", "/System/Library/CoreServices" ) } # Pfad zu den privaten Funktionen fuer die Weitergabe an parallele Runspaces $moduleBase = (Get-Module -Name MacRosettaAudit).ModuleBase $privateDir = Join-Path $moduleBase 'Private' $includeDeps = $IncludeDependencies.IsPresent # Private Funktionen einmalig als Scriptblocks laden (kein Disk-I/O pro Item im Parallel-Block) $privateScripts = @(Get-ChildItem -Path $privateDir -Filter "*.ps1" | ForEach-Object { [scriptblock]::Create((Get-Content $_.FullName -Raw)) }) # Phase 1: Filesystem-Scan (alle Roots sequentiell — schnell) Write-Host "[MacRosettaAudit] Suche App-Bundles..." -ForegroundColor Cyan $allAppPaths = [System.Collections.Generic.List[string]]::new() $rootBundleCount = [ordered]@{} foreach ($root in $appRoots) { if (-not (Test-Path -LiteralPath $root)) { Write-Verbose " Pfad nicht vorhanden, wird uebersprungen: $root" continue } # -Depth 4: findet /Applications/App.app (Tiefe 1), Helper-Apps in Bundles (bis 4) $paths = @(Get-ChildItem -LiteralPath $root -Recurse -Depth 4 -Directory -Filter "*.app" -Force | Select-Object -ExpandProperty FullName) $rootBundleCount[$root] = $paths.Count foreach ($p in $paths) { $allAppPaths.Add($p) } Write-Verbose " $root : $($paths.Count) App-Bundles" } $totalApps = $allAppPaths.Count Write-Host "[MacRosettaAudit] $totalApps App-Bundles gefunden — starte Analyse ($([Math]::Min(8, $totalApps)) Threads)..." -ForegroundColor Cyan # Phase 2: Binary-Analyse (parallel, max. 8 Threads, mit Fortschrittsanzeige) # Hinweis: ForEach-Object -Parallel unterstuetzt keine -ErrorAction; # Fehlerbehandlung via try/catch im Block. Verbose-Ausgabe im Sammel-Loop. $processed = 0 $allAppPaths | ForEach-Object -Parallel { $appPath = $_ $privateScripts = $using:privateScripts $includeDeps = $using:includeDeps # Verbose-Overhead in Runspaces unterdrücken — Ausgabe erfolgt im Sammel-Loop $VerbosePreference = 'SilentlyContinue' # Private-Funktionen nur einmal pro Runspace laden (Runspaces werden wiederverwendet) if (-not (Get-Command Get-AppMainExecutable -ErrorAction SilentlyContinue)) { foreach ($sb in $privateScripts) { . $sb } } $name = Split-Path $appPath -Leaf try { $exe = Get-AppMainExecutable -AppPath $appPath if ($exe) { New-AuditRecord ` -Category "Application" ` -Name $name ` -SourcePath $appPath ` -BundlePath $appPath ` -ExecutablePath $exe ` -IncludeDependencies $includeDeps } } catch { <# Einzelne Fehler still ignorieren #> } } -ThrottleLimit 8 | Where-Object { $null -ne $_ } | ForEach-Object { $processed++ Write-Progress -Activity "MacRosettaAudit — App-Analyse" ` -Status "Analysiert: $processed / $totalApps" ` -PercentComplete ([int]($processed * 100 / [Math]::Max($totalApps, 1))) ` -CurrentOperation $_.DisplayName Write-Verbose " $($_.DisplayName) — $($_.Status) / $($_.Architectures)" $records.Add($_) } Write-Progress -Activity "MacRosettaAudit — App-Analyse" -Completed # Ergebnis-Zusammenfassung pro Verzeichnis foreach ($root in $rootBundleCount.Keys) { Write-Host " $root — $($rootBundleCount[$root]) Bundle(s)" -ForegroundColor Gray } Write-Host " Ausfuehrbare Binaries analysiert: $processed" -ForegroundColor Gray if ($IncludeLaunchItems) { Write-Host "[MacRosettaAudit] Scanne LaunchAgents und LaunchDaemons..." -ForegroundColor Cyan Write-Verbose "Scanning LaunchAgents and LaunchDaemons..." $launchRoots = @( "/Library/LaunchAgents", "/Library/LaunchDaemons", "$HOME/Library/LaunchAgents" ) if ($IncludeSystemPaths) { $launchRoots += @( "/System/Library/LaunchAgents", "/System/Library/LaunchDaemons" ) } foreach ($root in $launchRoots) { $isDaemonPath = $root -match 'LaunchDaemon' if (-not $isRoot -and $isDaemonPath) { Write-Verbose " [root empfohlen] $root — ohne root ggf. unvollstaendig" } if (-not (Test-Path -LiteralPath $root)) { Write-Verbose " Pfad nicht vorhanden, wird uebersprungen: $root" continue } $plists = @(Get-ChildItem -LiteralPath $root -File -Filter "*.plist" -Force) $totalPlists = $plists.Count $plistIdx = 0 $beforeCount = $records.Count Write-Verbose " $root : $totalPlists Plist(s)" foreach ($plistFile in $plists) { $plistIdx++ Write-Progress -Activity "MacRosettaAudit — LaunchItems" ` -Status "$plistIdx / $totalPlists" ` -PercentComplete ([int]($plistIdx * 100 / [Math]::Max($totalPlists, 1))) ` -CurrentOperation $plistFile.Name $plist = $plistFile.FullName Write-Verbose " -> $($plistFile.Name)" $exe = Get-LaunchItemExecutable -PlistPath $plist if (-not [string]::IsNullOrWhiteSpace($exe)) { $record = New-AuditRecord ` -Category "LaunchItem" ` -Name $plistFile.Name ` -SourcePath $plist ` -PlistPath $plist ` -ExecutablePath $exe ` -IncludeDependencies $IncludeDependencies.IsPresent $records.Add($record) Write-Verbose " Executable: $exe Status: $($record.Status)" } } Write-Progress -Activity "MacRosettaAudit — LaunchItems" -Completed $launchFound = $records.Count - $beforeCount $rootHint = if (-not $isRoot -and $isDaemonPath) { ' (ggf. unvollstaendig ohne root)' } else { '' } Write-Host " $root — $launchFound Item(s)$rootHint" -ForegroundColor Gray } } if ($IncludeRunningProcesses) { Write-Host "[MacRosettaAudit] Scanne laufende Prozesse..." -ForegroundColor Cyan Write-Verbose "Scanning running processes and detecting active Rosetta usage..." $beforeCount = $records.Count # Pruefe ob ps den 'arch' Keyword kennt. # Achtung: bei unbekanntem Keyword gibt ps die Keyword-Liste auf STDOUT aus (kein Fehler auf stderr!). # Deshalb auf konkreten Arch-Wert pruefen, nicht auf non-empty. $currentPid = [System.Diagnostics.Process]::GetCurrentProcess().Id $archTest = (/bin/ps -o arch= -p $currentPid 2>$null) $archKwdOk = ($archTest -match '^(arm64|x86_64|i386)$') Write-Verbose " ps arch-Keyword verfuegbar: $archKwdOk" if ($archKwdOk) { # Optimale Erkennung: arch-Spalte zeigt direkt x86_64 (Rosetta) vs arm64 $processLines = /bin/ps -axo pid=,user=,arch=,comm= 2>$null foreach ($line in $processLines) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $parts = $line.Trim() -split "\s+", 4 if ($parts.Count -lt 4) { continue } $pid = $parts[0] $user = $parts[1] $processArch = $parts[2] $comm = $parts[3] if (-not (Test-Path -LiteralPath $comm)) { continue } $records.Add( (New-AuditRecord ` -Category "RunningProcess" ` -Name "PID $pid" ` -SourcePath $comm ` -ExecutablePath $comm ` -ProcessId $pid ` -ProcessUser $user ` -ProcessArchitecture $processArch ` -IncludeDependencies $IncludeDependencies.IsPresent) ) Write-Verbose " PID $pid ($user) [$processArch]: $comm" } } else { # Fallback: kein arch-Keyword — nur Intel-only Binaries werden gemeldet (via RosettaNeeded). # Performance: Binaries deduplizieren — jede unique Binary nur einmal analysieren. Write-Verbose " Fallback: ps ohne arch — nur Intel-only Prozesse (dedupliziert nach Binary)" $processLines = /bin/ps -axo pid=,user=,comm= 2>$null $procByComm = [System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[pscustomobject]]]::new() foreach ($line in $processLines) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $parts = $line.Trim() -split "\s+", 3 if ($parts.Count -lt 3) { continue } $comm = $parts[2] if (-not (Test-Path -LiteralPath $comm)) { continue } if (-not $procByComm.ContainsKey($comm)) { $procByComm[$comm] = [System.Collections.Generic.List[pscustomobject]]::new() } $procByComm[$comm].Add([pscustomobject]@{ Pid = $parts[0]; User = $parts[1] }) } Write-Verbose " $($procByComm.Count) unique Binaries aus $($processLines.Count) Prozessen" foreach ($comm in $procByComm.Keys) { $archInfo = Get-BinaryArchitecture -Path $comm if (-not $archInfo.RosettaNeeded) { continue } # native / universal: ueberspringen foreach ($entry in $procByComm[$comm]) { $records.Add( (New-AuditRecord ` -Category "RunningProcess" ` -Name "PID $($entry.Pid)" ` -SourcePath $comm ` -ExecutablePath $comm ` -ProcessId $entry.Pid ` -ProcessUser $entry.User ` -ProcessArchitecture "" ` -IncludeDependencies $IncludeDependencies.IsPresent) ) Write-Verbose " PID $($entry.Pid) ($($entry.User)) [intel-only]: $comm" } } } $procFound = $records.Count - $beforeCount $rosettaProc = @($records | Select-Object -Last $procFound | Where-Object { $_.CurrentlyUsingRosetta }).Count Write-Host " Prozesse — $procFound gefunden, $rosettaProc aktiv unter Rosetta" -ForegroundColor Gray } # Summary output $total = $records.Count $rosettaCount = @($records | Where-Object { $_.RosettaNeeded }).Count $activeCount = @($records | Where-Object { $_.CurrentlyUsingRosetta }).Count $nativeCount = @($records | Where-Object { $_.Status -eq 'native' }).Count $uniCount = @($records | Where-Object { $_.Status -eq 'universal' }).Count Write-Host "" Write-Host "[MacRosettaAudit] Scan abgeschlossen — $total Eintraege gesamt" -ForegroundColor Cyan Write-Host (" Rosetta erforderlich : {0,4}" -f $rosettaCount) -ForegroundColor $(if ($rosettaCount -gt 0) { 'Yellow' } else { 'Green' }) Write-Host (" Aktuell unter Rosetta : {0,4}" -f $activeCount) -ForegroundColor $(if ($activeCount -gt 0) { 'Red' } else { 'Green' }) Write-Host (" Native arm64 : {0,4}" -f $nativeCount) -ForegroundColor Gray Write-Host (" Universal : {0,4}" -f $uniCount) -ForegroundColor Gray if (-not $isRoot) { $psd1Path = Join-Path $moduleBase 'MacRosettaAudit.psd1' Write-Host "" Write-Host " [!] Fuer vollstaendige LaunchDaemon-Abdeckung als root ausfuehren:" -ForegroundColor Yellow Write-Host " sudo pwsh -NoProfile -Command `"Import-Module '$psd1Path'; Get-MacRosettaAudit`"" -ForegroundColor DarkYellow } Write-Host "" # Deduplicate and emit to pipeline $records | Sort-Object Category, DisplayName, ExecutablePath, ProcessId -Unique } |