Merge branch 'main' into HEAD
This commit is contained in:
@@ -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("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."));
|
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);
|
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 ArrayShape(TypeShape Item, int? MinItems = null) : TypeShape;
|
||||||
|
|
||||||
|
internal sealed record MapShape(TypeShape Value) : TypeShape;
|
||||||
|
|
||||||
internal static class SchemaBuilder
|
internal static class SchemaBuilder
|
||||||
{
|
{
|
||||||
public static string Build(ObjectSpec root)
|
public static string Build(ObjectSpec root)
|
||||||
@@ -512,6 +631,13 @@ internal static class SchemaBuilder
|
|||||||
|
|
||||||
return arraySchema;
|
return arraySchema;
|
||||||
|
|
||||||
|
case MapShape mapShape:
|
||||||
|
return new JsonObject
|
||||||
|
{
|
||||||
|
["type"] = "object",
|
||||||
|
["additionalProperties"] = BuildPropertySchema(mapShape.Value, defs, visitedObjects, visitedEnums)
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException("Unsupported property type.");
|
throw new InvalidOperationException("Unsupported property type.");
|
||||||
}
|
}
|
||||||
@@ -692,6 +818,9 @@ internal static class TypeScriptEmitter
|
|||||||
case ArrayShape arrayShape:
|
case ArrayShape arrayShape:
|
||||||
lines.AddRange(EmitArrayAssertion(arrayShape, accessor, pathExpression));
|
lines.AddRange(EmitArrayAssertion(arrayShape, accessor, pathExpression));
|
||||||
break;
|
break;
|
||||||
|
case MapShape mapShape:
|
||||||
|
lines.AddRange(EmitMapAssertion(mapShape, accessor, pathExpression));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
lines.Add("// Unsupported type encountered during validation.");
|
lines.Add("// Unsupported type encountered during validation.");
|
||||||
break;
|
break;
|
||||||
@@ -754,6 +883,29 @@ internal static class TypeScriptEmitter
|
|||||||
return lines;
|
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)
|
private static string ToTsType(TypeShape type)
|
||||||
=> type switch
|
=> type switch
|
||||||
{
|
{
|
||||||
@@ -766,6 +918,7 @@ internal static class TypeScriptEmitter
|
|||||||
EnumShape enumShape => enumShape.Enum.Name,
|
EnumShape enumShape => enumShape.Enum.Name,
|
||||||
ObjectShape objectShape => objectShape.Object.Name,
|
ObjectShape objectShape => objectShape.Object.Name,
|
||||||
ArrayShape array => $"Array<{ToTsType(array.Item)}>",
|
ArrayShape array => $"Array<{ToTsType(array.Item)}>",
|
||||||
|
MapShape map => $"Record<string, {ToTsType(map.Value)}>",
|
||||||
_ => "unknown"
|
_ => "unknown"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -898,6 +1051,9 @@ internal static class GoEmitter
|
|||||||
case ArrayShape arrayShape:
|
case ArrayShape arrayShape:
|
||||||
EmitArrayValidation(builder, arrayShape, accessor, path, indent);
|
EmitArrayValidation(builder, arrayShape, accessor, path, indent);
|
||||||
break;
|
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)
|
private static void EmitArrayItemValidation(StringBuilder builder, TypeShape itemType, string accessor, string path, string indexVar, int indent)
|
||||||
{
|
{
|
||||||
switch (itemType)
|
switch (itemType)
|
||||||
@@ -1091,6 +1303,7 @@ internal static class GoEmitter
|
|||||||
ObjectShape objectShape when property.Required => objectShape.Object.Name,
|
ObjectShape objectShape when property.Required => objectShape.Object.Name,
|
||||||
ObjectShape objectShape => "*" + objectShape.Object.Name,
|
ObjectShape objectShape => "*" + objectShape.Object.Name,
|
||||||
ArrayShape arrayShape => $"[]{GoElementType(arrayShape.Item)}",
|
ArrayShape arrayShape => $"[]{GoElementType(arrayShape.Item)}",
|
||||||
|
MapShape mapShape => $"map[string]{GoElementType(mapShape.Value)}",
|
||||||
_ => "interface{}"
|
_ => "interface{}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1104,6 +1317,7 @@ internal static class GoEmitter
|
|||||||
EnumShape enumShape => enumShape.Enum.Name,
|
EnumShape enumShape => enumShape.Enum.Name,
|
||||||
ObjectShape objectShape => objectShape.Object.Name,
|
ObjectShape objectShape => objectShape.Object.Name,
|
||||||
ArrayShape nested => $"[]{GoElementType(nested.Item)}",
|
ArrayShape nested => $"[]{GoElementType(nested.Item)}",
|
||||||
|
MapShape mapShape => $"map[string]{GoElementType(mapShape.Value)}",
|
||||||
_ => "interface{}"
|
_ => "interface{}"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<UseConcelierTestInfra>false</UseConcelierTestInfra>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
<PackageReference Include="FluentAssertions" Version="6.12.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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\..\StellaOps.Attestor.Types\samples\**\*.json" LinkBase="samples" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="..\..\StellaOps.Attestor.Types\samples\**\*.json" LinkBase="samples" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
<None Include="..\..\StellaOps.Attestor.Types\schemas\**\*.schema.json" LinkBase="schemas" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
53
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceBundle.cs
Normal file
53
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceBundle.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace StellaOps.Evidence.Bundle;
|
||||||
|
|
||||||
|
/// <summary>A complete evidence bundle for a single finding/alert. Contains all evidence required for triage decision.</summary>
|
||||||
|
public sealed class EvidenceBundle
|
||||||
|
{
|
||||||
|
public string BundleId { get; init; } = Guid.NewGuid().ToString("N");
|
||||||
|
public string SchemaVersion { get; init; } = "1.0";
|
||||||
|
public required string AlertId { get; init; }
|
||||||
|
public required string ArtifactId { get; init; }
|
||||||
|
public ReachabilityEvidence? Reachability { get; init; }
|
||||||
|
public CallStackEvidence? CallStack { get; init; }
|
||||||
|
public ProvenanceEvidence? Provenance { get; init; }
|
||||||
|
public VexStatusEvidence? VexStatus { get; init; }
|
||||||
|
public DiffEvidence? Diff { get; init; }
|
||||||
|
public GraphRevisionEvidence? GraphRevision { get; init; }
|
||||||
|
public required EvidenceHashSet Hashes { get; init; }
|
||||||
|
public required DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Compute evidence completeness score (0-4 based on core evidence types).</summary>
|
||||||
|
public int ComputeCompletenessScore()
|
||||||
|
{
|
||||||
|
var score = 0;
|
||||||
|
if (Reachability?.Status == EvidenceStatus.Available) score++;
|
||||||
|
if (CallStack?.Status == EvidenceStatus.Available) score++;
|
||||||
|
if (Provenance?.Status == EvidenceStatus.Available) score++;
|
||||||
|
if (VexStatus?.Status == EvidenceStatus.Available) score++;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Create status summary from evidence.</summary>
|
||||||
|
public EvidenceStatusSummary CreateStatusSummary() => new()
|
||||||
|
{
|
||||||
|
Reachability = Reachability?.Status ?? EvidenceStatus.Unavailable,
|
||||||
|
CallStack = CallStack?.Status ?? EvidenceStatus.Unavailable,
|
||||||
|
Provenance = Provenance?.Status ?? EvidenceStatus.Unavailable,
|
||||||
|
VexStatus = VexStatus?.Status ?? EvidenceStatus.Unavailable,
|
||||||
|
Diff = Diff?.Status ?? EvidenceStatus.Unavailable,
|
||||||
|
GraphRevision = GraphRevision?.Status ?? EvidenceStatus.Unavailable
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Create DSSE predicate for signing.</summary>
|
||||||
|
public EvidenceBundlePredicate ToSigningPredicate() => new()
|
||||||
|
{
|
||||||
|
BundleId = BundleId,
|
||||||
|
AlertId = AlertId,
|
||||||
|
ArtifactId = ArtifactId,
|
||||||
|
CompletenessScore = ComputeCompletenessScore(),
|
||||||
|
Hashes = Hashes,
|
||||||
|
StatusSummary = CreateStatusSummary(),
|
||||||
|
CreatedAt = CreatedAt,
|
||||||
|
SchemaVersion = SchemaVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
namespace StellaOps.Evidence.Bundle;
|
||||||
|
|
||||||
|
/// <summary>Builder for constructing evidence bundles.</summary>
|
||||||
|
public sealed class EvidenceBundleBuilder
|
||||||
|
{
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
private string? _alertId;
|
||||||
|
private string? _artifactId;
|
||||||
|
private ReachabilityEvidence? _reachability;
|
||||||
|
private CallStackEvidence? _callStack;
|
||||||
|
private ProvenanceEvidence? _provenance;
|
||||||
|
private VexStatusEvidence? _vexStatus;
|
||||||
|
private DiffEvidence? _diff;
|
||||||
|
private GraphRevisionEvidence? _graphRevision;
|
||||||
|
|
||||||
|
public EvidenceBundleBuilder(TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public EvidenceBundleBuilder() : this(TimeProvider.System) { }
|
||||||
|
|
||||||
|
public EvidenceBundleBuilder WithAlertId(string alertId) { _alertId = alertId; return this; }
|
||||||
|
public EvidenceBundleBuilder WithArtifactId(string artifactId) { _artifactId = artifactId; return this; }
|
||||||
|
public EvidenceBundleBuilder WithReachability(ReachabilityEvidence evidence) { _reachability = evidence; return this; }
|
||||||
|
public EvidenceBundleBuilder WithCallStack(CallStackEvidence evidence) { _callStack = evidence; return this; }
|
||||||
|
public EvidenceBundleBuilder WithProvenance(ProvenanceEvidence evidence) { _provenance = evidence; return this; }
|
||||||
|
public EvidenceBundleBuilder WithVexStatus(VexStatusEvidence evidence) { _vexStatus = evidence; return this; }
|
||||||
|
public EvidenceBundleBuilder WithDiff(DiffEvidence evidence) { _diff = evidence; return this; }
|
||||||
|
public EvidenceBundleBuilder WithGraphRevision(GraphRevisionEvidence evidence) { _graphRevision = evidence; return this; }
|
||||||
|
|
||||||
|
public EvidenceBundle Build()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_alertId))
|
||||||
|
throw new InvalidOperationException("AlertId is required");
|
||||||
|
if (string.IsNullOrWhiteSpace(_artifactId))
|
||||||
|
throw new InvalidOperationException("ArtifactId is required");
|
||||||
|
|
||||||
|
var hashes = new Dictionary<string, string>();
|
||||||
|
if (_reachability?.Hash is not null) hashes["reachability"] = _reachability.Hash;
|
||||||
|
if (_callStack?.Hash is not null) hashes["callstack"] = _callStack.Hash;
|
||||||
|
if (_provenance?.Hash is not null) hashes["provenance"] = _provenance.Hash;
|
||||||
|
if (_vexStatus?.Hash is not null) hashes["vex"] = _vexStatus.Hash;
|
||||||
|
if (_diff?.Hash is not null) hashes["diff"] = _diff.Hash;
|
||||||
|
if (_graphRevision?.Hash is not null) hashes["graph"] = _graphRevision.Hash;
|
||||||
|
|
||||||
|
return new EvidenceBundle
|
||||||
|
{
|
||||||
|
AlertId = _alertId,
|
||||||
|
ArtifactId = _artifactId,
|
||||||
|
Reachability = _reachability,
|
||||||
|
CallStack = _callStack,
|
||||||
|
Provenance = _provenance,
|
||||||
|
VexStatus = _vexStatus,
|
||||||
|
Diff = _diff,
|
||||||
|
GraphRevision = _graphRevision,
|
||||||
|
Hashes = hashes.Count > 0 ? EvidenceHashSet.Compute(hashes) : EvidenceHashSet.Empty(),
|
||||||
|
CreatedAt = _timeProvider.GetUtcNow()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace StellaOps.Evidence.Bundle;
|
||||||
|
|
||||||
|
/// <summary>DSSE predicate for signed evidence bundles. Predicate type: stellaops.dev/predicates/evidence-bundle@v1</summary>
|
||||||
|
public sealed class EvidenceBundlePredicate
|
||||||
|
{
|
||||||
|
public const string PredicateType = "stellaops.dev/predicates/evidence-bundle@v1";
|
||||||
|
public required string BundleId { get; init; }
|
||||||
|
public required string AlertId { get; init; }
|
||||||
|
public required string ArtifactId { get; init; }
|
||||||
|
public required int CompletenessScore { get; init; }
|
||||||
|
public required EvidenceHashSet Hashes { get; init; }
|
||||||
|
public required EvidenceStatusSummary StatusSummary { get; init; }
|
||||||
|
public required DateTimeOffset CreatedAt { get; init; }
|
||||||
|
public string SchemaVersion { get; init; } = "1.0";
|
||||||
|
}
|
||||||
37
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceHashSet.cs
Normal file
37
src/__Libraries/StellaOps.Evidence.Bundle/EvidenceHashSet.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace StellaOps.Evidence.Bundle;
|
||||||
|
|
||||||
|
/// <summary>Content-addressed hash set for all evidence artifacts.</summary>
|
||||||
|
public sealed class EvidenceHashSet
|
||||||
|
{
|
||||||
|
public string Algorithm { get; init; } = "SHA-256";
|
||||||
|
public required IReadOnlyList<string> Hashes { get; init; }
|
||||||
|
public required string CombinedHash { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, string>? LabeledHashes { get; init; }
|
||||||
|
|
||||||
|
public static EvidenceHashSet Compute(IDictionary<string, string> labeledHashes)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(labeledHashes);
|
||||||
|
var sorted = labeledHashes.OrderBy(kvp => kvp.Key, StringComparer.Ordinal).ToList();
|
||||||
|
var combined = string.Join(":", sorted.Select(kvp => $"{kvp.Key}={kvp.Value}"));
|
||||||
|
var hash = ComputeSha256(combined);
|
||||||
|
return new EvidenceHashSet
|
||||||
|
{
|
||||||
|
Hashes = sorted.Select(kvp => kvp.Value).ToList(),
|
||||||
|
CombinedHash = hash,
|
||||||
|
LabeledHashes = sorted.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EvidenceHashSet Empty() => new()
|
||||||
|
{
|
||||||
|
Hashes = Array.Empty<string>(),
|
||||||
|
CombinedHash = ComputeSha256(string.Empty),
|
||||||
|
LabeledHashes = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ComputeSha256(string input) =>
|
||||||
|
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))).ToLowerInvariant();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace StellaOps.Evidence.Bundle;
|
||||||
|
|
||||||
|
/// <summary>Summary of evidence status for all evidence types.</summary>
|
||||||
|
public sealed class EvidenceStatusSummary
|
||||||
|
{
|
||||||
|
public required EvidenceStatus Reachability { get; init; }
|
||||||
|
public required EvidenceStatus CallStack { get; init; }
|
||||||
|
public required EvidenceStatus Provenance { get; init; }
|
||||||
|
public required EvidenceStatus VexStatus { get; init; }
|
||||||
|
public EvidenceStatus Diff { get; init; } = EvidenceStatus.Unavailable;
|
||||||
|
public EvidenceStatus GraphRevision { get; init; } = EvidenceStatus.Unavailable;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace StellaOps.Evidence.Bundle;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddEvidenceBundleServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
services.TryAddSingleton(TimeProvider.System);
|
||||||
|
services.TryAddTransient<EvidenceBundleBuilder>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddEvidenceBundleServices(this IServiceCollection services, TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||||
|
services.AddSingleton(timeProvider);
|
||||||
|
services.TryAddTransient<EvidenceBundleBuilder>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user