Skip to content

Commit

Permalink
feat: try to canonicalize git-URLs (#1120)
Browse files Browse the repository at this point in the history
Changed: `Factories.FromNodePackageJson.ExternalReferenceFactory.makeVcs()` tries to canonicalize git-URLs

fixes #1119

---------

Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck authored Jul 15, 2024
1 parent b193f41 commit 0df6b74
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 64 deletions.
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ All notable changes to this project will be documented in this file.

<!-- add unreleased items here -->

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

[#1119]: https://github.com/CycloneDX/cyclonedx-javascript-library/issues/1119
[#1120]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1120
[#1121]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1121
[#1122]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1122

Expand Down
55 changes: 55 additions & 0 deletions src/_helpers/gitUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*!
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 _sshConnStringRE = /^(?<user>[^@:]+)@(?<host>[^:]+):(?<path>.*)$/
interface _sshConnStringRE_groups {
user: string
host: string
path: string
}

/**
* try to convert git connection string to actual valid URL
*/
export function tryCanonicalizeGitUrl (value: string | undefined): URL | string | undefined {
if (value === undefined || value.length <= 0) {
return undefined
}

try {
return new URL(value)
} catch {
/* pass */
}

const sshGs = _sshConnStringRE.exec(value)?.groups as _sshConnStringRE_groups | undefined
if (sshGs !== undefined) {
try {
// utilize URL so needed chars are properly url-encoded
const u = new URL(`git+ssh://${sshGs.host}`)
u.username = sshGs.user
u.pathname = sshGs.path
return u
} catch {
/* pass */
}
}

return value
}
3 changes: 2 additions & 1 deletion src/_helpers/packageJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ export interface PackageJson {
}
homepage?: string
repository?: string | {
type?: string
url?: string
directory?: string
}
// .. to be continued
// ... to be continued
}
18 changes: 10 additions & 8 deletions src/factories/fromNodePackageJson.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved.

import type { PackageURL } from 'packageurl-js'

import {tryCanonicalizeGitUrl} from "../_helpers/gitUrl"
import { isNotUndefined } from '../_helpers/notUndefined'
import type { PackageJson } from '../_helpers/packageJson'
import { PackageUrlQualifierNames } from '../_helpers/packageUrl'
Expand Down Expand Up @@ -56,20 +57,21 @@ export class ExternalReferenceFactory {
let url
let comment: string | undefined
if (typeof repository === 'object') {
url = repository.url
url = tryCanonicalizeGitUrl(repository.url)
comment = 'as detected from PackageJson property "repository.url"'
if (typeof repository.directory === 'string' && typeof url === 'string' && url.length > 0) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
url += '#' + repository.directory
if (typeof repository.directory === 'string' && url instanceof URL) {
// node does not properly encode `#` in the hash ... need to manually esscape
url.hash = repository.directory.replace(/#/g, '%23')
comment += ' and "repository.directory"'
}
} else {
url = repository
url = tryCanonicalizeGitUrl(repository)
comment = 'as detected from PackageJson property "repository"'
}
return typeof url === 'string' && url.length > 0
? new ExternalReference(url, ExternalReferenceType.VCS, { comment })
: undefined
return url === undefined
? undefined
// cast to string so the URL is frozen/immutable
: new ExternalReference(url.toString(), ExternalReferenceType.VCS, { comment })
}

makeHomepage (data: PackageJson): ExternalReference | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,15 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
const salt = Math.random()

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

const sut = new ComponentBuilder(extRefFactory, licenseFactory);

[
[
'minimal',
{ name: 'foo_bar' },
new Models.Component(Enums.ComponentType.Library, 'foo_bar',
{ externalReferences: new Models.ExternalReferenceRepository([`FAKE REFERENCES ${salt}`]) })
new Models.Component(Enums.ComponentType.Library, 'foo_bar')
],
[
'full',
Expand All @@ -53,15 +49,19 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
description: `dummy lib ${salt}`,
author: {
name: 'Jane Doe',
url: 'https://acme.org/~jd'
url: 'https://example.com/~jd'
},
license: `dummy license ${salt}`,
licenses: [
{
type: `some license ${salt}`,
url: `https://acme.org/license/${salt}`
url: `https://example.com/license/${salt}`
}
]
],
repository: {
type: "git",
url: "https://github.com/foo/bar.git"
}
// to be continued
},
new Models.Component(
Expand All @@ -70,12 +70,20 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
{
author: 'Jane Doe',
description: `dummy lib ${salt}`,
externalReferences: new Models.ExternalReferenceRepository([`FAKE REFERENCES ${salt}`]),
licenses: new Models.LicenseRepository([
{ name: `FAKE LICENSE: dummy license ${salt}` },
{ name: `FAKE DISJUNCTIVE LICENSE: some license ${salt}`, url: `https://acme.org/license/${salt}` }
externalReferences: new Models.ExternalReferenceRepository([
new Models.ExternalReference(
'https://github.com/foo/bar.git',
Enums.ExternalReferenceType.VCS,
{
comment: 'as detected from PackageJson property "repository.url"'
}
)
]),
group: '@foo',
licenses: new Models.LicenseRepository([
new Models.NamedLicense(`dummy license ${salt}`),
new Models.NamedLicense(`some license ${salt}`),
]),
version: `1.33.7-alpha.23.${salt}`
}
)
Expand All @@ -89,10 +97,7 @@ suite('Builders.FromNodePackageJson.ComponentBuilder', () => {
new Models.Component(
Enums.ComponentType.Library,
'bar/baz',
{
group: '@foo',
externalReferences: new Models.ExternalReferenceRepository([`FAKE REFERENCES ${salt}`])
}
{ group: '@foo' }
)
]
].forEach(([purpose, data, expected]) => {
Expand Down
Loading

0 comments on commit 0df6b74

Please sign in to comment.