@cdxgen/cdxgen 13.1.0 → 13.2.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 (186) hide show
  1. package/README.md +8 -7
  2. package/bin/cdxgen.js +16 -3
  3. package/bin/evinse.js +2 -0
  4. package/data/crypto-oid.json +88 -12
  5. package/data/remediations.json +23 -0
  6. package/data/rules/ai-bom.yaml +78 -0
  7. package/data/rules/cargo-rust.yaml +132 -0
  8. package/data/rules/mcp-servers.yaml +122 -0
  9. package/lib/audit/targets.js +6 -1
  10. package/lib/cli/bomAssembly.js +12 -0
  11. package/lib/cli/index.js +192 -1
  12. package/lib/cli/jsBom.js +59 -2
  13. package/lib/cli/jvmBom.js +22 -0
  14. package/lib/cli/managedBom.js +152 -0
  15. package/lib/cli/nativeBom.js +798 -1
  16. package/lib/core/activity.js +120 -2
  17. package/lib/core/env.js +39 -1
  18. package/lib/core/userAgent.js +151 -0
  19. package/lib/ecosystems/cargoCache.js +364 -0
  20. package/lib/ecosystems/caxa.js +13 -5
  21. package/lib/ecosystems/cppEvidence.js +40 -2
  22. package/lib/ecosystems/ecosystems.js +241 -91
  23. package/lib/ecosystems/golem.js +11 -1
  24. package/lib/ecosystems/gradleutils.js +143 -2
  25. package/lib/ecosystems/kosi.js +1256 -0
  26. package/lib/ecosystems/parsers-crystal.js +179 -0
  27. package/lib/ecosystems/parsers-dotnet.js +69 -39
  28. package/lib/ecosystems/parsers-elm.js +816 -0
  29. package/lib/ecosystems/parsers-gleam.js +1 -1
  30. package/lib/ecosystems/parsers-julia.js +268 -0
  31. package/lib/ecosystems/parsers-lua.js +218 -0
  32. package/lib/ecosystems/parsers-misc.js +172 -0
  33. package/lib/ecosystems/parsers-nim.js +221 -0
  34. package/lib/ecosystems/parsers-opam.js +180 -0
  35. package/lib/ecosystems/parsers-perl.js +277 -0
  36. package/lib/ecosystems/parsers-python.js +1 -1
  37. package/lib/ecosystems/parsers-r.js +148 -0
  38. package/lib/ecosystems/parsers-rebar.js +411 -0
  39. package/lib/ecosystems/parsers-rust.js +932 -17
  40. package/lib/ecosystems/parsers-spack.js +204 -0
  41. package/lib/ecosystems/parsers-stack.js +264 -0
  42. package/lib/ecosystems/parsers-terraform.js +194 -0
  43. package/lib/ecosystems/rusi.js +126 -6
  44. package/lib/evinser/evinser.js +111 -1
  45. package/lib/helpers/agenticToolutils.js +925 -0
  46. package/lib/inventory/agentFormulationParser.js +57 -4
  47. package/lib/inventory/aiCollector.js +504 -10
  48. package/lib/inventory/aiInventory.js +12 -0
  49. package/lib/inventory/analyzer.js +488 -30
  50. package/lib/inventory/astWorker.js +6 -0
  51. package/lib/inventory/auditCategories.js +8 -0
  52. package/lib/inventory/cbomutils.js +30 -0
  53. package/lib/inventory/cdxrs.js +7 -2
  54. package/lib/inventory/communityAiConfigParser.js +215 -2
  55. package/lib/inventory/dosai.js +77 -0
  56. package/lib/inventory/envcontext.js +9 -6
  57. package/lib/inventory/evidenceUtils.js +13 -0
  58. package/lib/inventory/fetchBatch.js +74 -2
  59. package/lib/inventory/fetchRate.js +74 -11
  60. package/lib/inventory/forgeEnricher.js +3 -2
  61. package/lib/inventory/jvmToolEnv.js +6 -5
  62. package/lib/inventory/mcpConfigParser.js +166 -2
  63. package/lib/inventory/mcpDiscovery.js +11 -0
  64. package/lib/inventory/mcpServerManifest.js +389 -0
  65. package/lib/inventory/mcpbBundle.js +419 -0
  66. package/lib/inventory/plugins.js +3 -0
  67. package/lib/inventory/protobom.js +54 -88
  68. package/lib/inventory/protobomLoader.js +4 -0
  69. package/lib/inventory/rustFormulationParser.js +1 -1
  70. package/lib/inventory/source.js +10 -4
  71. package/lib/inventory/stylePackageRefs.js +41 -0
  72. package/lib/inventory/svelteUtils.js +524 -0
  73. package/lib/parsers/huggingfaceUtils.js +15 -7
  74. package/lib/parsers/onnx.js +294 -0
  75. package/lib/parsers/safetensors.js +114 -0
  76. package/lib/stages/postgen/introspection/markers.js +29 -6
  77. package/lib/stages/postgen/postgen.js +169 -1
  78. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  79. package/package.json +25 -25
  80. package/types/lib/audit/targets.d.ts.map +1 -1
  81. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  82. package/types/lib/cli/index.d.ts +2 -2
  83. package/types/lib/cli/index.d.ts.map +1 -1
  84. package/types/lib/cli/jsBom.d.ts +23 -0
  85. package/types/lib/cli/jsBom.d.ts.map +1 -1
  86. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  87. package/types/lib/cli/managedBom.d.ts +33 -0
  88. package/types/lib/cli/managedBom.d.ts.map +1 -1
  89. package/types/lib/cli/nativeBom.d.ts +88 -8
  90. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  91. package/types/lib/core/activity.d.ts.map +1 -1
  92. package/types/lib/core/env.d.ts +21 -0
  93. package/types/lib/core/env.d.ts.map +1 -1
  94. package/types/lib/core/userAgent.d.ts +56 -0
  95. package/types/lib/core/userAgent.d.ts.map +1 -0
  96. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  97. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  98. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  99. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  100. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  101. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  102. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  103. package/types/lib/ecosystems/gradleutils.d.ts +20 -0
  104. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  105. package/types/lib/ecosystems/kosi.d.ts +73 -0
  106. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  107. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  108. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  109. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  110. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  111. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  112. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  113. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  114. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  115. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  116. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  117. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  118. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  120. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  121. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  122. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  123. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  124. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  125. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  126. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  127. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  128. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  129. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  130. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  131. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  132. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  133. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  134. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  135. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  136. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  137. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  138. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  139. package/types/lib/evinser/evinser.d.ts +29 -0
  140. package/types/lib/evinser/evinser.d.ts.map +1 -1
  141. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  142. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  143. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  144. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  145. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  146. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  147. package/types/lib/inventory/auditCategories.d.ts +4 -0
  148. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  149. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  150. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  151. package/types/lib/inventory/dosai.d.ts +23 -0
  152. package/types/lib/inventory/dosai.d.ts.map +1 -1
  153. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  154. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  155. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  156. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  157. package/types/lib/inventory/fetchRate.d.ts +9 -6
  158. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  159. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  160. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  161. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  162. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  163. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  164. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  165. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  166. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  167. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  168. package/types/lib/inventory/plugins.d.ts.map +1 -1
  169. package/types/lib/inventory/protobom.d.ts +18 -13
  170. package/types/lib/inventory/protobom.d.ts.map +1 -1
  171. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  172. package/types/lib/inventory/source.d.ts.map +1 -1
  173. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  174. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  175. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  176. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  177. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  178. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  179. package/types/lib/parsers/onnx.d.ts +28 -0
  180. package/types/lib/parsers/onnx.d.ts.map +1 -0
  181. package/types/lib/parsers/safetensors.d.ts +22 -0
  182. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  183. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  184. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  185. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  186. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
