365Architect

CipherShift365 — Branded PDF Generator

Component: CipherShift365.Portal.Pdf
Library: QuestPDF (pure .NET, no browser/external renderer)
Target framework: .NET 10.0
License mode: QuestPDF Community (free for <$1M revenue organizations)


What It Does

The Branded PDF Generator produces polished, client-ready PQC Readiness Reports from a DiscoveryReport. Every report is self-contained: deterministic layout, embedded branding, no dynamic content from the internet. It is designed to be run air-gapped — QuestPDF renders everything 100% in-process using the SkiaSharp graphics engine.

The generator produces a multi-page A4 document containing:

  1. Branded header — organization name, report title, date, colored rule line
  2. Executive summary — plain-language narrative with finding counts, risk score, and migration effort
  3. Score cards — six color-coded metric tiles (Quantum Broken, Quantum Vulnerable, Mitigable, Quantum Safe, System Score, Migration Items)
  4. Findings table — every cryptographic asset with algorithm, role, risk, source location, and PQC target
  5. Migration plan table — prioritized migration items with urgency, algorithm, target, effort, and rationale
  6. CNSA 2.0 compliance section (optional) — overall verdict, compliance profile, gap analysis table
  7. Provenance block — scan path, KB version, profile version, render timestamp, scan duration
  8. Branded footer — organization legal text, page numbers

Why It's Needed (Value Proposition)

Need How the PDF generator addresses it
Client deliverable CISO engagements produce a branded, presentable PDF — not a raw CLI dump.
Air-gapped rendering No browser (Puppeteer), no Office interop, no cloud service. QuestPDF renders entirely in-process.
Deterministic output Same DiscoveryReport + same BrandingProfile = byte-identical PDF. Verifiable by hash.
Compliance evidence CNSA 2.0 verdict, provenance block, and migration plan are all in one document — ready for auditor submission.
Zero per-use cost QuestPDF Community tier covers organizations under $1M annual revenue. No SaaS watermark, no page limit.
Pure .NET No native dependencies beyond .NET runtime and SkiaSharp. Deployable as a single NuGet reference.

Installation

Prerequisites

Requirement Minimum
.NET SDK 10.0 (build); runtime: 8.0+
OS Windows 10+ / Windows Server 2019+ / Linux x64
QuestPDF license Community (automatic for <$1M revenue)
Dependencies CipherShift365.Core.Analysis (project reference)

Package Reference

XML
<PackageReference Include="QuestPDF" Version="2026.6.1" />

Or add via CLI:

BASH
dotnet add package QuestPDF --version 2026.6.1

Build

BASH
cd src/Portal/CipherShift365.Portal.Pdf
dotnet restore
dotnet build

How to Run / Use

Quick Start — Generate a Report

CSHARP
using CipherShift365.Portal.Pdf;
using CipherShift365.Core.Analysis;

// 1. Define branding
var brand = new BrandingProfile(
    OrganizationName: "Acme Corp",
    PrimaryColor: "#1a3a5c",
    AccentColor: "#c0392b",
    LegalFooter: "CipherShift365 by 365Architect — www.365architect.com — Confidential"
);

// 2. Create generator
var generator = new PdfReportGenerator(brand);

// 3. Generate PDF (DiscoveryReport comes from ScanService)
byte[] pdfBytes = generator.Generate(discoveryReport, cnsaValidationResult);

// 4. Save to disk
await File.WriteAllBytesAsync("acme-pqc-report.pdf", pdfBytes);

Via the REST API

Call POST /v1/reports/render with a JSON body:

BASH
curl -X POST http://localhost:5000/v1/reports/render \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"project":".", "organization":"Acme Corp"}' \
  --output report.pdf

Via the Blazor Portal

Navigate to /reports, click "Branded PDF", and the browser will download the file. The portal calls ApiClient.GetPdfAsync(project) which POSTs to the render endpoint.


User Manual

Branding Profile

