Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
155 lines
4.7 KiB
C#
155 lines
4.7 KiB
C#
using Claunia.PropertyList;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.OS.Pkgutil;
|
|
|
|
/// <summary>
|
|
/// Parses macOS pkgutil receipt .plist files from /var/db/receipts/.
|
|
/// </summary>
|
|
internal sealed class PkgutilReceiptParser
|
|
{
|
|
/// <summary>
|
|
/// Parses a pkgutil receipt plist file.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a pkgutil receipt plist from a stream.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Discovers and parses all receipt plist files in the receipts directory.
|
|
/// </summary>
|
|
public IReadOnlyList<PkgutilReceipt> DiscoverReceipts(
|
|
string rootPath,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
|
|
|
var receiptsPath = Path.Combine(rootPath, "var", "db", "receipts");
|
|
if (!Directory.Exists(receiptsPath))
|
|
{
|
|
return Array.Empty<PkgutilReceipt>();
|
|
}
|
|
|
|
var results = new List<PkgutilReceipt>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents parsed macOS pkgutil receipt metadata.
|
|
/// </summary>
|
|
internal sealed record PkgutilReceipt(
|
|
string Identifier,
|
|
string Version,
|
|
DateTimeOffset? InstallDate,
|
|
string? InstallPrefixPath,
|
|
string VolumePath,
|
|
string? InstallProcessName,
|
|
string? SourcePath);
|