252 lines
6.8 KiB
C#
252 lines
6.8 KiB
C#
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Scanner.Analyzers.OS.Windows.Msi.Tests;
|
|
|
|
public class MsiDatabaseParserTests
|
|
{
|
|
private readonly MsiDatabaseParser _parser = new();
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithValidMsiFile_ExtractsMetadata()
|
|
{
|
|
// Arrange - Create a minimal valid OLE compound document
|
|
var tempFile = CreateMinimalMsiFile("TestProduct-1.2.3.msi");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(tempFile, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("TestProduct", result.ProductName);
|
|
Assert.Equal("1.2.3", result.ProductVersion);
|
|
Assert.Equal(tempFile, result.FilePath);
|
|
Assert.True(result.FileSize > 0);
|
|
Assert.StartsWith("sha256:", result.FileHash);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempFile))
|
|
{
|
|
File.Delete(tempFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithVersionedFilename_ExtractsVersionFromName()
|
|
{
|
|
// Arrange
|
|
var tempFile = CreateMinimalMsiFile("SomeProduct_v2.0.1.msi");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(tempFile, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("SomeProduct", result.ProductName);
|
|
Assert.Equal("2.0.1", result.ProductVersion);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempFile))
|
|
{
|
|
File.Delete(tempFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithSpaceVersionedFilename_ExtractsVersionFromName()
|
|
{
|
|
// Arrange
|
|
var tempFile = CreateMinimalMsiFile("Application Setup 3.0.0.msi");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(tempFile, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("Application Setup", result.ProductName);
|
|
Assert.Equal("3.0.0", result.ProductVersion);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempFile))
|
|
{
|
|
File.Delete(tempFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithUnversionedFilename_UsesDefaultVersion()
|
|
{
|
|
// Arrange
|
|
var tempFile = CreateMinimalMsiFile("SimpleInstaller.msi");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(tempFile, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("SimpleInstaller", result.ProductName);
|
|
Assert.Equal("0.0.0", result.ProductVersion);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempFile))
|
|
{
|
|
File.Delete(tempFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithNonExistentFile_ReturnsNull()
|
|
{
|
|
// Act
|
|
var result = _parser.Parse("/nonexistent/path/file.msi", CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithInvalidMsiFile_ReturnsNull()
|
|
{
|
|
// Arrange - Create a file with invalid content
|
|
var tempFile = Path.GetTempFileName();
|
|
File.WriteAllText(tempFile, "Not an MSI file");
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(tempFile, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(tempFile);
|
|
}
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithEmptyPath_ReturnsNull()
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(string.Empty, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Parse_WithNullPath_ReturnsNull()
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(null!, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Theory]
|
|
[InlineData("Product-1.0.msi", "Product", "1.0")]
|
|
[InlineData("Product-1.0.0.msi", "Product", "1.0.0")]
|
|
[InlineData("Product-1.0.0.1.msi", "Product", "1.0.0.1")]
|
|
[InlineData("My-Product_v5.2.msi", "My-Product", "5.2")]
|
|
[InlineData("App 10.0.msi", "App", "10.0")]
|
|
public void Parse_WithVariousFilenamePatterns_ExtractsCorrectNameAndVersion(
|
|
string filename, string expectedName, string expectedVersion)
|
|
{
|
|
// Arrange
|
|
var tempFile = CreateMinimalMsiFile(filename);
|
|
|
|
try
|
|
{
|
|
// Act
|
|
var result = _parser.Parse(tempFile, CancellationToken.None);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(expectedName, result.ProductName);
|
|
Assert.Equal(expectedVersion, result.ProductVersion);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(tempFile))
|
|
{
|
|
File.Delete(tempFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a minimal valid OLE compound document (MSI) file for testing.
|
|
/// </summary>
|
|
private static string CreateMinimalMsiFile(string filename)
|
|
{
|
|
var tempDir = Path.Combine(Path.GetTempPath(), "msi-test-" + Guid.NewGuid().ToString("N")[..8]);
|
|
Directory.CreateDirectory(tempDir);
|
|
|
|
var filePath = Path.Combine(tempDir, filename);
|
|
|
|
// OLE compound document header (512 bytes minimum)
|
|
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/
|
|
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 (typically 0x003E)
|
|
header[0x18] = 0x3E;
|
|
header[0x19] = 0x00;
|
|
|
|
// Major version (3 for sector size 512, 4 for sector size 4096)
|
|
header[0x1A] = 0x03;
|
|
header[0x1B] = 0x00;
|
|
|
|
// Byte order (0xFFFE = little endian)
|
|
header[0x1C] = 0xFE;
|
|
header[0x1D] = 0xFF;
|
|
|
|
// Sector size power (9 = 512 bytes)
|
|
header[0x1E] = 0x09;
|
|
header[0x1F] = 0x00;
|
|
|
|
// Mini sector size power (6 = 64 bytes)
|
|
header[0x20] = 0x06;
|
|
header[0x21] = 0x00;
|
|
|
|
File.WriteAllBytes(filePath, header);
|
|
return filePath;
|
|
}
|
|
}
|