Skip to content

Commit aaf6f92

Browse files
committed
fix: reproducible bom-refs
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent 704203b commit aaf6f92

File tree

5 files changed

+2532
-2431
lines changed

5 files changed

+2532
-2431
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,6 @@
124124
"suiteName": "jest tests",
125125
"outputDirectory": "reports/jest",
126126
"outputName": "tests.junit.xml"
127-
}
127+
},
128+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
128129
}

src/_helpers.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ SPDX-License-Identifier: Apache-2.0
1717
Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

20+
import { spawnSync } from "node:child_process";
21+
import type { BinaryLike} from "node:crypto";
22+
import { createHash } from "node:crypto";
2023
import { existsSync, readFileSync } from 'node:fs'
21-
import { dirname, isAbsolute, join, sep } from 'node:path'
24+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
2225

2326
import normalizePackageData from 'normalize-package-data'
2427

@@ -132,3 +135,90 @@ export function normalizePackageManifest (data: any, warn?: normalizePackageData
132135
}
133136
}
134137

138+
function sha256(data: BinaryLike): string {
139+
return createHash('sha256').update(data).digest('hex')
140+
}
141+
142+
// region relative paths
143+
144+
const YarnBerryVirtualCacheRE = /^.*[/\\].yarn[/\\]__virtual__[/\\][^/\\]+[/\\]\d[/\\].yarn[/\\]berry[/\\]cache[/\\]/
145+
146+
const _YarnCacheFolders = new Map<string, string | null | undefined>()
147+
function getYarnCacheFolder(cwd: string): string | null {
148+
let cf = _YarnCacheFolders.get(cwd)
149+
if (undefined === cf) {
150+
cf = ''
151+
// yarn 2+
152+
const sr2 = spawnSync('yarn', ['config', 'get', 'cacheFolder'], {
153+
stdio: ['ignore', 'pipe', 'ignore'],
154+
encoding: 'utf-8',
155+
cwd
156+
})
157+
if (sr2.status === 0) {
158+
cf = sr2.stdout.trim()
159+
} else {
160+
// yarn 1
161+
const sr1 = spawnSync('yarn', ['cache', 'dir'], {
162+
stdio: ['ignore', 'pipe', 'ignore'],
163+
encoding: 'utf-8',
164+
cwd
165+
})
166+
if (sr1.status === 0) {
167+
cf = sr1.stdout.trim()
168+
}
169+
}
170+
cf = cf.length > 0
171+
? `${resolve(cf)}${sep}`
172+
: null
173+
_YarnCacheFolders.set(cwd, cf)
174+
}
175+
return cf
176+
}
177+
178+
const _BunCacheFolders = new Map<string, string | null | undefined>()
179+
function getBunCacheFolder(cwd: string): string | null {
180+
let cf = _BunCacheFolders.get(cwd)
181+
if (undefined === cf) {
182+
cf = ''
183+
const sr = spawnSync('bun', ['pm', 'cache'], {
184+
stdio: ['ignore', 'pipe', 'ignore'],
185+
encoding: 'utf-8',
186+
cwd
187+
})
188+
if (sr.status === 0) {
189+
cf = sr.stdout.trim()
190+
}
191+
cf = cf.length > 0
192+
? `${resolve(cf)}${sep}`
193+
: null
194+
_BunCacheFolders.set(cwd, cf)
195+
}
196+
return cf
197+
}
198+
199+
function mkRelativePath(absRoot: string, absPath: string): string {
200+
const ybvcf = YarnBerryVirtualCacheRE.exec(absPath)?.[0]
201+
if (ybvcf !== undefined) {
202+
return `yarnCache:${absPath.slice(ybvcf.length)}`
203+
}
204+
205+
const ycf = getYarnCacheFolder(absRoot)
206+
if (ycf !== null && absPath.startsWith(ycf)) {
207+
return `yarnCache:${absPath.slice(ycf.length)}`
208+
}
209+
210+
const bcf = getBunCacheFolder(absRoot)
211+
if (bcf !== null && absPath.startsWith(bcf)) {
212+
return `bunCache:${absPath.slice(bcf.length)}`
213+
}
214+
215+
return relative(absRoot, absPath)
216+
}
217+
218+
export function mkRelativePathReproducibleHash(absRoot: string, absPath: string): string {
219+
return sha256(
220+
mkRelativePath(absRoot, absPath).replace(sep, '/')
221+
)
222+
}
223+
224+
// endregion relative paths

src/extractor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ SPDX-License-Identifier: Apache-2.0
1717
Copyright (c) OWASP Foundation. All Rights Reserved.
1818
*/
1919

