Implement VEX document verification system with issuer management and signature verification
- 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.
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user