This commit is contained in:
StellaOps Bot
2025-12-14 23:20:14 +02:00
parent 3411e825cd
commit b058dbe031
356 changed files with 68310 additions and 1108 deletions

View File

@@ -0,0 +1,28 @@
# StellaOps.Scanner.SmartDiff — Agent Charter
## Mission
Deliver Smart-Diff primitives and detection logic that enable deterministic, attestable differential analysis between two scans, reducing noise to material risk changes.
## Responsibilities
- Define Smart-Diff predicate models and deterministic serialization helpers.
- Implement reachability gate computation and Smart-Diff delta structures.
- Provide material change detection and scoring (see `SPRINT_3500*`).
## Interfaces & Dependencies
- Consumed by Scanner WebService/Worker and downstream policy/VEX flows.
- Predicate schemas are versioned/registered under `src/Attestor/StellaOps.Attestor.Types`.
## Testing Expectations
- Unit tests for reachability gate computation and enum serialization.
- Golden predicate fixtures to ensure deterministic output.
## Required Reading
- `docs/product-advisories/14-Dec-2025 - Smart-Diff Technical Reference.md`
- `docs/modules/scanner/architecture.md`
- `docs/modules/platform/architecture-overview.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in the sprint file `/docs/implplan/SPRINT_*.md` when starting/finishing work.
- 2. Keep outputs deterministic (stable ordering, UTC timestamps, invariant formatting).
- 3. Avoid cross-module edits unless explicitly referenced in the sprint and recorded in Decisions & Risks.

View File

@@ -0,0 +1,26 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.SmartDiff;
public static class SmartDiffJsonSerializer
{
public static string Serialize(SmartDiffPredicate predicate, bool indent = false)
{
ArgumentNullException.ThrowIfNull(predicate);
return JsonSerializer.Serialize(predicate, CreateDefault(indent));
}
private static JsonSerializerOptions CreateDefault(bool indent)
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = indent,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
return options;
}
}

View File

@@ -0,0 +1,176 @@
namespace StellaOps.Scanner.SmartDiff;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
/// <summary>
/// Smart-Diff predicate for DSSE attestation.
/// </summary>
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<MaterialChange>? 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<string>? FilesAdded = null,
[property: JsonPropertyName("filesRemoved")] ImmutableArray<string>? FilesRemoved = null,
[property: JsonPropertyName("filesChanged")] ImmutableArray<FileChange>? FilesChanged = null,
[property: JsonPropertyName("packagesChanged")] ImmutableArray<PackageChange>? PackagesChanged = null,
[property: JsonPropertyName("packagesAdded")] ImmutableArray<PackageRef>? PackagesAdded = null,
[property: JsonPropertyName("packagesRemoved")] ImmutableArray<PackageRef>? PackagesRemoved = null);
public sealed record FileChange(
[property: JsonPropertyName("path")] string Path,
[property: JsonPropertyName("hunks")] ImmutableArray<DiffHunk>? 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<string>? Added = null,
[property: JsonPropertyName("removed")] ImmutableArray<string>? 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<string>? Entrypoint = null,
[property: JsonPropertyName("env")] ImmutableDictionary<string, string>? 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<string>? Caps = null);
/// <summary>
/// 3-bit reachability gate derived from the 7-state lattice.
/// </summary>
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)
{
/// <summary>
/// Computes the 3-bit class from the gate values.
/// Returns -1 if any gate value is unknown (null).
/// </summary>
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);
}
/// <summary>
/// Creates a ReachabilityGate with auto-computed class.
/// </summary>
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<MaterialChangeType>))]
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<string>? PolicyFlags = null);
[JsonConverter(typeof(JsonStringEnumConverter<VexStatusType>))]
public enum VexStatusType
{
[JsonStringEnumMemberName("affected")]
Affected,
[JsonStringEnumMemberName("not_affected")]
NotAffected,
[JsonStringEnumMemberName("fixed")]
Fixed,
[JsonStringEnumMemberName("under_investigation")]
UnderInvestigation,
[JsonStringEnumMemberName("unknown")]
Unknown
}

View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>