Gaps fill up, fixes, ui restructuring

This commit is contained in:
master
2026-02-19 22:10:54 +02:00
parent b5829dce5c
commit 04cacdca8a
331 changed files with 42859 additions and 2174 deletions

View File

@@ -121,6 +121,18 @@ public static class IntegrationEndpoints
.WithName("CheckIntegrationHealth")
.WithDescription("Performs a health check on an integration.");
// Impact map
group.MapGet("/{id:guid}/impact", async (
[FromServices] IntegrationService service,
Guid id,
CancellationToken cancellationToken) =>
{
var result = await service.GetImpactAsync(id, cancellationToken);
return result is null ? Results.NotFound() : Results.Ok(result);
})
.WithName("GetIntegrationImpact")
.WithDescription("Returns affected workflows and severity impact for an integration.");
// Get supported providers
group.MapGet("/providers", ([FromServices] IntegrationService service) =>
{

View File

@@ -269,6 +269,31 @@ public sealed class IntegrationService
result.Duration);
}
public async Task<IntegrationImpactResponse?> GetImpactAsync(Guid id, CancellationToken cancellationToken = default)
{
var integration = await _repository.GetByIdAsync(id, cancellationToken);
if (integration is null)
{
return null;
}
var impactedWorkflows = BuildImpactedWorkflows(integration)
.OrderBy(workflow => workflow.Workflow, StringComparer.Ordinal)
.ToList();
var blockingCount = impactedWorkflows.Count(workflow => workflow.Blocking);
return new IntegrationImpactResponse(
IntegrationId: integration.Id,
IntegrationName: integration.Name,
Type: integration.Type,
Provider: integration.Provider,
Status: integration.Status,
Severity: DetermineSeverity(integration.Status, blockingCount),
BlockingWorkflowCount: blockingCount,
TotalWorkflowCount: impactedWorkflows.Count,
ImpactedWorkflows: impactedWorkflows);
}
public IReadOnlyList<ProviderInfo> GetSupportedProviders()
{
return _pluginLoader.Plugins.Select(p => new ProviderInfo(
@@ -277,6 +302,55 @@ public sealed class IntegrationService
p.Provider)).ToList();
}
private static IReadOnlyList<ImpactedWorkflow> BuildImpactedWorkflows(Integration integration)
{
var blockedByStatus = integration.Status is IntegrationStatus.Failed or IntegrationStatus.Disabled or IntegrationStatus.Archived;
return integration.Type switch
{
IntegrationType.Registry =>
[
new ImpactedWorkflow("bundle-materialization", "release-control", blockedByStatus, "Container digest fetch and verification path.", "restore-registry-connectivity"),
new ImpactedWorkflow("sbom-attachment", "evidence", blockedByStatus, "SBOM/image digest correlation during pack generation.", "re-run-bundle-sync"),
],
IntegrationType.Scm =>
[
new ImpactedWorkflow("bundle-changelog", "release-control", blockedByStatus, "Repository changelog enrichment for bundle versions.", "reconnect-scm-app"),
new ImpactedWorkflow("policy-drift-audit", "administration", blockedByStatus, "Policy governance change audit extraction.", "refresh-scm-access-token"),
],
IntegrationType.CiCd =>
[
new ImpactedWorkflow("promotion-preflight", "release-control", blockedByStatus, "Deployment signal and gate preflight signal stream.", "revalidate-ci-runner-credentials"),
new ImpactedWorkflow("ops-job-health", "platform-ops", blockedByStatus, "Pipeline lag/health insights for nightly report.", "replay-latest-ci-webhooks"),
],
IntegrationType.RepoSource =>
[
new ImpactedWorkflow("dependency-resolution", "security-risk", blockedByStatus, "Package advisory resolution and normalization.", "verify-repository-mirror"),
new ImpactedWorkflow("hot-lookup-projection", "security-risk", blockedByStatus, "Hot-lookup enrichment for findings explorer.", "resync-package-index"),
],
IntegrationType.RuntimeHost =>
[
new ImpactedWorkflow("runtime-reachability", "security-risk", blockedByStatus, "Runtime witness ingestion for reachability confidence.", "restart-runtime-agent"),
new ImpactedWorkflow("ops-confidence", "platform-ops", blockedByStatus, "Data-confidence score for approvals and dashboard.", "clear-runtime-dlq"),
],
IntegrationType.FeedMirror =>
[
new ImpactedWorkflow("advisory-freshness", "security-risk", blockedByStatus, "Advisory source freshness and conflict views.", "refresh-feed-mirror"),
new ImpactedWorkflow("rescore-pipeline", "platform-ops", blockedByStatus, "Nightly rescoring jobs and stale SBOM remediation.", "trigger-feed-replay"),
],
_ => []
};
}
private static string DetermineSeverity(IntegrationStatus status, int blockingCount)
{
if (status is IntegrationStatus.Failed or IntegrationStatus.Disabled or IntegrationStatus.Archived)
{
return "high";
}
return blockingCount > 0 ? "medium" : "low";
}
private static IntegrationConfig BuildConfig(Integration integration, string? resolvedSecret)
{
IReadOnlyDictionary<string, object>? extendedConfig = null;
@@ -321,3 +395,21 @@ public sealed class IntegrationService
/// Information about a supported provider.
/// </summary>
public sealed record ProviderInfo(string Name, IntegrationType Type, IntegrationProvider Provider);
public sealed record IntegrationImpactResponse(
Guid IntegrationId,
string IntegrationName,
IntegrationType Type,
IntegrationProvider Provider,
IntegrationStatus Status,
string Severity,
int BlockingWorkflowCount,
int TotalWorkflowCount,
IReadOnlyList<ImpactedWorkflow> ImpactedWorkflows);
public sealed record ImpactedWorkflow(
string Workflow,
string Domain,
bool Blocking,
string Impact,
string RecommendedAction);

View File

@@ -21,7 +21,13 @@ public enum IntegrationType
RuntimeHost = 5,
/// <summary>Advisory/vulnerability feed mirror.</summary>
FeedMirror = 6
FeedMirror = 6,
/// <summary>Symbol/debug pack source (Microsoft Symbols, debuginfod, partner feeds).</summary>
SymbolSource = 7,
/// <summary>Remediation marketplace source (community, partner, vendor fix templates).</summary>
Marketplace = 8
}
/// <summary>
@@ -75,6 +81,18 @@ public enum IntegrationProvider
NvdMirror = 601,
OsvMirror = 602,
// Symbol sources
MicrosoftSymbols = 700,
UbuntuDebuginfod = 701,
FedoraDebuginfod = 702,
DebianDebuginfod = 703,
PartnerSymbols = 704,
// Marketplace sources
CommunityFixes = 800,
PartnerFixes = 801,
VendorFixes = 802,
// Generic / testing
InMemory = 900,
Custom = 999

