up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-07 23:38:50 +02:00
parent 68bc53a07b
commit 3d01bf9edc
49 changed files with 8269 additions and 1728 deletions

View File

@@ -1,8 +1,8 @@
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { CONCELIER_EXPORTER_API_BASE_URL } from './core/api/concelier-exporter.client';
import {
AUTHORITY_CONSOLE_API,
@@ -29,47 +29,48 @@ import { AuthSessionStore } from './core/auth/auth-session.store';
import { OperatorMetadataInterceptor } from './core/orchestrator/operator-metadata.interceptor';
import { MockNotifyApiService } from './testing/mock-notify-api.service';
import { seedAuthSession, type StubAuthSession } from './testing';
import { CVSS_API_BASE_URL } from './core/api/cvss.client';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
{
provide: APP_INITIALIZER,
multi: true,
useFactory: (configService: AppConfigService) => () =>
configService.load(),
deps: [AppConfigService],
},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthHttpInterceptor,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: OperatorMetadataInterceptor,
multi: true,
},
{
provide: CONCELIER_EXPORTER_API_BASE_URL,
useValue: '/api/v1/concelier/exporters/trivy-db',
},
{
provide: AUTHORITY_CONSOLE_API_BASE_URL,
deps: [AppConfigService],
useFactory: (config: AppConfigService) => {
const authorityBase = config.config.apiBaseUrls.authority;
try {
return new URL('/console', authorityBase).toString();
} catch {
const normalized = authorityBase.endsWith('/')
? authorityBase.slice(0, -1)
: authorityBase;
return `${normalized}/console`;
}
},
},
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
{
provide: APP_INITIALIZER,
multi: true,
useFactory: (configService: AppConfigService) => () =>
configService.load(),
deps: [AppConfigService],
},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthHttpInterceptor,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: OperatorMetadataInterceptor,
multi: true,
},
{
provide: CONCELIER_EXPORTER_API_BASE_URL,
useValue: '/api/v1/concelier/exporters/trivy-db',
},
{
provide: AUTHORITY_CONSOLE_API_BASE_URL,
deps: [AppConfigService],
useFactory: (config: AppConfigService) => {
const authorityBase = config.config.apiBaseUrls.authority;
try {
return new URL('/console', authorityBase).toString();
} catch {
const normalized = authorityBase.endsWith('/')
? authorityBase.slice(0, -1)
: authorityBase;
return `${normalized}/console`;
}
},
},
AuthorityConsoleApiHttpClient,
{
provide: AUTHORITY_CONSOLE_API,
@@ -105,6 +106,19 @@ export const appConfig: ApplicationConfig = {
}
},
},
{
provide: CVSS_API_BASE_URL,
deps: [AppConfigService],
useFactory: (config: AppConfigService) => {
const policyBase = config.config.apiBaseUrls.policy;
try {
return new URL('/api/cvss', policyBase).toString();
} catch {
const normalized = policyBase.endsWith('/') ? policyBase.slice(0, -1) : policyBase;
return `${normalized}/api/cvss`;
}
},
},
RiskHttpClient,
MockRiskApi,
{
@@ -166,10 +180,10 @@ export const appConfig: ApplicationConfig = {
provide: NOTIFY_TENANT_ID,
useValue: 'tenant-dev',
},
MockNotifyApiService,
{
provide: NOTIFY_API,
useExisting: MockNotifyApiService,
},
],
};
MockNotifyApiService,
{
provide: NOTIFY_API,
useExisting: MockNotifyApiService,
},
],
};

View File

