feat: Implement IsolatedReplayContext for deterministic audit replay

- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls.
- Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation.
- Created supporting interfaces and options for context configuration.

feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison

- Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison.
- Implemented detailed drift detection and error handling during replay execution.
- Added interfaces for policy evaluation and replay execution options.

feat: Add ScanSnapshotFetcher for fetching scan data and snapshots

- Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation.
- Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements.
- Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
This commit is contained in:
StellaOps Bot
2025-12-23 07:46:34 +02:00
parent e47627cfff
commit 7e384ab610
77 changed files with 153346 additions and 209 deletions

View File

@@ -0,0 +1,292 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_4100_0004_0001 - Security State Delta & Verdict
// Task: T6 - Add Delta API endpoints
using System.ComponentModel.DataAnnotations;
using StellaOps.Policy.Deltas;
namespace StellaOps.Policy.Gateway.Contracts;
/// <summary>
/// Request to compute a security state delta.
/// </summary>
public sealed record ComputeDeltaRequest
{
/// <summary>
/// Artifact digest (required).
/// </summary>
[Required]
public required string ArtifactDigest { get; init; }
/// <summary>
/// Artifact name (optional).
/// </summary>
public string? ArtifactName { get; init; }
/// <summary>
/// Artifact tag (optional).
/// </summary>
public string? ArtifactTag { get; init; }
/// <summary>
/// Target snapshot ID (required).
/// </summary>
[Required]
public required string TargetSnapshotId { get; init; }
/// <summary>
/// Explicit baseline snapshot ID (optional).
/// If not provided, baseline selection strategy is used.
/// </summary>
public string? BaselineSnapshotId { get; init; }
/// <summary>
/// Baseline selection strategy (optional, defaults to LastApproved).
/// Values: PreviousBuild, LastApproved, ProductionDeployed, BranchBase
/// </summary>
public string? BaselineStrategy { get; init; }
}
/// <summary>
/// Response from computing a security state delta.
/// </summary>
public sealed record ComputeDeltaResponse
{
/// <summary>
/// The computed delta ID.
/// </summary>
public required string DeltaId { get; init; }
/// <summary>
/// Baseline snapshot ID used.
/// </summary>
public required string BaselineSnapshotId { get; init; }
/// <summary>
/// Target snapshot ID.
/// </summary>
public required string TargetSnapshotId { get; init; }
/// <summary>
/// When the delta was computed.
/// </summary>
public required DateTimeOffset ComputedAt { get; init; }
/// <summary>
/// Summary statistics.
/// </summary>
public required DeltaSummaryDto Summary { get; init; }
/// <summary>
/// Number of drivers identified.
/// </summary>
public int DriverCount { get; init; }
}
/// <summary>
/// Summary statistics DTO.
/// </summary>
public sealed record DeltaSummaryDto
{
public int TotalChanges { get; init; }
public int RiskIncreasing { get; init; }
public int RiskDecreasing { get; init; }
public int Neutral { get; init; }
public decimal RiskScore { get; init; }
public required string RiskDirection { get; init; }
public static DeltaSummaryDto FromModel(DeltaSummary summary) => new()
{
TotalChanges = summary.TotalChanges,
RiskIncreasing = summary.RiskIncreasing,
RiskDecreasing = summary.RiskDecreasing,
Neutral = summary.Neutral,
RiskScore = summary.RiskScore,
RiskDirection = summary.RiskDirection
};
}
/// <summary>
/// Full delta response DTO.
/// </summary>
public sealed record DeltaResponse
{
public required string DeltaId { get; init; }
public required DateTimeOffset ComputedAt { get; init; }
public required string BaselineSnapshotId { get; init; }
public required string TargetSnapshotId { get; init; }
public required ArtifactRefDto Artifact { get; init; }
public required SbomDeltaDto Sbom { get; init; }
public required ReachabilityDeltaDto Reachability { get; init; }
public required VexDeltaDto Vex { get; init; }
public required PolicyDeltaDto Policy { get; init; }
public required UnknownsDeltaDto Unknowns { get; init; }
public required IReadOnlyList<DeltaDriverDto> Drivers { get; init; }
public required DeltaSummaryDto Summary { get; init; }
public static DeltaResponse FromModel(SecurityStateDelta delta) => new()
{
DeltaId = delta.DeltaId,
ComputedAt = delta.ComputedAt,
BaselineSnapshotId = delta.BaselineSnapshotId,
TargetSnapshotId = delta.TargetSnapshotId,
Artifact = ArtifactRefDto.FromModel(delta.Artifact),
Sbom = SbomDeltaDto.FromModel(delta.Sbom),
Reachability = ReachabilityDeltaDto.FromModel(delta.Reachability),
Vex = VexDeltaDto.FromModel(delta.Vex),
Policy = PolicyDeltaDto.FromModel(delta.Policy),
Unknowns = UnknownsDeltaDto.FromModel(delta.Unknowns),
Drivers = delta.Drivers.Select(DeltaDriverDto.FromModel).ToList(),
Summary = DeltaSummaryDto.FromModel(delta.Summary)
};
}
public sealed record ArtifactRefDto
{
public required string Digest { get; init; }
public string? Name { get; init; }
public string? Tag { get; init; }
public static ArtifactRefDto FromModel(ArtifactRef artifact) => new()
{
Digest = artifact.Digest,
Name = artifact.Name,
Tag = artifact.Tag
};
}
public sealed record SbomDeltaDto
{
public int PackagesAdded { get; init; }
public int PackagesRemoved { get; init; }
public int PackagesModified { get; init; }
public static SbomDeltaDto FromModel(SbomDelta sbom) => new()
{
PackagesAdded = sbom.PackagesAdded,
PackagesRemoved = sbom.PackagesRemoved,
PackagesModified = sbom.PackagesModified
};
}
public sealed record ReachabilityDeltaDto
{
public int NewReachable { get; init; }
public int NewUnreachable { get; init; }
public int ChangedReachability { get; init; }
public static ReachabilityDeltaDto FromModel(ReachabilityDelta reach) => new()
{
NewReachable = reach.NewReachable,
NewUnreachable = reach.NewUnreachable,
ChangedReachability = reach.ChangedReachability
};
}
public sealed record VexDeltaDto
{
public int NewVexStatements { get; init; }
public int RevokedVexStatements { get; init; }
public int CoverageIncrease { get; init; }
public int CoverageDecrease { get; init; }
public static VexDeltaDto FromModel(VexDelta vex) => new()
{
NewVexStatements = vex.NewVexStatements,
RevokedVexStatements = vex.RevokedVexStatements,
CoverageIncrease = vex.CoverageIncrease,
CoverageDecrease = vex.CoverageDecrease
};
}
public sealed record PolicyDeltaDto
{
public int NewViolations { get; init; }
public int ResolvedViolations { get; init; }
public int PolicyVersionChanged { get; init; }
public static PolicyDeltaDto FromModel(PolicyDelta policy) => new()
{
NewViolations = policy.NewViolations,
ResolvedViolations = policy.ResolvedViolations,
PolicyVersionChanged = policy.PolicyVersionChanged
};
}
public sealed record UnknownsDeltaDto
{
public int NewUnknowns { get; init; }
public int ResolvedUnknowns { get; init; }
public int TotalBaselineUnknowns { get; init; }
public int TotalTargetUnknowns { get; init; }
public static UnknownsDeltaDto FromModel(UnknownsDelta unknowns) => new()
{
NewUnknowns = unknowns.NewUnknowns,
ResolvedUnknowns = unknowns.ResolvedUnknowns,
TotalBaselineUnknowns = unknowns.TotalBaselineUnknowns,
TotalTargetUnknowns = unknowns.TotalTargetUnknowns
};
}
public sealed record DeltaDriverDto
{
public required string Type { get; init; }
public required string Severity { get; init; }
public required string Description { get; init; }
public string? CveId { get; init; }
public string? Purl { get; init; }
public static DeltaDriverDto FromModel(DeltaDriver driver) => new()
{
Type = driver.Type,
Severity = driver.Severity.ToString().ToLowerInvariant(),
Description = driver.Description,
CveId = driver.CveId,
Purl = driver.Purl
};
}
/// <summary>
/// Request to evaluate a delta verdict.
/// </summary>
public sealed record EvaluateDeltaRequest
{
/// <summary>
/// Exception IDs to apply.
/// </summary>
public IReadOnlyList<string>? Exceptions { get; init; }
}
/// <summary>
/// Delta verdict response DTO.
/// </summary>
public sealed record DeltaVerdictResponse
{
public required string VerdictId { get; init; }
public required string DeltaId { get; init; }
public required DateTimeOffset EvaluatedAt { get; init; }
public required string Status { get; init; }
public required string RecommendedGate { get; init; }
public int RiskPoints { get; init; }
public required IReadOnlyList<DeltaDriverDto> BlockingDrivers { get; init; }
public required IReadOnlyList<DeltaDriverDto> WarningDrivers { get; init; }
public required IReadOnlyList<string> AppliedExceptions { get; init; }
public string? Explanation { get; init; }
public required IReadOnlyList<string> Recommendations { get; init; }
public static DeltaVerdictResponse FromModel(DeltaVerdict verdict) => new()
{
VerdictId = verdict.VerdictId,
DeltaId = verdict.DeltaId,
EvaluatedAt = verdict.EvaluatedAt,
Status = verdict.Status.ToString().ToLowerInvariant(),
RecommendedGate = verdict.RecommendedGate.ToString(),
RiskPoints = verdict.RiskPoints,
BlockingDrivers = verdict.BlockingDrivers.Select(DeltaDriverDto.FromModel).ToList(),
WarningDrivers = verdict.WarningDrivers.Select(DeltaDriverDto.FromModel).ToList(),
AppliedExceptions = verdict.AppliedExceptions.ToList(),
Explanation = verdict.Explanation,
Recommendations = verdict.Recommendations.ToList()
};
}