View File

@@ -0,0 +1,269 @@
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Integrations.Contracts;
using StellaOps.Integrations.Core;
using StellaOps.Integrations.Persistence;
using StellaOps.Integrations.WebService;
using StellaOps.TestKit;
namespace StellaOps.Integrations.Tests;
public sealed class IntegrationImpactEndpointsTests : IClassFixture<IntegrationImpactWebApplicationFactory>
{
private readonly HttpClient _client;
public IntegrationImpactEndpointsTests(IntegrationImpactWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task ImpactEndpoint_ReturnsDeterministicWorkflowMap()
{
var createRequest = new CreateIntegrationRequest(
Name: $"NVD Mirror {Guid.NewGuid():N}",
Description: "Feed mirror",
Type: IntegrationType.FeedMirror,
Provider: IntegrationProvider.NvdMirror,
Endpoint: "https://feeds.example.local/nvd",
AuthRefUri: null,
OrganizationId: null,
ExtendedConfig: null,
Tags: ["feed"]);
var createResponse = await _client.PostAsJsonAsync(
"/api/v1/integrations/",
createRequest,
TestContext.Current.CancellationToken);
createResponse.EnsureSuccessStatusCode();
var created = await createResponse.Content.ReadFromJsonAsync<IntegrationResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
var first = await _client.GetFromJsonAsync<IntegrationImpactResponse>(
$"/api/v1/integrations/{created!.Id}/impact",
TestContext.Current.CancellationToken);
var second = await _client.GetFromJsonAsync<IntegrationImpactResponse>(
$"/api/v1/integrations/{created.Id}/impact",
TestContext.Current.CancellationToken);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(first!.IntegrationId, second!.IntegrationId);
Assert.Equal(first.IntegrationName, second.IntegrationName);
Assert.Equal(first.Type, second.Type);
Assert.Equal(first.Provider, second.Provider);
Assert.Equal(first.Status, second.Status);
Assert.Equal(first.Severity, second.Severity);
Assert.Equal(first.BlockingWorkflowCount, second.BlockingWorkflowCount);
Assert.Equal(first.TotalWorkflowCount, second.TotalWorkflowCount);
Assert.Equal(
first.ImpactedWorkflows.Select(workflow => workflow.Workflow).ToArray(),
second.ImpactedWorkflows.Select(workflow => workflow.Workflow).ToArray());
Assert.Equal("low", first!.Severity);
Assert.Equal(0, first.BlockingWorkflowCount);
var ordered = first.ImpactedWorkflows
.Select(workflow => workflow.Workflow)
.OrderBy(workflow => workflow, StringComparer.Ordinal)
.ToArray();
Assert.Equal(ordered, first.ImpactedWorkflows.Select(workflow => workflow.Workflow).ToArray());
}
[Trait("Category", TestCategories.Integration)]
[Fact]
public async Task ImpactEndpoint_WithUnknownId_ReturnsNotFound()
{
var response = await _client.GetAsync(
$"/api/v1/integrations/{Guid.NewGuid()}/impact",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
public sealed class IntegrationImpactWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IIntegrationRepository>();
services.AddSingleton<IIntegrationRepository, InMemoryIntegrationRepository>();
});
}
}
internal sealed class InMemoryIntegrationRepository : IIntegrationRepository
{
private readonly Dictionary<Guid, Integration> _items = new();
private readonly object _gate = new();
public Task<Integration?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
lock (_gate)
{
_items.TryGetValue(id, out var value);
return Task.FromResult<Integration?>(value is null ? null : Clone(value));
}
}
public Task<IReadOnlyList<Integration>> GetAllAsync(IntegrationQuery query, CancellationToken cancellationToken = default)
{
lock (_gate)
{
IEnumerable<Integration> values = _items.Values;
if (!query.IncludeDeleted)
{
values = values.Where(item => !item.IsDeleted);
}
if (query.Type.HasValue)
{
values = values.Where(item => item.Type == query.Type.Value);
}
if (query.Provider.HasValue)
{
values = values.Where(item => item.Provider == query.Provider.Value);
}
if (query.Status.HasValue)
{
values = values.Where(item => item.Status == query.Status.Value);
}
if (!string.IsNullOrWhiteSpace(query.TenantId))
{
values = values.Where(item => string.Equals(item.TenantId, query.TenantId, StringComparison.Ordinal));
}
if (!string.IsNullOrWhiteSpace(query.Search))
{
values = values.Where(item =>
item.Name.Contains(query.Search, StringComparison.OrdinalIgnoreCase) ||
(item.Description?.Contains(query.Search, StringComparison.OrdinalIgnoreCase) ?? false));
}
values = values.OrderBy(item => item.Name, StringComparer.Ordinal)
.Skip(Math.Max(0, query.Skip))
.Take(Math.Max(1, query.Take));
return Task.FromResult<IReadOnlyList<Integration>>(values.Select(Clone).ToList());
}
}
public Task<int> CountAsync(IntegrationQuery query, CancellationToken cancellationToken = default)
{
lock (_gate)
{
return Task.FromResult(_items.Values.Count(item => query.IncludeDeleted || !item.IsDeleted));
}
}
public Task<Integration> CreateAsync(Integration integration, CancellationToken cancellationToken = default)
{
lock (_gate)
{
_items[integration.Id] = Clone(integration);
return Task.FromResult(Clone(integration));
}
}
public Task<Integration> UpdateAsync(Integration integration, CancellationToken cancellationToken = default)
{
lock (_gate)
{
_items[integration.Id] = Clone(integration);
return Task.FromResult(Clone(integration));
}
}
public Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
{
lock (_gate)
{
if (_items.TryGetValue(id, out var existing))
{
existing.IsDeleted = true;
existing.Status = IntegrationStatus.Archived;
_items[id] = existing;
}
}
return Task.CompletedTask;
}
public Task<IReadOnlyList<Integration>> GetByProviderAsync(IntegrationProvider provider, CancellationToken cancellationToken = default)
{
lock (_gate)
{
var items = _items.Values
.Where(item => item.Provider == provider && !item.IsDeleted)
.OrderBy(item => item.Name, StringComparer.Ordinal)
.Select(Clone)
.ToList();
return Task.FromResult<IReadOnlyList<Integration>>(items);
}
}
public Task<IReadOnlyList<Integration>> GetActiveByTypeAsync(IntegrationType type, CancellationToken cancellationToken = default)
{
lock (_gate)
{
var items = _items.Values
.Where(item => item.Type == type && item.Status == IntegrationStatus.Active && !item.IsDeleted)
.OrderBy(item => item.Name, StringComparer.Ordinal)
.Select(Clone)
.ToList();
return Task.FromResult<IReadOnlyList<Integration>>(items);
}
}
public Task UpdateHealthStatusAsync(Guid id, HealthStatus status, DateTimeOffset checkedAt, CancellationToken cancellationToken = default)
{
lock (_gate)
{
if (_items.TryGetValue(id, out var existing))
{
existing.LastHealthStatus = status;
existing.LastHealthCheckAt = checkedAt;
_items[id] = existing;
}
}
return Task.CompletedTask;
}
private static Integration Clone(Integration source)
{
return new Integration
{
Id = source.Id,
Name = source.Name,
Description = source.Description,
Type = source.Type,
Provider = source.Provider,
Status = source.Status,
Endpoint = source.Endpoint,
AuthRefUri = source.AuthRefUri,
OrganizationId = source.OrganizationId,
ConfigJson = source.ConfigJson,
LastHealthStatus = source.LastHealthStatus,
LastHealthCheckAt = source.LastHealthCheckAt,
CreatedAt = source.CreatedAt,
UpdatedAt = source.UpdatedAt,
CreatedBy = source.CreatedBy,
UpdatedBy = source.UpdatedBy,
TenantId = source.TenantId,
Tags = source.Tags.ToList(),
IsDeleted = source.IsDeleted
};
}
}

