diff --git a/HISTORY.md b/HISTORY.md index 62b2ff17f..b3180df9b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,15 @@ All notable changes to this project will be documented in this file. +* Added + * `factories.FromNodePackageJson.makeExternalReferences()` supports "dist" field ([#1247] via [#1246]) + * New symbols under `utils.NpmjsUtility` (via [#1246]) + * `defaultRegistryMatcher` + * `parsePackageIntegrity` + +[#1246]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1246 +[#1247]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1247 + ## 8.1.0 -- 2025-06-04 Support for _Node.js_ v24. diff --git a/package.json b/package.json index b89b056c3..0f091fb5c 100644 --- a/package.json +++ b/package.json @@ -149,8 +149,8 @@ "default": "./dist.node/types/index.js" }, "./Utils": { - "types": "./dist.d/utils/index.d.ts", - "default": "./dist.node/utils/index.js" + "types": "./dist.d/utils/index.node.d.ts", + "default": "./dist.node/utils/index.node.js" }, "./Validation": { "types": "./dist.d/validation/index.node.d.ts", diff --git a/src/_helpers/packageJson.ts b/src/_helpers/packageJson.ts index 056b06464..c7bdf6554 100644 --- a/src/_helpers/packageJson.ts +++ b/src/_helpers/packageJson.ts @@ -62,4 +62,5 @@ export interface PackageJson { directory?: string } // ... to be continued + dist?: any // see https://github.com/CycloneDX/cyclonedx-node-npm/issues/1300 } diff --git a/src/factories/fromNodePackageJson.node.ts b/src/factories/fromNodePackageJson.node.ts index 1d32899e8..80073b66d 100644 --- a/src/factories/fromNodePackageJson.node.ts +++ b/src/factories/fromNodePackageJson.node.ts @@ -29,12 +29,15 @@ Copyright (c) OWASP Foundation. All Rights Reserved. import type { PackageURL } from 'packageurl-js' import { PurlQualifierNames } from 'packageurl-js' -import {tryCanonicalizeGitUrl} from "../_helpers/gitUrl" +import { tryCanonicalizeGitUrl } from "../_helpers/gitUrl" import { isNotUndefined } from '../_helpers/notUndefined' import type { PackageJson } from '../_helpers/packageJson' import { ExternalReferenceType } from '../enums/externalReferenceType' +import { HashAlgorithm } from "../enums/hashAlogorithm"; import type { Component } from '../models/component' import { ExternalReference } from '../models/externalReference' +import { HashDictionary } from '../models/hash' +import { defaultRegistryMatcher, parsePackageIntegrity } from '../utils/npmjsUtility.node' import { PackageUrlFactory as PlainPackageUrlFactory } from './packageUrl' /** @@ -47,6 +50,7 @@ export class ExternalReferenceFactory { try { refs.push(this.makeVcs(data)) } catch { /* pass */ } try { refs.push(this.makeHomepage(data)) } catch { /* pass */ } try { refs.push(this.makeIssueTracker(data)) } catch { /* pass */ } + try { refs.push(this.makeDist(data)) } catch { /* pass */ } return refs.filter(isNotUndefined) } @@ -100,13 +104,32 @@ export class ExternalReferenceFactory { ? new ExternalReference(url, ExternalReferenceType.IssueTracker, { comment }) : undefined } -} -/** - * The default repository is `https://registry.npmjs.org`. - * @see {@link https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#npm} - */ -const npmDefaultRepositoryMatcher = /^https?:\/\/registry\.npmjs\.org(:?\/|$)/ + makeDist(data: PackageJson): ExternalReference | undefined { + // "dist" might be used in bundled dependencies' manifests. + // docs: https://blog.npmjs.org/post/172999548390/new-pgp-machinery + /* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- acknowledged */ + const { tarball, integrity, shasum } = data.dist ?? {} + if (typeof tarball === 'string') { + const hashes = new HashDictionary() + let comment = 'as detected from PackageJson property "dist.tarball"' + if (typeof integrity === 'string') { + try { + // actually not the hash of the file, but more of an integrity-check -- lets use it anyway. + // see https://blog.npmjs.org/post/172999548390/new-pgp-machinery + hashes.set(...parsePackageIntegrity(integrity)) + comment += ' and property "dist.integrity"' + } catch { /* pass */ } + } + if (typeof shasum === 'string' && shasum.length === 40) { + hashes.set(HashAlgorithm["SHA-1"], shasum) + comment += ' and property "dist.shasum"' + } + return new ExternalReference(tarball, ExternalReferenceType.Distribution, { hashes, comment }) + } + return undefined + } +} /** * Node-specific PackageUrlFactory. @@ -134,13 +157,13 @@ export class PackageUrlFactory extends PlainPackageUrlFactory<'npm'> { * - "download_url" is stripped, if it is NPM's default registry ("registry.npmjs.org") * - "checksum" is stripped, unless a "download_url" or "vcs_url" is given. */ - #finalizeQualifiers (purl: PackageURL): PackageURL { + #finalizeQualifiers(purl: PackageURL): PackageURL { const qualifiers = new Map(Object.entries(purl.qualifiers ?? {})) const downloadUrl = qualifiers.get(PurlQualifierNames.DownloadUrl) if (downloadUrl !== undefined) { qualifiers.delete(PurlQualifierNames.VcsUrl) - if (npmDefaultRepositoryMatcher.test(downloadUrl)) { + if (defaultRegistryMatcher.test(downloadUrl)) { qualifiers.delete(PurlQualifierNames.DownloadUrl) } } diff --git a/src/index.common.ts b/src/index.common.ts index 1012bbddd..3c9dc9913 100644 --- a/src/index.common.ts +++ b/src/index.common.ts @@ -22,5 +22,4 @@ export * as Models from './models' export * as SPDX from './spdx' export * as Spec from './spec' export * as Types from './types' -export * as Utils from './utils' // do not export the _helpers, they are for internal use only diff --git a/src/index.node.ts b/src/index.node.ts index 7122afd83..cace8af4e 100644 --- a/src/index.node.ts +++ b/src/index.node.ts @@ -28,6 +28,7 @@ export * from './index.common' export * as Builders from './builders/index.node' export * as Factories from './factories/index.node' export * as Serialize from './serialize/index.node' +export * as Utils from './utils/index.node' export * as Validation from './validation/index.node' /** diff --git a/src/index.web.ts b/src/index.web.ts index 48de0863a..f4efdf3d6 100644 --- a/src/index.web.ts +++ b/src/index.web.ts @@ -23,6 +23,7 @@ export * from './index.common' export * as Factories from './factories/index.web' export * as Serialize from './serialize/index.web' +export * as Utils from './utils/index.web' export * as Validation from './validation/index.web' // endregion web-specifics diff --git a/src/utils/index.ts b/src/utils/index.common.ts similarity index 100% rename from src/utils/index.ts rename to src/utils/index.common.ts diff --git a/src/utils/index.node.ts b/src/utils/index.node.ts new file mode 100644 index 000000000..48df09c1f --- /dev/null +++ b/src/utils/index.node.ts @@ -0,0 +1,26 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +export * from './index.common' + +// region node-specifics + +export * as NpmjsUtility from './npmjsUtility.node' + +// endregion node-specifics diff --git a/src/utils/index.web.ts b/src/utils/index.web.ts new file mode 100644 index 000000000..b2beefadf --- /dev/null +++ b/src/utils/index.web.ts @@ -0,0 +1,26 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +export * from './index.common' + +// region web-specifics + +// ... nothing yet + +// endregion web-specifics diff --git a/src/utils/npmjsUtility.node.ts b/src/utils/npmjsUtility.node.ts new file mode 100644 index 000000000..14208cf0b --- /dev/null +++ b/src/utils/npmjsUtility.node.ts @@ -0,0 +1,89 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +import {HashAlgorithm} from '../enums/hashAlogorithm' + +/** + * See {@link https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json#packages | package lock docs} for "integrity". + * See {@link https://blog.npmjs.org/post/172999548390/new-pgp-machinery | new pgp machinery} for "integrity". + * + * integrity: A sha512 or sha1 [Standard Subresource Integrity](https://w3c.github.io/webappsec/specs/subresourceintegrity/) string for the artifact that was unpacked in this location. + */ +const integrityRE: ReadonlyMap = new Map([ + // !!! this list is pre-sorted, starting with most-common usage. + + /* base64 alphabet: `A-Za-z0-9+/` and `=` for padding + * SHA-512 => base64 over 512 bit => 86 chars + 2 chars padding. + * examples: + * - sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== + * - sha512-DXUS22Y57/LAFSg3x7Vi6RNAuLpTXwxB9S2nIA7msBb/Zt8p7XqMwdpdc1IU7CkOQUPgAqR5fWvxuKCbneKGmA== + * - sha512-5BejraMXMC+2UjefDvrH0Fo/eLwZRV6859SXRg+FgbhA0R0l6lDqDGAQYhKbXhPN2ofk2kY5sgGyLNL907UXpA== + */ + [HashAlgorithm['SHA-512'], /^sha512-([a-z0-9+/]{86}==)$/i], + + /* base64 alphabet: `A-Za-z0-9+/` and `=` for padding + * SHA-1 => base64 over 160 bit => 27 chars + 1 chars padding. + * examples: + * - sha1-aSbRsZT7xze47tUTdW3i/Np+pAg= + * - sha1-Kq5sNclPz7QV2+lfQIuc6R7oRu0= + * - sha1-XV8g50dxuFICXD7bZslGLuuRPQM= + */ + [HashAlgorithm['SHA-1'], /^sha1-([a-z0-9+/]{27}=)$/i], + + /* base64 alphabet: `A-Za-z0-9+/` and `=` for padding + * SHA-256 => base64 over 256 bit => 43 chars + 1 chars padding. + * examples: + * - sha256-jxzgcB+8dLn7Cjjyg7stGWMftZf6rbdvgoE85TOzmT4= + * - sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= + * - sha256-+8Gp+Fjqnhd5FpZL2Iw9N7kaHoRBJ2XimVB3fyZcS3U= + */ + [HashAlgorithm['SHA-256'], /^sha256-([a-z0-9+/]{43}=)$/i], + + /* base64 alphabet: `A-Za-z0-9+/` and `=` for padding + * SHA-384 => base64 over 384 bit => 64 chars + 0 chars padding. + * example: + * - sha384-aDkxLz2zQ0dwcNPAsr7NQXs1cVTUh5TQHXjPtGF+1auBmne2gy9lQt0Yu3OBMe9+ + * - sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC + * - sha384-/b2OdaZ/KfcBpOBAOF4uI5hjA+oQI5IRr5B/y7g1eLPkF8txzmRu/QgZ3YwIjeG9 + */ + [HashAlgorithm['SHA-384'], /^sha384-([a-z0-9+/]{64})$/i] +]) + +/** + * @throws {@link RangeError} if value is unparsable + */ +export function parsePackageIntegrity (integrity: string): [HashAlgorithm, string] { + for (const [hashAlgorithm, hashRE] of integrityRE) { + const hashMatchBase64 = hashRE.exec(integrity) ?? [] + if (hashMatchBase64.length === 2) { + return [ + hashAlgorithm, + Buffer.from(hashMatchBase64[1], 'base64').toString('hex') + ] + } + } + throw new RangeError('unparsable value') +} + + +/** + * The default registry is `https://registry.npmjs.org`. + * @see {@link https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#npm} + */ +export const defaultRegistryMatcher = /^https?:\/\/registry\.npmjs\.org(:?\/|$)/ diff --git a/tests/integration/Factories.FromNodePackageJson.ExternalReferenceFactory.test.js b/tests/integration/Factories.FromNodePackageJson.ExternalReferenceFactory.test.js index e2d53c252..f57be4b42 100644 --- a/tests/integration/Factories.FromNodePackageJson.ExternalReferenceFactory.test.js +++ b/tests/integration/Factories.FromNodePackageJson.ExternalReferenceFactory.test.js @@ -22,8 +22,8 @@ const assert = require('node:assert') const { suite, test } = require('mocha') const { - Enums: { ExternalReferenceType }, - Models: { ExternalReference }, + Enums: { ExternalReferenceType, HashAlgorithm }, + Models: { ExternalReference, HashDictionary }, Factories: { FromNodePackageJson: { ExternalReferenceFactory } } } = require('../../') @@ -39,7 +39,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { homepage: 'https://example.com' } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('is empty string', () => { const data = { homepage: '' } @@ -62,7 +62,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { bugs: 'https://example.com' } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('is empty string', () => { const data = { bugs: '' } @@ -84,7 +84,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { bugs: { url: 'https://example.com' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('is empty string', () => { const data = { bugs: { url: '' } } @@ -97,6 +97,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () assert.strictEqual(actual.length, 0) }) }) + suite('from "repository"', () => { test('non-empty string', () => { const expected = [new ExternalReference( @@ -106,7 +107,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: '../foo/bar' } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('implicit-git-url', () => { const expected = [new ExternalReference( @@ -116,7 +117,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: 'git@example.com:foo/bar' } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('explicit-git-url', () => { const expected = [new ExternalReference( @@ -126,7 +127,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: 'git+https://example.com/dings.git' } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('implicit-svn-url', () => { const expected = [new ExternalReference( @@ -136,19 +137,19 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: 'svn://example.com/foo/trunk' } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('empty string', () => { const expected = [] const data = { repository: '' } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('undefined', () => { const expected = [] const data = { } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) }) suite('from "repository.url"', () => { @@ -160,7 +161,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: '../foo/bar' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('implicit-git-url', () => { const expected = [new ExternalReference( @@ -170,7 +171,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'git@example.com:foo/bar' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('explicit-git-url', () => { const expected = [new ExternalReference( @@ -180,7 +181,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'git+https://example.com/dings.git' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('implicit-svn-url', () => { const expected = [new ExternalReference( @@ -190,19 +191,19 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'svn://example.com/foo/trunk' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('empty string', () => { const expected = [] const data = { repository: { url: '' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('undefined', () => { const expected = [] const data = { repository: { } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) }) suite('from "repository.directory"', () => { @@ -214,7 +215,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: '../foo/bar', directory: 'some/other#23/dir#42' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('implicit-git-url', () => { const expected = [new ExternalReference( @@ -224,7 +225,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'git@example.com:foo/bar', directory: 'some/other#23/dir#42' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('explicit-git-url', () => { const expected = [new ExternalReference( @@ -234,7 +235,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'git+https://example.com/dings.git', directory: 'some/other#23/dir#42' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('implicit-svn-url', () => { const expected = [new ExternalReference( @@ -244,7 +245,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'svn://example.com/foo/trunk', directory: 'some/other#23/dir#42' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('empty string', () => { const expected = [new ExternalReference( @@ -254,7 +255,7 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'http://example.com/foo', directory: '' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) }) test('undefined', () => { const expected = [new ExternalReference( @@ -264,7 +265,59 @@ suite('integration: Factories.FromNodePackageJson.ExternalReferenceFactory', () )] const data = { repository: { url: 'http://example.com/foo' } } const actual = sut.makeExternalReferences(data) - assert.deepEqual(actual, expected) + assert.deepStrictEqual(actual, expected) + }) + }) + + suite('from "dist"', () => { + test('with tarball', () => { + const expected = [new ExternalReference( + 'https://example.com/foo.tgz', + ExternalReferenceType.Distribution, + { comment: 'as detected from PackageJson property "dist.tarball"' } + )] + const data = { dist: { tarball: 'https://example.com/foo.tgz' } } + const actual = sut.makeExternalReferences(data) + assert.deepStrictEqual(actual, expected) + }) + test('with tarball and integrity', () => { + const expected = [new ExternalReference( + 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.3.tgz', + ExternalReferenceType.Distribution, + { + hashes: new HashDictionary([[HashAlgorithm['SHA-512'], 'b0572e8afb0367df5f6344dbbee442e820d707caffca569f8c900c9db485d32e0430cd7fd43b50a38d06d962b3d6b05bca2cf848b01cdd66bac99c82e1748639']]), + comment: 'as detected from PackageJson property "dist.tarball" and property "dist.integrity"' + } + )] + const data = { dist: { tarball: 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.3.tgz', integrity: 'sha512-sFcuivsDZ99fY0TbvuRC6CDXB8r/ylafjJAMnbSF0y4EMM1/1DtQo40G2WKz1rBbyiz4SLAc3Wa6yZyC4XSGOQ==' } } + const actual = sut.makeExternalReferences(data) + assert.deepStrictEqual(actual, expected) + }) + test('with tarball and shasum', () => { + const expected = [new ExternalReference( + 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.3.tgz', + ExternalReferenceType.Distribution, + { + hashes: new HashDictionary([[HashAlgorithm['SHA-1'], 'c305f0113d81d880f846d84f80c7f3237f197bab']]), + comment: 'as detected from PackageJson property "dist.tarball" and property "dist.shasum"' + } + )] + const data = { dist: { tarball: 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.3.tgz', shasum: 'c305f0113d81d880f846d84f80c7f3237f197bab' } } + const actual = sut.makeExternalReferences(data) + assert.deepStrictEqual(actual, expected) + }) + test('with tarball and integrity and shasum', () => { + const expected = [new ExternalReference( + 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.3.tgz', + ExternalReferenceType.Distribution, + { + hashes: new HashDictionary([[HashAlgorithm['SHA-1'], 'c305f0113d81d880f846d84f80c7f3237f197bab'], [HashAlgorithm['SHA-512'], 'b0572e8afb0367df5f6344dbbee442e820d707caffca569f8c900c9db485d32e0430cd7fd43b50a38d06d962b3d6b05bca2cf848b01cdd66bac99c82e1748639']]), + comment: 'as detected from PackageJson property "dist.tarball" and property "dist.integrity" and property "dist.shasum"' + } + )] + const data = { dist: { tarball: 'https://registry.npmjs.org/light-cycle/-/light-cycle-1.4.3.tgz', integrity: 'sha512-sFcuivsDZ99fY0TbvuRC6CDXB8r/ylafjJAMnbSF0y4EMM1/1DtQo40G2WKz1rBbyiz4SLAc3Wa6yZyC4XSGOQ==', shasum: 'c305f0113d81d880f846d84f80c7f3237f197bab' } } + const actual = sut.makeExternalReferences(data) + assert.deepStrictEqual(actual, expected) }) }) }) diff --git a/tests/unit/Factories.FromNodePackageJson.PackageUrlFactory.js b/tests/unit/Factories.FromNodePackageJson.PackageUrlFactory.js index 411959584..a91c4b1c1 100644 --- a/tests/unit/Factories.FromNodePackageJson.PackageUrlFactory.js +++ b/tests/unit/Factories.FromNodePackageJson.PackageUrlFactory.js @@ -36,7 +36,7 @@ suite('unit: Factories.FromNodePackageJson.PackageUrlFactory', () => { assert.deepEqual(actual, 'TODO') }) - test('strips default repo', () => { + test('strips default registry from qualifiers', () => { // see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#npm const component = new Component(ComponentType.Library, 'testing', { externalReferences: new ExternalReferenceRepository([ @@ -58,7 +58,7 @@ suite('unit: Factories.FromNodePackageJson.PackageUrlFactory', () => { }) }) - test('dont strip BA repo', () => { + test('dont strip BA registry from qualifiers', () => { // regression test for https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1073 const component = new Component(ComponentType.Library, 'testing', { externalReferences: new ExternalReferenceRepository([ diff --git a/tests/unit/Utils.NpmjsUtility.spec.js b/tests/unit/Utils.NpmjsUtility.spec.js new file mode 100644 index 000000000..b05cca8ba --- /dev/null +++ b/tests/unit/Utils.NpmjsUtility.spec.js @@ -0,0 +1,95 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +const assert = require('node:assert') + +const { suite, test } = require('mocha') + +const { + Enums: { + HashAlgorithm + }, + Utils: { + NpmjsUtility + } +} = require('../../') + +suite('unit: Utils.NpmjsUtility.defaultRegistryMatcher', () => { + test('matches pure domain', () => { + const actual = NpmjsUtility.defaultRegistryMatcher.test('https://registry.npmjs.org') + assert.strictEqual(actual, true) + }) + test('matches with path', () => { + const actual = NpmjsUtility.defaultRegistryMatcher.test('https://registry.npmjs.org/foo/bar') + assert.strictEqual(actual, true) + }) + suite('not match unexpected', () => { + for (const c of [ + 'https://my-own=registry.local', + 'https://registry.npmjs.org.uk', + 'https://registry.npmjs.org.uk/foo/bar' + ]) { + test(c, () => { + const actual = NpmjsUtility.defaultRegistryMatcher.test(c) + assert.strictEqual(actual, false) + }) + } + }) +}) + +suite('unit: Utils.NpmjsUtility.parsePackageIntegrity', () => { + suite('as expected', () => { + for (const [c, ...expected] of [ + ['sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==', + HashAlgorithm['SHA-512'], + 'cef8fae53905788b778ba6a3e5b22f243ce38d0406b38a3fb0da1ece0d45d6461aa7d36eda3714769c334522531cc3331b41fb6d9927e2b350a489a1bb1597d8' + ], + ['sha1-Kq5sNclPz7QV2+lfQIuc6R7oRu0=', + HashAlgorithm['SHA-1'], + '2aae6c35c94fcfb415dbe95f408b9ce91ee846ed' + ], + ['sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=', + HashAlgorithm['SHA-256'], + 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + ], + ['sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC', + HashAlgorithm['SHA-384'], + 'a2a56e01f5d129aa7b7dd81c098e6eca433af91f46a90f0afeec72f6bc7b1cd42519897590fcd0868d70c7827063cc02' + ], + ]) { + test(c, () => { + const actual = NpmjsUtility.parsePackageIntegrity(c) + assert.deepStrictEqual(actual, expected) + }) + } + }) + suite('fails', () => { + for (const c of [ + 'sha1-Kq5sNclPz7QV2+lfQIuc6R7oRu0', // missing character + 'sha1-Kq5sNclPz7QV2+lfQIuc6R7oRu0==', // additional character + 'sha512-Kq5sNclPz7QV2+lfQIuc6R7oRu0=', // alg and hash dont match + ]) { + test(c, () => { + assert.throws(() => { + NpmjsUtility.parsePackageIntegrity(c) + }) + }) + } + }) +})