View File

@@ -0,0 +1,373 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_4100_0004_0001 - Security State Delta & Verdict
// Task: T6 - Add Delta API endpoints
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Policy.Deltas;
using StellaOps.Policy.Gateway.Contracts;
namespace StellaOps.Policy.Gateway.Endpoints;
/// <summary>
/// Delta API endpoints for Policy Gateway.
/// </summary>
public static class DeltasEndpoints
{
private const string DeltaCachePrefix = "delta:";
private static readonly TimeSpan DeltaCacheDuration = TimeSpan.FromMinutes(30);
/// <summary>
/// Maps delta endpoints to the application.
/// </summary>
public static void MapDeltasEndpoints(this WebApplication app)
{
var deltas = app.MapGroup("/api/policy/deltas")
.WithTags("Deltas");
// POST /api/policy/deltas/compute - Compute a security state delta
deltas.MapPost("/compute", async Task<IResult>(
ComputeDeltaRequest request,
IDeltaComputer deltaComputer,
IBaselineSelector baselineSelector,
IMemoryCache cache,
ILogger<DeltaComputer> logger,
CancellationToken cancellationToken) =>
{
if (request is null)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Request body required",
Status = 400
});
}
if (string.IsNullOrWhiteSpace(request.ArtifactDigest))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Artifact digest required",
Status = 400
});
}
if (string.IsNullOrWhiteSpace(request.TargetSnapshotId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Target snapshot ID required",
Status = 400
});
}
try
{
// Select baseline
BaselineSelectionResult baselineResult;
if (!string.IsNullOrWhiteSpace(request.BaselineSnapshotId))
{
baselineResult = await baselineSelector.SelectExplicitAsync(
request.BaselineSnapshotId,
cancellationToken);
}
else
{
var strategy = ParseStrategy(request.BaselineStrategy);
baselineResult = await baselineSelector.SelectBaselineAsync(
request.ArtifactDigest,
strategy,
cancellationToken);
}
if (!baselineResult.IsFound)
{
return Results.NotFound(new ProblemDetails
{
Title = "Baseline not found",
Status = 404,
Detail = baselineResult.Error
});
}
// Compute delta
var delta = await deltaComputer.ComputeDeltaAsync(
baselineResult.Snapshot!.SnapshotId,
request.TargetSnapshotId,
new ArtifactRef(
request.ArtifactDigest,
request.ArtifactName,
request.ArtifactTag),
cancellationToken);
// Cache the delta for subsequent retrieval
cache.Set(
DeltaCachePrefix + delta.DeltaId,
delta,
DeltaCacheDuration);
logger.LogInformation(
"Computed delta {DeltaId} between {Baseline} and {Target}",
delta.DeltaId, delta.BaselineSnapshotId, delta.TargetSnapshotId);
return Results.Ok(new ComputeDeltaResponse
{
DeltaId = delta.DeltaId,
BaselineSnapshotId = delta.BaselineSnapshotId,
TargetSnapshotId = delta.TargetSnapshotId,
ComputedAt = delta.ComputedAt,
Summary = DeltaSummaryDto.FromModel(delta.Summary),
DriverCount = delta.Drivers.Count
});
}
catch (InvalidOperationException ex) when (ex.Message.Contains("not found"))
{
return Results.NotFound(new ProblemDetails
{
Title = "Snapshot not found",
Status = 404,
Detail = ex.Message
});
}
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
// GET /api/policy/deltas/{deltaId} - Get a delta by ID
deltas.MapGet("/{deltaId}", async Task<IResult>(
string deltaId,
IMemoryCache cache,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(deltaId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Delta ID required",
Status = 400
});
}
// Try to retrieve from cache
if (!cache.TryGetValue(DeltaCachePrefix + deltaId, out SecurityStateDelta? delta) || delta is null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Delta not found",
Status = 404,
Detail = $"No delta found with ID: {deltaId}. Deltas are cached for {DeltaCacheDuration.TotalMinutes} minutes after computation."
});
}
return Results.Ok(DeltaResponse.FromModel(delta));
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
// POST /api/policy/deltas/{deltaId}/evaluate - Evaluate delta and get verdict
deltas.MapPost("/{deltaId}/evaluate", async Task<IResult>(
string deltaId,
EvaluateDeltaRequest? request,
IMemoryCache cache,
ILogger<DeltaComputer> logger,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(deltaId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Delta ID required",
Status = 400
});
}
// Try to retrieve delta from cache
if (!cache.TryGetValue(DeltaCachePrefix + deltaId, out SecurityStateDelta? delta) || delta is null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Delta not found",
Status = 404,
Detail = $"No delta found with ID: {deltaId}"
});
}
// Build verdict from delta drivers
var builder = new DeltaVerdictBuilder();
// Apply risk points based on summary
builder.WithRiskPoints((int)delta.Summary.RiskScore);
// Categorize drivers as blocking or warning
foreach (var driver in delta.Drivers)
{
if (IsBlockingDriver(driver))
{
builder.AddBlockingDriver(driver);
}
else if (driver.Severity >= DeltaDriverSeverity.Medium)
{
builder.AddWarningDriver(driver);
}
}
// Apply exceptions if provided
if (request?.Exceptions is not null)
{
foreach (var exceptionId in request.Exceptions)
{
builder.AddException(exceptionId);
}
}
// Add recommendations based on drivers
AddRecommendations(builder, delta.Drivers);
var verdict = builder.Build(deltaId);
// Cache the verdict
cache.Set(
DeltaCachePrefix + deltaId + ":verdict",
verdict,
DeltaCacheDuration);
logger.LogInformation(
"Evaluated delta {DeltaId}: status={Status}, gate={Gate}",
deltaId, verdict.Status, verdict.RecommendedGate);
return Results.Ok(DeltaVerdictResponse.FromModel(verdict));
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
// GET /api/policy/deltas/{deltaId}/attestation - Get signed attestation
deltas.MapGet("/{deltaId}/attestation", async Task<IResult>(
string deltaId,
IMemoryCache cache,
IDeltaVerdictAttestor? attestor,
ILogger<DeltaComputer> logger,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(deltaId))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Delta ID required",
Status = 400
});
}
// Try to retrieve delta from cache
if (!cache.TryGetValue(DeltaCachePrefix + deltaId, out SecurityStateDelta? delta) || delta is null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Delta not found",
Status = 404,
Detail = $"No delta found with ID: {deltaId}"
});
}
// Try to retrieve verdict from cache
if (!cache.TryGetValue(DeltaCachePrefix + deltaId + ":verdict", out DeltaVerdict? verdict) || verdict is null)
{
return Results.NotFound(new ProblemDetails
{
Title = "Verdict not found",
Status = 404,
Detail = "Delta must be evaluated before attestation can be generated. Call POST /evaluate first."
});
}
if (attestor is null)
{
return Results.Problem(new ProblemDetails
{
Title = "Attestor not configured",
Status = 501,
Detail = "Delta verdict attestation requires a signer to be configured"
});
}
try
{
var envelope = await attestor.AttestAsync(delta, verdict, cancellationToken);
logger.LogInformation(
"Created attestation for delta {DeltaId} verdict {VerdictId}",
deltaId, verdict.VerdictId);
return Results.Ok(envelope);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create attestation for delta {DeltaId}", deltaId);
return Results.Problem(new ProblemDetails
{
Title = "Attestation failed",
Status = 500,
Detail = "Failed to create signed attestation"
});
}
})
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
}
private static BaselineSelectionStrategy ParseStrategy(string? strategy)
{
if (string.IsNullOrWhiteSpace(strategy))
return BaselineSelectionStrategy.LastApproved;
return strategy.ToLowerInvariant() switch
{
"previousbuild" or "previous_build" or "previous-build" => BaselineSelectionStrategy.PreviousBuild,
"lastapproved" or "last_approved" or "last-approved" => BaselineSelectionStrategy.LastApproved,
"productiondeployed" or "production_deployed" or "production-deployed" or "production" => BaselineSelectionStrategy.ProductionDeployed,
"branchbase" or "branch_base" or "branch-base" => BaselineSelectionStrategy.BranchBase,
_ => BaselineSelectionStrategy.LastApproved
};
}
private static bool IsBlockingDriver(DeltaDriver driver)
{
// Block on critical/high severity negative drivers
if (driver.Severity is DeltaDriverSeverity.Critical or DeltaDriverSeverity.High)
{
// These types indicate risk increase
return driver.Type is
"new-reachable-cve" or
"lost-vex-coverage" or
"vex-status-downgrade" or
"new-policy-violation";
}
return false;
}
private static void AddRecommendations(DeltaVerdictBuilder builder, IReadOnlyList<DeltaDriver> drivers)
{
var hasReachableCve = drivers.Any(d => d.Type == "new-reachable-cve");
var hasLostVex = drivers.Any(d => d.Type == "lost-vex-coverage");
var hasNewViolation = drivers.Any(d => d.Type == "new-policy-violation");
var hasNewUnknowns = drivers.Any(d => d.Type == "new-unknowns");
if (hasReachableCve)
{
builder.AddRecommendation("Review new reachable CVEs and apply VEX statements or patches");
}
if (hasLostVex)
{
builder.AddRecommendation("Investigate lost VEX coverage - statements may have expired or been revoked");
}
if (hasNewViolation)
{
builder.AddRecommendation("Address policy violations or request exceptions");
}
if (hasNewUnknowns)
{
builder.AddRecommendation("Investigate new unknown packages - consider adding SBOM metadata");
}
}
}

