- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
212 lines
7.3 KiB
C#
212 lines
7.3 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.IO.Abstractions;
|
|
using System.Linq;
|
|
|
|
namespace StellaOps.Excititor.Connectors.MSRC.CSAF.Configuration;
|
|
|
|
public sealed class MsrcConnectorOptions
|
|
{
|
|
public const string TokenClientName = "excititor.connector.msrc.token";
|
|
public const string DefaultScope = "https://api.msrc.microsoft.com/.default";
|
|
public const string ApiClientName = "excititor.connector.msrc.api";
|
|
public const string DefaultBaseUri = "https://api.msrc.microsoft.com/sug/v2.0/";
|
|
public const string DefaultLocale = "en-US";
|
|
public const string DefaultApiVersion = "2024-08-01";
|
|
|
|
/// <summary>
|
|
/// Azure AD tenant identifier (GUID or domain).
|
|
/// </summary>
|
|
public string TenantId { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Azure AD application (client) identifier.
|
|
/// </summary>
|
|
public string ClientId { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Azure AD application secret for client credential flow.
|
|
/// </summary>
|
|
public string? ClientSecret { get; set; }
|
|
/// <summary>
|
|
/// OAuth scope requested for MSRC API access.
|
|
/// </summary>
|
|
public string Scope { get; set; } = DefaultScope;
|
|
|
|
/// <summary>
|
|
/// When true, token acquisition is skipped and the connector expects offline handling.
|
|
/// </summary>
|
|
public bool PreferOfflineToken { get; set; }
|
|
/// <summary>
|
|
/// Optional path to a pre-provisioned bearer token used when <see cref="PreferOfflineToken"/> is enabled.
|
|
/// </summary>
|
|
public string? OfflineTokenPath { get; set; }
|
|
/// <summary>
|
|
/// Optional fixed bearer token for constrained environments (e.g., short-lived offline bundles).
|
|
/// </summary>
|
|
public string? StaticAccessToken { get; set; }
|
|
/// <summary>
|
|
/// Minimum buffer (seconds) subtracted from token expiry before refresh.
|
|
/// </summary>
|
|
public int ExpiryLeewaySeconds { get; set; } = 60;
|
|
|
|
/// <summary>
|
|
/// Base URI for MSRC Security Update Guide API.
|
|
/// </summary>
|
|
public Uri BaseUri { get; set; } = new(DefaultBaseUri, UriKind.Absolute);
|
|
|
|
/// <summary>
|
|
/// Locale requested when fetching summaries.
|
|
/// </summary>
|
|
public string Locale { get; set; } = DefaultLocale;
|
|
|
|
/// <summary>
|
|
/// API version appended to MSRC requests.
|
|
/// </summary>
|
|
public string ApiVersion { get; set; } = DefaultApiVersion;
|
|
|
|
/// <summary>
|
|
/// Page size used while enumerating summaries.
|
|
/// </summary>
|
|
public int PageSize { get; set; } = 100;
|
|
|
|
/// <summary>
|
|
/// Maximum CSAF advisories fetched per connector run.
|
|
/// </summary>
|
|
public int MaxAdvisoriesPerFetch { get; set; } = 200;
|
|
|
|
/// <summary>
|
|
/// Overlap window applied when resuming from the last modified cursor.
|
|
/// </summary>
|
|
public TimeSpan CursorOverlap { get; set; } = TimeSpan.FromMinutes(10);
|
|
|
|
/// <summary>
|
|
/// Delay between CSAF downloads to respect rate limits.
|
|
/// </summary>
|
|
public TimeSpan RequestDelay { get; set; } = TimeSpan.FromMilliseconds(250);
|
|
|
|
/// <summary>
|
|
/// Maximum retry attempts for summary/detail fetch operations.
|
|
/// </summary>
|
|
public int MaxRetryAttempts { get; set; } = 3;
|
|
|
|
/// <summary>
|
|
/// Base delay applied between retries (jitter handled by connector).
|
|
/// </summary>
|
|
public TimeSpan RetryBaseDelay { get; set; } = TimeSpan.FromSeconds(2);
|
|
|
|
/// <summary>
|
|
/// Optional lower bound for initial synchronisation when no cursor is stored.
|
|
/// </summary>
|
|
public DateTimeOffset? InitialLastModified { get; set; } = DateTimeOffset.UtcNow.AddDays(-30);
|
|
|
|
/// <summary>
|
|
/// Maximum number of document digests persisted for deduplication.
|
|
/// </summary>
|
|
public int MaxTrackedDigests { get; set; } = 2048;
|
|
|
|
public void Validate(IFileSystem? fileSystem = null)
|
|
{
|
|
if (PreferOfflineToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(OfflineTokenPath) && string.IsNullOrWhiteSpace(StaticAccessToken))
|
|
{
|
|
throw new InvalidOperationException("OfflineTokenPath or StaticAccessToken must be provided when PreferOfflineToken is enabled.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (string.IsNullOrWhiteSpace(TenantId))
|
|
{
|
|
throw new InvalidOperationException("TenantId is required when not operating in offline token mode.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(ClientId))
|
|
{
|
|
throw new InvalidOperationException("ClientId is required when not operating in offline token mode.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(ClientSecret))
|
|
{
|
|
throw new InvalidOperationException("ClientSecret is required when not operating in offline token mode.");
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(Scope))
|
|
{
|
|
Scope = DefaultScope;
|
|
}
|
|
|
|
if (ExpiryLeewaySeconds < 10)
|
|
{
|
|
ExpiryLeewaySeconds = 10;
|
|
}
|
|
|
|
if (BaseUri is null || !BaseUri.IsAbsoluteUri)
|
|
{
|
|
throw new InvalidOperationException("BaseUri must be an absolute URI.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(Locale))
|
|
{
|
|
throw new InvalidOperationException("Locale must be provided.");
|
|
}
|
|
|
|
if (!CultureInfo.GetCultures(CultureTypes.AllCultures).Any(c => string.Equals(c.Name, Locale, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
throw new InvalidOperationException($"Locale '{Locale}' is not recognised.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(ApiVersion))
|
|
{
|
|
throw new InvalidOperationException("ApiVersion must be provided.");
|
|
}
|
|
|
|
if (PageSize <= 0 || PageSize > 500)
|
|
{
|
|
throw new InvalidOperationException($"{nameof(PageSize)} must be between 1 and 500.");
|
|
}
|
|
|
|
if (MaxAdvisoriesPerFetch <= 0)
|
|
{
|
|
throw new InvalidOperationException($"{nameof(MaxAdvisoriesPerFetch)} must be greater than zero.");
|
|
}
|
|
|
|
if (CursorOverlap < TimeSpan.Zero || CursorOverlap > TimeSpan.FromHours(6))
|
|
{
|
|
throw new InvalidOperationException($"{nameof(CursorOverlap)} must be within 0-6 hours.");
|
|
}
|
|
|
|
if (RequestDelay < TimeSpan.Zero || RequestDelay > TimeSpan.FromSeconds(10))
|
|
{
|
|
throw new InvalidOperationException($"{nameof(RequestDelay)} must be between 0 and 10 seconds.");
|
|
}
|
|
|
|
if (MaxRetryAttempts <= 0 || MaxRetryAttempts > 10)
|
|
{
|
|
throw new InvalidOperationException($"{nameof(MaxRetryAttempts)} must be between 1 and 10.");
|
|
}
|
|
|
|
if (RetryBaseDelay < TimeSpan.Zero || RetryBaseDelay > TimeSpan.FromMinutes(5))
|
|
{
|
|
throw new InvalidOperationException($"{nameof(RetryBaseDelay)} must be between 0 and 5 minutes.");
|
|
}
|
|
|
|
if (MaxTrackedDigests <= 0 || MaxTrackedDigests > 10000)
|
|
{
|
|
throw new InvalidOperationException($"{nameof(MaxTrackedDigests)} must be between 1 and 10000.");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(OfflineTokenPath))
|
|
{
|
|
var fs = fileSystem ?? new FileSystem();
|
|
var directory = Path.GetDirectoryName(OfflineTokenPath);
|
|
if (!string.IsNullOrWhiteSpace(directory) && !fs.Directory.Exists(directory))
|
|
{
|
|
fs.Directory.CreateDirectory(directory);
|
|
}
|
|
}
|
|
}
|
|
}
|