Files
git.stella-ops.org/src/StellaOps.Concelier.Models/AliasSchemeRegistry.cs
2025-10-18 20:46:16 +03:00

167 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string?, bool> Predicate,
Func<string?, string> 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<string?, bool> predicate, Func<string?, string> normalizer)
=> new(
name,
predicate,
alias => normalizer(alias));
private static readonly ImmutableHashSet<string> 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<string> 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);
}
}