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(
|
||||
|
||||
@@ -130,9 +130,19 @@ builder.Services.AddSingleton<IScanCoordinator, InMemoryScanCoordinator>();
|
||||
builder.Services.AddSingleton<IReachabilityComputeService, NullReachabilityComputeService>();
|
||||
builder.Services.AddSingleton<IReachabilityQueryService, NullReachabilityQueryService>();
|
||||
builder.Services.AddSingleton<IReachabilityExplainService, NullReachabilityExplainService>();
|
||||
builder.Services.AddSingleton<ISarifExportService, NullSarifExportService>();
|
||||
|
||||
// SARIF export services (Sprint: SPRINT_20260109_010_001)
|
||||
builder.Services.AddSingleton<StellaOps.Scanner.Sarif.Rules.ISarifRuleRegistry, StellaOps.Scanner.Sarif.Rules.SarifRuleRegistry>();
|
||||
builder.Services.AddSingleton<StellaOps.Scanner.Sarif.Fingerprints.IFingerprintGenerator, StellaOps.Scanner.Sarif.Fingerprints.FingerprintGenerator>();
|
||||
builder.Services.AddSingleton<StellaOps.Scanner.Sarif.ISarifExportService, StellaOps.Scanner.Sarif.SarifExportService>();
|
||||
builder.Services.AddSingleton<ISarifExportService, ScanFindingsSarifExportService>();
|
||||
|
||||
builder.Services.AddSingleton<ICycloneDxExportService, NullCycloneDxExportService>();
|
||||
builder.Services.AddSingleton<IOpenVexExportService, NullOpenVexExportService>();
|
||||
|
||||
// GitHub Code Scanning integration (Sprint: SPRINT_20260109_010_002)
|
||||
builder.Services.AddSingleton<IGitHubCodeScanningService, NullGitHubCodeScanningService>();
|
||||
|
||||
builder.Services.AddSingleton<IEvidenceCompositionService, EvidenceCompositionService>();
|
||||
builder.Services.AddSingleton<IPolicyDecisionAttestationService, PolicyDecisionAttestationService>();
|
||||
builder.Services.AddSingleton<IRichGraphAttestationService, RichGraphAttestationService>();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// <copyright file="NullGitHubCodeScanningService.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Null implementation of IGitHubCodeScanningService.
|
||||
/// Returns empty results and logged warnings for unconfigured GitHub integration.
|
||||
/// Sprint: SPRINT_20260109_010_002 Task: API endpoints
|
||||
/// </summary>
|
||||
internal sealed class NullGitHubCodeScanningService : IGitHubCodeScanningService
|
||||
{
|
||||
public Task<GitHubUploadResult> UploadSarifAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
object sarifDocument,
|
||||
string? commitSha,
|
||||
string gitRef,
|
||||
CancellationToken ct)
|
||||
{
|
||||
// Return a mock result for development/testing
|
||||
return Task.FromResult(new GitHubUploadResult
|
||||
{
|
||||
SarifId = $"mock-sarif-{Guid.NewGuid():N}",
|
||||
Url = null
|
||||
});
|
||||
}
|
||||
|
||||
public Task<GitHubUploadStatus?> GetUploadStatusAsync(string sarifId, CancellationToken ct)
|
||||
{
|
||||
if (!sarifId.StartsWith("mock-sarif-", StringComparison.Ordinal))
|
||||
{
|
||||
return Task.FromResult<GitHubUploadStatus?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<GitHubUploadStatus?>(new GitHubUploadStatus
|
||||
{
|
||||
ProcessingStatus = "complete",
|
||||
AnalysesUrl = null,
|
||||
Errors = null
|
||||
});
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<object>> ListAlertsAsync(
|
||||
string? state,
|
||||
string? severity,
|
||||
string? sort,
|
||||
string? direction,
|
||||
int page,
|
||||
int perPage,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<object>>(Array.Empty<object>());
|
||||
}
|
||||
|
||||
public Task<object?> GetAlertAsync(int alertNumber, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult<object?>(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// <copyright file="ScanFindingsSarifExportService.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Sarif;
|
||||
using StellaOps.Scanner.Sarif.Models;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// SARIF export service that bridges WebService findings to the Scanner.Sarif library.
|
||||
/// Sprint: SPRINT_20260109_010_001 Task: Implement API endpoint
|
||||
/// </summary>
|
||||
public sealed class ScanFindingsSarifExportService : ISarifExportService
|
||||
{
|
||||
private readonly IReachabilityQueryService _reachabilityService;
|
||||
private readonly Sarif.ISarifExportService _sarifExporter;
|
||||
private readonly ILogger<ScanFindingsSarifExportService> _logger;
|
||||
|
||||
public ScanFindingsSarifExportService(
|
||||
IReachabilityQueryService reachabilityService,
|
||||
Sarif.ISarifExportService sarifExporter,
|
||||
ILogger<ScanFindingsSarifExportService> logger)
|
||||
{
|
||||
_reachabilityService = reachabilityService;
|
||||
_sarifExporter = sarifExporter;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<object?> ExportAsync(ScanId scanId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug("Exporting findings as SARIF for scan {ScanId}", scanId);
|
||||
|
||||
// Get all findings for the scan
|
||||
var findings = await _reachabilityService.GetFindingsAsync(
|
||||
scanId,
|
||||
cveFilter: null,
|
||||
statusFilter: null,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (findings is null || findings.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("No findings to export for scan {ScanId}", scanId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to FindingInput
|
||||
var inputs = MapToFindingInputs(findings, scanId);
|
||||
|
||||
// Export via the Sarif library
|
||||
var options = new SarifExportOptions
|
||||
{
|
||||
ToolName = "StellaOps Scanner",
|
||||
ToolVersion = GetToolVersion(),
|
||||
IncludeEvidenceUris = true,
|
||||
IncludeReachability = true,
|
||||
IncludeVexStatus = true,
|
||||
FingerprintStrategy = FingerprintStrategy.Standard
|
||||
};
|
||||
|
||||
var sarifLog = await _sarifExporter.ExportAsync(inputs, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Exported {Count} findings as SARIF for scan {ScanId}",
|
||||
findings.Count,
|
||||
scanId);
|
||||
|
||||
return sarifLog;
|
||||
}
|
||||
|
||||
private static IEnumerable<FindingInput> MapToFindingInputs(
|
||||
IReadOnlyList<ReachabilityFinding> findings,
|
||||
ScanId scanId)
|
||||
{
|
||||
foreach (var finding in findings)
|
||||
{
|
||||
yield return new FindingInput
|
||||
{
|
||||
Type = FindingType.Vulnerability,
|
||||
VulnerabilityId = finding.CveId,
|
||||
ComponentPurl = finding.Purl,
|
||||
ComponentName = ExtractComponentName(finding.Purl),
|
||||
ComponentVersion = ExtractComponentVersion(finding.Purl),
|
||||
Severity = ParseSeverity(finding.Severity),
|
||||
Title = $"Vulnerability {finding.CveId} in {finding.Purl}",
|
||||
Description = finding.AffectedVersions is not null
|
||||
? $"Affected versions: {finding.AffectedVersions}"
|
||||
: null,
|
||||
Reachability = ParseReachabilityStatus(finding.Status),
|
||||
EvidenceUris = BuildEvidenceUris(finding, scanId),
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
["latticeState"] = finding.LatticeState ?? "unknown",
|
||||
["confidence"] = finding.Confidence
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ExtractComponentName(string purl)
|
||||
{
|
||||
// pkg:npm/lodash@4.17.21 -> lodash
|
||||
if (string.IsNullOrEmpty(purl)) return null;
|
||||
|
||||
var atIndex = purl.LastIndexOf('@');
|
||||
var slashIndex = purl.LastIndexOf('/');
|
||||
|
||||
if (slashIndex < 0) return null;
|
||||
|
||||
var endIndex = atIndex > slashIndex ? atIndex : purl.Length;
|
||||
return purl.Substring(slashIndex + 1, endIndex - slashIndex - 1);
|
||||
}
|
||||
|
||||
private static string? ExtractComponentVersion(string purl)
|
||||
{
|
||||
// pkg:npm/lodash@4.17.21 -> 4.17.21
|
||||
if (string.IsNullOrEmpty(purl)) return null;
|
||||
|
||||
var atIndex = purl.LastIndexOf('@');
|
||||
if (atIndex < 0) return null;
|
||||
|
||||
return purl.Substring(atIndex + 1);
|
||||
}
|
||||
|
||||
private static Severity ParseSeverity(string? severity)
|
||||
{
|
||||
if (string.IsNullOrEmpty(severity)) return Severity.Unknown;
|
||||
|
||||
return severity.ToUpperInvariant() switch
|
||||
{
|
||||
"CRITICAL" => Severity.Critical,
|
||||
"HIGH" => Severity.High,
|
||||
"MEDIUM" => Severity.Medium,
|
||||
"LOW" => Severity.Low,
|
||||
_ => Severity.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static ReachabilityStatus ParseReachabilityStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrEmpty(status)) return ReachabilityStatus.Unknown;
|
||||
|
||||
return status.ToUpperInvariant() switch
|
||||
{
|
||||
"REACHABLE" or "STATIC_REACHABLE" => ReachabilityStatus.StaticReachable,
|
||||
"UNREACHABLE" or "STATIC_UNREACHABLE" => ReachabilityStatus.StaticUnreachable,
|
||||
"RUNTIME_REACHABLE" => ReachabilityStatus.RuntimeReachable,
|
||||
"RUNTIME_UNREACHABLE" => ReachabilityStatus.RuntimeUnreachable,
|
||||
"CONTESTED" or "POTENTIALLY_REACHABLE" or "INCONCLUSIVE" => ReachabilityStatus.Contested,
|
||||
_ => ReachabilityStatus.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildEvidenceUris(ReachabilityFinding finding, ScanId scanId)
|
||||
{
|
||||
var uris = new List<string>();
|
||||
|
||||
// Add standard evidence URIs
|
||||
if (!string.IsNullOrEmpty(finding.CveId))
|
||||
{
|
||||
uris.Add($"stella://vuln/{finding.CveId}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(finding.Purl))
|
||||
{
|
||||
uris.Add($"stella://component/{Uri.EscapeDataString(finding.Purl)}");
|
||||
}
|
||||
|
||||
uris.Add($"stella://scan/{scanId.Value}");
|
||||
|
||||
return uris;
|
||||
}
|
||||
|
||||
private static string GetToolVersion()
|
||||
{
|
||||
// Get version from assembly
|
||||
var assembly = typeof(ScanFindingsSarifExportService).Assembly;
|
||||
var version = assembly.GetName().Version;
|
||||
return version?.ToString(3) ?? "1.0.0";
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Orchestration/StellaOps.Scanner.Orchestration.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Sources/StellaOps.Scanner.Sources.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Sarif/StellaOps.Scanner.Sarif.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Validation/StellaOps.Scanner.Validation.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user