Add signal contracts for reachability, exploitability, trust, and unknown symbols
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals DSSE Sign & Evidence Locker / sign-signals-artifacts (push) Has been cancelled
Signals DSSE Sign & Evidence Locker / verify-signatures (push) Has been cancelled

- Introduced `ReachabilityState`, `RuntimeHit`, `ExploitabilitySignal`, `ReachabilitySignal`, `SignalEnvelope`, `SignalType`, `TrustSignal`, and `UnknownSymbolSignal` records to define various signal types and their properties.
- Implemented JSON serialization attributes for proper data interchange.
- Created project files for the new signal contracts library and corresponding test projects.
- Added deterministic test fixtures for micro-interaction testing.
- Included cryptographic keys for secure operations with cosign.
This commit is contained in:
StellaOps Bot
2025-12-05 00:27:00 +02:00
parent b018949a8d
commit 8768c27f30
192 changed files with 27569 additions and 2552 deletions

View File

@@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0-rc.2.25502.107" />

View File

@@ -0,0 +1,13 @@
namespace StellaOps.Microservice.SourceGen;
/// <summary>
/// Placeholder type for the source generator project.
/// This will be replaced with actual source generator implementation in a later sprint.
/// </summary>
public static class Placeholder
{
/// <summary>
/// Indicates the source generator is not yet implemented.
/// </summary>
public const string Status = "NotImplemented";
}

View File

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

View File

@@ -0,0 +1,24 @@
using StellaOps.Router.Common;
namespace StellaOps.Microservice;
/// <summary>
/// Configuration for a router endpoint.
/// </summary>
public sealed class RouterEndpointConfig
{
/// <summary>
/// Gets or sets the router host.
/// </summary>
public required string Host { get; set; }
/// <summary>
/// Gets or sets the router port.
/// </summary>
public required int Port { get; set; }
/// <summary>
/// Gets or sets the transport type to use.
/// </summary>
public TransportType TransportType { get; set; } = TransportType.Tcp;
}

View File

@@ -0,0 +1,28 @@
using Microsoft.Extensions.DependencyInjection;
namespace StellaOps.Microservice;
/// <summary>
/// Extension methods for registering Stella microservice services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds Stella microservice services to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configure">Action to configure the microservice options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddStellaMicroservice(
this IServiceCollection services,
Action<StellaMicroserviceOptions> configure)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
// Stub implementation - will be filled in later sprints
services.Configure(configure);
return services;
}
}

View File

