up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,25 +1,25 @@
using System.Net;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class AuthorizationTests
{
[Fact]
public async Task ApiRoutesRequireAuthenticationWhenAuthorityEnabled()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:authority:enabled"] = "true";
configuration["scanner:authority:allowAnonymousFallback"] = "false";
configuration["scanner:authority:issuer"] = "https://authority.local";
configuration["scanner:authority:audiences:0"] = "scanner-api";
configuration["scanner:authority:clientId"] = "scanner-web";
configuration["scanner:authority:clientSecret"] = "secret";
});
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/__auth-probe");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
using System.Net;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class AuthorizationTests
{
[Fact]
public async Task ApiRoutesRequireAuthenticationWhenAuthorityEnabled()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:authority:enabled"] = "true";
configuration["scanner:authority:allowAnonymousFallback"] = "false";
configuration["scanner:authority:issuer"] = "https://authority.local";
configuration["scanner:authority:audiences:0"] = "scanner-api";
configuration["scanner:authority:clientId"] = "scanner-web";
configuration["scanner:authority:clientSecret"] = "secret";
});
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/__auth-probe");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}

View File

@@ -1,49 +1,49 @@
using System.Net.Http.Json;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class HealthEndpointsTests
{
[Fact]
public async Task HealthAndReadyEndpointsRespond()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var healthResponse = await client.GetAsync("/healthz");
Assert.True(healthResponse.IsSuccessStatusCode, $"Expected 200 from /healthz, received {(int)healthResponse.StatusCode}.");
var readyResponse = await client.GetAsync("/readyz");
Assert.True(readyResponse.IsSuccessStatusCode, $"Expected 200 from /readyz, received {(int)readyResponse.StatusCode}.");
var healthDocument = await healthResponse.Content.ReadFromJsonAsync<HealthDocument>();
Assert.NotNull(healthDocument);
Assert.Equal("healthy", healthDocument!.Status);
Assert.True(healthDocument.UptimeSeconds >= 0);
Assert.NotNull(healthDocument.Telemetry);
var readyDocument = await readyResponse.Content.ReadFromJsonAsync<ReadyDocument>();
Assert.NotNull(readyDocument);
Assert.Equal("ready", readyDocument!.Status);
Assert.Null(readyDocument.Error);
}
private sealed record HealthDocument(
string Status,
DateTimeOffset StartedAt,
DateTimeOffset CapturedAt,
double UptimeSeconds,
TelemetryDocument Telemetry);
private sealed record TelemetryDocument(
bool Enabled,
bool Logging,
bool Metrics,
bool Tracing);
private sealed record ReadyDocument(
string Status,
DateTimeOffset CheckedAt,
double? LatencyMs,
string? Error);
}
using System.Net.Http.Json;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class HealthEndpointsTests
{
[Fact]
public async Task HealthAndReadyEndpointsRespond()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var healthResponse = await client.GetAsync("/healthz");
Assert.True(healthResponse.IsSuccessStatusCode, $"Expected 200 from /healthz, received {(int)healthResponse.StatusCode}.");
var readyResponse = await client.GetAsync("/readyz");
Assert.True(readyResponse.IsSuccessStatusCode, $"Expected 200 from /readyz, received {(int)readyResponse.StatusCode}.");
var healthDocument = await healthResponse.Content.ReadFromJsonAsync<HealthDocument>();
Assert.NotNull(healthDocument);
Assert.Equal("healthy", healthDocument!.Status);
Assert.True(healthDocument.UptimeSeconds >= 0);
Assert.NotNull(healthDocument.Telemetry);
var readyDocument = await readyResponse.Content.ReadFromJsonAsync<ReadyDocument>();
Assert.NotNull(readyDocument);
Assert.Equal("ready", readyDocument!.Status);
Assert.Null(readyDocument.Error);
}
private sealed record HealthDocument(
string Status,
DateTimeOffset StartedAt,
DateTimeOffset CapturedAt,
double UptimeSeconds,
TelemetryDocument Telemetry);
private sealed record TelemetryDocument(
bool Enabled,
bool Logging,
bool Metrics,
bool Tracing);
private sealed record ReadyDocument(
string Status,
DateTimeOffset CheckedAt,
double? LatencyMs,
string? Error);
}

View File

