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:
287
src/Policy/StellaOps.Policy.Engine/Domain/ExceptionContracts.cs
Normal file
287
src/Policy/StellaOps.Policy.Engine/Domain/ExceptionContracts.cs
Normal file
@@ -0,0 +1,287 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Domain;
|
||||
|
||||
// ============================================================================
|
||||
// Exception API DTOs - Request Models
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Request to create a new exception.
|
||||
/// </summary>
|
||||
public sealed record CreateExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of exception: vulnerability, policy, unknown, or component.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scope constraints for the exception.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required ExceptionScopeDto Scope { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Categorized reason for the exception.
|
||||
/// </summary>
|
||||
[Required]
|
||||
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.")]
|
||||
public required string Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the exception should expire. Required and must be in the future.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required DateTimeOffset ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressed references to supporting evidence.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? EvidenceRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Compensating controls in place that mitigate the risk.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? CompensatingControls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata for organization-specific tracking.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ticket or tracking system reference (e.g., JIRA-1234).
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TicketRef { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception scope constraints.
|
||||
/// </summary>
|
||||
public sealed record ExceptionScopeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Specific artifact digest (sha256:...) this exception applies to.
|
||||
/// </summary>
|
||||
public string? ArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PURL pattern this exception applies to (supports wildcards).
|
||||
/// </summary>
|
||||
public string? PurlPattern { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific vulnerability ID (CVE-XXXX-XXXXX) this exception applies to.
|
||||
/// </summary>
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy rule identifier this exception bypasses.
|
||||
/// </summary>
|
||||
public string? PolicyRuleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Environments where this exception is valid. Empty means all environments.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Environments { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to update an existing exception.
|
||||
/// </summary>
|
||||
public sealed record UpdateExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the exception for optimistic concurrency.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required int Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated rationale (if changing).
|
||||
/// </summary>
|
||||
[MinLength(50, ErrorMessage = "Rationale must be at least 50 characters.")]
|
||||
public string? Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated evidence references.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? EvidenceRefs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated compensating controls.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? CompensatingControls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated metadata.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Updated ticket reference.
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
public string? TicketRef { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to approve an exception.
|
||||
/// </summary>
|
||||
public sealed record ApproveExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional comment from the approver.
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Comment { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to extend an exception's expiry.
|
||||
/// </summary>
|
||||
public sealed record ExtendExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the exception for optimistic concurrency.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required int Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// New expiry date (must be in the future, max 1 year from now).
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required DateTimeOffset NewExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the extension.
|
||||
/// </summary>
|
||||
[StringLength(500)]
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to revoke an exception.
|
||||
/// </summary>
|
||||
public sealed record RevokeExceptionRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Reason for revoking the exception.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(500)]
|
||||
public required string Reason { get; init; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Exception API DTOs - Response Models
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Full exception response.
|
||||
/// </summary>
|
||||
public sealed record ExceptionDto
|
||||
{
|
||||
public required string ExceptionId { get; init; }
|
||||
public required int Version { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required ExceptionScopeDto Scope { get; init; }
|
||||
public required string OwnerId { get; init; }
|
||||
public required string RequesterId { get; init; }
|
||||
public IReadOnlyList<string> ApproverIds { get; init; } = [];
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required DateTimeOffset UpdatedAt { get; init; }
|
||||
public DateTimeOffset? ApprovedAt { get; init; }
|
||||
public required DateTimeOffset ExpiresAt { get; init; }
|
||||
public required string ReasonCode { get; init; }
|
||||
public required string Rationale { get; init; }
|
||||
public IReadOnlyList<string> EvidenceRefs { get; init; } = [];
|
||||
public IReadOnlyList<string> CompensatingControls { get; init; } = [];
|
||||
public IReadOnlyDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
|
||||
public string? TicketRef { get; init; }
|
||||
public bool IsEffective { get; init; }
|
||||
public bool HasExpired { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary exception for list responses.
|
||||
/// </summary>
|
||||
public sealed record ExceptionSummaryDto
|
||||
{
|
||||
public required string ExceptionId { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public string? VulnerabilityId { get; init; }
|
||||
public string? PurlPattern { get; init; }
|
||||
public required string OwnerId { get; init; }
|
||||
public required DateTimeOffset ExpiresAt { get; init; }
|
||||
public required string ReasonCode { get; init; }
|
||||
public bool IsEffective { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paginated list response.
|
||||
/// </summary>
|
||||
public sealed record ExceptionListResponse
|
||||
{
|
||||
public required IReadOnlyList<ExceptionSummaryDto> Items { get; init; }
|
||||
public required int TotalCount { get; init; }
|
||||
public required int Limit { get; init; }
|
||||
public required int Offset { get; init; }
|
||||
public bool HasMore => Offset + Items.Count < TotalCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception event for audit history.
|
||||
/// </summary>
|
||||
public sealed record ExceptionEventDto
|
||||
{
|
||||
public required Guid EventId { get; init; }
|
||||
public required int SequenceNumber { get; init; }
|
||||
public required string EventType { get; init; }
|
||||
public required string ActorId { get; init; }
|
||||
public required DateTimeOffset OccurredAt { get; init; }
|
||||
public string? PreviousStatus { get; init; }
|
||||
public required string NewStatus { get; init; }
|
||||
public int NewVersion { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Details { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception history response.
|
||||
/// </summary>
|
||||
public sealed record ExceptionHistoryResponse
|
||||
{
|
||||
public required string ExceptionId { get; init; }
|
||||
public required IReadOnlyList<ExceptionEventDto> Events { get; init; }
|
||||
public required int EventCount { get; init; }
|
||||
public DateTimeOffset? FirstEventAt { get; init; }
|
||||
public DateTimeOffset? LastEventAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception counts by status.
|
||||
/// </summary>
|
||||
public sealed record ExceptionCountsDto
|
||||
{
|
||||
public int Total { get; init; }
|
||||
public int Proposed { get; init; }
|
||||
public int Approved { get; init; }
|
||||
public int Active { get; init; }
|
||||
public int Expired { get; init; }
|
||||
public int Revoked { get; init; }
|
||||
public int ExpiringSoon { get; init; }
|
||||
}
|
||||
260
src/Policy/StellaOps.Policy.Engine/Domain/ExceptionMapper.cs
Normal file
260
src/Policy/StellaOps.Policy.Engine/Domain/ExceptionMapper.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Maps between Exception domain models and API DTOs.
|
||||
/// </summary>
|
||||
public static class ExceptionMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps an ExceptionObject to a full DTO.
|
||||
/// </summary>
|
||||
public static ExceptionDto ToDto(ExceptionObject exception)
|
||||
{
|
||||
return new ExceptionDto
|
||||
{
|
||||
ExceptionId = exception.ExceptionId,
|
||||
Version = exception.Version,
|
||||
Status = StatusToString(exception.Status),
|
||||
Type = TypeToString(exception.Type),
|
||||
Scope = ToScopeDto(exception.Scope),
|
||||
OwnerId = exception.OwnerId,
|
||||
RequesterId = exception.RequesterId,
|
||||
ApproverIds = exception.ApproverIds.ToList(),
|
||||
CreatedAt = exception.CreatedAt,
|
||||
UpdatedAt = exception.UpdatedAt,
|
||||
ApprovedAt = exception.ApprovedAt,
|
||||
ExpiresAt = exception.ExpiresAt,
|
||||
ReasonCode = ReasonToString(exception.ReasonCode),
|
||||
Rationale = exception.Rationale,
|
||||
EvidenceRefs = exception.EvidenceRefs.ToList(),
|
||||
CompensatingControls = exception.CompensatingControls.ToList(),
|
||||
Metadata = exception.Metadata,
|
||||
TicketRef = exception.TicketRef,
|
||||
IsEffective = exception.IsEffective,
|
||||
HasExpired = exception.HasExpired
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExceptionObject to a summary DTO for list responses.
|
||||
/// </summary>
|
||||
public static ExceptionSummaryDto ToSummaryDto(ExceptionObject exception)
|
||||
{
|
||||
return new ExceptionSummaryDto
|
||||
{
|
||||
ExceptionId = exception.ExceptionId,
|
||||
Status = StatusToString(exception.Status),
|
||||
Type = TypeToString(exception.Type),
|
||||
VulnerabilityId = exception.Scope.VulnerabilityId,
|
||||
PurlPattern = exception.Scope.PurlPattern,
|
||||
OwnerId = exception.OwnerId,
|
||||
ExpiresAt = exception.ExpiresAt,
|
||||
ReasonCode = ReasonToString(exception.ReasonCode),
|
||||
IsEffective = exception.IsEffective
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExceptionScope to a DTO.
|
||||
/// </summary>
|
||||
public static ExceptionScopeDto ToScopeDto(ExceptionScope scope)
|
||||
{
|
||||
return new ExceptionScopeDto
|
||||
{
|
||||
ArtifactDigest = scope.ArtifactDigest,
|
||||
PurlPattern = scope.PurlPattern,
|
||||
VulnerabilityId = scope.VulnerabilityId,
|
||||
PolicyRuleId = scope.PolicyRuleId,
|
||||
Environments = scope.Environments.IsEmpty ? null : scope.Environments.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExceptionEvent to a DTO.
|
||||
/// </summary>
|
||||
public static ExceptionEventDto ToEventDto(ExceptionEvent evt)
|
||||
{
|
||||
return new ExceptionEventDto
|
||||
{
|
||||
EventId = evt.EventId,
|
||||
SequenceNumber = evt.SequenceNumber,
|
||||
EventType = EventTypeToString(evt.EventType),
|
||||
ActorId = evt.ActorId,
|
||||
OccurredAt = evt.OccurredAt,
|
||||
PreviousStatus = evt.PreviousStatus.HasValue ? StatusToString(evt.PreviousStatus.Value) : null,
|
||||
NewStatus = StatusToString(evt.NewStatus),
|
||||
NewVersion = evt.NewVersion,
|
||||
Description = evt.Description,
|
||||
Details = evt.Details.IsEmpty ? null : evt.Details
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExceptionHistory to a response DTO.
|
||||
/// </summary>
|
||||
public static ExceptionHistoryResponse ToHistoryResponse(ExceptionHistory history)
|
||||
{
|
||||
return new ExceptionHistoryResponse
|
||||
{
|
||||
ExceptionId = history.ExceptionId,
|
||||
Events = history.Events.Select(ToEventDto).ToList(),
|
||||
EventCount = history.EventCount,
|
||||
FirstEventAt = history.FirstEventAt,
|
||||
LastEventAt = history.LastEventAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps ExceptionCounts to a DTO.
|
||||
/// </summary>
|
||||
public static ExceptionCountsDto ToCountsDto(ExceptionCounts counts)
|
||||
{
|
||||
return new ExceptionCountsDto
|
||||
{
|
||||
Total = counts.Total,
|
||||
Proposed = counts.Proposed,
|
||||
Approved = counts.Approved,
|
||||
Active = counts.Active,
|
||||
Expired = counts.Expired,
|
||||
Revoked = counts.Revoked,
|
||||
ExpiringSoon = counts.ExpiringSoon
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ExceptionScope from a DTO.
|
||||
/// </summary>
|
||||
public static ExceptionScope FromScopeDto(ExceptionScopeDto dto, Guid? tenantId = null)
|
||||
{
|
||||
return new ExceptionScope
|
||||
{
|
||||
ArtifactDigest = dto.ArtifactDigest,
|
||||
PurlPattern = dto.PurlPattern,
|
||||
VulnerabilityId = dto.VulnerabilityId,
|
||||
PolicyRuleId = dto.PolicyRuleId,
|
||||
Environments = dto.Environments?.ToImmutableArray() ?? [],
|
||||
TenantId = tenantId
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an ExceptionObject from a create request.
|
||||
/// </summary>
|
||||
public static ExceptionObject FromCreateRequest(
|
||||
CreateExceptionRequest request,
|
||||
string exceptionId,
|
||||
string ownerId,
|
||||
string requesterId,
|
||||
Guid? tenantId = null)
|
||||
{
|
||||
return new ExceptionObject
|
||||
{
|
||||
ExceptionId = exceptionId,
|
||||
Version = 1,
|
||||
Status = ExceptionStatus.Proposed,
|
||||
Type = ParseType(request.Type),
|
||||
Scope = FromScopeDto(request.Scope, tenantId),
|
||||
OwnerId = ownerId,
|
||||
RequesterId = requesterId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
ReasonCode = ParseReason(request.ReasonCode),
|
||||
Rationale = request.Rationale,
|
||||
EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? [],
|
||||
CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? [],
|
||||
Metadata = request.Metadata?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
|
||||
TicketRef = request.TicketRef
|
||||
};
|
||||
}
|
||||
|
||||
#region String Conversions
|
||||
|
||||
public static string StatusToString(ExceptionStatus status) => status switch
|
||||
{
|
||||
ExceptionStatus.Proposed => "proposed",
|
||||
ExceptionStatus.Approved => "approved",
|
||||
ExceptionStatus.Active => "active",
|
||||
ExceptionStatus.Expired => "expired",
|
||||
ExceptionStatus.Revoked => "revoked",
|
||||
_ => throw new ArgumentException($"Unknown status: {status}", nameof(status))
|
||||
};
|
||||
|
||||
public static ExceptionStatus ParseStatus(string status) => status.ToLowerInvariant() switch
|
||||
{
|
||||
"proposed" => ExceptionStatus.Proposed,
|
||||
"approved" => ExceptionStatus.Approved,
|
||||
"active" => ExceptionStatus.Active,
|
||||
"expired" => ExceptionStatus.Expired,
|
||||
"revoked" => ExceptionStatus.Revoked,
|
||||
_ => throw new ArgumentException($"Unknown status: {status}", nameof(status))
|
||||
};
|
||||
|
||||
public static string TypeToString(ExceptionType type) => type switch
|
||||
{
|
||||
ExceptionType.Vulnerability => "vulnerability",
|
||||
ExceptionType.Policy => "policy",
|
||||
ExceptionType.Unknown => "unknown",
|
||||
ExceptionType.Component => "component",
|
||||
_ => throw new ArgumentException($"Unknown type: {type}", nameof(type))
|
||||
};
|
||||
|
||||
public static ExceptionType ParseType(string type) => type.ToLowerInvariant() switch
|
||||
{
|
||||
"vulnerability" => ExceptionType.Vulnerability,
|
||||
"policy" => ExceptionType.Policy,
|
||||
"unknown" => ExceptionType.Unknown,
|
||||
"component" => ExceptionType.Component,
|
||||
_ => throw new ArgumentException($"Unknown type: {type}", nameof(type))
|
||||
};
|
||||
|
||||
public static string ReasonToString(ExceptionReason reason) => reason switch
|
||||
{
|
||||
ExceptionReason.FalsePositive => "false_positive",
|
||||
ExceptionReason.AcceptedRisk => "accepted_risk",
|
||||
ExceptionReason.CompensatingControl => "compensating_control",
|
||||
ExceptionReason.TestOnly => "test_only",
|
||||
ExceptionReason.VendorNotAffected => "vendor_not_affected",
|
||||
ExceptionReason.ScheduledFix => "scheduled_fix",
|
||||
ExceptionReason.DeprecationInProgress => "deprecation_in_progress",
|
||||
ExceptionReason.RuntimeMitigation => "runtime_mitigation",
|
||||
ExceptionReason.NetworkIsolation => "network_isolation",
|
||||
ExceptionReason.Other => "other",
|
||||
_ => throw new ArgumentException($"Unknown reason: {reason}", nameof(reason))
|
||||
};
|
||||
|
||||
public static ExceptionReason ParseReason(string reason) => reason.ToLowerInvariant() switch
|
||||
{
|
||||
"false_positive" => ExceptionReason.FalsePositive,
|
||||
"accepted_risk" => ExceptionReason.AcceptedRisk,
|
||||
"compensating_control" => ExceptionReason.CompensatingControl,
|
||||
"test_only" => ExceptionReason.TestOnly,
|
||||
"vendor_not_affected" => ExceptionReason.VendorNotAffected,
|
||||
"scheduled_fix" => ExceptionReason.ScheduledFix,
|
||||
"deprecation_in_progress" => ExceptionReason.DeprecationInProgress,
|
||||
"runtime_mitigation" => ExceptionReason.RuntimeMitigation,
|
||||
"network_isolation" => ExceptionReason.NetworkIsolation,
|
||||
"other" => ExceptionReason.Other,
|
||||
_ => throw new ArgumentException($"Unknown reason: {reason}", nameof(reason))
|
||||
};
|
||||
|
||||
public static string EventTypeToString(ExceptionEventType eventType) => eventType switch
|
||||
{
|
||||
ExceptionEventType.Created => "created",
|
||||
ExceptionEventType.Updated => "updated",
|
||||
ExceptionEventType.Approved => "approved",
|
||||
ExceptionEventType.Activated => "activated",
|
||||
ExceptionEventType.Extended => "extended",
|
||||
ExceptionEventType.Revoked => "revoked",
|
||||
ExceptionEventType.Expired => "expired",
|
||||
ExceptionEventType.EvidenceAttached => "evidence_attached",
|
||||
ExceptionEventType.CompensatingControlAdded => "compensating_control_added",
|
||||
ExceptionEventType.Rejected => "rejected",
|
||||
_ => throw new ArgumentException($"Unknown event type: {eventType}", nameof(eventType))
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy/StellaOps.Policy.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy.Exceptions/StellaOps.Policy.Exceptions.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Policy.Unknowns/StellaOps.Policy.Unknowns.csproj" />
|
||||
<ProjectReference Include="../StellaOps.PolicyDsl/StellaOps.PolicyDsl.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user