< 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: 77
Uncovered lines: 0
Coverable lines: 77
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_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
 114    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
 115        new ReadOnlyDictionary<string, string>(
 116            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>
 8850    public ThreatAssessment(
 8851        ThreatSeverity severity,
 8852        string category,
 8853        string reasonCode,
 8854        string description,
 8855        GovernanceDecisionOutcome recommendedOutcome,
 8856        double confidence = MaximumConfidence,
 8857        IReadOnlyDictionary<string, string>? metadata = null)
 58    {
 8859        if (!Enum.IsDefined(severity))
 60        {
 161            throw new ArgumentOutOfRangeException(nameof(severity), severity, "Threat severity must be defined.");
 62        }
 63
 8764        if (!Enum.IsDefined(recommendedOutcome))
 65        {
 266            throw new ArgumentOutOfRangeException(
 267                nameof(recommendedOutcome),
 268                recommendedOutcome,
 269                "Recommended governance outcome must be defined.");
 70        }
 71
 8572        ArgumentException.ThrowIfNullOrWhiteSpace(category);
 8473        ArgumentException.ThrowIfNullOrWhiteSpace(reasonCode);
 8374        ArgumentException.ThrowIfNullOrWhiteSpace(description);
 75
 8276        if (confidence is < MinimumConfidence or > MaximumConfidence)
 77        {
 278            throw new ArgumentOutOfRangeException(
 279                nameof(confidence),
 280                confidence,
 281                $"{nameof(confidence)} must be between {MinimumConfidence} and {MaximumConfidence}.");
 82        }
 83
 8084        Severity = severity;
 8085        Category = category.Trim();
 8086        ReasonCode = reasonCode.Trim();
 8087        Description = description.Trim();
 8088        RecommendedOutcome = recommendedOutcome;
 8089        Confidence = confidence;
 8090        Metadata = NormalizeMetadata(metadata);
 7191    }
 92
 93    /// <summary>
 94    /// Gets the severity reported by the contributor.
 95    /// </summary>
 96    public ThreatSeverity Severity { get; }
 97
 98    /// <summary>
 99    /// Gets the threat category reported by the contributor.
 100    /// </summary>
 101    public string Category { get; }
 102
 103    /// <summary>
 104    /// Gets the machine-readable reason code.
 105    /// </summary>
 106    public string ReasonCode { get; }
 107
 108    /// <summary>
 109    /// Gets the human-readable threat description.
 110    /// </summary>
 111    public string Description { get; }
 112
 113    /// <summary>
 114    /// Gets the governance outcome recommended by the contributor.
 115    /// </summary>
 116    public GovernanceDecisionOutcome RecommendedOutcome { get; }
 117
 118    /// <summary>
 119    /// Gets the contributor confidence from 0.0 to 1.0.
 120    /// </summary>
 121    public double Confidence { get; }
 122
 123    /// <summary>
 124    /// Gets optional contributor-supplied metadata retained on generated operation reasons.
 125    /// </summary>
 126    public IReadOnlyDictionary<string, string> Metadata { get; }
 127
 128    /// <summary>
 129    /// Gets a value indicating whether the assessment should influence the composed decision.
 130    /// </summary>
 64131    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    {
 4139        return new ThreatAssessment(
 4140            ThreatSeverity.None,
 4141            ThreatCategories.None,
 4142            "asibackbone.threat.none",
 4143            "No threat indicators were reported.",
 4144            GovernanceDecisionOutcome.Allowed,
 4145            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    {
 84180        return new ThreatAssessment(
 84181            severity,
 84182            category,
 84183            reasonCode,
 84184            description,
 84185            recommendedOutcome,
 84186            confidence,
 84187            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    {
 63200        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 63201        {
 63202            ["threat.category"] = Category,
 63203            ["threat.severity"] = Severity.ToString(),
 63204            ["threat.recommended_outcome"] = RecommendedOutcome.ToString(),
 63205            ["threat.effective_outcome"] = (effectiveOutcome ?? RecommendedOutcome).ToString(),
 63206            ["threat.confidence"] = Confidence.ToString("G", System.Globalization.CultureInfo.InvariantCulture)
 63207        };
 208
 63209        if (!string.IsNullOrWhiteSpace(contributorName))
 210        {
 61211            metadata["threat.contributor"] = contributorName.Trim();
 212        }
 213
 212214        foreach (KeyValuePair<string, string> item in Metadata)
 215        {
 43216            metadata[item.Key] = item.Value;
 217        }
 218
 63219        return OperationReason.Create(ReasonCode, Description, metadata);
 220    }
 221
 222    private static IReadOnlyDictionary<string, string> NormalizeMetadata(
 223        IReadOnlyDictionary<string, string>? metadata)
 224    {
 80225        if (metadata is null || metadata.Count == 0)
 226        {
 47227            return EmptyMetadata;
 228        }
 229
 33230        Dictionary<string, string> normalizedMetadata = new(StringComparer.Ordinal);
 231
 163232        foreach (KeyValuePair<string, string> item in metadata)
 233        {
 53234            if (string.IsNullOrWhiteSpace(item.Key))
 235            {
 236                continue;
 237            }
 238
 52239            string normalizedKey = item.Key.Trim();
 240
 52241            if (normalizedKey.StartsWith(ReservedMetadataPrefix, StringComparison.OrdinalIgnoreCase))
 242            {
 9243                throw new ArgumentException(
 9244                    $"Contributor metadata keys beginning with '{ReservedMetadataPrefix}' are reserved for framework-gen
 9245                    nameof(metadata));
 246            }
 247
 43248            normalizedMetadata[normalizedKey] = item.Value?.Trim() ?? string.Empty;
 249        }
 250
 24251        return normalizedMetadata.Count == 0
 24252            ? EmptyMetadata
 24253            : new ReadOnlyDictionary<string, string>(normalizedMetadata);
 254    }
 255}