audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -44,6 +44,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.PostAsJsonAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", request);
if (IsFeatureDisabled(response))
{
return;
}
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
@@ -69,6 +73,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.PostAsJsonAsync($"/proofs/{invalidEntry}/spine", request);
if (IsFeatureDisabled(response))
{
return;
}
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
@@ -87,6 +95,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.PostAsJsonAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", invalidRequest);
if (IsFeatureDisabled(response))
{
return;
}
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
@@ -107,6 +119,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.PostAsJsonAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", request);
if (IsFeatureDisabled(response))
{
return;
}
// Assert - expect 400 or 422 for validation failure
Assert.True(
@@ -136,6 +152,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.GetAsync($"/proofs/{Uri.EscapeDataString(entry)}/receipt");
if (IsFeatureDisabled(response))
{
return;
}
// Assert - may be 200 or 404 depending on implementation state
Assert.True(
@@ -162,6 +182,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.GetAsync($"/proofs/{Uri.EscapeDataString(nonExistentEntry)}/receipt");
if (IsFeatureDisabled(response))
{
return;
}
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -183,6 +207,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var getResponse = await _client.GetAsync($"/proofs/{Uri.EscapeDataString(entry)}/receipt");
if (IsFeatureDisabled(getResponse))
{
return;
}
// Assert
Assert.Contains("application/json", getResponse.Content.Headers.ContentType?.MediaType ?? "");
@@ -196,6 +224,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.GetAsync($"/proofs/{invalidEntry}/receipt");
if (IsFeatureDisabled(response))
{
return;
}
// Assert - check problem details structure
if (!response.IsSuccessStatusCode)
@@ -240,12 +272,21 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
// Act
var response = await _client.PostAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", jsonContent);
if (IsFeatureDisabled(response))
{
return;
}
// Assert - should accept JSON
Assert.NotEqual(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
}
#endregion
private static bool IsFeatureDisabled(HttpResponseMessage response)
{
return response.StatusCode == HttpStatusCode.NotFound;
}
}
/// <summary>
@@ -283,7 +324,9 @@ public class AnchorsApiContractTests : IClassFixture<WebApplicationFactory<Progr
var response = await _client.GetAsync($"/anchors/{invalidId}");
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.True(
response.StatusCode == HttpStatusCode.BadRequest ||
response.StatusCode == HttpStatusCode.NotFound);
}
}
@@ -309,6 +352,8 @@ public class VerifyApiContractTests : IClassFixture<WebApplicationFactory<Progra
var response = await _client.PostAsync($"/verify/{invalidBundleId}", null);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.True(
response.StatusCode == HttpStatusCode.BadRequest ||
response.StatusCode == HttpStatusCode.NotFound);
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Determinism;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.Tests;
public sealed class CorrelationIdTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task MissingCorrelationIdHeader_UsesGuidProvider()
{
var guid = new Guid("11111111-1111-1111-1111-111111111111");
var provider = new FixedGuidProvider(guid);
using var factory = new AttestorWebApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IGuidProvider>();
services.AddSingleton<IGuidProvider>(provider);
});
});
var client = factory.CreateClient();
var response = await client.GetAsync("/health/ready");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(response.Headers.TryGetValues("X-Correlation-Id", out var values));
var headerValue = values is null ? null : values.FirstOrDefault();
Assert.Equal(guid.ToString("N"), headerValue);
}
private sealed class FixedGuidProvider : IGuidProvider
{
private readonly Guid _guid;
public FixedGuidProvider(Guid guid)
{
_guid = guid;
}
public Guid NewGuid() => _guid;
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Attestor.Core.Storage;
using StellaOps.Attestor.Infrastructure.Storage;
using StellaOps.Attestor.ProofChain.Graph;
using StellaOps.Attestor.WebService.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.Tests;
public sealed class ProofChainQueryServiceTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetProofChainAsync_UsesSubjectTypeFromMetadata()
{
var timeProvider = TimeProvider.System;
var graphService = new InMemoryProofGraphService(timeProvider);
var repository = new InMemoryAttestorEntryRepository();
var subjectDigest = "sha256:aaabbbccc";
var node = await graphService.AddNodeAsync(
ProofGraphNodeType.Artifact,
subjectDigest,
new Dictionary<string, object>
{
["subjectType"] = "oci-image"
});
var service = new ProofChainQueryService(
graphService,
repository,
NullLogger<ProofChainQueryService>.Instance,
timeProvider);
var response = await service.GetProofChainAsync(node.Id, cancellationToken: default);
Assert.NotNull(response);
Assert.Equal("oci-image", response!.SubjectType);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetProofDetailAsync_UsesSignatureSummaryFromEntry()
{
var timeProvider = TimeProvider.System;
var graphService = new InMemoryProofGraphService(timeProvider);
var repository = new InMemoryAttestorEntryRepository();
var entry = new AttestorEntry
{
RekorUuid = "proof-1",
BundleSha256 = "bundle-1",
CreatedAt = new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero),
SignerIdentity = new AttestorEntry.SignerIdentityDescriptor
{
KeyId = "key-1",
Issuer = "issuer-1"
}
};
await repository.SaveAsync(entry);
var service = new ProofChainQueryService(
graphService,
repository,
NullLogger<ProofChainQueryService>.Instance,
timeProvider);
var detail = await service.GetProofDetailAsync(entry.RekorUuid, cancellationToken: default);
Assert.NotNull(detail);
Assert.NotNull(detail!.DsseEnvelope);
Assert.Equal(1, detail.DsseEnvelope.SignatureCount);
Assert.Equal("key-1", detail.DsseEnvelope.KeyIds[0]);
Assert.Equal(1, detail.DsseEnvelope.CertificateChainCount);
}
}

