@pnpm/deps.compliance.sbom 1000.0.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ import type { LockfileObject } from '@pnpm/lockfile.types';
2
+ import type { DependenciesField, ProjectId, Registries } from '@pnpm/types';
3
+ import type { SbomComponentType, SbomResult } from './types.js';
4
+ export interface CollectSbomComponentsOptions {
5
+ lockfile: LockfileObject;
6
+ rootName: string;
7
+ rootVersion: string;
8
+ rootLicense?: string;
9
+ rootDescription?: string;
10
+ rootAuthor?: string;
11
+ rootRepository?: string;
12
+ sbomType?: SbomComponentType;
13
+ include?: {
14
+ [dependenciesField in DependenciesField]: boolean;
15
+ };
16
+ registries: Registries;
17
+ lockfileDir: string;
18
+ includedImporterIds?: ProjectId[];
19
+ lockfileOnly?: boolean;
20
+ storeDir?: string;
21
+ virtualStoreDirMaxLength?: number;
22
+ }
23
+ export declare function collectSbomComponents(opts: CollectSbomComponentsOptions): Promise<SbomResult>;
@@ -0,0 +1,75 @@
1
+ import { DepType, detectDepTypes } from '@pnpm/lockfile.detect-dep-types';
2
+ import { nameVerFromPkgSnapshot, pkgSnapshotToResolution } from '@pnpm/lockfile.utils';
3
+ import { lockfileWalkerGroupImporterSteps, } from '@pnpm/lockfile.walker';
4
+ import { StoreIndex } from '@pnpm/store.index';
5
+ import { getPkgMetadata } from './getPkgMetadata.js';
6
+ import { buildPurl, encodePurlName } from './purl.js';
7
+ export async function collectSbomComponents(opts) {
8
+ const depTypes = detectDepTypes(opts.lockfile);
9
+ const importerIds = opts.includedImporterIds ?? Object.keys(opts.lockfile.importers);
10
+ const importerWalkers = lockfileWalkerGroupImporterSteps(opts.lockfile, importerIds, { include: opts.include });
11
+ const componentsMap = new Map();
12
+ const relationships = [];
13
+ const rootPurl = `pkg:npm/${encodePurlName(opts.rootName)}@${opts.rootVersion}`;
14
+ const storeIndex = (!opts.lockfileOnly && opts.storeDir)
15
+ ? new StoreIndex(opts.storeDir)
16
+ : undefined;
17
+ const metadataOpts = (storeIndex && opts.storeDir)
18
+ ? {
19
+ storeDir: opts.storeDir,
20
+ storeIndex,
21
+ lockfileDir: opts.lockfileDir,
22
+ virtualStoreDirMaxLength: opts.virtualStoreDirMaxLength ?? 120,
23
+ }
24
+ : undefined;
25
+ await Promise.all(importerWalkers.map(async ({ step }) => {
26
+ await walkStep(step, rootPurl, depTypes, componentsMap, relationships, opts, metadataOpts);
27
+ }));
28
+ storeIndex?.close();
29
+ return {
30
+ rootComponent: {
31
+ name: opts.rootName,
32
+ version: opts.rootVersion,
33
+ type: opts.sbomType ?? 'library',
34
+ license: opts.rootLicense,
35
+ description: opts.rootDescription,
36
+ author: opts.rootAuthor,
37
+ repository: opts.rootRepository,
38
+ },
39
+ components: Array.from(componentsMap.values()),
40
+ relationships,
41
+ };
42
+ }
43
+ async function walkStep(step, parentPurl, depTypes, componentsMap, relationships, opts, metadataOpts) {
44
+ await Promise.all(step.dependencies.map(async (dep) => {
45
+ const { depPath, pkgSnapshot, next } = dep;
46
+ const { name, version, nonSemverVersion } = nameVerFromPkgSnapshot(depPath, pkgSnapshot);
47
+ if (!name || !version)
48
+ return;
49
+ const purl = buildPurl({ name, version, nonSemverVersion: nonSemverVersion ?? undefined });
50
+ relationships.push({ from: parentPurl, to: purl });
51
+ if (componentsMap.has(purl))
52
+ return;
53
+ const integrity = pkgSnapshot.resolution.integrity;
54
+ const resolution = pkgSnapshotToResolution(depPath, pkgSnapshot, opts.registries);
55
+ const tarballUrl = resolution.tarball;
56
+ let metadata = {};
57
+ if (metadataOpts) {
58
+ metadata = await getPkgMetadata(depPath, pkgSnapshot, opts.registries, metadataOpts);
59
+ }
60
+ const component = {
61
+ name,
62
+ version,
63
+ purl,
64
+ depPath,
65
+ depType: depTypes[depPath] ?? DepType.ProdOnly,
66
+ integrity,
67
+ tarballUrl,
68
+ ...metadata,
69
+ };
70
+ componentsMap.set(purl, component);
71
+ const subStep = next();
72
+ await walkStep(subStep, purl, depTypes, componentsMap, relationships, opts, metadataOpts);
73
+ }));
74
+ }
75
+ //# sourceMappingURL=collectComponents.js.map
@@ -0,0 +1,17 @@
1
+ import { type PackageSnapshot } from '@pnpm/lockfile.utils';
2
+ import type { StoreIndex } from '@pnpm/store.index';
3
+ import type { Registries } from '@pnpm/types';
4
+ export interface PkgMetadata {
5
+ license?: string;
6
+ description?: string;
7
+ author?: string;
8
+ homepage?: string;
9
+ repository?: string;
10
+ }
11
+ export interface GetPkgMetadataOptions {
12
+ storeDir: string;
13
+ storeIndex: StoreIndex;
14
+ lockfileDir: string;
15
+ virtualStoreDirMaxLength: number;
16
+ }
17
+ export declare function getPkgMetadata(depPath: string, snapshot: PackageSnapshot, registries: Registries, opts: GetPkgMetadataOptions): Promise<PkgMetadata>;
@@ -0,0 +1,71 @@
1
+ import { pkgSnapshotToResolution } from '@pnpm/lockfile.utils';
2
+ import { readPackageJson } from '@pnpm/pkg-manifest.reader';
3
+ import { readPackageFileMap } from '@pnpm/store.pkg-finder';
4
+ import pLimit from 'p-limit';
5
+ const limitMetadataReads = pLimit(4);
6
+ export async function getPkgMetadata(depPath, snapshot, registries, opts) {
7
+ return limitMetadataReads(() => getPkgMetadataUnclamped(depPath, snapshot, registries, opts));
8
+ }
9
+ async function getPkgMetadataUnclamped(depPath, snapshot, registries, opts) {
10
+ const id = snapshot.id ?? depPath;
11
+ const resolution = pkgSnapshotToResolution(depPath, snapshot, registries);
12
+ let files;
13
+ try {
14
+ const result = await readPackageFileMap(resolution, id, opts);
15
+ if (!result)
16
+ return {};
17
+ files = result;
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ const manifestPath = files.get('package.json');
23
+ if (!manifestPath)
24
+ return {};
25
+ const manifest = await readPackageJson(manifestPath);
26
+ return extractMetadata(manifest);
27
+ }
28
+ function extractMetadata(manifest) {
29
+ return {
30
+ license: parseLicenseField(manifest.license),
31
+ description: manifest.description,
32
+ author: parseAuthorField(manifest.author),
33
+ homepage: manifest.homepage,
34
+ repository: parseRepositoryField(manifest.repository),
35
+ };
36
+ }
37
+ function parseLicenseField(field) {
38
+ if (typeof field === 'string')
39
+ return field;
40
+ if (field && typeof field === 'object' && 'type' in field) {
41
+ return field.type;
42
+ }
43
+ if (Array.isArray(field)) {
44
+ return field
45
+ .map((l) => l.type)
46
+ .filter(Boolean)
47
+ .join(' OR ') || undefined;
48
+ }
49
+ return undefined;
50
+ }
51
+ function parseAuthorField(field) {
52
+ if (!field)
53
+ return undefined;
54
+ if (typeof field === 'string')
55
+ return field;
56
+ if (typeof field === 'object' && 'name' in field) {
57
+ return field.name;
58
+ }
59
+ return undefined;
60
+ }
61
+ function parseRepositoryField(field) {
62
+ if (!field)
63
+ return undefined;
64
+ if (typeof field === 'string')
65
+ return field;
66
+ if (typeof field === 'object' && 'url' in field) {
67
+ return field.url;
68
+ }
69
+ return undefined;
70
+ }
71
+ //# sourceMappingURL=getPkgMetadata.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { collectSbomComponents, type CollectSbomComponentsOptions } from './collectComponents.js';
2
+ export { integrityToHashes } from './integrity.js';
3
+ export { buildPurl, encodePurlName } from './purl.js';
4
+ export { type CycloneDxOptions, serializeCycloneDx } from './serializeCycloneDx.js';
5
+ export { serializeSpdx } from './serializeSpdx.js';
6
+ export type { SbomComponent, SbomComponentType, SbomFormat, SbomRelationship, SbomResult } from './types.js';
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { collectSbomComponents } from './collectComponents.js';
2
+ export { integrityToHashes } from './integrity.js';
3
+ export { buildPurl, encodePurlName } from './purl.js';
4
+ export { serializeCycloneDx } from './serializeCycloneDx.js';
5
+ export { serializeSpdx } from './serializeSpdx.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ export interface HashDigest {
2
+ algorithm: string;
3
+ digest: string;
4
+ }
5
+ /**
6
+ * Convert an SRI integrity string to a list of algorithm + hex digest pairs.
7
+ * e.g. "sha512-abc123..." → [{ algorithm: "SHA-512", digest: "..." }]
8
+ */
9
+ export declare function integrityToHashes(integrity: string | undefined): HashDigest[];
@@ -0,0 +1,38 @@
1
+ import ssri from 'ssri';
2
+ /**
3
+ * Convert an SRI integrity string to a list of algorithm + hex digest pairs.
4
+ * e.g. "sha512-abc123..." → [{ algorithm: "SHA-512", digest: "..." }]
5
+ */
6
+ export function integrityToHashes(integrity) {
7
+ if (!integrity)
8
+ return [];
9
+ const parsed = ssri.parse(integrity);
10
+ const hashes = [];
11
+ for (const [algo, entries] of Object.entries(parsed)) {
12
+ if (!entries?.length)
13
+ continue;
14
+ for (const entry of entries) {
15
+ const hexDigest = Buffer.from(entry.digest, 'base64').toString('hex');
16
+ hashes.push({
17
+ algorithm: normalizeShaAlgorithm(algo),
18
+ digest: hexDigest,
19
+ });
20
+ }
21
+ }
22
+ return hashes;
23
+ }
24
+ function normalizeShaAlgorithm(algo) {
25
+ switch (algo) {
26
+ case 'sha1':
27
+ return 'SHA-1';
28
+ case 'sha256':
29
+ return 'SHA-256';
30
+ case 'sha384':
31
+ return 'SHA-384';
32
+ case 'sha512':
33
+ return 'SHA-512';
34
+ default:
35
+ return algo.toUpperCase();
36
+ }
37
+ }
38
+ //# sourceMappingURL=integrity.js.map
@@ -0,0 +1,11 @@
1
+ export declare function classifyLicense(license: string): {
2
+ license: {
3
+ id: string;
4
+ };
5
+ } | {
6
+ license: {
7
+ name: string;
8
+ };
9
+ } | {
10
+ expression: string;
11
+ };
package/lib/license.js ADDED
@@ -0,0 +1,18 @@
1
+ // Sub-path import to pull only the SPDX module — avoids dragging in the
2
+ // validation/serialize layers with optional native deps that break esbuild bundling.
3
+ import { isSupportedSpdxId, isValidSpdxLicenseExpression } from '@cyclonedx/cyclonedx-library/SPDX';
4
+ // Classifies a license string into the appropriate CycloneDX representation.
5
+ // Uses the CycloneDX library's own SPDX list rather than spdx-license-ids,
6
+ // since CycloneDX maintains its own subset of recognized IDs.
7
+ // Order matters: check ID first because "MIT" matches both isSupportedSpdxId
8
+ // and isValidSpdxLicenseExpression, but we prefer the more specific license.id form.
9
+ export function classifyLicense(license) {
10
+ if (isSupportedSpdxId(license)) {
11
+ return { license: { id: license } };
12
+ }
13
+ if (isValidSpdxLicenseExpression(license)) {
14
+ return { expression: license };
15
+ }
16
+ return { license: { name: license } };
17
+ }
18
+ //# sourceMappingURL=license.js.map
package/lib/purl.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Encode a package name for use in a PURL.
3
+ * Scoped packages: @scope/name → %40scope/name
4
+ */
5
+ export declare function encodePurlName(name: string): string;
6
+ /**
7
+ * Build a Package URL (PURL) for a given package.
8
+ * Spec: https://github.com/package-url/purl-spec
9
+ */
10
+ export declare function buildPurl(opts: {
11
+ name: string;
12
+ version: string;
13
+ nonSemverVersion?: string;
14
+ }): string;
package/lib/purl.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Encode a package name for use in a PURL.
3
+ * Scoped packages: @scope/name → %40scope/name
4
+ */
5
+ export function encodePurlName(name) {
6
+ if (name.startsWith('@')) {
7
+ return `%40${name.slice(1)}`;
8
+ }
9
+ return name;
10
+ }
11
+ /**
12
+ * Build a Package URL (PURL) for a given package.
13
+ * Spec: https://github.com/package-url/purl-spec
14
+ */
15
+ export function buildPurl(opts) {
16
+ if (opts.nonSemverVersion) {
17
+ // Git-hosted or tarball dep — encode the raw version as a qualifier
18
+ const encodedUrl = encodeURIComponent(opts.nonSemverVersion);
19
+ return `pkg:npm/${encodePurlName(opts.name)}@${encodeURIComponent(opts.version)}?vcs_url=${encodedUrl}`;
20
+ }
21
+ return `pkg:npm/${encodePurlName(opts.name)}@${opts.version}`;
22
+ }
23
+ //# sourceMappingURL=purl.js.map
@@ -0,0 +1,8 @@
1
+ import type { SbomResult } from './types.js';
2
+ export interface CycloneDxOptions {
3
+ pnpmVersion?: string;
4
+ lockfileOnly?: boolean;
5
+ sbomAuthors?: string[];
6
+ sbomSupplier?: string;
7
+ }
8
+ export declare function serializeCycloneDx(result: SbomResult, opts?: CycloneDxOptions): string;
@@ -0,0 +1,149 @@
1
+ import crypto from 'node:crypto';
2
+ import { integrityToHashes } from './integrity.js';
3
+ import { classifyLicense } from './license.js';
4
+ import { encodePurlName } from './purl.js';
5
+ export function serializeCycloneDx(result, opts) {
6
+ const { rootComponent, components, relationships } = result;
7
+ const rootBomRef = `pkg:npm/${encodePurlName(rootComponent.name)}@${rootComponent.version}`;
8
+ const bomComponents = components.map((comp) => {
9
+ const { group, name } = splitScopedName(comp.name);
10
+ const cdxComp = {
11
+ type: 'library',
12
+ name,
13
+ version: comp.version,
14
+ purl: comp.purl,
15
+ 'bom-ref': comp.purl,
16
+ };
17
+ if (group) {
18
+ cdxComp.group = group;
19
+ }
20
+ if (comp.description) {
21
+ cdxComp.description = comp.description;
22
+ }
23
+ // CycloneDX supplier is the registry/distributor, not the package author
24
+ if (comp.author) {
25
+ cdxComp.authors = [{ name: comp.author }];
26
+ }
27
+ if (comp.license) {
28
+ cdxComp.licenses = [classifyLicense(comp.license)];
29
+ }
30
+ const externalRefs = [];
31
+ // Lockfile integrity is a tarball hash, not a source hash — belongs on the
32
+ // distribution reference, not component.hashes
33
+ if (comp.tarballUrl) {
34
+ const hashes = integrityToHashes(comp.integrity);
35
+ const distRef = {
36
+ type: 'distribution',
37
+ url: comp.tarballUrl,
38
+ };
39
+ if (hashes.length > 0) {
40
+ distRef.hashes = hashes.map((h) => ({
41
+ alg: h.algorithm,
42
+ content: h.digest,
43
+ }));
44
+ }
45
+ externalRefs.push(distRef);
46
+ }
47
+ if (comp.homepage) {
48
+ externalRefs.push({
49
+ type: 'website',
50
+ url: comp.homepage,
51
+ });
52
+ }
53
+ if (comp.repository) {
54
+ externalRefs.push({
55
+ type: 'vcs',
56
+ url: comp.repository,
57
+ });
58
+ }
59
+ if (externalRefs.length > 0) {
60
+ cdxComp.externalReferences = externalRefs;
61
+ }
62
+ return cdxComp;
63
+ });
64
+ // Group relationships by source
65
+ const depMap = new Map();
66
+ depMap.set(rootBomRef, []);
67
+ for (const comp of components) {
68
+ depMap.set(comp.purl, []);
69
+ }
70
+ for (const rel of relationships) {
71
+ const deps = depMap.get(rel.from);
72
+ if (deps) {
73
+ deps.push(rel.to);
74
+ }
75
+ }
76
+ const bomDependencies = Array.from(depMap.entries()).map(([ref, dependsOn]) => ({
77
+ ref,
78
+ dependsOn: [...new Set(dependsOn)],
79
+ }));
80
+ const { group: rootGroup, name: rootName } = splitScopedName(rootComponent.name);
81
+ const rootCdxComponent = {
82
+ type: rootComponent.type,
83
+ name: rootName,
84
+ version: rootComponent.version,
85
+ purl: rootBomRef,
86
+ 'bom-ref': rootBomRef,
87
+ };
88
+ if (rootGroup) {
89
+ rootCdxComponent.group = rootGroup;
90
+ }
91
+ if (rootComponent.author) {
92
+ rootCdxComponent.authors = [{ name: rootComponent.author }];
93
+ }
94
+ if (rootComponent.license) {
95
+ rootCdxComponent.licenses = [classifyLicense(rootComponent.license)];
96
+ }
97
+ if (rootComponent.description) {
98
+ rootCdxComponent.description = rootComponent.description;
99
+ }
100
+ if (rootComponent.repository) {
101
+ rootCdxComponent.externalReferences = [{
102
+ type: 'vcs',
103
+ url: rootComponent.repository,
104
+ }];
105
+ }
106
+ const toolComponents = [];
107
+ if (opts?.pnpmVersion) {
108
+ toolComponents.push({
109
+ type: 'application',
110
+ name: 'pnpm',
111
+ version: opts.pnpmVersion,
112
+ });
113
+ }
114
+ const metadata = {
115
+ timestamp: new Date().toISOString(),
116
+ lifecycles: [{ phase: opts?.lockfileOnly ? 'pre-build' : 'build' }],
117
+ tools: { components: toolComponents },
118
+ component: rootCdxComponent,
119
+ };
120
+ // authors/supplier describe who authored/supplies the BOM document,
121
+ // not the tool — opt-in via --sbom-authors and --sbom-supplier
122
+ if (opts?.sbomAuthors?.length) {
123
+ metadata.authors = opts.sbomAuthors.map((name) => ({ name }));
124
+ }
125
+ if (opts?.sbomSupplier) {
126
+ metadata.supplier = { name: opts.sbomSupplier };
127
+ }
128
+ const bom = {
129
+ $schema: 'http://cyclonedx.org/schema/bom-1.7.schema.json',
130
+ bomFormat: 'CycloneDX',
131
+ specVersion: '1.7',
132
+ serialNumber: `urn:uuid:${crypto.randomUUID()}`,
133
+ version: 1,
134
+ metadata,
135
+ components: bomComponents,
136
+ dependencies: bomDependencies,
137
+ };
138
+ return JSON.stringify(bom, null, 2);
139
+ }
140
+ function splitScopedName(fullName) {
141
+ if (fullName.startsWith('@')) {
142
+ const slashIdx = fullName.indexOf('/');
143
+ if (slashIdx > 0) {
144
+ return { group: fullName.slice(0, slashIdx), name: fullName.slice(slashIdx + 1) };
145
+ }
146
+ }
147
+ return { group: undefined, name: fullName };
148
+ }
149
+ //# sourceMappingURL=serializeCycloneDx.js.map
@@ -0,0 +1,2 @@
1
+ import type { SbomResult } from './types.js';
2
+ export declare function serializeSpdx(result: SbomResult): string;
@@ -0,0 +1,145 @@
1
+ import crypto from 'node:crypto';
2
+ import { integrityToHashes } from './integrity.js';
3
+ import { encodePurlName } from './purl.js';
4
+ export function serializeSpdx(result) {
5
+ const { rootComponent, components, relationships } = result;
6
+ const rootSpdxId = 'SPDXRef-RootPackage';
7
+ const documentNamespace = `https://spdx.org/spdxdocs/${sanitizeSpdxId(rootComponent.name)}-${rootComponent.version}-${crypto.randomUUID()}`;
8
+ const rootPurl = `pkg:npm/${encodePurlName(rootComponent.name)}@${rootComponent.version}`;
9
+ const rootPackage = {
10
+ SPDXID: rootSpdxId,
11
+ name: rootComponent.name,
12
+ versionInfo: rootComponent.version,
13
+ downloadLocation: 'NOASSERTION',
14
+ filesAnalyzed: false,
15
+ primaryPackagePurpose: rootComponent.type === 'application' ? 'APPLICATION' : 'LIBRARY',
16
+ externalRefs: [
17
+ {
18
+ referenceCategory: 'PACKAGE-MANAGER',
19
+ referenceType: 'purl',
20
+ referenceLocator: rootPurl,
21
+ },
22
+ ],
23
+ };
24
+ if (rootComponent.license) {
25
+ rootPackage.licenseConcluded = rootComponent.license;
26
+ rootPackage.licenseDeclared = rootComponent.license;
27
+ }
28
+ else {
29
+ rootPackage.licenseConcluded = 'NOASSERTION';
30
+ rootPackage.licenseDeclared = 'NOASSERTION';
31
+ }
32
+ rootPackage.copyrightText = 'NOASSERTION';
33
+ if (rootComponent.description) {
34
+ rootPackage.description = rootComponent.description;
35
+ }
36
+ if (rootComponent.author) {
37
+ rootPackage.supplier = `Person: ${rootComponent.author}`;
38
+ }
39
+ if (rootComponent.repository) {
40
+ rootPackage.homepage = rootComponent.repository;
41
+ }
42
+ const purlToSpdxId = new Map();
43
+ purlToSpdxId.set(rootPurl, rootSpdxId);
44
+ const spdxPackages = components.map((comp, idx) => {
45
+ const spdxId = `SPDXRef-Package-${sanitizeSpdxId(comp.name)}-${sanitizeSpdxId(comp.version)}-${idx}`;
46
+ purlToSpdxId.set(comp.purl, spdxId);
47
+ const pkg = {
48
+ SPDXID: spdxId,
49
+ name: comp.name,
50
+ versionInfo: comp.version,
51
+ downloadLocation: comp.tarballUrl ?? 'NOASSERTION',
52
+ filesAnalyzed: false,
53
+ externalRefs: [
54
+ {
55
+ referenceCategory: 'PACKAGE-MANAGER',
56
+ referenceType: 'purl',
57
+ referenceLocator: comp.purl,
58
+ },
59
+ ],
60
+ };
61
+ if (comp.license) {
62
+ pkg.licenseConcluded = comp.license;
63
+ pkg.licenseDeclared = comp.license;
64
+ }
65
+ else {
66
+ pkg.licenseConcluded = 'NOASSERTION';
67
+ pkg.licenseDeclared = 'NOASSERTION';
68
+ }
69
+ pkg.copyrightText = 'NOASSERTION';
70
+ if (comp.description) {
71
+ pkg.description = comp.description;
72
+ }
73
+ if (comp.homepage) {
74
+ pkg.homepage = comp.homepage;
75
+ }
76
+ if (comp.author) {
77
+ pkg.supplier = `Person: ${comp.author}`;
78
+ }
79
+ const hashes = integrityToHashes(comp.integrity);
80
+ if (hashes.length > 0) {
81
+ pkg.checksums = hashes.map((h) => ({
82
+ algorithm: spdxHashAlgorithm(h.algorithm),
83
+ checksumValue: h.digest,
84
+ }));
85
+ }
86
+ return pkg;
87
+ });
88
+ const spdxRelationships = [
89
+ {
90
+ spdxElementId: 'SPDXRef-DOCUMENT',
91
+ relatedSpdxElement: rootSpdxId,
92
+ relationshipType: 'DESCRIBES',
93
+ },
94
+ ];
95
+ const seenRelationships = new Set();
96
+ for (const rel of relationships) {
97
+ const fromId = purlToSpdxId.get(rel.from);
98
+ const toId = purlToSpdxId.get(rel.to);
99
+ if (fromId && toId) {
100
+ const key = `${fromId}|${toId}`;
101
+ if (seenRelationships.has(key))
102
+ continue;
103
+ seenRelationships.add(key);
104
+ spdxRelationships.push({
105
+ spdxElementId: fromId,
106
+ relatedSpdxElement: toId,
107
+ relationshipType: 'DEPENDS_ON',
108
+ });
109
+ }
110
+ }
111
+ const doc = {
112
+ spdxVersion: 'SPDX-2.3',
113
+ dataLicense: 'CC0-1.0',
114
+ SPDXID: 'SPDXRef-DOCUMENT',
115
+ name: rootComponent.name,
116
+ documentNamespace,
117
+ creationInfo: {
118
+ created: new Date().toISOString(),
119
+ creators: [
120
+ 'Tool: pnpm',
121
+ ],
122
+ },
123
+ packages: [rootPackage, ...spdxPackages],
124
+ relationships: spdxRelationships,
125
+ };
126
+ return JSON.stringify(doc, null, 2);
127
+ }
128
+ function sanitizeSpdxId(value) {
129
+ return value.replace(/[^a-z0-9.-]/gi, '-');
130
+ }
131
+ function spdxHashAlgorithm(algo) {
132
+ switch (algo) {
133
+ case 'SHA-1':
134
+ return 'SHA1';
135
+ case 'SHA-256':
136
+ return 'SHA256';
137
+ case 'SHA-384':
138
+ return 'SHA384';
139
+ case 'SHA-512':
140
+ return 'SHA512';
141
+ default:
142
+ return algo;
143
+ }
144
+ }
145
+ //# sourceMappingURL=serializeSpdx.js.map
package/lib/types.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { DepType } from '@pnpm/lockfile.detect-dep-types';
2
+ export interface SbomComponent {
3
+ name: string;
4
+ version: string;
5
+ purl: string;
6
+ depPath: string;
7
+ depType: DepType;
8
+ integrity?: string;
9
+ tarballUrl?: string;
10
+ license?: string;
11
+ description?: string;
12
+ author?: string;
13
+ homepage?: string;
14
+ repository?: string;
15
+ }
16
+ export interface SbomRelationship {
17
+ from: string;
18
+ to: string;
19
+ }
20
+ export interface SbomResult {
21
+ rootComponent: {
22
+ name: string;
23
+ version: string;
24
+ type: 'library' | 'application';
25
+ license?: string;
26
+ description?: string;
27
+ author?: string;
28
+ repository?: string;
29
+ };
30
+ components: SbomComponent[];
31
+ relationships: SbomRelationship[];
32
+ }
33
+ export type SbomFormat = 'cyclonedx' | 'spdx';
34
+ export type SbomComponentType = 'library' | 'application';
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@pnpm/deps.compliance.sbom",
3
+ "version": "1000.0.0-0",
4
+ "description": "Generate SBOM from pnpm lockfile",
5
+ "keywords": [
6
+ "pnpm",
7
+ "pnpm11",
8
+ "cyclonedx",
9
+ "sbom",
10
+ "spdx"
11
+ ],
12
+ "license": "MIT",
13
+ "funding": "https://opencollective.com/pnpm",
14
+ "repository": "https://github.com/pnpm/pnpm/tree/main/deps/compliance/sbom",
15
+ "homepage": "https://github.com/pnpm/pnpm/tree/main/deps/compliance/sbom#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/pnpm/pnpm/issues"
18
+ },
19
+ "type": "module",
20
+ "main": "lib/index.js",
21
+ "types": "lib/index.d.ts",
22
+ "exports": {
23
+ ".": "./lib/index.js"
24
+ },
25
+ "files": [
26
+ "lib",
27
+ "!*.map"
28
+ ],
29
+ "dependencies": {
30
+ "@cyclonedx/cyclonedx-library": "9.4.1",
31
+ "p-limit": "^7.1.0",
32
+ "ssri": "13.0.0",
33
+ "@pnpm/lockfile.detect-dep-types": "1001.0.16",
34
+ "@pnpm/lockfile.utils": "1003.0.3",
35
+ "@pnpm/lockfile.types": "1002.0.2",
36
+ "@pnpm/pkg-manifest.reader": "1000.1.2",
37
+ "@pnpm/types": "1000.9.0",
38
+ "@pnpm/lockfile.walker": "1001.0.16",
39
+ "@pnpm/store.index": "1000.0.0-0",
40
+ "@pnpm/store.pkg-finder": "1000.0.0-0"
41
+ },
42
+ "peerDependencies": {
43
+ "@pnpm/logger": ">=1001.0.0 <1002.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@jest/globals": "30.0.5",
47
+ "@types/ssri": "^7.1.5",
48
+ "@pnpm/deps.compliance.sbom": "1000.0.0-0",
49
+ "@pnpm/logger": "1001.0.1"
50
+ },
51
+ "engines": {
52
+ "node": ">=22.13"
53
+ },
54
+ "jest": {
55
+ "preset": "@pnpm/jest-config"
56
+ },
57
+ "scripts": {
58
+ "test": "pnpm run compile && pnpm run _test",
59
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
60
+ "_test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest",
61
+ "compile": "tsgo --build && pnpm run lint --fix"
62
+ }
63
+ }