Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ All notable changes to this project will be documented in this file.

<!-- unreleased changes go here -->


* Changed
* Utilizes license file gatherer of `@cyclonedx/cyclonedx-library`, previously used own implementation (via [#1303])
* Runtime Dependencies
* Raised `@cyclonedx/cyclonedx-library@^8.2.0`, was `@^8.0.0` (via [#1301])
* Raised `@cyclonedx/cyclonedx-library@^8.4.0`, was `@^8.0.0` (via [#1301], [#1303])
* Raised `commander@^14.0.0`, was `@^13.1.0` (via [#1297])

[#1297]: https://github.com/CycloneDX/cyclonedx-node-npm/pull/1297
[#1301]: https://github.com/CycloneDX/cyclonedx-node-npm/pull/1301
[#1303]: https://github.com/CycloneDX/cyclonedx-node-npm/pull/1303

## 3.0.0 - 2025-04-08

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
}
],
"dependencies": {
"@cyclonedx/cyclonedx-library": "^8.2.0",
"@cyclonedx/cyclonedx-library": "^8.4.0",
"commander": "^14.0.0",
"normalize-package-data": "^7.0.0",
"xmlbuilder2": "^3.0.2"
Expand Down
44 changes: 0 additions & 44 deletions src/_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import { readFileSync, writeSync } from 'node:fs'
import { extname, parse } from 'node:path'

export const structuredClonePolyfill: <T>(value: T) => T = typeof structuredClone === 'function'
? structuredClone
Expand Down Expand Up @@ -68,49 +67,6 @@ export function tryRemoveSecretsFromUrl (url: string): string {
}
}

// region MIME

export type MimeType = string

const MIME_TEXT_PLAIN: MimeType = 'text/plain'

const MAP_TEXT_EXTENSION_MIME: Readonly<Record<string, MimeType>> = {
'': MIME_TEXT_PLAIN,
// https://www.iana.org/assignments/media-types/media-types.xhtml
'.csv': 'text/csv',
'.htm': 'text/html',
'.html': 'text/html',
'.md': 'text/markdown',
'.txt': MIME_TEXT_PLAIN,
'.rst': 'text/prs.fallenstein.rst',
'.xml': 'text/xml', // not `application/xml` -- our scope is text!
// add more mime types above this line. pull-requests welcome!
// license-specific files
'.license': MIME_TEXT_PLAIN,
'.licence': MIME_TEXT_PLAIN
} as const

export function getMimeForTextFile (filename: string): MimeType | undefined {
return MAP_TEXT_EXTENSION_MIME[extname(filename).toLowerCase()]
}

const LICENSE_FILENAME_BASE = new Set(['licence', 'license'])
const LICENSE_FILENAME_EXT = new Set([
'.apache',
'.bsd',
'.gpl',
'.mit'
])

export function getMimeForLicenseFile (filename: string): MimeType | undefined {
const { name, ext } = parse(filename.toLowerCase())
return LICENSE_FILENAME_BASE.has(name) && LICENSE_FILENAME_EXT.has(ext)
? MIME_TEXT_PLAIN
: MAP_TEXT_EXTENSION_MIME[ext]
}

// endregion MIME

// region version compare

export type Version = readonly number[]
Expand Down
61 changes: 20 additions & 41 deletions src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ Copyright (c) OWASP Foundation. All Rights Reserved.

/* eslint-disable max-lines -- ack */

import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { existsSync } from 'node:fs'
import path from 'node:path'

import { type Builders, Enums, type Factories, Models, Utils } from '@cyclonedx/cyclonedx-library'
import normalizePackageJson from 'normalize-package-data'

import {
getMimeForLicenseFile,
isString,
loadJsonFile,
structuredClonePolyfill,
Expand Down Expand Up @@ -57,6 +56,7 @@ type AllComponents = Map<cPath, Models.Component>
export class BomBuilder {
npmRunner: NpmRunner
componentBuilder: Builders.FromNodePackageJson.ComponentBuilder
leGatherer: Utils.LicenseUtility.LicenseEvidenceGatherer
treeBuilder: TreeBuilder
purlFactory: Factories.FromNodePackageJson.PackageUrlFactory

Expand All @@ -81,13 +81,15 @@ export class BomBuilder {
componentBuilder: BomBuilder['componentBuilder'],
treeBuilder: BomBuilder['treeBuilder'],
purlFactory: BomBuilder['purlFactory'],
leFetcher: BomBuilder['leGatherer'],
options: BomBuilderOptions,
console_: BomBuilder['console']
) {
this.npmRunner = npmRunner
this.componentBuilder = componentBuilder
this.treeBuilder = treeBuilder
this.purlFactory = purlFactory
this.leGatherer = leFetcher

this.ignoreNpmErrors = options.ignoreNpmErrors ?? false
this.metaComponentType = options.metaComponentType ?? Enums.ComponentType.Library
Expand Down Expand Up @@ -420,11 +422,9 @@ export class BomBuilder {
component.evidence = new Models.ComponentEvidence()
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- false-positive */
for (const license of this.fetchLicenseEvidence(data.path)) {
if (license != null) {
// only create a evidence if a license attachment is found
component.evidence ??= new Models.ComponentEvidence();
component.evidence.licenses.add(license)
}
}
}
}
Expand Down Expand Up @@ -575,46 +575,25 @@ export class BomBuilder {
}
}

readonly #LICENSE_FILENAME_PATTERN = /^(?:UN)?LICEN[CS]E|.\.LICEN[CS]E$|^NOTICE$/i

private * fetchLicenseEvidence (dirPath: string): Generator<Models.License | null, void, void> {
const files = readdirSync(dirPath)
for (const file of files) {
if (!this.#LICENSE_FILENAME_PATTERN.test(file)) {
continue
}
const fp = path.join(dirPath, file)

// Ignore all directories - they are not files :-)
// Don't follow symlinks for security reasons!
if (!statSync(fp).isFile()) {
continue
}

const contentType = getMimeForLicenseFile(file)
if (contentType === undefined) {
continue
private * fetchLicenseEvidence (dirPath: string): Generator<Models.License> {
const files = this.leGatherer.getFileAttachments(
dirPath,
(error: Error): void => {
/* c8 ignore next 2 */
this.console.info(`INFO | ${error.message}`)
this.console.debug(`DEBUG | ${error.message} -`, error)
}

try {
yield new Models.NamedLicense(
`file: ${file}`,
{
text: new Models.Attachment(
readFileSync(fp).toString('base64'),
{
contentType,
encoding: Enums.AttachmentEncoding.Base64
}
)
})
}
/* c8 ignore next 3 */
catch (err) {
this.console.info('INFO | skipped license file %s', fp)
this.console.debug('DEBUG | skipped license file %s: %s', fp, err)
)
try {
for (const {file, text} of files) {
yield new Models.NamedLicense(`file: ${file}`, {text})
}
}
/* c8 ignore next 3 */
catch (e) {
// generator will not throw before first `.nest()` is called ...
this.console.warn('WARN | collecting license evidence in', dirPath, 'failed:', e)
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
import { existsSync, mkdirSync, openSync } from 'node:fs'
import { dirname, resolve } from 'node:path'

import { Builders, Enums, Factories, Serialize, Spec, Validation } from '@cyclonedx/cyclonedx-library'
import { Builders, Enums, Factories, Serialize, Spec, Utils,Validation } from '@cyclonedx/cyclonedx-library'
import { Argument, Command, Option } from 'commander'

import { loadJsonFile, type Version, versionCompare, versionTuple, writeAllSync } from './_helpers'
Expand Down Expand Up @@ -310,6 +310,7 @@ export async function run (process_: NodeJS.Process): Promise<number> {
),
new TreeBuilder(),
new Factories.FromNodePackageJson.PackageUrlFactory('npm'),
new Utils.LicenseUtility.LicenseEvidenceGatherer(),
{
ignoreNpmErrors: options.ignoreNpmErrors,
metaComponentType: options.mcType,
Expand Down
36 changes: 0 additions & 36 deletions tests/unit/_helpers.spec.js

This file was deleted.