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"; /// /// Azure AD tenant identifier (GUID or domain). /// public string TenantId { get; set; } = string.Empty; /// /// Azure AD application (client) identifier. /// public string ClientId { get; set; } = string.Empty; /// /// Azure AD application secret for client credential flow. /// public string? ClientSecret { get; set; } /// /// OAuth scope requested for MSRC API access. /// public string Scope { get; set; } = DefaultScope; /// /// When true, token acquisition is skipped and the connector expects offline handling. /// public bool PreferOfflineToken { get; set; } /// /// Optional path to a pre-provisioned bearer token used when is enabled. /// public string? OfflineTokenPath { get; set; } /// /// Optional fixed bearer token for constrained environments (e.g., short-lived offline bundles). /// public string? StaticAccessToken { get; set; } /// /// Minimum buffer (seconds) subtracted from token expiry before refresh. /// public int ExpiryLeewaySeconds { get; set; } = 60; /// /// Base URI for MSRC Security Update Guide API. /// public Uri BaseUri { get; set; } = new(DefaultBaseUri, UriKind.Absolute); /// /// Locale requested when fetching summaries. /// public string Locale { get; set; } = DefaultLocale; /// /// API version appended to MSRC requests. /// public string ApiVersion { get; set; } = DefaultApiVersion; /// /// Page size used while enumerating summaries. /// public int PageSize { get; set; } = 100; /// /// Maximum CSAF advisories fetched per connector run. /// public int MaxAdvisoriesPerFetch { get; set; } = 200; /// /// Overlap window applied when resuming from the last modified cursor. /// public TimeSpan CursorOverlap { get; set; } = TimeSpan.FromMinutes(10); /// /// Delay between CSAF downloads to respect rate limits. /// public TimeSpan RequestDelay { get; set; } = TimeSpan.FromMilliseconds(250); /// /// Maximum retry attempts for summary/detail fetch operations. /// public int MaxRetryAttempts { get; set; } = 3; /// /// Base delay applied between retries (jitter handled by connector). /// public TimeSpan RetryBaseDelay { get; set; } = TimeSpan.FromSeconds(2); /// /// Optional lower bound for initial synchronisation when no cursor is stored. /// public DateTimeOffset? InitialLastModified { get; set; } = DateTimeOffset.UtcNow.AddDays(-30); /// /// Maximum number of document digests persisted for deduplication. /// 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); } } } }