20-
import { dirname } from 'node:path'
20+
import {dirname} from 'node:path'
2121

2222
import type { Builders as FromNodePackageJsonBuilders } from '@cyclonedx/cyclonedx-library/Contrib/FromNodePackageJson'
2323
import type { Utils as LicenseUtils } from '@cyclonedx/cyclonedx-library/Contrib/License'
@@ -27,7 +27,7 @@ import { ComponentEvidence, LicenseRepository, NamedLicense } from '@cyclonedx/c
2727
import type normalizePackageData from "normalize-package-data";
2828
import type { Compilation, Module } from 'webpack'
2929

30-
import type { PackageDescription } from './_helpers'
30+
import type { PackageDescription} from './_helpers'
3131
import {
3232
getPackageConfig,
3333
isNonNullable,

src/plugin.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,12 @@ import spdxExpressionParse from "spdx-expression-parse"
3434
import type { Compiler } from 'webpack'
3535
import { Compilation, sources, version as WEBPACK_VERSION } from 'webpack'
3636

37-
import type { PackageDescription } from './_helpers'
38-
import {
39-
getPackageConfig,
37+
import type { PackageDescription} from './_helpers';
38+
import { getPackageConfig,
4039
iterableSome,
4140
loadJsonFile,
42-
normalizePackageManifest
43-
} from './_helpers'
41+
mkRelativePathReproducibleHash,
42+
normalizePackageManifest} from './_helpers'
4443
import { Extractor } from './extractor'
4544
import { PackageUrlFactory } from './factories'
4645

@@ -259,8 +258,7 @@ export class CycloneDxWebpackPlugin {
259258
}
260259
}
261260

262-
const rcPath = getPackageConfig(compilation.compiler.context)?.path
263-
?? compilation.compiler.context
261+
const compilerContext = compilation.compiler.context
264262

265263
compilation.hooks.afterOptimizeTree.tap(
266264
pluginName,
@@ -275,12 +273,21 @@ export class CycloneDxWebpackPlugin {
275273

276274
thisLogger.log('generating components...')
277275
const components = extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))
278-
const rcComponentDetected = components.get(rcPath)
279-
if ( undefined !== rcComponentDetected ) {
276+
if ( this.reproducibleResults ) {
277+
components.forEach((component, pkgPath) => {
278+
/* eslint-disable-next-line no-param-reassign -- ack */
279+
component.bomRef.value = mkRelativePathReproducibleHash(compilerContext, pkgPath)
280+
})
281+
}
282+
283+
const rcPath = getPackageConfig(compilerContext)?.path
284+
const rcComponentDetected = rcPath === undefined ? undefined : components.get(rcPath)
285+
if (undefined !== rcComponentDetected ) {
280286
if (this.rootComponentAutodetect) {
281287
thisLogger.debug('set bom.metadata.component', rcComponentDetected)
282288
bom.metadata.component = rcComponentDetected
283-
components.delete(rcPath)
289+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ack */
290+
components.delete(rcPath!)
284291
} else {
285292
const rcComponent = cdxComponentBuilder.makeComponent({
286293
name: this.rootComponentName,
@@ -295,13 +302,15 @@ export class CycloneDxWebpackPlugin {
295302
}
296303
thisLogger.debug('add to bom.metadata.component', rcComponentDetected)
297304
bom.metadata.component = rcComponent
298-
components.delete(rcPath)
305+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ack */
306+
components.delete(rcPath!)
299307
}
300308
}
301309
}
310+
302311
for (const component of components.values()) {
303-
thisLogger.debug('add to bom.components', component)
304-
bom.components.add(component)
312+
thisLogger.debug('add to bom.components', component)
313+
bom.components.add(component)
305314
}
306315
thisLogger.log('generated components.')
307316

@@ -420,11 +429,12 @@ export class CycloneDxWebpackPlugin {
420429
}
421430

422431
const rComponent = bom.metadata.component
423-
if (rComponent !== undefined) {
432+
if (undefined !== rComponent) {
424433
this.#addRootComponentExtRefs(rComponent, logger)
425434
/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ack */
426435
rComponent.type = this.rootComponentType as Component['type']
427-
rComponent.bomRef.value ??= '__root_component__'
436+
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intended for empty strings */
437+
rComponent.bomRef.value ||= '__root_component__'
428438
}
429439
/* eslint-enable no-param-reassign */
430440
}

tests/integration/__snapshots__/index.test.js.snap

Lines changed: 2412 additions & 2412 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)