feat: Add VEX Lens CI and Load Testing Plan
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

- Introduced a comprehensive CI job structure for VEX Lens, including build, test, linting, and load testing.
- Defined load test parameters and SLOs for VEX Lens API and Issuer Directory.
- Created Grafana dashboards and alerting mechanisms for monitoring API performance and error rates.
- Established offline posture guidelines for CI jobs and load testing.

feat: Implement deterministic projection verification script

- Added `verify_projection.sh` script for verifying the integrity of projection exports against expected hashes.
- Ensured robust error handling for missing files and hash mismatches.

feat: Develop Vuln Explorer CI and Ops Plan

- Created CI jobs for Vuln Explorer, including build, test, and replay verification.
- Implemented backup and disaster recovery strategies for MongoDB and Redis.
- Established Merkle anchoring verification and automation for ledger projector.

feat: Introduce EventEnvelopeHasher for hashing event envelopes

- Implemented `EventEnvelopeHasher` to compute SHA256 hashes for event envelopes.

feat: Add Risk Store and Dashboard components

- Developed `RiskStore` for managing risk data and state.
- Created `RiskDashboardComponent` for displaying risk profiles with filtering capabilities.
- Implemented unit tests for `RiskStore` and `RiskDashboardComponent`.

feat: Enhance Vulnerability Detail Component

- Developed `VulnerabilityDetailComponent` for displaying detailed information about vulnerabilities.
- Implemented error handling for missing vulnerability IDs and loading failures.
This commit is contained in:
StellaOps Bot
2025-12-02 07:18:28 +02:00
parent 44171930ff
commit 885ce86af4
83 changed files with 2090 additions and 97 deletions

View File

@@ -0,0 +1,10 @@
{
"solution": {
"path": "StellaOps.Scanner.sln",
"projects": [
"__Tests/StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests/StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests.csproj",
"__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj",
"__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj"
]
}
}

View File

@@ -0,0 +1,2 @@
<Solution>
</Solution>

View File

