using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Scanner.Analyzers.OS.Windows.Msi; using Xunit; namespace StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests; public class MsiPackageAnalyzerTests { private readonly MsiPackageAnalyzer _analyzer; private readonly ILogger _logger; public MsiPackageAnalyzerTests() { _logger = NullLoggerFactory.Instance.CreateLogger(); _analyzer = new MsiPackageAnalyzer((ILogger)_logger); } private OSPackageAnalyzerContext CreateContext(string rootPath) { return new OSPackageAnalyzerContext( rootPath, workspacePath: null, TimeProvider.System, _logger); } [Fact] public void AnalyzerId_ReturnsCorrectValue() { Assert.Equal("windows-msi", _analyzer.AnalyzerId); } [Fact] public async Task AnalyzeAsync_WithNoMsiDirectory_ReturnsEmptyList() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); Directory.CreateDirectory(tempDir); try { var context = CreateContext(tempDir); // Act var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Empty(result.Packages); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task AnalyzeAsync_WithMsiFiles_ReturnsPackageRecords() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); var installerDir = Path.Combine(tempDir, "Windows", "Installer"); Directory.CreateDirectory(installerDir); // Create test MSI files CreateMinimalMsiFile(Path.Combine(installerDir, "TestApp-1.0.0.msi")); CreateMinimalMsiFile(Path.Combine(installerDir, "AnotherApp-2.5.0.msi")); try { var context = CreateContext(tempDir); // Act var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert Assert.Equal(2, result.Packages.Count); var testApp = result.Packages.FirstOrDefault(r => r.Name == "TestApp"); Assert.NotNull(testApp); Assert.Equal("1.0.0", testApp.Version); Assert.Equal(PackageEvidenceSource.WindowsMsi, testApp.EvidenceSource); Assert.StartsWith("pkg:generic/windows-msi/testapp@1.0.0", testApp.PackageUrl); Assert.True(testApp.VendorMetadata.ContainsKey("msi:file_path")); var anotherApp = result.Packages.FirstOrDefault(r => r.Name == "AnotherApp"); Assert.NotNull(anotherApp); Assert.Equal("2.5.0", anotherApp.Version); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task AnalyzeAsync_WithNestedMsiFiles_DiscoversMsisRecursively() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); var cacheDir = Path.Combine(tempDir, "ProgramData", "Package Cache", "subfolder"); Directory.CreateDirectory(cacheDir); CreateMinimalMsiFile(Path.Combine(cacheDir, "NestedApp-3.0.0.msi")); try { var context = CreateContext(tempDir); // Act var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert Assert.Single(result.Packages); Assert.Equal("NestedApp", result.Packages[0].Name); Assert.Equal("3.0.0", result.Packages[0].Version); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task AnalyzeAsync_WithUserAppDataCache_ScansMsisInUserDirectories() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); var userCacheDir = Path.Combine(tempDir, "Users", "testuser", "AppData", "Local", "Package Cache"); Directory.CreateDirectory(userCacheDir); CreateMinimalMsiFile(Path.Combine(userCacheDir, "UserApp-1.0.0.msi")); try { var context = CreateContext(tempDir); // Act var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert Assert.Single(result.Packages); Assert.Equal("UserApp", result.Packages[0].Name); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task AnalyzeAsync_WithInvalidMsiFile_SkipsInvalidFile() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); var installerDir = Path.Combine(tempDir, "Windows", "Installer"); Directory.CreateDirectory(installerDir); // Create invalid MSI file File.WriteAllText(Path.Combine(installerDir, "Invalid.msi"), "Not an MSI"); // Create valid MSI file CreateMinimalMsiFile(Path.Combine(installerDir, "Valid-1.0.0.msi")); try { var context = CreateContext(tempDir); // Act var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert - Only valid MSI should be returned Assert.Single(result.Packages); Assert.Equal("Valid", result.Packages[0].Name); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task AnalyzeAsync_ResultsAreSortedDeterministically() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); var installerDir = Path.Combine(tempDir, "Windows", "Installer"); Directory.CreateDirectory(installerDir); // Create MSI files in random order CreateMinimalMsiFile(Path.Combine(installerDir, "ZetaApp-1.0.0.msi")); CreateMinimalMsiFile(Path.Combine(installerDir, "AlphaApp-1.0.0.msi")); CreateMinimalMsiFile(Path.Combine(installerDir, "MidApp-1.0.0.msi")); try { var context = CreateContext(tempDir); // Act var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert - Results should be sorted Assert.Equal(3, result.Packages.Count); Assert.Equal("AlphaApp", result.Packages[0].Name); Assert.Equal("MidApp", result.Packages[1].Name); Assert.Equal("ZetaApp", result.Packages[2].Name); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task AnalyzeAsync_WithDuplicateMsiFiles_DeduplicatesByPath() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); var installerDir = Path.Combine(tempDir, "Windows", "Installer"); Directory.CreateDirectory(installerDir); var msiPath = Path.Combine(installerDir, "TestApp-1.0.0.msi"); CreateMinimalMsiFile(msiPath); try { var context = CreateContext(tempDir); // Act - Execute twice to ensure no duplicates var result1 = await _analyzer.AnalyzeAsync(context, CancellationToken.None); var result2 = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert Assert.Single(result1.Packages); Assert.Single(result2.Packages); } finally { Directory.Delete(tempDir, recursive: true); } } [Fact] public async Task AnalyzeAsync_SetsCorrectFileEvidence() { // Arrange var tempDir = Path.Combine(Path.GetTempPath(), "msi-analyzer-test-" + Guid.NewGuid().ToString("N")[..8]); var installerDir = Path.Combine(tempDir, "Windows", "Installer"); Directory.CreateDirectory(installerDir); CreateMinimalMsiFile(Path.Combine(installerDir, "TestApp-1.0.0.msi")); try { var context = CreateContext(tempDir); // Act var result = await _analyzer.AnalyzeAsync(context, CancellationToken.None); // Assert Assert.Single(result.Packages); var record = result.Packages[0]; Assert.Single(record.Files); var file = record.Files[0]; Assert.Contains("TestApp-1.0.0.msi", file.Path); Assert.NotNull(file.Sha256); Assert.StartsWith("sha256:", file.Sha256); Assert.True(file.SizeBytes > 0); } finally { Directory.Delete(tempDir, recursive: true); } } /// /// Creates a minimal valid OLE compound document (MSI) file for testing. /// private static void CreateMinimalMsiFile(string filePath) { var directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // OLE compound document header (512 bytes minimum) var header = new byte[512]; // Magic number: D0 CF 11 E0 A1 B1 1A E1 header[0] = 0xD0; header[1] = 0xCF; header[2] = 0x11; header[3] = 0xE0; header[4] = 0xA1; header[5] = 0xB1; header[6] = 0x1A; header[7] = 0xE1; // Minor version header[0x18] = 0x3E; header[0x19] = 0x00; // Major version header[0x1A] = 0x03; header[0x1B] = 0x00; // Byte order (little endian) header[0x1C] = 0xFE; header[0x1D] = 0xFF; // Sector size power header[0x1E] = 0x09; header[0x1F] = 0x00; // Mini sector size power header[0x20] = 0x06; header[0x21] = 0x00; File.WriteAllBytes(filePath, header); } }