Template Tags
Complete reference for Vulnsy DOCX template tags — fields, filters, conditionals, loops, and the version-agnostic CVSS object for report exports.
Template tags are placeholders in your Word template that get replaced with real data on export.
Vulnsy uses a small, composable tag grammar: a handful of fields, a set of filters that transform them (| date, | sort, | where, …), and one block syntax ({#…} … {/}) that serves as both loop and conditional. You don't memorise hundreds of tags — you compose a few.
Tag Syntax
| Form | Meaning | Example |
|---|---|---|
{field} | Plain-text value | {report.title} |
{~field} | Rich HTML → Word formatting (bold, lists, links, tables) | {~description} |
{field | filter:"arg"} | Transform a value with a filter | {report.startDate | date:"UK"} |
{#expr} … {/} | Loop over a collection or a conditional block | {#findings} … {/} |
{%image} | Image (base64 data, inside an evidence loop) | {%image} |
{@field} | Raw Word XML (advanced/pre-formatted) | {@captionXml} |
Loop/conditional tags ({#…}, {/}) and image tags ({%image}) must each sit in their own paragraph — not inline with other text.
Plain vs. rich. {field} renders a value as plain text; {~field} renders an HTML field (description, remediation, narrative sections) with Word formatting. To force an HTML field to plain text — e.g. inside a table cell — pipe it through | text: {description | text}.
Close any block with a bare {/} (it closes the nearest open block), or name it explicitly with {/findings}.
Filters
Filters transform a value. Write them after a pipe, and chain as many as you like — they apply left to right.
{report.startDate | date:"UK"}
{#findings | where:"severity":"critical" | sort:"cvss"} … {/}
{severity | title}| Filter | Purpose | Example |
|---|---|---|
date:"fmt" | Format a raw date | {report.startDate | date:"US_abbr"} |
sort:"order" | Order a findings collection | {#findings | sort:"cvss"} |
where:"field":"value" | Keep only matching items | {#findings | where:"severity":"high"} |
count | Number of items in a collection | {findings | where:"severity":"critical" | count} |
join:"sep" | Join a collection into text (default separator ", ") | {affectedAssets | join:", "} |
default:"text" | Fallback when a value is empty | {report.overallRisk | default:"Not assessed"} |
text | Strip HTML to plain text | {description | text} |
upper / lower / title | Change letter case | {severity | title} → "Critical" |
Filter arguments:
dateformats:long(default — "17th March 2025"),US,US_short,US_abbr,UK,UK_abbr,month,monthYear,iso.sortorders:severity,cvss(3.1),cvss3,cvss4(4.0),auto(the report's configured order).
count returns a number, and 0 is falsy — so it doubles as a conditional: {#findings | where:"severity":"critical" | count}There are critical findings.{/}.
Conditionals
A {#…} … {/} block renders its content only when the expression is truthy. Inside, you can compare real fields — there are no separate boolean tags.
{#report.overallRisk == "critical"}
This assessment identified critical risk.
{/}
{#cvss.score >= 7.0}High severity by CVSS.{/}
{#severity == "high" || severity == "critical"}
Priority remediation required.
{/}Permitted operators: == != > < >= <= && || !. A bare {#cvss.temporal} … {/} is a presence check — it renders only when that data exists.
Compare against the stored value, not the display label. Severity values are critical, high, medium, low, info (lowercase) — {#severity == "Informational"} never matches; use {#severity == "info"}.
Report Fields
| Tag | Description |
|---|---|
{report.title} | Report title |
{report.version} | Version number (defaults to "1.0") |
{report.status} | Status: draft, review, or final |
{report.overallRisk} | Overall risk: critical, high, medium, low, informational, or Not Set |
{report.sortOrder} | Finding sort order: severity, cvss, or cvss4 |
{~report.executiveSummary} | Executive summary (HTML → Word). Plain text: {report.executiveSummary | text} |
{~report.methodology} | Methodology (HTML → Word). Plain text: {report.methodology | text} |
{report.startDate} | Assessment start date (raw — reformat with | date) |
{report.endDate} | Assessment end date (raw — reformat with | date) |
Conditional on risk: {#report.overallRisk == "critical"} … {/}. Conditional on sort order: {#report.sortOrder == "cvss"} … {/}.
Assessment dates resolve to the report's override if set, otherwise the parent project's dates.
Narrative & Scope Sections
Every narrative and scope section you write becomes a slug under report.sections (lowercased, non-alphanumeric → underscore). Keeping them under .sections means a section titled "Findings" can never collide with report.findings.
A section titled "Introduction" → {~report.sections.introduction}.
A section titled "Technical Summary" → {~report.sections.technical_summary}.
Use {~report.sections.<slug>} for formatted HTML, or {report.sections.<slug> | text} for plain text.
Client Fields
| Tag | Description |
|---|---|
{client.name} | Company name |
{client.contactName} | Contact person |
{client.contactEmail} | Contact email |
{client.contactPhone} | Contact phone |
{client.address} | Address |
Custom fields (key/value pairs you add on the client) are exposed as a loop — add a field in the app and it appears in the report with no template change:
{#client.customFields}{label}: {value}
{/}Project Fields
| Tag | Description |
|---|---|
{project.name} | Project name |
{project.description} | Description |
{project.type} | Type (web_app, infrastructure, mobile, cloud, api, iot) |
{project.status} | Status |
{project.startDate} | Start date (raw — reformat with | date) |
{project.endDate} | End date (raw — reformat with | date) |
project.* dates are always the literal project dates. Use report.startDate / report.endDate when you want the assessment-specific values (which honour a per-report override).
Custom fields: {#project.customFields}{label}: {value}{/}.
Dates
Dates are raw values. A bare tag renders the long format; the date filter reformats it:
| Tag | Output |
|---|---|
{report.startDate} | 17th March 2025 (long, the default) |
{report.startDate | date:"US"} | 03/17/2025 |
{report.startDate | date:"UK"} | 17/03/2025 |
{report.startDate | date:"US_abbr"} | Mar/17/2025 |
{report.startDate | date:"month"} | March |
{report.startDate | date:"monthYear"} | March 2025 |
{report.startDate | date:"iso"} | 2025-03-17 |
The same works for project.startDate, project.endDate, and metadata.generatedDate.
Summary Counts
Severity tallies for exec-summary tables. Each is a number.
| Tag | Description |
|---|---|
{summary.critical} | Count of critical findings |
{summary.high} | Count of high findings |
{summary.medium} | Count of medium findings |
{summary.low} | Count of low findings |
{summary.info} | Count of informational findings |
{summary.total} | Total finding count |
These are the same numbers as {findings | where:"severity":"critical" | count}, kept as a convenience because nearly every report has a severity table.
Organization & Metadata
| Tag | Description |
|---|---|
{organization.name} | Organization name |
{metadata.generatedDate} | Generation date (raw — reformat with | date) |
{metadata.generatedBy} | Name of the user who exported |
Consultants
Loop with {#consultants} … {/}:
| Tag | Description |
|---|---|
{name} | Consultant name (falls back to email) |
{email} | Email address |
{jobTitle} | Job title |
{separator} | , after every consultant except the last |
Outside the loop, {consultantNames} gives all names pre-joined with commas — handy for a single table cell.
Findings
Loop with {#findings} … {/}. Each finding exposes:
Identification & Body
| Tag | Description |
|---|---|
{order} | Sequential number after sorting (1, 2, 3…) |
{findingId} | Formatted reference ID, e.g. REF-C1, REF-H2 |
{title} | Finding title |
{severity} | critical, high, medium, low, info (e.g. {severity | title}) |
{status} | open, in_progress, remediated, accepted_risk |
{~description} | Description (HTML → Word, with embedded pasted images) |
{~remediation} | Remediation (HTML → Word) |
{~evidenceNarrative} | Evidence narrative (HTML → Word) |
{~references} | References (HTML → Word) |
For plain text of any HTML field, pipe through | text — e.g. {description | text}.
CVSS
CVSS is one version-agnostic object. cvss.* reflects whatever version the finding is scored with; {cvss.version} tells you which ("3.1" or "4.0"). Write your template against cvss.* and it works for both — no version branching.
| Tag | Description |
|---|---|
{cvss.version} | 3.1 or 4.0 |
{cvss.score} | Effective overall score (e.g. "7.5") |
{cvss.vector} | Full vector string |
{cvss.severity} | Severity label ("Critical", "High"…) |
{cvss.base.score} / {cvss.base.severity} / {cvss.base.calc} | Base metrics (.calc is a readable breakdown, 3.1) |
{cvss.temporal.score} / .severity / .calc | Temporal metrics (3.1) |
{cvss.threat.score} / .severity | Threat metrics (4.0) |
{cvss.environmental.score} / .severity / .calc | Environmental metrics |
Presence conditionals (each renders only when that data exists — 0/absent is falsy):
{#cvss.present}CVSS v{cvss.version}: {cvss.score} ({cvss.severity}){/}
{#cvss.hasTemporal}Temporal: {cvss.temporal.score}{/}
{#cvss.hasThreat}Threat: {cvss.threat.score}{/}
{#cvss.hasEnvironmental}Environmental: {cvss.environmental.score}{/}Showing both versions side by side — the explicit cvss.v3_1 and cvss.v4_0 objects (each present only if the finding carries that version):
{#cvss.v3_1}CVSS 3.1: {cvss.v3_1.score} — {cvss.v3_1.vector}{/}
{#cvss.v4_0}CVSS 4.0: {cvss.v4_0.score} — {cvss.v4_0.vector}{/}Each has the same nested shape (.score, .vector, .severity, .base.*, etc.).
Evidence (nested loop)
{#evidence} … {/}:
| Tag | Description |
|---|---|
{fileName} | File name |
{description} | Description / caption |
{%image} | The image (base64 data) |
{figureNumber} | Sequential figure number across the document |
{caption} | "Figure N - description" |
{@captionXml} | Pre-formatted Word caption XML (Caption style + SEQ field) |
Affected Assets (nested loop)
{#affectedAssets} … {/}:
| Tag | Description |
|---|---|
{type} | url, ip, hostname, network, other |
{value} | The asset value |
{description} | Asset description |
For a single cell, join them instead of looping: {affectedAssets | join:", "} (or {affectedAssets | join:"\n"} for one per line).
Custom Fields (nested loop)
Per-finding key/value fields: {#customFields}{title}: {content}{/}.
Ordering & Filtering Findings
Instead of separate pre-sorted arrays, apply sort and where to the findings collection:
{#findings | sort:"cvss"} … {/} ← highest CVSS first
{#findings | where:"severity":"critical"} … {/} ← critical only
{#findings | where:"severity":"high" | sort:"cvss"} … {/} ← high, CVSS-orderedCount matches (also works as a conditional): {findings | where:"severity":"critical" | count}.
Tabs
A report can have multiple tabs (e.g. "Web Application", "Infrastructure"). Loop every tab with {#tabs} … {/}. Inside the loop:
| Tag | Description |
|---|---|
{name} | Tab name |
{exportTagPrefix} | The tab's prefix, e.g. "appsec" |
{overallRisk} | Tab's overall risk — conditional: {#overallRisk == "critical"} … {/} |
{sortOrder} | Tab's finding sort order |
{findingCount} | Number of findings in this tab (use as a conditional to skip empty tabs) |
{~sections.<slug>} | This tab's narrative sections by slug |
{#findings} … {/} | Findings scoped to this tab (supports | sort / | where) |
{#tabs}
{name} — {findingCount} findings
{#findings | sort:"severity"}
{findingId}. {title} ({severity | title})
{~description}
{/}
{/tabs}Prefix-Scoped Tags (no loop)
When a tab has a custom exportTagPrefix (e.g. appsec), its data is also reachable at the top level under that prefix — useful for fixed-layout templates that don't loop tabs:
| Tag | Description |
|---|---|
{#appsec.findings} … {/} | That tab's findings (supports | sort / | where) |
{appsec.findingCount} | That tab's finding count |
{appsec.sortOrder} | That tab's sort order |
{appsec.overallRisk} | That tab's overall risk |
{~appsec.sections.<slug>} | That tab's narrative section by slug |
Replace appsec with any custom prefix. When the prefix is report (the default), these live under report.*.
Template Patterns
Simple Report
{report.title}
Client: {client.name}
Date: {report.startDate | date:"UK"} — {report.endDate | date:"UK"}
{~report.executiveSummary}
{#findings | sort:"severity"}
{findingId} - {title} ({severity | title})
CVSS: {cvss.score} ({cvss.severity})
{~description}
{~remediation}
{#evidence}
{%image}
{@captionXml}
{/evidence}
{/findings}Severity Sections (via where)
{#tabs}
{#findingCount}
{name} Technical Findings
{#findings | where:"severity":"critical"}
{title} — {cvss.score}
{~description}
{~remediation}
{/}
{#findings | where:"severity":"high"}
{title} — {cvss.score}
{~description}
{/}
{/findingCount}
{/tabs}Wrap tab content in {#findingCount} … {/} to skip tabs with no findings, and put page breaks inside the severity blocks so they're skipped when empty.
Consultants Table
| Consultant | Role | |
|---|---|---|
{#consultants}{name} | {email} | {jobTitle}{/consultants} |
Or a single string: {consultantNames}.
Moving From the Old Tags
If you have a template written for Vulnsy's earlier tag set, here's what changed. The old tags are no longer provided — update to the replacements below.
| Old tag | Replacement |
|---|---|
{report.executiveSummary_plain}, any {field_plain} | {report.executiveSummary | text}, {field | text} |
{report.introduction}, {~report.introduction} | {~report.sections.introduction} |
{project.startDateUK}, {…startMonthYear}, all date variants | {project.startDate | date:"UK"}, {… | date:"monthYear"} |
{cvssScore}, {cvssVector}, {cvssBaseScore}, {cvssBaseCalc} | {cvss.score}, {cvss.vector}, {cvss.base.score}, {cvss.base.calc} |
{cvss3Score} / {cvss4Score} | {cvss.v3_1.score} / {cvss.v4_0.score} |
{#hasCvss}, {#hasTemporalScore} | {#cvss.present}, {#cvss.hasTemporal} |
{#findingsBySeverity}, {#findingsByCvss} | {#findings | sort:"severity"}, {#findings | sort:"cvss"} |
{#criticalFindings}, {#highFindings}, … | {#findings | where:"severity":"critical"}, {… "high"}, … |
{#report.isSortedByCvss}, tab {#isCritical} | {#report.sortOrder == "cvss"}, {#overallRisk == "critical"} |
{affectedAssets_text} | {affectedAssets | join:", "} |
{#allTabs}, {#appsec_tabs} | {#tabs} |
{#appsec.criticalFindings} | {#appsec.findings | where:"severity":"critical"} |