Skip to content

Commit 00ff7a2

Browse files
committed
fix: replace auto-detected root component with user-provided one
Previously, disabling root component auto-detection while specifying a root component left the auto-detected root in the BOM, with dependencies still attached to it (issue #1418). This change ensures that all instances of the auto-detected root are replaced by the user-provided component using a `componentSubstitutionMap` during component generation. Regression tests were added for that particular case Signed-off-by: Maxim Bagryantsev <[email protected]>
1 parent dc16c7e commit 00ff7a2

File tree

14 files changed

+6699
-128
lines changed

14 files changed

+6699
-128
lines changed

src/_helpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
2020
import { existsSync, readFileSync } from 'node:fs'
2121
import { dirname, isAbsolute, join, sep } from 'node:path'
2222

23+
import type * as CDX from '@cyclonedx/cyclonedx-library'
2324
import normalizePackageData from 'normalize-package-data'
2425

2526

@@ -45,6 +46,10 @@ export interface PackageDescription {
4546
packageJson: NonNullable<any>
4647
}
4748

49+
export interface RootComponentCreationResult {
50+
rootComponent: CDX.Models.Component
51+
detectedRootComponent: CDX.Models.Component | undefined
52+
}
4853

4954
const PACKAGE_MANIFEST_FILENAME = 'package.json'
5055

@@ -138,3 +143,9 @@ export function normalizePackageManifest (data: any, warn?: normalizePackageData
138143
data.version = oVersion.trim()
139144
}
140145
}
146+
147+
export function doComponentsMatch(first: CDX.Models.Component, second: CDX.Models.Component): boolean {
148+
return first.group === second.group &&
149+
first.name === second.name &&
150+
first.version === second.version
151+
}

src/extractor.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import * as CDX from '@cyclonedx/cyclonedx-library'
2323
import type { Compilation, Module } from 'webpack'
2424

2525
import {
26+
doComponentsMatch,
2627
getPackageDescription,
2728
isNonNullable,
2829
normalizePackageManifest,
2930
type PackageDescription,
31+
type RootComponentCreationResult,
3032
structuredClonePolyfill} from './_helpers'
3133

3234
type WebpackLogger = Compilation['logger']
@@ -49,7 +51,7 @@ export class Extractor {
4951
this.#leGatherer = leFetcher
5052
}
5153

