new advisories work and features gaps work

This commit is contained in:
master
2026-01-14 18:39:19 +02:00
parent 95d5898650
commit 15aeac8e8b
148 changed files with 16731 additions and 554 deletions

View File

@@ -0,0 +1,69 @@
// <copyright file="PathWitnessPredicateTypes.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_006_ATTESTOR_path_witness_predicate (PW-ATT-003)
// </copyright>
namespace StellaOps.Attestor.Core;
/// <summary>
/// Constants for path witness predicate types used in attestations.
/// </summary>
public static class PathWitnessPredicateTypes
{
/// <summary>
/// Canonical predicate type for path witness attestations.
/// </summary>
public const string PathWitnessV1 = "https://stella.ops/predicates/path-witness/v1";
/// <summary>
/// Alias predicate type using @ version format.
/// </summary>
public const string PathWitnessV1Alias = "stella.ops/pathWitness@v1";
/// <summary>
/// Alias predicate type using HTTPS with camelCase.
/// </summary>
public const string PathWitnessV1HttpsAlias = "https://stella.ops/pathWitness/v1";
/// <summary>
/// All accepted predicate types for path witness attestations.
/// </summary>
public static readonly IReadOnlyList<string> AllAcceptedTypes =
[
PathWitnessV1,
PathWitnessV1Alias,
PathWitnessV1HttpsAlias
];
/// <summary>
/// Checks if the given predicate type is a path witness type.
/// </summary>
/// <param name="predicateType">The predicate type to check.</param>
/// <returns>True if it's a path witness type, false otherwise.</returns>
public static bool IsPathWitnessType(string? predicateType)
{
if (string.IsNullOrEmpty(predicateType))
{
return false;
}
return string.Equals(predicateType, PathWitnessV1, StringComparison.Ordinal)
|| string.Equals(predicateType, PathWitnessV1Alias, StringComparison.Ordinal)
|| string.Equals(predicateType, PathWitnessV1HttpsAlias, StringComparison.Ordinal);
}
/// <summary>
/// Normalizes a path witness predicate type to the canonical form.
/// </summary>
/// <param name="predicateType">The predicate type to normalize.</param>
/// <returns>The canonical predicate type, or the original if not a path witness type.</returns>
public static string NormalizeToCanonical(string predicateType)
{
if (IsPathWitnessType(predicateType))
{
return PathWitnessV1;
}
return predicateType;
}
}

View File

