@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,145 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ getPropertyValue,
5
+ getProvenanceComponents,
6
+ getTrustedComponents,
7
+ getTrustedPublishingComponentCounts,
8
+ hasAnyPropertyValue,
9
+ hasComponentRegistryProvenance,
10
+ hasComponentRegistryProvenanceEvidence,
11
+ hasComponentTrustedPublishing,
12
+ hasRegistryProvenanceEvidenceProperties,
13
+ hasTrustedPublishingProperties,
14
+ } from "./provenanceUtils.js";
15
+
16
+ describe("provenanceUtils", () => {
17
+ const npmTrustedComponent = {
18
+ name: "left-pad",
19
+ properties: [
20
+ {
21
+ name: "cdx:npm:trustedPublishing",
22
+ value: "true",
23
+ },
24
+ ],
25
+ };
26
+ const pypiProvenanceComponent = {
27
+ name: "requests",
28
+ properties: [
29
+ {
30
+ name: "cdx:pypi:provenanceUrl",
31
+ value: "https://pypi.org/integrity/example",
32
+ },
33
+ ],
34
+ };
35
+ const plainComponent = {
36
+ name: "lodash",
37
+ properties: [],
38
+ };
39
+
40
+ it("detects trusted publishing and registry provenance metadata", () => {
41
+ assert.strictEqual(
42
+ hasComponentTrustedPublishing(npmTrustedComponent),
43
+ true,
44
+ );
45
+ assert.strictEqual(
46
+ hasComponentRegistryProvenance(pypiProvenanceComponent),
47
+ true,
48
+ );
49
+ assert.strictEqual(
50
+ hasComponentRegistryProvenanceEvidence(pypiProvenanceComponent),
51
+ true,
52
+ );
53
+ assert.strictEqual(hasComponentRegistryProvenance(plainComponent), false);
54
+ });
55
+
56
+ it("filters trusted components and counts trusted publishing by ecosystem", () => {
57
+ assert.deepStrictEqual(
58
+ getTrustedComponents([
59
+ plainComponent,
60
+ npmTrustedComponent,
61
+ pypiProvenanceComponent,
62
+ ]).map((component) => component.name),
63
+ ["left-pad"],
64
+ );
65
+ assert.deepStrictEqual(
66
+ getProvenanceComponents([
67
+ plainComponent,
68
+ npmTrustedComponent,
69
+ pypiProvenanceComponent,
70
+ ]).map((component) => component.name),
71
+ ["requests"],
72
+ );
73
+ assert.deepStrictEqual(
74
+ getTrustedPublishingComponentCounts([
75
+ npmTrustedComponent,
76
+ {
77
+ name: "urllib3",
78
+ properties: [
79
+ {
80
+ name: "cdx:pypi:trustedPublishing",
81
+ value: "true",
82
+ },
83
+ ],
84
+ },
85
+ plainComponent,
86
+ ]),
87
+ {
88
+ npm: 1,
89
+ pypi: 1,
90
+ total: 2,
91
+ },
92
+ );
93
+ });
94
+
95
+ it("supports property-array checks used by display and audit code", () => {
96
+ const properties = [
97
+ {
98
+ name: "cdx:npm:provenanceKeyId",
99
+ value: "sigstore-key",
100
+ },
101
+ {
102
+ name: "cdx:npm:trustedPublishing",
103
+ value: "true",
104
+ },
105
+ ];
106
+ assert.strictEqual(
107
+ getPropertyValue(properties, "cdx:npm:provenanceKeyId"),
108
+ "sigstore-key",
109
+ );
110
+ assert.strictEqual(
111
+ hasAnyPropertyValue(properties, ["cdx:npm:provenanceKeyId"]),
112
+ true,
113
+ );
114
+ assert.strictEqual(hasTrustedPublishingProperties(properties), true);
115
+ assert.strictEqual(
116
+ hasRegistryProvenanceEvidenceProperties(properties),
117
+ true,
118
+ );
119
+ });
120
+
121
+ it("counts total trusted publishing components once even with multiple registry flags", () => {
122
+ assert.deepStrictEqual(
123
+ getTrustedPublishingComponentCounts([
124
+ {
125
+ name: "dual-published",
126
+ properties: [
127
+ {
128
+ name: "cdx:npm:trustedPublishing",
129
+ value: "true",
130
+ },
131
+ {
132
+ name: "cdx:pypi:trustedPublishing",
133
+ value: "true",
134
+ },
135
+ ],
136
+ },
137
+ ]),
138
+ {
139
+ npm: 1,
140
+ pypi: 1,
141
+ total: 1,
142
+ },
143
+ );
144
+ });
145
+ });
@@ -0,0 +1,281 @@
1
+ import { basename } from "node:path";
2
+
3
+ const PYLOCK_FILE_REGEX = /^pylock(\.[^.]+)?\.toml$/;
4
+ const DEFAULT_PYPI_REGISTRIES = new Set([
5
+ "https://pypi.org/simple",
6
+ "https://pypi.org/simple/",
7
+ ]);
8
+
9
+ const PYLOCK_TOP_LEVEL_KEYS = [
10
+ "lock-version",
11
+ "environments",
12
+ "requires-python",
13
+ "extras",
14
+ "dependency-groups",
15
+ "default-groups",
16
+ "created-by",
17
+ "tool",
18
+ ];
19
+
20
+ const PYLOCK_PACKAGE_CUSTOM_KEYS = [
21
+ "marker",
22
+ "index",
23
+ "dependencies",
24
+ "extras",
25
+ "dependency-groups",
26
+ "attestation-identities",
27
+ "tool",
28
+ "vcs",
29
+ "directory",
30
+ "archive",
31
+ "sdist",
32
+ "wheels",
33
+ ];
34
+
35
+ /**
36
+ * Check whether a file name conforms to pylock naming.
37
+ *
38
+ * @param {string} lockFilePath lock file path
39
+ * @returns {boolean} true if this is a pylock file
40
+ */
41
+ export function isPyLockFile(lockFilePath) {
42
+ if (!lockFilePath) {
43
+ return false;
44
+ }
45
+ return PYLOCK_FILE_REGEX.test(basename(lockFilePath));
46
+ }
47
+
48
+ /**
49
+ * Check whether a parsed toml object follows pylock format.
50
+ *
51
+ * @param {object} lockTomlObj parsed toml object
52
+ * @returns {boolean} true if object appears to be pylock data
53
+ */
54
+ export function isPyLockObject(lockTomlObj) {
55
+ return !!(
56
+ lockTomlObj?.["lock-version"] && Array.isArray(lockTomlObj.packages)
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Get package entries from py lock data in a format-agnostic way.
62
+ *
63
+ * @param {object} lockTomlObj parsed toml object
64
+ * @returns {Array<object>} package entries
65
+ */
66
+ export function getPyLockPackages(lockTomlObj) {
67
+ if (Array.isArray(lockTomlObj?.package)) {
68
+ return lockTomlObj.package;
69
+ }
70
+ if (Array.isArray(lockTomlObj?.packages)) {
71
+ return lockTomlObj.packages;
72
+ }
73
+ return [];
74
+ }
75
+
76
+ /**
77
+ * Convert top-level pylock keys to custom cdx properties.
78
+ *
79
+ * @param {object} lockTomlObj parsed toml object
80
+ * @returns {Array<object>} custom properties
81
+ */
82
+ export function collectPyLockTopLevelProperties(lockTomlObj) {
83
+ const properties = [];
84
+ for (const akey of PYLOCK_TOP_LEVEL_KEYS) {
85
+ if (lockTomlObj?.[akey] !== undefined) {
86
+ properties.push({
87
+ name: `cdx:pylock:${akey.replaceAll("-", "_")}`,
88
+ value: toPropertyValue(lockTomlObj[akey]),
89
+ });
90
+ }
91
+ }
92
+ return properties;
93
+ }
94
+
95
+ /**
96
+ * Convert package-level pylock keys to custom cdx properties.
97
+ *
98
+ * @param {object} pkg pylock package entry
99
+ * @returns {Array<object>} custom properties
100
+ */
101
+ export function collectPyLockPackageProperties(pkg) {
102
+ const properties = [];
103
+ for (const akey of PYLOCK_PACKAGE_CUSTOM_KEYS) {
104
+ if (pkg?.[akey] !== undefined) {
105
+ properties.push({
106
+ name: `cdx:pylock:${akey.replaceAll("-", "_")}`,
107
+ value: toPropertyValue(pkg[akey]),
108
+ });
109
+ }
110
+ }
111
+ return properties;
112
+ }
113
+
114
+ /**
115
+ * Build file components from pylock source entries.
116
+ *
117
+ * @param {object} pkg pylock package entry
118
+ * @param {string} lockFile lock file path
119
+ * @returns {Array<object>} file components
120
+ */
121
+ export function collectPyLockFileComponents(pkg, lockFile) {
122
+ const fileComponents = [];
123
+ if (pkg?.archive) {
124
+ const archiveComp = createArtifactComponent(
125
+ pkg.archive,
126
+ "archive",
127
+ lockFile,
128
+ pkg.name,
129
+ );
130
+ if (archiveComp) {
131
+ fileComponents.push(archiveComp);
132
+ }
133
+ }
134
+ if (pkg?.sdist) {
135
+ const sdistComp = createArtifactComponent(
136
+ pkg.sdist,
137
+ "sdist",
138
+ lockFile,
139
+ pkg.name,
140
+ );
141
+ if (sdistComp) {
142
+ fileComponents.push(sdistComp);
143
+ }
144
+ }
145
+ if (Array.isArray(pkg?.wheels)) {
146
+ for (const awheel of pkg.wheels) {
147
+ const wheelComp = createArtifactComponent(
148
+ awheel,
149
+ "wheel",
150
+ lockFile,
151
+ pkg.name,
152
+ );
153
+ if (wheelComp) {
154
+ fileComponents.push(wheelComp);
155
+ }
156
+ }
157
+ }
158
+ return fileComponents;
159
+ }
160
+
161
+ /**
162
+ * Check whether index points to the default pypi registry.
163
+ *
164
+ * @param {string} indexUrl index URL from pylock
165
+ * @returns {boolean} true for default pypi
166
+ */
167
+ export function isDefaultPypiRegistry(indexUrl) {
168
+ if (!indexUrl) {
169
+ return false;
170
+ }
171
+ return DEFAULT_PYPI_REGISTRIES.has(indexUrl);
172
+ }
173
+
174
+ function createArtifactComponent(artifact, sourceType, lockFile, packageName) {
175
+ if (!artifact) {
176
+ return null;
177
+ }
178
+ const properties = [{ name: "SrcFile", value: lockFile }];
179
+ properties.push({
180
+ name: "cdx:pylock:file:source_type",
181
+ value: sourceType,
182
+ });
183
+ if (artifact.url) {
184
+ properties.push({
185
+ name: "cdx:pylock:file:url",
186
+ value: artifact.url,
187
+ });
188
+ }
189
+ if (artifact.path) {
190
+ properties.push({
191
+ name: "cdx:pylock:file:path",
192
+ value: artifact.path,
193
+ });
194
+ }
195
+ if (artifact.size !== undefined) {
196
+ properties.push({
197
+ name: "cdx:pylock:file:size",
198
+ value: `${artifact.size}`,
199
+ });
200
+ }
201
+ if (artifact["upload-time"]) {
202
+ properties.push({
203
+ name: "cdx:pylock:file:upload_time",
204
+ value: toPropertyValue(artifact["upload-time"]),
205
+ });
206
+ }
207
+ if (artifact.subdirectory) {
208
+ properties.push({
209
+ name: "cdx:pylock:file:subdirectory",
210
+ value: artifact.subdirectory,
211
+ });
212
+ }
213
+ return {
214
+ type: "file",
215
+ name: resolveArtifactName(artifact, packageName, sourceType),
216
+ hashes: toHashes(artifact.hashes),
217
+ properties,
218
+ };
219
+ }
220
+
221
+ function toHashes(hashesObj) {
222
+ if (!hashesObj || typeof hashesObj !== "object") {
223
+ return undefined;
224
+ }
225
+ const hashes = [];
226
+ for (const [alg, content] of Object.entries(hashesObj)) {
227
+ if (!content) {
228
+ continue;
229
+ }
230
+ const normalizedAlg = normalizeHashAlgorithm(alg);
231
+ hashes.push({ alg: normalizedAlg, content: `${content}` });
232
+ }
233
+ return hashes.length ? hashes : undefined;
234
+ }
235
+
236
+ function resolveArtifactName(artifact, packageName, sourceType) {
237
+ if (artifact.name) {
238
+ return artifact.name;
239
+ }
240
+ if (artifact.path) {
241
+ return basename(artifact.path);
242
+ }
243
+ if (artifact.url) {
244
+ try {
245
+ return basename(new URL(artifact.url).pathname);
246
+ } catch (_err) {
247
+ return `${packageName || "package"}-${sourceType}-invalid-url`;
248
+ }
249
+ }
250
+ return `${packageName || "package"}-${sourceType}`;
251
+ }
252
+
253
+ function normalizeHashAlgorithm(algorithm) {
254
+ const normalized = `${algorithm}`.toLowerCase();
255
+ if (normalized.startsWith("sha3")) {
256
+ return `SHA3-${normalized.slice(4).replace(/^[-_]/, "")}`;
257
+ }
258
+ if (normalized.startsWith("sha")) {
259
+ return `SHA-${normalized.slice(3).replace(/^[-_]/, "").toUpperCase()}`;
260
+ }
261
+ return normalized.toUpperCase();
262
+ }
263
+
264
+ function toPropertyValue(value) {
265
+ if (value === null) {
266
+ return "null";
267
+ }
268
+ if (value === undefined) {
269
+ return "";
270
+ }
271
+ if (typeof value === "string") {
272
+ return value;
273
+ }
274
+ if (typeof value === "number" || typeof value === "boolean") {
275
+ return `${value}`;
276
+ }
277
+ if (value instanceof Date) {
278
+ return value.toISOString();
279
+ }
280
+ return JSON.stringify(value);
281
+ }
@@ -0,0 +1,48 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ collectPyLockTopLevelProperties,
5
+ getPyLockPackages,
6
+ isPyLockFile,
7
+ isPyLockObject,
8
+ } from "./pylockutils.js";
9
+
10
+ describe("pylockutils", () => {
11
+ it("detects valid pylock file names", () => {
12
+ assert.ok(isPyLockFile("/tmp/pylock.toml"));
13
+ assert.ok(isPyLockFile("/tmp/pylock.api.toml"));
14
+ assert.ok(!isPyLockFile("/tmp/poetry.lock"));
15
+ });
16
+
17
+ it("detects pylock object shape and packages", () => {
18
+ const pylockData = {
19
+ "lock-version": "1.0",
20
+ packages: [{ name: "attrs", version: "1.0.0" }],
21
+ };
22
+ assert.ok(isPyLockObject(pylockData));
23
+ assert.deepStrictEqual(getPyLockPackages(pylockData).length, 1);
24
+ });
25
+
26
+ it("collects pylock top-level custom properties", () => {
27
+ const properties = collectPyLockTopLevelProperties({
28
+ "lock-version": "1.0",
29
+ "requires-python": ">=3.11",
30
+ "created-by": "uv",
31
+ });
32
+ assert.ok(
33
+ properties.some(
34
+ (p) => p.name === "cdx:pylock:lock_version" && p.value === "1.0",
35
+ ),
36
+ );
37
+ assert.ok(
38
+ properties.some(
39
+ (p) => p.name === "cdx:pylock:requires_python" && p.value === ">=3.11",
40
+ ),
41
+ );
42
+ assert.ok(
43
+ properties.some(
44
+ (p) => p.name === "cdx:pylock:created_by" && p.value === "uv",
45
+ ),
46
+ );
47
+ });
48
+ });