compose and authority fixes. finish sprints.

This commit is contained in:
master
2026-02-17 21:59:47 +02:00
parent fb46a927ad
commit 49cdebe2f1
187 changed files with 23189 additions and 1439 deletions

View File

@@ -0,0 +1,540 @@
using HttpResults = Microsoft.AspNetCore.Http.Results;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace StellaOps.Concelier.WebService.Extensions;
/// <summary>
/// Management endpoints for feed mirrors, bundles, version locks, and offline status.
/// These endpoints serve the frontend dashboard at /operations/feeds.
/// Routes: /api/v1/concelier/mirrors, /bundles, /version-locks, /offline-status, /imports, /snapshots
/// </summary>
internal static class FeedMirrorManagementEndpoints
{
public static void MapFeedMirrorManagementEndpoints(this WebApplication app)
{
// Mirror management
var mirrors = app.MapGroup("/api/v1/concelier/mirrors")
.WithTags("FeedMirrors");
mirrors.MapGet(string.Empty, ListMirrors);
mirrors.MapGet("/{mirrorId}", GetMirror);
mirrors.MapPatch("/{mirrorId}", UpdateMirrorConfig);
mirrors.MapPost("/{mirrorId}/sync", TriggerSync);
mirrors.MapGet("/{mirrorId}/snapshots", ListMirrorSnapshots);
mirrors.MapGet("/{mirrorId}/retention", GetRetentionConfig);
mirrors.MapPut("/{mirrorId}/retention", UpdateRetentionConfig);
// Snapshot operations (by snapshotId)
var snapshots = app.MapGroup("/api/v1/concelier/snapshots")
.WithTags("FeedSnapshots");
snapshots.MapGet("/{snapshotId}", GetSnapshot);
snapshots.MapPost("/{snapshotId}/download", DownloadSnapshot);
snapshots.MapPatch("/{snapshotId}", PinSnapshot);
snapshots.MapDelete("/{snapshotId}", DeleteSnapshot);
// Bundle management
var bundles = app.MapGroup("/api/v1/concelier/bundles")
.WithTags("AirGapBundles");
bundles.MapGet(string.Empty, ListBundles);
bundles.MapGet("/{bundleId}", GetBundle);
bundles.MapPost(string.Empty, CreateBundle);
bundles.MapDelete("/{bundleId}", DeleteBundle);
bundles.MapPost("/{bundleId}/download", DownloadBundle);
// Import operations
var imports = app.MapGroup("/api/v1/concelier/imports")
.WithTags("AirGapImports");
imports.MapPost("/validate", ValidateImport);
imports.MapPost("/", StartImport);
imports.MapGet("/{importId}", GetImportProgress);
// Version lock operations
var versionLocks = app.MapGroup("/api/v1/concelier/version-locks")
.WithTags("VersionLocks");
versionLocks.MapGet(string.Empty, ListVersionLocks);
versionLocks.MapGet("/{feedType}", GetVersionLock);
versionLocks.MapPut("/{feedType}", SetVersionLock);
versionLocks.MapDelete("/{lockId}", RemoveVersionLock);
// Offline status
app.MapGet("/api/v1/concelier/offline-status", GetOfflineSyncStatus)
.WithTags("OfflineStatus");
}
// ---- Mirror Handlers ----
private static IResult ListMirrors(
[FromQuery] string? feedTypes,
[FromQuery] string? syncStatuses,
[FromQuery] bool? enabled,
[FromQuery] string? search)
{
var result = MirrorSeedData.Mirrors.AsEnumerable();
if (!string.IsNullOrWhiteSpace(feedTypes))
{
var types = feedTypes.Split(',', StringSplitOptions.RemoveEmptyEntries);
result = result.Where(m => types.Contains(m.FeedType, StringComparer.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(syncStatuses))
{
var statuses = syncStatuses.Split(',', StringSplitOptions.RemoveEmptyEntries);
result = result.Where(m => statuses.Contains(m.SyncStatus, StringComparer.OrdinalIgnoreCase));
}
if (enabled.HasValue)
{
result = result.Where(m => m.Enabled == enabled.Value);
}
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.ToLowerInvariant();
result = result.Where(m =>
m.Name.Contains(term, StringComparison.OrdinalIgnoreCase) ||
m.FeedType.Contains(term, StringComparison.OrdinalIgnoreCase));
}
return HttpResults.Ok(result.ToList());
}
private static IResult GetMirror(string mirrorId)
{
var mirror = MirrorSeedData.Mirrors.FirstOrDefault(m => m.MirrorId == mirrorId);
return mirror is not null ? HttpResults.Ok(mirror) : HttpResults.NotFound();
}
private static IResult UpdateMirrorConfig(string mirrorId, [FromBody] MirrorConfigUpdateDto config)
{
var mirror = MirrorSeedData.Mirrors.FirstOrDefault(m => m.MirrorId == mirrorId);
if (mirror is null) return HttpResults.NotFound();
return HttpResults.Ok(mirror with
{
Enabled = config.Enabled ?? mirror.Enabled,
SyncIntervalMinutes = config.SyncIntervalMinutes ?? mirror.SyncIntervalMinutes,
UpstreamUrl = config.UpstreamUrl ?? mirror.UpstreamUrl,
UpdatedAt = DateTimeOffset.UtcNow.ToString("o"),
});
}
private static IResult TriggerSync(string mirrorId)
{
var mirror = MirrorSeedData.Mirrors.FirstOrDefault(m => m.MirrorId == mirrorId);
if (mirror is null) return HttpResults.NotFound();
return HttpResults.Ok(new
{
mirrorId,
success = true,
snapshotId = $"snap-{mirror.FeedType}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}",
recordsUpdated = 542,
durationSeconds = 25,
error = (string?)null,
});
}
// ---- Snapshot Handlers ----
private static IResult ListMirrorSnapshots(string mirrorId)
{
var snapshots = MirrorSeedData.Snapshots.Where(s => s.MirrorId == mirrorId).ToList();
return HttpResults.Ok(snapshots);
}
private static IResult GetSnapshot(string snapshotId)
{
var snapshot = MirrorSeedData.Snapshots.FirstOrDefault(s => s.SnapshotId == snapshotId);
return snapshot is not null ? HttpResults.Ok(snapshot) : HttpResults.NotFound();
}
private static IResult DownloadSnapshot(string snapshotId)
{
var snapshot = MirrorSeedData.Snapshots.FirstOrDefault(s => s.SnapshotId == snapshotId);
if (snapshot is null) return HttpResults.NotFound();
return HttpResults.Ok(new
{
snapshotId,
status = "completed",
bytesDownloaded = snapshot.SizeBytes,
totalBytes = snapshot.SizeBytes,
percentComplete = 100,
estimatedSecondsRemaining = (int?)null,
error = (string?)null,
});
}
private static IResult PinSnapshot(string snapshotId, [FromBody] PinSnapshotDto request)
{
var snapshot = MirrorSeedData.Snapshots.FirstOrDefault(s => s.SnapshotId == snapshotId);
if (snapshot is null) return HttpResults.NotFound();
return HttpResults.Ok(snapshot with { IsPinned = request.IsPinned });
}
private static IResult DeleteSnapshot(string snapshotId)
{
var exists = MirrorSeedData.Snapshots.Any(s => s.SnapshotId == snapshotId);
return exists ? HttpResults.NoContent() : HttpResults.NotFound();
}
private static IResult GetRetentionConfig(string mirrorId)
{
return HttpResults.Ok(new
{
mirrorId,
policy = "keep_n",
keepCount = 10,
excludePinned = true,
});
}
private static IResult UpdateRetentionConfig(string mirrorId, [FromBody] RetentionConfigDto config)
{
return HttpResults.Ok(new
{
mirrorId,
policy = config.Policy ?? "keep_n",
keepCount = config.KeepCount ?? 10,
excludePinned = config.ExcludePinned ?? true,
});
}
// ---- Bundle Handlers ----
private static IResult ListBundles()
{
return HttpResults.Ok(MirrorSeedData.Bundles);
}
private static IResult GetBundle(string bundleId)
{
var bundle = MirrorSeedData.Bundles.FirstOrDefault(b => b.BundleId == bundleId);
return bundle is not null ? HttpResults.Ok(bundle) : HttpResults.NotFound();
}
private static IResult CreateBundle([FromBody] CreateBundleDto request)
{
var bundle = new AirGapBundleDto
{
BundleId = $"bundle-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}",
Name = request.Name,
Description = request.Description,
Status = "pending",
CreatedAt = DateTimeOffset.UtcNow.ToString("o"),
SizeBytes = 0,
ChecksumSha256 = "",
ChecksumSha512 = "",
IncludedFeeds = request.IncludedFeeds ?? Array.Empty<string>(),
SnapshotIds = request.SnapshotIds ?? Array.Empty<string>(),
FeedVersions = new Dictionary<string, string>(),
CreatedBy = "api",
Metadata = new Dictionary<string, object>(),
};
return HttpResults.Created($"/api/v1/concelier/bundles/{bundle.BundleId}", bundle);
}
private static IResult DeleteBundle(string bundleId)
{
var exists = MirrorSeedData.Bundles.Any(b => b.BundleId == bundleId);
return exists ? HttpResults.NoContent() : HttpResults.NotFound();
}
private static IResult DownloadBundle(string bundleId)
{
var bundle = MirrorSeedData.Bundles.FirstOrDefault(b => b.BundleId == bundleId);
if (bundle is null) return HttpResults.NotFound();
return HttpResults.Ok(new
{
snapshotId = bundleId,
status = "completed",
bytesDownloaded = bundle.SizeBytes,
totalBytes = bundle.SizeBytes,
percentComplete = 100,
estimatedSecondsRemaining = (int?)null,
error = (string?)null,
});
}
// ---- Import Handlers ----
private static IResult ValidateImport()
{
return HttpResults.Ok(new
{
bundleId = "import-validation-temp",
status = "valid",
checksumValid = true,
signatureValid = true,
manifestValid = true,
feedsFound = new[] { "nvd", "ghsa", "oval" },
snapshotsFound = new[] { "snap-nvd-imported", "snap-ghsa-imported", "snap-oval-imported" },
totalRecords = 325000,
validationErrors = Array.Empty<string>(),
warnings = new[] { "OVAL data is 3 days older than NVD data" },
canImport = true,
});
}
private static IResult StartImport([FromBody] StartImportDto request)
{
return HttpResults.Ok(new
{
importId = $"import-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}",
bundleId = request.BundleId,
status = "importing",
currentFeed = "nvd",
feedsCompleted = 0,
feedsTotal = 3,
recordsImported = 0,
recordsTotal = 325000,
percentComplete = 0,
startedAt = DateTimeOffset.UtcNow.ToString("o"),
completedAt = (string?)null,
error = (string?)null,
});
}
private static IResult GetImportProgress(string importId)
{
return HttpResults.Ok(new
{
importId,
bundleId = "bundle-full-20251229",
status = "completed",
currentFeed = (string?)null,
feedsCompleted = 3,
feedsTotal = 3,
recordsImported = 325000,
recordsTotal = 325000,
percentComplete = 100,
startedAt = "2025-12-29T10:00:00Z",
completedAt = "2025-12-29T10:15:00Z",
error = (string?)null,
});
}
// ---- Version Lock Handlers ----
private static IResult ListVersionLocks()
{
return HttpResults.Ok(MirrorSeedData.VersionLocks);
}
private static IResult GetVersionLock(string feedType)
{
var vLock = MirrorSeedData.VersionLocks.FirstOrDefault(l =>
string.Equals(l.FeedType, feedType, StringComparison.OrdinalIgnoreCase));
return vLock is not null ? HttpResults.Ok(vLock) : HttpResults.Ok((object?)null);
}
private static IResult SetVersionLock(string feedType, [FromBody] SetVersionLockDto request)
{
var newLock = new VersionLockDto
{
LockId = $"lock-{feedType}-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}",
FeedType = feedType,
Mode = request.Mode ?? "pinned",
PinnedVersion = request.PinnedVersion,
PinnedSnapshotId = request.PinnedSnapshotId,
LockedDate = request.LockedDate,
Enabled = true,
CreatedAt = DateTimeOffset.UtcNow.ToString("o"),
CreatedBy = "api",
Notes = request.Notes,
};
return HttpResults.Ok(newLock);
}
private static IResult RemoveVersionLock(string lockId)
{
var exists = MirrorSeedData.VersionLocks.Any(l => l.LockId == lockId);
return exists ? HttpResults.NoContent() : HttpResults.NotFound();
}
// ---- Offline Status Handler ----
private static IResult GetOfflineSyncStatus()
{
return HttpResults.Ok(new
{
state = "partial",
lastOnlineAt = "2025-12-29T08:00:00Z",
mirrorStats = new { total = 6, synced = 3, stale = 1, error = 1 },
feedStats = new Dictionary<string, object>
{
["nvd"] = new { lastUpdated = "2025-12-29T08:00:00Z", recordCount = 245832, isStale = false },
["ghsa"] = new { lastUpdated = "2025-12-29T09:30:00Z", recordCount = 48523, isStale = false },
["oval"] = new { lastUpdated = "2025-12-27T08:00:00Z", recordCount = 35621, isStale = true },
["osv"] = new { lastUpdated = "2025-12-28T20:00:00Z", recordCount = 125432, isStale = true },
["epss"] = new { lastUpdated = "2025-12-29T00:00:00Z", recordCount = 245000, isStale = false },
["kev"] = new { lastUpdated = "2025-12-15T00:00:00Z", recordCount = 1123, isStale = true },
["custom"] = new { lastUpdated = (string?)null, recordCount = 0, isStale = false },
},
totalStorageBytes = 5_145_000_000L,
oldestDataAge = "2025-12-15T00:00:00Z",
recommendations = new[]
{
"OSV mirror has sync errors - check network connectivity",
"OVAL mirror is 2 days stale - trigger manual sync",
"KEV mirror is disabled - enable for complete coverage",
},
});
}
// ---- DTOs ----
public sealed record FeedMirrorDto
{
public required string MirrorId { get; init; }
public required string Name { get; init; }
public required string FeedType { get; init; }
public required string UpstreamUrl { get; init; }
public required string LocalPath { get; init; }
public bool Enabled { get; init; }
public required string SyncStatus { get; init; }
public string? LastSyncAt { get; init; }
public string? NextSyncAt { get; init; }
public int SyncIntervalMinutes { get; init; }
public int SnapshotCount { get; init; }
public long TotalSizeBytes { get; init; }
public string? LatestSnapshotId { get; init; }
public string? ErrorMessage { get; init; }
public required string CreatedAt { get; init; }
public required string UpdatedAt { get; init; }
}
public sealed record FeedSnapshotDto
{
public required string SnapshotId { get; init; }
public required string MirrorId { get; init; }
public required string Version { get; init; }
public required string CreatedAt { get; init; }
public long SizeBytes { get; init; }
public required string ChecksumSha256 { get; init; }
public required string ChecksumSha512 { get; init; }
public int RecordCount { get; init; }
public required string FeedDate { get; init; }
public bool IsLatest { get; init; }
public bool IsPinned { get; init; }
public required string DownloadUrl { get; init; }
public string? ExpiresAt { get; init; }
public Dictionary<string, object> Metadata { get; init; } = new();
}
public sealed record AirGapBundleDto
{
public required string BundleId { get; init; }
public required string Name { get; init; }
public string? Description { get; init; }
public required string Status { get; init; }
public required string CreatedAt { get; init; }
public string? ExpiresAt { get; init; }
public long SizeBytes { get; init; }
public required string ChecksumSha256 { get; init; }
public required string ChecksumSha512 { get; init; }
public string[] IncludedFeeds { get; init; } = Array.Empty<string>();
public string[] SnapshotIds { get; init; } = Array.Empty<string>();
public Dictionary<string, string> FeedVersions { get; init; } = new();
public string? DownloadUrl { get; init; }
public string? SignatureUrl { get; init; }
public string? ManifestUrl { get; init; }
public required string CreatedBy { get; init; }
public Dictionary<string, object> Metadata { get; init; } = new();
}
public sealed record VersionLockDto
{
public required string LockId { get; init; }
public required string FeedType { get; init; }
public required string Mode { get; init; }
public string? PinnedVersion { get; init; }
public string? PinnedSnapshotId { get; init; }
public string? LockedDate { get; init; }
public bool Enabled { get; init; }
public required string CreatedAt { get; init; }
public required string CreatedBy { get; init; }
public string? Notes { get; init; }
}
public sealed record MirrorConfigUpdateDto
{
public bool? Enabled { get; init; }
public int? SyncIntervalMinutes { get; init; }
public string? UpstreamUrl { get; init; }
}
public sealed record PinSnapshotDto
{
public bool IsPinned { get; init; }
}
public sealed record RetentionConfigDto
{
public string? MirrorId { get; init; }
public string? Policy { get; init; }
public int? KeepCount { get; init; }
public bool? ExcludePinned { get; init; }
}
public sealed record CreateBundleDto
{
public required string Name { get; init; }
public string? Description { get; init; }
public string[]? IncludedFeeds { get; init; }
public string[]? SnapshotIds { get; init; }
public int? ExpirationDays { get; init; }
}
public sealed record StartImportDto
{
public string? BundleId { get; init; }
}
public sealed record SetVersionLockDto
{
public string? Mode { get; init; }
public string? PinnedVersion { get; init; }
public string? PinnedSnapshotId { get; init; }
public string? LockedDate { get; init; }
public string? Notes { get; init; }
}
// ---- Seed Data ----
internal static class MirrorSeedData
{
public static readonly List<FeedMirrorDto> Mirrors = new()
{
new() { MirrorId = "mirror-nvd-001", Name = "NVD Mirror", FeedType = "nvd", UpstreamUrl = "https://nvd.nist.gov/feeds/json/cve/1.1", LocalPath = "/data/mirrors/nvd", Enabled = true, SyncStatus = "synced", LastSyncAt = "2025-12-29T08:00:00Z", NextSyncAt = "2025-12-29T14:00:00Z", SyncIntervalMinutes = 360, SnapshotCount = 12, TotalSizeBytes = 2_500_000_000, LatestSnapshotId = "snap-nvd-20251229", CreatedAt = "2024-01-15T10:00:00Z", UpdatedAt = "2025-12-29T08:00:00Z" },
new() { MirrorId = "mirror-ghsa-001", Name = "GitHub Security Advisories", FeedType = "ghsa", UpstreamUrl = "https://github.com/advisories", LocalPath = "/data/mirrors/ghsa", Enabled = true, SyncStatus = "syncing", LastSyncAt = "2025-12-29T06:00:00Z", SyncIntervalMinutes = 120, SnapshotCount = 24, TotalSizeBytes = 850_000_000, LatestSnapshotId = "snap-ghsa-20251229", CreatedAt = "2024-01-15T10:00:00Z", UpdatedAt = "2025-12-29T09:30:00Z" },
new() { MirrorId = "mirror-oval-rhel-001", Name = "RHEL OVAL Definitions", FeedType = "oval", UpstreamUrl = "https://www.redhat.com/security/data/oval/v2", LocalPath = "/data/mirrors/oval-rhel", Enabled = true, SyncStatus = "stale", LastSyncAt = "2025-12-27T08:00:00Z", NextSyncAt = "2025-12-29T08:00:00Z", SyncIntervalMinutes = 1440, SnapshotCount = 8, TotalSizeBytes = 420_000_000, LatestSnapshotId = "snap-oval-rhel-20251227", CreatedAt = "2024-02-01T10:00:00Z", UpdatedAt = "2025-12-27T08:00:00Z" },
new() { MirrorId = "mirror-osv-001", Name = "OSV Database", FeedType = "osv", UpstreamUrl = "https://osv.dev/api", LocalPath = "/data/mirrors/osv", Enabled = true, SyncStatus = "error", LastSyncAt = "2025-12-28T20:00:00Z", SyncIntervalMinutes = 240, SnapshotCount = 18, TotalSizeBytes = 1_200_000_000, LatestSnapshotId = "snap-osv-20251228", ErrorMessage = "Connection timeout after 30s.", CreatedAt = "2024-01-20T10:00:00Z", UpdatedAt = "2025-12-28T20:15:00Z" },
new() { MirrorId = "mirror-epss-001", Name = "EPSS Scores", FeedType = "epss", UpstreamUrl = "https://api.first.org/data/v1/epss", LocalPath = "/data/mirrors/epss", Enabled = true, SyncStatus = "synced", LastSyncAt = "2025-12-29T00:00:00Z", NextSyncAt = "2025-12-30T00:00:00Z", SyncIntervalMinutes = 1440, SnapshotCount = 30, TotalSizeBytes = 150_000_000, LatestSnapshotId = "snap-epss-20251229", CreatedAt = "2024-03-01T10:00:00Z", UpdatedAt = "2025-12-29T00:00:00Z" },
new() { MirrorId = "mirror-kev-001", Name = "CISA KEV Catalog", FeedType = "kev", UpstreamUrl = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json", LocalPath = "/data/mirrors/kev", Enabled = false, SyncStatus = "disabled", LastSyncAt = "2025-12-15T00:00:00Z", SyncIntervalMinutes = 720, SnapshotCount = 5, TotalSizeBytes = 25_000_000, LatestSnapshotId = "snap-kev-20251215", CreatedAt = "2024-04-01T10:00:00Z", UpdatedAt = "2025-12-15T00:00:00Z" },
};
public static readonly List<FeedSnapshotDto> Snapshots = new()
{
new() { SnapshotId = "snap-nvd-20251229", MirrorId = "mirror-nvd-001", Version = "2025.12.29-001", CreatedAt = "2025-12-29T08:00:00Z", SizeBytes = 245_000_000, ChecksumSha256 = "a1b2c3d4e5f67890abcdef1234567890fedcba0987654321a1b2c3d4e5f67890", ChecksumSha512 = "sha512-checksum-placeholder", RecordCount = 245_832, FeedDate = "2025-12-29", IsLatest = true, IsPinned = false, DownloadUrl = "/api/mirrors/nvd/snapshots/snap-nvd-20251229/download", Metadata = new() { ["cveCount"] = 245832, ["modifiedCount"] = 1523 } },
new() { SnapshotId = "snap-nvd-20251228", MirrorId = "mirror-nvd-001", Version = "2025.12.28-001", CreatedAt = "2025-12-28T08:00:00Z", SizeBytes = 244_800_000, ChecksumSha256 = "b2c3d4e5f67890abcdef1234567890fedcba0987654321a1b2c3d4e5f67890ab", ChecksumSha512 = "sha512-checksum-placeholder-2", RecordCount = 245_621, FeedDate = "2025-12-28", IsLatest = false, IsPinned = true, DownloadUrl = "/api/mirrors/nvd/snapshots/snap-nvd-20251228/download", Metadata = new() { ["cveCount"] = 245621, ["modifiedCount"] = 892 } },
new() { SnapshotId = "snap-nvd-20251227", MirrorId = "mirror-nvd-001", Version = "2025.12.27-001", CreatedAt = "2025-12-27T08:00:00Z", SizeBytes = 244_500_000, ChecksumSha256 = "c3d4e5f67890abcdef1234567890fedcba0987654321a1b2c3d4e5f67890abcd", ChecksumSha512 = "sha512-checksum-placeholder-3", RecordCount = 245_412, FeedDate = "2025-12-27", IsLatest = false, IsPinned = false, DownloadUrl = "/api/mirrors/nvd/snapshots/snap-nvd-20251227/download", ExpiresAt = "2026-01-27T08:00:00Z", Metadata = new() { ["cveCount"] = 245412, ["modifiedCount"] = 756 } },
};
public static readonly List<AirGapBundleDto> Bundles = new()
{
new() { BundleId = "bundle-full-20251229", Name = "Full Feed Bundle - December 2025", Description = "Complete vulnerability feed bundle for air-gapped deployment", Status = "ready", CreatedAt = "2025-12-29T06:00:00Z", ExpiresAt = "2026-03-29T06:00:00Z", SizeBytes = 4_500_000_000, ChecksumSha256 = "bundle-sha256-checksum-full-20251229", ChecksumSha512 = "bundle-sha512-checksum-full-20251229", IncludedFeeds = new[] { "nvd", "ghsa", "oval", "osv", "epss" }, SnapshotIds = new[] { "snap-nvd-20251229", "snap-ghsa-20251229", "snap-oval-20251229" }, FeedVersions = new() { ["nvd"] = "2025.12.29-001", ["ghsa"] = "2025.12.29-001", ["oval"] = "2025.12.27-001", ["osv"] = "2025.12.28-001", ["epss"] = "2025.12.29-001" }, DownloadUrl = "/api/airgap/bundles/bundle-full-20251229/download", SignatureUrl = "/api/airgap/bundles/bundle-full-20251229/signature", ManifestUrl = "/api/airgap/bundles/bundle-full-20251229/manifest", CreatedBy = "system", Metadata = new() { ["totalRecords"] = 850000 } },
new() { BundleId = "bundle-critical-20251229", Name = "Critical Feeds Only - December 2025", Description = "NVD and KEV feeds for minimal deployment", Status = "building", CreatedAt = "2025-12-29T09:00:00Z", SizeBytes = 0, ChecksumSha256 = "", ChecksumSha512 = "", IncludedFeeds = new[] { "nvd", "kev" }, CreatedBy = "admin@stellaops.io" },
};
public static readonly List<VersionLockDto> VersionLocks = new()
{
new() { LockId = "lock-nvd-001", FeedType = "nvd", Mode = "pinned", PinnedVersion = "2025.12.28-001", PinnedSnapshotId = "snap-nvd-20251228", Enabled = true, CreatedAt = "2025-12-28T10:00:00Z", CreatedBy = "security-team", Notes = "Pinned for Q4 compliance audit" },
new() { LockId = "lock-epss-001", FeedType = "epss", Mode = "latest", Enabled = true, CreatedAt = "2025-11-01T10:00:00Z", CreatedBy = "risk-team", Notes = "Always use latest EPSS scores" },
};
}
}

View File

@@ -647,17 +647,24 @@ if (authorityConfigured)
resourceOptions.MetadataAddress = concelierOptions.Authority.MetadataAddress;
}
foreach (var audience in concelierOptions.Authority.Audiences)
// Read collections directly from IConfiguration to work around
// .NET Configuration.Bind() not populating IList<string> in nested init objects.
var authSection = builder.Configuration.GetSection("Authority");
var cfgAudiences = authSection.GetSection("Audiences").Get<string[]>() ?? [];
foreach (var audience in cfgAudiences)
{
resourceOptions.Audiences.Add(audience);
}
foreach (var scope in concelierOptions.Authority.RequiredScopes)
var cfgScopes = authSection.GetSection("RequiredScopes").Get<string[]>() ?? [];
foreach (var scope in cfgScopes)
{
resourceOptions.RequiredScopes.Add(scope);
}
foreach (var network in concelierOptions.Authority.BypassNetworks)
var cfgBypassNetworks = authSection.GetSection("BypassNetworks").Get<string[]>() ?? [];
foreach (var network in cfgBypassNetworks)
{
resourceOptions.BypassNetworks.Add(network);
}
@@ -762,7 +769,13 @@ if (authorityConfigured)
resourceOptions.BackchannelTimeout = TimeSpan.FromSeconds(authority.BackchannelTimeoutSeconds);
resourceOptions.TokenClockSkew = TimeSpan.FromSeconds(authority.TokenClockSkewSeconds);
foreach (var audience in authority.Audiences)
// Also read collections directly from IConfiguration here (TestSigningSecret branch)
// to work around .NET Configuration.Bind() not populating IList<string>.
var cfg = builder.Configuration;
var authCfgSection = cfg.GetSection("Authority");
var cfgAudiences2 = authCfgSection.GetSection("Audiences").Get<string[]>() ?? [];
foreach (var audience in cfgAudiences2)
{
if (!resourceOptions.Audiences.Contains(audience))
{
@@ -770,7 +783,8 @@ if (authorityConfigured)
}
}
foreach (var scope in authority.RequiredScopes)
var cfgScopes2 = authCfgSection.GetSection("RequiredScopes").Get<string[]>() ?? [];
foreach (var scope in cfgScopes2)
{
if (!resourceOptions.RequiredScopes.Contains(scope))
{
@@ -778,7 +792,8 @@ if (authorityConfigured)
}
}
foreach (var network in authority.BypassNetworks)
var cfgBypass2 = authCfgSection.GetSection("BypassNetworks").Get<string[]>() ?? [];
foreach (var network in cfgBypass2)
{
if (!resourceOptions.BypassNetworks.Contains(network))
{
@@ -786,7 +801,8 @@ if (authorityConfigured)
}
}
foreach (var tenant in authority.RequiredTenants)
var cfgTenants2 = authCfgSection.GetSection("RequiredTenants").Get<string[]>() ?? [];
foreach (var tenant in cfgTenants2)
{
if (!resourceOptions.RequiredTenants.Contains(tenant))
{
@@ -898,6 +914,15 @@ app.MapInterestScoreEndpoints();
// Federation endpoints for site-to-site bundle sync
app.MapConcelierFederationEndpoints();
// AirGap endpoints for sealed-mode operations
app.MapConcelierAirGapEndpoints();
// Feed snapshot endpoints for atomic multi-source snapshots
app.MapFeedSnapshotEndpoints();
// Feed mirror management, bundles, version locks, offline status
app.MapFeedMirrorManagementEndpoints();
app.MapGet("/.well-known/openapi", ([FromServices] OpenApiDiscoveryDocumentProvider provider, HttpContext context) =>
{
var (payload, etag) = provider.GetDocument();