old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions

This commit is contained in:
master
2026-01-15 18:37:59 +02:00
parent c631bacee2
commit 88a85cdd92
208 changed files with 32271 additions and 2287 deletions

View File

@@ -1,8 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) 2025 StellaOps
using StellaOps.Signals.EvidenceWeightedScore;
namespace StellaOps.Signals.EvidenceWeightedScore.Normalizers;
/// <summary>
/// Anchor metadata for evidence attestation.
/// Sprint: SPRINT_20260112_004_BE_findings_scoring_attested_reduction (EWS-API-002)
/// </summary>
public sealed record EvidenceAnchor
{
/// <summary>Whether the evidence is anchored (has attestation).</summary>
public required bool Anchored { get; init; }
/// <summary>DSSE envelope digest if anchored.</summary>
public string? EnvelopeDigest { get; init; }
/// <summary>Predicate type of the attestation.</summary>
public string? PredicateType { get; init; }
/// <summary>Rekor log index if transparency-anchored.</summary>
public long? RekorLogIndex { get; init; }
/// <summary>Rekor entry ID if transparency-anchored.</summary>
public string? RekorEntryId { get; init; }
/// <summary>Scope of the attestation (e.g., finding, package, image).</summary>
public string? Scope { get; init; }
/// <summary>Verification status of the anchor.</summary>
public bool? Verified { get; init; }
/// <summary>When the attestation was created.</summary>
public DateTimeOffset? AttestedAt { get; init; }
/// <summary>
/// Creates an unanchored evidence anchor.
/// </summary>
public static EvidenceAnchor Unanchored => new() { Anchored = false };
}
/// <summary>
/// Aggregated evidence from all sources for a single finding.
/// Used as input to the normalizer aggregator.
@@ -31,6 +69,29 @@ public sealed record FindingEvidence
/// <summary>Active mitigations evidence (maps to MitigationInput).</summary>
public MitigationInput? Mitigations { get; init; }
// Sprint: SPRINT_20260112_004_BE_findings_scoring_attested_reduction (EWS-API-002)
/// <summary>
/// Anchor metadata for the primary evidence source.
/// Populated when evidence has attestation/DSSE anchoring.
/// </summary>
public EvidenceAnchor? Anchor { get; init; }
/// <summary>
/// Anchor metadata for reachability evidence.
/// </summary>
public EvidenceAnchor? ReachabilityAnchor { get; init; }
/// <summary>
/// Anchor metadata for runtime evidence.
/// </summary>
public EvidenceAnchor? RuntimeAnchor { get; init; }
/// <summary>
/// Anchor metadata for VEX/mitigation evidence.
/// </summary>
public EvidenceAnchor? VexAnchor { get; init; }
/// <summary>
/// Creates FindingEvidence from an existing EvidenceWeightedScoreInput.
/// Extracts the detailed input records if present.

View File

@@ -1,9 +1,16 @@
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Services;
public interface IEventsPublisher
{
Task PublishFactUpdatedAsync(global::StellaOps.Signals.Models.ReachabilityFactDocument fact, CancellationToken cancellationToken);
/// <summary>
/// Publishes a runtime.updated event when runtime observations change.
/// Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-002)
/// </summary>
Task PublishRuntimeUpdatedAsync(RuntimeUpdatedEvent runtimeEvent, CancellationToken cancellationToken);
}

View File

@@ -36,4 +36,14 @@ internal sealed class InMemoryEventsPublisher : IEventsPublisher
logger.LogInformation(json);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task PublishRuntimeUpdatedAsync(RuntimeUpdatedEvent runtimeEvent, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(runtimeEvent);
var json = JsonSerializer.Serialize(runtimeEvent, SerializerOptions);
logger.LogInformation("RuntimeUpdated: {Json}", json);
return Task.CompletedTask;
}
}

View File

@@ -146,4 +146,25 @@ internal sealed class MessagingEventsPublisher : IEventsPublisher
_logger.LogWarning(ex, "Failed to publish reachability event to DLQ stream {Stream}.", _options.DeadLetterStream);
}
}
/// <inheritdoc />
public Task PublishRuntimeUpdatedAsync(RuntimeUpdatedEvent runtimeEvent, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(runtimeEvent);
cancellationToken.ThrowIfCancellationRequested();
if (!_options.Enabled)
{
return Task.CompletedTask;
}
// For now, log the event. Full stream publishing will be added when runtime event stream is provisioned.
_logger.LogInformation(
"RuntimeUpdatedEvent: Subject={SubjectKey}, Type={UpdateType}, TriggerReanalysis={TriggerReanalysis}",
runtimeEvent.SubjectKey,
runtimeEvent.UpdateType,
runtimeEvent.TriggerReanalysis);
return Task.CompletedTask;
}
}

