feat: Add DigestUpsertRequest and LockEntity models
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
Export Center CI / export-ci (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

- Introduced DigestUpsertRequest for handling digest upsert requests with properties like ChannelId, Recipient, DigestKey, Events, and CollectUntil.
- Created LockEntity to represent a lightweight distributed lock entry with properties such as Id, TenantId, Resource, Owner, ExpiresAt, and CreatedAt.

feat: Implement ILockRepository interface and LockRepository class

- Defined ILockRepository interface with methods for acquiring and releasing locks.
- Implemented LockRepository class with methods to try acquiring a lock and releasing it, using SQL for upsert operations.

feat: Add SurfaceManifestPointer record for manifest pointers

- Introduced SurfaceManifestPointer to represent a minimal pointer to a Surface.FS manifest associated with an image digest.

feat: Create PolicySimulationInputLock and related validation logic

- Added PolicySimulationInputLock record to describe policy simulation inputs and expected digests.
- Implemented validation logic for policy simulation inputs, including checks for digest drift and shadow mode requirements.

test: Add unit tests for ReplayVerificationService and ReplayVerifier

- Created ReplayVerificationServiceTests to validate the behavior of the ReplayVerificationService under various scenarios.
- Developed ReplayVerifierTests to ensure the correctness of the ReplayVerifier logic.

test: Implement PolicySimulationInputLockValidatorTests

- Added tests for PolicySimulationInputLockValidator to verify the validation logic against expected inputs and conditions.

chore: Add cosign key example and signing scripts

- Included a placeholder cosign key example for development purposes.
- Added a script for signing Signals artifacts using cosign with support for both v2 and v3.

chore: Create script for uploading evidence to the evidence locker

- Developed a script to upload evidence to the evidence locker, ensuring required environment variables are set.
This commit is contained in:
StellaOps Bot
2025-12-03 07:51:50 +02:00
parent 37cba83708
commit e923880694
171 changed files with 6567 additions and 2952 deletions

View File

@@ -1,3 +1,11 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Contracts;
using StellaOps.Excititor.WebService.Services;
using Xunit;
@@ -25,21 +33,113 @@ public class AirgapImportEndpointTests
}
[Fact]
public void Import_accepts_valid_payload()
public async Task Import_records_actor_and_scope_and_timeline()
{
var validator = new AirgapImportValidator();
var store = new CapturingAirgapStore();
using var factory = new TestWebApplicationFactory(
configureConfiguration: config =>
{
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string?>("Excititor:Airgap:SealedMode", "false"),
new KeyValuePair<string, string?>("Excititor:Airgap:MirrorOnly", "false"),
});
},
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.RemoveAll<IAirgapImportStore>();
services.AddSingleton<IAirgapImportStore>(store);
services.AddTestAuthentication();
});
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "vex.admin");
var request = new AirgapImportRequest
{
BundleId = "bundle-123",
BundleId = "bundle-abc",
MirrorGeneration = "1",
SignedAt = DateTimeOffset.UtcNow,
Publisher = "mirror-test",
PayloadHash = "sha256:" + new string('a', 64),
PayloadHash = "sha256:" + new string('b', 64),
Signature = Convert.ToBase64String(new byte[] { 1, 2, 3 })
};
var errors = validator.Validate(request, DateTimeOffset.UtcNow);
var response = await client.PostAsJsonAsync("/airgap/v1/vex/import", request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
Assert.Empty(errors);
var saved = store.LastSaved;
Assert.NotNull(saved);
Assert.Equal("test-user", saved!.ImportActor);
Assert.Equal("vex.admin", saved.ImportScopes);
Assert.All(saved.Timeline, e =>
{
Assert.Equal("test-user", e.Actor);
Assert.Equal("vex.admin", e.Scopes);
});
}
[Fact]
public async Task Import_returns_remediation_for_sealed_mode_violation()
{
var store = new CapturingAirgapStore();
using var factory = new TestWebApplicationFactory(
configureConfiguration: config =>
{
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string?>("Excititor:Airgap:SealedMode", "true"),
new KeyValuePair<string, string?>("Excititor:Airgap:MirrorOnly", "true"),
});
},
configureServices: services =>
{
TestServiceOverrides.Apply(services);
services.RemoveAll<IAirgapImportStore>();
services.AddSingleton<IAirgapImportStore>(store);
services.AddTestAuthentication();
});
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "vex.admin");
var request = new AirgapImportRequest
{
BundleId = "bundle-xyz",
MirrorGeneration = "2",
SignedAt = DateTimeOffset.UtcNow,
Publisher = "mirror-test",
PayloadHash = "sha256:" + new string('c', 64),
PayloadUrl = "https://example.com/payload.tgz",
Signature = Convert.ToBase64String(new byte[] { 9, 9, 9 })
};
var response = await client.PostAsJsonAsync("/airgap/v1/vex/import", request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
var raw = await response.Content.ReadAsStringAsync();
Assert.Contains("AIRGAP_EGRESS_BLOCKED", raw);
Assert.Contains("remediation", raw);
}
private sealed class CapturingAirgapStore : IAirgapImportStore
{
public AirgapImportRecord? LastSaved { get; private set; }
public Task SaveAsync(AirgapImportRecord record, CancellationToken cancellationToken)
{
LastSaved = record;
return Task.CompletedTask;
}
public Task<AirgapImportRecord?> FindByBundleIdAsync(string tenantId, string bundleId, string? mirrorGeneration, CancellationToken cancellationToken)
=> Task.FromResult(LastSaved);
public Task<IReadOnlyList<AirgapImportRecord>> ListAsync(string tenantId, string? publisherFilter, DateTimeOffset? importedAfter, int limit, int offset, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<AirgapImportRecord>>(LastSaved is null ? Array.Empty<AirgapImportRecord>() : new[] { LastSaved });
public Task<int> CountAsync(string tenantId, string? publisherFilter, DateTimeOffset? importedAfter, CancellationToken cancellationToken)
=> Task.FromResult(LastSaved is null ? 0 : 1);
}
}