@@ -0,0 +1,87 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { AuthSessionStore } from '../auth/auth-session.store';
import { CvssClient, CVSS_API_BASE_URL } from './cvss.client';
import { CvssReceipt, CvssReceiptDto } from './cvss.models';
class FakeAuthSessionStore {
getActiveTenantId(): string | null {
return 'tenant-123';
}
}
describe('CvssClient', () => {
let httpMock: HttpTestingController;
let client: CvssClient;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
CvssClient,
{ provide: CVSS_API_BASE_URL, useValue: '/api/cvss' },
{ provide: AuthSessionStore, useClass: FakeAuthSessionStore },
],
});
httpMock = TestBed.inject(HttpTestingController);
client = TestBed.inject(CvssClient);
});
afterEach(() => {
httpMock.verify();
});
it('adds tenant headers and maps receipt response', () => {
const dto: CvssReceiptDto = {
receiptId: 'rcpt-1',
vulnerabilityId: 'CVE-2025-0001',
createdAt: '2025-12-07T12:00:00Z',
createdBy: 'tester@example.com',
vectorString: 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H',
severity: 'Critical',
scores: {
baseScore: 9.0,
threatScore: 9.0,
environmentalScore: 9.1,
fullScore: 9.1,
effectiveScore: 9.1,
effectiveScoreType: 'Environmental',
},
policyRef: { policyId: 'default', version: '1.0.0', hash: 'sha256:abc' },
evidence: [
{
uri: 'cas://evidence/1',
description: 'Vendor advisory evidence',
source: 'vendor',
collectedAt: '2025-12-07T10:00:00Z',
},
],
history: [
{
historyId: 'hist-1',
reason: 'Initial scoring',
actor: 'tester@example.com',
createdAt: '2025-12-07T12:00:00Z',
},
],
};
let receipt: CvssReceipt | undefined;
client.getReceipt(dto.receiptId).subscribe((result) => (receipt = result));
const req = httpMock.expectOne('/api/cvss/receipts/rcpt-1');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('X-Stella-Tenant')).toBe('tenant-123');
expect(req.request.headers.has('X-Stella-Trace-Id')).toBeTrue();
req.flush(dto);
expect(receipt?.score.overall).toBe(9.1);
expect(receipt?.score.effectiveType).toBe('Environmental');
expect(receipt?.policy.policyId).toBe('default');
expect(receipt?.evidence[0].uri).toBe('cas://evidence/1');
expect(receipt?.history[0].reason).toBe('Initial scoring');
});
});

View File

