feat: Implement MongoDB orchestrator storage with registry, commands, and heartbeats
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added NullAdvisoryObservationEventTransport for handling advisory observation events.
- Created IOrchestratorRegistryStore interface for orchestrator registry operations.
- Implemented MongoOrchestratorRegistryStore for MongoDB interactions with orchestrator data.
- Defined OrchestratorCommandDocument and OrchestratorCommandRecord for command handling.
- Added OrchestratorHeartbeatDocument and OrchestratorHeartbeatRecord for heartbeat tracking.
- Created OrchestratorRegistryDocument and OrchestratorRegistryRecord for registry management.
- Developed tests for orchestrator collections migration and MongoOrchestratorRegistryStore functionality.
- Introduced AirgapImportRequest and AirgapImportValidator for air-gapped VEX bundle imports.
- Added incident mode rules sample JSON for notifier configuration.
This commit is contained in:
StellaOps Bot
2025-11-22 12:35:38 +02:00
parent cbdc05b24d
commit f43e828b4e
96 changed files with 3425 additions and 976 deletions

View File

@@ -0,0 +1,35 @@
using System;
using System.Text.Json.Serialization;
namespace StellaOps.Excititor.WebService.Contracts;
/// <summary>
/// Envelope for air-gapped VEX bundle imports.
/// Mirrors the thin mirror bundle schema and carries signing metadata.
/// </summary>
public sealed class AirgapImportRequest
{
[JsonPropertyName("bundleId")]
public string? BundleId { get; init; }
[JsonPropertyName("mirrorGeneration")]
public string? MirrorGeneration { get; init; }
[JsonPropertyName("signedAt")]
public DateTimeOffset? SignedAt { get; init; }
[JsonPropertyName("publisher")]
public string? Publisher { get; init; }
[JsonPropertyName("payloadHash")]
public string? PayloadHash { get; init; }
[JsonPropertyName("payloadUrl")]
public string? PayloadUrl { get; init; }
[JsonPropertyName("signature")]
public string? Signature { get; init; }
[JsonPropertyName("transparencyLog")]
public string? TransparencyLog { get; init; }
}

View File

@@ -48,6 +48,7 @@ services.AddCsafNormalizer();
services.AddCycloneDxNormalizer();
services.AddOpenVexNormalizer();
services.AddSingleton<IVexSignatureVerifier, NoopVexSignatureVerifier>();
services.AddSingleton<AirgapImportValidator>();
services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>();
services.AddScoped<IVexObservationLookup, MongoVexObservationLookup>();
services.AddOptions<ExcititorObservabilityOptions>()
@@ -140,6 +141,33 @@ app.MapGet("/excititor/status", async (HttpContext context,
app.MapHealthChecks("/excititor/health");
app.MapPost("/airgap/v1/vex/import", async (
[FromServices] AirgapImportValidator validator,
[FromServices] TimeProvider timeProvider,
[FromBody] AirgapImportRequest request,
CancellationToken cancellationToken) =>
{
var errors = validator.Validate(request, timeProvider.GetUtcNow());
if (errors.Count > 0)
{
var first = errors[0];
return Results.BadRequest(new
{
error = new
{
code = first.Code,
message = first.Message
}
});
}
return Results.Accepted($"/airgap/v1/vex/import/{request.BundleId}", new
{
bundleId = request.BundleId,
generation = request.MirrorGeneration
});
});
app.MapPost("/v1/attestations/verify", async (
[FromServices] IVexAttestationClient attestationClient,
[FromBody] AttestationVerifyRequest request,

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Globalization;
namespace StellaOps.Excititor.WebService.Services;
internal sealed class AirgapImportValidator
{
private static readonly TimeSpan AllowedSkew = TimeSpan.FromSeconds(5);
public IReadOnlyList<ValidationError> Validate(AirgapImportRequest request, DateTimeOffset nowUtc)
{
var errors = new List<ValidationError>();
if (request is null)
{
errors.Add(new ValidationError("invalid_request", "Request body is required."));
return errors;
}
if (string.IsNullOrWhiteSpace(request.BundleId))
{
errors.Add(new ValidationError("bundle_id_missing", "bundleId is required."));
}
if (string.IsNullOrWhiteSpace(request.MirrorGeneration))
{
errors.Add(new ValidationError("mirror_generation_missing", "mirrorGeneration is required."));
}
if (string.IsNullOrWhiteSpace(request.Publisher))
{
errors.Add(new ValidationError("publisher_missing", "publisher is required."));
}
if (string.IsNullOrWhiteSpace(request.PayloadHash))
{
errors.Add(new ValidationError("payload_hash_missing", "payloadHash is required."));
}
if (string.IsNullOrWhiteSpace(request.Signature))
{
errors.Add(new ValidationError("AIRGAP_SIGNATURE_MISSING", "signature is required for air-gapped imports."));
}
if (request.SignedAt is null)
{
errors.Add(new ValidationError("signed_at_missing", "signedAt is required."));
}
else
{
var delta = (nowUtc - request.SignedAt.Value).Duration();
if (delta > AllowedSkew)
{
errors.Add(new ValidationError(
"AIRGAP_PAYLOAD_STALE",
$"signedAt exceeds allowed skew of {AllowedSkew.TotalSeconds.ToString(CultureInfo.InvariantCulture)} seconds."));
}
}
return errors;
}
public readonly record struct ValidationError(string Code, string Message);
}

View File

@@ -0,0 +1,54 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Excititor.WebService.Contracts;
using Xunit;
namespace StellaOps.Excititor.WebService.Tests;
public class AirgapImportEndpointTests : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client;
public AirgapImportEndpointTests(TestWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task Import_returns_bad_request_when_signature_missing()
{
var request = new AirgapImportRequest
{
BundleId = "bundle-123",
MirrorGeneration = "gen-1",
SignedAt = DateTimeOffset.UtcNow,
Publisher = "mirror-test",
PayloadHash = "sha256:abc"
};
var response = await _client.PostAsJsonAsync("/airgap/v1/vex/import", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("AIRGAP_SIGNATURE_MISSING", json.GetProperty("error").GetProperty("code").GetString());
}
[Fact]
public async Task Import_accepts_valid_payload()
{
var request = new AirgapImportRequest
{
BundleId = "bundle-123",
MirrorGeneration = "gen-1",
SignedAt = DateTimeOffset.UtcNow,
Publisher = "mirror-test",
PayloadHash = "sha256:abc",
Signature = "sig"
};
using var response = await _client.PostAsJsonAsync("/airgap/v1/vex/import", request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
}
}