Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: 'npm' # See documentation for possible values
directory: '/' # Location of package manifests
schedule:
interval: 'weekly'
allow:
- dependency-type: 'production'
38 changes: 38 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This workflow will run tests using node and then publish a package to the
# npm registry when a release is created.
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages

name: NPM Package

on:
release:
types: [created]

permissions:
id-token: write
contents: read
actions: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24.x
- run: npm ci
- run: npm test

publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24.x
registry-url: https://registry.npmjs.org/
- run: npm i -g npm@11
- run: npm ci
- run: npm publish
26 changes: 26 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Run Tests
on:
pull_request:
branches:
- '**'
push:
branches:
- main
permissions:
contents: read
actions: read
jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [22.x, 24.x]

steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules/*
/dist/*
/build
/coverage
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/dist
/coverage
/lib/css
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]
Copyright 2026 Bundesamt für Sicherheit in der Informationstechnik (BSI)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
162 changes: 160 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,160 @@
# html-template
A js library to access the html template used by secvisogram
# @secvisogram/html-template

A JavaScript library for rendering [CSAF](https://oasis-open.github.io/csaf-documentation/)
(Common Security Advisory Framework) documents (versions 2.0 and 2.1) to HTML,
using the same Mustache templates as [Secvisogram](https://github.com/secvisogram/secvisogram).

It is used by [`@secvisogram/cli`](https://github.com/secvisogram/cli) to
render CSAF documents from the command line, and by the Secvisogram web app
itself for its preview/export features.

## Installation

```sh
npm install @secvisogram/html-template
```

## Usage

Rendering a document is a three-step pipeline: enrich the raw CSAF document
with the data the template needs, render markdown fields, then render the
HTML template. All three steps apply to both CSAF versions.

### CSAF 2.0

```js
import {
enrichDocumentV2_0,
HTMLTemplate2_0,
renderMarkdown,
} from '@secvisogram/html-template'

const csafDocument = JSON.parse(await readFile('advisory.json', 'utf8'))

const { document: enriched } = enrichDocumentV2_0(csafDocument)
const parsed = renderMarkdown(enriched)
const html = HTMLTemplate2_0({ document: parsed })
```

### CSAF 2.1

```js
import {
enrichDocumentV2_1,
HTMLTemplate2_1,
renderMarkdown,
} from '@secvisogram/html-template'

const csafDocument = JSON.parse(await readFile('advisory.json', 'utf8'))

const { document: enriched } = enrichDocumentV2_1(csafDocument)
const parsed = renderMarkdown(enriched)
const html = HTMLTemplate2_1({ document: parsed })
```

> [!IMPORTANT]
> Always call `renderMarkdown` on the _enriched_ document (the return value
> of `enrichDocumentV2_0`/`enrichDocumentV2_1`), not the original raw
> document, and always pass its result into `HTMLTemplate2_0`/
> `HTMLTemplate2_1`. Skipping this step - or passing the wrong document -
> means markdown syntax (e.g. `**bold**`) in text fields such as notes,
> references, and remediation details will appear as raw, unrendered
> markdown in the output HTML.

## API

### `enrichDocumentV2_0(document)` / `enrichDocumentV2_1(document)`

Takes a raw, parsed CSAF 2.0 or 2.1 document (a plain object, as produced by
`JSON.parse`) and returns `{ document }`, a deep-cloned copy enriched with
the extra, denormalised data the Mustache templates rely on - for example:

- resolving `product_id`/`group_id` references (in `product_status`,
`remediations`, `threats`, `product_groups`, ...) to include the
product/group's display name
- attaching the matching CVSS vector string/base score (and, for CSAF 2.1,
which CVSS version it came from) to each affected product
- computing `document.max_base_score`, the highest CVSS base score across
all vulnerabilities
- splitting `notes` (both document-level and per-vulnerability) and
`remediations`/`threats` into per-category buckets (e.g.
`notes_summary`, `remediations_vendor_fix`, `threats_impact`, ...), sorted
by date where applicable
- attaching the [Mustache lambda helpers](#mustache-lambda-helpers) the
templates use, as properties on the returned document

The original input document is not mutated.

`enrichDocumentV2_0` reads CVSS data from `vulnerability.scores[].cvss_v3`
(the CSAF 2.0 shape, which only ever carries CVSS v3). `enrichDocumentV2_1`
reads it from `vulnerability.metrics[].content` (the CSAF 2.1 shape), where
`cvss_v2`, `cvss_v3`, and `cvss_v4` are all optional, independent siblings -
a single metric may carry more than one CVSS version at once. When more than
one is present, `enrichDocumentV2_1` prefers the highest version (v4, then
v3, then v2) as the "primary" score/vector shown for that product, and also
exposes which version was picked via `cvssVersion` on each resolved product
entry.

### `renderMarkdown(document)`

Renders [GitHub Flavored Markdown](https://github.github.com/gfm/) syntax to
HTML in a fixed allow-list of text fields (e.g. `document.notes[].text`,
`vulnerabilities[].remediations[].details`, ...) throughout the document,
mutating it in place, and also returning it. Fields not on the allow-list are
left untouched, even if they happen to contain markdown-like syntax.

If a field's content doesn't actually use any markdown syntax, it's left as
plain text rather than being wrapped in a `<p>` tag.

### `HTMLTemplate2_0({ document })` / `HTMLTemplate2_1({ document })`

Renders the enriched (and markdown-processed) document into a complete HTML
document string, using the bundled Mustache template for that CSAF version.
There is currently no way to supply a custom template.

### Mustache lambda helpers

`enrichDocumentV2_0`/`enrichDocumentV2_1` attach four
[Mustache lambdas](https://github.com/janl/mustache.js#lambdas) onto the
returned document, which the bundled templates invoke as e.g.
`{{#secureHref}}{{someUrl}}{{/secureHref}}`:

- **`secureHref`** - only emits an `href="..."` attribute if the URL's
scheme (or, for `data:` URLs, MIME type) is on an allow-list
(`#`, `mailto:`, `tel:`, `http(s):`, `ftp:`, and base64-encoded
`image/png`, `image/jpeg`, or `image/gif` data URIs); otherwise renders
nothing. This is a deliberate defense against untrusted advisory content
(e.g. `javascript:` URLs) ending up as clickable links.
- **`upperCase`** - capitalises the first character of the rendered text.
- **`replaceUnderscores`** - replaces all `_` with spaces (e.g. for
CSAF's `snake_case` category enum values).
- **`removeTrailingComma`** - strips a trailing comma from the rendered
text (for comma-joined lists built with a trailing separator).

## Known limitations

- Templates are not customisable - `HTMLTemplate2_0`/`HTMLTemplate2_1`
always use the bundled template.

## Rendered HTML output

The bundled `lib/css` stylesheets (a vendored copy of
Comment thread
DevPhilB marked this conversation as resolved.
[gutenberg-css](https://github.com/BafS/Gutenberg)'s base and `modern` theme
stylesheets, plus Secvisogram's own `preview.css`) are inlined directly into
`<style>` tags in the rendered HTML, rather than linked via
`<link href="...">`. This makes the output a
single, self-contained HTML string/file with no separate network request or
file path that needs to resolve correctly - which matters both for
[`@secvisogram/cli`](https://github.com/secvisogram/cli)'s output (a
standalone file, possibly opened directly from disk) and for the
Secvisogram web app's preview (rendered into a blank `<iframe>` with no
base URL of its own, where relative/absolute file paths can't reliably
resolve at all).

## Development

```sh
npm install
npm test # type-check, prettier --check, and run the test suite
npm run test-coverage
```
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { enrichDocumentV2_0, enrichDocumentV2_1 } from './lib/enrichDocument.js'
export { default as HTMLTemplate2_0 } from './lib/HTMLTemplate2_0.js'
export { default as HTMLTemplate2_1 } from './lib/HTMLTemplate2_1.js'
export { renderMarkdown } from './lib/markdownParser.js'
46 changes: 46 additions & 0 deletions lib/HTMLTemplate2_0.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Mustache from 'mustache'
import Template from './templates/csaf_2_0.js'
import {
ACKNOWLEDGEMENT,
DOCUMENT_NOTE,
REFERENCE,
REMEDIATION,
THREAT,
URL,
VULNERABILITY_NOTE,
} from './mustachePartials.js'

const PRODUCT_STATUS_HEADER = `
<thead>
<tr>
<th>Product</th>
<th>CVSS-Vector</th>
<th>CVSS Base Score</th>
</tr>
</thead>`

const PRODUCT_STATUS_ROW = `
<tr>
<td>{{name}}</td>
<td>{{vectorString}}</td>
<td>{{baseScore}}</td>
</tr>`

/**
* Encapsulates the rendering of the mustache template.
*
* @param {{ document: {} }} props
*/
export default function HTMLTemplate2_0({ document }) {
return Mustache.render(Template, document, {
product_status_header: PRODUCT_STATUS_HEADER,
product_status_row: PRODUCT_STATUS_ROW,
remediation: REMEDIATION,
threat: THREAT,
vulnerability_note: VULNERABILITY_NOTE,
document_note: DOCUMENT_NOTE,
acknowledgment: ACKNOWLEDGEMENT,
reference: REFERENCE,
url: URL,
})
}
48 changes: 48 additions & 0 deletions lib/HTMLTemplate2_1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Mustache from 'mustache'
import Template from './templates/csaf_2_1.js'
import {
ACKNOWLEDGEMENT,
DOCUMENT_NOTE,
REFERENCE,
REMEDIATION,
THREAT,
URL,
VULNERABILITY_NOTE,
} from './mustachePartials.js'

const PRODUCT_STATUS_HEADER = `
<thead>
<tr>
<th>Product</th>
<th>CVSS Version</th>
<th>CVSS-Vector</th>
<th>CVSS Base Score</th>
</tr>
</thead>`

const PRODUCT_STATUS_ROW = `
<tr>
<td>{{name}}</td>
<td>{{cvssVersion}}</td>
<td>{{vectorString}}</td>
<td>{{baseScore}}</td>
</tr>`

/**
* Encapsulates the rendering of the mustache template.
*
* @param {{ document: {} }} props
*/
export default function HTMLTemplate2_1({ document }) {
return Mustache.render(Template, document, {
product_status_header: PRODUCT_STATUS_HEADER,
product_status_row: PRODUCT_STATUS_ROW,
remediation: REMEDIATION,
threat: THREAT,
vulnerability_note: VULNERABILITY_NOTE,
document_note: DOCUMENT_NOTE,
acknowledgment: ACKNOWLEDGEMENT,
reference: REFERENCE,
url: URL,
})
}
11 changes: 11 additions & 0 deletions lib/css/gutenberg.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading