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/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/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..5dc00842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "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", "bcp47": "^1.1.2", @@ -190,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", @@ -216,6 +200,15 @@ "node": ">=14" } }, + "node_modules/@secvisogram/is-leap-second": { + "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", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", diff --git a/package.json b/package.json index 2f346ba4..4f6a757e 100644 --- a/package.json +++ b/package.json @@ -118,8 +118,7 @@ "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", "bcp47": "^1.1.2", 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 + }) +})