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,553 @@
|
||||
// <copyright file="ExceptionEndpoints.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. All rights reserved.
|
||||
// Licensed under the AGPL-3.0-or-later license.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Policy.Exceptions.Models;
|
||||
using StellaOps.Policy.Exceptions.Repositories;
|
||||
using StellaOps.Policy.Gateway.Contracts;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Exception API endpoints for Policy Gateway.
|
||||
/// </summary>
|
||||
public static class ExceptionEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps exception endpoints to the application.
|
||||
/// </summary>
|
||||
public static void MapExceptionEndpoints(this WebApplication app)
|
||||
{
|
||||
var exceptions = app.MapGroup("/api/policy/exceptions")
|
||||
.WithTags("Exceptions");
|
||||
|
||||
// GET /api/policy/exceptions - List exceptions with filters
|
||||
exceptions.MapGet(string.Empty, async Task<IResult>(
|
||||
[FromQuery] string? status,
|
||||
[FromQuery] string? type,
|
||||
[FromQuery] string? vulnerabilityId,
|
||||
[FromQuery] string? purlPattern,
|
||||
[FromQuery] string? environment,
|
||||
[FromQuery] string? ownerId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var filter = new ExceptionFilter
|
||||
{
|
||||
Status = ParseStatus(status),
|
||||
Type = ParseType(type),
|
||||
VulnerabilityId = vulnerabilityId,
|
||||
PurlPattern = purlPattern,
|
||||
Environment = environment,
|
||||
OwnerId = ownerId,
|
||||
Limit = Math.Clamp(limit ?? 50, 1, 100),
|
||||
Offset = offset ?? 0
|
||||
};
|
||||
|
||||
var results = await repository.GetByFilterAsync(filter, cancellationToken);
|
||||
var counts = await repository.GetCountsAsync(null, cancellationToken);
|
||||
|
||||
return Results.Ok(new ExceptionListResponse
|
||||
{
|
||||
Items = results.Select(ToDto).ToList(),
|
||||
TotalCount = counts.Total,
|
||||
Offset = filter.Offset,
|
||||
Limit = filter.Limit
|
||||
});
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
|
||||
// GET /api/policy/exceptions/counts - Get exception counts
|
||||
exceptions.MapGet("/counts", async Task<IResult>(
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var counts = await repository.GetCountsAsync(null, cancellationToken);
|
||||
return Results.Ok(new ExceptionCountsResponse
|
||||
{
|
||||
Total = counts.Total,
|
||||
Proposed = counts.Proposed,
|
||||
Approved = counts.Approved,
|
||||
Active = counts.Active,
|
||||
Expired = counts.Expired,
|
||||
Revoked = counts.Revoked,
|
||||
ExpiringSoon = counts.ExpiringSoon
|
||||
});
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
|
||||
// GET /api/policy/exceptions/{id} - Get exception by ID
|
||||
exceptions.MapGet("/{id}", async Task<IResult>(
|
||||
string id,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var exception = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (exception is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Exception not found",
|
||||
Status = 404,
|
||||
Detail = $"No exception found with ID: {id}"
|
||||
});
|
||||
}
|
||||
return Results.Ok(ToDto(exception));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
|
||||
// GET /api/policy/exceptions/{id}/history - Get exception history
|
||||
exceptions.MapGet("/{id}/history", async Task<IResult>(
|
||||
string id,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var history = await repository.GetHistoryAsync(id, cancellationToken);
|
||||
return Results.Ok(new ExceptionHistoryResponse
|
||||
{
|
||||
ExceptionId = history.ExceptionId,
|
||||
Events = history.Events.Select(e => new ExceptionEventDto
|
||||
{
|
||||
EventId = e.EventId,
|
||||
SequenceNumber = e.SequenceNumber,
|
||||
EventType = e.EventType.ToString().ToLowerInvariant(),
|
||||
ActorId = e.ActorId,
|
||||
OccurredAt = e.OccurredAt,
|
||||
PreviousStatus = e.PreviousStatus?.ToString().ToLowerInvariant(),
|
||||
NewStatus = e.NewStatus.ToString().ToLowerInvariant(),
|
||||
Description = e.Description
|
||||
}).ToList()
|
||||
});
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
|
||||
// POST /api/policy/exceptions - Create exception
|
||||
exceptions.MapPost(string.Empty, async Task<IResult>(
|
||||
CreateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Request body required",
|
||||
Status = 400
|
||||
});
|
||||
}
|
||||
|
||||
// Validate expiry is in future
|
||||
if (request.ExpiresAt <= DateTimeOffset.UtcNow)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid expiry",
|
||||
Status = 400,
|
||||
Detail = "Expiry date must be in the future"
|
||||
});
|
||||
}
|
||||
|
||||
// Validate expiry is not more than 1 year
|
||||
if (request.ExpiresAt > DateTimeOffset.UtcNow.AddYears(1))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid expiry",
|
||||
Status = 400,
|
||||
Detail = "Expiry date cannot be more than 1 year in the future"
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
var exceptionId = $"EXC-{Guid.NewGuid():N}"[..20];
|
||||
|
||||
var exception = new ExceptionObject
|
||||
{
|
||||
ExceptionId = exceptionId,
|
||||
Version = 1,
|
||||
Status = ExceptionStatus.Proposed,
|
||||
Type = ParseTypeRequired(request.Type),
|
||||
Scope = new ExceptionScope
|
||||
{
|
||||
ArtifactDigest = request.Scope.ArtifactDigest,
|
||||
PurlPattern = request.Scope.PurlPattern,
|
||||
VulnerabilityId = request.Scope.VulnerabilityId,
|
||||
PolicyRuleId = request.Scope.PolicyRuleId,
|
||||
Environments = request.Scope.Environments?.ToImmutableArray() ?? []
|
||||
},
|
||||
OwnerId = request.OwnerId,
|
||||
RequesterId = actorId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
ReasonCode = ParseReasonRequired(request.ReasonCode),
|
||||
Rationale = request.Rationale,
|
||||
EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? [],
|
||||
CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? [],
|
||||
TicketRef = request.TicketRef,
|
||||
Metadata = request.Metadata?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
|
||||
};
|
||||
|
||||
var created = await repository.CreateAsync(exception, actorId, clientInfo, cancellationToken);
|
||||
return Results.Created($"/api/policy/exceptions/{created.ExceptionId}", ToDto(created));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor));
|
||||
|
||||
// PUT /api/policy/exceptions/{id} - Update exception
|
||||
exceptions.MapPut("/{id}", async Task<IResult>(
|
||||
string id,
|
||||
UpdateExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Exception not found",
|
||||
Status = 404
|
||||
});
|
||||
}
|
||||
|
||||
if (existing.Status is ExceptionStatus.Expired or ExceptionStatus.Revoked)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Cannot update",
|
||||
Status = 400,
|
||||
Detail = "Cannot update an expired or revoked exception"
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Rationale = request.Rationale ?? existing.Rationale,
|
||||
EvidenceRefs = request.EvidenceRefs?.ToImmutableArray() ?? existing.EvidenceRefs,
|
||||
CompensatingControls = request.CompensatingControls?.ToImmutableArray() ?? existing.CompensatingControls,
|
||||
TicketRef = request.TicketRef ?? existing.TicketRef,
|
||||
Metadata = request.Metadata?.ToImmutableDictionary() ?? existing.Metadata
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
updated, ExceptionEventType.Updated, actorId, "Exception updated", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor));
|
||||
|
||||
// POST /api/policy/exceptions/{id}/approve - Approve exception
|
||||
exceptions.MapPost("/{id}/approve", async Task<IResult>(
|
||||
string id,
|
||||
ApproveExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status != ExceptionStatus.Proposed)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid state transition",
|
||||
Status = 400,
|
||||
Detail = "Only proposed exceptions can be approved"
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
// Approver cannot be requester
|
||||
if (actorId == existing.RequesterId)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Self-approval not allowed",
|
||||
Status = 400,
|
||||
Detail = "Requester cannot approve their own exception"
|
||||
});
|
||||
}
|
||||
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Approved,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ApprovedAt = DateTimeOffset.UtcNow,
|
||||
ApproverIds = existing.ApproverIds.Add(actorId)
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
updated, ExceptionEventType.Approved, actorId, request?.Comment ?? "Exception approved", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
|
||||
// POST /api/policy/exceptions/{id}/activate - Activate approved exception
|
||||
exceptions.MapPost("/{id}/activate", async Task<IResult>(
|
||||
string id,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status != ExceptionStatus.Approved)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid state transition",
|
||||
Status = 400,
|
||||
Detail = "Only approved exceptions can be activated"
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Active,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
updated, ExceptionEventType.Activated, actorId, "Exception activated", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
|
||||
// POST /api/policy/exceptions/{id}/extend - Extend expiry
|
||||
exceptions.MapPost("/{id}/extend", async Task<IResult>(
|
||||
string id,
|
||||
ExtendExceptionRequest request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status != ExceptionStatus.Active)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid state",
|
||||
Status = 400,
|
||||
Detail = "Only active exceptions can be extended"
|
||||
});
|
||||
}
|
||||
|
||||
if (request.NewExpiresAt <= existing.ExpiresAt)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid expiry",
|
||||
Status = 400,
|
||||
Detail = "New expiry must be after current expiry"
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = request.NewExpiresAt
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
updated, ExceptionEventType.Extended, actorId, request.Reason, clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
|
||||
// DELETE /api/policy/exceptions/{id} - Revoke exception
|
||||
exceptions.MapDelete("/{id}", async Task<IResult>(
|
||||
string id,
|
||||
[FromBody] RevokeExceptionRequest? request,
|
||||
HttpContext context,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var existing = await repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails { Title = "Exception not found", Status = 404 });
|
||||
}
|
||||
|
||||
if (existing.Status is ExceptionStatus.Expired or ExceptionStatus.Revoked)
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid state",
|
||||
Status = 400,
|
||||
Detail = "Exception is already expired or revoked"
|
||||
});
|
||||
}
|
||||
|
||||
var actorId = GetActorId(context);
|
||||
var clientInfo = GetClientInfo(context);
|
||||
|
||||
var updated = existing with
|
||||
{
|
||||
Version = existing.Version + 1,
|
||||
Status = ExceptionStatus.Revoked,
|
||||
UpdatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var result = await repository.UpdateAsync(
|
||||
updated, ExceptionEventType.Revoked, actorId, request?.Reason ?? "Exception revoked", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
|
||||
// GET /api/policy/exceptions/expiring - Get exceptions expiring soon
|
||||
exceptions.MapGet("/expiring", async Task<IResult>(
|
||||
[FromQuery] int? days,
|
||||
IExceptionRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var horizon = TimeSpan.FromDays(days ?? 7);
|
||||
var results = await repository.GetExpiringAsync(horizon, cancellationToken);
|
||||
return Results.Ok(results.Select(ToDto).ToList());
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static string GetActorId(HttpContext context)
|
||||
{
|
||||
return context.User.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? context.User.FindFirstValue("sub")
|
||||
?? "anonymous";
|
||||
}
|
||||
|
||||
private static string? GetClientInfo(HttpContext context)
|
||||
{
|
||||
var ip = context.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = context.Request.Headers.UserAgent.FirstOrDefault();
|
||||
return string.IsNullOrEmpty(ip) ? null : $"{ip}; {userAgent}";
|
||||
}
|
||||
|
||||
private static ExceptionResponse ToDto(ExceptionObject ex) => new()
|
||||
{
|
||||
ExceptionId = ex.ExceptionId,
|
||||
Version = ex.Version,
|
||||
Status = ex.Status.ToString().ToLowerInvariant(),
|
||||
Type = ex.Type.ToString().ToLowerInvariant(),
|
||||
Scope = new ExceptionScopeDto
|
||||
{
|
||||
ArtifactDigest = ex.Scope.ArtifactDigest,
|
||||
PurlPattern = ex.Scope.PurlPattern,
|
||||
VulnerabilityId = ex.Scope.VulnerabilityId,
|
||||
PolicyRuleId = ex.Scope.PolicyRuleId,
|
||||
Environments = ex.Scope.Environments.ToList()
|
||||
},
|
||||
OwnerId = ex.OwnerId,
|
||||
RequesterId = ex.RequesterId,
|
||||
ApproverIds = ex.ApproverIds.ToList(),
|
||||
CreatedAt = ex.CreatedAt,
|
||||
UpdatedAt = ex.UpdatedAt,
|
||||
ApprovedAt = ex.ApprovedAt,
|
||||
ExpiresAt = ex.ExpiresAt,
|
||||
ReasonCode = ex.ReasonCode.ToString().ToLowerInvariant(),
|
||||
Rationale = ex.Rationale,
|
||||
EvidenceRefs = ex.EvidenceRefs.ToList(),
|
||||
CompensatingControls = ex.CompensatingControls.ToList(),
|
||||
TicketRef = ex.TicketRef,
|
||||
Metadata = ex.Metadata.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
};
|
||||
|
||||
private static ExceptionStatus? ParseStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrEmpty(status)) return null;
|
||||
return status.ToLowerInvariant() switch
|
||||
{
|
||||
"proposed" => ExceptionStatus.Proposed,
|
||||
"approved" => ExceptionStatus.Approved,
|
||||
"active" => ExceptionStatus.Active,
|
||||
"expired" => ExceptionStatus.Expired,
|
||||
"revoked" => ExceptionStatus.Revoked,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static ExceptionType? ParseType(string? type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(type)) return null;
|
||||
return type.ToLowerInvariant() switch
|
||||
{
|
||||
"vulnerability" => ExceptionType.Vulnerability,
|
||||
"policy" => ExceptionType.Policy,
|
||||
"unknown" => ExceptionType.Unknown,
|
||||
"component" => ExceptionType.Component,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static ExceptionType ParseTypeRequired(string type)
|
||||
{
|
||||
return type.ToLowerInvariant() switch
|
||||
{
|
||||
"vulnerability" => ExceptionType.Vulnerability,
|
||||
"policy" => ExceptionType.Policy,
|
||||
"unknown" => ExceptionType.Unknown,
|
||||
"component" => ExceptionType.Component,
|
||||
_ => throw new ArgumentException($"Invalid exception type: {type}")
|
||||
};
|
||||
}
|
||||
|
||||
private static ExceptionReason ParseReasonRequired(string reason)
|
||||
{
|
||||
return reason.ToLowerInvariant() switch
|
||||
{
|
||||
"false_positive" or "falsepositive" => ExceptionReason.FalsePositive,
|
||||
"accepted_risk" or "acceptedrisk" => ExceptionReason.AcceptedRisk,
|
||||
"compensating_control" or "compensatingcontrol" => ExceptionReason.CompensatingControl,
|
||||
"test_only" or "testonly" => ExceptionReason.TestOnly,
|
||||
"vendor_not_affected" or "vendornotaffected" => ExceptionReason.VendorNotAffected,
|
||||
"scheduled_fix" or "scheduledfix" => ExceptionReason.ScheduledFix,
|
||||
"deprecation_in_progress" or "deprecationinprogress" => ExceptionReason.DeprecationInProgress,
|
||||
"runtime_mitigation" or "runtimemitigation" => ExceptionReason.RuntimeMitigation,
|
||||
"network_isolation" or "networkisolation" => ExceptionReason.NetworkIsolation,
|
||||
"other" => ExceptionReason.Other,
|
||||
_ => throw new ArgumentException($"Invalid reason code: {reason}")
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user