Implement missing backend endpoints for release orchestration

TASK-002: 11 deployment monitoring endpoints in JobEngine
  (list, get, logs, events, metrics, pause/resume/cancel/rollback/retry)
TASK-003: 6 evidence management endpoints in JobEngine
  (list, get, verify, export, raw, timeline)
TASK-005: 3 release dashboard endpoints in JobEngine
  (dashboard summary, approve/reject promotion)
TASK-006: 2 registry image search endpoints in Scanner
  (search with 9 mock images, digests lookup)

All endpoints return seed/mock data for testing. Auth policies
match existing patterns. Dual route registration on both
/api/ and /api/v1/ prefixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-23 15:52:20 +02:00
parent d3353e9d16
commit dd29786e38
17 changed files with 2066 additions and 26 deletions

View File

@@ -0,0 +1,259 @@
// -----------------------------------------------------------------------------
// RegistryEndpoints.cs
// Description: HTTP endpoints for registry image search and digest lookup.
// Returns mock data until real registry integration is wired.
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Security;
namespace StellaOps.Scanner.WebService.Endpoints;
/// <summary>
/// Endpoints for container registry image search and digest queries.
/// Used by Create Version / Create Hotfix wizards in the UI.
/// </summary>
internal static class RegistryEndpoints
{
/// <summary>
/// Maps registry image endpoints under /registries.
/// </summary>
public static void MapRegistryEndpoints(this RouteGroupBuilder apiGroup, string prefix = "/registries")
{
ArgumentNullException.ThrowIfNull(apiGroup);
var group = apiGroup.MapGroup(prefix)
.WithTags("Registries");
// GET /api/v1/registries/images/search?q={query}
group.MapGet("/images/search", HandleSearchImages)
.WithName("scanner.registries.images.search")
.WithDescription("Search container registry images by name")
.Produces<RegistrySearchResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /api/v1/registries/images/digests?repository={repo}
group.MapGet("/images/digests", HandleGetImageDigests)
.WithName("scanner.registries.images.digests")
.WithDescription("Get image digests and tags for a repository")
.Produces<RegistryDigestResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
}
private static IResult HandleSearchImages(string? q)
{
if (string.IsNullOrWhiteSpace(q) || q.Length < 2)
{
return Results.BadRequest(new { error = "Query must be at least 2 characters" });
}
var filtered = MockImages.Where(i =>
i.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
i.Repository.Contains(q, StringComparison.OrdinalIgnoreCase)
).ToArray();
return Results.Ok(new RegistrySearchResponse
{
Items = filtered,
TotalCount = filtered.Length,
RegistryId = null
});
}
private static IResult HandleGetImageDigests(string? repository)
{
if (string.IsNullOrWhiteSpace(repository))
{
return Results.BadRequest(new { error = "Repository parameter is required" });
}
var match = MockImages.FirstOrDefault(i =>
string.Equals(i.Repository, repository, StringComparison.OrdinalIgnoreCase));
if (match is null)
{
// Return a stub for unknown repositories
var repoName = repository.Contains('/')
? repository.Split('/').Last()
: repository;
return Results.Ok(new RegistryDigestResponse
{
Name = repoName,
Repository = repository,
Tags = new[] { "latest" },
Digests = new[]
{
new RegistryDigestEntry
{
Tag = "latest",
Digest = $"sha256:{Guid.NewGuid():N}",
PushedAt = "2026-03-20T10:00:00Z"
}
}
});
}
return Results.Ok(new RegistryDigestResponse
{
Name = match.Name,
Repository = match.Repository,
Tags = match.Tags,
Digests = match.Digests
});
}
// -------------------------------------------------------------------------
// Response DTOs
// -------------------------------------------------------------------------
internal sealed class RegistrySearchResponse
{
public RegistryImageDto[] Items { get; set; } = Array.Empty<RegistryImageDto>();
public int TotalCount { get; set; }
public string? RegistryId { get; set; }
}
internal sealed class RegistryDigestResponse
{
public string Name { get; set; } = string.Empty;
public string Repository { get; set; } = string.Empty;
public string[] Tags { get; set; } = Array.Empty<string>();
public RegistryDigestEntry[] Digests { get; set; } = Array.Empty<RegistryDigestEntry>();
}
internal sealed class RegistryImageDto
{
public string Name { get; set; } = string.Empty;
public string Repository { get; set; } = string.Empty;
public string[] Tags { get; set; } = Array.Empty<string>();
public RegistryDigestEntry[] Digests { get; set; } = Array.Empty<RegistryDigestEntry>();
public string LastPushed { get; set; } = string.Empty;
}
internal sealed class RegistryDigestEntry
{
public string Tag { get; set; } = string.Empty;
public string Digest { get; set; } = string.Empty;
public string PushedAt { get; set; } = string.Empty;
}
// -------------------------------------------------------------------------
// Mock data (to be replaced with real registry integration)
// -------------------------------------------------------------------------
private static readonly RegistryImageDto[] MockImages = new[]
{
new RegistryImageDto
{
Name = "nginx",
Repository = "library/nginx",
Tags = new[] { "latest", "1.27", "1.27-alpine" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", PushedAt = "2026-03-20T10:00:00Z" },
new RegistryDigestEntry { Tag = "1.27", Digest = "sha256:b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3", PushedAt = "2026-03-18T14:30:00Z" },
},
LastPushed = "2026-03-20T10:00:00Z"
},
new RegistryImageDto
{
Name = "redis",
Repository = "library/redis",
Tags = new[] { "latest", "7.4", "7.4-alpine" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4", PushedAt = "2026-03-19T08:00:00Z" },
new RegistryDigestEntry { Tag = "7.4", Digest = "sha256:d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5", PushedAt = "2026-03-17T16:45:00Z" },
},
LastPushed = "2026-03-19T08:00:00Z"
},
new RegistryImageDto
{
Name = "postgres",
Repository = "library/postgres",
Tags = new[] { "latest", "16.2", "16.2-alpine" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6", PushedAt = "2026-03-21T06:15:00Z" },
new RegistryDigestEntry { Tag = "16.2", Digest = "sha256:f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7", PushedAt = "2026-03-15T09:30:00Z" },
},
LastPushed = "2026-03-21T06:15:00Z"
},
new RegistryImageDto
{
Name = "api-gateway",
Repository = "stella-ops/api-gateway",
Tags = new[] { "latest", "2.8.1", "2.8.0" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8", PushedAt = "2026-03-22T11:00:00Z" },
new RegistryDigestEntry { Tag = "2.8.1", Digest = "sha256:b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9", PushedAt = "2026-03-22T11:00:00Z" },
},
LastPushed = "2026-03-22T11:00:00Z"
},
new RegistryImageDto
{
Name = "payment-svc",
Repository = "stella-ops/payment-svc",
Tags = new[] { "latest", "3.1.0", "3.0.9" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0", PushedAt = "2026-03-21T17:20:00Z" },
new RegistryDigestEntry { Tag = "3.1.0", Digest = "sha256:d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1", PushedAt = "2026-03-21T17:20:00Z" },
},
LastPushed = "2026-03-21T17:20:00Z"
},
new RegistryImageDto
{
Name = "auth-service",
Repository = "stella-ops/auth-service",
Tags = new[] { "latest", "1.14.2", "1.14.1" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2", PushedAt = "2026-03-20T13:45:00Z" },
new RegistryDigestEntry { Tag = "1.14.2", Digest = "sha256:f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3", PushedAt = "2026-03-20T13:45:00Z" },
},
LastPushed = "2026-03-20T13:45:00Z"
},
new RegistryImageDto
{
Name = "node",
Repository = "library/node",
Tags = new[] { "latest", "22-alpine", "20-slim" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4", PushedAt = "2026-03-22T07:00:00Z" },
new RegistryDigestEntry { Tag = "22-alpine", Digest = "sha256:b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5", PushedAt = "2026-03-22T07:00:00Z" },
},
LastPushed = "2026-03-22T07:00:00Z"
},
new RegistryImageDto
{
Name = "python",
Repository = "library/python",
Tags = new[] { "latest", "3.13-slim", "3.12-bookworm" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", PushedAt = "2026-03-21T09:30:00Z" },
new RegistryDigestEntry { Tag = "3.13-slim", Digest = "sha256:d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7", PushedAt = "2026-03-21T09:30:00Z" },
},
LastPushed = "2026-03-21T09:30:00Z"
},
new RegistryImageDto
{
Name = "golang",
Repository = "library/golang",
Tags = new[] { "latest", "1.23-alpine", "1.22-bookworm" },
Digests = new[]
{
new RegistryDigestEntry { Tag = "latest", Digest = "sha256:e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8", PushedAt = "2026-03-20T15:00:00Z" },
new RegistryDigestEntry { Tag = "1.23-alpine", Digest = "sha256:f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9", PushedAt = "2026-03-20T15:00:00Z" },
},
LastPushed = "2026-03-20T15:00:00Z"
},
};
}

View File

@@ -805,6 +805,7 @@ apiGroup.MapUnknownsEndpoints();
apiGroup.MapSecretDetectionSettingsEndpoints(); // Sprint: SPRINT_20260104_006_BE
apiGroup.MapSecurityAdapterEndpoints(); // Pack v2 security adapter routes
apiGroup.MapScanPolicyEndpoints(); // Sprint: S1-T03 Scan Policy CRUD
apiGroup.MapRegistryEndpoints(); // Registry image search + digest lookup for release wizards
if (resolvedOptions.Features.EnablePolicyPreview)
{