The BrandingProfile record controls every visual aspect of the report:

CSHARP
public sealed record BrandingProfile(
    string OrganizationName,    // Appears in header, adjusts pronouns in summary
    string? LogoPath = null,    // Future: embed PNG logo in header (currently unused by renderer)
    string PrimaryColor = "#1a3a5c",   // Header text, rule lines, table headers
    string AccentColor = "#c0392b",    // Non-compliant verdict color
    string SafeColor = "#27ae60",      // Compliant verdict / QuantumSafe color
    string LegalFooter = "CipherShift365 by 365Architect — www.365architect.com — Post-Quantum Cryptography Readiness Platform"
);

Default profile:

CSHARP
BrandingProfile.Default  // OrganizationName: "365Architect", all colors at defaults

Customization examples:

CSHARP
// Financial services firm
new BrandingProfile("Goldman Partners", PrimaryColor: "#003366", AccentColor: "#cc0000");

// Government agency — CNSA-focused
new BrandingProfile("Department of Defense — PQC Office",
    PrimaryColor: "#1a3a5c",
    LegalFooter: "CNSA 2.0 Readiness Assessment — For Official Use Only");

// Minimalist
new BrandingProfile("StartupXYZ", "#2c3e50", "#e74c3c", "#27ae60",
    "Generated by CipherShift365 — PQC Platform by 365Architect");

Color Reference

Colors are specified as hex strings and mapped to QuestPDF's Colors palette:

Profile field CSS function QuestPDF mapping
PrimaryColor #1a3a5c Used as-is by QuestPDF FontColor()
AccentColor #c0392b Used by Colors.Red.Darken1 for gap/warning rows
SafeColor #27ae60 Used by Colors.Green.Darken1 for compliant verdicts

Only the PrimaryColor is used directly from the hex string. AccentColor and SafeColor are defined in the profile record but the renderer currently uses QuestPDF's built-in palette (Colors.Red.Darken1, Colors.Green.Darken1). Future versions will use these profile values for full brand customization.

Report Structure

│ CNSA 2.0 COMPLIANCE (optional) │ │ Verdict: NonCompliant │ │ Profile: CNSA 2.0 v1.0.0 | KB: v1.0.0 │ │ Gap Analysis (table) │ │ Algorithm | Requirement | Action │ ├─────────────────────────────────────────────┤ │ PROVENANCE │ │ Scan path, KB version, profile, timestamp │ └─────────────────────────────────────────────┘ │ FOOTER: Legal text Page X / Y │ └─────────────────────────────────────────────┘


---

## Developer Manual

### Architecture

