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
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user