Implement incident mode management service and models
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added IPackRunIncidentModeService interface for managing incident mode activation, deactivation, and status retrieval.
- Created PackRunIncidentModeService class implementing the service interface with methods for activating, deactivating, and escalating incident modes.
- Introduced incident mode status model (PackRunIncidentModeStatus) and related enums for escalation levels and activation sources.
- Developed retention policy, telemetry settings, and debug capture settings models to manage incident mode configurations.
- Implemented SLO breach notification handling to activate incident mode based on severity.
- Added in-memory store (InMemoryPackRunIncidentModeStore) for testing purposes.
- Created comprehensive unit tests for incident mode service, covering activation, deactivation, status retrieval, and SLO breach handling.
This commit is contained in:
StellaOps Bot
2025-12-06 22:33:00 +02:00
parent 4042fc2184
commit 9bd6a73926
23 changed files with 7779 additions and 12 deletions

View File

@@ -0,0 +1,232 @@
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry.Storage;
/// <summary>
/// Storage entity for policy pack.
/// </summary>
public sealed record PolicyPackEntity
{
public required Guid TenantId { get; init; }
public required Guid PackId { get; init; }
public required string Name { get; init; }
public required string Version { get; init; }
public string? Description { get; init; }
public required PolicyPackStatus Status { get; init; }
public IReadOnlyList<PolicyRule>? Rules { get; init; }
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public required DateTimeOffset UpdatedAt { get; init; }
public DateTimeOffset? PublishedAt { get; init; }
public string? Digest { get; init; }
public string? CreatedBy { get; init; }
public string? UpdatedBy { get; init; }
/// <summary>
/// Converts to API contract.
/// </summary>
public PolicyPack ToContract() => new()
{
PackId = PackId,
Name = Name,
Version = Version,
Description = Description,
Status = Status,
Rules = Rules,
Metadata = Metadata,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt,
PublishedAt = PublishedAt,
Digest = Digest
};
}
/// <summary>
/// Storage entity for verification policy.
/// </summary>
public sealed record VerificationPolicyEntity
{
public required Guid TenantId { get; init; }
public required string PolicyId { get; init; }
public required string Version { get; init; }
public string? Description { get; init; }
public required string TenantScope { get; init; }
public required IReadOnlyList<string> PredicateTypes { get; init; }
public required SignerRequirements SignerRequirements { get; init; }
public ValidityWindow? ValidityWindow { get; init; }
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public required DateTimeOffset UpdatedAt { get; init; }
public string? CreatedBy { get; init; }
public string? UpdatedBy { get; init; }
/// <summary>
/// Converts to API contract.
/// </summary>
public VerificationPolicy ToContract() => new()
{
PolicyId = PolicyId,
Version = Version,
Description = Description,
TenantScope = TenantScope,
PredicateTypes = PredicateTypes,
SignerRequirements = SignerRequirements,
ValidityWindow = ValidityWindow,
Metadata = Metadata,
CreatedAt = CreatedAt,
UpdatedAt = UpdatedAt
};
}
/// <summary>
/// Storage entity for snapshot.
/// </summary>
public sealed record SnapshotEntity
{
public required Guid TenantId { get; init; }
public required Guid SnapshotId { get; init; }
public required string Digest { get; init; }
public string? Description { get; init; }
public IReadOnlyList<Guid>? PackIds { get; init; }
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public string? CreatedBy { get; init; }
/// <summary>
/// Converts to API contract.
/// </summary>
public Snapshot ToContract() => new()
{
SnapshotId = SnapshotId,
Digest = Digest,
Description = Description,
PackIds = PackIds,
Metadata = Metadata,
CreatedAt = CreatedAt,
CreatedBy = CreatedBy
};
}
/// <summary>
/// Storage entity for violation.
/// </summary>
public sealed record ViolationEntity
{
public required Guid TenantId { get; init; }
public required Guid ViolationId { get; init; }
public string? PolicyId { get; init; }
public required string RuleId { get; init; }
public required Severity Severity { get; init; }
public required string Message { get; init; }
public string? Purl { get; init; }
public string? CveId { get; init; }
public IReadOnlyDictionary<string, object>? Context { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Converts to API contract.
/// </summary>
public Violation ToContract() => new()
{
ViolationId = ViolationId,
PolicyId = PolicyId,
RuleId = RuleId,
Severity = Severity,
Message = Message,
Purl = Purl,
CveId = CveId,
Context = Context,
CreatedAt = CreatedAt
};
}
/// <summary>
/// Storage entity for override.
/// </summary>
public sealed record OverrideEntity
{
public required Guid TenantId { get; init; }
public required Guid OverrideId { get; init; }
public Guid? ProfileId { get; init; }
public required string RuleId { get; init; }
public required OverrideStatus Status { get; init; }
public string? Reason { get; init; }
public OverrideScope? Scope { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
public string? ApprovedBy { get; init; }
public DateTimeOffset? ApprovedAt { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public string? CreatedBy { get; init; }
/// <summary>
/// Converts to API contract.
/// </summary>
public Override ToContract() => new()
{
OverrideId = OverrideId,
ProfileId = ProfileId,
RuleId = RuleId,
Status = Status,
Reason = Reason,
Scope = Scope,
ExpiresAt = ExpiresAt,
ApprovedBy = ApprovedBy,
ApprovedAt = ApprovedAt,
CreatedAt = CreatedAt,
CreatedBy = CreatedBy
};
}
/// <summary>
/// History entry for policy pack changes.
/// </summary>
public sealed record PolicyPackHistoryEntry
{
public required Guid PackId { get; init; }
public required string Action { get; init; }
public required DateTimeOffset Timestamp { get; init; }
public string? PerformedBy { get; init; }
public PolicyPackStatus? PreviousStatus { get; init; }
public PolicyPackStatus? NewStatus { get; init; }
public string? Description { get; init; }
}
/// <summary>
/// Result for paginated policy pack list.
/// </summary>
public sealed record PolicyPackListResult
{
public required IReadOnlyList<PolicyPackEntity> Items { get; init; }
public string? NextPageToken { get; init; }
public int TotalCount { get; init; }
}
/// <summary>
/// Result for paginated verification policy list.
/// </summary>
public sealed record VerificationPolicyListResult
{
public required IReadOnlyList<VerificationPolicyEntity> Items { get; init; }
public string? NextPageToken { get; init; }
public int TotalCount { get; init; }
}
/// <summary>
/// Result for paginated snapshot list.
/// </summary>
public sealed record SnapshotListResult
{
public required IReadOnlyList<SnapshotEntity> Items { get; init; }
public string? NextPageToken { get; init; }
public int TotalCount { get; init; }
}
/// <summary>
/// Result for paginated violation list.
/// </summary>
public sealed record ViolationListResult
{
public required IReadOnlyList<ViolationEntity> Items { get; init; }
public string? NextPageToken { get; init; }
public int TotalCount { get; init; }
}

View File

@@ -0,0 +1,212 @@
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry.Storage;
/// <summary>
/// Store interface for policy pack workspace operations with history tracking.
/// Implements REGISTRY-API-27-002: Workspace storage with CRUD + history.
/// </summary>
public interface IPolicyPackStore
{
/// <summary>
/// Creates a new policy pack.
/// </summary>
Task<PolicyPackEntity> CreateAsync(
Guid tenantId,
CreatePolicyPackRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets a policy pack by ID.
/// </summary>
Task<PolicyPackEntity?> GetByIdAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets a policy pack by name.
/// </summary>
Task<PolicyPackEntity?> GetByNameAsync(
Guid tenantId,
string name,
CancellationToken cancellationToken = default);
/// <summary>
/// Lists policy packs with optional filtering.
/// </summary>
Task<PolicyPackListResult> ListAsync(
Guid tenantId,
PolicyPackStatus? status = null,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates a policy pack.
/// </summary>
Task<PolicyPackEntity?> UpdateAsync(
Guid tenantId,
Guid packId,
UpdatePolicyPackRequest request,
string? updatedBy = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a policy pack (only drafts can be deleted).
/// </summary>
Task<bool> DeleteAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates the status of a policy pack.
/// </summary>
Task<PolicyPackEntity?> UpdateStatusAsync(
Guid tenantId,
Guid packId,
PolicyPackStatus newStatus,
string? updatedBy = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the history of changes for a policy pack.
/// </summary>
Task<IReadOnlyList<PolicyPackHistoryEntry>> GetHistoryAsync(
Guid tenantId,
Guid packId,
int limit = 50,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Store interface for verification policy operations.
/// </summary>
public interface IVerificationPolicyStore
{
Task<VerificationPolicyEntity> CreateAsync(
Guid tenantId,
CreateVerificationPolicyRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default);
Task<VerificationPolicyEntity?> GetByIdAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default);
Task<VerificationPolicyListResult> ListAsync(
Guid tenantId,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default);
Task<VerificationPolicyEntity?> UpdateAsync(
Guid tenantId,
string policyId,
UpdateVerificationPolicyRequest request,
string? updatedBy = null,
CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Store interface for policy snapshot operations.
/// </summary>
public interface ISnapshotStore
{
Task<SnapshotEntity> CreateAsync(
Guid tenantId,
CreateSnapshotRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default);
Task<SnapshotEntity?> GetByIdAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default);
Task<SnapshotEntity?> GetByDigestAsync(
Guid tenantId,
string digest,
CancellationToken cancellationToken = default);
Task<SnapshotListResult> ListAsync(
Guid tenantId,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Store interface for violation operations.
/// </summary>
public interface IViolationStore
{
Task<ViolationEntity> AppendAsync(
Guid tenantId,
CreateViolationRequest request,
CancellationToken cancellationToken = default);
Task<ViolationBatchResult> AppendBatchAsync(
Guid tenantId,
IReadOnlyList<CreateViolationRequest> requests,
CancellationToken cancellationToken = default);
Task<ViolationEntity?> GetByIdAsync(
Guid tenantId,
Guid violationId,
CancellationToken cancellationToken = default);
Task<ViolationListResult> ListAsync(
Guid tenantId,
Severity? severity = null,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Store interface for override operations.
/// </summary>
public interface IOverrideStore
{
Task<OverrideEntity> CreateAsync(
Guid tenantId,
CreateOverrideRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default);
Task<OverrideEntity?> GetByIdAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default);
Task<OverrideEntity?> ApproveAsync(
Guid tenantId,
Guid overrideId,
string? approvedBy = null,
string? comment = null,
CancellationToken cancellationToken = default);
Task<OverrideEntity?> DisableAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,108 @@
using System.Collections.Concurrent;
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry.Storage;
/// <summary>
/// In-memory implementation of IOverrideStore for testing and development.
/// </summary>
public sealed class InMemoryOverrideStore : IOverrideStore
{
private readonly ConcurrentDictionary<(Guid TenantId, Guid OverrideId), OverrideEntity> _overrides = new();
public Task<OverrideEntity> CreateAsync(
Guid tenantId,
CreateOverrideRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var overrideId = Guid.NewGuid();
var entity = new OverrideEntity
{
TenantId = tenantId,
OverrideId = overrideId,
ProfileId = request.ProfileId,
RuleId = request.RuleId,
Status = OverrideStatus.Pending,
Reason = request.Reason,
Scope = request.Scope,
ExpiresAt = request.ExpiresAt,
CreatedAt = now,
CreatedBy = createdBy
};
_overrides[(tenantId, overrideId)] = entity;
return Task.FromResult(entity);
}
public Task<OverrideEntity?> GetByIdAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
_overrides.TryGetValue((tenantId, overrideId), out var entity);
return Task.FromResult(entity);
}
public Task<bool> DeleteAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
return Task.FromResult(_overrides.TryRemove((tenantId, overrideId), out _));
}
public Task<OverrideEntity?> ApproveAsync(
Guid tenantId,
Guid overrideId,
string? approvedBy = null,
string? comment = null,
CancellationToken cancellationToken = default)
{
if (!_overrides.TryGetValue((tenantId, overrideId), out var existing))
{
return Task.FromResult<OverrideEntity?>(null);
}
// Only pending overrides can be approved
if (existing.Status != OverrideStatus.Pending)
{
return Task.FromResult<OverrideEntity?>(null);
}
var now = DateTimeOffset.UtcNow;
var updated = existing with
{
Status = OverrideStatus.Approved,
ApprovedBy = approvedBy,
ApprovedAt = now
};
_overrides[(tenantId, overrideId)] = updated;
return Task.FromResult<OverrideEntity?>(updated);
}
public Task<OverrideEntity?> DisableAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
if (!_overrides.TryGetValue((tenantId, overrideId), out var existing))
{
return Task.FromResult<OverrideEntity?>(null);
}
var updated = existing with
{
Status = OverrideStatus.Disabled
};
_overrides[(tenantId, overrideId)] = updated;
return Task.FromResult<OverrideEntity?>(updated);
}
}

View File

@@ -0,0 +1,260 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry.Storage;
/// <summary>
/// In-memory implementation of IPolicyPackStore for testing and development.
/// </summary>
public sealed class InMemoryPolicyPackStore : IPolicyPackStore
{
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PolicyPackEntity> _packs = new();
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), List<PolicyPackHistoryEntry>> _history = new();
public Task<PolicyPackEntity> CreateAsync(
Guid tenantId,
CreatePolicyPackRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var packId = Guid.NewGuid();
var entity = new PolicyPackEntity
{
TenantId = tenantId,
PackId = packId,
Name = request.Name,
Version = request.Version,
Description = request.Description,
Status = PolicyPackStatus.Draft,
Rules = request.Rules,
Metadata = request.Metadata,
CreatedAt = now,
UpdatedAt = now,
CreatedBy = createdBy
};
_packs[(tenantId, packId)] = entity;
// Add history entry
AddHistoryEntry(tenantId, packId, "created", createdBy, null, PolicyPackStatus.Draft, "Policy pack created");
return Task.FromResult(entity);
}
public Task<PolicyPackEntity?> GetByIdAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
_packs.TryGetValue((tenantId, packId), out var entity);
return Task.FromResult(entity);
}
public Task<PolicyPackEntity?> GetByNameAsync(
Guid tenantId,
string name,
CancellationToken cancellationToken = default)
{
var entity = _packs.Values
.Where(p => p.TenantId == tenantId && p.Name == name)
.OrderByDescending(p => p.UpdatedAt)
.FirstOrDefault();
return Task.FromResult(entity);
}
public Task<PolicyPackListResult> ListAsync(
Guid tenantId,
PolicyPackStatus? status = null,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default)
{
var query = _packs.Values
.Where(p => p.TenantId == tenantId);
if (status.HasValue)
{
query = query.Where(p => p.Status == status.Value);
}
var items = query
.OrderByDescending(p => p.UpdatedAt)
.ToList();
int skip = 0;
if (!string.IsNullOrEmpty(pageToken) && int.TryParse(pageToken, out var offset))
{
skip = offset;
}
var pagedItems = items.Skip(skip).Take(pageSize).ToList();
string? nextToken = skip + pagedItems.Count < items.Count
? (skip + pagedItems.Count).ToString()
: null;
return Task.FromResult(new PolicyPackListResult
{
Items = pagedItems,
NextPageToken = nextToken,
TotalCount = items.Count
});
}
public Task<PolicyPackEntity?> UpdateAsync(
Guid tenantId,
Guid packId,
UpdatePolicyPackRequest request,
string? updatedBy = null,
CancellationToken cancellationToken = default)
{
if (!_packs.TryGetValue((tenantId, packId), out var existing))
{
return Task.FromResult<PolicyPackEntity?>(null);
}
// Only allow updates to drafts
if (existing.Status != PolicyPackStatus.Draft)
{
return Task.FromResult<PolicyPackEntity?>(null);
}
var updated = existing with
{
Name = request.Name ?? existing.Name,
Description = request.Description ?? existing.Description,
Rules = request.Rules ?? existing.Rules,
Metadata = request.Metadata ?? existing.Metadata,
UpdatedAt = DateTimeOffset.UtcNow,
UpdatedBy = updatedBy
};
_packs[(tenantId, packId)] = updated;
AddHistoryEntry(tenantId, packId, "updated", updatedBy, existing.Status, updated.Status, "Policy pack updated");
return Task.FromResult<PolicyPackEntity?>(updated);
}
public Task<bool> DeleteAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
if (!_packs.TryGetValue((tenantId, packId), out var existing))
{
return Task.FromResult(false);
}
// Only allow deletion of drafts
if (existing.Status != PolicyPackStatus.Draft)
{
return Task.FromResult(false);
}
var removed = _packs.TryRemove((tenantId, packId), out _);
if (removed)
{
_history.TryRemove((tenantId, packId), out _);
}
return Task.FromResult(removed);
}
public Task<PolicyPackEntity?> UpdateStatusAsync(
Guid tenantId,
Guid packId,
PolicyPackStatus newStatus,
string? updatedBy = null,
CancellationToken cancellationToken = default)
{
if (!_packs.TryGetValue((tenantId, packId), out var existing))
{
return Task.FromResult<PolicyPackEntity?>(null);
}
var now = DateTimeOffset.UtcNow;
var updated = existing with
{
Status = newStatus,
UpdatedAt = now,
UpdatedBy = updatedBy,
PublishedAt = newStatus == PolicyPackStatus.Published ? now : existing.PublishedAt,
Digest = newStatus == PolicyPackStatus.Published ? ComputeDigest(existing) : existing.Digest
};
_packs[(tenantId, packId)] = updated;
AddHistoryEntry(tenantId, packId, $"status_changed_to_{newStatus}", updatedBy, existing.Status, newStatus,
$"Status changed from {existing.Status} to {newStatus}");
return Task.FromResult<PolicyPackEntity?>(updated);
}
public Task<IReadOnlyList<PolicyPackHistoryEntry>> GetHistoryAsync(
Guid tenantId,
Guid packId,
int limit = 50,
CancellationToken cancellationToken = default)
{
if (!_history.TryGetValue((tenantId, packId), out var history))
{
return Task.FromResult<IReadOnlyList<PolicyPackHistoryEntry>>(Array.Empty<PolicyPackHistoryEntry>());
}
var entries = history
.OrderByDescending(h => h.Timestamp)
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<PolicyPackHistoryEntry>>(entries);
}
private void AddHistoryEntry(
Guid tenantId,
Guid packId,
string action,
string? performedBy,
PolicyPackStatus? previousStatus,
PolicyPackStatus? newStatus,
string? description)
{
var entry = new PolicyPackHistoryEntry
{
PackId = packId,
Action = action,
Timestamp = DateTimeOffset.UtcNow,
PerformedBy = performedBy,
PreviousStatus = previousStatus,
NewStatus = newStatus,
Description = description
};
_history.AddOrUpdate(
(tenantId, packId),
_ => [entry],
(_, list) =>
{
list.Add(entry);
return list;
});
}
private static string ComputeDigest(PolicyPackEntity pack)
{
var content = JsonSerializer.Serialize(new
{
pack.Name,
pack.Version,
pack.Rules
});
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -0,0 +1,115 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry.Storage;
/// <summary>
/// In-memory implementation of ISnapshotStore for testing and development.
/// </summary>
public sealed class InMemorySnapshotStore : ISnapshotStore
{
private readonly ConcurrentDictionary<(Guid TenantId, Guid SnapshotId), SnapshotEntity> _snapshots = new();
public Task<SnapshotEntity> CreateAsync(
Guid tenantId,
CreateSnapshotRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var snapshotId = Guid.NewGuid();
// Compute digest from pack IDs and timestamp for uniqueness
var digest = ComputeDigest(request.PackIds, now);
var entity = new SnapshotEntity
{
TenantId = tenantId,
SnapshotId = snapshotId,
Digest = digest,
Description = request.Description,
PackIds = request.PackIds,
Metadata = request.Metadata,
CreatedAt = now,
CreatedBy = createdBy
};
_snapshots[(tenantId, snapshotId)] = entity;
return Task.FromResult(entity);
}
public Task<SnapshotEntity?> GetByIdAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default)
{
_snapshots.TryGetValue((tenantId, snapshotId), out var entity);
return Task.FromResult(entity);
}
public Task<SnapshotEntity?> GetByDigestAsync(
Guid tenantId,
string digest,
CancellationToken cancellationToken = default)
{
var entity = _snapshots.Values
.Where(s => s.TenantId == tenantId && s.Digest == digest)
.FirstOrDefault();
return Task.FromResult(entity);
}
public Task<SnapshotListResult> ListAsync(
Guid tenantId,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default)
{
var items = _snapshots.Values
.Where(s => s.TenantId == tenantId)
.OrderByDescending(s => s.CreatedAt)
.ToList();
int skip = 0;
if (!string.IsNullOrEmpty(pageToken) && int.TryParse(pageToken, out var offset))
{
skip = offset;
}
var pagedItems = items.Skip(skip).Take(pageSize).ToList();
string? nextToken = skip + pagedItems.Count < items.Count
? (skip + pagedItems.Count).ToString()
: null;
return Task.FromResult(new SnapshotListResult
{
Items = pagedItems,
NextPageToken = nextToken,
TotalCount = items.Count
});
}
public Task<bool> DeleteAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default)
{
return Task.FromResult(_snapshots.TryRemove((tenantId, snapshotId), out _));
}
private static string ComputeDigest(IReadOnlyList<Guid> packIds, DateTimeOffset timestamp)
{
var content = JsonSerializer.Serialize(new
{
PackIds = packIds.OrderBy(id => id).ToList(),
Timestamp = timestamp.ToUnixTimeMilliseconds()
});
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -0,0 +1,121 @@
using System.Collections.Concurrent;
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry.Storage;
/// <summary>
/// In-memory implementation of IVerificationPolicyStore for testing and development.
/// </summary>
public sealed class InMemoryVerificationPolicyStore : IVerificationPolicyStore
{
private readonly ConcurrentDictionary<(Guid TenantId, string PolicyId), VerificationPolicyEntity> _policies = new();
public Task<VerificationPolicyEntity> CreateAsync(
Guid tenantId,
CreateVerificationPolicyRequest request,
string? createdBy = null,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var entity = new VerificationPolicyEntity
{
TenantId = tenantId,
PolicyId = request.PolicyId,
Version = request.Version,
Description = request.Description,
TenantScope = request.TenantScope ?? tenantId.ToString(),
PredicateTypes = request.PredicateTypes,
SignerRequirements = request.SignerRequirements ?? new SignerRequirements
{
MinimumSignatures = 1,
TrustedKeyFingerprints = Array.Empty<string>()
},
ValidityWindow = request.ValidityWindow,
Metadata = request.Metadata,
CreatedAt = now,
UpdatedAt = now,
CreatedBy = createdBy
};
_policies[(tenantId, request.PolicyId)] = entity;
return Task.FromResult(entity);
}
public Task<VerificationPolicyEntity?> GetByIdAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default)
{
_policies.TryGetValue((tenantId, policyId), out var entity);
return Task.FromResult(entity);
}
public Task<VerificationPolicyListResult> ListAsync(
Guid tenantId,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default)
{
var items = _policies.Values
.Where(p => p.TenantId == tenantId)
.OrderByDescending(p => p.UpdatedAt)
.ToList();
int skip = 0;
if (!string.IsNullOrEmpty(pageToken) && int.TryParse(pageToken, out var offset))
{
skip = offset;
}
var pagedItems = items.Skip(skip).Take(pageSize).ToList();
string? nextToken = skip + pagedItems.Count < items.Count
? (skip + pagedItems.Count).ToString()
: null;
return Task.FromResult(new VerificationPolicyListResult
{
Items = pagedItems,
NextPageToken = nextToken,
TotalCount = items.Count
});
}
public Task<VerificationPolicyEntity?> UpdateAsync(
Guid tenantId,
string policyId,
UpdateVerificationPolicyRequest request,
string? updatedBy = null,
CancellationToken cancellationToken = default)
{
if (!_policies.TryGetValue((tenantId, policyId), out var existing))
{
return Task.FromResult<VerificationPolicyEntity?>(null);
}
var updated = existing with
{
Version = request.Version ?? existing.Version,
Description = request.Description ?? existing.Description,
PredicateTypes = request.PredicateTypes ?? existing.PredicateTypes,
SignerRequirements = request.SignerRequirements ?? existing.SignerRequirements,
ValidityWindow = request.ValidityWindow ?? existing.ValidityWindow,
Metadata = request.Metadata ?? existing.Metadata,
UpdatedAt = DateTimeOffset.UtcNow,
UpdatedBy = updatedBy
};
_policies[(tenantId, policyId)] = updated;
return Task.FromResult<VerificationPolicyEntity?>(updated);
}
public Task<bool> DeleteAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default)
{
return Task.FromResult(_policies.TryRemove((tenantId, policyId), out _));
}
}

View File

@@ -0,0 +1,139 @@
using System.Collections.Concurrent;
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry.Storage;
/// <summary>
/// In-memory implementation of IViolationStore for testing and development.
/// </summary>
public sealed class InMemoryViolationStore : IViolationStore
{
private readonly ConcurrentDictionary<(Guid TenantId, Guid ViolationId), ViolationEntity> _violations = new();
public Task<ViolationEntity> AppendAsync(
Guid tenantId,
CreateViolationRequest request,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
var violationId = Guid.NewGuid();
var entity = new ViolationEntity
{
TenantId = tenantId,
ViolationId = violationId,
PolicyId = request.PolicyId,
RuleId = request.RuleId,
Severity = request.Severity,
Message = request.Message,
Purl = request.Purl,
CveId = request.CveId,
Context = request.Context,
CreatedAt = now
};
_violations[(tenantId, violationId)] = entity;
return Task.FromResult(entity);
}
public Task<ViolationBatchResult> AppendBatchAsync(
Guid tenantId,
IReadOnlyList<CreateViolationRequest> requests,
CancellationToken cancellationToken = default)
{
var now = DateTimeOffset.UtcNow;
int created = 0;
int failed = 0;
var errors = new List<BatchError>();
for (int i = 0; i < requests.Count; i++)
{
try
{
var request = requests[i];
var violationId = Guid.NewGuid();
var entity = new ViolationEntity
{
TenantId = tenantId,
ViolationId = violationId,
PolicyId = request.PolicyId,
RuleId = request.RuleId,
Severity = request.Severity,
Message = request.Message,
Purl = request.Purl,
CveId = request.CveId,
Context = request.Context,
CreatedAt = now
};
_violations[(tenantId, violationId)] = entity;
created++;
}
catch (Exception ex)
{
failed++;
errors.Add(new BatchError
{
Index = i,
Error = ex.Message
});
}
}
return Task.FromResult(new ViolationBatchResult
{
Created = created,
Failed = failed,
Errors = errors.Count > 0 ? errors : null
});
}
public Task<ViolationEntity?> GetByIdAsync(
Guid tenantId,
Guid violationId,
CancellationToken cancellationToken = default)
{
_violations.TryGetValue((tenantId, violationId), out var entity);
return Task.FromResult(entity);
}
public Task<ViolationListResult> ListAsync(
Guid tenantId,
Severity? severity = null,
int pageSize = 20,
string? pageToken = null,
CancellationToken cancellationToken = default)
{
var query = _violations.Values
.Where(v => v.TenantId == tenantId);
if (severity.HasValue)
{
query = query.Where(v => v.Severity == severity.Value);
}
var items = query
.OrderByDescending(v => v.CreatedAt)
.ToList();
int skip = 0;
if (!string.IsNullOrEmpty(pageToken) && int.TryParse(pageToken, out var offset))
{
skip = offset;
}
var pagedItems = items.Skip(skip).Take(pageSize).ToList();
string? nextToken = skip + pagedItems.Count < items.Count
? (skip + pagedItems.Count).ToString()
: null;
return Task.FromResult(new ViolationListResult
{
Items = pagedItems,
NextPageToken = nextToken,
TotalCount = items.Count
});
}
}