Writing Report Templates

Templating System

VulnScout uses Jinja2 as its templating engine, which has great community support, documentation, and supports any text-based format (AsciiDoc, HTML, Markdown, CSV, plain text, etc.).

Template Locations

Path

Purpose

src/views/templates/

Built-in templates shipped with VulnScout.

.vulnscout/templates/

Per-project host-side templates (mounted automatically if the directory exists).

Adding a Custom Template

Pass the file path directly to --report — it stages and runs the template in one step:

./vulnscout --project demo --report /path/to/my-report.adoc

Automatic Generation

Set the GENERATE_DOCUMENTS environment variable to automatically build templates at the end of every scan. Multiple templates are comma-separated (values are trimmed):

./vulnscout config GENERATE_DOCUMENTS "summary.adoc, all_assessments.adoc"

Generated files are written to the outputs directory (default: .vulnscout/outputs/). All templates can also be run on-demand from the web interface using the export button in the toolbar.


Global Variables Available in Templates

Variable

Type

Description

vulnerabilities

dict

All vulnerabilities found in the project, keyed by ID. Use vulnerabilities | as_list to get a list.

packages

dict

All packages found in the project, keyed by ID. Use packages | as_list to get a list.

assessments

dict

All assessments found in the project, keyed by ID. Use assessments | as_list to get a list.

author

string

Company name producing this document (from AUTHOR_NAME config).

export_date

string

Date of export as YYYY-MM-DD.

scan_date

string

Date and time of the last scan, or unknown date if unavailable.

client_name

string

Customer company name. Set via CLIENT_NAME config or overridden per-export from the web UI. May be empty.

unfiltered_vulnerabilities

dict

All vulnerabilities, bypassing any active export filter. Use | as_list to get a list. Always the full dataset.

unfiltered_assessments

dict

All assessments, bypassing any active export filter. Keyed by assessment ID.

failed_vulns

list[string]

List of vulnerability IDs that triggered the --match-condition expression. Empty when no condition was set or no vulnerability matched. Use unfiltered_vulnerabilities[vuln_id] to get the full object.

projects

dict

All projects, keyed by UUID. Use projects | as_list to get a list. Each value has the fields described in the Project Object section below.

variants

dict

All variants, keyed by UUID. Use variants | as_list to get a list. Each value has the fields described in the Variant Object section below.

scans

dict

All scans, keyed by UUID. Use scans | as_list to get a list. Each value has the fields described in the Scan Object section below.

sbom_documents

dict

All SBOM documents, keyed by UUID. Use sbom_documents | as_list to get a list. Each value has the fields described in the SBOM Document Object section below.


Environment Variables Available in Templates

Use env(name, default="") to access host environment variables in your templates.

For example, to fetch the value of the DISTRO environment variable:

{{ env("DISTRO") }}
{{ env("DISTRO", "unknown") }}

VulnScout will automatically scan your templates for env("...") patterns and pass the corresponding environment variable values from the host system.

When exporting, users can add filters to export only some vulnerabilities. To bypass these filters and access all vulnerabilities, use unfiltered_vulnerabilities instead of vulnerabilities and unfiltered_assessments instead of assessments. This is useful when producing a summary or showing the number of filtered-out vulnerabilities.


Vulnerability Object

Field

Type

Description

id

string

CVE / vulnerability ID.

found_by

list[string]

Tools that found the vulnerability (e.g. grype, yocto, spdx3).

datasource

string

Primary URL for the vulnerability.

namespace

string

Database name (e.g. nvd).

aliases

list[string]

Alternative IDs for this vulnerability.

related_vulnerabilities

list[string]

IDs of related vulnerabilities.

urls

list[string]

Additional reference URLs.

texts

dict[str, str]

Descriptions and notes. Key is the title, value is the content.

severity.severity

string

Severity level: low, medium, high, critical, unknown.

severity.min_score

float

Minimum CVSS score (0–10).

severity.max_score

float

Maximum CVSS score (0–10).

severity.cvss

list[CVSS]

List of CVSS scoring objects (version, score, vector, author).