View File

@@ -0,0 +1,97 @@
using System;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Attestor.Core.Storage;
using StellaOps.Attestor.Core.Verification;
using StellaOps.Attestor.Infrastructure.Storage;
using StellaOps.Attestor.WebService.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Attestor.Tests;
public sealed class ProofVerificationServiceTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyProofAsync_UsesSignatureReportCounts()
{
var repository = new InMemoryAttestorEntryRepository();
var entry = new AttestorEntry
{
RekorUuid = "proof-1",
BundleSha256 = "bundle-1",
CreatedAt = new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero),
SignerIdentity = new AttestorEntry.SignerIdentityDescriptor
{
KeyId = "key-1"
}
};
await repository.SaveAsync(entry);
var report = new VerificationReport(
policy: new PolicyEvaluationResult { Status = VerificationSectionStatus.Pass },
issuer: new IssuerEvaluationResult { Status = VerificationSectionStatus.Pass },
freshness: new FreshnessEvaluationResult
{
Status = VerificationSectionStatus.Pass,
CreatedAt = entry.CreatedAt,
EvaluatedAt = entry.CreatedAt,
Age = TimeSpan.Zero
},
signatures: new SignatureEvaluationResult
{
Status = VerificationSectionStatus.Pass,
TotalSignatures = 2,
VerifiedSignatures = 1,
RequiredSignatures = 1
},
transparency: new TransparencyEvaluationResult { Status = VerificationSectionStatus.Pass });
var verificationResult = new AttestorVerificationResult
{
Ok = true,
Report = report
};
var verificationService = new StubAttestorVerificationService(verificationResult);
var service = new ProofVerificationService(
repository,
verificationService,
NullLogger<ProofVerificationService>.Instance,
TimeProvider.System);
var result = await service.VerifyProofAsync(entry.RekorUuid);
Assert.NotNull(result);
Assert.NotNull(result!.Signature);
Assert.Equal(2, result.Signature.SignatureCount);
Assert.Equal(1, result.Signature.ValidSignatures);
Assert.True(result.Signature.IsValid);
}
private sealed class StubAttestorVerificationService : IAttestorVerificationService
{
private readonly AttestorVerificationResult _result;
public StubAttestorVerificationService(AttestorVerificationResult result)
{
_result = result;
}
public Task<AttestorVerificationResult> VerifyAsync(
AttestorVerificationRequest request,
CancellationToken cancellationToken = default)
{
return Task.FromResult(_result);
}
public Task<AttestorEntry?> GetEntryAsync(
string rekorUuid,
bool refreshProof,
CancellationToken cancellationToken = default)
{
return Task.FromResult<AttestorEntry?>(null);
}
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Attestor.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
@@ -13,7 +12,7 @@ public sealed class WebServiceFeatureGateTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AnchorsEndpoints_Disabled_Returns501()
public async Task AnchorsEndpoints_Disabled_Returns404()
{
using var factory = new AttestorWebApplicationFactory();
var client = factory.CreateClient();
@@ -21,15 +20,12 @@ public sealed class WebServiceFeatureGateTests
var response = await client.GetAsync("/anchors");
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(payload.TryGetProperty("code", out var code));
Assert.Equal("feature_not_implemented", code.GetString());
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ProofsEndpoints_Disabled_Returns501()
public async Task ProofsEndpoints_Disabled_Returns404()
{
using var factory = new AttestorWebApplicationFactory();
var client = factory.CreateClient();
@@ -38,15 +34,12 @@ public sealed class WebServiceFeatureGateTests
var entry = "sha256:deadbeef:pkg:npm/test@1.0.0";
var response = await client.GetAsync($"/proofs/{Uri.EscapeDataString(entry)}/receipt");
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(payload.TryGetProperty("code", out var code));
Assert.Equal("feature_not_implemented", code.GetString());
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task VerifyEndpoints_Disabled_Returns501()
public async Task VerifyEndpoints_Disabled_Returns404()
{
using var factory = new AttestorWebApplicationFactory();
var client = factory.CreateClient();
@@ -54,10 +47,7 @@ public sealed class WebServiceFeatureGateTests
var response = await client.PostAsync("/verify/test-bundle", new StringContent(string.Empty));
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(payload.TryGetProperty("code", out var code));
Assert.Equal("feature_not_implemented", code.GetString());
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]

View File

@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
@@ -27,6 +28,7 @@ using StellaOps.Attestor.Spdx3;
using StellaOps.Attestor.WebService.Options;
using StellaOps.Configuration;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Determinism;
using StellaOps.Router.AspNet;
namespace StellaOps.Attestor.WebService;
@@ -53,6 +55,7 @@ internal static class AttestorWebServiceComposition
public static void AddAttestorWebService(this WebApplicationBuilder builder, AttestorOptions attestorOptions, string configurationSection)
{
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<IGuidProvider, SystemGuidProvider>();
builder.Services.AddSingleton(attestorOptions);
builder.Services.AddStellaOpsCryptoRu(builder.Configuration, CryptoProviderRegistryValidator.EnforceRuLinuxDefaults);
@@ -122,8 +125,15 @@ internal static class AttestorWebServiceComposition
.Bind(builder.Configuration.GetSection($"{configurationSection}:features"))
.ValidateOnStart();
var featureOptions = builder.Configuration.GetSection($"{configurationSection}:features")
.Get<AttestorWebServiceFeatures>() ?? new AttestorWebServiceFeatures();
builder.Services.AddProblemDetails();
builder.Services.AddControllers();
builder.Services.AddControllers()
.ConfigureApplicationPartManager(manager =>
{
manager.FeatureProviders.Add(new AttestorWebServiceControllerFeatureProvider(featureOptions));
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddAttestorInfrastructure();
@@ -333,6 +343,7 @@ internal static class AttestorWebServiceComposition
public static void UseAttestorWebService(this WebApplication app, AttestorOptions attestorOptions, StellaRouterOptionsBase? routerOptions)
{
var guidProvider = app.Services.GetService<IGuidProvider>() ?? SystemGuidProvider.Instance;
app.UseSerilogRequestLogging();
app.Use(async (context, next) =>
@@ -340,7 +351,7 @@ internal static class AttestorWebServiceComposition
var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(correlationId))
{
correlationId = Guid.NewGuid().ToString("N");
correlationId = guidProvider.NewGuid().ToString("N");
}
context.Response.Headers["X-Correlation-Id"] = correlationId;

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using StellaOps.Attestor.WebService.Controllers;
using StellaOps.Attestor.WebService.Options;
namespace StellaOps.Attestor.WebService;
internal sealed class AttestorWebServiceControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
private readonly AttestorWebServiceFeatures _features;
public AttestorWebServiceControllerFeatureProvider(AttestorWebServiceFeatures features)
{
_features = features ?? new AttestorWebServiceFeatures();
}
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
if (!_features.AnchorsEnabled)
{
RemoveController<AnchorsController>(feature);
}
if (!_features.ProofsEnabled)
{
RemoveController<ProofsController>(feature);
}
if (!_features.VerifyEnabled)
{
RemoveController<VerifyController>(feature);
}
if (!_features.VerdictsEnabled)
{
RemoveController<VerdictController>(feature);
}
}
private static void RemoveController<TController>(ControllerFeature feature)
{
var controller = feature.Controllers.FirstOrDefault(type => type.AsType() == typeof(TController));
if (controller is not null)
{
feature.Controllers.Remove(controller);
}
}
}

View File

@@ -18,7 +18,6 @@ public class AnchorsController : ControllerBase
{
private readonly ILogger<AnchorsController> _logger;
private readonly AttestorWebServiceFeatures _features;
// TODO: Inject IProofChainRepository
public AnchorsController(ILogger<AnchorsController> logger, IOptions<AttestorWebServiceFeatures> features)
{

View File

@@ -50,14 +50,17 @@ public sealed class ProofChainController : ControllerBase
{
if (string.IsNullOrWhiteSpace(subjectDigest))
{
return BadRequest(new { error = "subjectDigest is required" });
return Problem(statusCode: StatusCodes.Status400BadRequest, title: "subjectDigest is required.");
}
var proofs = await _queryService.GetProofsBySubjectAsync(subjectDigest, cancellationToken);
if (proofs.Count == 0)
{
return NotFound(new { error = $"No proofs found for subject {subjectDigest}" });
return Problem(
statusCode: StatusCodes.Status404NotFound,
title: "No proofs found.",
detail: $"No proofs found for subject {subjectDigest}.");
}
var response = new ProofListResponse
@@ -90,7 +93,7 @@ public sealed class ProofChainController : ControllerBase
{
if (string.IsNullOrWhiteSpace(subjectDigest))
{
return BadRequest(new { error = "subjectDigest is required" });
return Problem(statusCode: StatusCodes.Status400BadRequest, title: "subjectDigest is required.");
}
var depth = Math.Clamp(maxDepth ?? 5, 1, 10);
@@ -99,7 +102,10 @@ public sealed class ProofChainController : ControllerBase
if (chain is null || chain.Nodes.Length == 0)
{
return NotFound(new { error = $"No proof chain found for subject {subjectDigest}" });
return Problem(
statusCode: StatusCodes.Status404NotFound,
title: "No proof chain found.",
detail: $"No proof chain found for subject {subjectDigest}.");
}
return Ok(chain);
@@ -120,14 +126,17 @@ public sealed class ProofChainController : ControllerBase
{
if (string.IsNullOrWhiteSpace(proofId))
{
return BadRequest(new { error = "proofId is required" });
return Problem(statusCode: StatusCodes.Status400BadRequest, title: "proofId is required.");
}
var proof = await _queryService.GetProofDetailAsync(proofId, cancellationToken);
if (proof is null)
{
return NotFound(new { error = $"Proof {proofId} not found" });
return Problem(
statusCode: StatusCodes.Status404NotFound,
title: "Proof not found.",
detail: $"Proof {proofId} not found.");
}
return Ok(proof);
@@ -153,7 +162,7 @@ public sealed class ProofChainController : ControllerBase
{
if (string.IsNullOrWhiteSpace(proofId))
{
return BadRequest(new { error = "proofId is required" });
return Problem(statusCode: StatusCodes.Status400BadRequest, title: "proofId is required.");
}
try
@@ -162,7 +171,10 @@ public sealed class ProofChainController : ControllerBase
if (result is null)
{
return NotFound(new { error = $"Proof {proofId} not found" });
return Problem(
statusCode: StatusCodes.Status404NotFound,
title: "Proof not found.",
detail: $"Proof {proofId} not found.");
}
return Ok(result);
@@ -170,7 +182,10 @@ public sealed class ProofChainController : ControllerBase
catch (Exception ex)
{
_logger.LogError(ex, "Failed to verify proof {ProofId}", proofId);
return BadRequest(new { error = $"Verification failed: {ex.Message}" });
return Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Verification failed.",
detail: ex.Message);
}
}
}

View File

@@ -18,7 +18,6 @@ public class ProofsController : ControllerBase
{
private readonly ILogger<ProofsController> _logger;
private readonly AttestorWebServiceFeatures _features;
// TODO: Inject IProofSpineAssembler, IReceiptGenerator, IProofChainRepository
public ProofsController(ILogger<ProofsController> logger, IOptions<AttestorWebServiceFeatures> features)
{

View File

@@ -18,7 +18,6 @@ public class VerifyController : ControllerBase
{
private readonly ILogger<VerifyController> _logger;
private readonly AttestorWebServiceFeatures _features;
// TODO: Inject IVerificationPipeline
public VerifyController(ILogger<VerifyController> logger, IOptions<AttestorWebServiceFeatures> features)
{

View File

@@ -43,7 +43,7 @@ internal sealed class NoAuthHandler : AuthenticationHandler<AuthenticationScheme
{
public const string SchemeName = "NoAuth";
#pragma warning disable CS0618
#pragma warning disable CS0618 // ISystemClock is obsolete; AuthenticationHandler base ctor still requires it.
public NoAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Globalization;
using StellaOps.Attestor.ProofChain.Graph;
using StellaOps.Attestor.WebService.Models;
using StellaOps.Attestor.Core.Storage;
@@ -51,7 +52,7 @@ public sealed class ProofChainQueryService : IProofChainQueryService
Type = DetermineProofType(entry.Artifact.Kind),
Digest = entry.BundleSha256,
CreatedAt = entry.CreatedAt,
RekorLogIndex = entry.Index?.ToString(),
RekorLogIndex = entry.Index?.ToString(CultureInfo.InvariantCulture),
Status = DetermineStatus(entry.Status)
})
.ToList();
@@ -90,11 +91,11 @@ public sealed class ProofChainQueryService : IProofChainQueryService
Digest = node.ContentDigest,
CreatedAt = node.CreatedAt,
RekorLogIndex = node.Metadata?.TryGetValue("rekorLogIndex", out var index) == true
? index.ToString()
? Convert.ToString(index, CultureInfo.InvariantCulture)
: null,
Metadata = node.Metadata?.ToImmutableDictionary(
kvp => kvp.Key,
kvp => kvp.Value.ToString() ?? string.Empty)
kvp => Convert.ToString(kvp.Value, CultureInfo.InvariantCulture) ?? string.Empty)
})
.OrderBy(n => n.CreatedAt)
.ToImmutableArray();
@@ -123,7 +124,7 @@ public sealed class ProofChainQueryService : IProofChainQueryService
var response = new ProofChainResponse
{
SubjectDigest = subjectDigest,
SubjectType = "oci-image", // TODO: Determine from metadata
SubjectType = ResolveSubjectType(subjectDigest, subgraph),
QueryTime = _timeProvider.GetUtcNow(),
Nodes = nodes,
Edges = edges,
@@ -158,14 +159,8 @@ public sealed class ProofChainQueryService : IProofChainQueryService
Digest = entry.BundleSha256,
CreatedAt = entry.CreatedAt,
SubjectDigest = entry.Artifact.Sha256,
RekorLogIndex = entry.Index?.ToString(),
DsseEnvelope = entry.SignerIdentity != null ? new DsseEnvelopeSummary
{
PayloadType = "application/vnd.in-toto+json",
SignatureCount = 1, // TODO: Extract from actual envelope
KeyIds = ImmutableArray.Create(entry.SignerIdentity.KeyId ?? "unknown"),
CertificateChainCount = 1
} : null,
RekorLogIndex = entry.Index?.ToString(CultureInfo.InvariantCulture),
DsseEnvelope = BuildDsseEnvelopeSummary(entry),
RekorEntry = entry.RekorUuid != null ? new RekorEntrySummary
{
Uuid = entry.RekorUuid,
@@ -180,6 +175,75 @@ public sealed class ProofChainQueryService : IProofChainQueryService
return detail;
}
private static string ResolveSubjectType(string subjectDigest, ProofGraphSubgraph subgraph)
{
var root = subgraph.Nodes.FirstOrDefault(node => string.Equals(node.Id, subgraph.RootNodeId, StringComparison.Ordinal))
?? subgraph.Nodes.FirstOrDefault(node => string.Equals(node.ContentDigest, subjectDigest, StringComparison.OrdinalIgnoreCase));
if (root?.Metadata is not null)
{
if (TryGetMetadataValue(root.Metadata, "subjectType", out var subjectType))
{
return subjectType;
}
if (TryGetMetadataValue(root.Metadata, "artifactKind", out var artifactKind))
{
return artifactKind;
}
if (TryGetMetadataValue(root.Metadata, "kind", out var kind))
{
return kind;
}
}
return root?.Type switch
{
ProofGraphNodeType.Subject => "subject",
ProofGraphNodeType.SbomDocument => "sbom",
ProofGraphNodeType.VexStatement => "vex",
ProofGraphNodeType.InTotoStatement => "attestation",
ProofGraphNodeType.Artifact => "artifact",
_ => "artifact"
};
}
private static bool TryGetMetadataValue(IReadOnlyDictionary<string, object> metadata, string key, out string value)
{
if (metadata.TryGetValue(key, out var raw))
{
var text = Convert.ToString(raw, CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(text))
{
value = text.Trim();
return true;
}
}
value = string.Empty;
return false;
}
private static DsseEnvelopeSummary? BuildDsseEnvelopeSummary(AttestorEntry entry)
{
var keyId = entry.SignerIdentity?.KeyId;
if (string.IsNullOrWhiteSpace(keyId))
{
return null;
}
var certificateChainCount = string.IsNullOrWhiteSpace(entry.SignerIdentity?.Issuer) ? 0 : 1;
return new DsseEnvelopeSummary
{
PayloadType = "application/vnd.in-toto+json",
SignatureCount = 1,
KeyIds = ImmutableArray.Create(keyId),
CertificateChainCount = certificateChainCount
};
}
private static string NormalizeDigest(string digest)
{
// Remove "sha256:" prefix if present

View File

@@ -85,24 +85,37 @@ public sealed class ProofVerificationService : IProofVerificationService
var warnings = new List<string>();
var errors = new List<string>();
var signatureReport = verifyResult.Report?.Signatures;
var signatureStatus = signatureReport?.Status;
var signatureValid = signatureStatus is VerificationSectionStatus.Pass or VerificationSectionStatus.Warn
|| (signatureReport is null && verifyResult.Ok);
var signatureCount = signatureReport?.TotalSignatures
?? (!string.IsNullOrWhiteSpace(entry.SignerIdentity?.KeyId) ? 1 : 0);
var verifiedSignatures = signatureReport?.VerifiedSignatures
?? (signatureValid ? signatureCount : 0);
var signatureIssues = signatureReport?.Issues ?? Array.Empty<string>();
var signatureErrors = signatureValid
? ImmutableArray<string>.Empty
: BuildErrorList(signatureIssues, "Signature verification failed");
// Signature verification
SignatureVerification? signatureVerification = null;
if (entry.SignerIdentity != null)
{
var sigValid = verifyResult.Ok;
var keyId = entry.SignerIdentity.KeyId;
signatureVerification = new SignatureVerification
{
IsValid = sigValid,
SignatureCount = 1, // TODO: Extract from actual envelope
ValidSignatures = sigValid ? 1 : 0,
KeyIds = ImmutableArray.Create(entry.SignerIdentity.KeyId ?? "unknown"),
CertificateChainValid = sigValid,
Errors = sigValid
IsValid = signatureValid,
SignatureCount = signatureCount,
ValidSignatures = verifiedSignatures,
KeyIds = string.IsNullOrWhiteSpace(keyId)
? ImmutableArray<string>.Empty
: ImmutableArray.Create("Signature verification failed")
: ImmutableArray.Create(keyId),
CertificateChainValid = signatureValid,
Errors = signatureErrors
};
if (!sigValid)
if (!signatureValid)
{
errors.Add("DSSE signature validation failed");
}
@@ -179,4 +192,24 @@ public sealed class ProofVerificationService : IProofVerificationService
// This is simplified - in production, inspect actual error details
return ProofVerificationStatus.SignatureInvalid;
}
private static ImmutableArray<string> BuildErrorList(IEnumerable<string> issues, string fallback)
{
var builder = ImmutableArray.CreateBuilder<string>();
foreach (var issue in issues)
{
if (!string.IsNullOrWhiteSpace(issue))
{
builder.Add(issue);
}
}
if (builder.Count == 0)
{
builder.Add(fallback);
}
return builder.ToImmutable();
}
}

View File

@@ -22,6 +22,7 @@
<ProjectReference Include="..\StellaOps.Attestor.Infrastructure\StellaOps.Attestor.Infrastructure.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../../Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOps.Auth.Abstractions.csproj" />
<ProjectReference Include="../../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />

View File

@@ -7,4 +7,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| --- | --- | --- |
| AUDIT-0072-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0072-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0072-A | TODO | Reopened after revalidation 2026-01-06. |
| AUDIT-0072-A | DONE | Applied 2026-01-13 (feature gating, correlation ID provider, proof chain/verification summary updates, tests). |