save progress

This commit is contained in:
StellaOps Bot
2026-01-04 19:08:47 +02:00
parent f7d27c6fda
commit 75611a505f
97 changed files with 4531 additions and 293 deletions

View File

@@ -20,6 +20,7 @@ public sealed record BundleManifest
public ImmutableArray<CatalogComponent> Catalogs { get; init; } = [];
public RekorSnapshot? RekorSnapshot { get; init; }
public ImmutableArray<CryptoProviderComponent> CryptoProviders { get; init; } = [];
public ImmutableArray<RuleBundleComponent> RuleBundles { get; init; } = [];
public long TotalSizeBytes { get; init; }
public string? BundleDigest { get; init; }
}
@@ -102,3 +103,39 @@ public sealed record CryptoProviderComponent(
string Digest,
long SizeBytes,
ImmutableArray<string> SupportedAlgorithms);
/// <summary>
/// Component for a rule bundle (e.g., secrets detection rules).
/// </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="RelativePath">Relative path to the bundle directory.</param>
/// <param name="Digest">Combined digest of all files in the bundle.</param>
/// <param name="SizeBytes">Total size of the bundle in bytes.</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>
/// <param name="Files">List of files in the bundle.</param>
public sealed record RuleBundleComponent(
string BundleId,
string BundleType,
string Version,
string RelativePath,
string Digest,
long SizeBytes,
int RuleCount,
string? SignerKeyId,
DateTimeOffset? SignedAt,
ImmutableArray<RuleBundleFileComponent> Files);
/// <summary>
/// A file within a rule bundle component.
/// </summary>
/// <param name="Name">Filename (e.g., "secrets.ruleset.manifest.json").</param>
/// <param name="Digest">SHA256 digest of the file.</param>
/// <param name="SizeBytes">File size in bytes.</param>
public sealed record RuleBundleFileComponent(
string Name,
string Digest,
long SizeBytes);

View File

@@ -25,6 +25,7 @@ public sealed class KnowledgeSnapshotManifest
public List<VexSnapshotEntry> VexStatements { get; init; } = [];
public List<PolicySnapshotEntry> Policies { get; init; } = [];
public List<TrustRootSnapshotEntry> TrustRoots { get; init; } = [];
public List<RuleBundleSnapshotEntry> RuleBundles { get; init; } = [];
public TimeAnchorEntry? TimeAnchor { get; set; }
}
@@ -81,6 +82,79 @@ public sealed class TrustRootSnapshotEntry
public DateTimeOffset? ExpiresAt { get; init; }
}
/// <summary>
/// Entry for a rule bundle in the snapshot.
/// Used for detection rule bundles (secrets, malware, etc.).
/// </summary>
public sealed class RuleBundleSnapshotEntry
{
/// <summary>
/// Bundle identifier (e.g., "secrets.ruleset").
/// </summary>
public required string BundleId { get; init; }
/// <summary>
/// Bundle type (e.g., "secrets", "malware").
/// </summary>
public required string BundleType { get; init; }
/// <summary>
/// Bundle version in YYYY.MM format.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Relative path to the bundle directory in the snapshot.
/// </summary>
public required string RelativePath { get; init; }
/// <summary>
/// List of files in the bundle with their digests.
/// </summary>
public required List<RuleBundleFile> Files { get; init; }
/// <summary>
/// Number of rules in the bundle.
/// </summary>
public int RuleCount { get; init; }
/// <summary>
/// Key ID used to sign the bundle.
/// </summary>
public string? SignerKeyId { get; init; }
/// <summary>
/// When the bundle was signed.
/// </summary>
public DateTimeOffset? SignedAt { get; init; }
/// <summary>
/// When the bundle signature was verified during export.
/// </summary>
public DateTimeOffset? VerifiedAt { get; init; }
}
/// <summary>
/// A file within a rule bundle.
/// </summary>
public sealed class RuleBundleFile
{
/// <summary>
/// Filename (e.g., "secrets.ruleset.manifest.json").
/// </summary>
public required string Name { get; init; }
/// <summary>
/// SHA256 digest of the file.
/// </summary>
public required string Digest { get; init; }
/// <summary>
/// File size in bytes.
/// </summary>
public required long SizeBytes { get; init; }
}
/// <summary>
/// Time anchor entry in the manifest.
/// </summary>

View File

@@ -81,9 +81,64 @@ public sealed class BundleBuilder : IBundleBuilder
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 totalSize = feeds.Sum(f => f.SizeBytes) +
policies.Sum(p => p.SizeBytes) +
cryptoMaterials.Sum(c => c.SizeBytes);
cryptoMaterials.Sum(c => c.SizeBytes) +
ruleBundles.Sum(r => r.SizeBytes);
var manifest = new BundleManifest
{
@@ -96,6 +151,7 @@ public sealed class BundleBuilder : IBundleBuilder
Feeds = feeds.ToImmutableArray(),
Policies = policies.ToImmutableArray(),
CryptoMaterials = cryptoMaterials.ToImmutableArray(),
RuleBundles = ruleBundles.ToImmutableArray(),
TotalSizeBytes = totalSize
};
@@ -138,7 +194,8 @@ public sealed record BundleBuildRequest(
DateTimeOffset? ExpiresAt,
IReadOnlyList<FeedBuildConfig> Feeds,
IReadOnlyList<PolicyBuildConfig> Policies,
IReadOnlyList<CryptoBuildConfig> CryptoMaterials);
IReadOnlyList<CryptoBuildConfig> CryptoMaterials,
IReadOnlyList<RuleBundleBuildConfig> RuleBundles);
public abstract record BundleComponentSource(string SourcePath, string RelativePath);
@@ -169,3 +226,24 @@ public sealed record CryptoBuildConfig(
CryptoComponentType Type,
DateTimeOffset? ExpiresAt)
: BundleComponentSource(SourcePath, RelativePath);
/// <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);