epss.score

float

EPSS probability score (0–1).

epss.percentile

float

EPSS rank relative to all CVEs (0–1).

effort.optimistic

string

Optimistic fix time estimate in ISO 8601 duration (e.g. PT5H).

effort.likely

string

Likely fix time estimate in ISO 8601 duration (e.g. P1D).

effort.pessimistic

string

Pessimistic fix time estimate in ISO 8601 duration (e.g. P2DT4H).

packages

list[string]

Packages affected by the vulnerability, in form name@version.

published

string

CVE publish date as ISO 8601, or empty if unknown.

status

string

Current status string (see Assessment object for possible values).

simplified_status

string

Simplified status: pending, affected, fixed, or ignored.

last_assessment

Assessment

Most recent assessment object (shorthand for assessments[0]).

assessments

list[Assessment]

All assessments, sorted most recent first.

variant_ids

list[string]

UUIDs of every variant in which this vulnerability has at least one assessment.

assessments_by_variant

dict[string, list[Assessment]]

Assessments grouped by variant ID. Key is the variant UUID, value is the list of assessments for that variant.


Package Object

Field

Type

Description

name

string

Name of the package.

version

string

Version of the package.

cpe

list[string]

List of CPE identifiers.

purl

list[string]

List of PURL identifiers.

sbom_documents

dict[string, SBOM Document]

SBOM documents that list this package, keyed by UUID.

variants

list[string]

UUIDs of variants in which this package appears (derived from its SBOM documents).

vulnerabilities

dict[string, Vulnerability]

Vulnerabilities affecting this package, keyed by vulnerability ID.


Assessment Object

Field

Type

Description

id

string

Assessment ID (UUID).

vuln_id

string

ID of the vulnerability this assessment affects.

packages

list[string]

List of packages concerned.

timestamp

string

Creation datetime in ISO 8601 format.

last_update

string

Last update datetime in ISO 8601 format.

status

string

Status of the assessment (see possible values below).

simplified_status

string

Simplified status: pending, affected, fixed, or ignored.

status_notes

string

Free-text note about the status.

justification

string

Justification for the status (see possible values below).

impact_statement

string

Text explaining why the vulnerability does not apply.

responses

list[string]

List of remediation responses (see possible values below).

workaround

string

Workaround description.

workaround_timestamp

string

Datetime of this workaround in ISO 8601 format.

variant_id

string

UUID of the variant this assessment belongs to, or empty.


Project Object

Field

Type

Description

id

string

Project ID (UUID).

name

string

Name of the project.


Variant Object

Field

Type

Description

id

string

Variant ID (UUID).

name

string

Name of the variant (e.g. build configuration).

project_id

string

UUID of the project this variant belongs to.

scans

dict[string, Scan]

All scans belonging to this variant, keyed by UUID.

sbom_documents

dict[string, SBOM Document]

All SBOM documents belonging to this variant (across all its scans), keyed by UUID.

packages

dict[string, Package]

All packages found in this variant, keyed by name@version.

assessments

list[Assessment]

All assessments scoped to this variant.

vulnerabilities

dict[string, Vulnerability]

All vulnerabilities that have at least one assessment in this variant, keyed by vulnerability ID.


Scan Object

Field

Type

Description

id

string

Scan ID (UUID).

description

string

Description of the scan (may be empty).

timestamp

string

Scan datetime in ISO 8601 format.

variant_id

string

UUID of the variant this scan belongs to.

variant

Variant

The variant object this scan belongs to.

sbom_documents

dict[string, SBOM Document]

All SBOM documents produced by this scan, keyed by UUID.

packages

dict[string, Package]

All packages found across this scan’s SBOM documents, keyed by name@version.


SBOM Document Object

Field

Type

Description

id

string

SBOM document ID (UUID).

path

string

File path of the SBOM document.

source_name

string

Name of the source that produced this document.

format

string

Format of the document (e.g. spdx, cdx, openvex, yocto_cve_check).

scan_id

string

UUID of the scan this document belongs to.

