Add advisory source catalog UI, mirror wizard, and mirror dashboard

Source catalog component: browsable catalog of 75 advisory sources grouped
by 14 categories with search, filter, enable/disable toggles, batch
operations, health checks, and category descriptions.

Mirror domain builder: 3-step wizard (select sources → configure domain →
review & create) with category-level selection, auto-naming, format
choice, rate limits, signing options, and optional immediate generation.

Mirror dashboard: domain cards with staleness indicators, regenerate and
delete actions, consumer config panel, endpoint viewer, and empty-state
CTA leading to the wizard.

Catalog mirror header: mode badge, domain stats, and quick-access buttons
for mirror configuration integrated into the source catalog.

Supporting: source management API client (9 endpoints), mirror management
API client (12 endpoints), integration hub route wiring, onboarding hub
advisory section, security page health display fix, E2E Playwright tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
master
2026-03-15 13:31:04 +02:00
parent 3931b7e2cf
commit 0c723b4e07
12 changed files with 4823 additions and 16 deletions

View File

@@ -8,6 +8,30 @@ public sealed class MirrorDistributionOptions
{
public const string SectionName = "Excititor:Mirror";
/// <summary>
/// All source categories recognized by the mirror export system. This list must stay
/// in sync with <c>SourceCategory</c> in Concelier.Core. Used by the
/// <c>sourceCategory</c> filter shorthand in <see cref="MirrorExportOptions.ResolveFilters"/>.
/// Operators can specify one or more comma-separated values from this set.
/// </summary>
public static readonly IReadOnlyList<string> SupportedCategories = new[]
{
"Primary",
"Vendor",
"Distribution",
"Ecosystem",
"Cert",
"Csaf",
"Threat",
"Exploit",
"Container",
"Hardware",
"Ics",
"PackageManager",
"Mirror",
"Other",
};
/// <summary>
/// Global enable flag for mirror distribution surfaces and bundle generation.
/// </summary>
@@ -94,6 +118,12 @@ public sealed class MirrorExportOptions
/// into normalized multi-value lists. Source definitions are required for resolving
/// <c>sourceCategory</c> and <c>sourceTag</c> shorthands; pass <c>null</c> when
/// category/tag expansion is not needed.
/// <para>
/// Both <c>sourceCategory</c> and <c>sourceTag</c> accept comma-separated values,
/// e.g. <c>"Exploit,Container,Ics,PackageManager"</c>. All matching source IDs are
/// merged into the resolved <c>sourceVendor</c> list.
/// See <see cref="MirrorDistributionOptions.SupportedCategories"/> for valid category names.
/// </para>
/// </summary>
/// <param name="sourceDefinitions">
/// Optional catalog of source definitions used to resolve <c>sourceCategory</c> and
@@ -116,9 +146,13 @@ public sealed class MirrorExportOptions
if (key.Equals("sourceCategory", StringComparison.OrdinalIgnoreCase) && sourceDefinitions is not null)
{
// Resolve category to source IDs
// Resolve one or more comma-separated categories to source IDs.
// Supports both single ("Exploit") and multi-value ("Exploit,Container,Ics,PackageManager").
var categories = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var categorySet = new HashSet<string>(categories, StringComparer.OrdinalIgnoreCase);
var matchingIds = sourceDefinitions
.Where(s => s.Category.Equals(value, StringComparison.OrdinalIgnoreCase))
.Where(s => categorySet.Contains(s.Category))
.Select(s => s.Id)
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -130,9 +164,12 @@ public sealed class MirrorExportOptions
}
else if (key.Equals("sourceTag", StringComparison.OrdinalIgnoreCase) && sourceDefinitions is not null)
{
// Resolve tag to source IDs
// Resolve one or more comma-separated tags to source IDs.
var tags = value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var tagSet = new HashSet<string>(tags, StringComparer.OrdinalIgnoreCase);
var matchingIds = sourceDefinitions
.Where(s => s.Tags.Contains(value, StringComparer.OrdinalIgnoreCase))
.Where(s => s.Tags.Any(t => tagSet.Contains(t)))
.Select(s => s.Id)
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToList();