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);
}
}
}
}