| | | 1 | | using System.Collections.ObjectModel; |
| | | 2 | | using System.Globalization; |
| | | 3 | | using AsiBackbone.Core.Decisions; |
| | | 4 | | using AsiBackbone.Core.Results; |
| | | 5 | | |
| | | 6 | | namespace AsiBackbone.Core.Classification; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Represents the resolved provider-neutral policy response to a DLP or classification failure. |
| | | 10 | | /// </summary> |
| | | 11 | | public sealed class DlpFailurePolicyResolution |
| | | 12 | | { |
| | 0 | 13 | | private static readonly IReadOnlyDictionary<string, string> EmptyMetadata = |
| | 0 | 14 | | new ReadOnlyDictionary<string, string>( |
| | 0 | 15 | | new Dictionary<string, string>(StringComparer.Ordinal)); |
| | | 16 | | |
| | 30 | 17 | | private DlpFailurePolicyResolution( |
| | 30 | 18 | | DlpFailurePolicyContext context, |
| | 30 | 19 | | DlpFailureBehavior behavior, |
| | 30 | 20 | | OperationReason reason, |
| | 30 | 21 | | GovernanceDecision decision) |
| | | 22 | | { |
| | 30 | 23 | | Context = context; |
| | 30 | 24 | | Behavior = behavior; |
| | 30 | 25 | | Reason = reason; |
| | 30 | 26 | | Decision = decision; |
| | 30 | 27 | | } |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// Gets the original failure context. |
| | | 31 | | /// </summary> |
| | 6 | 32 | | public DlpFailurePolicyContext Context { get; } |
| | | 33 | | |
| | | 34 | | /// <summary> |
| | | 35 | | /// Gets the resolved provider-neutral failure behavior. |
| | | 36 | | /// </summary> |
| | 31 | 37 | | public DlpFailureBehavior Behavior { get; } |
| | | 38 | | |
| | | 39 | | /// <summary> |
| | | 40 | | /// Gets the machine-readable reason associated with the failure behavior. |
| | | 41 | | /// </summary> |
| | 77 | 42 | | public OperationReason Reason { get; } |
| | | 43 | | |
| | | 44 | | /// <summary> |
| | | 45 | | /// Gets the governance decision produced by the resolved behavior. |
| | | 46 | | /// </summary> |
| | 103 | 47 | | public GovernanceDecision Decision { get; } |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// Gets a value indicating whether the resolved decision allows immediate execution. |
| | | 51 | | /// </summary> |
| | 6 | 52 | | public bool CanProceed => Decision.CanProceed; |
| | | 53 | | |
| | | 54 | | /// <summary> |
| | | 55 | | /// Gets a value indicating whether the resolution uses a fail-open behavior. |
| | | 56 | | /// </summary> |
| | 8 | 57 | | public bool IsFailOpen => Behavior is DlpFailureBehavior.Allow or DlpFailureBehavior.WarnAndAllow; |
| | | 58 | | |
| | | 59 | | /// <summary> |
| | | 60 | | /// Gets a value indicating whether the resolution uses a fail-closed behavior. |
| | | 61 | | /// </summary> |
| | 8 | 62 | | public bool IsFailClosed => Behavior is DlpFailureBehavior.Deny; |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// Creates a policy resolution for the supplied context and behavior. |
| | | 66 | | /// </summary> |
| | | 67 | | /// <param name="context">The original DLP or classification failure context.</param> |
| | | 68 | | /// <param name="behavior">The resolved failure behavior.</param> |
| | | 69 | | /// <returns>The policy resolution.</returns> |
| | | 70 | | public static DlpFailurePolicyResolution Create( |
| | | 71 | | DlpFailurePolicyContext context, |
| | | 72 | | DlpFailureBehavior behavior) |
| | | 73 | | { |
| | 32 | 74 | | ArgumentNullException.ThrowIfNull(context); |
| | | 75 | | |
| | 31 | 76 | | if (!Enum.IsDefined(behavior)) |
| | | 77 | | { |
| | 1 | 78 | | throw new ArgumentOutOfRangeException(nameof(behavior), behavior, "DLP failure behavior must be defined."); |
| | | 79 | | } |
| | | 80 | | |
| | 30 | 81 | | var reason = OperationReason.Create( |
| | 30 | 82 | | DlpFailureReasonCodes.GetFor(context.FailureKind), |
| | 30 | 83 | | BuildMessage(context), |
| | 30 | 84 | | BuildReasonMetadata(context, behavior)); |
| | | 85 | | |
| | 30 | 86 | | GovernanceDecision decision = behavior switch |
| | 30 | 87 | | { |
| | 3 | 88 | | DlpFailureBehavior.Allow => GovernanceDecision.Allow( |
| | 3 | 89 | | correlationId: context.CorrelationId, |
| | 3 | 90 | | traceId: context.TraceId, |
| | 3 | 91 | | policyVersion: context.PolicyVersion, |
| | 3 | 92 | | policyHash: context.PolicyHash), |
| | 17 | 93 | | DlpFailureBehavior.WarnAndAllow => GovernanceDecision.Warning( |
| | 17 | 94 | | reason, |
| | 17 | 95 | | correlationId: context.CorrelationId, |
| | 17 | 96 | | traceId: context.TraceId, |
| | 17 | 97 | | policyVersion: context.PolicyVersion, |
| | 17 | 98 | | policyHash: context.PolicyHash), |
| | 2 | 99 | | DlpFailureBehavior.Deny => GovernanceDecision.Deny( |
| | 2 | 100 | | reason, |
| | 2 | 101 | | correlationId: context.CorrelationId, |
| | 2 | 102 | | traceId: context.TraceId, |
| | 2 | 103 | | policyVersion: context.PolicyVersion, |
| | 2 | 104 | | policyHash: context.PolicyHash), |
| | 3 | 105 | | DlpFailureBehavior.Defer => GovernanceDecision.Defer( |
| | 3 | 106 | | reason.Code, |
| | 3 | 107 | | reason.Message, |
| | 3 | 108 | | correlationId: context.CorrelationId, |
| | 3 | 109 | | traceId: context.TraceId, |
| | 3 | 110 | | policyVersion: context.PolicyVersion, |
| | 3 | 111 | | policyHash: context.PolicyHash), |
| | 2 | 112 | | DlpFailureBehavior.RequireAcknowledgment => GovernanceDecision.RequireAcknowledgment( |
| | 2 | 113 | | reason.Code, |
| | 2 | 114 | | reason.Message, |
| | 2 | 115 | | correlationId: context.CorrelationId, |
| | 2 | 116 | | traceId: context.TraceId, |
| | 2 | 117 | | policyVersion: context.PolicyVersion, |
| | 2 | 118 | | policyHash: context.PolicyHash), |
| | 3 | 119 | | DlpFailureBehavior.Escalate => GovernanceDecision.Escalate( |
| | 3 | 120 | | reason.Code, |
| | 3 | 121 | | reason.Message, |
| | 3 | 122 | | correlationId: context.CorrelationId, |
| | 3 | 123 | | traceId: context.TraceId, |
| | 3 | 124 | | policyVersion: context.PolicyVersion, |
| | 3 | 125 | | policyHash: context.PolicyHash), |
| | 0 | 126 | | _ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, "DLP failure behavior must be defined |
| | 30 | 127 | | }; |
| | | 128 | | |
| | 30 | 129 | | return new DlpFailurePolicyResolution(context, behavior, reason, decision); |
| | | 130 | | } |
| | | 131 | | |
| | | 132 | | private static string BuildMessage(DlpFailurePolicyContext context) |
| | | 133 | | { |
| | 30 | 134 | | return context.FailureKind switch |
| | 30 | 135 | | { |
| | 17 | 136 | | DlpClassificationFailureKind.ServiceUnavailable => "DLP/classification screening service was unavailable.", |
| | 4 | 137 | | DlpClassificationFailureKind.Timeout => context.Timeout.HasValue |
| | 4 | 138 | | ? $"DLP/classification screening timed out after {context.Timeout.Value.TotalMilliseconds:0} ms." |
| | 4 | 139 | | : "DLP/classification screening timed out.", |
| | 3 | 140 | | DlpClassificationFailureKind.IndeterminateResult => "DLP/classification screening returned an indeterminate |
| | 3 | 141 | | DlpClassificationFailureKind.BlockedResult => "DLP/classification screening returned a blocked result.", |
| | 3 | 142 | | DlpClassificationFailureKind.ClassifiedResult => "DLP/classification screening returned a classified result |
| | 0 | 143 | | _ => throw new ArgumentOutOfRangeException(nameof(context), context.FailureKind, "DLP failure kind must be d |
| | 30 | 144 | | }; |
| | | 145 | | } |
| | | 146 | | |
| | | 147 | | private static IReadOnlyDictionary<string, string> BuildReasonMetadata( |
| | | 148 | | DlpFailurePolicyContext context, |
| | | 149 | | DlpFailureBehavior behavior) |
| | | 150 | | { |
| | 30 | 151 | | Dictionary<string, string> metadata = new(StringComparer.Ordinal); |
| | | 152 | | |
| | 66 | 153 | | foreach (KeyValuePair<string, string> item in context.Metadata) |
| | | 154 | | { |
| | 3 | 155 | | if (string.IsNullOrWhiteSpace(item.Key)) |
| | | 156 | | { |
| | | 157 | | continue; |
| | | 158 | | } |
| | | 159 | | |
| | 3 | 160 | | metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty; |
| | | 161 | | } |
| | | 162 | | |
| | 30 | 163 | | metadata["dlp.failure_kind"] = ToMetadataValue(context.FailureKind); |
| | 30 | 164 | | metadata["dlp.risk_level"] = ToMetadataValue(context.RiskLevel); |
| | 30 | 165 | | metadata["dlp.behavior"] = ToMetadataValue(behavior); |
| | | 166 | | |
| | 30 | 167 | | if (!string.IsNullOrWhiteSpace(context.IntentCategory)) |
| | | 168 | | { |
| | 2 | 169 | | metadata["dlp.intent_category"] = context.IntentCategory; |
| | | 170 | | } |
| | | 171 | | |
| | 30 | 172 | | if (!string.IsNullOrWhiteSpace(context.Environment)) |
| | | 173 | | { |
| | 2 | 174 | | metadata["dlp.environment"] = context.Environment; |
| | | 175 | | } |
| | | 176 | | |
| | 30 | 177 | | if (context.Timeout.HasValue) |
| | | 178 | | { |
| | 1 | 179 | | metadata["dlp.timeout_ms"] = context.Timeout.Value.TotalMilliseconds |
| | 1 | 180 | | .ToString("0", CultureInfo.InvariantCulture); |
| | | 181 | | } |
| | | 182 | | |
| | 30 | 183 | | return metadata.Count == 0 |
| | 30 | 184 | | ? EmptyMetadata |
| | 30 | 185 | | : new ReadOnlyDictionary<string, string>(metadata); |
| | | 186 | | } |
| | | 187 | | |
| | | 188 | | private static string ToMetadataValue<TEnum>(TEnum value) |
| | | 189 | | where TEnum : struct, Enum |
| | | 190 | | { |
| | 90 | 191 | | string text = value.ToString(); |
| | | 192 | | |
| | 90 | 193 | | return string.Concat( |
| | 90 | 194 | | text.Select((character, index) => |
| | 978 | 195 | | index > 0 && char.IsUpper(character) |
| | 978 | 196 | | ? "_" + char.ToLowerInvariant(character) |
| | 978 | 197 | | : char.ToLowerInvariant(character).ToString())); |
| | | 198 | | } |
| | | 199 | | } |