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
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user