Most vulnerability management teams produce the same report over and over. An executive summary, a findings table, a severity breakdown, a remediation appendix. The structure rarely changes from one engagement to the next. What changes is the data underneath. Yet when reporting lives as a pile of one-off documents, every report becomes a manual rebuild. Sections get reordered by accident. A confidential watermark gets forgotten. A regulatory annex that should always appear quietly goes missing. The cost is not just time. It is inconsistency that a client, an auditor, or a regulator will eventually notice.
A report template solves this by separating structure from content. The template defines what a report looks like and what it contains. The platform fills it with live finding data at generation time. This article walks through how PMAP models report templates, how section configuration and conditional rendering work, where custom narrative blocks fit, and how a finished report moves from a private artifact to a securely shared and access-tracked deliverable. Every behavior described here maps directly to the PMAP report template and report share domains.
If you are mapping reporting into your broader analytics program, the pillar guide on vulnerability risk analytics and reporting sets the wider context. This article zooms into the template and signing layer specifically.
Why Reusable Templates Beat One-Off Reports
A one-off report is a snapshot frozen in a document editor. It captures the state of an engagement at a moment, then drifts out of sync the instant a finding is reopened or a remediation lands. To produce the next report, someone copies the last file, deletes the old numbers, and pastes in new ones. This pattern scales badly. It introduces transcription errors, it makes branding drift, and it makes any structural decision a per-report negotiation rather than a settled standard.
A report template inverts the model. In PMAP, the template is the configuration layer for the report generation pipeline. It is not a finished document. It is a definition that says which sections render, what findings qualify for each section, what narrative text accompanies them, which columns appear, what watermark overlays the output, and what format the export produces. When a report is generated, the report domain reads the template configuration and assembles the data model from current findings. The structure is fixed once. The content is fresh every time.
This separation has a direct operational payoff. A security manager configures the engagement report once. Every analyst who generates that report afterward gets identical structure, identical scoping rules, and identical presentation, without re-deciding anything. The template becomes the single source of truth for how a given report type should look across an entire team.
System Templates vs Custom Templates
PMAP ships with system templates and lets teams create their own. The distinction is more than cosmetic. It is enforced in the API.
System templates carry an is_system = true flag. They are immutable through the API. Any attempt to update or delete a system template is blocked. Both the update and delete paths fetch the template, check the flag, and return an HTTP 403 Forbidden with a clear message advising the user to clone the template to create a custom version. This protection means the shipped library cannot be accidentally mutated or destroyed by a normal user action. The baseline templates a platform administrator relies on stay intact.
Custom templates are fully editable. They are created by users, owned by the creator, and open to update and delete. The intended workflow is clone-then-edit. A team that wants to adapt a system template starts from it, produces an editable copy, and modifies the copy. The original stays as the canonical reference.
Ownership here is trustworthy by design. The created_by field is not taken from the request body. It is stamped in the handler from the authenticated user’s identity in the request context. This matters because the template list offers a “Mine” ownership tab alongside “All” and “System”. Since created_by cannot be spoofed through the payload, the “Mine” filter always reflects genuine ownership. System templates have a null creator, which is how they sort out of the personal view cleanly.
The listing endpoint backs this with filterable discovery. Templates can be filtered by category, scope type, language, active status, and a free-text search that matches the name or code. The owner tab narrows to all, system, or mine. Results use cursor-based pagination with a default page size of 50 and a hard cap of 100, so even a large template library stays responsive.
Section Configuration: Order, Filters and Conditions
The heart of a template is its section configuration. PMAP stores this as a JSONB section_config column, decoded at render time by the engine into a structured SectionConfig. That structure carries six concerns, and understanding them is the key to understanding how flexible a template can be.
The first concern is the ordered section list. sections is an array of section keys, in render order. A template might declare ["executive_summary", "findings"], and the report renders those sections in exactly that sequence. Reordering a report is a matter of reordering this list, not reshuffling a document by hand.
The remaining concerns layer behavior onto those sections. Custom text blocks inject narrative. Section filters scope which findings appear. Section conditions decide whether a section renders at all. Column configuration picks which fields show in tabular sections. A watermark overlays the whole output. Each is a separate field in the parsed configuration, so you tune one dimension without disturbing the others.
This design keeps the template declarative. You describe the report you want, and the engine produces it. There is no procedural report-building code to maintain per template. Adding a new report variant means writing a new configuration, not new logic. For the production side of that pipeline, see the related deep dive on asynchronous report generation and how the finished file is assembled.
Conditional Rendering: always, has_findings, has_severity
A static report shows every section whether or not it has anything to say. That produces empty headings and awkward “No findings in this category” placeholders that undercut a polished deliverable. PMAP avoids this with per-section render conditions.
The section_conditions map assigns each section a condition string. There are three forms. always renders the section unconditionally, which suits an executive summary or a methodology note that should appear regardless of data. has_findings renders the section only when matching findings exist, so an empty category simply disappears rather than printing a hollow header. has_severity:<sev> renders only when findings of a named severity are present, which lets you include a critical-findings callout that appears exclusively when there is something critical to call out.
The effect is a report that adapts to its own data. A clean engagement produces a lean report with no padding. A heavy engagement produces a complete one. The author never has to manually prune sections per run, because the condition does it.
Per-Section Finding Filters and Column Selection
Conditions decide if a section appears. Filters decide what goes inside it. The section_filters map carries a per-section filter object with severities, statuses, a limit, and a sort order. This lets a single report contain sections with deliberately different scopes. An executive summary section might filter to critical and high findings only, sorted by severity, capped at a small count. A full appendix section might include everything, sorted differently, with no cap. The same finding pool feeds both, scoped per section.
Column selection works alongside this. The column_config map holds a per-section ordered list of column keys. A tabular section shows exactly the columns the template names, in the order it names them. One section can present a compact view with a handful of columns while another presents a detailed view with many. The template author controls the information density of each section independently, without touching the underlying data.
Custom Text Blocks for Narrative Content
Findings tables tell you what was found. They do not tell the reader why it matters, what the engagement scope was, or what the recommended path forward is. That narrative is what turns a data dump into a report someone will act on. PMAP supports this through custom text blocks.
A CustomTextBlock is a free-text editor block injected into the report at a chosen position. Each block carries an after anchor, the section key it should follow. So a block can sit after the executive summary to add a scope statement, or after the findings section to add remediation guidance, or anywhere else the structure allows. The blocks live in the section configuration as a first-class array, parsed alongside the section list.
The important property is that this is configuration, not code. Adding narrative content does not require modifying the platform or writing a new report type. An author composes the prose, anchors it to a section, and it renders in place on every generation of that template. A consulting team can encode its standard methodology language, its standard caveats, and its standard recommendations once, then reuse them across every client report driven by that template.
Severity Mapping and Watermarks
Two smaller template features carry outsized weight in regulated and client-facing reporting: severity mapping and watermarks.
Scanners label severities in their own vocabularies. A regulatory framework or a client may expect a different scale. The template-level severity_mapping, stored as JSONB, remaps scanner severity labels to display labels. A template aligned to a particular compliance scheme can present severities in that scheme’s language without altering the source finding data. The mapping lives on the template, so two templates over the same findings can present severity differently, each correct for its audience.
The watermark is a single configuration field that overlays text across the rendered output. A value like CONFIDENTIAL stamps the report so its sensitivity is visible on every page. Because the watermark is part of the section configuration, it is applied consistently by the render engine and cannot be forgotten on an individual run. For a deliverable headed to an external party, this is a small control with real governance value.
Language Variants and Regulatory Templates
A platform that serves teams across regions needs reports in more than one language, and it needs to recognize when a report exists for compliance reasons rather than internal use. PMAP models both.
Each template carries a language field such as en or tr. Templates that represent the same report in different languages link through a shared parent_id, forming a family. Each member also carries a denormalized supported_languages list, which backs a language switcher in the interface so an author can move between variants of the same report without hunting for separate templates. When the report domain resolves a template by name, it can prefer the parent of a family, keeping name-based references stable across language variants.
Regulatory classification is explicit. An is_regulatory flag combined with a regulatory_type marks templates produced for compliance frameworks such as ISO 27001 or PCI-DSS. This is not just a label. It lets the template library distinguish compliance deliverables from operational reports in discovery and filtering, so a team preparing for an audit can find the right templates fast. The signed, watermarked, regulation-aligned report is a recurring need, and the template model treats it as a first-class case rather than an afterthought.
Live Preview Before You Generate
Configuring sections, conditions, filters, and watermarks blind is error-prone. You want to see the result before committing a real generation run against live findings. PMAP provides a live preview for exactly this.
The preview endpoint renders the template to HTML using synthetic sample data. The engine generates realistic placeholder findings through a sample view model, then applies the template’s actual section configuration and watermark before rendering. The report type for the sample data is derived from the template’s code, falling back to its scope type when no code is set. The result is returned as HTML suitable for embedding in an iframe in the editor, with a same-origin frame protection header set.
This gives an author a fast feedback loop. Adjust a condition, refresh the preview, see whether the right sections appear. Change the watermark, confirm it overlays correctly. Reorder the section list, verify the new flow. The preview exercises the same configuration the real report will use, so what you approve in preview is what you get in production. The preview is synchronous and does not spawn a background job, so the loop stays tight.
Sharing the Finished Report Securely
A configured, generated report is only useful once it reaches the people who need it. Often those people are external. Clients, auditors, and remediation owners who do not have a PMAP account still need the file. Emailing a sensitive PDF as an attachment is the wrong answer. It leaves uncontrolled copies in inboxes with no expiry, no access record, and no way to revoke. PMAP handles external distribution through share tokens instead.
A share token is a cryptographically random 64-character hex link scoped to a single report file. It is generated with a cryptographic random source, 32 bytes rendered as 64 hex characters, which yields roughly 256 bits of entropy. At that size, brute-forcing a valid token is infeasible at any realistic rate. The token is the access credential. Anyone holding a valid token can retrieve the specific report it points to, and nothing else.
The share domain operates purely on the finished output artifact. It sits downstream of report generation and is decoupled from scan and finding data entirely. The file must already exist and be stored before a token can serve it. This clean separation means sharing never reaches back into live finding data. It distributes a fixed, already-produced deliverable. The complementary deep dive on secure report sharing covers the recipient-side experience in more detail.
Optional Expiry and Password Protection
A share link should not live forever, and a sensitive one should require more than the link itself. PMAP supports both as optional controls on the token.
Expiry is set at creation through an expires_in_hours value. A token with an expiry carries a deadline, and that deadline is enforced server-side on every public access. The check evaluates the expiry against the current time on each request, and an expired token returns HTTP 410 Gone. A null expiry means the token never expires, which suits a durable internal link, but for an external recipient a bounded window is the safer default. Because expiry is checked at read time rather than only at creation, there is no stale grant lingering past its deadline.
Password protection adds a second factor to the link. The caller supplies a plaintext password at creation, and it is stored as a bcrypt hash at the default cost. The plaintext is never stored and never returned in any response. Every public access path verifies the hash before serving metadata or the file. So even if a token link leaks, the holder still cannot retrieve the report without the password. The platform does not implement a per-attempt rate limiter at the service layer for password guessing, so infrastructure-level controls are expected to cover brute-force protection at that boundary. The bcrypt cost and the token entropy together make the credentials themselves strong.
Public Download Without a PMAP Account
The defining property of share tokens is that recipients need no PMAP account. The public access routes live entirely outside the JWT authentication middleware. They are registered as direct routes on the root router rather than mounted under the authenticated subtree, which avoids any path shadowing against the authenticated reports routes and keeps the public surface cleanly separated.
The recipient experience is staged to be both secure and download-manager friendly. A public info lookup returns whether the token has a password and when it expires, without requiring the password itself. This lets the landing page decide whether to render a password prompt before attempting a download. When a password is required, a separate verify-password step validates the token and password together and returns a simple success response. Only then does the actual file download proceed. This split exists deliberately. It prevents an HTTP download manager from intercepting an authentication failure on a streaming binary endpoint, keeping the password check a clean, separate interaction.
The download itself serves the report file with correct content handling. The domain detects the file extension at serve time and sets the content type accordingly, distinguishing PDF, DOCX, and HTML outputs so the browser handles each correctly. The file bytes are loaded through the report service’s existing download path, which retrieves them from object storage. The share domain itself never touches storage directly. It delegates retrieval to the report service, and ownership enforcement happens there at the point of file retrieval.
Access Tracking and Token Revocation
Distributing a report and then losing all visibility into it is a governance gap. PMAP closes it with access tracking and revocation on every token.
Every successful download increments an access_count and records a last_accessed_at timestamp. This recording is fire-and-forget, so it never blocks or fails the download response, but the counter and timestamp are surfaced back to authenticated users. A report author listing the active tokens for a report sees, per token, who created it, when, how many times it has been accessed, and when it was last opened. That turns a shared link from a blind handoff into an auditable distribution. You can see whether the auditor actually opened the report, and how often.
Revocation is immediate and absolute. Revoking a token hard-deletes it, which instantly invalidates the public link. There is no grace window and no soft state to reason about. The moment a token is revoked, any request bearing it fails. If a link is shared too widely, or an engagement ends, or a recipient relationship changes, the owner cuts access in one action. Combined with expiry, this gives two independent ways to bound a link’s lifetime: a deadline you set in advance, and a kill switch you can pull at any time.
One scoping detail is worth naming for clarity. The public routes accept any valid, non-expired token regardless of company tenant, because the token is the sole access credential on those routes. Tenant scoping and ownership are enforced on the authenticated management side and at the report service’s file retrieval, not re-checked on the public path. The security model on the public side rests entirely on the strength and lifecycle of the token. That is why the token entropy, the expiry, the optional password, and the revocation all matter together. They are the complete control set for a credential that stands on its own.
How PMAP Goes From Template to Signed, Shared Report
Pulling the threads together, a report in PMAP moves through a clear lifecycle, and every stage maps to a concrete platform behavior.
It starts with a template. A security manager either selects a protected system template or clones one into an editable custom copy. They configure the section list, set per-section conditions so empty categories drop out, scope each section with its own finding filters, choose the columns each tabular section shows, anchor custom narrative blocks where the report needs prose, apply a severity mapping for the target audience, and set a watermark for a confidential deliverable. If the report serves a compliance need, they flag it regulatory and pick the right language variant. They verify the whole thing in live preview against sample data before committing.
From there the report domain reads the template configuration and generates the report against current findings, producing a file in the chosen format. That file is the fixed artifact. To distribute it, the author creates a share token scoped to that file, optionally bounded by an expiry and protected by a password. The token resolves to the report for any external recipient with no PMAP account, gated by the password check when one is set. Throughout the token’s life, access is counted and timestamped, visible to the author, and the token can be revoked the instant it should no longer work.
The result is a reporting workflow that is consistent by configuration and controlled by design. Structure is settled once in the template. Presentation adapts to the data through conditions and filters. Distribution is credentialed, time-bounded, auditable, and revocable. The same discipline that PMAP applies to ingesting, correlating, and triaging findings carries through to the report that communicates them.
To standardize this across your team and share every report securely, read the vulnerability risk analytics and reporting pillar for the full feature picture.
Frequently Asked Questions
Can I edit a system report template directly?
No. System templates are flagged is_system = true and are immutable through the API. Both update and delete return an HTTP 403 Forbidden with a message advising you to clone the template. The intended workflow is to clone a system template into an editable custom copy and modify the copy, which keeps the shipped baseline intact.
How do conditional report sections work?
Each section in a template gets a render condition. There are three forms. always renders the section unconditionally. has_findings renders it only when matching findings exist, so empty categories disappear. has_severity:<sev> renders it only when findings of the named severity are present. The conditions live in the section_conditions map inside the template’s section configuration, so the report adapts to its own data without manual pruning.
How are report share tokens secured?
A share token is a 64-character hex string generated from a cryptographic random source, 32 bytes that yield roughly 256 bits of entropy, making brute-force infeasible at any realistic rate. Tokens can carry an optional expiry, enforced server-side on every access with an HTTP 410 Gone response once past the deadline, and an optional password stored as a bcrypt hash at the default cost. The plaintext password is never stored or returned.
Do recipients need a PMAP account to open a shared report?
No. The public share routes live outside the JWT authentication middleware, so an external client, auditor, or remediation owner accesses the report using only the token. If the token has a password, the recipient passes a separate verify-password step first. The file is then served with the correct content type for PDF, DOCX, or HTML output.
Can I see who accessed a shared report?
Yes. Every successful download increments an access count and records a last-accessed timestamp on the token. When a report author lists the active tokens for a report, each token shows its creator, creation time, access count, and last-accessed time, turning a shared link into an auditable distribution record.
How do I revoke a shared report link?
Revoking a token hard-deletes it, which immediately invalidates the public link with no grace window. Any subsequent request bearing that token fails. Combined with an optional expiry deadline set at creation, revocation gives you two independent ways to bound a link’s lifetime, one scheduled in advance and one available on demand.
Can a single report template support multiple languages and output formats?
Yes. Each template has a language field, and templates for the same report in different languages link through a shared parent to form a family, surfaced through a language switcher. Each template also declares a default output format and a list of supported formats, such as DOCX and PDF, which the report domain uses to drive export options.