@@ -1,71 +1,71 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class PlatformEventPublisherRegistrationTests
{
[Fact]
public void NullPublisherRegisteredWhenEventsDisabled()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:events:enabled"] = "false";
configuration["scanner:events:dsn"] = string.Empty;
});
using var scope = factory.Services.CreateScope();
var publisher = scope.ServiceProvider.GetRequiredService<IPlatformEventPublisher>();
Assert.IsType<NullPlatformEventPublisher>(publisher);
}
[Fact]
public void RedisPublisherRegisteredWhenEventsEnabled()
{
var originalEnabled = Environment.GetEnvironmentVariable("SCANNER__EVENTS__ENABLED");
var originalDriver = Environment.GetEnvironmentVariable("SCANNER__EVENTS__DRIVER");
var originalDsn = Environment.GetEnvironmentVariable("SCANNER__EVENTS__DSN");
var originalStream = Environment.GetEnvironmentVariable("SCANNER__EVENTS__STREAM");
var originalTimeout = Environment.GetEnvironmentVariable("SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS");
var originalMax = Environment.GetEnvironmentVariable("SCANNER__EVENTS__MAXSTREAMLENGTH");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", "true");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DRIVER", "redis");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DSN", "localhost:6379");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__STREAM", "stella.events.tests");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS", "1");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__MAXSTREAMLENGTH", "100");
try
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:events:enabled"] = "true";
configuration["scanner:events:driver"] = "redis";
configuration["scanner:events:dsn"] = "localhost:6379";
configuration["scanner:events:stream"] = "stella.events.tests";
configuration["scanner:events:publishTimeoutSeconds"] = "1";
configuration["scanner:events:maxStreamLength"] = "100";
});
using var scope = factory.Services.CreateScope();
var options = scope.ServiceProvider.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
Assert.True(options.Events.Enabled);
Assert.Equal("redis", options.Events.Driver);
var publisher = scope.ServiceProvider.GetRequiredService<IPlatformEventPublisher>();
Assert.IsType<RedisPlatformEventPublisher>(publisher);
}
finally
{
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", originalEnabled);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DRIVER", originalDriver);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DSN", originalDsn);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__STREAM", originalStream);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS", originalTimeout);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__MAXSTREAMLENGTH", originalMax);
}
}
}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class PlatformEventPublisherRegistrationTests
{
[Fact]
public void NullPublisherRegisteredWhenEventsDisabled()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:events:enabled"] = "false";
configuration["scanner:events:dsn"] = string.Empty;
});
using var scope = factory.Services.CreateScope();
var publisher = scope.ServiceProvider.GetRequiredService<IPlatformEventPublisher>();
Assert.IsType<NullPlatformEventPublisher>(publisher);
}
[Fact]
public void RedisPublisherRegisteredWhenEventsEnabled()
{
var originalEnabled = Environment.GetEnvironmentVariable("SCANNER__EVENTS__ENABLED");
var originalDriver = Environment.GetEnvironmentVariable("SCANNER__EVENTS__DRIVER");
var originalDsn = Environment.GetEnvironmentVariable("SCANNER__EVENTS__DSN");
var originalStream = Environment.GetEnvironmentVariable("SCANNER__EVENTS__STREAM");
var originalTimeout = Environment.GetEnvironmentVariable("SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS");
var originalMax = Environment.GetEnvironmentVariable("SCANNER__EVENTS__MAXSTREAMLENGTH");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", "true");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DRIVER", "redis");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DSN", "localhost:6379");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__STREAM", "stella.events.tests");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS", "1");
Environment.SetEnvironmentVariable("SCANNER__EVENTS__MAXSTREAMLENGTH", "100");
try
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:events:enabled"] = "true";
configuration["scanner:events:driver"] = "redis";
configuration["scanner:events:dsn"] = "localhost:6379";
configuration["scanner:events:stream"] = "stella.events.tests";
configuration["scanner:events:publishTimeoutSeconds"] = "1";
configuration["scanner:events:maxStreamLength"] = "100";
});
using var scope = factory.Services.CreateScope();
var options = scope.ServiceProvider.GetRequiredService<IOptions<ScannerWebServiceOptions>>().Value;
Assert.True(options.Events.Enabled);
Assert.Equal("redis", options.Events.Driver);
var publisher = scope.ServiceProvider.GetRequiredService<IPlatformEventPublisher>();
Assert.IsType<RedisPlatformEventPublisher>(publisher);
}
finally
{
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", originalEnabled);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DRIVER", originalDriver);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__DSN", originalDsn);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__STREAM", originalStream);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS", originalTimeout);
Environment.SetEnvironmentVariable("SCANNER__EVENTS__MAXSTREAMLENGTH", originalMax);
}
}
}

View File

@@ -9,18 +9,18 @@ using System.Text.Json.Serialization;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Serialization;
using Xunit.Sdk;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class PlatformEventSamplesTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
[Theory]
namespace StellaOps.Scanner.WebService.Tests;
public sealed class PlatformEventSamplesTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
[Theory]
[InlineData("scanner.event.report.ready@1.sample.json", OrchestratorEventKinds.ScannerReportReady)]
[InlineData("scanner.event.scan.completed@1.sample.json", OrchestratorEventKinds.ScannerScanCompleted)]
public void PlatformEventSamplesStayCanonical(string fileName, string expectedKind)
@@ -174,5 +174,5 @@ public sealed class PlatformEventSamplesTests
var path = Path.Combine(AppContext.BaseDirectory, fileName);
Assert.True(File.Exists(path), $"Sample file not found at '{path}'.");
return File.ReadAllText(path);
}
}
}
}

View File

