feat: Add native binary analyzer test utilities and implement SM2 signing tests
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled

- Introduced `NativeTestBase` class for ELF, PE, and Mach-O binary parsing helpers and assertions.
- Created `TestCryptoFactory` for SM2 cryptographic provider setup and key generation.
- Implemented `Sm2SigningTests` to validate signing functionality with environment gate checks.
- Developed console export service and store with comprehensive unit tests for export status management.
This commit is contained in:
StellaOps Bot
2025-12-07 13:12:41 +02:00
parent d907729778
commit e53a282fbe
387 changed files with 21941 additions and 1518 deletions

View File

@@ -3,10 +3,16 @@ import { Inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AuthSessionStore } from '../auth/auth-session.store';
import { CONSOLE_API_BASE_URL } from './console-status.client';
import {
CONSOLE_API_BASE_URL,
DEFAULT_EVENT_SOURCE_FACTORY,
EVENT_SOURCE_FACTORY,
EventSourceFactory,
} from './console-status.client';
import {
ConsoleExportEvent,
ConsoleExportRequest,
ConsoleExportResponse,
ConsoleExportStatusDto,
} from './console-export.models';
import { generateTraceId } from './trace.util';
@@ -28,34 +34,63 @@ export class ConsoleExportClient {
constructor(
private readonly http: HttpClient,
private readonly authSession: AuthSessionStore,
@Inject(CONSOLE_API_BASE_URL) private readonly baseUrl: string
@Inject(CONSOLE_API_BASE_URL) private readonly baseUrl: string,
@Inject(EVENT_SOURCE_FACTORY)
private readonly eventSourceFactory: EventSourceFactory = DEFAULT_EVENT_SOURCE_FACTORY
) {}
createExport(
request: ConsoleExportRequest,
options: ExportRequestOptions = {}
): Observable<ConsoleExportResponse> {
): Observable<ConsoleExportStatusDto> {
const headers = options.idempotencyKey
? this.buildHeaders(options).set('Idempotency-Key', options.idempotencyKey)
: this.buildHeaders(options);
return this.http.post<ConsoleExportResponse>(`${this.baseUrl}/exports`, request, { headers });
return this.http.post<ConsoleExportStatusDto>(`${this.baseUrl}/exports`, request, { headers });
}
getExport(exportId: string, options: ExportGetOptions = {}): Observable<ConsoleExportResponse> {
getExport(exportId: string, options: ExportGetOptions = {}): Observable<ConsoleExportStatusDto> {
const headers = this.buildHeaders(options);
return this.http.get<ConsoleExportResponse>(
return this.http.get<ConsoleExportStatusDto>(
`${this.baseUrl}/exports/${encodeURIComponent(exportId)}`,
{ headers }
);
}
private buildHeaders(opts: { tenantId?: string; traceId?: string }): HttpHeaders {
const tenant = (opts.tenantId && opts.tenantId.trim()) || this.authSession.getActiveTenantId();
if (!tenant) {
throw new Error('ConsoleExportClient requires an active tenant identifier.');
}
streamExport(
exportId: string,
options: ExportGetOptions = {}
): Observable<ConsoleExportEvent> {
const tenant = this.resolveTenant(options.tenantId);
const trace = options.traceId ?? generateTraceId();
const url = `${this.baseUrl}/exports/${encodeURIComponent(
exportId
)}/events?tenant=${encodeURIComponent(tenant)}&traceId=${encodeURIComponent(trace)}`;
return new Observable<ConsoleExportEvent>((observer) => {
const source = this.eventSourceFactory(url);
source.onmessage = (event) => {
try {
const parsed = JSON.parse(event.data) as ConsoleExportEvent;
observer.next(parsed);
} catch (err) {
observer.error(err);
}
};
source.onerror = (err) => {
observer.error(err);
source.close();
};
return () => source.close();
});
}
private buildHeaders(opts: { tenantId?: string; traceId?: string }): HttpHeaders {
const tenant = this.resolveTenant(opts.tenantId);
const trace = opts.traceId ?? generateTraceId();
return new HttpHeaders({
@@ -64,4 +99,12 @@ export class ConsoleExportClient {
'X-Stella-Request-Id': trace,
});
}
private resolveTenant(tenantId?: string): string {
const tenant = (tenantId && tenantId.trim()) || this.authSession.getActiveTenantId();
if (!tenant) {
throw new Error('ConsoleExportClient requires an active tenant identifier.');
}
return tenant;
}
}

View File

@@ -1,38 +1,96 @@
export type ConsoleExportStatus =
| 'queued'
| 'running'
| 'succeeded'
| 'failed'
| 'expired';
export type ConsoleExportFormat = 'json' | 'csv' | 'ndjson' | 'pdf';
export interface ConsoleExportScope {
tenantId: string;
projectId?: string;
readonly tenantId: string;
readonly projectId?: string | null;
}
export type ConsoleExportSourceType = 'advisory' | 'vex' | 'policy' | 'scan';
export interface ConsoleExportSource {
type: string;
ids: string[];
}
export interface ConsoleExportFormats {
formats: string[];
readonly type: ConsoleExportSourceType | string;
readonly ids: readonly string[];
}
export interface ConsoleExportAttestations {
include: boolean;
sigstoreBundle?: boolean;
readonly include: boolean;
readonly sigstoreBundle?: boolean;
}
export interface ConsoleExportNotify {
webhooks?: string[];
readonly webhooks?: readonly string[];
readonly email?: readonly string[];
}
export type ConsoleExportPriority = 'low' | 'normal' | 'high' | string;
export type ConsoleExportPriority = 'low' | 'normal' | 'high';
export interface ConsoleExportRequest {
scope: ConsoleExportScope;
sources: ConsoleExportSource[];
formats: string[];
attestations?: ConsoleExportAttestations;
notify?: ConsoleExportNotify;
priority?: ConsoleExportPriority;
readonly scope: ConsoleExportScope;
readonly sources: readonly ConsoleExportSource[];
readonly formats: readonly ConsoleExportFormat[] | readonly string[];
readonly attestations?: ConsoleExportAttestations;
readonly notify?: ConsoleExportNotify;
readonly priority?: ConsoleExportPriority;
}
export interface ConsoleExportResponse {
exportId: string;
status: string;
export interface ConsoleExportOutput {
readonly type: string;
readonly format: ConsoleExportFormat | string;
readonly url: string;
readonly sha256?: string;
readonly expiresAt?: string | null;
}
export interface ConsoleExportProgress {
readonly percent: number;
readonly itemsCompleted?: number;
readonly itemsTotal?: number;
readonly assetsReady?: number;
}
export interface ConsoleExportError {
readonly code: string;
readonly message: string;
}
export interface ConsoleExportStatusDto {
readonly exportId: string;
readonly status: ConsoleExportStatus;
readonly estimateSeconds?: number | null;
readonly retryAfter?: number | null;
readonly createdAt?: string | null;
readonly updatedAt?: string | null;
readonly outputs?: readonly ConsoleExportOutput[];
readonly progress?: ConsoleExportProgress | null;
readonly errors?: readonly ConsoleExportError[];
}
export type ConsoleExportEventType =
| 'started'
| 'progress'
| 'asset_ready'
| 'completed'
| 'failed';
export interface ConsoleExportEvent {
readonly event: ConsoleExportEventType;
readonly exportId: string;
readonly percent?: number;
readonly itemsCompleted?: number;
readonly itemsTotal?: number;
readonly type?: string;
readonly id?: string;
readonly url?: string;
readonly sha256?: string;
readonly status?: ConsoleExportStatus;
readonly manifestUrl?: string;
readonly code?: string;
readonly message?: string;
}

View File

@@ -0,0 +1,70 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { AuthSessionStore } from '../auth/auth-session.store';
import { ConsoleExportClient } from '../api/console-export.client';
import { ConsoleExportRequest } from '../api/console-export.models';
import { ConsoleExportService } from './console-export.service';
import { ConsoleExportStore } from './console-export.store';
class MockExportClient {
createExport() {
return of({ exportId: 'exp-1', status: 'queued' });
}
getExport() {
return of({ exportId: 'exp-1', status: 'running' });
}
streamExport() {
return of({ event: 'completed', exportId: 'exp-1' });
}
}
describe('ConsoleExportService', () => {
let service: ConsoleExportService;
let store: ConsoleExportStore;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
ConsoleExportStore,
ConsoleExportService,
{ provide: ConsoleExportClient, useClass: MockExportClient },
{ provide: AuthSessionStore, useValue: { getActiveTenantId: () => 'tenant-default' } },
],
});
service = TestBed.inject(ConsoleExportService);
store = TestBed.inject(ConsoleExportStore);
});
it('startExport stores status and clears loading', (done) => {
const req: ConsoleExportRequest = {
scope: { tenantId: 't1' },
sources: [{ type: 'advisory', ids: ['a'] }],
formats: ['json'],
};
service.startExport(req).subscribe(() => {
expect(store.status()?.status).toBe('queued');
expect(store.loading()).toBe(false);
done();
});
});
it('refreshStatus updates status', (done) => {
service.refreshStatus('exp-1').subscribe(() => {
expect(store.status()?.status).toBe('running');
done();
});
});
it('streamExport appends events', (done) => {
service.streamExport('exp-1').subscribe(() => {
expect(store.events().length).toBe(1);
expect(store.events()[0].event).toBe('completed');
done();
});
});
});