View File

@@ -19,6 +19,8 @@ using StellaOps.Policy.Gateway.Endpoints;
using StellaOps.Policy.Gateway.Infrastructure;
using StellaOps.Policy.Gateway.Options;
using StellaOps.Policy.Gateway.Services;
using StellaOps.Policy.Deltas;
using StellaOps.Policy.Snapshots;
using StellaOps.Policy.Storage.Postgres;
using Polly;
using Polly.Extensions.Http;
@@ -119,6 +121,12 @@ builder.Services.AddScoped<IApprovalWorkflowService, ApprovalWorkflowService>();
builder.Services.AddSingleton<IExceptionNotificationService, NoOpExceptionNotificationService>();
builder.Services.AddHostedService<ExceptionExpiryWorker>();
// Delta services
builder.Services.AddScoped<IDeltaComputer, DeltaComputer>();
builder.Services.AddScoped<IBaselineSelector, BaselineSelector>();
builder.Services.AddScoped<ISnapshotStore, InMemorySnapshotStore>();
builder.Services.AddScoped<StellaOps.Policy.Deltas.ISnapshotService, DeltaSnapshotServiceAdapter>();
builder.Services.AddStellaOpsResourceServerAuthentication(
builder.Configuration,
configurationSection: $"{PolicyGatewayOptions.SectionName}:ResourceServer");
@@ -486,6 +494,9 @@ cvss.MapGet("/policies", async Task<IResult>(
// Exception management endpoints
app.MapExceptionEndpoints();
// Delta management endpoints
app.MapDeltasEndpoints();
app.Run();
static IAsyncPolicy<HttpResponseMessage> CreateAuthorityRetryPolicy(IServiceProvider provider)

View File

@@ -0,0 +1,67 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_4100_0004_0001 - Security State Delta & Verdict
// Task: T6 - Add Delta API endpoints
using StellaOps.Policy.Deltas;
using StellaOps.Policy.Snapshots;
namespace StellaOps.Policy.Gateway.Services;
/// <summary>
/// Adapter that bridges between the KnowledgeSnapshotManifest-based snapshot store
/// and the SnapshotData interface required by the DeltaComputer.
/// </summary>
public sealed class DeltaSnapshotServiceAdapter : StellaOps.Policy.Deltas.ISnapshotService
{
private readonly ISnapshotStore _snapshotStore;
private readonly ILogger<DeltaSnapshotServiceAdapter> _logger;
public DeltaSnapshotServiceAdapter(
ISnapshotStore snapshotStore,
ILogger<DeltaSnapshotServiceAdapter> logger)
{
_snapshotStore = snapshotStore ?? throw new ArgumentNullException(nameof(snapshotStore));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Gets snapshot data by ID, converting from KnowledgeSnapshotManifest.
/// </summary>
public async Task<SnapshotData?> GetSnapshotAsync(string snapshotId, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(snapshotId))
{
return null;
}
var manifest = await _snapshotStore.GetAsync(snapshotId, ct).ConfigureAwait(false);
if (manifest is null)
{
_logger.LogDebug("Snapshot {SnapshotId} not found in store", snapshotId);
return null;
}
return ConvertToSnapshotData(manifest);
}
private static SnapshotData ConvertToSnapshotData(KnowledgeSnapshotManifest manifest)
{
// Get policy version from manifest sources
var policySource = manifest.Sources.FirstOrDefault(s => s.Type == KnowledgeSourceTypes.Policy);
var policyVersion = policySource?.Digest;
// Note: In a full implementation, we would fetch and parse the bundled content
// from each source to extract packages, reachability, VEX statements, etc.
// For now, we return the manifest metadata only.
return new SnapshotData
{
SnapshotId = manifest.SnapshotId,
Packages = [],
Reachability = [],
VexStatements = [],
PolicyViolations = [],
Unknowns = [],
PolicyVersion = policyVersion
};
}
}

View File

@@ -19,6 +19,7 @@
<ProjectReference Include="../StellaOps.Policy.Scoring/StellaOps.Policy.Scoring.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Policy.Storage.Postgres/StellaOps.Policy.Storage.Postgres.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />