sprints work

This commit is contained in:
master
2026-01-10 11:15:28 +02:00
parent a21d3dbc1f
commit 701eb6b21c
71 changed files with 10854 additions and 136 deletions

View File

@@ -0,0 +1,371 @@
// <copyright file="GitHubCodeScanningEndpoints.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Constants;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Infrastructure;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Endpoints;
/// <summary>
/// API endpoints for GitHub Code Scanning integration.
/// Sprint: SPRINT_20260109_010_002 Task: API endpoints
/// </summary>
internal static class GitHubCodeScanningEndpoints
{
public static void MapGitHubCodeScanningEndpoints(this RouteGroupBuilder scansGroup)
{
ArgumentNullException.ThrowIfNull(scansGroup);
var github = scansGroup.MapGroup("/github")
.WithTags("GitHub", "Code Scanning");
// POST /scans/{scanId}/github/upload-sarif
// Upload scan results as SARIF to GitHub Code Scanning
github.MapPost("/{scanId}/github/upload-sarif", HandleUploadSarifAsync)
.WithName("scanner.scans.github.upload-sarif")
.Produces<SarifUploadResponse>(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansWrite);
// GET /scans/{scanId}/github/upload-status/{sarifId}
// Check the processing status of a SARIF upload
github.MapGet("/{scanId}/github/upload-status/{sarifId}", HandleGetUploadStatusAsync)
.WithName("scanner.scans.github.upload-status")
.Produces<SarifUploadStatusResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /scans/{scanId}/github/alerts
// List Code Scanning alerts for the repository
github.MapGet("/{scanId}/github/alerts", HandleListAlertsAsync)
.WithName("scanner.scans.github.alerts.list")
.Produces<AlertsListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /scans/{scanId}/github/alerts/{alertNumber}
// Get a specific Code Scanning alert
github.MapGet("/{scanId}/github/alerts/{alertNumber:int}", HandleGetAlertAsync)
.WithName("scanner.scans.github.alerts.get")
.Produces<AlertResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
}
private static async Task<IResult> HandleUploadSarifAsync(
string scanId,
SarifUploadRequest request,
IScanCoordinator coordinator,
ISarifExportService sarifExport,
IGitHubCodeScanningService gitHubService,
HttpContext context,
CancellationToken cancellationToken)
{
if (!ScanId.TryParse(scanId, out var parsed))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest);
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
StatusCodes.Status404NotFound);
}
if (string.IsNullOrEmpty(request.Owner) || string.IsNullOrEmpty(request.Repo))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Owner and repo are required",
StatusCodes.Status400BadRequest);
}
// Export SARIF
var sarifDoc = await sarifExport.ExportAsync(parsed, cancellationToken).ConfigureAwait(false);
if (sarifDoc is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No findings to export",
StatusCodes.Status404NotFound);
}
// Upload to GitHub
var result = await gitHubService.UploadSarifAsync(
request.Owner,
request.Repo,
sarifDoc,
request.CommitSha,
request.Ref ?? "refs/heads/main",
cancellationToken).ConfigureAwait(false);
return Results.Accepted(
value: new SarifUploadResponse
{
SarifId = result.SarifId,
Url = result.Url,
StatusUrl = $"/scans/{scanId}/github/upload-status/{result.SarifId}"
});
}
private static async Task<IResult> HandleGetUploadStatusAsync(
string scanId,
string sarifId,
IGitHubCodeScanningService gitHubService,
HttpContext context,
CancellationToken cancellationToken)
{
if (!ScanId.TryParse(scanId, out _))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest);
}
var status = await gitHubService.GetUploadStatusAsync(sarifId, cancellationToken)
.ConfigureAwait(false);
if (status is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"SARIF upload not found",
StatusCodes.Status404NotFound);
}
return Results.Ok(new SarifUploadStatusResponse
{
SarifId = sarifId,
ProcessingStatus = status.ProcessingStatus,
AnalysesUrl = status.AnalysesUrl,
Errors = status.Errors
});
}
private static async Task<IResult> HandleListAlertsAsync(
string scanId,
IGitHubCodeScanningService gitHubService,
HttpContext context,
string? state,
string? severity,
string? sort,
string? direction,
int? page,
int? perPage,
CancellationToken cancellationToken)
{
if (!ScanId.TryParse(scanId, out _))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest);
}
var alerts = await gitHubService.ListAlertsAsync(
state,
severity,
sort,
direction,
page ?? 1,
perPage ?? 30,
cancellationToken).ConfigureAwait(false);
return Results.Ok(new AlertsListResponse
{
Count = alerts.Count,
Alerts = alerts
});
}
private static async Task<IResult> HandleGetAlertAsync(
string scanId,
int alertNumber,
IGitHubCodeScanningService gitHubService,
HttpContext context,
CancellationToken cancellationToken)
{
if (!ScanId.TryParse(scanId, out _))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest);
}
var alert = await gitHubService.GetAlertAsync(alertNumber, cancellationToken)
.ConfigureAwait(false);
if (alert is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Alert not found",
StatusCodes.Status404NotFound);
}
return Results.Ok(new AlertResponse { Alert = alert });
}
}
#region Request/Response Models
/// <summary>
/// Request to upload SARIF to GitHub.
/// </summary>
public sealed record SarifUploadRequest
{
/// <summary>Repository owner.</summary>
public required string Owner { get; init; }
/// <summary>Repository name.</summary>
public required string Repo { get; init; }
/// <summary>Commit SHA (optional, uses scan's commit if not provided).</summary>
public string? CommitSha { get; init; }
/// <summary>Git ref (optional, uses scan's ref or defaults to main).</summary>
public string? Ref { get; init; }
}
/// <summary>
/// Response from SARIF upload.
/// </summary>
public sealed record SarifUploadResponse
{
/// <summary>The SARIF ID for tracking.</summary>
public required string SarifId { get; init; }
/// <summary>URL to the upload on GitHub.</summary>
public string? Url { get; init; }
/// <summary>URL to check upload status.</summary>
public string? StatusUrl { get; init; }
}
/// <summary>
/// Response for upload status check.
/// </summary>
public sealed record SarifUploadStatusResponse
{
/// <summary>The SARIF ID.</summary>
public required string SarifId { get; init; }
/// <summary>Processing status (pending, complete, failed).</summary>
public required string ProcessingStatus { get; init; }
/// <summary>URL to view analyses.</summary>
public string? AnalysesUrl { get; init; }
/// <summary>Any processing errors.</summary>
public IReadOnlyList<string>? Errors { get; init; }
}
/// <summary>
/// Response for alerts list.
/// </summary>
public sealed record AlertsListResponse
{
/// <summary>Number of alerts returned.</summary>
public int Count { get; init; }
/// <summary>The alerts.</summary>
public required IReadOnlyList<object> Alerts { get; init; }
}
/// <summary>
/// Response for single alert.
/// </summary>
public sealed record AlertResponse
{
/// <summary>The alert details.</summary>
public required object Alert { get; init; }
}
#endregion
#region Service Interface
/// <summary>
/// Service interface for GitHub Code Scanning operations.
/// Sprint: SPRINT_20260109_010_002 Task: API endpoints
/// </summary>
public interface IGitHubCodeScanningService
{
/// <summary>Upload SARIF to GitHub.</summary>
Task<GitHubUploadResult> UploadSarifAsync(
string owner,
string repo,
object sarifDocument,
string? commitSha,
string gitRef,
CancellationToken ct);
/// <summary>Get upload status.</summary>
Task<GitHubUploadStatus?> GetUploadStatusAsync(string sarifId, CancellationToken ct);
/// <summary>List alerts.</summary>
Task<IReadOnlyList<object>> ListAlertsAsync(
string? state,
string? severity,
string? sort,
string? direction,
int page,
int perPage,
CancellationToken ct);
/// <summary>Get a single alert.</summary>
Task<object?> GetAlertAsync(int alertNumber, CancellationToken ct);
}
/// <summary>
/// Result of uploading SARIF to GitHub.
/// </summary>
public sealed record GitHubUploadResult
{
/// <summary>The SARIF ID.</summary>
public required string SarifId { get; init; }
/// <summary>URL to the upload.</summary>
public string? Url { get; init; }
}
/// <summary>
/// Status of a SARIF upload.
/// </summary>
public sealed record GitHubUploadStatus
{
/// <summary>Processing status.</summary>
public required string ProcessingStatus { get; init; }
/// <summary>URL to analyses.</summary>
public string? AnalysesUrl { get; init; }
/// <summary>Processing errors.</summary>
public IReadOnlyList<string>? Errors { get; init; }
}
#endregion

View File

@@ -90,6 +90,7 @@ internal static class ScanEndpoints
scans.MapApprovalEndpoints();
scans.MapManifestEndpoints();
scans.MapLayerSbomEndpoints(); // Sprint: SPRINT_20260106_003_001
scans.MapGitHubCodeScanningEndpoints(); // Sprint: SPRINT_20260109_010_002
}
private static async Task<IResult> HandleSubmitAsync(