up
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
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
This commit is contained in:
@@ -46,12 +46,13 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
|
||||
public override string AnalyzerId => "macos-bundle";
|
||||
|
||||
protected override ValueTask<IReadOnlyList<OSPackageRecord>> ExecuteCoreAsync(
|
||||
protected override ValueTask<ExecutionResult> ExecuteCoreAsync(
|
||||
OSPackageAnalyzerContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var records = new List<OSPackageRecord>();
|
||||
var warnings = new List<string>();
|
||||
var warnings = new List<AnalyzerWarning>();
|
||||
var evidenceFactory = OsFileEvidenceFactory.Create(context.RootPath, context.Metadata);
|
||||
|
||||
// Scan standard application paths
|
||||
foreach (var appPath in ApplicationPaths)
|
||||
@@ -66,7 +67,7 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
|
||||
try
|
||||
{
|
||||
DiscoverBundles(fullPath, records, warnings, 0, cancellationToken);
|
||||
DiscoverBundles(context.RootPath, evidenceFactory, fullPath, records, warnings, 0, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
@@ -87,7 +88,7 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
var userAppsPath = Path.Combine(userDir, "Applications");
|
||||
if (Directory.Exists(userAppsPath))
|
||||
{
|
||||
DiscoverBundles(userAppsPath, records, warnings, 0, cancellationToken);
|
||||
DiscoverBundles(context.RootPath, evidenceFactory, userAppsPath, records, warnings, 0, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,25 +101,27 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
if (records.Count == 0)
|
||||
{
|
||||
Logger.LogInformation("No application bundles found; skipping analyzer.");
|
||||
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(EmptyPackages);
|
||||
return ValueTask.FromResult(ExecutionResult.FromPackages(EmptyPackages));
|
||||
}
|
||||
|
||||
foreach (var warning in warnings.Take(10)) // Limit warning output
|
||||
{
|
||||
Logger.LogWarning("Bundle scan warning: {Warning}", warning);
|
||||
Logger.LogWarning("Bundle scan warning ({Code}): {Message}", warning.Code, warning.Message);
|
||||
}
|
||||
|
||||
Logger.LogInformation("Discovered {Count} application bundles", records.Count);
|
||||
|
||||
// Sort for deterministic output
|
||||
records.Sort();
|
||||
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(records);
|
||||
return ValueTask.FromResult(ExecutionResult.From(records, warnings));
|
||||
}
|
||||
|
||||
private void DiscoverBundles(
|
||||
string rootPath,
|
||||
OsFileEvidenceFactory evidenceFactory,
|
||||
string searchPath,
|
||||
List<OSPackageRecord> records,
|
||||
List<string> warnings,
|
||||
List<AnalyzerWarning> warnings,
|
||||
int depth,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -150,7 +153,7 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
// Check if this is an app bundle
|
||||
if (name.EndsWith(".app", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var record = AnalyzeBundle(entry, warnings, cancellationToken);
|
||||
var record = AnalyzeBundle(rootPath, evidenceFactory, entry, warnings, cancellationToken);
|
||||
if (record is not null)
|
||||
{
|
||||
records.Add(record);
|
||||
@@ -159,14 +162,16 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
else
|
||||
{
|
||||
// Recurse into subdirectories (e.g., for nested apps)
|
||||
DiscoverBundles(entry, records, warnings, depth + 1, cancellationToken);
|
||||
DiscoverBundles(rootPath, evidenceFactory, entry, records, warnings, depth + 1, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OSPackageRecord? AnalyzeBundle(
|
||||
string rootPath,
|
||||
OsFileEvidenceFactory evidenceFactory,
|
||||
string bundlePath,
|
||||
List<string> warnings,
|
||||
List<AnalyzerWarning> warnings,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Find and parse Info.plist
|
||||
@@ -179,14 +184,18 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
|
||||
if (!File.Exists(infoPlistPath))
|
||||
{
|
||||
warnings.Add($"No Info.plist found in {bundlePath}");
|
||||
warnings.Add(AnalyzerWarning.From(
|
||||
"macos-bundle/missing-info-plist",
|
||||
$"No Info.plist found in {ToRootfsStylePath(OsPath.TryGetRootfsRelative(rootPath, bundlePath)) ?? "bundle"}"));
|
||||
return null;
|
||||
}
|
||||
|
||||
var bundleInfo = _infoPlistParser.Parse(infoPlistPath, cancellationToken);
|
||||
if (bundleInfo is null)
|
||||
{
|
||||
warnings.Add($"Failed to parse Info.plist in {bundlePath}");
|
||||
warnings.Add(AnalyzerWarning.From(
|
||||
"macos-bundle/invalid-info-plist",
|
||||
$"Failed to parse Info.plist in {ToRootfsStylePath(OsPath.TryGetRootfsRelative(rootPath, bundlePath)) ?? "bundle"}"));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -208,10 +217,10 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
var purl = PackageUrlBuilder.BuildMacOsBundle(bundleInfo.BundleIdentifier, version);
|
||||
|
||||
// Build vendor metadata
|
||||
var vendorMetadata = BuildVendorMetadata(bundleInfo, entitlements, codeResourcesHash, bundlePath);
|
||||
var vendorMetadata = BuildVendorMetadata(rootPath, bundleInfo, entitlements, codeResourcesHash, bundlePath);
|
||||
|
||||
// Discover key files
|
||||
var files = DiscoverBundleFiles(bundlePath, bundleInfo);
|
||||
var files = DiscoverBundleFiles(rootPath, evidenceFactory, bundlePath, bundleInfo);
|
||||
|
||||
// Extract display name
|
||||
var displayName = bundleInfo.BundleDisplayName ?? bundleInfo.BundleName;
|
||||
@@ -221,7 +230,7 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
purl,
|
||||
displayName,
|
||||
version,
|
||||
DetermineArchitecture(bundlePath),
|
||||
DetermineArchitecture(bundlePath, bundleInfo),
|
||||
PackageEvidenceSource.MacOsBundle,
|
||||
epoch: null,
|
||||
release: bundleInfo.Version != version ? bundleInfo.Version : null,
|
||||
@@ -235,16 +244,19 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> BuildVendorMetadata(
|
||||
string rootPath,
|
||||
BundleInfo bundleInfo,
|
||||
BundleEntitlements entitlements,
|
||||
string? codeResourcesHash,
|
||||
string bundlePath)
|
||||
{
|
||||
var bundlePathRelative = OsPath.TryGetRootfsRelative(rootPath, bundlePath);
|
||||
|
||||
var metadata = new Dictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["macos:bundle_id"] = bundleInfo.BundleIdentifier,
|
||||
["macos:bundle_type"] = bundleInfo.BundlePackageType,
|
||||
["macos:bundle_path"] = bundlePath,
|
||||
["macos:bundle_path"] = ToRootfsStylePath(bundlePathRelative) ?? bundlePath,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bundleInfo.MinimumSystemVersion))
|
||||
@@ -304,7 +316,11 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
}
|
||||
}
|
||||
|
||||
private static List<OSPackageFileEvidence> DiscoverBundleFiles(string bundlePath, BundleInfo bundleInfo)
|
||||
private static List<OSPackageFileEvidence> DiscoverBundleFiles(
|
||||
string rootPath,
|
||||
OsFileEvidenceFactory evidenceFactory,
|
||||
string bundlePath,
|
||||
BundleInfo bundleInfo)
|
||||
{
|
||||
var files = new List<OSPackageFileEvidence>();
|
||||
|
||||
@@ -317,44 +333,99 @@ internal sealed class MacOsBundleAnalyzer : OsPackageAnalyzerBase
|
||||
var execPath = Path.Combine(contentsPath, "MacOS", bundleInfo.Executable);
|
||||
if (File.Exists(execPath))
|
||||
{
|
||||
files.Add(new OSPackageFileEvidence(
|
||||
$"Contents/MacOS/{bundleInfo.Executable}",
|
||||
layerDigest: null,
|
||||
sha256: null,
|
||||
sizeBytes: null,
|
||||
isConfigFile: false));
|
||||
var relative = OsPath.TryGetRootfsRelative(rootPath, execPath);
|
||||
if (relative is not null)
|
||||
{
|
||||
files.Add(evidenceFactory.Create(relative, isConfigFile: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info.plist
|
||||
var infoPlistRelative = "Contents/Info.plist";
|
||||
if (File.Exists(Path.Combine(bundlePath, infoPlistRelative)))
|
||||
var infoPlistPath = Path.Combine(bundlePath, "Contents", "Info.plist");
|
||||
if (!File.Exists(infoPlistPath))
|
||||
{
|
||||
files.Add(new OSPackageFileEvidence(
|
||||
infoPlistRelative,
|
||||
layerDigest: null,
|
||||
sha256: null,
|
||||
sizeBytes: null,
|
||||
isConfigFile: true));
|
||||
infoPlistPath = Path.Combine(bundlePath, "Info.plist");
|
||||
}
|
||||
|
||||
if (File.Exists(infoPlistPath))
|
||||
{
|
||||
var relative = OsPath.TryGetRootfsRelative(rootPath, infoPlistPath);
|
||||
if (relative is not null)
|
||||
{
|
||||
files.Add(evidenceFactory.Create(relative, isConfigFile: true));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static string DetermineArchitecture(string bundlePath)
|
||||
private static string DetermineArchitecture(string bundlePath, BundleInfo bundleInfo)
|
||||
{
|
||||
// Check for universal binary indicators
|
||||
if (!string.IsNullOrWhiteSpace(bundleInfo.Executable))
|
||||
{
|
||||
var execPath = Path.Combine(bundlePath, "Contents", "MacOS", bundleInfo.Executable);
|
||||
var detected = TryDetectMachOArchitecture(execPath);
|
||||
if (!string.IsNullOrWhiteSpace(detected))
|
||||
{
|
||||
return detected;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to universal (noarch) for macOS bundles.
|
||||
var macosPath = Path.Combine(bundlePath, "Contents", "MacOS");
|
||||
if (Directory.Exists(macosPath))
|
||||
{
|
||||
// Look for architecture-specific subdirectories or lipo info
|
||||
// For now, default to universal
|
||||
return "universal";
|
||||
}
|
||||
|
||||
return "universal";
|
||||
}
|
||||
|
||||
private static string? TryDetectMachOArchitecture(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
Span<byte> header = stackalloc byte[16];
|
||||
var read = stream.Read(header);
|
||||
if (read < 12)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var magic = BitConverter.ToUInt32(header[..4]);
|
||||
return magic switch
|
||||
{
|
||||
0xCAFEBABE or 0xBEBAFECA => "universal",
|
||||
0xFEEDFACE or 0xCEFAEDFE => MapMachCpuType(BitConverter.ToUInt32(header.Slice(4, 4))),
|
||||
0xFEEDFACF or 0xCFFAEDFE => MapMachCpuType(BitConverter.ToUInt32(header.Slice(4, 4))),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? MapMachCpuType(uint cpuType) => cpuType switch
|
||||
{
|
||||
0x00000007 => "x86",
|
||||
0x01000007 => "x86_64",
|
||||
0x0000000C => "arm",
|
||||
0x0100000C => "arm64",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string? ToRootfsStylePath(string? relativePath)
|
||||
=> relativePath is null ? null : "/" + relativePath.TrimStart('/');
|
||||
|
||||
private static string? ExtractVendorFromBundleId(string bundleId)
|
||||
{
|
||||
var parts = bundleId.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
Reference in New Issue
Block a user