@cyclonedx/cdxgen 12.2.0 → 12.3.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.
Files changed (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,793 @@
1
+ function appendProperty(properties, name, value) {
2
+ if (!name || value === undefined || value === null || value === "") {
3
+ return;
4
+ }
5
+ properties.push({
6
+ name,
7
+ value: typeof value === "string" ? value : String(value),
8
+ });
9
+ }
10
+
11
+ function uniqueStrings(values) {
12
+ return [
13
+ ...new Set(values.filter(Boolean).map((value) => String(value).trim())),
14
+ ];
15
+ }
16
+
17
+ function parseTimestamp(value) {
18
+ if (!value || typeof value !== "string") {
19
+ return undefined;
20
+ }
21
+ const timestamp = Date.parse(value);
22
+ return Number.isNaN(timestamp) ? undefined : timestamp;
23
+ }
24
+
25
+ function sortReleaseEntries(entries) {
26
+ return entries.sort((left, right) => left.timestamp - right.timestamp);
27
+ }
28
+
29
+ function median(numbers) {
30
+ if (!numbers.length) {
31
+ return undefined;
32
+ }
33
+ const sorted = [...numbers].sort((left, right) => left - right);
34
+ const midpoint = Math.floor(sorted.length / 2);
35
+ if (sorted.length % 2 === 0) {
36
+ return (sorted[midpoint - 1] + sorted[midpoint]) / 2;
37
+ }
38
+ return sorted[midpoint];
39
+ }
40
+
41
+ function normalizeIdentity(value) {
42
+ if (!value) {
43
+ return undefined;
44
+ }
45
+ return String(value).trim().toLowerCase();
46
+ }
47
+
48
+ function uniqueIdentities(values) {
49
+ return [
50
+ ...new Set(values.map((value) => normalizeIdentity(value)).filter(Boolean)),
51
+ ];
52
+ }
53
+
54
+ function isDisjointIdentitySet(leftSet, rightSet) {
55
+ if (!leftSet.length || !rightSet.length) {
56
+ return false;
57
+ }
58
+ return leftSet.every(
59
+ (leftValue) => !rightSet.some((rightValue) => rightValue === leftValue),
60
+ );
61
+ }
62
+
63
+ function identityOverlapMetrics(leftSet, rightSet) {
64
+ if (!leftSet.length || !rightSet.length) {
65
+ return {};
66
+ }
67
+ const rightValues = new Set(rightSet);
68
+ const overlapCount = leftSet.filter((leftValue) =>
69
+ rightValues.has(leftValue),
70
+ ).length;
71
+ const unionCount = new Set([...leftSet, ...rightSet]).size;
72
+ return {
73
+ overlapCount,
74
+ overlapRatio: unionCount > 0 ? overlapCount / unionCount : undefined,
75
+ partialDrift:
76
+ overlapCount > 0 &&
77
+ overlapCount < unionCount &&
78
+ (overlapCount < leftSet.length || overlapCount < rightSet.length),
79
+ };
80
+ }
81
+
82
+ function extractMaintainerIdentities(maintainers) {
83
+ if (!Array.isArray(maintainers)) {
84
+ return [];
85
+ }
86
+ const identities = [];
87
+ for (const maintainer of maintainers) {
88
+ if (typeof maintainer === "string") {
89
+ identities.push(maintainer);
90
+ continue;
91
+ }
92
+ identities.push(maintainer?.name, maintainer?.email);
93
+ }
94
+ return uniqueIdentities(identities);
95
+ }
96
+
97
+ function releaseGapMetrics(releaseEntries, currentVersion) {
98
+ const sortedEntries = sortReleaseEntries([...releaseEntries]);
99
+ const currentIndex = sortedEntries.findIndex(
100
+ (entry) => entry.version === currentVersion,
101
+ );
102
+ if (currentIndex < 1) {
103
+ return {};
104
+ }
105
+ const currentGapDays =
106
+ (sortedEntries[currentIndex].timestamp -
107
+ sortedEntries[currentIndex - 1].timestamp) /
108
+ (1000 * 60 * 60 * 24);
109
+ const priorGapDays = [];
110
+ for (let index = 1; index < currentIndex; index += 1) {
111
+ priorGapDays.push(
112
+ (sortedEntries[index].timestamp - sortedEntries[index - 1].timestamp) /
113
+ (1000 * 60 * 60 * 24),
114
+ );
115
+ }
116
+ return {
117
+ baselineDays: median(priorGapDays),
118
+ currentGapDays,
119
+ sampleSize: priorGapDays.length,
120
+ };
121
+ }
122
+
123
+ function compressedCadenceMetrics(gapMetrics) {
124
+ const baselineDays = gapMetrics?.baselineDays;
125
+ const currentGapDays = gapMetrics?.currentGapDays;
126
+ const sampleSize = gapMetrics?.sampleSize;
127
+ if (
128
+ baselineDays === undefined ||
129
+ currentGapDays === undefined ||
130
+ sampleSize === undefined ||
131
+ sampleSize < 3 ||
132
+ baselineDays <= 0 ||
133
+ currentGapDays <= 0
134
+ ) {
135
+ return {};
136
+ }
137
+ const compressionRatio = currentGapDays / baselineDays;
138
+ return {
139
+ compressedCadence:
140
+ baselineDays >= 21 && currentGapDays <= 14 && compressionRatio <= 0.33,
141
+ compressionRatio,
142
+ };
143
+ }
144
+
145
+ function extractNestedValue(obj, paths) {
146
+ for (const path of paths) {
147
+ let current = obj;
148
+ for (const segment of path) {
149
+ current = current?.[segment];
150
+ if (current === undefined || current === null) {
151
+ break;
152
+ }
153
+ }
154
+ if (current !== undefined && current !== null && current !== "") {
155
+ return current;
156
+ }
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ function normalizeProvenanceUrl(value) {
162
+ if (!value) {
163
+ return undefined;
164
+ }
165
+ if (typeof value === "string") {
166
+ return value;
167
+ }
168
+ return extractNestedValue(value, [
169
+ ["url"],
170
+ ["provenanceUrl"],
171
+ ["attestationUrl"],
172
+ ["bundle", "url"],
173
+ ["provenance", "url"],
174
+ ["attestations", "url"],
175
+ ]);
176
+ }
177
+
178
+ function collectPathValues(value, pathSegments) {
179
+ if (value === undefined || value === null) {
180
+ return [];
181
+ }
182
+ if (!pathSegments.length) {
183
+ if (Array.isArray(value)) {
184
+ return value.flatMap((entry) => collectPathValues(entry, []));
185
+ }
186
+ return [value];
187
+ }
188
+ if (Array.isArray(value)) {
189
+ return value.flatMap((entry) => collectPathValues(entry, pathSegments));
190
+ }
191
+ const [currentSegment, ...remainingSegments] = pathSegments;
192
+ return collectPathValues(value?.[currentSegment], remainingSegments);
193
+ }
194
+
195
+ function normalizeCollectedValues(values) {
196
+ const normalizedValues = [];
197
+ for (const value of values) {
198
+ if (value === undefined || value === null || value === "") {
199
+ continue;
200
+ }
201
+ if (typeof value === "string") {
202
+ normalizedValues.push(value);
203
+ continue;
204
+ }
205
+ if (typeof value === "number" || typeof value === "boolean") {
206
+ normalizedValues.push(String(value));
207
+ }
208
+ }
209
+ return uniqueStrings(normalizedValues);
210
+ }
211
+
212
+ function collectNestedValues(value, paths) {
213
+ const collectedValues = [];
214
+ for (const path of paths) {
215
+ collectedValues.push(...collectPathValues(value, path));
216
+ }
217
+ return normalizeCollectedValues(collectedValues);
218
+ }
219
+
220
+ function appendJoinedProperty(properties, name, values) {
221
+ appendProperty(properties, name, uniqueStrings(values).join(", "));
222
+ }
223
+
224
+ function collectProvenanceDigests(value) {
225
+ return collectNestedValues(value, [
226
+ ["digest"],
227
+ ["hash"],
228
+ ["sha256"],
229
+ ["sha512"],
230
+ ["integrity"],
231
+ ["hashes", "sha256"],
232
+ ["hashes", "sha512"],
233
+ ["subject", "digest", "sha256"],
234
+ ["subject", "digest", "sha512"],
235
+ ["statement", "subject", "digest", "sha256"],
236
+ ["statement", "subject", "digest", "sha512"],
237
+ ["bundle", "subject", "digest", "sha256"],
238
+ ["bundle", "subject", "digest", "sha512"],
239
+ ]);
240
+ }
241
+
242
+ function collectProvenanceKeyIds(value) {
243
+ return collectNestedValues(value, [
244
+ ["keyid"],
245
+ ["keyId"],
246
+ ["publicKeyId"],
247
+ ["verificationKeyId"],
248
+ ["signingKeyId"],
249
+ ["signatures", "keyid"],
250
+ ["signatures", "keyId"],
251
+ ["verificationMaterial", "publicKey", "keyid"],
252
+ ["verificationMaterial", "publicKey", "keyId"],
253
+ ["verificationMaterial", "certificate", "keyid"],
254
+ ["verificationMaterial", "certificate", "keyId"],
255
+ ]);
256
+ }
257
+
258
+ function collectProvenanceSignatures(value) {
259
+ return collectNestedValues(value, [
260
+ ["signature"],
261
+ ["sig"],
262
+ ["signatures", "sig"],
263
+ ["signatures", "signature"],
264
+ ]);
265
+ }
266
+
267
+ function collectProvenancePredicateTypes(value) {
268
+ return collectNestedValues(value, [
269
+ ["predicateType"],
270
+ ["predicate_type"],
271
+ ["statement", "predicateType"],
272
+ ["bundle", "predicateType"],
273
+ ]);
274
+ }
275
+
276
+ function hasTrustedPublishingEvidence(value) {
277
+ if (!value) {
278
+ return false;
279
+ }
280
+ if (typeof value === "boolean") {
281
+ return value;
282
+ }
283
+ if (typeof value === "string") {
284
+ return /(trusted|oidc|attestation|provenance)/i.test(value);
285
+ }
286
+ if (Array.isArray(value)) {
287
+ return value.some((entry) => hasTrustedPublishingEvidence(entry));
288
+ }
289
+ return Boolean(
290
+ normalizeProvenanceUrl(value) ||
291
+ extractNestedValue(value, [
292
+ ["trustedPublishing"],
293
+ ["trusted_publishing"],
294
+ ["isTrustedPublishing"],
295
+ ["verifiedPublisher"],
296
+ ["oidc"],
297
+ ["predicateType"],
298
+ ]),
299
+ );
300
+ }
301
+
302
+ /**
303
+ * Extract advanced npm provenance and publishing properties from registry metadata.
304
+ *
305
+ * @param {object} packument npm packument body
306
+ * @param {string | undefined} version package version
307
+ * @returns {object[]} custom properties
308
+ */
309
+ export function collectNpmRegistryProvenanceProperties(packument, version) {
310
+ const properties = [];
311
+ const versionBody = version ? packument?.versions?.[version] : undefined;
312
+ const publishTime = version ? packument?.time?.[version] : undefined;
313
+ const versionPublishTimes = Object.entries(packument?.time || {})
314
+ .filter(
315
+ ([entryName, entryValue]) =>
316
+ !["created", "modified"].includes(entryName) &&
317
+ typeof entryValue === "string" &&
318
+ parseTimestamp(entryValue) !== undefined,
319
+ )
320
+ .map(([entryName, entryValue]) => ({
321
+ timestamp: parseTimestamp(entryValue),
322
+ version: entryName,
323
+ }));
324
+ const currentPublishTimestamp = parseTimestamp(publishTime);
325
+ const priorReleaseEntry = sortReleaseEntries(
326
+ versionPublishTimes.filter(
327
+ (entry) =>
328
+ entry.version !== version &&
329
+ currentPublishTimestamp !== undefined &&
330
+ entry.timestamp < currentPublishTimestamp,
331
+ ),
332
+ ).pop();
333
+ const priorVersionBody = priorReleaseEntry
334
+ ? packument?.versions?.[priorReleaseEntry.version]
335
+ : undefined;
336
+ const currentMaintainerSet = uniqueIdentities([
337
+ ...extractMaintainerIdentities(versionBody?.maintainers),
338
+ versionBody?._npmUser?.name,
339
+ versionBody?._npmUser?.email,
340
+ versionBody?.publisher?.name,
341
+ versionBody?.publisher?.email,
342
+ ]);
343
+ const priorMaintainerSet = uniqueIdentities([
344
+ ...extractMaintainerIdentities(priorVersionBody?.maintainers),
345
+ priorVersionBody?._npmUser?.name,
346
+ priorVersionBody?._npmUser?.email,
347
+ priorVersionBody?.publisher?.name,
348
+ priorVersionBody?.publisher?.email,
349
+ ]);
350
+ const maintainerSetDrift = isDisjointIdentitySet(
351
+ currentMaintainerSet,
352
+ priorMaintainerSet,
353
+ );
354
+ const gapMetrics = releaseGapMetrics(versionPublishTimes, version);
355
+ const overlapMetrics = identityOverlapMetrics(
356
+ currentMaintainerSet,
357
+ priorMaintainerSet,
358
+ );
359
+ const cadenceMetrics = compressedCadenceMetrics(gapMetrics);
360
+ const publisherName =
361
+ versionBody?._npmUser?.name ||
362
+ versionBody?.publisher?.name ||
363
+ packument?.maintainers?.[0]?.name;
364
+ const publisherEmail =
365
+ versionBody?._npmUser?.email ||
366
+ versionBody?.publisher?.email ||
367
+ packument?.maintainers?.[0]?.email;
368
+ const provenanceCandidate =
369
+ versionBody?.dist?.provenance ||
370
+ versionBody?.provenance ||
371
+ versionBody?.dist?.attestations ||
372
+ versionBody?.attestations;
373
+ const provenanceUrl = normalizeProvenanceUrl(provenanceCandidate);
374
+ const provenanceDigests = collectProvenanceDigests(provenanceCandidate);
375
+ const provenanceKeyIds = collectProvenanceKeyIds(provenanceCandidate);
376
+ const provenanceSignatures = collectProvenanceSignatures(provenanceCandidate);
377
+ const provenancePredicateTypes =
378
+ collectProvenancePredicateTypes(provenanceCandidate);
379
+ const priorPublisherName =
380
+ priorVersionBody?._npmUser?.name || priorVersionBody?.publisher?.name;
381
+ const publisherDrift =
382
+ publisherName &&
383
+ priorPublisherName &&
384
+ publisherName.trim().toLowerCase() !==
385
+ priorPublisherName.trim().toLowerCase();
386
+
387
+ appendProperty(
388
+ properties,
389
+ "cdx:npm:packageCreatedTime",
390
+ packument?.time?.created,
391
+ );
392
+ appendProperty(
393
+ properties,
394
+ "cdx:npm:lastModifiedTime",
395
+ packument?.time?.modified,
396
+ );
397
+ appendProperty(properties, "cdx:npm:publishTime", publishTime);
398
+ appendProperty(properties, "cdx:npm:publisher", publisherName);
399
+ appendProperty(properties, "cdx:npm:publisherEmail", publisherEmail);
400
+ appendProperty(
401
+ properties,
402
+ "cdx:npm:maintainerSet",
403
+ currentMaintainerSet.join(", "),
404
+ );
405
+ appendProperty(
406
+ properties,
407
+ "cdx:npm:priorMaintainerSet",
408
+ priorMaintainerSet.join(", "),
409
+ );
410
+ appendProperty(
411
+ properties,
412
+ "cdx:npm:maintainerSetCount",
413
+ currentMaintainerSet.length,
414
+ );
415
+ appendProperty(
416
+ properties,
417
+ "cdx:npm:priorMaintainerSetCount",
418
+ priorMaintainerSet.length,
419
+ );
420
+ appendProperty(
421
+ properties,
422
+ "cdx:npm:maintainerOverlapCount",
423
+ overlapMetrics.overlapCount,
424
+ );
425
+ appendProperty(
426
+ properties,
427
+ "cdx:npm:maintainerOverlapRatio",
428
+ overlapMetrics.overlapRatio?.toFixed(2),
429
+ );
430
+ if (maintainerSetDrift) {
431
+ appendProperty(properties, "cdx:npm:maintainerSetDrift", "true");
432
+ }
433
+ if (overlapMetrics.partialDrift) {
434
+ appendProperty(properties, "cdx:npm:maintainerSetPartialDrift", "true");
435
+ }
436
+ appendProperty(
437
+ properties,
438
+ "cdx:npm:versionCount",
439
+ versionPublishTimes.length,
440
+ );
441
+ appendProperty(
442
+ properties,
443
+ "cdx:npm:releaseGapDays",
444
+ gapMetrics.currentGapDays?.toFixed(2),
445
+ );
446
+ appendProperty(
447
+ properties,
448
+ "cdx:npm:releaseGapBaselineDays",
449
+ gapMetrics.baselineDays?.toFixed(2),
450
+ );
451
+ appendProperty(
452
+ properties,
453
+ "cdx:npm:releaseGapSampleSize",
454
+ gapMetrics.sampleSize,
455
+ );
456
+ appendProperty(
457
+ properties,
458
+ "cdx:npm:releaseCadenceCompressionRatio",
459
+ cadenceMetrics.compressionRatio?.toFixed(2),
460
+ );
461
+ appendProperty(
462
+ properties,
463
+ "cdx:npm:priorVersion",
464
+ priorReleaseEntry?.version,
465
+ );
466
+ appendProperty(
467
+ properties,
468
+ "cdx:npm:priorPublishTime",
469
+ priorReleaseEntry
470
+ ? packument?.time?.[priorReleaseEntry.version]
471
+ : undefined,
472
+ );
473
+ appendProperty(properties, "cdx:npm:priorPublisher", priorPublisherName);
474
+ if (publisherDrift) {
475
+ appendProperty(properties, "cdx:npm:publisherDrift", "true");
476
+ }
477
+ if (cadenceMetrics.compressedCadence) {
478
+ appendProperty(properties, "cdx:npm:compressedCadence", "true");
479
+ }
480
+ if (hasTrustedPublishingEvidence(provenanceCandidate)) {
481
+ appendProperty(properties, "cdx:npm:trustedPublishing", "true");
482
+ }
483
+ appendProperty(
484
+ properties,
485
+ "cdx:npm:artifactIntegrity",
486
+ versionBody?.dist?.integrity,
487
+ );
488
+ appendProperty(
489
+ properties,
490
+ "cdx:npm:artifactShasum",
491
+ versionBody?.dist?.shasum,
492
+ );
493
+ appendProperty(properties, "cdx:npm:provenanceUrl", provenanceUrl);
494
+ appendJoinedProperty(
495
+ properties,
496
+ "cdx:npm:provenanceDigest",
497
+ provenanceDigests,
498
+ );
499
+ appendJoinedProperty(properties, "cdx:npm:provenanceKeyId", provenanceKeyIds);
500
+ appendJoinedProperty(
501
+ properties,
502
+ "cdx:npm:provenanceSignature",
503
+ provenanceSignatures,
504
+ );
505
+ appendJoinedProperty(
506
+ properties,
507
+ "cdx:npm:provenancePredicateType",
508
+ provenancePredicateTypes,
509
+ );
510
+ return properties;
511
+ }
512
+
513
+ /**
514
+ * Extract advanced PyPI provenance and publishing properties from registry metadata.
515
+ *
516
+ * @param {object} projectBody PyPI JSON body
517
+ * @param {string | undefined} version package version
518
+ * @returns {object[]} custom properties
519
+ */
520
+ export function collectPypiRegistryProvenanceProperties(projectBody, version) {
521
+ const properties = [];
522
+ const releaseEntries = [];
523
+ for (const [releaseVersion, releaseFilesForVersion] of Object.entries(
524
+ projectBody?.releases || {},
525
+ )) {
526
+ if (
527
+ !Array.isArray(releaseFilesForVersion) ||
528
+ !releaseFilesForVersion.length
529
+ ) {
530
+ continue;
531
+ }
532
+ const releaseUploadTimes = uniqueStrings(
533
+ releaseFilesForVersion.map(
534
+ (file) =>
535
+ file?.upload_time_iso_8601 || file?.upload_time || file?.uploadTime,
536
+ ),
537
+ );
538
+ const earliestUploadTime = releaseUploadTimes
539
+ .map((uploadTime) => ({
540
+ raw: uploadTime,
541
+ timestamp: parseTimestamp(uploadTime),
542
+ }))
543
+ .filter((entry) => entry.timestamp !== undefined)
544
+ .sort((left, right) => left.timestamp - right.timestamp)[0];
545
+ if (!earliestUploadTime) {
546
+ continue;
547
+ }
548
+ releaseEntries.push({
549
+ publishers: uniqueStrings(
550
+ releaseFilesForVersion.map(
551
+ (file) => file?.uploader || file?.uploaded_by,
552
+ ),
553
+ ),
554
+ timestamp: earliestUploadTime.timestamp,
555
+ uploadTime: earliestUploadTime.raw,
556
+ version: releaseVersion,
557
+ });
558
+ }
559
+ const releaseFiles = Array.isArray(projectBody?.releases?.[version])
560
+ ? projectBody.releases[version]
561
+ : Array.isArray(projectBody?.urls)
562
+ ? projectBody.urls
563
+ : [];
564
+ const uploadTimes = uniqueStrings(
565
+ releaseFiles.map(
566
+ (file) =>
567
+ file?.upload_time_iso_8601 || file?.upload_time || file?.uploadTime,
568
+ ),
569
+ );
570
+ const uploaders = uniqueStrings(
571
+ releaseFiles.map((file) => file?.uploader || file?.uploaded_by),
572
+ );
573
+ const provenanceUrls = uniqueStrings(
574
+ releaseFiles.map(
575
+ (file) =>
576
+ normalizeProvenanceUrl(file?.provenance) ||
577
+ normalizeProvenanceUrl(file?.attestations) ||
578
+ normalizeProvenanceUrl(file?.provenance_url) ||
579
+ normalizeProvenanceUrl(file?.attestation_url),
580
+ ),
581
+ );
582
+ const artifactDigestSha256 = uniqueStrings(
583
+ releaseFiles.map((file) => file?.digests?.sha256 || file?.sha256_digest),
584
+ );
585
+ const artifactDigestBlake2b256 = uniqueStrings(
586
+ releaseFiles.map((file) => file?.digests?.blake2b_256 || file?.blake2b_256),
587
+ );
588
+ const artifactDigestMd5 = uniqueStrings(
589
+ releaseFiles.map((file) => file?.digests?.md5 || file?.md5_digest),
590
+ );
591
+ const provenanceDigests = uniqueStrings(
592
+ releaseFiles.flatMap((file) =>
593
+ collectProvenanceDigests(
594
+ file?.provenance ||
595
+ file?.attestations ||
596
+ file?.provenance_url ||
597
+ file?.attestation_url,
598
+ ),
599
+ ),
600
+ );
601
+ const provenanceKeyIds = uniqueStrings(
602
+ releaseFiles.flatMap((file) =>
603
+ collectProvenanceKeyIds(file?.provenance || file?.attestations),
604
+ ),
605
+ );
606
+ const provenanceSignatures = uniqueStrings(
607
+ releaseFiles.flatMap((file) =>
608
+ collectProvenanceSignatures(file?.provenance || file?.attestations),
609
+ ),
610
+ );
611
+ const provenancePredicateTypes = uniqueStrings(
612
+ releaseFiles.flatMap((file) =>
613
+ collectProvenancePredicateTypes(file?.provenance || file?.attestations),
614
+ ),
615
+ );
616
+ const trustedPublishing = releaseFiles.some((file) =>
617
+ hasTrustedPublishingEvidence(
618
+ file?.provenance ||
619
+ file?.attestations ||
620
+ file?.trusted_publishing ||
621
+ file?.uploaded_via ||
622
+ file?.uploaded_using ||
623
+ file?.provenance_url,
624
+ ),
625
+ );
626
+ const uploaderVerified = releaseFiles.some(
627
+ (file) =>
628
+ file?.uploader_verified === true || file?.uploaderVerified === true,
629
+ );
630
+ const currentPublishTimestamp = parseTimestamp(uploadTimes[0]);
631
+ const priorReleaseEntry = sortReleaseEntries(
632
+ releaseEntries.filter(
633
+ (entry) =>
634
+ entry.version !== version &&
635
+ currentPublishTimestamp !== undefined &&
636
+ entry.timestamp < currentPublishTimestamp,
637
+ ),
638
+ ).pop();
639
+ const currentUploaders = uniqueStrings(uploaders);
640
+ const currentUploaderSet = uniqueIdentities(currentUploaders);
641
+ const priorUploaderSet = uniqueIdentities(
642
+ priorReleaseEntry?.publishers || [],
643
+ );
644
+ const uploaderSetDrift = isDisjointIdentitySet(
645
+ currentUploaderSet,
646
+ priorUploaderSet,
647
+ );
648
+ const gapMetrics = releaseGapMetrics(releaseEntries, version);
649
+ const overlapMetrics = identityOverlapMetrics(
650
+ currentUploaderSet,
651
+ priorUploaderSet,
652
+ );
653
+ const cadenceMetrics = compressedCadenceMetrics(gapMetrics);
654
+ const publisherDrift =
655
+ currentUploaders.length > 0 &&
656
+ priorReleaseEntry?.publishers?.length > 0 &&
657
+ currentUploaders.every(
658
+ (currentUploader) =>
659
+ !priorReleaseEntry.publishers.some(
660
+ (previousUploader) =>
661
+ previousUploader.toLowerCase() === currentUploader.toLowerCase(),
662
+ ),
663
+ );
664
+
665
+ appendProperty(
666
+ properties,
667
+ "cdx:pypi:packageCreatedTime",
668
+ sortReleaseEntries([...releaseEntries])[0]?.uploadTime,
669
+ );
670
+ appendProperty(properties, "cdx:pypi:publishTime", uploadTimes[0]);
671
+ appendProperty(properties, "cdx:pypi:versionCount", releaseEntries.length);
672
+ appendProperty(properties, "cdx:pypi:publisher", uploaders.join(", "));
673
+ appendProperty(
674
+ properties,
675
+ "cdx:pypi:uploaderSet",
676
+ currentUploaderSet.join(", "),
677
+ );
678
+ appendProperty(
679
+ properties,
680
+ "cdx:pypi:priorUploaderSet",
681
+ priorUploaderSet.join(", "),
682
+ );
683
+ appendProperty(
684
+ properties,
685
+ "cdx:pypi:uploaderSetCount",
686
+ currentUploaderSet.length,
687
+ );
688
+ appendProperty(
689
+ properties,
690
+ "cdx:pypi:priorUploaderSetCount",
691
+ priorUploaderSet.length,
692
+ );
693
+ appendProperty(
694
+ properties,
695
+ "cdx:pypi:uploaderOverlapCount",
696
+ overlapMetrics.overlapCount,
697
+ );
698
+ appendProperty(
699
+ properties,
700
+ "cdx:pypi:uploaderOverlapRatio",
701
+ overlapMetrics.overlapRatio?.toFixed(2),
702
+ );
703
+ if (uploaderSetDrift) {
704
+ appendProperty(properties, "cdx:pypi:uploaderSetDrift", "true");
705
+ }
706
+ if (overlapMetrics.partialDrift) {
707
+ appendProperty(properties, "cdx:pypi:uploaderSetPartialDrift", "true");
708
+ }
709
+ appendProperty(
710
+ properties,
711
+ "cdx:pypi:releaseGapDays",
712
+ gapMetrics.currentGapDays?.toFixed(2),
713
+ );
714
+ appendProperty(
715
+ properties,
716
+ "cdx:pypi:releaseGapBaselineDays",
717
+ gapMetrics.baselineDays?.toFixed(2),
718
+ );
719
+ appendProperty(
720
+ properties,
721
+ "cdx:pypi:releaseGapSampleSize",
722
+ gapMetrics.sampleSize,
723
+ );
724
+ appendProperty(
725
+ properties,
726
+ "cdx:pypi:releaseCadenceCompressionRatio",
727
+ cadenceMetrics.compressionRatio?.toFixed(2),
728
+ );
729
+ appendProperty(
730
+ properties,
731
+ "cdx:pypi:priorVersion",
732
+ priorReleaseEntry?.version,
733
+ );
734
+ appendProperty(
735
+ properties,
736
+ "cdx:pypi:priorPublishTime",
737
+ priorReleaseEntry?.uploadTime,
738
+ );
739
+ appendProperty(
740
+ properties,
741
+ "cdx:pypi:priorPublisher",
742
+ priorReleaseEntry?.publishers?.join(", "),
743
+ );
744
+ if (publisherDrift) {
745
+ appendProperty(properties, "cdx:pypi:publisherDrift", "true");
746
+ }
747
+ if (cadenceMetrics.compressedCadence) {
748
+ appendProperty(properties, "cdx:pypi:compressedCadence", "true");
749
+ }
750
+ if (uploaderVerified) {
751
+ appendProperty(properties, "cdx:pypi:uploaderVerified", "true");
752
+ }
753
+ if (trustedPublishing) {
754
+ appendProperty(properties, "cdx:pypi:trustedPublishing", "true");
755
+ }
756
+ appendJoinedProperty(
757
+ properties,
758
+ "cdx:pypi:artifactDigestSha256",
759
+ artifactDigestSha256,
760
+ );
761
+ appendJoinedProperty(
762
+ properties,
763
+ "cdx:pypi:artifactDigestBlake2b256",
764
+ artifactDigestBlake2b256,
765
+ );
766
+ appendJoinedProperty(
767
+ properties,
768
+ "cdx:pypi:artifactDigestMd5",
769
+ artifactDigestMd5,
770
+ );
771
+ appendProperty(properties, "cdx:pypi:provenanceUrl", provenanceUrls[0]);
772
+ appendJoinedProperty(
773
+ properties,
774
+ "cdx:pypi:provenanceDigest",
775
+ provenanceDigests,
776
+ );
777
+ appendJoinedProperty(
778
+ properties,
779
+ "cdx:pypi:provenanceKeyId",
780
+ provenanceKeyIds,
781
+ );
782
+ appendJoinedProperty(
783
+ properties,
784
+ "cdx:pypi:provenanceSignature",
785
+ provenanceSignatures,
786
+ );
787
+ appendJoinedProperty(
788
+ properties,
789
+ "cdx:pypi:provenancePredicateType",
790
+ provenancePredicateTypes,
791
+ );
792
+ return properties;
793
+ }