View File

@@ -0,0 +1,79 @@
import { Injectable } from '@angular/core';
import { catchError, of, tap } from 'rxjs';
import { ConsoleExportClient } from '../api/console-export.client';
import {
ConsoleExportEvent,
ConsoleExportRequest,
ConsoleExportStatusDto,
} from '../api/console-export.models';
import { ConsoleExportStore } from './console-export.store';
@Injectable({ providedIn: 'root' })
export class ConsoleExportService {
constructor(
private readonly client: ConsoleExportClient,
private readonly store: ConsoleExportStore
) {}
startExport(
request: ConsoleExportRequest,
opts?: { tenantId?: string; traceId?: string; idempotencyKey?: string }
) {
this.store.setLoading(true);
this.store.setError(null);
return this.client.createExport(request, opts).pipe(
tap((status) => this.store.setStatus(status)),
tap(() => this.store.setLoading(false)),
catchError((err) => {
console.error('console export create failed', err);
this.store.setError('Unable to start export');
this.store.setLoading(false);
return of(null as ConsoleExportStatusDto | null);
})
);
}
refreshStatus(exportId: string, opts?: { tenantId?: string; traceId?: string }) {
this.store.setLoading(true);
this.store.setError(null);
return this.client.getExport(exportId, opts).pipe(
tap((status) => this.store.setStatus(status)),
tap(() => this.store.setLoading(false)),
catchError((err) => {
console.error('console export status failed', err);
this.store.setError('Unable to load export status');
this.store.setLoading(false);
return of(null as ConsoleExportStatusDto | null);
})
);
}
streamExport(exportId: string, opts?: { tenantId?: string; traceId?: string }) {
this.store.clearEvents();
return this.client.streamExport(exportId, opts).pipe(
tap((evt: ConsoleExportEvent) => this.store.appendEvent(evt)),
catchError((err) => {
console.error('console export stream failed', err);
this.store.setError('Export stream ended with error');
return of(null as ConsoleExportEvent | null);
})
);
}
get status() {
return this.store.status;
}
get loading() {
return this.store.loading;
}
get error() {
return this.store.error;
}
get events() {
return this.store.events;
}
}

