@cyclonedx/cdxgen 12.2.1 → 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 (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  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 +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  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/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
@@ -0,0 +1,452 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ collectNpmRegistryProvenanceProperties,
5
+ collectPypiRegistryProvenanceProperties,
6
+ } from "./registryProvenance.js";
7
+
8
+ function getProperty(properties, propertyName) {
9
+ return properties.find((property) => property.name === propertyName)?.value;
10
+ }
11
+
12
+ describe("collectNpmRegistryProvenanceProperties()", () => {
13
+ it("extracts trusted publishing and publisher details from npm metadata", () => {
14
+ const properties = collectNpmRegistryProvenanceProperties(
15
+ {
16
+ time: {
17
+ "1.2.0": "2025-01-01T10:00:00.000Z",
18
+ "1.2.1": "2025-01-15T10:00:00.000Z",
19
+ "1.2.2": "2026-03-01T10:00:00.000Z",
20
+ "1.2.3": "2026-04-01T10:00:00.000Z",
21
+ created: "2024-01-01T10:00:00.000Z",
22
+ modified: "2026-04-01T10:00:00.000Z",
23
+ },
24
+ versions: {
25
+ "1.2.0": {
26
+ _npmUser: {
27
+ name: "previous-publisher",
28
+ },
29
+ maintainers: [{ name: "alice" }, { email: "alice@example.com" }],
30
+ },
31
+ "1.2.1": {
32
+ _npmUser: {
33
+ name: "previous-publisher",
34
+ },
35
+ maintainers: [{ name: "alice" }, { email: "alice@example.com" }],
36
+ },
37
+ "1.2.2": {
38
+ _npmUser: {
39
+ name: "previous-publisher",
40
+ },
41
+ maintainers: [{ name: "alice" }, { email: "alice@example.com" }],
42
+ },
43
+ "1.2.3": {
44
+ _npmUser: {
45
+ email: "publisher@example.com",
46
+ name: "publisher",
47
+ },
48
+ maintainers: [{ name: "bob" }, { email: "bob@example.com" }],
49
+ dist: {
50
+ integrity: "sha512-artifact-integrity",
51
+ provenance: {
52
+ predicateType: "https://slsa.dev/provenance/v1",
53
+ signatures: [
54
+ {
55
+ keyid: "sigstore-npm-key",
56
+ sig: "MEUCIQDsig",
57
+ },
58
+ ],
59
+ subject: {
60
+ digest: {
61
+ sha256: "npm-subject-digest",
62
+ },
63
+ },
64
+ url: "https://registry.npmjs.org/-/npm/v1/attestations/example@1.2.3",
65
+ },
66
+ shasum: "deadbeefcafebabe",
67
+ },
68
+ },
69
+ },
70
+ },
71
+ "1.2.3",
72
+ );
73
+
74
+ assert.strictEqual(
75
+ getProperty(properties, "cdx:npm:trustedPublishing"),
76
+ "true",
77
+ );
78
+ assert.strictEqual(
79
+ getProperty(properties, "cdx:npm:provenanceUrl"),
80
+ "https://registry.npmjs.org/-/npm/v1/attestations/example@1.2.3",
81
+ );
82
+ assert.strictEqual(
83
+ getProperty(properties, "cdx:npm:publisher"),
84
+ "publisher",
85
+ );
86
+ assert.strictEqual(
87
+ getProperty(properties, "cdx:npm:publisherEmail"),
88
+ "publisher@example.com",
89
+ );
90
+ assert.strictEqual(
91
+ getProperty(properties, "cdx:npm:publishTime"),
92
+ "2026-04-01T10:00:00.000Z",
93
+ );
94
+ assert.strictEqual(
95
+ getProperty(properties, "cdx:npm:artifactIntegrity"),
96
+ "sha512-artifact-integrity",
97
+ );
98
+ assert.strictEqual(
99
+ getProperty(properties, "cdx:npm:artifactShasum"),
100
+ "deadbeefcafebabe",
101
+ );
102
+ assert.strictEqual(
103
+ getProperty(properties, "cdx:npm:provenanceDigest"),
104
+ "npm-subject-digest",
105
+ );
106
+ assert.strictEqual(
107
+ getProperty(properties, "cdx:npm:provenanceKeyId"),
108
+ "sigstore-npm-key",
109
+ );
110
+ assert.strictEqual(
111
+ getProperty(properties, "cdx:npm:provenancePredicateType"),
112
+ "https://slsa.dev/provenance/v1",
113
+ );
114
+ assert.strictEqual(
115
+ getProperty(properties, "cdx:npm:provenanceSignature"),
116
+ "MEUCIQDsig",
117
+ );
118
+ assert.strictEqual(getProperty(properties, "cdx:npm:versionCount"), "4");
119
+ assert.strictEqual(
120
+ getProperty(properties, "cdx:npm:priorVersion"),
121
+ "1.2.2",
122
+ );
123
+ assert.strictEqual(
124
+ getProperty(properties, "cdx:npm:priorPublisher"),
125
+ "previous-publisher",
126
+ );
127
+ assert.strictEqual(
128
+ getProperty(properties, "cdx:npm:publisherDrift"),
129
+ "true",
130
+ );
131
+ assert.strictEqual(
132
+ getProperty(properties, "cdx:npm:packageCreatedTime"),
133
+ "2024-01-01T10:00:00.000Z",
134
+ );
135
+ assert.strictEqual(
136
+ getProperty(properties, "cdx:npm:maintainerSet"),
137
+ "bob, bob@example.com, publisher, publisher@example.com",
138
+ );
139
+ assert.strictEqual(
140
+ getProperty(properties, "cdx:npm:priorMaintainerSet"),
141
+ "alice, alice@example.com, previous-publisher",
142
+ );
143
+ assert.strictEqual(
144
+ getProperty(properties, "cdx:npm:maintainerSetDrift"),
145
+ "true",
146
+ );
147
+ assert.strictEqual(
148
+ getProperty(properties, "cdx:npm:releaseGapDays"),
149
+ "31.00",
150
+ );
151
+ assert.strictEqual(
152
+ getProperty(properties, "cdx:npm:releaseGapBaselineDays"),
153
+ "212.00",
154
+ );
155
+ assert.strictEqual(
156
+ getProperty(properties, "cdx:npm:releaseGapSampleSize"),
157
+ "2",
158
+ );
159
+ });
160
+
161
+ it("extracts compressed cadence and partial maintainer overlap drift from npm metadata", () => {
162
+ const properties = collectNpmRegistryProvenanceProperties(
163
+ {
164
+ time: {
165
+ "0.9.0": "2024-11-01T10:00:00.000Z",
166
+ "1.0.0": "2025-01-01T10:00:00.000Z",
167
+ "1.1.0": "2025-03-15T10:00:00.000Z",
168
+ "1.2.0": "2025-05-27T10:00:00.000Z",
169
+ "1.2.1": "2025-06-05T10:00:00.000Z",
170
+ created: "2024-01-01T10:00:00.000Z",
171
+ modified: "2025-06-05T10:00:00.000Z",
172
+ },
173
+ versions: {
174
+ "0.9.0": {
175
+ _npmUser: {
176
+ name: "alice",
177
+ },
178
+ },
179
+ "1.0.0": {
180
+ _npmUser: {
181
+ name: "alice",
182
+ },
183
+ },
184
+ "1.1.0": {
185
+ _npmUser: {
186
+ name: "alice",
187
+ },
188
+ },
189
+ "1.2.0": {
190
+ _npmUser: {
191
+ name: "bob",
192
+ },
193
+ maintainers: [{ name: "alice" }],
194
+ },
195
+ "1.2.1": {
196
+ _npmUser: {
197
+ name: "bob",
198
+ },
199
+ maintainers: [{ name: "charlie" }],
200
+ },
201
+ },
202
+ },
203
+ "1.2.1",
204
+ );
205
+
206
+ assert.strictEqual(
207
+ getProperty(properties, "cdx:npm:maintainerSetPartialDrift"),
208
+ "true",
209
+ );
210
+ assert.strictEqual(
211
+ getProperty(properties, "cdx:npm:maintainerOverlapCount"),
212
+ "1",
213
+ );
214
+ assert.strictEqual(
215
+ getProperty(properties, "cdx:npm:maintainerOverlapRatio"),
216
+ "0.33",
217
+ );
218
+ assert.strictEqual(
219
+ getProperty(properties, "cdx:npm:compressedCadence"),
220
+ "true",
221
+ );
222
+ assert.strictEqual(
223
+ getProperty(properties, "cdx:npm:releaseCadenceCompressionRatio"),
224
+ "0.12",
225
+ );
226
+ assert.strictEqual(
227
+ getProperty(properties, "cdx:npm:maintainerSetDrift"),
228
+ undefined,
229
+ );
230
+ });
231
+ });
232
+
233
+ describe("collectPypiRegistryProvenanceProperties()", () => {
234
+ it("extracts trusted publishing and uploader details from PyPI metadata", () => {
235
+ const properties = collectPypiRegistryProvenanceProperties(
236
+ {
237
+ releases: {
238
+ "1.7.0": [
239
+ {
240
+ upload_time_iso_8601: "2025-11-20T08:15:30.000Z",
241
+ uploader: "previous-uploader",
242
+ },
243
+ ],
244
+ "1.8.0": [
245
+ {
246
+ upload_time_iso_8601: "2025-12-05T08:15:30.000Z",
247
+ uploader: "previous-uploader",
248
+ },
249
+ ],
250
+ "1.9.0": [
251
+ {
252
+ upload_time_iso_8601: "2025-12-20T08:15:30.000Z",
253
+ uploader: "previous-uploader",
254
+ },
255
+ ],
256
+ "2.0.0": [
257
+ {
258
+ digests: {
259
+ blake2b_256: "pypi-blake",
260
+ sha256: "pypi-sha256",
261
+ },
262
+ md5_digest: "pypi-md5",
263
+ provenance: {
264
+ predicateType: "https://docs.pypi.org/attestations/publish/v1",
265
+ signatures: [
266
+ {
267
+ keyid: "sigstore-pypi-key",
268
+ sig: "c2lnbmF0dXJl",
269
+ },
270
+ ],
271
+ subject: {
272
+ digest: {
273
+ sha256: "pypi-provenance-digest",
274
+ },
275
+ },
276
+ },
277
+ provenance_url:
278
+ "https://pypi.org/integrity/example/2.0.0/example-2.0.0.tar.gz/provenance",
279
+ upload_time_iso_8601: "2026-03-20T08:15:30.000Z",
280
+ uploader: "trusted-publisher",
281
+ uploader_verified: true,
282
+ },
283
+ ],
284
+ },
285
+ },
286
+ "2.0.0",
287
+ );
288
+
289
+ assert.strictEqual(
290
+ getProperty(properties, "cdx:pypi:trustedPublishing"),
291
+ "true",
292
+ );
293
+ assert.strictEqual(
294
+ getProperty(properties, "cdx:pypi:provenanceUrl"),
295
+ "https://pypi.org/integrity/example/2.0.0/example-2.0.0.tar.gz/provenance",
296
+ );
297
+ assert.strictEqual(
298
+ getProperty(properties, "cdx:pypi:publishTime"),
299
+ "2026-03-20T08:15:30.000Z",
300
+ );
301
+ assert.strictEqual(
302
+ getProperty(properties, "cdx:pypi:publisher"),
303
+ "trusted-publisher",
304
+ );
305
+ assert.strictEqual(
306
+ getProperty(properties, "cdx:pypi:uploaderVerified"),
307
+ "true",
308
+ );
309
+ assert.strictEqual(
310
+ getProperty(properties, "cdx:pypi:artifactDigestSha256"),
311
+ "pypi-sha256",
312
+ );
313
+ assert.strictEqual(
314
+ getProperty(properties, "cdx:pypi:artifactDigestBlake2b256"),
315
+ "pypi-blake",
316
+ );
317
+ assert.strictEqual(
318
+ getProperty(properties, "cdx:pypi:artifactDigestMd5"),
319
+ "pypi-md5",
320
+ );
321
+ assert.strictEqual(
322
+ getProperty(properties, "cdx:pypi:provenanceDigest"),
323
+ "pypi-provenance-digest",
324
+ );
325
+ assert.strictEqual(
326
+ getProperty(properties, "cdx:pypi:provenanceKeyId"),
327
+ "sigstore-pypi-key",
328
+ );
329
+ assert.strictEqual(
330
+ getProperty(properties, "cdx:pypi:provenancePredicateType"),
331
+ "https://docs.pypi.org/attestations/publish/v1",
332
+ );
333
+ assert.strictEqual(
334
+ getProperty(properties, "cdx:pypi:provenanceSignature"),
335
+ "c2lnbmF0dXJl",
336
+ );
337
+ assert.strictEqual(getProperty(properties, "cdx:pypi:versionCount"), "4");
338
+ assert.strictEqual(
339
+ getProperty(properties, "cdx:pypi:priorVersion"),
340
+ "1.9.0",
341
+ );
342
+ assert.strictEqual(
343
+ getProperty(properties, "cdx:pypi:priorPublisher"),
344
+ "previous-uploader",
345
+ );
346
+ assert.strictEqual(
347
+ getProperty(properties, "cdx:pypi:publisherDrift"),
348
+ "true",
349
+ );
350
+ assert.strictEqual(
351
+ getProperty(properties, "cdx:pypi:packageCreatedTime"),
352
+ "2025-11-20T08:15:30.000Z",
353
+ );
354
+ assert.strictEqual(
355
+ getProperty(properties, "cdx:pypi:uploaderSet"),
356
+ "trusted-publisher",
357
+ );
358
+ assert.strictEqual(
359
+ getProperty(properties, "cdx:pypi:priorUploaderSet"),
360
+ "previous-uploader",
361
+ );
362
+ assert.strictEqual(
363
+ getProperty(properties, "cdx:pypi:uploaderSetDrift"),
364
+ "true",
365
+ );
366
+ assert.strictEqual(
367
+ getProperty(properties, "cdx:pypi:releaseGapDays"),
368
+ "90.00",
369
+ );
370
+ assert.strictEqual(
371
+ getProperty(properties, "cdx:pypi:releaseGapBaselineDays"),
372
+ "15.00",
373
+ );
374
+ assert.strictEqual(
375
+ getProperty(properties, "cdx:pypi:releaseGapSampleSize"),
376
+ "2",
377
+ );
378
+ });
379
+
380
+ it("extracts compressed cadence and partial uploader overlap drift from PyPI metadata", () => {
381
+ const properties = collectPypiRegistryProvenanceProperties(
382
+ {
383
+ releases: {
384
+ "0.9.0": [
385
+ {
386
+ upload_time_iso_8601: "2024-11-01T08:15:30.000Z",
387
+ uploader: "alice",
388
+ },
389
+ ],
390
+ "1.0.0": [
391
+ {
392
+ upload_time_iso_8601: "2025-01-01T08:15:30.000Z",
393
+ uploader: "alice",
394
+ },
395
+ ],
396
+ "1.1.0": [
397
+ {
398
+ upload_time_iso_8601: "2025-03-15T08:15:30.000Z",
399
+ uploader: "alice",
400
+ },
401
+ ],
402
+ "1.2.0": [
403
+ {
404
+ upload_time_iso_8601: "2025-05-27T08:15:30.000Z",
405
+ uploader: "alice",
406
+ },
407
+ {
408
+ upload_time_iso_8601: "2025-05-27T08:16:30.000Z",
409
+ uploader: "bob",
410
+ },
411
+ ],
412
+ "1.2.1": [
413
+ {
414
+ upload_time_iso_8601: "2025-06-05T08:15:30.000Z",
415
+ uploader: "bob",
416
+ },
417
+ {
418
+ upload_time_iso_8601: "2025-06-05T08:16:30.000Z",
419
+ uploader: "charlie",
420
+ },
421
+ ],
422
+ },
423
+ },
424
+ "1.2.1",
425
+ );
426
+
427
+ assert.strictEqual(
428
+ getProperty(properties, "cdx:pypi:uploaderSetPartialDrift"),
429
+ "true",
430
+ );
431
+ assert.strictEqual(
432
+ getProperty(properties, "cdx:pypi:uploaderOverlapCount"),
433
+ "1",
434
+ );
435
+ assert.strictEqual(
436
+ getProperty(properties, "cdx:pypi:uploaderOverlapRatio"),
437
+ "0.33",
438
+ );
439
+ assert.strictEqual(
440
+ getProperty(properties, "cdx:pypi:compressedCadence"),
441
+ "true",
442
+ );
443
+ assert.strictEqual(
444
+ getProperty(properties, "cdx:pypi:releaseCadenceCompressionRatio"),
445
+ "0.12",
446
+ );
447
+ assert.strictEqual(
448
+ getProperty(properties, "cdx:pypi:uploaderSetDrift"),
449
+ undefined,
450
+ );
451
+ });
452
+ });