feat: add bulk triage view component and related stories

- Exported BulkTriageViewComponent and its related types from findings module.
- Created a new accessibility test suite for score components using axe-core.
- Introduced design tokens for score components to standardize styling.
- Enhanced score breakdown popover for mobile responsiveness with drag handle.
- Added date range selector functionality to score history chart component.
- Implemented unit tests for date range selector in score history chart.
- Created Storybook stories for bulk triage view and score history chart with date range selector.
This commit is contained in:
StellaOps Bot
2025-12-26 01:01:35 +02:00
parent ed3079543c
commit 17613acf57
45 changed files with 9418 additions and 64 deletions

View File

@@ -2,15 +2,25 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Attestor.Core.Rekor;
using StellaOps.Attestor.Core.Submission;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.GraphRoot.Models;
using StellaOps.Canonical.Json;
// Type aliases to resolve naming conflicts with StellaOps.Attestor.DsseEnvelope/DsseSignature
// Must use distinct names to avoid collision with types in StellaOps.Attestor namespace
using EnvDsseEnvelope = StellaOps.Attestor.Envelope.DsseEnvelope;
using EnvDsseSignature = StellaOps.Attestor.Envelope.DsseSignature;
using SubmissionDsseSignature = StellaOps.Attestor.Core.Submission.AttestorSubmissionRequest.DsseSignature;
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
@@ -27,6 +37,8 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
private readonly IMerkleRootComputer _merkleComputer;
private readonly EnvelopeSignatureService _signatureService;
private readonly Func<string?, EnvelopeKey?> _keyResolver;
private readonly IRekorClient? _rekorClient;
private readonly GraphRootAttestorOptions _options;
private readonly ILogger<GraphRootAttestor> _logger;
/// <summary>
@@ -36,16 +48,22 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
/// <param name="signatureService">Service for signing envelopes.</param>
/// <param name="keyResolver">Function to resolve signing keys by ID.</param>
/// <param name="logger">Logger instance.</param>
/// <param name="rekorClient">Optional Rekor client for transparency log publishing.</param>
/// <param name="options">Optional configuration options.</param>
public GraphRootAttestor(
IMerkleRootComputer merkleComputer,
EnvelopeSignatureService signatureService,
Func<string?, EnvelopeKey?> keyResolver,
ILogger<GraphRootAttestor> logger)
ILogger<GraphRootAttestor> logger,
IRekorClient? rekorClient = null,
IOptions<GraphRootAttestorOptions>? options = null)
{
_merkleComputer = merkleComputer ?? throw new ArgumentNullException(nameof(merkleComputer));
_signatureService = signatureService ?? throw new ArgumentNullException(nameof(signatureService));
_keyResolver = keyResolver ?? throw new ArgumentNullException(nameof(keyResolver));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_rekorClient = rekorClient;
_options = options?.Value ?? new GraphRootAttestorOptions();
}
/// <inheritdoc />
@@ -118,30 +136,159 @@ public sealed class GraphRootAttestor : IGraphRootAttestor
$"Signing failed: {signResult.Error?.Message}");
}
var dsseSignature = DsseSignature.FromBytes(signResult.Value!.Value.Span, signResult.Value.KeyId);
var envelope = new DsseEnvelope(PayloadType, payload, [dsseSignature]);
var dsseSignature = EnvDsseSignature.FromBytes(signResult.Value!.Value.Span, signResult.Value.KeyId);
var envelope = new EnvDsseEnvelope(PayloadType, payload, [dsseSignature]);
_logger.LogInformation(
"Created graph root attestation with root {RootHash} for {GraphType}",
rootHash,
request.GraphType);
// Note: Rekor publishing would be handled by a separate service
// that accepts the envelope after creation
// Publish to Rekor transparency log if requested
string? rekorLogIndex = null;
var shouldPublish = request.PublishToRekor || _options.DefaultPublishToRekor;
if (shouldPublish)
{
rekorLogIndex = await PublishToRekorAsync(
envelope,
payload,
rootHash,
request.ArtifactDigest,
ct);
}
return new GraphRootAttestationResult
{
RootHash = rootHash,
Envelope = envelope,
RekorLogIndex = null, // Would be set by Rekor service
RekorLogIndex = rekorLogIndex,
NodeCount = sortedNodeIds.Count,
EdgeCount = sortedEdgeIds.Count
};
}
private async Task<string?> PublishToRekorAsync(
EnvDsseEnvelope envelope,
ReadOnlyMemory<byte> payload,
string rootHash,
string artifactDigest,
CancellationToken ct)
{
if (_rekorClient is null)
{
_logger.LogWarning("Rekor publishing requested but no IRekorClient is configured");
return null;
}
if (_options.RekorBackend is null)
{
_logger.LogWarning("Rekor publishing requested but no RekorBackend is configured");
return null;
}
try
{
// Compute payload digest for Rekor
var payloadDigest = SHA256.HashData(payload.Span);
var payloadDigestHex = Convert.ToHexStringLower(payloadDigest);
// Build submission request
var submissionRequest = BuildRekorSubmissionRequest(
envelope,
payloadDigestHex,
rootHash,
artifactDigest);
_logger.LogDebug(
"Submitting graph root attestation to Rekor: {RootHash}",
rootHash);
var response = await _rekorClient.SubmitAsync(
submissionRequest,
_options.RekorBackend,
ct);
if (response.Index.HasValue)
{
_logger.LogInformation(
"Published graph root attestation to Rekor with log index {LogIndex}",
response.Index.Value);
return response.Index.Value.ToString();
}
_logger.LogWarning(
"Rekor submission succeeded but no log index returned. UUID: {Uuid}",
response.Uuid);
return response.Uuid;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to publish graph root attestation to Rekor");
if (_options.FailOnRekorError)
{
throw new InvalidOperationException(
"Failed to publish attestation to Rekor transparency log", ex);
}
return null;
}
}
private static AttestorSubmissionRequest BuildRekorSubmissionRequest(
EnvDsseEnvelope envelope,
string payloadDigestHex,
string rootHash,
string artifactDigest)
{
// Build DSSE envelope for submission
// Note: EnvDsseSignature.Signature is already base64-encoded
var EnvDsseEnvelope = new AttestorSubmissionRequest.DsseEnvelope
{
PayloadType = envelope.PayloadType,
PayloadBase64 = Convert.ToBase64String(envelope.Payload.Span),
Signatures = envelope.Signatures
.Select(s => new SubmissionDsseSignature
{
KeyId = s.KeyId,
Signature = s.Signature
})
.ToList()
};
// Compute bundle hash
var bundleJson = JsonSerializer.Serialize(EnvDsseEnvelope);
var bundleHash = SHA256.HashData(Encoding.UTF8.GetBytes(bundleJson));
return new AttestorSubmissionRequest
{
Bundle = new AttestorSubmissionRequest.SubmissionBundle
{
Dsse = EnvDsseEnvelope,
Mode = "keyed"
},
Meta = new AttestorSubmissionRequest.SubmissionMeta
{
BundleSha256 = Convert.ToHexStringLower(bundleHash),
LogPreference = "primary",
Archive = true,
Artifact = new AttestorSubmissionRequest.ArtifactInfo
{
Sha256 = payloadDigestHex,
Kind = "graph-root",
SubjectUri = rootHash,
ImageDigest = artifactDigest
}
}
};
}
/// <inheritdoc />
public async Task<GraphRootVerificationResult> VerifyAsync(
DsseEnvelope envelope,
StellaOps.Attestor.Envelope.DsseEnvelope envelope,
IReadOnlyList<GraphNodeData> nodes,
IReadOnlyList<GraphEdgeData> edges,
CancellationToken ct = default)

