using System.Net; namespace StellaOps.Feedser.Source.CertBund.Configuration; public sealed class CertBundOptions { public const string HttpClientName = "feedser.source.certbund"; /// /// RSS feed providing the latest CERT-Bund advisories. /// public Uri FeedUri { get; set; } = new("https://wid.cert-bund.de/content/public/securityAdvisory/rss"); /// /// Portal endpoint used to bootstrap session cookies (required for the SPA JSON API). /// public Uri PortalBootstrapUri { get; set; } = new("https://wid.cert-bund.de/portal/"); /// /// Detail API endpoint template; advisory identifier is appended as the name query parameter. /// public Uri DetailApiUri { get; set; } = new("https://wid.cert-bund.de/portal/api/securityadvisory"); /// /// Optional timeout override for feed/detail requests. /// public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30); /// /// Delay applied between successive detail fetches to respect upstream politeness. /// public TimeSpan RequestDelay { get; set; } = TimeSpan.FromMilliseconds(250); /// /// Backoff recorded in source state when a fetch attempt fails. /// public TimeSpan FailureBackoff { get; set; } = TimeSpan.FromMinutes(5); /// /// Maximum number of advisories to enqueue per fetch iteration. /// public int MaxAdvisoriesPerFetch { get; set; } = 50; /// /// Maximum number of advisory identifiers remembered to prevent re-processing. /// public int MaxKnownAdvisories { get; set; } = 512; public void Validate() { if (FeedUri is null || !FeedUri.IsAbsoluteUri) { throw new InvalidOperationException("CERT-Bund feed URI must be an absolute URI."); } if (PortalBootstrapUri is null || !PortalBootstrapUri.IsAbsoluteUri) { throw new InvalidOperationException("CERT-Bund portal bootstrap URI must be an absolute URI."); } if (DetailApiUri is null || !DetailApiUri.IsAbsoluteUri) { throw new InvalidOperationException("CERT-Bund detail API URI must be an absolute URI."); } if (RequestTimeout <= TimeSpan.Zero) { throw new InvalidOperationException($"{nameof(RequestTimeout)} must be positive."); } if (RequestDelay < TimeSpan.Zero) { throw new InvalidOperationException($"{nameof(RequestDelay)} cannot be negative."); } if (FailureBackoff <= TimeSpan.Zero) { throw new InvalidOperationException($"{nameof(FailureBackoff)} must be positive."); } if (MaxAdvisoriesPerFetch <= 0) { throw new InvalidOperationException($"{nameof(MaxAdvisoriesPerFetch)} must be greater than zero."); } if (MaxKnownAdvisories <= 0) { throw new InvalidOperationException($"{nameof(MaxKnownAdvisories)} must be greater than zero."); } } public Uri BuildDetailUri(string advisoryId) { if (string.IsNullOrWhiteSpace(advisoryId)) { throw new ArgumentException("Advisory identifier must be provided.", nameof(advisoryId)); } var builder = new UriBuilder(DetailApiUri); var queryPrefix = string.IsNullOrEmpty(builder.Query) ? string.Empty : builder.Query.TrimStart('?') + "&"; builder.Query = $"{queryPrefix}name={Uri.EscapeDataString(advisoryId)}"; return builder.Uri; } }