@@ -0,0 +1,333 @@
// <copyright file="RekorEntryEvent.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_007_ATTESTOR_rekor_entry_events (ATT-REKOR-001, ATT-REKOR-002)
// </copyright>
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.Core.Rekor;
/// <summary>
/// Event emitted when a DSSE bundle is logged to Rekor and inclusion proof is available.
/// Used to drive policy reanalysis and evidence graph updates.
/// </summary>
public sealed record RekorEntryEvent
{
/// <summary>
/// Unique event identifier (deterministic based on bundle digest and log index).
/// </summary>
[JsonPropertyName("eventId")]
public required string EventId { get; init; }
/// <summary>
/// Event type constant.
/// </summary>
[JsonPropertyName("eventType")]
public string EventType { get; init; } = RekorEventTypes.EntryLogged;
/// <summary>
/// Tenant identifier.
/// </summary>
[JsonPropertyName("tenant")]
public required string Tenant { get; init; }
/// <summary>
/// SHA-256 digest of the DSSE bundle that was logged.
/// </summary>
[JsonPropertyName("bundleDigest")]
public required string BundleDigest { get; init; }
/// <summary>
/// Predicate type from the DSSE envelope.
/// </summary>
[JsonPropertyName("predicateType")]
public required string PredicateType { get; init; }
/// <summary>
/// Log index where the entry was recorded.
/// </summary>
[JsonPropertyName("logIndex")]
public required long LogIndex { get; init; }
/// <summary>
/// Log ID identifying the Rekor instance.
/// </summary>
[JsonPropertyName("logId")]
public required string LogId { get; init; }
/// <summary>
/// Entry UUID in the Rekor log.
/// </summary>
[JsonPropertyName("entryUuid")]
public required string EntryUuid { get; init; }
/// <summary>
/// Unix timestamp when the entry was integrated.
/// </summary>
[JsonPropertyName("integratedTime")]
public required long IntegratedTime { get; init; }
/// <summary>
/// RFC3339 formatted integrated time for display.
/// </summary>
[JsonPropertyName("integratedTimeRfc3339")]
public required string IntegratedTimeRfc3339 { get; init; }
/// <summary>
/// URL to the Rekor entry for UI linking.
/// </summary>
[JsonPropertyName("entryUrl")]
public string? EntryUrl { get; init; }
/// <summary>
/// Whether inclusion proof was verified.
/// </summary>
[JsonPropertyName("inclusionVerified")]
public required bool InclusionVerified { get; init; }
/// <summary>
/// Policy reanalysis hints extracted from the predicate.
/// </summary>
[JsonPropertyName("reanalysisHints")]
public RekorReanalysisHints? ReanalysisHints { get; init; }
/// <summary>
/// UTC timestamp when this event was created.
/// </summary>
[JsonPropertyName("createdAtUtc")]
public required DateTimeOffset CreatedAtUtc { get; init; }
/// <summary>
/// Correlation ID for tracing.
/// </summary>
[JsonPropertyName("traceId")]
public string? TraceId { get; init; }
}
/// <summary>
/// Hints for policy reanalysis extracted from the logged predicate.
/// </summary>
public sealed record RekorReanalysisHints
{
/// <summary>
/// CVE identifiers affected by this attestation.
/// </summary>
[JsonPropertyName("cveIds")]
public ImmutableArray<string> CveIds { get; init; } = [];
/// <summary>
/// Product keys (PURLs) affected by this attestation.
/// </summary>
[JsonPropertyName("productKeys")]
public ImmutableArray<string> ProductKeys { get; init; } = [];
/// <summary>
/// Artifact digests covered by this attestation.
/// </summary>
[JsonPropertyName("artifactDigests")]
public ImmutableArray<string> ArtifactDigests { get; init; } = [];
/// <summary>
/// Whether this attestation may change a policy decision.
/// </summary>
[JsonPropertyName("mayAffectDecision")]
public bool MayAffectDecision { get; init; }
/// <summary>
/// Suggested reanalysis scope (e.g., "cve", "product", "artifact", "all").
/// </summary>
[JsonPropertyName("reanalysisScope")]
public string ReanalysisScope { get; init; } = "none";
}
/// <summary>
/// Well-known Rekor event types.
/// </summary>
public static class RekorEventTypes
{
/// <summary>
/// Entry was successfully logged to Rekor with verified inclusion.
/// </summary>
public const string EntryLogged = "rekor.entry.logged";
/// <summary>
/// Entry was queued for logging (offline mode).
/// </summary>
public const string EntryQueued = "rekor.entry.queued";
/// <summary>
/// Inclusion proof was verified for a previously logged entry.
/// </summary>
public const string InclusionVerified = "rekor.inclusion.verified";
/// <summary>
/// Entry logging failed.
/// </summary>
public const string EntryFailed = "rekor.entry.failed";
}
/// <summary>
/// Factory for creating deterministic Rekor entry events.
/// </summary>
public static class RekorEntryEventFactory
{
/// <summary>
/// Creates a Rekor entry logged event with deterministic event ID.
/// </summary>
public static RekorEntryEvent CreateEntryLogged(
string tenant,
string bundleDigest,
string predicateType,
RekorReceipt receipt,
DateTimeOffset createdAtUtc,
RekorReanalysisHints? reanalysisHints = null,
string? traceId = null)
{
var eventId = ComputeEventId(
RekorEventTypes.EntryLogged,
bundleDigest,
receipt.LogIndex);
var integratedTimeRfc3339 = DateTimeOffset
.FromUnixTimeSeconds(receipt.IntegratedTime)
.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
var entryUrl = !string.IsNullOrEmpty(receipt.LogUrl)
? $"{receipt.LogUrl.TrimEnd('/')}/api/v1/log/entries/{receipt.Uuid}"
: null;
return new RekorEntryEvent
{
EventId = eventId,
EventType = RekorEventTypes.EntryLogged,
Tenant = tenant,
BundleDigest = bundleDigest,
PredicateType = predicateType,
LogIndex = receipt.LogIndex,
LogId = receipt.LogId,
EntryUuid = receipt.Uuid,
IntegratedTime = receipt.IntegratedTime,
IntegratedTimeRfc3339 = integratedTimeRfc3339,
EntryUrl = entryUrl,
InclusionVerified = true,
ReanalysisHints = reanalysisHints,
CreatedAtUtc = createdAtUtc,
TraceId = traceId
};
}
/// <summary>
/// Creates a Rekor entry queued event (for offline mode).
/// </summary>
public static RekorEntryEvent CreateEntryQueued(
string tenant,
string bundleDigest,
string predicateType,
string queueId,
DateTimeOffset createdAtUtc,
RekorReanalysisHints? reanalysisHints = null,
string? traceId = null)
{
var eventId = ComputeEventId(
RekorEventTypes.EntryQueued,
bundleDigest,
0); // No log index yet
return new RekorEntryEvent
{
EventId = eventId,
EventType = RekorEventTypes.EntryQueued,
Tenant = tenant,
BundleDigest = bundleDigest,
PredicateType = predicateType,
LogIndex = -1, // Not yet logged
LogId = "pending",
EntryUuid = queueId,
IntegratedTime = 0,
IntegratedTimeRfc3339 = "pending",
EntryUrl = null,
InclusionVerified = false,
ReanalysisHints = reanalysisHints,
CreatedAtUtc = createdAtUtc,
TraceId = traceId
};
}
/// <summary>
/// Extracts reanalysis hints from a predicate based on its type.
/// </summary>
public static RekorReanalysisHints ExtractReanalysisHints(
string predicateType,
IReadOnlyList<string>? cveIds = null,
IReadOnlyList<string>? productKeys = null,
IReadOnlyList<string>? artifactDigests = null)
{
// Determine if this predicate type affects policy decisions
var mayAffect = IsDecisionAffectingPredicate(predicateType);
var scope = DetermineReanalysisScope(predicateType, cveIds, productKeys, artifactDigests);
return new RekorReanalysisHints
{
CveIds = cveIds?.ToImmutableArray() ?? [],
ProductKeys = productKeys?.ToImmutableArray() ?? [],
ArtifactDigests = artifactDigests?.ToImmutableArray() ?? [],
MayAffectDecision = mayAffect,
ReanalysisScope = scope
};
}
private static bool IsDecisionAffectingPredicate(string predicateType)
{
// Predicate types that can change policy decisions
return predicateType.Contains("vex", StringComparison.OrdinalIgnoreCase)
|| predicateType.Contains("verdict", StringComparison.OrdinalIgnoreCase)
|| predicateType.Contains("path-witness", StringComparison.OrdinalIgnoreCase)
|| predicateType.Contains("evidence", StringComparison.OrdinalIgnoreCase)
|| predicateType.Contains("override", StringComparison.OrdinalIgnoreCase);
}
private static string DetermineReanalysisScope(
string predicateType,
IReadOnlyList<string>? cveIds,
IReadOnlyList<string>? productKeys,
IReadOnlyList<string>? artifactDigests)
{
if (cveIds?.Count > 0)
{
return "cve";
}
if (productKeys?.Count > 0)
{
return "product";
}
if (artifactDigests?.Count > 0)
{
return "artifact";
}
// Default scope based on predicate type
if (predicateType.Contains("vex", StringComparison.OrdinalIgnoreCase))
{
return "product";
}
if (predicateType.Contains("sbom", StringComparison.OrdinalIgnoreCase))
{
return "artifact";
}
return "none";
}
private static string ComputeEventId(string eventType, string bundleDigest, long logIndex)
{
var input = $"{eventType}|{bundleDigest}|{logIndex}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"rekor-evt-{Convert.ToHexStringLower(hash)[..16]}";
}
}

View File

@@ -31,7 +31,13 @@ public sealed class PredicateTypeRouter : IPredicateTypeRouter
// Delta predicate types for lineage comparison (Sprint 20251228_007)
"stella.ops/vex-delta@v1",
"stella.ops/sbom-delta@v1",
"stella.ops/verdict-delta@v1"
"stella.ops/verdict-delta@v1",
// Path witness predicates (Sprint: SPRINT_20260112_006_ATTESTOR_path_witness_predicate PW-ATT-001)
// Canonical predicate type
"https://stella.ops/predicates/path-witness/v1",
// Aliases for backward compatibility
"stella.ops/pathWitness@v1",
"https://stella.ops/pathWitness/v1"
};
public PredicateTypeRouter(