Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

60
src/Attestor/AGENTS.md Normal file
View File

@@ -0,0 +1,60 @@
# Attestor Module — Agent Charter
## Mission
Manage the attestation and proof chain infrastructure for StellaOps:
- Accept DSSE-signed attestation bundles from Signer and other modules.
- Register attestations with Rekor v2 transparency log for tamper-evident anchoring.
- Provide verification APIs for proof chain validation (signature, payload, Rekor inclusion).
- Serve deterministic evidence bundles linking artifacts to SBOMs, VEX documents, and verdicts.
- Enable "Show Me The Proof" workflows with complete audit trails.
## Expectations
- Coordinate with Signer for cryptographic operations, Scanner/Excititor for attestation generation, and UI for proof visualization.
- Maintain deterministic serialization for reproducible verification outcomes.
- Support offline verification with bundled Rekor inclusion proofs.
- Provide REST APIs for proof chain queries, baseline selection, and trust indicators.
- Keep proof chain storage schema current with migrations.
## Key Components
- **StellaOps.Attestor**: Main attestation service and REST API endpoints
- **StellaOps.Attestor.Envelope**: DSSE envelope handling and serialization
- **StellaOps.Attestor.Types**: Core attestation models and schemas
- **StellaOps.Attestor.Verify**: Verification engine for signatures and Rekor proofs
- **__Libraries**: Shared attestation utilities and storage abstractions
- **__Tests**: Integration tests with Testcontainers for PostgreSQL
## Required Reading
- `docs/modules/attestor/README.md`
- `docs/modules/attestor/architecture.md`
- `docs/modules/attestor/implementation_plan.md`
- `docs/product-advisories/20-Dec-2025 - Stella Ops Reference Architecture.md`
- `docs/modules/platform/architecture-overview.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both corresponding sprint file `/docs/implplan/SPRINT_*.md` and the local `TASKS.md` when you start or finish work.
- 2. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
## Attestation Types
- **SBOM Attestations**: Link container images to SPDX/CycloneDX SBOMs
- **VEX Attestations**: Link OpenVEX statements to products
- **Verdict Attestations**: Link policy evaluation results to artifacts
- **Provenance Attestations**: SLSA provenance for build reproducibility
- **Reachability Attestations**: Link static analysis witness paths to findings
## Proof Chain Model
- **ProofNode**: Individual proof (SBOM, VEX, Verdict, Attestation) with digest and metadata
- **ProofEdge**: Relationship between nodes ("attests", "references", "supersedes")
- **ProofChain**: Complete directed graph from artifact to all linked evidence
- **ProofVerification**: Signature validation, payload hash check, Rekor inclusion proof
## Guardrails
- All attestations must use DSSE envelopes with multiple signature support.
- Rekor anchoring must be optional (support air-gapped deployments).
- Verification must work offline with bundled inclusion proofs.
- Proof chains must be deterministic (stable ordering, canonical serialization).
- Preserve determinism: sort outputs, normalize timestamps (UTC ISO-8601).
- Keep Offline Kit parity in mind—document air-gapped workflows for any new feature.
- Update runbooks/observability assets when operational characteristics change.

View File

@@ -0,0 +1,292 @@
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Attestor.Core.Validation;
using Xunit;
namespace StellaOps.Attestor.Core.Tests.Validation;
public sealed class PredicateSchemaValidatorTests
{
private readonly PredicateSchemaValidator _validator;
public PredicateSchemaValidatorTests()
{
_validator = new PredicateSchemaValidator(NullLogger<PredicateSchemaValidator>.Instance);
}
[Fact]
public void Validate_ValidSbomPredicate_ReturnsValid()
{
var json = """
{
"format": "spdx-3.0.1",
"digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"componentCount": 42,
"uri": "https://example.com/sbom.json",
"tooling": "syft",
"createdAt": "2025-12-22T00:00:00Z"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/sbom@v1", predicate);
Assert.True(result.IsValid);
Assert.Null(result.ErrorMessage);
}
[Fact]
public void Validate_ValidVexPredicate_ReturnsValid()
{
var json = """
{
"format": "openvex",
"statements": [
{
"vulnerability": "CVE-2024-12345",
"status": "not_affected",
"justification": "Component not used",
"products": ["pkg:npm/lodash@4.17.21"]
}
],
"digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"author": "security@example.com",
"timestamp": "2025-12-22T00:00:00Z"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/vex@v1", predicate);
Assert.True(result.IsValid);
}
[Fact]
public void Validate_ValidReachabilityPredicate_ReturnsValid()
{
var json = """
{
"result": "unreachable",
"confidence": 0.95,
"graphDigest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"paths": [],
"entrypoints": [
{
"type": "http",
"route": "/api/users",
"auth": "required"
}
],
"computedAt": "2025-12-22T00:00:00Z",
"expiresAt": "2025-12-29T00:00:00Z"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/reachability@v1", predicate);
Assert.True(result.IsValid);
}
[Fact]
public void Validate_ValidPolicyDecisionPredicate_ReturnsValid()
{
var json = """
{
"finding_id": "CVE-2024-12345@pkg:npm/lodash@4.17.20",
"cve": "CVE-2024-12345",
"component_purl": "pkg:npm/lodash@4.17.20",
"decision": "Block",
"reasoning": {
"rules_evaluated": 5,
"rules_matched": ["high-severity", "reachable"],
"final_score": 85.5,
"risk_multiplier": 1.2,
"reachability_state": "reachable",
"vex_status": "affected",
"summary": "High severity vulnerability is reachable"
},
"evidence_refs": [
"sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
],
"evaluated_at": "2025-12-22T00:00:00Z",
"expires_at": "2025-12-23T00:00:00Z",
"policy_version": "1.0.0",
"policy_hash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/policy-decision@v1", predicate);
Assert.True(result.IsValid);
}
[Fact]
public void Validate_ValidHumanApprovalPredicate_ReturnsValid()
{
var json = """
{
"schema": "human-approval-v1",
"approval_id": "approval-123",
"finding_id": "CVE-2024-12345",
"decision": "AcceptRisk",
"approver": {
"user_id": "alice@example.com",
"display_name": "Alice Smith",
"role": "Security Engineer"
},
"justification": "Risk accepted for legacy system scheduled for decommission in 30 days",
"approved_at": "2025-12-22T00:00:00Z",
"expires_at": "2026-01-22T00:00:00Z"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/human-approval@v1", predicate);
Assert.True(result.IsValid);
}
[Fact]
public void Validate_InvalidVexStatus_ReturnsFail()
{
var json = """
{
"format": "openvex",
"statements": [
{
"vulnerability": "CVE-2024-12345",
"status": "invalid_status",
"products": []
}
],
"digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/vex@v1", predicate);
Assert.False(result.IsValid);
Assert.NotNull(result.ErrorMessage);
}
[Fact]
public void Validate_MissingRequiredField_ReturnsFail()
{
var json = """
{
"format": "spdx-3.0.1",
"componentCount": 42
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/sbom@v1", predicate);
Assert.False(result.IsValid);
Assert.Contains("digest", result.ErrorMessage ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_UnknownPredicateType_ReturnsSkip()
{
var json = """
{
"someField": "someValue"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/unknown@v1", predicate);
Assert.True(result.IsValid);
Assert.Contains("skip", result.ErrorMessage ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Validate_InvalidDigestFormat_ReturnsFail()
{
var json = """
{
"format": "spdx-3.0.1",
"digest": "invalid-digest-format",
"componentCount": 42
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/sbom@v1", predicate);
Assert.False(result.IsValid);
Assert.NotEmpty(result.Errors);
}
[Fact]
public void Validate_NormalizePredicateType_HandlesWithAndWithoutPrefix()
{
var json = """
{
"format": "spdx-3.0.1",
"digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"componentCount": 42
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result1 = _validator.Validate("stella.ops/sbom@v1", predicate);
var result2 = _validator.Validate("sbom@v1", predicate);
Assert.True(result1.IsValid);
Assert.True(result2.IsValid);
}
[Fact]
public void Validate_ValidBoundaryPredicate_ReturnsValid()
{
var json = """
{
"surface": "http",
"exposure": "public",
"observedAt": "2025-12-22T00:00:00Z",
"endpoints": [
{
"route": "/api/users/:id",
"method": "GET",
"auth": "required"
}
],
"auth": {
"mechanism": "jwt",
"required_scopes": ["read:users"]
},
"controls": ["rate-limit", "WAF"],
"expiresAt": "2025-12-25T00:00:00Z"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/boundary@v1", predicate);
Assert.True(result.IsValid);
}
[Fact]
public void Validate_InvalidReachabilityConfidence_ReturnsFail()
{
var json = """
{
"result": "reachable",
"confidence": 1.5,
"graphDigest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
""";
var predicate = JsonDocument.Parse(json).RootElement;
var result = _validator.Validate("stella.ops/reachability@v1", predicate);
Assert.False(result.IsValid);
}
}

View File

@@ -0,0 +1,176 @@
using System.Text.Json;
using Json.Schema;
using Microsoft.Extensions.Logging;
namespace StellaOps.Attestor.Core.Validation;
/// <summary>
/// Validation result for predicate schema validation.
/// </summary>
public sealed record ValidationResult
{
public required bool IsValid { get; init; }
public required string? ErrorMessage { get; init; }
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
public static ValidationResult Valid() => new()
{
IsValid = true,
ErrorMessage = null
};
public static ValidationResult Invalid(string message, IReadOnlyList<string>? errors = null) => new()
{
IsValid = false,
ErrorMessage = message,
Errors = errors ?? Array.Empty<string>()
};
public static ValidationResult Skip(string reason) => new()
{
IsValid = true,
ErrorMessage = $"Skipped: {reason}"
};
}
/// <summary>
/// Interface for validating attestation predicates against JSON schemas.
/// </summary>
public interface IPredicateSchemaValidator
{
/// <summary>
/// Validates a predicate against its JSON schema.
/// </summary>
/// <param name="predicateType">The predicate type URI (e.g., "stella.ops/sbom@v1").</param>
/// <param name="predicate">The predicate JSON element to validate.</param>
/// <returns>Validation result.</returns>
ValidationResult Validate(string predicateType, JsonElement predicate);
}
/// <summary>
/// Validates attestation predicates against their JSON schemas.
/// </summary>
public sealed class PredicateSchemaValidator : IPredicateSchemaValidator
{
private readonly IReadOnlyDictionary<string, JsonSchema> _schemas;
private readonly ILogger<PredicateSchemaValidator> _logger;
public PredicateSchemaValidator(ILogger<PredicateSchemaValidator> logger)
{
_logger = logger;
_schemas = LoadSchemas();
}
public ValidationResult Validate(string predicateType, JsonElement predicate)
{
// Normalize predicate type (handle both with and without stella.ops/ prefix)
var normalizedType = NormalizePredicateType(predicateType);
if (!_schemas.TryGetValue(normalizedType, out var schema))
{
_logger.LogDebug("No schema found for predicate type {PredicateType}, skipping validation", predicateType);
return ValidationResult.Skip($"No schema for {predicateType}");
}
try
{
var results = schema.Evaluate(predicate, new EvaluationOptions
{
OutputFormat = OutputFormat.List
});
if (results.IsValid)
{
_logger.LogDebug("Predicate {PredicateType} validated successfully", predicateType);
return ValidationResult.Valid();
}
var errors = CollectErrors(results);
_logger.LogWarning("Predicate {PredicateType} validation failed: {ErrorCount} errors",
predicateType, errors.Count);
return ValidationResult.Invalid(
$"Schema validation failed for {predicateType}",
errors);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating predicate {PredicateType}", predicateType);
return ValidationResult.Invalid($"Validation error: {ex.Message}");
}
}
private static string NormalizePredicateType(string predicateType)
{
// Handle both "stella.ops/sbom@v1" and "sbom@v1" formats
if (predicateType.StartsWith("stella.ops/", StringComparison.OrdinalIgnoreCase))
{
return predicateType["stella.ops/".Length..];
}
return predicateType;
}
private static IReadOnlyList<string> CollectErrors(EvaluationResults results)
{
var errors = new List<string>();
if (results.HasErrors)
{
foreach (var detail in results.Details)
{
if (detail.HasErrors)
{
var errorMsg = detail.Errors?.FirstOrDefault()?.Value ?? "Unknown error";
var location = detail.InstanceLocation.ToString();
errors.Add($"{location}: {errorMsg}");
}
}
}
return errors;
}
private static IReadOnlyDictionary<string, JsonSchema> LoadSchemas()
{
var schemas = new Dictionary<string, JsonSchema>(StringComparer.OrdinalIgnoreCase);
// Load embedded schema resources
var assembly = typeof(PredicateSchemaValidator).Assembly;
var resourcePrefix = "StellaOps.Attestor.Core.Schemas.";
var schemaFiles = new[]
{
("sbom@v1", "sbom.v1.schema.json"),
("vex@v1", "vex.v1.schema.json"),
("reachability@v1", "reachability.v1.schema.json"),
("boundary@v1", "boundary.v1.schema.json"),
("policy-decision@v1", "policy-decision.v1.schema.json"),
("human-approval@v1", "human-approval.v1.schema.json")
};
foreach (var (key, fileName) in schemaFiles)
{
var resourceName = resourcePrefix + fileName;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
// Schema not embedded, skip gracefully
continue;
}
try
{
var schema = JsonSchema.FromStream(stream);
schemas[key] = schema;
}
catch (Exception ex)
{
// Log and continue - don't fail on single schema load error
Console.WriteLine($"Failed to load schema {fileName}: {ex.Message}");
}
}
return schemas;
}
}

View File

@@ -0,0 +1,19 @@
# Attestor __Libraries AGENTS
## Purpose & Scope
- Working directory: `src/Attestor/__Libraries/` (shared attestation libraries).
- Roles: backend engineer, QA automation.
## Required Reading (treat as read before DOING)
- `docs/README.md`
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
- `docs/modules/attestor/architecture.md`
- Relevant sprint files.
## Working Agreements
- Preserve DSSE/in-toto compatibility and deterministic serialization.
- Avoid network dependencies in libraries and tests.
- Record schema changes in attestor docs and sprint Decisions & Risks.
## Testing
- Add tests under the corresponding attestor test projects or `src/Attestor/__Tests`.

View File

@@ -155,6 +155,12 @@ public sealed class PredicateSchemaValidator : IJsonSchemaValidator
case "verdict.stella/v1":
errors.AddRange(ValidateVerdictPredicate(root));
break;
case "delta-verdict.stella/v1":
errors.AddRange(ValidateDeltaVerdictPredicate(root));
break;
case "reachability-subgraph.stella/v1":
errors.AddRange(ValidateReachabilitySubgraphPredicate(root));
break;
}
return errors.Count > 0
@@ -192,6 +198,8 @@ public sealed class PredicateSchemaValidator : IJsonSchemaValidator
"proofspine.stella/v1" => true,
"verdict.stella/v1" => true,
"https://stella-ops.org/predicates/sbom-linkage/v1" => true,
"delta-verdict.stella/v1" => true,
"reachability-subgraph.stella/v1" => true,
_ => false
};
}
@@ -248,4 +256,30 @@ public sealed class PredicateSchemaValidator : IJsonSchemaValidator
if (!root.TryGetProperty("verifiedAt", out _))
yield return new() { Path = "/verifiedAt", Message = "Required property missing", Keyword = "required" };
}
private static IEnumerable<SchemaValidationError> ValidateDeltaVerdictPredicate(JsonElement root)
{
// Required: beforeRevisionId, afterRevisionId, hasMaterialChange, priorityScore, changes, comparedAt
if (!root.TryGetProperty("beforeRevisionId", out _))
yield return new() { Path = "/beforeRevisionId", Message = "Required property missing", Keyword = "required" };
if (!root.TryGetProperty("afterRevisionId", out _))
yield return new() { Path = "/afterRevisionId", Message = "Required property missing", Keyword = "required" };
if (!root.TryGetProperty("hasMaterialChange", out _))
yield return new() { Path = "/hasMaterialChange", Message = "Required property missing", Keyword = "required" };
if (!root.TryGetProperty("priorityScore", out _))
yield return new() { Path = "/priorityScore", Message = "Required property missing", Keyword = "required" };
if (!root.TryGetProperty("changes", out _))
yield return new() { Path = "/changes", Message = "Required property missing", Keyword = "required" };
if (!root.TryGetProperty("comparedAt", out _))
yield return new() { Path = "/comparedAt", Message = "Required property missing", Keyword = "required" };
}
private static IEnumerable<SchemaValidationError> ValidateReachabilitySubgraphPredicate(JsonElement root)
{
// Required: graphDigest, analysis
if (!root.TryGetProperty("graphDigest", out _))
yield return new() { Path = "/graphDigest", Message = "Required property missing", Keyword = "required" };
if (!root.TryGetProperty("analysis", out _))
yield return new() { Path = "/analysis", Message = "Required property missing", Keyword = "required" };
}
}

View File

@@ -0,0 +1,55 @@
namespace StellaOps.Attestor.ProofChain.Models;
/// <summary>
/// Aggregated summary of unknowns for inclusion in attestations.
/// Provides verifiable data about unknown risk handled during evaluation.
/// </summary>
public sealed record UnknownsSummary
{
/// <summary>
/// Total count of unknowns encountered.
/// </summary>
public int Total { get; init; }
/// <summary>
/// Count of unknowns by reason code.
/// </summary>
public IReadOnlyDictionary<string, int> ByReasonCode { get; init; }
= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Count of unknowns that would block if not excepted.
/// </summary>
public int BlockingCount { get; init; }
/// <summary>
/// Count of unknowns that are covered by approved exceptions.
/// </summary>
public int ExceptedCount { get; init; }
/// <summary>
/// Policy thresholds that were evaluated.
/// </summary>
public IReadOnlyList<string> PolicyThresholdsApplied { get; init; } = [];
/// <summary>
/// Exception IDs that were applied to cover unknowns.
/// </summary>
public IReadOnlyList<string> ExceptionsApplied { get; init; } = [];
/// <summary>
/// Hash of the unknowns list for integrity verification.
/// </summary>
public string? UnknownsDigest { get; init; }
/// <summary>
/// Creates an empty summary for cases with no unknowns.
/// </summary>
public static UnknownsSummary Empty { get; } = new()
{
Total = 0,
ByReasonCode = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase),
BlockingCount = 0,
ExceptedCount = 0
};
}

View File

@@ -0,0 +1,184 @@
// -----------------------------------------------------------------------------
// DeltaVerdictPredicate.cs
// Sprint: SPRINT_4400_0001_0001_signed_delta_verdict
// Description: DSSE predicate for Smart-Diff delta verdict attestations.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.ProofChain.Predicates;
/// <summary>
/// DSSE predicate for Smart-Diff delta verdict attestation.
/// predicateType: delta-verdict.stella/v1
/// </summary>
public sealed record DeltaVerdictPredicate
{
/// <summary>
/// The predicate type URI for delta verdict attestations.
/// </summary>
public const string PredicateType = "delta-verdict.stella/v1";
/// <summary>
/// Revision identifier for the baseline scan.
/// </summary>
[JsonPropertyName("beforeRevisionId")]
public required string BeforeRevisionId { get; init; }
/// <summary>
/// Revision identifier for the current scan.
/// </summary>
[JsonPropertyName("afterRevisionId")]
public required string AfterRevisionId { get; init; }
/// <summary>
/// Whether any material change was detected.
/// </summary>
[JsonPropertyName("hasMaterialChange")]
public required bool HasMaterialChange { get; init; }
/// <summary>
/// Aggregate priority score for the delta.
/// </summary>
[JsonPropertyName("priorityScore")]
public required double PriorityScore { get; init; }
/// <summary>
/// Change details captured by Smart-Diff rules.
/// </summary>
[JsonPropertyName("changes")]
public ImmutableArray<DeltaVerdictChange> Changes { get; init; } = [];
/// <summary>
/// Digest of the baseline verdict attestation (if available).
/// </summary>
[JsonPropertyName("beforeVerdictDigest")]
public string? BeforeVerdictDigest { get; init; }
/// <summary>
/// Digest of the current verdict attestation (if available).
/// </summary>
[JsonPropertyName("afterVerdictDigest")]
public string? AfterVerdictDigest { get; init; }
/// <summary>
/// Reference to the baseline proof spine (if available).
/// </summary>
[JsonPropertyName("beforeProofSpine")]
public AttestationReference? BeforeProofSpine { get; init; }
/// <summary>
/// Reference to the current proof spine (if available).
/// </summary>
[JsonPropertyName("afterProofSpine")]
public AttestationReference? AfterProofSpine { get; init; }
/// <summary>
/// Graph revision identifier for the baseline analysis (if available).
/// </summary>
[JsonPropertyName("beforeGraphRevisionId")]
public string? BeforeGraphRevisionId { get; init; }
/// <summary>
/// Graph revision identifier for the current analysis (if available).
/// </summary>
[JsonPropertyName("afterGraphRevisionId")]
public string? AfterGraphRevisionId { get; init; }
/// <summary>
/// When the comparison was performed.
/// </summary>
[JsonPropertyName("comparedAt")]
public required DateTimeOffset ComparedAt { get; init; }
}
/// <summary>
/// Individual change captured in delta verdict.
/// </summary>
public sealed record DeltaVerdictChange
{
/// <summary>
/// Detection rule identifier.
/// </summary>
[JsonPropertyName("rule")]
public required string Rule { get; init; }
/// <summary>
/// Finding key (vulnerability and component).
/// </summary>
[JsonPropertyName("findingKey")]
public required DeltaFindingKey FindingKey { get; init; }
/// <summary>
/// Direction of risk change.
/// </summary>
[JsonPropertyName("direction")]
public required string Direction { get; init; }
/// <summary>
/// Change category (optional).
/// </summary>
[JsonPropertyName("changeType")]
public string? ChangeType { get; init; }
/// <summary>
/// Human-readable reason for the change.
/// </summary>
[JsonPropertyName("reason")]
public required string Reason { get; init; }
/// <summary>
/// Previous value observed (optional).
/// </summary>
[JsonPropertyName("previousValue")]
public string? PreviousValue { get; init; }
/// <summary>
/// Current value observed (optional).
/// </summary>
[JsonPropertyName("currentValue")]
public string? CurrentValue { get; init; }
/// <summary>
/// Weight contribution for this change (optional).
/// </summary>
[JsonPropertyName("weight")]
public double? Weight { get; init; }
}
/// <summary>
/// Finding key for delta verdict changes.
/// </summary>
public sealed record DeltaFindingKey
{
/// <summary>
/// Vulnerability identifier (CVE, GHSA, etc.).
/// </summary>
[JsonPropertyName("vulnId")]
public required string VulnId { get; init; }
/// <summary>
/// Component package URL.
/// </summary>
[JsonPropertyName("purl")]
public required string Purl { get; init; }
}
/// <summary>
/// Reference to an attestation or proof spine.
/// </summary>
public sealed record AttestationReference
{
/// <summary>
/// Digest of the attestation (sha256:... or blake3:...).
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// Optional URI where the attestation can be retrieved.
/// </summary>
[JsonPropertyName("uri")]
public string? Uri { get; init; }
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using StellaOps.Attestor.ProofChain.Models;
namespace StellaOps.Attestor.ProofChain.Predicates;
/// <summary>
/// Predicate type for policy decision attestations.
/// Predicate type: https://stella.ops/predicates/policy-decision@v2
/// </summary>
public sealed record PolicyDecisionPredicate
{
/// <summary>
/// The predicate type URI for policy decisions.
/// </summary>
public const string PredicateType = "https://stella.ops/predicates/policy-decision@v2";
/// <summary>
/// Reference to the policy that was evaluated.
/// </summary>
[JsonPropertyName("policyRef")]
public required string PolicyRef { get; init; }
/// <summary>
/// Final policy decision outcome.
/// </summary>
[JsonPropertyName("decision")]
public required PolicyDecision Decision { get; init; }
/// <summary>
/// Timestamp when the policy was evaluated.
/// </summary>
[JsonPropertyName("evaluatedAt")]
public required DateTimeOffset EvaluatedAt { get; init; }
/// <summary>
/// Summary of findings from the evaluation.
/// </summary>
[JsonPropertyName("findings")]
public IReadOnlyList<FindingSummary> Findings { get; init; } = [];
/// <summary>
/// Summary of unknowns and how they were handled.
/// </summary>
[JsonPropertyName("unknowns")]
public UnknownsSummary? Unknowns { get; init; }
/// <summary>
/// Whether unknowns were a factor in the decision.
/// </summary>
[JsonPropertyName("unknownsAffectedDecision")]
public bool UnknownsAffectedDecision { get; init; }
/// <summary>
/// Reason codes that caused blocking (if any).
/// </summary>
[JsonPropertyName("blockingReasonCodes")]
public IReadOnlyList<string> BlockingReasonCodes { get; init; } = [];
/// <summary>
/// Content-addressed ID of the knowledge snapshot used.
/// </summary>
[JsonPropertyName("knowledgeSnapshotId")]
public string? KnowledgeSnapshotId { get; init; }
}
/// <summary>
/// Policy decision outcome.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PolicyDecision
{
/// <summary>
/// Policy evaluation passed.
/// </summary>
Pass,
/// <summary>
/// Policy evaluation failed.
/// </summary>
Fail,
/// <summary>
/// Policy passed with approved exceptions.
/// </summary>
PassWithExceptions,
/// <summary>
/// Policy evaluation could not be completed.
/// </summary>
Indeterminate
}
/// <summary>
/// Summary of a finding from policy evaluation.
/// </summary>
public sealed record FindingSummary
{
/// <summary>
/// The finding identifier.
/// </summary>
[JsonPropertyName("id")]
public required string Id { get; init; }
/// <summary>
/// Severity of the finding.
/// </summary>
[JsonPropertyName("severity")]
public required string Severity { get; init; }
/// <summary>
/// Description of the finding.
/// </summary>
[JsonPropertyName("description")]
public string? Description { get; init; }
}

View File

@@ -0,0 +1,94 @@
// -----------------------------------------------------------------------------
// ReachabilitySubgraphPredicate.cs
// Sprint: SPRINT_4400_0001_0002_reachability_subgraph_attestation
// Description: DSSE predicate for reachability subgraph attestations.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Attestor.ProofChain.Predicates;
/// <summary>
/// DSSE predicate for reachability subgraph attestation.
/// predicateType: reachability-subgraph.stella/v1
/// </summary>
public sealed record ReachabilitySubgraphPredicate
{
/// <summary>
/// The predicate type URI for reachability subgraph attestations.
/// </summary>
public const string PredicateType = "reachability-subgraph.stella/v1";
/// <summary>
/// Schema version for the predicate payload.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "1.0.0";
/// <summary>
/// Content-addressed digest of the serialized subgraph.
/// </summary>
[JsonPropertyName("graphDigest")]
public required string GraphDigest { get; init; }
/// <summary>
/// Optional CAS URI for the subgraph content.
/// </summary>
[JsonPropertyName("graphCasUri")]
public string? GraphCasUri { get; init; }
/// <summary>
/// Finding keys covered by this subgraph (e.g., "CVE-2024-1234@pkg:...").
/// </summary>
[JsonPropertyName("findingKeys")]
public ImmutableArray<string> FindingKeys { get; init; } = [];
/// <summary>
/// Analysis metadata for the subgraph extraction.
/// </summary>
[JsonPropertyName("analysis")]
public required ReachabilitySubgraphAnalysis Analysis { get; init; }
}
/// <summary>
/// Metadata about subgraph extraction and analysis.
/// </summary>
public sealed record ReachabilitySubgraphAnalysis
{
/// <summary>
/// Analyzer name.
/// </summary>
[JsonPropertyName("analyzer")]
public required string Analyzer { get; init; }
/// <summary>
/// Analyzer version.
/// </summary>
[JsonPropertyName("analyzerVersion")]
public required string AnalyzerVersion { get; init; }
/// <summary>
/// Confidence score (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public required double Confidence { get; init; }
/// <summary>
/// Completeness indicator (full, partial, unknown).
/// </summary>
[JsonPropertyName("completeness")]
public required string Completeness { get; init; }
/// <summary>
/// When the subgraph was generated.
/// </summary>
[JsonPropertyName("generatedAt")]
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>
/// Hash algorithm used for graph digest.
/// </summary>
[JsonPropertyName("hashAlgorithm")]
public string HashAlgorithm { get; init; } = "blake3";
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Attestor.ProofChain.Models;
namespace StellaOps.Attestor.ProofChain.Services;
/// <summary>
/// Aggregates unknowns data into summary format for attestations.
/// </summary>
public sealed class UnknownsAggregator : IUnknownsAggregator
{
/// <summary>
/// Creates an unknowns summary from evaluation results.
/// </summary>
public UnknownsSummary Aggregate(
IReadOnlyList<UnknownItem> unknowns,
BudgetCheckResult? budgetResult = null,
IReadOnlyList<ExceptionRef>? exceptions = null)
{
if (unknowns.Count == 0)
return UnknownsSummary.Empty;
// Count by reason code
var byReasonCode = unknowns
.GroupBy(u => u.ReasonCode)
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase);
// Calculate blocking count (would block without exceptions)
var blockingCount = budgetResult?.Violations.Values.Sum(v => v.Count) ?? 0;
// Calculate excepted count
var exceptedCount = exceptions?.Count ?? 0;
// Compute digest of unknowns list for integrity
var unknownsDigest = ComputeUnknownsDigest(unknowns);
// Extract policy thresholds that were checked
var thresholds = budgetResult?.Violations.Keys
.Select(k => $"{k}:{budgetResult.Violations[k].Limit}")
.ToList() ?? new List<string>();
// Extract applied exception IDs
var exceptionIds = exceptions?
.Select(e => e.ExceptionId)
.ToList() ?? new List<string>();
return new UnknownsSummary
{
Total = unknowns.Count,
ByReasonCode = byReasonCode,
BlockingCount = blockingCount,
ExceptedCount = exceptedCount,
PolicyThresholdsApplied = thresholds,
ExceptionsApplied = exceptionIds,
UnknownsDigest = unknownsDigest
};
}
/// <summary>
/// Computes a deterministic digest of the unknowns list.
/// </summary>
private static string ComputeUnknownsDigest(IReadOnlyList<UnknownItem> unknowns)
{
// Sort for determinism
var sorted = unknowns
.OrderBy(u => u.PackageUrl)
.ThenBy(u => u.CveId)
.ThenBy(u => u.ReasonCode)
.ToList();
// Serialize to canonical JSON
var json = JsonSerializer.Serialize(sorted, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
});
// Hash the serialized data using SHA256
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(json));
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
}
/// <summary>
/// Interface for unknowns aggregation service.
/// </summary>
public interface IUnknownsAggregator
{
/// <summary>
/// Aggregates unknowns into a summary.
/// </summary>
UnknownsSummary Aggregate(
IReadOnlyList<UnknownItem> unknowns,
BudgetCheckResult? budgetResult = null,
IReadOnlyList<ExceptionRef>? exceptions = null);
}
/// <summary>
/// Input item for unknowns aggregation.
/// </summary>
public sealed record UnknownItem(
string PackageUrl,
string? CveId,
string ReasonCode,
string? RemediationHint);
/// <summary>
/// Reference to an applied exception.
/// </summary>
public sealed record ExceptionRef(
string ExceptionId,
string Status,
IReadOnlyList<string> CoveredReasonCodes);
/// <summary>
/// Result of a budget check operation.
/// </summary>
public sealed record BudgetCheckResult
{
/// <summary>
/// Budget violations by reason code.
/// </summary>
public required IReadOnlyDictionary<string, BudgetViolation> Violations { get; init; }
}
/// <summary>
/// Represents a budget violation for a specific reason code.
/// </summary>
public sealed record BudgetViolation(
int Count,
int Limit);

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
using StellaOps.Attestor.ProofChain.Predicates;
namespace StellaOps.Attestor.ProofChain.Statements;
/// <summary>
/// In-toto statement for Smart-Diff delta verdicts.
/// Predicate type: delta-verdict.stella/v1
/// </summary>
public sealed record DeltaVerdictStatement : InTotoStatement
{
/// <inheritdoc />
[JsonPropertyName("predicateType")]
public override string PredicateType => DeltaVerdictPredicate.PredicateType;
/// <summary>
/// The delta verdict predicate payload.
/// </summary>
[JsonPropertyName("predicate")]
public required DeltaVerdictPredicate Predicate { get; init; }
}

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
using StellaOps.Attestor.ProofChain.Predicates;
namespace StellaOps.Attestor.ProofChain.Statements;
/// <summary>
/// In-toto statement for reachability subgraph attestations.
/// Predicate type: reachability-subgraph.stella/v1
/// </summary>
public sealed record ReachabilitySubgraphStatement : InTotoStatement
{
/// <inheritdoc />
[JsonPropertyName("predicateType")]
public override string PredicateType => ReachabilitySubgraphPredicate.PredicateType;
/// <summary>
/// The reachability subgraph predicate payload.
/// </summary>
[JsonPropertyName("predicate")]
public required ReachabilitySubgraphPredicate Predicate { get; init; }
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Text.Json.Serialization;
using StellaOps.Attestor.ProofChain.Models;
namespace StellaOps.Attestor.ProofChain.Statements;
@@ -66,6 +67,20 @@ public sealed record VerdictReceiptPayload
/// </summary>
[JsonPropertyName("createdAt")]
public required DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Summary of unknowns encountered during evaluation.
/// Included for transparency about uncertainty in the verdict.
/// </summary>
[JsonPropertyName("unknowns")]
public UnknownsSummary? Unknowns { get; init; }
/// <summary>
/// Reference to the knowledge snapshot used for evaluation.
/// Enables replay and verification of inputs.
/// </summary>
[JsonPropertyName("knowledgeSnapshotId")]
public string? KnowledgeSnapshotId { get; init; }
}
/// <summary>

View File

@@ -0,0 +1,32 @@
using FluentAssertions;
using StellaOps.Attestor.ProofChain.Models;
using Xunit;
namespace StellaOps.Attestor.ProofChain.Tests.Models;
public class UnknownsSummaryTests
{
[Fact]
public void Empty_ReturnsZeroCounts()
{
var summary = UnknownsSummary.Empty;
summary.Total.Should().Be(0);
summary.ByReasonCode.Should().BeEmpty();
summary.BlockingCount.Should().Be(0);
summary.ExceptedCount.Should().Be(0);
summary.PolicyThresholdsApplied.Should().BeEmpty();
summary.ExceptionsApplied.Should().BeEmpty();
}
[Fact]
public void Empty_ProducesValidSummary()
{
var summary = UnknownsSummary.Empty;
summary.Should().NotBeNull();
summary.ByReasonCode.Should().NotBeNull();
summary.PolicyThresholdsApplied.Should().NotBeNull();
summary.ExceptionsApplied.Should().NotBeNull();
}
}

View File

@@ -0,0 +1,101 @@
using FluentAssertions;
using StellaOps.Attestor.ProofChain.Services;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace StellaOps.Attestor.ProofChain.Tests.Services;
public class UnknownsAggregatorTests
{
private readonly IUnknownsAggregator _aggregator;
public UnknownsAggregatorTests()
{
_aggregator = new UnknownsAggregator();
}
[Fact]
public void Aggregate_EmptyList_ReturnsEmptySummary()
{
var unknowns = new List<UnknownItem>();
var summary = _aggregator.Aggregate(unknowns);
summary.Total.Should().Be(0);
summary.ByReasonCode.Should().BeEmpty();
}
[Fact]
public void Aggregate_GroupsByReasonCode()
{
var unknowns = new List<UnknownItem>
{
new("pkg:npm/foo@1.0", null, "Reachability", null),
new("pkg:npm/bar@1.0", null, "Reachability", null),
new("pkg:npm/baz@1.0", null, "Identity", null)
};
var summary = _aggregator.Aggregate(unknowns);
summary.Total.Should().Be(3);
summary.ByReasonCode["Reachability"].Should().Be(2);
summary.ByReasonCode["Identity"].Should().Be(1);
}
[Fact]
public void Aggregate_ComputesDeterministicDigest()
{
var unknowns = CreateUnknowns();
var summary1 = _aggregator.Aggregate(unknowns);
var summary2 = _aggregator.Aggregate(unknowns.Reverse().ToList());
summary1.UnknownsDigest.Should().Be(summary2.UnknownsDigest);
summary1.UnknownsDigest.Should().NotBeNullOrEmpty();
}
[Fact]
public void Aggregate_IncludesExceptionIds()
{
var unknowns = CreateUnknowns();
var exceptions = new List<ExceptionRef>
{
new("EXC-001", "Approved", new[] { "Reachability" })
};
var summary = _aggregator.Aggregate(unknowns, null, exceptions);
summary.ExceptionsApplied.Should().Contain("EXC-001");
summary.ExceptedCount.Should().Be(1);
}
[Fact]
public void Aggregate_IncludesBudgetViolations()
{
var unknowns = CreateUnknowns();
var budgetResult = new BudgetCheckResult
{
Violations = new Dictionary<string, BudgetViolation>
{
["Reachability"] = new BudgetViolation(5, 3),
["Identity"] = new BudgetViolation(2, 1)
}
};
var summary = _aggregator.Aggregate(unknowns, budgetResult);
summary.BlockingCount.Should().Be(7); // 5 + 2
summary.PolicyThresholdsApplied.Should().HaveCount(2);
}
private static IReadOnlyList<UnknownItem> CreateUnknowns()
{
return new List<UnknownItem>
{
new("pkg:npm/foo@1.0", "CVE-2024-001", "Reachability", "Run reachability analysis"),
new("pkg:npm/bar@2.0", "CVE-2024-002", "Identity", "Add package digest"),
new("pkg:npm/baz@3.0", null, "VexConflict", "Review VEX statements")
};
}
}

View File

@@ -0,0 +1,99 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) StellaOps Contributors
using System.Text.Json;
using StellaOps.Attestor.ProofChain.Predicates;
using StellaOps.Attestor.ProofChain.Statements;
namespace StellaOps.Attestor.ProofChain.Tests.Statements;
public sealed class DeltaVerdictStatementTests
{
private static readonly DateTimeOffset FixedTime = new(2025, 12, 22, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void DeltaVerdictStatement_HasPredicateTypeAndPayload()
{
var statement = new DeltaVerdictStatement
{
Subject =
[
new Subject
{
Name = "sha256:before",
Digest = new Dictionary<string, string> { ["sha256"] = "before" }
},
new Subject
{
Name = "sha256:after",
Digest = new Dictionary<string, string> { ["sha256"] = "after" }
}
],
Predicate = new DeltaVerdictPredicate
{
BeforeRevisionId = "rev-before",
AfterRevisionId = "rev-after",
HasMaterialChange = true,
PriorityScore = 1750,
Changes =
[
new DeltaVerdictChange
{
Rule = "R1_ReachabilityFlip",
FindingKey = new DeltaFindingKey
{
VulnId = "CVE-2025-1234",
Purl = "pkg:npm/lodash@4.17.20"
},
Direction = "increased",
Reason = "Reachability changed from false to true"
}
],
ComparedAt = FixedTime
}
};
Assert.Equal("delta-verdict.stella/v1", statement.PredicateType);
Assert.Equal(2, statement.Subject.Count);
Assert.Equal("rev-before", statement.Predicate.BeforeRevisionId);
Assert.True(statement.Predicate.HasMaterialChange);
Assert.Single(statement.Predicate.Changes);
}
[Fact]
public void ReachabilitySubgraphStatement_RoundTrips()
{
var statement = new ReachabilitySubgraphStatement
{
Subject =
[
new Subject
{
Name = "sha256:graph",
Digest = new Dictionary<string, string> { ["sha256"] = "graph" }
}
],
Predicate = new ReachabilitySubgraphPredicate
{
GraphDigest = "blake3:deadbeef",
FindingKeys = ["CVE-2025-9999@pkg:npm/example@1.0.0"],
Analysis = new ReachabilitySubgraphAnalysis
{
Analyzer = "reachability",
AnalyzerVersion = "1.0.0",
Confidence = 0.9,
Completeness = "partial",
GeneratedAt = FixedTime
}
}
};
var json = JsonSerializer.Serialize(statement);
var restored = JsonSerializer.Deserialize<ReachabilitySubgraphStatement>(json);
Assert.NotNull(restored);
Assert.Equal("reachability-subgraph.stella/v1", restored!.PredicateType);
Assert.Equal("blake3:deadbeef", restored.Predicate.GraphDigest);
Assert.Single(restored.Predicate.FindingKeys);
}
}