up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 19:23:54 +02:00
parent d1cbb905f8
commit d040c001ac
36 changed files with 4668 additions and 9 deletions

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests")]

View File

@@ -0,0 +1,22 @@
<?xml version='1.0' encoding='utf-8'?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PackageId>StellaOps.Scanner.Analyzers.OS.Windows.WinSxS</PackageId>
<Version>0.1.0-alpha</Version>
<Description>Windows WinSxS assembly analyzer for StellaOps Scanner</Description>
<Authors>StellaOps</Authors>
<PackageLicenseExpression>AGPL-3.0-or-later</PackageLicenseExpression>
<RepositoryUrl>https://git.stella-ops.org/stella-ops.org/stellaops</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Analyzers.OS.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Plugin;
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS;
/// <summary>
/// Plugin that registers the Windows WinSxS assembly analyzer.
/// </summary>
public sealed class WinSxSAnalyzerPlugin : IOSAnalyzerPlugin
{
/// <inheritdoc />
public string Name => "StellaOps.Scanner.Analyzers.OS.Windows.WinSxS";
/// <inheritdoc />
public bool IsAvailable(IServiceProvider services) => services is not null;
/// <inheritdoc />
public IOSPackageAnalyzer CreateAnalyzer(IServiceProvider services)
{
ArgumentNullException.ThrowIfNull(services);
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
return new WinSxSPackageAnalyzer(loggerFactory.CreateLogger<WinSxSPackageAnalyzer>());
}
}

View File

@@ -0,0 +1,60 @@
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS;
/// <summary>
/// Represents metadata extracted from a Windows Side-by-Side (WinSxS) assembly manifest.
/// </summary>
internal sealed record WinSxSAssemblyMetadata(
/// <summary>Assembly name (e.g., "Microsoft.Windows.Common-Controls").</summary>
string Name,
/// <summary>Assembly version (e.g., "6.0.0.0").</summary>
string Version,
/// <summary>Processor architecture (e.g., "x86", "amd64", "arm64", "msil", "wow64").</summary>
string ProcessorArchitecture,
/// <summary>Public key token (e.g., "6595b64144ccf1df").</summary>
string? PublicKeyToken,
/// <summary>Language/culture (e.g., "en-us", "*", or empty).</summary>
string? Language,
/// <summary>Assembly type (e.g., "win32", "win32-policy").</summary>
string? Type,
/// <summary>Version scope (e.g., "nonSxS" for non-side-by-side assemblies).</summary>
string? VersionScope,
/// <summary>Manifest file path.</summary>
string ManifestPath,
/// <summary>Associated catalog (.cat) file path if found.</summary>
string? CatalogPath,
/// <summary>Catalog signature thumbprint if available.</summary>
string? CatalogThumbprint,
/// <summary>KB reference from patch manifest if available.</summary>
string? KbReference,
/// <summary>Files declared in the assembly manifest.</summary>
IReadOnlyList<WinSxSFileEntry> Files);
/// <summary>
/// Represents a file entry in a WinSxS assembly manifest.
/// </summary>
internal sealed record WinSxSFileEntry(
/// <summary>File name.</summary>
string Name,
/// <summary>File hash algorithm (e.g., "SHA256").</summary>
string? HashAlgorithm,
/// <summary>File hash value.</summary>
string? Hash,
/// <summary>File size in bytes.</summary>
long? Size,
/// <summary>Destination path within the assembly.</summary>
string? DestinationPath);

View File

