up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (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
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build 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
Policy Lint & Smoke / policy-lint (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
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
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build 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
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -24,3 +24,4 @@
|
||||
| UI-POLICY-23-005 | DONE (2025-12-05) | Simulator updated with SBOM/advisory pickers and explain trace view; uses PolicyApiService simulate. |
|
||||
| UI-POLICY-23-006 | DONE (2025-12-06) | Explain view route `/policy-studio/packs/:packId/explain/:runId` with trace + JSON/PDF export (uses offline-safe jsPDF shim). |
|
||||
| UI-POLICY-23-001 | DONE (2025-12-05) | Workspace route `/policy-studio/packs` with pack list + quick actions; cached pack store with offline fallback. |
|
||||
| CVSS-UI-190-011 | DONE (2025-12-07) | Added CVSS receipt viewer route (/cvss/receipts/:receiptId) with score badge, tabbed sections, stub client, and unit spec in src/Web/StellaOps.Web. |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import {
|
||||
requireOrchViewerGuard,
|
||||
requireOrchOperatorGuard,
|
||||
@@ -9,61 +9,61 @@ import {
|
||||
requirePolicyApproverGuard,
|
||||
requirePolicyViewerGuard,
|
||||
} from './core/auth';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'dashboard/sources',
|
||||
loadComponent: () =>
|
||||
import('./features/dashboard/sources-dashboard.component').then(
|
||||
(m) => m.SourcesDashboardComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'console/profile',
|
||||
loadComponent: () =>
|
||||
import('./features/console/console-profile.component').then(
|
||||
(m) => m.ConsoleProfileComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'console/status',
|
||||
loadComponent: () =>
|
||||
import('./features/console/console-status.component').then(
|
||||
(m) => m.ConsoleStatusComponent
|
||||
),
|
||||
},
|
||||
// Orchestrator routes - gated by orch:read scope (UI-ORCH-32-001)
|
||||
{
|
||||
path: 'orchestrator',
|
||||
canMatch: [requireOrchViewerGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-dashboard.component').then(
|
||||
(m) => m.OrchestratorDashboardComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orchestrator/jobs',
|
||||
canMatch: [requireOrchViewerGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-jobs.component').then(
|
||||
(m) => m.OrchestratorJobsComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orchestrator/jobs/:jobId',
|
||||
canMatch: [requireOrchViewerGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-job-detail.component').then(
|
||||
(m) => m.OrchestratorJobDetailComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orchestrator/quotas',
|
||||
canMatch: [requireOrchOperatorGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-quotas.component').then(
|
||||
(m) => m.OrchestratorQuotasComponent
|
||||
),
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'dashboard/sources',
|
||||
loadComponent: () =>
|
||||
import('./features/dashboard/sources-dashboard.component').then(
|
||||
(m) => m.SourcesDashboardComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'console/profile',
|
||||
loadComponent: () =>
|
||||
import('./features/console/console-profile.component').then(
|
||||
(m) => m.ConsoleProfileComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'console/status',
|
||||
loadComponent: () =>
|
||||
import('./features/console/console-status.component').then(
|
||||
(m) => m.ConsoleStatusComponent
|
||||
),
|
||||
},
|
||||
// Orchestrator routes - gated by orch:read scope (UI-ORCH-32-001)
|
||||
{
|
||||
path: 'orchestrator',
|
||||
canMatch: [requireOrchViewerGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-dashboard.component').then(
|
||||
(m) => m.OrchestratorDashboardComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orchestrator/jobs',
|
||||
canMatch: [requireOrchViewerGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-jobs.component').then(
|
||||
(m) => m.OrchestratorJobsComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orchestrator/jobs/:jobId',
|
||||
canMatch: [requireOrchViewerGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-job-detail.component').then(
|
||||
(m) => m.OrchestratorJobDetailComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'orchestrator/quotas',
|
||||
canMatch: [requireOrchOperatorGuard],
|
||||
loadComponent: () =>
|
||||
import('./features/orchestrator/orchestrator-quotas.component').then(
|
||||
(m) => m.OrchestratorQuotasComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'policy-studio/packs',
|
||||
@@ -132,61 +132,67 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'concelier/trivy-db-settings',
|
||||
loadComponent: () =>
|
||||
import('./features/trivy-db-settings/trivy-db-settings-page.component').then(
|
||||
(m) => m.TrivyDbSettingsPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'scans/:scanId',
|
||||
loadComponent: () =>
|
||||
import('./features/scans/scan-detail-page.component').then(
|
||||
(m) => m.ScanDetailPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'welcome',
|
||||
loadComponent: () =>
|
||||
import('./features/welcome/welcome-page.component').then(
|
||||
(m) => m.WelcomePageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'risk',
|
||||
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||
loadComponent: () =>
|
||||
import('./features/risk/risk-dashboard.component').then(
|
||||
(m) => m.RiskDashboardComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'vulnerabilities/:vulnId',
|
||||
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||
loadComponent: () =>
|
||||
import('./features/vulnerabilities/vulnerability-detail.component').then(
|
||||
(m) => m.VulnerabilityDetailComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'notify',
|
||||
loadComponent: () =>
|
||||
import('./features/notify/notify-panel.component').then(
|
||||
(m) => m.NotifyPanelComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'auth/callback',
|
||||
loadComponent: () =>
|
||||
import('./features/auth/auth-callback.component').then(
|
||||
(m) => m.AuthCallbackComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'console/profile',
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'console/profile',
|
||||
},
|
||||
];
|
||||
import('./features/trivy-db-settings/trivy-db-settings-page.component').then(
|
||||
(m) => m.TrivyDbSettingsPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'scans/:scanId',
|
||||
loadComponent: () =>
|
||||
import('./features/scans/scan-detail-page.component').then(
|
||||
(m) => m.ScanDetailPageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'welcome',
|
||||
loadComponent: () =>
|
||||
import('./features/welcome/welcome-page.component').then(
|
||||
(m) => m.WelcomePageComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'risk',
|
||||
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||
loadComponent: () =>
|
||||
import('./features/risk/risk-dashboard.component').then(
|
||||
(m) => m.RiskDashboardComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'vulnerabilities/:vulnId',
|
||||
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||
loadComponent: () =>
|
||||
import('./features/vulnerabilities/vulnerability-detail.component').then(
|
||||
(m) => m.VulnerabilityDetailComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'cvss/receipts/:receiptId',
|
||||
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||
loadComponent: () =>
|
||||
import('./features/cvss/cvss-receipt.component').then((m) => m.CvssReceiptComponent),
|
||||
},
|
||||
{
|
||||
path: 'notify',
|
||||
loadComponent: () =>
|
||||
import('./features/notify/notify-panel.component').then(
|
||||
(m) => m.NotifyPanelComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'auth/callback',
|
||||
loadComponent: () =>
|
||||
import('./features/auth/auth-callback.component').then(
|
||||
(m) => m.AuthCallbackComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'console/profile',
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'console/profile',
|
||||
},
|
||||
];
|
||||
|
||||
58
src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts
Normal file
58
src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
import { CvssReceipt } from './cvss.models';
|
||||
|
||||
/**
|
||||
* Placeholder CVSS client until Policy Gateway endpoint is wired.
|
||||
* Emits deterministic sample data for UI development and tests.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CvssClient {
|
||||
getReceipt(receiptId: string): Observable<CvssReceipt> {
|
||||
const sample: CvssReceipt = {
|
||||
receiptId,
|
||||
vulnerabilityId: 'CVE-2025-1234',
|
||||
createdAt: '2025-12-05T12:00:00Z',
|
||||
createdBy: 'analyst@example.org',
|
||||
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',
|
||||
},
|
||||
policy: {
|
||||
policyId: 'policy-bundle-main',
|
||||
policyHash: 'sha256:deadbeefcafec0ffee1234',
|
||||
version: '1.0.0',
|
||||
},
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return of(sample);
|
||||
}
|
||||
}
|
||||
38
src/Web/StellaOps.Web/src/app/core/api/cvss.models.ts
Normal file
38
src/Web/StellaOps.Web/src/app/core/api/cvss.models.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface CvssScoreBreakdown {
|
||||
readonly base: number;
|
||||
readonly threat: number;
|
||||
readonly environmental: number;
|
||||
readonly overall: number;
|
||||
readonly vector: string;
|
||||
readonly severity: string;
|
||||
}
|
||||
|
||||
export interface CvssPolicySummary {
|
||||
readonly policyId: string;
|
||||
readonly policyHash: string;
|
||||
readonly version?: string;
|
||||
}
|
||||
|
||||
export interface CvssEvidenceItem {
|
||||
readonly id: string;
|
||||
readonly description: string;
|
||||
readonly source: string;
|
||||
}
|
||||
|
||||
export interface CvssHistoryEntry {
|
||||
readonly version: number;
|
||||
readonly changedAt: string;
|
||||
readonly changedBy: string;
|
||||
readonly reason?: string;
|
||||
}
|
||||
|
||||
export interface CvssReceipt {
|
||||
readonly receiptId: string;
|
||||
readonly vulnerabilityId: string;
|
||||
readonly createdAt: string;
|
||||
readonly createdBy: string;
|
||||
readonly score: CvssScoreBreakdown;
|
||||
readonly policy: CvssPolicySummary;
|
||||
readonly evidence: readonly CvssEvidenceItem[];
|
||||
readonly history: readonly CvssHistoryEntry[];
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<section class="cvss-receipt" *ngIf="receipt$ | async as receipt">
|
||||
<header class="cvss-receipt__header">
|
||||
<div>
|
||||
<p class="cvss-receipt__label">CVSS Receipt</p>
|
||||
<h1>
|
||||
{{ receipt.vulnerabilityId }}
|
||||
<span class="cvss-receipt__id">#{{ receipt.receiptId }}</span>
|
||||
</h1>
|
||||
<p class="cvss-receipt__meta">
|
||||
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>
|
||||
</div>
|
||||
<p class="cvss-receipt__vector">{{ receipt.score.vector }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="cvss-tabs" aria-label="CVSS receipt sections">
|
||||
<button type="button" [class.active]="activeTab === 'base'" (click)="activeTab = 'base'">
|
||||
Base
|
||||
</button>
|
||||
<button type="button" [class.active]="activeTab === 'threat'" (click)="activeTab = 'threat'">
|
||||
Threat
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
[class.active]="activeTab === 'environmental'"
|
||||
(click)="activeTab = 'environmental'"
|
||||
>
|
||||
Environmental
|
||||
</button>
|
||||
<button type="button" [class.active]="activeTab === 'evidence'" (click)="activeTab = 'evidence'">
|
||||
Evidence
|
||||
</button>
|
||||
<button type="button" [class.active]="activeTab === 'policy'" (click)="activeTab = 'policy'">
|
||||
Policy
|
||||
</button>
|
||||
<button type="button" [class.active]="activeTab === 'history'" (click)="activeTab = 'history'">
|
||||
History
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section class="cvss-panel" *ngIf="activeTab === 'base'">
|
||||
<h2>Base Metrics</h2>
|
||||
<p>Base score: {{ receipt.score.base | number : '1.1-1' }}</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>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>Vector: {{ receipt.score.vector }}</p>
|
||||
</section>
|
||||
|
||||
<section class="cvss-panel" *ngIf="activeTab === 'evidence'">
|
||||
<h2>Evidence</h2>
|
||||
<ul>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="cvss-panel" *ngIf="activeTab === 'policy'">
|
||||
<h2>Policy</h2>
|
||||
<p>Policy ID: {{ receipt.policy.policyId }}</p>
|
||||
<p>Version: {{ receipt.policy.version ?? 'v1' }}</p>
|
||||
<p>Hash: {{ receipt.policy.policyHash }}</p>
|
||||
</section>
|
||||
|
||||
<section class="cvss-panel" *ngIf="activeTab === 'history'">
|
||||
<h2>History</h2>
|
||||
<ul>
|
||||
<li *ngFor="let entry of receipt.history">
|
||||
<p>
|
||||
v{{ entry.version }} · {{ entry.changedAt }} by {{ entry.changedBy }}
|
||||
<span *ngIf="entry.reason">— {{ entry.reason }}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
@@ -0,0 +1,95 @@
|
||||
.cvss-receipt {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cvss-receipt__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cvss-receipt__label {
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cvss-receipt__id {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cvss-receipt__meta {
|
||||
color: #555;
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.cvss-receipt__score {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cvss-score-badge {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 0.4rem;
|
||||
background: #0a5ac2;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.cvss-score-badge__label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cvss-score-badge--critical {
|
||||
background: #b3261e;
|
||||
}
|
||||
|
||||
.cvss-receipt__vector {
|
||||
margin: 0.35rem 0 0;
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cvss-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.cvss-tabs button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cvss-tabs button.active {
|
||||
border-bottom: 2px solid #0a5ac2;
|
||||
color: #0a5ac2;
|
||||
}
|
||||
|
||||
.cvss-panel {
|
||||
background: #f8f9fb;
|
||||
border: 1px solid #e1e4ea;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.evidence__id {
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.evidence__source {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { CvssClient } from '../../core/api/cvss.client';
|
||||
import { CvssReceiptComponent } from './cvss-receipt.component';
|
||||
import { CvssReceipt } from '../../core/api/cvss.models';
|
||||
|
||||
describe(CvssReceiptComponent.name, () => {
|
||||
let fixture: ComponentFixture<CvssReceiptComponent>;
|
||||
|
||||
const sample: CvssReceipt = {
|
||||
receiptId: 'rcpt-123',
|
||||
vulnerabilityId: 'CVE-2025-1234',
|
||||
createdAt: '2025-12-05T12:00:00Z',
|
||||
createdBy: 'analyst@example.org',
|
||||
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',
|
||||
},
|
||||
policy: {
|
||||
policyId: 'policy-bundle-main',
|
||||
policyHash: 'sha256:deadbeef',
|
||||
version: '1.0.0',
|
||||
},
|
||||
evidence: [],
|
||||
history: [],
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CvssReceiptComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
paramMap: of({
|
||||
get: (key: string) => (key === 'receiptId' ? sample.receiptId : null),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CvssClient,
|
||||
useValue: {
|
||||
getReceipt: () => of(sample),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CvssReceiptComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('renders receipt id and vulnerability', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.textContent).toContain(sample.vulnerabilityId);
|
||||
expect(compiled.textContent).toContain(sample.receiptId);
|
||||
});
|
||||
|
||||
it('renders overall score', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('.cvss-score-badge')?.textContent).toContain('8.1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { CvssClient } from '../../core/api/cvss.client';
|
||||
import { CvssReceipt } from '../../core/api/cvss.models';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-cvss-receipt',
|
||||
imports: [CommonModule, RouterModule],
|
||||
templateUrl: './cvss-receipt.component.html',
|
||||
styleUrls: ['./cvss-receipt.component.scss'],
|
||||
})
|
||||
export class CvssReceiptComponent implements OnInit {
|
||||
receipt$!: Observable<CvssReceipt>;
|
||||
|
||||
activeTab: 'base' | 'threat' | 'environmental' | 'evidence' | 'policy' | 'history' =
|
||||
'base';
|
||||
|
||||
constructor(private readonly route: ActivatedRoute, private readonly client: CvssClient) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.receipt$ = this.route.paramMap.pipe(
|
||||
map((params) => params.get('receiptId') ?? ''),
|
||||
switchMap((id) => this.client.getReceipt(id))
|
||||
);
|
||||
}
|
||||
|
||||
trackById(_: number, item: { id?: string }): string | undefined {
|
||||
return item.id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user