using Microsoft.AspNetCore.Mvc; using StellaOps.Policy.Engine.DeterminismGuard; namespace StellaOps.Policy.Engine.Endpoints; /// /// Endpoints for policy code linting and determinism analysis. /// Implements POLICY-AOC-19-001 per docs/modules/policy/design/policy-aoc-linting-rules.md. /// public static class PolicyLintEndpoints { public static IEndpointRouteBuilder MapPolicyLint(this IEndpointRouteBuilder routes) { var group = routes.MapGroup("/api/v1/policy/lint"); group.MapPost("/analyze", AnalyzeSourceAsync) .WithName("Policy.Lint.Analyze") .WithDescription("Analyze source code for determinism violations") .RequireAuthorization(policy => policy.RequireClaim("scope", "policy:read")); group.MapPost("/analyze-batch", AnalyzeBatchAsync) .WithName("Policy.Lint.AnalyzeBatch") .WithDescription("Analyze multiple source files for determinism violations") .RequireAuthorization(policy => policy.RequireClaim("scope", "policy:read")); group.MapGet("/rules", GetLintRulesAsync) .WithName("Policy.Lint.GetRules") .WithDescription("Get available lint rules and their severities") .AllowAnonymous(); return routes; } private static Task AnalyzeSourceAsync( [FromBody] LintSourceRequest request, CancellationToken cancellationToken) { if (request is null || string.IsNullOrWhiteSpace(request.Source)) { return Task.FromResult(Results.BadRequest(new { error = "LINT_SOURCE_REQUIRED", message = "Source code is required" })); } var analyzer = new ProhibitedPatternAnalyzer(); var options = new DeterminismGuardOptions { EnforcementEnabled = request.EnforceErrors ?? true, FailOnSeverity = ParseSeverity(request.MinSeverity), EnableStaticAnalysis = true, EnableRuntimeMonitoring = false }; var result = analyzer.AnalyzeSource(request.Source, request.FileName, options); return Task.FromResult(Results.Ok(new LintResultResponse { Passed = result.Passed, Violations = result.Violations.Select(MapViolation).ToList(), CountBySeverity = result.CountBySeverity.ToDictionary( kvp => kvp.Key.ToString().ToLowerInvariant(), kvp => kvp.Value), AnalysisDurationMs = result.AnalysisDurationMs, EnforcementEnabled = result.EnforcementEnabled })); } private static Task AnalyzeBatchAsync( [FromBody] LintBatchRequest request, CancellationToken cancellationToken) { if (request?.Files is null || request.Files.Count == 0) { return Task.FromResult(Results.BadRequest(new { error = "LINT_FILES_REQUIRED", message = "At least one file is required" })); } var analyzer = new ProhibitedPatternAnalyzer(); var options = new DeterminismGuardOptions { EnforcementEnabled = request.EnforceErrors ?? true, FailOnSeverity = ParseSeverity(request.MinSeverity), EnableStaticAnalysis = true, EnableRuntimeMonitoring = false }; var sources = request.Files.Select(f => (f.Source, f.FileName)); var result = analyzer.AnalyzeMultiple(sources, options); return Task.FromResult(Results.Ok(new LintResultResponse { Passed = result.Passed, Violations = result.Violations.Select(MapViolation).ToList(), CountBySeverity = result.CountBySeverity.ToDictionary( kvp => kvp.Key.ToString().ToLowerInvariant(), kvp => kvp.Value), AnalysisDurationMs = result.AnalysisDurationMs, EnforcementEnabled = result.EnforcementEnabled })); } private static Task GetLintRulesAsync(CancellationToken cancellationToken) { var rules = new List { // Wall-clock rules new("DET-001", "DateTime.Now", "error", "WallClock", "Use TimeProvider.GetUtcNow()"), new("DET-002", "DateTime.UtcNow", "error", "WallClock", "Use TimeProvider.GetUtcNow()"), new("DET-003", "DateTimeOffset.Now", "error", "WallClock", "Use TimeProvider.GetUtcNow()"), new("DET-004", "DateTimeOffset.UtcNow", "error", "WallClock", "Use TimeProvider.GetUtcNow()"), // Random/GUID rules new("DET-005", "Guid.NewGuid()", "error", "GuidGeneration", "Use StableIdGenerator or content hash"), new("DET-006", "new Random()", "error", "RandomNumber", "Use seeded random or remove"), new("DET-007", "RandomNumberGenerator", "error", "RandomNumber", "Remove from evaluation path"), // Network/Filesystem rules new("DET-008", "HttpClient in eval", "critical", "NetworkAccess", "Remove network from eval path"), new("DET-009", "File.Read* in eval", "critical", "FileSystemAccess", "Remove filesystem from eval path"), // Ordering rules new("DET-010", "Dictionary iteration", "warning", "UnstableIteration", "Use OrderBy or SortedDictionary"), new("DET-011", "HashSet iteration", "warning", "UnstableIteration", "Use OrderBy or SortedSet"), // Environment rules new("DET-012", "Environment.GetEnvironmentVariable", "error", "EnvironmentAccess", "Use evaluation context"), new("DET-013", "Environment.MachineName", "warning", "EnvironmentAccess", "Remove host-specific info") }; return Task.FromResult(Results.Ok(new { rules, categories = new[] { "WallClock", "RandomNumber", "GuidGeneration", "NetworkAccess", "FileSystemAccess", "EnvironmentAccess", "UnstableIteration", "FloatingPointHazard", "ConcurrencyHazard" }, severities = new[] { "info", "warning", "error", "critical" } })); } private static DeterminismViolationSeverity ParseSeverity(string? severity) { return severity?.ToLowerInvariant() switch { "info" => DeterminismViolationSeverity.Info, "warning" => DeterminismViolationSeverity.Warning, "error" => DeterminismViolationSeverity.Error, "critical" => DeterminismViolationSeverity.Critical, _ => DeterminismViolationSeverity.Error }; } private static LintViolationResponse MapViolation(DeterminismViolation v) { return new LintViolationResponse { Category = v.Category.ToString(), ViolationType = v.ViolationType, Message = v.Message, Severity = v.Severity.ToString().ToLowerInvariant(), SourceFile = v.SourceFile, LineNumber = v.LineNumber, MemberName = v.MemberName, Remediation = v.Remediation }; } } /// /// Request for single source analysis. /// public sealed record LintSourceRequest( string Source, string? FileName = null, string? MinSeverity = null, bool? EnforceErrors = null); /// /// Request for batch source analysis. /// public sealed record LintBatchRequest( List Files, string? MinSeverity = null, bool? EnforceErrors = null); /// /// Single file input for batch analysis. /// public sealed record LintFileInput( string Source, string FileName); /// /// Response for lint analysis. /// public sealed record LintResultResponse { public required bool Passed { get; init; } public required List Violations { get; init; } public required Dictionary CountBySeverity { get; init; } public required long AnalysisDurationMs { get; init; } public required bool EnforcementEnabled { get; init; } } /// /// Single violation in lint response. /// public sealed record LintViolationResponse { public required string Category { get; init; } public required string ViolationType { get; init; } public required string Message { get; init; } public required string Severity { get; init; } public string? SourceFile { get; init; } public int? LineNumber { get; init; } public string? MemberName { get; init; } public string? Remediation { get; init; } } /// /// Lint rule information. /// public sealed record LintRuleInfo( string RuleId, string Name, string DefaultSeverity, string Category, string Remediation);