part #2
This commit is contained in:
@@ -1,14 +1,8 @@
|
||||
|
||||
using StellaOps.AirGap.Bundle.Models;
|
||||
using StellaOps.AirGap.Bundle.Serialization;
|
||||
using StellaOps.AirGap.Bundle.Validation;
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.AirGap.Bundle.Services;
|
||||
|
||||
public sealed class BundleBuilder : IBundleBuilder
|
||||
public sealed partial class BundleBuilder : IBundleBuilder
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
@@ -38,603 +32,4 @@ public sealed class BundleBuilder : IBundleBuilder
|
||||
_ocspFetcher = ocspFetcher ?? new OcspResponseFetcher();
|
||||
_crlFetcher = crlFetcher ?? new CrlFetcher();
|
||||
}
|
||||
|
||||
public async Task<BundleManifest> BuildAsync(
|
||||
BundleBuildRequest request,
|
||||
string outputPath,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
Directory.CreateDirectory(outputPath);
|
||||
|
||||
var feeds = new List<FeedComponent>();
|
||||
var policies = new List<PolicyComponent>();
|
||||
var cryptoMaterials = new List<CryptoComponent>();
|
||||
|
||||
foreach (var feedConfig in request.Feeds)
|
||||
{
|
||||
// Validate relative path before combining
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, feedConfig.RelativePath);
|
||||
|
||||
var component = await CopyComponentAsync(feedConfig, outputPath, targetPath, ct).ConfigureAwait(false);
|
||||
feeds.Add(new FeedComponent(
|
||||
feedConfig.FeedId,
|
||||
feedConfig.Name,
|
||||
feedConfig.Version,
|
||||
component.RelativePath,
|
||||
component.Digest,
|
||||
component.SizeBytes,
|
||||
feedConfig.SnapshotAt,
|
||||
feedConfig.Format));
|
||||
}
|
||||
|
||||
foreach (var policyConfig in request.Policies)
|
||||
{
|
||||
// Validate relative path before combining
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, policyConfig.RelativePath);
|
||||
|
||||
var component = await CopyComponentAsync(policyConfig, outputPath, targetPath, ct).ConfigureAwait(false);
|
||||
policies.Add(new PolicyComponent(
|
||||
policyConfig.PolicyId,
|
||||
policyConfig.Name,
|
||||
policyConfig.Version,
|
||||
component.RelativePath,
|
||||
component.Digest,
|
||||
component.SizeBytes,
|
||||
policyConfig.Type));
|
||||
}
|
||||
|
||||
foreach (var cryptoConfig in request.CryptoMaterials)
|
||||
{
|
||||
// Validate relative path before combining
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, cryptoConfig.RelativePath);
|
||||
|
||||
var component = await CopyComponentAsync(cryptoConfig, outputPath, targetPath, ct).ConfigureAwait(false);
|
||||
cryptoMaterials.Add(new CryptoComponent(
|
||||
cryptoConfig.ComponentId,
|
||||
cryptoConfig.Name,
|
||||
component.RelativePath,
|
||||
component.Digest,
|
||||
component.SizeBytes,
|
||||
cryptoConfig.Type,
|
||||
cryptoConfig.ExpiresAt));
|
||||
}
|
||||
|
||||
var ruleBundles = new List<RuleBundleComponent>();
|
||||
foreach (var ruleBundleConfig in request.RuleBundles)
|
||||
{
|
||||
// Validate relative path before combining
|
||||
var targetDir = PathValidation.SafeCombine(outputPath, ruleBundleConfig.RelativePath);
|
||||
Directory.CreateDirectory(targetDir);
|
||||
|
||||
var files = new List<RuleBundleFileComponent>();
|
||||
long bundleTotalSize = 0;
|
||||
var digestBuilder = new System.Text.StringBuilder();
|
||||
|
||||
// Copy all files from source directory
|
||||
if (Directory.Exists(ruleBundleConfig.SourceDirectory))
|
||||
{
|
||||
foreach (var sourceFile in Directory.GetFiles(ruleBundleConfig.SourceDirectory)
|
||||
.OrderBy(f => Path.GetFileName(f), StringComparer.Ordinal))
|
||||
{
|
||||
var fileName = Path.GetFileName(sourceFile);
|
||||
var targetFile = Path.Combine(targetDir, fileName);
|
||||
|
||||
await using (var input = File.OpenRead(sourceFile))
|
||||
await using (var output = File.Create(targetFile))
|
||||
{
|
||||
await input.CopyToAsync(output, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await using var digestStream = File.OpenRead(targetFile);
|
||||
var hash = await SHA256.HashDataAsync(digestStream, ct).ConfigureAwait(false);
|
||||
var fileDigest = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
var fileInfo = new FileInfo(targetFile);
|
||||
files.Add(new RuleBundleFileComponent(fileName, fileDigest, fileInfo.Length));
|
||||
bundleTotalSize += fileInfo.Length;
|
||||
digestBuilder.Append(fileDigest);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute combined digest from all file digests
|
||||
var combinedDigest = Convert.ToHexString(
|
||||
SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(digestBuilder.ToString()))).ToLowerInvariant();
|
||||
|
||||
ruleBundles.Add(new RuleBundleComponent(
|
||||
ruleBundleConfig.BundleId,
|
||||
ruleBundleConfig.BundleType,
|
||||
ruleBundleConfig.Version,
|
||||
ruleBundleConfig.RelativePath,
|
||||
combinedDigest,
|
||||
bundleTotalSize,
|
||||
ruleBundleConfig.RuleCount,
|
||||
ruleBundleConfig.SignerKeyId,
|
||||
ruleBundleConfig.SignedAt,
|
||||
files.ToImmutableArray()));
|
||||
}
|
||||
|
||||
var timestamps = new List<TimestampEntry>();
|
||||
long timestampSizeBytes = 0;
|
||||
var timestampConfigs = request.Timestamps ?? Array.Empty<TimestampBuildConfig>();
|
||||
foreach (var timestampConfig in timestampConfigs)
|
||||
{
|
||||
switch (timestampConfig)
|
||||
{
|
||||
case Rfc3161TimestampBuildConfig rfc3161:
|
||||
var (rfcEntry, rfcSizeBytes) = await BuildRfc3161TimestampAsync(
|
||||
rfc3161,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
timestamps.Add(rfcEntry);
|
||||
timestampSizeBytes += rfcSizeBytes;
|
||||
break;
|
||||
case EidasQtsTimestampBuildConfig eidas:
|
||||
var qtsComponent = await CopyTimestampFileAsync(
|
||||
eidas.SourcePath,
|
||||
eidas.RelativePath,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
timestamps.Add(new EidasQtsTimestampEntry
|
||||
{
|
||||
QtsMetaPath = qtsComponent.RelativePath
|
||||
});
|
||||
timestampSizeBytes += qtsComponent.SizeBytes;
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
$"Unsupported timestamp build config type '{timestampConfig.GetType().Name}'.");
|
||||
}
|
||||
}
|
||||
|
||||
var artifacts = new List<BundleArtifact>();
|
||||
long artifactsSizeBytes = 0;
|
||||
var artifactConfigs = request.Artifacts ?? Array.Empty<BundleArtifactBuildConfig>();
|
||||
foreach (var artifactConfig in artifactConfigs)
|
||||
{
|
||||
var (artifact, sizeBytes) = await AddArtifactAsync(
|
||||
artifactConfig,
|
||||
outputPath,
|
||||
request.StrictInlineArtifacts,
|
||||
request.WarningSink,
|
||||
ct).ConfigureAwait(false);
|
||||
artifacts.Add(artifact);
|
||||
artifactsSizeBytes += sizeBytes;
|
||||
}
|
||||
|
||||
var totalSize = feeds.Sum(f => f.SizeBytes) +
|
||||
policies.Sum(p => p.SizeBytes) +
|
||||
cryptoMaterials.Sum(c => c.SizeBytes) +
|
||||
ruleBundles.Sum(r => r.SizeBytes) +
|
||||
timestampSizeBytes +
|
||||
artifactsSizeBytes;
|
||||
|
||||
var exportMode = request.ExportOptions?.Mode ?? BundleExportMode.Light;
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = _guidProvider.NewGuid().ToString(),
|
||||
SchemaVersion = "1.0.0",
|
||||
Name = request.Name,
|
||||
Version = request.Version,
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Feeds = feeds.ToImmutableArray(),
|
||||
Policies = policies.ToImmutableArray(),
|
||||
CryptoMaterials = cryptoMaterials.ToImmutableArray(),
|
||||
RuleBundles = ruleBundles.ToImmutableArray(),
|
||||
Timestamps = timestamps.ToImmutableArray(),
|
||||
Artifacts = artifacts.ToImmutableArray(),
|
||||
ExportMode = exportMode.ToString().ToLowerInvariant(),
|
||||
TotalSizeBytes = totalSize
|
||||
};
|
||||
|
||||
return BundleManifestSerializer.WithDigest(manifest);
|
||||
}
|
||||
|
||||
private static async Task<(BundleArtifact Artifact, long SizeBytes)> AddArtifactAsync(
|
||||
BundleArtifactBuildConfig config,
|
||||
string outputPath,
|
||||
bool strictInlineArtifacts,
|
||||
ICollection<string>? warningSink,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.Type))
|
||||
{
|
||||
throw new ArgumentException("Artifact type is required.", nameof(config));
|
||||
}
|
||||
|
||||
var hasSourcePath = !string.IsNullOrWhiteSpace(config.SourcePath);
|
||||
var hasContent = config.Content is { Length: > 0 };
|
||||
if (!hasSourcePath && !hasContent)
|
||||
{
|
||||
throw new ArgumentException("Artifact content or source path is required.", nameof(config));
|
||||
}
|
||||
|
||||
string? relativePath = string.IsNullOrWhiteSpace(config.RelativePath) ? null : config.RelativePath;
|
||||
if (!string.IsNullOrWhiteSpace(relativePath) && !PathValidation.IsSafeRelativePath(relativePath))
|
||||
{
|
||||
throw new ArgumentException($"Invalid relative path: {relativePath}", nameof(config));
|
||||
}
|
||||
|
||||
string digest;
|
||||
long sizeBytes;
|
||||
|
||||
if (hasSourcePath)
|
||||
{
|
||||
var sourcePath = Path.GetFullPath(config.SourcePath!);
|
||||
if (!File.Exists(sourcePath))
|
||||
{
|
||||
throw new FileNotFoundException("Artifact source file not found.", sourcePath);
|
||||
}
|
||||
|
||||
var info = new FileInfo(sourcePath);
|
||||
sizeBytes = info.Length;
|
||||
digest = await ComputeSha256DigestAsync(sourcePath, ct).ConfigureAwait(false);
|
||||
relativePath = ApplyInlineSizeGuard(
|
||||
relativePath,
|
||||
config,
|
||||
digest,
|
||||
sizeBytes,
|
||||
strictInlineArtifacts,
|
||||
warningSink);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
|
||||
File.Copy(sourcePath, targetPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var content = config.Content ?? Array.Empty<byte>();
|
||||
sizeBytes = content.Length;
|
||||
digest = ComputeSha256Digest(content);
|
||||
relativePath = ApplyInlineSizeGuard(
|
||||
relativePath,
|
||||
config,
|
||||
digest,
|
||||
sizeBytes,
|
||||
strictInlineArtifacts,
|
||||
warningSink);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
|
||||
await File.WriteAllBytesAsync(targetPath, content, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var artifact = new BundleArtifact(relativePath, config.Type, config.ContentType, digest, sizeBytes);
|
||||
return (artifact, sizeBytes);
|
||||
}
|
||||
|
||||
private static string? ApplyInlineSizeGuard(
|
||||
string? relativePath,
|
||||
BundleArtifactBuildConfig config,
|
||||
string digest,
|
||||
long sizeBytes,
|
||||
bool strictInlineArtifacts,
|
||||
ICollection<string>? warningSink)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
if (!BundleSizeValidator.RequiresExternalization(sizeBytes))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var warning = BundleSizeValidator.GetInlineSizeWarning(sizeBytes)
|
||||
?? "Inline artifact size exceeds the maximum allowed size.";
|
||||
|
||||
if (strictInlineArtifacts)
|
||||
{
|
||||
throw new InvalidOperationException(warning);
|
||||
}
|
||||
|
||||
warningSink?.Add(warning);
|
||||
|
||||
var fileName = string.IsNullOrWhiteSpace(config.FileName)
|
||||
? BuildInlineFallbackName(config.Type, digest)
|
||||
: EnsureSafeFileName(config.FileName);
|
||||
|
||||
var fallbackPath = $"artifacts/{fileName}";
|
||||
if (!PathValidation.IsSafeRelativePath(fallbackPath))
|
||||
{
|
||||
throw new ArgumentException($"Invalid artifact fallback path: {fallbackPath}", nameof(config));
|
||||
}
|
||||
|
||||
return fallbackPath;
|
||||
}
|
||||
|
||||
private static string BuildInlineFallbackName(string type, string digest)
|
||||
{
|
||||
var normalizedType = SanitizeFileSegment(type);
|
||||
var digestValue = digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? digest[7..]
|
||||
: digest;
|
||||
var shortDigest = digestValue.Length > 12 ? digestValue[..12] : digestValue;
|
||||
return $"{normalizedType}-{shortDigest}.blob";
|
||||
}
|
||||
|
||||
private static string SanitizeFileSegment(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "artifact";
|
||||
}
|
||||
|
||||
var buffer = new char[value.Length];
|
||||
var index = 0;
|
||||
foreach (var ch in value)
|
||||
{
|
||||
if (char.IsLetterOrDigit(ch) || ch == '-' || ch == '_')
|
||||
{
|
||||
buffer[index++] = ch;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[index++] = '-';
|
||||
}
|
||||
}
|
||||
|
||||
var cleaned = new string(buffer, 0, index).Trim('-');
|
||||
return string.IsNullOrWhiteSpace(cleaned) ? "artifact" : cleaned;
|
||||
}
|
||||
|
||||
private static string EnsureSafeFileName(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
throw new ArgumentException("Artifact file name is required.");
|
||||
}
|
||||
|
||||
if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 ||
|
||||
fileName.Contains('/') ||
|
||||
fileName.Contains('\\'))
|
||||
{
|
||||
throw new ArgumentException($"Invalid artifact file name: {fileName}");
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
private static async Task<string> ComputeSha256DigestAsync(string filePath, CancellationToken ct)
|
||||
{
|
||||
await using var stream = File.OpenRead(filePath);
|
||||
var hash = await SHA256.HashDataAsync(stream, ct).ConfigureAwait(false);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string ComputeSha256Digest(byte[] content)
|
||||
{
|
||||
var hash = SHA256.HashData(content);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static async Task<CopiedComponent> CopyComponentAsync(
|
||||
BundleComponentSource source,
|
||||
string outputPath,
|
||||
string targetPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
|
||||
|
||||
await using (var input = File.OpenRead(source.SourcePath))
|
||||
await using (var output = File.Create(targetPath))
|
||||
{
|
||||
await input.CopyToAsync(output, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await using var digestStream = File.OpenRead(targetPath);
|
||||
var hash = await SHA256.HashDataAsync(digestStream, ct).ConfigureAwait(false);
|
||||
var digest = Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
var info = new FileInfo(targetPath);
|
||||
return new CopiedComponent(source.RelativePath, digest, info.Length);
|
||||
}
|
||||
|
||||
private async Task<(Rfc3161TimestampEntry Entry, long SizeBytes)> BuildRfc3161TimestampAsync(
|
||||
Rfc3161TimestampBuildConfig config,
|
||||
string outputPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (config.TimeStampToken is not { Length: > 0 })
|
||||
{
|
||||
throw new ArgumentException("RFC3161 timestamp token is required.", nameof(config));
|
||||
}
|
||||
|
||||
var tokenHash = SHA256.HashData(config.TimeStampToken);
|
||||
var tokenPrefix = Convert.ToHexString(tokenHash).ToLowerInvariant()[..12];
|
||||
|
||||
var chainResult = await _tsaChainBundler.BundleAsync(
|
||||
config.TimeStampToken,
|
||||
outputPath,
|
||||
tokenPrefix,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var ocspBlobs = await _ocspFetcher.FetchAsync(chainResult.Certificates, ct).ConfigureAwait(false);
|
||||
var (ocspPaths, ocspSizeBytes) = await WriteRevocationBlobsAsync(
|
||||
"tsa/ocsp",
|
||||
"der",
|
||||
tokenPrefix,
|
||||
ocspBlobs,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var crlBlobs = await _crlFetcher.FetchAsync(chainResult.Certificates, ct).ConfigureAwait(false);
|
||||
var (crlPaths, crlSizeBytes) = await WriteRevocationBlobsAsync(
|
||||
"tsa/crl",
|
||||
"crl",
|
||||
tokenPrefix,
|
||||
crlBlobs,
|
||||
outputPath,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
var entry = new Rfc3161TimestampEntry
|
||||
{
|
||||
TsaChainPaths = chainResult.ChainPaths,
|
||||
OcspBlobs = ocspPaths,
|
||||
CrlBlobs = crlPaths,
|
||||
TstBase64 = Convert.ToBase64String(config.TimeStampToken)
|
||||
};
|
||||
|
||||
return (entry, chainResult.TotalSizeBytes + ocspSizeBytes + crlSizeBytes);
|
||||
}
|
||||
|
||||
private static async Task<(ImmutableArray<string> Paths, long SizeBytes)> WriteRevocationBlobsAsync(
|
||||
string baseDir,
|
||||
string extension,
|
||||
string prefix,
|
||||
IReadOnlyList<TsaRevocationBlob> blobs,
|
||||
string outputPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (blobs.Count == 0)
|
||||
{
|
||||
return ([], 0);
|
||||
}
|
||||
|
||||
var paths = new List<string>(blobs.Count);
|
||||
long totalSize = 0;
|
||||
|
||||
foreach (var blob in blobs
|
||||
.OrderBy(b => b.CertificateIndex)
|
||||
.ThenBy(b => ComputeShortHash(b.Data), StringComparer.Ordinal))
|
||||
{
|
||||
var hash = ComputeShortHash(blob.Data);
|
||||
var fileName = $"{prefix}-{blob.CertificateIndex:D2}-{hash}.{extension}";
|
||||
var relativePath = $"{baseDir}/{fileName}";
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, relativePath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
|
||||
await File.WriteAllBytesAsync(targetPath, blob.Data, ct).ConfigureAwait(false);
|
||||
|
||||
totalSize += blob.Data.Length;
|
||||
paths.Add(relativePath);
|
||||
}
|
||||
|
||||
return (paths.ToImmutableArray(), totalSize);
|
||||
}
|
||||
|
||||
private static string ComputeShortHash(byte[] data)
|
||||
{
|
||||
var hash = SHA256.HashData(data);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant()[..16];
|
||||
}
|
||||
|
||||
private static async Task<CopiedTimestampComponent> CopyTimestampFileAsync(
|
||||
string sourcePath,
|
||||
string relativePath,
|
||||
string outputPath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var targetPath = PathValidation.SafeCombine(outputPath, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? outputPath);
|
||||
|
||||
await using (var input = File.OpenRead(sourcePath))
|
||||
await using (var output = File.Create(targetPath))
|
||||
{
|
||||
await input.CopyToAsync(output, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var info = new FileInfo(targetPath);
|
||||
return new CopiedTimestampComponent(relativePath, info.Length);
|
||||
}
|
||||
|
||||
private sealed record CopiedComponent(string RelativePath, string Digest, long SizeBytes);
|
||||
private sealed record CopiedTimestampComponent(string RelativePath, long SizeBytes);
|
||||
}
|
||||
|
||||
public interface IBundleBuilder
|
||||
{
|
||||
Task<BundleManifest> BuildAsync(BundleBuildRequest request, string outputPath, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record BundleBuildRequest(
|
||||
string Name,
|
||||
string Version,
|
||||
DateTimeOffset? ExpiresAt,
|
||||
IReadOnlyList<FeedBuildConfig> Feeds,
|
||||
IReadOnlyList<PolicyBuildConfig> Policies,
|
||||
IReadOnlyList<CryptoBuildConfig> CryptoMaterials,
|
||||
IReadOnlyList<RuleBundleBuildConfig> RuleBundles,
|
||||
IReadOnlyList<TimestampBuildConfig>? Timestamps = null,
|
||||
IReadOnlyList<BundleArtifactBuildConfig>? Artifacts = null,
|
||||
bool StrictInlineArtifacts = false,
|
||||
ICollection<string>? WarningSink = null,
|
||||
BundleBuilderOptions? ExportOptions = null);
|
||||
|
||||
public abstract record BundleComponentSource(string SourcePath, string RelativePath);
|
||||
|
||||
public sealed record FeedBuildConfig(
|
||||
string FeedId,
|
||||
string Name,
|
||||
string Version,
|
||||
string SourcePath,
|
||||
string RelativePath,
|
||||
DateTimeOffset SnapshotAt,
|
||||
FeedFormat Format)
|
||||
: BundleComponentSource(SourcePath, RelativePath);
|
||||
|
||||
public sealed record PolicyBuildConfig(
|
||||
string PolicyId,
|
||||
string Name,
|
||||
string Version,
|
||||
string SourcePath,
|
||||
string RelativePath,
|
||||
PolicyType Type)
|
||||
: BundleComponentSource(SourcePath, RelativePath);
|
||||
|
||||
public sealed record CryptoBuildConfig(
|
||||
string ComponentId,
|
||||
string Name,
|
||||
string SourcePath,
|
||||
string RelativePath,
|
||||
CryptoComponentType Type,
|
||||
DateTimeOffset? ExpiresAt)
|
||||
: BundleComponentSource(SourcePath, RelativePath);
|
||||
|
||||
public abstract record TimestampBuildConfig;
|
||||
|
||||
public sealed record Rfc3161TimestampBuildConfig(byte[] TimeStampToken)
|
||||
: TimestampBuildConfig;
|
||||
|
||||
public sealed record EidasQtsTimestampBuildConfig(string SourcePath, string RelativePath)
|
||||
: TimestampBuildConfig;
|
||||
|
||||
public sealed record BundleArtifactBuildConfig
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public string? ContentType { get; init; }
|
||||
public string? SourcePath { get; init; }
|
||||
public byte[]? Content { get; init; }
|
||||
public string? RelativePath { get; init; }
|
||||
public string? FileName { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for building a rule bundle component.
|
||||
/// </summary>
|
||||
/// <param name="BundleId">Bundle identifier (e.g., "secrets.ruleset").</param>
|
||||
/// <param name="BundleType">Bundle type (e.g., "secrets", "malware").</param>
|
||||
/// <param name="Version">Bundle version in YYYY.MM format.</param>
|
||||
/// <param name="SourceDirectory">Source directory containing the rule bundle files.</param>
|
||||
/// <param name="RelativePath">Relative path in the output bundle.</param>
|
||||
/// <param name="RuleCount">Number of rules in the bundle.</param>
|
||||
/// <param name="SignerKeyId">Key ID used to sign the bundle.</param>
|
||||
/// <param name="SignedAt">When the bundle was signed.</param>
|
||||
public sealed record RuleBundleBuildConfig(
|
||||
string BundleId,
|
||||
string BundleType,
|
||||
string Version,
|
||||
string SourceDirectory,
|
||||
string RelativePath,
|
||||
int RuleCount,
|
||||
string? SignerKeyId,
|
||||
DateTimeOffset? SignedAt);
|
||||
|
||||
Reference in New Issue
Block a user