feat: Add Scanner CI runner and related artifacts
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Airgap Sealed CI Smoke / sealed-smoke (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled

- Implemented `run-scanner-ci.sh` to build and run tests for the Scanner solution with a warmed NuGet cache.
- Created `excititor-vex-traces.json` dashboard for monitoring Excititor VEX observations.
- Added Docker Compose configuration for the OTLP span sink in `docker-compose.spansink.yml`.
- Configured OpenTelemetry collector in `otel-spansink.yaml` to receive and process traces.
- Developed `run-spansink.sh` script to run the OTLP span sink for Excititor traces.
- Introduced `FileSystemRiskBundleObjectStore` for storing risk bundle artifacts in the filesystem.
- Built `RiskBundleBuilder` for creating risk bundles with associated metadata and providers.
- Established `RiskBundleJob` to execute the risk bundle creation and storage process.
- Defined models for risk bundle inputs, entries, and manifests in `RiskBundleModels.cs`.
- Implemented signing functionality for risk bundle manifests with `HmacRiskBundleManifestSigner`.
- Created unit tests for `RiskBundleBuilder`, `RiskBundleJob`, and signing functionality to ensure correctness.
- Added filesystem artifact reader tests to validate manifest parsing and artifact listing.
- Included test manifests for egress scenarios in the task runner tests.
- Developed timeline query service tests to verify tenant and event ID handling.
This commit is contained in:
StellaOps Bot
2025-11-30 19:12:35 +02:00
parent 17d45a6d30
commit 71e9a56cfd
92 changed files with 2596 additions and 387 deletions

View File

@@ -0,0 +1,70 @@
using System.IO.Compression;
using StellaOps.ExportCenter.RiskBundles;
namespace StellaOps.ExportCenter.Tests;
public sealed class RiskBundleBuilderTests
{
[Fact]
public void Build_WritesManifestAndFiles_Deterministically()
{
using var temp = new TempDir();
var kev = temp.WriteFile("kev.json", "{\"cve\":\"CVE-0001\"}");
var epss = temp.WriteFile("epss.csv", "cve,score\nCVE-0001,0.12\n");
var request = new RiskBundleBuildRequest(
BundleId: Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
Providers: new[]
{
new RiskBundleProviderInput("cisa-kev", kev, "CISA KEV"),
new RiskBundleProviderInput("first-epss", epss, "FIRST EPSS")
});
var builder = new RiskBundleBuilder();
var cancellation = TestContext.Current.CancellationToken;
var result = builder.Build(request, cancellation);
Assert.Equal(2, result.Manifest.Providers.Count);
Assert.Equal(new[] { "cisa-kev", "first-epss" }, result.Manifest.Providers.Select(p => p.ProviderId));
// Manifest hash stable
var second = builder.Build(request, cancellation);
Assert.Equal(result.RootHash, second.RootHash);
// Bundle contains manifest and provider files
using var gzip = new GZipStream(new MemoryStream(result.BundleStream.ToArray()), CompressionMode.Decompress, leaveOpen: false);
using var tar = new System.Formats.Tar.TarReader(gzip, leaveOpen: false);
var entries = new List<string>();
System.Formats.Tar.TarEntry? entry;
while ((entry = tar.GetNextEntry()) is not null)
{
entries.Add(entry.Name);
}
Assert.Contains("manifests/provider-manifest.json", entries);
Assert.Contains("providers/cisa-kev/snapshot", entries);
Assert.Contains("providers/first-epss/snapshot", entries);
}
private sealed class TempDir : IDisposable
{
public string Path { get; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "riskbundle-tests-" + Guid.NewGuid().ToString("N"));
public TempDir()
{
Directory.CreateDirectory(Path);
}
public string WriteFile(string name, string contents)
{
var full = System.IO.Path.Combine(Path, name);
File.WriteAllText(full, contents);
return full;
}
public void Dispose()
{
try { Directory.Delete(Path, recursive: true); } catch { /* ignore */ }
}
}
}

View File

@@ -0,0 +1,67 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.ExportCenter.RiskBundles;
namespace StellaOps.ExportCenter.Tests;
public sealed class RiskBundleJobTests
{
[Fact]
public async Task ExecuteAsync_StoresManifestAndBundle()
{
using var temp = new TempDir();
var providerPath = temp.WriteFile("kev.json", "{}\n");
var buildRequest = new RiskBundleBuildRequest(
Guid.NewGuid(),
Providers: new[] { new RiskBundleProviderInput("cisa-kev", providerPath, "CISA KEV") });
var job = new RiskBundleJob(
new RiskBundleBuilder(),
new HmacRiskBundleManifestSigner("secret", "risk-key"),
new InMemoryObjectStore(),
NullLogger<RiskBundleJob>.Instance);
var outcome = await job.ExecuteAsync(new RiskBundleJobRequest(buildRequest), TestContext.Current.CancellationToken);
Assert.Equal("risk-bundles/provider-manifest.json", outcome.ManifestStorage.StorageKey);
Assert.Equal("risk-bundles/provider-manifest.dsse", outcome.ManifestSignatureStorage.StorageKey);
Assert.Equal("risk-bundles/risk-bundle.tar.gz", outcome.BundleStorage.StorageKey);
Assert.False(string.IsNullOrWhiteSpace(outcome.ManifestJson));
Assert.False(string.IsNullOrWhiteSpace(outcome.ManifestSignatureJson));
}
private sealed class InMemoryObjectStore : IRiskBundleObjectStore
{
private readonly Dictionary<string, byte[]> _store = new(StringComparer.Ordinal);
public Task<RiskBundleStorageMetadata> StoreAsync(RiskBundleObjectStoreOptions options, Stream content, CancellationToken cancellationToken = default)
{
using var ms = new MemoryStream();
content.CopyTo(ms);
_store[options.StorageKey] = ms.ToArray();
return Task.FromResult(new RiskBundleStorageMetadata(options.StorageKey, ms.Length, options.ContentType));
}
}
private sealed class TempDir : IDisposable
{
public string Path { get; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "riskbundle-job-" + Guid.NewGuid().ToString("N"));
public TempDir()
{
Directory.CreateDirectory(Path);
}
public string WriteFile(string name, string contents)
{
var full = System.IO.Path.Combine(Path, name);
File.WriteAllText(full, contents);
return full;
}
public void Dispose()
{
try { Directory.Delete(Path, recursive: true); } catch { /* ignore */ }
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Text.Json;
using StellaOps.ExportCenter.RiskBundles;
namespace StellaOps.ExportCenter.Tests;
public class RiskBundleSignerTests
{
[Fact]
public async Task SignAsync_ProducesDsseEnvelope()
{
var signer = new HmacRiskBundleManifestSigner("secret-key", "test-key");
const string manifest = "{\"foo\":1}";
var doc = await signer.SignAsync(manifest, TestContext.Current.CancellationToken);
Assert.Equal("application/stellaops.risk-bundle.provider-manifest+json", doc.PayloadType);
Assert.NotNull(doc.Payload);
Assert.Single(doc.Signatures);
Assert.Equal("test-key", doc.Signatures[0].KeyId);
// ensure payload decodes to original manifest
var payloadBytes = Convert.FromBase64String(doc.Payload);
var decoded = System.Text.Encoding.UTF8.GetString(payloadBytes);
Assert.Equal(manifest, decoded);
}
}

View File

@@ -53,7 +53,7 @@
<ItemGroup>
<ItemGroup>
@@ -116,12 +116,13 @@
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj"/>
<ProjectReference Include="..\StellaOps.ExportCenter.Infrastructure\StellaOps.ExportCenter.Infrastructure.csproj"/>
<ProjectReference Include="..\StellaOps.ExportCenter.Core\StellaOps.ExportCenter.Core.csproj"/>
<ProjectReference Include="..\StellaOps.ExportCenter.Infrastructure\StellaOps.ExportCenter.Infrastructure.csproj"/>
<ProjectReference Include="..\..\StellaOps.ExportCenter.RiskBundles\StellaOps.ExportCenter.RiskBundles.csproj" />