using System.Collections.ObjectModel;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Analyzers.OS.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Analyzers;
using StellaOps.Scanner.Analyzers.OS.Helpers;
namespace StellaOps.Scanner.Analyzers.OS.Pkgutil;
///
/// Analyzes macOS pkgutil receipts to extract installed package information.
/// Parses receipt plists from /var/db/receipts/ and optionally enumerates
/// installed files from corresponding BOM files.
///
internal sealed class PkgutilPackageAnalyzer : OsPackageAnalyzerBase
{
private static readonly IReadOnlyList EmptyPackages =
new ReadOnlyCollection(Array.Empty());
private readonly PkgutilReceiptParser _receiptParser = new();
private readonly BomParser _bomParser = new();
///
/// Maximum number of files to enumerate from BOM per package.
///
private const int MaxFilesPerPackage = 1000;
public PkgutilPackageAnalyzer(ILogger logger)
: base(logger)
{
}
public override string AnalyzerId => "pkgutil";
protected override ValueTask ExecuteCoreAsync(
OSPackageAnalyzerContext context,
CancellationToken cancellationToken)
{
var receiptsPath = Path.Combine(context.RootPath, "var", "db", "receipts");
if (!Directory.Exists(receiptsPath))
{
Logger.LogInformation("pkgutil receipts directory not found at {Path}; skipping analyzer.", receiptsPath);
return ValueTask.FromResult(ExecutionResult.FromPackages(EmptyPackages));
}
var receipts = _receiptParser.DiscoverReceipts(context.RootPath, cancellationToken);
if (receipts.Count == 0)
{
Logger.LogInformation("No pkgutil receipts found; skipping analyzer.");
return ValueTask.FromResult(ExecutionResult.FromPackages(EmptyPackages));
}
Logger.LogInformation("Discovered {Count} pkgutil receipts", receipts.Count);
var evidenceFactory = OsFileEvidenceFactory.Create(context.RootPath, context.Metadata);
var records = new List(receipts.Count);
foreach (var receipt in receipts)
{
cancellationToken.ThrowIfCancellationRequested();
var record = CreateRecordFromReceipt(receipt, evidenceFactory, cancellationToken);
if (record is not null)
{
records.Add(record);
}
}
Logger.LogInformation("Created {Count} package records from pkgutil receipts", records.Count);
// Sort for deterministic output
records.Sort();
return ValueTask.FromResult(ExecutionResult.FromPackages(records));
}
private OSPackageRecord? CreateRecordFromReceipt(
PkgutilReceipt receipt,
OsFileEvidenceFactory evidenceFactory,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(receipt.Identifier) ||
string.IsNullOrWhiteSpace(receipt.Version))
{
return null;
}
// Build PURL
var purl = PackageUrlBuilder.BuildPkgutil(receipt.Identifier, receipt.Version);
// Determine architecture from identifier or install path heuristics
var architecture = DetectArchitecture(receipt);
// Extract files from BOM if available
var files = new List();
if (!string.IsNullOrWhiteSpace(receipt.SourcePath))
{
var bomPath = _bomParser.FindBomForReceipt(receipt.SourcePath);
if (bomPath is not null)
{
var bomEntries = _bomParser.Parse(bomPath, cancellationToken);
var count = 0;
foreach (var entry in bomEntries)
{
if (count >= MaxFilesPerPackage)
{
break;
}
if (!entry.IsDirectory)
{
files.Add(evidenceFactory.Create(entry.Path, IsConfigPath(entry.Path)));
count++;
}
}
}
}
// Build vendor metadata
var vendorMetadata = new Dictionary(StringComparer.Ordinal)
{
["pkgutil:identifier"] = receipt.Identifier,
["pkgutil:volume"] = receipt.VolumePath,
};
if (receipt.InstallDate.HasValue)
{
vendorMetadata["pkgutil:install_date"] = receipt.InstallDate.Value.ToString("o");
}
if (!string.IsNullOrWhiteSpace(receipt.InstallPrefixPath))
{
vendorMetadata["pkgutil:install_prefix"] = receipt.InstallPrefixPath;
}
if (!string.IsNullOrWhiteSpace(receipt.InstallProcessName))
{
vendorMetadata["pkgutil:installer"] = receipt.InstallProcessName;
}
// Extract package name from identifier (last component typically)
var name = ExtractNameFromIdentifier(receipt.Identifier);
return new OSPackageRecord(
AnalyzerId,
purl,
name,
receipt.Version,
architecture,
PackageEvidenceSource.PkgutilReceipt,
epoch: null,
release: null,
sourcePackage: ExtractVendorFromIdentifier(receipt.Identifier),
license: null,
cveHints: null,
provides: null,
depends: null,
files: files,
vendorMetadata: vendorMetadata);
}
private static string ExtractNameFromIdentifier(string identifier)
{
// Identifier format is typically: com.vendor.product or com.apple.pkg.Safari
var parts = identifier.Split('.', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
{
return identifier;
}
// Return the last meaningful part
var last = parts[^1];
// Skip common suffixes
if (parts.Length > 1 &&
(last.Equals("pkg", StringComparison.OrdinalIgnoreCase) ||
last.Equals("app", StringComparison.OrdinalIgnoreCase)))
{
return parts[^2];
}
return last;
}
private static string? ExtractVendorFromIdentifier(string identifier)
{
// Extract vendor from identifier (e.g., "com.apple.pkg.Safari" -> "apple")
var parts = identifier.Split('.', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2)
{
return parts[1];
}
return null;
}
private static string DetectArchitecture(PkgutilReceipt receipt)
{
// Check install path for architecture hints
var prefix = receipt.InstallPrefixPath ?? receipt.VolumePath;
if (!string.IsNullOrWhiteSpace(prefix))
{
if (prefix.Contains("/arm64/", StringComparison.OrdinalIgnoreCase) ||
prefix.Contains("/aarch64/", StringComparison.OrdinalIgnoreCase))
{
return "arm64";
}
if (prefix.Contains("/x86_64/", StringComparison.OrdinalIgnoreCase) ||
prefix.Contains("/amd64/", StringComparison.OrdinalIgnoreCase))
{
return "x86_64";
}
}
// Default to universal (noarch) for macOS packages
return "universal";
}
private static bool IsConfigPath(string path)
{
// Common macOS configuration paths
return path.Contains("/Preferences/", StringComparison.OrdinalIgnoreCase) ||
path.Contains("/etc/", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".plist", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".conf", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".cfg", StringComparison.OrdinalIgnoreCase);
}
}