< Summary

Information
Class: AsiBackbone.Core.ThreatModeling.ThreatAssessment
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/ThreatModeling/ThreatAssessment.cs
Line coverage
100%
Covered lines: 84
Uncovered lines: 0
Coverable lines: 84
Total lines: 255
Line coverage: 100%
Branch coverage
100%
Covered branches: 34
Total branches: 34
Branch coverage: 100%
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%1010100%
get_Severity()100%11100%
get_Category()100%11100%
get_ReasonCode()100%11100%
get_Description()100%11100%
get_RecommendedOutcome()100%11100%
get_Confidence()100%11100%
get_Metadata()100%11100%
get_IsActionable()100%22100%
NoThreat()100%11100%
Create(...)100%11100%
ToOperationReason(...)100%66100%
NormalizeMetadata(...)100%1616100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/ThreatModeling/ThreatAssessment.cs

#LineLine coverage
 1using System.Collections.ObjectModel;
 2using AsiBackbone.Core.Decisions;
 3using AsiBackbone.Core.Results;
 4
 5namespace AsiBackbone.Core.ThreatModeling;
 6
 7/// <summary>
 8/// Represents a structured threat-aware assessment contributed during policy evaluation.
 9/// </summary>
 10public sealed class ThreatAssessment
 11{
 12    private const string ReservedMetadataPrefix = "threat.";
 13
 214    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 215        new ReadOnlyDictionary<string, string>(
 216            new Dictionary<string, string>(StringComparer.Ordinal));
 17
 18    /// <summary>
 19    /// Defines the minimum confidence value accepted by a threat assessment.
 20    /// </summary>
 21    public const double MinimumConfidence = 0.0D;
 22
 23    /// <summary>
 24    /// Defines the maximum confidence value accepted by a threat assessment.
 25    /// </summary>
 26    public const double MaximumConfidence = 1.0D;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="ThreatAssessment" /> class.
 30    /// </summary>
 31    /// <param name="severity">The severity reported by the contributor.</param>
 32    /// <param name="category">The threat category reported by the contributor.</param>
 33    /// <param name="reasonCode">The machine-readable reason code.</param>
 34    /// <param name="description">The human-readable threat description.</param>
 35    /// <param name="recommendedOutcome">The governance outcome recommended by the contributor.</param>
 36    /// <param name="confidence">The contributor confidence from 0.0 to 1.0.</param>
 37    /// <param name="metadata">
 38    /// Optional contributor-supplied metadata retained on generated operation reasons.
 39    /// Keys beginning with <c>threat.</c> are reserved for framework-generated provenance.
 40    /// </param>
 41    /// <exception cref="ArgumentException">
 42    /// Thrown when <paramref name="category" />, <paramref name="reasonCode" />, or
 43    /// <paramref name="description" /> is null, empty, or whitespace, or when
 44    /// <paramref name="metadata" /> contains a key in the reserved <c>threat.</c> namespace.
 45    /// </exception>
 46    /// <exception cref="ArgumentOutOfRangeException">
 47    /// Thrown when <paramref name="severity" /> or <paramref name="recommendedOutcome" /> is undefined,
 48    /// or when <paramref name="confidence" /> is outside the supported range.
 49    /// </exception>
 17250    public ThreatAssessment(
 17251        ThreatSeverity severity,
 17252        string category,
 17253        string reasonCode,
 17254        string description,
 17255        GovernanceDecisionOutcome recommendedOutcome,
 17256        double confidence = MaximumConfidence,
 17257        IReadOnlyDictionary<string, string>? metadata = null)
 58    {
 17259        if (!Enum.IsDefined(severity))
 60        {
 261            throw new ArgumentOutOfRangeException(nameof(severity), severity, "Threat severity must be defined.");
 62        }
 63
 17064        if (!Enum.IsDefined(recommendedOutcome))
 65        {
 466            throw new ArgumentOutOfRangeException(
 467                nameof(recommendedOutcome),
 468                recommendedOutcome,
 469                "Recommended governance outcome must be defined.");
 70        }
 71
 16672        ArgumentException.ThrowIfNullOrWhiteSpace(category);
 16473        ArgumentException.ThrowIfNullOrWhiteSpace(reasonCode);
 16274        ArgumentException.ThrowIfNullOrWhiteSpace(description);
 75
 16076        if (confidence is < MinimumConfidence or > MaximumConfidence)
 77        {
 478            throw new ArgumentOutOfRangeException(
 479                nameof(confidence),
 480                confidence,
 481                $"{nameof(confidence)} must be between {MinimumConfidence} and {MaximumConfidence}.");
 82        }
 83
 15684        Severity = severity;
 15685        Category = category.Trim();
 15686        ReasonCode = reasonCode.Trim();
 15687        Description = description.Trim();
 15688        RecommendedOutcome = recommendedOutcome;
 15689        Confidence = confidence;
 15690        Metadata = NormalizeMetadata(metadata);
 13891    }
 92
 93    /// <summary>
 94    /// Gets the severity reported by the contributor.
 95    /// </summary>
 24896    public ThreatSeverity Severity { get; }
 97
 98    /// <summary>
 99    /// Gets the threat category reported by the contributor.
 100    /// </summary>
 124101    public string Category { get; }
 102
 103    /// <summary>
 104    /// Gets the machine-readable reason code.
 105    /// </summary>
 122106    public string ReasonCode { get; }
 107
 108    /// <summary>
 109    /// Gets the human-readable threat description.
 110    /// </summary>
 122111    public string Description { get; }
 112
 113    /// <summary>
 114    /// Gets the governance outcome recommended by the contributor.
 115    /// </summary>
 368116    public GovernanceDecisionOutcome RecommendedOutcome { get; }
 117
 118    /// <summary>
 119    /// Gets the contributor confidence from 0.0 to 1.0.
 120    /// </summary>
 122121    public double Confidence { get; }
 122
 123    /// <summary>
 124    /// Gets optional contributor-supplied metadata retained on generated operation reasons.
 125    /// </summary>
 126126    public IReadOnlyDictionary<string, string> Metadata { get; }
 127
 128    /// <summary>
 129    /// Gets a value indicating whether the assessment should influence the composed decision.
 130    /// </summary>
 124131    public bool IsActionable => Severity is not ThreatSeverity.None || RecommendedOutcome is not GovernanceDecisionOutco
 132
 133    /// <summary>
 134    /// Creates a no-threat assessment.
 135    /// </summary>
 136    /// <returns>A no-threat assessment that does not influence decision composition.</returns>
 137    public static ThreatAssessment NoThreat()
 138    {
 8139        return new ThreatAssessment(
 8140            ThreatSeverity.None,
 8141            ThreatCategories.None,
 8142            "asibackbone.threat.none",
 8143            "No threat indicators were reported.",
 8144            GovernanceDecisionOutcome.Allowed,
 8145            MinimumConfidence);
 146    }
 147
 148    /// <summary>
 149    /// Creates a threat assessment.
 150    /// </summary>
 151    /// <param name="severity">The severity reported by the contributor.</param>
 152    /// <param name="category">The threat category reported by the contributor.</param>
 153    /// <param name="reasonCode">The machine-readable reason code.</param>
 154    /// <param name="description">The human-readable threat description.</param>
 155    /// <param name="recommendedOutcome">The governance outcome recommended by the contributor.</param>
 156    /// <param name="confidence">The contributor confidence from 0.0 to 1.0.</param>
 157    /// <param name="metadata">
 158    /// Optional contributor-supplied metadata retained on generated operation reasons.
 159    /// Keys beginning with <c>threat.</c> are reserved for framework-generated provenance.
 160    /// </param>
 161    /// <returns>A threat assessment.</returns>
 162    /// <exception cref="ArgumentException">
 163    /// Thrown when <paramref name="category" />, <paramref name="reasonCode" />, or
 164    /// <paramref name="description" /> is null, empty, or whitespace, or when
 165    /// <paramref name="metadata" /> contains a key in the reserved <c>threat.</c> namespace.
 166    /// </exception>
 167    /// <exception cref="ArgumentOutOfRangeException">
 168    /// Thrown when <paramref name="severity" /> or <paramref name="recommendedOutcome" /> is undefined,
 169    /// or when <paramref name="confidence" /> is outside the supported range.
 170    /// </exception>
 171    public static ThreatAssessment Create(
 172        ThreatSeverity severity,
 173        string category,
 174        string reasonCode,
 175        string description,
 176        GovernanceDecisionOutcome recommendedOutcome,
 177        double confidence = MaximumConfidence,
 178        IReadOnlyDictionary<string, string>? metadata = null)
 179    {
 164180        return new ThreatAssessment(
 164181            severity,
 164182            category,
 164183            reasonCode,
 164184            description,
 164185            recommendedOutcome,
 164186            confidence,
 164187            metadata);
 188    }
 189
 190    /// <summary>
 191    /// Converts the assessment into an operation reason with threat metadata.
 192    /// </summary>
 193    /// <param name="contributorName">Optional contributor name to include in metadata.</param>
 194    /// <param name="effectiveOutcome">Optional effective outcome selected by the evaluator after safety promotion.</par
 195    /// <returns>An operation reason representing the assessment.</returns>
 196    public OperationReason ToOperationReason(
 197        string? contributorName = null,
 198        GovernanceDecisionOutcome? effectiveOutcome = null)
 199    {
 122200        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 122201        {
 122202            ["threat.category"] = Category,
 122203            ["threat.severity"] = Severity.ToString(),
 122204            ["threat.recommended_outcome"] = RecommendedOutcome.ToString(),
 122205            ["threat.effective_outcome"] = (effectiveOutcome ?? RecommendedOutcome).ToString(),
 122206            ["threat.confidence"] = Confidence.ToString("G", System.Globalization.CultureInfo.InvariantCulture)
 122207        };
 208
 122209        if (!string.IsNullOrWhiteSpace(contributorName))
 210        {
 118211            metadata["threat.contributor"] = contributorName.Trim();
 212        }
 213
 416214        foreach (KeyValuePair<string, string> item in Metadata)
 215        {
 86216            metadata[item.Key] = item.Value;
 217        }
 218
 122219        return OperationReason.Create(ReasonCode, Description, metadata);
 220    }
 221
 222    private static IReadOnlyDictionary<string, string> NormalizeMetadata(
 223        IReadOnlyDictionary<string, string>? metadata)
 224    {
 156225        if (metadata is null || metadata.Count == 0)
 226        {
 90227            return EmptyMetadata;
 228        }
 229
 66230        Dictionary<string, string> normalizedMetadata = new(StringComparer.Ordinal);
 231
 326232        foreach (KeyValuePair<string, string> item in metadata)
 233        {
 106234            if (string.IsNullOrWhiteSpace(item.Key))
 235            {
 236                continue;
 237            }
 238
 104239            string normalizedKey = item.Key.Trim();
 240
 104241            if (normalizedKey.StartsWith(ReservedMetadataPrefix, StringComparison.OrdinalIgnoreCase))
 242            {
 18243                throw new ArgumentException(
 18244                    $"Contributor metadata keys beginning with '{ReservedMetadataPrefix}' are reserved for framework-gen
 18245                    nameof(metadata));
 246            }
 247
 86248            normalizedMetadata[normalizedKey] = item.Value?.Trim() ?? string.Empty;
 249        }
 250
 48251        return normalizedMetadata.Count == 0
 48252            ? EmptyMetadata
 48253            : new ReadOnlyDictionary<string, string>(normalizedMetadata);
 254    }
 255}