new advisories work and features gaps work
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]}";
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user