@cyclonedx/cdxgen 12.1.5 → 12.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 (181) hide show
  1. package/README.md +47 -39
  2. package/bin/cdxgen.js +175 -96
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +327 -372
  14. package/lib/evinser/db.js +137 -0
  15. package/lib/{helpers → evinser}/db.poku.js +2 -6
  16. package/lib/evinser/evinser.js +2 -14
  17. package/lib/helpers/bomSigner.js +312 -0
  18. package/lib/helpers/bomSigner.poku.js +156 -0
  19. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  20. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  21. package/lib/helpers/ciParsers/circleCi.js +286 -0
  22. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  23. package/lib/helpers/ciParsers/common.js +24 -0
  24. package/lib/helpers/ciParsers/githubActions.js +636 -0
  25. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  26. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  27. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  28. package/lib/helpers/ciParsers/jenkins.js +181 -0
  29. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  30. package/lib/helpers/depsUtils.js +203 -0
  31. package/lib/helpers/depsUtils.poku.js +150 -0
  32. package/lib/helpers/display.js +423 -4
  33. package/lib/helpers/envcontext.js +18 -3
  34. package/lib/helpers/formulationParsers.js +351 -0
  35. package/lib/helpers/logger.js +14 -0
  36. package/lib/helpers/protobom.js +9 -9
  37. package/lib/helpers/pythonutils.js +9 -0
  38. package/lib/helpers/utils.js +681 -406
  39. package/lib/helpers/utils.poku.js +55 -255
  40. package/lib/helpers/versutils.js +202 -0
  41. package/lib/helpers/versutils.poku.js +315 -0
  42. package/lib/helpers/vsixutils.js +1061 -0
  43. package/lib/helpers/vsixutils.poku.js +2247 -0
  44. package/lib/managers/binary.js +19 -19
  45. package/lib/managers/docker.js +108 -1
  46. package/lib/managers/oci.js +10 -0
  47. package/lib/managers/piptree.js +3 -9
  48. package/lib/parsers/npmrc.js +17 -13
  49. package/lib/parsers/npmrc.poku.js +41 -5
  50. package/lib/server/openapi.yaml +1 -1
  51. package/lib/server/server.js +40 -11
  52. package/lib/server/server.poku.js +123 -144
  53. package/lib/stages/postgen/annotator.js +1 -1
  54. package/lib/stages/postgen/auditBom.js +197 -0
  55. package/lib/stages/postgen/auditBom.poku.js +378 -0
  56. package/lib/stages/postgen/postgen.js +54 -1
  57. package/lib/stages/postgen/postgen.poku.js +90 -1
  58. package/lib/stages/postgen/ruleEngine.js +369 -0
  59. package/lib/stages/pregen/envAudit.js +299 -0
  60. package/lib/stages/pregen/envAudit.poku.js +572 -0
  61. package/lib/stages/pregen/pregen.js +12 -8
  62. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  63. package/lib/validator/complianceEngine.js +241 -0
  64. package/lib/validator/complianceEngine.poku.js +168 -0
  65. package/lib/validator/complianceRules.js +1610 -0
  66. package/lib/validator/complianceRules.poku.js +328 -0
  67. package/lib/validator/index.js +222 -0
  68. package/lib/validator/index.poku.js +144 -0
  69. package/lib/validator/reporters/annotations.js +121 -0
  70. package/lib/validator/reporters/console.js +149 -0
  71. package/lib/validator/reporters/index.js +41 -0
  72. package/lib/validator/reporters/json.js +37 -0
  73. package/lib/validator/reporters/sarif.js +184 -0
  74. package/lib/validator/reporters.poku.js +150 -0
  75. package/package.json +8 -8
  76. package/types/bin/sign.d.ts +3 -0
  77. package/types/bin/sign.d.ts.map +1 -0
  78. package/types/bin/validate.d.ts +3 -0
  79. package/types/bin/validate.d.ts.map +1 -0
  80. package/types/helpers/utils.d.ts +0 -1
  81. package/types/lib/cli/index.d.ts +49 -52
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/evinser/db.d.ts +34 -0
  84. package/types/lib/evinser/db.d.ts.map +1 -0
  85. package/types/lib/evinser/evinser.d.ts +63 -16
  86. package/types/lib/evinser/evinser.d.ts.map +1 -1
  87. package/types/lib/helpers/bomSigner.d.ts +27 -0
  88. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  89. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  90. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  91. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  92. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  93. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  94. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  95. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  96. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  101. package/types/lib/helpers/depsUtils.d.ts +21 -0
  102. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  103. package/types/lib/helpers/display.d.ts +111 -11
  104. package/types/lib/helpers/display.d.ts.map +1 -1
  105. package/types/lib/helpers/envcontext.d.ts +19 -7
  106. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  107. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  108. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  109. package/types/lib/helpers/logger.d.ts +15 -1
  110. package/types/lib/helpers/logger.d.ts.map +1 -1
  111. package/types/lib/helpers/protobom.d.ts +2 -2
  112. package/types/lib/helpers/pythonutils.d.ts +10 -1
  113. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  114. package/types/lib/helpers/utils.d.ts +532 -128
  115. package/types/lib/helpers/utils.d.ts.map +1 -1
  116. package/types/lib/helpers/versutils.d.ts +8 -0
  117. package/types/lib/helpers/versutils.d.ts.map +1 -0
  118. package/types/lib/helpers/vsixutils.d.ts +130 -0
  119. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  120. package/types/lib/managers/docker.d.ts +12 -31
  121. package/types/lib/managers/docker.d.ts.map +1 -1
  122. package/types/lib/managers/oci.d.ts +11 -1
  123. package/types/lib/managers/oci.d.ts.map +1 -1
  124. package/types/lib/managers/piptree.d.ts.map +1 -1
  125. package/types/lib/parsers/npmrc.d.ts +4 -1
  126. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  127. package/types/lib/server/server.d.ts +21 -2
  128. package/types/lib/server/server.d.ts.map +1 -1
  129. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  130. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  131. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  132. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  133. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  134. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  135. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  136. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  137. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  138. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  139. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  140. package/types/lib/validator/complianceEngine.d.ts +66 -0
  141. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  142. package/types/lib/validator/complianceRules.d.ts +70 -0
  143. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  144. package/types/lib/validator/index.d.ts +70 -0
  145. package/types/lib/validator/index.d.ts.map +1 -0
  146. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  147. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  148. package/types/lib/validator/reporters/console.d.ts +30 -0
  149. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  150. package/types/lib/validator/reporters/index.d.ts +21 -0
  151. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  152. package/types/lib/validator/reporters/json.d.ts +11 -0
  153. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  154. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  155. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  156. package/lib/helpers/db.js +0 -162
  157. package/lib/stages/pregen/env-audit.js +0 -34
  158. package/lib/stages/pregen/env-audit.poku.js +0 -290
  159. package/types/helpers/db.d.ts +0 -35
  160. package/types/helpers/db.d.ts.map +0 -1
  161. package/types/lib/helpers/db.d.ts +0 -35
  162. package/types/lib/helpers/db.d.ts.map +0 -1
  163. package/types/lib/helpers/validator.d.ts.map +0 -1
  164. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  165. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  166. package/types/managers/binary.d.ts +0 -37
  167. package/types/managers/binary.d.ts.map +0 -1
  168. package/types/managers/docker.d.ts +0 -56
  169. package/types/managers/docker.d.ts.map +0 -1
  170. package/types/managers/oci.d.ts +0 -2
  171. package/types/managers/oci.d.ts.map +0 -1
  172. package/types/managers/piptree.d.ts +0 -2
  173. package/types/managers/piptree.d.ts.map +0 -1
  174. package/types/server/server.d.ts +0 -34
  175. package/types/server/server.d.ts.map +0 -1
  176. package/types/stages/postgen/annotator.d.ts +0 -27
  177. package/types/stages/postgen/annotator.d.ts.map +0 -1
  178. package/types/stages/postgen/postgen.d.ts +0 -51
  179. package/types/stages/postgen/postgen.d.ts.map +0 -1
  180. package/types/stages/pregen/pregen.d.ts +0 -59
  181. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -0,0 +1,1061 @@