@@ -0,0 +1,240 @@
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS;
/// <summary>
/// Parses Windows Side-by-Side (WinSxS) assembly manifest files.
/// </summary>
internal sealed class WinSxSManifestParser
{
// WinSxS XML namespace
private static readonly XNamespace AssemblyNs = "urn:schemas-microsoft-com:asm.v1";
private static readonly XNamespace AssemblyNsV3 = "urn:schemas-microsoft-com:asm.v3";
/// <summary>
/// Parses a WinSxS manifest file and extracts assembly metadata.
/// </summary>
public WinSxSAssemblyMetadata? Parse(string manifestPath, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(manifestPath) || !File.Exists(manifestPath))
{
return null;
}
try
{
var doc = XDocument.Load(manifestPath);
if (doc.Root is null)
{
return null;
}
cancellationToken.ThrowIfCancellationRequested();
// Find assemblyIdentity element (try both namespaces and no namespace)
var identity = FindAssemblyIdentity(doc.Root);
if (identity is null)
{
return null;
}
// Extract assembly identity attributes
var name = identity.Attribute("name")?.Value;
var version = identity.Attribute("version")?.Value;
var arch = identity.Attribute("processorArchitecture")?.Value;
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(version))
{
return null;
}
var publicKeyToken = identity.Attribute("publicKeyToken")?.Value;
var language = identity.Attribute("language")?.Value;
var type = identity.Attribute("type")?.Value;
var versionScope = identity.Attribute("versionScope")?.Value;
// Extract file entries
var files = ExtractFileEntries(doc.Root);
// Extract KB reference from manifest path if present
var kbReference = ExtractKbReference(manifestPath);
// Look for associated catalog file
var catalogPath = FindCatalogFile(manifestPath);
return new WinSxSAssemblyMetadata(
Name: name,
Version: version,
ProcessorArchitecture: arch ?? "neutral",
PublicKeyToken: publicKeyToken,
Language: language,
Type: type,
VersionScope: versionScope,
ManifestPath: manifestPath,
CatalogPath: catalogPath,
CatalogThumbprint: null, // Would require certificate parsing
KbReference: kbReference,
Files: files);
}
catch (Exception ex) when (ex is XmlException or IOException or UnauthorizedAccessException)
{
return null;
}
}
/// <summary>
/// Generates a deterministic assembly identity string for PURL construction.
/// Format: name_version_arch_publicKeyToken_language
/// </summary>
public static string BuildAssemblyIdentityString(WinSxSAssemblyMetadata metadata)
{
var builder = new StringBuilder();
builder.Append(metadata.Name.ToLowerInvariant());
builder.Append('_');
builder.Append(metadata.Version);
builder.Append('_');
builder.Append(metadata.ProcessorArchitecture?.ToLowerInvariant() ?? "neutral");
if (!string.IsNullOrWhiteSpace(metadata.PublicKeyToken))
{
builder.Append('_');
builder.Append(metadata.PublicKeyToken.ToLowerInvariant());
}
if (!string.IsNullOrWhiteSpace(metadata.Language) && metadata.Language != "*")
{
builder.Append('_');
builder.Append(metadata.Language.ToLowerInvariant());
}
return builder.ToString();
}
private static XElement? FindAssemblyIdentity(XElement root)
{
// Try different namespace variations
return root.Element(AssemblyNs + "assemblyIdentity")
?? root.Element(AssemblyNsV3 + "assemblyIdentity")
?? root.Element("assemblyIdentity")
?? root.Descendants().FirstOrDefault(e =>
e.Name.LocalName == "assemblyIdentity");
}
private static List<WinSxSFileEntry> ExtractFileEntries(XElement root)
{
var files = new List<WinSxSFileEntry>();
// Find all file elements
var fileElements = root.Descendants()
.Where(e => e.Name.LocalName == "file");
foreach (var file in fileElements)
{
var name = file.Attribute("name")?.Value;
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
var hashAttr = file.Attribute("hash");
var hashAlgAttr = file.Attribute("hashalg");
var sizeAttr = file.Attribute("size");
var destAttr = file.Attribute("destinationPath");
// Try to find hash in child element
var hashElement = file.Elements()
.FirstOrDefault(e => e.Name.LocalName == "hash");
string? hash = hashAttr?.Value;
string? hashAlg = hashAlgAttr?.Value;
if (hashElement is not null)
{
var digestMethod = hashElement.Elements()
.FirstOrDefault(e => e.Name.LocalName == "DigestMethod");
var digestValue = hashElement.Elements()
.FirstOrDefault(e => e.Name.LocalName == "DigestValue");
if (digestMethod is not null)
{
hashAlg = ParseHashAlgorithm(digestMethod.Attribute("Algorithm")?.Value);
}
if (digestValue is not null)
{
hash = digestValue.Value;
}
}
long? size = null;
if (long.TryParse(sizeAttr?.Value, out var parsedSize))
{
size = parsedSize;
}
files.Add(new WinSxSFileEntry(
Name: name,
HashAlgorithm: hashAlg,
Hash: hash,
Size: size,
DestinationPath: destAttr?.Value));
}
return files;
}
private static string? ParseHashAlgorithm(string? algorithmUri)
{
if (string.IsNullOrWhiteSpace(algorithmUri))
{
return null;
}
// Common XML Signature algorithm URIs
return algorithmUri switch
{
_ when algorithmUri.Contains("sha256", StringComparison.OrdinalIgnoreCase) => "SHA256",
_ when algorithmUri.Contains("sha1", StringComparison.OrdinalIgnoreCase) => "SHA1",
_ when algorithmUri.Contains("sha384", StringComparison.OrdinalIgnoreCase) => "SHA384",
_ when algorithmUri.Contains("sha512", StringComparison.OrdinalIgnoreCase) => "SHA512",
_ => null
};
}
private static string? ExtractKbReference(string manifestPath)
{
// KB references are often embedded in manifest file names
// e.g., "amd64_microsoft-windows-security-base_31bf3856ad364e35_10.0.19041.4170_none_kb5034441.manifest"
var fileName = Path.GetFileNameWithoutExtension(manifestPath);
var kbMatch = System.Text.RegularExpressions.Regex.Match(
fileName,
@"kb(\d+)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return kbMatch.Success ? $"KB{kbMatch.Groups[1].Value}" : null;
}
private static string? FindCatalogFile(string manifestPath)
{
// Catalog files are typically named similarly to manifests
var directory = Path.GetDirectoryName(manifestPath);
if (string.IsNullOrWhiteSpace(directory))
{
return null;
}
// Look in Catalogs directory
var catalogsDir = Path.Combine(Path.GetDirectoryName(directory) ?? "", "Catalogs");
if (!Directory.Exists(catalogsDir))
{
return null;
}
var baseName = Path.GetFileNameWithoutExtension(manifestPath);
// Try to find matching catalog
var catalogPath = Path.Combine(catalogsDir, baseName + ".cat");
return File.Exists(catalogPath) ? catalogPath : null;
}
}

