807 lines
25 KiB
C#
807 lines
25 KiB
C#
// <copyright file="GitHubCommandGroup.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
|
// </copyright>
|
|
|
|
using System.CommandLine;
|
|
using System.Globalization;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Spectre.Console;
|
|
using StellaOps.Integrations.Plugin.GitHubApp.CodeScanning;
|
|
|
|
namespace StellaOps.Cli.Commands;
|
|
|
|
/// <summary>
|
|
/// GitHub integration commands including Code Scanning.
|
|
/// Sprint: SPRINT_20260109_010_002
|
|
/// </summary>
|
|
public static class GitHubCommandGroup
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
public static Command BuildGitHubCommand(
|
|
IServiceProvider services,
|
|
Option<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var github = new Command("github", "GitHub integration commands.");
|
|
|
|
github.Add(BuildUploadSarifCommand(services, verboseOption, cancellationToken));
|
|
github.Add(BuildListAlertsCommand(services, verboseOption, cancellationToken));
|
|
github.Add(BuildGetAlertCommand(services, verboseOption, cancellationToken));
|
|
github.Add(BuildUpdateAlertCommand(services, verboseOption, cancellationToken));
|
|
github.Add(BuildUploadStatusCommand(services, verboseOption, cancellationToken));
|
|
|
|
return github;
|
|
}
|
|
|
|
private static Command BuildUploadSarifCommand(
|
|
IServiceProvider services,
|
|
Option<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var sarifFileArg = new Argument<string>("sarif-file")
|
|
{
|
|
Description = "Path to SARIF file to upload."
|
|
};
|
|
|
|
var repoOption = new Option<string>("--repo", new[] { "-r" })
|
|
{
|
|
Description = "Repository in owner/repo format",
|
|
Required = true
|
|
};
|
|
|
|
var refOption = new Option<string?>("--ref")
|
|
{
|
|
Description = "Git ref (e.g., refs/heads/main). Defaults to current branch."
|
|
};
|
|
|
|
var shaOption = new Option<string?>("--sha")
|
|
{
|
|
Description = "Commit SHA. Defaults to current HEAD."
|
|
};
|
|
|
|
var waitOption = new Option<bool>("--wait", new[] { "-w" })
|
|
{
|
|
Description = "Wait for processing to complete"
|
|
};
|
|
|
|
var timeoutOption = new Option<int>("--timeout", new[] { "-t" })
|
|
{
|
|
Description = "Wait timeout in seconds (default: 300)"
|
|
};
|
|
timeoutOption.SetDefaultValue(300);
|
|
|
|
var toolNameOption = new Option<string?>("--tool-name")
|
|
{
|
|
Description = "Tool name for GitHub categorization"
|
|
};
|
|
|
|
var githubUrlOption = new Option<string?>("--github-url")
|
|
{
|
|
Description = "GitHub API URL (for GitHub Enterprise Server)"
|
|
};
|
|
|
|
var cmd = new Command("upload-sarif", "Upload SARIF to GitHub Code Scanning.")
|
|
{
|
|
sarifFileArg,
|
|
repoOption,
|
|
refOption,
|
|
shaOption,
|
|
waitOption,
|
|
timeoutOption,
|
|
toolNameOption,
|
|
githubUrlOption,
|
|
verboseOption
|
|
};
|
|
|
|
cmd.SetAction(async (parseResult, _) =>
|
|
{
|
|
var sarifFile = parseResult.GetValue(sarifFileArg)!;
|
|
var repo = parseResult.GetValue(repoOption)!;
|
|
var gitRef = parseResult.GetValue(refOption);
|
|
var sha = parseResult.GetValue(shaOption);
|
|
var wait = parseResult.GetValue(waitOption);
|
|
var timeout = parseResult.GetValue(timeoutOption);
|
|
var toolName = parseResult.GetValue(toolNameOption);
|
|
var githubUrl = parseResult.GetValue(githubUrlOption);
|
|
var verbose = parseResult.GetValue(verboseOption);
|
|
|
|
try
|
|
{
|
|
await UploadSarifAsync(
|
|
services,
|
|
sarifFile,
|
|
repo,
|
|
gitRef,
|
|
sha,
|
|
wait,
|
|
TimeSpan.FromSeconds(timeout),
|
|
toolName,
|
|
githubUrl,
|
|
verbose,
|
|
cancellationToken);
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
|
|
return 1;
|
|
}
|
|
});
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private static async Task UploadSarifAsync(
|
|
IServiceProvider services,
|
|
string sarifFilePath,
|
|
string repo,
|
|
string? gitRef,
|
|
string? sha,
|
|
bool wait,
|
|
TimeSpan timeout,
|
|
string? toolName,
|
|
string? githubUrl,
|
|
bool verbose,
|
|
CancellationToken ct)
|
|
{
|
|
// Parse owner/repo
|
|
var parts = repo.Split('/');
|
|
if (parts.Length != 2)
|
|
{
|
|
throw new ArgumentException("Repository must be in owner/repo format");
|
|
}
|
|
|
|
var owner = parts[0];
|
|
var repoName = parts[1];
|
|
|
|
// Validate SARIF file
|
|
if (!File.Exists(sarifFilePath))
|
|
{
|
|
throw new FileNotFoundException($"SARIF file not found: {sarifFilePath}");
|
|
}
|
|
|
|
// Read SARIF content
|
|
var sarifContent = await File.ReadAllTextAsync(sarifFilePath, ct);
|
|
|
|
// Get git info if not provided
|
|
var commitSha = sha ?? await GetGitShaAsync(ct);
|
|
var refValue = gitRef ?? await GetGitRefAsync(ct);
|
|
|
|
AnsiConsole.MarkupLine($"[blue]Uploading SARIF to[/] [yellow]{owner}/{repoName}[/]");
|
|
AnsiConsole.MarkupLine($" Commit: [dim]{commitSha}[/]");
|
|
AnsiConsole.MarkupLine($" Ref: [dim]{refValue}[/]");
|
|
|
|
// Get client
|
|
var client = GetCodeScanningClient(services, githubUrl);
|
|
|
|
// Build request
|
|
var request = new SarifUploadRequest
|
|
{
|
|
CommitSha = commitSha,
|
|
Ref = refValue,
|
|
SarifContent = sarifContent,
|
|
ToolName = toolName ?? "StellaOps Scanner"
|
|
};
|
|
|
|
// Upload
|
|
var result = await client.UploadSarifAsync(owner, repoName, request, ct);
|
|
|
|
AnsiConsole.MarkupLine($"[green]Uploaded successfully![/]");
|
|
AnsiConsole.MarkupLine($" SARIF ID: [cyan]{result.Id}[/]");
|
|
AnsiConsole.MarkupLine($" Status URL: [dim]{result.Url}[/]");
|
|
|
|
// Wait if requested
|
|
if (wait)
|
|
{
|
|
AnsiConsole.MarkupLine("[blue]Waiting for processing...[/]");
|
|
|
|
var status = await AnsiConsole.Status()
|
|
.Spinner(Spinner.Known.Dots)
|
|
.StartAsync("Processing...", async ctx =>
|
|
{
|
|
return await client.WaitForProcessingAsync(
|
|
owner, repoName, result.Id, timeout, ct);
|
|
});
|
|
|
|
if (status.Status == ProcessingStatus.Complete)
|
|
{
|
|
AnsiConsole.MarkupLine($"[green]Processing complete![/]");
|
|
if (!string.IsNullOrEmpty(status.AnalysisUrl))
|
|
{
|
|
AnsiConsole.MarkupLine($" Analysis: [link]{status.AnalysisUrl}[/]");
|
|
}
|
|
}
|
|
else if (status.Status == ProcessingStatus.Failed)
|
|
{
|
|
AnsiConsole.MarkupLine($"[red]Processing failed![/]");
|
|
foreach (var error in status.Errors)
|
|
{
|
|
AnsiConsole.MarkupLine($" [red]- {error}[/]");
|
|
}
|
|
throw new InvalidOperationException("SARIF processing failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Command BuildListAlertsCommand(
|
|
IServiceProvider services,
|
|
Option<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var repoOption = new Option<string>("--repo", new[] { "-r" })
|
|
{
|
|
Description = "Repository in owner/repo format",
|
|
Required = true
|
|
};
|
|
|
|
var stateOption = new Option<string?>("--state", new[] { "-s" })
|
|
{
|
|
Description = "Filter by state: open, closed, dismissed, fixed"
|
|
};
|
|
|
|
var severityOption = new Option<string?>("--severity")
|
|
{
|
|
Description = "Filter by severity: critical, high, medium, low"
|
|
};
|
|
|
|
var toolOption = new Option<string?>("--tool")
|
|
{
|
|
Description = "Filter by tool name"
|
|
};
|
|
|
|
var refOption = new Option<string?>("--ref")
|
|
{
|
|
Description = "Filter by git ref"
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON"
|
|
};
|
|
|
|
var githubUrlOption = new Option<string?>("--github-url")
|
|
{
|
|
Description = "GitHub API URL (for GitHub Enterprise Server)"
|
|
};
|
|
|
|
var cmd = new Command("list-alerts", "List code scanning alerts for a repository.")
|
|
{
|
|
repoOption,
|
|
stateOption,
|
|
severityOption,
|
|
toolOption,
|
|
refOption,
|
|
jsonOption,
|
|
githubUrlOption,
|
|
verboseOption
|
|
};
|
|
|
|
cmd.SetAction(async (parseResult, _) =>
|
|
{
|
|
var repo = parseResult.GetValue(repoOption)!;
|
|
var state = parseResult.GetValue(stateOption);
|
|
var severity = parseResult.GetValue(severityOption);
|
|
var tool = parseResult.GetValue(toolOption);
|
|
var gitRef = parseResult.GetValue(refOption);
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var githubUrl = parseResult.GetValue(githubUrlOption);
|
|
|
|
try
|
|
{
|
|
await ListAlertsAsync(
|
|
services, repo, state, severity, tool, gitRef, json, githubUrl, cancellationToken);
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
|
|
return 1;
|
|
}
|
|
});
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private static async Task ListAlertsAsync(
|
|
IServiceProvider services,
|
|
string repo,
|
|
string? state,
|
|
string? severity,
|
|
string? tool,
|
|
string? gitRef,
|
|
bool json,
|
|
string? githubUrl,
|
|
CancellationToken ct)
|
|
{
|
|
var parts = repo.Split('/');
|
|
if (parts.Length != 2)
|
|
{
|
|
throw new ArgumentException("Repository must be in owner/repo format");
|
|
}
|
|
|
|
var owner = parts[0];
|
|
var repoName = parts[1];
|
|
|
|
var client = GetCodeScanningClient(services, githubUrl);
|
|
|
|
var filter = new AlertFilter
|
|
{
|
|
State = state,
|
|
Severity = severity,
|
|
Tool = tool,
|
|
Ref = gitRef
|
|
};
|
|
|
|
var alerts = await client.ListAlertsAsync(owner, repoName, filter, ct);
|
|
|
|
if (json)
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(alerts, JsonOptions));
|
|
return;
|
|
}
|
|
|
|
if (alerts.Count == 0)
|
|
{
|
|
AnsiConsole.MarkupLine("[dim]No alerts found.[/]");
|
|
return;
|
|
}
|
|
|
|
var table = new Table();
|
|
table.AddColumn("#");
|
|
table.AddColumn("State");
|
|
table.AddColumn("Severity");
|
|
table.AddColumn("Rule");
|
|
table.AddColumn("Tool");
|
|
table.AddColumn("Created");
|
|
|
|
foreach (var alert in alerts)
|
|
{
|
|
var stateColor = alert.State switch
|
|
{
|
|
"open" => "red",
|
|
"dismissed" => "yellow",
|
|
"fixed" => "green",
|
|
_ => "dim"
|
|
};
|
|
|
|
var severityColor = alert.RuleSeverity switch
|
|
{
|
|
"critical" or "error" => "red",
|
|
"high" => "yellow",
|
|
"medium" or "warning" => "blue",
|
|
_ => "dim"
|
|
};
|
|
|
|
table.AddRow(
|
|
alert.Number.ToString(CultureInfo.InvariantCulture),
|
|
$"[{stateColor}]{alert.State}[/]",
|
|
$"[{severityColor}]{alert.RuleSeverity}[/]",
|
|
alert.RuleId,
|
|
alert.Tool,
|
|
alert.CreatedAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
|
|
}
|
|
|
|
AnsiConsole.Write(table);
|
|
AnsiConsole.MarkupLine($"[dim]Total: {alerts.Count} alerts[/]");
|
|
}
|
|
|
|
private static Command BuildGetAlertCommand(
|
|
IServiceProvider services,
|
|
Option<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var alertNumberArg = new Argument<int>("alert-number")
|
|
{
|
|
Description = "Alert number to retrieve."
|
|
};
|
|
|
|
var repoOption = new Option<string>("--repo", new[] { "-r" })
|
|
{
|
|
Description = "Repository in owner/repo format",
|
|
Required = true
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON"
|
|
};
|
|
|
|
var githubUrlOption = new Option<string?>("--github-url")
|
|
{
|
|
Description = "GitHub API URL (for GitHub Enterprise Server)"
|
|
};
|
|
|
|
var cmd = new Command("get-alert", "Get details for a specific code scanning alert.")
|
|
{
|
|
alertNumberArg,
|
|
repoOption,
|
|
jsonOption,
|
|
githubUrlOption,
|
|
verboseOption
|
|
};
|
|
|
|
cmd.SetAction(async (parseResult, _) =>
|
|
{
|
|
var alertNumber = parseResult.GetValue(alertNumberArg);
|
|
var repo = parseResult.GetValue(repoOption)!;
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var githubUrl = parseResult.GetValue(githubUrlOption);
|
|
|
|
try
|
|
{
|
|
await GetAlertAsync(services, repo, alertNumber, json, githubUrl, cancellationToken);
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
|
|
return 1;
|
|
}
|
|
});
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private static async Task GetAlertAsync(
|
|
IServiceProvider services,
|
|
string repo,
|
|
int alertNumber,
|
|
bool json,
|
|
string? githubUrl,
|
|
CancellationToken ct)
|
|
{
|
|
var parts = repo.Split('/');
|
|
if (parts.Length != 2)
|
|
{
|
|
throw new ArgumentException("Repository must be in owner/repo format");
|
|
}
|
|
|
|
var owner = parts[0];
|
|
var repoName = parts[1];
|
|
|
|
var client = GetCodeScanningClient(services, githubUrl);
|
|
var alert = await client.GetAlertAsync(owner, repoName, alertNumber, ct);
|
|
|
|
if (json)
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(alert, JsonOptions));
|
|
return;
|
|
}
|
|
|
|
AnsiConsole.MarkupLine($"[bold]Alert #{alert.Number}[/]");
|
|
AnsiConsole.MarkupLine($" State: {alert.State}");
|
|
AnsiConsole.MarkupLine($" Rule: {alert.RuleId}");
|
|
AnsiConsole.MarkupLine($" Severity: {alert.RuleSeverity}");
|
|
AnsiConsole.MarkupLine($" Description: {alert.RuleDescription}");
|
|
AnsiConsole.MarkupLine($" Tool: {alert.Tool}");
|
|
AnsiConsole.MarkupLine($" Created: {alert.CreatedAt:yyyy-MM-dd HH:mm:ss}");
|
|
|
|
if (alert.DismissedAt.HasValue)
|
|
{
|
|
AnsiConsole.MarkupLine($" Dismissed: {alert.DismissedAt.Value:yyyy-MM-dd HH:mm:ss}");
|
|
AnsiConsole.MarkupLine($" Dismiss reason: {alert.DismissedReason}");
|
|
}
|
|
|
|
if (alert.MostRecentInstance != null)
|
|
{
|
|
AnsiConsole.MarkupLine($" Location: {alert.MostRecentInstance.Location?.Path}:{alert.MostRecentInstance.Location?.StartLine}");
|
|
}
|
|
|
|
AnsiConsole.MarkupLine($" URL: [link]{alert.HtmlUrl}[/]");
|
|
}
|
|
|
|
private static Command BuildUpdateAlertCommand(
|
|
IServiceProvider services,
|
|
Option<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var alertNumberArg = new Argument<int>("alert-number")
|
|
{
|
|
Description = "Alert number to update."
|
|
};
|
|
|
|
var repoOption = new Option<string>("--repo", new[] { "-r" })
|
|
{
|
|
Description = "Repository in owner/repo format",
|
|
Required = true
|
|
};
|
|
|
|
var stateOption = new Option<string>("--state", new[] { "-s" })
|
|
{
|
|
Description = "New state: dismissed, open",
|
|
Required = true
|
|
};
|
|
stateOption.FromAmong("dismissed", "open");
|
|
|
|
var reasonOption = new Option<string?>("--reason")
|
|
{
|
|
Description = "Dismiss reason: false_positive, wont_fix, used_in_tests"
|
|
};
|
|
|
|
var commentOption = new Option<string?>("--comment")
|
|
{
|
|
Description = "Dismiss comment"
|
|
};
|
|
|
|
var githubUrlOption = new Option<string?>("--github-url")
|
|
{
|
|
Description = "GitHub API URL (for GitHub Enterprise Server)"
|
|
};
|
|
|
|
var cmd = new Command("update-alert", "Update a code scanning alert state.")
|
|
{
|
|
alertNumberArg,
|
|
repoOption,
|
|
stateOption,
|
|
reasonOption,
|
|
commentOption,
|
|
githubUrlOption,
|
|
verboseOption
|
|
};
|
|
|
|
cmd.SetAction(async (parseResult, _) =>
|
|
{
|
|
var alertNumber = parseResult.GetValue(alertNumberArg);
|
|
var repo = parseResult.GetValue(repoOption)!;
|
|
var state = parseResult.GetValue(stateOption)!;
|
|
var reason = parseResult.GetValue(reasonOption);
|
|
var comment = parseResult.GetValue(commentOption);
|
|
var githubUrl = parseResult.GetValue(githubUrlOption);
|
|
|
|
try
|
|
{
|
|
await UpdateAlertAsync(
|
|
services, repo, alertNumber, state, reason, comment, githubUrl, cancellationToken);
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
|
|
return 1;
|
|
}
|
|
});
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private static async Task UpdateAlertAsync(
|
|
IServiceProvider services,
|
|
string repo,
|
|
int alertNumber,
|
|
string state,
|
|
string? reason,
|
|
string? comment,
|
|
string? githubUrl,
|
|
CancellationToken ct)
|
|
{
|
|
var parts = repo.Split('/');
|
|
if (parts.Length != 2)
|
|
{
|
|
throw new ArgumentException("Repository must be in owner/repo format");
|
|
}
|
|
|
|
var owner = parts[0];
|
|
var repoName = parts[1];
|
|
|
|
if (state == "dismissed" && string.IsNullOrEmpty(reason))
|
|
{
|
|
throw new ArgumentException("Dismiss reason is required when dismissing an alert");
|
|
}
|
|
|
|
var client = GetCodeScanningClient(services, githubUrl);
|
|
|
|
var update = new AlertUpdate
|
|
{
|
|
State = state,
|
|
DismissedReason = reason,
|
|
DismissedComment = comment
|
|
};
|
|
|
|
var alert = await client.UpdateAlertAsync(owner, repoName, alertNumber, update, ct);
|
|
|
|
AnsiConsole.MarkupLine($"[green]Alert #{alert.Number} updated to state: {alert.State}[/]");
|
|
}
|
|
|
|
private static Command BuildUploadStatusCommand(
|
|
IServiceProvider services,
|
|
Option<bool> verboseOption,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var sarifIdArg = new Argument<string>("sarif-id")
|
|
{
|
|
Description = "SARIF upload ID to check."
|
|
};
|
|
|
|
var repoOption = new Option<string>("--repo", new[] { "-r" })
|
|
{
|
|
Description = "Repository in owner/repo format",
|
|
Required = true
|
|
};
|
|
|
|
var jsonOption = new Option<bool>("--json")
|
|
{
|
|
Description = "Output as JSON"
|
|
};
|
|
|
|
var githubUrlOption = new Option<string?>("--github-url")
|
|
{
|
|
Description = "GitHub API URL (for GitHub Enterprise Server)"
|
|
};
|
|
|
|
var cmd = new Command("upload-status", "Check SARIF upload processing status.")
|
|
{
|
|
sarifIdArg,
|
|
repoOption,
|
|
jsonOption,
|
|
githubUrlOption,
|
|
verboseOption
|
|
};
|
|
|
|
cmd.SetAction(async (parseResult, _) =>
|
|
{
|
|
var sarifId = parseResult.GetValue(sarifIdArg)!;
|
|
var repo = parseResult.GetValue(repoOption)!;
|
|
var json = parseResult.GetValue(jsonOption);
|
|
var githubUrl = parseResult.GetValue(githubUrlOption);
|
|
|
|
try
|
|
{
|
|
await GetUploadStatusAsync(services, repo, sarifId, json, githubUrl, cancellationToken);
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
|
|
return 1;
|
|
}
|
|
});
|
|
|
|
return cmd;
|
|
}
|
|
|
|
private static async Task GetUploadStatusAsync(
|
|
IServiceProvider services,
|
|
string repo,
|
|
string sarifId,
|
|
bool json,
|
|
string? githubUrl,
|
|
CancellationToken ct)
|
|
{
|
|
var parts = repo.Split('/');
|
|
if (parts.Length != 2)
|
|
{
|
|
throw new ArgumentException("Repository must be in owner/repo format");
|
|
}
|
|
|
|
var owner = parts[0];
|
|
var repoName = parts[1];
|
|
|
|
var client = GetCodeScanningClient(services, githubUrl);
|
|
var status = await client.GetUploadStatusAsync(owner, repoName, sarifId, ct);
|
|
|
|
if (json)
|
|
{
|
|
Console.WriteLine(JsonSerializer.Serialize(status, JsonOptions));
|
|
return;
|
|
}
|
|
|
|
var statusColor = status.Status switch
|
|
{
|
|
ProcessingStatus.Complete => "green",
|
|
ProcessingStatus.Failed => "red",
|
|
_ => "yellow"
|
|
};
|
|
|
|
AnsiConsole.MarkupLine($"[bold]SARIF Upload Status[/]");
|
|
AnsiConsole.MarkupLine($" Status: [{statusColor}]{status.Status}[/]");
|
|
|
|
if (status.ProcessingStartedAt.HasValue)
|
|
{
|
|
AnsiConsole.MarkupLine($" Started: {status.ProcessingStartedAt.Value:yyyy-MM-dd HH:mm:ss}");
|
|
}
|
|
|
|
if (status.ProcessingCompletedAt.HasValue)
|
|
{
|
|
AnsiConsole.MarkupLine($" Completed: {status.ProcessingCompletedAt.Value:yyyy-MM-dd HH:mm:ss}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(status.AnalysisUrl))
|
|
{
|
|
AnsiConsole.MarkupLine($" Analysis: [link]{status.AnalysisUrl}[/]");
|
|
}
|
|
|
|
if (status.Errors.Length > 0)
|
|
{
|
|
AnsiConsole.MarkupLine("[red]Errors:[/]");
|
|
foreach (var error in status.Errors)
|
|
{
|
|
AnsiConsole.MarkupLine($" - {error}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IGitHubCodeScanningClient GetCodeScanningClient(
|
|
IServiceProvider services,
|
|
string? githubUrl)
|
|
{
|
|
// Try to get from DI first
|
|
var client = services.GetService<IGitHubCodeScanningClient>();
|
|
if (client != null)
|
|
{
|
|
return client;
|
|
}
|
|
|
|
// Fallback: create manually (this would use environment token)
|
|
throw new InvalidOperationException(
|
|
"GitHub Code Scanning client not configured. " +
|
|
"Please ensure GITHUB_TOKEN environment variable is set.");
|
|
}
|
|
|
|
private static async Task<string> GetGitShaAsync(CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "git",
|
|
Arguments = "rev-parse HEAD",
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
});
|
|
|
|
if (process == null)
|
|
{
|
|
throw new InvalidOperationException("Failed to start git process");
|
|
}
|
|
|
|
var sha = await process.StandardOutput.ReadToEndAsync(ct);
|
|
await process.WaitForExitAsync(ct);
|
|
|
|
return sha.Trim();
|
|
}
|
|
catch
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Could not determine commit SHA. Please provide --sha option.");
|
|
}
|
|
}
|
|
|
|
private static async Task<string> GetGitRefAsync(CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "git",
|
|
Arguments = "symbolic-ref HEAD",
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
});
|
|
|
|
if (process == null)
|
|
{
|
|
throw new InvalidOperationException("Failed to start git process");
|
|
}
|
|
|
|
var refVal = await process.StandardOutput.ReadToEndAsync(ct);
|
|
await process.WaitForExitAsync(ct);
|
|
|
|
return refVal.Trim();
|
|
}
|
|
catch
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Could not determine git ref. Please provide --ref option.");
|
|
}
|
|
}
|
|
}
|