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