@cdxgen/cdxgen 13.6.0 → 13.7.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.
- package/README.md +15 -8
- package/bin/audit.js +7 -4
- package/bin/cdxgen.js +256 -37
- package/bin/convert.js +146 -22
- package/bin/evinse.js +25 -3
- package/bin/repl.js +47 -3
- package/bin/sign.js +211 -34
- package/bin/tracebom.js +18 -6
- package/bin/validate.js +18 -3
- package/bin/verify.js +113 -5
- package/data/behavior-taxonomy.schema.json +1491 -0
- package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
- package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
- package/data/pqc-migration.json +239 -0
- package/data/remediations.json +52 -0
- package/data/rules/cbom-security.yaml +4 -3
- package/data/rules/package-integrity.yaml +28 -0
- package/lib/audit/index.js +25 -3
- package/lib/cli/bomAssembly.js +80 -1
- package/lib/cli/cliOptions.js +121 -17
- package/lib/cli/index.js +609 -898
- package/lib/cli/jsBom.js +3 -0
- package/lib/cli/jvmBom.js +365 -40
- package/lib/cli/managedBom.js +19 -3
- package/lib/cli/nativeBom.js +10 -0
- package/lib/core/env.js +26 -0
- package/lib/core/fs.js +126 -3
- package/lib/core/globPatterns.js +218 -0
- package/lib/core/processTree.js +99 -12
- package/lib/core/projectConfig.js +332 -25
- package/lib/core/toolRegistry.js +15 -1
- package/lib/ecosystems/denoutils.js +2 -2
- package/lib/ecosystems/golem.js +3 -8
- package/lib/ecosystems/gradleutils.js +1082 -6
- package/lib/ecosystems/jsEvidence.js +90 -38
- package/lib/ecosystems/kosi.js +3 -5
- package/lib/ecosystems/npmVersionResolution.js +376 -0
- package/lib/ecosystems/parsers-python.js +89 -28
- package/lib/ecosystems/parsers-rust.js +117 -15
- package/lib/ecosystems/remote/huggingface.js +22 -0
- package/lib/ecosystems/rusi.js +2 -4
- package/lib/ecosystems/scalasem.js +2 -1
- package/lib/evinser/evinser.js +196 -34
- package/lib/evinser/scalasem.js +5 -1
- package/lib/helpers/agenticToolutils.js +112 -1
- package/lib/helpers/bomSigner.js +43 -9
- package/lib/helpers/jssSigner.js +1276 -0
- package/lib/inventory/agentBlueprint.js +193 -0
- package/lib/inventory/agentToolNames.js +145 -0
- package/lib/inventory/aiCollector.js +153 -7
- package/lib/inventory/aiInventory.js +44 -8
- package/lib/inventory/aiProviders.js +324 -0
- package/lib/inventory/bomUtils.js +8 -1
- package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
- package/lib/inventory/citations.js +31 -2
- package/lib/inventory/communityAiConfigParser.js +978 -80
- package/lib/inventory/cyclonedx20.js +152 -0
- package/lib/inventory/cyclonedx20Common.js +286 -0
- package/lib/inventory/cyclonedx20Derived.js +159 -0
- package/lib/inventory/cyclonedx20Parties.js +552 -0
- package/lib/inventory/cyclonedx20Read.js +768 -0
- package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
- package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
- package/lib/inventory/cyclonedx20ReadContext.js +380 -0
- package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
- package/lib/inventory/cyclonedx20ReadSections.js +599 -0
- package/lib/inventory/cyclonedx20Render.js +766 -0
- package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
- package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
- package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
- package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
- package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
- package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
- package/lib/inventory/cyclonedx20RenderSections.js +499 -0
- package/lib/inventory/cyclonedx20Tables.js +594 -0
- package/lib/inventory/deps.js +106 -13
- package/lib/inventory/dosai.js +236 -1
- package/lib/inventory/dosaiParsers.js +3 -6
- package/lib/inventory/dotnetRuntime.js +164 -14
- package/lib/inventory/ecosystemFormulationParsers.js +979 -0
- package/lib/inventory/envcontext.js +132 -38
- package/lib/inventory/evidenceUtils.js +2 -1
- package/lib/inventory/formulationParsers.js +95 -13
- package/lib/inventory/hbom.js +45 -6
- package/lib/inventory/hbomLoader.js +166 -0
- package/lib/inventory/mcpConfigParser.js +5 -0
- package/lib/inventory/occurrenceKey.js +90 -0
- package/lib/inventory/pqcMigration.js +426 -0
- package/lib/inventory/rustFormulationParser.js +21 -10
- package/lib/inventory/source.js +65 -6
- package/lib/parsers/huggingfaceManifest.js +9 -1
- package/lib/parsers/huggingfaceUtils.js +18 -0
- package/lib/parsers/kiroPermissions.js +166 -0
- package/lib/server/server.js +36 -34
- package/lib/stages/postgen/postgen.js +41 -1
- package/lib/stages/postgen/specVersionCompat.js +76 -0
- package/lib/validator/bomValidator.js +8 -1
- package/lib/validator/complianceEngine.js +30 -3
- package/lib/validator/complianceRules.js +1416 -4
- package/lib/validator/generated/validate-2.0.mjs +57554 -13851
- package/lib/validator/index.js +48 -7
- package/lib/validator/reporters/json.js +6 -0
- package/package.json +9 -9
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +6 -2
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +16 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +6 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +2 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +15 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +30 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +10 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +4 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/globPatterns.d.ts +70 -0
- package/types/lib/core/globPatterns.d.ts.map +1 -0
- package/types/lib/core/paths.d.ts +9 -0
- package/types/lib/core/paths.d.ts.map +1 -1
- package/types/lib/core/processTree.d.ts +16 -5
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts +47 -8
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +248 -8
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
- package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts +58 -16
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +61 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/scalasem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +53 -19
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +20 -3
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/helpers/jssSigner.d.ts +288 -0
- package/types/lib/helpers/jssSigner.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +303 -19
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/inventory/agentBlueprint.d.ts +70 -0
- package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
- package/types/lib/inventory/agentToolNames.d.ts +51 -0
- package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/aiProviders.d.ts +85 -0
- package/types/lib/inventory/aiProviders.d.ts.map +1 -0
- package/types/lib/inventory/bomUtils.d.ts.map +1 -1
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
- package/types/lib/inventory/citations.d.ts +5 -0
- package/types/lib/inventory/citations.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/cyclonedx20.d.ts +99 -0
- package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +35 -2
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +19 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +22 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts +11 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomLoader.d.ts +54 -0
- package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/occurrenceKey.d.ts +52 -0
- package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
- package/types/lib/inventory/pqcMigration.d.ts +84 -0
- package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
- package/types/lib/inventory/scalaDedupe.d.ts +43 -0
- package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +15 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/kiroPermissions.d.ts +62 -0
- package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +7 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts +5 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceEngine.d.ts +4 -4
- package/types/lib/validator/complianceEngine.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts +14 -2
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
package/lib/core/fs.js
CHANGED
|
@@ -140,10 +140,126 @@ export function safeIsExecutableSync(filePath) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// writeFileSync flags that create or replace a file, which the no-follow
|
|
144
|
+
// write below reproduces with open(2) flags.
|
|
145
|
+
const NO_FOLLOW_WRITE_FLAGS = new Set([
|
|
146
|
+
"a",
|
|
147
|
+
"a+",
|
|
148
|
+
"as",
|
|
149
|
+
"as+",
|
|
150
|
+
"ax",
|
|
151
|
+
"ax+",
|
|
152
|
+
"w",
|
|
153
|
+
"w+",
|
|
154
|
+
"wx",
|
|
155
|
+
"wx+",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Determine whether a path names a device or a process file, such as
|
|
160
|
+
* /dev/stdout, /dev/null or the /dev/fd entry of a shell process
|
|
161
|
+
* substitution. These are symbolic links an operator names on purpose, so
|
|
162
|
+
* they are written through as given.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} filePath File path to check.
|
|
165
|
+
* @returns {boolean} True for paths under /dev or /proc on POSIX systems.
|
|
166
|
+
*/
|
|
167
|
+
function isDeviceOrProcessPath(filePath) {
|
|
168
|
+
if (isWin) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const absolutePath = resolve(filePath);
|
|
172
|
+
return absolutePath.startsWith("/dev/") || absolutePath.startsWith("/proc/");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Convert a writeFileSync flag string into open(2) flags.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} flag Flag such as "w", "a" or "wx".
|
|
179
|
+
* @returns {number} Numeric open flags.
|
|
180
|
+
*/
|
|
181
|
+
function writeOpenFlags(flag) {
|
|
182
|
+
let flags =
|
|
183
|
+
fsConstants.O_CREAT |
|
|
184
|
+
(flag.includes("+") ? fsConstants.O_RDWR : fsConstants.O_WRONLY) |
|
|
185
|
+
(flag.startsWith("a") ? fsConstants.O_APPEND : fsConstants.O_TRUNC);
|
|
186
|
+
if (flag.includes("x")) {
|
|
187
|
+
flags |= fsConstants.O_EXCL;
|
|
188
|
+
}
|
|
189
|
+
return flags;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Write a file without following a symbolic link in the last component of
|
|
194
|
+
* its path. A link found there is removed and a regular file is created in
|
|
195
|
+
* its place, so a link committed to a scanned repository under a name cdxgen
|
|
196
|
+
* writes, such as bom.json, cannot send the write to the file it points at.
|
|
197
|
+
* The open uses O_NOFOLLOW where the platform has it, so a link that appears
|
|
198
|
+
* after the check makes the write fail rather than follow it. Links in the
|
|
199
|
+
* parent directories are followed as usual.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} filePath File path to write.
|
|
202
|
+
* @param {string|Buffer|Uint8Array} data Data to write.
|
|
203
|
+
* @param {Object|string} [options] writeFileSync options or encoding.
|
|
204
|
+
* @returns {boolean} True when a symbolic link was replaced.
|
|
205
|
+
*/
|
|
206
|
+
function writeFileWithoutFollowingLink(filePath, data, options) {
|
|
207
|
+
const flag =
|
|
208
|
+
(typeof options === "object" && options?.flag
|
|
209
|
+
? String(options.flag)
|
|
210
|
+
: undefined) || "w";
|
|
211
|
+
if (
|
|
212
|
+
typeof filePath !== "string" ||
|
|
213
|
+
!NO_FOLLOW_WRITE_FLAGS.has(flag) ||
|
|
214
|
+
isDeviceOrProcessPath(filePath)
|
|
215
|
+
) {
|
|
216
|
+
writeFileSync(filePath, data, options);
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
let replacedLink = false;
|
|
220
|
+
let pathStats;
|
|
221
|
+
try {
|
|
222
|
+
pathStats = lstatSync(filePath);
|
|
223
|
+
} catch {
|
|
224
|
+
pathStats = undefined;
|
|
225
|
+
}
|
|
226
|
+
if (pathStats?.isSymbolicLink()) {
|
|
227
|
+
// rmSync removes the link itself, including a directory link on Windows.
|
|
228
|
+
rmSync(filePath, { force: true });
|
|
229
|
+
replacedLink = true;
|
|
230
|
+
}
|
|
231
|
+
const openFlags =
|
|
232
|
+
writeOpenFlags(flag) |
|
|
233
|
+
(fsConstants.O_NOFOLLOW || 0) |
|
|
234
|
+
(replacedLink ? fsConstants.O_EXCL : 0);
|
|
235
|
+
const mode =
|
|
236
|
+
typeof options === "object" && options?.mode !== undefined
|
|
237
|
+
? options.mode
|
|
238
|
+
: 0o666;
|
|
239
|
+
const fd = openSync(filePath, openFlags, mode);
|
|
240
|
+
try {
|
|
241
|
+
writeFileSync(fd, data, {
|
|
242
|
+
encoding: typeof options === "string" ? options : options?.encoding,
|
|
243
|
+
});
|
|
244
|
+
} finally {
|
|
245
|
+
closeSync(fd);
|
|
246
|
+
}
|
|
247
|
+
if (replacedLink) {
|
|
248
|
+
console.warn(
|
|
249
|
+
`'${filePath}' was a symbolic link. cdxgen replaced the link with a regular file instead of writing to the file it pointed at.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return replacedLink;
|
|
253
|
+
}
|
|
254
|
+
|
|
143
255
|
/**
|
|
144
256
|
* Permission- and dry-run-aware wrapper around writeFileSync. Records the
|
|
145
257
|
* activity and returns undefined when blocked.
|
|
146
258
|
*
|
|
259
|
+
* The last component of the path is never followed when it is a symbolic
|
|
260
|
+
* link: the link is replaced with a regular file. Paths under /dev and /proc,
|
|
261
|
+
* such as /dev/stdout, are written through as given.
|
|
262
|
+
*
|
|
147
263
|
* @param {string} filePath File path to write.
|
|
148
264
|
* @param {string|Buffer} data Data to write.
|
|
149
265
|
* @param {Object} [options] writeFileSync options (encoding, mode, flag).
|
|
@@ -153,8 +269,15 @@ export function safeWriteSync(filePath, data, options) {
|
|
|
153
269
|
if (isWriteBlocked(filePath)) {
|
|
154
270
|
return undefined;
|
|
155
271
|
}
|
|
156
|
-
|
|
157
|
-
recordFilesystemActivity(
|
|
272
|
+
const replacedLink = writeFileWithoutFollowingLink(filePath, data, options);
|
|
273
|
+
recordFilesystemActivity(
|
|
274
|
+
"write",
|
|
275
|
+
filePath,
|
|
276
|
+
"completed",
|
|
277
|
+
replacedLink
|
|
278
|
+
? "Replaced a symbolic link with a regular file instead of following it."
|
|
279
|
+
: undefined,
|
|
280
|
+
);
|
|
158
281
|
}
|
|
159
282
|
|
|
160
283
|
/**
|
|
@@ -173,7 +296,7 @@ export function safeWriteChunksSync(filePath, chunks) {
|
|
|
173
296
|
return undefined;
|
|
174
297
|
}
|
|
175
298
|
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
176
|
-
const fd = openSync(tempPath, "
|
|
299
|
+
const fd = openSync(tempPath, "wx");
|
|
177
300
|
try {
|
|
178
301
|
for (const chunk of chunks) {
|
|
179
302
|
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Longest exclude or include pattern cdxgen accepts from an untrusted source,
|
|
3
|
+
* such as a server request or a config file in the scanned directory.
|
|
4
|
+
*/
|
|
5
|
+
export const UNTRUSTED_GLOB_PATTERN_MAX_LENGTH = 512;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Most patterns a list of untrusted exclude or include patterns may stand
|
|
9
|
+
* for once brace expressions are expanded. Every expanded pattern is matched
|
|
10
|
+
* against every file the scan walks, so the total, not the length of the
|
|
11
|
+
* list as written, is what decides the cost.
|
|
12
|
+
*/
|
|
13
|
+
export const UNTRUSTED_GLOB_EXPANSION_BUDGET = 128;
|
|
14
|
+
|
|
15
|
+
function isIntegerText(text) {
|
|
16
|
+
const digits = text.startsWith("-") ? text.slice(1) : text;
|
|
17
|
+
return (
|
|
18
|
+
digits.length > 0 && [...digits].every((char) => char >= "0" && char <= "9")
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isLetter(text) {
|
|
23
|
+
return (
|
|
24
|
+
text.length === 1 &&
|
|
25
|
+
((text >= "a" && text <= "z") || (text >= "A" && text <= "Z"))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Count the members of a brace range such as `1..10`, `a..z` or `0..100..5`.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} body text between the braces
|
|
33
|
+
* @param {number} limit value returned for ranges at or beyond the limit
|
|
34
|
+
* @returns {number|undefined} member count, or undefined when the body is
|
|
35
|
+
* not a range
|
|
36
|
+
*/
|
|
37
|
+
function braceRangeSize(body, limit) {
|
|
38
|
+
const parts = body.split("..");
|
|
39
|
+
if (parts.length !== 2 && parts.length !== 3) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const [start, end, step = "1"] = parts;
|
|
43
|
+
if (!isIntegerText(step)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const increment = Math.abs(Number(step)) || 1;
|
|
47
|
+
let distance;
|
|
48
|
+
if (isIntegerText(start) && isIntegerText(end)) {
|
|
49
|
+
distance = Math.abs(Number(end) - Number(start));
|
|
50
|
+
} else if (isLetter(start) && isLetter(end)) {
|
|
51
|
+
distance = Math.abs(end.charCodeAt(0) - start.charCodeAt(0));
|
|
52
|
+
} else {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const size = Math.floor(distance / increment) + 1;
|
|
56
|
+
return Number.isFinite(size) && size < limit ? size : limit;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function newBraceGroup(start) {
|
|
60
|
+
return {
|
|
61
|
+
current: 1,
|
|
62
|
+
hasAnyComma: false,
|
|
63
|
+
hasComma: false,
|
|
64
|
+
hasDots: false,
|
|
65
|
+
hasGroup: false,
|
|
66
|
+
start,
|
|
67
|
+
sum: 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Count, from above, how many patterns a glob's brace expressions expand to,
|
|
73
|
+
* following the brace-expansion package that minimatch uses:
|
|
74
|
+
*
|
|
75
|
+
* - alternatives in `{a,b}` add up, and groups side by side or inside one
|
|
76
|
+
* another multiply;
|
|
77
|
+
* - a range such as `{1..9}`, `{a..z}` or `{0..100..5}` counts its members;
|
|
78
|
+
* - a group with no comma of its own but a list inside it, such as
|
|
79
|
+
* `{{a,b}c}`, is expanded and each result is braced and expanded again,
|
|
80
|
+
* so it counts as its contents, unless its text holds `..` and a result
|
|
81
|
+
* could become a range of any size.
|
|
82
|
+
*
|
|
83
|
+
* brace-expansion reads braces that form neither a list nor a range in ways
|
|
84
|
+
* that depend on the rest of the pattern, so a group that is neither, a
|
|
85
|
+
* closing brace with no group open, a group left open, and the `..` case
|
|
86
|
+
* above all count as more than `cap`. Patterns that list alternatives or
|
|
87
|
+
* ranges, such as `{test,tests}` or `*.{js,ts}`, get the exact count, or one
|
|
88
|
+
* slightly above it where a letter range such as `{Z..c}` spans the `\` that
|
|
89
|
+
* brace-expansion leaves out. `\` escapes the next character. The count stops growing at
|
|
90
|
+
* `cap + 1` and each character is read once, so a pattern that would expand
|
|
91
|
+
* to millions costs no more to count than its length.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} pattern glob pattern
|
|
94
|
+
* @param {number} [cap] largest count of interest
|
|
95
|
+
* @returns {number} upper bound on the number of expanded patterns, at most
|
|
96
|
+
* `cap + 1`
|
|
97
|
+
*/
|
|
98
|
+
export function globBraceExpansionCount(
|
|
99
|
+
pattern,
|
|
100
|
+
cap = Number.MAX_SAFE_INTEGER - 1,
|
|
101
|
+
) {
|
|
102
|
+
const limit = cap + 1;
|
|
103
|
+
const clamp = (value) => Math.min(value, limit);
|
|
104
|
+
const groups = [newBraceGroup(0)];
|
|
105
|
+
const text = String(pattern);
|
|
106
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
107
|
+
const character = text[index];
|
|
108
|
+
if (character === "\\") {
|
|
109
|
+
index += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const group = groups.at(-1);
|
|
113
|
+
if (character === "{") {
|
|
114
|
+
group.hasGroup = true;
|
|
115
|
+
groups.push(newBraceGroup(index + 1));
|
|
116
|
+
} else if (character === "." && text[index + 1] === ".") {
|
|
117
|
+
group.hasDots = true;
|
|
118
|
+
} else if (character === "," && groups.length > 1) {
|
|
119
|
+
group.sum = clamp(group.sum + group.current);
|
|
120
|
+
group.current = 1;
|
|
121
|
+
group.hasComma = true;
|
|
122
|
+
group.hasAnyComma = true;
|
|
123
|
+
} else if (character === "}") {
|
|
124
|
+
if (groups.length === 1) {
|
|
125
|
+
return limit;
|
|
126
|
+
}
|
|
127
|
+
let count;
|
|
128
|
+
if (group.hasComma) {
|
|
129
|
+
count = clamp(group.sum + group.current);
|
|
130
|
+
} else if (group.hasAnyComma) {
|
|
131
|
+
count = group.hasDots ? limit : group.current;
|
|
132
|
+
} else if (!group.hasGroup) {
|
|
133
|
+
count = braceRangeSize(text.slice(group.start, index), limit);
|
|
134
|
+
}
|
|
135
|
+
if (count === undefined) {
|
|
136
|
+
return limit;
|
|
137
|
+
}
|
|
138
|
+
groups.pop();
|
|
139
|
+
const parent = groups.at(-1);
|
|
140
|
+
parent.current = clamp(parent.current * count);
|
|
141
|
+
parent.hasAnyComma ||= group.hasAnyComma;
|
|
142
|
+
parent.hasDots ||= group.hasDots;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return groups.length === 1 ? groups[0].current : limit;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Split exclude or include patterns from an untrusted source into the ones
|
|
150
|
+
* cdxgen uses and the ones it refuses. Patterns are taken in order while
|
|
151
|
+
* they fit the expansion budget, so a list cannot multiply the work of a
|
|
152
|
+
* scan by brace expansion or sheer number.
|
|
153
|
+
*
|
|
154
|
+
* @param {unknown} patterns a pattern, a list of patterns, or nothing
|
|
155
|
+
* @param {Object} [options] bound options
|
|
156
|
+
* @param {boolean} [options.allowBraces=true] refuse every pattern with a
|
|
157
|
+
* brace when false
|
|
158
|
+
* @param {number} [options.budget] expanded patterns allowed in all
|
|
159
|
+
* @param {number} [options.used=0] expanded patterns earlier lists have
|
|
160
|
+
* already taken from the budget
|
|
161
|
+
* @returns {{accepted: string[], refused: {pattern: string, reason: string}[], expansions: number}}
|
|
162
|
+
* the patterns to use, the ones refused with the reason, and the expanded
|
|
163
|
+
* patterns the accepted ones use
|
|
164
|
+
*/
|
|
165
|
+
export function boundUntrustedGlobPatterns(
|
|
166
|
+
patterns,
|
|
167
|
+
{
|
|
168
|
+
allowBraces = true,
|
|
169
|
+
budget = UNTRUSTED_GLOB_EXPANSION_BUDGET,
|
|
170
|
+
used = 0,
|
|
171
|
+
} = {},
|
|
172
|
+
) {
|
|
173
|
+
const entries =
|
|
174
|
+
patterns === undefined || patterns === null
|
|
175
|
+
? []
|
|
176
|
+
: Array.isArray(patterns)
|
|
177
|
+
? patterns
|
|
178
|
+
: [patterns];
|
|
179
|
+
const accepted = [];
|
|
180
|
+
const refused = [];
|
|
181
|
+
let expansions = 0;
|
|
182
|
+
for (const entry of entries) {
|
|
183
|
+
if (typeof entry !== "string" && typeof entry !== "number") {
|
|
184
|
+
refused.push({ pattern: String(entry), reason: "is not a string" });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const pattern = String(entry);
|
|
188
|
+
if (pattern.length > UNTRUSTED_GLOB_PATTERN_MAX_LENGTH) {
|
|
189
|
+
refused.push({
|
|
190
|
+
pattern,
|
|
191
|
+
reason: `is longer than ${UNTRUSTED_GLOB_PATTERN_MAX_LENGTH} characters`,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!allowBraces && pattern.includes("{")) {
|
|
196
|
+
refused.push({ pattern, reason: "uses braces" });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const count = globBraceExpansionCount(pattern, budget);
|
|
200
|
+
if (count > budget) {
|
|
201
|
+
refused.push({
|
|
202
|
+
pattern,
|
|
203
|
+
reason: `expands to more than ${budget} patterns`,
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (used + expansions + count > budget) {
|
|
208
|
+
refused.push({
|
|
209
|
+
pattern,
|
|
210
|
+
reason: `would bring the patterns past ${budget} after brace expansion`,
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
expansions += count;
|
|
215
|
+
accepted.push(pattern);
|
|
216
|
+
}
|
|
217
|
+
return { accepted, expansions, refused };
|
|
218
|
+
}
|
package/lib/core/processTree.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and the dosai runners (layer 2) can share it.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
|
|
8
10
|
import { DEBUG_MODE } from "./activity.js";
|
|
9
11
|
import { safeSpawnSync } from "./fs.js";
|
|
10
12
|
import { isWin } from "./paths.js";
|
|
@@ -24,6 +26,54 @@ export function formatDuration(ms) {
|
|
|
24
26
|
return `${Math.round(ms / 100) / 10} seconds`;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Longest the PowerShell process snapshot behind reapProcessTree may run.
|
|
31
|
+
*/
|
|
32
|
+
const REAP_TIMEOUT_MS = 60000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Longest reapProcessTree waits for the processes it stopped to exit.
|
|
36
|
+
*/
|
|
37
|
+
const REAP_EXIT_WAIT_MS = 10000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Block the calling thread for a number of milliseconds.
|
|
41
|
+
*
|
|
42
|
+
* @param {number} ms Milliseconds to wait
|
|
43
|
+
*/
|
|
44
|
+
function sleepSync(ms) {
|
|
45
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a process with this pid is still running.
|
|
50
|
+
*
|
|
51
|
+
* @param {number} pid Process id
|
|
52
|
+
* @returns {boolean} true while the process exists
|
|
53
|
+
*/
|
|
54
|
+
function isRunning(pid) {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, 0);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// EPERM means the process exists but belongs to someone else.
|
|
60
|
+
return err?.code === "EPERM";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A UTC instant as a DMTF datetime, the form WQL compares CIM_DATETIME
|
|
66
|
+
* properties such as Win32_Process.CreationDate against.
|
|
67
|
+
*
|
|
68
|
+
* @param {number} ms Epoch milliseconds
|
|
69
|
+
* @returns {string} The instant as `yyyymmddHHMMSS.mmmmmm+000`
|
|
70
|
+
*/
|
|
71
|
+
export function toDmtfUtc(ms) {
|
|
72
|
+
const date = new Date(ms);
|
|
73
|
+
const pad = (value, width = 2) => `${value}`.padStart(width, "0");
|
|
74
|
+
return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}.${pad(date.getUTCMilliseconds(), 3)}000+000`;
|
|
75
|
+
}
|
|
76
|
+
|
|
27
77
|
/**
|
|
28
78
|
* Stop the descendants a timed-out run left behind on Windows.
|
|
29
79
|
*
|
|
@@ -32,13 +82,16 @@ export function formatDuration(ms) {
|
|
|
32
82
|
* workers, or the build hosts a .NET analyzer starts keep running while still
|
|
33
83
|
* pointing at the dead process's pid. `taskkill /T` on that pid finds nothing,
|
|
34
84
|
* because the root is already gone. The live descendants are therefore found
|
|
35
|
-
* by parent pid in a process snapshot
|
|
36
|
-
* created after the run started. The root's own
|
|
37
|
-
* its end: once the root is gone its pid can be
|
|
38
|
-
* process, whose children would otherwise look like
|
|
85
|
+
* by parent pid in a PowerShell process snapshot, limited to processes
|
|
86
|
+
* created after the run started, and stopped from Node.js. The root's own
|
|
87
|
+
* children must also predate its end: once the root is gone its pid can be
|
|
88
|
+
* handed to an unrelated process, whose children would otherwise look like
|
|
89
|
+
* the run's. Elsewhere this
|
|
39
90
|
* does nothing: an orphan is re-parented and can no longer be told apart.
|
|
40
91
|
* It returns once the stopped processes have exited, or after ten seconds,
|
|
41
|
-
* so the caller can remove files the run left half-written.
|
|
92
|
+
* so the caller can remove files the run left half-written. When the
|
|
93
|
+
* snapshot itself fails or runs past its time limit it warns and returns an
|
|
94
|
+
* empty list, since processes may then still be running.
|
|
42
95
|
*
|
|
43
96
|
* @param {number|undefined} rootPid pid of the process cdxgen spawned
|
|
44
97
|
* @param {number} startedAt Epoch milliseconds at which the run started
|
|
@@ -55,15 +108,24 @@ export function reapProcessTree(
|
|
|
55
108
|
if (!isWin || !Number.isInteger(rootPid) || rootPid <= 0) {
|
|
56
109
|
return [];
|
|
57
110
|
}
|
|
58
|
-
// All three values are integers,
|
|
111
|
+
// All three values are integers, and the DMTF string is built from digits
|
|
112
|
+
// only, so interpolating them cannot inject anything.
|
|
59
113
|
const since = Math.max(0, Math.floor(startedAt) - 1000);
|
|
60
114
|
const until = Math.max(since, Math.floor(endedAt) + 1000);
|
|
115
|
+
const snapshotProperties = "-Property ProcessId,ParentProcessId,CreationDate";
|
|
61
116
|
const script = [
|
|
62
117
|
`$root = ${rootPid}`,
|
|
63
118
|
`$since = [DateTimeOffset]::FromUnixTimeMilliseconds(${since}).UtcDateTime`,
|
|
64
119
|
`$until = [DateTimeOffset]::FromUnixTimeMilliseconds(${until}).UtcDateTime`,
|
|
120
|
+
// WMI drops the processes that predate the run before PowerShell sees
|
|
121
|
+
// them, which keeps the snapshot small on a busy host. The loop below
|
|
122
|
+
// applies the same bound, so a WMI that rejects the filter only costs the
|
|
123
|
+
// full snapshot.
|
|
124
|
+
"$procs = @()",
|
|
125
|
+
`try { $procs = @(Get-CimInstance Win32_Process -Filter "CreationDate >= '${toDmtfUtc(since)}'" ${snapshotProperties} -ErrorAction Stop) }`,
|
|
126
|
+
`catch { $procs = @(Get-CimInstance Win32_Process ${snapshotProperties}) }`,
|
|
65
127
|
"$children = @{}",
|
|
66
|
-
"foreach ($p in
|
|
128
|
+
"foreach ($p in $procs) {",
|
|
67
129
|
" if (-not $p.CreationDate) { continue }",
|
|
68
130
|
" $created = $p.CreationDate.ToUniversalTime()",
|
|
69
131
|
" if ($created -lt $since) { continue }",
|
|
@@ -78,22 +140,47 @@ export function reapProcessTree(
|
|
|
78
140
|
" if ($child -and -not $found.Contains($child)) { $found.Add($child); $queue.Enqueue($child) }",
|
|
79
141
|
" }",
|
|
80
142
|
"}",
|
|
81
|
-
"foreach ($child in $found) { Stop-Process -Id $child -Force -ErrorAction SilentlyContinue }",
|
|
82
|
-
// Stop-Process returns before the process exits, and a process that is
|
|
83
|
-
// still exiting keeps its files open, so wait for the exits.
|
|
84
|
-
"if ($found.Count) { Wait-Process -Id $found.ToArray() -Timeout 10 -ErrorAction SilentlyContinue }",
|
|
85
143
|
"$found -join ','",
|
|
86
144
|
].join("\n");
|
|
87
145
|
const result = safeSpawnSync(
|
|
88
146
|
"powershell",
|
|
89
147
|
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
90
|
-
{ timeout:
|
|
148
|
+
{ timeout: REAP_TIMEOUT_MS },
|
|
91
149
|
);
|
|
150
|
+
if (result?.error || result?.status !== 0) {
|
|
151
|
+
// An empty result here is not evidence that nothing was left running.
|
|
152
|
+
console.warn(
|
|
153
|
+
result?.error?.code === "ETIMEDOUT"
|
|
154
|
+
? `Could not look for processes the timed-out ${toolName} run left behind: the process snapshot took longer than ${formatDuration(REAP_TIMEOUT_MS)}.`
|
|
155
|
+
: `Could not look for processes the timed-out ${toolName} run left behind: PowerShell ${result?.error ? `failed to start (${result.error.message})` : `exited with status ${result?.status}`}.`,
|
|
156
|
+
);
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
92
159
|
const reaped = `${result?.stdout || ""}`
|
|
93
160
|
.trim()
|
|
94
161
|
.split(",")
|
|
95
162
|
.map((pid) => Number.parseInt(pid, 10))
|
|
96
163
|
.filter((pid) => pid > 0);
|
|
164
|
+
for (const pid of reaped) {
|
|
165
|
+
try {
|
|
166
|
+
process.kill(pid, "SIGKILL");
|
|
167
|
+
} catch {
|
|
168
|
+
// The process exited between the snapshot and the kill.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// A process that is still exiting keeps its files open, so wait for the
|
|
172
|
+
// exits, but never longer than REAP_EXIT_WAIT_MS.
|
|
173
|
+
const deadline = Date.now() + REAP_EXIT_WAIT_MS;
|
|
174
|
+
let running = reaped.filter(isRunning);
|
|
175
|
+
while (running.length && Date.now() < deadline) {
|
|
176
|
+
sleepSync(100);
|
|
177
|
+
running = running.filter(isRunning);
|
|
178
|
+
}
|
|
179
|
+
if (running.length) {
|
|
180
|
+
console.warn(
|
|
181
|
+
`${running.length} process(es) the timed-out ${toolName} run left behind were still exiting after ${formatDuration(REAP_EXIT_WAIT_MS)}: ${running.join(", ")}.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
97
184
|
if (DEBUG_MODE) {
|
|
98
185
|
console.log(
|
|
99
186
|
reaped.length
|