< Summary

Information
Class: AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator<T>
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Evaluation/DefaultAsiBackbonePolicyEvaluator.cs
Line coverage
96%
Covered lines: 387
Uncovered lines: 16
Coverable lines: 403
Total lines: 796
Line coverage: 96%
Branch coverage
86%
Covered branches: 142
Total branches: 164
Branch coverage: 86.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%44100%
EvaluateAsync()100%1010100%
ShouldConvertExceptionToDenial(...)100%44100%
IsCriticalException(...)100%1616100%
CreateConstraintResultsBuffer()100%22100%
AccumulateConstraintResult(...)100%88100%
EvaluateEmptyPolicyAsync()100%44100%
Compose(...)100%88100%
CreateNoConstraintDecision(...)100%22100%
CreateNoConstraintDecision(...)100%22100%
CreateDeniedDecision(...)100%22100%
CreateWarningDecision(...)100%22100%
CreateConstraintResultsView(...)100%44100%
CreateProtectedThreatWarningDecision(...)100%44100%
ApplyDecisionPolicyAsync()100%88100%
EvaluateThreatModelContributorsAsync()100%1616100%
CreateThreatContributorExceptionResult(...)100%11100%
CreateThreatEvaluationResult(...)85.71%7796.96%
GetEffectiveThreatOutcome(...)100%22100%
CreateInvalidAllowedThreatOutcomeException()100%11100%
SelectMoreRestrictiveOutcome(...)100%44100%
GetThreatOutcomeRank(...)85.71%7790%
GetContributorName(...)50%22100%
CreateConstraintExceptionDecisionAsync()100%22100%
LogEmptyPolicyAllowed(...)62.5%88100%
LogConstraintExceptionDenied(...)70%1010100%
LogThreatContributorExceptionDenied(...)12.5%43818.18%
.cctor()100%11100%
.ctor(...)100%11100%
get_Empty()100%11100%
get_WarningReasons()100%11100%
get_BlockingDecision()100%11100%
ForWarningReasons(...)100%11100%
ForBlockingDecision(...)100%11100%
get_FirstReason()100%11100%
get_Count()100%11100%
AddRange(...)100%22100%
ConcatAsArray(...)50%2285.71%
AsReadOnlyList()50%5466.66%
Add(...)66.66%6690%
CopyTo(...)75%4485.71%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Evaluation/DefaultAsiBackbonePolicyEvaluator.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using AsiBackbone.Core.Constraints;
 3using AsiBackbone.Core.Decisions;
 4using AsiBackbone.Core.Results;
 5using AsiBackbone.Core.ThreatModeling;
 6using Microsoft.Extensions.Logging;
 7
 8namespace AsiBackbone.Core.Evaluation;
 9
 10/// <summary>
 11/// Default policy evaluator that runs the active constraint structure and composes the result into a governance decisio
 12/// </summary>
 13/// <typeparam name="TContext">The framework-neutral evaluation context type.</typeparam>
 14public sealed class DefaultAsiBackbonePolicyEvaluator<TContext> : IAsiBackbonePolicyEvaluator<TContext>
 15    where TContext : IAsiBackboneConstraintEvaluationContext
 16{
 3117    private static readonly IReadOnlyList<ConstraintEvaluationResult> EmptyConstraintResults =
 3118        Array.AsReadOnly(Array.Empty<ConstraintEvaluationResult>());
 19
 20    private const string InvalidAllowedThreatOutcomeMessage =
 21        "Threat model contributors cannot return an Allowed outcome. Use ThreatAssessment.NoThreat() for no finding, or 
 22
 3123    private static readonly Action<ILogger, string, string, string, Exception?> EmptyPolicyAllowedWarning =
 3124        LoggerMessage.Define<string, string, string>(
 3125            LogLevel.Warning,
 3126            new EventId(4110, nameof(EmptyPolicyAllowedWarning)),
 3127            "Policy evaluation ran with zero constraints while DenyWhenNoConstraints is false; default empty-policy beha
 28
 3129    private static readonly Action<ILogger, string, string, string, string, string, Exception?> ConstraintExceptionDenie
 3130        LoggerMessage.Define<string, string, string, string, string>(
 3131            LogLevel.Error,
 3132            new EventId(4120, nameof(ConstraintExceptionDeniedError)),
 3133            "Policy constraint '{ConstraintName}' threw during evaluation and was converted to a denied governance decis
 34
 3135    private static readonly Action<ILogger, string, string, string, string, string, Exception?> ThreatContributorExcepti
 3136        LoggerMessage.Define<string, string, string, string, string>(
 3137            LogLevel.Error,
 3138            new EventId(4130, nameof(ThreatContributorExceptionDeniedError)),
 3139            "Threat model contributor '{ContributorName}' threw during evaluation and was converted to a denied governan
 40
 41    private readonly IAsiBackboneConstraint<TContext>[] constraints;
 42    private readonly IThreatModelContributor<TContext>[] threatModelContributors;
 43    private readonly IAsiBackboneDecisionPolicy<TContext>? decisionPolicy;
 44    private readonly ILogger<DefaultAsiBackbonePolicyEvaluator<TContext>>? logger;
 45    private readonly AsiBackbonePolicyEvaluatorOptions options;
 46
 47    /// <summary>
 48    /// Initializes a new instance of the <see cref="DefaultAsiBackbonePolicyEvaluator{TContext}" /> class.
 49    /// </summary>
 50    /// <param name="constraints">The constraints that make up the active policy structure.</param>
 51    /// <param name="decisionPolicy">Optional decision policy applied after constraint composition.</param>
 52    public DefaultAsiBackbonePolicyEvaluator(
 53        IEnumerable<IAsiBackboneConstraint<TContext>> constraints,
 54        IAsiBackboneDecisionPolicy<TContext>? decisionPolicy = null)
 7055        : this(constraints, threatModelContributors: null, decisionPolicy, options: null, logger: null)
 56    {
 6857    }
 58
 59    /// <summary>
 60    /// Initializes a new instance of the <see cref="DefaultAsiBackbonePolicyEvaluator{TContext}" /> class.
 61    /// </summary>
 62    /// <param name="constraints">The constraints that make up the active policy structure.</param>
 63    /// <param name="decisionPolicy">Optional decision policy applied after constraint composition.</param>
 64    /// <param name="options">Evaluator options applied during constraint composition.</param>
 65    public DefaultAsiBackbonePolicyEvaluator(
 66        IEnumerable<IAsiBackboneConstraint<TContext>> constraints,
 67        IAsiBackboneDecisionPolicy<TContext>? decisionPolicy,
 68        AsiBackbonePolicyEvaluatorOptions? options)
 7669        : this(constraints, threatModelContributors: null, decisionPolicy, options, logger: null)
 70    {
 5271    }
 72
 73    /// <summary>
 74    /// Initializes a new instance of the <see cref="DefaultAsiBackbonePolicyEvaluator{TContext}" /> class.
 75    /// </summary>
 76    /// <param name="constraints">The constraints that make up the active policy structure.</param>
 77    /// <param name="decisionPolicy">Optional decision policy applied after constraint composition.</param>
 78    /// <param name="options">Evaluator options applied during constraint composition.</param>
 79    /// <param name="logger">Optional logger used to emit operational warning signals.</param>
 80    public DefaultAsiBackbonePolicyEvaluator(
 81        IEnumerable<IAsiBackboneConstraint<TContext>> constraints,
 82        IAsiBackboneDecisionPolicy<TContext>? decisionPolicy,
 83        AsiBackbonePolicyEvaluatorOptions? options,
 84        ILogger<DefaultAsiBackbonePolicyEvaluator<TContext>>? logger)
 1085        : this(constraints, threatModelContributors: null, decisionPolicy, options, logger)
 86    {
 1087    }
 88
 89    /// <summary>
 90    /// Initializes a new instance of the <see cref="DefaultAsiBackbonePolicyEvaluator{TContext}" /> class.
 91    /// </summary>
 92    /// <param name="constraints">The constraints that make up the active policy structure.</param>
 93    /// <param name="threatModelContributors">Threat model contributors that inspect the context before constraint compo
 94    /// <param name="decisionPolicy">Optional decision policy applied after composition.</param>
 95    public DefaultAsiBackbonePolicyEvaluator(
 96        IEnumerable<IAsiBackboneConstraint<TContext>> constraints,
 97        IEnumerable<IThreatModelContributor<TContext>> threatModelContributors,
 98        IAsiBackboneDecisionPolicy<TContext>? decisionPolicy = null)
 7299        : this(constraints, threatModelContributors, decisionPolicy, options: null, logger: null)
 100    {
 72101    }
 102
 103    /// <summary>
 104    /// Initializes a new instance of the <see cref="DefaultAsiBackbonePolicyEvaluator{TContext}" /> class.
 105    /// </summary>
 106    /// <param name="constraints">The constraints that make up the active policy structure.</param>
 107    /// <param name="threatModelContributors">Threat model contributors that inspect the context before constraint compo
 108    /// <param name="decisionPolicy">Optional decision policy applied after composition.</param>
 109    /// <param name="options">Evaluator options applied during composition.</param>
 110    public DefaultAsiBackbonePolicyEvaluator(
 111        IEnumerable<IAsiBackboneConstraint<TContext>> constraints,
 112        IEnumerable<IThreatModelContributor<TContext>> threatModelContributors,
 113        IAsiBackboneDecisionPolicy<TContext>? decisionPolicy,
 114        AsiBackbonePolicyEvaluatorOptions? options)
 66115        : this(constraints, threatModelContributors, decisionPolicy, options, logger: null)
 116    {
 66117    }
 118
 119    /// <summary>
 120    /// Initializes a new instance of the <see cref="DefaultAsiBackbonePolicyEvaluator{TContext}" /> class.
 121    /// </summary>
 122    /// <param name="constraints">The constraints that make up the active policy structure.</param>
 123    /// <param name="threatModelContributors">Threat model contributors that inspect the context before constraint compo
 124    /// <param name="decisionPolicy">Optional decision policy applied after composition.</param>
 125    /// <param name="options">Evaluator options applied during composition.</param>
 126    /// <param name="logger">Optional logger used to emit operational warning signals.</param>
 294127    public DefaultAsiBackbonePolicyEvaluator(
 294128        IEnumerable<IAsiBackboneConstraint<TContext>> constraints,
 294129        IEnumerable<IThreatModelContributor<TContext>>? threatModelContributors,
 294130        IAsiBackboneDecisionPolicy<TContext>? decisionPolicy,
 294131        AsiBackbonePolicyEvaluatorOptions? options,
 294132        ILogger<DefaultAsiBackbonePolicyEvaluator<TContext>>? logger)
 133    {
 294134        ArgumentNullException.ThrowIfNull(constraints);
 135
 136        // Keep exact-sized private snapshots rather than wrapping caller-owned lists.
 137        // This avoids per-evaluator ReadOnlyCollection<T> wrappers and prevents later caller mutations
 138        // from changing deterministic constraint/contributor order or behavior.
 292139        this.constraints = [.. constraints];
 292140        this.threatModelContributors = threatModelContributors is null ? [] : [.. threatModelContributors];
 292141        this.decisionPolicy = decisionPolicy;
 292142        this.logger = logger;
 292143        this.options = options ?? new AsiBackbonePolicyEvaluatorOptions();
 292144        this.options.Validate();
 268145    }
 146
 147    /// <inheritdoc />
 148    public async ValueTask<GovernanceDecision> EvaluateAsync(
 149        TContext context,
 150        CancellationToken cancellationToken = default)
 151    {
 394152        ArgumentNullException.ThrowIfNull(context);
 392153        cancellationToken.ThrowIfCancellationRequested();
 154
 388155        ThreatEvaluationResult threatEvaluation = await EvaluateThreatModelContributorsAsync(
 388156            context,
 388157            cancellationToken)
 388158            .ConfigureAwait(false);
 159
 368160        if (threatEvaluation.BlockingDecision is GovernanceDecision threatDecision)
 161        {
 70162            return await ApplyDecisionPolicyAsync(
 70163                context,
 70164                threatDecision,
 70165                EmptyConstraintResults,
 70166                protectedThreatDecision: threatDecision,
 70167                cancellationToken)
 70168                .ConfigureAwait(false);
 169        }
 170
 298171        if (constraints.Length == 0)
 172        {
 21173            return await EvaluateEmptyPolicyAsync(
 21174                context,
 21175                threatEvaluation.WarningReasons,
 21176                cancellationToken)
 21177                .ConfigureAwait(false);
 178        }
 179
 277180        List<ConstraintEvaluationResult>? results = CreateConstraintResultsBuffer();
 277181        var denials = new OperationReasonAccumulator();
 277182        var warnings = new OperationReasonAccumulator();
 277183        warnings.AddRange(threatEvaluation.WarningReasons);
 184
 1525185        foreach (IAsiBackboneConstraint<TContext> constraint in constraints)
 186        {
 515187            cancellationToken.ThrowIfCancellationRequested();
 188
 189            ConstraintEvaluationResult result;
 190            try
 191            {
 513192                result = await constraint
 513193                    .EvaluateAsync(context, cancellationToken)
 513194                    .ConfigureAwait(false);
 478195            }
 35196            catch (Exception exception) when (ShouldConvertExceptionToDenial(exception, options.TreatConstraintException
 197            {
 15198                return await CreateConstraintExceptionDecisionAsync(
 15199                    context,
 15200                    constraint,
 15201                    results,
 15202                    exception,
 15203                    cancellationToken)
 15204                    .ConfigureAwait(false);
 205            }
 206
 478207            results?.Add(result);
 208
 478209            if (!AccumulateConstraintResult(
 478210                    result,
 478211                    ref denials,
 478212                    ref warnings,
 478213                    options.ShortCircuitOnFirstDenial))
 214            {
 215                break;
 216            }
 456217        }
 218
 240219        GovernanceDecision composedDecision = Compose(
 240220            context,
 240221            denials,
 240222            warnings,
 240223            includeWarningsWhenDenied: options.ShortCircuitOnFirstDenial);
 224
 240225        return await ApplyDecisionPolicyAsync(
 240226            context,
 240227            composedDecision,
 240228            CreateConstraintResultsView(results),
 240229            CreateProtectedThreatWarningDecision(context, threatEvaluation.WarningReasons, composedDecision),
 240230            cancellationToken)
 240231            .ConfigureAwait(false);
 346232    }
 233
 234    private static bool ShouldConvertExceptionToDenial(Exception exception, bool treatExceptionAsDenial)
 235    {
 63236        return treatExceptionAsDenial &&
 63237               exception is not OperationCanceledException &&
 63238               !IsCriticalException(exception);
 239    }
 240
 241    private static bool IsCriticalException(Exception exception)
 242    {
 57243        return exception is OutOfMemoryException or
 57244               StackOverflowException or
 57245               AccessViolationException or
 57246               AppDomainUnloadedException or
 57247               BadImageFormatException or
 57248               InvalidProgramException ||
 57249               (exception.InnerException is not null && IsCriticalException(exception.InnerException));
 250    }
 251
 252    private List<ConstraintEvaluationResult>? CreateConstraintResultsBuffer()
 253    {
 277254        return decisionPolicy is null
 277255            ? null
 277256            : new List<ConstraintEvaluationResult>(constraints.Length);
 257    }
 258
 259    private static bool AccumulateConstraintResult(
 260        ConstraintEvaluationResult result,
 261        ref OperationReasonAccumulator denials,
 262        ref OperationReasonAccumulator warnings,
 263        bool shortCircuitOnFirstDenial)
 264    {
 478265        if (result.IsDenied)
 266        {
 66267            denials.AddRange(result.Reasons);
 268
 66269            if (!shortCircuitOnFirstDenial)
 270            {
 44271                warnings = default;
 272            }
 273
 66274            return !shortCircuitOnFirstDenial;
 275        }
 276
 412277        if (result.IsWarning && denials.Count == 0)
 278        {
 182279            warnings.AddRange(result.Reasons);
 280        }
 281
 412282        return true;
 283    }
 284
 285    private async ValueTask<GovernanceDecision> EvaluateEmptyPolicyAsync(
 286        TContext context,
 287        IReadOnlyList<OperationReason> threatWarningReasons,
 288        CancellationToken cancellationToken)
 289    {
 21290        if (!options.DenyWhenNoConstraints)
 291        {
 6292            LogEmptyPolicyAllowed(context);
 293        }
 294
 21295        GovernanceDecision noConstraintDecision = CreateNoConstraintDecision(
 21296            context,
 21297            threatWarningReasons,
 21298            options.DenyWhenNoConstraints);
 299
 21300        return await ApplyDecisionPolicyAsync(
 21301            context,
 21302            noConstraintDecision,
 21303            EmptyConstraintResults,
 21304            protectedThreatDecision: threatWarningReasons.Count > 0 ? noConstraintDecision : null,
 21305            cancellationToken)
 21306            .ConfigureAwait(false);
 21307    }
 308
 309    private static GovernanceDecision Compose(
 310        TContext context,
 311        OperationReasonAccumulator denials,
 312        OperationReasonAccumulator warnings,
 313        bool includeWarningsWhenDenied)
 314    {
 240315        return denials.Count > 0
 240316            ? includeWarningsWhenDenied && warnings.Count > 0
 240317                ? GovernanceDecision.Deny(
 240318                    warnings.ConcatAsArray(denials),
 240319                    correlationId: context.CorrelationId,
 240320                    policyVersion: context.PolicyVersion,
 240321                    policyHash: context.PolicyHash)
 240322                : CreateDeniedDecision(context, denials)
 240323            : warnings.Count > 0
 240324            ? CreateWarningDecision(context, warnings)
 240325            : GovernanceDecision.Allow(
 240326            correlationId: context.CorrelationId,
 240327            policyVersion: context.PolicyVersion,
 240328            policyHash: context.PolicyHash);
 329    }
 330
 331    private static GovernanceDecision CreateNoConstraintDecision(
 332        TContext context,
 333        IReadOnlyList<OperationReason> threatWarningReasons)
 334    {
 6335        return threatWarningReasons.Count > 0
 6336            ? GovernanceDecision.Warning(
 6337                threatWarningReasons,
 6338                correlationId: context.CorrelationId,
 6339                policyVersion: context.PolicyVersion,
 6340                policyHash: context.PolicyHash)
 6341            : GovernanceDecision.Allow(
 6342            correlationId: context.CorrelationId,
 6343            policyVersion: context.PolicyVersion,
 6344            policyHash: context.PolicyHash);
 345    }
 346
 347    private GovernanceDecision CreateNoConstraintDecision(
 348        TContext context,
 349        IReadOnlyList<OperationReason> threatWarningReasons,
 350        bool denyWhenNoConstraints)
 351    {
 21352        return !denyWhenNoConstraints
 21353            ? CreateNoConstraintDecision(context, threatWarningReasons)
 21354            : GovernanceDecision.Deny(
 21355            options.NoConstraintsReasonCode,
 21356            options.NoConstraintsReasonMessage,
 21357            correlationId: context.CorrelationId,
 21358            policyVersion: context.PolicyVersion,
 21359            policyHash: context.PolicyHash);
 360    }
 361
 362    private static GovernanceDecision CreateDeniedDecision(
 363        TContext context,
 364        OperationReasonAccumulator denials)
 365    {
 38366        return denials.Count == 1
 38367            ? GovernanceDecision.Deny(
 38368                denials.FirstReason!,
 38369                correlationId: context.CorrelationId,
 38370                policyVersion: context.PolicyVersion,
 38371                policyHash: context.PolicyHash)
 38372            : GovernanceDecision.Deny(
 38373                denials.AsReadOnlyList(),
 38374                correlationId: context.CorrelationId,
 38375                policyVersion: context.PolicyVersion,
 38376                policyHash: context.PolicyHash);
 377    }
 378
 379    private static GovernanceDecision CreateWarningDecision(
 380        TContext context,
 381        OperationReasonAccumulator warnings)
 382    {
 158383        return warnings.Count == 1
 158384            ? GovernanceDecision.Warning(
 158385                warnings.FirstReason!,
 158386                correlationId: context.CorrelationId,
 158387                policyVersion: context.PolicyVersion,
 158388                policyHash: context.PolicyHash)
 158389            : GovernanceDecision.Warning(
 158390                warnings.AsReadOnlyList(),
 158391                correlationId: context.CorrelationId,
 158392                policyVersion: context.PolicyVersion,
 158393                policyHash: context.PolicyHash);
 394    }
 395
 396    private static IReadOnlyList<ConstraintEvaluationResult> CreateConstraintResultsView(
 397        List<ConstraintEvaluationResult>? results)
 398    {
 255399        return results is null || results.Count == 0
 255400            ? EmptyConstraintResults
 255401            : results.AsReadOnly();
 402    }
 403
 404    private static GovernanceDecision? CreateProtectedThreatWarningDecision(
 405        TContext context,
 406        IReadOnlyList<OperationReason> threatWarningReasons,
 407        GovernanceDecision composedDecision)
 408    {
 240409        return threatWarningReasons.Count > 0 && composedDecision.CanProceed
 240410            ? GovernanceDecision.Warning(
 240411                threatWarningReasons,
 240412                correlationId: context.CorrelationId,
 240413                policyVersion: context.PolicyVersion,
 240414                policyHash: context.PolicyHash)
 240415            : null;
 416    }
 417
 418    private async ValueTask<GovernanceDecision> ApplyDecisionPolicyAsync(
 419        TContext context,
 420        GovernanceDecision decision,
 421        IReadOnlyList<ConstraintEvaluationResult> results,
 422        GovernanceDecision? protectedThreatDecision,
 423        CancellationToken cancellationToken)
 424    {
 346425        if (decisionPolicy is null)
 426        {
 298427            return decision;
 428        }
 429
 48430        GovernanceDecision policyDecision = await decisionPolicy
 48431            .ApplyAsync(context, decision, results, cancellationToken)
 48432            .ConfigureAwait(false);
 433
 48434        return options.PreventThreatAssessmentAllowDowngrade && protectedThreatDecision is not null && policyDecision.Is
 48435            ? protectedThreatDecision
 48436            : policyDecision;
 346437    }
 438
 439    private async ValueTask<ThreatEvaluationResult> EvaluateThreatModelContributorsAsync(
 440        TContext context,
 441        CancellationToken cancellationToken)
 442    {
 388443        if (threatModelContributors.Length == 0)
 444        {
 262445            return ThreatEvaluationResult.Empty;
 446        }
 447
 126448        List<OperationReason>? reasons = null;
 126449        OperationReason? selectedReason = null;
 126450        GovernanceDecisionOutcome? selectedOutcome = null;
 451
 522452        foreach (IThreatModelContributor<TContext> contributor in threatModelContributors)
 453        {
 150454            cancellationToken.ThrowIfCancellationRequested();
 455
 456            ThreatAssessment? assessment;
 457            try
 458            {
 150459                assessment = await contributor
 150460                    .AssessAsync(context, cancellationToken)
 150461                    .ConfigureAwait(false);
 122462            }
 28463            catch (Exception exception) when (ShouldConvertExceptionToDenial(exception, options.TreatThreatContributorEx
 464            {
 10465                return CreateThreatContributorExceptionResult(context, contributor, exception);
 466            }
 467
 122468            if (assessment is null || !assessment.IsActionable)
 469            {
 470                continue;
 471            }
 472
 116473            GovernanceDecisionOutcome effectiveOutcome = GetEffectiveThreatOutcome(assessment);
 114474            var reason = assessment.ToOperationReason(GetContributorName(contributor), effectiveOutcome);
 114475            GovernanceDecisionOutcome moreRestrictiveOutcome = SelectMoreRestrictiveOutcome(selectedOutcome, effectiveOu
 476
 114477            if (selectedOutcome != moreRestrictiveOutcome)
 478            {
 104479                selectedOutcome = moreRestrictiveOutcome;
 104480                selectedReason = reason;
 481            }
 482
 114483            reasons ??= [];
 114484            reasons.Add(reason);
 114485        }
 486
 96487        return reasons is null || selectedOutcome is null
 96488            ? ThreatEvaluationResult.Empty
 96489            : CreateThreatEvaluationResult(context, selectedOutcome.Value, selectedReason!, reasons.AsReadOnly());
 368490    }
 491
 492    private ThreatEvaluationResult CreateThreatContributorExceptionResult(
 493        TContext context,
 494        IThreatModelContributor<TContext> contributor,
 495        Exception exception)
 496    {
 10497        string contributorName = GetContributorName(contributor);
 10498        LogThreatContributorExceptionDenied(context, contributorName, exception);
 499
 10500        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 10501        {
 10502            ["threat.contributor"] = contributorName,
 10503            ["threat.failure"] = exception.GetType().Name
 10504        };
 505
 10506        var reason = OperationReason.Create(
 10507            options.ThreatContributorExceptionReasonCode,
 10508            options.ThreatContributorExceptionReasonMessage,
 10509            metadata);
 510
 10511        var decision = GovernanceDecision.Deny(
 10512            reason,
 10513            correlationId: context.CorrelationId,
 10514            policyVersion: context.PolicyVersion,
 10515            policyHash: context.PolicyHash);
 516
 10517        return ThreatEvaluationResult.ForBlockingDecision(decision);
 518    }
 519
 520    private static ThreatEvaluationResult CreateThreatEvaluationResult(
 521        TContext context,
 522        GovernanceDecisionOutcome outcome,
 523        OperationReason selectedReason,
 524        ReadOnlyCollection<OperationReason> reasons)
 525    {
 92526        return outcome switch
 92527        {
 44528            GovernanceDecisionOutcome.Denied => ThreatEvaluationResult.ForBlockingDecision(
 44529                GovernanceDecision.Deny(
 44530                    reasons,
 44531                    correlationId: context.CorrelationId,
 44532                    policyVersion: context.PolicyVersion,
 44533                    policyHash: context.PolicyHash)),
 6534            GovernanceDecisionOutcome.Deferred => ThreatEvaluationResult.ForBlockingDecision(
 6535                GovernanceDecision.Defer(
 6536                    selectedReason.Code,
 6537                    selectedReason.Message,
 6538                    correlationId: context.CorrelationId,
 6539                    policyVersion: context.PolicyVersion,
 6540                    policyHash: context.PolicyHash)),
 6541            GovernanceDecisionOutcome.AcknowledgmentRequired => ThreatEvaluationResult.ForBlockingDecision(
 6542                GovernanceDecision.RequireAcknowledgment(
 6543                    selectedReason.Code,
 6544                    selectedReason.Message,
 6545                    correlationId: context.CorrelationId,
 6546                    policyVersion: context.PolicyVersion,
 6547                    policyHash: context.PolicyHash)),
 4548            GovernanceDecisionOutcome.EscalationRecommended => ThreatEvaluationResult.ForBlockingDecision(
 4549                GovernanceDecision.Escalate(
 4550                    selectedReason.Code,
 4551                    selectedReason.Message,
 4552                    correlationId: context.CorrelationId,
 4553                    policyVersion: context.PolicyVersion,
 4554                    policyHash: context.PolicyHash)),
 30555            GovernanceDecisionOutcome.Warning => ThreatEvaluationResult.ForWarningReasons(reasons),
 2556            GovernanceDecisionOutcome.Allowed => throw CreateInvalidAllowedThreatOutcomeException(),
 0557            _ => ThreatEvaluationResult.Empty
 92558        };
 559    }
 560
 561    private static GovernanceDecisionOutcome GetEffectiveThreatOutcome(ThreatAssessment assessment)
 562    {
 116563        return assessment.RecommendedOutcome is GovernanceDecisionOutcome.Allowed
 116564            ? throw CreateInvalidAllowedThreatOutcomeException()
 116565            : assessment.RecommendedOutcome;
 566    }
 567
 568    private static InvalidOperationException CreateInvalidAllowedThreatOutcomeException()
 569    {
 6570        return new InvalidOperationException(InvalidAllowedThreatOutcomeMessage);
 571    }
 572
 573    private static GovernanceDecisionOutcome SelectMoreRestrictiveOutcome(
 574        GovernanceDecisionOutcome? current,
 575        GovernanceDecisionOutcome candidate)
 576    {
 114577        return current is null
 114578            ? candidate
 114579            : GetThreatOutcomeRank(candidate) > GetThreatOutcomeRank(current.Value)
 114580            ? candidate
 114581            : current.Value;
 582    }
 583
 584    private static int GetThreatOutcomeRank(GovernanceDecisionOutcome outcome)
 585    {
 50586        return outcome switch
 50587        {
 4588            GovernanceDecisionOutcome.Denied => 5,
 8589            GovernanceDecisionOutcome.EscalationRecommended => 4,
 8590            GovernanceDecisionOutcome.AcknowledgmentRequired => 3,
 8591            GovernanceDecisionOutcome.Deferred => 2,
 20592            GovernanceDecisionOutcome.Warning => 1,
 2593            GovernanceDecisionOutcome.Allowed => throw CreateInvalidAllowedThreatOutcomeException(),
 0594            _ => 0
 50595        };
 596    }
 597
 598    private static string GetContributorName(IThreatModelContributor<TContext> contributor)
 599    {
 124600        return string.IsNullOrWhiteSpace(contributor.Name)
 124601            ? "<unnamed>"
 124602            : contributor.Name.Trim();
 603    }
 604
 605    private async ValueTask<GovernanceDecision> CreateConstraintExceptionDecisionAsync(
 606        TContext context,
 607        IAsiBackboneConstraint<TContext> constraint,
 608        List<ConstraintEvaluationResult>? results,
 609        Exception exception,
 610        CancellationToken cancellationToken)
 611    {
 15612        LogConstraintExceptionDenied(context, constraint.Name, exception);
 613
 15614        var exceptionResult = ConstraintEvaluationResult.Deny(
 15615            options.ConstraintExceptionReasonCode,
 15616            options.ConstraintExceptionReasonMessage);
 15617        results?.Add(exceptionResult);
 618
 15619        var exceptionDecision = GovernanceDecision.Deny(
 15620            exceptionResult.Reasons,
 15621            correlationId: context.CorrelationId,
 15622            policyVersion: context.PolicyVersion,
 15623            policyHash: context.PolicyHash);
 624
 15625        return await ApplyDecisionPolicyAsync(
 15626            context,
 15627            exceptionDecision,
 15628            CreateConstraintResultsView(results),
 15629            protectedThreatDecision: null,
 15630            cancellationToken)
 15631            .ConfigureAwait(false);
 15632    }
 633
 634    private void LogEmptyPolicyAllowed(TContext context)
 635    {
 6636        if (logger is null)
 637        {
 4638            return;
 639        }
 640
 2641        EmptyPolicyAllowedWarning(
 2642            logger,
 2643            context.CorrelationId ?? string.Empty,
 2644            context.PolicyVersion ?? string.Empty,
 2645            context.PolicyHash ?? string.Empty,
 2646            null);
 2647    }
 648
 649    private void LogConstraintExceptionDenied(
 650        TContext context,
 651        string constraintName,
 652        Exception exception)
 653    {
 15654        if (logger is null)
 655        {
 9656            return;
 657        }
 658
 6659        ConstraintExceptionDeniedError(
 6660            logger,
 6661            string.IsNullOrWhiteSpace(constraintName) ? "<unnamed>" : constraintName,
 6662            exception.GetType().Name,
 6663            context.CorrelationId ?? string.Empty,
 6664            context.PolicyVersion ?? string.Empty,
 6665            context.PolicyHash ?? string.Empty,
 6666            exception);
 6667    }
 668
 669    private void LogThreatContributorExceptionDenied(
 670        TContext context,
 671        string contributorName,
 672        Exception exception)
 673    {
 10674        if (logger is null)
 675        {
 10676            return;
 677        }
 678
 0679        ThreatContributorExceptionDeniedError(
 0680            logger,
 0681            contributorName,
 0682            exception.GetType().Name,
 0683            context.CorrelationId ?? string.Empty,
 0684            context.PolicyVersion ?? string.Empty,
 0685            context.PolicyHash ?? string.Empty,
 0686            exception);
 0687    }
 688
 689    private sealed class ThreatEvaluationResult
 690    {
 31691        private static readonly IReadOnlyList<OperationReason> EmptyReasons =
 31692            Array.AsReadOnly(Array.Empty<OperationReason>());
 693
 131694        private ThreatEvaluationResult(
 131695            IReadOnlyList<OperationReason> warningReasons,
 131696            GovernanceDecision? blockingDecision)
 697        {
 131698            WarningReasons = warningReasons;
 131699            BlockingDecision = blockingDecision;
 131700        }
 701
 299702        public static ThreatEvaluationResult Empty { get; } = new(EmptyReasons, blockingDecision: null);
 703
 538704        public IReadOnlyList<OperationReason> WarningReasons { get; }
 705
 368706        public GovernanceDecision? BlockingDecision { get; }
 707
 708        public static ThreatEvaluationResult ForWarningReasons(IReadOnlyList<OperationReason> reasons)
 709        {
 30710            return new ThreatEvaluationResult(reasons, blockingDecision: null);
 711        }
 712
 713        public static ThreatEvaluationResult ForBlockingDecision(GovernanceDecision decision)
 714        {
 70715            return new ThreatEvaluationResult(EmptyReasons, decision);
 716        }
 717    }
 718
 719    private struct OperationReasonAccumulator
 720    {
 721        private List<OperationReason>? additionalReasons;
 722
 476723        public OperationReason? FirstReason { get; private set; }
 724
 1528725        public int Count { get; private set; }
 726
 727        public void AddRange(IReadOnlyList<OperationReason> reasons)
 728        {
 1598729            for (int index = 0; index < reasons.Count; index++)
 730            {
 274731                Add(reasons[index]);
 732            }
 525733        }
 734
 735        public readonly OperationReason[] ConcatAsArray(OperationReasonAccumulator other)
 736        {
 16737            int totalCount = Count + other.Count;
 16738            if (totalCount == 0)
 739            {
 0740                return [];
 741            }
 742
 16743            var reasons = new OperationReason[totalCount];
 16744            int nextIndex = CopyTo(reasons, 0);
 16745            _ = other.CopyTo(reasons, nextIndex);
 746
 16747            return reasons;
 748        }
 749
 750        public readonly ReadOnlyCollection<OperationReason> AsReadOnlyList()
 751        {
 20752            return Count switch
 20753            {
 0754                0 => Array.AsReadOnly(Array.Empty<OperationReason>()),
 0755                1 => Array.AsReadOnly([FirstReason!]),
 20756                _ => additionalReasons!.AsReadOnly()
 20757            };
 758        }
 759
 760        private void Add(OperationReason? reason)
 761        {
 274762            if (reason is null)
 763            {
 0764                return;
 765            }
 766
 274767            if (Count == 0)
 768            {
 244769                FirstReason = reason;
 244770                Count = 1;
 244771                return;
 772            }
 773
 30774            additionalReasons ??= [FirstReason!];
 30775            additionalReasons.Add(reason);
 30776            Count++;
 30777        }
 778
 779        private readonly int CopyTo(OperationReason[] destination, int startIndex)
 780        {
 32781            if (Count == 0)
 782            {
 0783                return startIndex;
 784            }
 785
 32786            if (Count == 1)
 787            {
 26788                destination[startIndex] = FirstReason!;
 26789                return startIndex + 1;
 790            }
 791
 6792            additionalReasons!.CopyTo(destination, startIndex);
 6793            return startIndex + additionalReasons.Count;
 794        }
 795    }
 796}

Methods/Properties

.cctor()
.ctor(System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.Constraints.IAsiBackboneConstraint`1<TContext>>,AsiBackbone.Core.Evaluation.IAsiBackboneDecisionPolicy`1<TContext>)
.ctor(System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.Constraints.IAsiBackboneConstraint`1<TContext>>,AsiBackbone.Core.Evaluation.IAsiBackboneDecisionPolicy`1<TContext>,AsiBackbone.Core.Evaluation.AsiBackbonePolicyEvaluatorOptions)
.ctor(System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.Constraints.IAsiBackboneConstraint`1<TContext>>,AsiBackbone.Core.Evaluation.IAsiBackboneDecisionPolicy`1<TContext>,AsiBackbone.Core.Evaluation.AsiBackbonePolicyEvaluatorOptions,Microsoft.Extensions.Logging.ILogger`1<AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1<TContext>>)
.ctor(System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.Constraints.IAsiBackboneConstraint`1<TContext>>,System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.ThreatModeling.IThreatModelContributor`1<TContext>>,AsiBackbone.Core.Evaluation.IAsiBackboneDecisionPolicy`1<TContext>)
.ctor(System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.Constraints.IAsiBackboneConstraint`1<TContext>>,System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.ThreatModeling.IThreatModelContributor`1<TContext>>,AsiBackbone.Core.Evaluation.IAsiBackboneDecisionPolicy`1<TContext>,AsiBackbone.Core.Evaluation.AsiBackbonePolicyEvaluatorOptions)
.ctor(System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.Constraints.IAsiBackboneConstraint`1<TContext>>,System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.ThreatModeling.IThreatModelContributor`1<TContext>>,AsiBackbone.Core.Evaluation.IAsiBackboneDecisionPolicy`1<TContext>,AsiBackbone.Core.Evaluation.AsiBackbonePolicyEvaluatorOptions,Microsoft.Extensions.Logging.ILogger`1<AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1<TContext>>)
EvaluateAsync()
ShouldConvertExceptionToDenial(System.Exception,System.Boolean)
IsCriticalException(System.Exception)
CreateConstraintResultsBuffer()
AccumulateConstraintResult(AsiBackbone.Core.Constraints.ConstraintEvaluationResult,AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1/OperationReasonAccumulator<TContext>&,AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1/OperationReasonAccumulator<TContext>&,System.Boolean)
EvaluateEmptyPolicyAsync()
Compose(TContext,AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1/OperationReasonAccumulator<TContext>,AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1/OperationReasonAccumulator<TContext>,System.Boolean)
CreateNoConstraintDecision(TContext,System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>)
CreateNoConstraintDecision(TContext,System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>,System.Boolean)
CreateDeniedDecision(TContext,AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1/OperationReasonAccumulator<TContext>)
CreateWarningDecision(TContext,AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1/OperationReasonAccumulator<TContext>)
CreateConstraintResultsView(System.Collections.Generic.List`1<AsiBackbone.Core.Constraints.ConstraintEvaluationResult>)
CreateProtectedThreatWarningDecision(TContext,System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>,AsiBackbone.Core.Decisions.GovernanceDecision)
ApplyDecisionPolicyAsync()
EvaluateThreatModelContributorsAsync()
CreateThreatContributorExceptionResult(TContext,AsiBackbone.Core.ThreatModeling.IThreatModelContributor`1<TContext>,System.Exception)
CreateThreatEvaluationResult(TContext,AsiBackbone.Core.Decisions.GovernanceDecisionOutcome,AsiBackbone.Core.Results.OperationReason,System.Collections.ObjectModel.ReadOnlyCollection`1<AsiBackbone.Core.Results.OperationReason>)
GetEffectiveThreatOutcome(AsiBackbone.Core.ThreatModeling.ThreatAssessment)
CreateInvalidAllowedThreatOutcomeException()
SelectMoreRestrictiveOutcome(System.Nullable`1<AsiBackbone.Core.Decisions.GovernanceDecisionOutcome>,AsiBackbone.Core.Decisions.GovernanceDecisionOutcome)
GetThreatOutcomeRank(AsiBackbone.Core.Decisions.GovernanceDecisionOutcome)
GetContributorName(AsiBackbone.Core.ThreatModeling.IThreatModelContributor`1<TContext>)
CreateConstraintExceptionDecisionAsync()
LogEmptyPolicyAllowed(TContext)
LogConstraintExceptionDenied(TContext,System.String,System.Exception)
LogThreatContributorExceptionDenied(TContext,System.String,System.Exception)
.cctor()
.ctor(System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>,AsiBackbone.Core.Decisions.GovernanceDecision)
get_Empty()
get_WarningReasons()
get_BlockingDecision()
ForWarningReasons(System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>)
ForBlockingDecision(AsiBackbone.Core.Decisions.GovernanceDecision)
get_FirstReason()
get_Count()
AddRange(System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>)
ConcatAsArray(AsiBackbone.Core.Evaluation.DefaultAsiBackbonePolicyEvaluator`1/OperationReasonAccumulator<TContext>)
AsReadOnlyList()
Add(AsiBackbone.Core.Results.OperationReason)
CopyTo(AsiBackbone.Core.Results.OperationReason[],System.Int32)