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