using Claunia.PropertyList; namespace StellaOps.Scanner.Analyzers.OS.Pkgutil; /// /// Parses macOS pkgutil receipt .plist files from /var/db/receipts/. /// internal sealed class PkgutilReceiptParser { /// /// Parses a pkgutil receipt plist file. /// public PkgutilReceipt? Parse(string plistPath, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(plistPath); if (!File.Exists(plistPath)) { return null; } try { using var stream = File.OpenRead(plistPath); return Parse(stream, plistPath, cancellationToken); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or FormatException) { return null; } } /// /// Parses a pkgutil receipt plist from a stream. /// public PkgutilReceipt? Parse(Stream stream, string? sourcePath = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(stream); try { var plist = PropertyListParser.Parse(stream); if (plist is not NSDictionary root) { return null; } var identifier = GetString(root, "PackageIdentifier") ?? GetString(root, "packageIdentifier"); if (string.IsNullOrWhiteSpace(identifier)) { return null; } var version = GetString(root, "PackageVersion") ?? GetString(root, "packageVersion") ?? "0.0.0"; var installDate = GetDate(root, "InstallDate"); var installPrefixPath = GetString(root, "InstallPrefixPath"); var volumePath = GetString(root, "VolumePath") ?? "/"; var installProcessName = GetString(root, "InstallProcessName"); return new PkgutilReceipt( Identifier: identifier.Trim(), Version: version.Trim(), InstallDate: installDate, InstallPrefixPath: installPrefixPath?.Trim(), VolumePath: volumePath.Trim(), InstallProcessName: installProcessName?.Trim(), SourcePath: sourcePath); } catch (Exception ex) when (ex is FormatException or InvalidOperationException or ArgumentException) { return null; } } /// /// Discovers and parses all receipt plist files in the receipts directory. /// public IReadOnlyList DiscoverReceipts( string rootPath, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(rootPath); var receiptsPath = Path.Combine(rootPath, "var", "db", "receipts"); if (!Directory.Exists(receiptsPath)) { return Array.Empty(); } var results = new List(); try { foreach (var plistFile in Directory.EnumerateFiles(receiptsPath, "*.plist")) { cancellationToken.ThrowIfCancellationRequested(); var receipt = Parse(plistFile, cancellationToken); if (receipt is not null) { results.Add(receipt); } } } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // Partial results are acceptable } return results; } private static string? GetString(NSDictionary dict, string key) { if (dict.TryGetValue(key, out var value) && value is NSString nsString) { return nsString.Content; } return null; } private static DateTimeOffset? GetDate(NSDictionary dict, string key) { if (dict.TryGetValue(key, out var value) && value is NSDate nsDate) { var dateTime = nsDate.Date; if (dateTime.Kind == DateTimeKind.Local) { dateTime = dateTime.ToUniversalTime(); } else if (dateTime.Kind == DateTimeKind.Unspecified) { dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); } return new DateTimeOffset(dateTime, TimeSpan.Zero); } return null; } } /// /// Represents parsed macOS pkgutil receipt metadata. /// internal sealed record PkgutilReceipt( string Identifier, string Version, DateTimeOffset? InstallDate, string? InstallPrefixPath, string VolumePath, string? InstallProcessName, string? SourcePath);