< Summary

Information
Class: AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator<T>
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Evaluation/DefaultGovernancePolicyEvaluator.cs
Line coverage
95%
Covered lines: 372
Uncovered lines: 16
Coverable lines: 388
Total lines: 724
Line coverage: 95.8%
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

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Evaluation/DefaultGovernancePolicyEvaluator.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 DefaultGovernancePolicyEvaluator<TContext> : IGovernancePolicyEvaluator<TContext>
 15    where TContext : IGovernanceEvaluationContext
 16{
 1717    private static readonly IReadOnlyList<ConstraintEvaluationResult> EmptyConstraintResults =
 1718        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
 1723    private static readonly Action<ILogger, string, string, string, Exception?> EmptyPolicyAllowedWarning =
 1724        LoggerMessage.Define<string, string, string>(
 1725            LogLevel.Warning,
 1726            new EventId(4110, nameof(EmptyPolicyAllowedWarning)),
 1727            "Policy evaluation ran with zero constraints while DenyWhenNoConstraints is false; default empty-policy beha
 28
 1729    private static readonly Action<ILogger, string, string, string, string, string, Exception?> ConstraintExceptionDenie
 1730        LoggerMessage.Define<string, string, string, string, string>(
 1731            LogLevel.Error,
 1732            new EventId(4120, nameof(ConstraintExceptionDeniedError)),
 1733            "Policy constraint '{ConstraintName}' threw during evaluation and was converted to a denied governance decis
 34
 1735    private static readonly Action<ILogger, string, string, string, string, string, Exception?> ThreatContributorExcepti
 1736        LoggerMessage.Define<string, string, string, string, string>(
 1737            LogLevel.Error,
 1738            new EventId(4130, nameof(ThreatContributorExceptionDeniedError)),
 1739            "Threat model contributor '{ContributorName}' threw during evaluation and was converted to a denied governan
 40
 41    private readonly IGovernanceConstraint<TContext>[] constraints;
 42    private readonly IThreatModelContributor<TContext>[] threatModelContributors;
 43    private readonly IGovernanceDecisionPolicy<TContext>? decisionPolicy;
 44    private readonly ILogger<DefaultGovernancePolicyEvaluator<TContext>>? logger;
 45    private readonly GovernancePolicyOptions options;
 46
 47    /// <summary>
 48    /// Initializes a new instance of the <see cref="DefaultGovernancePolicyEvaluator{TContext}" /> class.
 49    /// </summary>
 50    /// <param name="constraints">The constraints that make up the active policy structure.</param>
 51    /// <param name="threatModelContributors">Threat model contributors that inspect the context before constraint compo
 52    /// <param name="decisionPolicy">Optional decision policy applied after composition.</param>
 53    /// <param name="options">Evaluator options applied during composition.</param>
 54    /// <param name="logger">Optional logger used to emit operational warning signals.</param>
 15655    public DefaultGovernancePolicyEvaluator(
 15656        IEnumerable<IGovernanceConstraint<TContext>> constraints,
 15657        IEnumerable<IThreatModelContributor<TContext>>? threatModelContributors,
 15658        IGovernanceDecisionPolicy<TContext>? decisionPolicy,
 15659        GovernancePolicyOptions? options,
 15660        ILogger<DefaultGovernancePolicyEvaluator<TContext>>? logger)
 61    {
 15662        ArgumentNullException.ThrowIfNull(constraints);
 63
 64        // Keep exact-sized private snapshots rather than wrapping caller-owned lists.
 65        // This avoids per-evaluator ReadOnlyCollection<T> wrappers and prevents later caller mutations
 66        // from changing deterministic constraint/contributor order or behavior.
 15567        this.constraints = [.. constraints];
 15568        this.threatModelContributors = threatModelContributors is null ? [] : [.. threatModelContributors];
 15569        this.decisionPolicy = decisionPolicy;
 15570        this.logger = logger;
 15571        this.options = options ?? new GovernancePolicyOptions();
 15572        this.options.Validate();
 14373    }
 74
 75    /// <inheritdoc />
 76    public async ValueTask<GovernanceDecision> EvaluateAsync(
 77        TContext context,
 78        CancellationToken cancellationToken = default)
 79    {
 20680        ArgumentNullException.ThrowIfNull(context);
 20581        cancellationToken.ThrowIfCancellationRequested();
 82
 20383        ThreatEvaluationResult threatEvaluation = await EvaluateThreatModelContributorsAsync(
 20384            context,
 20385            cancellationToken)
 20386            .ConfigureAwait(false);
 87
 19388        if (threatEvaluation.BlockingDecision is GovernanceDecision threatDecision)
 89        {
 3790            return await ApplyDecisionPolicyAsync(
 3791                context,
 3792                threatDecision,
 3793                EmptyConstraintResults,
 3794                protectedThreatDecision: threatDecision,
 3795                cancellationToken)
 3796                .ConfigureAwait(false);
 97        }
 98
 15699        if (constraints.Length == 0)
 100        {
 13101            return await EvaluateEmptyPolicyAsync(
 13102                context,
 13103                threatEvaluation.WarningReasons,
 13104                cancellationToken)
 13105                .ConfigureAwait(false);
 106        }
 107
 143108        List<ConstraintEvaluationResult>? results = CreateConstraintResultsBuffer();
 143109        var denials = new OperationReasonAccumulator();
 143110        var warnings = new OperationReasonAccumulator();
 143111        warnings.AddRange(threatEvaluation.WarningReasons);
 112
 786113        foreach (IGovernanceConstraint<TContext> constraint in constraints)
 114        {
 265115            cancellationToken.ThrowIfCancellationRequested();
 116
 117            ConstraintEvaluationResult result;
 118            try
 119            {
 264120                result = await constraint
 264121                    .EvaluateAsync(context, cancellationToken)
 264122                    .ConfigureAwait(false);
 246123            }
 18124            catch (Exception exception) when (ShouldConvertExceptionToDenial(exception, options.TreatConstraintException
 125            {
 8126                return await CreateConstraintExceptionDecisionAsync(
 8127                    context,
 8128                    constraint,
 8129                    results,
 8130                    exception,
 8131                    cancellationToken)
 8132                    .ConfigureAwait(false);
 133            }
 134
 246135            results?.Add(result);
 136
 246137            if (!AccumulateConstraintResult(
 246138                    result,
 246139                    ref denials,
 246140                    ref warnings,
 246141                    options.ShortCircuitOnFirstDenial))
 142            {
 143                break;
 144            }
 235145        }
 146
 124147        GovernanceDecision composedDecision = Compose(
 124148            context,
 124149            denials,
 124150            warnings,
 124151            includeWarningsWhenDenied: options.ShortCircuitOnFirstDenial);
 152
 124153        return await ApplyDecisionPolicyAsync(
 124154            context,
 124155            composedDecision,
 124156            CreateConstraintResultsView(results),
 124157            CreateProtectedThreatWarningDecision(context, threatEvaluation.WarningReasons, composedDecision),
 124158            cancellationToken)
 124159            .ConfigureAwait(false);
 182160    }
 161
 162    private static bool ShouldConvertExceptionToDenial(Exception exception, bool treatExceptionAsDenial)
 163    {
 32164        return treatExceptionAsDenial &&
 32165               exception is not OperationCanceledException &&
 32166               !IsCriticalException(exception);
 167    }
 168
 169    private static bool IsCriticalException(Exception exception)
 170    {
 29171        return exception is OutOfMemoryException or
 29172               StackOverflowException or
 29173               AccessViolationException or
 29174               AppDomainUnloadedException or
 29175               BadImageFormatException or
 29176               InvalidProgramException ||
 29177               (exception.InnerException is not null && IsCriticalException(exception.InnerException));
 178    }
 179
 180    private List<ConstraintEvaluationResult>? CreateConstraintResultsBuffer()
 181    {
 143182        return decisionPolicy is null
 143183            ? null
 143184            : new List<ConstraintEvaluationResult>(constraints.Length);
 185    }
 186
 187    private static bool AccumulateConstraintResult(
 188        ConstraintEvaluationResult result,
 189        ref OperationReasonAccumulator denials,
 190        ref OperationReasonAccumulator warnings,
 191        bool shortCircuitOnFirstDenial)
 192    {
 246193        if (result.IsDenied)
 194        {
 33195            denials.AddRange(result.Reasons);
 196
 33197            if (!shortCircuitOnFirstDenial)
 198            {
 22199                warnings = default;
 200            }
 201
 33202            return !shortCircuitOnFirstDenial;
 203        }
 204
 213205        if (result.IsWarning && denials.Count == 0)
 206        {
 91207            warnings.AddRange(result.Reasons);
 208        }
 209
 213210        return true;
 211    }
 212
 213    private async ValueTask<GovernanceDecision> EvaluateEmptyPolicyAsync(
 214        TContext context,
 215        IReadOnlyList<OperationReason> threatWarningReasons,
 216        CancellationToken cancellationToken)
 217    {
 13218        if (!options.DenyWhenNoConstraints)
 219        {
 4220            LogEmptyPolicyAllowed(context);
 221        }
 222
 13223        GovernanceDecision noConstraintDecision = CreateNoConstraintDecision(
 13224            context,
 13225            threatWarningReasons,
 13226            options.DenyWhenNoConstraints);
 227
 13228        return await ApplyDecisionPolicyAsync(
 13229            context,
 13230            noConstraintDecision,
 13231            EmptyConstraintResults,
 13232            protectedThreatDecision: threatWarningReasons.Count > 0 ? noConstraintDecision : null,
 13233            cancellationToken)
 13234            .ConfigureAwait(false);
 13235    }
 236
 237    private static GovernanceDecision Compose(
 238        TContext context,
 239        OperationReasonAccumulator denials,
 240        OperationReasonAccumulator warnings,
 241        bool includeWarningsWhenDenied)
 242    {
 124243        return denials.Count > 0
 124244            ? includeWarningsWhenDenied && warnings.Count > 0
 124245                ? GovernanceDecision.Deny(
 124246                    warnings.ConcatAsArray(denials),
 124247                    correlationId: context.CorrelationId,
 124248                    policyVersion: context.PolicyVersion,
 124249                    policyHash: context.PolicyHash)
 124250                : CreateDeniedDecision(context, denials)
 124251            : warnings.Count > 0
 124252            ? CreateWarningDecision(context, warnings)
 124253            : GovernanceDecision.Allow(
 124254            correlationId: context.CorrelationId,
 124255            policyVersion: context.PolicyVersion,
 124256            policyHash: context.PolicyHash);
 257    }
 258
 259    private static GovernanceDecision CreateNoConstraintDecision(
 260        TContext context,
 261        IReadOnlyList<OperationReason> threatWarningReasons)
 262    {
 4263        return threatWarningReasons.Count > 0
 4264            ? GovernanceDecision.Warning(
 4265                threatWarningReasons,
 4266                correlationId: context.CorrelationId,
 4267                policyVersion: context.PolicyVersion,
 4268                policyHash: context.PolicyHash)
 4269            : GovernanceDecision.Allow(
 4270            correlationId: context.CorrelationId,
 4271            policyVersion: context.PolicyVersion,
 4272            policyHash: context.PolicyHash);
 273    }
 274
 275    private GovernanceDecision CreateNoConstraintDecision(
 276        TContext context,
 277        IReadOnlyList<OperationReason> threatWarningReasons,
 278        bool denyWhenNoConstraints)
 279    {
 13280        return !denyWhenNoConstraints
 13281            ? CreateNoConstraintDecision(context, threatWarningReasons)
 13282            : GovernanceDecision.Deny(
 13283            options.NoConstraintsReasonCode,
 13284            options.NoConstraintsReasonMessage,
 13285            correlationId: context.CorrelationId,
 13286            policyVersion: context.PolicyVersion,
 13287            policyHash: context.PolicyHash);
 288    }
 289
 290    private static GovernanceDecision CreateDeniedDecision(
 291        TContext context,
 292        OperationReasonAccumulator denials)
 293    {
 19294        return denials.Count == 1
 19295            ? GovernanceDecision.Deny(
 19296                denials.FirstReason!,
 19297                correlationId: context.CorrelationId,
 19298                policyVersion: context.PolicyVersion,
 19299                policyHash: context.PolicyHash)
 19300            : GovernanceDecision.Deny(
 19301                denials.AsReadOnlyList(),
 19302                correlationId: context.CorrelationId,
 19303                policyVersion: context.PolicyVersion,
 19304                policyHash: context.PolicyHash);
 305    }
 306
 307    private static GovernanceDecision CreateWarningDecision(
 308        TContext context,
 309        OperationReasonAccumulator warnings)
 310    {
 79311        return warnings.Count == 1
 79312            ? GovernanceDecision.Warning(
 79313                warnings.FirstReason!,
 79314                correlationId: context.CorrelationId,
 79315                policyVersion: context.PolicyVersion,
 79316                policyHash: context.PolicyHash)
 79317            : GovernanceDecision.Warning(
 79318                warnings.AsReadOnlyList(),
 79319                correlationId: context.CorrelationId,
 79320                policyVersion: context.PolicyVersion,
 79321                policyHash: context.PolicyHash);
 322    }
 323
 324    private static IReadOnlyList<ConstraintEvaluationResult> CreateConstraintResultsView(
 325        List<ConstraintEvaluationResult>? results)
 326    {
 132327        return results is null || results.Count == 0
 132328            ? EmptyConstraintResults
 132329            : results.AsReadOnly();
 330    }
 331
 332    private static GovernanceDecision? CreateProtectedThreatWarningDecision(
 333        TContext context,
 334        IReadOnlyList<OperationReason> threatWarningReasons,
 335        GovernanceDecision composedDecision)
 336    {
 124337        return threatWarningReasons.Count > 0 && composedDecision.CanProceed
 124338            ? GovernanceDecision.Warning(
 124339                threatWarningReasons,
 124340                correlationId: context.CorrelationId,
 124341                policyVersion: context.PolicyVersion,
 124342                policyHash: context.PolicyHash)
 124343            : null;
 344    }
 345
 346    private async ValueTask<GovernanceDecision> ApplyDecisionPolicyAsync(
 347        TContext context,
 348        GovernanceDecision decision,
 349        IReadOnlyList<ConstraintEvaluationResult> results,
 350        GovernanceDecision? protectedThreatDecision,
 351        CancellationToken cancellationToken)
 352    {
 182353        if (decisionPolicy is null)
 354        {
 157355            return decision;
 356        }
 357
 25358        GovernanceDecision policyDecision = await decisionPolicy
 25359            .ApplyAsync(context, decision, results, cancellationToken)
 25360            .ConfigureAwait(false);
 361
 25362        return options.PreventThreatAssessmentAllowDowngrade && protectedThreatDecision is not null && policyDecision.Is
 25363            ? protectedThreatDecision
 25364            : policyDecision;
 182365    }
 366
 367    private async ValueTask<ThreatEvaluationResult> EvaluateThreatModelContributorsAsync(
 368        TContext context,
 369        CancellationToken cancellationToken)
 370    {
 203371        if (threatModelContributors.Length == 0)
 372        {
 138373            return ThreatEvaluationResult.Empty;
 374        }
 375
 65376        List<OperationReason>? reasons = null;
 65377        OperationReason? selectedReason = null;
 65378        GovernanceDecisionOutcome? selectedOutcome = null;
 379
 269380        foreach (IThreatModelContributor<TContext> contributor in threatModelContributors)
 381        {
 77382            cancellationToken.ThrowIfCancellationRequested();
 383
 384            ThreatAssessment? assessment;
 385            try
 386            {
 77387                assessment = await contributor
 77388                    .AssessAsync(context, cancellationToken)
 77389                    .ConfigureAwait(false);
 63390            }
 14391            catch (Exception exception) when (ShouldConvertExceptionToDenial(exception, options.TreatThreatContributorEx
 392            {
 5393                return CreateThreatContributorExceptionResult(context, contributor, exception);
 394            }
 395
 63396            if (assessment is null || !assessment.IsActionable)
 397            {
 398                continue;
 399            }
 400
 60401            GovernanceDecisionOutcome effectiveOutcome = GetEffectiveThreatOutcome(assessment);
 59402            var reason = assessment.ToOperationReason(GetContributorName(contributor), effectiveOutcome);
 59403            GovernanceDecisionOutcome moreRestrictiveOutcome = SelectMoreRestrictiveOutcome(selectedOutcome, effectiveOu
 404
 59405            if (selectedOutcome != moreRestrictiveOutcome)
 406            {
 54407                selectedOutcome = moreRestrictiveOutcome;
 54408                selectedReason = reason;
 409            }
 410
 59411            reasons ??= [];
 59412            reasons.Add(reason);
 59413        }
 414
 50415        return reasons is null || selectedOutcome is null
 50416            ? ThreatEvaluationResult.Empty
 50417            : CreateThreatEvaluationResult(context, selectedOutcome.Value, selectedReason!, reasons.AsReadOnly());
 193418    }
 419
 420    private ThreatEvaluationResult CreateThreatContributorExceptionResult(
 421        TContext context,
 422        IThreatModelContributor<TContext> contributor,
 423        Exception exception)
 424    {
 5425        string contributorName = GetContributorName(contributor);
 5426        LogThreatContributorExceptionDenied(context, contributorName, exception);
 427
 5428        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 5429        {
 5430            ["threat.contributor"] = contributorName,
 5431            ["threat.failure"] = exception.GetType().Name
 5432        };
 433
 5434        var reason = OperationReason.Create(
 5435            options.ThreatContributorExceptionReasonCode,
 5436            options.ThreatContributorExceptionReasonMessage,
 5437            metadata);
 438
 5439        var decision = GovernanceDecision.Deny(
 5440            reason,
 5441            correlationId: context.CorrelationId,
 5442            policyVersion: context.PolicyVersion,
 5443            policyHash: context.PolicyHash);
 444
 5445        return ThreatEvaluationResult.ForBlockingDecision(decision);
 446    }
 447
 448    private static ThreatEvaluationResult CreateThreatEvaluationResult(
 449        TContext context,
 450        GovernanceDecisionOutcome outcome,
 451        OperationReason selectedReason,
 452        ReadOnlyCollection<OperationReason> reasons)
 453    {
 48454        return outcome switch
 48455        {
 22456            GovernanceDecisionOutcome.Denied => ThreatEvaluationResult.ForBlockingDecision(
 22457                GovernanceDecision.Deny(
 22458                    reasons,
 22459                    correlationId: context.CorrelationId,
 22460                    policyVersion: context.PolicyVersion,
 22461                    policyHash: context.PolicyHash)),
 5462            GovernanceDecisionOutcome.Deferred => ThreatEvaluationResult.ForBlockingDecision(
 5463                GovernanceDecision.Defer(
 5464                    selectedReason.Code,
 5465                    selectedReason.Message,
 5466                    correlationId: context.CorrelationId,
 5467                    policyVersion: context.PolicyVersion,
 5468                    policyHash: context.PolicyHash)),
 3469            GovernanceDecisionOutcome.AcknowledgmentRequired => ThreatEvaluationResult.ForBlockingDecision(
 3470                GovernanceDecision.RequireAcknowledgment(
 3471                    selectedReason.Code,
 3472                    selectedReason.Message,
 3473                    correlationId: context.CorrelationId,
 3474                    policyVersion: context.PolicyVersion,
 3475                    policyHash: context.PolicyHash)),
 2476            GovernanceDecisionOutcome.EscalationRecommended => ThreatEvaluationResult.ForBlockingDecision(
 2477                GovernanceDecision.Escalate(
 2478                    selectedReason.Code,
 2479                    selectedReason.Message,
 2480                    correlationId: context.CorrelationId,
 2481                    policyVersion: context.PolicyVersion,
 2482                    policyHash: context.PolicyHash)),
 15483            GovernanceDecisionOutcome.Warning => ThreatEvaluationResult.ForWarningReasons(reasons),
 1484            GovernanceDecisionOutcome.Allowed => throw CreateInvalidAllowedThreatOutcomeException(),
 0485            _ => ThreatEvaluationResult.Empty
 48486        };
 487    }
 488
 489    private static GovernanceDecisionOutcome GetEffectiveThreatOutcome(ThreatAssessment assessment)
 490    {
 60491        return assessment.RecommendedOutcome is GovernanceDecisionOutcome.Allowed
 60492            ? throw CreateInvalidAllowedThreatOutcomeException()
 60493            : assessment.RecommendedOutcome;
 494    }
 495
 496    private static InvalidOperationException CreateInvalidAllowedThreatOutcomeException()
 497    {
 3498        return new InvalidOperationException(InvalidAllowedThreatOutcomeMessage);
 499    }
 500
 501    private static GovernanceDecisionOutcome SelectMoreRestrictiveOutcome(
 502        GovernanceDecisionOutcome? current,
 503        GovernanceDecisionOutcome candidate)
 504    {
 59505        return current is null
 59506            ? candidate
 59507            : GetThreatOutcomeRank(candidate) > GetThreatOutcomeRank(current.Value)
 59508            ? candidate
 59509            : current.Value;
 510    }
 511
 512    private static int GetThreatOutcomeRank(GovernanceDecisionOutcome outcome)
 513    {
 25514        return outcome switch
 25515        {
 2516            GovernanceDecisionOutcome.Denied => 5,
 4517            GovernanceDecisionOutcome.EscalationRecommended => 4,
 4518            GovernanceDecisionOutcome.AcknowledgmentRequired => 3,
 4519            GovernanceDecisionOutcome.Deferred => 2,
 10520            GovernanceDecisionOutcome.Warning => 1,
 1521            GovernanceDecisionOutcome.Allowed => throw CreateInvalidAllowedThreatOutcomeException(),
 0522            _ => 0
 25523        };
 524    }
 525
 526    private static string GetContributorName(IThreatModelContributor<TContext> contributor)
 527    {
 64528        return string.IsNullOrWhiteSpace(contributor.Name)
 64529            ? "<unnamed>"
 64530            : contributor.Name.Trim();
 531    }
 532
 533    private async ValueTask<GovernanceDecision> CreateConstraintExceptionDecisionAsync(
 534        TContext context,
 535        IGovernanceConstraint<TContext> constraint,
 536        List<ConstraintEvaluationResult>? results,
 537        Exception exception,
 538        CancellationToken cancellationToken)
 539    {
 8540        LogConstraintExceptionDenied(context, constraint.Name, exception);
 541
 8542        var exceptionResult = ConstraintEvaluationResult.Deny(
 8543            options.ConstraintExceptionReasonCode,
 8544            options.ConstraintExceptionReasonMessage);
 8545        results?.Add(exceptionResult);
 546
 8547        var exceptionDecision = GovernanceDecision.Deny(
 8548            exceptionResult.Reasons,
 8549            correlationId: context.CorrelationId,
 8550            policyVersion: context.PolicyVersion,
 8551            policyHash: context.PolicyHash);
 552
 8553        return await ApplyDecisionPolicyAsync(
 8554            context,
 8555            exceptionDecision,
 8556            CreateConstraintResultsView(results),
 8557            protectedThreatDecision: null,
 8558            cancellationToken)
 8559            .ConfigureAwait(false);
 8560    }
 561
 562    private void LogEmptyPolicyAllowed(TContext context)
 563    {
 4564        if (logger is null)
 565        {
 3566            return;
 567        }
 568
 1569        EmptyPolicyAllowedWarning(
 1570            logger,
 1571            context.CorrelationId ?? string.Empty,
 1572            context.PolicyVersion ?? string.Empty,
 1573            context.PolicyHash ?? string.Empty,
 1574            null);
 1575    }
 576
 577    private void LogConstraintExceptionDenied(
 578        TContext context,
 579        string constraintName,
 580        Exception exception)
 581    {
 8582        if (logger is null)
 583        {
 5584            return;
 585        }
 586
 3587        ConstraintExceptionDeniedError(
 3588            logger,
 3589            string.IsNullOrWhiteSpace(constraintName) ? "<unnamed>" : constraintName,
 3590            exception.GetType().Name,
 3591            context.CorrelationId ?? string.Empty,
 3592            context.PolicyVersion ?? string.Empty,
 3593            context.PolicyHash ?? string.Empty,
 3594            exception);
 3595    }
 596
 597    private void LogThreatContributorExceptionDenied(
 598        TContext context,
 599        string contributorName,
 600        Exception exception)
 601    {
 5602        if (logger is null)
 603        {
 5604            return;
 605        }
 606
 0607        ThreatContributorExceptionDeniedError(
 0608            logger,
 0609            contributorName,
 0610            exception.GetType().Name,
 0611            context.CorrelationId ?? string.Empty,
 0612            context.PolicyVersion ?? string.Empty,
 0613            context.PolicyHash ?? string.Empty,
 0614            exception);
 0615    }
 616
 617    private sealed class ThreatEvaluationResult
 618    {
 17619        private static readonly IReadOnlyList<OperationReason> EmptyReasons =
 17620            Array.AsReadOnly(Array.Empty<OperationReason>());
 621
 69622        private ThreatEvaluationResult(
 69623            IReadOnlyList<OperationReason> warningReasons,
 69624            GovernanceDecision? blockingDecision)
 625        {
 69626            WarningReasons = warningReasons;
 69627            BlockingDecision = blockingDecision;
 69628        }
 629
 630        public static ThreatEvaluationResult Empty { get; } = new(EmptyReasons, blockingDecision: null);
 631
 632        public IReadOnlyList<OperationReason> WarningReasons { get; }
 633
 634        public GovernanceDecision? BlockingDecision { get; }
 635
 636        public static ThreatEvaluationResult ForWarningReasons(IReadOnlyList<OperationReason> reasons)
 637        {
 15638            return new ThreatEvaluationResult(reasons, blockingDecision: null);
 639        }
 640
 641        public static ThreatEvaluationResult ForBlockingDecision(GovernanceDecision decision)
 642        {
 37643            return new ThreatEvaluationResult(EmptyReasons, decision);
 644        }
 645    }
 646
 647    private struct OperationReasonAccumulator
 648    {
 649        private List<OperationReason>? additionalReasons;
 650
 651        public OperationReason? FirstReason { get; private set; }
 652
 653        public int Count { get; private set; }
 654
 655        public void AddRange(IReadOnlyList<OperationReason> reasons)
 656        {
 808657            for (int index = 0; index < reasons.Count; index++)
 658            {
 137659                Add(reasons[index]);
 660            }
 267661        }
 662
 663        public readonly OperationReason[] ConcatAsArray(OperationReasonAccumulator other)
 664        {
 8665            int totalCount = Count + other.Count;
 8666            if (totalCount == 0)
 667            {
 0668                return [];
 669            }
 670
 8671            var reasons = new OperationReason[totalCount];
 8672            int nextIndex = CopyTo(reasons, 0);
 8673            _ = other.CopyTo(reasons, nextIndex);
 674
 8675            return reasons;
 676        }
 677
 678        public readonly ReadOnlyCollection<OperationReason> AsReadOnlyList()
 679        {
 10680            return Count switch
 10681            {
 0682                0 => Array.AsReadOnly(Array.Empty<OperationReason>()),
 0683                1 => Array.AsReadOnly([FirstReason!]),
 10684                _ => additionalReasons!.AsReadOnly()
 10685            };
 686        }
 687
 688        private void Add(OperationReason? reason)
 689        {
 137690            if (reason is null)
 691            {
 0692                return;
 693            }
 694
 137695            if (Count == 0)
 696            {
 122697                FirstReason = reason;
 122698                Count = 1;
 122699                return;
 700            }
 701
 15702            additionalReasons ??= [FirstReason!];
 15703            additionalReasons.Add(reason);
 15704            Count++;
 15705        }
 706
 707        private readonly int CopyTo(OperationReason[] destination, int startIndex)
 708        {
 16709            if (Count == 0)
 710            {
 0711                return startIndex;
 712            }
 713
 16714            if (Count == 1)
 715            {
 13716                destination[startIndex] = FirstReason!;
 13717                return startIndex + 1;
 718            }
 719
 3720            additionalReasons!.CopyTo(destination, startIndex);
 3721            return startIndex + additionalReasons.Count;
 722        }
 723    }
 724}

Methods/Properties

.cctor()
.ctor(System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.Constraints.IGovernanceConstraint`1<TContext>>,System.Collections.Generic.IEnumerable`1<AsiBackbone.Core.ThreatModeling.IThreatModelContributor`1<TContext>>,AsiBackbone.Core.Evaluation.IGovernanceDecisionPolicy`1<TContext>,AsiBackbone.Core.Evaluation.GovernancePolicyOptions,Microsoft.Extensions.Logging.ILogger`1<AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator`1<TContext>>)
EvaluateAsync()
ShouldConvertExceptionToDenial(System.Exception,System.Boolean)
IsCriticalException(System.Exception)
CreateConstraintResultsBuffer()
AccumulateConstraintResult(AsiBackbone.Core.Constraints.ConstraintEvaluationResult,AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator`1/OperationReasonAccumulator<TContext>&,AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator`1/OperationReasonAccumulator<TContext>&,System.Boolean)
EvaluateEmptyPolicyAsync()
Compose(TContext,AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator`1/OperationReasonAccumulator<TContext>,AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator`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.DefaultGovernancePolicyEvaluator`1/OperationReasonAccumulator<TContext>)
CreateWarningDecision(TContext,AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator`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)
ForWarningReasons(System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>)
ForBlockingDecision(AsiBackbone.Core.Decisions.GovernanceDecision)
AddRange(System.Collections.Generic.IReadOnlyList`1<AsiBackbone.Core.Results.OperationReason>)
ConcatAsArray(AsiBackbone.Core.Evaluation.DefaultGovernancePolicyEvaluator`1/OperationReasonAccumulator<TContext>)
AsReadOnlyList()
Add(AsiBackbone.Core.Results.OperationReason)
CopyTo(AsiBackbone.Core.Results.OperationReason[],System.Int32)