feat: Implement air-gap functionality with timeline impact and evidence snapshot services
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
- Added AirgapTimelineImpact, AirgapTimelineImpactInput, and AirgapTimelineImpactResult records for managing air-gap bundle import impacts. - Introduced EvidenceSnapshotRecord, EvidenceSnapshotLinkInput, and EvidenceSnapshotLinkResult records for linking findings to evidence snapshots. - Created IEvidenceSnapshotRepository interface for managing evidence snapshot records. - Developed StalenessValidationService to validate staleness and enforce freshness thresholds. - Implemented AirgapTimelineService for emitting timeline events related to bundle imports. - Added EvidenceSnapshotService for linking findings to evidence snapshots and verifying their validity. - Introduced AirGapOptions for configuring air-gap staleness enforcement and thresholds. - Added minimal jsPDF stub for offline/testing builds in the web application. - Created TypeScript definitions for jsPDF to enhance type safety in the web application.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.WebService.Options;
|
||||
using StellaOps.Concelier.WebService.Services;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.WebService.Diagnostics;
|
||||
using StellaOps.Concelier.WebService.Options;
|
||||
using StellaOps.Concelier.WebService.Results;
|
||||
using StellaOps.Concelier.WebService.Services;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Extensions;
|
||||
|
||||
@@ -24,7 +26,7 @@ internal static class MirrorEndpointExtensions
|
||||
var mirrorOptions = optionsMonitor.CurrentValue.Mirror ?? new ConcelierOptions.MirrorOptions();
|
||||
if (!mirrorOptions.Enabled)
|
||||
{
|
||||
return Results.NotFound();
|
||||
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
||||
}
|
||||
|
||||
if (!TryAuthorize(mirrorOptions.RequireAuthentication, enforceAuthority, context, authorityConfigured, out var unauthorizedResult))
|
||||
@@ -35,15 +37,15 @@ internal static class MirrorEndpointExtensions
|
||||
if (!limiter.TryAcquire("__index__", IndexScope, mirrorOptions.MaxIndexRequestsPerHour, out var retryAfter))
|
||||
{
|
||||
ApplyRetryAfter(context.Response, retryAfter);
|
||||
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
|
||||
return ConcelierProblemResultFactory.RateLimitExceeded(context, (int?)retryAfter?.TotalSeconds);
|
||||
}
|
||||
|
||||
if (!locator.TryResolveIndex(out var path, out _))
|
||||
{
|
||||
return Results.NotFound();
|
||||
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
||||
}
|
||||
|
||||
return await WriteFileAsync(path, context.Response, "application/json").ConfigureAwait(false);
|
||||
return await WriteFileAsync(context, path, "application/json").ConfigureAwait(false);
|
||||
});
|
||||
|
||||
app.MapGet("/concelier/exports/{**relativePath}", async (
|
||||
@@ -57,17 +59,17 @@ internal static class MirrorEndpointExtensions
|
||||
var mirrorOptions = optionsMonitor.CurrentValue.Mirror ?? new ConcelierOptions.MirrorOptions();
|
||||
if (!mirrorOptions.Enabled)
|
||||
{
|
||||
return Results.NotFound();
|
||||
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
return Results.NotFound();
|
||||
return ConcelierProblemResultFactory.MirrorNotFound(context);
|
||||
}
|
||||
|
||||
if (!locator.TryResolveRelativePath(relativePath, out var path, out _, out var domainId))
|
||||
{
|
||||
return Results.NotFound();
|
||||
return ConcelierProblemResultFactory.MirrorNotFound(context, relativePath);
|
||||
}
|
||||
|
||||
var domain = FindDomain(mirrorOptions, domainId);
|
||||
@@ -81,11 +83,11 @@ internal static class MirrorEndpointExtensions
|
||||
if (!limiter.TryAcquire(domain?.Id ?? "__mirror__", DownloadScope, limit, out var retryAfter))
|
||||
{
|
||||
ApplyRetryAfter(context.Response, retryAfter);
|
||||
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
|
||||
return ConcelierProblemResultFactory.RateLimitExceeded(context, (int?)retryAfter?.TotalSeconds);
|
||||
}
|
||||
|
||||
var contentType = ResolveContentType(path);
|
||||
return await WriteFileAsync(path, context.Response, contentType).ConfigureAwait(false);
|
||||
return await WriteFileAsync(context, path, contentType).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,12 +114,12 @@ internal static class MirrorEndpointExtensions
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryAuthorize(bool requireAuthentication, bool enforceAuthority, HttpContext context, bool authorityConfigured, out IResult result)
|
||||
{
|
||||
result = Results.Empty;
|
||||
if (!requireAuthentication)
|
||||
{
|
||||
return true;
|
||||
private static bool TryAuthorize(bool requireAuthentication, bool enforceAuthority, HttpContext context, bool authorityConfigured, out IResult result)
|
||||
{
|
||||
result = Results.Empty;
|
||||
if (!requireAuthentication)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!enforceAuthority || !authorityConfigured)
|
||||
@@ -128,19 +130,19 @@ internal static class MirrorEndpointExtensions
|
||||
if (context.User?.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
context.Response.Headers.WWWAuthenticate = "Bearer realm=\"StellaOps Concelier Mirror\"";
|
||||
result = Results.StatusCode(StatusCodes.Status401Unauthorized);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Task<IResult> WriteFileAsync(string path, HttpResponse response, string contentType)
|
||||
{
|
||||
}
|
||||
|
||||
context.Response.Headers.WWWAuthenticate = "Bearer realm=\"StellaOps Concelier Mirror\"";
|
||||
result = Results.StatusCode(StatusCodes.Status401Unauthorized);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Task<IResult> WriteFileAsync(HttpContext context, string path, string contentType)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return Task.FromResult(Results.NotFound());
|
||||
return Task.FromResult(ConcelierProblemResultFactory.MirrorNotFound(context, path));
|
||||
}
|
||||
|
||||
var stream = new FileStream(
|
||||
@@ -149,12 +151,12 @@ internal static class MirrorEndpointExtensions
|
||||
FileAccess.Read,
|
||||
FileShare.Read | FileShare.Delete);
|
||||
|
||||
response.Headers.CacheControl = BuildCacheControlHeader(path);
|
||||
response.Headers.LastModified = fileInfo.LastWriteTimeUtc.ToString("R", CultureInfo.InvariantCulture);
|
||||
response.ContentLength = fileInfo.Length;
|
||||
return Task.FromResult(Results.Stream(stream, contentType));
|
||||
}
|
||||
|
||||
context.Response.Headers.CacheControl = BuildCacheControlHeader(path);
|
||||
context.Response.Headers.LastModified = fileInfo.LastWriteTimeUtc.ToString("R", CultureInfo.InvariantCulture);
|
||||
context.Response.ContentLength = fileInfo.Length;
|
||||
return Task.FromResult(Results.Stream(stream, contentType));
|
||||
}
|
||||
|
||||
private static string ResolveContentType(string path)
|
||||
{
|
||||
if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -178,28 +180,28 @@ internal static class MirrorEndpointExtensions
|
||||
}
|
||||
|
||||
var seconds = Math.Max((int)Math.Ceiling(retryAfter.Value.TotalSeconds), 1);
|
||||
response.Headers.RetryAfter = seconds.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string BuildCacheControlHeader(string path)
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
if (fileName is null)
|
||||
{
|
||||
return "public, max-age=60";
|
||||
}
|
||||
|
||||
if (string.Equals(fileName, "index.json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "public, max-age=60";
|
||||
}
|
||||
|
||||
if (fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) ||
|
||||
fileName.EndsWith(".jws", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "public, max-age=300, immutable";
|
||||
}
|
||||
|
||||
return "public, max-age=300";
|
||||
}
|
||||
}
|
||||
response.Headers.RetryAfter = seconds.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string BuildCacheControlHeader(string path)
|
||||
{
|
||||
var fileName = Path.GetFileName(path);
|
||||
if (fileName is null)
|
||||
{
|
||||
return "public, max-age=60";
|
||||
}
|
||||
|
||||
if (string.Equals(fileName, "index.json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "public, max-age=60";
|
||||
}
|
||||
|
||||
if (fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) ||
|
||||
fileName.EndsWith(".jws", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "public, max-age=300, immutable";
|
||||
}
|
||||
|
||||
return "public, max-age=300";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user