up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
This commit is contained in:
@@ -12,6 +12,7 @@ namespace StellaOps.ExportCenter.RiskBundles;
|
||||
public sealed class RiskBundleBuilder
|
||||
{
|
||||
private const string ManifestVersion = "1";
|
||||
private static readonly string[] MandatoryProviderIds = { "cisa-kev" };
|
||||
private static readonly UnixFileMode DefaultFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead;
|
||||
private static readonly DateTimeOffset FixedTimestamp = new(2024, 01, 01, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
@@ -54,6 +55,7 @@ public sealed class RiskBundleBuilder
|
||||
private static List<RiskBundleProviderEntry> CollectProviders(RiskBundleBuildRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entries = new List<RiskBundleProviderEntry>(request.Providers.Count);
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var provider in request.Providers.OrderBy(p => p.ProviderId, StringComparer.Ordinal))
|
||||
{
|
||||
@@ -88,17 +90,26 @@ public sealed class RiskBundleBuilder
|
||||
var sha256 = ComputeSha256FromFile(fullPath);
|
||||
var size = new FileInfo(fullPath).Length;
|
||||
var bundlePath = $"providers/{provider.ProviderId}/snapshot";
|
||||
var signaturePath = ResolveSignaturePath(provider);
|
||||
string? signatureSha256 = null;
|
||||
if (!string.IsNullOrWhiteSpace(signaturePath) && File.Exists(signaturePath))
|
||||
{
|
||||
signatureSha256 = ComputeSha256FromFile(signaturePath);
|
||||
}
|
||||
|
||||
seen.Add(provider.ProviderId);
|
||||
|
||||
entries.Add(new RiskBundleProviderEntry(
|
||||
provider.ProviderId,
|
||||
provider.Source,
|
||||
provider.SnapshotDate,
|
||||
sha256,
|
||||
signatureSha256,
|
||||
size,
|
||||
provider.Optional,
|
||||
bundlePath,
|
||||
fullPath,
|
||||
SignaturePath: null));
|
||||
signaturePath));
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
@@ -106,6 +117,14 @@ public sealed class RiskBundleBuilder
|
||||
throw new InvalidOperationException("No provider artefacts collected. Provide at least one valid provider input.");
|
||||
}
|
||||
|
||||
foreach (var mandatory in MandatoryProviderIds)
|
||||
{
|
||||
if (!seen.Contains(mandatory))
|
||||
{
|
||||
throw new InvalidOperationException($"Mandatory provider '{mandatory}' is missing from build inputs.");
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -135,6 +154,8 @@ public sealed class RiskBundleBuilder
|
||||
.Append(entry.Optional ? '1' : '0')
|
||||
.Append('\0')
|
||||
.Append(entry.SnapshotDate?.ToString("yyyy-MM-dd") ?? string.Empty)
|
||||
.Append('\0')
|
||||
.Append(entry.SignatureSha256 ?? string.Empty)
|
||||
.Append('\0');
|
||||
}
|
||||
|
||||
@@ -207,6 +228,18 @@ public sealed class RiskBundleBuilder
|
||||
DataStream = dataStream
|
||||
};
|
||||
writer.WriteEntry(tarEntry);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entry.SignaturePath) && File.Exists(entry.SignaturePath))
|
||||
{
|
||||
using var sigStream = new FileStream(entry.SignaturePath, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, FileOptions.SequentialScan);
|
||||
var sigEntry = new PaxTarEntry(TarEntryType.RegularFile, $"{Path.GetDirectoryName(entry.BundlePath)?.TrimEnd('/')}/signature")
|
||||
{
|
||||
Mode = DefaultFileMode,
|
||||
ModificationTime = FixedTimestamp,
|
||||
DataStream = sigStream
|
||||
};
|
||||
writer.WriteEntry(sigEntry);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteTextEntry(TarWriter writer, string path, string content, UnixFileMode mode)
|
||||
@@ -238,4 +271,15 @@ public sealed class RiskBundleBuilder
|
||||
stream.Write(buffer);
|
||||
stream.Position = originalPosition;
|
||||
}
|
||||
|
||||
private static string? ResolveSignaturePath(RiskBundleProviderInput provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(provider.SignaturePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var full = Path.GetFullPath(provider.SignaturePath);
|
||||
return File.Exists(full) ? full : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public sealed record RiskBundleProviderInput(
|
||||
string ProviderId,
|
||||
string SourcePath,
|
||||
string Source,
|
||||
string? SignaturePath = null,
|
||||
bool Optional = false,
|
||||
DateOnly? SnapshotDate = null);
|
||||
|
||||
@@ -14,6 +15,7 @@ public sealed record RiskBundleProviderEntry(
|
||||
string Source,
|
||||
DateOnly? SnapshotDate,
|
||||
string Sha256,
|
||||
string? SignatureSha256,
|
||||
long SizeBytes,
|
||||
bool Optional,
|
||||
string BundlePath,
|
||||
|
||||
@@ -10,14 +10,15 @@ public sealed class RiskBundleBuilderTests
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
var kev = temp.WriteFile("kev.json", "{\"cve\":\"CVE-0001\"}");
|
||||
var kevSig = temp.WriteFile("kev.sig", "sig");
|
||||
var epss = temp.WriteFile("epss.csv", "cve,score\nCVE-0001,0.12\n");
|
||||
|
||||
var request = new RiskBundleBuildRequest(
|
||||
BundleId: Guid.Parse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"),
|
||||
Providers: new[]
|
||||
{
|
||||
new RiskBundleProviderInput("cisa-kev", kev, "CISA KEV"),
|
||||
new RiskBundleProviderInput("first-epss", epss, "FIRST EPSS")
|
||||
new RiskBundleProviderInput("cisa-kev", kev, "CISA KEV", SignaturePath: kevSig),
|
||||
new RiskBundleProviderInput("first-epss", epss, "FIRST EPSS", Optional: true)
|
||||
});
|
||||
|
||||
var builder = new RiskBundleBuilder();
|
||||
@@ -26,6 +27,8 @@ public sealed class RiskBundleBuilderTests
|
||||
|
||||
Assert.Equal(2, result.Manifest.Providers.Count);
|
||||
Assert.Equal(new[] { "cisa-kev", "first-epss" }, result.Manifest.Providers.Select(p => p.ProviderId));
|
||||
Assert.NotNull(result.Manifest.Providers[0].SignatureSha256);
|
||||
Assert.Equal("providers/cisa-kev/snapshot", result.Manifest.Providers[0].BundlePath);
|
||||
|
||||
// Manifest hash stable
|
||||
var second = builder.Build(request, cancellation);
|
||||
@@ -44,6 +47,22 @@ public sealed class RiskBundleBuilderTests
|
||||
Assert.Contains("manifests/provider-manifest.json", entries);
|
||||
Assert.Contains("providers/cisa-kev/snapshot", entries);
|
||||
Assert.Contains("providers/first-epss/snapshot", entries);
|
||||
Assert.Contains("providers/cisa-kev/signature", entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_WhenMandatoryProviderMissing_Throws()
|
||||
{
|
||||
using var temp = new TempDir();
|
||||
var epss = temp.WriteFile("epss.csv", "cve,score\n");
|
||||
|
||||
var request = new RiskBundleBuildRequest(
|
||||
Guid.NewGuid(),
|
||||
Providers: new[] { new RiskBundleProviderInput("first-epss", epss, "FIRST EPSS", Optional: true) });
|
||||
|
||||
var builder = new RiskBundleBuilder();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => builder.Build(request, TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
private sealed class TempDir : IDisposable
|
||||
|
||||
Reference in New Issue
Block a user