View File

@@ -2,9 +2,13 @@ using System.Threading;
using System.Threading.Tasks;
using StellaOps.Signals.Models;
using StellaOps.Signals.Models;
namespace StellaOps.Signals.Services;
internal sealed class NullEventsPublisher : IEventsPublisher
{
public Task PublishFactUpdatedAsync(ReachabilityFactDocument fact, CancellationToken cancellationToken) => Task.CompletedTask;
public Task PublishRuntimeUpdatedAsync(RuntimeUpdatedEvent runtimeEvent, CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -157,6 +157,53 @@ internal sealed class RedisEventsPublisher : IEventsPublisher, IAsyncDisposable
}
}
/// <inheritdoc />
public async Task PublishRuntimeUpdatedAsync(RuntimeUpdatedEvent runtimeEvent, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(runtimeEvent);
cancellationToken.ThrowIfCancellationRequested();
if (!options.Enabled)
{
return;
}
var json = JsonSerializer.Serialize(runtimeEvent, SerializerOptions);
try
{
var database = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
var entries = new[]
{
new NameValueEntry("event", json),
new NameValueEntry("event_id", runtimeEvent.EventId),
new NameValueEntry("event_type", RuntimeEventTypes.Updated),
new NameValueEntry("subject_key", runtimeEvent.SubjectKey),
new NameValueEntry("evidence_digest", runtimeEvent.EvidenceDigest),
new NameValueEntry("trigger_reanalysis", runtimeEvent.TriggerReanalysis.ToString(CultureInfo.InvariantCulture))
};
var streamName = options.Stream + ":runtime";
var publishTask = maxStreamLength.HasValue
? database.StreamAddAsync(streamName, entries, maxLength: maxStreamLength, useApproximateMaxLength: true)
: database.StreamAddAsync(streamName, entries);
if (publishTimeout > TimeSpan.Zero)
{
await publishTask.WaitAsync(publishTimeout, cancellationToken).ConfigureAwait(false);
}
else
{
await publishTask.ConfigureAwait(false);
}
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to publish runtime.updated event to Redis stream.");
}
}
public async ValueTask DisposeAsync()
{
if (disposed)

View File

@@ -94,6 +94,61 @@ internal sealed class RouterEventsPublisher : IEventsPublisher
}
}
/// <inheritdoc />
public async Task PublishRuntimeUpdatedAsync(RuntimeUpdatedEvent runtimeEvent, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(runtimeEvent);
cancellationToken.ThrowIfCancellationRequested();
var json = JsonSerializer.Serialize(runtimeEvent, SerializerOptions);
try
{
using var request = new HttpRequestMessage(HttpMethod.Post, options.Events.Router.Path);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
request.Headers.TryAddWithoutValidation("X-Signals-Topic", RuntimeEventTypes.Updated);
request.Headers.TryAddWithoutValidation("X-Signals-Tenant", runtimeEvent.Tenant);
request.Headers.TryAddWithoutValidation("X-Signals-Pipeline", options.Events.Pipeline);
if (!string.IsNullOrWhiteSpace(options.Events.Router.ApiKey))
{
request.Headers.TryAddWithoutValidation(
string.IsNullOrWhiteSpace(options.Events.Router.ApiKeyHeader)
? "X-API-Key"
: options.Events.Router.ApiKeyHeader,
options.Events.Router.ApiKey);
}
foreach (var header in options.Events.Router.Headers)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var body = response.Content is null
? string.Empty
: await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Router publish failed for {Topic} with status {StatusCode}: {Body}",
RuntimeEventTypes.Updated,
(int)response.StatusCode,
Truncate(body, 256));
}
else
{
logger.LogInformation(
"Router publish succeeded for runtime.updated ({StatusCode})",
(int)response.StatusCode);
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Router publish failed for runtime.updated");
}
}
private static string Truncate(string value, int maxLength)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)

View File

