Skip to content
Open
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
11 changes: 11 additions & 0 deletions src/_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
import { existsSync, readFileSync } from 'node:fs'
import { dirname, isAbsolute, join, sep } from 'node:path'

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


Expand All @@ -45,6 +46,10 @@ export interface PackageDescription {
packageJson: NonNullable<any>
}

export interface RootComponentCreationResult {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unintended design.

the function that returns this RootComponentCreationResult is private. so the type definition for this very function should happen where this private function is defined.

rootComponent: CDX.Models.Component
detectedRootComponent: CDX.Models.Component | undefined
}

const PACKAGE_MANIFEST_FILENAME = 'package.json'

Expand Down Expand Up @@ -138,3 +143,9 @@ export function normalizePackageManifest (data: any, warn?: normalizePackageData
data.version = oVersion.trim()
}
}

export function doComponentsMatch(first: CDX.Models.Component, second: CDX.Models.Component): boolean {
return first.group === second.group &&
first.name === second.name &&
first.version === second.version
}
14 changes: 10 additions & 4 deletions src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import * as CDX from '@cyclonedx/cyclonedx-library'
import type { Compilation, Module } from 'webpack'

import {
doComponentsMatch,
getPackageDescription,
isNonNullable,
normalizePackageManifest,
type PackageDescription,
type RootComponentCreationResult,
structuredClonePolyfill
} from './_helpers'

Expand All @@ -50,7 +52,7 @@ export class Extractor {
this.#leGatherer = leFetcher
}

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

Expand All @@ -69,7 +71,7 @@ export class Extractor {
if (component === undefined) {
logger?.log('try to build new Component from PkgPath:', pkg.path)
try {
component = this.makeComponent(pkg, collectEvidence, logger)
component = this.#makeComponent(pkg, collectEvidence, rootComponents, logger)
} catch (err) {
logger?.debug('unexpected error:', err)
logger?.warn('skipped Component from PkgPath', pkg.path)
Expand All @@ -91,7 +93,7 @@ export class Extractor {
/**
* @throws {@link Error} when no component could be fetched
*/
makeComponent (pkg: PackageDescription, collectEvidence: boolean, logger?: WebpackLogger): CDX.Models.Component {
#makeComponent (pkg: PackageDescription, collectEvidence: boolean, rootComponents: RootComponentCreationResult | undefined, logger?: WebpackLogger): CDX.Models.Component {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this function made private? this is an unexpected change.

try {
// work with a deep copy, because `normalizePackageManifest()` might modify the data
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- ach */
Expand All @@ -102,14 +104,18 @@ export class Extractor {
logger?.warn('normalizePackageJson from PkgPath', pkg.path, 'failed:', e)
}

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

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

if (rootComponents?.detectedRootComponent !== undefined && doComponentsMatch(component, rootComponents.detectedRootComponent)) {
component = rootComponents.rootComponent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unintended design change.

a "makeSomething" function makes something - no matter what.
it does not cut steps

}

component.licenses.forEach(l => {
/* eslint-disable no-param-reassign -- intended */
l.acknowledgement = CDX.Enums.LicenseAcknowledgement.Declared
Expand Down
43 changes: 29 additions & 14 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import * as CDX from '@cyclonedx/cyclonedx-library'
import { Compilation, type Compiler, sources, version as WEBPACK_VERSION } from 'webpack'

import {
doComponentsMatch,
getPackageDescription,
iterableSome,
loadJsonFile,
normalizePackageManifest,
type PackageDescription
type PackageDescription,
type RootComponentCreationResult
} from './_helpers'
import { Extractor } from './extractor'

Expand Down Expand Up @@ -214,7 +216,8 @@ export class CycloneDxWebpackPlugin {

const bom = new CDX.Models.Bom()
bom.metadata.lifecycles.add(CDX.Enums.LifecyclePhase.Build)
bom.metadata.component = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'))
const rootComponents = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'))
bom.metadata.component = rootComponents?.rootComponent
Comment on lines +219 to +220
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The variable name rootComponents (plural) is misleading as it contains a single root component along with metadata about detection. Consider renaming to rootComponentResult or rootComponentInfo to better reflect that it's a result object rather than a collection.

Suggested change
const rootComponents = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'))
bom.metadata.component = rootComponents?.rootComponent
const rootComponentResult = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'))
bom.metadata.component = rootComponentResult?.rootComponent

Copilot uses AI. Check for mistakes.

const serializeOptions: CDX.Serialize.Types.SerializerOptions & CDX.Serialize.Types.NormalizerOptions = {
sortLists: this.reproducibleResults,
Expand Down Expand Up @@ -267,11 +270,9 @@ export class CycloneDxWebpackPlugin {
)

thisLogger.log('generating components...')
for (const component of extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))) {
for (const component of extractor.generateComponents(modules, this.collectEvidence, rootComponents, thisLogger.getChildLogger('Extractor'))) {
if (bom.metadata.component !== undefined &&
bom.metadata.component.group === component.group &&
bom.metadata.component.name === component.name &&
bom.metadata.component.version === component.version
doComponentsMatch(bom.metadata.component, component)
) {
// metadata matches this exact component.
// -> so the component is actually treated as the root component.
Expand Down Expand Up @@ -380,19 +381,33 @@ export class CycloneDxWebpackPlugin {
path: string,
builder: CDX.Builders.FromNodePackageJson.ComponentBuilder,
logger: WebpackLogger
): CDX.Models.Component | undefined {
): RootComponentCreationResult | undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please explain what the idea behind the change of the algorithm of this function makeRootComponent is?

i mean, before, it had a name that pretty much described what it did.
now it does ... what?

/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expected */
const thisPackageJson = this.rootComponentAutodetect
? getPackageDescription(path)?.packageJson
: { name: this.rootComponentName, version: this.rootComponentVersion }
if (thisPackageJson === undefined) { return undefined }
normalizePackageManifest(
const detectedRootPackageJson = getPackageDescription(path)?.packageJson
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expected */
const rootPackageJson = this.rootComponentAutodetect ? detectedRootPackageJson
: { name: this.rootComponentName, version: this.rootComponentVersion }

thisPackageJson,
if (rootPackageJson === undefined) { return undefined }
normalizePackageManifest(
rootPackageJson,
w => { logger.debug('normalizePackageJson from PkgPath', path, 'caused:', w) }
)

return builder.makeComponent(thisPackageJson)
if (detectedRootPackageJson !== rootPackageJson) {
normalizePackageManifest(
detectedRootPackageJson,
w => { logger.debug('normalizePackageJson from PkgPath', path, 'caused:', w) }
)
}
Comment on lines +397 to +402
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null/undefined check for detectedRootPackageJson before calling normalizePackageManifest. If rootComponentAutodetect is false and detectedRootPackageJson is undefined, this will cause a runtime error when the condition is true.

Copilot uses AI. Check for mistakes.

const rootComponent = builder.makeComponent(rootPackageJson)
if(rootComponent === undefined) { return undefined }
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after 'if' keyword. Should be if (rootComponent === undefined) to maintain consistency with the project's coding style.

Suggested change
if(rootComponent === undefined) { return undefined }
if (rootComponent === undefined) { return undefined }

Copilot uses AI. Check for mistakes.

const detectedRootComponent = detectedRootPackageJson === rootPackageJson ? rootComponent
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- expected */
: builder.makeComponent(detectedRootPackageJson)
return { rootComponent, detectedRootComponent }
}

#finalizeBom (
Expand Down
Loading
Loading