Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: collect evidences for license #1309

Merged
merged 28 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ac8c142
feat(EvidenceCollection): collect evidences for licence and copyright…
Frozen-byte Oct 2, 2024
5bf84dc
chore(EvidenceCollection): sync readme parameters adding `collectEvid…
Frozen-byte Oct 3, 2024
30d5de9
fix(EvidenceCollection): refactor collectEvidence into an option obje…
Frozen-byte Oct 3, 2024
9906cbf
fix(EvidenceCollection): revert breaking change of node18
Frozen-byte Oct 3, 2024
107d2b7
fix(EvidenceCollection): remove copyright collection feature
Frozen-byte Oct 3, 2024
f32c1c7
fix(EvidenceCollection): remove spec check for evidence feature
Frozen-byte Oct 3, 2024
7dd053c
chore(EvidenceCollection): refactor to yielding
Frozen-byte Oct 4, 2024
6711ad2
fix(EvidenceCollection): use named licenses for evidence
Frozen-byte Oct 4, 2024
4d33603
chore(EvidenceCollection): uppercase the case-insensitive regex
Frozen-byte Oct 4, 2024
abe3bc3
chore(EvidenceCollection): use single switch instead of option obj
Frozen-byte Oct 4, 2024
1336a1b
chore(EvidenceCollection): keep related code together
Frozen-byte Oct 4, 2024
1976436
chore(EvidenceCollection): avoid explicit variable names that are jus…
Frozen-byte Oct 4, 2024
895b1c2
chore(EvidenceCollection): code-style
Frozen-byte Oct 4, 2024
1e84884
fix(EvidenceCollection): rm unused argument
Frozen-byte Oct 4, 2024
1293b13
chore(EvidenceCollection): refactor getComponentEvidence to extractor…
Frozen-byte Oct 4, 2024
873006b
fix(EvidenceCollection): as missing slash when concatenating path and…
Frozen-byte Oct 4, 2024
9b7b253
chore(Testing): enable collectEvidence option for tests and update sn…
Frozen-byte Oct 4, 2024
e2e44b0
Revert "chore(Testing): enable collectEvidence option for tests and u…
Frozen-byte Oct 5, 2024
9e41244
fix(EvidenceCollection): remove copyright collection documentation si…
Frozen-byte Oct 5, 2024
d4fd58f
feat(Tests): enable collectEvidence option for tests and update snapshot
Frozen-byte Oct 7, 2024
bf5b1d8
chore(docs): was created by v18
Frozen-byte Oct 7, 2024
b652ae0
fix(Tests): add dummy notice and licence file for the evidence collec…
Frozen-byte Oct 7, 2024
f9dede8
fix(Tests): update snapshots with expected license files
Frozen-byte Oct 7, 2024
796a513
tests: fix setup of new testbed
jkowalleck Oct 7, 2024
03b0346
fix(Tests): update snapshots with new timestamps
Frozen-byte Oct 7, 2024
b0a5c89
fix(Tests): use reproducibleResults in tests to keep snapshot in sync
Frozen-byte Oct 7, 2024
1f987ac
Update HISTORY.md
jkowalleck Oct 7, 2024
7faf23e
Merge branch 'master' into master
jkowalleck Oct 7, 2024
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ new CycloneDxWebpackPlugin(options?: object)
| **`specVersion`** | `{string}`<br/>one of: `"1.2"`, `"1.3"`, `"1.4"`, `"1.5"`, `"1.6"` | `"1.4"` | Which version of [CycloneDX-spec] to use.<br/> Supported values depend on the installed dependency [CycloneDX-javascript-library]. |
| **`reproducibleResults`** | `{boolean}` | `false` | Whether to go the extra mile and make the output reproducible.<br/> Reproducibility might result in loss of time- and random-based-values. |
| **`validateResults`** | `{boolean}` | `true` | Whether to validate the BOM result.<br/>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. |
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
{
"name": "Tristan Bastian",
"url": "https://github.com/reey"
},
{
"name": "Frozen_byte",
"url": "https://github.com/Frozen-byte"
}
],
"type": "commonjs",
Expand Down
52 changes: 50 additions & 2 deletions src/_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ SPDX-License-Identifier: Apache-2.0
Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import { existsSync, readFileSync } from 'fs'
import { dirname, isAbsolute, join, sep } from 'path'
import { existsSync, readdirSync, readFileSync } from 'fs'
import { dirname, extname, isAbsolute, join, sep } from 'path'

export interface PackageDescription {
path: string
Expand Down Expand Up @@ -74,3 +74,51 @@ export function loadJsonFile (path: string): any {
// as soon as this spec is properly implemented.
// 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<{
jkowalleck marked this conversation as resolved.
Show resolved Hide resolved
filepath: string
contentType: string
}> {
for (const dirent of readdirSync(searchFolder, { withFileTypes: true })) {
if (
!dirent.isFile() ||
!LICENSE_FILENAME_PATTERN.test(dirent.name)
) {
continue
}

const contentType = determineContentType(dirent.name)
if (contentType === undefined) {
continue
}

yield {
filepath: `${dirent.parentPath}/${dirent.name}`,
contentType
}
}
}

// common file endings that are used for notice/license files
const CONTENT_TYPE_MAP: Record<string, string> = {
'': 'text/plain',
'.txt': 'text/plain',
'.md': 'text/markdown',
'.xml': 'text/xml'
} 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)]
}
44 changes: 40 additions & 4 deletions src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import * as CDX from '@cyclonedx/cyclonedx-library'
import { readFileSync } from 'fs'
import * as normalizePackageJson from 'normalize-package-data'
import { basename, dirname } from 'path'
import { type Compilation, type Module } from 'webpack'

import { getPackageDescription, type PackageDescription } from './_helpers'
import { getPackageDescription, type PackageDescription, searchEvidenceSources } from './_helpers'

type WebpackLogger = Compilation['logger']

Expand All @@ -40,7 +42,7 @@ export class Extractor {
this.#purlFactory = purlFactory
}

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

Expand All @@ -59,7 +61,7 @@ export class Extractor {
if (component === undefined) {
logger?.log('try to build new Component from PkgPath:', pkg.path)
try {
component = this.makeComponent(pkg, logger)
component = this.makeComponent(pkg, collectEvidence, logger)
} catch (err) {
logger?.debug('unexpected error:', err)
logger?.warn('skipped Component from PkgPath', pkg.path)
Expand All @@ -81,7 +83,7 @@ export class Extractor {
/**
* @throws {Error} when no component could be fetched
*/
makeComponent (pkg: PackageDescription, 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? */)
Expand All @@ -108,6 +110,14 @@ 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)
jkowalleck marked this conversation as resolved.
Show resolved Hide resolved
}
}

return component
}

Expand All @@ -121,6 +131,32 @@ 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
}
)
}
))
}

return cdxComponentEvidence
}
}

function isNonNullable<T> (value: T): value is NonNullable<T> {
Expand Down
12 changes: 11 additions & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ 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.
Expand Down Expand Up @@ -119,6 +126,7 @@ export class CycloneDxWebpackPlugin {
specVersion: CDX.Spec.Version
reproducibleResults: boolean
validateResults: boolean
collectEvidence: boolean

resultXml: string
resultJson: string
Expand All @@ -133,6 +141,7 @@ export class CycloneDxWebpackPlugin {
specVersion = CDX.Spec.Version.v1dot4,
reproducibleResults = false,
validateResults = true,
collectEvidence = false,
outputLocation = './cyclonedx',
includeWellknown = true,
wellknownLocation = './.well-known',
Expand All @@ -144,6 +153,7 @@ export class CycloneDxWebpackPlugin {
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
Expand Down Expand Up @@ -224,7 +234,7 @@ export class CycloneDxWebpackPlugin {
const extractor = new Extractor(compilation, cdxComponentBuilder, cdxPurlFactory)

thisLogger.log('generating components...')
for (const component of extractor.generateComponents(modules, thisLogger.getChildLogger('Extractor'))) {
for (const component of extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))) {
if (bom.metadata.component !== undefined &&
bom.metadata.component.group === component.group &&
bom.metadata.component.name === component.name &&
Expand Down
1,656 changes: 1,656 additions & 0 deletions tests/integration/__snapshots__/index.test.js.snap

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions tests/integration/feature-issue676/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.

# Compiled output
/dist
/tmp
/out-tsc
/bazel-out

# Node
/node_modules
npm-debug.log
yarn-error.log

# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings

# System files
.DS_Store
Thumbs.db
Loading
Loading