Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace StellaOps.Scheduler.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic serializer for scheduler DTOs.
|
||||
/// </summary>
|
||||
public static class CanonicalJsonSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions CompactOptions = CreateOptions(writeIndented: false);
|
||||
private static readonly JsonSerializerOptions PrettyOptions = CreateOptions(writeIndented: true);
|
||||
|
||||
private static readonly IReadOnlyDictionary<Type, string[]> PropertyOrder = new Dictionary<Type, string[]>
|
||||
{
|
||||
[typeof(Schedule)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"id",
|
||||
"tenantId",
|
||||
"name",
|
||||
"enabled",
|
||||
"cronExpression",
|
||||
"timezone",
|
||||
"mode",
|
||||
"selection",
|
||||
"onlyIf",
|
||||
"notify",
|
||||
"limits",
|
||||
"subscribers",
|
||||
"createdAt",
|
||||
"createdBy",
|
||||
"updatedAt",
|
||||
"updatedBy",
|
||||
},
|
||||
[typeof(Selector)] = new[]
|
||||
{
|
||||
"scope",
|
||||
"tenantId",
|
||||
"namespaces",
|
||||
"repositories",
|
||||
"digests",
|
||||
"includeTags",
|
||||
"labels",
|
||||
"resolvesTags",
|
||||
},
|
||||
[typeof(LabelSelector)] = new[]
|
||||
{
|
||||
"key",
|
||||
"values",
|
||||
},
|
||||
[typeof(ScheduleOnlyIf)] = new[]
|
||||
{
|
||||
"lastReportOlderThanDays",
|
||||
"policyRevision",
|
||||
},
|
||||
[typeof(ScheduleNotify)] = new[]
|
||||
{
|
||||
"onNewFindings",
|
||||
"minSeverity",
|
||||
"includeKev",
|
||||
"includeQuietFindings",
|
||||
},
|
||||
[typeof(ScheduleLimits)] = new[]
|
||||
{
|
||||
"maxJobs",
|
||||
"ratePerSecond",
|
||||
"parallelism",
|
||||
"burst",
|
||||
},
|
||||
[typeof(Run)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"id",
|
||||
"tenantId",
|
||||
"scheduleId",
|
||||
"trigger",
|
||||
"state",
|
||||
"stats",
|
||||
"reason",
|
||||
"createdAt",
|
||||
"startedAt",
|
||||
"finishedAt",
|
||||
"error",
|
||||
"deltas",
|
||||
},
|
||||
[typeof(RunStats)] = new[]
|
||||
{
|
||||
"candidates",
|
||||
"deduped",
|
||||
"queued",
|
||||
"completed",
|
||||
"deltas",
|
||||
"newCriticals",
|
||||
"newHigh",
|
||||
"newMedium",
|
||||
"newLow",
|
||||
},
|
||||
[typeof(RunReason)] = new[]
|
||||
{
|
||||
"manualReason",
|
||||
"feedserExportId",
|
||||
"vexerExportId",
|
||||
"cursor",
|
||||
"impactWindowFrom",
|
||||
"impactWindowTo",
|
||||
},
|
||||
[typeof(DeltaSummary)] = new[]
|
||||
{
|
||||
"imageDigest",
|
||||
"newFindings",
|
||||
"newCriticals",
|
||||
"newHigh",
|
||||
"newMedium",
|
||||
"newLow",
|
||||
"kevHits",
|
||||
"topFindings",
|
||||
"reportUrl",
|
||||
"attestation",
|
||||
"detectedAt",
|
||||
},
|
||||
[typeof(DeltaFinding)] = new[]
|
||||
{
|
||||
"purl",
|
||||
"vulnerabilityId",
|
||||
"severity",
|
||||
"link",
|
||||
},
|
||||
[typeof(ImpactSet)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"selector",
|
||||
"images",
|
||||
"usageOnly",
|
||||
"generatedAt",
|
||||
"total",
|
||||
"snapshotId",
|
||||
},
|
||||
[typeof(ImpactImage)] = new[]
|
||||
{
|
||||
"imageDigest",
|
||||
"registry",
|
||||
"repository",
|
||||
"namespaces",
|
||||
"tags",
|
||||
"usedByEntrypoint",
|
||||
"labels",
|
||||
},
|
||||
[typeof(AuditRecord)] = new[]
|
||||
{
|
||||
"id",
|
||||
"tenantId",
|
||||
"category",
|
||||
"action",
|
||||
"occurredAt",
|
||||
"actor",
|
||||
"entityId",
|
||||
"scheduleId",
|
||||
"runId",
|
||||
"correlationId",
|
||||
"metadata",
|
||||
"message",
|
||||
},
|
||||
[typeof(AuditActor)] = new[]
|
||||
{
|
||||
"actorId",
|
||||
"displayName",
|
||||
"kind",
|
||||
},
|
||||
[typeof(GraphBuildJob)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"id",
|
||||
"tenantId",
|
||||
"sbomId",
|
||||
"sbomVersionId",
|
||||
"sbomDigest",
|
||||
"graphSnapshotId",
|
||||
"status",
|
||||
"trigger",
|
||||
"attempts",
|
||||
"cartographerJobId",
|
||||
"correlationId",
|
||||
"createdAt",
|
||||
"startedAt",
|
||||
"completedAt",
|
||||
"error",
|
||||
"metadata",
|
||||
},
|
||||
[typeof(GraphOverlayJob)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"id",
|
||||
"tenantId",
|
||||
"graphSnapshotId",
|
||||
"buildJobId",
|
||||
"overlayKind",
|
||||
"overlayKey",
|
||||
"subjects",
|
||||
"status",
|
||||
"trigger",
|
||||
"attempts",
|
||||
"correlationId",
|
||||
"createdAt",
|
||||
"startedAt",
|
||||
"completedAt",
|
||||
"error",
|
||||
"metadata",
|
||||
},
|
||||
[typeof(PolicyRunRequest)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"tenantId",
|
||||
"policyId",
|
||||
"policyVersion",
|
||||
"mode",
|
||||
"priority",
|
||||
"runId",
|
||||
"queuedAt",
|
||||
"requestedBy",
|
||||
"correlationId",
|
||||
"metadata",
|
||||
"inputs",
|
||||
},
|
||||
[typeof(PolicyRunInputs)] = new[]
|
||||
{
|
||||
"sbomSet",
|
||||
"advisoryCursor",
|
||||
"vexCursor",
|
||||
"environment",
|
||||
"captureExplain",
|
||||
},
|
||||
[typeof(PolicyRunStatus)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"runId",
|
||||
"tenantId",
|
||||
"policyId",
|
||||
"policyVersion",
|
||||
"mode",
|
||||
"status",
|
||||
"priority",
|
||||
"queuedAt",
|
||||
"startedAt",
|
||||
"finishedAt",
|
||||
"determinismHash",
|
||||
"errorCode",
|
||||
"error",
|
||||
"attempts",
|
||||
"traceId",
|
||||
"explainUri",
|
||||
"metadata",
|
||||
"stats",
|
||||
"inputs",
|
||||
},
|
||||
[typeof(PolicyRunJob)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"id",
|
||||
"tenantId",
|
||||
"policyId",
|
||||
"policyVersion",
|
||||
"mode",
|
||||
"priority",
|
||||
"priorityRank",
|
||||
"runId",
|
||||
"queuedAt",
|
||||
"requestedBy",
|
||||
"correlationId",
|
||||
"metadata",
|
||||
"inputs",
|
||||
"status",
|
||||
"attemptCount",
|
||||
"lastAttemptAt",
|
||||
"lastError",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"availableAt",
|
||||
"submittedAt",
|
||||
"completedAt",
|
||||
"leaseOwner",
|
||||
"leaseExpiresAt",
|
||||
"cancellationRequested",
|
||||
"cancellationRequestedAt",
|
||||
"cancellationReason",
|
||||
"cancelledAt",
|
||||
},
|
||||
[typeof(PolicyRunStats)] = new[]
|
||||
{
|
||||
"components",
|
||||
"rulesFired",
|
||||
"findingsWritten",
|
||||
"vexOverrides",
|
||||
"quieted",
|
||||
"suppressed",
|
||||
"durationSeconds",
|
||||
},
|
||||
[typeof(PolicyDiffSummary)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"added",
|
||||
"removed",
|
||||
"unchanged",
|
||||
"bySeverity",
|
||||
"ruleHits",
|
||||
},
|
||||
[typeof(PolicyDiffSeverityDelta)] = new[]
|
||||
{
|
||||
"up",
|
||||
"down",
|
||||
},
|
||||
[typeof(PolicyDiffRuleDelta)] = new[]
|
||||
{
|
||||
"ruleId",
|
||||
"ruleName",
|
||||
"up",
|
||||
"down",
|
||||
},
|
||||
[typeof(PolicyExplainTrace)] = new[]
|
||||
{
|
||||
"schemaVersion",
|
||||
"findingId",
|
||||
"policyId",
|
||||
"policyVersion",
|
||||
"tenantId",
|
||||
"runId",
|
||||
"evaluatedAt",
|
||||
"verdict",
|
||||
"ruleChain",
|
||||
"evidence",
|
||||
"vexImpacts",
|
||||
"history",
|
||||
"metadata",
|
||||
},
|
||||
[typeof(PolicyExplainVerdict)] = new[]
|
||||
{
|
||||
"status",
|
||||
"severity",
|
||||
"quiet",
|
||||
"score",
|
||||
"rationale",
|
||||
},
|
||||
[typeof(PolicyExplainRule)] = new[]
|
||||
{
|
||||
"ruleId",
|
||||
"ruleName",
|
||||
"action",
|
||||
"decision",
|
||||
"score",
|
||||
"condition",
|
||||
},
|
||||
[typeof(PolicyExplainEvidence)] = new[]
|
||||
{
|
||||
"type",
|
||||
"reference",
|
||||
"source",
|
||||
"status",
|
||||
"weight",
|
||||
"justification",
|
||||
"metadata",
|
||||
},
|
||||
[typeof(PolicyExplainVexImpact)] = new[]
|
||||
{
|
||||
"statementId",
|
||||
"provider",
|
||||
"status",
|
||||
"accepted",
|
||||
"justification",
|
||||
"confidence",
|
||||
},
|
||||
[typeof(PolicyExplainHistoryEvent)] = new[]
|
||||
{
|
||||
"status",
|
||||
"occurredAt",
|
||||
"actor",
|
||||
"note",
|
||||
},
|
||||
};
|
||||
|
||||
public static string Serialize<T>(T value)
|
||||
=> JsonSerializer.Serialize(value, CompactOptions);
|
||||
|
||||
public static string SerializeIndented<T>(T value)
|
||||
=> JsonSerializer.Serialize(value, PrettyOptions);
|
||||
|
||||
public static T Deserialize<T>(string json)
|
||||
=> JsonSerializer.Deserialize<T>(json, PrettyOptions)
|
||||
?? throw new InvalidOperationException($"Unable to deserialize {typeof(T).Name}.");
|
||||
|
||||
private static JsonSerializerOptions CreateOptions(bool writeIndented)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = writeIndented,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
var resolver = options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver();
|
||||
options.TypeInfoResolver = new DeterministicResolver(resolver);
|
||||
options.Converters.Add(new ScheduleModeConverter());
|
||||
options.Converters.Add(new SelectorScopeConverter());
|
||||
options.Converters.Add(new RunTriggerConverter());
|
||||
options.Converters.Add(new RunStateConverter());
|
||||
options.Converters.Add(new SeverityRankConverter());
|
||||
options.Converters.Add(new GraphJobStatusConverter());
|
||||
options.Converters.Add(new GraphBuildJobTriggerConverter());
|
||||
options.Converters.Add(new GraphOverlayJobTriggerConverter());
|
||||
options.Converters.Add(new GraphOverlayKindConverter());
|
||||
options.Converters.Add(new PolicyRunModeConverter());
|
||||
options.Converters.Add(new PolicyRunPriorityConverter());
|
||||
options.Converters.Add(new PolicyRunExecutionStatusConverter());
|
||||
options.Converters.Add(new PolicyVerdictStatusConverter());
|
||||
options.Converters.Add(new PolicyRunJobStatusConverter());
|
||||
return options;
|
||||
}
|
||||
|
||||
private sealed class DeterministicResolver : IJsonTypeInfoResolver
|
||||
{
|
||||
private readonly IJsonTypeInfoResolver _inner;
|
||||
|
||||
public DeterministicResolver(IJsonTypeInfoResolver inner)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
}
|
||||
|
||||
public JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
|
||||
{
|
||||
var info = _inner.GetTypeInfo(type, options);
|
||||
if (info is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to resolve JsonTypeInfo for '{type}'.");
|
||||
}
|
||||
|
||||
if (info.Kind is JsonTypeInfoKind.Object && info.Properties.Count > 1)
|
||||
{
|
||||
var ordered = info.Properties
|
||||
.OrderBy(property => ResolveOrder(type, property.Name))
|
||||
.ThenBy(property => property.Name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
info.Properties.Clear();
|
||||
foreach (var property in ordered)
|
||||
{
|
||||
info.Properties.Add(property);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private static int ResolveOrder(Type type, string propertyName)
|
||||
{
|
||||
if (PropertyOrder.TryGetValue(type, out var order))
|
||||
{
|
||||
var index = Array.IndexOf(order, propertyName);
|
||||
if (index >= 0)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user