This commit is contained in:
110
src/StellaOps.Excititor.Core/MirrorDistributionOptions.cs
Normal file
110
src/StellaOps.Excititor.Core/MirrorDistributionOptions.cs
Normal 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; }
|
||||
}
|
||||
47
src/StellaOps.Excititor.Core/MirrorExportPlanner.cs
Normal file
47
src/StellaOps.Excititor.Core/MirrorExportPlanner.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
78
src/StellaOps.Excititor.Core/VexQuietProvenance.cs
Normal file
78
src/StellaOps.Excititor.Core/VexQuietProvenance.cs
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user