using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions; namespace StellaOps.Concelier.Models; public static class AliasSchemeRegistry { private sealed record AliasScheme( string Name, Func Predicate, Func Normalizer); private static readonly AliasScheme[] SchemeDefinitions = { BuildScheme(AliasSchemes.Cve, alias => alias is not null && Matches(CvERegex, alias), alias => alias is null ? string.Empty : NormalizePrefix(alias, "CVE")), BuildScheme(AliasSchemes.Ghsa, alias => alias is not null && Matches(GhsaRegex, alias), alias => alias is null ? string.Empty : NormalizePrefix(alias, "GHSA")), BuildScheme(AliasSchemes.OsV, alias => alias is not null && Matches(OsVRegex, alias), alias => alias is null ? string.Empty : NormalizePrefix(alias, "OSV")), BuildScheme(AliasSchemes.Jvn, alias => alias is not null && Matches(JvnRegex, alias), alias => alias is null ? string.Empty : NormalizePrefix(alias, "JVN")), BuildScheme(AliasSchemes.Jvndb, alias => alias is not null && Matches(JvndbRegex, alias), alias => alias is null ? string.Empty : NormalizePrefix(alias, "JVNDB")), BuildScheme(AliasSchemes.Bdu, alias => alias is not null && Matches(BduRegex, alias), alias => alias is null ? string.Empty : NormalizePrefix(alias, "BDU")), BuildScheme(AliasSchemes.Vu, alias => alias is not null && alias.StartsWith("VU#", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "VU", preserveSeparator: '#')), BuildScheme(AliasSchemes.Msrc, alias => alias is not null && alias.StartsWith("MSRC-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "MSRC")), BuildScheme(AliasSchemes.CiscoSa, alias => alias is not null && alias.StartsWith("CISCO-SA-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "CISCO-SA")), BuildScheme(AliasSchemes.OracleCpu, alias => alias is not null && alias.StartsWith("ORACLE-CPU", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "ORACLE-CPU")), BuildScheme(AliasSchemes.Apsb, alias => alias is not null && alias.StartsWith("APSB-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "APSB")), BuildScheme(AliasSchemes.Apa, alias => alias is not null && alias.StartsWith("APA-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "APA")), BuildScheme(AliasSchemes.AppleHt, alias => alias is not null && alias.StartsWith("APPLE-HT", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "APPLE-HT")), BuildScheme(AliasSchemes.ChromiumPost, alias => alias is not null && (alias.StartsWith("CHROMIUM-POST", StringComparison.OrdinalIgnoreCase) || alias.StartsWith("CHROMIUM:", StringComparison.OrdinalIgnoreCase)), NormalizeChromium), BuildScheme(AliasSchemes.Vmsa, alias => alias is not null && alias.StartsWith("VMSA-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "VMSA")), BuildScheme(AliasSchemes.Rhsa, alias => alias is not null && alias.StartsWith("RHSA-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "RHSA")), BuildScheme(AliasSchemes.Usn, alias => alias is not null && alias.StartsWith("USN-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "USN")), BuildScheme(AliasSchemes.Dsa, alias => alias is not null && alias.StartsWith("DSA-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "DSA")), BuildScheme(AliasSchemes.SuseSu, alias => alias is not null && alias.StartsWith("SUSE-SU-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "SUSE-SU")), BuildScheme(AliasSchemes.Icsa, alias => alias is not null && alias.StartsWith("ICSA-", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "ICSA")), BuildScheme(AliasSchemes.Cwe, alias => alias is not null && Matches(CweRegex, alias), alias => alias is null ? string.Empty : NormalizePrefix(alias, "CWE")), BuildScheme(AliasSchemes.Cpe, alias => alias is not null && alias.StartsWith("cpe:", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "cpe", uppercase:false)), BuildScheme(AliasSchemes.Purl, alias => alias is not null && alias.StartsWith("pkg:", StringComparison.OrdinalIgnoreCase), alias => NormalizePrefix(alias, "pkg", uppercase:false)), }; private static AliasScheme BuildScheme(string name, Func predicate, Func normalizer) => new( name, predicate, alias => normalizer(alias)); private static readonly ImmutableHashSet SchemeNames = SchemeDefinitions .Select(static scheme => scheme.Name) .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); private static readonly Regex CvERegex = new("^CVE-\\d{4}-\\d{4,}$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex GhsaRegex = new("^GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex OsVRegex = new("^OSV-\\d{4}-\\d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex JvnRegex = new("^JVN-\\d{4}-\\d{6}$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex JvndbRegex = new("^JVNDB-\\d{4}-\\d{6}$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex BduRegex = new("^BDU-\\d{4}-\\d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex CweRegex = new("^CWE-\\d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); public static IReadOnlyCollection KnownSchemes => SchemeNames; public static bool IsKnownScheme(string? scheme) => !string.IsNullOrWhiteSpace(scheme) && SchemeNames.Contains(scheme); public static bool TryGetScheme(string? alias, out string scheme) { if (string.IsNullOrWhiteSpace(alias)) { scheme = string.Empty; return false; } var candidate = alias.Trim(); foreach (var entry in SchemeDefinitions) { if (entry.Predicate(candidate)) { scheme = entry.Name; return true; } } scheme = string.Empty; return false; } public static bool TryNormalize(string? alias, out string normalized, out string scheme) { normalized = string.Empty; scheme = string.Empty; if (string.IsNullOrWhiteSpace(alias)) { return false; } var candidate = alias.Trim(); foreach (var entry in SchemeDefinitions) { if (entry.Predicate(candidate)) { scheme = entry.Name; normalized = entry.Normalizer(candidate); return true; } } normalized = candidate; return false; } private static string NormalizePrefix(string? alias, string prefix, bool uppercase = true, char? preserveSeparator = null) { if (string.IsNullOrWhiteSpace(alias)) { return string.Empty; } var comparison = StringComparison.OrdinalIgnoreCase; if (!alias.StartsWith(prefix, comparison)) { return uppercase ? alias : alias.ToLowerInvariant(); } var remainder = alias[prefix.Length..]; if (preserveSeparator is { } separator && remainder.Length > 0 && remainder[0] != separator) { // Edge case: alias is expected to use a specific separator but does not – return unchanged. return uppercase ? prefix.ToUpperInvariant() + remainder : prefix + remainder; } var normalizedPrefix = uppercase ? prefix.ToUpperInvariant() : prefix.ToLowerInvariant(); return normalizedPrefix + remainder; } private static string NormalizeChromium(string? alias) { if (string.IsNullOrWhiteSpace(alias)) { return string.Empty; } if (alias.StartsWith("CHROMIUM-POST", StringComparison.OrdinalIgnoreCase)) { return NormalizePrefix(alias, "CHROMIUM-POST"); } if (alias.StartsWith("CHROMIUM:", StringComparison.OrdinalIgnoreCase)) { var remainder = alias["CHROMIUM".Length..]; return "CHROMIUM" + remainder; } return alias; } private static bool Matches(Regex? regex, string? candidate) { if (regex is null || string.IsNullOrWhiteSpace(candidate)) { return false; } return regex.IsMatch(candidate); } }