variant_id

string

UUID of the variant this document belongs to (derived from its scan).

scan

Scan

The scan object this document belongs to.

packages

dict[string, Package]

All packages listed in this document, keyed by name@version.

vulnerabilities

dict[string, Vulnerability]

All vulnerabilities affecting any package in this document, keyed by vulnerability ID.

Possible values for status

Value

Description

Filter

in_triage, under_investigation

Vulnerability found but presence not confirmed. Default status.

status_pending, status_active

affected, exploitable

Vulnerability confirmed and affecting the product.

status_affected, status_active

fixed, resolved, resolved_with_pedigree

Vulnerability is fixed and no longer exploitable.

status_fixed, status_inactive

not_affected, false_positive

Vulnerability is a false positive or not affecting us.

status_ignored, status_inactive

Possible values for justification

  • component_not_present

  • vulnerable_code_not_present

  • vulnerable_code_not_in_execute_path

  • vulnerable_code_cannot_be_controlled_by_adversary

  • inline_mitigations_already_exist

  • code_not_present

  • code_not_reachable

  • requires_configuration

  • requires_dependency

  • requires_environment

  • protected_by_compiler

  • protected_at_runtime

  • protected_at_perimeter

  • protected_by_mitigating_control

Possible values for responses

  • can_not_fix

  • will_not_fix

  • update

  • rollback

  • workaround_available


Filters and Helpers

In addition to Jinja built-in filters, the following custom filters are available.

Conversion

Filter

Description

as_list

Convert a dict to a list using .values().

limit(n)

Limit the number of results to n (int).

print_iso8601

Transform an ISO 8601 duration string into a human-readable format (e.g. P2DT4H2d 4h) or format a datetime string.

Status Filtering

Filter

Description

status(x)

Keep only vulnerabilities with status in x (str or list of str).

status_pending

Shorthand for in_triage + under_investigation.

status_affected

Shorthand for affected + exploitable.

status_fixed

Shorthand for fixed + resolved + resolved_with_pedigree.

status_ignored

Shorthand for not_affected + false_positive.

status_active

status_pending + status_affected.

status_inactive

status_fixed + status_ignored.

Score Filtering

Filter

Description

severity(x)

Keep only vulnerabilities with severity in x (str or list of str).

epss_score(x)

Keep only vulnerabilities with EPSS score ≥ x. x is a percentage in [0, 100].

Date Filtering

Both last_assessment_date and filter_by_publish_date accept date expressions in the following formats:

Format

Meaning

'>2026-01-01'

After this date (exclusive)

'>=2026-01-01'

After or on this date (inclusive)

'<2026-01-01'

Before this date (exclusive)

'<=2026-01-01'

Before or on this date (inclusive)

'2026-01-01..2026-01-31'

Between two dates (inclusive)

'2026-01-01'

Exact date match

Filter

Description

last_assessment_date(x)

Filter vulnerabilities by their last assessment date.

filter_by_publish_date(x, include_unknown=False)

Filter by CVE publish date. Pass include_unknown=True to also include vulnerabilities with no known publish date.

Note for filter_by_publish_date: Exact date match ignores time (hours, minutes, seconds). When include_unknown=True, also includes vulnerabilities without a published date.

Sorting

Filter

Description

sort_by_epss

Sort vulnerabilities by EPSS score, highest first.

sort_by_effort

Sort vulnerabilities by effort (effort.likely), most effort first.

sort_by_last_modified

Sort vulnerabilities by latest assessment date, most recent first.

sort_by_scan_date

Sort scans by timestamp, most recent first. Accepts a dict (keyed by ID) or a list.

Relational Filtering

Filter

Description

filter_by_variant(variant_id)

Keep only items that belong to the given variant. Works on assessments, scans, and vulnerabilities (matches against variant_ids). Accepts a dict or a list.

filter_by_project(project_id)

Keep only items whose project_id matches. Works on variants. Accepts a dict or a list.


Common Examples and Tips

Get total count of active/open vulnerabilities:

{{ vulnerabilities | as_list | status_active | length }}

