From 3c70acb3e1d51bc6bb895c5365a865637ec83ace Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Wed, 14 May 2025 11:12:11 +0200 Subject: [PATCH 1/5] feat(CSAF2.1): #199 test 6.3.14 for CSAF 2.1 --- README.md | 2 +- csaf_2_1/informativeTests.js | 1 + .../informativeTest_6_3_14.js | 130 ++++++++++++++++++ tests/csaf_2_1/informativeTest_6_3_14.js | 54 ++++++++ tests/csaf_2_1/oasis.js | 1 - 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 csaf_2_1/informativeTests/informativeTest_6_3_14.js create mode 100644 tests/csaf_2_1/informativeTest_6_3_14.js diff --git a/README.md b/README.md index 6f4f9297..2fde381f 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,6 @@ The following tests are not yet implemented and therefore missing: **Informative Tests** - Informative Test 6.2.13 -- Informative Test 6.2.14 - Informative Test 6.2.15 - Informative Test 6.2.16 - Informative Test 6.2.17 @@ -480,6 +479,7 @@ export const informativeTest_6_3_9: DocumentTest export const informativeTest_6_3_10: DocumentTest export const informativeTest_6_3_11: DocumentTest export const informativeTest_6_3_12: DocumentTest +export const informativeTest_6_3_14: DocumentTest ``` [(back to top)](#bsi-csaf-validator-lib) diff --git a/csaf_2_1/informativeTests.js b/csaf_2_1/informativeTests.js index b3142316..8715f5a9 100644 --- a/csaf_2_1/informativeTests.js +++ b/csaf_2_1/informativeTests.js @@ -12,3 +12,4 @@ export { informativeTest_6_3_1 } from './informativeTests/informativeTest_6_3_1. export { informativeTest_6_3_2 } from './informativeTests/informativeTest_6_3_2.js' export { informativeTest_6_3_4 } from './informativeTests/informativeTest_6_3_4.js' export { informativeTest_6_3_12 } from './informativeTests/informativeTest_6_3_12.js' +export { informativeTest_6_3_14 } from './informativeTests/informativeTest_6_3_14.js' \ No newline at end of file diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_14.js b/csaf_2_1/informativeTests/informativeTest_6_3_14.js new file mode 100644 index 00000000..dbce5fe6 --- /dev/null +++ b/csaf_2_1/informativeTests/informativeTest_6_3_14.js @@ -0,0 +1,130 @@ +import Ajv from 'ajv/dist/jtd.js' + +const ajv = new Ajv() + +/** + * @typedef {object} Selection + * @property {string} [name] + * @property {string} [namespace] + * @property {string} [version] + */ + +/** + * @typedef {object} Ssvc1 + * @property {Array} [selections] + */ + +/** + * @typedef {object} MetricContent + * @property {Ssvc1} [ssvc_v1] + */ + +/** + * @typedef {object} Metric + * @property {MetricContent} [content] + */ + +const inputSchema = /** @type {const} */ ({ + additionalProperties: true, + properties: { + document: { + additionalProperties: true, + properties: { + distribution: { + additionalProperties: true, + properties: { + tlp: { + additionalProperties: true, + properties: { + label: { type: 'string' }, + }, + }, + }, + }, + }, + }, + vulnerabilities: { + elements: { + additionalProperties: true, + optionalProperties: { + metrics: { + elements: { + additionalProperties: true, + optionalProperties: { + content: { + additionalProperties: true, + optionalProperties: { + ssvc_v1: { + additionalProperties: true, + optionalProperties: { + selections: { + elements: { + additionalProperties: true, + optionalProperties: { + name: { type: 'string' }, + namespace: { type: 'string' }, + version: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}) + +const validateInput = ajv.compile(inputSchema) + +/** + * @param {string | undefined } namespace + */ +function isPrivateNamespace(namespace) { + return namespace ? namespace.startsWith('x_') : false +} + +/** + * or each SSVC decision point given under selections, + * it MUST be tested the namespace is not a private one + * if the document is not labeled TLP:CLEAR. + * @param {unknown} doc + * @returns + */ +export function informativeTest_6_3_14(doc) { + const ctx = { + infos: /** @type {Array<{ message: string; instancePath: string }>} */ ([]), + } + + if (!validateInput(doc)) { + return ctx + } + + const vulnerabilities = doc.vulnerabilities + + vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { + /** @type {Array | undefined} */ + const metrics = vulnerability.metrics + metrics?.forEach((metric, metricIndex) => { + const selections = metric?.content?.ssvc_v1?.selections + selections?.forEach((selection, selectionIndex) => { + if ( + isPrivateNamespace(selection.namespace) && + doc.document.distribution.tlp.label !== 'TLP:CLEAR' + ) { + ctx.infos.push({ + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v1/selections/${selectionIndex}/namespace`, + message: `namespace is private and document is not labeled TLP:CLEAR`, + }) + } + }) + }) + }) + + return ctx +} diff --git a/tests/csaf_2_1/informativeTest_6_3_14.js b/tests/csaf_2_1/informativeTest_6_3_14.js new file mode 100644 index 00000000..ba15d8e4 --- /dev/null +++ b/tests/csaf_2_1/informativeTest_6_3_14.js @@ -0,0 +1,54 @@ +import assert from 'node:assert' +import { informativeTest_6_3_14 } from '../../csaf_2_1/informativeTests.js' + +describe('informativeTest_6_3_14', function () { + it('only runs on relevant documents', function () { + assert.equal(informativeTest_6_3_14({ document: 'mydoc' }).infos.length, 0) + }) + + it('test input schema with not considered json object in vulnerabilities', function () { + assert.equal( + informativeTest_6_3_14({ + document: { + distribution: { + tlp: { + label: 'GREEN', + }, + }, + }, + vulnerabilities: [ + {}, + { + cve: 'CVE-1900-0001', + metrics: [ + { + content: { + ssvc_v1: { + id: 'CVE-1900-0001', + schemaVersion: '1-0-1', + selections: [ + { + name: 'Technical Impact', + namespace: 'x_custom', + values: ['Total'], + version: '1.0.0', + }, + { + name: 'Technical Impact', + values: ['Total'], + version: '1.0.0', + }, + ], + timestamp: '2024-01-24T10:00:00.000Z', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], + }).infos.length, + 1 + ) + }) +}) diff --git a/tests/csaf_2_1/oasis.js b/tests/csaf_2_1/oasis.js index 0e9d2e60..7769050a 100644 --- a/tests/csaf_2_1/oasis.js +++ b/tests/csaf_2_1/oasis.js @@ -61,7 +61,6 @@ const excluded = [ '6.2.47', '6.3.12', '6.3.13', - '6.3.14', '6.3.15', '6.3.16', '6.3.17', From 5ebd1e8cf99d462efb92c64e2e3a3f02f8639a76 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:04:24 +0200 Subject: [PATCH 2/5] feat(CSAF2.1): #199 test 6.3.14 for CSAF 2.1 - fix quotes --- csaf_2_1/informativeTests/informativeTest_6_3_14.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_14.js b/csaf_2_1/informativeTests/informativeTest_6_3_14.js index dbce5fe6..0f8091d2 100644 --- a/csaf_2_1/informativeTests/informativeTest_6_3_14.js +++ b/csaf_2_1/informativeTests/informativeTest_6_3_14.js @@ -119,7 +119,7 @@ export function informativeTest_6_3_14(doc) { ) { ctx.infos.push({ instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v1/selections/${selectionIndex}/namespace`, - message: `namespace is private and document is not labeled TLP:CLEAR`, + message: 'namespace is private and document is not labeled TLP:CLEAR', }) } }) From b747229ad90a08aa373c877353c32b4a7a0f73c2 Mon Sep 17 00:00:00 2001 From: rschneider <97682836+rainer-exxcellent@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:44:15 +0200 Subject: [PATCH 3/5] feat(CSAF2.1): #199 test 6.3.14 for CSAF 2.1 - switch to ssvc_2 --- csaf_2_1/informativeTests.js | 2 +- .../informativeTest_6_3_14.js | 35 ++++++++++++------- tests/csaf_2_1/informativeTest_6_3_14.js | 4 +-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/csaf_2_1/informativeTests.js b/csaf_2_1/informativeTests.js index 8715f5a9..787fb360 100644 --- a/csaf_2_1/informativeTests.js +++ b/csaf_2_1/informativeTests.js @@ -12,4 +12,4 @@ export { informativeTest_6_3_1 } from './informativeTests/informativeTest_6_3_1. export { informativeTest_6_3_2 } from './informativeTests/informativeTest_6_3_2.js' export { informativeTest_6_3_4 } from './informativeTests/informativeTest_6_3_4.js' export { informativeTest_6_3_12 } from './informativeTests/informativeTest_6_3_12.js' -export { informativeTest_6_3_14 } from './informativeTests/informativeTest_6_3_14.js' \ No newline at end of file +export { informativeTest_6_3_14 } from './informativeTests/informativeTest_6_3_14.js' diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_14.js b/csaf_2_1/informativeTests/informativeTest_6_3_14.js index 0f8091d2..8fdc1e15 100644 --- a/csaf_2_1/informativeTests/informativeTest_6_3_14.js +++ b/csaf_2_1/informativeTests/informativeTest_6_3_14.js @@ -2,6 +2,16 @@ import Ajv from 'ajv/dist/jtd.js' const ajv = new Ajv() +/** from https://github.com/CERTCC/SSVC/blob/main/src/ssvc/namespaces.py */ +const REGISTERED_NAMESPACES = [ + 'ssvc', + 'cvss', + 'cisa', + 'basic', + 'example', + 'test', +] + /** * @typedef {object} Selection * @property {string} [name] @@ -10,13 +20,13 @@ const ajv = new Ajv() */ /** - * @typedef {object} Ssvc1 + * @typedef {object} Ssvc2 * @property {Array} [selections] */ /** * @typedef {object} MetricContent - * @property {Ssvc1} [ssvc_v1] + * @property {Ssvc2} [ssvc_v2] */ /** @@ -54,7 +64,7 @@ const inputSchema = /** @type {const} */ ({ content: { additionalProperties: true, optionalProperties: { - ssvc_v1: { + ssvc_v2: { additionalProperties: true, optionalProperties: { selections: { @@ -85,14 +95,14 @@ const validateInput = ajv.compile(inputSchema) /** * @param {string | undefined } namespace */ -function isPrivateNamespace(namespace) { - return namespace ? namespace.startsWith('x_') : false +function isRegisteredNamespace(namespace) { + return namespace ? REGISTERED_NAMESPACES.includes(namespace) : false } /** - * or each SSVC decision point given under selections, - * it MUST be tested the namespace is not a private one - * if the document is not labeled TLP:CLEAR. + * For each SSVC decision point given under `selections`, it MUST be tested that the base `namespace` is not an unregistered one + * if the document is not labeled `TLP:CLEAR`. + * Namespaces reserved for special purpose MUST be treated as per their definition. * @param {unknown} doc * @returns */ @@ -111,15 +121,16 @@ export function informativeTest_6_3_14(doc) { /** @type {Array | undefined} */ const metrics = vulnerability.metrics metrics?.forEach((metric, metricIndex) => { - const selections = metric?.content?.ssvc_v1?.selections + const selections = metric?.content?.ssvc_v2?.selections selections?.forEach((selection, selectionIndex) => { if ( - isPrivateNamespace(selection.namespace) && + !isRegisteredNamespace(selection.namespace) && doc.document.distribution.tlp.label !== 'TLP:CLEAR' ) { ctx.infos.push({ - instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v1/selections/${selectionIndex}/namespace`, - message: 'namespace is private and document is not labeled TLP:CLEAR', + instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v2/selections/${selectionIndex}/namespace`, + message: + 'namespace is not an unregistered one document is not labeled TLP:CLEAR', }) } }) diff --git a/tests/csaf_2_1/informativeTest_6_3_14.js b/tests/csaf_2_1/informativeTest_6_3_14.js index ba15d8e4..aa24d909 100644 --- a/tests/csaf_2_1/informativeTest_6_3_14.js +++ b/tests/csaf_2_1/informativeTest_6_3_14.js @@ -23,13 +23,13 @@ describe('informativeTest_6_3_14', function () { metrics: [ { content: { - ssvc_v1: { + ssvc_v2: { id: 'CVE-1900-0001', schemaVersion: '1-0-1', selections: [ { name: 'Technical Impact', - namespace: 'x_custom', + namespace: 'cisa', values: ['Total'], version: '1.0.0', }, From 1ba931c0a1b32af5d42ab24cdfd787c1e12353fd Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 27 Aug 2026 16:30:55 +0200 Subject: [PATCH 4/5] feat(csaf2.1): change unit test to vitest --- .../informativeTest_6_3_14.js | 82 +++++-------------- csaf_2_1/shared/ssvcNamespaces.js | 43 ++++++++++ tests/csaf_2_1/informativeTest_6_3_14.js | 79 ++++++++++++++++-- 3 files changed, 136 insertions(+), 68 deletions(-) diff --git a/csaf_2_1/informativeTests/informativeTest_6_3_14.js b/csaf_2_1/informativeTests/informativeTest_6_3_14.js index 8fdc1e15..ceba8789 100644 --- a/csaf_2_1/informativeTests/informativeTest_6_3_14.js +++ b/csaf_2_1/informativeTests/informativeTest_6_3_14.js @@ -1,51 +1,20 @@ -import Ajv from 'ajv/dist/jtd.js' +import { Ajv } from 'ajv/dist/jtd.js' +import { isRegisteredSsvcNamespace } from '#csaf_2_1/shared/ssvcNamespaces.js' const ajv = new Ajv() -/** from https://github.com/CERTCC/SSVC/blob/main/src/ssvc/namespaces.py */ -const REGISTERED_NAMESPACES = [ - 'ssvc', - 'cvss', - 'cisa', - 'basic', - 'example', - 'test', -] - -/** - * @typedef {object} Selection - * @property {string} [name] - * @property {string} [namespace] - * @property {string} [version] - */ - -/** - * @typedef {object} Ssvc2 - * @property {Array} [selections] - */ - -/** - * @typedef {object} MetricContent - * @property {Ssvc2} [ssvc_v2] - */ - -/** - * @typedef {object} Metric - * @property {MetricContent} [content] - */ - const inputSchema = /** @type {const} */ ({ additionalProperties: true, properties: { document: { additionalProperties: true, - properties: { + optionalProperties: { distribution: { additionalProperties: true, - properties: { + optionalProperties: { tlp: { additionalProperties: true, - properties: { + optionalProperties: { label: { type: 'string' }, }, }, @@ -71,9 +40,7 @@ const inputSchema = /** @type {const} */ ({ elements: { additionalProperties: true, optionalProperties: { - name: { type: 'string' }, namespace: { type: 'string' }, - version: { type: 'string' }, }, }, }, @@ -93,18 +60,13 @@ const inputSchema = /** @type {const} */ ({ const validateInput = ajv.compile(inputSchema) /** - * @param {string | undefined } namespace - */ -function isRegisteredNamespace(namespace) { - return namespace ? REGISTERED_NAMESPACES.includes(namespace) : false -} - -/** - * For each SSVC decision point given under `selections`, it MUST be tested that the base `namespace` is not an unregistered one - * if the document is not labeled `TLP:CLEAR`. - * Namespaces reserved for special purpose MUST be treated as per their definition. + * This implements the informative test 6.3.14 of the CSAF 2.1 standard. + * + * For each decision point, it SHALL be tested that the namespace is not an + * unregistered one if the document is not labeled TLP:CLEAR. + * * @param {unknown} doc - * @returns + * @returns {{ infos: Array<{ message: string; instancePath: string }> }} */ export function informativeTest_6_3_14(doc) { const ctx = { @@ -115,22 +77,22 @@ export function informativeTest_6_3_14(doc) { return ctx } + if (doc.document?.distribution?.tlp?.label === 'CLEAR') { + return ctx + } + const vulnerabilities = doc.vulnerabilities vulnerabilities.forEach((vulnerability, vulnerabilityIndex) => { - /** @type {Array | undefined} */ - const metrics = vulnerability.metrics - metrics?.forEach((metric, metricIndex) => { - const selections = metric?.content?.ssvc_v2?.selections - selections?.forEach((selection, selectionIndex) => { - if ( - !isRegisteredNamespace(selection.namespace) && - doc.document.distribution.tlp.label !== 'TLP:CLEAR' - ) { + vulnerability.metrics?.forEach((metric, metricIndex) => { + const selections = metric.content?.ssvc_v2?.selections + if (!selections) return + selections.forEach((selection, selectionIndex) => { + if (!selection.namespace) return + if (!isRegisteredSsvcNamespace(selection.namespace)) { ctx.infos.push({ instancePath: `/vulnerabilities/${vulnerabilityIndex}/metrics/${metricIndex}/content/ssvc_v2/selections/${selectionIndex}/namespace`, - message: - 'namespace is not an unregistered one document is not labeled TLP:CLEAR', + message: `SSVC decision point namespace "${selection.namespace}" is unregistered and the document is not labeled TLP:CLEAR`, }) } }) diff --git a/csaf_2_1/shared/ssvcNamespaces.js b/csaf_2_1/shared/ssvcNamespaces.js index 88cb9584..a5b6a32d 100644 --- a/csaf_2_1/shared/ssvcNamespaces.js +++ b/csaf_2_1/shared/ssvcNamespaces.js @@ -2,6 +2,49 @@ * SSVC Namespace Specification: https://certcc.github.io/SSVC/reference/code/namespaces/ */ +/** + * Registered SSVC base namespaces for production use, according to the SSVC + * Registered Namespace specification (SSVC-RNS): + * https://certcc.github.io/SSVC/reference/code/namespaces/#registered-namespace + */ +export const registeredSsvcNamespaces = [ + 'ssvc', + 'cvss', + 'cisa', + 'basic', + 'example', + 'test', + 'nist', +] + +/** + * Extracts the base namespace from a full SSVC namespace string. + * Strips everything after the first '#' or '/'. + * @param {string} namespace + * @returns {string} + */ +export function getSsvcBaseNamespace(namespace) { + const hashIdx = namespace.indexOf('#') + const slashIdx = namespace.indexOf('/') + + let endIdx = namespace.length + if (hashIdx !== -1) endIdx = Math.min(endIdx, hashIdx) + if (slashIdx !== -1) endIdx = Math.min(endIdx, slashIdx) + + return namespace.substring(0, endIdx) +} + +/** + * Returns true if the namespace belongs to a registered SSVC base namespace. + * @param {string} namespace - full namespace string + * @returns {boolean} + */ +export function isRegisteredSsvcNamespace(namespace) { + const base = getSsvcBaseNamespace(namespace) + if (base.startsWith('x_')) return false + return registeredSsvcNamespaces.includes(base) +} + /** * Returns true if the given full SSVC namespace string uses an extension. * diff --git a/tests/csaf_2_1/informativeTest_6_3_14.js b/tests/csaf_2_1/informativeTest_6_3_14.js index aa24d909..4217b276 100644 --- a/tests/csaf_2_1/informativeTest_6_3_14.js +++ b/tests/csaf_2_1/informativeTest_6_3_14.js @@ -1,13 +1,77 @@ -import assert from 'node:assert' import { informativeTest_6_3_14 } from '../../csaf_2_1/informativeTests.js' describe('informativeTest_6_3_14', function () { it('only runs on relevant documents', function () { - assert.equal(informativeTest_6_3_14({ document: 'mydoc' }).infos.length, 0) + expect(informativeTest_6_3_14({ document: 'mydoc' }).infos.length).toBe(0) }) - it('test input schema with not considered json object in vulnerabilities', function () { - assert.equal( + it('skips metrics without selections', function () { + expect( + informativeTest_6_3_14({ + document: { + distribution: { + tlp: { + label: 'GREEN', + }, + }, + }, + vulnerabilities: [ + {}, + { + cve: 'CVE-1900-0001', + metrics: [ + { + content: {}, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], + }).infos.length + ).toBe(0) + }) + + it('does not report unregistered namespaces when the document is labeled TLP:CLEAR', function () { + expect( + informativeTest_6_3_14({ + document: { + distribution: { + tlp: { + label: 'CLEAR', + }, + }, + }, + vulnerabilities: [ + { + cve: 'CVE-1900-0001', + metrics: [ + { + content: { + ssvc_v2: { + id: 'CVE-1900-0001', + schemaVersion: '1-0-1', + selections: [ + { + name: 'Technical Impact', + namespace: 'acme', + values: ['Total'], + version: '1.0.0', + }, + ], + timestamp: '2024-01-24T10:00:00.000Z', + }, + }, + products: ['CSAFPID-9080700'], + }, + ], + }, + ], + }).infos.length + ).toBe(0) + }) + + it('skips selections without namespace and reports the unregistered ones', function () { + expect( informativeTest_6_3_14({ document: { distribution: { @@ -29,7 +93,7 @@ describe('informativeTest_6_3_14', function () { selections: [ { name: 'Technical Impact', - namespace: 'cisa', + namespace: 'acme', values: ['Total'], version: '1.0.0', }, @@ -47,8 +111,7 @@ describe('informativeTest_6_3_14', function () { ], }, ], - }).infos.length, - 1 - ) + }).infos.length + ).toBe(1) }) }) From 855dcb16440faf5211fc0506465c36d27b026157 Mon Sep 17 00:00:00 2001 From: bendo-eXX Date: Thu, 15 Oct 2026 14:25:14 +0200 Subject: [PATCH 5/5] feat(csaf2.1): remove duplicated code --- csaf_2_1/shared/ssvcNamespaces.js | 43 ------------------------------- 1 file changed, 43 deletions(-) diff --git a/csaf_2_1/shared/ssvcNamespaces.js b/csaf_2_1/shared/ssvcNamespaces.js index cbd04708..335769fc 100644 --- a/csaf_2_1/shared/ssvcNamespaces.js +++ b/csaf_2_1/shared/ssvcNamespaces.js @@ -84,49 +84,6 @@ export function isRegisteredSsvcNamespace(namespace) { return registeredSsvcNamespaces.includes(base) } -/** - * Registered SSVC base namespaces for production use, according to the SSVC - * Registered Namespace specification (SSVC-RNS): - * https://certcc.github.io/SSVC/reference/code/namespaces/#registered-namespace - */ -export const registeredSsvcNamespaces = [ - 'ssvc', - 'cvss', - 'cisa', - 'basic', - 'example', - 'test', - 'nist', -] - -/** - * Extracts the base namespace from a full SSVC namespace string. - * Strips everything after the first '#' or '/'. - * @param {string} namespace - * @returns {string} - */ -export function getSsvcBaseNamespace(namespace) { - const hashIdx = namespace.indexOf('#') - const slashIdx = namespace.indexOf('/') - - let endIdx = namespace.length - if (hashIdx !== -1) endIdx = Math.min(endIdx, hashIdx) - if (slashIdx !== -1) endIdx = Math.min(endIdx, slashIdx) - - return namespace.substring(0, endIdx) -} - -/** - * Returns true if the namespace belongs to a registered SSVC base namespace. - * @param {string} namespace - full namespace string - * @returns {boolean} - */ -export function isRegisteredSsvcNamespace(namespace) { - const base = getSsvcBaseNamespace(namespace) - if (base.startsWith('x_')) return false - return registeredSsvcNamespaces.includes(base) -} - /** * Returns true if the given full SSVC namespace string uses an extension. *