This commit is contained in:
StellaOps Bot
2025-12-15 09:03:56 +02:00
parent b058dbe031
commit 8c8f0c632d
8 changed files with 423 additions and 0 deletions

View File

@@ -255,6 +255,123 @@ internal sealed class TypeRegistry
customEvidence.Properties.Add(new PropertySpec("generatedAt", PrimitiveShape.String(format: "date-time"), true, "Timestamp when the evidence was generated."));
customEvidence.Properties.Add(new PropertySpec("properties", new ArrayShape(new ObjectShape(customProperty), 0), false, "Optional key/value properties for additional context."));
// Smart-Diff predicate (schema-only; consumed as in-toto predicate payload)
var smartDiffMaterialChangeType = RegisterEnum(
"MaterialChangeType",
"Material change types emitted by Smart-Diff.",
"reachability_flip",
"vex_flip",
"range_boundary",
"intelligence_flip");
var smartDiffVexStatusType = RegisterEnum(
"VexStatusType",
"VEX status values captured in Smart-Diff risk state.",
"affected",
"not_affected",
"fixed",
"under_investigation",
"unknown");
var findingKey = RegisterObject("FindingKey", "Unique identifier for a vulnerability finding.");
findingKey.Properties.Add(new PropertySpec("componentPurl", PrimitiveShape.String(), true, "Component package URL (purl)."));
findingKey.Properties.Add(new PropertySpec("componentVersion", PrimitiveShape.String(), true, "Component version string."));
findingKey.Properties.Add(new PropertySpec("cveId", PrimitiveShape.String(), true, "Vulnerability identifier (e.g., CVE)."));
var riskState = RegisterObject("RiskState", "Risk state captured for a finding at a point in time.");
riskState.Properties.Add(new PropertySpec("reachable", PrimitiveShape.Boolean(), false, "Reachability flag (null/absent indicates unknown)."));
riskState.Properties.Add(new PropertySpec("vexStatus", new EnumShape(smartDiffVexStatusType), true, "VEX status value."));
riskState.Properties.Add(new PropertySpec("inAffectedRange", PrimitiveShape.Boolean(), false, "True if the component version is within the affected range."));
riskState.Properties.Add(new PropertySpec("kev", PrimitiveShape.Boolean(), true, "True if the vulnerability is in the KEV catalog."));
riskState.Properties.Add(new PropertySpec("epssScore", PrimitiveShape.Number(minimum: 0, maximum: 1), false, "EPSS score (0..1)."));
riskState.Properties.Add(new PropertySpec("policyFlags", new ArrayShape(PrimitiveShape.String(), 0), false, "Policy flags contributing to the decision."));
var materialChange = RegisterObject("MaterialChange", "Single material change detected for a finding.");
materialChange.Properties.Add(new PropertySpec("findingKey", new ObjectShape(findingKey), true, "Finding key for the change."));
materialChange.Properties.Add(new PropertySpec("changeType", new EnumShape(smartDiffMaterialChangeType), true, "Type of material change detected."));
materialChange.Properties.Add(new PropertySpec("reason", PrimitiveShape.String(), true, "Human-readable reason for the change."));
materialChange.Properties.Add(new PropertySpec("previousState", new ObjectShape(riskState), false, "Previous risk state (when available)."));
materialChange.Properties.Add(new PropertySpec("currentState", new ObjectShape(riskState), false, "Current risk state (when available)."));
materialChange.Properties.Add(new PropertySpec("priorityScore", PrimitiveShape.Integer(minimum: 0), false, "Priority score derived from change rules and intelligence."));
var imageReference = RegisterObject("ImageReference", "Reference to a container image.");
imageReference.Properties.Add(new PropertySpec("digest", PrimitiveShape.String(pattern: "^sha256:[A-Fa-f0-9]{64}$"), true, "Image digest."));
imageReference.Properties.Add(new PropertySpec("name", PrimitiveShape.String(), false, "Image name."));
imageReference.Properties.Add(new PropertySpec("tag", PrimitiveShape.String(), false, "Image tag."));
var licenseDelta = RegisterObject("LicenseDelta", "License delta for a package or file.");
licenseDelta.Properties.Add(new PropertySpec("added", new ArrayShape(PrimitiveShape.String(), 0), false, "Licenses added."));
licenseDelta.Properties.Add(new PropertySpec("removed", new ArrayShape(PrimitiveShape.String(), 0), false, "Licenses removed."));
var packageChange = RegisterObject("PackageChange", "Package version change between the base and target scan.");
packageChange.Properties.Add(new PropertySpec("name", PrimitiveShape.String(), true, "Package name."));
packageChange.Properties.Add(new PropertySpec("from", PrimitiveShape.String(), true, "Previous package version."));
packageChange.Properties.Add(new PropertySpec("to", PrimitiveShape.String(), true, "Current package version."));
packageChange.Properties.Add(new PropertySpec("purl", PrimitiveShape.String(), false, "Package URL (purl)."));
packageChange.Properties.Add(new PropertySpec("licenseDelta", new ObjectShape(licenseDelta), false, "License delta between versions."));
var packageRef = RegisterObject("PackageRef", "Package reference used in diffs.");
packageRef.Properties.Add(new PropertySpec("name", PrimitiveShape.String(), true, "Package name."));
packageRef.Properties.Add(new PropertySpec("version", PrimitiveShape.String(), true, "Package version."));
packageRef.Properties.Add(new PropertySpec("purl", PrimitiveShape.String(), false, "Package URL (purl)."));
var diffHunk = RegisterObject("DiffHunk", "Single diff hunk for a file change.");
diffHunk.Properties.Add(new PropertySpec("startLine", PrimitiveShape.Integer(minimum: 0), true, "Start line number."));
diffHunk.Properties.Add(new PropertySpec("lineCount", PrimitiveShape.Integer(minimum: 0), true, "Number of lines in the hunk."));
diffHunk.Properties.Add(new PropertySpec("content", PrimitiveShape.String(), false, "Optional hunk content."));
var fileChange = RegisterObject("FileChange", "File-level delta captured by Smart-Diff.");
fileChange.Properties.Add(new PropertySpec("path", PrimitiveShape.String(), true, "File path."));
fileChange.Properties.Add(new PropertySpec("hunks", new ArrayShape(new ObjectShape(diffHunk), 0), false, "Optional hunks describing the file change."));
fileChange.Properties.Add(new PropertySpec("fromHash", PrimitiveShape.String(), false, "Previous file hash (when available)."));
fileChange.Properties.Add(new PropertySpec("toHash", PrimitiveShape.String(), false, "Current file hash (when available)."));
var diffPayload = RegisterObject("DiffPayload", "Diff payload describing file and package deltas.");
diffPayload.Properties.Add(new PropertySpec("filesAdded", new ArrayShape(PrimitiveShape.String(), 0), false, "Paths of files added."));
diffPayload.Properties.Add(new PropertySpec("filesRemoved", new ArrayShape(PrimitiveShape.String(), 0), false, "Paths of files removed."));
diffPayload.Properties.Add(new PropertySpec("filesChanged", new ArrayShape(new ObjectShape(fileChange), 0), false, "Collection of file changes."));
diffPayload.Properties.Add(new PropertySpec("packagesChanged", new ArrayShape(new ObjectShape(packageChange), 0), false, "Collection of package changes."));
diffPayload.Properties.Add(new PropertySpec("packagesAdded", new ArrayShape(new ObjectShape(packageRef), 0), false, "Packages added."));
diffPayload.Properties.Add(new PropertySpec("packagesRemoved", new ArrayShape(new ObjectShape(packageRef), 0), false, "Packages removed."));
var userContext = RegisterObject("UserContext", "Runtime user context for the image.");
userContext.Properties.Add(new PropertySpec("uid", PrimitiveShape.Integer(minimum: 0), false, "User ID."));
userContext.Properties.Add(new PropertySpec("gid", PrimitiveShape.Integer(minimum: 0), false, "Group ID."));
userContext.Properties.Add(new PropertySpec("caps", new ArrayShape(PrimitiveShape.String(), 0), false, "Linux capabilities (string names)."));
var runtimeContext = RegisterObject("RuntimeContext", "Runtime context used for reachability gating and policy decisions.");
runtimeContext.Properties.Add(new PropertySpec("entrypoint", new ArrayShape(PrimitiveShape.String(), 0), false, "Entrypoint command array."));
runtimeContext.Properties.Add(new PropertySpec("env", new MapShape(PrimitiveShape.String()), false, "Environment variables map."));
runtimeContext.Properties.Add(new PropertySpec("user", new ObjectShape(userContext), false, "Runtime user context."));
var reachabilityGate = RegisterObject("ReachabilityGate", "3-bit reachability gate derived from the 7-state lattice.");
reachabilityGate.Properties.Add(new PropertySpec("reachable", PrimitiveShape.Boolean(), false, "True/false if reachability is known; absent indicates unknown."));
reachabilityGate.Properties.Add(new PropertySpec("configActivated", PrimitiveShape.Boolean(), false, "True if configuration activates the finding."));
reachabilityGate.Properties.Add(new PropertySpec("runningUser", PrimitiveShape.Boolean(), false, "True if running user enables the finding."));
reachabilityGate.Properties.Add(new PropertySpec("class", PrimitiveShape.Integer(minimum: -1, maximum: 7), true, "Derived 3-bit class (0..7), or -1 if any bit is unknown."));
reachabilityGate.Properties.Add(new PropertySpec("rationale", PrimitiveShape.String(), false, "Optional human-readable rationale for the gate."));
var scannerInfo = RegisterObject("ScannerInfo", "Scanner identity and ruleset information.");
scannerInfo.Properties.Add(new PropertySpec("name", PrimitiveShape.String(), true, "Scanner name."));
scannerInfo.Properties.Add(new PropertySpec("version", PrimitiveShape.String(), true, "Scanner version string."));
scannerInfo.Properties.Add(new PropertySpec("ruleset", PrimitiveShape.String(), false, "Optional ruleset identifier."));
var smartDiffPredicate = RegisterObject(
"SmartDiffPredicate",
"Smart-Diff predicate describing differential analysis between two scans.",
isRoot: true,
schemaFileStem: "stellaops-smart-diff.v1",
qualifiedVersion: "1.0.0");
smartDiffPredicate.Properties.Add(new PropertySpec("schemaVersion", PrimitiveShape.String(constValue: "1.0.0"), true, "Schema version (semver)."));
smartDiffPredicate.Properties.Add(new PropertySpec("baseImage", new ObjectShape(imageReference), true, "Base scan image reference."));
smartDiffPredicate.Properties.Add(new PropertySpec("targetImage", new ObjectShape(imageReference), true, "Target scan image reference."));
smartDiffPredicate.Properties.Add(new PropertySpec("diff", new ObjectShape(diffPayload), true, "Diff payload between base and target."));
smartDiffPredicate.Properties.Add(new PropertySpec("context", new ObjectShape(runtimeContext), false, "Optional runtime context."));
smartDiffPredicate.Properties.Add(new PropertySpec("reachabilityGate", new ObjectShape(reachabilityGate), true, "Derived reachability gate."));
smartDiffPredicate.Properties.Add(new PropertySpec("scanner", new ObjectShape(scannerInfo), true, "Scanner identity."));
smartDiffPredicate.Properties.Add(new PropertySpec("suppressedCount", PrimitiveShape.Integer(minimum: 0), false, "Number of findings suppressed by pre-filters."));
smartDiffPredicate.Properties.Add(new PropertySpec("materialChanges", new ArrayShape(new ObjectShape(materialChange), 0), false, "Optional list of material changes."));
return new TypeRegistry(roots, objects, enums);
}
}
@@ -336,6 +453,8 @@ internal sealed record ObjectShape(ObjectSpec Object) : TypeShape;
internal sealed record ArrayShape(TypeShape Item, int? MinItems = null) : TypeShape;
internal sealed record MapShape(TypeShape Value) : TypeShape;
internal static class SchemaBuilder
{
public static string Build(ObjectSpec root)
@@ -512,6 +631,13 @@ internal static class SchemaBuilder
return arraySchema;
case MapShape mapShape:
return new JsonObject
{
["type"] = "object",
["additionalProperties"] = BuildPropertySchema(mapShape.Value, defs, visitedObjects, visitedEnums)
};
default:
throw new InvalidOperationException("Unsupported property type.");
}
@@ -692,6 +818,9 @@ internal static class TypeScriptEmitter
case ArrayShape arrayShape:
lines.AddRange(EmitArrayAssertion(arrayShape, accessor, pathExpression));
break;
case MapShape mapShape:
lines.AddRange(EmitMapAssertion(mapShape, accessor, pathExpression));
break;
default:
lines.Add("// Unsupported type encountered during validation.");
break;
@@ -754,6 +883,29 @@ internal static class TypeScriptEmitter
return lines;
}
private static IReadOnlyList<string> EmitMapAssertion(MapShape mapShape, string accessor, string pathExpression)
{
var lines = new List<string>
{
"if (!isRecord(" + accessor + ")) {",
" throw new Error(`${pathString(" + pathExpression + ")} must be an object.`);",
"}"
};
lines.Add("for (const key of Object.keys(" + accessor + ")) {");
lines.Add(" const entry = (" + accessor + " as Record<string, unknown>)[key];");
lines.Add(" const entryPath = [..." + pathExpression + ", key];");
var childLines = EmitTsAssertion(mapShape.Value, "entry", "entryPath");
foreach (var line in childLines)
{
lines.Add(" " + line);
}
lines.Add("}");
return lines;
}
private static string ToTsType(TypeShape type)
=> type switch
{
@@ -766,6 +918,7 @@ internal static class TypeScriptEmitter
EnumShape enumShape => enumShape.Enum.Name,
ObjectShape objectShape => objectShape.Object.Name,
ArrayShape array => $"Array<{ToTsType(array.Item)}>",
MapShape map => $"Record<string, {ToTsType(map.Value)}>",
_ => "unknown"
};
}
@@ -898,6 +1051,9 @@ internal static class GoEmitter
case ArrayShape arrayShape:
EmitArrayValidation(builder, arrayShape, accessor, path, indent);
break;
case MapShape mapShape:
EmitMapValidation(builder, mapShape, accessor, path, indent);
break;
}
}
@@ -1014,6 +1170,62 @@ internal static class GoEmitter
}
}
private static void EmitMapValidation(StringBuilder builder, MapShape mapShape, string accessor, string path, int indent)
{
if (!NeedsArrayItemValidation(mapShape.Value))
{
return;
}
AppendLine(builder, indent, $"for key, item := range {accessor} {{");
EmitMapValueValidation(builder, mapShape.Value, "item", path, "key", indent + 1);
AppendLine(builder, indent, "}");
}
private static void EmitMapValueValidation(StringBuilder builder, TypeShape valueType, string accessor, string path, string keyVar, int indent)
{
switch (valueType)
{
case PrimitiveShape primitive:
EmitMapPrimitiveValidation(builder, primitive, accessor, path, keyVar, indent);
break;
case EnumShape:
AppendLine(builder, indent, $"if err := {accessor}.Validate(); err != nil {{");
AppendLine(builder, indent + 1, $"return fmt.Errorf(\"invalid {path}[%s]: %w\", {keyVar}, err)");
AppendLine(builder, indent, "}");
break;
case ObjectShape:
AppendLine(builder, indent, $"if err := {accessor}.Validate(); err != nil {{");
AppendLine(builder, indent + 1, $"return fmt.Errorf(\"invalid {path}[%s]: %w\", {keyVar}, err)");
AppendLine(builder, indent, "}");
break;
}
}
private static void EmitMapPrimitiveValidation(StringBuilder builder, PrimitiveShape primitive, string accessor, string path, string keyVar, int indent)
{
if (primitive.Kind == PrimitiveKind.String && !string.IsNullOrEmpty(primitive.ConstValue))
{
AppendLine(builder, indent, $"if {accessor} != \"{primitive.ConstValue}\" {{");
AppendLine(builder, indent + 1, $"return fmt.Errorf(\"{path}[%s] must equal {primitive.ConstValue}\", {keyVar})");
AppendLine(builder, indent, "}");
}
if ((primitive.Kind == PrimitiveKind.Number || primitive.Kind == PrimitiveKind.Integer) && primitive.Minimum.HasValue)
{
AppendLine(builder, indent, $"if {accessor} < {primitive.Minimum.Value} {{");
AppendLine(builder, indent + 1, $"return fmt.Errorf(\"{path}[%s] must be >= {primitive.Minimum.Value}\", {keyVar})");
AppendLine(builder, indent, "}");
}
if ((primitive.Kind == PrimitiveKind.Number || primitive.Kind == PrimitiveKind.Integer) && primitive.Maximum.HasValue)
{
AppendLine(builder, indent, $"if {accessor} > {primitive.Maximum.Value} {{");
AppendLine(builder, indent + 1, $"return fmt.Errorf(\"{path}[%s] must be <= {primitive.Maximum.Value}\", {keyVar})");
AppendLine(builder, indent, "}");
}
}
private static void EmitArrayItemValidation(StringBuilder builder, TypeShape itemType, string accessor, string path, string indexVar, int indent)
{
switch (itemType)
@@ -1091,6 +1303,7 @@ internal static class GoEmitter
ObjectShape objectShape when property.Required => objectShape.Object.Name,
ObjectShape objectShape => "*" + objectShape.Object.Name,
ArrayShape arrayShape => $"[]{GoElementType(arrayShape.Item)}",
MapShape mapShape => $"map[string]{GoElementType(mapShape.Value)}",
_ => "interface{}"
};
}
@@ -1104,6 +1317,7 @@ internal static class GoEmitter
EnumShape enumShape => enumShape.Enum.Name,
ObjectShape objectShape => objectShape.Object.Name,
ArrayShape nested => $"[]{GoElementType(nested.Item)}",
MapShape mapShape => $"map[string]{GoElementType(mapShape.Value)}",
_ => "interface{}"
};

View File

@@ -1,15 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="JsonSchema.Net" Version="7.3.2" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\StellaOps.Attestor.Types\samples\**\*.json" LinkBase="samples" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\StellaOps.Attestor.Types\schemas\**\*.schema.json" LinkBase="schemas" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>