feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stella-ops.org/schemas/attestation/uncertainty-budget-statement.v1.json",
|
||||
"title": "Uncertainty Budget Statement",
|
||||
"description": "In-toto predicate type for uncertainty budget evaluation attestations. Sprint: SPRINT_4300_0002_0002 (UATT-007).",
|
||||
"type": "object",
|
||||
"required": ["_type", "subject", "predicateType", "predicate"],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"type": "string",
|
||||
"const": "https://in-toto.io/Statement/v1"
|
||||
},
|
||||
"subject": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["digest"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Subject identifier (e.g., environment name or image reference)"
|
||||
},
|
||||
"digest": {
|
||||
"type": "object",
|
||||
"description": "Cryptographic digest of the subject",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-fA-F0-9]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"predicateType": {
|
||||
"type": "string",
|
||||
"const": "uncertainty-budget.stella/v1"
|
||||
},
|
||||
"predicate": {
|
||||
"$ref": "#/$defs/UncertaintyBudgetPredicate"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"UncertaintyBudgetPredicate": {
|
||||
"type": "object",
|
||||
"required": ["environment", "isWithinBudget", "action", "totalUnknowns", "evaluatedAt"],
|
||||
"properties": {
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Environment against which budget was evaluated (e.g., production, staging)"
|
||||
},
|
||||
"isWithinBudget": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the evaluation passed the budget check"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["pass", "warn", "block"],
|
||||
"description": "Recommended action based on budget evaluation"
|
||||
},
|
||||
"totalUnknowns": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Total count of unknowns in evaluation"
|
||||
},
|
||||
"totalLimit": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Configured total unknown limit for this environment"
|
||||
},
|
||||
"percentageUsed": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "Percentage of budget consumed"
|
||||
},
|
||||
"violationCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Number of budget rule violations"
|
||||
},
|
||||
"violations": {
|
||||
"type": "array",
|
||||
"description": "Detailed violation information",
|
||||
"items": {
|
||||
"$ref": "#/$defs/BudgetViolation"
|
||||
}
|
||||
},
|
||||
"budget": {
|
||||
"$ref": "#/$defs/BudgetDefinition",
|
||||
"description": "Budget definition that was applied"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Human-readable budget status message"
|
||||
},
|
||||
"evaluatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO-8601 timestamp of budget evaluation"
|
||||
},
|
||||
"policyRevisionId": {
|
||||
"type": "string",
|
||||
"description": "Policy revision ID containing the budget rules"
|
||||
},
|
||||
"imageDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[a-fA-F0-9]{64}$",
|
||||
"description": "Optional container image digest"
|
||||
},
|
||||
"uncertaintyStatementId": {
|
||||
"type": "string",
|
||||
"description": "Reference to the linked uncertainty statement attestation ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BudgetViolation": {
|
||||
"type": "object",
|
||||
"required": ["reasonCode", "count", "limit"],
|
||||
"properties": {
|
||||
"reasonCode": {
|
||||
"type": "string",
|
||||
"enum": ["U-RCH", "U-ID", "U-PROV", "U-VEX", "U-FEED", "U-CONFIG", "U-ANALYZER"],
|
||||
"description": "Unknown reason code that violated the budget"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Actual count of unknowns for this reason"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Configured limit for this reason"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high", "critical"],
|
||||
"description": "Severity of the violation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BudgetDefinition": {
|
||||
"type": "object",
|
||||
"required": ["name", "environment"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Budget rule name"
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"description": "Target environment"
|
||||
},
|
||||
"totalLimit": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Total unknown limit"
|
||||
},
|
||||
"tierMax": {
|
||||
"type": "string",
|
||||
"enum": ["T1", "T2", "T3", "T4"],
|
||||
"description": "Maximum allowed uncertainty tier"
|
||||
},
|
||||
"entropyMax": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Maximum allowed mean entropy"
|
||||
},
|
||||
"reasonLimits": {
|
||||
"type": "object",
|
||||
"description": "Per-reason-code limits",
|
||||
"additionalProperties": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["warn", "block", "warnUnlessException"],
|
||||
"description": "Action to take when budget is exceeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://stella-ops.org/schemas/attestation/uncertainty-statement.v1.json",
|
||||
"title": "Uncertainty Statement",
|
||||
"description": "In-toto predicate type for uncertainty state attestations. Sprint: SPRINT_4300_0002_0002 (UATT-007).",
|
||||
"type": "object",
|
||||
"required": ["_type", "subject", "predicateType", "predicate"],
|
||||
"properties": {
|
||||
"_type": {
|
||||
"type": "string",
|
||||
"const": "https://in-toto.io/Statement/v1"
|
||||
},
|
||||
"subject": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["digest"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Subject identifier (e.g., SBOM file name or image reference)"
|
||||
},
|
||||
"digest": {
|
||||
"type": "object",
|
||||
"description": "Cryptographic digest of the subject",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-fA-F0-9]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"predicateType": {
|
||||
"type": "string",
|
||||
"const": "uncertainty.stella/v1"
|
||||
},
|
||||
"predicate": {
|
||||
"$ref": "#/$defs/UncertaintyPredicate"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"UncertaintyPredicate": {
|
||||
"type": "object",
|
||||
"required": ["graphRevisionId", "aggregateTier", "meanEntropy", "unknownCount", "evaluatedAt"],
|
||||
"properties": {
|
||||
"graphRevisionId": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the knowledge graph revision used in evaluation"
|
||||
},
|
||||
"aggregateTier": {
|
||||
"type": "string",
|
||||
"enum": ["T1", "T2", "T3", "T4"],
|
||||
"description": "Aggregate uncertainty tier (T1 = highest uncertainty, T4 = lowest)"
|
||||
},
|
||||
"meanEntropy": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Mean entropy across all unknowns (0.0 = certain, 1.0 = maximum uncertainty)"
|
||||
},
|
||||
"unknownCount": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Total count of unknowns in this evaluation"
|
||||
},
|
||||
"markers": {
|
||||
"type": "array",
|
||||
"description": "Breakdown of unknowns by marker kind",
|
||||
"items": {
|
||||
"$ref": "#/$defs/UnknownMarker"
|
||||
}
|
||||
},
|
||||
"evaluatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO-8601 timestamp of uncertainty evaluation"
|
||||
},
|
||||
"policyRevisionId": {
|
||||
"type": "string",
|
||||
"description": "Optional policy revision ID if uncertainty was evaluated with policy"
|
||||
},
|
||||
"imageDigest": {
|
||||
"type": "string",
|
||||
"pattern": "^sha256:[a-fA-F0-9]{64}$",
|
||||
"description": "Optional container image digest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UnknownMarker": {
|
||||
"type": "object",
|
||||
"required": ["kind", "count", "entropy"],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["U-RCH", "U-ID", "U-PROV", "U-VEX", "U-FEED", "U-CONFIG", "U-ANALYZER"],
|
||||
"description": "Unknown marker kind code"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Count of unknowns with this marker"
|
||||
},
|
||||
"entropy": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "Mean entropy for this marker kind"
|
||||
},
|
||||
"tier": {
|
||||
"type": "string",
|
||||
"enum": ["T1", "T2", "T3", "T4"],
|
||||
"description": "Uncertainty tier for this marker kind"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonSchema.Net" Version="7.3.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj" />
|
||||
|
||||
@@ -118,11 +118,14 @@ public sealed class PredicateSchemaValidator : IPredicateSchemaValidator
|
||||
{
|
||||
foreach (var detail in results.Details)
|
||||
{
|
||||
if (detail.HasErrors)
|
||||
if (detail.HasErrors && detail.Errors is not null)
|
||||
{
|
||||
var errorMsg = detail.Errors?.FirstOrDefault()?.Value ?? "Unknown error";
|
||||
var location = detail.InstanceLocation.ToString();
|
||||
errors.Add($"{location}: {errorMsg}");
|
||||
foreach (var error in detail.Errors)
|
||||
{
|
||||
var errorMsg = error.Value ?? "Unknown error";
|
||||
var location = detail.InstanceLocation.ToString();
|
||||
errors.Add($"{location}: {errorMsg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +164,9 @@ public sealed class PredicateSchemaValidator : IPredicateSchemaValidator
|
||||
|
||||
try
|
||||
{
|
||||
var schema = JsonSchema.FromStream(stream);
|
||||
using var reader = new StreamReader(stream);
|
||||
var schemaJson = reader.ReadToEnd();
|
||||
var schema = JsonSchema.FromText(schemaJson);
|
||||
schemas[key] = schema;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -73,6 +73,18 @@ public sealed record ProofSpineRequest
|
||||
/// Key profile to use for signing the spine statement.
|
||||
/// </summary>
|
||||
public SigningKeyProfile SigningProfile { get; init; } = SigningKeyProfile.Authority;
|
||||
|
||||
/// <summary>
|
||||
/// Optional: ID of the uncertainty state attestation to include in the spine.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
public string? UncertaintyStatementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: ID of the uncertainty budget attestation to include in the spine.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
public string? UncertaintyBudgetStatementId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -92,4 +92,26 @@ public interface IStatementBuilder
|
||||
SbomLinkageStatement BuildSbomLinkageStatement(
|
||||
IReadOnlyList<ProofSubject> subjects,
|
||||
SbomLinkagePayload predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Build an Uncertainty statement for signing.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
/// <param name="subject">The artifact subject this uncertainty relates to.</param>
|
||||
/// <param name="predicate">The uncertainty payload.</param>
|
||||
/// <returns>An UncertaintyStatement ready for signing.</returns>
|
||||
UncertaintyStatement BuildUncertaintyStatement(
|
||||
ProofSubject subject,
|
||||
UncertaintyPayload predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Build an Uncertainty Budget statement for signing.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
/// <param name="subject">The artifact subject this budget evaluation relates to.</param>
|
||||
/// <param name="predicate">The uncertainty budget payload.</param>
|
||||
/// <returns>An UncertaintyBudgetStatement ready for signing.</returns>
|
||||
UncertaintyBudgetStatement BuildUncertaintyBudgetStatement(
|
||||
ProofSubject subject,
|
||||
UncertaintyBudgetPayload predicate);
|
||||
}
|
||||
|
||||
@@ -103,4 +103,34 @@ public sealed class StatementBuilder : IStatementBuilder
|
||||
Predicate = predicate
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UncertaintyStatement BuildUncertaintyStatement(
|
||||
ProofSubject subject,
|
||||
UncertaintyPayload predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subject);
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
|
||||
return new UncertaintyStatement
|
||||
{
|
||||
Subject = [subject.ToSubject()],
|
||||
Predicate = predicate
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UncertaintyBudgetStatement BuildUncertaintyBudgetStatement(
|
||||
ProofSubject subject,
|
||||
UncertaintyBudgetPayload predicate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subject);
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
|
||||
return new UncertaintyBudgetStatement
|
||||
{
|
||||
Subject = [subject.ToSubject()],
|
||||
Predicate = predicate
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,13 @@ public sealed record DeltaVerdictPredicate
|
||||
/// </summary>
|
||||
[JsonPropertyName("comparedAt")]
|
||||
public required DateTimeOffset ComparedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unknowns budget evaluation result (if available).
|
||||
/// Sprint: SPRINT_5100_0004_0001 Task T5
|
||||
/// </summary>
|
||||
[JsonPropertyName("unknownsBudget")]
|
||||
public UnknownsBudgetPredicate? UnknownsBudget { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UnknownsBudgetPredicate.cs
|
||||
// Sprint: SPRINT_5100_0004_0001_unknowns_budget_ci_gates
|
||||
// Task: T5 - Attestation Integration
|
||||
// Description: DSSE predicate for unknowns budget evaluation in verdict attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
/// <summary>
|
||||
/// DSSE predicate for unknowns budget evaluation within verdict attestations.
|
||||
/// predicateType: unknowns-budget.stella/v1
|
||||
/// </summary>
|
||||
public sealed record UnknownsBudgetPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// The predicate type URI for unknowns budget attestations.
|
||||
/// </summary>
|
||||
public const string PredicateType = "unknowns-budget.stella/v1";
|
||||
|
||||
/// <summary>
|
||||
/// Environment for which the budget was evaluated (prod, stage, dev).
|
||||
/// </summary>
|
||||
[JsonPropertyName("environment")]
|
||||
public required string Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of unknowns found in the scan.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalUnknowns")]
|
||||
public required int TotalUnknowns { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum unknowns allowed by the budget (null if unlimited).
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalLimit")]
|
||||
public int? TotalLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the scan is within budget limits.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isWithinBudget")]
|
||||
public required bool IsWithinBudget { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Percentage of budget used (0-100+).
|
||||
/// </summary>
|
||||
[JsonPropertyName("percentageUsed")]
|
||||
public decimal PercentageUsed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Action recommended when budget is exceeded.
|
||||
/// </summary>
|
||||
[JsonPropertyName("recommendedAction")]
|
||||
public string? RecommendedAction { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Violations by reason code (if any).
|
||||
/// </summary>
|
||||
[JsonPropertyName("violations")]
|
||||
public ImmutableArray<BudgetViolationPredicate> Violations { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Breakdown of unknowns by reason code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("byReasonCode")]
|
||||
public ImmutableDictionary<string, int> ByReasonCode { get; init; }
|
||||
= ImmutableDictionary<string, int>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the budget was evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evaluatedAt")]
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional message describing the budget status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual budget violation for a specific reason code.
|
||||
/// </summary>
|
||||
public sealed record BudgetViolationPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Reason code for this violation (e.g., Reachability, Identity).
|
||||
/// </summary>
|
||||
[JsonPropertyName("reasonCode")]
|
||||
public required string ReasonCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of unknowns with this reason code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("count")]
|
||||
public required int Count { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed for this reason code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("limit")]
|
||||
public required int Limit { get; init; }
|
||||
}
|
||||
@@ -61,4 +61,18 @@ public sealed record ProofSpinePayload
|
||||
/// </summary>
|
||||
[JsonPropertyName("proofBundleId")]
|
||||
public required string ProofBundleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: ID of the uncertainty state attestation.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
[JsonPropertyName("uncertaintyStatementId")]
|
||||
public string? UncertaintyStatementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: ID of the uncertainty budget evaluation attestation.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
[JsonPropertyName("uncertaintyBudgetStatementId")]
|
||||
public string? UncertaintyBudgetStatementId { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UncertaintyBudgetStatement.cs
|
||||
// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
// Description: In-toto predicate type for uncertainty budget evaluation attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Statements;
|
||||
|
||||
/// <summary>
|
||||
/// In-toto statement for uncertainty budget evaluation attestations.
|
||||
/// Predicate type: uncertainty-budget.stella/v1
|
||||
/// </summary>
|
||||
public sealed record UncertaintyBudgetStatement : InTotoStatement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("predicateType")]
|
||||
public override string PredicateType => "uncertainty-budget.stella/v1";
|
||||
|
||||
/// <summary>
|
||||
/// The uncertainty budget evaluation payload.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate")]
|
||||
public required UncertaintyBudgetPayload Predicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Payload for uncertainty budget evaluation statements.
|
||||
/// </summary>
|
||||
public sealed record UncertaintyBudgetPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version for this predicate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public string SchemaVersion { get; init; } = "1.0";
|
||||
|
||||
/// <summary>
|
||||
/// The environment this budget was evaluated for (prod, staging, dev).
|
||||
/// </summary>
|
||||
[JsonPropertyName("environment")]
|
||||
public required string Environment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the evaluation passed (within budget).
|
||||
/// </summary>
|
||||
[JsonPropertyName("passed")]
|
||||
public required bool Passed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The action recommended by the budget policy.
|
||||
/// Values: pass, warn, block.
|
||||
/// </summary>
|
||||
[JsonPropertyName("action")]
|
||||
public required string Action { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The budget definition that was applied.
|
||||
/// </summary>
|
||||
[JsonPropertyName("budget")]
|
||||
public required BudgetDefinition Budget { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual counts observed during evaluation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("observed")]
|
||||
public required BudgetObservation Observed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Violations detected during budget evaluation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("violations")]
|
||||
public IReadOnlyList<BudgetViolationEntry>? Violations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Exceptions that were applied to cover violations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exceptionsApplied")]
|
||||
public IReadOnlyList<BudgetExceptionEntry>? ExceptionsApplied { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when this budget was evaluated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evaluatedAt")]
|
||||
public required DateTimeOffset EvaluatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digest of the policy bundle containing the budget rules.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyDigest")]
|
||||
public string? PolicyDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable summary message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Definition of a budget with limits.
|
||||
/// </summary>
|
||||
public sealed record BudgetDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Budget identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("budgetId")]
|
||||
public required string BudgetId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum total unknowns allowed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalLimit")]
|
||||
public int? TotalLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-reason-code limits.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reasonLimits")]
|
||||
public IReadOnlyDictionary<string, int>? ReasonLimits { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Per-tier limits (e.g., T1 = 0, T2 = 5).
|
||||
/// </summary>
|
||||
[JsonPropertyName("tierLimits")]
|
||||
public IReadOnlyDictionary<string, int>? TierLimits { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed cumulative entropy.
|
||||
/// </summary>
|
||||
[JsonPropertyName("maxCumulativeEntropy")]
|
||||
public double? MaxCumulativeEntropy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Observed values during budget evaluation.
|
||||
/// </summary>
|
||||
public sealed record BudgetObservation
|
||||
{
|
||||
/// <summary>
|
||||
/// Total unknowns observed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalUnknowns")]
|
||||
public required int TotalUnknowns { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unknowns by reason code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("byReasonCode")]
|
||||
public IReadOnlyDictionary<string, int>? ByReasonCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unknowns by tier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("byTier")]
|
||||
public IReadOnlyDictionary<string, int>? ByTier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cumulative entropy observed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("cumulativeEntropy")]
|
||||
public double? CumulativeEntropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mean entropy per unknown.
|
||||
/// </summary>
|
||||
[JsonPropertyName("meanEntropy")]
|
||||
public double? MeanEntropy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A specific budget violation.
|
||||
/// </summary>
|
||||
public sealed record BudgetViolationEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of limit violated (total, reason, tier, entropy).
|
||||
/// </summary>
|
||||
[JsonPropertyName("limitType")]
|
||||
public required string LimitType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Specific limit key (e.g., "U-RCH" for reason, "T1" for tier).
|
||||
/// </summary>
|
||||
[JsonPropertyName("limitKey")]
|
||||
public string? LimitKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The configured limit value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("limit")]
|
||||
public required double Limit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The observed value that exceeded the limit.
|
||||
/// </summary>
|
||||
[JsonPropertyName("observed")]
|
||||
public required double Observed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount by which the limit was exceeded.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exceeded")]
|
||||
public required double Exceeded { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Severity of this violation (critical, high, medium, low).
|
||||
/// </summary>
|
||||
[JsonPropertyName("severity")]
|
||||
public string? Severity { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An exception applied to cover a budget violation.
|
||||
/// </summary>
|
||||
public sealed record BudgetExceptionEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("exceptionId")]
|
||||
public required string ExceptionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason codes covered by this exception.
|
||||
/// </summary>
|
||||
[JsonPropertyName("coveredReasons")]
|
||||
public IReadOnlyList<string>? CoveredReasons { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tiers covered by this exception.
|
||||
/// </summary>
|
||||
[JsonPropertyName("coveredTiers")]
|
||||
public IReadOnlyList<string>? CoveredTiers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this exception expires (if time-limited).
|
||||
/// </summary>
|
||||
[JsonPropertyName("expiresAt")]
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for the exception.
|
||||
/// </summary>
|
||||
[JsonPropertyName("justification")]
|
||||
public string? Justification { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Who approved this exception.
|
||||
/// </summary>
|
||||
[JsonPropertyName("approvedBy")]
|
||||
public string? ApprovedBy { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UncertaintyStatement.cs
|
||||
// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
// Description: In-toto predicate type for uncertainty state attestations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Statements;
|
||||
|
||||
/// <summary>
|
||||
/// In-toto statement for uncertainty state attestations.
|
||||
/// Predicate type: uncertainty.stella/v1
|
||||
/// </summary>
|
||||
public sealed record UncertaintyStatement : InTotoStatement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("predicateType")]
|
||||
public override string PredicateType => "uncertainty.stella/v1";
|
||||
|
||||
/// <summary>
|
||||
/// The uncertainty state payload.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate")]
|
||||
public required UncertaintyPayload Predicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Payload for uncertainty state statements.
|
||||
/// </summary>
|
||||
public sealed record UncertaintyPayload
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version for this predicate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public string SchemaVersion { get; init; } = "1.0";
|
||||
|
||||
/// <summary>
|
||||
/// The aggregate uncertainty tier (T1-T4).
|
||||
/// T1 = High uncertainty, T4 = Negligible.
|
||||
/// </summary>
|
||||
[JsonPropertyName("aggregateTier")]
|
||||
public required string AggregateTier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mean entropy across all uncertainty states (0.0-1.0).
|
||||
/// </summary>
|
||||
[JsonPropertyName("meanEntropy")]
|
||||
public required double MeanEntropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total count of uncertainty markers.
|
||||
/// </summary>
|
||||
[JsonPropertyName("markerCount")]
|
||||
public required int MarkerCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk modifier applied due to uncertainty (multiplier, e.g., 1.5 = 50% boost).
|
||||
/// </summary>
|
||||
[JsonPropertyName("riskModifier")]
|
||||
public required double RiskModifier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Individual uncertainty states that contribute to this aggregate.
|
||||
/// </summary>
|
||||
[JsonPropertyName("states")]
|
||||
public required IReadOnlyList<UncertaintyStateEntry> States { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Evidence references supporting the uncertainty claims.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidence")]
|
||||
public IReadOnlyList<UncertaintyEvidence>? Evidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when this uncertainty state was computed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("computedAt")]
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the knowledge snapshot used.
|
||||
/// </summary>
|
||||
[JsonPropertyName("knowledgeSnapshotId")]
|
||||
public string? KnowledgeSnapshotId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An individual uncertainty state entry.
|
||||
/// </summary>
|
||||
public sealed record UncertaintyStateEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Uncertainty code (U1-U4 or custom).
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public required string Code { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable name for this uncertainty type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Entropy value for this state (0.0-1.0).
|
||||
/// Higher values indicate more uncertainty.
|
||||
/// </summary>
|
||||
[JsonPropertyName("entropy")]
|
||||
public required double Entropy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tier classification for this state (T1-T4).
|
||||
/// </summary>
|
||||
[JsonPropertyName("tier")]
|
||||
public required string Tier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Marker kind that triggered this uncertainty.
|
||||
/// </summary>
|
||||
[JsonPropertyName("markerKind")]
|
||||
public string? MarkerKind { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence band (high, medium, low).
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidenceBand")]
|
||||
public string? ConfidenceBand { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence supporting an uncertainty claim.
|
||||
/// </summary>
|
||||
public sealed record UncertaintyEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of evidence (advisory, binary, purl, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the evidence source.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reference")]
|
||||
public required string Reference { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional digest for content-addressed evidence.
|
||||
/// </summary>
|
||||
[JsonPropertyName("digest")]
|
||||
public string? Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
@@ -183,4 +183,18 @@ public sealed record VerdictOutputs
|
||||
/// </summary>
|
||||
[JsonPropertyName("vexVerdictId")]
|
||||
public required string VexVerdictId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: ID of the uncertainty state attestation.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
[JsonPropertyName("uncertaintyStatementId")]
|
||||
public string? UncertaintyStatementId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional: ID of the uncertainty budget attestation.
|
||||
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
/// </summary>
|
||||
[JsonPropertyName("uncertaintyBudgetStatementId")]
|
||||
public string? UncertaintyBudgetStatementId { get; init; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UncertaintyStatementTests.cs
|
||||
// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
|
||||
// Description: Unit tests for uncertainty attestation statements.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.ProofChain.Builders;
|
||||
using StellaOps.Attestor.ProofChain.Statements;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Tests.Statements;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for UncertaintyStatement and UncertaintyBudgetStatement.
|
||||
/// </summary>
|
||||
public sealed class UncertaintyStatementTests
|
||||
{
|
||||
private readonly StatementBuilder _builder = new();
|
||||
private readonly DateTimeOffset _fixedTime = new(2025, 12, 22, 10, 0, 0, TimeSpan.Zero);
|
||||
|
||||
[Fact]
|
||||
public void BuildUncertaintyStatement_SetsPredicateTypeAndSubject()
|
||||
{
|
||||
var subject = CreateSubject("image:demo@sha256:abc123", "abc123");
|
||||
var predicate = new UncertaintyPayload
|
||||
{
|
||||
AggregateTier = "T2",
|
||||
MeanEntropy = 0.45,
|
||||
MarkerCount = 3,
|
||||
RiskModifier = 1.25,
|
||||
States = new[]
|
||||
{
|
||||
new UncertaintyStateEntry
|
||||
{
|
||||
Code = "U1",
|
||||
Name = "MissingSymbolResolution",
|
||||
Entropy = 0.5,
|
||||
Tier = "T2",
|
||||
MarkerKind = "missing_symbol"
|
||||
},
|
||||
new UncertaintyStateEntry
|
||||
{
|
||||
Code = "U2",
|
||||
Name = "MissingPurl",
|
||||
Entropy = 0.4,
|
||||
Tier = "T3"
|
||||
}
|
||||
},
|
||||
ComputedAt = _fixedTime
|
||||
};
|
||||
|
||||
var statement = _builder.BuildUncertaintyStatement(subject, predicate);
|
||||
|
||||
Assert.Equal("https://in-toto.io/Statement/v1", statement.Type);
|
||||
Assert.Equal("uncertainty.stella/v1", statement.PredicateType);
|
||||
Assert.Single(statement.Subject);
|
||||
Assert.Equal(subject.Name, statement.Subject[0].Name);
|
||||
Assert.Equal("T2", statement.Predicate.AggregateTier);
|
||||
Assert.Equal(0.45, statement.Predicate.MeanEntropy);
|
||||
Assert.Equal(2, statement.Predicate.States.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildUncertaintyBudgetStatement_SetsPredicateTypeAndSubject()
|
||||
{
|
||||
var subject = CreateSubject("image:demo@sha256:abc123", "abc123");
|
||||
var predicate = new UncertaintyBudgetPayload
|
||||
{
|
||||
Environment = "production",
|
||||
Passed = false,
|
||||
Action = "block",
|
||||
Budget = new BudgetDefinition
|
||||
{
|
||||
BudgetId = "prod-budget-v1",
|
||||
TotalLimit = 5,
|
||||
ReasonLimits = new Dictionary<string, int>
|
||||
{
|
||||
["U-RCH"] = 2,
|
||||
["U-ID"] = 3
|
||||
}
|
||||
},
|
||||
Observed = new BudgetObservation
|
||||
{
|
||||
TotalUnknowns = 8,
|
||||
ByReasonCode = new Dictionary<string, int>
|
||||
{
|
||||
["U-RCH"] = 4,
|
||||
["U-ID"] = 4
|
||||
}
|
||||
},
|
||||
Violations = new[]
|
||||
{
|
||||
new BudgetViolationEntry
|
||||
{
|
||||
LimitType = "total",
|
||||
Limit = 5,
|
||||
Observed = 8,
|
||||
Exceeded = 3,
|
||||
Severity = "high"
|
||||
}
|
||||
},
|
||||
EvaluatedAt = _fixedTime
|
||||
};
|
||||
|
||||
var statement = _builder.BuildUncertaintyBudgetStatement(subject, predicate);
|
||||
|
||||
Assert.Equal("https://in-toto.io/Statement/v1", statement.Type);
|
||||
Assert.Equal("uncertainty-budget.stella/v1", statement.PredicateType);
|
||||
Assert.Single(statement.Subject);
|
||||
Assert.Equal("production", statement.Predicate.Environment);
|
||||
Assert.False(statement.Predicate.Passed);
|
||||
Assert.Equal("block", statement.Predicate.Action);
|
||||
Assert.NotNull(statement.Predicate.Violations);
|
||||
Assert.Single(statement.Predicate.Violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UncertaintyStatement_RoundTripsViaJson()
|
||||
{
|
||||
var subject = CreateSubject("image:demo", "abc123");
|
||||
var statement = _builder.BuildUncertaintyStatement(subject, new UncertaintyPayload
|
||||
{
|
||||
AggregateTier = "T3",
|
||||
MeanEntropy = 0.25,
|
||||
MarkerCount = 1,
|
||||
RiskModifier = 1.1,
|
||||
States = new[]
|
||||
{
|
||||
new UncertaintyStateEntry
|
||||
{
|
||||
Code = "U3",
|
||||
Name = "UntrustedAdvisory",
|
||||
Entropy = 0.25,
|
||||
Tier = "T3"
|
||||
}
|
||||
},
|
||||
ComputedAt = _fixedTime,
|
||||
KnowledgeSnapshotId = "ksm:sha256:abc123"
|
||||
});
|
||||
|
||||
var json = JsonSerializer.Serialize(statement);
|
||||
var restored = JsonSerializer.Deserialize<UncertaintyStatement>(json);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(statement.PredicateType, restored.PredicateType);
|
||||
Assert.Equal(statement.Subject[0].Name, restored.Subject[0].Name);
|
||||
Assert.Equal(statement.Predicate.AggregateTier, restored.Predicate.AggregateTier);
|
||||
Assert.Equal(statement.Predicate.MeanEntropy, restored.Predicate.MeanEntropy);
|
||||
Assert.Equal(statement.Predicate.KnowledgeSnapshotId, restored.Predicate.KnowledgeSnapshotId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UncertaintyBudgetStatement_RoundTripsViaJson()
|
||||
{
|
||||
var subject = CreateSubject("image:demo", "abc123");
|
||||
var statement = _builder.BuildUncertaintyBudgetStatement(subject, new UncertaintyBudgetPayload
|
||||
{
|
||||
Environment = "staging",
|
||||
Passed = true,
|
||||
Action = "pass",
|
||||
Budget = new BudgetDefinition
|
||||
{
|
||||
BudgetId = "staging-budget",
|
||||
TotalLimit = 10
|
||||
},
|
||||
Observed = new BudgetObservation
|
||||
{
|
||||
TotalUnknowns = 3
|
||||
},
|
||||
EvaluatedAt = _fixedTime,
|
||||
Message = "Budget check passed"
|
||||
});
|
||||
|
||||
var json = JsonSerializer.Serialize(statement);
|
||||
var restored = JsonSerializer.Deserialize<UncertaintyBudgetStatement>(json);
|
||||
|
||||
Assert.NotNull(restored);
|
||||
Assert.Equal(statement.PredicateType, restored.PredicateType);
|
||||
Assert.Equal(statement.Predicate.Environment, restored.Predicate.Environment);
|
||||
Assert.True(restored.Predicate.Passed);
|
||||
Assert.Equal("Budget check passed", restored.Predicate.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UncertaintyBudgetStatement_WithExceptions_SerializesCorrectly()
|
||||
{
|
||||
var subject = CreateSubject("image:demo", "abc123");
|
||||
var predicate = new UncertaintyBudgetPayload
|
||||
{
|
||||
Environment = "production",
|
||||
Passed = true,
|
||||
Action = "pass",
|
||||
Budget = new BudgetDefinition
|
||||
{
|
||||
BudgetId = "prod-budget",
|
||||
TotalLimit = 5
|
||||
},
|
||||
Observed = new BudgetObservation
|
||||
{
|
||||
TotalUnknowns = 7,
|
||||
ByReasonCode = new Dictionary<string, int>
|
||||
{
|
||||
["U-RCH"] = 4,
|
||||
["U-ID"] = 3
|
||||
}
|
||||
},
|
||||
ExceptionsApplied = new[]
|
||||
{
|
||||
new BudgetExceptionEntry
|
||||
{
|
||||
ExceptionId = "EXC-2025-001",
|
||||
CoveredReasons = new[] { "U-RCH" },
|
||||
Justification = "Known limitation in reachability analysis",
|
||||
ApprovedBy = "security-team",
|
||||
ExpiresAt = _fixedTime.AddDays(30)
|
||||
}
|
||||
},
|
||||
EvaluatedAt = _fixedTime
|
||||
};
|
||||
|
||||
var statement = _builder.BuildUncertaintyBudgetStatement(subject, predicate);
|
||||
var json = JsonSerializer.Serialize(statement, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
Assert.Contains("EXC-2025-001", json);
|
||||
Assert.Contains("U-RCH", json);
|
||||
Assert.Contains("security-team", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildUncertaintyStatement_NullSubject_Throws()
|
||||
{
|
||||
var predicate = new UncertaintyPayload
|
||||
{
|
||||
AggregateTier = "T4",
|
||||
MeanEntropy = 0.05,
|
||||
MarkerCount = 0,
|
||||
RiskModifier = 1.0,
|
||||
States = Array.Empty<UncertaintyStateEntry>(),
|
||||
ComputedAt = _fixedTime
|
||||
};
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => _builder.BuildUncertaintyStatement(null!, predicate));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildUncertaintyBudgetStatement_NullPredicate_Throws()
|
||||
{
|
||||
var subject = CreateSubject("image:demo", "abc123");
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => _builder.BuildUncertaintyBudgetStatement(subject, null!));
|
||||
}
|
||||
|
||||
private static ProofSubject CreateSubject(string name, string sha256Digest)
|
||||
=> new()
|
||||
{
|
||||
Name = name,
|
||||
Digest = new Dictionary<string, string> { ["sha256"] = sha256Digest }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// UnknownsBudgetPredicateTests.cs
|
||||
// Sprint: SPRINT_5100_0004_0001_unknowns_budget_ci_gates
|
||||
// Task: T6 - Unit Tests
|
||||
// Description: Tests for UnknownsBudgetPredicate attestation integration.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Attestor.ProofChain.Predicates;
|
||||
|
||||
namespace StellaOps.Attestor.ProofChain.Tests.Statements;
|
||||
|
||||
public sealed class UnknownsBudgetPredicateTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void PredicateType_IsCorrect()
|
||||
{
|
||||
Assert.Equal("unknowns-budget.stella/v1", UnknownsBudgetPredicate.PredicateType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithinBudget_SetsCorrectProperties()
|
||||
{
|
||||
var predicate = new UnknownsBudgetPredicate
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalUnknowns = 3,
|
||||
TotalLimit = 10,
|
||||
IsWithinBudget = true,
|
||||
PercentageUsed = 30m,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
Assert.Equal("prod", predicate.Environment);
|
||||
Assert.Equal(3, predicate.TotalUnknowns);
|
||||
Assert.Equal(10, predicate.TotalLimit);
|
||||
Assert.True(predicate.IsWithinBudget);
|
||||
Assert.Equal(30m, predicate.PercentageUsed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_ExceedsBudget_SetsCorrectProperties()
|
||||
{
|
||||
var predicate = new UnknownsBudgetPredicate
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalUnknowns = 15,
|
||||
TotalLimit = 10,
|
||||
IsWithinBudget = false,
|
||||
PercentageUsed = 150m,
|
||||
RecommendedAction = "Block",
|
||||
Message = "Budget exceeded: 15 unknowns exceed limit of 10",
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
Assert.False(predicate.IsWithinBudget);
|
||||
Assert.Equal("Block", predicate.RecommendedAction);
|
||||
Assert.Contains("Budget exceeded", predicate.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithViolations_SerializesCorrectly()
|
||||
{
|
||||
var violations = ImmutableArray.Create(
|
||||
new BudgetViolationPredicate
|
||||
{
|
||||
ReasonCode = "Reachability",
|
||||
Count = 5,
|
||||
Limit = 3
|
||||
},
|
||||
new BudgetViolationPredicate
|
||||
{
|
||||
ReasonCode = "Identity",
|
||||
Count = 2,
|
||||
Limit = 1
|
||||
}
|
||||
);
|
||||
|
||||
var predicate = new UnknownsBudgetPredicate
|
||||
{
|
||||
Environment = "stage",
|
||||
TotalUnknowns = 7,
|
||||
TotalLimit = 5,
|
||||
IsWithinBudget = false,
|
||||
Violations = violations,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
Assert.Equal(2, predicate.Violations.Length);
|
||||
Assert.Equal("Reachability", predicate.Violations[0].ReasonCode);
|
||||
Assert.Equal(5, predicate.Violations[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithByReasonCode_SerializesCorrectly()
|
||||
{
|
||||
var byReasonCode = ImmutableDictionary.CreateRange(new[]
|
||||
{
|
||||
new KeyValuePair<string, int>("Reachability", 5),
|
||||
new KeyValuePair<string, int>("Identity", 2),
|
||||
new KeyValuePair<string, int>("VexConflict", 1)
|
||||
});
|
||||
|
||||
var predicate = new UnknownsBudgetPredicate
|
||||
{
|
||||
Environment = "dev",
|
||||
TotalUnknowns = 8,
|
||||
TotalLimit = 20,
|
||||
IsWithinBudget = true,
|
||||
ByReasonCode = byReasonCode,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
Assert.Equal(3, predicate.ByReasonCode.Count);
|
||||
Assert.Equal(5, predicate.ByReasonCode["Reachability"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_ToJson_ProducesValidOutput()
|
||||
{
|
||||
var predicate = new UnknownsBudgetPredicate
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalUnknowns = 3,
|
||||
TotalLimit = 10,
|
||||
IsWithinBudget = true,
|
||||
PercentageUsed = 30m,
|
||||
EvaluatedAt = new DateTimeOffset(2025, 12, 22, 12, 0, 0, TimeSpan.Zero)
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(predicate, JsonOptions);
|
||||
|
||||
Assert.Contains("\"environment\": \"prod\"", json);
|
||||
Assert.Contains("\"totalUnknowns\": 3", json);
|
||||
Assert.Contains("\"totalLimit\": 10", json);
|
||||
Assert.Contains("\"isWithinBudget\": true", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_FromJson_RestoresProperties()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"environment": "stage",
|
||||
"totalUnknowns": 7,
|
||||
"totalLimit": 5,
|
||||
"isWithinBudget": false,
|
||||
"percentageUsed": 140.0,
|
||||
"recommendedAction": "Warn",
|
||||
"violations": [
|
||||
{
|
||||
"reasonCode": "Reachability",
|
||||
"count": 5,
|
||||
"limit": 3
|
||||
}
|
||||
],
|
||||
"evaluatedAt": "2025-12-22T12:00:00Z"
|
||||
}
|
||||
""";
|
||||
|
||||
var predicate = JsonSerializer.Deserialize<UnknownsBudgetPredicate>(json, JsonOptions);
|
||||
|
||||
Assert.NotNull(predicate);
|
||||
Assert.Equal("stage", predicate.Environment);
|
||||
Assert.Equal(7, predicate.TotalUnknowns);
|
||||
Assert.Equal(5, predicate.TotalLimit);
|
||||
Assert.False(predicate.IsWithinBudget);
|
||||
Assert.Equal(140.0m, predicate.PercentageUsed);
|
||||
Assert.Single(predicate.Violations);
|
||||
Assert.Equal("Reachability", predicate.Violations[0].ReasonCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeltaVerdictPredicate_IncludesUnknownsBudget()
|
||||
{
|
||||
var budget = new UnknownsBudgetPredicate
|
||||
{
|
||||
Environment = "prod",
|
||||
TotalUnknowns = 2,
|
||||
TotalLimit = 10,
|
||||
IsWithinBudget = true,
|
||||
EvaluatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var verdict = new DeltaVerdictPredicate
|
||||
{
|
||||
BeforeRevisionId = "rev-1",
|
||||
AfterRevisionId = "rev-2",
|
||||
HasMaterialChange = true,
|
||||
PriorityScore = 0.5,
|
||||
ComparedAt = DateTimeOffset.UtcNow,
|
||||
UnknownsBudget = budget
|
||||
};
|
||||
|
||||
Assert.NotNull(verdict.UnknownsBudget);
|
||||
Assert.Equal("prod", verdict.UnknownsBudget.Environment);
|
||||
Assert.True(verdict.UnknownsBudget.IsWithinBudget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeltaVerdictPredicate_WithoutUnknownsBudget_SerializesCorrectly()
|
||||
{
|
||||
var verdict = new DeltaVerdictPredicate
|
||||
{
|
||||
BeforeRevisionId = "rev-1",
|
||||
AfterRevisionId = "rev-2",
|
||||
HasMaterialChange = false,
|
||||
PriorityScore = 0.0,
|
||||
ComparedAt = DateTimeOffset.UtcNow,
|
||||
UnknownsBudget = null
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(verdict, JsonOptions);
|
||||
|
||||
Assert.DoesNotContain("unknownsBudget", json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BudgetViolationPredicate_Properties_AreCorrect()
|
||||
{
|
||||
var violation = new BudgetViolationPredicate
|
||||
{
|
||||
ReasonCode = "FeedGap",
|
||||
Count = 10,
|
||||
Limit = 5
|
||||
};
|
||||
|
||||
Assert.Equal("FeedGap", violation.ReasonCode);
|
||||
Assert.Equal(10, violation.Count);
|
||||
Assert.Equal(5, violation.Limit);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user