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:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -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"
}
}
}
}
}

View File

@@ -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"
}
}
}
}
}

View File

@@ -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" />

View File

@@ -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)

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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
};
}
}

View File

@@ -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>

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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 }
};
}

View File

@@ -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);
}
}