View File

@@ -0,0 +1,27 @@
import { ConsoleExportStore } from './console-export.store';
import { ConsoleExportEvent } from '../api/console-export.models';
describe('ConsoleExportStore', () => {
it('stores status, errors, events, and loading', () => {
const store = new ConsoleExportStore();
store.setLoading(true);
expect(store.loading()).toBe(true);
store.setError('err');
expect(store.error()).toBe('err');
store.setStatus({ exportId: 'exp-1', status: 'queued' });
expect(store.status()).toEqual({ exportId: 'exp-1', status: 'queued' });
const evt: ConsoleExportEvent = { event: 'started', exportId: 'exp-1' };
store.appendEvent(evt);
expect(store.events()).toEqual([evt]);
store.clear();
expect(store.status()).toBeNull();
expect(store.error()).toBeNull();
expect(store.loading()).toBe(false);
expect(store.events()).toEqual([]);
});
});

View File

@@ -0,0 +1,43 @@
import { Injectable, computed, signal } from '@angular/core';
import { ConsoleExportEvent, ConsoleExportStatusDto } from '../api/console-export.models';
@Injectable({ providedIn: 'root' })
export class ConsoleExportStore {
private readonly statusSignal = signal<ConsoleExportStatusDto | null>(null);
private readonly loadingSignal = signal(false);
private readonly errorSignal = signal<string | null>(null);
private readonly eventsSignal = signal<ConsoleExportEvent[]>([]);
readonly status = computed(() => this.statusSignal());
readonly loading = computed(() => this.loadingSignal());
readonly error = computed(() => this.errorSignal());
readonly events = computed(() => this.eventsSignal());
setLoading(value: boolean): void {
this.loadingSignal.set(value);
}
setError(message: string | null): void {
this.errorSignal.set(message);
}
setStatus(status: ConsoleExportStatusDto | null): void {
this.statusSignal.set(status);
}
appendEvent(evt: ConsoleExportEvent): void {
const next = [...this.eventsSignal(), evt].slice(-100);
this.eventsSignal.set(next);
}
clearEvents(): void {
this.eventsSignal.set([]);
}
clear(): void {
this.statusSignal.set(null);
this.loadingSignal.set(false);
this.errorSignal.set(null);
this.eventsSignal.set([]);
}
}