Add unit tests for PhpFrameworkSurface and PhpPharScanner
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled

- Implement comprehensive tests for PhpFrameworkSurface, covering scenarios such as empty surfaces, presence of routes, controllers, middlewares, CLI commands, cron jobs, and event listeners.
- Validate metadata creation for route counts, HTTP methods, protected and public routes, and route patterns.
- Introduce tests for PhpPharScanner, including handling of non-existent files, null or empty paths, invalid PHAR files, and minimal PHAR structures.
- Ensure correct computation of SHA256 for valid PHAR files and validate the properties of PhpPharArchive, PhpPharEntry, and PhpPharScanResult.
This commit is contained in:
StellaOps Bot
2025-12-07 13:44:13 +02:00
parent af30fc322f
commit 965cbf9574
49 changed files with 11935 additions and 152 deletions

View File

@@ -0,0 +1,460 @@
namespace StellaOps.Findings.Ledger.WebService.Contracts;
using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure.Snapshot;
// === Snapshot Contracts ===
/// <summary>
/// Request to create a snapshot.
/// </summary>
public sealed record CreateSnapshotRequest(
string? Label = null,
string? Description = null,
DateTimeOffset? AtTimestamp = null,
long? AtSequence = null,
int? ExpiresInHours = null,
IReadOnlyList<string>? IncludeEntityTypes = null,
bool Sign = false,
Dictionary<string, object>? Metadata = null)
{
public CreateSnapshotInput ToInput(string tenantId) => new(
TenantId: tenantId,
Label: Label,
Description: Description,
AtTimestamp: AtTimestamp,
AtSequence: AtSequence,
ExpiresIn: ExpiresInHours.HasValue ? TimeSpan.FromHours(ExpiresInHours.Value) : null,
IncludeEntityTypes: IncludeEntityTypes?.Select(ParseEntityType).ToList(),
Sign: Sign,
Metadata: Metadata);
private static EntityType ParseEntityType(string s) =>
Enum.TryParse<EntityType>(s, true, out var et) ? et : EntityType.Finding;
}
/// <summary>
/// Response for a snapshot.
/// </summary>
public sealed record SnapshotResponse(
Guid SnapshotId,
string? Label,
string? Description,
string Status,
DateTimeOffset CreatedAt,
DateTimeOffset? ExpiresAt,
long SequenceNumber,
DateTimeOffset Timestamp,
SnapshotStatisticsResponse Statistics,
string? MerkleRoot,
string? DsseDigest,
Dictionary<string, object>? Metadata);
/// <summary>
/// Response for snapshot statistics.
/// </summary>
public sealed record SnapshotStatisticsResponse(
long FindingsCount,
long VexStatementsCount,
long AdvisoriesCount,
long SbomsCount,
long EventsCount,
long SizeBytes);
/// <summary>
/// Result of creating a snapshot.
/// </summary>
public sealed record CreateSnapshotResponse(
bool Success,
SnapshotResponse? Snapshot,
string? Error);
/// <summary>
/// Response for listing snapshots.
/// </summary>
public sealed record SnapshotListResponse(
IReadOnlyList<SnapshotResponse> Snapshots,
string? NextPageToken);
// === Time-Travel Contracts ===
/// <summary>
/// Request for historical query.
/// </summary>
public sealed record HistoricalQueryApiRequest(
DateTimeOffset? AtTimestamp = null,
long? AtSequence = null,
Guid? SnapshotId = null,
string? Status = null,
decimal? SeverityMin = null,
decimal? SeverityMax = null,
string? PolicyVersion = null,
string? ArtifactId = null,
string? VulnId = null,
int PageSize = 500,
string? PageToken = null)
{
public HistoricalQueryRequest ToRequest(string tenantId, EntityType entityType) => new(
TenantId: tenantId,
AtTimestamp: AtTimestamp,
AtSequence: AtSequence,
SnapshotId: SnapshotId,
EntityType: entityType,
Filters: new TimeQueryFilters(
Status: Status,
SeverityMin: SeverityMin,
SeverityMax: SeverityMax,
PolicyVersion: PolicyVersion,
ArtifactId: ArtifactId,
VulnId: VulnId),
PageSize: PageSize,
PageToken: PageToken);
}
/// <summary>
/// Response for historical query.
/// </summary>
public sealed record HistoricalQueryApiResponse<T>(
QueryPointResponse QueryPoint,
string EntityType,
IReadOnlyList<T> Items,
string? NextPageToken,
long TotalCount);
/// <summary>
/// Query point response.
/// </summary>
public sealed record QueryPointResponse(
DateTimeOffset Timestamp,
long SequenceNumber,
Guid? SnapshotId);
/// <summary>
/// Finding history item response.
/// </summary>
public sealed record FindingHistoryResponse(
string FindingId,
string ArtifactId,
string VulnId,
string Status,
decimal? Severity,
string? PolicyVersion,
DateTimeOffset FirstSeen,
DateTimeOffset LastUpdated,
Dictionary<string, string>? Labels);
/// <summary>
/// VEX history item response.
/// </summary>
public sealed record VexHistoryResponse(
string StatementId,
string VulnId,
string ProductId,
string Status,
string? Justification,
DateTimeOffset IssuedAt,
DateTimeOffset? ExpiresAt);
/// <summary>
/// Advisory history item response.
/// </summary>
public sealed record AdvisoryHistoryResponse(
string AdvisoryId,
string Source,
string Title,
decimal? CvssScore,
DateTimeOffset PublishedAt,
DateTimeOffset? ModifiedAt);
// === Replay Contracts ===
/// <summary>
/// Request for replaying events.
/// </summary>
public sealed record ReplayApiRequest(
long? FromSequence = null,
long? ToSequence = null,
DateTimeOffset? FromTimestamp = null,
DateTimeOffset? ToTimestamp = null,
IReadOnlyList<Guid>? ChainIds = null,
IReadOnlyList<string>? EventTypes = null,
bool IncludePayload = true,
int PageSize = 1000)
{
public ReplayRequest ToRequest(string tenantId) => new(
TenantId: tenantId,
FromSequence: FromSequence,
ToSequence: ToSequence,
FromTimestamp: FromTimestamp,
ToTimestamp: ToTimestamp,
ChainIds: ChainIds,
EventTypes: EventTypes,
IncludePayload: IncludePayload,
PageSize: PageSize);
}
/// <summary>
/// Response for replay.
/// </summary>
public sealed record ReplayApiResponse(
IReadOnlyList<ReplayEventResponse> Events,
ReplayMetadataResponse Metadata);
/// <summary>
/// Replay event response.
/// </summary>
public sealed record ReplayEventResponse(
Guid EventId,
long SequenceNumber,
Guid ChainId,
int ChainSequence,
string EventType,
DateTimeOffset OccurredAt,
DateTimeOffset RecordedAt,
string? ActorId,
string? ActorType,
string? ArtifactId,
string? FindingId,
string? PolicyVersion,
string EventHash,
string PreviousHash,
object? Payload);
/// <summary>
/// Replay metadata response.
/// </summary>
public sealed record ReplayMetadataResponse(
long FromSequence,
long ToSequence,
long EventsCount,
bool HasMore,
long ReplayDurationMs);
// === Diff Contracts ===
/// <summary>
/// Request for computing diff.
/// </summary>
public sealed record DiffApiRequest(
DiffPointRequest From,
DiffPointRequest To,
IReadOnlyList<string>? EntityTypes = null,
bool IncludeUnchanged = false,
string OutputFormat = "Summary")
{
public DiffRequest ToRequest(string tenantId) => new(
TenantId: tenantId,
From: From.ToDiffPoint(),
To: To.ToDiffPoint(),
EntityTypes: EntityTypes?.Select(ParseEntityType).ToList(),
IncludeUnchanged: IncludeUnchanged,
OutputFormat: Enum.TryParse<DiffOutputFormat>(OutputFormat, true, out var fmt)
? fmt : DiffOutputFormat.Summary);
private static EntityType ParseEntityType(string s) =>
Enum.TryParse<EntityType>(s, true, out var et) ? et : EntityType.Finding;
}
/// <summary>
/// Diff point request.
/// </summary>
public sealed record DiffPointRequest(
DateTimeOffset? Timestamp = null,
long? SequenceNumber = null,
Guid? SnapshotId = null)
{
public DiffPoint ToDiffPoint() => new(Timestamp, SequenceNumber, SnapshotId);
}
/// <summary>
/// Response for diff.
/// </summary>
public sealed record DiffApiResponse(
QueryPointResponse FromPoint,
QueryPointResponse ToPoint,
DiffSummaryResponse Summary,
IReadOnlyList<DiffEntryResponse>? Changes,
string? NextPageToken);
/// <summary>
/// Diff summary response.
/// </summary>
public sealed record DiffSummaryResponse(
int Added,
int Modified,
int Removed,
int Unchanged,
Dictionary<string, DiffCountsResponse>? ByEntityType);
/// <summary>
/// Diff counts response.
/// </summary>
public sealed record DiffCountsResponse(int Added, int Modified, int Removed);
/// <summary>
/// Diff entry response.
/// </summary>
public sealed record DiffEntryResponse(
string EntityType,
string EntityId,
string ChangeType,
object? FromState,
object? ToState,
IReadOnlyList<string>? ChangedFields);
// === Changelog Contracts ===
/// <summary>
/// Changelog entry response.
/// </summary>
public sealed record ChangeLogEntryResponse(
long SequenceNumber,
DateTimeOffset Timestamp,
string EntityType,
string EntityId,
string EventType,
string? EventHash,
string? ActorId,
string? Summary);
// === Staleness Contracts ===
/// <summary>
/// Staleness check response.
/// </summary>
public sealed record StalenessResponse(
bool IsStale,
DateTimeOffset CheckedAt,
DateTimeOffset? LastEventAt,
string StalenessThreshold,
string? StalenessDuration,
Dictionary<string, EntityStalenessResponse>? ByEntityType);
/// <summary>
/// Entity staleness response.
/// </summary>
public sealed record EntityStalenessResponse(
bool IsStale,
DateTimeOffset? LastEventAt,
long EventsBehind);
// === Extension Methods ===
public static class SnapshotExtensions
{
public static SnapshotResponse ToResponse(this LedgerSnapshot snapshot) => new(
SnapshotId: snapshot.SnapshotId,
Label: snapshot.Label,
Description: snapshot.Description,
Status: snapshot.Status.ToString(),
CreatedAt: snapshot.CreatedAt,
ExpiresAt: snapshot.ExpiresAt,
SequenceNumber: snapshot.SequenceNumber,
Timestamp: snapshot.Timestamp,
Statistics: snapshot.Statistics.ToResponse(),
MerkleRoot: snapshot.MerkleRoot,
DsseDigest: snapshot.DsseDigest,
Metadata: snapshot.Metadata);
public static SnapshotStatisticsResponse ToResponse(this SnapshotStatistics stats) => new(
FindingsCount: stats.FindingsCount,
VexStatementsCount: stats.VexStatementsCount,
AdvisoriesCount: stats.AdvisoriesCount,
SbomsCount: stats.SbomsCount,
EventsCount: stats.EventsCount,
SizeBytes: stats.SizeBytes);
public static QueryPointResponse ToResponse(this QueryPoint point) => new(
Timestamp: point.Timestamp,
SequenceNumber: point.SequenceNumber,
SnapshotId: point.SnapshotId);
public static FindingHistoryResponse ToResponse(this FindingHistoryItem item) => new(
FindingId: item.FindingId,
ArtifactId: item.ArtifactId,
VulnId: item.VulnId,
Status: item.Status,
Severity: item.Severity,
PolicyVersion: item.PolicyVersion,
FirstSeen: item.FirstSeen,
LastUpdated: item.LastUpdated,
Labels: item.Labels);
public static VexHistoryResponse ToResponse(this VexHistoryItem item) => new(
StatementId: item.StatementId,
VulnId: item.VulnId,
ProductId: item.ProductId,
Status: item.Status,
Justification: item.Justification,
IssuedAt: item.IssuedAt,
ExpiresAt: item.ExpiresAt);
public static AdvisoryHistoryResponse ToResponse(this AdvisoryHistoryItem item) => new(
AdvisoryId: item.AdvisoryId,
Source: item.Source,
Title: item.Title,
CvssScore: item.CvssScore,
PublishedAt: item.PublishedAt,
ModifiedAt: item.ModifiedAt);
public static ReplayEventResponse ToResponse(this ReplayEvent e) => new(
EventId: e.EventId,
SequenceNumber: e.SequenceNumber,
ChainId: e.ChainId,
ChainSequence: e.ChainSequence,
EventType: e.EventType,
OccurredAt: e.OccurredAt,
RecordedAt: e.RecordedAt,
ActorId: e.ActorId,
ActorType: e.ActorType,
ArtifactId: e.ArtifactId,
FindingId: e.FindingId,
PolicyVersion: e.PolicyVersion,
EventHash: e.EventHash,
PreviousHash: e.PreviousHash,
Payload: e.Payload);
public static ReplayMetadataResponse ToResponse(this ReplayMetadata m) => new(
FromSequence: m.FromSequence,
ToSequence: m.ToSequence,
EventsCount: m.EventsCount,
HasMore: m.HasMore,
ReplayDurationMs: m.ReplayDurationMs);
public static DiffSummaryResponse ToResponse(this DiffSummary summary) => new(
Added: summary.Added,
Modified: summary.Modified,
Removed: summary.Removed,
Unchanged: summary.Unchanged,
ByEntityType: summary.ByEntityType?.ToDictionary(
kv => kv.Key.ToString(),
kv => new DiffCountsResponse(kv.Value.Added, kv.Value.Modified, kv.Value.Removed)));
public static DiffEntryResponse ToResponse(this DiffEntry entry) => new(
EntityType: entry.EntityType.ToString(),
EntityId: entry.EntityId,
ChangeType: entry.ChangeType.ToString(),
FromState: entry.FromState,
ToState: entry.ToState,
ChangedFields: entry.ChangedFields);
public static ChangeLogEntryResponse ToResponse(this ChangeLogEntry entry) => new(
SequenceNumber: entry.SequenceNumber,
Timestamp: entry.Timestamp,
EntityType: entry.EntityType.ToString(),
EntityId: entry.EntityId,
EventType: entry.EventType,
EventHash: entry.EventHash,
ActorId: entry.ActorId,
Summary: entry.Summary);
public static StalenessResponse ToResponse(this StalenessResult result) => new(
IsStale: result.IsStale,
CheckedAt: result.CheckedAt,
LastEventAt: result.LastEventAt,
StalenessThreshold: result.StalenessThreshold.ToString(),
StalenessDuration: result.StalenessDuration?.ToString(),
ByEntityType: result.ByEntityType?.ToDictionary(
kv => kv.Key.ToString(),
kv => new EntityStalenessResponse(kv.Value.IsStale, kv.Value.LastEventAt, kv.Value.EventsBehind)));
}