feat(audit): Apply TreatWarningsAsErrors=true to 160+ production csproj files
Sprint: SPRINT_20251229_049_BE_csproj_audit_maint_tests Tasks: AUDIT-0001 through AUDIT-0147 APPLY tasks (approved decisions 1-9) Changes: - Set TreatWarningsAsErrors=true for all production .NET projects - Fixed nullable warnings in Scanner.EntryTrace, Scanner.Evidence, Scheduler.Worker, Concelier connectors, and other modules - Injected TimeProvider/IGuidProvider for deterministic time/ID generation - Added path traversal validation in AirGap.Bundle - Fixed NULL handling in various cursor classes - Third-party GostCryptography retains TreatWarningsAsErrors=false (preserves original) - Test projects excluded per user decision (rejected decision 10) Note: All 17 ACSC connector tests pass after snapshot fixture sync
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
|
||||
@@ -32,6 +32,18 @@ builder.Configuration
|
||||
|
||||
builder.Services.AddAdvisoryAiCore(builder.Configuration);
|
||||
|
||||
// Authorization service
|
||||
builder.Services.AddSingleton<StellaOps.AdvisoryAI.WebService.Services.IAuthorizationService, StellaOps.AdvisoryAI.WebService.Services.HeaderBasedAuthorizationService>();
|
||||
|
||||
// Rate limits service with configuration
|
||||
builder.Services.AddOptions<StellaOps.AdvisoryAI.WebService.Services.RateLimitsOptions>()
|
||||
.Bind(builder.Configuration.GetSection(StellaOps.AdvisoryAI.WebService.Services.RateLimitsOptions.SectionName))
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddSingleton<StellaOps.AdvisoryAI.WebService.Services.IRateLimitsService, StellaOps.AdvisoryAI.WebService.Services.ConfigDrivenRateLimitsService>();
|
||||
|
||||
// TimeProvider for deterministic timestamps
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
|
||||
// VEX-AI-016: Consent and justification services
|
||||
builder.Services.AddSingleton<IAiConsentStore, InMemoryAiConsentStore>();
|
||||
builder.Services.AddSingleton<IAiJustificationGenerator, DefaultAiJustificationGenerator>();
|
||||
@@ -645,9 +657,12 @@ static async Task<IResult> HandlePolicyValidate(
|
||||
}
|
||||
|
||||
// POLICY-19: POST /v1/advisory-ai/policy/studio/compile
|
||||
// NOTE: This is a stub implementation. In production, this would compile rules into a PolicyBundle.
|
||||
// The stub returns experimental markers to indicate incomplete implementation.
|
||||
static Task<IResult> HandlePolicyCompile(
|
||||
HttpContext httpContext,
|
||||
PolicyCompileApiRequest request,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.policy_compile", ActivityKind.Server);
|
||||
@@ -659,9 +674,14 @@ static Task<IResult> HandlePolicyCompile(
|
||||
return Task.FromResult(Results.StatusCode(StatusCodes.Status403Forbidden));
|
||||
}
|
||||
|
||||
// In a real implementation, this would compile rules into a PolicyBundle
|
||||
var bundleId = $"bundle:{Guid.NewGuid():N}";
|
||||
var now = DateTime.UtcNow;
|
||||
// STUB: This endpoint is experimental and not wired to real policy compilation.
|
||||
// Return a deterministic bundle ID derived from input to avoid nondeterministic output.
|
||||
var inputHash = ComputeDeterministicBundleId(request.BundleName, request.RuleIds);
|
||||
var bundleId = $"bundle:stub:{inputHash}";
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
// Compute content hash deterministically from the rule IDs
|
||||
var contentHash = ComputeDeterministicContentHash(request.RuleIds);
|
||||
|
||||
var response = new PolicyBundleApiResponse
|
||||
{
|
||||
@@ -670,13 +690,29 @@ static Task<IResult> HandlePolicyCompile(
|
||||
Version = "1.0.0",
|
||||
RuleCount = request.RuleIds.Count,
|
||||
CompiledAt = now.ToString("O"),
|
||||
ContentHash = $"sha256:{Guid.NewGuid():N}",
|
||||
ContentHash = $"sha256:{contentHash}",
|
||||
SignatureId = null // Would be signed in production
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
// Deterministic hash computation for stub bundle ID
|
||||
static string ComputeDeterministicBundleId(string bundleName, IReadOnlyList<string> ruleIds)
|
||||
{
|
||||
var input = $"{bundleName}:{string.Join(",", ruleIds.OrderBy(x => x, StringComparer.Ordinal))}";
|
||||
var bytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(bytes)[..32].ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Deterministic content hash for stub bundles
|
||||
static string ComputeDeterministicContentHash(IReadOnlyList<string> ruleIds)
|
||||
{
|
||||
var input = string.Join(",", ruleIds.OrderBy(x => x, StringComparer.Ordinal));
|
||||
var bytes = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
// VEX-AI-016: Consent handler functions
|
||||
static string GetTenantId(HttpContext context)
|
||||
{
|
||||
@@ -869,41 +905,24 @@ static async Task<IResult> HandleRemediate(
|
||||
}
|
||||
}
|
||||
|
||||
// VEX-AI-016: Rate limits handler
|
||||
// VEX-AI-016: Rate limits handler using config-driven service
|
||||
static Task<IResult> HandleGetRateLimits(
|
||||
HttpContext httpContext,
|
||||
StellaOps.AdvisoryAI.WebService.Services.IRateLimitsService rateLimitsService,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Return current rate limit info for each feature
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var resetTime = now.AddMinutes(1);
|
||||
var limits = rateLimitsService.GetRateLimits(timeProvider);
|
||||
|
||||
var limits = new List<AiRateLimitInfoResponse>
|
||||
var response = limits.Select(l => new AiRateLimitInfoResponse
|
||||
{
|
||||
new AiRateLimitInfoResponse
|
||||
{
|
||||
Feature = "explain",
|
||||
Limit = 10,
|
||||
Remaining = 10,
|
||||
ResetsAt = resetTime.ToString("O")
|
||||
},
|
||||
new AiRateLimitInfoResponse
|
||||
{
|
||||
Feature = "remediate",
|
||||
Limit = 5,
|
||||
Remaining = 5,
|
||||
ResetsAt = resetTime.ToString("O")
|
||||
},
|
||||
new AiRateLimitInfoResponse
|
||||
{
|
||||
Feature = "justify",
|
||||
Limit = 3,
|
||||
Remaining = 3,
|
||||
ResetsAt = resetTime.ToString("O")
|
||||
}
|
||||
};
|
||||
Feature = l.Feature,
|
||||
Limit = l.Limit,
|
||||
Remaining = l.Remaining,
|
||||
ResetsAt = l.ResetsAt.ToString("O")
|
||||
}).ToList();
|
||||
|
||||
return Task.FromResult(Results.Ok(limits));
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
internal sealed record PipelinePlanRequest(
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using StellaOps.AdvisoryAI.Orchestration;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Consolidated authorization service for advisory-ai endpoints.
|
||||
/// Provides consistent scope-based authorization checks.
|
||||
/// </summary>
|
||||
public interface IAuthorizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the request is authorized for the given task type.
|
||||
/// </summary>
|
||||
bool IsAuthorized(HttpContext context, AdvisoryTaskType taskType);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the request is authorized for explanation operations.
|
||||
/// </summary>
|
||||
bool IsExplainAuthorized(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the request is authorized for remediation operations.
|
||||
/// </summary>
|
||||
bool IsRemediationAuthorized(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the request is authorized for policy studio operations.
|
||||
/// </summary>
|
||||
bool IsPolicyAuthorized(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the request is authorized for justification operations.
|
||||
/// </summary>
|
||||
bool IsJustifyAuthorized(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tenant ID from the request headers.
|
||||
/// </summary>
|
||||
string GetTenantId(HttpContext context);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user ID from the request headers.
|
||||
/// </summary>
|
||||
string GetUserId(HttpContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of authorization service using header-based scopes.
|
||||
/// </summary>
|
||||
public sealed class HeaderBasedAuthorizationService : IAuthorizationService
|
||||
{
|
||||
private const string ScopesHeader = "X-StellaOps-Scopes";
|
||||
private const string TenantHeader = "X-StellaOps-Tenant";
|
||||
private const string UserHeader = "X-StellaOps-User";
|
||||
|
||||
public bool IsAuthorized(HttpContext context, AdvisoryTaskType taskType)
|
||||
{
|
||||
var scopes = GetScopes(context);
|
||||
if (scopes.Contains("advisory:run"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return scopes.Contains($"advisory:{taskType.ToString().ToLowerInvariant()}");
|
||||
}
|
||||
|
||||
public bool IsExplainAuthorized(HttpContext context)
|
||||
{
|
||||
var scopes = GetScopes(context);
|
||||
return scopes.Contains("advisory:run") || scopes.Contains("advisory:explain");
|
||||
}
|
||||
|
||||
public bool IsRemediationAuthorized(HttpContext context)
|
||||
{
|
||||
var scopes = GetScopes(context);
|
||||
return scopes.Contains("advisory:run") || scopes.Contains("advisory:remediate");
|
||||
}
|
||||
|
||||
public bool IsPolicyAuthorized(HttpContext context)
|
||||
{
|
||||
var scopes = GetScopes(context);
|
||||
return scopes.Contains("advisory:run") || scopes.Contains("policy:write");
|
||||
}
|
||||
|
||||
public bool IsJustifyAuthorized(HttpContext context)
|
||||
{
|
||||
var scopes = GetScopes(context);
|
||||
return scopes.Contains("advisory:run") || scopes.Contains("advisory:justify");
|
||||
}
|
||||
|
||||
public string GetTenantId(HttpContext context)
|
||||
{
|
||||
return context.Request.Headers.TryGetValue(TenantHeader, out var value)
|
||||
? value.ToString()
|
||||
: "default";
|
||||
}
|
||||
|
||||
public string GetUserId(HttpContext context)
|
||||
{
|
||||
return context.Request.Headers.TryGetValue(UserHeader, out var value)
|
||||
? value.ToString()
|
||||
: "anonymous";
|
||||
}
|
||||
|
||||
private static HashSet<string> GetScopes(HttpContext context)
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue(ScopesHeader, out var scopes))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return scopes
|
||||
.SelectMany(value => value?.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? [])
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for feature-specific rate limits.
|
||||
/// </summary>
|
||||
public sealed class RateLimitsOptions
|
||||
{
|
||||
public const string SectionName = "AdvisoryAI:RateLimits";
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit for the explain feature.
|
||||
/// </summary>
|
||||
public FeatureRateLimitOptions Explain { get; set; } = new() { Limit = 10, PeriodMinutes = 1 };
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit for the remediate feature.
|
||||
/// </summary>
|
||||
public FeatureRateLimitOptions Remediate { get; set; } = new() { Limit = 5, PeriodMinutes = 1 };
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit for the justify feature.
|
||||
/// </summary>
|
||||
public FeatureRateLimitOptions Justify { get; set; } = new() { Limit = 3, PeriodMinutes = 1 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit configuration for a single feature.
|
||||
/// </summary>
|
||||
public sealed class FeatureRateLimitOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of requests allowed per period.
|
||||
/// </summary>
|
||||
public int Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Period duration in minutes.
|
||||
/// </summary>
|
||||
public int PeriodMinutes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents rate limit information for a feature.
|
||||
/// </summary>
|
||||
public sealed class RateLimitInfo
|
||||
{
|
||||
public required string Feature { get; init; }
|
||||
public required int Limit { get; init; }
|
||||
public required int Remaining { get; init; }
|
||||
public required DateTimeOffset ResetsAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing rate limit state and reporting.
|
||||
/// </summary>
|
||||
public interface IRateLimitsService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current rate limit information for all features.
|
||||
/// </summary>
|
||||
IReadOnlyList<RateLimitInfo> GetRateLimits(TimeProvider timeProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of rate limits service using configuration.
|
||||
/// In production, this would integrate with the actual rate limiter state.
|
||||
/// </summary>
|
||||
public sealed class ConfigDrivenRateLimitsService : IRateLimitsService
|
||||
{
|
||||
private readonly RateLimitsOptions _options;
|
||||
|
||||
public ConfigDrivenRateLimitsService(IOptions<RateLimitsOptions> options)
|
||||
{
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<RateLimitInfo> GetRateLimits(TimeProvider timeProvider)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
return
|
||||
[
|
||||
new RateLimitInfo
|
||||
{
|
||||
Feature = "explain",
|
||||
Limit = _options.Explain.Limit,
|
||||
Remaining = _options.Explain.Limit, // Would integrate with actual limiter state
|
||||
ResetsAt = now.AddMinutes(_options.Explain.PeriodMinutes)
|
||||
},
|
||||
new RateLimitInfo
|
||||
{
|
||||
Feature = "remediate",
|
||||
Limit = _options.Remediate.Limit,
|
||||
Remaining = _options.Remediate.Limit, // Would integrate with actual limiter state
|
||||
ResetsAt = now.AddMinutes(_options.Remediate.PeriodMinutes)
|
||||
},
|
||||
new RateLimitInfo
|
||||
{
|
||||
Feature = "justify",
|
||||
Limit = _options.Justify.Limit,
|
||||
Remaining = _options.Justify.Limit, // Would integrate with actual limiter state
|
||||
ResetsAt = now.AddMinutes(_options.Justify.PeriodMinutes)
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
|
||||
@@ -12,6 +12,10 @@ namespace StellaOps.AdvisoryAI.Worker.Services;
|
||||
|
||||
internal sealed class AdvisoryTaskWorker : BackgroundService
|
||||
{
|
||||
private const int MaxRetryDelaySeconds = 60;
|
||||
private const int BaseRetryDelaySeconds = 2;
|
||||
private const double JitterFactor = 0.2;
|
||||
|
||||
private readonly IAdvisoryTaskQueue _queue;
|
||||
private readonly IAdvisoryPlanCache _cache;
|
||||
private readonly IAdvisoryPipelineOrchestrator _orchestrator;
|
||||
@@ -19,6 +23,7 @@ internal sealed class AdvisoryTaskWorker : BackgroundService
|
||||
private readonly IAdvisoryPipelineExecutor _executor;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<AdvisoryTaskWorker> _logger;
|
||||
private int _consecutiveErrors;
|
||||
|
||||
public AdvisoryTaskWorker(
|
||||
IAdvisoryTaskQueue queue,
|
||||
@@ -61,11 +66,28 @@ internal sealed class AdvisoryTaskWorker : BackgroundService
|
||||
var fromCache = plan is not null && !message.Request.ForceRefresh;
|
||||
activity?.SetTag("advisory.plan_cache_hit", fromCache);
|
||||
|
||||
// When cache miss occurs, preserve the original plan cache key by storing
|
||||
// under the message's key as an alias
|
||||
string effectiveCacheKey = message.PlanCacheKey;
|
||||
if (!fromCache)
|
||||
{
|
||||
var start = _timeProvider.GetTimestamp();
|
||||
plan = await _orchestrator.CreatePlanAsync(message.Request, stoppingToken).ConfigureAwait(false);
|
||||
|
||||
// Store under both the new cache key and the original message key
|
||||
await _cache.SetAsync(plan.CacheKey, plan, stoppingToken).ConfigureAwait(false);
|
||||
|
||||
// If the new plan's cache key differs from the original request,
|
||||
// also store under the original key as an alias
|
||||
if (!string.Equals(plan.CacheKey, message.PlanCacheKey, StringComparison.Ordinal))
|
||||
{
|
||||
await _cache.SetAsync(message.PlanCacheKey, plan, stoppingToken).ConfigureAwait(false);
|
||||
_logger.LogDebug(
|
||||
"Plan cache key changed from {OriginalKey} to {NewKey}; stored alias",
|
||||
message.PlanCacheKey,
|
||||
plan.CacheKey);
|
||||
}
|
||||
|
||||
var elapsed = _timeProvider.GetElapsedTime(start);
|
||||
_metrics.RecordPlanCreated(elapsed.TotalSeconds, message.Request.TaskType);
|
||||
}
|
||||
@@ -85,18 +107,48 @@ internal sealed class AdvisoryTaskWorker : BackgroundService
|
||||
var totalElapsed = _timeProvider.GetElapsedTime(processStart);
|
||||
_metrics.RecordPipelineLatency(message.Request.TaskType, totalElapsed.TotalSeconds, fromCache);
|
||||
activity?.SetTag("advisory.pipeline_latency_seconds", totalElapsed.TotalSeconds);
|
||||
|
||||
// Reset consecutive error count on success
|
||||
_consecutiveErrors = 0;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
// graceful shutdown
|
||||
// Graceful shutdown - exit the loop cleanly
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing advisory task queue message");
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken).ConfigureAwait(false);
|
||||
_consecutiveErrors++;
|
||||
|
||||
// Apply exponential backoff with jitter
|
||||
var delaySeconds = ComputeRetryDelay(_consecutiveErrors);
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delaySeconds), stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
// Graceful shutdown during delay - exit cleanly
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Advisory pipeline worker stopping");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes retry delay with exponential backoff and jitter.
|
||||
/// </summary>
|
||||
private double ComputeRetryDelay(int errorCount)
|
||||
{
|
||||
// Exponential backoff: base * 2^(errorCount-1), capped at max
|
||||
var backoff = Math.Min(BaseRetryDelaySeconds * Math.Pow(2, errorCount - 1), MaxRetryDelaySeconds);
|
||||
|
||||
// Add jitter (+/- JitterFactor percent)
|
||||
var jitter = backoff * JitterFactor * (2 * Random.Shared.NextDouble() - 1);
|
||||
|
||||
return Math.Max(BaseRetryDelaySeconds, backoff + jitter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
|
||||
Reference in New Issue
Block a user