@@ -0,0 +1,57 @@
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Worker.Processing.Surface;
internal sealed record DsseEnvelope(string MediaType, string Uri, string Digest, ReadOnlyMemory<byte> Content);
internal interface IDsseEnvelopeSigner
{
Task<DsseEnvelope> SignAsync(string payloadType, ReadOnlyMemory<byte> content, string suggestedKind, string merkleRoot, string? view, CancellationToken cancellationToken);
}
/// <summary>
/// Deterministic fallback signer that encodes sha256 hash as the signature. Replace with real Attestor/Signer when available.
/// </summary>
internal sealed class DeterministicDsseEnvelopeSigner : IDsseEnvelopeSigner
{
public Task<DsseEnvelope> SignAsync(string payloadType, ReadOnlyMemory<byte> content, string suggestedKind, string merkleRoot, string? view, CancellationToken cancellationToken)
{
var signature = ComputeSha256Hex(content.Span);
var envelope = new
{
payloadType,
payload = Base64UrlEncode(content.Span),
signatures = new[]
{
new { keyid = "scanner-deterministic", sig = Base64UrlEncode(Encoding.UTF8.GetBytes(signature)) }
}
};
var json = JsonSerializer.Serialize(envelope, new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = false
});
var bytes = Encoding.UTF8.GetBytes(json);
var digest = $"sha256:{signature}";
var uri = $"cas://attestations/{suggestedKind}/{signature}.json";
return Task.FromResult(new DsseEnvelope("application/vnd.dsse+json", uri, digest, bytes));
}
private static string ComputeSha256Hex(ReadOnlySpan<byte> data)
{
Span<byte> hash = stackalloc byte[32];
System.Security.Cryptography.SHA256.HashData(data, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string Base64UrlEncode(ReadOnlySpan<byte> data)
{
var base64 = Convert.ToBase64String(data);
return base64.Replace("+", "-").Replace("/", "_").TrimEnd('=');
}
}

View File

@@ -117,7 +117,7 @@ internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
WorkerInstance = request.WorkerInstance,
Attempt = request.Attempt
},
Artifacts = artifacts.ToImmutableArray(),
Artifacts = AttachAttestations(artifacts).ToImmutableArray(),
DeterminismMerkleRoot = request.DeterminismMerkleRoot,
ReplayBundle = string.IsNullOrWhiteSpace(request.ReplayBundleUri)
? null
@@ -196,6 +196,61 @@ internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
DeterminismMerkleRoot: request.DeterminismMerkleRoot);
}
private static IReadOnlyList<SurfaceManifestArtifact> AttachAttestations(IReadOnlyList<SurfaceManifestArtifact> artifacts)
{
if (artifacts.Count == 0)
{
return artifacts;
}
var dsseArtifacts = artifacts.Where(a => a.Kind.EndsWith(".dsse", StringComparison.Ordinal)).ToList();
if (dsseArtifacts.Count == 0)
{
return artifacts;
}
var updated = artifacts.ToList();
foreach (var dsse in dsseArtifacts)
{
var targetKind = dsse.Kind switch
{
"composition.recipe.dsse" => "composition.recipe",
"layer.fragments.dsse" => "layer.fragments",
_ => null
};
if (targetKind is null)
{
continue;
}
var targetIndex = updated.FindIndex(a => string.Equals(a.Kind, targetKind, StringComparison.Ordinal));
if (targetIndex < 0)
{
continue;
}
var attestation = new SurfaceManifestAttestation
{
Kind = "dsse",
MediaType = dsse.MediaType,
Digest = dsse.Digest,
Uri = dsse.Uri
};
var existing = updated[targetIndex].Attestations ?? Array.Empty<SurfaceManifestAttestation>();
var attList = existing.Concat(new[] { attestation })
.OrderBy(a => a.Kind, StringComparer.Ordinal)
.ThenBy(a => a.Uri, StringComparer.Ordinal)
.ToList();
updated[targetIndex] = updated[targetIndex] with { Attestations = attList };
}
return updated;
}
private async Task<SurfaceManifestArtifact> StorePayloadAsync(SurfaceManifestPayload payload, string tenant, CancellationToken cancellationToken)
{
var digest = ComputeDigest(payload.Content.Span);

View File

@@ -44,6 +44,7 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
private readonly ICryptoHash _hash;
private readonly IRubyPackageInventoryStore _rubyPackageStore;
private readonly Determinism.DeterminismContext _determinism;
private readonly IDsseEnvelopeSigner _dsseSigner;
private readonly string _componentVersion;
public SurfaceManifestStageExecutor(
@@ -55,7 +56,8 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
ILogger<SurfaceManifestStageExecutor> logger,
ICryptoHash hash,
IRubyPackageInventoryStore rubyPackageStore,
Determinism.DeterminismContext determinism)
Determinism.DeterminismContext determinism,
IDsseEnvelopeSigner dsseSigner)
{
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
_manifestWriter = manifestWriter ?? throw new ArgumentNullException(nameof(manifestWriter));
@@ -66,6 +68,7 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
_hash = hash ?? throw new ArgumentNullException(nameof(hash));
_rubyPackageStore = rubyPackageStore ?? throw new ArgumentNullException(nameof(rubyPackageStore));
_determinism = determinism ?? throw new ArgumentNullException(nameof(determinism));
_dsseSigner = dsseSigner ?? throw new ArgumentNullException(nameof(dsseSigner));
_componentVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
}
@@ -78,10 +81,10 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
var payloads = CollectPayloads(context);
await PersistRubyPackagesAsync(context, cancellationToken).ConfigureAwait(false);
var determinismPayload = BuildDeterminismPayload(context, payloads, out var merkleRoot);
if (determinismPayload is not null)
var determinismPayloads = BuildDeterminismPayloads(context, payloads, out var merkleRoot);
if (determinismPayloads is not null && determinismPayloads.Count > 0)
{
payloads.Add(determinismPayload);
payloads.AddRange(determinismPayloads);
}
if (payloads.Count == 0)
{
@@ -251,7 +254,7 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
return payloads;
}
private SurfaceManifestPayload? BuildDeterminismPayload(ScanJobContext context, IEnumerable<SurfaceManifestPayload> payloads, out string? merkleRoot)
private IReadOnlyList<SurfaceManifestPayload> BuildDeterminismPayloads(ScanJobContext context, IEnumerable<SurfaceManifestPayload> payloads, out string? merkleRoot)
{
merkleRoot = null;
var pins = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -283,9 +286,10 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
var evidence = new Determinism.DeterminismEvidence(artifactHashes, recipeSha256);
context.Analysis.Set(ScanAnalysisKeys.DeterminismEvidence, evidence);
var payloadList = payloads.ToList();
// Publish composition recipe as a manifest artifact for offline replay.
payloads = payloads.ToList();
((List<SurfaceManifestPayload>)payloads).Add(new SurfaceManifestPayload(
payloadList.Add(new SurfaceManifestPayload(
ArtifactDocumentType.CompositionRecipe,
ArtifactDocumentFormat.CompositionRecipeJson,
Kind: "composition.recipe",
@@ -297,14 +301,61 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
["merkleRoot"] = recipeSha256,
}));
// Attach DSSE envelope for the recipe (deterministic local signature = sha256 hash bytes).
var recipeDsse = _dsseSigner.SignAsync(
payloadType: "application/vnd.stellaops.composition.recipe+json",
content: recipeBytes,
suggestedKind: "composition.recipe.dsse",
merkleRoot: recipeSha256,
view: null,
cancellationToken: CancellationToken.None).Result;
payloadList.Add(new SurfaceManifestPayload(
ArtifactDocumentType.Attestation,
ArtifactDocumentFormat.DsseJson,
Kind: "composition.recipe.dsse",
MediaType: recipeDsse.MediaType,
Content: recipeDsse.Content,
Metadata: new Dictionary<string, string>
{
["merkleRoot"] = recipeSha256,
["payloadType"] = "application/vnd.dsse+json"
}));
// Attach DSSE envelope for layer fragments when present.
foreach (var fragmentPayload in payloadList.Where(p => p.Kind == "layer.fragments"))
{
var dsse = _dsseSigner.SignAsync(
payloadType: fragmentPayload.MediaType,
content: fragmentPayload.Content,
suggestedKind: "layer.fragments.dsse",
merkleRoot: recipeSha256,
view: fragmentPayload.View,
cancellationToken: CancellationToken.None).Result;
payloadList.Add(new SurfaceManifestPayload(
ArtifactDocumentType.Attestation,
ArtifactDocumentFormat.DsseJson,
Kind: "layer.fragments.dsse",
MediaType: dsse.MediaType,
Content: dsse.Content,
View: fragmentPayload.View,
Metadata: new Dictionary<string, string>
{
["merkleRoot"] = recipeSha256,
["payloadType"] = fragmentPayload.MediaType
}));
}
var json = JsonSerializer.Serialize(report, JsonOptions);
return new SurfaceManifestPayload(
payloadList.Add(new SurfaceManifestPayload(
ArtifactDocumentType.SurfaceObservation,
ArtifactDocumentFormat.ObservationJson,
Kind: "determinism.json",
MediaType: "application/json",
Content: Encoding.UTF8.GetBytes(json),
View: "replay");
View: "replay"));
return payloadList.Skip(payloads.Count()).ToList();
}
private static (Dictionary<string, string> Hashes, byte[] RecipeBytes, string RecipeSha256) BuildCompositionRecipe(IEnumerable<SurfaceManifestPayload> payloads)
@@ -332,6 +383,48 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
return (new Dictionary<string, string>(map, StringComparer.OrdinalIgnoreCase), recipeBytes, merkleRoot);
}
private SurfaceManifestPayload BuildDsseEnvelopePayload(
string payloadType,
ReadOnlyMemory<byte> content,
string kind,
string mediaType,
string merkleRoot)
{
var signature = ComputeDigest(content.Span).Replace("sha256:", string.Empty, StringComparison.OrdinalIgnoreCase);
var envelope = new
{
payloadType,
payload = Base64UrlEncode(content.Span),
signatures = new[]
{
new
{
keyid = "scanner-offline",
sig = Base64UrlEncode(Encoding.UTF8.GetBytes(signature))
}
}
};
var json = JsonSerializer.Serialize(envelope, JsonOptions);
return new SurfaceManifestPayload(
ArtifactDocumentType.Attestation,
ArtifactDocumentFormat.DsseJson,
Kind: kind,
MediaType: mediaType,
Content: Encoding.UTF8.GetBytes(json),
Metadata: new Dictionary<string, string>
{
["merkleRoot"] = merkleRoot,
["payloadType"] = payloadType
});
}
private static string Base64UrlEncode(ReadOnlySpan<byte> data)
{
var base64 = Convert.ToBase64String(data);
return base64.Replace("+", "-").Replace("/", "_").TrimEnd('=');
}
private static string? GetReplayBundleUri(ScanJobContext context)
=> context.Lease.Metadata.TryGetValue("replay.bundle.uri", out var value) && !string.IsNullOrWhiteSpace(value)
? value.Trim()

