340 lines
12 KiB
C#
340 lines
12 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.Scanner.Analyzers.OS.MacOsBundle;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Scanner.Analyzers.OS.MacOsBundle.Tests;
|
|
|
|
public sealed class MacOsBundleAnalyzerTests
|
|
{
|
|
private static readonly string FixturesRoot = Path.Combine(
|
|
AppContext.BaseDirectory,
|
|
"Fixtures");
|
|
|
|
private readonly MacOsBundleAnalyzer _analyzer;
|
|
private readonly ILogger _logger;
|
|
|
|
public MacOsBundleAnalyzerTests()
|
|
{
|
|
_logger = NullLoggerFactory.Instance.CreateLogger<MacOsBundleAnalyzer>();
|
|
_analyzer = new MacOsBundleAnalyzer((ILogger<MacOsBundleAnalyzer>)_logger);
|
|
}
|
|
|
|
private OSPackageAnalyzerContext CreateContext(string rootPath)
|
|
{
|
|
return new OSPackageAnalyzerContext(
|
|
rootPath,
|
|
workspacePath: null,
|
|
TimeProvider.System,
|
|
_logger);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void AnalyzerId_ReturnsMacosBundleIdentifier()
|
|
{
|
|
Assert.Equal("macos-bundle", _analyzer.AnalyzerId);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_WithValidBundles_ReturnsPackages()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("macos-bundle", result.AnalyzerId);
|
|
Assert.True(result.Packages.Count > 0, "Expected at least one bundle");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_FindsTestApp()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var testApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.testapp");
|
|
Assert.NotNull(testApp);
|
|
Assert.Equal("1.2.3", testApp.Version);
|
|
Assert.Equal("Test Application", testApp.Name);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_ExtractsVersionCorrectly()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var testApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.testapp");
|
|
Assert.NotNull(testApp);
|
|
// ShortVersion takes precedence
|
|
Assert.Equal("1.2.3", testApp.Version);
|
|
// Build number goes to release
|
|
Assert.Equal("123", testApp.Release);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_BuildsCorrectPurl()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var testApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.testapp");
|
|
Assert.NotNull(testApp);
|
|
Assert.Contains("pkg:generic/macos-app/com.stellaops.testapp@1.2.3", testApp.PackageUrl);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_ExtractsVendorFromBundleId()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var testApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.testapp");
|
|
Assert.NotNull(testApp);
|
|
Assert.Equal("stellaops", testApp.SourcePackage);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_SetsEvidenceSourceToMacOsBundle()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
foreach (var package in result.Packages)
|
|
{
|
|
Assert.Equal(PackageEvidenceSource.MacOsBundle, package.EvidenceSource);
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_ExtractsVendorMetadata()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var testApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.testapp");
|
|
Assert.NotNull(testApp);
|
|
Assert.Equal("com.stellaops.testapp", testApp.VendorMetadata["macos:bundle_id"]);
|
|
Assert.Equal("APPL", testApp.VendorMetadata["macos:bundle_type"]);
|
|
Assert.Equal("12.0", testApp.VendorMetadata["macos:min_os_version"]);
|
|
Assert.Equal("TestApp", testApp.VendorMetadata["macos:executable"]);
|
|
Assert.Equal("MacOSX", testApp.VendorMetadata["macos:platforms"]);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_IncludesCodeResourcesHash()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var testApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.testapp");
|
|
Assert.NotNull(testApp);
|
|
Assert.True(testApp.VendorMetadata.ContainsKey("macos:code_resources_hash"));
|
|
var hash = testApp.VendorMetadata["macos:code_resources_hash"];
|
|
Assert.StartsWith("sha256:", hash);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_DetectsSandboxedApp()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var sandboxedApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.sandboxed");
|
|
Assert.NotNull(sandboxedApp);
|
|
Assert.Equal("true", sandboxedApp.VendorMetadata["macos:sandboxed"]);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_DetectsHighRiskEntitlements()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var sandboxedApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.sandboxed");
|
|
Assert.NotNull(sandboxedApp);
|
|
Assert.True(sandboxedApp.VendorMetadata.ContainsKey("macos:high_risk_entitlements"));
|
|
var highRisk = sandboxedApp.VendorMetadata["macos:high_risk_entitlements"];
|
|
// Full entitlement keys are stored
|
|
Assert.Contains("com.apple.security.device.camera", highRisk);
|
|
Assert.Contains("com.apple.security.device.microphone", highRisk);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_DetectsCapabilityCategories()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var sandboxedApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.sandboxed");
|
|
Assert.NotNull(sandboxedApp);
|
|
Assert.True(sandboxedApp.VendorMetadata.ContainsKey("macos:capability_categories"));
|
|
var categories = sandboxedApp.VendorMetadata["macos:capability_categories"];
|
|
Assert.Contains("network", categories);
|
|
Assert.Contains("camera", categories);
|
|
Assert.Contains("microphone", categories);
|
|
Assert.Contains("filesystem", categories);
|
|
Assert.Contains("sandbox", categories);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_IncludesFileEvidence()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
var testApp = result.Packages.FirstOrDefault(p =>
|
|
p.VendorMetadata.TryGetValue("macos:bundle_id", out var id) &&
|
|
id == "com.stellaops.testapp");
|
|
Assert.NotNull(testApp);
|
|
Assert.True(testApp.Files.Count > 0);
|
|
|
|
var executable = testApp.Files.FirstOrDefault(f => f.Path.Contains("MacOS/TestApp"));
|
|
Assert.NotNull(executable);
|
|
Assert.False(executable.IsConfigFile);
|
|
|
|
var infoPlist = testApp.Files.FirstOrDefault(f => f.Path.Contains("Info.plist"));
|
|
Assert.NotNull(infoPlist);
|
|
Assert.True(infoPlist.IsConfigFile);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_ResultsAreDeterministicallySorted()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result1 = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
var result2 = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
Assert.Equal(result1.Packages.Count, result2.Packages.Count);
|
|
for (int i = 0; i < result1.Packages.Count; i++)
|
|
{
|
|
Assert.Equal(result1.Packages[i].PackageUrl, result2.Packages[i].PackageUrl);
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_NoApplicationsDirectory_ReturnsEmptyPackages()
|
|
{
|
|
// Arrange - use temp directory without Applications
|
|
var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
|
Directory.CreateDirectory(tempPath);
|
|
|
|
try
|
|
{
|
|
var context = CreateContext(tempPath);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
Assert.Empty(result.Packages);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(tempPath, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task AnalyzeAsync_PopulatesTelemetry()
|
|
{
|
|
// Arrange
|
|
var context = CreateContext(FixturesRoot);
|
|
|
|
// Act
|
|
var result = await _analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
|
|
|
// Assert
|
|
Assert.NotNull(result.Telemetry);
|
|
Assert.True(result.Telemetry.PackageCount > 0);
|
|
Assert.True(result.Telemetry.Duration > TimeSpan.Zero);
|
|
}
|
|
}
|