- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation. - Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments. - Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats. - Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats. - Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction. - Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
242 lines
8.8 KiB
C#
242 lines
8.8 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using StellaOps.Policy.Engine.DeterminismGuard;
|
|
|
|
namespace StellaOps.Policy.Engine.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Endpoints for policy code linting and determinism analysis.
|
|
/// Implements POLICY-AOC-19-001 per docs/modules/policy/design/policy-aoc-linting-rules.md.
|
|
/// </summary>
|
|
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<IResult> 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<IResult> 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<IResult> GetLintRulesAsync(CancellationToken cancellationToken)
|
|
{
|
|
var rules = new List<LintRuleInfo>
|
|
{
|
|
// 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
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request for single source analysis.
|
|
/// </summary>
|
|
public sealed record LintSourceRequest(
|
|
string Source,
|
|
string? FileName = null,
|
|
string? MinSeverity = null,
|
|
bool? EnforceErrors = null);
|
|
|
|
/// <summary>
|
|
/// Request for batch source analysis.
|
|
/// </summary>
|
|
public sealed record LintBatchRequest(
|
|
List<LintFileInput> Files,
|
|
string? MinSeverity = null,
|
|
bool? EnforceErrors = null);
|
|
|
|
/// <summary>
|
|
/// Single file input for batch analysis.
|
|
/// </summary>
|
|
public sealed record LintFileInput(
|
|
string Source,
|
|
string FileName);
|
|
|
|
/// <summary>
|
|
/// Response for lint analysis.
|
|
/// </summary>
|
|
public sealed record LintResultResponse
|
|
{
|
|
public required bool Passed { get; init; }
|
|
public required List<LintViolationResponse> Violations { get; init; }
|
|
public required Dictionary<string, int> CountBySeverity { get; init; }
|
|
public required long AnalysisDurationMs { get; init; }
|
|
public required bool EnforcementEnabled { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single violation in lint response.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lint rule information.
|
|
/// </summary>
|
|
public sealed record LintRuleInfo(
|
|
string RuleId,
|
|
string Name,
|
|
string DefaultSeverity,
|
|
string Category,
|
|
string Remediation);
|