@@ -94,6 +94,15 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
await cache.SetAsync(persisted, cancellationToken).ConfigureAwait(false);
await eventsPublisher.PublishFactUpdatedAsync(persisted, cancellationToken).ConfigureAwait(false);
// Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-002)
// Emit runtime.updated event for policy reanalysis
await EmitRuntimeUpdatedEventAsync(
persisted,
existing,
aggregated,
request,
cancellationToken).ConfigureAwait(false);
await RecomputeReachabilityAsync(persisted, aggregated, request, cancellationToken).ConfigureAwait(false);
logger.LogInformation(
@@ -636,4 +645,119 @@ public sealed class RuntimeFactsIngestionService : IRuntimeFactsIngestionService
return hash.ToHashCode();
}
}
/// <summary>
/// Emits a runtime.updated event when runtime observations change.
/// Sprint: SPRINT_20260112_008_SIGNALS_runtime_telemetry_events (SIG-RUN-002)
/// </summary>
private async Task EmitRuntimeUpdatedEventAsync(
ReachabilityFactDocument persisted,
ReachabilityFactDocument? existing,
IReadOnlyList<RuntimeFact> aggregated,
RuntimeFactsIngestRequest request,
CancellationToken cancellationToken)
{
// Determine update type based on existing state
var updateType = DetermineUpdateType(existing, aggregated);
// Extract node hashes from runtime facts
var observedNodeHashes = aggregated
.Where(f => !string.IsNullOrWhiteSpace(f.SymbolDigest))
.Select(f => f.SymbolDigest!)
.Distinct(StringComparer.Ordinal)
.OrderBy(h => h, StringComparer.Ordinal)
.ToList();
// Compute evidence digest from the persisted document
var evidenceDigest = ComputeEvidenceDigest(persisted);
// Determine previous and new state
var previousState = existing?.RuntimeFacts?.Any() == true ? "observed" : null;
var newState = "observed";
// Extract tenant from metadata
var tenant = request.Metadata?.TryGetValue("tenant_id", out var t) == true ? t ?? "default" : "default";
// Compute confidence based on hit counts
var totalHits = aggregated.Sum(f => f.HitCount);
var confidence = Math.Min(1.0, 0.5 + (totalHits * 0.01)); // Base 0.5, +0.01 per hit, max 1.0
var runtimeEvent = RuntimeUpdatedEventFactory.Create(
tenant: tenant,
subjectKey: persisted.SubjectKey,
evidenceDigest: evidenceDigest,
updateType: updateType,
newState: newState,
confidence: confidence,
fromRuntime: true,
occurredAtUtc: timeProvider.GetUtcNow(),
cveId: request.Subject.CveId,
purl: request.Subject.Purl,
callgraphId: request.CallgraphId,
previousState: previousState,
runtimeMethod: request.Metadata?.TryGetValue("source", out var src) == true ? src : "ebpf",
observedNodeHashes: observedNodeHashes,
pathHash: null,
traceId: request.Metadata?.TryGetValue("trace_id", out var traceId) == true ? traceId : null);
await eventsPublisher.PublishRuntimeUpdatedAsync(runtimeEvent, cancellationToken).ConfigureAwait(false);
if (runtimeEvent.TriggerReanalysis)
{
logger.LogInformation(
"Emitted runtime.updated event for {SubjectKey} with reanalysis trigger: {Reason}",
persisted.SubjectKey,
runtimeEvent.ReanalysisReason);
}
}
private static RuntimeUpdateType DetermineUpdateType(
ReachabilityFactDocument? existing,
IReadOnlyList<RuntimeFact> newFacts)
{
if (existing?.RuntimeFacts is null || existing.RuntimeFacts.Count == 0)
{
return RuntimeUpdateType.NewObservation;
}
var existingSymbols = existing.RuntimeFacts
.Select(f => f.SymbolId)
.ToHashSet(StringComparer.Ordinal);
var newSymbols = newFacts
.Select(f => f.SymbolId)
.Where(s => !existingSymbols.Contains(s))
.ToList();
if (newSymbols.Count > 0)
{
return RuntimeUpdateType.NewCallPath;
}
// Check for confidence increase (more hits)
var existingTotalHits = existing.RuntimeFacts.Sum(f => f.HitCount);
var newTotalHits = newFacts.Sum(f => f.HitCount);
if (newTotalHits > existingTotalHits)
{
return RuntimeUpdateType.ConfidenceIncrease;
}
return RuntimeUpdateType.StateChange;
}
private static string ComputeEvidenceDigest(ReachabilityFactDocument document)
{
// Create a deterministic digest from key fields
var content = string.Join("|",
document.SubjectKey ?? string.Empty,
document.CallgraphId ?? string.Empty,
document.RuntimeFacts?.Count.ToString(CultureInfo.InvariantCulture) ?? "0",
document.RuntimeFacts?.Sum(f => f.HitCount).ToString(CultureInfo.InvariantCulture) ?? "0",
document.ComputedAt.ToString("O", CultureInfo.InvariantCulture));
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(content));
return "sha256:" + Convert.ToHexStringLower(hash);
}
}