@@ -1,108 +1,108 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using StellaOps.Policy;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class PolicyEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task PolicySchemaReturnsEmbeddedSchema()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/policy/schema");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/schema+json", response.Content.Headers.ContentType?.MediaType);
var payload = await response.Content.ReadAsStringAsync();
Assert.Contains("\"$schema\"", payload);
Assert.Contains("\"properties\"", payload);
}
[Fact]
public async Task PolicyDiagnosticsReturnsRecommendations()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new PolicyDiagnosticsRequestDto
{
Policy = new PolicyPreviewPolicyDto
{
Content = "version: \"1.0\"\nrules: []\n",
Format = "yaml",
Actor = "tester",
Description = "empty ruleset"
}
};
var response = await client.PostAsJsonAsync("/api/v1/policy/diagnostics", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var diagnostics = await response.Content.ReadFromJsonAsync<PolicyDiagnosticsResponseDto>(SerializerOptions);
Assert.NotNull(diagnostics);
Assert.False(diagnostics!.Success);
Assert.True(diagnostics.ErrorCount >= 0);
Assert.NotEmpty(diagnostics.Recommendations);
}
[Fact]
public async Task PolicyPreviewUsesProposedPolicy()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
const string policyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var request = new PolicyPreviewRequestDto
{
ImageDigest = "sha256:abc123",
Findings = new[]
{
new PolicyPreviewFindingDto
{
Id = "finding-1",
Severity = "Critical",
Source = "NVD",
Tags = new[] { "reachability:runtime" }
}
},
Policy = new PolicyPreviewPolicyDto
{
Content = policyYaml,
Format = "yaml",
Actor = "preview",
Description = "test policy"
}
};
var response = await client.PostAsJsonAsync("/api/v1/policy/preview", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var preview = await response.Content.ReadFromJsonAsync<PolicyPreviewResponseDto>(SerializerOptions);
Assert.NotNull(preview);
Assert.True(preview!.Success);
Assert.Equal(1, preview.Changed);
var diff = Assert.Single(preview.Diffs);
Assert.Equal("finding-1", diff.Projected?.FindingId);
Assert.Equal("Blocked", diff.Projected?.Status);
Assert.Equal(PolicyScoringConfig.Default.Version, diff.Projected?.ConfigVersion);
Assert.NotNull(diff.Projected?.Inputs);
Assert.True(diff.Projected!.Inputs!.ContainsKey("severityWeight"));
Assert.Equal("NVD", diff.Projected.SourceTrust);
Assert.Equal("runtime", diff.Projected.Reachability);
}
}
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using StellaOps.Policy;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class PolicyEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
[Fact]
public async Task PolicySchemaReturnsEmbeddedSchema()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/policy/schema");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("application/schema+json", response.Content.Headers.ContentType?.MediaType);
var payload = await response.Content.ReadAsStringAsync();
Assert.Contains("\"$schema\"", payload);
Assert.Contains("\"properties\"", payload);
}
[Fact]
public async Task PolicyDiagnosticsReturnsRecommendations()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new PolicyDiagnosticsRequestDto
{
Policy = new PolicyPreviewPolicyDto
{
Content = "version: \"1.0\"\nrules: []\n",
Format = "yaml",
Actor = "tester",
Description = "empty ruleset"
}
};
var response = await client.PostAsJsonAsync("/api/v1/policy/diagnostics", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var diagnostics = await response.Content.ReadFromJsonAsync<PolicyDiagnosticsResponseDto>(SerializerOptions);
Assert.NotNull(diagnostics);
Assert.False(diagnostics!.Success);
Assert.True(diagnostics.ErrorCount >= 0);
Assert.NotEmpty(diagnostics.Recommendations);
}
[Fact]
public async Task PolicyPreviewUsesProposedPolicy()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
const string policyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var request = new PolicyPreviewRequestDto
{
ImageDigest = "sha256:abc123",
Findings = new[]
{
new PolicyPreviewFindingDto
{
Id = "finding-1",
Severity = "Critical",
Source = "NVD",
Tags = new[] { "reachability:runtime" }
}
},
Policy = new PolicyPreviewPolicyDto
{
Content = policyYaml,
Format = "yaml",
Actor = "preview",
Description = "test policy"
}
};
var response = await client.PostAsJsonAsync("/api/v1/policy/preview", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var preview = await response.Content.ReadFromJsonAsync<PolicyPreviewResponseDto>(SerializerOptions);
Assert.NotNull(preview);
Assert.True(preview!.Success);
Assert.Equal(1, preview.Changed);
var diff = Assert.Single(preview.Diffs);
Assert.Equal("finding-1", diff.Projected?.FindingId);
Assert.Equal("Blocked", diff.Projected?.Status);
Assert.Equal(PolicyScoringConfig.Default.Version, diff.Projected?.ConfigVersion);
Assert.NotNull(diff.Projected?.Inputs);
Assert.True(diff.Projected!.Inputs!.ContainsKey("severityWeight"));
Assert.Equal("NVD", diff.Projected.SourceTrust);
Assert.Equal("runtime", diff.Projected.Reachability);
}
}

View File

@@ -1,35 +1,35 @@
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ReportSamplesTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
[Fact]
public async Task ReportSampleEnvelope_RemainsCanonical()
{
var baseDirectory = AppContext.BaseDirectory;
var repoRoot = Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", ".."));
var path = Path.Combine(repoRoot, "samples", "api", "reports", "report-sample.dsse.json");
Assert.True(File.Exists(path), $"Sample file not found at {path}.");
await using var stream = File.OpenRead(path);
var response = await JsonSerializer.DeserializeAsync<ReportResponseDto>(stream, SerializerOptions);
Assert.NotNull(response);
Assert.NotNull(response!.Report);
Assert.NotNull(response.Dsse);
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using StellaOps.Scanner.WebService.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ReportSamplesTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
[Fact]
public async Task ReportSampleEnvelope_RemainsCanonical()
{
var baseDirectory = AppContext.BaseDirectory;
var repoRoot = Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", ".."));
var path = Path.Combine(repoRoot, "samples", "api", "reports", "report-sample.dsse.json");
Assert.True(File.Exists(path), $"Sample file not found at {path}.");
await using var stream = File.OpenRead(path);
var response = await JsonSerializer.DeserializeAsync<ReportResponseDto>(stream, SerializerOptions);
Assert.NotNull(response);
Assert.NotNull(response!.Report);
Assert.NotNull(response.Dsse);
var reportBytes = JsonSerializer.SerializeToUtf8Bytes(response.Report, SerializerOptions);
var expectedPayload = Convert.ToBase64String(reportBytes);
Assert.Equal(expectedPayload, response.Dsse!.Payload);
}
}
}

View File

