using StellaOps.Scanner.Contracts; using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Serialization; namespace StellaOps.Scanner.ReachabilityDrift; public sealed record CodeChangeFact { [JsonPropertyName("id")] public required Guid Id { get; init; } [JsonPropertyName("scanId")] public required string ScanId { get; init; } [JsonPropertyName("baseScanId")] public required string BaseScanId { get; init; } [JsonPropertyName("language")] public required string Language { get; init; } [JsonPropertyName("nodeId")] public string? NodeId { get; init; } [JsonPropertyName("file")] public required string File { get; init; } [JsonPropertyName("symbol")] public required string Symbol { get; init; } [JsonPropertyName("kind")] public required CodeChangeKind Kind { get; init; } [JsonPropertyName("details")] public JsonElement? Details { get; init; } [JsonPropertyName("detectedAt")] public required DateTimeOffset DetectedAt { get; init; } } [JsonConverter(typeof(JsonStringEnumConverter))] public enum CodeChangeKind { [JsonStringEnumMemberName("added")] Added, [JsonStringEnumMemberName("removed")] Removed, [JsonStringEnumMemberName("signature_changed")] SignatureChanged, [JsonStringEnumMemberName("guard_changed")] GuardChanged, [JsonStringEnumMemberName("dependency_changed")] DependencyChanged, [JsonStringEnumMemberName("visibility_changed")] VisibilityChanged } public sealed record ReachabilityDriftResult { [JsonPropertyName("id")] public required Guid Id { get; init; } [JsonPropertyName("baseScanId")] public required string BaseScanId { get; init; } [JsonPropertyName("headScanId")] public required string HeadScanId { get; init; } [JsonPropertyName("language")] public required string Language { get; init; } [JsonPropertyName("detectedAt")] public required DateTimeOffset DetectedAt { get; init; } [JsonPropertyName("newlyReachable")] public required ImmutableArray NewlyReachable { get; init; } [JsonPropertyName("newlyUnreachable")] public required ImmutableArray NewlyUnreachable { get; init; } [JsonPropertyName("resultDigest")] public required string ResultDigest { get; init; } [JsonPropertyName("totalDriftCount")] public int TotalDriftCount => NewlyReachable.Length + NewlyUnreachable.Length; [JsonPropertyName("hasMaterialDrift")] public bool HasMaterialDrift => NewlyReachable.Length > 0; } public sealed record DriftedSink { [JsonPropertyName("id")] public required Guid Id { get; init; } [JsonPropertyName("sinkNodeId")] public required string SinkNodeId { get; init; } [JsonPropertyName("symbol")] public required string Symbol { get; init; } [JsonPropertyName("sinkCategory")] public required SinkCategory SinkCategory { get; init; } [JsonPropertyName("direction")] public required DriftDirection Direction { get; init; } [JsonPropertyName("cause")] public required DriftCause Cause { get; init; } [JsonPropertyName("path")] public required CompressedPath Path { get; init; } [JsonPropertyName("associatedVulns")] public ImmutableArray AssociatedVulns { get; init; } = ImmutableArray.Empty; } [JsonConverter(typeof(JsonStringEnumConverter))] public enum DriftDirection { [JsonStringEnumMemberName("became_reachable")] BecameReachable, [JsonStringEnumMemberName("became_unreachable")] BecameUnreachable } public sealed record DriftCause { [JsonPropertyName("kind")] public required DriftCauseKind Kind { get; init; } [JsonPropertyName("description")] public required string Description { get; init; } [JsonPropertyName("changedSymbol")] public string? ChangedSymbol { get; init; } [JsonPropertyName("changedFile")] public string? ChangedFile { get; init; } [JsonPropertyName("changedLine")] public int? ChangedLine { get; init; } [JsonPropertyName("codeChangeId")] public Guid? CodeChangeId { get; init; } public static DriftCause GuardRemoved(string symbol) => new() { Kind = DriftCauseKind.GuardRemoved, Description = $"Guard condition removed in {symbol}", ChangedSymbol = symbol }; public static DriftCause NewPublicRoute(string symbol) => new() { Kind = DriftCauseKind.NewPublicRoute, Description = $"New public entrypoint: {symbol}", ChangedSymbol = symbol }; public static DriftCause VisibilityEscalated(string symbol) => new() { Kind = DriftCauseKind.VisibilityEscalated, Description = $"Visibility escalated to public: {symbol}", ChangedSymbol = symbol }; public static DriftCause DependencyUpgraded(string package, string? fromVersion, string? toVersion) => new() { Kind = DriftCauseKind.DependencyUpgraded, Description = $"Dependency changed: {package} {fromVersion ?? "?"} -> {toVersion ?? "?"}", ChangedSymbol = package }; public static DriftCause GuardAdded(string symbol) => new() { Kind = DriftCauseKind.GuardAdded, Description = $"Guard condition added in {symbol}", ChangedSymbol = symbol }; public static DriftCause SymbolRemoved(string symbol) => new() { Kind = DriftCauseKind.SymbolRemoved, Description = $"Symbol removed: {symbol}", ChangedSymbol = symbol }; public static DriftCause Unknown() => new() { Kind = DriftCauseKind.Unknown, Description = "Cause could not be determined" }; } [JsonConverter(typeof(JsonStringEnumConverter))] public enum DriftCauseKind { [JsonStringEnumMemberName("guard_removed")] GuardRemoved, [JsonStringEnumMemberName("guard_added")] GuardAdded, [JsonStringEnumMemberName("new_public_route")] NewPublicRoute, [JsonStringEnumMemberName("visibility_escalated")] VisibilityEscalated, [JsonStringEnumMemberName("dependency_upgraded")] DependencyUpgraded, [JsonStringEnumMemberName("symbol_removed")] SymbolRemoved, [JsonStringEnumMemberName("unknown")] Unknown } public sealed record CompressedPath { [JsonPropertyName("entrypoint")] public required PathNode Entrypoint { get; init; } [JsonPropertyName("sink")] public required PathNode Sink { get; init; } [JsonPropertyName("intermediateCount")] public required int IntermediateCount { get; init; } [JsonPropertyName("keyNodes")] public required ImmutableArray KeyNodes { get; init; } [JsonPropertyName("fullPath")] public ImmutableArray? FullPath { get; init; } } public sealed record PathNode { [JsonPropertyName("nodeId")] public required string NodeId { get; init; } [JsonPropertyName("symbol")] public required string Symbol { get; init; } [JsonPropertyName("file")] public string? File { get; init; } [JsonPropertyName("line")] public int? Line { get; init; } [JsonPropertyName("package")] public string? Package { get; init; } [JsonPropertyName("isChanged")] public bool IsChanged { get; init; } [JsonPropertyName("changeKind")] public CodeChangeKind? ChangeKind { get; init; } } public sealed record AssociatedVuln { [JsonPropertyName("cveId")] public required string CveId { get; init; } [JsonPropertyName("epss")] public double? Epss { get; init; } [JsonPropertyName("cvss")] public double? Cvss { get; init; } [JsonPropertyName("vexStatus")] public string? VexStatus { get; init; } [JsonPropertyName("packagePurl")] public string? PackagePurl { get; init; } }