View File

@@ -408,6 +408,38 @@ public sealed class SnapshotBundleReader : ISnapshotBundleReader
entries.Add(new BundleEntry(trust.RelativePath, digest, content.Length));
}
foreach (var ruleBundle in manifest.RuleBundles)
{
// Verify each file in the rule bundle
foreach (var file in ruleBundle.Files)
{
var relativePath = $"{ruleBundle.RelativePath}/{file.Name}";
var filePath = Path.Combine(bundleDir, relativePath.Replace('/', Path.DirectorySeparatorChar));
if (!File.Exists(filePath))
{
return new MerkleVerificationResult
{
Verified = false,
Error = $"Missing rule bundle file: {relativePath}"
};
}
var content = await File.ReadAllBytesAsync(filePath, cancellationToken);
var digest = ComputeSha256(content);
if (digest != file.Digest)
{
return new MerkleVerificationResult
{
Verified = false,
Error = $"Digest mismatch for rule bundle file {relativePath}"
};
}
entries.Add(new BundleEntry(relativePath, digest, content.Length));
}
}
// Compute merkle root
var computedRoot = ComputeMerkleRoot(entries);

View File

@@ -186,6 +186,52 @@ public sealed class SnapshotBundleWriter : ISnapshotBundleWriter
}
}
// Write rule bundles
if (request.RuleBundles is { Count: > 0 })
{
var rulesDir = Path.Combine(tempDir, "rules");
Directory.CreateDirectory(rulesDir);
foreach (var ruleBundle in request.RuleBundles)
{
var bundleDir = Path.Combine(rulesDir, ruleBundle.BundleId);
Directory.CreateDirectory(bundleDir);
var bundleFiles = new List<RuleBundleFile>();
var bundleRelativePath = $"rules/{ruleBundle.BundleId}";
foreach (var file in ruleBundle.Files)
{
var filePath = Path.Combine(bundleDir, file.Name);
await File.WriteAllBytesAsync(filePath, file.Content, cancellationToken);
var relativePath = $"{bundleRelativePath}/{file.Name}";
var digest = ComputeSha256(file.Content);
entries.Add(new BundleEntry(relativePath, digest, file.Content.Length));
bundleFiles.Add(new RuleBundleFile
{
Name = file.Name,
Digest = digest,
SizeBytes = file.Content.Length
});
}
manifest.RuleBundles.Add(new RuleBundleSnapshotEntry
{
BundleId = ruleBundle.BundleId,
BundleType = ruleBundle.BundleType,
Version = ruleBundle.Version,
RelativePath = bundleRelativePath,
Files = bundleFiles,
RuleCount = ruleBundle.RuleCount,
SignerKeyId = ruleBundle.SignerKeyId,
SignedAt = ruleBundle.SignedAt,
VerifiedAt = ruleBundle.VerifiedAt
});
}
}
// Write time anchor
if (request.TimeAnchor is not null)
{
@@ -389,6 +435,7 @@ public sealed record SnapshotBundleRequest
public List<VexContent> VexStatements { get; init; } = [];
public List<PolicyContent> Policies { get; init; } = [];
public List<TrustRootContent> TrustRoots { get; init; } = [];
public List<RuleBundleContent> RuleBundles { get; init; } = [];
public TimeAnchorContent? TimeAnchor { get; init; }
/// <summary>
@@ -445,6 +492,68 @@ public sealed record TrustRootContent
public DateTimeOffset? ExpiresAt { get; init; }
}
/// <summary>
/// Content for a rule bundle (e.g., secrets detection rules).
/// </summary>
public sealed record RuleBundleContent
{
/// <summary>
/// Bundle identifier (e.g., "secrets.ruleset").
/// </summary>
public required string BundleId { get; init; }
/// <summary>
/// Bundle type (e.g., "secrets", "malware").
/// </summary>
public required string BundleType { get; init; }
/// <summary>
/// Bundle version in YYYY.MM format.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Files in the bundle.
/// </summary>
public required List<RuleBundleFileContent> Files { get; init; }
/// <summary>
/// Number of rules in the bundle.
/// </summary>
public int RuleCount { get; init; }
/// <summary>
/// Key ID used to sign the bundle.
/// </summary>
public string? SignerKeyId { get; init; }
/// <summary>
/// When the bundle was signed.
/// </summary>
public DateTimeOffset? SignedAt { get; init; }
/// <summary>
/// When the bundle signature was verified during export.
/// </summary>
public DateTimeOffset? VerifiedAt { get; init; }
}
/// <summary>
/// A file within a rule bundle.
/// </summary>
public sealed record RuleBundleFileContent
{
/// <summary>
/// Filename (e.g., "secrets.ruleset.manifest.json").
/// </summary>
public required string Name { get; init; }
/// <summary>
/// File content.
/// </summary>
public required byte[] Content { get; init; }
}
public sealed record TimeAnchorContent
{
public required DateTimeOffset AnchorTime { get; init; }