View File

@@ -99,8 +99,9 @@ if (!string.IsNullOrWhiteSpace(connectionString))
{
builder.Services.AddScannerStorage(storageSection);
builder.Services.AddSingleton<IConfigureOptions<ScannerStorageOptions>, ScannerStorageSurfaceSecretConfigurator>();
builder.Services.AddSingleton<ISurfaceManifestPublisher, SurfaceManifestPublisher>();
builder.Services.AddSingleton<IScanStageExecutor, SurfaceManifestStageExecutor>();
builder.Services.AddSingleton<ISurfaceManifestPublisher, SurfaceManifestPublisher>();
builder.Services.AddSingleton<IScanStageExecutor, SurfaceManifestStageExecutor>();
builder.Services.AddSingleton<IDsseEnvelopeSigner, DeterministicDsseEnvelopeSigner>();
}
else
{

View File

@@ -157,6 +157,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Reachabil
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Replay.Core", "..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj", "{F812FD49-2D45-4503-A367-ABA55153D9B3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests\StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests.csproj", "{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1055,6 +1057,18 @@ Global
{F812FD49-2D45-4503-A367-ABA55153D9B3}.Release|x64.Build.0 = Release|Any CPU
{F812FD49-2D45-4503-A367-ABA55153D9B3}.Release|x86.ActiveCfg = Release|Any CPU
{F812FD49-2D45-4503-A367-ABA55153D9B3}.Release|x86.Build.0 = Release|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Debug|x64.ActiveCfg = Debug|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Debug|x64.Build.0 = Debug|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Debug|x86.ActiveCfg = Debug|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Debug|x86.Build.0 = Debug|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Release|Any CPU.Build.0 = Release|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Release|x64.ActiveCfg = Release|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Release|x64.Build.0 = Release|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Release|x86.ActiveCfg = Release|Any CPU
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1106,5 +1120,6 @@ Global
{F4A239E0-AC66-4105-8423-4805B2029ABE} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{01F66FFA-8399-480E-A463-BB2B456C8814} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
{D31CFFE3-72B3-48D7-A284-710B14380062} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
{C8EE1699-99B6-4D64-B0DB-9E876C6E9EE4} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
EndGlobalSection
EndGlobal

View File

@@ -133,6 +133,26 @@ public sealed record SurfaceManifestArtifact
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
= null;
[JsonPropertyName("attestations")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<SurfaceManifestAttestation>? Attestations { get; init; }
= null;
}
public sealed record SurfaceManifestAttestation
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = string.Empty;
[JsonPropertyName("mediaType")]
public string MediaType { get; init; } = string.Empty;
[JsonPropertyName("digest")]
public string Digest { get; init; } = string.Empty;
[JsonPropertyName("uri")]
public string Uri { get; init; } = string.Empty;
}
/// <summary>

View File

@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
<!-- Keep graph tight: only Lang.Node tests + core contracts. Reuse compiled binaries to avoid dragging full solution build. -->
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
</ItemGroup>

View File

@@ -102,12 +102,14 @@ public sealed class SurfaceManifestStageExecutorTests
Assert.Equal(publisher.LastManifestDigest, result!.ManifestDigest);
Assert.Equal(result.DeterminismMerkleRoot, publisher.LastRequest!.DeterminismMerkleRoot);
Assert.Equal(6, cache.Entries.Count);
Assert.Equal(8, cache.Entries.Count);
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.entrytrace.graph" && key.Tenant == "tenant-a");
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.entrytrace.ndjson" && key.Tenant == "tenant-a");
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.layer.fragments" && key.Tenant == "tenant-a");
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.determinism.json" && key.Tenant == "tenant-a");
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.composition.recipe" && key.Tenant == "tenant-a");
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.composition.recipe.dsse" && key.Tenant == "tenant-a");
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.layer.fragments.dsse" && key.Tenant == "tenant-a");
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.manifests" && key.Tenant == "tenant-a");
var publishedMetrics = listener.Measurements
@@ -116,7 +118,7 @@ public sealed class SurfaceManifestStageExecutorTests
Assert.Single(publishedMetrics);
Assert.Equal(1, publishedMetrics[0].Value);
Assert.Equal("published", publishedMetrics[0]["surface.result"]);
Assert.Equal(5, Convert.ToInt32(publishedMetrics[0]["surface.payload_count"]));
Assert.Equal(7, Convert.ToInt32(publishedMetrics[0]["surface.payload_count"]));
var payloadMetrics = listener.Measurements
.Where(m => m.InstrumentName == "scanner_worker_surface_payload_persisted_total")