@cyclonedx/cdxgen 12.1.5 → 12.2.1
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.
- package/README.md +51 -40
- package/bin/cdxgen.js +194 -97
- package/bin/evinse.js +4 -4
- package/bin/repl.js +1 -1
- package/bin/sign.js +102 -0
- package/bin/validate.js +233 -0
- package/bin/verify.js +69 -28
- package/data/queries.json +1 -1
- package/data/rules/ci-permissions.yaml +186 -0
- package/data/rules/dependency-sources.yaml +123 -0
- package/data/rules/package-integrity.yaml +135 -0
- package/data/rules/vscode-extensions.yaml +228 -0
- package/lib/cli/index.js +449 -429
- package/lib/cli/index.poku.js +117 -0
- package/lib/evinser/db.js +137 -0
- package/lib/{helpers → evinser}/db.poku.js +2 -6
- package/lib/evinser/evinser.js +2 -14
- package/lib/helpers/analyzer.js +606 -3
- package/lib/helpers/analyzer.poku.js +230 -0
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/bomSigner.poku.js +156 -0
- package/lib/helpers/ciParsers/azurePipelines.js +295 -0
- package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
- package/lib/helpers/ciParsers/circleCi.js +286 -0
- package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
- package/lib/helpers/ciParsers/common.js +24 -0
- package/lib/helpers/ciParsers/githubActions.js +636 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
- package/lib/helpers/ciParsers/gitlabCi.js +213 -0
- package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
- package/lib/helpers/ciParsers/jenkins.js +181 -0
- package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
- package/lib/helpers/depsUtils.js +219 -0
- package/lib/helpers/depsUtils.poku.js +207 -0
- package/lib/helpers/display.js +426 -5
- package/lib/helpers/envcontext.js +18 -3
- package/lib/helpers/formulationParsers.js +351 -0
- package/lib/helpers/logger.js +14 -0
- package/lib/helpers/protobom.js +9 -9
- package/lib/helpers/pythonutils.js +9 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/utils.js +865 -416
- package/lib/helpers/utils.poku.js +172 -265
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/versutils.poku.js +315 -0
- package/lib/helpers/vsixutils.js +1061 -0
- package/lib/helpers/vsixutils.poku.js +2247 -0
- package/lib/managers/binary.js +19 -19
- package/lib/managers/docker.js +108 -1
- package/lib/managers/oci.js +10 -0
- package/lib/managers/piptree.js +3 -9
- package/lib/parsers/npmrc.js +17 -13
- package/lib/parsers/npmrc.poku.js +41 -5
- package/lib/server/openapi.yaml +34 -1
- package/lib/server/server.js +50 -13
- package/lib/server/server.poku.js +332 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +196 -0
- package/lib/stages/postgen/auditBom.poku.js +378 -0
- package/lib/stages/postgen/postgen.js +54 -1
- package/lib/stages/postgen/postgen.poku.js +90 -1
- package/lib/stages/postgen/ruleEngine.js +369 -0
- package/lib/stages/pregen/envAudit.js +299 -0
- package/lib/stages/pregen/envAudit.poku.js +572 -0
- package/lib/stages/pregen/pregen.js +12 -8
- package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceEngine.poku.js +168 -0
- package/lib/validator/complianceRules.js +1610 -0
- package/lib/validator/complianceRules.poku.js +328 -0
- package/lib/validator/index.js +222 -0
- package/lib/validator/index.poku.js +144 -0
- package/lib/validator/reporters/annotations.js +121 -0
- package/lib/validator/reporters/console.js +149 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/lib/validator/reporters.poku.js +150 -0
- package/package.json +8 -9
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/validate.d.ts +3 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/helpers/utils.d.ts +0 -1
- package/types/lib/cli/index.d.ts +49 -52
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/db.d.ts +34 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -16
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +27 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/common.d.ts +11 -0
- package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +21 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +111 -11
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +19 -7
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts +50 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
- package/types/lib/helpers/logger.d.ts +15 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -2
- package/types/lib/helpers/pythonutils.d.ts +10 -1
- package/types/lib/helpers/pythonutils.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +533 -128
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +12 -31
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +11 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/npmrc.d.ts +4 -1
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +22 -2
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +20 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +8 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +8 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +66 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +70 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +70 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +31 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +30 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +21 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +16 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/lib/helpers/db.js +0 -162
- package/lib/stages/pregen/env-audit.js +0 -34
- package/lib/stages/pregen/env-audit.poku.js +0 -290
- package/types/helpers/db.d.ts +0 -35
- package/types/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/db.d.ts +0 -35
- package/types/lib/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/validator.d.ts.map +0 -1
- package/types/lib/stages/pregen/env-audit.d.ts +0 -2
- package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
- package/types/managers/binary.d.ts +0 -37
- package/types/managers/binary.d.ts.map +0 -1
- package/types/managers/docker.d.ts +0 -56
- package/types/managers/docker.d.ts.map +0 -1
- package/types/managers/oci.d.ts +0 -2
- package/types/managers/oci.d.ts.map +0 -1
- package/types/managers/piptree.d.ts +0 -2
- package/types/managers/piptree.d.ts.map +0 -1
- package/types/server/server.d.ts +0 -34
- package/types/server/server.d.ts.map +0 -1
- package/types/stages/postgen/annotator.d.ts +0 -27
- package/types/stages/postgen/annotator.d.ts.map +0 -1
- package/types/stages/postgen/postgen.d.ts +0 -51
- package/types/stages/postgen/postgen.d.ts.map +0 -1
- package/types/stages/pregen/pregen.d.ts +0 -59
- 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
|
+
}
|