compose and authority fixes. finish sprints.
This commit is contained in:
@@ -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" },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user