View File

@@ -0,0 +1,32 @@
using System;
using StellaOps.Attestor.Core.Rekor;
namespace StellaOps.Attestor.GraphRoot;
/// <summary>
/// Configuration options for the graph root attestor.
/// </summary>
public sealed class GraphRootAttestorOptions
{
/// <summary>
/// Configuration section name for binding.
/// </summary>
public const string SectionName = "Attestor:GraphRoot";
/// <summary>
/// Rekor backend configuration for transparency log publishing.
/// When null, Rekor publishing is disabled even if requested.
/// </summary>
public RekorBackend? RekorBackend { get; set; }
/// <summary>
/// Default behavior for Rekor publishing when not specified in request.
/// </summary>
public bool DefaultPublishToRekor { get; set; } = false;
/// <summary>
/// Whether to fail attestation if Rekor publishing fails.
/// When false, attestation succeeds but without Rekor log index.
/// </summary>
public bool FailOnRekorError { get; set; } = false;
}

View File

@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.GraphRoot.Models;
namespace StellaOps.Attestor.GraphRoot;
@@ -32,7 +31,7 @@ public interface IGraphRootAttestor
/// <param name="ct">Cancellation token.</param>
/// <returns>The verification result.</returns>
Task<GraphRootVerificationResult> VerifyAsync(
DsseEnvelope envelope,
StellaOps.Attestor.Envelope.DsseEnvelope envelope,
IReadOnlyList<GraphNodeData> nodes,
IReadOnlyList<GraphEdgeData> edges,
CancellationToken ct = default);

View File

@@ -1,5 +1,3 @@
using StellaOps.Attestor.Envelope;
namespace StellaOps.Attestor.GraphRoot.Models;
/// <summary>
@@ -15,7 +13,7 @@ public sealed record GraphRootAttestationResult
/// <summary>
/// Signed DSSE envelope containing the in-toto statement.
/// </summary>
public required DsseEnvelope Envelope { get; init; }
public required StellaOps.Attestor.Envelope.DsseEnvelope Envelope { get; init; }
/// <summary>
/// Rekor log index if the attestation was published to transparency log.

View File

@@ -13,10 +13,15 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Evidence.Core\StellaOps.Evidence.Core.csproj" />
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
<ProjectReference Include="..\..\StellaOps.Attestor\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
</ItemGroup>
</Project>