Repair release investigation workspace contracts
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using StellaOps.SbomService.Models;
|
||||
using StellaOps.SbomService.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.SbomService.Tests;
|
||||
|
||||
public sealed class ChangeTraceCompatibilityEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
|
||||
{
|
||||
private const string TenantId = "github.com/acme/change-trace";
|
||||
|
||||
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
|
||||
|
||||
public ChangeTraceCompatibilityEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(_ => { });
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Build_endpoint_returns_deterministic_change_trace_for_uploaded_artifacts()
|
||||
{
|
||||
var client = CreateAuthenticatedClient();
|
||||
var firstUpload = await UploadAsync(client, "1.0.0");
|
||||
var secondUpload = await UploadAsync(client, "1.1.0");
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/change-traces/build", new ChangeTraceBuildRequest
|
||||
{
|
||||
TenantId = TenantId,
|
||||
FromDigest = firstUpload.Digest,
|
||||
ToDigest = secondUpload.Digest,
|
||||
IncludeByteDiff = false,
|
||||
});
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
var payload = await response.Content.ReadFromJsonAsync<ChangeTraceDocument>();
|
||||
|
||||
payload.Should().NotBeNull();
|
||||
payload!.TraceId.Should().NotBeNullOrWhiteSpace();
|
||||
payload.Subject.FromDigest.Should().Be(firstUpload.Digest);
|
||||
payload.Subject.ToDigest.Should().Be(secondUpload.Digest);
|
||||
payload.Deltas.Should().ContainSingle();
|
||||
payload.Deltas[0].ChangeType.Should().BeOneOf("upgraded", "patched");
|
||||
payload.Summary.ChangedPackages.Should().Be(1);
|
||||
payload.Commitment.Should().NotBeNull();
|
||||
payload.Commitment!.Sha256.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Get_endpoint_rehydrates_trace_from_trace_id()
|
||||
{
|
||||
var client = CreateAuthenticatedClient();
|
||||
var firstUpload = await UploadAsync(client, "2.0.0");
|
||||
var secondUpload = await UploadAsync(client, "2.0.1");
|
||||
|
||||
var buildResponse = await client.PostAsJsonAsync("/api/change-traces/build", new ChangeTraceBuildRequest
|
||||
{
|
||||
TenantId = TenantId,
|
||||
FromDigest = firstUpload.Digest,
|
||||
ToDigest = secondUpload.Digest,
|
||||
IncludeByteDiff = false,
|
||||
});
|
||||
buildResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var builtTrace = await buildResponse.Content.ReadFromJsonAsync<ChangeTraceDocument>();
|
||||
builtTrace.Should().NotBeNull();
|
||||
|
||||
var getResponse = await client.GetAsync($"/api/change-traces/{Uri.EscapeDataString(builtTrace!.TraceId)}");
|
||||
getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var rehydratedTrace = await getResponse.Content.ReadFromJsonAsync<ChangeTraceDocument>();
|
||||
rehydratedTrace.Should().NotBeNull();
|
||||
rehydratedTrace!.TraceId.Should().Be(builtTrace.TraceId);
|
||||
rehydratedTrace.Subject.FromDigest.Should().Be(firstUpload.Digest);
|
||||
rehydratedTrace.Subject.ToDigest.Should().Be(secondUpload.Digest);
|
||||
rehydratedTrace.Summary.ChangedPackages.Should().Be(1);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task Get_endpoint_rejects_invalid_trace_id()
|
||||
{
|
||||
var client = CreateAuthenticatedClient();
|
||||
|
||||
var response = await client.GetAsync("/api/change-traces/not-a-valid-trace-id");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
payload.GetProperty("error").GetString().Should().Be("invalid traceId");
|
||||
}
|
||||
|
||||
private async Task<SbomUploadResponse> UploadAsync(HttpClient client, string version)
|
||||
{
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/sbom/upload",
|
||||
new SbomUploadRequest
|
||||
{
|
||||
ArtifactRef = "acme/change-trace:demo",
|
||||
Sbom = JsonDocument.Parse($$"""
|
||||
{
|
||||
"spdxVersion": "SPDX-2.3",
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "sample",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-lodash",
|
||||
"name": "lodash",
|
||||
"versionInfo": "{{version}}",
|
||||
"licenseDeclared": "MIT",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceType": "purl",
|
||||
"referenceLocator": "pkg:npm/lodash@{{version}}",
|
||||
"referenceCategory": "PACKAGE-MANAGER"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""").RootElement.Clone(),
|
||||
Source = new SbomUploadSource
|
||||
{
|
||||
Tool = "syft",
|
||||
Version = "1.0.0",
|
||||
CiContext = new SbomUploadCiContext
|
||||
{
|
||||
BuildId = $"build-{version}",
|
||||
Repository = TenantId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var payload = await response.Content.ReadFromJsonAsync<SbomUploadResponse>();
|
||||
payload.Should().NotBeNull();
|
||||
return payload!;
|
||||
}
|
||||
|
||||
private HttpClient CreateAuthenticatedClient()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Tenant-Id", TenantId);
|
||||
client.DefaultRequestHeaders.Add("X-User-Id", "change-trace-test");
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -1079,6 +1079,103 @@ app.MapGet("/api/v1/lineage/compare", async Task<IResult> (
|
||||
.RequireAuthorization(SbomPolicies.Read)
|
||||
.RequireTenant();
|
||||
|
||||
app.MapPost("/api/change-traces/build", async Task<IResult> (
|
||||
[FromBody] ChangeTraceBuildRequest request,
|
||||
[FromServices] ILineageCompareService compareService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = request.TenantId?.Trim();
|
||||
var fromDigest = request.FromDigest?.Trim();
|
||||
var toDigest = request.ToDigest?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fromDigest))
|
||||
{
|
||||
fromDigest = request.FromScanId?.Trim();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(toDigest))
|
||||
{
|
||||
toDigest = request.ToScanId?.Trim();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenantId is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fromDigest) || string.IsNullOrWhiteSpace(toDigest))
|
||||
{
|
||||
return Results.BadRequest(new { error = "fromDigest and toDigest are required" });
|
||||
}
|
||||
|
||||
var compareResult = await compareService.CompareAsync(
|
||||
fromDigest,
|
||||
toDigest,
|
||||
tenantId,
|
||||
new LineageCompareOptions
|
||||
{
|
||||
IncludeSbomDiff = true,
|
||||
IncludeVexDeltas = true,
|
||||
IncludeReachabilityDeltas = true,
|
||||
IncludeAttestations = true,
|
||||
IncludeReplayHashes = true,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
if (compareResult is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "comparison data not found for the specified artifacts" });
|
||||
}
|
||||
|
||||
return Results.Ok(ChangeTraceCompatibilityService.BuildDocument(
|
||||
compareResult,
|
||||
tenantId,
|
||||
request.IncludeByteDiff));
|
||||
})
|
||||
.WithName("BuildChangeTrace")
|
||||
.WithDescription("Builds a deterministic change trace document for the supplied tenant and artifact digest pair. Accepts modern fromDigest/toDigest fields and legacy fromScanId/toScanId compatibility fields.")
|
||||
.RequireAuthorization(SbomPolicies.Read)
|
||||
.RequireTenant();
|
||||
|
||||
app.MapGet("/api/change-traces/{traceId}", async Task<IResult> (
|
||||
[FromRoute] string traceId,
|
||||
[FromServices] ILineageCompareService compareService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!ChangeTraceCompatibilityService.TryDecodeTraceId(traceId, out var lookup))
|
||||
{
|
||||
return Results.BadRequest(new { error = "invalid traceId" });
|
||||
}
|
||||
|
||||
var compareResult = await compareService.CompareAsync(
|
||||
lookup.FromDigest,
|
||||
lookup.ToDigest,
|
||||
lookup.TenantId,
|
||||
new LineageCompareOptions
|
||||
{
|
||||
IncludeSbomDiff = true,
|
||||
IncludeVexDeltas = true,
|
||||
IncludeReachabilityDeltas = true,
|
||||
IncludeAttestations = true,
|
||||
IncludeReplayHashes = true,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
if (compareResult is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "comparison data not found for the traceId" });
|
||||
}
|
||||
|
||||
return Results.Ok(ChangeTraceCompatibilityService.BuildDocument(
|
||||
compareResult,
|
||||
lookup.TenantId,
|
||||
lookup.IncludeByteDiff));
|
||||
})
|
||||
.WithName("GetChangeTrace")
|
||||
.WithDescription("Reconstructs a deterministic change trace document from a shareable traceId without requiring persisted trace blobs.")
|
||||
.RequireAuthorization(SbomPolicies.Read)
|
||||
.RequireTenant();
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Replay Verification API (LIN-BE-033)
|
||||
// Sprint: SPRINT_20251228_007_BE_sbom_lineage_graph_ii
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.SbomService.Services;
|
||||
|
||||
internal static class ChangeTraceCompatibilityService
|
||||
{
|
||||
public static string EncodeTraceId(string tenantId, string fromDigest, string toDigest, bool includeByteDiff)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(new ChangeTraceLookup(
|
||||
TenantId: tenantId,
|
||||
FromDigest: fromDigest,
|
||||
ToDigest: toDigest,
|
||||
IncludeByteDiff: includeByteDiff));
|
||||
|
||||
return Base64UrlEncode(Encoding.UTF8.GetBytes(payload));
|
||||
}
|
||||
|
||||
public static bool TryDecodeTraceId(string traceId, out ChangeTraceLookup lookup)
|
||||
{
|
||||
lookup = default!;
|
||||
|
||||
try
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(Base64UrlDecode(traceId));
|
||||
var decoded = JsonSerializer.Deserialize<ChangeTraceLookup>(json);
|
||||
if (decoded is null
|
||||
|| string.IsNullOrWhiteSpace(decoded.TenantId)
|
||||
|| string.IsNullOrWhiteSpace(decoded.FromDigest)
|
||||
|| string.IsNullOrWhiteSpace(decoded.ToDigest))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lookup = decoded;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static ChangeTraceDocument BuildDocument(
|
||||
LineageCompareResponse compare,
|
||||
string tenantId,
|
||||
bool includeByteDiff)
|
||||
{
|
||||
var traceId = EncodeTraceId(tenantId, compare.FromDigest, compare.ToDigest, includeByteDiff);
|
||||
var riskTrend = (compare.Summary.RiskTrend ?? "unchanged").Trim().ToLowerInvariant();
|
||||
var deltas = BuildPackageDeltas(compare, riskTrend);
|
||||
|
||||
var subject = new ChangeTraceSubjectDocument
|
||||
{
|
||||
Type = "artifact.compare",
|
||||
ImageRef = compare.ToArtifact?.Name
|
||||
?? compare.FromArtifact?.Name
|
||||
?? compare.ToDigest,
|
||||
Digest = compare.ToDigest,
|
||||
FromDigest = compare.FromDigest,
|
||||
ToDigest = compare.ToDigest,
|
||||
FromScanId = compare.FromDigest,
|
||||
ToScanId = compare.ToDigest,
|
||||
};
|
||||
|
||||
var basis = new ChangeTraceBasisDocument
|
||||
{
|
||||
AnalyzedAt = compare.ComputedAt,
|
||||
Policies = new[]
|
||||
{
|
||||
"lineage.compare",
|
||||
"release-investigation.workspace",
|
||||
},
|
||||
DiffMethods = includeByteDiff ? new[] { "pkg", "byte" } : new[] { "pkg" },
|
||||
EngineVersion = "sbomservice-lineage-compare",
|
||||
EngineDigest = compare.ReplayHashes?.ToReplayHash ?? compare.ReplayHashes?.FromReplayHash,
|
||||
};
|
||||
|
||||
var summary = new ChangeTraceSummaryDocument
|
||||
{
|
||||
ChangedPackages = deltas.Count,
|
||||
PackagesAdded = deltas.Count(delta => string.Equals(delta.ChangeType, "added", StringComparison.Ordinal)),
|
||||
PackagesRemoved = deltas.Count(delta => string.Equals(delta.ChangeType, "removed", StringComparison.Ordinal)),
|
||||
ChangedSymbols = 0,
|
||||
ChangedBytes = 0,
|
||||
RiskDelta = riskTrend switch
|
||||
{
|
||||
"improved" => -1,
|
||||
"degraded" => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Verdict = riskTrend switch
|
||||
{
|
||||
"improved" => "risk_down",
|
||||
"degraded" => "risk_up",
|
||||
_ => "neutral",
|
||||
},
|
||||
};
|
||||
|
||||
var document = new ChangeTraceDocument
|
||||
{
|
||||
TraceId = traceId,
|
||||
Schema = "stella.change-trace/v1",
|
||||
Subject = subject,
|
||||
Basis = basis,
|
||||
Deltas = deltas,
|
||||
Summary = summary,
|
||||
};
|
||||
|
||||
document.Commitment = new ChangeTraceCommitmentDocument
|
||||
{
|
||||
Algorithm = "sha256",
|
||||
Sha256 = ComputeCommitmentHash(document),
|
||||
};
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ChangeTracePackageDeltaDocument> BuildPackageDeltas(
|
||||
LineageCompareResponse compare,
|
||||
string riskTrend)
|
||||
{
|
||||
var deltas = new List<ChangeTracePackageDeltaDocument>();
|
||||
var sbomDiff = compare.SbomDiff;
|
||||
if (sbomDiff is null)
|
||||
{
|
||||
return deltas;
|
||||
}
|
||||
|
||||
deltas.AddRange(sbomDiff.Added.Select((change, index) =>
|
||||
BuildPackageDelta(change, null, change.Version, "added", riskTrend, change.Vulnerabilities?.Count ?? 0, 0, index)));
|
||||
deltas.AddRange(sbomDiff.Removed.Select((change, index) =>
|
||||
BuildPackageDelta(change, change.Version, null, "removed", riskTrend, 0, change.Vulnerabilities?.Count ?? 0, index)));
|
||||
deltas.AddRange(sbomDiff.Modified.Select((change, index) =>
|
||||
BuildModifiedPackageDelta(change, riskTrend, index)));
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static ChangeTracePackageDeltaDocument BuildPackageDelta(
|
||||
LineageComponentChange change,
|
||||
string? fromVersion,
|
||||
string? toVersion,
|
||||
string changeType,
|
||||
string riskTrend,
|
||||
int introducedVulns,
|
||||
int resolvedVulns,
|
||||
int index)
|
||||
{
|
||||
return new ChangeTracePackageDeltaDocument
|
||||
{
|
||||
Purl = change.Purl,
|
||||
FromVersion = fromVersion,
|
||||
ToVersion = toVersion,
|
||||
ChangeType = changeType,
|
||||
Symbols = Array.Empty<ChangeTraceSymbolDeltaDocument>(),
|
||||
Bytes = Array.Empty<ChangeTraceByteDeltaDocument>(),
|
||||
TrustDelta = BuildTrustDelta(
|
||||
riskTrend,
|
||||
introducedVulns,
|
||||
resolvedVulns,
|
||||
changeType,
|
||||
fromVersion,
|
||||
toVersion,
|
||||
index),
|
||||
};
|
||||
}
|
||||
|
||||
private static ChangeTracePackageDeltaDocument BuildModifiedPackageDelta(
|
||||
LineageComponentModification change,
|
||||
string riskTrend,
|
||||
int index)
|
||||
{
|
||||
var introducedCount = change.IntroducedVulnerabilities?.Count ?? 0;
|
||||
var resolvedCount = change.FixedVulnerabilities?.Count ?? 0;
|
||||
|
||||
return new ChangeTracePackageDeltaDocument
|
||||
{
|
||||
Purl = change.Purl,
|
||||
FromVersion = change.FromVersion,
|
||||
ToVersion = change.ToVersion,
|
||||
ChangeType = ResolveModifiedChangeType(change),
|
||||
Symbols = Array.Empty<ChangeTraceSymbolDeltaDocument>(),
|
||||
Bytes = Array.Empty<ChangeTraceByteDeltaDocument>(),
|
||||
TrustDelta = BuildTrustDelta(
|
||||
riskTrend,
|
||||
introducedCount,
|
||||
resolvedCount,
|
||||
"modified",
|
||||
change.FromVersion,
|
||||
change.ToVersion,
|
||||
index,
|
||||
change.UpgradeType,
|
||||
change.FixedVulnerabilities,
|
||||
change.IntroducedVulnerabilities),
|
||||
};
|
||||
}
|
||||
|
||||
private static ChangeTraceTrustDeltaDocument BuildTrustDelta(
|
||||
string riskTrend,
|
||||
int introducedCount,
|
||||
int resolvedCount,
|
||||
string changeType,
|
||||
string? fromVersion,
|
||||
string? toVersion,
|
||||
int index,
|
||||
string? upgradeType = null,
|
||||
IReadOnlyList<string>? fixedVulnerabilities = null,
|
||||
IReadOnlyList<string>? introducedVulnerabilities = null)
|
||||
{
|
||||
var score = introducedCount - resolvedCount;
|
||||
var afterScore = Math.Clamp(50 + (resolvedCount - introducedCount), 0, 100);
|
||||
var proofSteps = new List<string>
|
||||
{
|
||||
$"delta[{index}] compared {fromVersion ?? "none"} -> {toVersion ?? "none"} ({changeType})",
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(upgradeType))
|
||||
{
|
||||
proofSteps.Add($"version change classified as {upgradeType}");
|
||||
}
|
||||
|
||||
foreach (var vulnerability in fixedVulnerabilities ?? Array.Empty<string>())
|
||||
{
|
||||
proofSteps.Add($"resolved {vulnerability}");
|
||||
}
|
||||
|
||||
foreach (var vulnerability in introducedVulnerabilities ?? Array.Empty<string>())
|
||||
{
|
||||
proofSteps.Add($"introduced {vulnerability}");
|
||||
}
|
||||
|
||||
proofSteps.Add(riskTrend switch
|
||||
{
|
||||
"improved" => "comparison verdict risk_down",
|
||||
"degraded" => "comparison verdict risk_up",
|
||||
_ => "comparison verdict neutral",
|
||||
});
|
||||
|
||||
return new ChangeTraceTrustDeltaDocument
|
||||
{
|
||||
Score = score,
|
||||
BeforeScore = 50,
|
||||
AfterScore = afterScore,
|
||||
ReachabilityImpact = riskTrend switch
|
||||
{
|
||||
"improved" => "reduced",
|
||||
"degraded" => "increased",
|
||||
_ => "unchanged",
|
||||
},
|
||||
ExploitabilityImpact = riskTrend switch
|
||||
{
|
||||
"improved" => "down",
|
||||
"degraded" => "up",
|
||||
_ => "unchanged",
|
||||
},
|
||||
ProofSteps = proofSteps,
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveModifiedChangeType(LineageComponentModification change)
|
||||
{
|
||||
if (string.Equals(change.FromVersion, change.ToVersion, StringComparison.Ordinal))
|
||||
{
|
||||
return "rebuilt";
|
||||
}
|
||||
|
||||
if (string.Equals(change.UpgradeType, "patch", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "patched";
|
||||
}
|
||||
|
||||
if (LooksLikeDowngrade(change.FromVersion, change.ToVersion))
|
||||
{
|
||||
return "downgraded";
|
||||
}
|
||||
|
||||
return "upgraded";
|
||||
}
|
||||
|
||||
private static bool LooksLikeDowngrade(string fromVersion, string toVersion)
|
||||
{
|
||||
var fromParts = fromVersion.Split('.');
|
||||
var toParts = toVersion.Split('.');
|
||||
var length = Math.Max(fromParts.Length, toParts.Length);
|
||||
|
||||
for (var index = 0; index < length; index++)
|
||||
{
|
||||
var fromPart = index < fromParts.Length && int.TryParse(fromParts[index], out var fromValue)
|
||||
? fromValue
|
||||
: 0;
|
||||
var toPart = index < toParts.Length && int.TryParse(toParts[index], out var toValue)
|
||||
? toValue
|
||||
: 0;
|
||||
|
||||
if (toPart < fromPart)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (toPart > fromPart)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeCommitmentHash(ChangeTraceDocument document)
|
||||
{
|
||||
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
document.TraceId,
|
||||
document.Schema,
|
||||
document.Subject,
|
||||
document.Basis,
|
||||
document.Deltas,
|
||||
document.Summary,
|
||||
});
|
||||
|
||||
var hash = SHA256.HashData(payload);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
private static string Base64UrlEncode(byte[] value)
|
||||
{
|
||||
return Convert.ToBase64String(value)
|
||||
.TrimEnd('=')
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
}
|
||||
|
||||
private static byte[] Base64UrlDecode(string value)
|
||||
{
|
||||
var normalized = value
|
||||
.Replace('-', '+')
|
||||
.Replace('_', '/');
|
||||
|
||||
switch (normalized.Length % 4)
|
||||
{
|
||||
case 2:
|
||||
normalized += "==";
|
||||
break;
|
||||
case 3:
|
||||
normalized += "=";
|
||||
break;
|
||||
}
|
||||
|
||||
return Convert.FromBase64String(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceBuildRequest
|
||||
{
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
public string? FromDigest { get; init; }
|
||||
|
||||
public string? ToDigest { get; init; }
|
||||
|
||||
public string? FromScanId { get; init; }
|
||||
|
||||
public string? ToScanId { get; init; }
|
||||
|
||||
public bool IncludeByteDiff { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceLookup(
|
||||
string TenantId,
|
||||
string FromDigest,
|
||||
string ToDigest,
|
||||
bool IncludeByteDiff);
|
||||
|
||||
public sealed record ChangeTraceDocument
|
||||
{
|
||||
public required string TraceId { get; init; }
|
||||
|
||||
public required string Schema { get; init; }
|
||||
|
||||
public required ChangeTraceSubjectDocument Subject { get; init; }
|
||||
|
||||
public required ChangeTraceBasisDocument Basis { get; init; }
|
||||
|
||||
public required IReadOnlyList<ChangeTracePackageDeltaDocument> Deltas { get; init; }
|
||||
|
||||
public required ChangeTraceSummaryDocument Summary { get; init; }
|
||||
|
||||
public ChangeTraceCommitmentDocument? Commitment { get; set; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceSubjectDocument
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
|
||||
public required string ImageRef { get; init; }
|
||||
|
||||
public required string Digest { get; init; }
|
||||
|
||||
public required string FromDigest { get; init; }
|
||||
|
||||
public required string ToDigest { get; init; }
|
||||
|
||||
public string? FromScanId { get; init; }
|
||||
|
||||
public string? ToScanId { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceBasisDocument
|
||||
{
|
||||
public required DateTimeOffset AnalyzedAt { get; init; }
|
||||
|
||||
public required IReadOnlyList<string> Policies { get; init; }
|
||||
|
||||
public required IReadOnlyList<string> DiffMethods { get; init; }
|
||||
|
||||
public required string EngineVersion { get; init; }
|
||||
|
||||
public string? EngineDigest { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTracePackageDeltaDocument
|
||||
{
|
||||
public required string Purl { get; init; }
|
||||
|
||||
public string? FromVersion { get; init; }
|
||||
|
||||
public string? ToVersion { get; init; }
|
||||
|
||||
public required string ChangeType { get; init; }
|
||||
|
||||
public required IReadOnlyList<ChangeTraceSymbolDeltaDocument> Symbols { get; init; }
|
||||
|
||||
public required IReadOnlyList<ChangeTraceByteDeltaDocument> Bytes { get; init; }
|
||||
|
||||
public required ChangeTraceTrustDeltaDocument TrustDelta { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceSymbolDeltaDocument
|
||||
{
|
||||
public required string SymbolName { get; init; }
|
||||
|
||||
public required string ChangeType { get; init; }
|
||||
|
||||
public int SizeDelta { get; init; }
|
||||
|
||||
public string? FromHash { get; init; }
|
||||
|
||||
public string? ToHash { get; init; }
|
||||
|
||||
public double Confidence { get; init; }
|
||||
|
||||
public string? MatchMethod { get; init; }
|
||||
|
||||
public string? Explanation { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceByteDeltaDocument
|
||||
{
|
||||
public int Offset { get; init; }
|
||||
|
||||
public int Size { get; init; }
|
||||
|
||||
public required string FromHash { get; init; }
|
||||
|
||||
public required string ToHash { get; init; }
|
||||
|
||||
public string? Section { get; init; }
|
||||
|
||||
public string? Context { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceTrustDeltaDocument
|
||||
{
|
||||
public int Score { get; init; }
|
||||
|
||||
public int BeforeScore { get; init; }
|
||||
|
||||
public int AfterScore { get; init; }
|
||||
|
||||
public required string ReachabilityImpact { get; init; }
|
||||
|
||||
public required string ExploitabilityImpact { get; init; }
|
||||
|
||||
public required IReadOnlyList<string> ProofSteps { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceSummaryDocument
|
||||
{
|
||||
public int ChangedPackages { get; init; }
|
||||
|
||||
public int PackagesAdded { get; init; }
|
||||
|
||||
public int PackagesRemoved { get; init; }
|
||||
|
||||
public int ChangedSymbols { get; init; }
|
||||
|
||||
public int ChangedBytes { get; init; }
|
||||
|
||||
public int RiskDelta { get; init; }
|
||||
|
||||
public required string Verdict { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ChangeTraceCommitmentDocument
|
||||
{
|
||||
public required string Sha256 { get; init; }
|
||||
|
||||
public required string Algorithm { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user