CipherShift365 — Blazor CISO Portal
Component: CipherShift365.Portal.Web
Framework: Blazor Server (.NET 10.0)
Render mode: InteractiveServerRenderMode
Auth: JWT (via REST API)
Accessibility: WCAG 2.1 AA target
What It Does
The Blazor CISO Portal is the user-facing web application for the CipherShift365 platform. It provides Chief Information Security Officers, security architects, and compliance auditors with a single-pane-of-glass view over their cryptographic estate's post-quantum readiness. Every data point is read from the REST API /v1 — the portal has zero direct access to files, databases, or cryptographic material.
Key features:
- Sign-in with role selection (no passwords — engagement-tier design)
- Posture dashboard with risk score cards and finding counts
- Findings explorer with filterable/sortable table (risk, role, confidence, free-text search)
- Migration plan viewer with urgency, effort estimates, and rationale
- CNSA 2.0 verdict page with compliant/non-compliant breakdown and gap analysis
- Report download in HTML, Markdown, and branded PDF formats
- Questionnaire assessment with scoring and recommendations
- WCAG accessibility via semantic HTML,
aria-labelattributes, and dark-mode support - Role-driven views — navigation links and action buttons appear/disappear based on role
Why It's Needed (Value Proposition)
| Need | How the portal addresses it |
|---|---|
| Executive visibility | CEO/Executive role sees dashboards, scores, and reports without technical noise. |
| Consultant-led engagements | ConsultantAdmin role can trigger scans, render PDFs, view audit trails, and submit assessments — all from the browser. |
| Compliance evidence | Auditor role sees findings, CNSA verdicts, and can take assessments to document readiness. |
| Zero-install access | Runs in any modern browser. No desktop software, no PowerShell, no CLI required for leadership. |
| Single source of truth | The portal reads exclusively from the REST API. No data discrepancies between curl output and dashboard. |
| Air-gapped ready | Works on an isolated network — no CDN scripts, no external fonts, no telemetry. |
Installation
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 10.0 |
| OS | Windows 10+ / Windows Server 2019+ / Linux x64 |
| Browser | Chrome 100+, Edge 100+, Firefox 100+, Safari 16+ |
| REST API | Running at http://localhost:5000 (or configured base URL) |
| Network | None external — portal-to-API is loopback or same-network only |
Build & Run
cd src/Portal/CipherShift365.Portal.Web
dotnet restore
# Set the API base URL (defaults to http://localhost:5000)
$env:ApiBaseUrl = "http://localhost:5000"
dotnet run --urls http://localhost:5001
The portal starts at http://localhost:5001. Open it in a browser.
Configuration
All configuration lives in appsettings.json:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ApiBaseUrl": "http://localhost:5000"
}
| Setting | Description | Default |
|---|---|---|
ApiBaseUrl |
The REST API base URL the portal calls | http://localhost:5000 |
AllowedHosts |
Host header filtering | * |
For deployment, set ApiBaseUrl via environment variable:
# Windows
$env:ApiBaseUrl = "https://api.internal.example.com"
# Linux
export ApiBaseUrl=https://api.internal.example.com
How to Run / Use
Starting the Full Stack
# Terminal 1 — Start the API
cd src/Portal/CipherShift365.Portal.Api
dotnet run --urls http://localhost:5000
# Terminal 2 — Start the Portal
cd src/Portal/CipherShift365.Portal.Web
$env:ApiBaseUrl = "http://localhost:5000"
dotnet run --urls http://localhost:5001
Open http://localhost:5001 in a browser.
Sign-In Flow
- The landing page shows three role cards: Executive, Consultant Admin, Auditor.
- Click a card — the portal calls
POST /v1/auth/tokenwith the correspondingclientIdand receives a JWT. - The JWT is stored in-memory in
PortalUserStateand attached asAuthorization: Bearerto every subsequent API call. - The sidebar navigation updates dynamically based on role capabilities.
- To switch roles, click Sign Out and choose a different card.
There are no passwords. The engagement-tier design uses client ID as the sole identity assertion. In production, replace with real credential validation or OIDC federation.
User Manual
Page Reference
| Page | Route | Who sees it | What it does |
|---|---|---|---|
| Sign In | /signin |
Everyone (unauthenticated) | Role selection |
| Dashboard | / |
All authenticated users | Score cards, finding summaries, scan trigger |
| Findings | /findings |
All authenticated users | Filterable cryptographic findings table |
| Plan | /plan |
All authenticated users | Prioritized migration plan table |
| CNSA 2.0 | /cnsa |
All authenticated users | CNSA compliance verdict and gap analysis |
| Reports | /reports |
Executive, ConsultantAdmin | HTML preview, Markdown/PDF download |
| Questionnaire | /questionnaire |
ConsultantAdmin, Auditor | PQC readiness assessment |
Role-Driven Views
The navigation menu adapts to the authenticated role:
| Feature | Executive | ConsultantAdmin | Auditor |
|---|---|---|---|
| Dashboard | Visible | Visible | Visible |
| Findings Explorer | Visible | Visible | Visible |
| Migration Plan | Visible | Visible | Visible |
| CNSA Verdict | Visible | Visible | Visible |
| Reports (download) | Visible | Visible | Hidden |
| Questionnaire | Hidden | Visible | Visible |
| Run Scan (button) | Hidden | Visible | Hidden |
| Audit Trail (API only) | Hidden | Visible | Hidden |
This is enforced at two levels:
- Portal UI layer —
PortalUserState.CanScan/CanAudit/CanViewReportscontrol link and button visibility. - API authorization layer — The REST API independently validates the JWT role claim against
EndpointPermissions.
A compromised client cannot escalate privileges because the API rejects unauthorized requests with 403 Forbidden.
Dashboard
The posture dashboard at / displays:
- Six score cards showing the distribution of findings across risk categories: Quantum Broken (red), Quantum Vulnerable (orange), Mitigable (amber), Quantum Safe (green), Total Findings, and System Score.
- A findings summary table with algorithm, role, risk, confidence, and source location.
- A "Run Scan" button (ConsultantAdmin only) that triggers
POST /v1/scansand refreshes the data.
All data is fetched from GET /v1/findings?project=. on page load.
Findings Explorer
The findings explorer at /findings provides an interactive filterable table:
- Risk filter — dropdown for
QuantumBroken,QuantumVulnerable,Mitigable,QuantumSafe - Role filter — dropdown for
Signature,KeyExchange,Kem,Symmetric,Hash,Mac - Confidence filter — dropdown for
High,Medium,Low - Free-text search — type an algorithm name to filter instantly
Filters are applied client-side over the in-memory JSON response — no additional API calls.
Migration Plan
The migration plan at /plan shows:
- Total effort in person-days and item count
- A prioritized table with urgency, current algorithm, target PQC algorithm, effort in days, and rationale
Urgency levels are color-coded: Critical (red), High (orange).
CNSA 2.0 Compliance
The CNSA verdict at /cnsa displays:
- A verdict banner — green for
Compliant, red forNonCompliant - Compliant / NonCompliant counts
- Gap analysis table — for each gap: algorithm, gap description, recommended action
- Evidence footer — profile name and version used for validation
Reports
The reports page at /reports offers three download actions:
- HTML Report — fetches
GET /v1/reports?project=.&format=htmland renders it in an embedded<iframe>. - Markdown Report — fetches the Markdown version and displays it as text.
- Branded PDF — calls
POST /v1/reports/renderand triggers a browser download viawindow.downloadFile()inapp.js.
Only users in the Executive or ConsultantAdmin role see this page.
Questionnaire
See the Questionnaire Engine documentation for details. The portal page:
- Loads the
pqc-readiness-v1template fromGET /v1/questionnaire/templates/pqc-readiness-v1 - Renders each section with questions and appropriate input types (radio groups, text inputs)
- Computes client-side scores for immediate feedback
- Submits via
POST /v1/questionnaire/submitand displays the scored result with recommendations
WCAG Accessibility
The portal targets WCAG 2.1 Level AA compliance:
| Criterion | Implementation |
|---|---|
| 1.1.1 Non-text Content | All interactive elements have text labels. Score cards use <dl>/<dt>/<dd> semantic structure. |
| 1.3.1 Info and Relationships | Tables use <thead>, <tbody>, <th scope>. Forms use <label> wrappers. Navigation is an <nav> landmark with aria-label. |
| 1.4.3 Contrast (Minimum) | Color palette (:root custom properties) meets 4.5:1 for normal text. Dark mode is supported via prefers-color-scheme: dark. |
| 1.4.4 Resize Text | Relative units (rem, em) throughout. Page zooms to 200% without horizontal scroll. |
| 2.1.1 Keyboard | All buttons and links are natively keyboard-operable. No custom widgets trap focus. |
| 2.4.1 Bypass Blocks | Main content is in <main role="main">. Sidebar uses <nav aria-label="Main navigation">. |
| 2.4.2 Page Titled | Each page has an <h1> describing its purpose. |
| 3.3.2 Labels or Instructions | All inputs have associated labels. Question options have explicit radio <label> wrappers. |
| 4.1.2 Name, Role, Value | Standard HTML elements used throughout — <button>, <select>, <input>, <table>. |
Color and contrast key:
--crit/--warn/--ambr/--safedistinguish risk levels and meet non-color-redundancy requirements (all are also conveyed via text labels).- Focus indicators:
.btn-primary:focus-visibledraws a 2px outline with offset.
Developer Manual
Architecture
Project Structure
CipherShift365.Portal.Web/
├── Program.cs # Host builder, DI registration
├── PortalServices.cs # PortalUserState, PortalAuthStateProvider, ApiClient
├── Components/
│ ├── App.razor # Root component
│ ├── Routes.razor # Route table
│ ├── _Imports.razor # Shared usings
│ ├── Layout/
│ │ ├── MainLayout.razor # Sidebar + main content shell
│ │ ├── MainLayout.razor.css # Layout-specific styles
│ │ ├── NavMenu.razor # Bootstrap-based menu (legacy, not primary)
│ │ ├── NavMenu.razor.css
│ │ ├── ReconnectModal.razor # Blazor circuit reconnect UI
│ │ ├── ReconnectModal.razor.css
│ │ └── ReconnectModal.razor.js
│ ├── Pages/
│ │ ├── SignIn.razor # Role card selection
│ │ ├── Dashboard.razor # Posture dashboard + scan trigger
│ │ ├── Findings.razor # Filterable findings explorer
│ │ ├── Plan.razor # Migration plan viewer
│ │ ├── CnsaVerdict.razor # CNSA compliance verdict
│ │ ├── Reports.razor # Report downloads (HTML/MD/PDF)
│ │ ├── Questionnaire.razor # Assessment UI
│ │ └── NotFound.razor # 404 fallback
│ └── Shared/ # Shared components (empty — available for extension)
├── wwwroot/
│ ├── app.css # Full application stylesheet
│ ├── app.js # JS interop (file download)
│ └── favicon.png # Tab icon
├── appsettings.json
├── appsettings.Development.json
└── CipherShift365.Portal.Web.csproj
Authentication Flow (Portal Side)
- User clicks a role card →
SignIn.razorcallsApiClient.SignInAsync(clientId). SignInAsyncPOSTs to/v1/auth/token, parses the JWT from the response.- The JWT is stored in
PortalUserState.Token. PortalAuthStateProvider.SignIn(...)populates claims and callsNotifyAuthenticationStateChanged(...).- Blazor's
[Authorize]attribute on pages and the<AuthorizeView>component react to the auth state. ApiClient.SetAuth()attachesAuthorization: Bearer <token>to every subsequent HTTP call.
Adding a New Page
- Create a
.razorfile inComponents/Pages/. - Add
@page "/your-route"at the top. - Add
@attribute [Authorize]if authenticated access is required. - Add a role guard if needed:
@if (AuthState.State.CanViewReports) { ... }
- Add the navigation link in
MainLayout.razor:
<NavLink href="/your-route">Your Page</NavLink>
Adding a New API Call
Extend ApiClient in PortalServices.cs:
public async Task<JsonDocument?> GetSomethingAsync()
{
SetAuth();
var response = await _http.GetAsync("/v1/something");
if (!response.IsSuccessStatusCode) return null;
var json = await response.Content.ReadAsStringAsync();
return JsonDocument.Parse(json);
}
Styling
All styles are in wwwroot/app.css. The design system uses CSS custom properties:
| Variable | Purpose | Light default | Dark default |
|---|---|---|---|
--bg |
Page background | #f8f9fa |
#1a1a2e |
--fg |
Text color | #1a1a1a |
#e0e0e0 |
--accent |
Brand color / links / headers | #1a3a5c |
#4a8fd4 |
--crit |
QuantumBroken / Critical urgency | #c0392b |
#e74c3c |
--warn |
QuantumVulnerable / High urgency | #d35400 |
#f39c12 |
--ambr |
Mitigable | #b8860b |
#f1c40f |
--safe |
QuantumSafe / Compliant | #27ae60 |
#2ecc71 |
--border |
Table/input borders | #ddd |
#444 |
--card |
Card background | #fff |
#16213e |
The layout uses a sidebar + main content pattern via CSS flexbox (display: flex). At widths below 768px, the layout switches to vertical stacking.
JavaScript Interop
The only JavaScript interop is in app.js — window.downloadFile(filename, mimeType, base64) — which creates a temporary <a> element with a data URI and triggers a click. It is called from Reports.razor for PDF download.
Code Examples
Custom Role-Guarded Component
@page "/admin-panel"
@attribute [Authorize]
@inject PortalAuthStateProvider AuthState
@if (AuthState.State.CanScan)
{
<button class="btn-primary" @onclick="DoAdminAction">Admin Action</button>
}
else
{
<p>This section requires ConsultantAdmin privileges.</p>
}
Raw API Data Display
@page "/debug"
@attribute [Authorize]
@inject ApiClient Api
<h1>Raw Scores</h1>
@if (_json is not null)
{
<pre>@_json.RootElement.GetRawText()</pre>
}
@code {
private JsonDocument? _json;
protected override async Task OnInitializedAsync()
{
_json = await Api.GetAsync("/v1/scores?project=.");
}
}
Programmatic Scan Trigger
@inject ApiClient Api
<button @onclick="RunScan">Start Scan</button>
<p>@_status</p>
@code {
private string _status = "";
private async Task RunScan()
{
_status = "Scanning...";
var result = await Api.PostAsync("/v1/scans", new { project = "." });
if (result is not null)
{
_status = $"Scan {result.RootElement.GetProperty("status").GetString()} — {result.RootElement.GetProperty("findings").GetInt32()} findings";
}
}
}
Configuration Options
| Setting | File | Default | Description |
|---|---|---|---|
ApiBaseUrl |
appsettings.json | http://localhost:5000 |
REST API base URL the portal targets |
Logging:LogLevel:Default |
appsettings.json | Information |
Log verbosity |
AllowedHosts |
appsettings.json | * |
Host header filtering |
BlazorDisableThrowNavigationException |
csproj | true |
Suppresses navigation exceptions for 404 handling |
Third-Party Dependencies
| Package | Version | License | Use |
|---|---|---|---|
Microsoft.AspNetCore.Components.Authorization |
(framework) | MIT | Blazor auth state management |
Microsoft.Extensions.Http |
(framework) | MIT | Typed HttpClient via DI |
CipherShift365.Core.Runtime |
(project ref) | Proprietary | Shared type references |
The portal has no NuGet package dependencies beyond the .NET 10.0 framework — all rendering is standard Blazor. No Bootstrap, no MudBlazor, no JavaScript framework.
Design Decisions & Rationale
| Decision | Rationale |
|---|---|
| Blazor Server (not WASM) | No WebAssembly download for air-gapped networks. SignalR circuit is local (same network as API). |
| No CSS framework | ~200 lines of custom CSS with dark mode — no build pipeline for SCSS/SASS, no node_modules. |
Singleton PortalUserState |
Single user per server process (engagement-tier design). Multi-user deployments would use scoped services or per-circuit state. |
| API-only data access | No direct database or file access. The portal is a UI shell — all logic lives in the API. |
JsonDocument over typed models |
Avoids duplicating server-side DTOs in the Blazor project. Trade-off: no compile-time type safety in Razor code. |
| No anti-forgery for API calls | Anti-forgery is enabled for Blazor forms but API calls use JWT bearer tokens (not cookies). |