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:
@@ -0,0 +1,84 @@
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction for creating rescan jobs in the scheduler.
|
||||
/// Allows Signals to integrate with the Scheduler module without tight coupling.
|
||||
/// </summary>
|
||||
public interface ISchedulerJobClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a targeted rescan job for a specific package.
|
||||
/// </summary>
|
||||
/// <param name="request">The rescan job request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result indicating success or failure with job ID.</returns>
|
||||
Task<SchedulerJobResult> CreateRescanJobAsync(
|
||||
RescanJobRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Creates multiple rescan jobs in a batch.
|
||||
/// </summary>
|
||||
Task<BatchSchedulerJobResult> CreateRescanJobsAsync(
|
||||
IReadOnlyList<RescanJobRequest> requests,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for creating a rescan job.
|
||||
/// </summary>
|
||||
/// <param name="TenantId">Tenant identifier.</param>
|
||||
/// <param name="UnknownId">ID of the unknown being rescanned.</param>
|
||||
/// <param name="PackageUrl">Package URL (purl) to rescan.</param>
|
||||
/// <param name="PackageVersion">Version to rescan (optional).</param>
|
||||
/// <param name="Priority">Job priority level.</param>
|
||||
/// <param name="CorrelationId">Correlation ID for tracing.</param>
|
||||
public sealed record RescanJobRequest(
|
||||
string TenantId,
|
||||
string UnknownId,
|
||||
string PackageUrl,
|
||||
string? PackageVersion,
|
||||
RescanJobPriority Priority,
|
||||
string? CorrelationId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Priority level for rescan jobs.
|
||||
/// </summary>
|
||||
public enum RescanJobPriority
|
||||
{
|
||||
/// <summary>Immediate processing (HOT band).</summary>
|
||||
High,
|
||||
/// <summary>Normal processing (WARM band).</summary>
|
||||
Normal,
|
||||
/// <summary>Low priority batch processing (COLD band).</summary>
|
||||
Low
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from creating a scheduler job.
|
||||
/// </summary>
|
||||
/// <param name="Success">Whether the job was created.</param>
|
||||
/// <param name="JobId">Scheduler job ID if successful.</param>
|
||||
/// <param name="RunId">Run ID in the scheduler.</param>
|
||||
/// <param name="ErrorMessage">Error message if failed.</param>
|
||||
public sealed record SchedulerJobResult(
|
||||
bool Success,
|
||||
string? JobId = null,
|
||||
string? RunId = null,
|
||||
string? ErrorMessage = null)
|
||||
{
|
||||
public static SchedulerJobResult Succeeded(string jobId, string runId)
|
||||
=> new(true, jobId, runId);
|
||||
|
||||
public static SchedulerJobResult Failed(string error)
|
||||
=> new(false, ErrorMessage: error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result from batch job creation.
|
||||
/// </summary>
|
||||
public sealed record BatchSchedulerJobResult(
|
||||
int TotalRequested,
|
||||
int SuccessCount,
|
||||
int FailureCount,
|
||||
IReadOnlyList<SchedulerJobResult> Results);
|
||||
@@ -0,0 +1,65 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Null implementation of <see cref="ISchedulerJobClient"/> that logs requests
|
||||
/// but does not actually create jobs. Used when Scheduler integration is not configured.
|
||||
/// </summary>
|
||||
public sealed class NullSchedulerJobClient : ISchedulerJobClient
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<NullSchedulerJobClient> _logger;
|
||||
|
||||
public NullSchedulerJobClient(
|
||||
TimeProvider timeProvider,
|
||||
ILogger<NullSchedulerJobClient> logger)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public Task<SchedulerJobResult> CreateRescanJobAsync(
|
||||
RescanJobRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Null scheduler client: Would create rescan job for unknown {UnknownId} (purl={Purl})",
|
||||
request.UnknownId,
|
||||
request.PackageUrl);
|
||||
|
||||
// Generate a fake job ID for testing/development
|
||||
var jobId = $"null-job-{Guid.NewGuid():N}";
|
||||
var runId = $"null-run-{_timeProvider.GetUtcNow():yyyyMMddHHmmss}";
|
||||
|
||||
return Task.FromResult(SchedulerJobResult.Succeeded(jobId, runId));
|
||||
}
|
||||
|
||||
public Task<BatchSchedulerJobResult> CreateRescanJobsAsync(
|
||||
IReadOnlyList<RescanJobRequest> requests,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requests);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Null scheduler client: Would create {Count} rescan jobs",
|
||||
requests.Count);
|
||||
|
||||
var results = requests
|
||||
.Select(r =>
|
||||
{
|
||||
var jobId = $"null-job-{Guid.NewGuid():N}";
|
||||
var runId = $"null-run-{_timeProvider.GetUtcNow():yyyyMMddHHmmss}";
|
||||
return SchedulerJobResult.Succeeded(jobId, runId);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(new BatchSchedulerJobResult(
|
||||
requests.Count,
|
||||
requests.Count,
|
||||
0,
|
||||
results));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Signals.Models;
|
||||
|
||||
namespace StellaOps.Signals.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IRescanOrchestrator"/> that integrates with
|
||||
/// the Scheduler module via <see cref="ISchedulerJobClient"/>.
|
||||
/// </summary>
|
||||
public sealed class SchedulerRescanOrchestrator : IRescanOrchestrator
|
||||
{
|
||||
private readonly ISchedulerJobClient _schedulerClient;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<SchedulerRescanOrchestrator> _logger;
|
||||
|
||||
public SchedulerRescanOrchestrator(
|
||||
ISchedulerJobClient schedulerClient,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<SchedulerRescanOrchestrator> logger)
|
||||
{
|
||||
_schedulerClient = schedulerClient ?? throw new ArgumentNullException(nameof(schedulerClient));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<RescanResult> TriggerRescanAsync(
|
||||
UnknownSymbolDocument unknown,
|
||||
RescanPriority priority,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(unknown);
|
||||
|
||||
var request = CreateJobRequest(unknown, priority);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Creating rescan job for unknown {UnknownId} (purl={Purl}, priority={Priority})",
|
||||
unknown.Id,
|
||||
unknown.Purl,
|
||||
priority);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _schedulerClient.CreateRescanJobAsync(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Rescan job {JobId} created for unknown {UnknownId}",
|
||||
result.JobId,
|
||||
unknown.Id);
|
||||
|
||||
return new RescanResult(
|
||||
unknown.Id,
|
||||
Success: true,
|
||||
NextScheduledRescan: ComputeNextRescan(priority));
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Failed to create rescan job for unknown {UnknownId}: {Error}",
|
||||
unknown.Id,
|
||||
result.ErrorMessage);
|
||||
|
||||
return new RescanResult(
|
||||
unknown.Id,
|
||||
Success: false,
|
||||
ErrorMessage: result.ErrorMessage);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "Exception creating rescan job for unknown {UnknownId}", unknown.Id);
|
||||
|
||||
return new RescanResult(
|
||||
unknown.Id,
|
||||
Success: false,
|
||||
ErrorMessage: ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BatchRescanResult> TriggerBatchRescanAsync(
|
||||
IReadOnlyList<UnknownSymbolDocument> unknowns,
|
||||
RescanPriority priority,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(unknowns);
|
||||
|
||||
if (unknowns.Count == 0)
|
||||
{
|
||||
return new BatchRescanResult(0, 0, 0, []);
|
||||
}
|
||||
|
||||
var requests = unknowns
|
||||
.Select(u => CreateJobRequest(u, priority))
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Creating {Count} rescan jobs with priority {Priority}",
|
||||
requests.Count,
|
||||
priority);
|
||||
|
||||
try
|
||||
{
|
||||
var batchResult = await _schedulerClient.CreateRescanJobsAsync(requests, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var rescanResults = batchResult.Results
|
||||
.Zip(unknowns, (jobResult, unknown) => new RescanResult(
|
||||
unknown.Id,
|
||||
jobResult.Success,
|
||||
jobResult.ErrorMessage,
|
||||
jobResult.Success ? ComputeNextRescan(priority) : null))
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Batch rescan complete: {Success}/{Total} succeeded",
|
||||
batchResult.SuccessCount,
|
||||
batchResult.TotalRequested);
|
||||
|
||||
return new BatchRescanResult(
|
||||
batchResult.TotalRequested,
|
||||
batchResult.SuccessCount,
|
||||
batchResult.FailureCount,
|
||||
rescanResults);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "Exception in batch rescan for {Count} unknowns", unknowns.Count);
|
||||
|
||||
var failedResults = unknowns
|
||||
.Select(u => new RescanResult(u.Id, Success: false, ErrorMessage: ex.Message))
|
||||
.ToList();
|
||||
|
||||
return new BatchRescanResult(
|
||||
unknowns.Count,
|
||||
0,
|
||||
unknowns.Count,
|
||||
failedResults);
|
||||
}
|
||||
}
|
||||
|
||||
private RescanJobRequest CreateJobRequest(UnknownSymbolDocument unknown, RescanPriority priority)
|
||||
{
|
||||
var jobPriority = priority switch
|
||||
{
|
||||
RescanPriority.Immediate => RescanJobPriority.High,
|
||||
RescanPriority.Scheduled => RescanJobPriority.Normal,
|
||||
_ => RescanJobPriority.Low
|
||||
};
|
||||
|
||||
// Extract tenant from the unknown context
|
||||
// For now, use a default tenant if not available
|
||||
var tenantId = ExtractTenantId(unknown);
|
||||
|
||||
return new RescanJobRequest(
|
||||
TenantId: tenantId,
|
||||
UnknownId: unknown.Id,
|
||||
PackageUrl: unknown.Purl ?? unknown.SubjectKey,
|
||||
PackageVersion: unknown.PurlVersion,
|
||||
Priority: jobPriority,
|
||||
CorrelationId: unknown.CallgraphId);
|
||||
}
|
||||
|
||||
private static string ExtractTenantId(UnknownSymbolDocument unknown)
|
||||
{
|
||||
// The CallgraphId often follows pattern: {tenant}:{graph-id}
|
||||
// If not available, use a default
|
||||
if (string.IsNullOrEmpty(unknown.CallgraphId))
|
||||
{
|
||||
return "default";
|
||||
}
|
||||
|
||||
var colonIndex = unknown.CallgraphId.IndexOf(':', StringComparison.Ordinal);
|
||||
return colonIndex > 0
|
||||
? unknown.CallgraphId[..colonIndex]
|
||||
: "default";
|
||||
}
|
||||
|
||||
private DateTimeOffset ComputeNextRescan(RescanPriority priority)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
return priority switch
|
||||
{
|
||||
RescanPriority.Immediate => now.AddMinutes(15), // Re-evaluate after 15 min
|
||||
RescanPriority.Scheduled => now.AddHours(24), // Next day for WARM
|
||||
_ => now.AddDays(7) // Weekly for COLD
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user