@@ -1,58 +1,117 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Inject, Injectable, InjectionToken } from '@angular/core';
import { Observable, map } from 'rxjs';
import { CvssReceipt } from './cvss.models';
import { AuthSessionStore } from '../auth/auth-session.store';
import {
CvssEvidenceDto,
CvssHistoryDto,
CvssHistoryEntry,
CvssReceipt,
CvssReceiptDto,
CvssScoresDto,
CvssEvidenceItem,
} from './cvss.models';
import { generateTraceId } from './trace.util';
export const CVSS_API_BASE_URL = new InjectionToken<string>('CVSS_API_BASE_URL');
/**
* Placeholder CVSS client until Policy Gateway endpoint is wired.
* Emits deterministic sample data for UI development and tests.
*/
@Injectable({
providedIn: 'root',
})
export class CvssClient {
constructor(
private readonly http: HttpClient,
private readonly authSession: AuthSessionStore,
@Inject(CVSS_API_BASE_URL) private readonly baseUrl: string
) {}
getReceipt(receiptId: string): Observable<CvssReceipt> {
const sample: CvssReceipt = {
receiptId,
vulnerabilityId: 'CVE-2025-1234',
createdAt: '2025-12-05T12:00:00Z',
createdBy: 'analyst@example.org',
const tenant = this.resolveTenant();
const headers = this.buildHeaders(tenant);
const url = `${this.baseUrl}/receipts/${encodeURIComponent(receiptId)}`;
return this.http
.get<CvssReceiptDto>(url, { headers })
.pipe(map((dto) => this.toView(dto)));
}
private toView(dto: CvssReceiptDto): CvssReceipt {
const scores: CvssScoresDto = dto.scores ?? ({} as CvssScoresDto);
const policyRef = dto.policyRef;
const overall =
scores.effectiveScore ??
scores.fullScore ??
scores.environmentalScore ??
scores.threatScore ??
scores.baseScore;
return {
receiptId: dto.receiptId,
vulnerabilityId: dto.vulnerabilityId,
createdAt: dto.createdAt,
createdBy: dto.createdBy,
score: {
base: 7.6,
threat: 7.6,
environmental: 8.1,
overall: 8.1,
vector:
'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H',
severity: 'High',
base: scores.baseScore,
threat: scores.threatScore ?? scores.baseScore,
environmental: scores.environmentalScore ?? scores.threatScore ?? scores.baseScore,
full: scores.fullScore ?? scores.environmentalScore ?? scores.threatScore ?? scores.baseScore,
overall: overall ?? 0,
effectiveType: scores.effectiveScoreType,
vector: dto.vectorString,
severity: dto.severity,
},
policy: {
policyId: 'policy-bundle-main',
policyHash: 'sha256:deadbeefcafec0ffee1234',
version: '1.0.0',
policyId: policyRef?.policyId ?? 'unknown',
policyHash: policyRef?.hash,
version: policyRef?.version,
activatedAt: policyRef?.activatedAt,
},
evidence: [
{
id: 'ev-001',
description: 'Upstream advisory references vulnerable TLS parser',
source: 'NVD',
},
{
id: 'ev-002',
description: 'Vendor bulletin confirms threat active in region',
source: 'Vendor',
},
],
history: [
{
version: 1,
changedAt: '2025-12-05T12:00:00Z',
changedBy: 'analyst@example.org',
reason: 'Initial scoring',
},
],
evidence: (dto.evidence ?? []).map((item, idx) => this.mapEvidence(item, idx)),
history: (dto.history ?? []).map((entry, idx) => this.mapHistory(entry, idx, dto)),
};
}
return of(sample);
private mapEvidence(item: CvssEvidenceDto, index: number): CvssEvidenceItem {
const id = item.uri ?? item.dsseRef ?? `evidence-${index + 1}`;
return {
id,
description: item.description ?? item.type ?? item.uri ?? 'Evidence item',
source: item.source,
uri: item.uri,
dsseRef: item.dsseRef,
collectedAt: item.collectedAt,
retentionClass: item.retentionClass,
isAuthoritative: item.isAuthoritative,
verifiedAt: item.verifiedAt,
isRedacted: item.isRedacted,
};
}
private mapHistory(entry: CvssHistoryDto, index: number, dto: CvssReceiptDto): CvssHistoryEntry {
return {
id: entry.historyId ?? `history-${index + 1}`,
changedAt: entry.createdAt ?? dto.modifiedAt ?? dto.createdAt,
changedBy: entry.actor ?? dto.modifiedBy ?? dto.createdBy,
reason: entry.reason,
field: entry.field,
previousValue: entry.previousValue,
newValue: entry.newValue,
referenceUri: entry.referenceUri,
};
}
private buildHeaders(tenantId: string): HttpHeaders {
let headers = new HttpHeaders({ 'X-Stella-Tenant': tenantId, 'X-Stella-Trace-Id': generateTraceId() });
return headers;
}
private resolveTenant(): string {
const tenant = this.authSession.getActiveTenantId();
if (!tenant) {
throw new Error('CvssClient requires an active tenant identifier.');
}
return tenant;
}
}

View File

@@ -1,29 +1,112 @@
export interface CvssScoresDto {
readonly baseScore: number;
readonly threatScore?: number;
readonly environmentalScore?: number;
readonly fullScore?: number;
readonly effectiveScore: number;
readonly effectiveScoreType: string;
}
export interface CvssPolicyRefDto {
readonly policyId: string;
readonly version: string;
readonly hash: string;
readonly activatedAt?: string;
}
export interface CvssEvidenceDto {
readonly type?: string;
readonly uri?: string;
readonly description?: string;
readonly source?: string;
readonly collectedAt?: string;
readonly dsseRef?: string;
readonly isAuthoritative?: boolean;
readonly isRedacted?: boolean;
readonly verifiedAt?: string;
readonly retentionClass?: string;
}
export interface CvssHistoryDto {
readonly historyId: string;
readonly field?: string;
readonly previousValue?: string;
readonly newValue?: string;
readonly reason?: string;
readonly referenceUri?: string;
readonly actor?: string;
readonly createdAt?: string;
}
export interface CvssReceiptDto {
readonly receiptId: string;
readonly schemaVersion?: string;
readonly format?: string;
readonly vulnerabilityId: string;
readonly tenantId?: string;
readonly createdAt: string;
readonly createdBy: string;
readonly modifiedAt?: string;
readonly modifiedBy?: string;
readonly cvssVersion?: string;
readonly baseMetrics?: unknown;
readonly threatMetrics?: unknown;
readonly environmentalMetrics?: unknown;
readonly supplementalMetrics?: unknown;
readonly scores: CvssScoresDto;
readonly vectorString: string;
readonly severity: string;
readonly policyRef: CvssPolicyRefDto;
readonly evidence?: readonly CvssEvidenceDto[];
readonly exportHash?: string;
readonly attestationRefs?: readonly string[];
readonly inputHash?: string;
readonly history?: readonly CvssHistoryDto[];
readonly amendsReceiptId?: string;
readonly supersedesReceiptId?: string;
readonly isActive?: boolean;
}
export interface CvssScoreBreakdown {
readonly base: number;
readonly threat: number;
readonly environmental: number;
readonly base?: number;
readonly threat?: number;
readonly environmental?: number;
readonly full?: number;
readonly overall: number;
readonly effectiveType?: string;
readonly vector: string;
readonly severity: string;
}
export interface CvssPolicySummary {
readonly policyId: string;
readonly policyHash: string;
readonly policyHash?: string;
readonly version?: string;
readonly activatedAt?: string;
}
export interface CvssEvidenceItem {
readonly id: string;
readonly description: string;
readonly source: string;
readonly description?: string;
readonly source?: string;
readonly uri?: string;
readonly dsseRef?: string;
readonly collectedAt?: string;
readonly retentionClass?: string;
readonly isAuthoritative?: boolean;
readonly verifiedAt?: string;
readonly isRedacted?: boolean;
}
export interface CvssHistoryEntry {
readonly version: number;
readonly id: string;
readonly changedAt: string;
readonly changedBy: string;
readonly reason?: string;
readonly field?: string;
readonly previousValue?: string;
readonly newValue?: string;
readonly referenceUri?: string;
}
export interface CvssReceipt {

View File

@@ -7,14 +7,17 @@
<span class="cvss-receipt__id">#{{ receipt.receiptId }}</span>
</h1>
<p class="cvss-receipt__meta">
Created {{ receipt.createdAt }} by {{ receipt.createdBy }} · Policy
Created {{ receipt.createdAt }} by {{ receipt.createdBy }} - Policy
{{ receipt.policy.policyId }} ({{ receipt.policy.version ?? 'v1' }})
</p>
</div>
<div class="cvss-receipt__score">
<div class="cvss-score-badge" [class.cvss-score-badge--critical]="receipt.score.overall >= 9">
{{ receipt.score.overall | number : '1.1-1' }}
<span class="cvss-score-badge__label">{{ receipt.score.severity }}</span>
<span class="cvss-score-badge__label">
{{ receipt.score.severity }}
<span class="cvss-score-badge__type">({{ receipt.score.effectiveType ?? 'Effective' }})</span>
</span>
</div>
<p class="cvss-receipt__vector">{{ receipt.score.vector }}</p>
</div>
@@ -47,19 +50,19 @@
<section class="cvss-panel" *ngIf="activeTab === 'base'">
<h2>Base Metrics</h2>
<p>Base score: {{ receipt.score.base | number : '1.1-1' }}</p>
<p>Base score: {{ receipt.score.base ?? 'n/a' }}</p>
<p>Vector: {{ receipt.score.vector }}</p>
</section>
<section class="cvss-panel" *ngIf="activeTab === 'threat'">
<h2>Threat Metrics</h2>
<p>Threat-adjusted score: {{ receipt.score.threat | number : '1.1-1' }}</p>
<p>Threat-adjusted score: {{ receipt.score.threat ?? 'n/a' }}</p>
<p>Vector: {{ receipt.score.vector }}</p>
</section>
<section class="cvss-panel" *ngIf="activeTab === 'environmental'">
<h2>Environmental Metrics</h2>
<p>Environmental score: {{ receipt.score.environmental | number : '1.1-1' }}</p>
<p>Environmental score: {{ receipt.score.environmental ?? 'n/a' }}</p>
<p>Vector: {{ receipt.score.vector }}</p>
</section>
@@ -69,7 +72,8 @@
<li *ngFor="let item of receipt.evidence; trackBy: trackById">
<p class="evidence__id">{{ item.id }}</p>
<p>{{ item.description }}</p>
<p class="evidence__source">Source: {{ item.source }}</p>
<p class="evidence__source">Source: {{ item.source ?? 'unknown' }}</p>
<p *ngIf="item.uri" class="evidence__uri">{{ item.uri }}</p>
</li>
</ul>
</section>
@@ -78,16 +82,17 @@
<h2>Policy</h2>
<p>Policy ID: {{ receipt.policy.policyId }}</p>
<p>Version: {{ receipt.policy.version ?? 'v1' }}</p>
<p>Hash: {{ receipt.policy.policyHash }}</p>
<p>Hash: {{ receipt.policy.policyHash ?? 'n/a' }}</p>
</section>
<section class="cvss-panel" *ngIf="activeTab === 'history'">
<h2>History</h2>
<ul>
<li *ngFor="let entry of receipt.history">
<li *ngFor="let entry of receipt.history; trackBy: trackById">
<p>
v{{ entry.version }} · {{ entry.changedAt }} by {{ entry.changedBy }}
<span *ngIf="entry.reason"> {{ entry.reason }}</span>
{{ entry.changedAt }} by {{ entry.changedBy }}
<span *ngIf="entry.reason">- {{ entry.reason }}</span>
<span *ngIf="entry.field"> ({{ entry.field }} -> {{ entry.newValue ?? 'updated' }})</span>
</p>
</li>
</ul>

View File

@@ -18,7 +18,9 @@ describe(CvssReceiptComponent.name, () => {
base: 7.6,
threat: 7.6,
environmental: 8.1,
full: 8.1,
overall: 8.1,
effectiveType: 'Environmental',
vector: 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H',
severity: 'High',
},
@@ -28,7 +30,14 @@ describe(CvssReceiptComponent.name, () => {
version: '1.0.0',
},
evidence: [],
history: [],
history: [
{
id: 'history-1',
changedAt: '2025-12-05T12:00:00Z',
changedBy: 'analyst@example.org',
reason: 'Initial scoring',
},
],
};
beforeEach(async () => {

View File

@@ -29,7 +29,7 @@ export class CvssReceiptComponent implements OnInit {
);
}
trackById(_: number, item: { id?: string }): string | undefined {
return item.id;
trackById(index: number, item: { id?: string }): string {
return item.id ?? `${index}`;
}
}