View File

@@ -19,6 +19,7 @@ public sealed class EvidenceLockerEndpointTests : IAsyncLifetime
{
private readonly string _tempDir = Path.Combine(Path.GetTempPath(), "excititor-locker-tests-" + Guid.NewGuid());
private TestWebApplicationFactory _factory = null!;
private StubAirgapImportStore _stubStore = null!;
[Fact]
public async Task LockerEndpoint_ReturnsHashesFromLocalFiles_WhenLockerRootConfigured()
@@ -48,8 +49,7 @@ public sealed class EvidenceLockerEndpointTests : IAsyncLifetime
SignedAt = DateTimeOffset.UtcNow,
};
var stub = (StubAirgapImportStore)_factory.Services.GetRequiredService<IAirgapImportStore>();
await stub.SaveAsync(record, CancellationToken.None);
await _stubStore.SaveAsync(record, CancellationToken.None);
using var client = _factory.WithWebHostBuilder(_ => { }).CreateClient();
@@ -68,21 +68,61 @@ public sealed class EvidenceLockerEndpointTests : IAsyncLifetime
Assert.Equal(12, payload.EvidenceSizeBytes);
}
[Fact]
public async Task LockerManifestFile_StreamsContent_WithETag()
{
Directory.CreateDirectory(_tempDir);
var manifestRel = Path.Combine("locker", "bundle-2", "g2", "manifest.json");
Directory.CreateDirectory(Path.GetDirectoryName(Path.Combine(_tempDir, manifestRel))!);
var manifestBody = "{\"hello\":\"world\"}\n";
await File.WriteAllTextAsync(Path.Combine(_tempDir, manifestRel), manifestBody);
var record = new AirgapImportRecord
{
Id = "bundle-2:g2",
TenantId = "test",
BundleId = "bundle-2",
MirrorGeneration = "g2",
Publisher = "pub",
PayloadHash = "sha256:payload",
Signature = "sig",
PortableManifestPath = manifestRel,
PortableManifestHash = "sha256:old",
EvidenceLockerPath = "locker/bundle-2/g2/bundle.ndjson",
ImportedAt = DateTimeOffset.UtcNow,
SignedAt = DateTimeOffset.UtcNow,
};
await _stubStore.SaveAsync(record, CancellationToken.None);
using var client = _factory.WithWebHostBuilder(_ => { }).CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "vex.read");
var response = await client.GetAsync($"/evidence/vex/locker/{record.BundleId}/manifest/file");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/json", response.Content.Headers.ContentType!.MediaType);
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(manifestBody, body);
Assert.Equal("sha256:6a47c31b7b7c3b9a1dbc960669f4674ce088c8fc9d9a4f7e9fcc3f6a81f7b86c", response.Headers.ETag?.Tag?.Trim('"'));
}
public Task InitializeAsync()
{
_factory = new TestWebApplicationFactory(
configureConfiguration: config =>
{
config.AddInMemoryCollection(new[]
_stubStore = new StubAirgapImportStore();
_factory = new TestWebApplicationFactory(
configureConfiguration: config =>
{
new KeyValuePair<string, string?>("Excititor:Airgap:LockerRootPath", _tempDir)
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string?>("Excititor:Airgap:LockerRootPath", _tempDir)
});
},
configureServices: services =>
{
// Enable test authentication so evidence endpoints that enforce scopes accept the bearer header set in the tests.
services.AddTestAuthentication();
services.RemoveAll<IAirgapImportStore>();
services.AddSingleton<IAirgapImportStore>(_stubStore);
});
},
configureServices: services =>
{
services.RemoveAll<IAirgapImportStore>();
services.AddSingleton<IAirgapImportStore>(new StubAirgapImportStore());
});
return Task.CompletedTask;
}

View File

@@ -33,6 +33,7 @@
<Compile Include="AirgapImportValidatorTests.cs" />
<Compile Include="AirgapModeEnforcerTests.cs" />
<Compile Include="EvidenceTelemetryTests.cs" />
<Compile Include="EvidenceLockerEndpointTests.cs" />
<Compile Include="DevRuntimeEnvironmentStub.cs" />
<Compile Include="TestAuthentication.cs" />
<Compile Include="TestServiceOverrides.cs" />