feat: Enhance traceability and logging in Risk and Vulnerability clients
- Implemented shared trace ID generation utility for Risk and Vulnerability clients, ensuring consistent trace headers across API calls. - Updated RiskHttpClient and VulnerabilityHttpClient to utilize the new trace ID generation method. - Added validation for artifact metadata in PackRun endpoints, ensuring all artifacts include a digest and positive size. - Enhanced logging payloads in PackRun to include artifact digests and sizes. - Created a utility for generating trace IDs, preferring crypto.randomUUID when available, with a fallback to a ULID-style string. - Added unit tests to verify the presence of trace IDs in HTTP requests for VulnerabilityHttpClient. - Documented query-hash metrics for Vuln Explorer, detailing hashing rules and logging filters to ensure compliance with privacy standards. - Consolidated findings from late-November reviews into a comprehensive advisory for Scanner and SBOM/VEX areas, outlining remediation tracks and gaps.
This commit is contained in:
@@ -284,6 +284,8 @@ public sealed record LogEntryResponse(
|
||||
string Level,
|
||||
string Source,
|
||||
string Message,
|
||||
string Digest,
|
||||
long SizeBytes,
|
||||
DateTimeOffset Timestamp,
|
||||
string? Data)
|
||||
{
|
||||
@@ -293,6 +295,8 @@ public sealed record LogEntryResponse(
|
||||
log.Level.ToString().ToLowerInvariant(),
|
||||
log.Source,
|
||||
log.Message,
|
||||
log.Digest,
|
||||
log.SizeBytes,
|
||||
log.Timestamp,
|
||||
log.Data);
|
||||
}
|
||||
|
||||
@@ -535,6 +535,15 @@ public static class PackRunEndpoints
|
||||
var artifactIds = new List<Guid>();
|
||||
if (request.Artifacts is { Count: > 0 })
|
||||
{
|
||||
if (request.Artifacts.Any(a => string.IsNullOrWhiteSpace(a.Digest) || a.SizeBytes is null or <= 0))
|
||||
{
|
||||
return Results.BadRequest(new PackRunErrorResponse(
|
||||
"invalid_artifact",
|
||||
"All artifacts must include digest and positive sizeBytes.",
|
||||
packRunId,
|
||||
null));
|
||||
}
|
||||
|
||||
var artifacts = request.Artifacts.Select(a => new Artifact(
|
||||
ArtifactId: Guid.NewGuid(),
|
||||
TenantId: tenantId,
|
||||
@@ -616,7 +625,9 @@ public static class PackRunEndpoints
|
||||
packVersion = packRun.PackVersion,
|
||||
exitCode = request.ExitCode,
|
||||
durationMs,
|
||||
artifactCount = artifactIds.Count
|
||||
artifactCount = artifactIds.Count,
|
||||
artifactDigests = request.Artifacts?.Select(a => a.Digest).ToArray() ?? Array.Empty<string>(),
|
||||
artifactSizes = request.Artifacts?.Select(a => a.SizeBytes ?? 0).ToArray() ?? Array.Empty<long>()
|
||||
}));
|
||||
await eventPublisher.PublishAsync(envelope, cancellationToken);
|
||||
|
||||
|
||||
@@ -282,6 +282,8 @@ internal sealed record PackRunLogPayload(
|
||||
string Level,
|
||||
string Source,
|
||||
string Message,
|
||||
string Digest,
|
||||
long SizeBytes,
|
||||
DateTimeOffset Timestamp,
|
||||
string? Data)
|
||||
{
|
||||
@@ -290,6 +292,8 @@ internal sealed record PackRunLogPayload(
|
||||
log.Level.ToString().ToLowerInvariant(),
|
||||
log.Source,
|
||||
log.Message,
|
||||
log.Digest,
|
||||
log.SizeBytes,
|
||||
log.Timestamp,
|
||||
log.Data);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using Xunit;
|
||||
|
||||
@@ -34,7 +37,8 @@ public sealed class FileSurfaceManifestStoreTests : IAsyncDisposable
|
||||
_store = new FileSurfaceManifestStore(
|
||||
cacheOptions,
|
||||
manifestOptions,
|
||||
NullLogger<FileSurfaceManifestStore>.Instance);
|
||||
NullLogger<FileSurfaceManifestStore>.Instance,
|
||||
new TestCryptoHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -201,6 +205,31 @@ public sealed class FileSurfaceManifestStoreTests : IAsyncDisposable
|
||||
$"{hex}.json");
|
||||
}
|
||||
|
||||
private sealed class TestCryptoHash : ICryptoHash
|
||||
{
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> SHA256.HashData(data);
|
||||
|
||||
public string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToHexString(ComputeHash(data, algorithmId)).ToLowerInvariant();
|
||||
|
||||
public string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null)
|
||||
=> Convert.ToBase64String(ComputeHash(data, algorithmId));
|
||||
|
||||
public async ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var buffer = new MemoryStream();
|
||||
await stream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
return SHA256.HashData(buffer.ToArray());
|
||||
}
|
||||
|
||||
public async ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hash = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
| WEB-AOC-19-002 | DONE (2025-11-30) | Added provenance builder, checksum utilities, and DSSE/CMS signature verification helpers with unit tests. |
|
||||
| WEB-AOC-19-003 | DONE (2025-11-30) | Added client-side guard validator (forbidden/derived/unknown fields, provenance/signature checks) with unit fixtures. |
|
||||
| WEB-CONSOLE-23-002 | DOING (2025-12-01) | Console status polling + SSE run stream client/store/UI added; tests pending once env fixed. |
|
||||
| WEB-RISK-66-001 | DOING (2025-12-02) | Added risk gateway HTTP client (trace-id headers), store, `/risk` dashboard with filters, empty state, vuln link, auth guard; added `/vulnerabilities/:vulnId` detail + specs; risk/vuln providers switch via quickstart; awaiting gateway endpoints/test harness. |
|
||||
| WEB-RISK-66-001 | DOING (2025-12-02) | Added risk + vuln gateway HTTP clients (shared trace util), store, `/risk` dashboard with filters/empty state/vuln link, auth guard; added `/vulnerabilities/:vulnId` detail + specs; providers switch via quickstart; awaiting gateway endpoints/test harness. |
|
||||
| WEB-EXC-25-001 | TODO | Exceptions workflow CRUD pending policy scopes. |
|
||||
| WEB-TEN-47-CONTRACT | DONE (2025-12-01) | Gateway tenant auth/ABAC contract doc v1.0 published (`docs/api/gateway/tenant-auth.md`). |
|
||||
| WEB-VULN-29-LEDGER-DOC | DONE (2025-12-01) | Findings Ledger proxy contract doc v1.0 with idempotency + retries (`docs/api/gateway/findings-ledger-proxy.md`). |
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Observable, map } from 'rxjs';
|
||||
import { AuthSessionStore } from '../auth/auth-session.store';
|
||||
import { RiskApi } from './risk.client';
|
||||
import { RiskQueryOptions, RiskResultPage, RiskStats } from './risk.models';
|
||||
import { generateTraceId } from './trace.util';
|
||||
|
||||
export const RISK_API_BASE_URL = new InjectionToken<string>('RISK_API_BASE_URL');
|
||||
|
||||
@@ -18,7 +19,7 @@ export class RiskHttpClient implements RiskApi {
|
||||
|
||||
list(options: RiskQueryOptions): Observable<RiskResultPage> {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? crypto.randomUUID?.() ?? this.generateTraceId();
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
const headers = this.buildHeaders(tenant, options.projectId, traceId);
|
||||
|
||||
let params = new HttpParams();
|
||||
@@ -40,7 +41,7 @@ export class RiskHttpClient implements RiskApi {
|
||||
|
||||
stats(options: Pick<RiskQueryOptions, 'tenantId' | 'projectId' | 'traceId'>): Observable<RiskStats> {
|
||||
const tenant = this.resolveTenant(options.tenantId);
|
||||
const traceId = options.traceId ?? crypto.randomUUID?.() ?? this.generateTraceId();
|
||||
const traceId = options.traceId ?? generateTraceId();
|
||||
const headers = this.buildHeaders(tenant, options.projectId, traceId);
|
||||
|
||||
return this.http
|
||||
@@ -60,13 +61,6 @@ export class RiskHttpClient implements RiskApi {
|
||||
return headers;
|
||||
}
|
||||
|
||||
private generateTraceId(): string {
|
||||
// Lightweight ULID-like generator (time + random) for trace correlation.
|
||||
const time = Date.now().toString(36);
|
||||
const rand = crypto.getRandomValues(new Uint32Array(1))[0].toString(36).padStart(6, '0');
|
||||
return `${time}-${rand}`;
|
||||
}
|
||||
|
||||
private resolveTenant(tenantId?: string): string {
|
||||
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
|
||||
if (!tenant) {
|
||||
|
||||
13
src/Web/StellaOps.Web/src/app/core/api/trace.util.ts
Normal file
13
src/Web/StellaOps.Web/src/app/core/api/trace.util.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Generate a correlation/trace identifier.
|
||||
* Prefers crypto.randomUUID when available; falls back to a lightweight ULID-style string.
|
||||
*/
|
||||
export function generateTraceId(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
const time = Date.now().toString(36);
|
||||
const rand = crypto.getRandomValues(new Uint32Array(1))[0].toString(36).padStart(6, '0');
|
||||
return `${time}-${rand}`;
|
||||
}
|
||||
@@ -40,6 +40,7 @@ describe('VulnerabilityHttpClient', () => {
|
||||
|
||||
const req = httpMock.expectOne('https://api.example.local/vuln?page=1&pageSize=5');
|
||||
expect(req.request.headers.get('X-Stella-Tenant')).toBe('tenant-dev');
|
||||
expect(req.request.headers.has('X-Stella-Trace-Id')).toBeTrue();
|
||||
req.flush(stub);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Vulnerability,
|
||||
VulnerabilityStats,
|
||||
} from './vulnerability.models';
|
||||
import { generateTraceId } from './trace.util';
|
||||
import { VulnerabilityApi } from './vulnerability.client';
|
||||
|
||||
export const VULNERABILITY_API_BASE_URL = new InjectionToken<string>('VULNERABILITY_API_BASE_URL');
|
||||
@@ -23,7 +24,8 @@ export class VulnerabilityHttpClient implements VulnerabilityApi {
|
||||
|
||||
listVulnerabilities(options?: VulnerabilitiesQueryOptions): Observable<VulnerabilitiesResponse> {
|
||||
const tenant = this.resolveTenant(options?.tenantId);
|
||||
const headers = this.buildHeaders(tenant, options?.projectId, options?.traceId);
|
||||
const traceId = options?.traceId ?? generateTraceId();
|
||||
const headers = this.buildHeaders(tenant, options?.projectId, traceId);
|
||||
|
||||
let params = new HttpParams();
|
||||
if (options?.page) params = params.set('page', options.page);
|
||||
@@ -39,13 +41,15 @@ export class VulnerabilityHttpClient implements VulnerabilityApi {
|
||||
|
||||
getVulnerability(vulnId: string): Observable<Vulnerability> {
|
||||
const tenant = this.resolveTenant();
|
||||
const headers = this.buildHeaders(tenant, undefined, undefined);
|
||||
const traceId = generateTraceId();
|
||||
const headers = this.buildHeaders(tenant, undefined, traceId);
|
||||
return this.http.get<Vulnerability>(`${this.baseUrl}/vuln/${encodeURIComponent(vulnId)}`, { headers });
|
||||
}
|
||||
|
||||
getStats(): Observable<VulnerabilityStats> {
|
||||
const tenant = this.resolveTenant();
|
||||
const headers = this.buildHeaders(tenant, undefined, undefined);
|
||||
const traceId = generateTraceId();
|
||||
const headers = this.buildHeaders(tenant, undefined, traceId);
|
||||
return this.http.get<VulnerabilityStats>(`${this.baseUrl}/vuln/status`, { headers });
|
||||
}
|
||||
|
||||
@@ -63,4 +67,5 @@ export class VulnerabilityHttpClient implements VulnerabilityApi {
|
||||
}
|
||||
return tenant;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user