diff --git a/HISTORY.md b/HISTORY.md
index 790aa5dc..7a13eaf1 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file.
* Added
- * Feature for collecting (license) evidence ([#676] via [#1309])
+ * Feature for collecting (license) evidence ([#676] via [#1309], [#1312])
Controlled with option `collectEvidence`, disabled by default.
* Build
* Use _TypeScript_ `v5.6.2` now, was `v5.5.3` (via [#1302], [#1306])
@@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file.
[#1302]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1302
[#1306]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1306
[#1309]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1309
+[#1312]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1312
## 3.13.0 - 2024-07-21
diff --git a/README.md b/README.md
index 5fa9e24e..50c0c764 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,6 @@ new CycloneDxWebpackPlugin(options?: object)
| **`specVersion`** | `{string}`
one of: `"1.2"`, `"1.3"`, `"1.4"`, `"1.5"`, `"1.6"` | `"1.4"` | Which version of [CycloneDX-spec] to use.
Supported values depend on the installed dependency [CycloneDX-javascript-library]. |
| **`reproducibleResults`** | `{boolean}` | `false` | Whether to go the extra mile and make the output reproducible.
Reproducibility might result in loss of time- and random-based-values. |
| **`validateResults`** | `{boolean}` | `true` | Whether to validate the BOM result.
Validation is skipped, if requirements not met. Requires [transitive optional dependencies](https://github.com/CycloneDX/cyclonedx-javascript-library#optional-dependencies). |
-| **`collectEvidence`** | `{boolean}` | `false` | Look for common files that may provide licenses and attach them to the component as evidence. |
| **`outputLocation`** | `{string}` | `"./cyclonedx"` | Path to write the output to. The path is relative to _webpack_'s overall output path. |
| **`includeWellknown`** | `{boolean}` | `true` | Whether to write the Wellknowns. |
| **`wellknownLocation`** | `{string}` | `"./.well-known"` | Path to write the Wellknowns to. The path is relative to _webpack_'s overall output path. |
@@ -57,6 +56,7 @@ new CycloneDxWebpackPlugin(options?: object)
| **`rootComponentType`** | `{string}` | `"application"` | Set the RootComponent's type.
See [the list of valid values](https://cyclonedx.org/docs/1.4/json/#metadata_component_type). Supported values depend on [CycloneDX-javascript-library]'s enum `ComponentType`. |
| **`rootComponentName`** | optional `{string}` | `undefined` | If `rootComponentAutodetect` is disabled, then this value is assumed as the "name" of the `package.json`. |
| **`rootComponentVersion`** | optional `{string}` | `undefined` | If `rootComponentAutodetect` is disabled, then this value is assumed as the "version" of the `package.json`. |
+| **`collectEvidence`** | `{boolean}` | `false` | Whether to collect (license) evidence and attach them to the resulting SBOM. |
### Example
diff --git a/src/_helpers.ts b/src/_helpers.ts
index 83a8de10..3bebb204 100644
--- a/src/_helpers.ts
+++ b/src/_helpers.ts
@@ -17,7 +17,7 @@ SPDX-License-Identifier: Apache-2.0
Copyright (c) OWASP Foundation. All Rights Reserved.
*/
-import { existsSync, readdirSync, readFileSync } from 'fs'
+import { existsSync, readFileSync } from 'fs'
import { dirname, extname, isAbsolute, join, sep } from 'path'
export function isNonNullable (value: T): value is NonNullable {
@@ -88,50 +88,20 @@ export function loadJsonFile (path: string): any {
// see https://github.com/tc39/proposal-import-attributes
}
-const LICENSE_FILENAME_PATTERN = /^(?:UN)?LICEN[CS]E|NOTICE/i
-/**
- * Searches typical files in the package path which have typical a license notice text inside
- *
- * @param {string} searchFolder folder to look for common filenames
- *
- * @yields {{ filepath: string, contentType: string}} Next matching file containing path and MIME type
- */
-export function * searchEvidenceSources (searchFolder: string): Generator<{
- filepath: string
- contentType: string
-}> {
- for (const dirent of readdirSync(searchFolder, { withFileTypes: true })) {
- if (
- !dirent.isFile() ||
- !LICENSE_FILENAME_PATTERN.test(dirent.name)
- ) {
- continue
- }
+// region MIME
- const contentType = determineContentType(dirent.name)
- if (contentType === undefined) {
- continue
- }
+export type MimeType = string
- yield {
- filepath: `${dirent.parentPath}/${dirent.name}`,
- contentType
- }
- }
-}
-
-// common file endings that are used for notice/license files
-const CONTENT_TYPE_MAP: Record = {
+const MAP_TEXT_EXTENSION_MIME: Readonly> = {
'': 'text/plain',
- '.txt': 'text/plain',
'.md': 'text/markdown',
- '.xml': 'text/xml'
+ '.rst': 'text/prs.fallenstein.rst',
+ '.txt': 'text/plain',
+ '.xml': 'text/xml' // not `application/xml` -- our scope is text!
} as const
-/**
- * Returns the MIME type for the file or undefined if nothing was matched
- * @param {string} filename filename or complete filepath
- */
-export function determineContentType (filename: string): string | undefined {
- return CONTENT_TYPE_MAP[extname(filename)]
+export function getMimeForTextFile (filename: string): MimeType | undefined {
+ return MAP_TEXT_EXTENSION_MIME[extname(filename)]
}
+
+// endregion MIME
diff --git a/src/extractor.ts b/src/extractor.ts
index 42f114fb..bdc58049 100644
--- a/src/extractor.ts
+++ b/src/extractor.ts
@@ -18,12 +18,12 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
*/
import * as CDX from '@cyclonedx/cyclonedx-library'
-import { readFileSync } from 'fs'
+import { readdirSync, readFileSync } from 'fs'
import * as normalizePackageJson from 'normalize-package-data'
-import { basename, dirname } from 'path'
+import { dirname, join } from 'path'
import type { Compilation, Module } from 'webpack'
-import { getPackageDescription, isNonNullable, type PackageDescription, searchEvidenceSources, structuredClonePolyfill } from './_helpers'
+import { getMimeForTextFile, getPackageDescription, isNonNullable, type PackageDescription, structuredClonePolyfill } from './_helpers'
type WebpackLogger = Compilation['logger']
@@ -42,7 +42,7 @@ export class Extractor {
this.#purlFactory = purlFactory
}
- generateComponents (modules: Iterable, collectEvidence?: boolean, logger?: WebpackLogger): Iterable {
+ generateComponents (modules: Iterable, collectEvidence: boolean, logger?: WebpackLogger): Iterable {
const pkgs: Record = {}
const components = new Map()
@@ -83,7 +83,7 @@ export class Extractor {
/**
* @throws {Error} when no component could be fetched
*/
- makeComponent (pkg: PackageDescription, collectEvidence?: boolean, logger?: WebpackLogger): CDX.Models.Component {
+ makeComponent (pkg: PackageDescription, collectEvidence: boolean, logger?: WebpackLogger): CDX.Models.Component {
try {
const _packageJson = structuredClonePolyfill(pkg.packageJson)
normalizePackageJson(_packageJson as object /* add debug for warnings? */)
@@ -110,12 +110,10 @@ export class Extractor {
component.purl = this.#purlFactory.makeFromComponent(component)
component.bomRef.value = component.purl?.toString()
- if (collectEvidence === true) {
- try {
- component.evidence = this.makeComponentEvidence(pkg)
- } catch (e) {
- logger?.warn('collecting Evidence from PkgPath', pkg.path, 'failed:', e)
- }
+ if (collectEvidence) {
+ component.evidence = new CDX.Models.ComponentEvidence({
+ licenses: new CDX.Models.LicenseRepository(this.getLicenseEvidence(dirname(pkg.path), logger))
+ })
}
return component
@@ -132,29 +130,48 @@ export class Extractor {
}
}
- /**
- * Look for common files that may provide licenses and attach them to the component as evidence
- * @param pkg
- */
- makeComponentEvidence (pkg: PackageDescription): CDX.Models.ComponentEvidence {
- const cdxComponentEvidence = new CDX.Models.ComponentEvidence()
-
- // Add license evidence
- for (const { contentType, filepath } of searchEvidenceSources(dirname(pkg.path))) {
- cdxComponentEvidence.licenses.add(new CDX.Models.NamedLicense(
- `file: ${basename(filepath)}`,
- {
- text: new CDX.Models.Attachment(
- readFileSync(filepath).toString('base64'),
- {
- contentType,
- encoding: CDX.Enums.AttachmentEncoding.Base64
- }
- )
- }
- ))
+ readonly #LICENSE_FILENAME_PATTERN = /^(?:UN)?LICEN[CS]E|^NOTICE$/i
+
+ public * getLicenseEvidence (
+ packageDir: string,
+ logger?: WebpackLogger
+ ): Generator {
+ let pcis
+ try {
+ pcis = readdirSync(packageDir, { withFileTypes: true })
+ } catch (e) {
+ logger?.warn('collecting license evidence in', packageDir, 'failed:', e)
+ return
}
+ for (const pci of pcis) {
+ if (
+ !pci.isFile() ||
+ !this.#LICENSE_FILENAME_PATTERN.test(pci.name)
+ ) {
+ continue
+ }
+
+ const contentType = getMimeForTextFile(pci.name)
+ if (contentType === undefined) {
+ continue
+ }
- return cdxComponentEvidence
+ const fp = join(packageDir, pci.name)
+ try {
+ yield new CDX.Models.NamedLicense(
+ `file: ${pci.name}`,
+ {
+ text: new CDX.Models.Attachment(
+ readFileSync(fp).toString('base64'),
+ {
+ contentType,
+ encoding: CDX.Enums.AttachmentEncoding.Base64
+ }
+ )
+ })
+ } catch (e) { // may throw if `readFileSync()` fails
+ logger?.warn('collecting license evidence from', fp, 'failed:', e)
+ }
+ }
}
}
diff --git a/src/plugin.ts b/src/plugin.ts
index c941fe32..e4137c8a 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -53,13 +53,6 @@ export interface CycloneDxWebpackPluginOptions {
*/
validateResults?: CycloneDxWebpackPlugin['validateResults']
- /**
- * Look for common files that may provide licenses and attach them to the component as evidence
- *
- * @default false
- */
- collectEvidence?: boolean
-
/**
* Path to write the output to.
* The path is relative to webpack's overall output path.
@@ -111,6 +104,13 @@ export interface CycloneDxWebpackPluginOptions {
* @default undefined
*/
rootComponentVersion?: CycloneDxWebpackPlugin['rootComponentVersion']
+
+ /**
+ * Whether to collect (license) evidence and attach them to the resulting SBOM.
+ *
+ * @default false
+ */
+ collectEvidence?: boolean
}
class ValidationError extends Error {
@@ -126,7 +126,6 @@ export class CycloneDxWebpackPlugin {
specVersion: CDX.Spec.Version
reproducibleResults: boolean
validateResults: boolean
- collectEvidence: boolean
resultXml: string
resultJson: string
@@ -137,23 +136,24 @@ export class CycloneDxWebpackPlugin {
rootComponentName: CDX.Models.Component['name'] | undefined
rootComponentVersion: CDX.Models.Component['version'] | undefined
+ collectEvidence: boolean
+
constructor ({
specVersion = CDX.Spec.Version.v1dot4,
reproducibleResults = false,
validateResults = true,
- collectEvidence = false,
outputLocation = './cyclonedx',
includeWellknown = true,
wellknownLocation = './.well-known',
rootComponentAutodetect = true,
rootComponentType = CDX.Enums.ComponentType.Application,
rootComponentName = undefined,
- rootComponentVersion = undefined
+ rootComponentVersion = undefined,
+ collectEvidence = false
}: CycloneDxWebpackPluginOptions = {}) {
this.specVersion = specVersion
this.reproducibleResults = reproducibleResults
this.validateResults = validateResults
- this.collectEvidence = collectEvidence
this.resultXml = joinPath(outputLocation, './bom.xml')
this.resultJson = joinPath(outputLocation, './bom.json')
this.resultWellknown = includeWellknown
@@ -163,6 +163,7 @@ export class CycloneDxWebpackPlugin {
this.rootComponentType = rootComponentType
this.rootComponentName = rootComponentName
this.rootComponentVersion = rootComponentVersion
+ this.collectEvidence = collectEvidence
}
apply (compiler: Compiler): void {
diff --git a/tests/integration/__snapshots__/index.test.js.snap b/tests/integration/__snapshots__/index.test.js.snap
index eb155a39..095712df 100644
--- a/tests/integration/__snapshots__/index.test.js.snap
+++ b/tests/integration/__snapshots__/index.test.js.snap
@@ -476,16 +476,6 @@ exports[`integration feature: component evidence generated json file: dist/.bom/
],
"evidence": {
"licenses": [
- {
- "license": {
- "name": "file: CopyrightNotice.txt",
- "text": {
- "content": "LyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg0KQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uDQoNClBlcm1pc3Npb24gdG8gdXNlLCBjb3B5LCBtb2RpZnksIGFuZC9vciBkaXN0cmlidXRlIHRoaXMgc29mdHdhcmUgZm9yIGFueQ0KcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLg0KDQpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiBBTkQgVEhFIEFVVEhPUiBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSA0KUkVHQVJEIFRPIFRISVMgU09GVFdBUkUgSU5DTFVESU5HIEFMTCBJTVBMSUVEIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZDQpBTkQgRklUTkVTUy4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUiBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsDQpJTkRJUkVDVCwgT1IgQ09OU0VRVUVOVElBTCBEQU1BR0VTIE9SIEFOWSBEQU1BR0VTIFdIQVRTT0VWRVIgUkVTVUxUSU5HIEZST00NCkxPU1MgT0YgVVNFLCBEQVRBIE9SIFBST0ZJVFMsIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBORUdMSUdFTkNFIE9SDQpPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SDQpQRVJGT1JNQU5DRSBPRiBUSElTIFNPRlRXQVJFLg0KKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiogKi8NCg0K",
- "contentType": "text/plain",
- "encoding": "base64"
- }
- }
- },
{
"license": {
"name": "file: LICENSE.txt",
@@ -1085,16 +1075,6 @@ exports[`integration feature: component evidence generated json file: dist/.well
],
"evidence": {
"licenses": [
- {
- "license": {
- "name": "file: CopyrightNotice.txt",
- "text": {
- "content": "LyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg0KQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uDQoNClBlcm1pc3Npb24gdG8gdXNlLCBjb3B5LCBtb2RpZnksIGFuZC9vciBkaXN0cmlidXRlIHRoaXMgc29mdHdhcmUgZm9yIGFueQ0KcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLg0KDQpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiBBTkQgVEhFIEFVVEhPUiBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSA0KUkVHQVJEIFRPIFRISVMgU09GVFdBUkUgSU5DTFVESU5HIEFMTCBJTVBMSUVEIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZDQpBTkQgRklUTkVTUy4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUiBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsDQpJTkRJUkVDVCwgT1IgQ09OU0VRVUVOVElBTCBEQU1BR0VTIE9SIEFOWSBEQU1BR0VTIFdIQVRTT0VWRVIgUkVTVUxUSU5HIEZST00NCkxPU1MgT0YgVVNFLCBEQVRBIE9SIFBST0ZJVFMsIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBORUdMSUdFTkNFIE9SDQpPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SDQpQRVJGT1JNQU5DRSBPRiBUSElTIFNPRlRXQVJFLg0KKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiogKi8NCg0K",
- "contentType": "text/plain",
- "encoding": "base64"
- }
- }
- },
{
"license": {
"name": "file: LICENSE.txt",
@@ -1569,10 +1549,6 @@ exports[`integration feature: component evidence generated xml file: dist/.bom/b
-
- file: CopyrightNotice.txt
- LyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg0KQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uDQoNClBlcm1pc3Npb24gdG8gdXNlLCBjb3B5LCBtb2RpZnksIGFuZC9vciBkaXN0cmlidXRlIHRoaXMgc29mdHdhcmUgZm9yIGFueQ0KcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLg0KDQpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiBBTkQgVEhFIEFVVEhPUiBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSA0KUkVHQVJEIFRPIFRISVMgU09GVFdBUkUgSU5DTFVESU5HIEFMTCBJTVBMSUVEIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZDQpBTkQgRklUTkVTUy4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUiBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsDQpJTkRJUkVDVCwgT1IgQ09OU0VRVUVOVElBTCBEQU1BR0VTIE9SIEFOWSBEQU1BR0VTIFdIQVRTT0VWRVIgUkVTVUxUSU5HIEZST00NCkxPU1MgT0YgVVNFLCBEQVRBIE9SIFBST0ZJVFMsIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBORUdMSUdFTkNFIE9SDQpPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SDQpQRVJGT1JNQU5DRSBPRiBUSElTIFNPRlRXQVJFLg0KKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiogKi8NCg0K
-
file: LICENSE.txt
Q29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uDQoNClBlcm1pc3Npb24gdG8gdXNlLCBjb3B5LCBtb2RpZnksIGFuZC9vciBkaXN0cmlidXRlIHRoaXMgc29mdHdhcmUgZm9yIGFueQ0KcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLg0KDQpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiBBTkQgVEhFIEFVVEhPUiBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSA0KUkVHQVJEIFRPIFRISVMgU09GVFdBUkUgSU5DTFVESU5HIEFMTCBJTVBMSUVEIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZDQpBTkQgRklUTkVTUy4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUiBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsDQpJTkRJUkVDVCwgT1IgQ09OU0VRVUVOVElBTCBEQU1BR0VTIE9SIEFOWSBEQU1BR0VTIFdIQVRTT0VWRVIgUkVTVUxUSU5HIEZST00NCkxPU1MgT0YgVVNFLCBEQVRBIE9SIFBST0ZJVFMsIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBORUdMSUdFTkNFIE9SDQpPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SDQpQRVJGT1JNQU5DRSBPRiBUSElTIFNPRlRXQVJFLg==
diff --git a/tests/integration/feature-issue676/webpack-build.config.js b/tests/integration/feature-issue676/webpack-build.config.js
index 44d9d4df..713db613 100644
--- a/tests/integration/feature-issue676/webpack-build.config.js
+++ b/tests/integration/feature-issue676/webpack-build.config.js
@@ -4,8 +4,9 @@ const { CycloneDxWebpackPlugin } = require('@cyclonedx/webpack-plugin')
const cycloneDxWebpackPluginOptions = new CycloneDxWebpackPlugin({
specVersion: '1.4',
outputLocation: '.bom',
- collectEvidence: true,
- reproducibleResults: true
+ reproducibleResults: true,
+ validateResults: true,
+ collectEvidence: true
})
module.exports = {