- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips. - Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling. - Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation. - Updated project references to include the new Reachability Drift library.
205 lines
7.1 KiB
C#
205 lines
7.1 KiB
C#
// -----------------------------------------------------------------------------
|
|
// PolicyDecisionAttestationService.cs
|
|
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
|
|
// Description: Implementation of policy decision attestation service.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Scanner.WebService.Contracts;
|
|
using StellaOps.Scanner.WebService.Domain;
|
|
|
|
namespace StellaOps.Scanner.WebService.Services;
|
|
|
|
/// <summary>
|
|
/// Implementation of the policy decision attestation service.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Creates in-toto statements for policy decisions. The actual DSSE signing
|
|
/// is deferred to the Attestor module when available.
|
|
/// </remarks>
|
|
public sealed class PolicyDecisionAttestationService : IPolicyDecisionAttestationService
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
|
WriteIndented = false
|
|
};
|
|
|
|
private readonly ILogger<PolicyDecisionAttestationService> _logger;
|
|
private readonly TimeProvider _timeProvider;
|
|
private readonly PolicyDecisionAttestationOptions _options;
|
|
|
|
// In-memory store for attestations (production would use persistent storage)
|
|
private readonly ConcurrentDictionary<string, PolicyDecisionAttestationResult> _attestations = new();
|
|
|
|
public PolicyDecisionAttestationService(
|
|
ILogger<PolicyDecisionAttestationService> logger,
|
|
IOptions<PolicyDecisionAttestationOptions>? options = null,
|
|
TimeProvider? timeProvider = null)
|
|
{
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
|
_options = options?.Value ?? new PolicyDecisionAttestationOptions();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<PolicyDecisionAttestationResult> CreateAttestationAsync(
|
|
PolicyDecisionInput input,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(input);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(input.FindingId);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(input.Cve);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(input.ComponentPurl);
|
|
|
|
try
|
|
{
|
|
var now = _timeProvider.GetUtcNow();
|
|
var ttl = input.DecisionTtl ?? TimeSpan.FromDays(_options.DefaultDecisionTtlDays);
|
|
var expiresAt = now.Add(ttl);
|
|
|
|
// Build the statement
|
|
var statement = BuildStatement(input, now, expiresAt);
|
|
|
|
// Compute content-addressed ID
|
|
var attestationId = ComputeAttestationId(statement);
|
|
|
|
// Store the attestation
|
|
var key = BuildKey(input.ScanId, input.FindingId);
|
|
var result = PolicyDecisionAttestationResult.Succeeded(
|
|
statement,
|
|
attestationId,
|
|
dsseEnvelope: null // Signing deferred to Attestor module
|
|
);
|
|
|
|
_attestations[key] = result;
|
|
|
|
_logger.LogInformation(
|
|
"Created policy decision attestation for {FindingId}: {Decision} (score={Score}, attestation={AttestationId})",
|
|
input.FindingId,
|
|
input.Decision,
|
|
input.Reasoning.FinalScore,
|
|
attestationId);
|
|
|
|
return Task.FromResult(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to create policy decision attestation for {FindingId}", input.FindingId);
|
|
return Task.FromResult(PolicyDecisionAttestationResult.Failed(ex.Message));
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<PolicyDecisionAttestationResult?> GetAttestationAsync(
|
|
ScanId scanId,
|
|
string findingId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var key = BuildKey(scanId, findingId);
|
|
if (_attestations.TryGetValue(key, out var result))
|
|
{
|
|
return Task.FromResult<PolicyDecisionAttestationResult?>(result);
|
|
}
|
|
|
|
return Task.FromResult<PolicyDecisionAttestationResult?>(null);
|
|
}
|
|
|
|
private PolicyDecisionStatement BuildStatement(
|
|
PolicyDecisionInput input,
|
|
DateTimeOffset evaluatedAt,
|
|
DateTimeOffset expiresAt)
|
|
{
|
|
// Build subjects - the scan and finding are the subjects of this attestation
|
|
var subjects = new List<PolicyDecisionSubject>
|
|
{
|
|
new()
|
|
{
|
|
Name = $"scan:{input.ScanId.Value}",
|
|
Digest = new Dictionary<string, string>
|
|
{
|
|
["sha256"] = ComputeSha256(input.ScanId.Value)
|
|
}
|
|
},
|
|
new()
|
|
{
|
|
Name = $"finding:{input.FindingId}",
|
|
Digest = new Dictionary<string, string>
|
|
{
|
|
["sha256"] = ComputeSha256(input.FindingId)
|
|
}
|
|
}
|
|
};
|
|
|
|
// Build predicate
|
|
var predicate = new PolicyDecisionPredicate
|
|
{
|
|
FindingId = input.FindingId,
|
|
Cve = input.Cve,
|
|
ComponentPurl = input.ComponentPurl,
|
|
Decision = input.Decision,
|
|
Reasoning = input.Reasoning,
|
|
EvidenceRefs = input.EvidenceRefs,
|
|
EvaluatedAt = evaluatedAt,
|
|
ExpiresAt = expiresAt,
|
|
PolicyVersion = input.PolicyVersion,
|
|
PolicyHash = input.PolicyHash
|
|
};
|
|
|
|
return new PolicyDecisionStatement
|
|
{
|
|
Subject = subjects,
|
|
Predicate = predicate
|
|
};
|
|
}
|
|
|
|
private static string ComputeAttestationId(PolicyDecisionStatement statement)
|
|
{
|
|
var json = JsonSerializer.Serialize(statement, JsonOptions);
|
|
var hash = ComputeSha256(json);
|
|
return $"sha256:{hash}";
|
|
}
|
|
|
|
private static string ComputeSha256(string input)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(input);
|
|
var hashBytes = SHA256.HashData(bytes);
|
|
return Convert.ToHexStringLower(hashBytes);
|
|
}
|
|
|
|
private static string BuildKey(ScanId scanId, string findingId)
|
|
=> $"{scanId.Value}:{findingId}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configuration options for policy decision attestations.
|
|
/// </summary>
|
|
public sealed class PolicyDecisionAttestationOptions
|
|
{
|
|
/// <summary>
|
|
/// Default TTL for policy decisions in days.
|
|
/// </summary>
|
|
public int DefaultDecisionTtlDays { get; set; } = 30;
|
|
|
|
/// <summary>
|
|
/// Whether to enable DSSE signing when Attestor is available.
|
|
/// </summary>
|
|
public bool EnableSigning { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Key profile to use for signing attestations.
|
|
/// </summary>
|
|
public string SigningKeyProfile { get; set; } = "Reasoning";
|
|
}
|