sprints and audit work
This commit is contained in:
@@ -191,7 +191,25 @@ public sealed class LanguageAnalyzerSmokeRunner
|
||||
|
||||
ValidateManifest(manifest, profile, options.PluginDirectoryName);
|
||||
|
||||
var pluginAssemblyPath = Path.Combine(pluginRoot, manifest.EntryPoint.Assembly);
|
||||
// Validate assembly path to prevent path traversal attacks
|
||||
var assemblyName = manifest.EntryPoint.Assembly;
|
||||
if (string.IsNullOrWhiteSpace(assemblyName) ||
|
||||
Path.IsPathRooted(assemblyName) ||
|
||||
assemblyName.Contains("..") ||
|
||||
assemblyName.Contains('\0'))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid assembly path in manifest: path traversal or absolute path detected in '{assemblyName}'.");
|
||||
}
|
||||
|
||||
var pluginAssemblyPath = Path.GetFullPath(Path.Combine(pluginRoot, assemblyName));
|
||||
var normalizedPluginRoot = Path.GetFullPath(pluginRoot);
|
||||
if (!pluginAssemblyPath.StartsWith(normalizedPluginRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Invalid assembly path in manifest: '{assemblyName}' escapes plugin root directory.");
|
||||
}
|
||||
|
||||
if (!File.Exists(pluginAssemblyPath))
|
||||
{
|
||||
throw new FileNotFoundException($"Plug-in assembly '{manifest.EntryPoint.Assembly}' not found under '{pluginRoot}'.", pluginAssemblyPath);
|
||||
|
||||
@@ -4,14 +4,29 @@ public static class NotifySmokeCheckApp
|
||||
{
|
||||
public static async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
// Handle Ctrl+C for graceful cancellation
|
||||
Console.CancelKeyPress += (_, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
cts.Cancel();
|
||||
Console.Error.WriteLine("[INFO] Cancellation requested...");
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var options = NotifySmokeOptions.FromEnvironment(Environment.GetEnvironmentVariable);
|
||||
var runner = new NotifySmokeCheckRunner(options, Console.WriteLine, Console.Error.WriteLine);
|
||||
await runner.RunAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
await runner.RunAsync(cts.Token).ConfigureAwait(false);
|
||||
Console.WriteLine("[OK] Notify smoke validation completed successfully.");
|
||||
return 0;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.Error.WriteLine("[CANCELLED] Operation was cancelled.");
|
||||
return 130; // Standard exit code for SIGINT
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[FAIL] {ex.Message}");
|
||||
|
||||
@@ -158,12 +158,18 @@ public sealed record NotifyDeliveryRecord(string Kind, string? Status);
|
||||
public sealed class NotifySmokeCheckRunner
|
||||
{
|
||||
private readonly NotifySmokeOptions _options;
|
||||
private readonly HttpClient? _httpClient;
|
||||
private readonly Action<string> _info;
|
||||
private readonly Action<string> _error;
|
||||
|
||||
public NotifySmokeCheckRunner(NotifySmokeOptions options, Action<string>? info = null, Action<string>? error = null)
|
||||
public NotifySmokeCheckRunner(
|
||||
NotifySmokeOptions options,
|
||||
Action<string>? info = null,
|
||||
Action<string>? error = null,
|
||||
HttpClient? httpClient = null)
|
||||
{
|
||||
_options = options;
|
||||
_httpClient = httpClient;
|
||||
_info = info ?? (_ => { });
|
||||
_error = error ?? (_ => { });
|
||||
}
|
||||
@@ -192,25 +198,39 @@ public sealed class NotifySmokeCheckRunner
|
||||
var deliveriesUrl = BuildDeliveriesUrl(_options.Delivery.BaseUri, sinceThreshold, _options.Delivery.Limit);
|
||||
_info($"[INFO] Querying Notify deliveries via {deliveriesUrl}.");
|
||||
|
||||
using var httpClient = BuildHttpClient(_options.Delivery);
|
||||
using var response = await GetWithRetriesAsync(httpClient, deliveriesUrl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
// Use injected HttpClient if provided, otherwise create one (for standalone tool usage)
|
||||
var ownedClient = _httpClient is null;
|
||||
var httpClient = _httpClient ?? BuildHttpClient(_options.Delivery);
|
||||
try
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Notify deliveries request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}");
|
||||
ConfigureHttpClient(httpClient, _options.Delivery);
|
||||
using var response = await GetWithRetriesAsync(httpClient, deliveriesUrl, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Notify deliveries request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}");
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
Ensure(!string.IsNullOrWhiteSpace(json), "Notify deliveries response body was empty.");
|
||||
|
||||
var deliveries = ParseDeliveries(json);
|
||||
Ensure(deliveries.Count > 0, "Notify deliveries response did not return any records.");
|
||||
|
||||
var missingDeliveryKinds = FindMissingDeliveryKinds(deliveries, _options.ExpectedKinds);
|
||||
Ensure(missingDeliveryKinds.Count == 0, $"Notify deliveries missing successful records for kinds: {string.Join(", ", missingDeliveryKinds)}");
|
||||
|
||||
_info("[INFO] Notify deliveries include the expected scanner events.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Only dispose if we created the client
|
||||
if (ownedClient)
|
||||
{
|
||||
httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
Ensure(!string.IsNullOrWhiteSpace(json), "Notify deliveries response body was empty.");
|
||||
|
||||
var deliveries = ParseDeliveries(json);
|
||||
Ensure(deliveries.Count > 0, "Notify deliveries response did not return any records.");
|
||||
|
||||
var missingDeliveryKinds = FindMissingDeliveryKinds(deliveries, _options.ExpectedKinds);
|
||||
Ensure(missingDeliveryKinds.Count == 0, $"Notify deliveries missing successful records for kinds: {string.Join(", ", missingDeliveryKinds)}");
|
||||
|
||||
_info("[INFO] Notify deliveries include the expected scanner events.");
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<NotifyDeliveryRecord> ParseDeliveries(string json)
|
||||
@@ -405,18 +425,31 @@ public sealed class NotifySmokeCheckRunner
|
||||
return await ConnectionMultiplexer.ConnectAsync(options).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private HttpClient BuildHttpClient(NotifyDeliveryOptions delivery)
|
||||
private static HttpClient BuildHttpClient(NotifyDeliveryOptions delivery)
|
||||
{
|
||||
var httpClient = new HttpClient
|
||||
return new HttpClient
|
||||
{
|
||||
Timeout = delivery.Timeout,
|
||||
};
|
||||
}
|
||||
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", delivery.Token);
|
||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
httpClient.DefaultRequestHeaders.Add(delivery.TenantHeader, delivery.Tenant);
|
||||
private static void ConfigureHttpClient(HttpClient httpClient, NotifyDeliveryOptions delivery)
|
||||
{
|
||||
// Only set headers if not already set (allows injected client to have pre-configured headers)
|
||||
if (httpClient.DefaultRequestHeaders.Authorization is null)
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", delivery.Token);
|
||||
}
|
||||
|
||||
return httpClient;
|
||||
if (!httpClient.DefaultRequestHeaders.Accept.Any())
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
if (!httpClient.DefaultRequestHeaders.Contains(delivery.TenantHeader))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add(delivery.TenantHeader, delivery.Tenant);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> GetWithRetriesAsync(HttpClient httpClient, Uri url, CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user