View File

@@ -0,0 +1,228 @@
using System.Collections.ObjectModel;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Analyzers.OS.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Analyzers;
using StellaOps.Scanner.Analyzers.OS.Helpers;
namespace StellaOps.Scanner.Analyzers.OS.Windows.WinSxS;
/// <summary>
/// Analyzes Windows Side-by-Side (WinSxS) assembly manifests to extract component metadata.
/// Scans Windows/WinSxS/Manifests/ for .manifest files.
/// </summary>
internal sealed class WinSxSPackageAnalyzer : OsPackageAnalyzerBase
{
private static readonly IReadOnlyList<OSPackageRecord> EmptyPackages =
new ReadOnlyCollection<OSPackageRecord>(Array.Empty<OSPackageRecord>());
/// <summary>
/// Path to WinSxS manifests directory.
/// </summary>
private const string ManifestsPath = "Windows/WinSxS/Manifests";
/// <summary>
/// Maximum number of manifests to process (limit for large systems).
/// </summary>
private const int MaxManifests = 50000;
private readonly WinSxSManifestParser _parser = new();
public WinSxSPackageAnalyzer(ILogger<WinSxSPackageAnalyzer> logger)
: base(logger)
{
}
public override string AnalyzerId => "windows-winsxs";
protected override ValueTask<IReadOnlyList<OSPackageRecord>> ExecuteCoreAsync(
OSPackageAnalyzerContext context,
CancellationToken cancellationToken)
{
var manifestsDir = Path.Combine(context.RootPath, ManifestsPath);
if (!Directory.Exists(manifestsDir))
{
Logger.LogInformation("WinSxS manifests directory not found at {Path}; skipping analyzer.", manifestsDir);
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(EmptyPackages);
}
var records = new List<OSPackageRecord>();
var warnings = new List<string>();
var processedCount = 0;
Logger.LogInformation("Scanning WinSxS manifests in {Path}", manifestsDir);
try
{
var manifests = Directory.EnumerateFiles(manifestsDir, "*.manifest", SearchOption.TopDirectoryOnly);
foreach (var manifestPath in manifests)
{
cancellationToken.ThrowIfCancellationRequested();
if (processedCount >= MaxManifests)
{
Logger.LogWarning("Reached maximum manifest limit ({Max}); truncating results.", MaxManifests);
break;
}
var record = AnalyzeManifest(manifestPath, warnings, cancellationToken);
if (record is not null)
{
records.Add(record);
}
processedCount++;
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(ex, "Failed to enumerate WinSxS manifests");
}
if (records.Count == 0)
{
Logger.LogInformation("No valid WinSxS assemblies found; skipping analyzer.");
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(EmptyPackages);
}
foreach (var warning in warnings.Take(10))
{
Logger.LogWarning("WinSxS scan warning: {Warning}", warning);
}
Logger.LogInformation("Discovered {Count} WinSxS assemblies from {Processed} manifests", records.Count, processedCount);
// Sort for deterministic output
records.Sort();
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(records);
}
private OSPackageRecord? AnalyzeManifest(
string manifestPath,
List<string> warnings,
CancellationToken cancellationToken)
{
var metadata = _parser.Parse(manifestPath, cancellationToken);
if (metadata is null)
{
return null;
}
// Build PURL using assembly identity
var assemblyIdentity = WinSxSManifestParser.BuildAssemblyIdentityString(metadata);
var purl = PackageUrlBuilder.BuildWindowsWinSxS(metadata.Name, metadata.Version, metadata.ProcessorArchitecture);
// Build vendor metadata
var vendorMetadata = BuildVendorMetadata(metadata);
// Build file evidence
var files = metadata.Files.Select(f => new OSPackageFileEvidence(
f.Name,
layerDigest: null,
sha256: FormatHash(f.Hash, f.HashAlgorithm),
sizeBytes: f.Size,
isConfigFile: f.Name.EndsWith(".config", StringComparison.OrdinalIgnoreCase)
)).ToList();
// Add manifest file itself
files.Insert(0, new OSPackageFileEvidence(
Path.GetFileName(manifestPath),
layerDigest: null,
sha256: null,
sizeBytes: null,
isConfigFile: true));
return new OSPackageRecord(
AnalyzerId,
purl,
metadata.Name,
metadata.Version,
metadata.ProcessorArchitecture,
PackageEvidenceSource.WindowsWinSxS,
epoch: null,
release: null,
sourcePackage: ExtractPublisher(metadata.Name),
license: null,
cveHints: metadata.KbReference is not null ? [metadata.KbReference] : null,
provides: null,
depends: null,
files: files,
vendorMetadata: vendorMetadata);
}
private static Dictionary<string, string?> BuildVendorMetadata(WinSxSAssemblyMetadata metadata)
{
var vendorMetadata = new Dictionary<string, string?>(StringComparer.Ordinal)
{
["winsxs:name"] = metadata.Name,
["winsxs:version"] = metadata.Version,
["winsxs:arch"] = metadata.ProcessorArchitecture,
["winsxs:manifest_path"] = metadata.ManifestPath,
};
if (!string.IsNullOrWhiteSpace(metadata.PublicKeyToken))
{
vendorMetadata["winsxs:public_key_token"] = metadata.PublicKeyToken;
}
if (!string.IsNullOrWhiteSpace(metadata.Language) && metadata.Language != "*")
{
vendorMetadata["winsxs:language"] = metadata.Language;
}
if (!string.IsNullOrWhiteSpace(metadata.Type))
{
vendorMetadata["winsxs:type"] = metadata.Type;
}
if (!string.IsNullOrWhiteSpace(metadata.VersionScope))
{
vendorMetadata["winsxs:version_scope"] = metadata.VersionScope;
}
if (!string.IsNullOrWhiteSpace(metadata.CatalogPath))
{
vendorMetadata["winsxs:catalog_path"] = metadata.CatalogPath;
}
if (!string.IsNullOrWhiteSpace(metadata.CatalogThumbprint))
{
vendorMetadata["winsxs:catalog_thumbprint"] = metadata.CatalogThumbprint;
}
if (!string.IsNullOrWhiteSpace(metadata.KbReference))
{
vendorMetadata["winsxs:kb_reference"] = metadata.KbReference;
}
vendorMetadata["winsxs:file_count"] = metadata.Files.Count.ToString();
return vendorMetadata;
}
private static string? FormatHash(string? hash, string? algorithm)
{
if (string.IsNullOrWhiteSpace(hash))
{
return null;
}
var prefix = algorithm?.ToLowerInvariant() switch
{
"sha256" => "sha256:",
"sha1" => "sha1:",
"sha384" => "sha384:",
"sha512" => "sha512:",
_ => ""
};
return prefix + hash.ToLowerInvariant();
}
private static string? ExtractPublisher(string assemblyName)
{
// Extract publisher from assembly name (e.g., "Microsoft.Windows.Common-Controls" -> "Microsoft")
var firstDot = assemblyName.IndexOf('.');
return firstDot > 0 ? assemblyName[..firstDot] : null;
}
}