Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Merge.Identity;
/// <summary>
/// Represents a connected component of advisories that refer to the same vulnerability.
/// </summary>
public sealed class AdvisoryIdentityCluster
{
public AdvisoryIdentityCluster(string advisoryKey, IEnumerable<Advisory> advisories, IEnumerable<AliasIdentity> aliases)
{
AdvisoryKey = !string.IsNullOrWhiteSpace(advisoryKey)
? advisoryKey.Trim()
: throw new ArgumentException("Canonical advisory key must be provided.", nameof(advisoryKey));
var advisoriesArray = (advisories ?? throw new ArgumentNullException(nameof(advisories)))
.Where(static advisory => advisory is not null)
.OrderBy(static advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
.ThenBy(static advisory => advisory.Provenance.Length)
.ThenBy(static advisory => advisory.Title, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
if (advisoriesArray.IsDefaultOrEmpty)
{
throw new ArgumentException("At least one advisory is required for a cluster.", nameof(advisories));
}
var aliasArray = (aliases ?? throw new ArgumentNullException(nameof(aliases)))
.Where(static alias => alias is not null && !string.IsNullOrWhiteSpace(alias.Value))
.GroupBy(static alias => alias.Value, StringComparer.OrdinalIgnoreCase)
.Select(static group =>
{
var representative = group
.OrderBy(static entry => entry.Scheme ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.Value, StringComparer.OrdinalIgnoreCase)
.First();
return representative;
})
.OrderBy(static alias => alias.Scheme ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ThenBy(static alias => alias.Value, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
Advisories = advisoriesArray;
Aliases = aliasArray;
}
public string AdvisoryKey { get; }
public ImmutableArray<Advisory> Advisories { get; }
public ImmutableArray<AliasIdentity> Aliases { get; }
}

View File

@@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Merge.Identity;
/// <summary>
/// Builds an alias-driven identity graph that groups advisories referring to the same vulnerability.
/// </summary>
public sealed class AdvisoryIdentityResolver
{
private static readonly string[] CanonicalAliasPriority =
{
AliasSchemes.Cve,
AliasSchemes.Rhsa,
AliasSchemes.Usn,
AliasSchemes.Dsa,
AliasSchemes.SuseSu,
AliasSchemes.Msrc,
AliasSchemes.CiscoSa,
AliasSchemes.OracleCpu,
AliasSchemes.Vmsa,
AliasSchemes.Apsb,
AliasSchemes.Apa,
AliasSchemes.AppleHt,
AliasSchemes.ChromiumPost,
AliasSchemes.Icsa,
AliasSchemes.Jvndb,
AliasSchemes.Jvn,
AliasSchemes.Bdu,
AliasSchemes.Vu,
AliasSchemes.Ghsa,
AliasSchemes.OsV,
};
/// <summary>
/// Groups the provided advisories into identity clusters using normalized aliases.
/// </summary>
public IReadOnlyList<AdvisoryIdentityCluster> Resolve(IEnumerable<Advisory> advisories)
{
ArgumentNullException.ThrowIfNull(advisories);
var materialized = advisories
.Where(static advisory => advisory is not null)
.Distinct()
.ToArray();
if (materialized.Length == 0)
{
return Array.Empty<AdvisoryIdentityCluster>();
}
var aliasIndex = BuildAliasIndex(materialized);
var visited = new HashSet<Advisory>();
var clusters = new List<AdvisoryIdentityCluster>();
foreach (var advisory in materialized)
{
if (!visited.Add(advisory))
{
continue;
}
var component = TraverseComponent(advisory, visited, aliasIndex);
var key = DetermineCanonicalKey(component);
var aliases = component
.SelectMany(static entry => entry.Aliases)
.Select(static alias => new AliasIdentity(alias.Normalized, alias.Scheme));
clusters.Add(new AdvisoryIdentityCluster(key, component.Select(static entry => entry.Advisory), aliases));
}
return clusters
.OrderBy(static cluster => cluster.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
}
private static Dictionary<string, List<AdvisoryAliasEntry>> BuildAliasIndex(IEnumerable<Advisory> advisories)
{
var index = new Dictionary<string, List<AdvisoryAliasEntry>>(StringComparer.OrdinalIgnoreCase);
foreach (var advisory in advisories)
{
foreach (var alias in ExtractAliases(advisory))
{
if (!index.TryGetValue(alias.Normalized, out var list))
{
list = new List<AdvisoryAliasEntry>();
index[alias.Normalized] = list;
}
list.Add(new AdvisoryAliasEntry(advisory, alias.Normalized, alias.Scheme));
}
}
return index;
}
private static IReadOnlyList<AliasBinding> TraverseComponent(
Advisory root,
HashSet<Advisory> visited,
Dictionary<string, List<AdvisoryAliasEntry>> aliasIndex)
{
var stack = new Stack<Advisory>();
stack.Push(root);
var bindings = new Dictionary<Advisory, AliasBinding>(ReferenceEqualityComparer<Advisory>.Instance);
while (stack.Count > 0)
{
var advisory = stack.Pop();
if (!bindings.TryGetValue(advisory, out var binding))
{
binding = new AliasBinding(advisory);
bindings[advisory] = binding;
}
foreach (var alias in ExtractAliases(advisory))
{
binding.AddAlias(alias.Normalized, alias.Scheme);
if (!aliasIndex.TryGetValue(alias.Normalized, out var neighbors))
{
continue;
}
foreach (var neighbor in neighbors.Select(static entry => entry.Advisory))
{
if (visited.Add(neighbor))
{
stack.Push(neighbor);
}
if (!bindings.TryGetValue(neighbor, out var neighborBinding))
{
neighborBinding = new AliasBinding(neighbor);
bindings[neighbor] = neighborBinding;
}
neighborBinding.AddAlias(alias.Normalized, alias.Scheme);
}
}
}
return bindings.Values
.OrderBy(static binding => binding.Advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
}
private static string DetermineCanonicalKey(IReadOnlyList<AliasBinding> component)
{
var aliases = component
.SelectMany(static binding => binding.Aliases)
.Where(static alias => !string.IsNullOrWhiteSpace(alias.Normalized))
.ToArray();
foreach (var scheme in CanonicalAliasPriority)
{
var candidate = aliases
.Where(alias => string.Equals(alias.Scheme, scheme, StringComparison.OrdinalIgnoreCase))
.Select(static alias => alias.Normalized)
.OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
if (candidate is not null)
{
return candidate;
}
}
var fallbackAlias = aliases
.Select(static alias => alias.Normalized)
.OrderBy(static alias => alias, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(fallbackAlias))
{
return fallbackAlias;
}
var advisoryKey = component
.Select(static binding => binding.Advisory.AdvisoryKey)
.Where(static value => !string.IsNullOrWhiteSpace(value))
.OrderBy(static value => value, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(advisoryKey))
{
return advisoryKey.Trim();
}
throw new InvalidOperationException("Unable to determine canonical advisory key for cluster.");
}
private static IEnumerable<AliasProjection> ExtractAliases(Advisory advisory)
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in EnumerateAliasCandidates(advisory))
{
if (string.IsNullOrWhiteSpace(candidate))
{
continue;
}
var trimmed = candidate.Trim();
if (!seen.Add(trimmed))
{
continue;
}
if (AliasSchemeRegistry.TryNormalize(trimmed, out var normalized, out var scheme) &&
!string.IsNullOrWhiteSpace(normalized))
{
yield return new AliasProjection(normalized.Trim(), string.IsNullOrWhiteSpace(scheme) ? null : scheme);
}
else if (!string.IsNullOrWhiteSpace(normalized))
{
yield return new AliasProjection(normalized.Trim(), null);
}
}
}
private static IEnumerable<string> EnumerateAliasCandidates(Advisory advisory)
{
if (!string.IsNullOrWhiteSpace(advisory.AdvisoryKey))
{
yield return advisory.AdvisoryKey;
}
if (!advisory.Aliases.IsDefaultOrEmpty)
{
foreach (var alias in advisory.Aliases)
{
if (!string.IsNullOrWhiteSpace(alias))
{
yield return alias;
}
}
}
}
private readonly record struct AdvisoryAliasEntry(Advisory Advisory, string Normalized, string? Scheme);
private readonly record struct AliasProjection(string Normalized, string? Scheme);
private sealed class AliasBinding
{
private readonly HashSet<AliasProjection> _aliases = new(HashSetAliasComparer.Instance);
public AliasBinding(Advisory advisory)
{
Advisory = advisory ?? throw new ArgumentNullException(nameof(advisory));
}
public Advisory Advisory { get; }
public IReadOnlyCollection<AliasProjection> Aliases => _aliases;
public void AddAlias(string normalized, string? scheme)
{
if (string.IsNullOrWhiteSpace(normalized))
{
return;
}
_aliases.Add(new AliasProjection(normalized.Trim(), scheme is null ? null : scheme.Trim()));
}
}
private sealed class HashSetAliasComparer : IEqualityComparer<AliasProjection>
{
public static readonly HashSetAliasComparer Instance = new();
public bool Equals(AliasProjection x, AliasProjection y)
=> string.Equals(x.Normalized, y.Normalized, StringComparison.OrdinalIgnoreCase)
&& string.Equals(x.Scheme, y.Scheme, StringComparison.OrdinalIgnoreCase);
public int GetHashCode(AliasProjection obj)
{
var hash = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Normalized);
if (!string.IsNullOrWhiteSpace(obj.Scheme))
{
hash = HashCode.Combine(hash, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Scheme));
}
return hash;
}
}
private sealed class ReferenceEqualityComparer<T> : IEqualityComparer<T>
where T : class
{
public static readonly ReferenceEqualityComparer<T> Instance = new();
public bool Equals(T? x, T? y) => ReferenceEquals(x, y);
public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
}
}

View File

@@ -0,0 +1,24 @@
using System;
namespace StellaOps.Concelier.Merge.Identity;
/// <summary>
/// Normalized alias representation used within identity clusters.
/// </summary>
public sealed class AliasIdentity
{
public AliasIdentity(string value, string? scheme)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Alias value must be provided.", nameof(value));
}
Value = value.Trim();
Scheme = string.IsNullOrWhiteSpace(scheme) ? null : scheme.Trim();
}
public string Value { get; }
public string? Scheme { get; }
}