Sprint 2+3+5: Registry search, workflow chain, unified security data

Sprint 2 — Registry image search (S2-T01/T02/T03):
  Harbor plugin: SearchRepositoriesAsync + ListArtifactsAsync calling
    Harbor /api/v2.0/search and /api/v2.0/projects/*/repositories/*/artifacts
  Platform endpoint: GET /api/v1/registries/images/search proxies to
    Harbor fixture, returns aggregated RegistryImage[] response
  Frontend: release-management.client.ts now calls /api/v1/registries/*
    instead of the nonexistent /api/registry/* path
  Gateway route: /api/v1/registries → platform (ReverseProxy)

Sprint 3 — Workflow chain links (S3-T01/T02/T03/T05):
  S3-T01: Integration detail health tab shows "Scan your first image"
    CTA after successful registry connection test
  S3-T02: Scan submit page already had "View findings" link (verified)
  S3-T03: Triage findings detail shows "Check policy gates" banner
    after recording a VEX decision
  S3-T05: Promotions list + detail show "Review blocking finding"
    link when promotion is blocked by gate failure

Sprint 5 — Unified security data (S5-T01):
  Security Posture now queries VULNERABILITY_API for triage stats
  Risk Posture card shows real finding count from triage (was hardcoded 0)
  Risk label computed from triage severity breakdown (GUARDED→HIGH)
  Blocking Items shows critical+high counts from triage
  "View in Vulnerabilities workspace" drilldown link added

Angular build: 0 errors. .NET builds: 0 errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-16 16:08:22 +02:00
parent 189171c594
commit efa33efdbc
11 changed files with 617 additions and 14 deletions

View File

@@ -160,11 +160,84 @@ public sealed class HarborConnectorPlugin : IIntegrationConnectorPlugin
return client;
}
/// <summary>
/// Searches Harbor repositories matching the given query string.
/// Uses Harbor v2.0 global search or project-scoped repository listing.
/// </summary>
public async Task<List<RepositoryInfo>> SearchRepositoriesAsync(IntegrationConfig config, string query, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(query))
return [];
using var client = CreateHttpClient(config);
try
{
// Use Harbor global search endpoint
var response = await client.GetAsync($"/api/v2.0/search?q={Uri.EscapeDataString(query)}", ct);
if (!response.IsSuccessStatusCode)
return [];
var content = await response.Content.ReadAsStringAsync(ct);
var searchResult = JsonSerializer.Deserialize<HarborSearchResponse>(content, JsonOptions);
return (searchResult?.Repository ?? [])
.Select(r => new RepositoryInfo
{
Name = r.RepositoryName ?? string.Empty,
Project = r.ProjectName ?? string.Empty,
Tags = []
})
.ToList();
}
catch
{
return [];
}
}
/// <summary>
/// Lists artifacts for a specific repository within a project.
/// </summary>
public async Task<List<ArtifactInfo>> ListArtifactsAsync(IntegrationConfig config, string project, string repository, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(project) || string.IsNullOrWhiteSpace(repository))
return [];
using var client = CreateHttpClient(config);
try
{
var encodedRepo = Uri.EscapeDataString(repository);
var response = await client.GetAsync($"/api/v2.0/projects/{Uri.EscapeDataString(project)}/repositories/{encodedRepo}/artifacts", ct);
if (!response.IsSuccessStatusCode)
return [];
var content = await response.Content.ReadAsStringAsync(ct);
var artifacts = JsonSerializer.Deserialize<List<HarborArtifactDto>>(content, JsonOptions);
return (artifacts ?? [])
.Select(a => new ArtifactInfo
{
Digest = a.Digest ?? string.Empty,
Tags = (a.Tags ?? []).Select(t => t.Name ?? string.Empty).Where(n => n.Length > 0).ToList(),
PushedAt = a.PushTime
})
.ToList();
}
catch
{
return [];
}
}
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
// ── Harbor API DTOs ────────────────────────────────────────────
private sealed class HarborHealthResponse
{
public string? Status { get; set; }
@@ -176,4 +249,47 @@ public sealed class HarborConnectorPlugin : IIntegrationConnectorPlugin
public string Name { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
}
private sealed class HarborSearchResponse
{
public List<HarborSearchRepository>? Repository { get; set; }
}
private sealed class HarborSearchRepository
{
public string? RepositoryName { get; set; }
public string? ProjectName { get; set; }
}
private sealed class HarborArtifactDto
{
public string? Digest { get; set; }
public List<HarborTagDto>? Tags { get; set; }
public DateTimeOffset? PushTime { get; set; }
}
private sealed class HarborTagDto
{
public string? Name { get; set; }
}
}
/// <summary>
/// Information about a Harbor repository returned from search.
/// </summary>
public sealed class RepositoryInfo
{
public string Name { get; set; } = string.Empty;
public string Project { get; set; } = string.Empty;
public List<string> Tags { get; set; } = [];
}
/// <summary>
/// Information about a Harbor artifact (image manifest).
/// </summary>
public sealed class ArtifactInfo
{
public string Digest { get; set; } = string.Empty;
public List<string> Tags { get; set; } = [];
public DateTimeOffset? PushedAt { get; set; }
}