- import toml from "@iarna/toml";
3
+ import * as toml from "smol-toml";
4
4
 
5
5
  import { tryBuildPurl } from "../inventory/purl.js";
6
6
 
@@ -0,0 +1,268 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import * as toml from "smol-toml";
4
+
5
+ import { tryBuildPurl } from "../inventory/purl.js";
6
+
7
+ /**
8
+ * Julia project parser.
9
+ *
10
+ * Two files are consulted:
11
+ * - `Project.toml` — the manifest holding the project name, UUID, version,
12
+ * the names (and UUIDs) of its direct dependencies, and version
13
+ * compatibility constraints.
14
+ * - `Manifest.toml` — the lock file written by Pkg on resolve, pinning every
15
+ * package in the environment with its UUID, version, content hash, and the
16
+ * names of the packages it depends on.
17
+ *
18
+ * Packages are identified with the registered `julia` purl type, which carries
19
+ * the package UUID as a required qualifier (`pkg:julia/JSON@0.21.4?uuid=…`).
20
+ * The UUID is what Julia itself uses to identify packages, so keeping it in
21
+ * the purl lets consumers match components across differently named mirrors.
22
+ *
23
+ * Julia standard libraries ship with the language itself and carry no version
24
+ * in the manifest; they are emitted without a version and marked as standard
25
+ * libraries so they can be told apart from registry packages.
26
+ */
27
+
28
+ /**
29
+ * Parse a Julia environment from its `Project.toml` and optional
30
+ * `Manifest.toml`.
31
+ *
32
+ * @param {string} [projectTomlFile] Path to `Project.toml`, if present
33
+ * @param {string} [manifestTomlFile] Path to `Manifest.toml`, if present
34
+ * @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs?: string[] }}
35
+ */
36
+ export function parseJuliaProject(projectTomlFile, manifestTomlFile) {
37
+ const project = projectTomlFile ? readTomlFile(projectTomlFile) : undefined;
38
+
39
+ const parentComponent = {};
40
+ const directNames = new Set(
41
+ project?.deps && typeof project.deps === "object"
42
+ ? Object.keys(project.deps)
43
+ : [],
44
+ );
45
+ if (project?.name) {
46
+ const properties = [{ name: "internal:SrcFile", value: projectTomlFile }];
47
+ if (typeof project.uuid === "string" && project.uuid) {
48
+ properties.push({ name: "cdx:julia:uuid", value: project.uuid });
49
+ }
50
+ parentComponent.type = "application";
51
+ parentComponent.name = project.name;
52
+ if (typeof project.version === "string" && project.version) {
53
+ parentComponent.version = project.version;
54
+ }
55
+ parentComponent.description = `Julia project: ${project.name}`;
56
+ parentComponent.properties = properties;
57
+ }
58
+
59
+ let manifest;
60
+ if (manifestTomlFile) {
61
+ manifest = readTomlFile(manifestTomlFile);
62
+ }
63
+
64
+ // Manifest packages live under `[[deps.<Name>]]` array-of-table entries,
65
+ // which smol-toml surfaces as a plain object keyed by package name.
66
+ const manifestDeps = manifest?.deps;
67
+ if (!manifestDeps || typeof manifestDeps !== "object") {
68
+ // Lockless fallback: emit the declared direct dependencies without
69
+ // resolved versions, using the UUIDs from Project.toml.
70
+ const pkgList = [...directNames].map((name) =>
71
+ juliaPackage(name, undefined, project?.deps?.[name], {
72
+ direct: true,
73
+ stdlib: JULIA_STDLIBS.has(name),
74
+ srcFile: projectTomlFile,
75
+ }),
76
+ );
77
+ return {
78
+ pkgList,
79
+ dependencies: [],
80
+ parentComponent,
81
+ rootInputs: pkgList.map((p) => p["bom-ref"]),
82
+ };
83
+ }
84
+
85
+ const pkgList = [];
86
+ const edges = [];
87
+ const refByName = new Map();
88
+ for (const [name, raw] of Object.entries(manifestDeps)) {
89
+ // `[[deps.<Name>]]` is an array of tables; a name repeats only when two
90
+ // sources of the same package are pinned, and the first entry is the one
91
+ // the environment resolves first.
92
+ const entry = Array.isArray(raw) ? raw[0] : raw;
93
+ const uuid =
94
+ typeof entry?.uuid === "string" && entry.uuid ? entry.uuid : undefined;
95
+ const registryVersion =
96
+ typeof entry?.version === "string" && entry.version
97
+ ? entry.version
98
+ : undefined;
99
+ // Entries without a registry version are pinned by content hash (git or
100
+ // local path sources); the hash is the most precise version available.
101
+ const gitTreeSha1 =
102
+ typeof entry?.["git-tree-sha1"] === "string" && entry["git-tree-sha1"]
103
+ ? entry["git-tree-sha1"]
104
+ : undefined;
105
+ const version = registryVersion || gitTreeSha1;
106
+ const pkg = juliaPackage(name, version, uuid, {
107
+ direct: directNames.has(name),
108
+ stdlib: !registryVersion && JULIA_STDLIBS.has(name),
109
+ srcFile: manifestTomlFile,
110
+ });
111
+ pkgList.push(pkg);
112
+ refByName.set(name, pkg["bom-ref"]);
113
+ }
114
+ for (const [name, raw] of Object.entries(manifestDeps)) {
115
+ const ref = refByName.get(name);
116
+ if (!ref) {
117
+ continue;
118
+ }
119
+ const entry = Array.isArray(raw) ? raw[0] : raw;
120
+ const dependsOn = (Array.isArray(entry?.deps) ? entry.deps : [])
121
+ .map((depName) => refByName.get(depName))
122
+ .filter(Boolean);
123
+ edges.push({ ref, dependsOn: [...new Set(dependsOn)].sort() });
124
+ }
125
+ const rootInputs = [...directNames]
126
+ .map((name) => refByName.get(name))
127
+ .filter(Boolean);
128
+ return { pkgList, dependencies: edges, parentComponent, rootInputs };
129
+ }
130
+
131
+ /**
132
+ * Tell whether a `Project.toml` declares a Julia environment.
133
+ *
134
+ * Cloud Native Buildpacks and Gleam use the same file names, so the caller
135
+ * needs a content check before dispatching. A Julia project always carries a
136
+ * package `uuid`, a `[deps]` table, or a `[compat]` table.
137
+ *
138
+ * @param {string} filePath Path to the candidate `Project.toml`
139
+ * @returns {boolean} true when the file is a Julia project
140
+ */
141
+ export function isJuliaProjectFile(filePath) {
142
+ const project = readTomlFile(filePath);
143
+ return Boolean(
144
+ project &&
145
+ (typeof project.uuid === "string" ||
146
+ (project.deps && typeof project.deps === "object") ||
147
+ (project.compat && typeof project.compat === "object")),
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Tell whether a `Manifest.toml` is a Julia lock file.
153
+ *
154
+ * Gleam writes a lowercase `manifest.toml` that case-insensitive filesystems
155
+ * surface under the same glob; a Julia manifest is recognised by its
156
+ * `manifest_format` marker or its `[[deps.<Name>]]` tables.
157
+ *
158
+ * @param {string} filePath Path to the candidate `Manifest.toml`
159
+ * @returns {boolean} true when the file is a Julia manifest
160
+ */
161
+ export function isJuliaManifestFile(filePath) {
162
+ const manifest = readTomlFile(filePath);
163
+ return Boolean(
164
+ manifest &&
165
+ (typeof manifest.manifest_format === "string" ||
166
+ (manifest.deps && typeof manifest.deps === "object")),
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Build a component-like package record for a Julia dependency.
172
+ *
173
+ * @param {string} name Package name as Julia writes it
174
+ * @param {string|undefined} version Resolved version, if known
175
+ * @param {string|undefined} uuid Package UUID, if known
176
+ * @param {object} opts Extra context (`direct`, `stdlib`, `srcFile`)
177
+ * @returns {object} Package record
178
+ */
179
+ function juliaPackage(name, version, uuid, opts) {
180
+ // The julia purl type requires the uuid qualifier; when it is unknown the
181
+ // purl is dropped entirely rather than emitted without its identity anchor.
182
+ const purl =
183
+ name && uuid
184
+ ? tryBuildPurl({
185
+ type: "julia",
186
+ name,
187
+ version: version || undefined,
188
+ qualifiers: { uuid },
189
+ })
190
+ : null;
191
+ const properties = [
192
+ { name: "internal:SrcFile", value: opts.srcFile },
193
+ {
194
+ name: "cdx:julia:dependency",
195
+ value: opts.direct ? "direct" : "transitive",
196
+ },
197
+ ];
198
+ if (uuid) {
199
+ properties.push({ name: "cdx:julia:uuid", value: uuid });
200
+ }
201
+ if (opts.stdlib) {
202
+ properties.push({ name: "cdx:julia:stdlib", value: "true" });
203
+ }
204
+ const pkg = {
205
+ name,
206
+ ...(version ? { version } : {}),
207
+ type: "library",
208
+ scope: "required",
209
+ properties,
210
+ };
211
+ if (purl) {
212
+ pkg.purl = purl;
213
+ pkg["bom-ref"] = decodeURIComponent(purl);
214
+ } else {
215
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
216
+ }
217
+ return pkg;
218
+ }
219
+
220
+ /**
221
+ * Julia standard libraries ship with the language itself. They carry no
222
+ * version in the manifest, so they are recognised by name against this list.
223
+ *
224
+ * @type {Set<string>}
225
+ */
226
+ const JULIA_STDLIBS = new Set([
227
+ "Base",
228
+ "CRC32c",
229
+ "Dates",
230
+ "DelimitedFiles",
231
+ "Distributed",
232
+ "FileWatching",
233
+ "Future",
234
+ "LibGit2",
235
+ "Libdl",
236
+ "LinearAlgebra",
237
+ "Logging",
238
+ "Markdown",
239
+ "Mmap",
240
+ "Printf",
241
+ "Profile",
242
+ "Random",
243
+ "REPL",
244
+ "Serialization",
245
+ "SharedArrays",
246
+ "Sockets",
247
+ "SparseArrays",
248
+ "Statistics",
249
+ "TOML",
250
+ "Test",
251
+ "UUIDs",
252
+ "Unicode",
253
+ ]);
254
+
255
+ /**
256
+ * Read and parse a TOML file, warning instead of throwing on invalid input.
257
+ *
258
+ * @param {string} filePath File to read
259
+ * @returns {object|undefined} Parsed value
260
+ */
261
+ function readTomlFile(filePath) {
262
+ try {
263
+ return toml.parse(readFileSync(filePath, "utf-8"));
264
+ } catch (error) {
265
+ console.warn(`Failed to parse ${filePath}: ${error.message}`);
266
+ return undefined;
267
+ }
268
+ }
@@ -0,0 +1,218 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { tryBuildPurl } from "../inventory/purl.js";
4
+
5
+ /**
6
+ * Lua parser for rockspecs and `luarocks.lock`.
7
+ *
8
+ * A rockspec is a Lua source file whose top level is a sequence of
9
+ * assignments describing one package: its name, version, source, and a
10
+ * `dependencies` table of constraint strings (`"lua >= 5.1"`). LuaRocks 3
11
+ * can also persist the resolved set to `luarocks.lock`. Both are read with a
12
+ * small line scanner over the shapes these files actually use, rather than a
13
+ * Lua interpreter.
14
+ *
15
+ * Rocks are identified with the registered `luarocks` purl type
16
+ * (`pkg:luarocks/<name>@<version>`). Lua versions carry a rock revision
17
+ * suffix (`5.4.6-1`); the full rock version is kept verbatim because the
18
+ * revision distinguishes distinct builds.
19
+ */
20
+
21
+ /**
22
+ * Parse a rockspec manifest.
23
+ *
24
+ * @param {string} rockspecFile Path to the `.rockspec` file
25
+ * @returns {{ pkgList: object[], parentComponent: object }}
26
+ */
27
+ export function parseRockspecFile(rockspecFile) {
28
+ const fields = readLuaAssignments(rockspecFile);
29
+ const name = fields.get("package");
30
+ const version = fields.get("version");
31
+ const declaredDeps = fields.get("dependencies");
32
+
33
+ const parentComponent = {};
34
+ if (name) {
35
+ parentComponent.type = "application";
36
+ parentComponent.name = name;
37
+ if (version) {
38
+ parentComponent.version = version;
39
+ }
40
+ parentComponent.description = `Lua rockspec: ${name}`;
41
+ parentComponent.properties = [
42
+ { name: "internal:SrcFile", value: rockspecFile },
43
+ ];
44
+ }
45
+
46
+ const directNames = new Set(
47
+ Array.isArray(declaredDeps) ? declaredDeps.map((dep) => dep.name) : [],
48
+ );
49
+ const pkgList = [...directNames].map((depName) =>
50
+ luaPackage(depName, undefined, { direct: true, srcFile: rockspecFile }),
51
+ );
52
+ return { pkgList, parentComponent };
53
+ }
54
+
55
+ /**
56
+ * Parse a `luarocks.lock` file.
57
+ *
58
+ * @param {string} lockFile Path to `luarocks.lock`
59
+ * @param {string} [rockspecFile] Path to the rockspec, to mark direct deps
60
+ * @returns {{ pkgList: object[], parentComponent: object }}
61
+ */
62
+ export function parseLuaRocksLock(lockFile, rockspecFile) {
63
+ const fields = readLuaAssignments(lockFile);
64
+ const locked = fields.get("dependencies");
65
+ if (!Array.isArray(locked)) {
66
+ return { pkgList: [], parentComponent: {} };
67
+ }
68
+ const directNames = new Set();
69
+ if (rockspecFile) {
70
+ const declared = readLuaAssignments(rockspecFile).get("dependencies");
71
+ if (Array.isArray(declared)) {
72
+ for (const dep of declared) {
73
+ if (typeof dep?.name === "string") {
74
+ directNames.add(dep.name);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ const pkgList = locked
80
+ .filter((dep) => typeof dep?.name === "string" && dep.name)
81
+ .map((dep) =>
82
+ luaPackage(dep.name, dep.version, {
83
+ // Without a rockspec there is no declared set to compare against, so
84
+ // the whole closure is reported direct rather than guessing.
85
+ direct: rockspecFile ? directNames.has(dep.name) : true,
86
+ srcFile: lockFile,
87
+ }),
88
+ );
89
+ return { pkgList, parentComponent: {} };
90
+ }
91
+
92
+ /**
93
+ * Build a component-like package record for a Lua rock.
94
+ *
95
+ * @param {string} name Rock name
96
+ * @param {string|undefined} version Rock version including revision suffix
97
+ * @param {object} opts Extra context (`direct`, `srcFile`)
98
+ * @returns {object} Package record
99
+ */
100
+ function luaPackage(name, version, opts) {
101
+ const purl = tryBuildPurl({
102
+ type: "luarocks",
103
+ name,
104
+ version: version || undefined,
105
+ });
106
+ const properties = [
107
+ { name: "internal:SrcFile", value: opts.srcFile },
108
+ {
109
+ name: "cdx:luarocks:dependency",
110
+ value: opts.direct ? "direct" : "transitive",
111
+ },
112
+ ];
113
+ const pkg = {
114
+ name,
115
+ ...(version ? { version } : {}),
116
+ type: "library",
117
+ scope: "required",
118
+ properties,
119
+ };
120
+ if (purl) {
121
+ pkg.purl = purl;
122
+ pkg["bom-ref"] = decodeURIComponent(purl);
123
+ } else {
124
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
125
+ }
126
+ return pkg;
127
+ }
128
+
129
+ /**
130
+ * Read the top-level assignments of a Lua configuration file.
131
+ *
132
+ * The files cdxgen consumes use a constrained surface: `key = value` lines
133
+ * where the value is a quoted string, a table of strings or
134
+ * `["key"] = "value"` entries, or a short list expression. Values are
135
+ * normalised to strings, arrays of `{name, constraint}` records, or arrays
136
+ * of `{key, value}` records for mixed tables.
137
+ *
138
+ * @param {string} filePath File to read
139
+ * @returns {Map<string, *>} Parsed top-level fields
140
+ */
141
+ export function readLuaAssignments(filePath) {
142
+ const fields = new Map();
143
+ let text;
144
+ try {
145
+ text = readFileSync(filePath, "utf-8");
146
+ } catch (error) {
147
+ console.warn(`Failed to read ${filePath}: ${error.message}`);
148
+ return fields;
149
+ }
150
+ let currentTable;
151
+ let depth = 0;
152
+ for (const rawLine of text.split("\n")) {
153
+ const line = rawLine.trim();
154
+ if (!line || line.startsWith("--") || line.startsWith("#")) {
155
+ continue;
156
+ }
157
+ if (depth === 0) {
158
+ const assignment = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/u);
159
+ if (!assignment) {
160
+ continue;
161
+ }
162
+ const [, key, rest] = assignment;
163
+ const stringValue = rest.match(/^"([^"]*)"/u);
164
+ if (stringValue) {
165
+ fields.set(key, stringValue[1]);
166
+ continue;
167
+ }
168
+ if (rest.startsWith("{")) {
169
+ currentTable = [];
170
+ fields.set(key, currentTable);
171
+ depth = countBraces(rest, 0);
172
+ }
173
+ continue;
174
+ }
175
+ // Inside a table: collect string items, `key = "value"` pairs, and the
176
+ // bracketed `["key"] = "value"` form LuaRocks uses for names that are not
177
+ // valid Lua identifiers.
178
+ depth = countBraces(line, depth);
179
+ const keyed = line.match(
180
+ /^(?:\[\s*"([^"]+)"\s*\]|([A-Za-z_][A-Za-z0-9_]*))\s*=\s*"([^"]*)"/u,
181
+ );
182
+ if (keyed && currentTable) {
183
+ currentTable.push({ name: keyed[1] ?? keyed[2], version: keyed[3] });
184
+ continue;
185
+ }
186
+ const item = line.match(/^"([^"]+)"/u);
187
+ if (item && currentTable) {
188
+ const tokens = item[1].split(/\s+/u);
189
+ currentTable.push({
190
+ name: tokens[0],
191
+ constraint: tokens.slice(1).join(" "),
192
+ });
193
+ }
194
+ if (depth <= 0) {
195
+ depth = 0;
196
+ }
197
+ }
198
+ return fields;
199
+ }
200
+
201
+ /**
202
+ * Track table nesting depth for one line.
203
+ *
204
+ * @param {string} line Line to scan
205
+ * @param {number} depth Depth before the line
206
+ * @returns {number} Depth after the line
207
+ */
208
+ function countBraces(line, depth) {
209
+ let next = depth;
210
+ for (const char of line) {
211
+ if (char === "{") {
212
+ next += 1;
213
+ } else if (char === "}") {
214
+ next -= 1;
215
+ }
216
+ }
217
+ return next;
218
+ }
@@ -41,6 +41,7 @@ import {
41
41
  mapConanPkgRefToPurlStringAndNameAndVersion,
42
42
  nixGenericPurl,
43
43
  purlFromUrlString,
44
+ tryBuildPurl,
44
45
  } from "../inventory/purl.js";
45
46
  import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
46
47
  import { getDartMetadata } from "./ecosystems.js";
@@ -3203,3 +3204,174 @@ async function fullScanCocoaPod(dependency, component, options) {
3203
3204
  component.externalReferences = externalRefs;
3204
3205
  }
3205
3206
  }
3207
+
3208
+ /**
3209
+ * Parse an xmake `xmake-requires.lock` file.
3210
+ *
3211
+ * The lock is a Lua table, written when the `package.requires_lock` policy is
3212
+ * enabled. Its top level holds a `__meta__` entry plus one section per build
3213
+ * configuration, keyed `platform|arch`; each section maps a requirement key
3214
+ * (`"zlib#31fecfc4"`, or `"sol2 v3.2.1#72267bd5"` when the requirement
3215
+ * carries a version) to a table with the resolved `version` and the
3216
+ * `repo` revision the package definition came from.
3217
+ *
3218
+ * The same package appears once per configuration, so entries are collapsed
3219
+ * by name and version, and the configurations a package was resolved for are
3220
+ * kept as a property. A requirement key without a `version` field names a
3221
+ * system package xmake did not resolve; it is inventoried without a version.
3222
+ *
3223
+ * No `xmake` purl type is registered, so packages are identified as generic
3224
+ * carrying a `cdx:purl:proposedType=xmake` property, following the
3225
+ * convention used for nix and zig.
3226
+ *
3227
+ * @param {string} lockFile Path to `xmake-requires.lock`
3228
+ * @returns {Object[]} Package records for the pinned requirements
3229
+ */
3230
+ export function parseXmakeRequiresLock(lockFile) {
3231
+ let text;
3232
+ try {
3233
+ text = readFileSync(lockFile, "utf-8");
3234
+ } catch (error) {
3235
+ console.warn(`Failed to read ${lockFile}: ${error.message}`);
3236
+ return [];
3237
+ }
3238
+ const byKey = new Map();
3239
+ // The outermost `{` is the file's own table, so a configuration section
3240
+ // opens at depth 1 and a requirement inside it at depth 2. Each is dropped
3241
+ // again once the brace depth falls back to where it opened.
3242
+ const CONFIGURATION_DEPTH = 1;
3243
+ const REQUIREMENT_DEPTH = 2;
3244
+ let configuration;
3245
+ let requirement;
3246
+ let depth = 0;
3247
+ for (const rawLine of text.split("\n")) {
3248
+ const line = rawLine.trim();
3249
+ if (!line || line.startsWith("--")) {
3250
+ continue;
3251
+ }
3252
+ // `["macosx|arm64"] = {` opens a configuration; `["zlib#hash"] = {` opens
3253
+ // a requirement inside one.
3254
+ const opener = line.match(/^\[\s*"([^"]+)"\s*\]\s*=\s*\{/u);
3255
+ if (opener && depth === CONFIGURATION_DEPTH) {
3256
+ // `__meta__` is normally written unquoted, but a lock that quotes it
3257
+ // must not become a configuration section either.
3258
+ configuration = opener[1] === "__meta__" ? undefined : opener[1];
3259
+ } else if (opener && depth === REQUIREMENT_DEPTH && configuration) {
3260
+ requirement = xmakeRequirementName(opener[1]);
3261
+ const key = `${configuration}|${requirement}`;
3262
+ if (requirement && !byKey.has(key)) {
3263
+ byKey.set(key, { name: requirement, configuration });
3264
+ }
3265
+ } else if (!opener && configuration && requirement) {
3266
+ const version = line.match(/^version\s*=\s*"([^"]*)"/u);
3267
+ if (version) {
3268
+ const record = byKey.get(`${configuration}|${requirement}`);
3269
+ if (record) {
3270
+ record.version = version[1];
3271
+ }
3272
+ }
3273
+ }
3274
+ depth = Math.max(0, depth + countXmakeBraces(line));
3275
+ if (depth <= REQUIREMENT_DEPTH) {
3276
+ requirement = undefined;
3277
+ }
3278
+ if (depth <= CONFIGURATION_DEPTH) {
3279
+ configuration = undefined;
3280
+ }
3281
+ }
3282
+
3283
+ // Collapse the per-configuration entries into one component per resolved
3284
+ // coordinate, listing the configurations it was resolved for.
3285
+ const merged = new Map();
3286
+ for (const record of byKey.values()) {
3287
+ const key = `${record.name}@${record.version || ""}`;
3288
+ const existing = merged.get(key);
3289
+ if (existing) {
3290
+ existing.configurations.add(record.configuration);
3291
+ continue;
3292
+ }
3293
+ merged.set(key, {
3294
+ name: record.name,
3295
+ version: record.version,
3296
+ configurations: new Set([record.configuration]),
3297
+ });
3298
+ }
3299
+
3300
+ const pkgList = [];
3301
+ for (const record of merged.values()) {
3302
+ // A malformed coordinate in an untrusted lock costs one entry, not the
3303
+ // whole scan.
3304
+ const purl = tryBuildPurl({
3305
+ type: "generic",
3306
+ namespace: "xmake",
3307
+ name: record.name,
3308
+ version: record.version || null,
3309
+ });
3310
+ if (!purl) {
3311
+ continue;
3312
+ }
3313
+ pkgList.push({
3314
+ name: record.name,
3315
+ ...(record.version ? { version: record.version } : {}),
3316
+ type: "library",
3317
+ scope: "required",
3318
+ purl,
3319
+ "bom-ref": decodeURIComponent(purl),
3320
+ properties: [
3321
+ { name: "internal:SrcFile", value: lockFile },
3322
+ { name: "cdx:purl:proposedType", value: "xmake" },
3323
+ {
3324
+ name: "cdx:xmake:configurations",
3325
+ value: [...record.configurations].sort().join(", "),
3326
+ },
3327
+ ],
3328
+ evidence: {
3329
+ identity: {
3330
+ field: "purl",
3331
+ confidence: 0.5,
3332
+ methods: [
3333
+ {
3334
+ technique: "source-code-analysis",
3335
+ confidence: 0.5,
3336
+ value: `Filename ${lockFile}`,
3337
+ },
3338
+ ],
3339
+ },
3340
+ },
3341
+ });
3342
+ }
3343
+ return pkgList;
3344
+ }
3345
+
3346
+ /**
3347
+ * Reduce an xmake requirement key to the package name.
3348
+ *
3349
+ * Keys look like `zlib#31fecfc4` or `sol2 v3.2.1#72267bd5`: the trailing
3350
+ * `#<hash>` identifies the resolved configuration and the optional second
3351
+ * token is the requested version range, neither of which is the name.
3352
+ *
3353
+ * @param {string} key Requirement key as written in the lock
3354
+ * @returns {string|undefined} Package name
3355
+ */
3356
+ function xmakeRequirementName(key) {
3357
+ const name = key.split("#")[0].trim().split(/\s+/u)[0];
3358
+ return name || undefined;
3359
+ }
3360
+
3361
+ /**
3362
+ * Net brace change contributed by one line of a Lua table.
3363
+ *
3364
+ * @param {string} line Line to scan
3365
+ * @returns {number} Opens minus closes
3366
+ */
3367
+ function countXmakeBraces(line) {
3368
+ let delta = 0;
3369
+ for (const char of line) {
3370
+ if (char === "{") {
3371
+ delta += 1;
3372
+ } else if (char === "}") {
3373
+ delta -= 1;
3374
+ }
3375
+ }
3376
+ return delta;
3377
+ }