1
+ import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+
6
+ import StreamZip from "node-stream-zip";
7
+ import { PackageURL } from "packageurl-js";
8
+ import { xml2js } from "xml-js";
9
+
10
+ import {
11
+ DEBUG_MODE,
12
+ getTmpDir,
13
+ isMac,
14
+ isWin,
15
+ safeExistsSync,
16
+ } from "./utils.js";
17
+ import { toVersRange } from "./versutils.js";
18
+
19
+ /**
20
+ * The purl type for VS Code extensions as defined by the packageurl spec.
21
+ */
22
+ export const VSCODE_EXTENSION_PURL_TYPE = "vscode-extension";
23
+
24
+ /**
25
+ * Confidence value for extension metadata discovered via manifest analysis.
26
+ */
27
+ const MANIFEST_ANALYSIS_CONFIDENCE = 0.6;
28
+
29
+ /**
30
+ * IDE configuration entries describing where each IDE stores its extensions.
31
+ * Each entry contains the IDE name and an array of candidate extension
32
+ * directory paths for Windows, macOS, and Linux (including remote/server
33
+ * environments).
34
+ *
35
+ * The paths use platform-specific logic via `homedir()` and common
36
+ * environment variables.
37
+ */
38
+ export function getIdeExtensionDirs() {
39
+ const home = homedir();
40
+ const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
41
+ const localAppData =
42
+ process.env.LOCALAPPDATA || join(home, "AppData", "Local");
43
+ const xdgDataHome =
44
+ process.env.XDG_DATA_HOME || join(home, ".local", "share");
45
+
46
+ // Each entry: { name, dirs: string[] }
47
+ // Only include directories that are relevant for the current platform,
48
+ // plus well-known remote/server paths that are always Linux.
49
+ const ides = [
50
+ {
51
+ name: "VS Code",
52
+ dirs: isWin
53
+ ? [join(appData, "Code", "User", "extensions")]
54
+ : isMac
55
+ ? [
56
+ join(
57
+ home,
58
+ "Library",
59
+ "Application Support",
60
+ "Code",
61
+ "User",
62
+ "extensions",
63
+ ),
64
+ ]
65
+ : [join(home, ".vscode", "extensions")],
66
+ },
67
+ {
68
+ name: "VS Code Insiders",
69
+ dirs: isWin
70
+ ? [join(appData, "Code - Insiders", "User", "extensions")]
71
+ : isMac
72
+ ? [
73
+ join(
74
+ home,
75
+ "Library",
76
+ "Application Support",
77
+ "Code - Insiders",
78
+ "User",
79
+ "extensions",
80
+ ),
81
+ ]
82
+ : [join(home, ".vscode-insiders", "extensions")],
83
+ },
84
+ {
85
+ name: "VSCodium",
86
+ dirs: isWin
87
+ ? [join(appData, "VSCodium", "User", "extensions")]
88
+ : isMac
89
+ ? [
90
+ join(
91
+ home,
92
+ "Library",
93
+ "Application Support",
94
+ "VSCodium",
95
+ "User",
96
+ "extensions",
97
+ ),
98
+ ]
99
+ : [
100
+ join(home, ".vscode-oss", "extensions"),
101
+ join(home, ".config", "VSCodium", "User", "extensions"),
102
+ ],
103
+ },
104
+ {
105
+ name: "Cursor",
106
+ dirs: isWin
107
+ ? [
108
+ join(appData, "Cursor", "User", "extensions"),
109
+ join(localAppData, "cursor", "extensions"),
110
+ ]
111
+ : isMac
112
+ ? [
113
+ join(
114
+ home,
115
+ "Library",
116
+ "Application Support",
117
+ "Cursor",
118
+ "User",
119
+ "extensions",
120
+ ),
121
+ ]
122
+ : [join(home, ".cursor", "extensions")],
123
+ },
124
+ {
125
+ name: "Windsurf",
126
+ dirs: isWin
127
+ ? [join(appData, "Windsurf", "User", "extensions")]
128
+ : isMac
129
+ ? [
130
+ join(
131
+ home,
132
+ "Library",
133
+ "Application Support",
134
+ "Windsurf",
135
+ "User",
136
+ "extensions",
137
+ ),
138
+ ]
139
+ : [join(home, ".windsurf", "extensions")],
140
+ },
141
+ {
142
+ name: "Positron",
143
+ dirs: isWin
144
+ ? [join(appData, "Positron", "User", "extensions")]
145
+ : isMac
146
+ ? [
147
+ join(
148
+ home,
149
+ "Library",
150
+ "Application Support",
151
+ "Positron",
152
+ "User",
153
+ "extensions",
154
+ ),
155
+ ]
156
+ : [join(home, ".positron", "extensions")],
157
+ },
158
+ {
159
+ name: "Theia",
160
+ dirs: isWin
161
+ ? [join(appData, "Theia", "extensions")]
162
+ : isMac
163
+ ? [
164
+ join(
165
+ home,
166
+ "Library",
167
+ "Application Support",
168
+ "Theia",
169
+ "extensions",
170
+ ),
171
+ ]
172
+ : [
173
+ join(home, ".theia", "extensions"),
174
+ join(xdgDataHome, "theia", "extensions"),
175
+ ],
176
+ },
177
+ // Remote / server environments (Linux only)
178
+ {
179
+ name: "code-server",
180
+ dirs: [join(xdgDataHome, "code-server", "extensions")],
181
+ },
182
+ {
183
+ name: "VS Code Remote",
184
+ dirs: [join(home, ".vscode-remote", "extensions")],
185
+ },
186
+ {
187
+ name: "OpenVSCode Server",
188
+ dirs: [join(xdgDataHome, "openvscode-server", "extensions")],
189
+ },
190
+ {
191
+ name: "Trae",
192
+ dirs: isWin
193
+ ? [join(appData, "Trae", "User", "extensions")]
194
+ : isMac
195
+ ? [
196
+ join(
197
+ home,
198
+ "Library",
199
+ "Application Support",
200
+ "Trae",
201
+ "User",
202
+ "extensions",
203
+ ),
204
+ ]
205
+ : [join(home, ".trae", "extensions")],
206
+ },
207
+ {
208
+ name: "Augment Code",
209
+ dirs: isWin
210
+ ? [join(appData, "Augment Code", "User", "extensions")]
211
+ : isMac
212
+ ? [
213
+ join(
214
+ home,
215
+ "Library",
216
+ "Application Support",
217
+ "Augment Code",
218
+ "User",
219
+ "extensions",
220
+ ),
221
+ ]
222
+ : [join(home, ".augment-code", "extensions")],
223
+ },
224
+ ];
225
+
226
+ return ides;
227
+ }
228
+
229
+ /**
230
+ * Discover all existing IDE extension directories on the current system.
231
+ *
232
+ * @returns {Array<{name: string, dir: string}>} Array of objects with IDE name
233
+ * and the existing directory path.
234
+ */
235
+ export function discoverIdeExtensionDirs() {
236
+ const ides = getIdeExtensionDirs();
237
+ const found = [];
238
+ for (const ide of ides) {
239
+ for (const dir of ide.dirs) {
240
+ if (safeExistsSync(dir)) {
241
+ found.push({ name: ide.name, dir });
242
+ }
243
+ }
244
+ }
245
+ return found;
246
+ }
247
+
248
+ /**
249
+ * Parse a `.vsixmanifest` XML string and extract extension metadata.
250
+ *
251
+ * @param {string} manifestData Raw XML content of a `.vsixmanifest` file
252
+ * @returns {Object|undefined} Object with { publisher, name, version, displayName, description, platform, tags } or undefined on failure
253
+ */
254
+ export function parseVsixManifest(manifestData) {
255
+ if (!manifestData?.trim()) {
256
+ return undefined;
257
+ }
258
+ try {
259
+ const parsed = xml2js(manifestData, {
260
+ compact: true,
261
+ alwaysArray: false,
262
+ spaces: 4,
263
+ textKey: "_",
264
+ attributesKey: "$",
265
+ });
266
+ const manifest =
267
+ parsed.PackageManifest || parsed["PackageManifest:PackageManifest"];
268
+ if (!manifest) {
269
+ return undefined;
270
+ }
271
+ const metadata = manifest.Metadata || manifest["PackageManifest:Metadata"];
272
+ if (!metadata) {
273
+ return undefined;
274
+ }
275
+ const identity = metadata.Identity || metadata["PackageManifest:Identity"];
276
+ if (!identity?.$) {
277
+ return undefined;
278
+ }
279
+ const attrs = identity.$;
280
+ const publisher =
281
+ attrs.Publisher || attrs.publisher || attrs["d:Publisher"] || "";
282
+ const name = attrs.Id || attrs.id || attrs["d:Id"] || "";
283
+ const version = attrs.Version || attrs.version || attrs["d:Version"] || "";
284
+ const targetPlatform =
285
+ attrs.TargetPlatform ||
286
+ attrs.targetPlatform ||
287
+ attrs["d:TargetPlatform"] ||
288
+ "";
289
+ const tags = metadata?.Tags?._?.split(",").map((s) => s.trim());
290
+ const displayNameNode =
291
+ metadata.DisplayName || metadata["PackageManifest:DisplayName"];
292
+ const descriptionNode =
293
+ metadata.Description || metadata["PackageManifest:Description"];
294
+ const displayName = displayNameNode?._ || displayNameNode || "";
295
+ const description = descriptionNode?._ || descriptionNode || "";
296
+
297
+ // Parse Properties tag for additional metadata
298
+ const properties = {};
299
+ const propsNode = metadata?.Properties;
300
+ if (propsNode?.Property) {
301
+ const propEntries = Array.isArray(propsNode.Property)
302
+ ? propsNode.Property
303
+ : [propsNode.Property];
304
+ for (const prop of propEntries) {
305
+ const propId = prop?.$?.Id || "";
306
+ const propValue = prop?.$?.Value || "";
307
+ if (propId && propValue) {
308
+ properties[propId] = propValue;
309
+ }
310
+ }
311
+ }
312
+
313
+ const result = {
314
+ publisher: publisher,
315
+ name: name,
316
+ version,
317
+ displayName: typeof displayName === "string" ? displayName : "",
318
+ description: typeof description === "string" ? description : "",
319
+ platform: targetPlatform || "",
320
+ tags,
321
+ };
322
+
323
+ // Map well-known VSIX properties to structured fields
324
+ if (properties["Microsoft.VisualStudio.Code.Engine"]) {
325
+ result.vscodeEngine = properties["Microsoft.VisualStudio.Code.Engine"];
326
+ }
327
+ if (properties["Microsoft.VisualStudio.Code.ExtensionDependencies"]) {
328
+ const deps =
329
+ properties["Microsoft.VisualStudio.Code.ExtensionDependencies"];
330
+ if (deps) {
331
+ result.extensionDependencies = deps.split(",").map((s) => s.trim());
332
+ }
333
+ }
334
+ if (properties["Microsoft.VisualStudio.Code.ExtensionPack"]) {
335
+ const pack = properties["Microsoft.VisualStudio.Code.ExtensionPack"];
336
+ if (pack) {
337
+ result.extensionPack = pack.split(",").map((s) => s.trim());
338
+ }
339
+ }
340
+ if (properties["Microsoft.VisualStudio.Code.ExtensionKind"]) {
341
+ const kind = properties["Microsoft.VisualStudio.Code.ExtensionKind"];
342
+ if (kind) {
343
+ result.extensionKind = kind.split(",").map((s) => s.trim());
344
+ }
345
+ }
346
+ if (properties["Microsoft.VisualStudio.Code.ExecutesCode"]) {
347
+ result.executesCode =
348
+ properties["Microsoft.VisualStudio.Code.ExecutesCode"] === "true";
349
+ }
350
+ // Collect links from properties
351
+ const links = {};
352
+ for (const [id, value] of Object.entries(properties)) {
353
+ if (id.startsWith("Microsoft.VisualStudio.Services.Links.") && value) {
354
+ const linkType = id.replace(
355
+ "Microsoft.VisualStudio.Services.Links.",
356
+ "",
357
+ );
358
+ links[linkType] = value;
359
+ }
360
+ }
361
+ if (Object.keys(links).length) {
362
+ result.links = links;
363
+ }
364
+
365
+ return result;
366
+ } catch (e) {
367
+ if (DEBUG_MODE) {
368
+ console.log("Error parsing vsixmanifest:", e.message);
369
+ }
370
+ return undefined;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Parse npm-style dependency maps from a VS Code extension's package.json
376
+ * and create CycloneDX component objects with versionRange attributes.
377
+ *
378
+ * @param {Object} pkg Parsed package.json object
379
+ * @param {string} extensionPurl The purl of the parent extension (for dependency tree)
380
+ * @returns {{ components: Object[], dependencies: Object[] }} CycloneDX components and dependency tree
381
+ */
382
+ export function parseExtensionDependencies(pkg, extensionPurl) {
383
+ const components = [];
384
+ const dependsOn = [];
385
+ const seen = new Set();
386
+
387
+ const depGroups = [
388
+ { key: "dependencies", scope: "required" },
389
+ { key: "devDependencies", scope: "optional" },
390
+ { key: "peerDependencies", scope: "optional" },
391
+ { key: "optionalDependencies", scope: "optional" },
392
+ ];
393
+
394
+ for (const { key, scope } of depGroups) {
395
+ const deps = pkg[key];
396
+ if (!deps || typeof deps !== "object") {
397
+ continue;
398
+ }
399
+ for (const [depName, depVersion] of Object.entries(deps)) {
400
+ if (!depName || typeof depVersion !== "string") {
401
+ continue;
402
+ }
403
+ // Parse scoped npm package names
404
+ let group = "";
405
+ let name = depName;
406
+ if (depName.startsWith("@") && depName.includes("/")) {
407
+ const parts = depName.split("/");
408
+ group = parts[0];
409
+ name = parts.slice(1).join("/");
410
+ }
411
+ const purlObj = new PackageURL(
412
+ "npm",
413
+ group || null,
414
+ name,
415
+ null,
416
+ null,
417
+ null,
418
+ );
419
+ const purlString = purlObj.toString();
420
+ if (seen.has(purlString)) {
421
+ continue;
422
+ }
423
+ seen.add(purlString);
424
+ const versRange = toVersRange(depVersion);
425
+ const component = {
426
+ group,
427
+ name,
428
+ purl: purlString,
429
+ "bom-ref": decodeURIComponent(purlString),
430
+ type: "library",
431
+ scope,
432
+ };
433
+ if (versRange) {
434
+ component.versionRange = versRange;
435
+ }
436
+ components.push(component);
437
+ dependsOn.push(decodeURIComponent(purlString));
438
+ }
439
+ }
440
+
441
+ const dependencies = [];
442
+ if (extensionPurl && dependsOn.length) {
443
+ dependencies.push({
444
+ ref: decodeURIComponent(extensionPurl),
445
+ dependsOn: dependsOn.sort(),
446
+ });
447
+ }
448
+
449
+ return { components, dependencies };
450
+ }
451
+
452
+ /**
453
+ * Parse a VS Code extension's `package.json` and extract metadata
454
+ * including deep capability and permission information.
455
+ *
456
+ * @param {string|Object} packageJsonData Either raw JSON string or parsed object
457
+ * @param {string} [srcPath] Optional path to the source directory for evidence
458
+ * @returns {Object|undefined} Object with metadata and capabilities or undefined
459
+ */
460
+ export function parseVsixPackageJson(packageJsonData, srcPath) {
461
+ try {
462
+ const pkg =
463
+ typeof packageJsonData === "string"
464
+ ? JSON.parse(packageJsonData)
465
+ : packageJsonData;
466
+ if (!pkg?.name) {
467
+ return undefined;
468
+ }
469
+ const externalReferences = [];
470
+ if (pkg.repository?.url) {
471
+ externalReferences.push({ type: "vcs", url: pkg.repository.url });
472
+ }
473
+ return {
474
+ publisher: pkg.publisher || "",
475
+ name: pkg.name || "",
476
+ version: pkg.version || "",
477
+ displayName: pkg.displayName || "",
478
+ description: pkg.description || "",
479
+ platform: "",
480
+ srcPath,
481
+ externalReferences: externalReferences.length
482
+ ? externalReferences
483
+ : undefined,
484
+ capabilities: extractExtensionCapabilities(pkg),
485
+ dependencies: pkg.dependencies,
486
+ devDependencies: pkg.devDependencies,
487
+ peerDependencies: pkg.peerDependencies,
488
+ optionalDependencies: pkg.optionalDependencies,
489
+ };
490
+ } catch (e) {
491
+ if (DEBUG_MODE) {
492
+ console.log("Error parsing extension package.json:", e.message);
493
+ }
494
+ return undefined;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Extract deep capability and permission information from a VS Code
500
+ * extension package.json.
501
+ *
502
+ * This captures security-relevant metadata such as:
503
+ * - activationEvents: when the extension activates (e.g., `*` means always)
504
+ * - extensionKind: where the extension runs (ui, workspace, or both)
505
+ * - permissions: workspace trust, virtual workspace support
506
+ * - contributes: commands, debuggers, terminal profiles, task providers, fs providers
507
+ * - extensionDependencies/extensionPack: required extensions
508
+ * - scripts: whether postinstall or other lifecycle scripts exist
509
+ * - main/browser: entry points for analysis
510
+ *
511
+ * @param {Object} pkg Parsed package.json object
512
+ * @returns {Object} Capabilities object with structured metadata
513
+ */
514
+ export function extractExtensionCapabilities(pkg) {
515
+ if (!pkg) {
516
+ return {};
517
+ }
518
+ const capabilities = {};
519
+
520
+ // Activation events - security relevant: "*" means the extension activates for every workspace
521
+ if (pkg.activationEvents?.length) {
522
+ capabilities.activationEvents = pkg.activationEvents;
523
+ }
524
+
525
+ // Extension kind - where the extension runs (ui=local, workspace=remote, both)
526
+ if (pkg.extensionKind?.length) {
527
+ capabilities.extensionKind = pkg.extensionKind;
528
+ }
529
+
530
+ // Extension dependencies - other extensions this requires
531
+ if (pkg.extensionDependencies?.length) {
532
+ capabilities.extensionDependencies = pkg.extensionDependencies;
533
+ }
534
+
535
+ // Extension pack - bundled extensions
536
+ if (pkg.extensionPack?.length) {
537
+ capabilities.extensionPack = pkg.extensionPack;
538
+ }
539
+
540
+ // Workspace trust configuration
541
+ if (pkg.capabilities?.untrustedWorkspaces) {
542
+ capabilities.untrustedWorkspaces = pkg.capabilities.untrustedWorkspaces;
543
+ }
544
+ if (pkg.capabilities?.virtualWorkspaces) {
545
+ capabilities.virtualWorkspaces = pkg.capabilities.virtualWorkspaces;
546
+ }
547
+
548
+ // Contributed features
549
+ const contributes = pkg.contributes || {};
550
+ const contributedFeatures = [];
551
+ for (const feature of [
552
+ "authentication",
553
+ "breakpoints",
554
+ "commands",
555
+ "chatInstructions",
556
+ "chatPromptFiles",
557
+ "customEditors",
558
+ "configuration",
559
+ "debuggers",
560
+ "taskDefinitions",
561
+ "terminal",
562
+ "views",
563
+ ]) {
564
+ if (contributes[feature]?.length) {
565
+ contributedFeatures.push(
566
+ `${feature}:count:${contributes[feature].length}`,
567
+ );
568
+ }
569
+ }
570
+ if (contributes.terminal?.length || contributes.taskDefinitions?.length) {
571
+ contributedFeatures.push("terminal-access");
572
+ }
573
+ if (contributes["terminal.profiles"]?.length) {
574
+ contributedFeatures.push("terminal-profiles");
575
+ }
576
+ if (
577
+ contributes.typescriptServerPlugins?.length ||
578
+ contributes.jsonValidation?.length
579
+ ) {
580
+ contributedFeatures.push("language-server-plugins");
581
+ }
582
+ if (
583
+ contributes["resourceLabelFormatters"]?.length ||
584
+ contributes["fileSystemProviders"]?.length
585
+ ) {
586
+ contributedFeatures.push("filesystem-provider");
587
+ }
588
+ if (contributes.authentication?.length) {
589
+ contributedFeatures.push("authentication-provider");
590
+ }
591
+ if (contributes.walkthroughs?.length) {
592
+ contributedFeatures.push("walkthroughs");
593
+ }
594
+ if (contributedFeatures.length) {
595
+ capabilities.contributes = contributedFeatures;
596
+ }
597
+ if (pkg.main) {
598
+ capabilities.main = pkg.main;
599
+ }
600
+ if (pkg.browser) {
601
+ capabilities.browser = pkg.browser;
602
+ }
603
+ const scripts = pkg.scripts || {};
604
+ const lifecycleScripts = [];
605
+ for (const scriptName of [
606
+ "postinstall",
607
+ "preinstall",
608
+ "install",
609
+ "prepare",
610
+ "prepublish",
611
+ "vscode:prepublish",
612
+ "vscode:uninstall",
613
+ ]) {
614
+ if (scripts[scriptName]) {
615
+ lifecycleScripts.push(scriptName);
616
+ }
617
+ }
618
+ if (lifecycleScripts.length) {
619
+ capabilities.lifecycleScripts = lifecycleScripts;
620
+ }
621
+
622
+ return capabilities;
623
+ }
624
+
625
+ /**
626
+ * Convert parsed extension metadata into a CycloneDX component object.
627
+ *
628
+ * @param {Object} extInfo Object with { publisher, name, version, displayName, description, platform, srcPath, capabilities }
629
+ * @param {string} [ideName] Optional IDE name for properties
630
+ * @returns {Object|undefined} CycloneDX component object or undefined
631
+ */
632
+ export function toComponent(extInfo, ideName) {
633
+ if (!extInfo?.name) {
634
+ return undefined;
635
+ }
636
+ const qualifiers = {};
637
+ if (extInfo.platform) {
638
+ qualifiers.platform = extInfo.platform;
639
+ }
640
+ const purl = new PackageURL(
641
+ VSCODE_EXTENSION_PURL_TYPE,
642
+ extInfo.publisher || null,
643
+ extInfo.name,
644
+ extInfo.version || null,
645
+ Object.keys(qualifiers).length ? qualifiers : null,
646
+ null,
647
+ ).toString();
648
+ const component = {
649
+ publisher: extInfo.publisher || "",
650
+ group: extInfo.publisher || "",
651
+ name: extInfo.name,
652
+ version: extInfo.version || "",
653
+ description: extInfo.displayName || extInfo.description || "",
654
+ purl,
655
+ "bom-ref": decodeURIComponent(purl),
656
+ type: "application",
657
+ };
658
+ if (extInfo.description && extInfo.description !== component.description) {
659
+ component.description = extInfo.description;
660
+ }
661
+ const props = [];
662
+ if (ideName) {
663
+ props.push({ name: "cdx:vscode-extension:ide", value: ideName });
664
+ }
665
+ if (extInfo.srcPath) {
666
+ props.push({ name: "SrcFile", value: extInfo.srcPath });
667
+ }
668
+ // Add capability properties from deep extension analysis
669
+ const caps = extInfo.capabilities || {};
670
+ if (caps.activationEvents?.length) {
671
+ props.push({
672
+ name: "cdx:vscode-extension:activationEvents",
673
+ value: caps.activationEvents.join(", "),
674
+ });
675
+ }
676
+ // extensionKind can come from capabilities (package.json) or directly from manifest Properties
677
+ const extensionKind = caps.extensionKind || extInfo.extensionKind;
678
+ if (extensionKind?.length) {
679
+ props.push({
680
+ name: "cdx:vscode-extension:extensionKind",
681
+ value: extensionKind.join(", "),
682
+ });
683
+ }
684
+ // extensionDependencies can come from capabilities or manifest Properties
685
+ const extensionDeps =
686
+ caps.extensionDependencies || extInfo.extensionDependencies;
687
+ if (extensionDeps?.length) {
688
+ props.push({
689
+ name: "cdx:vscode-extension:extensionDependencies",
690
+ value: extensionDeps.join(", "),
691
+ });
692
+ }
693
+ // extensionPack can come from capabilities or manifest Properties
694
+ const extensionPack = caps.extensionPack || extInfo.extensionPack;
695
+ if (extensionPack?.length) {
696
+ props.push({
697
+ name: "cdx:vscode-extension:extensionPack",
698
+ value: extensionPack.join(", "),
699
+ });
700
+ }
701
+ if (caps.untrustedWorkspaces !== undefined) {
702
+ const uws = caps.untrustedWorkspaces;
703
+ props.push({
704
+ name: "cdx:vscode-extension:untrustedWorkspaces",
705
+ value:
706
+ typeof uws === "object" && uws.supported !== undefined
707
+ ? String(uws.supported)
708
+ : String(uws),
709
+ });
710
+ }
711
+ if (caps.virtualWorkspaces !== undefined) {
712
+ const vws = caps.virtualWorkspaces;
713
+ props.push({
714
+ name: "cdx:vscode-extension:virtualWorkspaces",
715
+ value:
716
+ typeof vws === "object" && vws.supported !== undefined
717
+ ? String(vws.supported)
718
+ : String(vws),
719
+ });
720
+ }
721
+ if (caps.contributes?.length) {
722
+ props.push({
723
+ name: "cdx:vscode-extension:contributes",
724
+ value: caps.contributes.join(", "),
725
+ });
726
+ }
727
+ if (caps.main) {
728
+ props.push({ name: "cdx:vscode-extension:main", value: caps.main });
729
+ }
730
+ if (caps.browser) {
731
+ props.push({ name: "cdx:vscode-extension:browser", value: caps.browser });
732
+ }
733
+ if (caps.lifecycleScripts?.length) {
734
+ props.push({
735
+ name: "cdx:vscode-extension:lifecycleScripts",
736
+ value: caps.lifecycleScripts.join(", "),
737
+ });
738
+ }
739
+ // Properties from vsixmanifest Properties tag
740
+ if (extInfo.executesCode !== undefined) {
741
+ props.push({
742
+ name: "cdx:vscode-extension:executesCode",
743
+ value: String(extInfo.executesCode),
744
+ });
745
+ }
746
+ if (extInfo.vscodeEngine) {
747
+ props.push({
748
+ name: "cdx:vscode-extension:vscodeEngine",
749
+ value: extInfo.vscodeEngine,
750
+ });
751
+ }
752
+ if (props.length) {
753
+ component.properties = props;
754
+ }
755
+ // Build externalReferences from links (manifest Properties) or from package.json repository
756
+ const externalRefs = [];
757
+ if (extInfo.externalReferences?.length) {
758
+ externalRefs.push(...extInfo.externalReferences);
759
+ }
760
+ if (extInfo.links) {
761
+ if (extInfo.links.Source || extInfo.links.GitHub) {
762
+ const vcsUrl = extInfo.links.Source || extInfo.links.GitHub;
763
+ if (!externalRefs.some((r) => r.type === "vcs")) {
764
+ externalRefs.push({ type: "vcs", url: vcsUrl });
765
+ }
766
+ }
767
+ if (extInfo.links.Support) {
768
+ externalRefs.push({ type: "issue-tracker", url: extInfo.links.Support });
769
+ }
770
+ if (extInfo.links.Learn) {
771
+ externalRefs.push({ type: "documentation", url: extInfo.links.Learn });
772
+ }
773
+ if (extInfo.links.Getstarted) {
774
+ externalRefs.push({ type: "website", url: extInfo.links.Getstarted });
775
+ }
776
+ }
777
+ if (externalRefs.length) {
778
+ component.externalReferences = externalRefs;
779
+ }
780
+ component.evidence = {
781
+ identity: {
782
+ field: "purl",
783
+ confidence: MANIFEST_ANALYSIS_CONFIDENCE,
784
+ methods: [
785
+ {
786
+ technique: "manifest-analysis",
787
+ confidence: MANIFEST_ANALYSIS_CONFIDENCE,
788
+ value: extInfo.srcPath || "",
789
+ },
790
+ ],
791
+ },
792
+ };
793
+ return component;
794
+ }
795
+
796
+ /**
797
+ * Extract a `.vsix` file (ZIP archive) to a temporary directory for deep
798
+ * analysis. The caller is responsible for cleaning up the temp directory.
799
+ *
800
+ * @param {string} vsixFile Absolute path to the `.vsix` file
801
+ * @returns {Promise<string|undefined>} Path to the extracted temp directory, or undefined on failure
802
+ */
803
+ export async function extractVsixToTempDir(vsixFile) {
804
+ let tempDir;
805
+ let zip;
806
+ try {
807
+ tempDir = mkdtempSync(join(getTmpDir(), "vsix-deps-"));
808
+ zip = new StreamZip.async({ file: vsixFile });
809
+ await zip.extract(null, tempDir);
810
+ // Most vsix files have content under extension/ subdirectory
811
+ const extensionSubDir = join(tempDir, "extension");
812
+ if (safeExistsSync(extensionSubDir)) {
813
+ return extensionSubDir;
814
+ }
815
+ return tempDir;
816
+ } catch (e) {
817
+ if (DEBUG_MODE) {
818
+ console.log(`Error extracting vsix file ${vsixFile}:`, e.message);
819
+ }
820
+ cleanupTempDir(tempDir);
821
+ return undefined;
822
+ } finally {
823
+ if (zip) {
824
+ try {
825
+ await zip.close();
826
+ } catch (_e) {
827
+ // Best effort close
828
+ }
829
+ }
830
+ }
831
+ }
832
+
833
+ /**
834
+ * Clean up a temporary directory created during vsix extraction.
835
+ *
836
+ * @param {string} tempDir Path to the temp directory to remove
837
+ */
838
+ export function cleanupTempDir(tempDir) {
839
+ if (!tempDir) {
840
+ return;
841
+ }
842
+ // The tempDir might be a subdirectory (e.g., "extension" inside the actual temp dir)
843
+ // Walk up to verify the parent is under the temp base
844
+ const resolvedDir = resolve(tempDir);
845
+ const dirToRemove =
846
+ basename(resolvedDir) === "extension"
847
+ ? resolve(resolvedDir, "..")
848
+ : resolvedDir;
849
+ try {
850
+ // Safety: only remove dirs that are direct children of the temp base with vsix-deps- prefix
851
+ const expectedBase = resolve(getTmpDir());
852
+ const dirBaseName = basename(dirToRemove);
853
+ if (
854
+ dirBaseName.startsWith("vsix-deps-") &&
855
+ resolve(dirToRemove, "..") === expectedBase
856
+ ) {
857
+ rmSync(dirToRemove, { recursive: true, force: true });
858
+ }
859
+ } catch (_e) {
860
+ // Best effort cleanup
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Parse a `.vsix` file (ZIP archive) and extract the extension metadata.
866
+ *
867
+ * @param {string} vsixFile Absolute path to the `.vsix` file
868
+ * @returns {Promise<Object|undefined>} CycloneDX component object or undefined
869
+ */
870
+ export async function parseVsixFile(vsixFile) {
871
+ let zip;
872
+ try {
873
+ zip = new StreamZip.async({ file: vsixFile });
874
+ const entries = await zip.entries();
875
+ let extInfo;
876
+
877
+ // Try .vsixmanifest first
878
+ for (const entry of Object.values(entries)) {
879
+ if (entry.isDirectory) {
880
+ continue;
881
+ }
882
+ if (
883
+ entry.name.endsWith(".vsixmanifest") ||
884
+ entry.name.endsWith("extension.vsixmanifest")
885
+ ) {
886
+ const fileData = await zip.entryData(entry.name);
887
+ const manifestData = fileData.toString("utf-8");
888
+ extInfo = parseVsixManifest(manifestData);
889
+ if (extInfo) {
890
+ extInfo.srcPath = vsixFile;
891
+ break;
892
+ }
893
+ }
894
+ }
895
+
896
+ // Fall back to package.json inside the extension/ directory
897
+ if (!extInfo) {
898
+ for (const entry of Object.values(entries)) {
899
+ if (entry.isDirectory) {
900
+ continue;
901
+ }
902
+ if (
903
+ entry.name === "extension/package.json" ||
904
+ entry.name === "package.json"
905
+ ) {
906
+ const fileData = await zip.entryData(entry.name);
907
+ const packageJsonData = fileData.toString("utf-8");
908
+ extInfo = parseVsixPackageJson(packageJsonData, vsixFile);
909
+ if (extInfo) {
910
+ break;
911
+ }
912
+ }
913
+ }
914
+ }
915
+
916
+ if (extInfo) {
917
+ return toComponent(extInfo);
918
+ }
919
+ return undefined;
920
+ } catch (e) {
921
+ if (DEBUG_MODE) {
922
+ console.log(`Error parsing vsix file ${vsixFile}:`, e.message);
923
+ }
924
+ return undefined;
925
+ } finally {
926
+ if (zip) {
927
+ try {
928
+ await zip.close();
929
+ } catch (_e) {
930
+ // Best effort close
931
+ }
932
+ }
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Parse a single installed extension directory (already extracted).
938
+ * Looks for `package.json` (preferred) and `.vsixmanifest`.
939
+ *
940
+ * @param {string} extDir Absolute path to the extension directory (e.g. `~/.vscode/extensions/ms-python.python-2023.1.0`)
941
+ * @param {string} [ideName] Optional IDE name
942
+ * @returns {Object|undefined} CycloneDX component object or undefined
943
+ */
944
+ export function parseInstalledExtensionDir(extDir, ideName) {
945
+ // First try package.json at the root of the extension directory
946
+ const packageJsonPath = join(extDir, "package.json");
947
+ if (safeExistsSync(packageJsonPath)) {
948
+ try {
949
+ const data = readFileSync(packageJsonPath, { encoding: "utf-8" });
950
+ const extInfo = parseVsixPackageJson(data, extDir);
951
+ if (extInfo?.name) {
952
+ return toComponent(extInfo, ideName);
953
+ }
954
+ } catch (_e) {
955
+ // Fall through to vsixmanifest
956
+ }
957
+ }
958
+
959
+ // Try .vsixmanifest at the root
960
+ const manifestPath = join(extDir, ".vsixmanifest");
961
+ if (safeExistsSync(manifestPath)) {
962
+ try {
963
+ const data = readFileSync(manifestPath, { encoding: "utf-8" });
964
+ const extInfo = parseVsixManifest(data);
965
+ if (extInfo) {
966
+ extInfo.srcPath = extDir;
967
+ return toComponent(extInfo, ideName);
968
+ }
969
+ } catch (_e) {
970
+ // Ignore
971
+ }
972
+ }
973
+
974
+ // Try to infer from directory name (publisher.name-version pattern)
975
+ return parseExtensionDirName(extDir, ideName);
976
+ }
977
+
978
+ /**
979
+ * Attempt to extract extension metadata from a directory name following the
980
+ * pattern `publisher.name-version`.
981
+ *
982
+ * @param {string} extDir Absolute path to extension directory
983
+ * @param {string} [ideName] IDE name
984
+ * @returns {Object|undefined} CycloneDX component or undefined
985
+ */
986
+ export function parseExtensionDirName(extDir, ideName) {
987
+ const dirName = extDir.split(/[/\\]/).pop();
988
+ if (!dirName) {
989
+ return undefined;
990
+ }
991
+ // Pattern: publisher.name-version (e.g., ms-python.python-2023.25.0)
992
+ // Use a non-backtracking approach: split on the last hyphen followed by a digit
993
+ const dotIdx = dirName.indexOf(".");
994
+ if (dotIdx < 1) {
995
+ return undefined;
996
+ }
997
+ const publisher = dirName.substring(0, dotIdx);
998
+ const rest = dirName.substring(dotIdx + 1);
999
+ // Find the last hyphen followed by a digit to separate name from version
1000
+ let versionStart = -1;
1001
+ for (let i = rest.length - 1; i >= 0; i--) {
1002
+ if (rest[i] === "-" && i + 1 < rest.length && /\d/.test(rest[i + 1])) {
1003
+ versionStart = i;
1004
+ break;
1005
+ }
1006
+ }
1007
+ if (versionStart < 1) {
1008
+ return undefined;
1009
+ }
1010
+ const name = rest.substring(0, versionStart);
1011
+ const version = rest.substring(versionStart + 1);
1012
+ if (name && version) {
1013
+ const extInfo = {
1014
+ publisher: publisher,
1015
+ name: name,
1016
+ version,
1017
+ displayName: "",
1018
+ description: "",
1019
+ platform: "",
1020
+ srcPath: extDir,
1021
+ };
1022
+ return toComponent(extInfo, ideName);
1023
+ }
1024
+ return undefined;
1025
+ }
1026
+
1027
+ /**
1028
+ * Collect all installed extensions from a set of IDE extension directories.
1029
+ *
1030
+ * @param {Array<{name: string, dir: string}>} ideDirs Array of { name, dir } from discoverIdeExtensionDirs
1031
+ * @returns {Object[]} Array of CycloneDX component objects
1032
+ */
1033
+ export function collectInstalledExtensions(ideDirs) {
1034
+ const pkgList = [];
1035
+ const seen = new Set();
1036
+
1037
+ for (const { name: ideName, dir } of ideDirs) {
1038
+ let entries;
1039
+ try {
1040
+ entries = readdirSync(dir, { withFileTypes: true });
1041
+ } catch (_e) {
1042
+ continue;
1043
+ }
1044
+ for (const entry of entries) {
1045
+ if (!entry.isDirectory()) {
1046
+ continue;
1047
+ }
1048
+ // Skip hidden directories and special directories
1049
+ if (entry.name.startsWith(".")) {
1050
+ continue;
1051
+ }
1052
+ const extDir = join(dir, entry.name);
1053
+ const component = parseInstalledExtensionDir(extDir, ideName);
1054
+ if (component && !seen.has(component.purl)) {
1055
+ seen.add(component.purl);
1056
+ pkgList.push(component);
1057
+ }
1058
+ }
1059
+ }
1060
+ return pkgList;
1061
+ }