finish secrets finding work and audit remarks work save

This commit is contained in:
StellaOps Bot
2026-01-04 21:48:13 +02:00
parent 75611a505f
commit 8862e112c4
157 changed files with 11702 additions and 416 deletions

View File

@@ -0,0 +1,373 @@
// -----------------------------------------------------------------------------
// SecretDetectionSettingsEndpoints.cs
// Sprint: SPRINT_20260104_006_BE (Secret Detection Configuration API)
// Task: SDC-005 - Create Settings CRUD API endpoints
// Description: HTTP endpoints for secret detection configuration.
// -----------------------------------------------------------------------------
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Endpoints;
/// <summary>
/// Endpoints for secret detection configuration.
/// Per SPRINT_20260104_006_BE.
/// </summary>
internal static class SecretDetectionSettingsEndpoints
{
/// <summary>
/// Maps secret detection settings endpoints.
/// </summary>
public static void MapSecretDetectionSettingsEndpoints(this RouteGroupBuilder apiGroup, string prefix = "/secrets/config")
{
ArgumentNullException.ThrowIfNull(apiGroup);
var settings = apiGroup.MapGroup($"{prefix}/settings")
.WithTags("Secret Detection Settings");
var exceptions = apiGroup.MapGroup($"{prefix}/exceptions")
.WithTags("Secret Detection Exceptions");
var rules = apiGroup.MapGroup($"{prefix}/rules")
.WithTags("Secret Detection Rules");
// ====================================================================
// Settings Endpoints
// ====================================================================
// GET /v1/secrets/config/settings/{tenantId} - Get settings
settings.MapGet("/{tenantId:guid}", HandleGetSettingsAsync)
.WithName("scanner.secrets.settings.get")
.WithDescription("Get secret detection settings for a tenant.")
.Produces<SecretDetectionSettingsResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.SecretSettingsRead);
// POST /v1/secrets/config/settings/{tenantId} - Create default settings
settings.MapPost("/{tenantId:guid}", HandleCreateSettingsAsync)
.WithName("scanner.secrets.settings.create")
.WithDescription("Create default secret detection settings for a tenant.")
.Produces<SecretDetectionSettingsResponseDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status409Conflict)
.RequireAuthorization(ScannerPolicies.SecretSettingsWrite);
// PUT /v1/secrets/config/settings/{tenantId} - Update settings
settings.MapPut("/{tenantId:guid}", HandleUpdateSettingsAsync)
.WithName("scanner.secrets.settings.update")
.WithDescription("Update secret detection settings for a tenant.")
.Produces<SecretDetectionSettingsResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict)
.RequireAuthorization(ScannerPolicies.SecretSettingsWrite);
// ====================================================================
// Exception Pattern Endpoints
// ====================================================================
// GET /v1/secrets/config/exceptions/{tenantId} - List exception patterns
exceptions.MapGet("/{tenantId:guid}", HandleListExceptionsAsync)
.WithName("scanner.secrets.exceptions.list")
.WithDescription("List secret exception patterns for a tenant.")
.Produces<SecretExceptionPatternListResponseDto>(StatusCodes.Status200OK)
.RequireAuthorization(ScannerPolicies.SecretExceptionsRead);
// GET /v1/secrets/config/exceptions/{tenantId}/{exceptionId} - Get exception pattern
exceptions.MapGet("/{tenantId:guid}/{exceptionId:guid}", HandleGetExceptionAsync)
.WithName("scanner.secrets.exceptions.get")
.WithDescription("Get a specific secret exception pattern.")
.Produces<SecretExceptionPatternResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.SecretExceptionsRead);
// POST /v1/secrets/config/exceptions/{tenantId} - Create exception pattern
exceptions.MapPost("/{tenantId:guid}", HandleCreateExceptionAsync)
.WithName("scanner.secrets.exceptions.create")
.WithDescription("Create a new secret exception pattern.")
.Produces<SecretExceptionPatternResponseDto>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.SecretExceptionsWrite);
// PUT /v1/secrets/config/exceptions/{tenantId}/{exceptionId} - Update exception pattern
exceptions.MapPut("/{tenantId:guid}/{exceptionId:guid}", HandleUpdateExceptionAsync)
.WithName("scanner.secrets.exceptions.update")
.WithDescription("Update a secret exception pattern.")
.Produces<SecretExceptionPatternResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.SecretExceptionsWrite);
// DELETE /v1/secrets/config/exceptions/{tenantId}/{exceptionId} - Delete exception pattern
exceptions.MapDelete("/{tenantId:guid}/{exceptionId:guid}", HandleDeleteExceptionAsync)
.WithName("scanner.secrets.exceptions.delete")
.WithDescription("Delete a secret exception pattern.")
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.SecretExceptionsWrite);
// ====================================================================
// Rule Catalog Endpoints
// ====================================================================
// GET /v1/secrets/config/rules/categories - Get available rule categories
rules.MapGet("/categories", HandleGetRuleCategoriesAsync)
.WithName("scanner.secrets.rules.categories")
.WithDescription("Get available secret detection rule categories.")
.Produces<RuleCategoriesResponseDto>(StatusCodes.Status200OK)
.RequireAuthorization(ScannerPolicies.SecretSettingsRead);
}
// ========================================================================
// Settings Handlers
// ========================================================================
private static async Task<IResult> HandleGetSettingsAsync(
Guid tenantId,
ISecretDetectionSettingsService service,
CancellationToken cancellationToken)
{
var settings = await service.GetSettingsAsync(tenantId, cancellationToken);
if (settings is null)
{
return Results.NotFound(new
{
type = "not-found",
title = "Settings not found",
detail = $"No secret detection settings found for tenant '{tenantId}'."
});
}
return Results.Ok(settings);
}
private static async Task<IResult> HandleCreateSettingsAsync(
Guid tenantId,
ISecretDetectionSettingsService service,
HttpContext context,
CancellationToken cancellationToken)
{
// Check if settings already exist
var existing = await service.GetSettingsAsync(tenantId, cancellationToken);
if (existing is not null)
{
return Results.Conflict(new
{
type = "conflict",
title = "Settings already exist",
detail = $"Secret detection settings already exist for tenant '{tenantId}'."
});
}
var username = context.User.Identity?.Name ?? "system";
var settings = await service.CreateSettingsAsync(tenantId, username, cancellationToken);
return Results.Created($"/v1/secrets/config/settings/{tenantId}", settings);
}
private static async Task<IResult> HandleUpdateSettingsAsync(
Guid tenantId,
UpdateSecretDetectionSettingsRequestDto request,
ISecretDetectionSettingsService service,
HttpContext context,
CancellationToken cancellationToken)
{
var username = context.User.Identity?.Name ?? "system";
var (success, settings, error) = await service.UpdateSettingsAsync(
tenantId,
request.Settings,
request.ExpectedVersion,
username,
cancellationToken);
if (!success)
{
if (error?.Contains("not found", StringComparison.OrdinalIgnoreCase) == true)
{
return Results.NotFound(new
{
type = "not-found",
title = "Settings not found",
detail = error
});
}
if (error?.Contains("conflict", StringComparison.OrdinalIgnoreCase) == true)
{
return Results.Conflict(new
{
type = "conflict",
title = "Version conflict",
detail = error
});
}
return Results.BadRequest(new
{
type = "validation-error",
title = "Validation failed",
detail = error
});
}
return Results.Ok(settings);
}
// ========================================================================
// Exception Pattern Handlers
// ========================================================================
private static async Task<IResult> HandleListExceptionsAsync(
Guid tenantId,
ISecretExceptionPatternService service,
bool includeInactive = false,
CancellationToken cancellationToken = default)
{
var patterns = await service.GetPatternsAsync(tenantId, includeInactive, cancellationToken);
return Results.Ok(patterns);
}
private static async Task<IResult> HandleGetExceptionAsync(
Guid tenantId,
Guid exceptionId,
ISecretExceptionPatternService service,
CancellationToken cancellationToken)
{
var pattern = await service.GetPatternAsync(exceptionId, cancellationToken);
if (pattern is null || pattern.TenantId != tenantId)
{
return Results.NotFound(new
{
type = "not-found",
title = "Exception pattern not found",
detail = $"No exception pattern found with ID '{exceptionId}'."
});
}
return Results.Ok(pattern);
}
private static async Task<IResult> HandleCreateExceptionAsync(
Guid tenantId,
SecretExceptionPatternDto request,
ISecretExceptionPatternService service,
HttpContext context,
CancellationToken cancellationToken)
{
var username = context.User.Identity?.Name ?? "system";
var (pattern, errors) = await service.CreatePatternAsync(tenantId, request, username, cancellationToken);
if (errors.Count > 0)
{
return Results.BadRequest(new
{
type = "validation-error",
title = "Validation failed",
detail = string.Join("; ", errors),
errors
});
}
return Results.Created($"/v1/secrets/config/exceptions/{tenantId}/{pattern!.Id}", pattern);
}
private static async Task<IResult> HandleUpdateExceptionAsync(
Guid tenantId,
Guid exceptionId,
SecretExceptionPatternDto request,
ISecretExceptionPatternService service,
HttpContext context,
CancellationToken cancellationToken)
{
// Verify pattern belongs to tenant
var existing = await service.GetPatternAsync(exceptionId, cancellationToken);
if (existing is null || existing.TenantId != tenantId)
{
return Results.NotFound(new
{
type = "not-found",
title = "Exception pattern not found",
detail = $"No exception pattern found with ID '{exceptionId}'."
});
}
var username = context.User.Identity?.Name ?? "system";
var (success, pattern, errors) = await service.UpdatePatternAsync(
exceptionId,
request,
username,
cancellationToken);
if (!success)
{
if (errors.Count > 0 && errors[0].Contains("not found", StringComparison.OrdinalIgnoreCase))
{
return Results.NotFound(new
{
type = "not-found",
title = "Exception pattern not found",
detail = errors[0]
});
}
return Results.BadRequest(new
{
type = "validation-error",
title = "Validation failed",
detail = string.Join("; ", errors),
errors
});
}
return Results.Ok(pattern);
}
private static async Task<IResult> HandleDeleteExceptionAsync(
Guid tenantId,
Guid exceptionId,
ISecretExceptionPatternService service,
CancellationToken cancellationToken)
{
// Verify pattern belongs to tenant
var existing = await service.GetPatternAsync(exceptionId, cancellationToken);
if (existing is null || existing.TenantId != tenantId)
{
return Results.NotFound(new
{
type = "not-found",
title = "Exception pattern not found",
detail = $"No exception pattern found with ID '{exceptionId}'."
});
}
var deleted = await service.DeletePatternAsync(exceptionId, cancellationToken);
if (!deleted)
{
return Results.NotFound(new
{
type = "not-found",
title = "Exception pattern not found",
detail = $"No exception pattern found with ID '{exceptionId}'."
});
}
return Results.NoContent();
}
// ========================================================================
// Rule Catalog Handlers
// ========================================================================
private static async Task<IResult> HandleGetRuleCategoriesAsync(
ISecretDetectionSettingsService service,
CancellationToken cancellationToken)
{
var categories = await service.GetRuleCategoriesAsync(cancellationToken);
return Results.Ok(categories);
}
}