sprints work
This commit is contained in:
@@ -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
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user