using System.Security.Cryptography; using System.Xml.Linq; namespace StellaOps.Scanner.Analyzers.OS.Windows.Chocolatey; /// /// Parses Chocolatey/NuGet .nuspec files to extract package metadata. /// internal sealed class NuspecParser { private static readonly XNamespace NuspecNs = "http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd"; private static readonly XNamespace NuspecNsOld = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"; private static readonly XNamespace NuspecNsOld2 = "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"; /// /// Parses a .nuspec file and extracts package metadata. /// public ChocolateyPackageMetadata? Parse(string nuspecPath, string installDir, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(nuspecPath) || !File.Exists(nuspecPath)) { return null; } try { var doc = XDocument.Load(nuspecPath); if (doc.Root is null) { return null; } cancellationToken.ThrowIfCancellationRequested(); // Find metadata element var metadata = FindElement(doc.Root, "metadata"); if (metadata is null) { return null; } // Extract required fields var id = GetElementValue(metadata, "id"); var version = GetElementValue(metadata, "version"); if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(version)) { return null; } // Extract optional fields var title = GetElementValue(metadata, "title"); var authors = GetElementValue(metadata, "authors"); var description = GetElementValue(metadata, "description"); var licenseUrl = GetElementValue(metadata, "licenseUrl"); var projectUrl = GetElementValue(metadata, "projectUrl"); // Look for install script and compute hash var installScriptHash = ComputeInstallScriptHash(installDir); // Enumerate installed files var installedFiles = EnumerateInstalledFiles(installDir); return new ChocolateyPackageMetadata( Id: id, Version: version, Title: title, Authors: authors, Description: description, LicenseUrl: licenseUrl, ProjectUrl: projectUrl, Checksum: null, ChecksumType: null, SourceFeed: null, InstallDir: installDir, InstallScriptHash: installScriptHash, InstalledFiles: installedFiles); } catch (Exception ex) when (ex is System.Xml.XmlException or IOException or UnauthorizedAccessException) { return null; } } /// /// Parses basic package info from directory name pattern. /// Format: packageid.version /// public static (string Id, string Version)? ParsePackageDirectory(string directoryName) { if (string.IsNullOrWhiteSpace(directoryName)) { return null; } // Find the first dot followed by a digit (start of version) // Iterating from left to right because package IDs can contain dots // (e.g., "Microsoft.WindowsTerminal.1.18.0" -> id="Microsoft.WindowsTerminal", version="1.18.0") var versionStartIndex = -1; for (var i = 0; i < directoryName.Length - 1; i++) { if (directoryName[i] == '.' && char.IsDigit(directoryName[i + 1])) { versionStartIndex = i; break; } } if (versionStartIndex <= 0) { return null; } var id = directoryName[..versionStartIndex]; var version = directoryName[(versionStartIndex + 1)..]; return (id, version); } private static XElement? FindElement(XElement parent, string localName) { return parent.Element(NuspecNs + localName) ?? parent.Element(NuspecNsOld + localName) ?? parent.Element(NuspecNsOld2 + localName) ?? parent.Element(localName); } private static string? GetElementValue(XElement parent, string localName) { var element = FindElement(parent, localName); return element?.Value?.Trim(); } private static string? ComputeInstallScriptHash(string installDir) { // Look for chocolateyinstall.ps1 script var scriptPath = Path.Combine(installDir, "tools", "chocolateyinstall.ps1"); if (!File.Exists(scriptPath)) { scriptPath = Path.Combine(installDir, "chocolateyinstall.ps1"); } if (!File.Exists(scriptPath)) { return null; } try { using var stream = File.OpenRead(scriptPath); var hash = SHA256.HashData(stream); return $"sha256:{Convert.ToHexStringLower(hash)}"; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return null; } } private static List EnumerateInstalledFiles(string installDir) { var files = new List(); if (!Directory.Exists(installDir)) { return files; } try { foreach (var file in Directory.EnumerateFiles(installDir, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(installDir, file); files.Add(relativePath); } } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // Ignore access errors } return files; } }