From 5ea3898e2768cf04e1d3eacb7daae74f04e846ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 24 Jun 2026 10:08:22 +0200 Subject: [PATCH 1/4] fix: adapt date comparison logic to support leap seconds This uses the new `@secvisogram/is-leap-second` library now. --- lib/shared/dateHelper.js | 29 +++++++++++++++-------------- package-lock.json | 7 +++++++ package.json | 1 + 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/shared/dateHelper.js b/lib/shared/dateHelper.js index bde93512..ca082f20 100644 --- a/lib/shared/dateHelper.js +++ b/lib/shared/dateHelper.js @@ -1,28 +1,29 @@ -import { Duration, ZonedDateTime } from '@js-joda/core' +import { toTime } from '@secvisogram/is-leap-second' /** - * compare ZonedDateTimes from js-joda - * returns a negative number if a is less than b, positive if a is greater than b, and zero if they are equal. - * This function also returns 0 if one of the given values could not be parsed. - * - * @param {ZonedDateTime | string} a - * @param {ZonedDateTime | string} b - * @returns {0|1|-1} + * Compares two date-time strings using `toTime` from `@secvisogram/is-leap-second`. + * Returns a negative number if `a` is less than `b`, a positive number if `a` is greater + * than `b`, and zero if they are equal. Also returns 0 if either value cannot be parsed. + * Follows the comparator convention used by `Array.prototype.sort`. * + * @param {string} a - The first date-time string to compare. + * @param {string} b - The second date-time string to compare. + * @returns {0|1|-1} Negative if `a < b`, positive if `a > b`, zero if equal or unparseable. */ export const compareZonedDateTimes = (a, b) => { - // catch js-joda exception if a or b can't be parsed + // catch TypeError exception if a or b can't be parsed try { - const date1 = a instanceof ZonedDateTime ? a : ZonedDateTime.parse(a) - const date2 = b instanceof ZonedDateTime ? b : ZonedDateTime.parse(b) - const duration = Duration.between(date1, date2) + const date1 = toTime(a) + const date2 = toTime(b) + if (date1 === null || date2 === null) return 0 + const duration = date2 - date1 // return number based on js sort function // > negative if a is less than b, positive if a is greater than b, and zero if they are equal. // [Sort Documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#comparefn) - if (duration.isZero()) { + if (duration === 0n) { return 0 - } else if (duration.isNegative()) { + } else if (duration < 0n) { return 1 } else { return -1 diff --git a/package-lock.json b/package-lock.json index 09f766ed..3cb1fd05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@js-joda/core": "^5.6.1", "@js-joda/timezone": "^2.18.2", + "@secvisogram/is-leap-second": "^0.1.0", "ajv": "^8.11.2", "ajv-formats": "^3.0.1", "bcp47": "^1.1.2", @@ -216,6 +217,12 @@ "node": ">=14" } }, + "node_modules/@secvisogram/is-leap-second": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@secvisogram/is-leap-second/-/is-leap-second-0.1.0.tgz", + "integrity": "sha512-Z+wH2KgDwMLqLVQF1Nb8P0EXGVByaFWe3st40L9Lj+EOIumaQbQ3r2ZJbuDSVz6fyxyMOe+PxDoD7v5j2vnPAg==", + "license": "Apache-2.0" + }, "node_modules/@types/chai": { "version": "4.3.20", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", diff --git a/package.json b/package.json index 2f346ba4..8e0e2a3b 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "dependencies": { "@js-joda/core": "^5.6.1", "@js-joda/timezone": "^2.18.2", + "@secvisogram/is-leap-second": "^0.1.0", "ajv": "^8.11.2", "ajv-formats": "^3.0.1", "bcp47": "^1.1.2", From f207eda09037543b98834f9da70dba9e9f5a566e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 1 Jul 2026 16:09:20 +0200 Subject: [PATCH 2/4] fix: adapt date check logic to check for leap seconds --- lib/shared/csafAjv.js | 8 ++++++++ tests/schemaTests.js | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/schemaTests.js diff --git a/lib/shared/csafAjv.js b/lib/shared/csafAjv.js index 712d853d..03b113d7 100644 --- a/lib/shared/csafAjv.js +++ b/lib/shared/csafAjv.js @@ -3,6 +3,7 @@ import { Ajv2020 } from 'ajv/dist/2020.js' import cvss_v2_0 from '../../schemas/cvss-v2.0.js' import cvss_v3_0 from '../../schemas/cvss-v3.0.js' import cvss_v3_1 from '../../schemas/cvss-v3.1.js' +import { toTime } from '@secvisogram/is-leap-second' const csafAjv = new Ajv2020({ strict: false, allErrors: true }) addFormats.default(csafAjv) @@ -11,3 +12,10 @@ csafAjv.addSchema(cvss_v3_0, 'https://www.first.org/cvss/cvss-v3.0.json') csafAjv.addSchema(cvss_v3_1, 'https://www.first.org/cvss/cvss-v3.1.json') export default csafAjv + +csafAjv.addFormat('date-time', { + type: 'string', + validate: (v) => { + return toTime(v) !== null + }, +}) diff --git a/tests/schemaTests.js b/tests/schemaTests.js new file mode 100644 index 00000000..dfa78122 --- /dev/null +++ b/tests/schemaTests.js @@ -0,0 +1,48 @@ +import { expect } from 'chai' +import { csaf_2_0 } from '../schemaTests.js' +import minimalCSAFBaseDoc from './shared/minimalCSAFBaseDoc.js' + +describe('Schema test csaf_2_0', function () { + // Scenario from https://github.com/secvisogram/secvisogram/issues/778: + // Dates with seconds=60 that are not real leap seconds should be rejected + // by the date-time format validation in the csaf_2_0 schema validator. + it('should reject a document with a non-leap-second date with seconds=60', function () { + const result = csaf_2_0({ + ...minimalCSAFBaseDoc, + document: { + ...minimalCSAFBaseDoc.document, + tracking: { + ...minimalCSAFBaseDoc.document.tracking, + revision_history: [ + { + date: '2026-12-31T23:59:60Z', + number: '1', + summary: 'Summary', + }, + ], + }, + }, + }) + expect(result.isValid).to.be.false + }) + + it('should accept a document with a real historical leap second date', function () { + const result = csaf_2_0({ + ...minimalCSAFBaseDoc, + document: { + ...minimalCSAFBaseDoc.document, + tracking: { + ...minimalCSAFBaseDoc.document.tracking, + revision_history: [ + { + date: '2016-12-31T23:59:60Z', + number: '1', + summary: 'Summary', + }, + ], + }, + }, + }) + expect(result.isValid).to.be.true + }) +}) From fd4e81b3b59df1961b8eb4d480fefbe00ce6a673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Wed, 1 Jul 2026 16:09:41 +0200 Subject: [PATCH 3/4] chore: update is-leap-second version range --- package-lock.json | 13 ++++++++----- package.json | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3cb1fd05..7fd8d700 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@js-joda/core": "^5.6.1", "@js-joda/timezone": "^2.18.2", - "@secvisogram/is-leap-second": "^0.1.0", + "@secvisogram/is-leap-second": "^1.0.0", "ajv": "^8.11.2", "ajv-formats": "^3.0.1", "bcp47": "^1.1.2", @@ -218,10 +218,13 @@ } }, "node_modules/@secvisogram/is-leap-second": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@secvisogram/is-leap-second/-/is-leap-second-0.1.0.tgz", - "integrity": "sha512-Z+wH2KgDwMLqLVQF1Nb8P0EXGVByaFWe3st40L9Lj+EOIumaQbQ3r2ZJbuDSVz6fyxyMOe+PxDoD7v5j2vnPAg==", - "license": "Apache-2.0" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@secvisogram/is-leap-second/-/is-leap-second-1.0.0.tgz", + "integrity": "sha512-2UiPgPJDGvv/MrsHE8rm4R9d6Rtx8uCyX0Fp6Rdo+QH34p7aA5qsI7T4bOZQ+g7bpqSD3BUWYs7QqBQh1e+wsA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/@types/chai": { "version": "4.3.20", diff --git a/package.json b/package.json index 8e0e2a3b..0331c7b6 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "dependencies": { "@js-joda/core": "^5.6.1", "@js-joda/timezone": "^2.18.2", - "@secvisogram/is-leap-second": "^0.1.0", + "@secvisogram/is-leap-second": "^1.0.0", "ajv": "^8.11.2", "ajv-formats": "^3.0.1", "bcp47": "^1.1.2", From da5766af02ec88fea92e1d3d662d5101bc0c0c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Burgd=C3=B6rfer?= Date: Thu, 2 Jul 2026 09:37:58 +0200 Subject: [PATCH 4/4] chore: remove js-joda dependency --- README.md | 2 -- package-lock.json | 17 ----------------- package.json | 2 -- 3 files changed, 21 deletions(-) diff --git a/README.md b/README.md index afdaa0c6..2910de45 100644 --- a/README.md +++ b/README.md @@ -617,7 +617,5 @@ For the complete list of dependencies please take a look at [package.json](https - [packageurl-js](https://github.com/package-url/packageurl-js) - [semver](https://github.com/npm/node-semver) - [undici](https://undici.nodejs.org) -- [@js-joda/core](https://js-joda.github.io/js-joda/) -- [@js-joda/timezone](https://js-joda.github.io/js-joda/) [(back to top)](#bsi-csaf-validator-lib) diff --git a/package-lock.json b/package-lock.json index 7fd8d700..5dc00842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,6 @@ "version": "2.0.26", "license": "MIT", "dependencies": { - "@js-joda/core": "^5.6.1", - "@js-joda/timezone": "^2.18.2", "@secvisogram/is-leap-second": "^1.0.0", "ajv": "^8.11.2", "ajv-formats": "^3.0.1", @@ -191,21 +189,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@js-joda/core": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", - "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", - "license": "BSD-3-Clause" - }, - "node_modules/@js-joda/timezone": { - "version": "2.25.1", - "resolved": "https://registry.npmjs.org/@js-joda/timezone/-/timezone-2.25.1.tgz", - "integrity": "sha512-s79ts8bXrWqM9dIBKc0AdgGuAUFpu9gmzYhOCPHJlks/Sf7FSbJHRauWlFYUwjSTZevimqthEvJycrwrVz5m4g==", - "license": "BSD-3-Clause", - "peerDependencies": { - "@js-joda/core": ">=5.7.0" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", diff --git a/package.json b/package.json index 0331c7b6..4f6a757e 100644 --- a/package.json +++ b/package.json @@ -118,8 +118,6 @@ "access": "public" }, "dependencies": { - "@js-joda/core": "^5.6.1", - "@js-joda/timezone": "^2.18.2", "@secvisogram/is-leap-second": "^1.0.0", "ajv": "^8.11.2", "ajv-formats": "^3.0.1",