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
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:
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
87
src/Web/StellaOps.Web/src/app/core/api/cvss.client.spec.ts
Normal file
87
src/Web/StellaOps.Web/src/app/core/api/cvss.client.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user