using System.Collections.Immutable; using System.Text.Json.Serialization; namespace StellaOps.Scanner.SmartDiff; /// /// Smart-Diff predicate for DSSE attestation. /// public sealed record SmartDiffPredicate( [property: JsonPropertyName("schemaVersion")] string SchemaVersion, [property: JsonPropertyName("baseImage")] ImageReference BaseImage, [property: JsonPropertyName("targetImage")] ImageReference TargetImage, [property: JsonPropertyName("diff")] DiffPayload Diff, [property: JsonPropertyName("reachabilityGate")] ReachabilityGate ReachabilityGate, [property: JsonPropertyName("scanner")] ScannerInfo Scanner, [property: JsonPropertyName("context")] RuntimeContext? Context = null, [property: JsonPropertyName("suppressedCount")] int SuppressedCount = 0, [property: JsonPropertyName("materialChanges")] ImmutableArray? MaterialChanges = null) { public const string PredicateType = "stellaops.dev/predicates/smart-diff@v1"; public const string CurrentSchemaVersion = "1.0.0"; } public sealed record ImageReference( [property: JsonPropertyName("digest")] string Digest, [property: JsonPropertyName("name")] string? Name = null, [property: JsonPropertyName("tag")] string? Tag = null); public sealed record DiffPayload( [property: JsonPropertyName("filesAdded")] ImmutableArray? FilesAdded = null, [property: JsonPropertyName("filesRemoved")] ImmutableArray? FilesRemoved = null, [property: JsonPropertyName("filesChanged")] ImmutableArray? FilesChanged = null, [property: JsonPropertyName("packagesChanged")] ImmutableArray? PackagesChanged = null, [property: JsonPropertyName("packagesAdded")] ImmutableArray? PackagesAdded = null, [property: JsonPropertyName("packagesRemoved")] ImmutableArray? PackagesRemoved = null); public sealed record FileChange( [property: JsonPropertyName("path")] string Path, [property: JsonPropertyName("hunks")] ImmutableArray? Hunks = null, [property: JsonPropertyName("fromHash")] string? FromHash = null, [property: JsonPropertyName("toHash")] string? ToHash = null); public sealed record DiffHunk( [property: JsonPropertyName("startLine")] int StartLine, [property: JsonPropertyName("lineCount")] int LineCount, [property: JsonPropertyName("content")] string? Content = null); public sealed record PackageChange( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("from")] string From, [property: JsonPropertyName("to")] string To, [property: JsonPropertyName("purl")] string? Purl = null, [property: JsonPropertyName("licenseDelta")] LicenseDelta? LicenseDelta = null); public sealed record LicenseDelta( [property: JsonPropertyName("added")] ImmutableArray? Added = null, [property: JsonPropertyName("removed")] ImmutableArray? Removed = null); public sealed record PackageRef( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string Version, [property: JsonPropertyName("purl")] string? Purl = null); public sealed record RuntimeContext( [property: JsonPropertyName("entrypoint")] ImmutableArray? Entrypoint = null, [property: JsonPropertyName("env")] ImmutableDictionary? Env = null, [property: JsonPropertyName("user")] UserContext? User = null); public sealed record UserContext( [property: JsonPropertyName("uid")] int? Uid = null, [property: JsonPropertyName("gid")] int? Gid = null, [property: JsonPropertyName("caps")] ImmutableArray? Caps = null); /// /// 3-bit reachability gate derived from the 7-state lattice. /// public sealed record ReachabilityGate( [property: JsonPropertyName("reachable")] bool? Reachable, [property: JsonPropertyName("configActivated")] bool? ConfigActivated, [property: JsonPropertyName("runningUser")] bool? RunningUser, [property: JsonPropertyName("class")] int Class, [property: JsonPropertyName("rationale")] string? Rationale = null) { /// /// Computes the 3-bit class from the gate values. /// Returns -1 if any gate value is unknown (null). /// public static int ComputeClass(bool? reachable, bool? configActivated, bool? runningUser) { if (reachable is null || configActivated is null || runningUser is null) { return -1; } return (reachable.Value ? 4 : 0) + (configActivated.Value ? 2 : 0) + (runningUser.Value ? 1 : 0); } /// /// Creates a ReachabilityGate with auto-computed class. /// public static ReachabilityGate Create( bool? reachable, bool? configActivated, bool? runningUser, string? rationale = null) => new( reachable, configActivated, runningUser, ComputeClass(reachable, configActivated, runningUser), rationale); } public sealed record ScannerInfo( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string Version, [property: JsonPropertyName("ruleset")] string? Ruleset = null); public sealed record MaterialChange( [property: JsonPropertyName("findingKey")] FindingKey FindingKey, [property: JsonPropertyName("changeType")] MaterialChangeType ChangeType, [property: JsonPropertyName("reason")] string Reason, [property: JsonPropertyName("previousState")] RiskState? PreviousState = null, [property: JsonPropertyName("currentState")] RiskState? CurrentState = null, [property: JsonPropertyName("priorityScore")] int? PriorityScore = null); public sealed record FindingKey( [property: JsonPropertyName("componentPurl")] string ComponentPurl, [property: JsonPropertyName("componentVersion")] string ComponentVersion, [property: JsonPropertyName("cveId")] string CveId); [JsonConverter(typeof(JsonStringEnumConverter))] public enum MaterialChangeType { [JsonStringEnumMemberName("reachability_flip")] ReachabilityFlip, [JsonStringEnumMemberName("vex_flip")] VexFlip, [JsonStringEnumMemberName("range_boundary")] RangeBoundary, [JsonStringEnumMemberName("intelligence_flip")] IntelligenceFlip } public sealed record RiskState( [property: JsonPropertyName("reachable")] bool? Reachable = null, [property: JsonPropertyName("vexStatus")] VexStatusType VexStatus = VexStatusType.Unknown, [property: JsonPropertyName("inAffectedRange")] bool? InAffectedRange = null, [property: JsonPropertyName("kev")] bool Kev = false, [property: JsonPropertyName("epssScore")] double? EpssScore = null, [property: JsonPropertyName("policyFlags")] ImmutableArray? PolicyFlags = null); [JsonConverter(typeof(JsonStringEnumConverter))] public enum VexStatusType { [JsonStringEnumMemberName("affected")] Affected, [JsonStringEnumMemberName("not_affected")] NotAffected, [JsonStringEnumMemberName("fixed")] Fixed, [JsonStringEnumMemberName("under_investigation")] UnderInvestigation, [JsonStringEnumMemberName("unknown")] Unknown }