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:
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user