using System.Net; namespace StellaOps.Concelier.Connector.Ru.Nkcki.Configuration; /// /// Connector options for the Russian NKTsKI bulletin ingestion pipeline. /// public sealed class RuNkckiOptions { public const string HttpClientName = "ru-nkcki"; private static readonly TimeSpan DefaultRequestTimeout = TimeSpan.FromSeconds(90); private static readonly TimeSpan DefaultFailureBackoff = TimeSpan.FromMinutes(20); private static readonly TimeSpan DefaultListingCache = TimeSpan.FromMinutes(10); /// /// Base endpoint used for resolving relative resource links. /// public Uri BaseAddress { get; set; } = new("https://cert.gov.ru/", UriKind.Absolute); /// /// Relative path to the bulletin listing page. /// public string ListingPath { get; set; } = "materialy/uyazvimosti/"; /// /// Timeout applied to listing and bulletin fetch requests. /// public TimeSpan RequestTimeout { get; set; } = DefaultRequestTimeout; /// /// Backoff applied when the listing or attachments cannot be retrieved. /// public TimeSpan FailureBackoff { get; set; } = DefaultFailureBackoff; /// /// Maximum number of bulletin attachments downloaded per fetch run. /// public int MaxBulletinsPerFetch { get; set; } = 5; /// /// Maximum number of listing pages visited per fetch cycle. /// public int MaxListingPagesPerFetch { get; set; } = 3; /// /// Maximum number of vulnerabilities ingested per fetch cycle across all attachments. /// public int MaxVulnerabilitiesPerFetch { get; set; } = 250; /// /// Maximum bulletin identifiers remembered to avoid refetching historical files. /// public int KnownBulletinCapacity { get; set; } = 512; /// /// Delay between sequential bulletin downloads. /// public TimeSpan RequestDelay { get; set; } = TimeSpan.FromMilliseconds(250); /// /// Duration the HTML listing can be cached before forcing a refetch. /// public TimeSpan ListingCacheDuration { get; set; } = DefaultListingCache; public string UserAgent { get; set; } = "StellaOps/Concelier (+https://stella-ops.org)"; public string AcceptLanguage { get; set; } = "ru-RU,ru;q=0.9,en-US;q=0.6,en;q=0.4"; /// /// Absolute URI for the listing page. /// public Uri ListingUri => new(BaseAddress, ListingPath); /// /// Optional directory for caching downloaded bulletins (relative paths resolve under the content root). /// public string? CacheDirectory { get; set; } = null; public void Validate() { if (BaseAddress is null || !BaseAddress.IsAbsoluteUri) { throw new InvalidOperationException("RuNkcki BaseAddress must be an absolute URI."); } if (string.IsNullOrWhiteSpace(ListingPath)) { throw new InvalidOperationException("RuNkcki ListingPath must be provided."); } if (RequestTimeout <= TimeSpan.Zero) { throw new InvalidOperationException("RuNkcki RequestTimeout must be positive."); } if (FailureBackoff < TimeSpan.Zero) { throw new InvalidOperationException("RuNkcki FailureBackoff cannot be negative."); } if (MaxBulletinsPerFetch <= 0) { throw new InvalidOperationException("RuNkcki MaxBulletinsPerFetch must be greater than zero."); } if (MaxListingPagesPerFetch <= 0) { throw new InvalidOperationException("RuNkcki MaxListingPagesPerFetch must be greater than zero."); } if (MaxVulnerabilitiesPerFetch <= 0) { throw new InvalidOperationException("RuNkcki MaxVulnerabilitiesPerFetch must be greater than zero."); } if (KnownBulletinCapacity <= 0) { throw new InvalidOperationException("RuNkcki KnownBulletinCapacity must be greater than zero."); } if (CacheDirectory is not null && CacheDirectory.Trim().Length == 0) { throw new InvalidOperationException("RuNkcki CacheDirectory cannot be whitespace."); } if (string.IsNullOrWhiteSpace(UserAgent)) { throw new InvalidOperationException("RuNkcki UserAgent cannot be empty."); } if (string.IsNullOrWhiteSpace(AcceptLanguage)) { throw new InvalidOperationException("RuNkcki AcceptLanguage cannot be empty."); } } }