PdfReportGenerator ├── ctor(BrandingProfile) ├── Generate(DiscoveryReport, CnsaValidationResult?) → byte[] │ └── Document.Create(container => │ └── Page (A4, 40pt margin) │ ├── Header() — org name, date, rule line │ ├── Content() — all sections │ │ ├── ExecutiveSummary() │ │ ├── ScoreCards() — 6 colored tiles │ │ ├── FindingsTable() — sorted by risk severity │ │ ├── MigrationPlan() — with effort + rationale │ │ ├── CnsaSection() — conditional on cnsaResult │ │ └── Provenance() — scan metadata │ └── Footer() — legal text + page numbers └── Helper methods: ├── TableHeader(TableDescriptor, params string[]) ├── TableRow(TableDescriptor, params string[]) ├── ScoreCard(RowDescriptor, label, value, color) └── Truncate(string?, max) → string


### Project File

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>CipherShift365.Portal.Pdf</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="QuestPDF" Version="2026.6.1" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\Core\CipherShift365.Core.Analysis\CipherShift365.Core.Analysis.csproj" />
  </ItemGroup>
</Project>

Deterministic Rendering

The PDF generator is deterministic by design:

  1. Same input → same output. Given identical DiscoveryReport, BrandingProfile, and CnsaValidationResult, the output PDF bytes are byte-identical.
  2. Timestamp in content, not in layout. The render timestamp and scan duration are embedded as text in the provenance block — they do not affect layout decisions.
  3. Pagination is deterministic. QuestPDF's Page() composable with fixed font sizes, margins, and column definitions produces the same page breaks for the same data.
  4. No external assets. No web fonts, no CDN-hosted images, no API calls during rendering. Fonts use QuestPDF's built-in defaults (Lato-like).

Verification:

BASH
# Generate twice
dotnet run -- generate report1.pdf
dotnet run -- generate report2.pdf

# Compare
Get-FileHash report1.pdf | Select-Object -ExpandProperty Hash
Get-FileHash report2.pdf | Select-Object -ExpandProperty Hash
# → Identical hashes (if no CnsaValidationResult timestamp difference)

QuestPDF License

The generator sets the QuestPDF license in the static constructor:

CSHARP
static PdfReportGenerator() { QuestPDF.Settings.License = LicenseType.Community; }

Community tier conditions:

  • Organization revenue < $1M USD/year OR
  • Individual developer / open-source project

Organizations exceeding the revenue threshold must purchase a QuestPDF Professional or Enterprise license. See questpdf.com/license.

Adding a New Section

Sections are rendered via col.Item().Element(x => RenderMySection(x, report)):

CSHARP
private static void RenderMySection(IContainer c, DiscoveryReport r)
{
    c.Column(col =>
    {
        col.Item().Text("My Custom Section").FontSize(12).Bold();
        col.Item().Text($"Data: {r.Analysis.Findings.Count} findings").FontSize(10);
        // Add tables, rows, score cards as needed
    });
}

// Register in RenderContent():
col.Item().Element(x => RenderMySection(x, r));

Table Helpers

Two helper methods eliminate boilerplate for table rendering:

CSHARP
// Header row with grey background
private static void TableHeader(TableDescriptor t, params string[] h)
{
    t.Header(hdr =>
    {
        foreach (var x in h)
            hdr.Cell().Background(Colors.Grey.Lighten2).Padding(4)
                .Text(x).FontSize(8).Bold();
    });
}

// Data row with light bottom border
private static void TableRow(TableDescriptor t, params string[] cells)
{
    foreach (var c in cells)
        t.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2)
            .Padding(3).Text(c).FontSize(7).LineHeight(1.2f);
}

Column Definitions

Tables use relative columns (not absolute widths):

CSHARP
table.ColumnsDefinition(cd =>
{
    cd.RelativeColumn(2);     // 2 parts (Algorithm)
    cd.RelativeColumn(1);     // 1 part (Role)
    cd.RelativeColumn(1.5f);  // 1.5 parts (Risk)
    cd.RelativeColumn(3);     // 3 parts (Location)
    cd.RelativeColumn(1.5f);  // 1.5 parts (PQC Target)
});
// Total: 9 parts → 2/9 = ~22%, 1/9 = ~11%, etc.

This ensures consistent proportions across different page sizes (A4, Letter, Legal).

Truncation Strategy

Long text fields (rationale, gap actions) are truncated to fit table cells:

CSHARP
private static string Truncate(string? text, int max) =>
    text is null || text.Length <= max ? text ?? "" : text[..(max - 3)] + "...";
  • Executive summary rationale: 60 chars
  • CNSA gap actions: 60 chars
  • CNSA gap requirements: 40 chars

Code Examples

Standalone CLI PDF Generator

CSHARP
using CipherShift365.Portal.Pdf;
using CipherShift365.Core.Analysis;
using CipherShift365.Core.Runtime;

// Minimal example: assemble a DiscoveryReport manually
var kb = new InMemoryKnowledgeBase(new KbVersion(1, 0, 0), Array.Empty<KbEntry>());
var riskModel = new RiskModel(kb);
var engine = new AnalysisEngine(riskModel);

var pipeline = new DiscoveryPipeline(engine, kb, new IdentityService(), riskModel, new PolicyEngine(riskModel));
var report = await pipeline.RunAsync(".", ct: CancellationToken.None);

