diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..943a03a --- /dev/null +++ b/.github/dependabot.yml @@ -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' diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..643d4c4 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -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 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..03ef504 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3150af0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules/* +/dist/* +/build +/coverage diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9ac8356 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +/dist +/coverage +/lib/css diff --git a/LICENSE b/LICENSE index 261eeb9..a6cb839 100644 --- a/LICENSE +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index f67a783..83959ad 100644 --- a/README.md +++ b/README.md @@ -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 `
` 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 +[gutenberg-css](https://github.com/BafS/Gutenberg)'s base and `modern` theme +stylesheets, plus Secvisogram's own `preview.css`) are inlined directly into +` + + + + + +
+| Publisher: {{document.publisher.name}} | +Document category: {{document.category}} | +
| Initial release date: {{document.tracking.initial_release_date}} | +Engine: {{#document.tracking.generator.engine}}{{name}}{{#version}} {{.}}{{/version}}{{/document.tracking.generator.engine}} | +
| Current release date: {{document.tracking.current_release_date}} | +Build Date: {{document.tracking.generator.date}} | +
| Current version: {{document.tracking.version}} | +Status: {{document.tracking.status}} | +
| Severity: + {{#document.aggregate_severity.namespace}} {{document.aggregate_severity.text}}{{/document.aggregate_severity.namespace}} + {{^document.aggregate_severity.namespace}} {{document.aggregate_severity.text}}{{/document.aggregate_severity.namespace}} + | +|
| Original language: {{document.source_lang}} | +Language: {{document.lang}} | +
| Also referred to: {{#removeTrailingComma}}{{#document.tracking.aliases}}{{.}}, {{/document.tracking.aliases}}{{/removeTrailingComma}} | +|
Namespace: {{namespace}}
+{{contact_details}}
+{{{issuing_authority}}}
+ {{/document.publisher}} + + {{#document.references.length}} +| Version | +Date of the revision | +Summary of the revision | +
|---|---|---|
| {{number}} | +{{date}} | +{{{summary}}} | +
`
+
+export const TAIL = ` {{#tlp}}
+ {{#label}}TLP:{{.}}{{/label}}
+ For the TLP version see: {{#url}}{{.}}{{/url}}{{^url}}https://www.first.org/tlp/{{/url}}
+ {{/tlp}}
+
{{{text}}}
+ {{/document.distribution}} + + {{#document.notes_legal_disclaimer}} + {{> document_note}} + {{/document.notes_legal_disclaimer}} + + + +