Get active vulnerability with highest EPSS score:

{{ vulnerabilities | as_list | status_active | sort_by_epss | first }}

Get vulnerabilities assessed after a specific date:

{{ vulnerabilities | as_list | last_assessment_date('>2026-01-01') | length }}

Get vulnerabilities assessed in January 2026:

{{ vulnerabilities | as_list | last_assessment_date('2026-01-01..2026-01-31') }}

Get critical vulnerabilities assessed recently (after or on 2026-01-15):

{{ vulnerabilities | as_list | severity('critical') | last_assessment_date('>=2026-01-15') | sort_by_last_modified }}

List all critical unresolved vulnerabilities, sorted by most recent assessment:

{% for vuln in vulnerabilities | as_list | severity('critical') | status_active | sort_by_last_modified %}
- {{ vuln.id }} (CVSS: {{ vuln.severity.max_score }})
{% endfor %}

Use an environment variable in a template:

Product: {{ env("PRODUCT_NAME", "unknown") }}
Version: {{ env("PRODUCT_VERSION", "unknown") }}

List all projects and their variants:

{% for project in projects | as_list %}
Project: {{ project.name }}
  {% for variant in variants | filter_by_project(project.id) %}
  - Variant: {{ variant.name }}
  {% endfor %}
{% endfor %}

List most recent scans for a variant:

{% for scan in scans | filter_by_variant(variant_id) | sort_by_scan_date | limit(5) %}
- {{ scan.timestamp | print_iso8601 }}: {{ scan.description }}
{% endfor %}

List SBOM documents:

{% for doc in sbom_documents | as_list %}
- {{ doc.source_name }} ({{ doc.format }}): {{ doc.path }}
{% endfor %}

Traversing the data models

Every entity exposes its related entities as embedded fields, so you can traverse the data model in any direction.

The relationships are:

Project
  └─ Variant  (.scans, .sbom_documents, .packages, .vulnerabilities, .assessments)
       └─ Scan  (.sbom_documents, .packages, .variant)
            └─ SBOM Document  (.packages, .vulnerabilities, .scan)
                 └─ Package  (.sbom_documents, .variants, .vulnerabilities)
                      └─ Vulnerability  (.variant_ids, .assessments_by_variant)

Loop over vulnerabilities per variant:

{% for variant in variants | as_list %}
{{ variant.name }}
  {% for vuln in variant.vulnerabilities.values() | sort_by_epss %}
  - {{ vuln.id }} (EPSS: {{ vuln.epss.score }})
  {% endfor %}
{% endfor %}

Loop over packages per SBOM document:

{% for doc in sbom_documents | as_list %}
{{ doc.source_name }} — {{ doc.path }}
  {% for pkg_id, pkg in doc.packages.items() %}
  - {{ pkg.name }} {{ pkg.version }}
  {% endfor %}
{% endfor %}

Full traversal:

{% for project in projects | as_list %}
  {% for variant in variants | filter_by_project(project.id) %}
    {% for doc in variant.sbom_documents.values() %}
      {% for vuln in doc.vulnerabilities.values() | status_active | sort_by_epss %}
        {{ vuln.id }} in {{ doc.path }}
      {% endfor %}
    {% endfor %}
  {% endfor %}
{% endfor %}

Per-variant assessment breakdown for a single vulnerability:

{% for variant_id, assessments in vuln.assessments_by_variant.items() %}
  Variant {{ variant_id }}: {{ assessments | first | attr('status') }}
{% endfor %}

Packages that appear in multiple variants:

{% for pkg_id, pkg in packages.items() %}
  {% if pkg.variants | length > 1 %}
  {{ pkg.name }} {{ pkg.version }} appears in {{ pkg.variants | length }} variants
  {% endif %}
{% endfor %}

Most recent scan for a variant (using embedded .scans):

{% for variant in variants | as_list %}
  {% set latest = variant.scans | sort_by_scan_date | first %}
  {{ variant.name }}: last scanned {{ latest.timestamp | print_iso8601 }}
{% endfor %}