View File

@@ -324,6 +324,50 @@ public class IntegrationServiceTests
result.Should().BeEmpty();
}
[Trait("Category", "Unit")]
[Fact]
public async Task GetImpactAsync_WithNonExistingIntegration_ReturnsNull()
{
// Arrange
var id = Guid.NewGuid();
_repositoryMock
.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
.ReturnsAsync((Integration?)null);
// Act
var result = await _service.GetImpactAsync(id);
// Assert
result.Should().BeNull();
}
[Trait("Category", "Unit")]
[Fact]
public async Task GetImpactAsync_WithFailedFeedMirror_ReturnsBlockingHighSeverity()
{
// Arrange
var integration = CreateTestIntegration(
type: IntegrationType.FeedMirror,
provider: IntegrationProvider.NvdMirror);
integration.Status = IntegrationStatus.Failed;
integration.Name = "NVD Mirror";
_repositoryMock
.Setup(r => r.GetByIdAsync(integration.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(integration);
// Act
var result = await _service.GetImpactAsync(integration.Id);
// Assert
result.Should().NotBeNull();
result!.Severity.Should().Be("high");
result.BlockingWorkflowCount.Should().Be(result.TotalWorkflowCount);
result.ImpactedWorkflows.Should().HaveCount(2);
result.ImpactedWorkflows.Select(workflow => workflow.Workflow)
.Should().BeInAscendingOrder(StringComparer.Ordinal);
}
private static Integration CreateTestIntegration(
IntegrationType type = IntegrationType.Registry,
IntegrationProvider provider = IntegrationProvider.Harbor)