Add SBOM, symbols, traces, and VEX files for CVE-2022-21661 SQLi case
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created CycloneDX and SPDX SBOM files for both reachable and unreachable images. - Added symbols.json detailing function entry and sink points in the WordPress code. - Included runtime traces for function calls in both reachable and unreachable scenarios. - Developed OpenVEX files indicating vulnerability status and justification for both cases. - Updated README for evaluator harness to guide integration with scanner output.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Excititor.Attestation.Verification;
|
||||
using StellaOps.Excititor.Attestation.Extensions;
|
||||
@@ -17,13 +20,17 @@ using StellaOps.Excititor.Formats.OpenVEX;
|
||||
using StellaOps.Excititor.Policy;
|
||||
using StellaOps.Excititor.Storage.Mongo;
|
||||
using StellaOps.Excititor.WebService.Endpoints;
|
||||
using StellaOps.Excititor.WebService.Extensions;
|
||||
using StellaOps.Excititor.WebService.Options;
|
||||
using StellaOps.Excititor.WebService.Services;
|
||||
using StellaOps.Excititor.Core.Aoc;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Bson;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
var services = builder.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
var services = builder.Services;
|
||||
services.AddOptions<VexMongoStorageOptions>()
|
||||
.Bind(configuration.GetSection("Excititor:Storage:Mongo"))
|
||||
.ValidateOnStart();
|
||||
@@ -34,8 +41,11 @@ services.AddCycloneDxNormalizer();
|
||||
services.AddOpenVexNormalizer();
|
||||
services.AddSingleton<IVexSignatureVerifier, NoopVexSignatureVerifier>();
|
||||
services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>();
|
||||
services.AddOptions<ExcititorObservabilityOptions>()
|
||||
.Bind(configuration.GetSection("Excititor:Observability"));
|
||||
services.AddScoped<ExcititorHealthService>();
|
||||
services.AddExcititorAocGuards();
|
||||
services.AddVexExportEngine();
|
||||
services.AddVexExportEngine();
|
||||
services.AddVexExportCacheServices();
|
||||
services.AddVexAttestation();
|
||||
services.Configure<VexAttestationClientOptions>(configuration.GetSection("Excititor:Attestation:Client"));
|
||||
@@ -85,14 +95,17 @@ if (offlineSection.Exists())
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddHealthChecks();
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
services.AddMemoryCache();
|
||||
services.AddAuthentication();
|
||||
services.AddAuthorization();
|
||||
services.AddMemoryCache();
|
||||
services.AddAuthentication();
|
||||
services.AddAuthorization();
|
||||
|
||||
builder.ConfigureExcititorTelemetry();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseObservabilityHeaders();
|
||||
|
||||
app.MapGet("/excititor/status", async (HttpContext context,
|
||||
IEnumerable<IVexArtifactStore> artifactStores,
|
||||
@@ -143,26 +156,428 @@ app.MapGet("/excititor/statements/{vulnerabilityId}/{productKey}", async (
|
||||
return Results.Ok(claims);
|
||||
});
|
||||
|
||||
app.MapPost("/excititor/admin/backfill-statements", async (
|
||||
VexStatementBackfillRequest? request,
|
||||
VexStatementBackfillService backfillService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
app.MapPost("/excititor/admin/backfill-statements", async (
|
||||
VexStatementBackfillRequest? request,
|
||||
VexStatementBackfillService backfillService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
request ??= new VexStatementBackfillRequest();
|
||||
var result = await backfillService.RunAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
var message = FormattableString.Invariant(
|
||||
$"Backfill completed: evaluated {result.DocumentsEvaluated}, backfilled {result.DocumentsBackfilled}, claims written {result.ClaimsWritten}, skipped {result.SkippedExisting}, failures {result.NormalizationFailures}.");
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
message,
|
||||
summary = result
|
||||
});
|
||||
});
|
||||
|
||||
IngestEndpoints.MapIngestEndpoints(app);
|
||||
ResolveEndpoint.MapResolveEndpoint(app);
|
||||
MirrorEndpoints.MapMirrorEndpoints(app);
|
||||
return Results.Ok(new
|
||||
{
|
||||
message,
|
||||
summary = result
|
||||
});
|
||||
});
|
||||
|
||||
app.MapPost("/ingest/vex", async (
|
||||
HttpContext context,
|
||||
VexIngestRequest request,
|
||||
IVexRawStore rawStore,
|
||||
IOptions<VexMongoStorageOptions> storageOptions,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<Program> logger,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.admin");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: true, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
VexRawDocument document;
|
||||
try
|
||||
{
|
||||
document = VexRawRequestMapper.Map(request, tenant, timeProvider);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or FormatException)
|
||||
{
|
||||
return ValidationProblem(ex.Message);
|
||||
}
|
||||
|
||||
var existing = await rawStore.FindByDigestAsync(document.Digest, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await rawStore.StoreAsync(document, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (ExcititorAocGuardException guardException)
|
||||
{
|
||||
logger.LogWarning(
|
||||
guardException,
|
||||
"AOC guard rejected VEX ingest tenant={Tenant} digest={Digest}",
|
||||
tenant,
|
||||
document.Digest);
|
||||
return MapGuardException(guardException);
|
||||
}
|
||||
|
||||
var inserted = existing is null;
|
||||
if (inserted)
|
||||
{
|
||||
context.Response.Headers.Location = $"/vex/raw/{Uri.EscapeDataString(document.Digest)}";
|
||||
}
|
||||
|
||||
var response = new VexIngestResponse(document.Digest, inserted, tenant, document.RetrievedAt);
|
||||
return Results.Json(response, statusCode: inserted ? StatusCodes.Status201Created : StatusCodes.Status200OK);
|
||||
});
|
||||
|
||||
app.MapGet("/vex/raw", async (
|
||||
HttpContext context,
|
||||
IMongoDatabase database,
|
||||
IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out _, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var collection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Raw);
|
||||
var query = context.Request.Query;
|
||||
var filters = new List<FilterDefinition<BsonDocument>>();
|
||||
var builder = Builders<BsonDocument>.Filter;
|
||||
|
||||
if (query.TryGetValue("providerId", out var providerValues))
|
||||
{
|
||||
var providers = providerValues
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value!.Trim())
|
||||
.ToArray();
|
||||
if (providers.Length > 0)
|
||||
{
|
||||
filters.Add(builder.In("ProviderId", providers));
|
||||
}
|
||||
}
|
||||
|
||||
if (query.TryGetValue("digest", out var digestValues))
|
||||
{
|
||||
var digests = digestValues
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value!.Trim())
|
||||
.ToArray();
|
||||
if (digests.Length > 0)
|
||||
{
|
||||
filters.Add(builder.In("Digest", digests));
|
||||
}
|
||||
}
|
||||
|
||||
if (query.TryGetValue("format", out var formatValues))
|
||||
{
|
||||
var formats = formatValues
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value!.Trim().ToLowerInvariant())
|
||||
.ToArray();
|
||||
if (formats.Length > 0)
|
||||
{
|
||||
filters.Add(builder.In("Format", formats));
|
||||
}
|
||||
}
|
||||
|
||||
if (query.TryGetValue("since", out var sinceValues) && DateTimeOffset.TryParse(sinceValues.FirstOrDefault(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var sinceValue))
|
||||
{
|
||||
filters.Add(builder.Gte("RetrievedAt", sinceValue.UtcDateTime));
|
||||
}
|
||||
|
||||
var cursorToken = query.TryGetValue("cursor", out var cursorValues) ? cursorValues.FirstOrDefault() : null;
|
||||
DateTime? cursorTimestamp = null;
|
||||
string? cursorDigest = null;
|
||||
if (!string.IsNullOrWhiteSpace(cursorToken) && TryDecodeCursor(cursorToken, out var cursorTime, out var cursorId))
|
||||
{
|
||||
cursorTimestamp = cursorTime.UtcDateTime;
|
||||
cursorDigest = cursorId;
|
||||
}
|
||||
|
||||
if (cursorTimestamp is not null && cursorDigest is not null)
|
||||
{
|
||||
var ltTime = builder.Lt("RetrievedAt", cursorTimestamp.Value);
|
||||
var eqTimeLtDigest = builder.And(
|
||||
builder.Eq("RetrievedAt", cursorTimestamp.Value),
|
||||
builder.Lt("Digest", cursorDigest));
|
||||
filters.Add(builder.Or(ltTime, eqTimeLtDigest));
|
||||
}
|
||||
|
||||
var limit = 50;
|
||||
if (query.TryGetValue("limit", out var limitValues) && int.TryParse(limitValues.FirstOrDefault(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var requestedLimit))
|
||||
{
|
||||
limit = Math.Clamp(requestedLimit, 1, 200);
|
||||
}
|
||||
|
||||
var filter = filters.Count == 0 ? builder.Empty : builder.And(filters);
|
||||
var sort = Builders<BsonDocument>.Sort.Descending("RetrievedAt").Descending("Digest");
|
||||
var documents = await collection
|
||||
.Find(filter)
|
||||
.Sort(sort)
|
||||
.Limit(limit)
|
||||
.Project(Builders<BsonDocument>.Projection.Include("Digest").Include("ProviderId").Include("Format").Include("SourceUri").Include("RetrievedAt").Include("Metadata").Include("GridFsObjectId"))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var summaries = new List<VexRawSummaryResponse>(documents.Count);
|
||||
foreach (var document in documents)
|
||||
{
|
||||
var digest = document.TryGetValue("Digest", out var digestValue) && digestValue.IsString ? digestValue.AsString : string.Empty;
|
||||
var providerId = document.TryGetValue("ProviderId", out var providerValue) && providerValue.IsString ? providerValue.AsString : string.Empty;
|
||||
var format = document.TryGetValue("Format", out var formatValue) && formatValue.IsString ? formatValue.AsString : string.Empty;
|
||||
var sourceUri = document.TryGetValue("SourceUri", out var sourceValue) && sourceValue.IsString ? sourceValue.AsString : string.Empty;
|
||||
var retrievedAt = document.TryGetValue("RetrievedAt", out var retrievedValue) && retrievedValue is BsonDateTime bsonDate
|
||||
? bsonDate.ToUniversalTime()
|
||||
: DateTime.UtcNow;
|
||||
var metadata = ReadMetadata(document.TryGetValue("Metadata", out var metadataValue) ? metadataValue : BsonNull.Value);
|
||||
var inlineContent = !document.TryGetValue("GridFsObjectId", out var gridId) || gridId.IsBsonNull || (gridId.IsString && string.IsNullOrWhiteSpace(gridId.AsString));
|
||||
|
||||
summaries.Add(new VexRawSummaryResponse(
|
||||
digest,
|
||||
providerId,
|
||||
format,
|
||||
sourceUri,
|
||||
new DateTimeOffset(retrievedAt),
|
||||
inlineContent,
|
||||
metadata));
|
||||
}
|
||||
|
||||
var hasMore = documents.Count == limit;
|
||||
string? nextCursor = null;
|
||||
if (hasMore && documents.Count > 0)
|
||||
{
|
||||
var last = documents[^1];
|
||||
var lastTime = last.GetValue("RetrievedAt", BsonNull.Value).ToUniversalTime();
|
||||
var lastDigest = last.GetValue("Digest", BsonNull.Value).AsString;
|
||||
nextCursor = EncodeCursor(lastTime, lastDigest);
|
||||
}
|
||||
|
||||
return Results.Json(new VexRawListResponse(summaries, nextCursor, hasMore));
|
||||
});
|
||||
|
||||
app.MapGet("/vex/raw/{digest}", async (
|
||||
string digest,
|
||||
HttpContext context,
|
||||
IVexRawStore rawStore,
|
||||
IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out _, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
return ValidationProblem("digest is required.");
|
||||
}
|
||||
|
||||
var record = await rawStore.FindByDigestAsync(digest.Trim(), cancellationToken).ConfigureAwait(false);
|
||||
if (record is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var rawDocument = VexRawDocumentMapper.ToRawModel(record, storageOptions.Value.DefaultTenant);
|
||||
var response = new VexRawRecordResponse(record.Digest, rawDocument, record.RetrievedAt);
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapGet("/vex/raw/{digest}/provenance", async (
|
||||
string digest,
|
||||
HttpContext context,
|
||||
IVexRawStore rawStore,
|
||||
IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out _, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
return ValidationProblem("digest is required.");
|
||||
}
|
||||
|
||||
var record = await rawStore.FindByDigestAsync(digest.Trim(), cancellationToken).ConfigureAwait(false);
|
||||
if (record is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var rawDocument = VexRawDocumentMapper.ToRawModel(record, storageOptions.Value.DefaultTenant);
|
||||
var response = new VexRawProvenanceResponse(
|
||||
record.Digest,
|
||||
rawDocument.Tenant,
|
||||
rawDocument.Source,
|
||||
rawDocument.Upstream,
|
||||
record.RetrievedAt);
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapPost("/aoc/verify", async (
|
||||
HttpContext context,
|
||||
VexAocVerifyRequest? request,
|
||||
IMongoDatabase database,
|
||||
IVexRawStore rawStore,
|
||||
IVexRawWriteGuard guard,
|
||||
IOptions<VexMongoStorageOptions> storageOptions,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.admin");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var since = (request?.Since ?? now.AddHours(-24)).UtcDateTime;
|
||||
var until = (request?.Until ?? now).UtcDateTime;
|
||||
if (since >= until)
|
||||
{
|
||||
since = until.AddHours(-1);
|
||||
}
|
||||
|
||||
var limit = Math.Clamp(request?.Limit ?? 100, 1, 500);
|
||||
var sources = request?.Sources?
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value!.Trim())
|
||||
.ToArray();
|
||||
var requestedCodes = request?.Codes?
|
||||
.Where(static value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(static value => value!.Trim())
|
||||
.ToArray();
|
||||
|
||||
var builder = Builders<BsonDocument>.Filter;
|
||||
var filter = builder.And(
|
||||
builder.Gte("RetrievedAt", since),
|
||||
builder.Lte("RetrievedAt", until));
|
||||
|
||||
if (sources is { Length: > 0 })
|
||||
{
|
||||
filter &= builder.In("ProviderId", sources);
|
||||
}
|
||||
|
||||
var collection = database.GetCollection<BsonDocument>(VexMongoCollectionNames.Raw);
|
||||
var digests = await collection
|
||||
.Find(filter)
|
||||
.Sort(Builders<BsonDocument>.Sort.Descending("RetrievedAt"))
|
||||
.Limit(limit)
|
||||
.Project(Builders<BsonDocument>.Projection.Include("Digest").Include("RetrievedAt").Include("ProviderId"))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var checkedCount = 0;
|
||||
var violationMap = new Dictionary<string, (int Count, List<VexAocVerifyViolationExample> Examples)>(StringComparer.OrdinalIgnoreCase);
|
||||
const int MaxExamplesPerCode = 5;
|
||||
|
||||
foreach (var digestDocument in digests)
|
||||
{
|
||||
var digestValue = digestDocument.GetValue("Digest", BsonNull.Value).AsString;
|
||||
var provider = digestDocument.GetValue("ProviderId", BsonNull.Value).AsString;
|
||||
|
||||
var domainDocument = await rawStore.FindByDigestAsync(digestValue, cancellationToken).ConfigureAwait(false);
|
||||
if (domainDocument is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rawDocument = VexRawDocumentMapper.ToRawModel(domainDocument, storageOptions.Value.DefaultTenant);
|
||||
try
|
||||
{
|
||||
guard.EnsureValid(rawDocument);
|
||||
checkedCount++;
|
||||
}
|
||||
catch (ExcititorAocGuardException guardException)
|
||||
{
|
||||
checkedCount++;
|
||||
foreach (var violation in guardException.Violations)
|
||||
{
|
||||
var code = violation.ErrorCode;
|
||||
if (requestedCodes is { Length: > 0 } && !requestedCodes.Contains(code, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!violationMap.TryGetValue(code, out var aggregate))
|
||||
{
|
||||
aggregate = (0, new List<VexAocVerifyViolationExample>(MaxExamplesPerCode));
|
||||
}
|
||||
|
||||
aggregate.Count++;
|
||||
if (aggregate.Examples.Count < MaxExamplesPerCode)
|
||||
{
|
||||
aggregate.Examples.Add(new VexAocVerifyViolationExample(
|
||||
provider,
|
||||
digestValue,
|
||||
rawDocument.Upstream.ContentHash,
|
||||
violation.Path));
|
||||
}
|
||||
|
||||
violationMap[code] = aggregate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var violations = violationMap
|
||||
.Select(pair => new VexAocVerifyViolation(pair.Key, pair.Value.Count, pair.Value.Examples))
|
||||
.OrderByDescending(violation => violation.Count)
|
||||
.ToList();
|
||||
|
||||
var response = new VexAocVerifyResponse(
|
||||
tenant,
|
||||
new VexAocVerifyWindow(new DateTimeOffset(since, TimeSpan.Zero), new DateTimeOffset(until, TimeSpan.Zero)),
|
||||
new VexAocVerifyChecked(0, checkedCount),
|
||||
violations,
|
||||
new VexAocVerifyMetrics(checkedCount, violations.Sum(v => v.Count)),
|
||||
digests.Count == limit);
|
||||
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapGet("/obs/excititor/health", async (
|
||||
HttpContext httpContext,
|
||||
ExcititorHealthService healthService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(httpContext, "vex.admin");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
var payload = await healthService.GetAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(payload);
|
||||
});
|
||||
|
||||
IngestEndpoints.MapIngestEndpoints(app);
|
||||
ResolveEndpoint.MapResolveEndpoint(app);
|
||||
MirrorEndpoints.MapMirrorEndpoints(app);
|
||||
|
||||
app.Run();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user