up
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
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
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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)
|
||||
{
|
||||
return new DateTimeOffset(nsDate.Date, 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);
|
||||
Reference in New Issue
Block a user