52-
generateComponents (modules: Iterable<Module>, collectEvidence: boolean, logger?: WebpackLogger): Iterable<CDX.Models.Component> {
54+
generateComponents (modules: Iterable<Module>, collectEvidence: boolean, rootComponents: RootComponentCreationResult | undefined, logger?: WebpackLogger): Iterable<CDX.Models.Component> {
5355
const pkgs: Record<string, CDX.Models.Component | undefined> = {}
5456
const components = new Map<Module, CDX.Models.Component>()
5557

@@ -68,7 +70,7 @@ export class Extractor {
6870
if (component === undefined) {
6971
logger?.log('try to build new Component from PkgPath:', pkg.path)
7072
try {
71-
component = this.makeComponent(pkg, collectEvidence, logger)
73+
component = this.#makeComponent(pkg, collectEvidence, rootComponents, logger)
7274
} catch (err) {
7375
logger?.debug('unexpected error:', err)
7476
logger?.warn('skipped Component from PkgPath', pkg.path)
@@ -90,7 +92,7 @@ export class Extractor {
9092
/**
9193
* @throws {@link Error} when no component could be fetched
9294
*/
93-
makeComponent (pkg: PackageDescription, collectEvidence: boolean, logger?: WebpackLogger): CDX.Models.Component {
95+
#makeComponent (pkg: PackageDescription, collectEvidence: boolean, rootComponents: RootComponentCreationResult | undefined, logger?: WebpackLogger): CDX.Models.Component {
9496
try {
9597
// work with a deep copy, because `normalizePackageManifest()` might modify the data
9698
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- ach */
@@ -101,14 +103,18 @@ export class Extractor {
101103
logger?.warn('normalizePackageJson from PkgPath', pkg.path, 'failed:', e)
102104
}
103105

104-
const component = this.#componentBuilder.makeComponent(
106+
let component = this.#componentBuilder.makeComponent(
105107
/* @ts-expect-error TS2559 */
106108
pkg.packageJson as PackageDescription) /* eslint-disable-line @typescript-eslint/no-unsafe-type-assertion -- ack */
107109

108110
if (component === undefined) {
109111
throw new Error(`failed building Component from PkgPath ${pkg.path}`)
110112
}
111113

114+
if (rootComponents?.detectedRootComponent !== undefined && doComponentsMatch(component, rootComponents.detectedRootComponent)) {
115+
component = rootComponents.rootComponent
116+
}
117+
112118
component.licenses.forEach(l => {
113119
l.acknowledgement = CDX.Enums.LicenseAcknowledgement.Declared
114120
})

src/plugin.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import * as CDX from '@cyclonedx/cyclonedx-library'
2424
import { Compilation, type Compiler, sources, version as WEBPACK_VERSION } from 'webpack'
2525

2626
import {
27+
doComponentsMatch,
2728
getPackageDescription,
2829
iterableSome,
2930
loadJsonFile,
3031
normalizePackageManifest,
31-
type PackageDescription
32+
type PackageDescription,
33+
type RootComponentCreationResult
3234
} from './_helpers'
3335
import { Extractor } from './extractor'
3436

@@ -214,7 +216,8 @@ export class CycloneDxWebpackPlugin {
214216

215217
const bom = new CDX.Models.Bom()
216218
bom.metadata.lifecycles.add(CDX.Enums.LifecyclePhase.Build)
217-
bom.metadata.component = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'))
219+
const rootComponents = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'))
220+
bom.metadata.component = rootComponents?.rootComponent
218221

219222
const serializeOptions: CDX.Serialize.Types.SerializerOptions & CDX.Serialize.Types.NormalizerOptions = {
220223
sortLists: this.reproducibleResults,
@@ -267,11 +270,9 @@ export class CycloneDxWebpackPlugin {
267270
)
268271

269272
thisLogger.log('generating components...')
270-
for (const component of extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))) {
273+
for (const component of extractor.generateComponents(modules, this.collectEvidence, rootComponents, thisLogger.getChildLogger('Extractor'))) {
271274
if (bom.metadata.component !== undefined &&
272-
bom.metadata.component.group === component.group &&
273-
bom.metadata.component.name === component.name &&
274-
bom.metadata.component.version === component.version
275+
doComponentsMatch(bom.metadata.component, component)
275276
) {
276277
// metadata matches this exact component.
277278
// -> so the component is actually treated as the root component.
@@ -380,19 +381,33 @@ export class CycloneDxWebpackPlugin {
380381
path: string,
381382
builder: CDX.Builders.FromNodePackageJson.ComponentBuilder,
382383
logger: WebpackLogger
383-
): CDX.Models.Component | undefined {
384+
): RootComponentCreationResult | undefined {
384385
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expected */
385-
const thisPackageJson = this.rootComponentAutodetect
386-
? getPackageDescription(path)?.packageJson
387-
: { name: this.rootComponentName, version: this.rootComponentVersion }
388-
if (thisPackageJson === undefined) { return undefined }
386+
const detectedRootPackageJson = getPackageDescription(path)?.packageJson
387+
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expected */
388+
const rootPackageJson = this.rootComponentAutodetect ? detectedRootPackageJson
389+
: { name: this.rootComponentName, version: this.rootComponentVersion }
390+
391+
if (rootPackageJson === undefined) { return undefined }
389392
normalizePackageManifest(
390-
391-
thisPackageJson,
393+
rootPackageJson,
392394
w => { logger.debug('normalizePackageJson from PkgPath', path, 'caused:', w) }
393395
)
394-
395-
return builder.makeComponent(thisPackageJson)
396+
397+
if (detectedRootPackageJson !== rootPackageJson) {
398+
normalizePackageManifest(
399+
detectedRootPackageJson,
400+
w => { logger.debug('normalizePackageJson from PkgPath', path, 'caused:', w) }
401+
)
402+
}
403+
404+
const rootComponent = builder.makeComponent(rootPackageJson)
405+
if(rootComponent === undefined) { return undefined }
406+
407+
const detectedRootComponent = detectedRootPackageJson === rootPackageJson ? rootComponent
408+
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- expected */
409+
: builder.makeComponent(detectedRootPackageJson)
410+
return { rootComponent, detectedRootComponent }
396411
}
397412

398413
#finalizeBom (

0 commit comments

Comments
 (0)