Add unit tests for ExceptionEvaluator, ExceptionEvent, ExceptionHistory, and ExceptionObject models
- Implemented comprehensive unit tests for the ExceptionEvaluator service, covering various scenarios including matching exceptions, environment checks, and evidence references. - Created tests for the ExceptionEvent model to validate event creation methods and ensure correct event properties. - Developed tests for the ExceptionHistory model to verify event count, order, and timestamps. - Added tests for the ExceptionObject domain model to ensure validity checks and property preservation for various fields.
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
// <copyright file="ExceptionContracts.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under the AGPL-3.0-or-later license.
|
||||
// </copyright>
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Request to create a new exception.
|
||||
/// </summary>
|
||||
public sealed record CreateExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of exception (vulnerability, policy, unknown, component).
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("type")]
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Exception scope defining what this exception applies to.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("scope")]
|
||||
public required ExceptionScopeDto Scope { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Owner ID (user or team accountable).
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("ownerId")]
|
||||
public required string OwnerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason code for the exception.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("reasonCode")]
|
||||
public required string ReasonCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed rationale explaining why this exception is necessary.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(50, ErrorMessage = "Rationale must be at least 50 characters.")]
|
||||
[JsonPropertyName("rationale")]
|
||||
public required string Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the exception should expire.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("expiresAt")]
|
||||
public required DateTimeOffset ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressed evidence references.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceRefs")]
|
||||
public IReadOnlyList<string>? EvidenceRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Compensating controls in place.
|
||||
/// </summary>
|
||||
[JsonPropertyName("compensatingControls")]
|
||||
public IReadOnlyList<string>? CompensatingControls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// External ticket reference (e.g., JIRA-1234).
|
||||
/// </summary>
|
||||
[JsonPropertyName("ticketRef")]
|
||||
public string? TicketRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception scope DTO.
|
||||
/// </summary>
|
||||
public sealed record ExceptionScopeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Specific artifact digest (sha256:...).
|
||||
/// </summary>
|
||||
[JsonPropertyName("artifactDigest")]
|
||||
public string? ArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PURL pattern (supports wildcards: pkg:npm/lodash@*).
|
||||
/// </summary>
|
||||
[JsonPropertyName("purlPattern")]
|
||||
public string? PurlPattern { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific vulnerability ID (CVE-XXXX-XXXXX).
|
||||
/// </summary>
|
||||
[JsonPropertyName("vulnerabilityId")]
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy rule identifier to bypass.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyRuleId")]
|
||||
public string? PolicyRuleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environments where exception is valid (empty = all).
|
||||
/// </summary>
|
||||
[JsonPropertyName("environments")]
|
||||
public IReadOnlyList<string>? Environments { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to update an exception.
|
||||
/// </summary>
|
||||
public sealed record UpdateExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Updated rationale.
|
||||
/// </summary>
|
||||
[MinLength(50, ErrorMessage = "Rationale must be at least 50 characters.")]
|
||||
[JsonPropertyName("rationale")]
|
||||
public string? Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated evidence references.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceRefs")]
|
||||
public IReadOnlyList<string>? EvidenceRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated compensating controls.
|
||||
/// </summary>
|
||||
[JsonPropertyName("compensatingControls")]
|
||||
public IReadOnlyList<string>? CompensatingControls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated ticket reference.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ticketRef")]
|
||||
public string? TicketRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to approve an exception.
|
||||
/// </summary>
|
||||
public sealed record ApproveExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional comment from approver.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public string? Comment { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to extend an exception's expiry.
|
||||
/// </summary>
|
||||
public sealed record ExtendExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// New expiry date.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonPropertyName("newExpiresAt")]
|
||||
public required DateTimeOffset NewExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for extension.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(20, ErrorMessage = "Extension reason must be at least 20 characters.")]
|
||||
[JsonPropertyName("reason")]
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to revoke an exception.
|
||||
/// </summary>
|
||||
public sealed record RevokeExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Reason for revocation.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(10, ErrorMessage = "Revocation reason must be at least 10 characters.")]
|
||||
[JsonPropertyName("reason")]
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception response DTO.
|
||||
/// </summary>
|
||||
public sealed record ExceptionResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique exception ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exceptionId")]
|
||||
public required string ExceptionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version for optimistic concurrency.
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public required int Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public required string Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Exception type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Exception scope.
|
||||
/// </summary>
|
||||
[JsonPropertyName("scope")]
|
||||
public required ExceptionScopeDto Scope { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Owner ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ownerId")]
|
||||
public required string OwnerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Requester ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("requesterId")]
|
||||
public required string RequesterId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Approver IDs.
|
||||
/// </summary>
|
||||
[JsonPropertyName("approverIds")]
|
||||
public required IReadOnlyList<string> ApproverIds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Created timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Last updated timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("updatedAt")]
|
||||
public required DateTimeOffset UpdatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Approved timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("approvedAt")]
|
||||
public DateTimeOffset? ApprovedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expiry timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expiresAt")]
|
||||
public required DateTimeOffset ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reasonCode")]
|
||||
public required string ReasonCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rationale.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rationale")]
|
||||
public required string Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence references.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidenceRefs")]
|
||||
public required IReadOnlyList<string> EvidenceRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Compensating controls.
|
||||
/// </summary>
|
||||
[JsonPropertyName("compensatingControls")]
|
||||
public required IReadOnlyList<string> CompensatingControls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ticket reference.
|
||||
/// </summary>
|
||||
[JsonPropertyName("ticketRef")]
|
||||
public string? TicketRef { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public required IReadOnlyDictionary<string, string> Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paginated list of exceptions.
|
||||
/// </summary>
|
||||
public sealed record ExceptionListResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// List of exceptions.
|
||||
/// </summary>
|
||||
[JsonPropertyName("items")]
|
||||
public required IReadOnlyList<ExceptionResponse> Items { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalCount")]
|
||||
public required int TotalCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset.
|
||||
/// </summary>
|
||||
[JsonPropertyName("offset")]
|
||||
public required int Offset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Limit.
|
||||
/// </summary>
|
||||
[JsonPropertyName("limit")]
|
||||
public required int Limit { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception event DTO.
|
||||
/// </summary>
|
||||
public sealed record ExceptionEventDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Event ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("eventId")]
|
||||
public required Guid EventId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sequence number.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sequenceNumber")]
|
||||
public required int SequenceNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("eventType")]
|
||||
public required string EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actor ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("actorId")]
|
||||
public required string ActorId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurred timestamp.
|
||||
/// </summary>
|
||||
[JsonPropertyName("occurredAt")]
|
||||
public required DateTimeOffset OccurredAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Previous status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("previousStatus")]
|
||||
public string? PreviousStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("newStatus")]
|
||||
public required string NewStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception history response.
|
||||
/// </summary>
|
||||
public sealed record ExceptionHistoryResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exceptionId")]
|
||||
public required string ExceptionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Events in chronological order.
|
||||
/// </summary>
|
||||
[JsonPropertyName("events")]
|
||||
public required IReadOnlyList<ExceptionEventDto> Events { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception counts summary.
|
||||
/// </summary>
|
||||
public sealed record ExceptionCountsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Total count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("total")]
|
||||
public required int Total { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Proposed count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("proposed")]
|
||||
public required int Proposed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Approved count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("approved")]
|
||||
public required int Approved { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Active count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("active")]
|
||||
public required int Active { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expired count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expired")]
|
||||
public required int Expired { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Revoked count.
|
||||
/// </summary>
|
||||
[JsonPropertyName("revoked")]
|
||||
public required int Revoked { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Count expiring within 7 days.
|
||||
/// </summary>
|
||||
[JsonPropertyName("expiringSoon")]
|
||||
public required int ExpiringSoon { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user