Restructure solution layout by module
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Docs CI / lint-and-preview (push) Has been cancelled
				
			This commit is contained in:
		@@ -0,0 +1,132 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Concelier.Connector.Vndr.Msrc.Configuration;
 | 
			
		||||
 | 
			
		||||
public sealed class MsrcOptions
 | 
			
		||||
{
 | 
			
		||||
    public const string HttpClientName = "concelier.source.vndr.msrc";
 | 
			
		||||
    public const string TokenClientName = "concelier.source.vndr.msrc.token";
 | 
			
		||||
 | 
			
		||||
    public Uri BaseUri { get; set; } = new("https://api.msrc.microsoft.com/sug/v2.0/", UriKind.Absolute);
 | 
			
		||||
 | 
			
		||||
    public string Locale { get; set; } = "en-US";
 | 
			
		||||
 | 
			
		||||
    public string ApiVersion { get; set; } = "2024-08-01";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Azure AD tenant identifier used for client credential flow.
 | 
			
		||||
    /// </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 client secret used for token acquisition.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ClientSecret { get; set; } = string.Empty;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Scope requested during client-credential token acquisition.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Scope { get; set; } = "api://api.msrc.microsoft.com/.default";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Maximum advisories to fetch per cycle.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int MaxAdvisoriesPerFetch { get; set; } = 200;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Page size used when iterating the MSRC API.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public int PageSize { get; set; } = 100;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Overlap window added when resuming from the last modified cursor.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public TimeSpan CursorOverlap { get; set; } = TimeSpan.FromMinutes(10);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// When enabled the connector downloads the CVRF artefact referenced by each advisory.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool DownloadCvrf { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
    public TimeSpan RequestDelay { get; set; } = TimeSpan.FromMilliseconds(250);
 | 
			
		||||
 | 
			
		||||
    public TimeSpan FailureBackoff { get; set; } = TimeSpan.FromMinutes(5);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional lower bound for the initial sync if the cursor is empty.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public DateTimeOffset? InitialLastModified { get; set; } = DateTimeOffset.UtcNow.AddDays(-30);
 | 
			
		||||
 | 
			
		||||
    public void Validate()
 | 
			
		||||
    {
 | 
			
		||||
        if (BaseUri is null || !BaseUri.IsAbsoluteUri)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("MSRC base URI must be absolute.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(Locale))
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("Locale must be provided.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(Locale) && !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("API version must be provided.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Guid.TryParse(TenantId, out _))
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("TenantId must be a valid GUID.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(ClientId))
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("ClientId must be provided.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(ClientSecret))
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("ClientSecret must be provided.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(Scope))
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException("Scope must be provided.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (MaxAdvisoriesPerFetch <= 0)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException($"{nameof(MaxAdvisoriesPerFetch)} must be greater than zero.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (PageSize <= 0 || PageSize > 500)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException($"{nameof(PageSize)} must be between 1 and 500.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (CursorOverlap < TimeSpan.Zero || CursorOverlap > TimeSpan.FromHours(6))
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException($"{nameof(CursorOverlap)} must be within 0-6 hours.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (RequestDelay < TimeSpan.Zero)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException($"{nameof(RequestDelay)} cannot be negative.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (FailureBackoff <= TimeSpan.Zero)
 | 
			
		||||
        {
 | 
			
		||||
            throw new InvalidOperationException($"{nameof(FailureBackoff)} must be positive.");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user