var brand = new BrandingProfile("My Organization");
var generator = new PdfReportGenerator(brand);
var pdf = generator.Generate(report);

await File.WriteAllBytesAsync("output.pdf", pdf);
Console.WriteLine($"Generated {pdf.Length} bytes");

Batch Generation with Different Brands

CSHARP
var clients = new[]
{
    new BrandingProfile("Client Alpha", PrimaryColor: "#003399"),
    new BrandingProfile("Client Beta", PrimaryColor: "#990033"),
};

foreach (var brand in clients)
{
    var gen = new PdfReportGenerator(brand);
    var pdf = gen.Generate(report);
    var filename = $"pqc-report-{brand.OrganizationName.Replace(" ", "-").ToLower()}.pdf";
    await File.WriteAllBytesAsync(filename, pdf);
}

Embedding in ASP.NET Controller

CSHARP
[HttpPost("export/pdf")]
public async Task<IActionResult> ExportPdf([FromBody] ExportRequest req)
{
    var report = await _scanService.ScanAsync(req.Project);
    var brand = new BrandingProfile(req.Organization ?? "365Architect");
    var generator = new PdfReportGenerator(brand);
    var pdf = generator.Generate(report, _cnsaValidator.Validate(report.SignedCbom));

    return File(pdf, "application/pdf",
        $"ciphershift-{DateTime.UtcNow:yyyyMMdd-HHmmss}.pdf");
}

Configuration Options

Setting Location Default Description
QuestPDF.Settings.License Static ctor LicenseType.Community QuestPDF licensing tier
Page size Generate() PageSizes.A4 A4 (210mm × 297mm)
Page margin Generate() 40 pts All four sides
Default font size Page() style 10 pt Base text size
Header font size RenderHeader() 16 pt Organization name
Section title size Section methods 12–14 pt Section headings
Table header size TableHeader() 8 pt bold Column headers
Table cell size TableRow() 7 pt Data cells
Score card value size ScoreCard() 16 pt bold Metric numbers
Score card label size ScoreCard() 8 pt Metric labels
Footer size RenderFooter() 7 pt Legal text and page numbers

Third-Party Dependencies

Package Version License Use
QuestPDF 2026.6.1 Community / Commercial PDF layout engine (SkiaSharp-based)
CipherShift365.Core.Analysis (project ref) Proprietary DiscoveryReport, CnsaValidationResult types

No other dependencies. The generator does not reference:

  • System.Drawing (Windows-only — QuestPDF uses SkiaSharp which is cross-platform)
  • Any HTML-to-PDF converter (Puppeteer, wkhtmltopdf, iText)
  • Any Microsoft Office interop libraries

Design Decisions & Rationale

Decision Rationale
QuestPDF over Syncfusion/itext QuestPDF Community tier has no watermark, no page limits, and no per-document fee. iText is AGPL (restrictive). Syncfusion Community requires registration.
Fluent API (no templates) QuestPDF's C# fluent API (Document.Create, container.Page, col.Item()) is type-safe, discoverable via IntelliSense, and avoids a separate templating language (Razor, Liquid, XSL-FO).
No HTML-to-PDF bridge Eliminates Puppeteer/Playwright dependency. The report is authored directly in QuestPDF's layout model.
Static helper methods All rendering methods are static — the generator is a pure-function pipeline. Only the BrandingProfile field is instance state.
CNSA section is optional Not every scan needs CNSA validation. The cnsaResult parameter is nullable — if null, the section is omitted entirely.
Truncation, not wrapping Table cells have fixed proportions. Long text is truncated rather than wrapping and breaking column alignment. Full text is available in the JSON API and Markdown report.
Share

Keyboard Shortcuts

⌘ K
Open search
/
Focus search
?
Show shortcuts
b
Toggle bookmark
Alt+←
Previous page
Alt+→
Next page
Esc
Close overlay