Complete Entrypoint Detection Re-Engineering Program (Sprints 0410-0415) and Sprint 3500.0002.0003 (Proof Replay + API)
Entrypoint Detection Program (100% complete): - Sprint 0411: Semantic Entrypoint Engine - all 25 tasks DONE - Sprint 0412: Temporal & Mesh Entrypoint - all 19 tasks DONE - Sprint 0413: Speculative Execution Engine - all 19 tasks DONE - Sprint 0414: Binary Intelligence - all 19 tasks DONE - Sprint 0415: Predictive Risk Scoring - all tasks DONE Key deliverables: - SemanticEntrypoint schema with ApplicationIntent/CapabilityClass - TemporalEntrypointGraph and MeshEntrypointGraph - ShellSymbolicExecutor with PathEnumerator and PathConfidenceScorer - CodeFingerprint index with symbol recovery - RiskScore with multi-dimensional risk assessment Sprint 3500.0002.0003 (Proof Replay + API): - ManifestEndpoints with DSSE content negotiation - Proof bundle endpoints by root hash - IdempotencyMiddleware with RFC 9530 Content-Digest - Rate limiting (100 req/hr per tenant) - OpenAPI documentation updates Tests: 357 EntryTrace tests pass, WebService tests blocked by pre-existing infrastructure issue
This commit is contained in:
@@ -46,13 +46,15 @@ public interface ISymbolicExecutor
|
||||
/// <param name="ConstraintEvaluator">Evaluator for path feasibility.</param>
|
||||
/// <param name="TrackAllCommands">Whether to track all commands or just terminal ones.</param>
|
||||
/// <param name="PruneInfeasiblePaths">Whether to prune paths with unsatisfiable constraints.</param>
|
||||
/// <param name="ScriptPath">Path to the script being analyzed (for reporting).</param>
|
||||
public sealed record SymbolicExecutionOptions(
|
||||
int MaxDepth = 100,
|
||||
int MaxPaths = 1000,
|
||||
IReadOnlyDictionary<string, string>? InitialEnvironment = null,
|
||||
IConstraintEvaluator? ConstraintEvaluator = null,
|
||||
bool TrackAllCommands = false,
|
||||
bool PruneInfeasiblePaths = true)
|
||||
bool PruneInfeasiblePaths = true,
|
||||
string? ScriptPath = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options with reasonable limits.
|
||||
|
||||
@@ -43,6 +43,20 @@ public sealed class PathConfidenceScorer
|
||||
{
|
||||
weights ??= DefaultWeights;
|
||||
|
||||
// Short-circuit: Infeasible paths have near-zero confidence
|
||||
if (!path.IsFeasible)
|
||||
{
|
||||
return new PathConfidenceAnalysis(
|
||||
path.PathId,
|
||||
0.05f, // Near-zero confidence for infeasible paths
|
||||
ImmutableArray.Create(new ConfidenceFactor(
|
||||
"Feasibility",
|
||||
0.0f,
|
||||
1.0f,
|
||||
"path is infeasible")),
|
||||
ConfidenceLevel.Low);
|
||||
}
|
||||
|
||||
var factors = new List<ConfidenceFactor>();
|
||||
|
||||
// Factor 1: Constraint complexity
|
||||
|
||||
@@ -47,7 +47,10 @@ public sealed class ShellSymbolicExecutor : ISymbolicExecutor
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var script = ShellParser.Parse(source);
|
||||
return ExecuteAsync(script, options ?? SymbolicExecutionOptions.Default, cancellationToken);
|
||||
var opts = options ?? SymbolicExecutionOptions.Default;
|
||||
// Ensure the scriptPath is carried through to the execution tree
|
||||
var optionsWithPath = opts with { ScriptPath = scriptPath };
|
||||
return ExecuteAsync(script, optionsWithPath, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -56,7 +59,8 @@ public sealed class ShellSymbolicExecutor : ISymbolicExecutor
|
||||
SymbolicExecutionOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var builder = new ExecutionTreeBuilder("script", options.MaxDepth);
|
||||
var scriptPath = options.ScriptPath ?? "script";
|
||||
var builder = new ExecutionTreeBuilder(scriptPath, options.MaxDepth);
|
||||
var constraintEvaluator = options.ConstraintEvaluator ?? PatternConstraintEvaluator.Instance;
|
||||
|
||||
var initialState = options.InitialEnvironment is { } env
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IdempotencyKeyRow.cs
|
||||
// Sprint: SPRINT_3500_0002_0003_proof_replay_api
|
||||
// Task: T3 - Idempotency Middleware
|
||||
// Description: Entity for idempotency key storage
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Entity mapping to scanner.idempotency_keys table.
|
||||
/// Stores idempotency keys for POST endpoint deduplication.
|
||||
/// </summary>
|
||||
public sealed class IdempotencyKeyRow
|
||||
{
|
||||
/// <summary>Unique identifier for this key.</summary>
|
||||
public Guid KeyId { get; set; }
|
||||
|
||||
/// <summary>Tenant identifier for multi-tenant isolation.</summary>
|
||||
public string TenantId { get; set; } = default!;
|
||||
|
||||
/// <summary>RFC 9530 Content-Digest header value.</summary>
|
||||
public string ContentDigest { get; set; } = default!;
|
||||
|
||||
/// <summary>Request path for scoping the idempotency key.</summary>
|
||||
public string EndpointPath { get; set; } = default!;
|
||||
|
||||
/// <summary>HTTP status code of the cached response.</summary>
|
||||
public int ResponseStatus { get; set; }
|
||||
|
||||
/// <summary>Cached response body as JSON.</summary>
|
||||
public string? ResponseBody { get; set; }
|
||||
|
||||
/// <summary>Additional response headers to replay.</summary>
|
||||
public string? ResponseHeaders { get; set; }
|
||||
|
||||
/// <summary>When this key was created.</summary>
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
|
||||
/// <summary>When this key expires (24-hour window).</summary>
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
-- Migration: 017_idempotency_keys.sql
|
||||
-- Sprint: SPRINT_3500_0002_0003_proof_replay_api
|
||||
-- Task: T3 - Idempotency Middleware
|
||||
-- Description: Creates table for idempotency key storage with 24-hour window.
|
||||
|
||||
-- Idempotency keys for POST endpoint deduplication
|
||||
CREATE TABLE IF NOT EXISTS scanner.idempotency_keys (
|
||||
key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
content_digest TEXT NOT NULL, -- RFC 9530 Content-Digest header value
|
||||
endpoint_path TEXT NOT NULL, -- Request path for scoping
|
||||
|
||||
-- Cached response
|
||||
response_status INTEGER NOT NULL,
|
||||
response_body JSONB,
|
||||
response_headers JSONB, -- Additional headers to replay
|
||||
|
||||
-- Timing
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + interval '24 hours'),
|
||||
|
||||
-- Unique constraint for idempotency check
|
||||
CONSTRAINT uk_idempotency_tenant_digest_path UNIQUE (tenant_id, content_digest, endpoint_path)
|
||||
);
|
||||
|
||||
-- Index for efficient lookups by tenant and digest
|
||||
CREATE INDEX IF NOT EXISTS ix_idempotency_keys_tenant_digest
|
||||
ON scanner.idempotency_keys (tenant_id, content_digest);
|
||||
|
||||
-- Index for expiration cleanup
|
||||
CREATE INDEX IF NOT EXISTS ix_idempotency_keys_expires_at
|
||||
ON scanner.idempotency_keys (expires_at);
|
||||
|
||||
-- Automatically delete expired keys
|
||||
CREATE OR REPLACE FUNCTION scanner.cleanup_expired_idempotency_keys()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
deleted_count INTEGER;
|
||||
BEGIN
|
||||
DELETE FROM scanner.idempotency_keys
|
||||
WHERE expires_at < now();
|
||||
|
||||
GET DIAGNOSTICS deleted_count = ROW_COUNT;
|
||||
RETURN deleted_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON TABLE scanner.idempotency_keys IS 'Stores idempotency keys for POST endpoint deduplication with 24-hour TTL';
|
||||
COMMENT ON COLUMN scanner.idempotency_keys.content_digest IS 'RFC 9530 Content-Digest header value (e.g., sha-256=:base64:)';
|
||||
COMMENT ON COLUMN scanner.idempotency_keys.expires_at IS '24-hour expiration window for idempotency';
|
||||
@@ -0,0 +1,144 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PostgresIdempotencyKeyRepository.cs
|
||||
// Sprint: SPRINT_3500_0002_0003_proof_replay_api
|
||||
// Task: T3 - Idempotency Middleware
|
||||
// Description: PostgreSQL implementation of idempotency key repository
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
using StellaOps.Scanner.Storage.Entities;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IIdempotencyKeyRepository"/>.
|
||||
/// </summary>
|
||||
public sealed class PostgresIdempotencyKeyRepository : IIdempotencyKeyRepository
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly ILogger<PostgresIdempotencyKeyRepository> _logger;
|
||||
|
||||
public PostgresIdempotencyKeyRepository(
|
||||
NpgsqlDataSource dataSource,
|
||||
ILogger<PostgresIdempotencyKeyRepository> logger)
|
||||
{
|
||||
_dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IdempotencyKeyRow?> TryGetAsync(
|
||||
string tenantId,
|
||||
string contentDigest,
|
||||
string endpointPath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT key_id, tenant_id, content_digest, endpoint_path,
|
||||
response_status, response_body, response_headers,
|
||||
created_at, expires_at
|
||||
FROM scanner.idempotency_keys
|
||||
WHERE tenant_id = @tenantId
|
||||
AND content_digest = @contentDigest
|
||||
AND endpoint_path = @endpointPath
|
||||
AND expires_at > now()
|
||||
""";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("tenantId", tenantId);
|
||||
cmd.Parameters.AddWithValue("contentDigest", contentDigest);
|
||||
cmd.Parameters.AddWithValue("endpointPath", endpointPath);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new IdempotencyKeyRow
|
||||
{
|
||||
KeyId = reader.GetGuid(0),
|
||||
TenantId = reader.GetString(1),
|
||||
ContentDigest = reader.GetString(2),
|
||||
EndpointPath = reader.GetString(3),
|
||||
ResponseStatus = reader.GetInt32(4),
|
||||
ResponseBody = reader.IsDBNull(5) ? null : reader.GetString(5),
|
||||
ResponseHeaders = reader.IsDBNull(6) ? null : reader.GetString(6),
|
||||
CreatedAt = reader.GetDateTime(7),
|
||||
ExpiresAt = reader.GetDateTime(8)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IdempotencyKeyRow> SaveAsync(
|
||||
IdempotencyKeyRow key,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO scanner.idempotency_keys
|
||||
(key_id, tenant_id, content_digest, endpoint_path,
|
||||
response_status, response_body, response_headers,
|
||||
created_at, expires_at)
|
||||
VALUES
|
||||
(@keyId, @tenantId, @contentDigest, @endpointPath,
|
||||
@responseStatus, @responseBody::jsonb, @responseHeaders::jsonb,
|
||||
@createdAt, @expiresAt)
|
||||
ON CONFLICT (tenant_id, content_digest, endpoint_path) DO UPDATE
|
||||
SET response_status = EXCLUDED.response_status,
|
||||
response_body = EXCLUDED.response_body,
|
||||
response_headers = EXCLUDED.response_headers,
|
||||
created_at = EXCLUDED.created_at,
|
||||
expires_at = EXCLUDED.expires_at
|
||||
RETURNING key_id
|
||||
""";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
|
||||
if (key.KeyId == Guid.Empty)
|
||||
{
|
||||
key.KeyId = Guid.NewGuid();
|
||||
}
|
||||
|
||||
cmd.Parameters.AddWithValue("keyId", key.KeyId);
|
||||
cmd.Parameters.AddWithValue("tenantId", key.TenantId);
|
||||
cmd.Parameters.AddWithValue("contentDigest", key.ContentDigest);
|
||||
cmd.Parameters.AddWithValue("endpointPath", key.EndpointPath);
|
||||
cmd.Parameters.AddWithValue("responseStatus", key.ResponseStatus);
|
||||
cmd.Parameters.AddWithValue("responseBody", (object?)key.ResponseBody ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("responseHeaders", (object?)key.ResponseHeaders ?? DBNull.Value);
|
||||
cmd.Parameters.AddWithValue("createdAt", key.CreatedAt);
|
||||
cmd.Parameters.AddWithValue("expiresAt", key.ExpiresAt);
|
||||
|
||||
var keyId = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
key.KeyId = (Guid)keyId!;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Saved idempotency key {KeyId} for tenant {TenantId}, digest {ContentDigest}",
|
||||
key.KeyId, key.TenantId, key.ContentDigest);
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> DeleteExpiredAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = "SELECT scanner.cleanup_expired_idempotency_keys()";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
|
||||
var result = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
var deletedCount = Convert.ToInt32(result);
|
||||
|
||||
if (deletedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("Cleaned up {Count} expired idempotency keys", deletedCount);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// IIdempotencyKeyRepository.cs
|
||||
// Sprint: SPRINT_3500_0002_0003_proof_replay_api
|
||||
// Task: T3 - Idempotency Middleware
|
||||
// Description: Repository interface for idempotency key operations
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using StellaOps.Scanner.Storage.Entities;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository interface for idempotency key operations.
|
||||
/// </summary>
|
||||
public interface IIdempotencyKeyRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get an existing idempotency key.
|
||||
/// </summary>
|
||||
/// <param name="tenantId">Tenant identifier.</param>
|
||||
/// <param name="contentDigest">RFC 9530 Content-Digest header value.</param>
|
||||
/// <param name="endpointPath">Request path.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The idempotency key if found and not expired, null otherwise.</returns>
|
||||
Task<IdempotencyKeyRow?> TryGetAsync(
|
||||
string tenantId,
|
||||
string contentDigest,
|
||||
string endpointPath,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a new idempotency key.
|
||||
/// </summary>
|
||||
/// <param name="key">The idempotency key to save.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The saved idempotency key.</returns>
|
||||
Task<IdempotencyKeyRow> SaveAsync(
|
||||
IdempotencyKeyRow key,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes expired idempotency keys.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Number of deleted keys.</returns>
|
||||
Task<int> DeleteExpiredAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
Reference in New Issue
Block a user