finish off sprint advisories and sprints
This commit is contained in:
@@ -129,7 +129,11 @@ public sealed class EvidenceReconciler : IEvidenceReconciler
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Step 4: VEX ingestion + lattice merge.
|
||||
var (mergedStatements, conflictCount) = await MergeVexStatementsAsync(index, options, ct).ConfigureAwait(false);
|
||||
var (mergedStatements, conflictCount) = await MergeVexStatementsAsync(
|
||||
index,
|
||||
Path.Combine(inputDirectory, "attestations"),
|
||||
options,
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
// Step 5: Graph emission.
|
||||
var graph = BuildGraph(
|
||||
@@ -247,6 +251,7 @@ public sealed class EvidenceReconciler : IEvidenceReconciler
|
||||
|
||||
private static async Task<(Dictionary<string, VexStatement> Statements, int ConflictCount)> MergeVexStatementsAsync(
|
||||
ArtifactIndex index,
|
||||
string attestationsDirectory,
|
||||
ReconciliationOptions options,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -258,9 +263,12 @@ public sealed class EvidenceReconciler : IEvidenceReconciler
|
||||
{
|
||||
foreach (var vexRef in entry.VexDocuments)
|
||||
{
|
||||
// Resolve relative path to absolute
|
||||
var absolutePath = Path.Combine(attestationsDirectory, vexRef.FilePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
if (!documentCache.TryGetValue(vexRef.FilePath, out var document))
|
||||
{
|
||||
var loaded = await TryLoadOpenVexDocumentAsync(vexRef.FilePath, ct).ConfigureAwait(false);
|
||||
var loaded = await TryLoadOpenVexDocumentAsync(absolutePath, ct).ConfigureAwait(false);
|
||||
if (loaded is null)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -248,6 +248,7 @@ public sealed record NormalizationOptions
|
||||
SortArrays = true,
|
||||
LowercaseUris = true,
|
||||
StripTimestamps = true,
|
||||
StripVolatileFields = true,
|
||||
NormalizeKeys = true
|
||||
};
|
||||
|
||||
@@ -266,6 +267,13 @@ public sealed record NormalizationOptions
|
||||
/// </summary>
|
||||
public bool StripTimestamps { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Strip SBOM-specific volatile fields that vary between generation runs
|
||||
/// (e.g., serialNumber, metadata.tools, creationInfo.creators).
|
||||
/// See docs/contracts/sbom-volatile-fields.json for the authoritative field list.
|
||||
/// </summary>
|
||||
public bool StripVolatileFields { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalize JSON keys to camelCase.
|
||||
/// </summary>
|
||||
|
||||
@@ -233,6 +233,7 @@ public sealed class SbomNormalizer
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes CycloneDX metadata.
|
||||
/// Strips volatile fields: timestamp, tools (per docs/contracts/sbom-volatile-fields.json).
|
||||
/// </summary>
|
||||
private JsonNode NormalizeCycloneDxMetadata(JsonNode node)
|
||||
{
|
||||
@@ -245,7 +246,12 @@ public sealed class SbomNormalizer
|
||||
|
||||
var sortedKeys = obj
|
||||
.Select(kv => kv.Key)
|
||||
.Where(key => _options.StripTimestamps ? key != "timestamp" : true)
|
||||
.Where(key =>
|
||||
{
|
||||
if (_options.StripTimestamps && key == "timestamp") return false;
|
||||
if (_options.StripVolatileFields && key is "tools" or "authors") return false;
|
||||
return true;
|
||||
})
|
||||
.OrderBy(k => k, StringComparer.Ordinal);
|
||||
|
||||
foreach (var key in sortedKeys)
|
||||
@@ -386,6 +392,7 @@ public sealed class SbomNormalizer
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes SPDX creation info.
|
||||
/// Strips volatile fields: created, creators, licenseListVersion (per docs/contracts/sbom-volatile-fields.json).
|
||||
/// </summary>
|
||||
private JsonNode NormalizeSpdxCreationInfo(JsonNode node)
|
||||
{
|
||||
@@ -398,7 +405,12 @@ public sealed class SbomNormalizer
|
||||
|
||||
var sortedKeys = obj
|
||||
.Select(kv => kv.Key)
|
||||
.Where(key => _options.StripTimestamps ? key != "created" : true)
|
||||
.Where(key =>
|
||||
{
|
||||
if (_options.StripTimestamps && key == "created") return false;
|
||||
if (_options.StripVolatileFields && key is "creators" or "licenseListVersion") return false;
|
||||
return true;
|
||||
})
|
||||
.OrderBy(k => k, StringComparer.Ordinal);
|
||||
|
||||
foreach (var key in sortedKeys)
|
||||
@@ -442,14 +454,23 @@ public sealed class SbomNormalizer
|
||||
return obj.ToJsonString();
|
||||
}
|
||||
|
||||
private static bool ShouldStripCycloneDxField(string key)
|
||||
private bool ShouldStripCycloneDxField(string key)
|
||||
{
|
||||
// Fields that should be stripped for canonical form
|
||||
return key == "$schema";
|
||||
// Always strip $schema (non-content metadata)
|
||||
if (key == "$schema") return true;
|
||||
|
||||
if (!_options.StripVolatileFields) return false;
|
||||
|
||||
// Volatile fields per docs/contracts/sbom-volatile-fields.json
|
||||
return key is "serialNumber";
|
||||
}
|
||||
|
||||
private static bool ShouldStripSpdxField(string key)
|
||||
private bool ShouldStripSpdxField(string key)
|
||||
{
|
||||
if (!_options.StripVolatileFields) return false;
|
||||
|
||||
// No root-level SPDX fields are stripped; volatile fields live
|
||||
// inside creationInfo and are handled by NormalizeSpdxCreationInfo.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user