Update
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
2025-10-21 18:54:26 +03:00
committed by Vladimir Moushkov
parent 791e12baab
commit cfaea5efd9
50 changed files with 3027 additions and 596 deletions

View File

@@ -0,0 +1,110 @@
using System.Collections.Generic;
namespace StellaOps.Excititor.Core;
public sealed class MirrorDistributionOptions
{
public const string SectionName = "Excititor:Mirror";
/// <summary>
/// Global enable flag for mirror distribution surfaces and bundle generation.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Optional absolute or relative path for mirror artifacts. When unset, publishers
/// may fall back to artifact-store specific defaults.
/// </summary>
public string? OutputRoot { get; set; }
/// <summary>
/// Directory name created under <see cref="OutputRoot"/> that holds mirror artifacts.
/// Defaults to <c>mirror</c> to align with offline kit layouts.
/// </summary>
public string DirectoryName { get; set; } = "mirror";
/// <summary>
/// Optional human-readable hint describing where downstream mirrors should publish
/// bundles (e.g., s3://mirror/excititor). Propagated to manifests and index payloads.
/// </summary>
public string? TargetRepository { get; set; }
/// <summary>
/// Signing configuration applied to generated bundle payloads.
/// </summary>
public MirrorSigningOptions Signing { get; } = new();
/// <summary>
/// Domains exposed for mirror consumption. Each domain groups a set of export plans.
/// </summary>
public List<MirrorDomainOptions> Domains { get; } = new();
}
public sealed class MirrorDomainOptions
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public bool RequireAuthentication { get; set; } = false;
/// <summary>
/// Maximum index requests allowed per rolling window.
/// </summary>
public int MaxIndexRequestsPerHour { get; set; } = 120;
/// <summary>
/// Maximum export downloads allowed per rolling window.
/// </summary>
public int MaxDownloadRequestsPerHour { get; set; } = 600;
public List<MirrorExportOptions> Exports { get; } = new();
}
public sealed class MirrorExportOptions
{
public string Key { get; set; } = string.Empty;
public string Format { get; set; } = string.Empty;
public Dictionary<string, string> Filters { get; } = new();
public Dictionary<string, bool> Sort { get; } = new();
public int? Limit { get; set; } = null;
public int? Offset { get; set; } = null;
public string? View { get; set; } = null;
}
public sealed class MirrorSigningOptions
{
/// <summary>
/// Enables signing of mirror bundle payloads when true. When false the publisher
/// omits detached JWS artifacts.
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// Signing algorithm requested (for example, ES256). The publisher validates that
/// the selected provider can satisfy the requested algorithm.
/// </summary>
public string? Algorithm { get; set; }
/// <summary>
/// Optional key identifier resolved against the configured crypto provider registry.
/// </summary>
public string? KeyId { get; set; }
/// <summary>
/// Optional provider hint used to resolve signing providers when multiple are registered.
/// </summary>
public string? Provider { get; set; }
/// <summary>
/// Optional file path to a signing key (PEM). Used when the requested provider does
/// not already have the key loaded into its key store.
/// </summary>
public string? KeyPath { get; set; }
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Linq;
namespace StellaOps.Excititor.Core;
public sealed record MirrorExportPlan(
string Key,
VexExportFormat Format,
VexQuery Query,
VexQuerySignature Signature);
public static class MirrorExportPlanner
{
public static bool TryBuild(MirrorExportOptions exportOptions, out MirrorExportPlan plan, out string? error)
{
if (exportOptions is null)
{
plan = null!;
error = "invalid_export_configuration";
return false;
}
if (string.IsNullOrWhiteSpace(exportOptions.Key))
{
plan = null!;
error = "missing_export_key";
return false;
}
if (string.IsNullOrWhiteSpace(exportOptions.Format) ||
!Enum.TryParse(exportOptions.Format, ignoreCase: true, out VexExportFormat format))
{
plan = null!;
error = "unsupported_export_format";
return false;
}
var filters = exportOptions.Filters.Select(pair => new VexQueryFilter(pair.Key, pair.Value));
var sorts = exportOptions.Sort.Select(pair => new VexQuerySort(pair.Key, pair.Value));
var query = VexQuery.Create(filters, sorts, exportOptions.Limit, exportOptions.Offset, exportOptions.View);
var signature = VexQuerySignature.FromQuery(query);
plan = new MirrorExportPlan(exportOptions.Key.Trim(), format, query, signature);
error = null;
return true;
}
}

View File

@@ -230,13 +230,33 @@ public static class VexCanonicalJsonSerializer
"sourceProviders",
"consensusRevision",
"policyRevisionId",
"policyDigest",
"consensusDigest",
"scoreDigest",
"attestation",
"sizeBytes",
}
},
"policyDigest",
"consensusDigest",
"scoreDigest",
"quietProvenance",
"attestation",
"sizeBytes",
}
},
{
typeof(VexQuietProvenance),
new[]
{
"vulnerabilityId",
"productKey",
"statements",
}
},
{
typeof(VexQuietStatement),
new[]
{
"providerId",
"statementId",
"justification",
"signature",
}
},
{
typeof(VexScoreEnvelope),
new[]

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Runtime.Serialization;
using System.Text;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
namespace StellaOps.Excititor.Core;
@@ -19,9 +20,10 @@ public sealed record VexExportManifest
string? policyRevisionId = null,
string? policyDigest = null,
VexContentAddress? consensusDigest = null,
VexContentAddress? scoreDigest = null,
VexAttestationMetadata? attestation = null,
long sizeBytes = 0)
VexContentAddress? scoreDigest = null,
IEnumerable<VexQuietProvenance>? quietProvenance = null,
VexAttestationMetadata? attestation = null,
long sizeBytes = 0)
{
if (string.IsNullOrWhiteSpace(exportId))
{
@@ -48,11 +50,12 @@ public sealed record VexExportManifest
SourceProviders = NormalizeProviders(sourceProviders);
ConsensusRevision = string.IsNullOrWhiteSpace(consensusRevision) ? null : consensusRevision.Trim();
PolicyRevisionId = string.IsNullOrWhiteSpace(policyRevisionId) ? null : policyRevisionId.Trim();
PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
ConsensusDigest = consensusDigest;
ScoreDigest = scoreDigest;
Attestation = attestation;
SizeBytes = sizeBytes;
PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
ConsensusDigest = consensusDigest;
ScoreDigest = scoreDigest;
QuietProvenance = NormalizeQuietProvenance(quietProvenance);
Attestation = attestation;
SizeBytes = sizeBytes;
}
public string ExportId { get; }
@@ -79,13 +82,15 @@ public sealed record VexExportManifest
public VexContentAddress? ConsensusDigest { get; }
public VexContentAddress? ScoreDigest { get; }
public VexAttestationMetadata? Attestation { get; }
public VexContentAddress? ScoreDigest { get; }
public ImmutableArray<VexQuietProvenance> QuietProvenance { get; }
public VexAttestationMetadata? Attestation { get; }
public long SizeBytes { get; }
private static ImmutableArray<string> NormalizeProviders(IEnumerable<string> providers)
private static ImmutableArray<string> NormalizeProviders(IEnumerable<string> providers)
{
if (providers is null)
{
@@ -103,11 +108,24 @@ public sealed record VexExportManifest
set.Add(provider.Trim());
}
return set.Count == 0
? ImmutableArray<string>.Empty
: set.ToImmutableArray();
}
}
return set.Count == 0
? ImmutableArray<string>.Empty
: set.ToImmutableArray();
}
private static ImmutableArray<VexQuietProvenance> NormalizeQuietProvenance(IEnumerable<VexQuietProvenance>? quietProvenance)
{
if (quietProvenance is null)
{
return ImmutableArray<VexQuietProvenance>.Empty;
}
return quietProvenance
.OrderBy(static entry => entry.VulnerabilityId, StringComparer.Ordinal)
.ThenBy(static entry => entry.ProductKey, StringComparer.Ordinal)
.ToImmutableArray();
}
}
public sealed record VexContentAddress
{

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Excititor.Core;
public sealed record VexQuietProvenance
{
public VexQuietProvenance(string vulnerabilityId, string productKey, IEnumerable<VexQuietStatement> statements)
{
if (string.IsNullOrWhiteSpace(vulnerabilityId))
{
throw new ArgumentException("Vulnerability id must be provided.", nameof(vulnerabilityId));
}
if (string.IsNullOrWhiteSpace(productKey))
{
throw new ArgumentException("Product key must be provided.", nameof(productKey));
}
VulnerabilityId = vulnerabilityId.Trim();
ProductKey = productKey.Trim();
Statements = NormalizeStatements(statements);
}
public string VulnerabilityId { get; }
public string ProductKey { get; }
public ImmutableArray<VexQuietStatement> Statements { get; }
private static ImmutableArray<VexQuietStatement> NormalizeStatements(IEnumerable<VexQuietStatement> statements)
{
if (statements is null)
{
throw new ArgumentNullException(nameof(statements));
}
return statements
.OrderBy(static s => s.ProviderId, StringComparer.Ordinal)
.ThenBy(static s => s.StatementId, StringComparer.Ordinal)
.ToImmutableArray();
}
}
public sealed record VexQuietStatement
{
public VexQuietStatement(
string providerId,
string statementId,
VexJustification? justification,
VexSignatureMetadata? signature)
{
if (string.IsNullOrWhiteSpace(providerId))
{
throw new ArgumentException("Provider id must be provided.", nameof(providerId));
}
if (string.IsNullOrWhiteSpace(statementId))
{
throw new ArgumentException("Statement id must be provided.", nameof(statementId));
}
ProviderId = providerId.Trim();
StatementId = statementId.Trim();
Justification = justification;
Signature = signature;
}
public string ProviderId { get; }
public string StatementId { get; }
public VexJustification? Justification { get; }
public VexSignatureMetadata? Signature { get; }
}