@@ -1,5 +1,5 @@
using System.Net;
using System.Net.Http.Json;
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -12,113 +12,113 @@ using StellaOps.Policy;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Services;
using System.Linq;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ReportsEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
[Fact]
public async Task ReportsEndpointReturnsSignedEnvelope()
{
const string policyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var hmacKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("scanner-report-hmac-key-2025!"));
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:signing:enabled"] = "true";
configuration["scanner:signing:keyId"] = "scanner-report-signing";
configuration["scanner:signing:algorithm"] = "hs256";
configuration["scanner:signing:keyPem"] = hmacKey;
configuration["scanner:features:enableSignedReports"] = "true";
});
var store = factory.Services.GetRequiredService<PolicySnapshotStore>();
await store.SaveAsync(
new PolicySnapshotContent(policyYaml, PolicyDocumentFormat.Yaml, "tester", "seed", "initial"),
CancellationToken.None);
using var client = factory.CreateClient();
var request = new ReportRequestDto
{
ImageDigest = "sha256:deadbeef",
Findings = new[]
{
new PolicyPreviewFindingDto
{
Id = "finding-1",
Severity = "Critical",
Source = "NVD",
Tags = new[] { "reachability:runtime" }
}
}
};
var response = await client.PostAsJsonAsync("/api/v1/reports", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var raw = await response.Content.ReadAsStringAsync();
Assert.False(string.IsNullOrWhiteSpace(raw), raw);
var payload = JsonSerializer.Deserialize<ReportResponseDto>(raw, SerializerOptions);
Assert.NotNull(payload);
Assert.NotNull(payload!.Report);
Assert.NotNull(payload.Dsse);
Assert.StartsWith("report-", payload.Report.ReportId, StringComparison.Ordinal);
Assert.Equal("blocked", payload.Report.Verdict);
var dsse = payload.Dsse!;
Assert.Equal("application/vnd.stellaops.report+json", dsse.PayloadType);
var decodedPayload = Convert.FromBase64String(dsse.Payload);
var canonicalPayload = JsonSerializer.SerializeToUtf8Bytes(payload.Report, SerializerOptions);
var expectedBase64 = Convert.ToBase64String(canonicalPayload);
Assert.Equal(expectedBase64, dsse.Payload);
var reportVerdict = Assert.Single(payload.Report.Verdicts);
Assert.Equal("NVD", reportVerdict.SourceTrust);
Assert.Equal("runtime", reportVerdict.Reachability);
Assert.NotNull(reportVerdict.Inputs);
Assert.True(reportVerdict.Inputs!.ContainsKey("severityWeight"));
Assert.Equal(PolicyScoringConfig.Default.Version, reportVerdict.ConfigVersion);
var signature = Assert.Single(dsse.Signatures);
Assert.Equal("scanner-report-signing", signature.KeyId);
Assert.Equal("hs256", signature.Algorithm, ignoreCase: true);
using var hmac = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(hmacKey));
var expectedSig = Convert.ToBase64String(hmac.ComputeHash(decodedPayload));
var actualSig = signature.Signature;
Assert.True(expectedSig == actualSig, $"expected:{expectedSig}, actual:{actualSig}");
}
[Fact]
public async Task ReportsEndpointValidatesDigest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new ReportRequestDto
{
ImageDigest = "",
Findings = Array.Empty<PolicyPreviewFindingDto>()
};
var response = await client.PostAsJsonAsync("/api/v1/reports", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
namespace StellaOps.Scanner.WebService.Tests;
public sealed class ReportsEndpointsTests
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
[Fact]
public async Task ReportsEndpointReturnsSignedEnvelope()
{
const string policyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var hmacKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("scanner-report-hmac-key-2025!"));
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:signing:enabled"] = "true";
configuration["scanner:signing:keyId"] = "scanner-report-signing";
configuration["scanner:signing:algorithm"] = "hs256";
configuration["scanner:signing:keyPem"] = hmacKey;
configuration["scanner:features:enableSignedReports"] = "true";
});
var store = factory.Services.GetRequiredService<PolicySnapshotStore>();
await store.SaveAsync(
new PolicySnapshotContent(policyYaml, PolicyDocumentFormat.Yaml, "tester", "seed", "initial"),
CancellationToken.None);
using var client = factory.CreateClient();
var request = new ReportRequestDto
{
ImageDigest = "sha256:deadbeef",
Findings = new[]
{
new PolicyPreviewFindingDto
{
Id = "finding-1",
Severity = "Critical",
Source = "NVD",
Tags = new[] { "reachability:runtime" }
}
}
};
var response = await client.PostAsJsonAsync("/api/v1/reports", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var raw = await response.Content.ReadAsStringAsync();
Assert.False(string.IsNullOrWhiteSpace(raw), raw);
var payload = JsonSerializer.Deserialize<ReportResponseDto>(raw, SerializerOptions);
Assert.NotNull(payload);
Assert.NotNull(payload!.Report);
Assert.NotNull(payload.Dsse);
Assert.StartsWith("report-", payload.Report.ReportId, StringComparison.Ordinal);
Assert.Equal("blocked", payload.Report.Verdict);
var dsse = payload.Dsse!;
Assert.Equal("application/vnd.stellaops.report+json", dsse.PayloadType);
var decodedPayload = Convert.FromBase64String(dsse.Payload);
var canonicalPayload = JsonSerializer.SerializeToUtf8Bytes(payload.Report, SerializerOptions);
var expectedBase64 = Convert.ToBase64String(canonicalPayload);
Assert.Equal(expectedBase64, dsse.Payload);
var reportVerdict = Assert.Single(payload.Report.Verdicts);
Assert.Equal("NVD", reportVerdict.SourceTrust);
Assert.Equal("runtime", reportVerdict.Reachability);
Assert.NotNull(reportVerdict.Inputs);
Assert.True(reportVerdict.Inputs!.ContainsKey("severityWeight"));
Assert.Equal(PolicyScoringConfig.Default.Version, reportVerdict.ConfigVersion);
var signature = Assert.Single(dsse.Signatures);
Assert.Equal("scanner-report-signing", signature.KeyId);
Assert.Equal("hs256", signature.Algorithm, ignoreCase: true);
using var hmac = new System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(hmacKey));
var expectedSig = Convert.ToBase64String(hmac.ComputeHash(decodedPayload));
var actualSig = signature.Signature;
Assert.True(expectedSig == actualSig, $"expected:{expectedSig}, actual:{actualSig}");
}
[Fact]
public async Task ReportsEndpointValidatesDigest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new ReportRequestDto
{
ImageDigest = "",
Findings = Array.Empty<PolicyPreviewFindingDto>()
};
var response = await client.PostAsJsonAsync("/api/v1/reports", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task ReportsEndpointReturnsServiceUnavailableWhenPolicyMissing()
{
using var factory = new ScannerApplicationFactory();

View File

@@ -1,357 +1,357 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Policy;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Zastava.Core.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class RuntimeEndpointsTests
{
[Fact]
public async Task RuntimeEventsEndpointPersistsEvents()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new RuntimeEventsIngestRequestDto
{
BatchId = "batch-1",
Events = new[]
{
CreateEnvelope("evt-001", buildId: "ABCDEF1234567890ABCDEF1234567890ABCDEF12"),
CreateEnvelope("evt-002", buildId: "abcdef1234567890abcdef1234567890abcdef12")
}
};
var response = await client.PostAsJsonAsync("/api/v1/runtime/events", request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<RuntimeEventsIngestResponseDto>();
Assert.NotNull(payload);
Assert.Equal(2, payload!.Accepted);
Assert.Equal(0, payload.Duplicates);
using var scope = factory.Services.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
var stored = await repository.ListAsync(CancellationToken.None);
Assert.Equal(2, stored.Count);
Assert.Contains(stored, doc => doc.EventId == "evt-001");
Assert.All(stored, doc =>
{
Assert.Equal("tenant-alpha", doc.Tenant);
Assert.True(doc.ExpiresAt > doc.ReceivedAt);
Assert.Equal("sha256:deadbeef", doc.ImageDigest);
Assert.Equal("abcdef1234567890abcdef1234567890abcdef12", doc.BuildId);
});
}
[Fact]
public async Task RuntimeEventsEndpointRejectsUnsupportedSchema()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var envelope = CreateEnvelope("evt-100", schemaVersion: "zastava.runtime.event@v2.0");
var request = new RuntimeEventsIngestRequestDto
{
Events = new[] { envelope }
};
var response = await client.PostAsJsonAsync("/api/v1/runtime/events", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task RuntimeEventsEndpointEnforcesRateLimit()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:runtime:perNodeBurst"] = "1";
configuration["scanner:runtime:perNodeEventsPerSecond"] = "1";
configuration["scanner:runtime:perTenantBurst"] = "1";
configuration["scanner:runtime:perTenantEventsPerSecond"] = "1";
});
using var client = factory.CreateClient();
var request = new RuntimeEventsIngestRequestDto
{
Events = new[]
{
CreateEnvelope("evt-500"),
CreateEnvelope("evt-501")
}
};
var response = await client.PostAsJsonAsync("/api/v1/runtime/events", request);
Assert.Equal((HttpStatusCode)StatusCodes.Status429TooManyRequests, response.StatusCode);
Assert.NotNull(response.Headers.RetryAfter);
using var scope = factory.Services.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
var count = await repository.CountAsync(CancellationToken.None);
Assert.Equal(0, count);
}
[Fact]
public async Task RuntimePolicyEndpointReturnsDecisions()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:runtime:policyCacheTtlSeconds"] = "600";
});
const string imageDigest = "sha256:deadbeef";
using var client = factory.CreateClient();
using (var scope = factory.Services.CreateScope())
{
var artifacts = scope.ServiceProvider.GetRequiredService<ArtifactRepository>();
var links = scope.ServiceProvider.GetRequiredService<LinkRepository>();
var policyStore = scope.ServiceProvider.GetRequiredService<PolicySnapshotStore>();
var runtimeRepository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
await runtimeRepository.TruncateAsync(CancellationToken.None);
const string policyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var saveResult = await policyStore.SaveAsync(
new PolicySnapshotContent(policyYaml, PolicyDocumentFormat.Yaml, "tester", "tests", "seed"),
CancellationToken.None);
Assert.True(saveResult.Success);
var snapshot = await policyStore.GetLatestAsync(CancellationToken.None);
Assert.NotNull(snapshot);
var sbomArtifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.ImageBom, "sha256:sbomdigest");
var attestationArtifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.Attestation, "sha256:attdigest");
await artifacts.UpsertAsync(new ArtifactDocument
{
Id = sbomArtifactId,
Type = ArtifactDocumentType.ImageBom,
Format = ArtifactDocumentFormat.CycloneDxJson,
MediaType = "application/json",
BytesSha256 = "sha256:sbomdigest",
RefCount = 1
}, CancellationToken.None);
await artifacts.UpsertAsync(new ArtifactDocument
{
Id = attestationArtifactId,
Type = ArtifactDocumentType.Attestation,
Format = ArtifactDocumentFormat.DsseJson,
MediaType = "application/vnd.dsse.envelope+json",
BytesSha256 = "sha256:attdigest",
RefCount = 1,
Rekor = new RekorReference { Uuid = "rekor-uuid", Url = "https://rekor.example/uuid/rekor-uuid", Index = 7 }
}, CancellationToken.None);
await links.UpsertAsync(new LinkDocument
{
Id = Guid.NewGuid().ToString("N"),
FromType = LinkSourceType.Image,
FromDigest = imageDigest,
ArtifactId = sbomArtifactId,
CreatedAtUtc = DateTime.UtcNow
}, CancellationToken.None);
await links.UpsertAsync(new LinkDocument
{
Id = Guid.NewGuid().ToString("N"),
FromType = LinkSourceType.Image,
FromDigest = imageDigest,
ArtifactId = attestationArtifactId,
CreatedAtUtc = DateTime.UtcNow
}, CancellationToken.None);
}
var ingestRequest = new RuntimeEventsIngestRequestDto
{
Events = new[]
{
CreateEnvelope("evt-210", imageDigest: imageDigest, buildId: "1122aabbccddeeff00112233445566778899aabb"),
CreateEnvelope("evt-211", imageDigest: imageDigest, buildId: "1122AABBCCDDEEFF00112233445566778899AABB")
}
};
var ingestResponse = await client.PostAsJsonAsync("/api/v1/runtime/events", ingestRequest);
Assert.Equal(HttpStatusCode.Accepted, ingestResponse.StatusCode);
var request = new RuntimePolicyRequestDto
{
Namespace = "payments",
Images = new[] { imageDigest, imageDigest },
Labels = new Dictionary<string, string> { ["app"] = "api" }
};
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var raw = await response.Content.ReadAsStringAsync();
Assert.False(string.IsNullOrWhiteSpace(raw), "Runtime policy response body was empty.");
var payload = JsonSerializer.Deserialize<RuntimePolicyResponseDto>(raw);
Assert.True(payload is not null, $"Runtime policy response: {raw}");
Assert.Equal(600, payload!.TtlSeconds);
Assert.NotNull(payload.PolicyRevision);
Assert.True(payload.ExpiresAtUtc > DateTimeOffset.UtcNow);
var decision = payload.Results[imageDigest];
Assert.Equal("pass", decision.PolicyVerdict);
Assert.True(decision.Signed);
Assert.True(decision.HasSbomReferrers);
Assert.True(decision.HasSbomLegacy);
Assert.Empty(decision.Reasons);
Assert.NotNull(decision.Rekor);
Assert.Equal("rekor-uuid", decision.Rekor!.Uuid);
Assert.True(decision.Rekor.Verified);
Assert.NotNull(decision.Confidence);
Assert.InRange(decision.Confidence!.Value, 0.0, 1.0);
Assert.False(decision.Quieted.GetValueOrDefault());
Assert.Null(decision.QuietedBy);
Assert.NotNull(decision.BuildIds);
Assert.Contains("1122aabbccddeeff00112233445566778899aabb", decision.BuildIds!);
var metadataString = decision.Metadata;
Console.WriteLine($"Runtime policy metadata: {metadataString ?? "<null>"}");
Assert.False(string.IsNullOrWhiteSpace(metadataString));
using var metadataDocument = JsonDocument.Parse(decision.Metadata!);
Assert.True(metadataDocument.RootElement.TryGetProperty("heuristics", out _));
}
[Fact]
public async Task RuntimePolicyEndpointFlagsUnsignedAndMissingSbom()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
const string imageDigest = "sha256:feedface";
using (var scope = factory.Services.CreateScope())
{
var runtimeRepository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
var policyStore = scope.ServiceProvider.GetRequiredService<PolicySnapshotStore>();
const string policyYaml = """
version: "1.0"
rules: []
""";
await policyStore.SaveAsync(
new PolicySnapshotContent(policyYaml, PolicyDocumentFormat.Yaml, "tester", "tests", "baseline"),
CancellationToken.None);
// Intentionally skip artifacts/links to simulate missing metadata.
await runtimeRepository.TruncateAsync(CancellationToken.None);
}
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", new RuntimePolicyRequestDto
{
Namespace = "payments",
Images = new[] { imageDigest }
});
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<RuntimePolicyResponseDto>();
Assert.NotNull(payload);
var decision = payload!.Results[imageDigest];
Assert.Equal("fail", decision.PolicyVerdict);
Assert.False(decision.Signed);
Assert.False(decision.HasSbomReferrers);
Assert.Contains("image.metadata.missing", decision.Reasons);
Assert.Contains("unsigned", decision.Reasons);
Assert.Contains("missing SBOM", decision.Reasons);
Assert.NotNull(decision.Confidence);
Assert.InRange(decision.Confidence!.Value, 0.0, 1.0);
if (!string.IsNullOrWhiteSpace(decision.Metadata))
{
using var failureMetadata = JsonDocument.Parse(decision.Metadata!);
if (failureMetadata.RootElement.TryGetProperty("heuristics", out var heuristicsElement))
{
var heuristics = heuristicsElement.EnumerateArray().Select(item => item.GetString()).ToArray();
Assert.Contains("image.metadata.missing", heuristics);
Assert.Contains("unsigned", heuristics);
}
}
}
[Fact]
public async Task RuntimePolicyEndpointValidatesRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new RuntimePolicyRequestDto
{
Images = Array.Empty<string>()
};
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private static RuntimeEventEnvelope CreateEnvelope(
string eventId,
string? schemaVersion = null,
string? imageDigest = null,
string? buildId = null)
{
var digest = string.IsNullOrWhiteSpace(imageDigest) ? "sha256:deadbeef" : imageDigest;
var runtimeEvent = new RuntimeEvent
{
EventId = eventId,
When = DateTimeOffset.UtcNow,
Kind = RuntimeEventKind.ContainerStart,
Tenant = "tenant-alpha",
Node = "node-a",
Runtime = new RuntimeEngine
{
Engine = "containerd",
Version = "1.7.0"
},
Workload = new RuntimeWorkload
{
Platform = "kubernetes",
Namespace = "default",
Pod = "api-123",
Container = "api",
ContainerId = "containerd://abc",
ImageRef = $"ghcr.io/example/api@{digest}"
},
Delta = new RuntimeDelta
{
BaselineImageDigest = digest
},
Process = new RuntimeProcess
{
Pid = 123,
Entrypoint = new[] { "/bin/start" },
EntryTrace = Array.Empty<RuntimeEntryTrace>(),
BuildId = buildId
}
};
if (schemaVersion is null)
{
return RuntimeEventEnvelope.Create(runtimeEvent, ZastavaContractVersions.RuntimeEvent);
}
return new RuntimeEventEnvelope
{
SchemaVersion = schemaVersion,
Event = runtimeEvent
};
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Policy;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Zastava.Core.Contracts;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class RuntimeEndpointsTests
{
[Fact]
public async Task RuntimeEventsEndpointPersistsEvents()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new RuntimeEventsIngestRequestDto
{
BatchId = "batch-1",
Events = new[]
{
CreateEnvelope("evt-001", buildId: "ABCDEF1234567890ABCDEF1234567890ABCDEF12"),
CreateEnvelope("evt-002", buildId: "abcdef1234567890abcdef1234567890abcdef12")
}
};
var response = await client.PostAsJsonAsync("/api/v1/runtime/events", request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<RuntimeEventsIngestResponseDto>();
Assert.NotNull(payload);
Assert.Equal(2, payload!.Accepted);
Assert.Equal(0, payload.Duplicates);
using var scope = factory.Services.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
var stored = await repository.ListAsync(CancellationToken.None);
Assert.Equal(2, stored.Count);
Assert.Contains(stored, doc => doc.EventId == "evt-001");
Assert.All(stored, doc =>
{
Assert.Equal("tenant-alpha", doc.Tenant);
Assert.True(doc.ExpiresAt > doc.ReceivedAt);
Assert.Equal("sha256:deadbeef", doc.ImageDigest);
Assert.Equal("abcdef1234567890abcdef1234567890abcdef12", doc.BuildId);
});
}
[Fact]
public async Task RuntimeEventsEndpointRejectsUnsupportedSchema()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var envelope = CreateEnvelope("evt-100", schemaVersion: "zastava.runtime.event@v2.0");
var request = new RuntimeEventsIngestRequestDto
{
Events = new[] { envelope }
};
var response = await client.PostAsJsonAsync("/api/v1/runtime/events", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task RuntimeEventsEndpointEnforcesRateLimit()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:runtime:perNodeBurst"] = "1";
configuration["scanner:runtime:perNodeEventsPerSecond"] = "1";
configuration["scanner:runtime:perTenantBurst"] = "1";
configuration["scanner:runtime:perTenantEventsPerSecond"] = "1";
});
using var client = factory.CreateClient();
var request = new RuntimeEventsIngestRequestDto
{
Events = new[]
{
CreateEnvelope("evt-500"),
CreateEnvelope("evt-501")
}
};
var response = await client.PostAsJsonAsync("/api/v1/runtime/events", request);
Assert.Equal((HttpStatusCode)StatusCodes.Status429TooManyRequests, response.StatusCode);
Assert.NotNull(response.Headers.RetryAfter);
using var scope = factory.Services.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
var count = await repository.CountAsync(CancellationToken.None);
Assert.Equal(0, count);
}
[Fact]
public async Task RuntimePolicyEndpointReturnsDecisions()
{
using var factory = new ScannerApplicationFactory(configuration =>
{
configuration["scanner:runtime:policyCacheTtlSeconds"] = "600";
});
const string imageDigest = "sha256:deadbeef";
using var client = factory.CreateClient();
using (var scope = factory.Services.CreateScope())
{
var artifacts = scope.ServiceProvider.GetRequiredService<ArtifactRepository>();
var links = scope.ServiceProvider.GetRequiredService<LinkRepository>();
var policyStore = scope.ServiceProvider.GetRequiredService<PolicySnapshotStore>();
var runtimeRepository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
await runtimeRepository.TruncateAsync(CancellationToken.None);
const string policyYaml = """
version: "1.0"
rules:
- name: Block Critical
severity: [Critical]
action: block
""";
var saveResult = await policyStore.SaveAsync(
new PolicySnapshotContent(policyYaml, PolicyDocumentFormat.Yaml, "tester", "tests", "seed"),
CancellationToken.None);
Assert.True(saveResult.Success);
var snapshot = await policyStore.GetLatestAsync(CancellationToken.None);
Assert.NotNull(snapshot);
var sbomArtifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.ImageBom, "sha256:sbomdigest");
var attestationArtifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.Attestation, "sha256:attdigest");
await artifacts.UpsertAsync(new ArtifactDocument
{
Id = sbomArtifactId,
Type = ArtifactDocumentType.ImageBom,
Format = ArtifactDocumentFormat.CycloneDxJson,
MediaType = "application/json",
BytesSha256 = "sha256:sbomdigest",
RefCount = 1
}, CancellationToken.None);
await artifacts.UpsertAsync(new ArtifactDocument
{
Id = attestationArtifactId,
Type = ArtifactDocumentType.Attestation,
Format = ArtifactDocumentFormat.DsseJson,
MediaType = "application/vnd.dsse.envelope+json",
BytesSha256 = "sha256:attdigest",
RefCount = 1,
Rekor = new RekorReference { Uuid = "rekor-uuid", Url = "https://rekor.example/uuid/rekor-uuid", Index = 7 }
}, CancellationToken.None);
await links.UpsertAsync(new LinkDocument
{
Id = Guid.NewGuid().ToString("N"),
FromType = LinkSourceType.Image,
FromDigest = imageDigest,
ArtifactId = sbomArtifactId,
CreatedAtUtc = DateTime.UtcNow
}, CancellationToken.None);
await links.UpsertAsync(new LinkDocument
{
Id = Guid.NewGuid().ToString("N"),
FromType = LinkSourceType.Image,
FromDigest = imageDigest,
ArtifactId = attestationArtifactId,
CreatedAtUtc = DateTime.UtcNow
}, CancellationToken.None);
}
var ingestRequest = new RuntimeEventsIngestRequestDto
{
Events = new[]
{
CreateEnvelope("evt-210", imageDigest: imageDigest, buildId: "1122aabbccddeeff00112233445566778899aabb"),
CreateEnvelope("evt-211", imageDigest: imageDigest, buildId: "1122AABBCCDDEEFF00112233445566778899AABB")
}
};
var ingestResponse = await client.PostAsJsonAsync("/api/v1/runtime/events", ingestRequest);
Assert.Equal(HttpStatusCode.Accepted, ingestResponse.StatusCode);
var request = new RuntimePolicyRequestDto
{
Namespace = "payments",
Images = new[] { imageDigest, imageDigest },
Labels = new Dictionary<string, string> { ["app"] = "api" }
};
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var raw = await response.Content.ReadAsStringAsync();
Assert.False(string.IsNullOrWhiteSpace(raw), "Runtime policy response body was empty.");
var payload = JsonSerializer.Deserialize<RuntimePolicyResponseDto>(raw);
Assert.True(payload is not null, $"Runtime policy response: {raw}");
Assert.Equal(600, payload!.TtlSeconds);
Assert.NotNull(payload.PolicyRevision);
Assert.True(payload.ExpiresAtUtc > DateTimeOffset.UtcNow);
var decision = payload.Results[imageDigest];
Assert.Equal("pass", decision.PolicyVerdict);
Assert.True(decision.Signed);
Assert.True(decision.HasSbomReferrers);
Assert.True(decision.HasSbomLegacy);
Assert.Empty(decision.Reasons);
Assert.NotNull(decision.Rekor);
Assert.Equal("rekor-uuid", decision.Rekor!.Uuid);
Assert.True(decision.Rekor.Verified);
Assert.NotNull(decision.Confidence);
Assert.InRange(decision.Confidence!.Value, 0.0, 1.0);
Assert.False(decision.Quieted.GetValueOrDefault());
Assert.Null(decision.QuietedBy);
Assert.NotNull(decision.BuildIds);
Assert.Contains("1122aabbccddeeff00112233445566778899aabb", decision.BuildIds!);
var metadataString = decision.Metadata;
Console.WriteLine($"Runtime policy metadata: {metadataString ?? "<null>"}");
Assert.False(string.IsNullOrWhiteSpace(metadataString));
using var metadataDocument = JsonDocument.Parse(decision.Metadata!);
Assert.True(metadataDocument.RootElement.TryGetProperty("heuristics", out _));
}
[Fact]
public async Task RuntimePolicyEndpointFlagsUnsignedAndMissingSbom()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
const string imageDigest = "sha256:feedface";
using (var scope = factory.Services.CreateScope())
{
var runtimeRepository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
var policyStore = scope.ServiceProvider.GetRequiredService<PolicySnapshotStore>();
const string policyYaml = """
version: "1.0"
rules: []
""";
await policyStore.SaveAsync(
new PolicySnapshotContent(policyYaml, PolicyDocumentFormat.Yaml, "tester", "tests", "baseline"),
CancellationToken.None);
// Intentionally skip artifacts/links to simulate missing metadata.
await runtimeRepository.TruncateAsync(CancellationToken.None);
}
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", new RuntimePolicyRequestDto
{
Namespace = "payments",
Images = new[] { imageDigest }
});
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<RuntimePolicyResponseDto>();
Assert.NotNull(payload);
var decision = payload!.Results[imageDigest];
Assert.Equal("fail", decision.PolicyVerdict);
Assert.False(decision.Signed);
Assert.False(decision.HasSbomReferrers);
Assert.Contains("image.metadata.missing", decision.Reasons);
Assert.Contains("unsigned", decision.Reasons);
Assert.Contains("missing SBOM", decision.Reasons);
Assert.NotNull(decision.Confidence);
Assert.InRange(decision.Confidence!.Value, 0.0, 1.0);
if (!string.IsNullOrWhiteSpace(decision.Metadata))
{
using var failureMetadata = JsonDocument.Parse(decision.Metadata!);
if (failureMetadata.RootElement.TryGetProperty("heuristics", out var heuristicsElement))
{
var heuristics = heuristicsElement.EnumerateArray().Select(item => item.GetString()).ToArray();
Assert.Contains("image.metadata.missing", heuristics);
Assert.Contains("unsigned", heuristics);
}
}
}
[Fact]
public async Task RuntimePolicyEndpointValidatesRequest()
{
using var factory = new ScannerApplicationFactory();
using var client = factory.CreateClient();
var request = new RuntimePolicyRequestDto
{
Images = Array.Empty<string>()
};
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private static RuntimeEventEnvelope CreateEnvelope(
string eventId,
string? schemaVersion = null,
string? imageDigest = null,
string? buildId = null)
{
var digest = string.IsNullOrWhiteSpace(imageDigest) ? "sha256:deadbeef" : imageDigest;
var runtimeEvent = new RuntimeEvent
{
EventId = eventId,
When = DateTimeOffset.UtcNow,
Kind = RuntimeEventKind.ContainerStart,
Tenant = "tenant-alpha",
Node = "node-a",
Runtime = new RuntimeEngine
{
Engine = "containerd",
Version = "1.7.0"
},
Workload = new RuntimeWorkload
{
Platform = "kubernetes",
Namespace = "default",
Pod = "api-123",
Container = "api",
ContainerId = "containerd://abc",
ImageRef = $"ghcr.io/example/api@{digest}"
},
Delta = new RuntimeDelta
{
BaselineImageDigest = digest
},
Process = new RuntimeProcess
{
Pid = 123,
Entrypoint = new[] { "/bin/start" },
EntryTrace = Array.Empty<RuntimeEntryTrace>(),
BuildId = buildId
}
};
if (schemaVersion is null)
{
return RuntimeEventEnvelope.Create(runtimeEvent, ZastavaContractVersions.RuntimeEvent);
}
return new RuntimeEventEnvelope
{
SchemaVersion = schemaVersion,
Event = runtimeEvent
};
}
}