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);