Skip to content

Commit 0df6b74

Browse files
authored
feat: try to canonicalize git-URLs (#1120)
Changed: `Factories.FromNodePackageJson.ExternalReferenceFactory.makeVcs()` tries to canonicalize git-URLs fixes #1119 --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent b193f41 commit 0df6b74

File tree

6 files changed

+235
-64
lines changed

6 files changed

+235
-64
lines changed

HISTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ All notable changes to this project will be documented in this file.
66

77
<!-- add unreleased items here -->
88

9+
* Changed
10+
* `Factories.FromNodePackageJson.ExternalReferenceFactory.makeVcs()` tries to canonicalize git-URLs ([#1119] via [#1120])
911
* Fixed
1012
* Improved URL sanitizer (via [#1121])
1113
* Build
1214
* Use _webpack_ `v5.93.0` now, was `v5.92.1` (via [#1122])
1315

16+
[#1119]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1119
17+
[#1120]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1120
1418
[#1121]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1121
1519
[#1122]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1122
1620

src/_helpers/gitUrl.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*!
2+
This file is part of CycloneDX JavaScript Library.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
16+
SPDX-License-Identifier: Apache-2.0
17+
Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
20+
const _sshConnStringRE = /^(?<user>[^@:]+)@(?<host>[^:]+):(?<path>.*)$/
21+
interface _sshConnStringRE_groups {
22+
user: string
23+
host: string
24+
path: string
25+
}
26+
27+
/**
28+
* try to convert git connection string to actual valid URL
29+
*/
30+
export function tryCanonicalizeGitUrl (value: string | undefined): URL | string | undefined {
31+
if (value === undefined || value.length <= 0) {
32+
return undefined
33+
}
34+
35+
try {
36+
return new URL(value)
37+
} catch {
38+
/* pass */
39+
}
40+
41+
const sshGs = _sshConnStringRE.exec(value)?.groups as _sshConnStringRE_groups | undefined
42+
if (sshGs !== undefined) {
43+
try {
44+
// utilize URL so needed chars are properly url-encoded
45+
const u = new URL(`git+ssh://${sshGs.host}`)
46+
u.username = sshGs.user
47+
u.pathname = sshGs.path
48+
return u
49+
} catch {
50+
/* pass */
51+
}
52+
}
53+
54+
return value
55+
}

src/_helpers/packageJson.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ export interface PackageJson {
5757
}
5858
homepage?: string
5959
repository?: string | {
60+
type?: string
6061
url?: string
6162
directory?: string
6263
}
63-
// .. to be continued
64+
// ... to be continued
6465
}

src/factories/fromNodePackageJson.node.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
2828

2929
import type { PackageURL } from 'packageurl-js'
3030

31+
import {tryCanonicalizeGitUrl} from "../_helpers/gitUrl"
3132
import { isNotUndefined } from '../_helpers/notUndefined'
3233
import type { PackageJson } from '../_helpers/packageJson'
3334
import { PackageUrlQualifierNames } from '../_helpers/packageUrl'
@@ -56,20 +57,21 @@ export class ExternalReferenceFactory {
5657
let url
5758
let comment: string | undefined
5859
if (typeof repository === 'object') {
59-
url = repository.url
60+
url = tryCanonicalizeGitUrl(repository.url)
6061
comment = 'as detected from PackageJson property "repository.url"'
61-
if (typeof repository.directory === 'string' && typeof url === 'string' && url.length > 0) {
62-
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
63-
url += '#' + repository.directory
62+
if (typeof repository.directory === 'string' && url instanceof URL) {
63+
// node does not properly encode `#` in the hash ... need to manually esscape
64+
url.hash = repository.directory.replace(/#/g, '%23')
6465
comment += ' and "repository.directory"'
6566
}
6667
} else {
67-
url = repository
68+
url = tryCanonicalizeGitUrl(repository)
6869
comment = 'as detected from PackageJson property "repository"'
6970
}
70-
return typeof url === 'string' && url.length > 0
71-
? new ExternalReference(url, ExternalReferenceType.VCS, { comment })
72-
: undefined
71+
return url === undefined
72+
? undefined
73+
// cast to string so the URL is frozen/immutable
74+
: new ExternalReference(url.toString(), ExternalReferenceType.VCS, { comment })
7375
}
7476

7577
makeHomepage (data: PackageJson): ExternalReference | undefined {

tests/integration/Builders.FromNodePackageJson.ComponentBuilder.test.js

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,15 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
3131
const salt = Math.random()
3232

3333
const extRefFactory = new Factories.FromNodePackageJson.ExternalReferenceFactory()
34-
extRefFactory.makeExternalReferences = () => [`FAKE REFERENCES ${salt}`]
3534
const licenseFactory = new Factories.LicenseFactory()
36-
licenseFactory.makeFromString = (s) => ({ name: `FAKE LICENSE: ${s}` })
37-
licenseFactory.makeDisjunctive = (s) => ({ name: `FAKE DISJUNCTIVE LICENSE: ${s}` })
3835

3936
const sut = new ComponentBuilder(extRefFactory, licenseFactory);
4037

4138
[
4239
[
4340
'minimal',
4441
{ name: 'foo_bar' },
45-
new Models.Component(Enums.ComponentType.Library, 'foo_bar',
46-
{ externalReferences: new Models.ExternalReferenceRepository([`FAKE REFERENCES ${salt}`]) })
42+
new Models.Component(Enums.ComponentType.Library, 'foo_bar')
4743
],
4844
[
4945
'full',
@@ -53,15 +49,19 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
5349
description: `dummy lib ${salt}`,
5450
author: {
5551
name: 'Jane Doe',
56-
url: 'https://acme.org/~jd'
52+
url: 'https://example.com/~jd'
5753
},
5854
license: `dummy license ${salt}`,
5955
licenses: [
6056
{
6157
type: `some license ${salt}`,
62-
url: `https://acme.org/license/${salt}`
58+
url: `https://example.com/license/${salt}`
6359
}
64-
]
60+
],
61+
repository: {
62+
type: "git",
63+
url: "https://github.com/foo/bar.git"
64+
}
6565
// to be continued
6666
},
6767
new Models.Component(
@@ -70,12 +70,20 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
7070
{
7171
author: 'Jane Doe',
7272
description: `dummy lib ${salt}`,
73-
externalReferences: new Models.ExternalReferenceRepository([`FAKE REFERENCES ${salt}`]),
74-
licenses: new Models.LicenseRepository([
75-
{ name: `FAKE LICENSE: dummy license ${salt}` },
76-
{ name: `FAKE DISJUNCTIVE LICENSE: some license ${salt}`, url: `https://acme.org/license/${salt}` }
73+
externalReferences: new Models.ExternalReferenceRepository([
74+
new Models.ExternalReference(
75+
'https://github.com/foo/bar.git',
76+
Enums.ExternalReferenceType.VCS,
77+
{
78+
comment: 'as detected from PackageJson property "repository.url"'
79+
}
80+
)
7781
]),
7882
group: '@foo',
83+
licenses: new Models.LicenseRepository([
84+
new Models.NamedLicense(`dummy license ${salt}`),
85+
new Models.NamedLicense(`some license ${salt}`),
86+
]),
7987
version: `1.33.7-alpha.23.${salt}`
8088
}
8189
)
@@ -89,10 +97,7 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
8997
new Models.Component(
9098
Enums.ComponentType.Library,
9199
'bar/baz',
92-
{
93-
group: '@foo',
94-
externalReferences: new Models.ExternalReferenceRepository([`FAKE REFERENCES ${salt}`])
95-
}
100+
{ group: '@foo' }
96101
)
97102
]
98103
].forEach(([purpose, data, expected]) => {

0 commit comments

Comments
 (0)