feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
- Added ConsoleExportClient for managing export requests and responses. - Introduced ConsoleExportRequest and ConsoleExportResponse models. - Implemented methods for creating and retrieving exports with appropriate headers. feat(crypto): Add Software SM2/SM3 Cryptography Provider - Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography. - Added support for signing and verification using SM2 algorithm. - Included hashing functionality with SM3 algorithm. - Configured options for loading keys from files and environment gate checks. test(crypto): Add unit tests for SmSoftCryptoProvider - Created comprehensive tests for signing, verifying, and hashing functionalities. - Ensured correct behavior for key management and error handling. feat(api): Enhance Console Export Models - Expanded ConsoleExport models to include detailed status and event types. - Added support for various export formats and notification options. test(time): Implement TimeAnchorPolicyService tests - Developed tests for TimeAnchorPolicyService to validate time anchors. - Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Policy.Registry.Contracts;
|
||||
using StellaOps.Policy.Registry.Storage;
|
||||
|
||||
namespace StellaOps.Policy.Registry.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of publish pipeline service.
|
||||
/// Handles policy pack publication with attestation generation.
|
||||
/// </summary>
|
||||
public sealed class PublishPipelineService : IPublishPipelineService
|
||||
{
|
||||
private const string BuilderId = "https://stellaops.io/policy-registry/v1";
|
||||
private const string BuildType = "https://stellaops.io/policy-registry/v1/publish";
|
||||
private const string AttestationPredicateType = "https://slsa.dev/provenance/v1";
|
||||
|
||||
private readonly IPolicyPackStore _packStore;
|
||||
private readonly IPolicyPackCompiler _compiler;
|
||||
private readonly IReviewWorkflowService _reviewService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PublicationStatus> _publications = new();
|
||||
private readonly ConcurrentDictionary<(Guid TenantId, Guid PackId), PolicyPackAttestation> _attestations = new();
|
||||
|
||||
public PublishPipelineService(
|
||||
IPolicyPackStore packStore,
|
||||
IPolicyPackCompiler compiler,
|
||||
IReviewWorkflowService reviewService,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_packStore = packStore ?? throw new ArgumentNullException(nameof(packStore));
|
||||
_compiler = compiler ?? throw new ArgumentNullException(nameof(compiler));
|
||||
_reviewService = reviewService ?? throw new ArgumentNullException(nameof(reviewService));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<PublishResult> PublishAsync(
|
||||
Guid tenantId,
|
||||
Guid packId,
|
||||
PublishPackRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Get the policy pack
|
||||
var pack = await _packStore.GetByIdAsync(tenantId, packId, cancellationToken);
|
||||
if (pack is null)
|
||||
{
|
||||
return new PublishResult
|
||||
{
|
||||
Success = false,
|
||||
Error = $"Policy pack {packId} not found"
|
||||
};
|
||||
}
|
||||
|
||||
// Verify pack is in correct state
|
||||
if (pack.Status != PolicyPackStatus.PendingReview)
|
||||
{
|
||||
return new PublishResult
|
||||
{
|
||||
Success = false,
|
||||
Error = $"Policy pack must be in PendingReview status to publish. Current status: {pack.Status}"
|
||||
};
|
||||
}
|
||||
|
||||
// Compile to get digest
|
||||
var compilationResult = await _compiler.CompileAsync(tenantId, packId, cancellationToken);
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
return new PublishResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Policy pack compilation failed. Cannot publish."
|
||||
};
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var digest = compilationResult.Digest!;
|
||||
|
||||
// Get review information if available
|
||||
var reviews = await _reviewService.ListReviewsAsync(tenantId, ReviewStatus.Approved, packId, 1, null, cancellationToken);
|
||||
var review = reviews.Items.FirstOrDefault();
|
||||
|
||||
// Build attestation
|
||||
var attestation = BuildAttestation(
|
||||
pack,
|
||||
digest,
|
||||
compilationResult,
|
||||
review,
|
||||
request,
|
||||
now);
|
||||
|
||||
// Update pack status to Published
|
||||
var updatedPack = await _packStore.UpdateStatusAsync(tenantId, packId, PolicyPackStatus.Published, request.PublishedBy, cancellationToken);
|
||||
if (updatedPack is null)
|
||||
{
|
||||
return new PublishResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Failed to update policy pack status"
|
||||
};
|
||||
}
|
||||
|
||||
// Create publication status
|
||||
var status = new PublicationStatus
|
||||
{
|
||||
PackId = packId,
|
||||
PackVersion = pack.Version,
|
||||
Digest = digest,
|
||||
State = PublishState.Published,
|
||||
PublishedAt = now,
|
||||
PublishedBy = request.PublishedBy,
|
||||
SignatureKeyId = request.SigningOptions?.KeyId,
|
||||
SignatureAlgorithm = request.SigningOptions?.Algorithm
|
||||
};
|
||||
|
||||
_publications[(tenantId, packId)] = status;
|
||||
_attestations[(tenantId, packId)] = attestation;
|
||||
|
||||
return new PublishResult
|
||||
{
|
||||
Success = true,
|
||||
PackId = packId,
|
||||
Digest = digest,
|
||||
Status = status,
|
||||
Attestation = attestation
|
||||
};
|
||||
}
|
||||
|
||||
public Task<PublicationStatus?> GetPublicationStatusAsync(
|
||||
Guid tenantId,
|
||||
Guid packId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_publications.TryGetValue((tenantId, packId), out var status);
|
||||
return Task.FromResult(status);
|
||||
}
|
||||
|
||||
public Task<PolicyPackAttestation?> GetAttestationAsync(
|
||||
Guid tenantId,
|
||||
Guid packId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_attestations.TryGetValue((tenantId, packId), out var attestation);
|
||||
return Task.FromResult(attestation);
|
||||
}
|
||||
|
||||
public async Task<AttestationVerificationResult> VerifyAttestationAsync(
|
||||
Guid tenantId,
|
||||
Guid packId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var checks = new List<VerificationCheck>();
|
||||
var errors = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
// Check publication exists
|
||||
if (!_publications.TryGetValue((tenantId, packId), out var status))
|
||||
{
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Valid = false,
|
||||
Errors = ["Policy pack is not published"]
|
||||
};
|
||||
}
|
||||
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "publication_exists",
|
||||
Passed = true,
|
||||
Details = $"Published at {status.PublishedAt:O}"
|
||||
});
|
||||
|
||||
// Check not revoked
|
||||
if (status.State == PublishState.Revoked)
|
||||
{
|
||||
errors.Add($"Policy pack was revoked at {status.RevokedAt:O}: {status.RevokeReason}");
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "not_revoked",
|
||||
Passed = false,
|
||||
Details = status.RevokeReason
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "not_revoked",
|
||||
Passed = true,
|
||||
Details = "Policy pack has not been revoked"
|
||||
});
|
||||
}
|
||||
|
||||
// Check attestation exists
|
||||
if (!_attestations.TryGetValue((tenantId, packId), out var attestation))
|
||||
{
|
||||
errors.Add("Attestation not found");
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "attestation_exists",
|
||||
Passed = false,
|
||||
Details = "No attestation on record"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "attestation_exists",
|
||||
Passed = true,
|
||||
Details = $"Found {attestation.Signatures.Count} signature(s)"
|
||||
});
|
||||
|
||||
// Verify signatures
|
||||
foreach (var sig in attestation.Signatures)
|
||||
{
|
||||
// In a real implementation, this would verify the actual cryptographic signature
|
||||
var sigValid = !string.IsNullOrEmpty(sig.Signature);
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = $"signature_{sig.KeyId}",
|
||||
Passed = sigValid,
|
||||
Details = sigValid ? $"Signature verified for key {sig.KeyId}" : "Invalid signature"
|
||||
});
|
||||
|
||||
if (!sigValid)
|
||||
{
|
||||
errors.Add($"Invalid signature for key {sig.KeyId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify pack still exists and matches digest
|
||||
var pack = await _packStore.GetByIdAsync(tenantId, packId, cancellationToken);
|
||||
if (pack is null)
|
||||
{
|
||||
errors.Add("Policy pack no longer exists");
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "pack_exists",
|
||||
Passed = false,
|
||||
Details = "Policy pack has been deleted"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "pack_exists",
|
||||
Passed = true,
|
||||
Details = $"Pack version: {pack.Version}"
|
||||
});
|
||||
|
||||
// Verify digest matches
|
||||
var digestMatch = pack.Digest == status.Digest;
|
||||
checks.Add(new VerificationCheck
|
||||
{
|
||||
Name = "digest_match",
|
||||
Passed = digestMatch,
|
||||
Details = digestMatch ? "Digest matches" : $"Digest mismatch: expected {status.Digest}, got {pack.Digest}"
|
||||
});
|
||||
|
||||
if (!digestMatch)
|
||||
{
|
||||
errors.Add("Policy pack has been modified since publication");
|
||||
}
|
||||
}
|
||||
|
||||
return new AttestationVerificationResult
|
||||
{
|
||||
Valid = errors.Count == 0,
|
||||
Checks = checks,
|
||||
Errors = errors.Count > 0 ? errors : null,
|
||||
Warnings = warnings.Count > 0 ? warnings : null
|
||||
};
|
||||
}
|
||||
|
||||
public Task<PublishedPackList> ListPublishedAsync(
|
||||
Guid tenantId,
|
||||
int pageSize = 20,
|
||||
string? pageToken = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = _publications
|
||||
.Where(kv => kv.Key.TenantId == tenantId)
|
||||
.Select(kv => kv.Value)
|
||||
.OrderByDescending(p => p.PublishedAt)
|
||||
.ToList();
|
||||
|
||||
int skip = 0;
|
||||
if (!string.IsNullOrEmpty(pageToken) && int.TryParse(pageToken, out var offset))
|
||||
{
|
||||
skip = offset;
|
||||
}
|
||||
|
||||
var pagedItems = items.Skip(skip).Take(pageSize).ToList();
|
||||
string? nextToken = skip + pagedItems.Count < items.Count
|
||||
? (skip + pagedItems.Count).ToString()
|
||||
: null;
|
||||
|
||||
return Task.FromResult(new PublishedPackList
|
||||
{
|
||||
Items = pagedItems,
|
||||
NextPageToken = nextToken,
|
||||
TotalCount = items.Count
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<RevokeResult> RevokeAsync(
|
||||
Guid tenantId,
|
||||
Guid packId,
|
||||
RevokePackRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_publications.TryGetValue((tenantId, packId), out var status))
|
||||
{
|
||||
return new RevokeResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Policy pack is not published"
|
||||
};
|
||||
}
|
||||
|
||||
if (status.State == PublishState.Revoked)
|
||||
{
|
||||
return new RevokeResult
|
||||
{
|
||||
Success = false,
|
||||
Error = "Policy pack is already revoked"
|
||||
};
|
||||
}
|
||||
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var updatedStatus = status with
|
||||
{
|
||||
State = PublishState.Revoked,
|
||||
RevokedAt = now,
|
||||
RevokedBy = request.RevokedBy,
|
||||
RevokeReason = request.Reason
|
||||
};
|
||||
|
||||
_publications[(tenantId, packId)] = updatedStatus;
|
||||
|
||||
// Update pack status to archived
|
||||
await _packStore.UpdateStatusAsync(tenantId, packId, PolicyPackStatus.Archived, request.RevokedBy, cancellationToken);
|
||||
|
||||
return new RevokeResult
|
||||
{
|
||||
Success = true,
|
||||
Status = updatedStatus
|
||||
};
|
||||
}
|
||||
|
||||
private PolicyPackAttestation BuildAttestation(
|
||||
PolicyPackEntity pack,
|
||||
string digest,
|
||||
PolicyPackCompilationResult compilationResult,
|
||||
ReviewRequest? review,
|
||||
PublishPackRequest request,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
var subject = new AttestationSubject
|
||||
{
|
||||
Name = $"policy-pack/{pack.Name}",
|
||||
Digest = new Dictionary<string, string>
|
||||
{
|
||||
["sha256"] = digest.Replace("sha256:", "")
|
||||
}
|
||||
};
|
||||
|
||||
var predicate = new AttestationPredicate
|
||||
{
|
||||
BuildType = BuildType,
|
||||
Builder = new AttestationBuilder
|
||||
{
|
||||
Id = BuilderId,
|
||||
Version = "1.0.0"
|
||||
},
|
||||
BuildStartedOn = pack.CreatedAt,
|
||||
BuildFinishedOn = now,
|
||||
Compilation = new PolicyPackCompilationMetadata
|
||||
{
|
||||
Digest = digest,
|
||||
RuleCount = compilationResult.Statistics?.TotalRules ?? 0,
|
||||
CompiledAt = now,
|
||||
Statistics = compilationResult.Statistics?.SeverityCounts
|
||||
},
|
||||
Review = review is not null ? new PolicyPackReviewMetadata
|
||||
{
|
||||
ReviewId = review.ReviewId,
|
||||
ApprovedAt = review.ResolvedAt ?? now,
|
||||
ApprovedBy = review.ResolvedBy,
|
||||
Reviewers = review.Reviewers
|
||||
} : null,
|
||||
Metadata = request.Metadata?.ToDictionary(kv => kv.Key, kv => (object)kv.Value)
|
||||
};
|
||||
|
||||
var payload = new AttestationPayload
|
||||
{
|
||||
Type = "https://in-toto.io/Statement/v1",
|
||||
PredicateType = request.AttestationOptions?.PredicateType ?? AttestationPredicateType,
|
||||
Subject = subject,
|
||||
Predicate = predicate
|
||||
};
|
||||
|
||||
var payloadJson = JsonSerializer.Serialize(payload, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false
|
||||
});
|
||||
|
||||
var payloadBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson));
|
||||
|
||||
// Generate signature (simulated - in production would use actual signing)
|
||||
var signature = GenerateSignature(payloadBase64, request.SigningOptions);
|
||||
|
||||
return new PolicyPackAttestation
|
||||
{
|
||||
PayloadType = "application/vnd.in-toto+json",
|
||||
Payload = payloadBase64,
|
||||
Signatures =
|
||||
[
|
||||
new AttestationSignature
|
||||
{
|
||||
KeyId = request.SigningOptions?.KeyId ?? "default",
|
||||
Signature = signature,
|
||||
Timestamp = request.SigningOptions?.IncludeTimestamp == true ? now : null
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private static string GenerateSignature(string payload, SigningOptions? options)
|
||||
{
|
||||
// In production, this would use actual cryptographic signing
|
||||
// For now, we generate a deterministic mock signature
|
||||
var content = $"{payload}:{options?.KeyId ?? "default"}:{options?.Algorithm ?? SigningAlgorithm.ECDSA_P256_SHA256}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user