Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests/MsiPackageAnalyzerTests.cs
StellaOps Bot d040c001ac
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
up
2025-11-28 19:23:54 +02:00

325 lines
10 KiB
C#

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<MsiPackageAnalyzer>();
_analyzer = new MsiPackageAnalyzer((ILogger<MsiPackageAnalyzer>)_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);
}
}
/// <summary>
/// Creates a minimal valid OLE compound document (MSI) file for testing.
/// </summary>
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);
}
}