@@ -0,0 +1,40 @@
using StellaOps.Router.Common;
namespace StellaOps.Microservice;
/// <summary>
/// Options for configuring a Stella microservice.
/// </summary>
public sealed class StellaMicroserviceOptions
{
/// <summary>
/// Gets or sets the service name.
/// </summary>
public required string ServiceName { get; set; }
/// <summary>
/// Gets or sets the semantic version.
/// </summary>
public required string Version { get; set; }
/// <summary>
/// Gets or sets the region where this instance is deployed.
/// </summary>
public required string Region { get; set; }
/// <summary>
/// Gets or sets the unique instance identifier.
/// </summary>
public string InstanceId { get; set; } = Guid.NewGuid().ToString("N");
/// <summary>
/// Gets the router endpoints to connect to.
/// At least one router endpoint is required.
/// </summary>
public List<RouterEndpointConfig> Routers { get; set; } = [];
/// <summary>
/// Gets or sets the optional path to a YAML config file for endpoint overrides.
/// </summary>
public string? EndpointConfigPath { get; set; }
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Represents a claim requirement for endpoint authorization.
/// </summary>
public sealed record ClaimRequirement
{
/// <summary>
/// Gets the claim type that must be present.
/// </summary>
public required string Type { get; init; }
/// <summary>
/// Gets the optional claim value that must match.
/// </summary>
public string? Value { get; init; }
}

View File

@@ -0,0 +1,42 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Represents the state of a connection between a microservice and the router.
/// </summary>
public sealed class ConnectionState
{
/// <summary>
/// Gets the unique identifier for this connection.
/// </summary>
public required string ConnectionId { get; init; }
/// <summary>
/// Gets the instance descriptor for the connected microservice.
/// </summary>
public required InstanceDescriptor Instance { get; init; }
/// <summary>
/// Gets or sets the health status of this connection.
/// </summary>
public InstanceHealthStatus Status { get; set; } = InstanceHealthStatus.Unknown;
/// <summary>
/// Gets or sets the UTC timestamp of the last heartbeat.
/// </summary>
public DateTime LastHeartbeatUtc { get; set; } = DateTime.UtcNow;
/// <summary>
/// Gets or sets the average ping time in milliseconds.
/// </summary>
public double AveragePingMs { get; set; }
/// <summary>
/// Gets the endpoints served by this connection.
/// </summary>
public Dictionary<(string Method, string Path), EndpointDescriptor> Endpoints { get; } = new();
/// <summary>
/// Gets the transport type used for this connection.
/// </summary>
public required TransportType TransportType { get; init; }
}

View File

@@ -0,0 +1,42 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Describes an endpoint's identity and metadata.
/// </summary>
public sealed record EndpointDescriptor
{
/// <summary>
/// Gets the name of the service that owns this endpoint.
/// </summary>
public required string ServiceName { get; init; }
/// <summary>
/// Gets the semantic version of the service.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Gets the HTTP method (GET, POST, PUT, PATCH, DELETE).
/// </summary>
public required string Method { get; init; }
/// <summary>
/// Gets the path template (e.g., "/billing/invoices/{id}").
/// </summary>
public required string Path { get; init; }
/// <summary>
/// Gets the default timeout for this endpoint.
/// </summary>
public TimeSpan DefaultTimeout { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Gets the claim requirements for authorization.
/// </summary>
public IReadOnlyList<ClaimRequirement> RequiringClaims { get; init; } = [];
/// <summary>
/// Gets a value indicating whether this endpoint supports streaming.
/// </summary>
public bool SupportsStreaming { get; init; }
}

View File

@@ -0,0 +1,22 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Represents a protocol frame in the router transport layer.
/// </summary>
public sealed record Frame
{
/// <summary>
/// Gets the type of this frame.
/// </summary>
public required FrameType Type { get; init; }
/// <summary>
/// Gets the correlation ID for request/response matching.
/// </summary>
public string? CorrelationId { get; init; }
/// <summary>
/// Gets the raw payload bytes.
/// </summary>
public ReadOnlyMemory<byte> Payload { get; init; }
}

View File

@@ -0,0 +1,47 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Defines the frame types used in the router protocol.
/// </summary>
public enum FrameType
{
/// <summary>
/// Initial registration frame sent by microservice.
/// </summary>
Hello,
/// <summary>
/// Periodic health check frame.
/// </summary>
Heartbeat,
/// <summary>
/// Request frame containing method, path, headers, and body.
/// </summary>
Request,
/// <summary>
/// Response frame containing status, headers, and body.
/// </summary>
Response,
/// <summary>
/// Streaming request data frame.
/// </summary>
RequestStreamData,
/// <summary>
/// Streaming response data frame.
/// </summary>
ResponseStreamData,
/// <summary>
/// Cancellation frame for aborting in-flight requests.
/// </summary>
Cancel,
/// <summary>
/// Optional frame for updating endpoint metadata at runtime.
/// </summary>
EndpointsUpdate
}

View File

@@ -0,0 +1,41 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Provides global routing state derived from all live connections.
/// </summary>
public interface IGlobalRoutingState
{
/// <summary>
/// Resolves an HTTP request to an endpoint descriptor.
/// </summary>
/// <param name="method">The HTTP method.</param>
/// <param name="path">The request path.</param>
/// <returns>The endpoint descriptor, or null if not found.</returns>
EndpointDescriptor? ResolveEndpoint(string method, string path);
/// <summary>
/// Gets all connections that can handle the specified endpoint.
/// </summary>
/// <param name="serviceName">The service name.</param>
/// <param name="version">The service version.</param>
/// <param name="method">The HTTP method.</param>
/// <param name="path">The request path.</param>
/// <returns>The available connection states.</returns>
IEnumerable<ConnectionState> GetConnectionsForEndpoint(
string serviceName,
string version,
string method,
string path);
/// <summary>
/// Registers a connection and its endpoints.
/// </summary>
/// <param name="connection">The connection state to register.</param>
void RegisterConnection(ConnectionState connection);
/// <summary>
/// Removes a connection from the routing state.
/// </summary>
/// <param name="connectionId">The connection ID to remove.</param>
void UnregisterConnection(string connectionId);
}

View File

@@ -0,0 +1,24 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Provides extensibility for routing decisions.
/// </summary>
public interface IRoutingPlugin
{
/// <summary>
/// Gets the priority of this plugin. Lower values run first.
/// </summary>
int Priority { get; }
/// <summary>
/// Filters or reorders candidate connections for routing.
/// </summary>
/// <param name="candidates">The candidate connections.</param>
/// <param name="endpoint">The target endpoint.</param>
/// <param name="gatewayRegion">The gateway's region.</param>
/// <returns>The filtered/reordered connections.</returns>
IEnumerable<ConnectionState> Filter(
IEnumerable<ConnectionState> candidates,
EndpointDescriptor endpoint,
string gatewayRegion);
}

View File

@@ -0,0 +1,24 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Represents a transport client that connects to routers.
/// </summary>
public interface ITransportClient : IAsyncDisposable
{
/// <summary>
/// Gets the transport type for this client.
/// </summary>
TransportType TransportType { get; }
/// <summary>
/// Connects to a router endpoint.
/// </summary>
/// <param name="host">The router host.</param>
/// <param name="port">The router port.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The established connection.</returns>
Task<ITransportConnection> ConnectAsync(
string host,
int port,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,37 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Represents a bidirectional transport connection.
/// </summary>
public interface ITransportConnection : IAsyncDisposable
{
/// <summary>
/// Gets the unique identifier for this connection.
/// </summary>
string ConnectionId { get; }
/// <summary>
/// Gets a value indicating whether the connection is open.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Sends a frame over the connection.
/// </summary>
/// <param name="frame">The frame to send.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask SendAsync(Frame frame, CancellationToken cancellationToken = default);
/// <summary>
/// Receives the next frame from the connection.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The received frame, or null if connection closed.</returns>
ValueTask<Frame?> ReceiveAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Closes the connection gracefully.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task CloseAsync(CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,45 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Represents a transport server that accepts connections from microservices.
/// </summary>
public interface ITransportServer : IAsyncDisposable
{
/// <summary>
/// Gets the transport type for this server.
/// </summary>
TransportType TransportType { get; }
/// <summary>
/// Starts listening for incoming connections.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Stops accepting new connections.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task StopAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Occurs when a new connection is established.
/// </summary>
event EventHandler<TransportConnectionEventArgs>? ConnectionEstablished;
/// <summary>
/// Occurs when a connection is closed.
/// </summary>
event EventHandler<TransportConnectionEventArgs>? ConnectionClosed;
}
/// <summary>
/// Event arguments for transport connection events.
/// </summary>
public sealed class TransportConnectionEventArgs : EventArgs
{
/// <summary>
/// Gets the connection that triggered the event.
/// </summary>
public required ITransportConnection Connection { get; init; }
}

View File

@@ -0,0 +1,27 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Describes a microservice instance's identity.
/// </summary>
public sealed record InstanceDescriptor
{
/// <summary>
/// Gets the unique identifier for this instance.
/// </summary>
public required string InstanceId { get; init; }
/// <summary>
/// Gets the name of the service.
/// </summary>
public required string ServiceName { get; init; }
/// <summary>
/// Gets the semantic version of the service.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Gets the region where this instance is deployed.
/// </summary>
public required string Region { get; init; }
}

View File

@@ -0,0 +1,32 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Defines the health status of a microservice instance.
/// </summary>
public enum InstanceHealthStatus
{
/// <summary>
/// Health status is not yet determined.
/// </summary>
Unknown,
/// <summary>
/// Instance is healthy and can accept requests.
/// </summary>
Healthy,
/// <summary>
/// Instance is degraded but can still accept requests.
/// </summary>
Degraded,
/// <summary>
/// Instance is draining and will not accept new requests.
/// </summary>
Draining,
/// <summary>
/// Instance is unhealthy and should not receive requests.
/// </summary>
Unhealthy
}

View File

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

View File

@@ -0,0 +1,32 @@
namespace StellaOps.Router.Common;
/// <summary>
/// Defines the transport types supported for microservice-to-router communication.
/// </summary>
public enum TransportType
{
/// <summary>
/// In-memory transport for testing and local development.
/// </summary>
InMemory,
/// <summary>
/// UDP transport for small/bounded payloads.
/// </summary>
Udp,
/// <summary>
/// TCP transport with length-prefixed framing.
/// </summary>
Tcp,
/// <summary>
/// TLS/mTLS transport with certificate-based authentication.
/// </summary>
Tls,
/// <summary>
/// RabbitMQ transport for queue-based communication.
/// </summary>
RabbitMq
}

View File

@@ -0,0 +1,25 @@
namespace StellaOps.Router.Config;
/// <summary>
/// Configuration for payload and memory limits.
/// </summary>
public sealed class PayloadLimits
{
/// <summary>
/// Gets or sets the maximum request bytes per call.
/// Default: 10 MB.
/// </summary>
public long MaxRequestBytesPerCall { get; set; } = 10 * 1024 * 1024;
/// <summary>
/// Gets or sets the maximum request bytes per connection.
/// Default: 100 MB.
/// </summary>
public long MaxRequestBytesPerConnection { get; set; } = 100 * 1024 * 1024;
/// <summary>
/// Gets or sets the maximum aggregate in-flight bytes across all requests.
/// Default: 1 GB.
/// </summary>
public long MaxAggregateInflightBytes { get; set; } = 1024 * 1024 * 1024;
}

View File

@@ -0,0 +1,17 @@
namespace StellaOps.Router.Config;
/// <summary>
/// Root configuration for the router.
/// </summary>
public sealed class RouterConfig
{
/// <summary>
/// Gets or sets the payload limits configuration.
/// </summary>
public PayloadLimits PayloadLimits { get; set; } = new();
/// <summary>
/// Gets or sets the service configurations.
/// </summary>
public List<ServiceConfig> Services { get; set; } = [];
}

View File

@@ -0,0 +1,60 @@
using StellaOps.Router.Common;
namespace StellaOps.Router.Config;
/// <summary>
/// Configuration for a service in the router.
/// </summary>
public sealed class ServiceConfig
{
/// <summary>
/// Gets or sets the service name.
/// </summary>
public required string ServiceName { get; set; }
/// <summary>
/// Gets or sets the default version for requests without explicit version.
/// </summary>
public string? DefaultVersion { get; set; }
/// <summary>
/// Gets or sets the default transport type for this service.
/// </summary>
public TransportType DefaultTransport { get; set; } = TransportType.Tcp;
/// <summary>
/// Gets or sets the endpoint configurations.
/// </summary>
public List<EndpointConfig> Endpoints { get; set; } = [];
}
/// <summary>
/// Configuration for an endpoint in a service.
/// </summary>
public sealed class EndpointConfig
{
/// <summary>
/// Gets or sets the HTTP method.
/// </summary>
public required string Method { get; set; }
/// <summary>
/// Gets or sets the path template.
/// </summary>
public required string Path { get; set; }
/// <summary>
/// Gets or sets the default timeout.
/// </summary>
public TimeSpan? DefaultTimeout { get; set; }
/// <summary>
/// Gets or sets whether streaming is supported.
/// </summary>
public bool SupportsStreaming { get; set; }
/// <summary>
/// Gets or sets the claim requirements.
/// </summary>
public List<ClaimRequirement> RequiringClaims { get; set; } = [];
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Router.Common\StellaOps.Router.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
# StellaOps.Signals.Contracts
Shared signal contracts for cross-module signal communication in StellaOps.
## Purpose
This library provides the common contracts (interfaces and DTOs) for signal-based communication between StellaOps modules. It enables:
- **Concelier** to emit reachability and trust signals
- **Scanner** to emit entropy and unknown symbol signals
- **Policy Engine** to consume all signal types for risk scoring
- **Signals service** to aggregate and cache signals
- **Authority** to emit trust/provenance signals
## Signal Types
| Type | Producer | Description |
|------|----------|-------------|
| `Reachability` | Concelier, Scanner | Whether vulnerable code paths are reachable |
| `Entropy` | Scanner | Code complexity and risk metrics |
| `Exploitability` | Concelier | KEV status, EPSS scores, exploit availability |
| `Trust` | Authority, Scanner | Publisher reputation, provenance, signatures |
| `UnknownSymbol` | Scanner | Unresolved dependencies during analysis |
| `Custom` | Any | Extension point for module-specific signals |
## Usage
### Emitting Signals
```csharp
public class MySignalProducer
{
private readonly ISignalEmitter _emitter;
private readonly ISignalContext _context;
public async Task EmitReachabilityAsync(string purl, bool isReachable)
{
var signal = new ReachabilitySignal
{
Purl = purl,
IsReachable = isReachable,
Confidence = 0.95
};
var envelope = _context.CreateReachabilityEnvelope(purl, signal);
await _emitter.EmitAsync(envelope);
}
}
```
### Consuming Signals
```csharp
public class MySignalConsumer
{
private readonly ISignalConsumer _consumer;
public async Task ProcessSignalsAsync(CancellationToken ct)
{
await foreach (var signal in _consumer.ConsumeAsync(SignalType.Reachability, ct))
{
// Process signal
}
}
}
```
## Dependencies
- `Microsoft.Extensions.DependencyInjection.Abstractions` — DI registration helpers
## Implementation Notes
This library contains only contracts. Actual transport implementations are provided by:
- `StellaOps.Signals.Nats` — NATS JetStream transport
- `StellaOps.Signals.InMemory` — In-memory transport for testing
## Related
- [Signal Flow Architecture](../../docs/modules/signals/architecture.md)
- [Policy Engine Signals Integration](../../docs/modules/policy/signals.md)

View File

@@ -0,0 +1,38 @@
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Interface for consuming signals from the signal bus.
/// Implemented by services that process signals.
/// </summary>
public interface ISignalConsumer
{
/// <summary>
/// Consumes signals from the signal bus as an async enumerable.
/// </summary>
/// <param name="filterType">Optional signal type to filter by.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Async enumerable of signal envelopes.</returns>
IAsyncEnumerable<SignalEnvelope> ConsumeAsync(
SignalType? filterType = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the latest signal for a given key.
/// </summary>
/// <param name="signalKey">The signal key to look up.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The signal envelope if found, null otherwise.</returns>
ValueTask<SignalEnvelope?> GetLatestAsync(
string signalKey,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all signals for a given PURL.
/// </summary>
/// <param name="purl">The package URL to look up.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Collection of signal envelopes for the PURL.</returns>
ValueTask<IReadOnlyList<SignalEnvelope>> GetByPurlAsync(
string purl,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,58 @@
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Interface for signal context providing metadata and correlation.
/// Used by signal producers to add context to emitted signals.
/// </summary>
public interface ISignalContext
{
/// <summary>
/// Gets the current tenant ID.
/// </summary>
string? TenantId { get; }
/// <summary>
/// Gets the current correlation ID for distributed tracing.
/// </summary>
string? CorrelationId { get; }
/// <summary>
/// Gets the name of the service producing signals.
/// </summary>
string ServiceName { get; }
/// <summary>
/// Creates a signal envelope with context metadata.
/// </summary>
/// <typeparam name="T">Type of the signal value.</typeparam>
/// <param name="signalKey">Unique key for the signal.</param>
/// <param name="signalType">Type of the signal.</param>
/// <param name="value">The signal value.</param>
/// <returns>A fully populated signal envelope.</returns>
SignalEnvelope CreateEnvelope<T>(string signalKey, SignalType signalType, T value) where T : notnull;
/// <summary>
/// Creates a reachability signal envelope.
/// </summary>
SignalEnvelope CreateReachabilityEnvelope(string purl, ReachabilitySignal signal);
/// <summary>
/// Creates an entropy signal envelope.
/// </summary>
SignalEnvelope CreateEntropyEnvelope(string purl, EntropySignal signal);
/// <summary>
/// Creates an exploitability signal envelope.
/// </summary>
SignalEnvelope CreateExploitabilityEnvelope(string cveId, ExploitabilitySignal signal);
/// <summary>
/// Creates a trust signal envelope.
/// </summary>
SignalEnvelope CreateTrustEnvelope(string purl, TrustSignal signal);
/// <summary>
/// Creates an unknown symbol signal envelope.
/// </summary>
SignalEnvelope CreateUnknownSymbolEnvelope(string symbolId, UnknownSymbolSignal signal);
}

View File

@@ -0,0 +1,22 @@
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Interface for emitting signals to the signal bus.
/// Implemented by services that produce signals.
/// </summary>
public interface ISignalEmitter
{
/// <summary>
/// Emits a single signal to the signal bus.
/// </summary>
/// <param name="signal">The signal envelope to emit.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask EmitAsync(SignalEnvelope signal, CancellationToken cancellationToken = default);
/// <summary>
/// Emits a batch of signals to the signal bus.
/// </summary>
/// <param name="signals">The signal envelopes to emit.</param>
/// <param name="cancellationToken">Cancellation token.</param>
ValueTask EmitBatchAsync(IEnumerable<SignalEnvelope> signals, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,63 @@
using Microsoft.Extensions.DependencyInjection;
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Extension methods for registering signal services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds signal contracts services to the service collection.
/// This is a marker method for modules that want to declare dependency on Signals.Contracts.
/// Actual implementations are registered by specific transport libraries.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddSignalContracts(this IServiceCollection services)
{
// This method serves as a marker for dependency on Signals.Contracts.
// Actual implementations of ISignalEmitter, ISignalConsumer, and ISignalContext
// are provided by transport-specific libraries (e.g., NATS, in-memory, etc.)
return services;
}
/// <summary>
/// Adds a signal emitter implementation to the service collection.
/// </summary>
/// <typeparam name="TEmitter">The emitter implementation type.</typeparam>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddSignalEmitter<TEmitter>(this IServiceCollection services)
where TEmitter : class, ISignalEmitter
{
services.AddSingleton<ISignalEmitter, TEmitter>();
return services;
}
/// <summary>
/// Adds a signal consumer implementation to the service collection.
/// </summary>
/// <typeparam name="TConsumer">The consumer implementation type.</typeparam>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddSignalConsumer<TConsumer>(this IServiceCollection services)
where TConsumer : class, ISignalConsumer
{
services.AddSingleton<ISignalConsumer, TConsumer>();
return services;
}
/// <summary>
/// Adds a signal context implementation to the service collection.
/// </summary>
/// <typeparam name="TContext">The context implementation type.</typeparam>
/// <param name="services">The service collection.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddSignalContext<TContext>(this IServiceCollection services)
where TContext : class, ISignalContext
{
services.AddScoped<ISignalContext, TContext>();
return services;
}
}

View File

@@ -0,0 +1,64 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Entropy signal from code complexity and risk analysis.
/// Measures complexity metrics that may indicate higher risk.
/// </summary>
public sealed record EntropySignal
{
/// <summary>
/// Package URL (PURL) of the analyzed package.
/// </summary>
[JsonPropertyName("purl")]
public required string Purl { get; init; }
/// <summary>
/// Overall entropy score (0.0-1.0, higher = more complex/risky).
/// </summary>
[JsonPropertyName("score")]
public required double Score { get; init; }
/// <summary>
/// Cyclomatic complexity metric.
/// </summary>
[JsonPropertyName("cyclomaticComplexity")]
public double? CyclomaticComplexity { get; init; }
/// <summary>
/// Ratio of opaque/binary content in the package.
/// </summary>
[JsonPropertyName("opaqueRatio")]
public double OpaqueRatio { get; init; }
/// <summary>
/// Number of suspicious patterns detected.
/// </summary>
[JsonPropertyName("suspiciousPatternCount")]
public int SuspiciousPatternCount { get; init; }
/// <summary>
/// Total lines of code analyzed.
/// </summary>
[JsonPropertyName("linesOfCode")]
public int? LinesOfCode { get; init; }
/// <summary>
/// Dependency depth in the tree.
/// </summary>
[JsonPropertyName("dependencyDepth")]
public int? DependencyDepth { get; init; }
/// <summary>
/// Penalty applied from entropy analysis.
/// </summary>
[JsonPropertyName("penalty")]
public double Penalty { get; init; }
/// <summary>
/// Top file contributing to entropy (if applicable).
/// </summary>
[JsonPropertyName("topOffendingFile")]
public string? TopOffendingFile { get; init; }
}

View File

@@ -0,0 +1,305 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts.Evidence;
/// <summary>
/// Single call path from entry point to vulnerable symbol.
/// </summary>
public sealed record CallPath
{
/// <summary>
/// Unique path identifier.
/// </summary>
[JsonPropertyName("pathId")]
public required string PathId { get; init; }
/// <summary>
/// Call depth (number of hops).
/// </summary>
[JsonPropertyName("depth")]
public required int Depth { get; init; }
/// <summary>
/// Path confidence (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public double Confidence { get; init; } = 1.0;
/// <summary>
/// How the path was discovered.
/// </summary>
[JsonPropertyName("pathType")]
public PathDiscoveryType PathType { get; init; } = PathDiscoveryType.Static;
/// <summary>
/// Entry point for this path.
/// </summary>
[JsonPropertyName("entryPoint")]
public EntryPoint? EntryPoint { get; init; }
/// <summary>
/// Ordered list of nodes in the path.
/// </summary>
[JsonPropertyName("nodes")]
public required IReadOnlyList<CallPathNode> Nodes { get; init; }
/// <summary>
/// Edges connecting the nodes.
/// </summary>
[JsonPropertyName("edges")]
public IReadOnlyList<CallEdge> Edges { get; init; } = [];
}
/// <summary>
/// How a call path was discovered.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PathDiscoveryType
{
/// <summary>Discovered through static analysis.</summary>
Static,
/// <summary>Observed at runtime.</summary>
Dynamic,
/// <summary>Inferred from other evidence.</summary>
Inferred
}
/// <summary>
/// Node in a call path.
/// </summary>
public sealed record CallPathNode
{
/// <summary>
/// Unique node identifier.
/// </summary>
[JsonPropertyName("nodeId")]
public required string NodeId { get; init; }
/// <summary>
/// Function/method name.
/// </summary>
[JsonPropertyName("functionName")]
public string? FunctionName { get; init; }
/// <summary>
/// Package URL of containing package.
/// </summary>
[JsonPropertyName("purl")]
public string? Purl { get; init; }
/// <summary>
/// Immutable code identity anchor.
/// </summary>
[JsonPropertyName("codeAnchor")]
public CodeAnchor? CodeAnchor { get; init; }
/// <summary>
/// Whether this is the vulnerable function.
/// </summary>
[JsonPropertyName("isVulnerable")]
public bool IsVulnerable { get; init; }
/// <summary>
/// Whether this is an entry point.
/// </summary>
[JsonPropertyName("isEntryPoint")]
public bool IsEntryPoint { get; init; }
/// <summary>
/// Number of runtime observations at this node.
/// </summary>
[JsonPropertyName("runtimeHitCount")]
public int RuntimeHitCount { get; init; }
}
/// <summary>
/// Edge between call path nodes.
/// </summary>
public sealed record CallEdge
{
/// <summary>
/// Source node ID.
/// </summary>
[JsonPropertyName("from")]
public required string From { get; init; }
/// <summary>
/// Target node ID.
/// </summary>
[JsonPropertyName("to")]
public required string To { get; init; }
/// <summary>
/// Call edge type.
/// </summary>
[JsonPropertyName("kind")]
public required CallEdgeKind Kind { get; init; }
/// <summary>
/// Edge confidence (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public double Confidence { get; init; } = 1.0;
/// <summary>
/// Evidence sources (e.g., reloc:.plt.got, bb-target:0x40f0ff).
/// </summary>
[JsonPropertyName("evidence")]
public IReadOnlyList<string> Evidence { get; init; } = [];
/// <summary>
/// Reason for edge.
/// </summary>
[JsonPropertyName("reason")]
public EdgeReason Reason { get; init; } = EdgeReason.Direct;
/// <summary>
/// Whether edge was revoked/disproven.
/// </summary>
[JsonPropertyName("revoked")]
public bool Revoked { get; init; }
}
/// <summary>
/// Type of call edge.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CallEdgeKind
{
/// <summary>Direct static call.</summary>
Static,
/// <summary>Virtual/polymorphic call.</summary>
Virtual,
/// <summary>Dynamic dispatch.</summary>
Dynamic,
/// <summary>Import/external call.</summary>
Import,
/// <summary>Callback/function pointer.</summary>
Callback,
/// <summary>Reflection-based call.</summary>
Reflection
}
/// <summary>
/// Reason for edge existence.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EdgeReason
{
/// <summary>Direct call observed.</summary>
Direct,
/// <summary>Indirect call inferred.</summary>
Indirect,
/// <summary>Observed at runtime.</summary>
RuntimeObserved,
/// <summary>Inferred from other signals.</summary>
Inferred,
/// <summary>Contested/uncertain.</summary>
Contested
}
/// <summary>
/// Application entry point.
/// </summary>
public sealed record EntryPoint
{
/// <summary>
/// Entry point identifier.
/// </summary>
[JsonPropertyName("entryPointId")]
public required string EntryPointId { get; init; }
/// <summary>
/// Entry point type.
/// </summary>
[JsonPropertyName("type")]
public required EntryPointType Type { get; init; }
/// <summary>
/// Entry point name.
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; init; }
/// <summary>
/// HTTP route pattern if applicable.
/// </summary>
[JsonPropertyName("route")]
public string? Route { get; init; }
/// <summary>
/// Code identity anchor.
/// </summary>
[JsonPropertyName("codeAnchor")]
public CodeAnchor? CodeAnchor { get; init; }
/// <summary>
/// Execution phase (load, init, runtime).
/// </summary>
[JsonPropertyName("phase")]
public ExecutionPhase Phase { get; init; } = ExecutionPhase.Runtime;
}
/// <summary>
/// Type of application entry point.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EntryPointType
{
/// <summary>Main function.</summary>
Main,
/// <summary>HTTP request handler.</summary>
HttpHandler,
/// <summary>Event handler.</summary>
EventHandler,
/// <summary>Scheduled task.</summary>
ScheduledTask,
/// <summary>Init array constructor.</summary>
InitArray,
/// <summary>Class constructor.</summary>
Constructor,
/// <summary>CLI command handler.</summary>
CliCommand,
/// <summary>gRPC method.</summary>
GrpcMethod,
/// <summary>Message queue handler.</summary>
MessageHandler,
/// <summary>Other entry point.</summary>
Other
}
/// <summary>
/// Execution phase.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ExecutionPhase
{
/// <summary>Load-time execution.</summary>
Load,
/// <summary>Initialization phase.</summary>
Init,
/// <summary>Runtime execution.</summary>
Runtime
}

View File

@@ -0,0 +1,167 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts.Evidence;
/// <summary>
/// Immutable code identity anchor (code_id) for function-level evidence.
/// Anchors every vulnerability finding to an immutable {artifact_digest, code_id} tuple.
/// </summary>
public sealed record CodeAnchor
{
/// <summary>
/// Binary/source format.
/// </summary>
[JsonPropertyName("format")]
public required CodeFormat Format { get; init; }
/// <summary>
/// Artifact content digest (sha256:...).
/// </summary>
[JsonPropertyName("artifactDigest")]
public required string ArtifactDigest { get; init; }
/// <summary>
/// Build ID (.note.gnu.build-id or equivalent).
/// </summary>
[JsonPropertyName("buildId")]
public string? BuildId { get; init; }
/// <summary>
/// Section name (e.g., .text, .init).
/// </summary>
[JsonPropertyName("section")]
public string? Section { get; init; }
/// <summary>
/// Start address (hex, e.g., 0x4012a0).
/// </summary>
[JsonPropertyName("startAddress")]
public string? StartAddress { get; init; }
/// <summary>
/// Code block length in bytes.
/// </summary>
[JsonPropertyName("length")]
public int? Length { get; init; }
/// <summary>
/// Optional hash of code bytes (blake3:...).
/// </summary>
[JsonPropertyName("codeBlockHash")]
public string? CodeBlockHash { get; init; }
/// <summary>
/// Symbol information.
/// </summary>
[JsonPropertyName("symbol")]
public SymbolInfo? Symbol { get; init; }
}
/// <summary>
/// Binary/source format for code anchors.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CodeFormat
{
/// <summary>ELF binary format.</summary>
ELF,
/// <summary>Windows PE format.</summary>
PE,
/// <summary>macOS Mach-O format.</summary>
MachO,
/// <summary>Java bytecode.</summary>
JVM,
/// <summary>.NET Common Language Runtime.</summary>
CLR,
/// <summary>WebAssembly.</summary>
WASM,
/// <summary>Source code.</summary>
SOURCE
}
/// <summary>
/// Symbol information with demangling metadata.
/// </summary>
public sealed record SymbolInfo
{
/// <summary>
/// Mangled symbol name.
/// </summary>
[JsonPropertyName("mangled")]
public string? Mangled { get; init; }
/// <summary>
/// Demangled/human-readable name.
/// </summary>
[JsonPropertyName("demangled")]
public string? Demangled { get; init; }
/// <summary>
/// Symbol source (DWARF, PDB, SYM, STABS, EXPORTS, none).
/// </summary>
[JsonPropertyName("source")]
public SymbolSource Source { get; init; } = SymbolSource.None;
/// <summary>
/// Symbol resolution confidence (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public double Confidence { get; init; } = 1.0;
/// <summary>
/// Inferred source language.
/// </summary>
[JsonPropertyName("language")]
public SourceLanguage? Language { get; init; }
}
/// <summary>
/// Symbol debug info source.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SymbolSource
{
/// <summary>No symbol info available.</summary>
None,
/// <summary>DWARF debug info.</summary>
DWARF,
/// <summary>Windows PDB file.</summary>
PDB,
/// <summary>Symbol table.</summary>
SYM,
/// <summary>STABS format.</summary>
STABS,
/// <summary>Export table.</summary>
EXPORTS
}
/// <summary>
/// Inferred source language.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SourceLanguage
{
Unknown,
C,
Cpp,
Rust,
Go,
Java,
CSharp,
Python,
JavaScript,
TypeScript,
Ruby,
PHP
}

View File

@@ -0,0 +1,300 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts.Evidence;
/// <summary>
/// Complete reachability evidence chain with call paths, runtime hits, and attestations.
/// Used by CLI 'stella graph explain' and UI explain drawer.
/// </summary>
public sealed record ReachabilityEvidenceChain
{
/// <summary>
/// Unique evidence chain identifier.
/// </summary>
[JsonPropertyName("evidenceId")]
public required Guid EvidenceId { get; init; }
/// <summary>
/// Type of evidence.
/// </summary>
[JsonPropertyName("evidenceType")]
public required EvidenceType EvidenceType { get; init; }
/// <summary>
/// Static call graph analysis evidence.
/// </summary>
[JsonPropertyName("graphEvidence")]
public GraphEvidence? GraphEvidence { get; init; }
/// <summary>
/// Runtime observation evidence.
/// </summary>
[JsonPropertyName("runtimeEvidence")]
public RuntimeEvidence? RuntimeEvidence { get; init; }
/// <summary>
/// Immutable code identity anchors.
/// </summary>
[JsonPropertyName("codeAnchors")]
public IReadOnlyList<CodeAnchor> CodeAnchors { get; init; } = [];
/// <summary>
/// Application entry points analyzed.
/// </summary>
[JsonPropertyName("entryPoints")]
public IReadOnlyList<EntryPoint> EntryPoints { get; init; } = [];
/// <summary>
/// Unresolved symbols/edges for uncertainty tracking.
/// </summary>
[JsonPropertyName("unknowns")]
public IReadOnlyList<UnknownRecord> Unknowns { get; init; } = [];
}
/// <summary>
/// Type of evidence.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EvidenceType
{
/// <summary>Static call graph only.</summary>
CallGraph,
/// <summary>Runtime trace only.</summary>
RuntimeTrace,
/// <summary>Combination of static and runtime.</summary>
Hybrid,
/// <summary>Manual assessment.</summary>
Manual
}
/// <summary>
/// Static call graph analysis evidence.
/// </summary>
public sealed record GraphEvidence
{
/// <summary>
/// BLAKE3 hash of canonical graph.
/// </summary>
[JsonPropertyName("graphHash")]
public required string GraphHash { get; init; }
/// <summary>
/// CAS URI to graph (cas://reachability/graphs/{hash}).
/// </summary>
[JsonPropertyName("graphCasUri")]
public string? GraphCasUri { get; init; }
/// <summary>
/// Graph schema version.
/// </summary>
[JsonPropertyName("graphKind")]
public GraphKind GraphKind { get; init; } = GraphKind.RichGraphV1;
/// <summary>
/// Number of nodes in the graph.
/// </summary>
[JsonPropertyName("nodeCount")]
public int NodeCount { get; init; }
/// <summary>
/// Number of edges in the graph.
/// </summary>
[JsonPropertyName("edgeCount")]
public int EdgeCount { get; init; }
/// <summary>
/// Percentage of code covered by analysis.
/// </summary>
[JsonPropertyName("coveragePercent")]
public double CoveragePercent { get; init; }
/// <summary>
/// Analyzer information.
/// </summary>
[JsonPropertyName("analyzer")]
public AnalyzerInfo? Analyzer { get; init; }
}
/// <summary>
/// Graph schema version.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GraphKind
{
/// <summary>Rich graph v1 schema.</summary>
[JsonPropertyName("richgraph-v1")]
RichGraphV1,
/// <summary>Simple call graph v1.</summary>
[JsonPropertyName("simplecg-v1")]
SimpleCgV1,
/// <summary>Entry trace v1.</summary>
[JsonPropertyName("entry-trace-v1")]
EntryTraceV1
}
/// <summary>
/// Runtime observation evidence.
/// </summary>
public sealed record RuntimeEvidence
{
/// <summary>
/// BLAKE3 hash of trace data.
/// </summary>
[JsonPropertyName("traceHash")]
public string? TraceHash { get; init; }
/// <summary>
/// CAS URI to trace data.
/// </summary>
[JsonPropertyName("traceCasUri")]
public string? TraceCasUri { get; init; }
/// <summary>
/// Observation window start.
/// </summary>
[JsonPropertyName("observationStart")]
public DateTimeOffset? ObservationStart { get; init; }
/// <summary>
/// Observation window end.
/// </summary>
[JsonPropertyName("observationEnd")]
public DateTimeOffset? ObservationEnd { get; init; }
/// <summary>
/// Total hit count.
/// </summary>
[JsonPropertyName("hitCount")]
public int HitCount { get; init; }
/// <summary>
/// Number of unique functions hit.
/// </summary>
[JsonPropertyName("uniqueFunctionsHit")]
public int UniqueFunctionsHit { get; init; }
/// <summary>
/// Runtime probe type.
/// </summary>
[JsonPropertyName("probeType")]
public ProbeType ProbeType { get; init; } = ProbeType.Instrumentation;
}
/// <summary>
/// Runtime probe type.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ProbeType
{
/// <summary>.NET EventPipe.</summary>
EventPipe,
/// <summary>Java Flight Recorder.</summary>
JFR,
/// <summary>Linux eBPF.</summary>
eBPF,
/// <summary>Code instrumentation.</summary>
Instrumentation
}
/// <summary>
/// Information about the analyzer that produced evidence.
/// </summary>
public sealed record AnalyzerInfo
{
/// <summary>
/// Analyzer name.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Analyzer version.
/// </summary>
[JsonPropertyName("version")]
public required string Version { get; init; }
/// <summary>
/// Analyzer binary/image digest.
/// </summary>
[JsonPropertyName("digest")]
public string? Digest { get; init; }
/// <summary>
/// Hash of analyzer configuration.
/// </summary>
[JsonPropertyName("configHash")]
public string? ConfigHash { get; init; }
}
/// <summary>
/// Unresolved symbol or edge for uncertainty tracking.
/// </summary>
public sealed record UnknownRecord
{
/// <summary>
/// Type of unknown.
/// </summary>
[JsonPropertyName("unknownType")]
public required UnknownType UnknownType { get; init; }
/// <summary>
/// Identifier for the unknown.
/// </summary>
[JsonPropertyName("identifier")]
public required string Identifier { get; init; }
/// <summary>
/// Context where unknown was encountered.
/// </summary>
[JsonPropertyName("context")]
public string? Context { get; init; }
/// <summary>
/// Uncertainty tier (U1, U2, U3).
/// </summary>
[JsonPropertyName("uncertaintyLevel")]
public UncertaintyLevel UncertaintyLevel { get; init; } = UncertaintyLevel.U1;
}
/// <summary>
/// Type of unknown element.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UnknownType
{
/// <summary>Unresolved symbol.</summary>
Symbol,
/// <summary>Unresolved edge.</summary>
Edge,
/// <summary>Unresolved import.</summary>
Import,
/// <summary>Unresolved reference.</summary>
Reference
}
/// <summary>
/// Uncertainty tier.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UncertaintyLevel
{
/// <summary>Low uncertainty.</summary>
U1,
/// <summary>Medium uncertainty.</summary>
U2,
/// <summary>High uncertainty.</summary>
U3
}

View File

@@ -0,0 +1,319 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts.Evidence;
/// <summary>
/// Request to explain reachability for a vulnerability/package.
/// Used by CLI 'stella graph explain' command.
/// </summary>
public sealed record ReachabilityExplainRequest
{
/// <summary>
/// Unique request identifier for idempotency.
/// </summary>
[JsonPropertyName("requestId")]
public required Guid RequestId { get; init; }
/// <summary>
/// Correlation ID for tracing.
/// </summary>
[JsonPropertyName("correlationId")]
public string? CorrelationId { get; init; }
/// <summary>
/// Tenant scope.
/// </summary>
[JsonPropertyName("tenantId")]
public required string TenantId { get; init; }
/// <summary>
/// Subject being analyzed for reachability.
/// </summary>
[JsonPropertyName("subject")]
public required ReachabilitySubject Subject { get; init; }
/// <summary>
/// Options for the explain request.
/// </summary>
[JsonPropertyName("options")]
public ExplainOptions? Options { get; init; }
}
/// <summary>
/// Subject being analyzed for reachability.
/// </summary>
public sealed record ReachabilitySubject
{
/// <summary>
/// Package URL of the component.
/// </summary>
[JsonPropertyName("purl")]
public required string Purl { get; init; }
/// <summary>
/// CVE identifier if analyzing vulnerability.
/// </summary>
[JsonPropertyName("cve")]
public string? Cve { get; init; }
/// <summary>
/// Vulnerable function/method name.
/// </summary>
[JsonPropertyName("vulnerableSymbol")]
public string? VulnerableSymbol { get; init; }
/// <summary>
/// Container image digest.
/// </summary>
[JsonPropertyName("imageDigest")]
public string? ImageDigest { get; init; }
/// <summary>
/// Scan job identifier.
/// </summary>
[JsonPropertyName("scanId")]
public string? ScanId { get; init; }
/// <summary>
/// Artifact content digest.
/// </summary>
[JsonPropertyName("artifactDigest")]
public string? ArtifactDigest { get; init; }
}
/// <summary>
/// Options for explain request.
/// </summary>
public sealed record ExplainOptions
{
/// <summary>
/// Maximum number of paths to return (1-100, default 10).
/// </summary>
[JsonPropertyName("maxPaths")]
public int MaxPaths { get; init; } = 10;
/// <summary>
/// Maximum call depth to analyze (1-50, default 20).
/// </summary>
[JsonPropertyName("maxDepth")]
public int MaxDepth { get; init; } = 20;
/// <summary>
/// Whether to include runtime hits.
/// </summary>
[JsonPropertyName("includeRuntimeHits")]
public bool IncludeRuntimeHits { get; init; } = true;
/// <summary>
/// Whether to include unresolved symbols.
/// </summary>
[JsonPropertyName("includeUnknowns")]
public bool IncludeUnknowns { get; init; }
/// <summary>
/// Only return attested evidence.
/// </summary>
[JsonPropertyName("requireAttestation")]
public bool RequireAttestation { get; init; }
/// <summary>
/// Output format.
/// </summary>
[JsonPropertyName("format")]
public ExplainOutputFormat Format { get; init; } = ExplainOutputFormat.Json;
}
/// <summary>
/// Output format for explain results.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ExplainOutputFormat
{
/// <summary>JSON format.</summary>
Json,
/// <summary>SARIF format.</summary>
Sarif,
/// <summary>Graphviz DOT format.</summary>
Graphviz
}
/// <summary>
/// Response containing reachability explanation with call paths and evidence.
/// </summary>
public sealed record ReachabilityExplainResponse
{
/// <summary>
/// Original request identifier.
/// </summary>
[JsonPropertyName("requestId")]
public required Guid RequestId { get; init; }
/// <summary>
/// Response status.
/// </summary>
[JsonPropertyName("status")]
public required ExplainStatus Status { get; init; }
/// <summary>
/// Reachability state determination.
/// </summary>
[JsonPropertyName("reachabilityState")]
public ReachabilityState? ReachabilityState { get; init; }
/// <summary>
/// Evidence chain supporting the determination.
/// </summary>
[JsonPropertyName("evidenceChain")]
public ReachabilityEvidenceChain? EvidenceChain { get; init; }
/// <summary>
/// Ordered call paths from entry points to vulnerable symbol.
/// </summary>
[JsonPropertyName("callPaths")]
public IReadOnlyList<CallPath> CallPaths { get; init; } = [];
/// <summary>
/// Runtime observations confirming reachability.
/// </summary>
[JsonPropertyName("runtimeHits")]
public IReadOnlyList<RuntimeHit> RuntimeHits { get; init; } = [];
/// <summary>
/// DSSE attestations backing the evidence.
/// </summary>
[JsonPropertyName("attestations")]
public IReadOnlyList<AttestationReference> Attestations { get; init; } = [];
/// <summary>
/// Analysis metadata.
/// </summary>
[JsonPropertyName("analysisMetadata")]
public AnalysisMetadata? AnalysisMetadata { get; init; }
/// <summary>
/// Error details if status is ERROR.
/// </summary>
[JsonPropertyName("error")]
public ReachabilityError? Error { get; init; }
}
/// <summary>
/// Explain response status.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ExplainStatus
{
/// <summary>Full explanation available.</summary>
Success,
/// <summary>Partial explanation (some data missing).</summary>
Partial,
/// <summary>Subject not found.</summary>
NotFound,
/// <summary>Error during explanation.</summary>
Error
}
/// <summary>
/// Analysis metadata.
/// </summary>
public sealed record AnalysisMetadata
{
/// <summary>
/// Analysis identifier.
/// </summary>
[JsonPropertyName("analysisId")]
public Guid? AnalysisId { get; init; }
/// <summary>
/// When analysis started.
/// </summary>
[JsonPropertyName("startedAt")]
public DateTimeOffset? StartedAt { get; init; }
/// <summary>
/// When analysis completed.
/// </summary>
[JsonPropertyName("completedAt")]
public DateTimeOffset? CompletedAt { get; init; }
/// <summary>
/// Duration in milliseconds.
/// </summary>
[JsonPropertyName("durationMs")]
public long? DurationMs { get; init; }
/// <summary>
/// Analyzer information.
/// </summary>
[JsonPropertyName("analyzer")]
public AnalyzerInfo? Analyzer { get; init; }
/// <summary>
/// Whether analysis can be replayed.
/// </summary>
[JsonPropertyName("replayable")]
public bool Replayable { get; init; }
/// <summary>
/// URI to replay manifest.
/// </summary>
[JsonPropertyName("replayManifestUri")]
public string? ReplayManifestUri { get; init; }
}
/// <summary>
/// Reachability error details.
/// </summary>
public sealed record ReachabilityError
{
/// <summary>
/// Error code.
/// </summary>
[JsonPropertyName("code")]
public required ReachabilityErrorCode Code { get; init; }
/// <summary>
/// Human-readable error message.
/// </summary>
[JsonPropertyName("message")]
public required string Message { get; init; }
/// <summary>
/// Additional error details.
/// </summary>
[JsonPropertyName("details")]
public IDictionary<string, object>? Details { get; init; }
}
/// <summary>
/// Reachability error codes.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ReachabilityErrorCode
{
/// <summary>Subject not found.</summary>
SubjectNotFound,
/// <summary>Call graph not available.</summary>
GraphNotAvailable,
/// <summary>Analysis failed.</summary>
AnalysisFailed,
/// <summary>Attestation invalid.</summary>
AttestationInvalid,
/// <summary>Transparency log unavailable.</summary>
TransparencyUnavailable,
/// <summary>Operation timed out.</summary>
Timeout,
/// <summary>Internal error.</summary>
InternalError
}

View File

@@ -0,0 +1,95 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts.Evidence;
/// <summary>
/// Lattice-based reachability state determination.
/// </summary>
public sealed record ReachabilityState
{
/// <summary>
/// Reachability lattice state.
/// </summary>
[JsonPropertyName("state")]
public required ReachabilityLatticeState State { get; init; }
/// <summary>
/// Confidence score (0.0-1.0).
/// </summary>
[JsonPropertyName("confidence")]
public double Confidence { get; init; } = 1.0;
/// <summary>
/// Analysis method used.
/// </summary>
[JsonPropertyName("analysisMethod")]
public AnalysisMethod AnalysisMethod { get; init; } = AnalysisMethod.Static;
/// <summary>
/// Number of paths reaching vulnerable symbol.
/// </summary>
[JsonPropertyName("callPathCount")]
public int CallPathCount { get; init; }
/// <summary>
/// Minimum call depth from entry point.
/// </summary>
[JsonPropertyName("minCallDepth")]
public int? MinCallDepth { get; init; }
/// <summary>
/// Maximum call depth from entry point.
/// </summary>
[JsonPropertyName("maxCallDepth")]
public int? MaxCallDepth { get; init; }
/// <summary>
/// Number of runtime observations.
/// </summary>
[JsonPropertyName("runtimeHitCount")]
public int RuntimeHitCount { get; init; }
}
/// <summary>
/// Reachability lattice states.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ReachabilityLatticeState
{
/// <summary>Vulnerable symbol is reachable from entry points.</summary>
Reachable,
/// <summary>Vulnerable symbol is not reachable from any entry point.</summary>
Unreachable,
/// <summary>Vulnerable symbol may be reachable but analysis is incomplete.</summary>
PotentiallyReachable,
/// <summary>Reachability cannot be determined.</summary>
Unknown,
/// <summary>Awaiting manual review.</summary>
UnderReview
}
/// <summary>
/// Analysis method used to determine reachability.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AnalysisMethod
{
/// <summary>Static analysis only.</summary>
Static,
/// <summary>Dynamic/runtime analysis.</summary>
Dynamic,
/// <summary>Combination of static and dynamic.</summary>
Hybrid,
/// <summary>Runtime observation only.</summary>
Runtime,
/// <summary>Inferred from other signals.</summary>
Inferred
}

View File

@@ -0,0 +1,165 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts.Evidence;
/// <summary>
/// Runtime observation of function execution.
/// </summary>
public sealed record RuntimeHit
{
/// <summary>
/// Symbol identifier.
/// </summary>
[JsonPropertyName("symbolId")]
public required string SymbolId { get; init; }
/// <summary>
/// Code identity anchor.
/// </summary>
[JsonPropertyName("codeAnchor")]
public CodeAnchor? CodeAnchor { get; init; }
/// <summary>
/// Number of times function was hit.
/// </summary>
[JsonPropertyName("hitCount")]
public required int HitCount { get; init; }
/// <summary>
/// When hit was observed.
/// </summary>
[JsonPropertyName("observedAt")]
public required DateTimeOffset ObservedAt { get; init; }
/// <summary>
/// Runtime loader base address (hex).
/// </summary>
[JsonPropertyName("loaderBase")]
public string? LoaderBase { get; init; }
/// <summary>
/// Process identifier.
/// </summary>
[JsonPropertyName("processId")]
public string? ProcessId { get; init; }
/// <summary>
/// Container identifier.
/// </summary>
[JsonPropertyName("containerId")]
public string? ContainerId { get; init; }
/// <summary>
/// CAS URI to raw trace data.
/// </summary>
[JsonPropertyName("casUri")]
public string? CasUri { get; init; }
}
/// <summary>
/// Reference to DSSE attestation.
/// </summary>
public sealed record AttestationReference
{
/// <summary>
/// in-toto predicate type URI.
/// </summary>
[JsonPropertyName("predicateType")]
public required string PredicateType { get; init; }
/// <summary>
/// Attestation envelope digest.
/// </summary>
[JsonPropertyName("digest")]
public required string Digest { get; init; }
/// <summary>
/// CAS URI to attestation.
/// </summary>
[JsonPropertyName("casUri")]
public string? CasUri { get; init; }
/// <summary>
/// Signer key identifier.
/// </summary>
[JsonPropertyName("signerId")]
public string? SignerId { get; init; }
/// <summary>
/// When attestation was signed.
/// </summary>
[JsonPropertyName("signedAt")]
public DateTimeOffset? SignedAt { get; init; }
/// <summary>
/// Rekor transparency log index.
/// </summary>
[JsonPropertyName("rekorLogIndex")]
public long? RekorLogIndex { get; init; }
}
/// <summary>
/// Reference to call graph artifact.
/// </summary>
public sealed record CallGraphReference
{
/// <summary>
/// BLAKE3 hash of the graph.
/// </summary>
[JsonPropertyName("graphHash")]
public required string GraphHash { get; init; }
/// <summary>
/// CAS URI to graph data.
/// </summary>
[JsonPropertyName("casUri")]
public string? CasUri { get; init; }
/// <summary>
/// URI to DSSE envelope.
/// </summary>
[JsonPropertyName("dsseUri")]
public string? DsseUri { get; init; }
/// <summary>
/// Graph schema version.
/// </summary>
[JsonPropertyName("kind")]
public GraphKind Kind { get; init; } = GraphKind.RichGraphV1;
}
/// <summary>
/// Reference to transparency log entry.
/// </summary>
public sealed record TransparencyLogReference
{
/// <summary>
/// Log identifier.
/// </summary>
[JsonPropertyName("logId")]
public string? LogId { get; init; }
/// <summary>
/// Entry index in the log.
/// </summary>
[JsonPropertyName("logIndex")]
public long? LogIndex { get; init; }
/// <summary>
/// When entry was integrated.
/// </summary>
[JsonPropertyName("integratedTime")]
public DateTimeOffset? IntegratedTime { get; init; }
/// <summary>
/// URI to the log entry.
/// </summary>
[JsonPropertyName("entryUri")]
public string? EntryUri { get; init; }
/// <summary>
/// Base64-encoded inclusion proof.
/// </summary>
[JsonPropertyName("inclusionProof")]
public string? InclusionProof { get; init; }
}

View File

@@ -0,0 +1,64 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Exploitability signal from KEV and exploit databases.
/// Indicates known exploitation status and availability.
/// </summary>
public sealed record ExploitabilitySignal
{
/// <summary>
/// CVE identifier for the vulnerability.
/// </summary>
[JsonPropertyName("cveId")]
public required string CveId { get; init; }
/// <summary>
/// Whether this vulnerability is in CISA KEV (Known Exploited Vulnerabilities).
/// </summary>
[JsonPropertyName("inKev")]
public bool InKev { get; init; }
/// <summary>
/// Date added to KEV catalog (if applicable).
/// </summary>
[JsonPropertyName("kevAddedDate")]
public DateTimeOffset? KevAddedDate { get; init; }
/// <summary>
/// Whether a public exploit is available.
/// </summary>
[JsonPropertyName("hasPublicExploit")]
public bool HasPublicExploit { get; init; }
/// <summary>
/// Exploit maturity level (e.g., "poc", "functional", "weaponized").
/// </summary>
[JsonPropertyName("exploitMaturity")]
public string? ExploitMaturity { get; init; }
/// <summary>
/// EPSS (Exploit Prediction Scoring System) score (0.0-1.0).
/// </summary>
[JsonPropertyName("epssScore")]
public double? EpssScore { get; init; }
/// <summary>
/// EPSS percentile ranking.
/// </summary>
[JsonPropertyName("epssPercentile")]
public double? EpssPercentile { get; init; }
/// <summary>
/// References to exploit sources.
/// </summary>
[JsonPropertyName("exploitReferences")]
public IReadOnlyList<string> ExploitReferences { get; init; } = [];
/// <summary>
/// Last date exploit information was updated.
/// </summary>
[JsonPropertyName("lastUpdated")]
public DateTimeOffset? LastUpdated { get; init; }
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Reachability signal from callgraph analysis.
/// Indicates whether a vulnerable code path is reachable from application entry points.
/// </summary>
public sealed record ReachabilitySignal
{
/// <summary>
/// Package URL (PURL) of the analyzed package.
/// </summary>
[JsonPropertyName("purl")]
public required string Purl { get; init; }
/// <summary>
/// Whether the vulnerable symbol is reachable.
/// </summary>
[JsonPropertyName("isReachable")]
public required bool IsReachable { get; init; }
/// <summary>
/// Confidence score (0.0-1.0) of the reachability determination.
/// </summary>
[JsonPropertyName("confidence")]
public double Confidence { get; init; } = 1.0;
/// <summary>
/// Number of call paths that reach the vulnerable symbol.
/// </summary>
[JsonPropertyName("callPathCount")]
public int CallPathCount { get; init; }
/// <summary>
/// Minimum call depth to reach the vulnerable symbol.
/// </summary>
[JsonPropertyName("minCallDepth")]
public int? MinCallDepth { get; init; }
/// <summary>
/// List of entry points that can reach the vulnerable symbol.
/// </summary>
[JsonPropertyName("reachingEntryPoints")]
public IReadOnlyList<string> ReachingEntryPoints { get; init; } = [];
/// <summary>
/// Analysis method used (e.g., "static", "dynamic", "hybrid").
/// </summary>
[JsonPropertyName("analysisMethod")]
public string AnalysisMethod { get; init; } = "static";
/// <summary>
/// The vulnerable symbol identifier.
/// </summary>
[JsonPropertyName("vulnerableSymbol")]
public string? VulnerableSymbol { get; init; }
}

View File

@@ -0,0 +1,64 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Universal envelope for carrying signal data across modules.
/// Provides a common structure for all signal types with metadata.
/// </summary>
public sealed record SignalEnvelope
{
/// <summary>
/// Unique key identifying this signal (e.g., "pkg:npm/lodash@4.17.21:reachability").
/// </summary>
[JsonPropertyName("signalKey")]
public required string SignalKey { get; init; }
/// <summary>
/// Type of signal contained in this envelope.
/// </summary>
[JsonPropertyName("signalType")]
public required SignalType SignalType { get; init; }
/// <summary>
/// The signal payload (type depends on SignalType).
/// </summary>
[JsonPropertyName("value")]
public required object Value { get; init; }
/// <summary>
/// Timestamp when this signal was computed.
/// </summary>
[JsonPropertyName("computedAt")]
public required DateTimeOffset ComputedAt { get; init; }
/// <summary>
/// Name of the service that produced this signal.
/// </summary>
[JsonPropertyName("sourceService")]
public required string SourceService { get; init; }
/// <summary>
/// Optional tenant ID for multi-tenant isolation.
/// </summary>
[JsonPropertyName("tenantId")]
public string? TenantId { get; init; }
/// <summary>
/// Optional correlation ID for distributed tracing.
/// </summary>
[JsonPropertyName("correlationId")]
public string? CorrelationId { get; init; }
/// <summary>
/// Optional content digest for provenance verification.
/// </summary>
[JsonPropertyName("provenanceDigest")]
public string? ProvenanceDigest { get; init; }
/// <summary>
/// Optional schema version for backward compatibility.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = "1.0";
}

View File

@@ -0,0 +1,42 @@
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Enumeration of supported signal types in the StellaOps platform.
/// </summary>
public enum SignalType
{
/// <summary>
/// Reachability signal from callgraph analysis.
/// Indicates whether a vulnerable code path is reachable.
/// </summary>
Reachability,
/// <summary>
/// Entropy signal from code complexity analysis.
/// Measures code complexity and associated risk metrics.
/// </summary>
Entropy,
/// <summary>
/// Exploitability signal from KEV/exploit databases.
/// Indicates known exploitation status and availability.
/// </summary>
Exploitability,
/// <summary>
/// Trust signal from reputation and chain-of-custody scoring.
/// Measures publisher/maintainer trustworthiness.
/// </summary>
Trust,
/// <summary>
/// Unknown symbol signal for unresolved dependencies.
/// Flags symbols that could not be resolved during analysis.
/// </summary>
UnknownSymbol,
/// <summary>
/// Custom signal type for extensibility.
/// </summary>
Custom
}

View File

@@ -0,0 +1,76 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Trust signal from reputation and chain-of-custody scoring.
/// Measures publisher/maintainer trustworthiness.
/// </summary>
public sealed record TrustSignal
{
/// <summary>
/// Package URL (PURL) of the analyzed package.
/// </summary>
[JsonPropertyName("purl")]
public required string Purl { get; init; }
/// <summary>
/// Overall trust score (0.0-1.0, higher = more trustworthy).
/// </summary>
[JsonPropertyName("score")]
public required double Score { get; init; }
/// <summary>
/// Publisher/maintainer reputation score.
/// </summary>
[JsonPropertyName("publisherReputation")]
public double? PublisherReputation { get; init; }
/// <summary>
/// Whether the package has verified provenance.
/// </summary>
[JsonPropertyName("hasVerifiedProvenance")]
public bool HasVerifiedProvenance { get; init; }
/// <summary>
/// Whether the package is signed.
/// </summary>
[JsonPropertyName("isSigned")]
public bool IsSigned { get; init; }
/// <summary>
/// Signature type if signed (e.g., "sigstore", "gpg", "npm-provenance").
/// </summary>
[JsonPropertyName("signatureType")]
public string? SignatureType { get; init; }
/// <summary>
/// Age of the package in days since first publish.
/// </summary>
[JsonPropertyName("packageAgeDays")]
public int? PackageAgeDays { get; init; }
/// <summary>
/// Number of maintainers.
/// </summary>
[JsonPropertyName("maintainerCount")]
public int? MaintainerCount { get; init; }
/// <summary>
/// Community adoption score based on downloads/stars.
/// </summary>
[JsonPropertyName("adoptionScore")]
public double? AdoptionScore { get; init; }
/// <summary>
/// Known security incidents for this publisher.
/// </summary>
[JsonPropertyName("publisherIncidentCount")]
public int PublisherIncidentCount { get; init; }
/// <summary>
/// Trust factors that contributed to the score.
/// </summary>
[JsonPropertyName("trustFactors")]
public IReadOnlyDictionary<string, double> TrustFactors { get; init; } = new Dictionary<string, double>();
}

View File

@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
namespace StellaOps.Signals.Contracts;
/// <summary>
/// Unknown symbol signal for unresolved dependencies.
/// Flags symbols that could not be resolved during analysis.
/// </summary>
public sealed record UnknownSymbolSignal
{
/// <summary>
/// The unresolved symbol identifier.
/// </summary>
[JsonPropertyName("symbolId")]
public required string SymbolId { get; init; }
/// <summary>
/// Programming language of the symbol.
/// </summary>
[JsonPropertyName("language")]
public string? Language { get; init; }
/// <summary>
/// Expected package containing the symbol.
/// </summary>
[JsonPropertyName("expectedPackage")]
public string? ExpectedPackage { get; init; }
/// <summary>
/// Reason the symbol could not be resolved.
/// </summary>
[JsonPropertyName("reason")]
public required string Reason { get; init; }
/// <summary>
/// Severity of the unresolved symbol (e.g., "warning", "error").
/// </summary>
[JsonPropertyName("severity")]
public string Severity { get; init; } = "warning";
/// <summary>
/// File path where the symbol was referenced.
/// </summary>
[JsonPropertyName("referencingFile")]
public string? ReferencingFile { get; init; }
/// <summary>
/// Line number where the symbol was referenced.
/// </summary>
[JsonPropertyName("lineNumber")]
public int? LineNumber { get; init; }
/// <summary>
/// Suggested resolutions for this symbol.
/// </summary>
[JsonPropertyName("suggestedResolutions")]
public IReadOnlyList<string> SuggestedResolutions { get; init; } = [];
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Description>Shared signal contracts for cross-module signal communication in StellaOps</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
</Project>