This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Scanner.Analyzers.OS.Windows.WinSxS.Tests")]
|
||||
@@ -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>
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user