old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -5,6 +5,7 @@ namespace StellaOps.Policy.Determinization.Models;
|
||||
/// <summary>
|
||||
/// Result of determinization evaluation.
|
||||
/// Combines observation state, uncertainty score, and guardrails.
|
||||
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-001)
|
||||
/// </summary>
|
||||
public sealed record DeterminizationResult
|
||||
{
|
||||
@@ -50,6 +51,13 @@ public sealed record DeterminizationResult
|
||||
[JsonPropertyName("rationale")]
|
||||
public string? Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reanalysis fingerprint for deterministic replay.
|
||||
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-001)
|
||||
/// </summary>
|
||||
[JsonPropertyName("fingerprint")]
|
||||
public ReanalysisFingerprint? Fingerprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates result for determined observation (low uncertainty).
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
// <copyright file="ReanalysisFingerprint.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-001)
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic fingerprint for reanalysis triggering and replay verification.
|
||||
/// Content-addressed to enable reproducible policy evaluations.
|
||||
/// </summary>
|
||||
public sealed record ReanalysisFingerprint
|
||||
{
|
||||
private static readonly JsonSerializerOptions CanonicalOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressed fingerprint ID (sha256:...).
|
||||
/// </summary>
|
||||
[JsonPropertyName("fingerprint_id")]
|
||||
public required string FingerprintId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE bundle digest for evidence provenance.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dsse_bundle_digest")]
|
||||
public string? DsseBundleDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sorted list of evidence digests contributing to this fingerprint.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidence_digests")]
|
||||
public IReadOnlyList<string> EvidenceDigests { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Tool versions used for evaluation (deterministic ordering).
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_versions")]
|
||||
public IReadOnlyDictionary<string, string> ToolVersions { get; init; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Product version under evaluation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("product_version")]
|
||||
public string? ProductVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy configuration hash at evaluation time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policy_config_hash")]
|
||||
public string? PolicyConfigHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal weights hash for determinism verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signal_weights_hash")]
|
||||
public string? SignalWeightsHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this fingerprint was computed (UTC ISO-8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("computed_at")]
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Triggers that caused this reanalysis.
|
||||
/// </summary>
|
||||
[JsonPropertyName("triggers")]
|
||||
public IReadOnlyList<ReanalysisTrigger> Triggers { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Suggested next actions based on current state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("next_actions")]
|
||||
public IReadOnlyList<string> NextActions { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger that caused a reanalysis.
|
||||
/// </summary>
|
||||
public sealed record ReanalysisTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// Event type that triggered reanalysis (e.g., epss.updated, vex.changed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("event_type")]
|
||||
public required string EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event version for schema compatibility.
|
||||
/// </summary>
|
||||
[JsonPropertyName("event_version")]
|
||||
public int EventVersion { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Source of the event (e.g., scanner, excititor, signals).
|
||||
/// </summary>
|
||||
[JsonPropertyName("source")]
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the event was received (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("received_at")]
|
||||
public DateTimeOffset ReceivedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event correlation ID for traceability.
|
||||
/// </summary>
|
||||
[JsonPropertyName("correlation_id")]
|
||||
public string? CorrelationId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating deterministic reanalysis fingerprints.
|
||||
/// </summary>
|
||||
public sealed class ReanalysisFingerprintBuilder
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private string? _dsseBundleDigest;
|
||||
private readonly List<string> _evidenceDigests = [];
|
||||
private readonly SortedDictionary<string, string> _toolVersions = new(StringComparer.Ordinal);
|
||||
private string? _productVersion;
|
||||
private string? _policyConfigHash;
|
||||
private string? _signalWeightsHash;
|
||||
private readonly List<ReanalysisTrigger> _triggers = [];
|
||||
private readonly List<string> _nextActions = [];
|
||||
|
||||
public ReanalysisFingerprintBuilder(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithDsseBundleDigest(string? digest)
|
||||
{
|
||||
_dsseBundleDigest = digest;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddEvidenceDigest(string digest)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
_evidenceDigests.Add(digest);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddEvidenceDigests(IEnumerable<string> digests)
|
||||
{
|
||||
foreach (var digest in digests)
|
||||
{
|
||||
AddEvidenceDigest(digest);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithToolVersion(string tool, string version)
|
||||
{
|
||||
_toolVersions[tool] = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithProductVersion(string? version)
|
||||
{
|
||||
_productVersion = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithPolicyConfigHash(string? hash)
|
||||
{
|
||||
_policyConfigHash = hash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithSignalWeightsHash(string? hash)
|
||||
{
|
||||
_signalWeightsHash = hash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddTrigger(ReanalysisTrigger trigger)
|
||||
{
|
||||
_triggers.Add(trigger);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddTrigger(string eventType, int eventVersion = 1, string? source = null, string? correlationId = null)
|
||||
{
|
||||
_triggers.Add(new ReanalysisTrigger
|
||||
{
|
||||
EventType = eventType,
|
||||
EventVersion = eventVersion,
|
||||
Source = source,
|
||||
ReceivedAt = _timeProvider.GetUtcNow(),
|
||||
CorrelationId = correlationId
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddNextAction(string action)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(action))
|
||||
{
|
||||
_nextActions.Add(action);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the fingerprint with a deterministic content-addressed ID.
|
||||
/// </summary>
|
||||
public ReanalysisFingerprint Build()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Sort evidence digests for determinism
|
||||
var sortedDigests = _evidenceDigests
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(d => d, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// Sort triggers by event type then received_at for determinism
|
||||
var sortedTriggers = _triggers
|
||||
.OrderBy(t => t.EventType, StringComparer.Ordinal)
|
||||
.ThenBy(t => t.ReceivedAt)
|
||||
.ToList();
|
||||
|
||||
// Sort next actions for determinism
|
||||
var sortedActions = _nextActions
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(a => a, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// Compute content-addressed fingerprint ID
|
||||
var fingerprintId = ComputeFingerprintId(
|
||||
_dsseBundleDigest,
|
||||
sortedDigests,
|
||||
_toolVersions,
|
||||
_productVersion,
|
||||
_policyConfigHash,
|
||||
_signalWeightsHash);
|
||||
|
||||
return new ReanalysisFingerprint
|
||||
{
|
||||
FingerprintId = fingerprintId,
|
||||
DsseBundleDigest = _dsseBundleDigest,
|
||||
EvidenceDigests = sortedDigests,
|
||||
ToolVersions = new Dictionary<string, string>(_toolVersions),
|
||||
ProductVersion = _productVersion,
|
||||
PolicyConfigHash = _policyConfigHash,
|
||||
SignalWeightsHash = _signalWeightsHash,
|
||||
ComputedAt = now,
|
||||
Triggers = sortedTriggers,
|
||||
NextActions = sortedActions
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeFingerprintId(
|
||||
string? dsseBundleDigest,
|
||||
IReadOnlyList<string> evidenceDigests,
|
||||
IReadOnlyDictionary<string, string> toolVersions,
|
||||
string? productVersion,
|
||||
string? policyConfigHash,
|
||||
string? signalWeightsHash)
|
||||
{
|
||||
// Create canonical representation for hashing
|
||||
var canonical = new
|
||||
{
|
||||
dsse = dsseBundleDigest,
|
||||
evidence = evidenceDigests,
|
||||
tools = toolVersions,
|
||||
product = productVersion,
|
||||
policy = policyConfigHash,
|
||||
weights = signalWeightsHash
|
||||
};
|
||||
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(canonical, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
});
|
||||
|
||||
var hash = SHA256.HashData(json);
|
||||
return "sha256:" + Convert.ToHexStringLower(hash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// <copyright file="SignalConflictExtensions.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-002)
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for signal conflict detection.
|
||||
/// </summary>
|
||||
public static class SignalConflictExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if VEX status is "not_affected".
|
||||
/// </summary>
|
||||
public static bool IsNotAffected(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
return vex.HasValue && vex.Value!.IsNotAffected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if VEX status is "affected".
|
||||
/// </summary>
|
||||
public static bool IsAffected(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
return vex.HasValue && string.Equals(vex.Value!.Status, "affected", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if reachability shows exploitable path.
|
||||
/// </summary>
|
||||
public static bool IsExploitable(this SignalState<ReachabilityEvidence> reachability)
|
||||
{
|
||||
return reachability.HasValue && reachability.Value!.IsReachable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if static analysis shows unreachable.
|
||||
/// </summary>
|
||||
public static bool IsStaticUnreachable(this SignalState<ReachabilityEvidence> reachability)
|
||||
{
|
||||
return reachability.HasValue && reachability.Value!.Status == ReachabilityStatus.Unreachable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if runtime telemetry detected execution.
|
||||
/// </summary>
|
||||
public static bool HasExecution(this SignalState<RuntimeEvidence> runtime)
|
||||
{
|
||||
return runtime.HasValue && runtime.Value!.Detected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if multiple VEX sources exist.
|
||||
/// </summary>
|
||||
public static bool HasMultipleSources(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
return vex.HasValue && vex.Value!.StatementCount > 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if VEX sources have conflicting status.
|
||||
/// This is determined by low confidence when multiple sources exist.
|
||||
/// </summary>
|
||||
public static bool HasConflictingStatus(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
// If there are multiple sources and confidence is below 0.7, they likely conflict
|
||||
return vex.HasValue && vex.Value!.StatementCount > 1 && vex.Value!.Confidence < 0.7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if backport evidence indicates fix is applied.
|
||||
/// </summary>
|
||||
public static bool IsBackported(this SignalState<BackportEvidence> backport)
|
||||
{
|
||||
return backport.HasValue && backport.Value!.Detected;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user