@cdxgen/cdxgen 13.4.0 → 13.6.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 +9 -9
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +29 -5
- package/bin/evinse.js +8 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/bin/validate.js +6 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/license-aliases.json +24 -2
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +407 -1
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -0
- package/data/rules/cbom-security.yaml +235 -4
- package/data/rules/go-binaries.yaml +46 -0
- package/data/rules/golem-go.yaml +80 -0
- package/data/rules/hbom-compliance.yaml +40 -22
- package/data/rules/hbom-performance.yaml +7 -1
- package/data/rules/host-topology.yaml +1 -1
- package/data/rules/obom-runtime.yaml +31 -2
- package/data/rules/php-runtime.yaml +143 -0
- package/data/rules/vscode-extensions.yaml +75 -0
- package/data/spdx-license-list.json +121 -7
- package/data/spdx-licenses.json +69 -0
- package/data/spdx.schema.json +845 -2
- package/lib/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +120 -6
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +182 -6
- package/lib/cli/jsBom.js +153 -64
- package/lib/cli/jvmBom.js +363 -66
- package/lib/cli/managedBom.js +853 -87
- package/lib/cli/nativeBom.js +267 -33
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +38 -8
- package/lib/core/fs.js +40 -19
- package/lib/core/httpClient.js +172 -46
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +5 -0
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -0
- package/lib/core/toolRegistry.js +170 -0
- package/lib/ecosystems/cppEvidence.js +263 -106
- package/lib/ecosystems/cppIncludeResolver.js +16 -5
- package/lib/ecosystems/ecosystems.js +25 -10
- package/lib/ecosystems/gemHostProbe.js +301 -0
- package/lib/ecosystems/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +858 -244
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +438 -64
- package/lib/ecosystems/parsers-python.js +100 -12
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/rubyutils.js +544 -34
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +4 -1
- package/lib/ecosystems/scalasem.js +798 -151
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +472 -61
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/helpers/agenticToolutils.js +413 -93
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +110 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +41 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +639 -63
- package/lib/inventory/dotnetRuntime.js +217 -0
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +475 -18
- package/lib/inventory/evidenceUtils.js +447 -23
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +32 -3
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -0
- package/lib/inventory/hostTopology.js +21 -0
- package/lib/inventory/jvmLocalRepos.js +9 -2
- package/lib/inventory/jvmToolEnv.js +29 -0
- package/lib/inventory/licenseEnhancer.js +12 -0
- package/lib/inventory/mavenCentralMirror.js +534 -0
- package/lib/inventory/nodeBuildInfo.js +218 -0
- package/lib/inventory/nodeHostProbe.js +202 -0
- package/lib/inventory/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +271 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/phpBuildInfo.js +438 -0
- package/lib/inventory/phpHostProbe.js +185 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/rubyBuildInfo.js +972 -0
- package/lib/inventory/rubyHostProbe.js +218 -0
- package/lib/inventory/scalaCoords.js +4 -3
- package/lib/inventory/scalaDedupe.js +440 -0
- package/lib/inventory/source.js +256 -5
- package/lib/inventory/spdx.js +144 -0
- package/lib/inventory/traceRunner.js +5 -1
- package/lib/inventory/tracedCrypto.js +129 -0
- package/lib/managers/binary.js +866 -186
- package/lib/managers/docker.js +245 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +62 -13
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +198 -2
- package/lib/stages/postgen/annotator.js +17 -1
- package/lib/stages/postgen/introspection/reflect.js +4 -1
- package/lib/stages/postgen/postgen.js +5 -5
- package/lib/stages/postgen/ruleEngine.js +71 -6
- package/lib/stages/postgen/specVersionCompat.js +30 -0
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +231 -62
- package/lib/validator/bomValidator.js +32 -6
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +27 -8
- package/lib/validator/generated/validate-1.7.mjs +32 -13
- package/lib/validator/generated/validate-2.0.mjs +31 -12
- package/lib/validator/index.js +1 -1
- package/package.json +19 -18
- package/types/bin/cdxgen-secure.d.ts +3 -0
- package/types/bin/cdxgen-secure.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +22 -3
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +43 -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 +1 -17
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +20 -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 +70 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -0
- package/types/lib/core/brotliProbe.d.ts +37 -0
- package/types/lib/core/brotliProbe.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +22 -2
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +24 -0
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/lockPaths.d.ts +19 -0
- package/types/lib/core/lockPaths.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +2 -0
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/projectTooling.d.ts +41 -0
- package/types/lib/core/projectTooling.d.ts.map +1 -0
- package/types/lib/core/propertySanitizer.d.ts +26 -0
- package/types/lib/core/propertySanitizer.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts +36 -0
- package/types/lib/core/toolRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
- package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
- package/types/lib/ecosystems/golem.d.ts +81 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +57 -21
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +85 -13
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +17 -20
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +14 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +19 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +144 -12
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
- package/types/lib/ecosystems/venvProbe.d.ts +40 -0
- package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +88 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +10 -0
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
- package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
- package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
- package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +33 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/brotliDecoder.d.ts +58 -0
- package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
- package/types/lib/inventory/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/certificates.d.ts +205 -0
- package/types/lib/inventory/certificates.d.ts.map +1 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts +2 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts +18 -0
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +162 -7
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
- package/types/lib/inventory/dynamic.d.ts +6 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts +92 -8
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +29 -10
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/goBuildInfo.d.ts +10 -0
- package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/gtfobins.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
- package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
- package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
- package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
- package/types/lib/inventory/hostTopology.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
- package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
- package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
- package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/osPackagePurl.d.ts +99 -0
- package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +44 -2
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/osinfo.d.ts +31 -6
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/osqueryTransform.d.ts +51 -0
- package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/phpHostProbe.d.ts +35 -0
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/rootfsCerts.d.ts +27 -0
- package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
- package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +38 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +15 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +4 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +22 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +10 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +24 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/email.d.ts +19 -0
- package/types/lib/parsers/email.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +17 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +8 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +26 -9
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
// Bounded Ruby build-version reader for unpackaged Ruby installs, and the one
|
|
2
|
+
// place a Ruby version becomes a runtime component, for the container scan,
|
|
3
|
+
// the OS scan, formulation and the source scan alike.
|
|
4
|
+
//
|
|
5
|
+
// The Ruby of a Docker Official image, ruby-build, rbenv, rvm, asdf, mise,
|
|
6
|
+
// chruby and the RubyInstaller is owned by no package manager, so a BOM sees it
|
|
7
|
+
// only as files. Its version decides whether the interpreter is affected by
|
|
8
|
+
// advisories in its core, such as the Onigmo fixes of Ruby 3.3.13, 3.4.12 and
|
|
9
|
+
// 4.0.9 (CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, CVE-2026-110264).
|
|
10
|
+
//
|
|
11
|
+
// Three facts identify a build, read in this order:
|
|
12
|
+
// - rbconfig.rb, which `make install` writes under
|
|
13
|
+
// <prefix>/lib/ruby/<abi>/<arch>/ (or <prefix>/lib/<triplet>/ruby/<abi>/ in
|
|
14
|
+
// a Debian layout). RUBY_PROGRAM_VERSION in it is the unexpanded template
|
|
15
|
+
// "$(MAJOR).$(MINOR).$(TEENY)", so the version is assembled from the MAJOR,
|
|
16
|
+
// MINOR and TEENY entries, with PATCHLEVEL beside them:
|
|
17
|
+
// https://github.com/ruby/ruby/blob/v3_3_6/tool/mkconfig.rb
|
|
18
|
+
// - the description string compiled into libruby (or a static `ruby`), the
|
|
19
|
+
// first line `ruby -v` prints: `ruby 3.3.6 (2024-11-05 revision 75015d4c1f)
|
|
20
|
+
// [x86_64-linux]`. It also names a preview or release candidate, which a
|
|
21
|
+
// PATCHLEVEL of -1 in rbconfig.rb leaves unsaid:
|
|
22
|
+
// https://github.com/ruby/ruby/blob/v3_3_6/version.c
|
|
23
|
+
// - the libruby soname, libruby.so.3.3.6, when nothing else could be read.
|
|
24
|
+
//
|
|
25
|
+
// Every file in an image or on a host is hostile: only regular files inside
|
|
26
|
+
// the root are opened, reads are capped, nothing is executed, and a missing or
|
|
27
|
+
// odd file yields no result rather than throwing.
|
|
28
|
+
|
|
29
|
+
import { readdirSync, statSync } from "node:fs";
|
|
30
|
+
import { basename, join } from "node:path";
|
|
31
|
+
|
|
32
|
+
import { readRegularTextFile, scanFileMarkers } from "../core/boundedFile.js";
|
|
33
|
+
import { applyPurl, tryBuildPurl } from "./purl.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The most Ruby-named files one scan opens in search of a build version.
|
|
37
|
+
*
|
|
38
|
+
* @type {number}
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_RUBY_PROBES = 64;
|
|
41
|
+
|
|
42
|
+
const MAX_RBCONFIG_BYTES = 512 * 1024;
|
|
43
|
+
const MAX_LISTED_DIRECTORIES = 32;
|
|
44
|
+
const MAX_VERSION_LENGTH = 32;
|
|
45
|
+
const DESCRIPTION_MARKER = Buffer.from("ruby ");
|
|
46
|
+
|
|
47
|
+
// MRI versions: 3.3.6, 3.4.0-preview2 (also written 3.4.0preview2), 3.4.0-rc1,
|
|
48
|
+
// 3.5.0dev. A patchlevel suffix (p108) belongs to the description, not the
|
|
49
|
+
// version.
|
|
50
|
+
const VERSION_RE = /^(\d+\.\d+\.\d+)(?:-?(preview|rc)(\d+)|(dev))?$/;
|
|
51
|
+
// The Ruby ABI directory: 3.3.0, or 3.5.0+0 for a development build.
|
|
52
|
+
const ABI_RE = /^\d+\.\d+\.\d+(?:\+\d+)?$/;
|
|
53
|
+
const RBCONFIG_ENTRY_RE = /^\s*CONFIG\["([A-Za-z_]+)"\]\s*=\s*"([^"\\]*)"\s*$/;
|
|
54
|
+
const RBCONFIG_KEYS = new Set([
|
|
55
|
+
"MAJOR",
|
|
56
|
+
"MINOR",
|
|
57
|
+
"TEENY",
|
|
58
|
+
"PATCHLEVEL",
|
|
59
|
+
"ruby_version",
|
|
60
|
+
"arch",
|
|
61
|
+
"RUBY_BASE_NAME",
|
|
62
|
+
"RUBY_PROGRAM_VERSION",
|
|
63
|
+
"RUBY_SO_NAME",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse an MRI version and normalize a pre-release to the hyphenated form the
|
|
68
|
+
* release tarballs use: `3.4.0preview2` becomes `3.4.0-preview2`.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} text candidate version, without a patchlevel
|
|
71
|
+
* @returns {string|undefined} the version, or undefined
|
|
72
|
+
*/
|
|
73
|
+
export function parseRubyVersion(text) {
|
|
74
|
+
if (typeof text !== "string" || !text || text.length > MAX_VERSION_LENGTH) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const match = VERSION_RE.exec(text.trim());
|
|
78
|
+
if (!match) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (match[2]) {
|
|
82
|
+
return `${match[1]}-${match[2]}${match[3]}`;
|
|
83
|
+
}
|
|
84
|
+
return match[4] ? `${match[1]}-dev` : match[1];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read the first line `ruby -v` prints, or the description compiled into
|
|
89
|
+
* libruby, for MRI, JRuby and TruffleRuby:
|
|
90
|
+
* - `ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [x86_64-linux]`
|
|
91
|
+
* - `ruby 2.7.8p225 (2023-03-30 revision 1f4d455848) [x86_64-linux]`
|
|
92
|
+
* - `ruby 3.4.0preview2 (2024-10-07 master 32c733f57b) +PRISM [x86_64-linux]`
|
|
93
|
+
* - `jruby 9.4.8.0 (3.1.4) 2024-07-02 4d41e55a67 OpenJDK 64-Bit ...`
|
|
94
|
+
* - `truffleruby 24.1.0, like ruby 3.2.4, Oracle GraalVM Native [x86_64-linux]`
|
|
95
|
+
*
|
|
96
|
+
* @param {string} text description line
|
|
97
|
+
* @returns {{engine: string, version: string, patchlevel?: string,
|
|
98
|
+
* rubyVersion?: string, platform?: string}|undefined} engine is `ruby`,
|
|
99
|
+
* `jruby` or `truffleruby`; for the last two, rubyVersion is the Ruby
|
|
100
|
+
* language version the engine implements
|
|
101
|
+
*/
|
|
102
|
+
export function parseRubyDescription(text) {
|
|
103
|
+
if (typeof text !== "string") {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const line = text.split(/\r?\n/, 1)[0].trim().slice(0, 256);
|
|
107
|
+
const space = line.indexOf(" ");
|
|
108
|
+
if (space <= 0) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const engine = line.slice(0, space);
|
|
112
|
+
const rest = line.slice(space + 1);
|
|
113
|
+
if (engine === "ruby") {
|
|
114
|
+
const end = rest.indexOf(" ");
|
|
115
|
+
if (end <= 0 || rest[end + 1] !== "(") {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
let token = rest.slice(0, end);
|
|
119
|
+
let patchlevel;
|
|
120
|
+
const pIndex = token.lastIndexOf("p");
|
|
121
|
+
if (pIndex > 0 && /^\d+$/.test(token.slice(pIndex + 1))) {
|
|
122
|
+
// `p108`, but not the `p` of `preview2`
|
|
123
|
+
const before = token.slice(0, pIndex);
|
|
124
|
+
if (parseRubyVersion(before)) {
|
|
125
|
+
patchlevel = token.slice(pIndex + 1);
|
|
126
|
+
token = before;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const version = parseRubyVersion(token);
|
|
130
|
+
if (!version) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const result = { engine, version };
|
|
134
|
+
if (patchlevel !== undefined) {
|
|
135
|
+
result.patchlevel = patchlevel;
|
|
136
|
+
}
|
|
137
|
+
const open = rest.lastIndexOf("[");
|
|
138
|
+
const close = rest.lastIndexOf("]");
|
|
139
|
+
if (open !== -1 && close > open + 1) {
|
|
140
|
+
result.platform = rest.slice(open + 1, close);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
if (engine === "jruby") {
|
|
145
|
+
const match = /^(\d+(?:\.\d+){2,3}) \((\d+\.\d+\.\d+)\)/.exec(rest);
|
|
146
|
+
return match
|
|
147
|
+
? { engine, version: match[1], rubyVersion: match[2] }
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
if (engine === "truffleruby") {
|
|
151
|
+
const match = /^(\d+(?:\.\d+){1,3}), like ruby (\d+\.\d+\.\d+)/.exec(rest);
|
|
152
|
+
return match
|
|
153
|
+
? { engine, version: match[1], rubyVersion: match[2] }
|
|
154
|
+
: undefined;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Read the entries of an rbconfig.rb that identify a build. Only literal
|
|
161
|
+
* `CONFIG["KEY"] = "value"` lines are read; Ruby is never evaluated.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} text rbconfig.rb contents
|
|
164
|
+
* @returns {{version: string, patchlevel?: string, abi?: string,
|
|
165
|
+
* arch?: string, baseName?: string}|undefined}
|
|
166
|
+
*/
|
|
167
|
+
export function parseRbConfig(text) {
|
|
168
|
+
if (typeof text !== "string" || !text) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const entries = Object.create(null);
|
|
172
|
+
for (const line of text.split("\n")) {
|
|
173
|
+
if (!line.includes("CONFIG[")) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const match = RBCONFIG_ENTRY_RE.exec(line);
|
|
177
|
+
if (match && RBCONFIG_KEYS.has(match[1]) && !(match[1] in entries)) {
|
|
178
|
+
entries[match[1]] = match[2];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const { MAJOR, MINOR, TEENY } = entries;
|
|
182
|
+
let version;
|
|
183
|
+
if ([MAJOR, MINOR, TEENY].every((part) => /^\d+$/.test(part || ""))) {
|
|
184
|
+
version = `${MAJOR}.${MINOR}.${TEENY}`;
|
|
185
|
+
} else {
|
|
186
|
+
version = parseRubyVersion(entries.RUBY_PROGRAM_VERSION);
|
|
187
|
+
}
|
|
188
|
+
if (!version) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const result = { version };
|
|
192
|
+
if (/^-?\d+$/.test(entries.PATCHLEVEL || "")) {
|
|
193
|
+
result.patchlevel = entries.PATCHLEVEL;
|
|
194
|
+
}
|
|
195
|
+
if (ABI_RE.test(entries.ruby_version || "")) {
|
|
196
|
+
result.abi = entries.ruby_version;
|
|
197
|
+
}
|
|
198
|
+
if (entries.arch && entries.arch.length <= 64) {
|
|
199
|
+
result.arch = entries.arch;
|
|
200
|
+
}
|
|
201
|
+
if (entries.RUBY_BASE_NAME && entries.RUBY_BASE_NAME.length <= 32) {
|
|
202
|
+
result.baseName = entries.RUBY_BASE_NAME;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Whether a file name is one a Ruby interpreter is installed under: `ruby`,
|
|
209
|
+
* a versioned `ruby3.3` as distros and `--program-suffix` builds name it,
|
|
210
|
+
* `ruby.exe` and `rubyw.exe`.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} fileName basename or path of the candidate
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
export function isRubyBinaryName(fileName) {
|
|
216
|
+
if (typeof fileName !== "string" || !fileName) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const name = basename(fileName.replaceAll("\\", "/"));
|
|
220
|
+
const lower = name.toLowerCase();
|
|
221
|
+
return (
|
|
222
|
+
/^ruby(\d+(\.\d+){0,2})?$/.test(name) ||
|
|
223
|
+
lower === "ruby.exe" ||
|
|
224
|
+
lower === "rubyw.exe"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Whether a file name is the shared Ruby library: libruby.so.3.3.6, Debian's
|
|
230
|
+
* libruby-3.1.so.3.1.2,
|
|
231
|
+
* libruby.3.3.dylib, or the RubyInstaller's x64-ucrt-ruby340.dll.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} fileName basename or path of the candidate
|
|
234
|
+
* @returns {boolean}
|
|
235
|
+
*/
|
|
236
|
+
export function isLibrubyName(fileName) {
|
|
237
|
+
if (typeof fileName !== "string" || !fileName) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const name = basename(fileName.replaceAll("\\", "/"));
|
|
241
|
+
return (
|
|
242
|
+
/^libruby(-\d+\.\d+)?(-static)?\.so(\.\d+){0,3}$/.test(name) ||
|
|
243
|
+
/^libruby(\.\d+){0,3}\.dylib$/.test(name) ||
|
|
244
|
+
/^[a-z0-9_]+(-[a-z0-9_]+)?-ruby\d{2,3}\.dll$/i.test(name)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The version a libruby soname spells, libruby.so.3.3.6 -> 3.3.6.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} fileName basename or path
|
|
252
|
+
* @returns {string|undefined}
|
|
253
|
+
*/
|
|
254
|
+
export function versionFromLibrubyName(fileName) {
|
|
255
|
+
const name = basename(`${fileName || ""}`.replaceAll("\\", "/"));
|
|
256
|
+
const match = /^libruby(?:-\d+\.\d+)?\.so\.(\d+\.\d+\.\d+)$/.exec(name);
|
|
257
|
+
if (match) {
|
|
258
|
+
return match[1];
|
|
259
|
+
}
|
|
260
|
+
const dylib = /^libruby\.(\d+\.\d+\.\d+)\.dylib$/.exec(name);
|
|
261
|
+
return dylib ? dylib[1] : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// lib/ruby/<abi> holds hundreds of .rb files and dozens of library
|
|
265
|
+
// directories beside the one arch directory, so the cap applies to the
|
|
266
|
+
// directories accepted, after a bounded read.
|
|
267
|
+
const MAX_LISTED_ENTRIES = 8192;
|
|
268
|
+
|
|
269
|
+
function listDirectories(directory, accept = () => true) {
|
|
270
|
+
try {
|
|
271
|
+
const directories = [];
|
|
272
|
+
for (const name of readdirSync(directory)
|
|
273
|
+
.slice(0, MAX_LISTED_ENTRIES)
|
|
274
|
+
.sort()) {
|
|
275
|
+
if (name.endsWith(".rb") || !accept(name)) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
if (statSync(join(directory, name)).isDirectory()) {
|
|
280
|
+
directories.push(name);
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// gone, or not readable
|
|
284
|
+
}
|
|
285
|
+
if (directories.length >= MAX_LISTED_DIRECTORIES) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return directories;
|
|
290
|
+
} catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function listFiles(directory) {
|
|
296
|
+
try {
|
|
297
|
+
return readdirSync(directory).sort();
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The install prefix of a file under bin, sbin or lib: /usr/local/bin/ruby ->
|
|
305
|
+
* /usr/local, C:\Ruby34-x64\bin\ruby.exe -> C:\Ruby34-x64.
|
|
306
|
+
*
|
|
307
|
+
* @param {string} absPath absolute path
|
|
308
|
+
* @returns {string|undefined}
|
|
309
|
+
*/
|
|
310
|
+
export function rubyInstallPrefix(absPath) {
|
|
311
|
+
if (typeof absPath !== "string" || !absPath) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
// Paths from an image are POSIX whatever the host is, so the separator is
|
|
315
|
+
// read from the path rather than taken from the platform.
|
|
316
|
+
const separator =
|
|
317
|
+
absPath.includes("\\") && !absPath.includes("/") ? "\\" : "/";
|
|
318
|
+
const parts = absPath.replaceAll("\\", "/").split("/");
|
|
319
|
+
for (let i = parts.length - 2; i >= 0; i--) {
|
|
320
|
+
const part = parts[i].toLowerCase();
|
|
321
|
+
if (part === "bin" || part === "sbin" || part === "lib") {
|
|
322
|
+
return parts.slice(0, i).join(separator) || separator;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The rbconfig.rb files an install prefix holds, at most a few levels down:
|
|
330
|
+
* <prefix>/lib/ruby/<abi>/<arch>/rbconfig.rb, and the Debian layout
|
|
331
|
+
* <prefix>/lib/<triplet>/ruby/<abi>/rbconfig.rb.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} prefix install prefix
|
|
334
|
+
* @returns {string[]} absolute paths
|
|
335
|
+
*/
|
|
336
|
+
export function findRbConfigFiles(prefix) {
|
|
337
|
+
const found = [];
|
|
338
|
+
const libDir = join(prefix, "lib");
|
|
339
|
+
const rubyDir = join(libDir, "ruby");
|
|
340
|
+
for (const abi of listDirectories(rubyDir, (name) => ABI_RE.test(name))) {
|
|
341
|
+
if (!ABI_RE.test(abi)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
// An arch is cpu-os (x86_64-linux, x64-mingw-ucrt, arm64-darwin24); the
|
|
345
|
+
// library directories beside it (bigdecimal, did_you_mean) have no dash.
|
|
346
|
+
for (const arch of listDirectories(join(rubyDir, abi), (name) =>
|
|
347
|
+
name.includes("-"),
|
|
348
|
+
)) {
|
|
349
|
+
const candidate = join(rubyDir, abi, arch, "rbconfig.rb");
|
|
350
|
+
if (listFiles(join(rubyDir, abi, arch)).includes("rbconfig.rb")) {
|
|
351
|
+
found.push(candidate);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
for (const triplet of listDirectories(libDir, (name) => name.includes("-"))) {
|
|
356
|
+
if (triplet === "ruby" || !triplet.includes("-")) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
for (const abi of listDirectories(join(libDir, triplet, "ruby"))) {
|
|
360
|
+
if (!ABI_RE.test(abi)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (
|
|
364
|
+
listFiles(join(libDir, triplet, "ruby", abi)).includes("rbconfig.rb")
|
|
365
|
+
) {
|
|
366
|
+
found.push(join(libDir, triplet, "ruby", abi, "rbconfig.rb"));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return found;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// The major.minor a file name pins: ruby3.3 -> 3.3, libruby.so.3.3.6 -> 3.3,
|
|
374
|
+
// x64-ucrt-ruby330.dll -> 3.3.
|
|
375
|
+
function pinnedSeries(fileName) {
|
|
376
|
+
const name = basename(`${fileName || ""}`.replaceAll("\\", "/"));
|
|
377
|
+
let match = /^ruby(\d+)\.(\d+)/.exec(name);
|
|
378
|
+
if (!match) {
|
|
379
|
+
match = /^libruby(?:-\d+\.\d+)?(?:-static)?\.so\.(\d+)\.(\d+)/.exec(name);
|
|
380
|
+
}
|
|
381
|
+
if (!match) {
|
|
382
|
+
match = /^libruby\.(\d+)\.(\d+)/.exec(name);
|
|
383
|
+
}
|
|
384
|
+
if (!match) {
|
|
385
|
+
match = /-ruby(\d)(\d)\d\.dll$/i.exec(name);
|
|
386
|
+
}
|
|
387
|
+
return match ? `${match[1]}.${match[2]}` : undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function readRbConfigFile(file, rootDir) {
|
|
391
|
+
const text = readRegularTextFile(file, MAX_RBCONFIG_BYTES, rootDir);
|
|
392
|
+
return text ? parseRbConfig(text) : undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Find the description compiled into a file, `ruby 3.3.6 (2024-11-05 ...`.
|
|
397
|
+
*
|
|
398
|
+
* @param {string} absPath absolute path of a ruby binary or libruby
|
|
399
|
+
* @param {string|undefined} rootDir extracted root
|
|
400
|
+
* @returns {ReturnType<typeof parseRubyDescription>}
|
|
401
|
+
*/
|
|
402
|
+
export function readRubyDescription(absPath, rootDir) {
|
|
403
|
+
const scan = scanFileMarkers(absPath, rootDir, [DESCRIPTION_MARKER], {
|
|
404
|
+
contextBytes: 160,
|
|
405
|
+
beforeBytes: 1,
|
|
406
|
+
maxHits: 256,
|
|
407
|
+
});
|
|
408
|
+
if (!scan || scan.skipped) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
for (const hit of scan.hits[0]) {
|
|
412
|
+
// The description is a C string of its own, so a NUL precedes it.
|
|
413
|
+
if (hit.before.length && hit.before[0] !== 0) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const after = hit.after;
|
|
417
|
+
const nul = after.indexOf(0);
|
|
418
|
+
const text = `ruby ${after.toString("latin1", 0, nul === -1 ? after.length : nul)}`;
|
|
419
|
+
const parsed = parseRubyDescription(text);
|
|
420
|
+
if (parsed?.engine === "ruby") {
|
|
421
|
+
return parsed;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// The libruby beside a ruby binary, where a shared build keeps the description.
|
|
428
|
+
function siblingLibraries(prefix) {
|
|
429
|
+
const libraries = [];
|
|
430
|
+
for (const directory of [join(prefix, "lib"), join(prefix, "bin")]) {
|
|
431
|
+
for (const name of listFiles(directory)) {
|
|
432
|
+
if (isLibrubyName(name)) {
|
|
433
|
+
libraries.push(join(directory, name));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return libraries.slice(0, 8);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Read the Ruby build version of a ruby binary or libruby.
|
|
442
|
+
*
|
|
443
|
+
* @param {string} absPath absolute path of the candidate
|
|
444
|
+
* @param {string|undefined} rootDir extracted root, when the file came from one
|
|
445
|
+
* @returns {{version: string, source: string, patchlevel?: string,
|
|
446
|
+
* abi?: string, arch?: string}|undefined} source is `rbconfig`, `binary`
|
|
447
|
+
* or `soname`
|
|
448
|
+
*/
|
|
449
|
+
export function readRubyBuildVersion(absPath, rootDir) {
|
|
450
|
+
const prefix = rubyInstallPrefix(absPath);
|
|
451
|
+
const series = pinnedSeries(absPath);
|
|
452
|
+
let build;
|
|
453
|
+
let ambiguous = false;
|
|
454
|
+
if (prefix) {
|
|
455
|
+
const configs = findRbConfigFiles(prefix)
|
|
456
|
+
.map((file) => readRbConfigFile(file, rootDir))
|
|
457
|
+
.filter(Boolean)
|
|
458
|
+
.filter(
|
|
459
|
+
(config) =>
|
|
460
|
+
!series || config.version.split(".").slice(0, 2).join(".") === series,
|
|
461
|
+
);
|
|
462
|
+
const versions = new Set(configs.map((config) => config.version));
|
|
463
|
+
if (versions.size === 1) {
|
|
464
|
+
build = { ...configs[0], source: "rbconfig" };
|
|
465
|
+
}
|
|
466
|
+
// Two Rubies share the prefix and the name does not say which this is:
|
|
467
|
+
// only the file's own description can, not a sibling's.
|
|
468
|
+
ambiguous = versions.size > 1;
|
|
469
|
+
}
|
|
470
|
+
// A PATCHLEVEL of -1 is a preview, release candidate or development build,
|
|
471
|
+
// which only the description names. Two installs sharing a prefix also need
|
|
472
|
+
// the binary to tell them apart.
|
|
473
|
+
if (!build || build.patchlevel === "-1") {
|
|
474
|
+
const files = [
|
|
475
|
+
absPath,
|
|
476
|
+
...(prefix && !ambiguous ? siblingLibraries(prefix) : []),
|
|
477
|
+
];
|
|
478
|
+
for (const file of files) {
|
|
479
|
+
const description = readRubyDescription(file, rootDir);
|
|
480
|
+
if (!description) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (
|
|
484
|
+
build &&
|
|
485
|
+
description.version.split("-")[0] !== build.version.split("-")[0]
|
|
486
|
+
) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
build = {
|
|
490
|
+
...(build || {}),
|
|
491
|
+
version: description.version,
|
|
492
|
+
source: build ? build.source : "binary",
|
|
493
|
+
};
|
|
494
|
+
if (description.patchlevel !== undefined) {
|
|
495
|
+
build.patchlevel = description.patchlevel;
|
|
496
|
+
}
|
|
497
|
+
if (!build.arch && description.platform) {
|
|
498
|
+
build.arch = description.platform;
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!build) {
|
|
504
|
+
const fromName = versionFromLibrubyName(absPath);
|
|
505
|
+
if (fromName) {
|
|
506
|
+
build = { version: fromName, source: "soname" };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!build) {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
const result = { version: build.version, source: build.source };
|
|
513
|
+
for (const key of ["patchlevel", "abi", "arch"]) {
|
|
514
|
+
if (build[key] !== undefined) {
|
|
515
|
+
result[key] = build[key];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const ENGINE_IDENTITY = {
|
|
522
|
+
ruby: { vendor: "ruby-lang", product: "ruby" },
|
|
523
|
+
jruby: { vendor: "jruby", product: "jruby" },
|
|
524
|
+
truffleruby: undefined,
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* The CPE that lets a downstream scanner match an interpreter version. NVD
|
|
529
|
+
* names MRI ruby-lang:ruby and keeps a pre-release in the update field, as in
|
|
530
|
+
* cpe:2.3:a:ruby-lang:ruby:3.4.0:preview1:*:*:*:*:*:*.
|
|
531
|
+
*
|
|
532
|
+
* @param {string} version interpreter version
|
|
533
|
+
* @param {string} [engine="ruby"] `ruby` or `jruby`
|
|
534
|
+
* @returns {string|undefined} CPE 2.3 string, or undefined for an engine NVD
|
|
535
|
+
* does not name
|
|
536
|
+
*/
|
|
537
|
+
export function rubyCpe(version, engine = "ruby") {
|
|
538
|
+
const identity = ENGINE_IDENTITY[engine];
|
|
539
|
+
if (!identity || typeof version !== "string" || !version) {
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
let base = version;
|
|
543
|
+
let update = "*";
|
|
544
|
+
const dash = version.indexOf("-");
|
|
545
|
+
if (dash !== -1) {
|
|
546
|
+
base = version.slice(0, dash);
|
|
547
|
+
update = version.slice(dash + 1).toLowerCase();
|
|
548
|
+
}
|
|
549
|
+
// NVD names no development snapshot, so a dev build has no CPE to match.
|
|
550
|
+
if (
|
|
551
|
+
!/^\d+(\.\d+){1,3}$/.test(base) ||
|
|
552
|
+
!/^(\*|[a-z0-9]+)$/.test(update) ||
|
|
553
|
+
update === "dev"
|
|
554
|
+
) {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
return `cpe:2.3:a:${identity.vendor}:${identity.product}:${base}:${update}:*:*:*:*:*:*`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* A Ruby runtime is one `platform` component per distinct engine and version,
|
|
562
|
+
* like the PHP and Node.js runtime components. Its identity is
|
|
563
|
+
* `pkg:generic/ruby` (or `jruby`, `truffleruby`).
|
|
564
|
+
*
|
|
565
|
+
* @param {string} version interpreter version
|
|
566
|
+
* @param {Object} [options] options
|
|
567
|
+
* @param {boolean} [options.withCpe=true] false for a Ruby a distro packaged,
|
|
568
|
+
* which backports fixes under its old upstream version
|
|
569
|
+
* @param {string} [options.engine="ruby"] `ruby`, `jruby` or `truffleruby`
|
|
570
|
+
* @returns {Object} component with `purl`, `bom-ref` and, when one applies, `cpe`
|
|
571
|
+
*/
|
|
572
|
+
export function createRubyRuntimeComponent(
|
|
573
|
+
version,
|
|
574
|
+
{ withCpe = true, engine = "ruby" } = {},
|
|
575
|
+
) {
|
|
576
|
+
const name = engine in ENGINE_IDENTITY ? engine : "ruby";
|
|
577
|
+
const component = { type: "platform", name, version };
|
|
578
|
+
if (withCpe) {
|
|
579
|
+
const cpe = rubyCpe(version, name);
|
|
580
|
+
if (cpe) {
|
|
581
|
+
component.cpe = cpe;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return applyPurl(component, tryBuildPurl({ type: "generic", name, version }));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The bom-ref of the runtime component for a version.
|
|
589
|
+
*
|
|
590
|
+
* @param {string} version interpreter version
|
|
591
|
+
* @param {string} [engine="ruby"] engine
|
|
592
|
+
* @returns {string|undefined} bom-ref, which is the runtime's purl
|
|
593
|
+
*/
|
|
594
|
+
export function rubyRuntimeRef(version, engine = "ruby") {
|
|
595
|
+
return createRubyRuntimeComponent(version, { engine })["bom-ref"];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* The properties a Ruby file component carries once its build is known. They
|
|
600
|
+
* belong on unpackaged `file` components only: a package-owned Ruby is
|
|
601
|
+
* identified by its package.
|
|
602
|
+
*
|
|
603
|
+
* @param {{version: string, source: string, patchlevel?: string, abi?: string, arch?: string}} build
|
|
604
|
+
* result of readRubyBuildVersion
|
|
605
|
+
* @returns {{name: string, value: string}[]} properties to append
|
|
606
|
+
*/
|
|
607
|
+
export function rubyBuildProperties(build) {
|
|
608
|
+
const properties = [
|
|
609
|
+
{ name: "cdx:ruby:buildVersion", value: build.version },
|
|
610
|
+
{ name: "cdx:ruby:buildVersionSource", value: build.source },
|
|
611
|
+
];
|
|
612
|
+
if (build.patchlevel !== undefined) {
|
|
613
|
+
properties.push({ name: "cdx:ruby:patchlevel", value: build.patchlevel });
|
|
614
|
+
}
|
|
615
|
+
if (build.abi) {
|
|
616
|
+
properties.push({ name: "cdx:ruby:abiVersion", value: build.abi });
|
|
617
|
+
}
|
|
618
|
+
if (build.arch) {
|
|
619
|
+
properties.push({ name: "cdx:ruby:platform", value: build.arch });
|
|
620
|
+
}
|
|
621
|
+
return properties;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Start collecting the runtime components and edges of one scan.
|
|
626
|
+
*
|
|
627
|
+
* @returns {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}}
|
|
628
|
+
*/
|
|
629
|
+
export function createRubyRuntimeInventory() {
|
|
630
|
+
return { runtimeComponents: [], dependencies: [], runtimeRefs: new Set() };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Tag a Ruby file component with its build facts and link it to the runtime
|
|
635
|
+
* component of its version, creating that component on first use. Mutates the
|
|
636
|
+
* component in place.
|
|
637
|
+
*
|
|
638
|
+
* @param {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}} inventory
|
|
639
|
+
* accumulator from createRubyRuntimeInventory
|
|
640
|
+
* @param {Object} component file component, with a `properties` array
|
|
641
|
+
* @param {{version: string, source: string, patchlevel?: string, abi?: string, arch?: string}} build
|
|
642
|
+
* result of readRubyBuildVersion
|
|
643
|
+
*/
|
|
644
|
+
export function attachRubyBuild(inventory, component, build) {
|
|
645
|
+
component.properties.push(...rubyBuildProperties(build));
|
|
646
|
+
// A PATCHLEVEL of -1 is a pre-release whose tag, when no description named
|
|
647
|
+
// it, is unknown: the bare version would claim the release's CPE.
|
|
648
|
+
const untaggedPrerelease =
|
|
649
|
+
build.patchlevel === "-1" && !build.version.includes("-");
|
|
650
|
+
const runtime = createRubyRuntimeComponent(build.version, {
|
|
651
|
+
withCpe: !untaggedPrerelease,
|
|
652
|
+
});
|
|
653
|
+
const runtimeRef = runtime["bom-ref"];
|
|
654
|
+
if (!runtimeRef) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (!inventory.runtimeRefs.has(runtimeRef)) {
|
|
658
|
+
inventory.runtimeRefs.add(runtimeRef);
|
|
659
|
+
if (build.patchlevel !== undefined && build.patchlevel !== "-1") {
|
|
660
|
+
runtime.properties = [
|
|
661
|
+
{ name: "cdx:ruby:patchlevel", value: build.patchlevel },
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
inventory.runtimeComponents.push(runtime);
|
|
665
|
+
inventory.dependencies.push({ ref: runtimeRef, dependsOn: [] });
|
|
666
|
+
}
|
|
667
|
+
if (component["bom-ref"]) {
|
|
668
|
+
inventory.dependencies.push({
|
|
669
|
+
ref: component["bom-ref"],
|
|
670
|
+
dependsOn: [runtimeRef],
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// The OS packages that hold the Ruby interpreter itself. The library package
|
|
676
|
+
// is preferred, since libruby holds the interpreter core (Onigmo included);
|
|
677
|
+
// the interpreter package stands in where a distro ships no library package.
|
|
678
|
+
const LIBRARY_PACKAGES = [
|
|
679
|
+
/^libruby\d+\.\d+(t64)?$/, // Debian, Ubuntu
|
|
680
|
+
/^ruby-libs$/, // RHEL, Fedora, Alpine
|
|
681
|
+
/^libruby\d+_\d+-\d+_\d+$/, // openSUSE
|
|
682
|
+
];
|
|
683
|
+
const INTERPRETER_PACKAGES = [
|
|
684
|
+
/^ruby\d+\.\d+$/, // Debian, Ubuntu, openSUSE
|
|
685
|
+
/^ruby-\d+\.\d+$/, // Wolfi, Chainguard
|
|
686
|
+
/^ruby$/, // RHEL, Fedora, Alpine, Arch, Homebrew
|
|
687
|
+
/^ruby@\d+(\.\d+)?$/, // Homebrew versioned formulae
|
|
688
|
+
];
|
|
689
|
+
const OS_PURL_TYPES = new Set([
|
|
690
|
+
"deb",
|
|
691
|
+
"rpm",
|
|
692
|
+
"apk",
|
|
693
|
+
"alpm",
|
|
694
|
+
"brew",
|
|
695
|
+
"chocolatey",
|
|
696
|
+
"swid",
|
|
697
|
+
]);
|
|
698
|
+
// The RubyInstaller registers itself with Windows Installer as
|
|
699
|
+
// `Ruby 3.4.10-1-x64`, which an OBOM names pkg:swid/Ruby%2B3.4.10-1-x64.
|
|
700
|
+
// The DevKit edition registers as `Ruby 3.3.6-1-x64 with MSYS2`.
|
|
701
|
+
const RUBYINSTALLER_PROGRAM =
|
|
702
|
+
/^Ruby[ +](\d+\.\d+\.\d+)-\d+-(x64|x86|arm64)([ +]with[ +]MSYS2)?$/;
|
|
703
|
+
// Package managers that ship the upstream release as built, without
|
|
704
|
+
// backporting fixes under an old version, so the release's CPE applies.
|
|
705
|
+
const UPSTREAM_BUILD_PURL_TYPES = new Set(["brew", "chocolatey", "swid"]);
|
|
706
|
+
|
|
707
|
+
function osPurlType(component) {
|
|
708
|
+
const purl = component?.purl;
|
|
709
|
+
if (typeof purl !== "string" || !purl.startsWith("pkg:")) {
|
|
710
|
+
return undefined;
|
|
711
|
+
}
|
|
712
|
+
const type = purl.slice(4, purl.indexOf("/"));
|
|
713
|
+
return OS_PURL_TYPES.has(type) ? type : undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* The upstream Ruby version an OS package carries: 1:3.1.2-7+deb12u1 -> 3.1.2,
|
|
718
|
+
* 3.3.10-r0 -> 3.3.10, 3.0.7-168.el9_8 -> 3.0.7, 3.4.8_1 -> 3.4.8, and the
|
|
719
|
+
* Chocolatey 3.4.10.1 -> 3.4.10.
|
|
720
|
+
*
|
|
721
|
+
* @param {string} version package version
|
|
722
|
+
* @param {string} [purlType] purl type of the package
|
|
723
|
+
* @returns {string|undefined}
|
|
724
|
+
*/
|
|
725
|
+
export function upstreamRubyVersion(version, purlType) {
|
|
726
|
+
if (typeof version !== "string" || !version) {
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
let text = version;
|
|
730
|
+
// Chocolatey appends a package fix number: 3.4.10.1 packages Ruby 3.4.10.
|
|
731
|
+
if (purlType === "chocolatey") {
|
|
732
|
+
const match = /^(\d+\.\d+\.\d+)(\.\d+)?$/.exec(text);
|
|
733
|
+
return match ? match[1] : undefined;
|
|
734
|
+
}
|
|
735
|
+
const colon = text.indexOf(":");
|
|
736
|
+
if (colon !== -1 && /^\d+$/.test(text.slice(0, colon))) {
|
|
737
|
+
text = text.slice(colon + 1);
|
|
738
|
+
}
|
|
739
|
+
// Debian sorts a pre-release before its release with a tilde:
|
|
740
|
+
// 2.7.0~preview1-1 packages Ruby 2.7.0-preview1.
|
|
741
|
+
const prerelease = /^(\d+\.\d+\.\d+)~(preview|rc)(\d+)(?=$|[-_+])/.exec(text);
|
|
742
|
+
if (prerelease) {
|
|
743
|
+
return `${prerelease[1]}-${prerelease[2]}${prerelease[3]}`;
|
|
744
|
+
}
|
|
745
|
+
const match = /^(\d+\.\d+\.\d+)(?=$|[-_~+])/.exec(text);
|
|
746
|
+
return match ? match[1] : undefined;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Describe the Ruby an OS package installed with its upstream identity,
|
|
751
|
+
* `pkg:generic/ruby@<upstream>`, so a BOM names the interpreter both ways. The
|
|
752
|
+
* component of a distro package carries no CPE, since distros backport fixes
|
|
753
|
+
* under their old upstream version; Homebrew and Chocolatey ship the upstream
|
|
754
|
+
* release, so theirs does. `cdx:os:installedBy` names the package to judge it
|
|
755
|
+
* by.
|
|
756
|
+
*
|
|
757
|
+
* @param {Object[]} components BOM components, OS packages among them
|
|
758
|
+
* @returns {Object[]} runtime components to add, one per distinct version
|
|
759
|
+
*/
|
|
760
|
+
export function deriveOsPackagedRubyRuntimes(components) {
|
|
761
|
+
if (!Array.isArray(components)) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
const osPackages = components.filter((component) => osPurlType(component));
|
|
765
|
+
const pick = (patterns) =>
|
|
766
|
+
osPackages.filter(
|
|
767
|
+
(component) =>
|
|
768
|
+
osPurlType(component) !== "swid" &&
|
|
769
|
+
patterns.some((pattern) => pattern.test(`${component.name || ""}`)),
|
|
770
|
+
);
|
|
771
|
+
let owners = pick(LIBRARY_PACKAGES);
|
|
772
|
+
if (!owners.length) {
|
|
773
|
+
owners = pick(INTERPRETER_PACKAGES);
|
|
774
|
+
}
|
|
775
|
+
owners = owners.concat(
|
|
776
|
+
osPackages.filter(
|
|
777
|
+
(component) =>
|
|
778
|
+
osPurlType(component) === "swid" &&
|
|
779
|
+
RUBYINSTALLER_PROGRAM.test(`${component.name || ""}`),
|
|
780
|
+
),
|
|
781
|
+
);
|
|
782
|
+
const existing = new Set(
|
|
783
|
+
components.map((component) => component?.["bom-ref"]),
|
|
784
|
+
);
|
|
785
|
+
const byRef = new Map();
|
|
786
|
+
for (const owner of owners) {
|
|
787
|
+
const type = osPurlType(owner);
|
|
788
|
+
const version =
|
|
789
|
+
type === "swid"
|
|
790
|
+
? RUBYINSTALLER_PROGRAM.exec(owner.name)[1]
|
|
791
|
+
: upstreamRubyVersion(owner.version, type);
|
|
792
|
+
if (!version) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const runtime = createRubyRuntimeComponent(version, {
|
|
796
|
+
withCpe: UPSTREAM_BUILD_PURL_TYPES.has(type),
|
|
797
|
+
});
|
|
798
|
+
const ref = runtime["bom-ref"];
|
|
799
|
+
if (!ref || existing.has(ref)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const ownerRef = decodeURIComponent(owner.purl);
|
|
803
|
+
const known = byRef.get(ref);
|
|
804
|
+
if (known) {
|
|
805
|
+
if (
|
|
806
|
+
!known.properties.some(
|
|
807
|
+
(prop) =>
|
|
808
|
+
prop.name === "cdx:os:installedBy" && prop.value === ownerRef,
|
|
809
|
+
)
|
|
810
|
+
) {
|
|
811
|
+
known.properties.push({ name: "cdx:os:installedBy", value: ownerRef });
|
|
812
|
+
}
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
runtime.properties = [
|
|
816
|
+
{ name: "cdx:os:installedBy", value: ownerRef },
|
|
817
|
+
{ name: "cdx:ruby:buildVersionSource", value: "os-package" },
|
|
818
|
+
];
|
|
819
|
+
byRef.set(ref, runtime);
|
|
820
|
+
}
|
|
821
|
+
return [...byRef.values()];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Where the default gems of an install live: the gemspec paths
|
|
826
|
+
* <prefix>/lib/ruby/gems/<abi>/specifications/default/*.gemspec.
|
|
827
|
+
*
|
|
828
|
+
* @param {string} gemspecPath path of a gemspec, either separator
|
|
829
|
+
* @returns {{prefix: string, abi: string}|undefined} the install prefix and
|
|
830
|
+
* ABI, when the gemspec is a default gem of a lib/ruby/gems tree
|
|
831
|
+
*/
|
|
832
|
+
export function defaultGemInstall(gemspecPath) {
|
|
833
|
+
if (typeof gemspecPath !== "string") {
|
|
834
|
+
return undefined;
|
|
835
|
+
}
|
|
836
|
+
const parts = gemspecPath.replaceAll("\\", "/").split("/");
|
|
837
|
+
const n = parts.length;
|
|
838
|
+
if (
|
|
839
|
+
n < 7 ||
|
|
840
|
+
parts[n - 2] !== "default" ||
|
|
841
|
+
parts[n - 3] !== "specifications" ||
|
|
842
|
+
!ABI_RE.test(parts[n - 4]) ||
|
|
843
|
+
parts[n - 5] !== "gems" ||
|
|
844
|
+
parts[n - 6] !== "ruby" ||
|
|
845
|
+
parts[n - 7] !== "lib"
|
|
846
|
+
) {
|
|
847
|
+
return undefined;
|
|
848
|
+
}
|
|
849
|
+
return { prefix: parts.slice(0, n - 7).join("/") || "/", abi: parts[n - 4] };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Whether a path lies inside an installed gem's own directory,
|
|
854
|
+
* <gem home>/gems/<name>-<version>/..., beside the specifications directory of
|
|
855
|
+
* that gem home. The lockfiles, manifests and nested gemspecs there belong to
|
|
856
|
+
* the gem's own development, not to anything installed.
|
|
857
|
+
*
|
|
858
|
+
* Only the part of the path from the scanned directory down counts, so a scan
|
|
859
|
+
* of one installed gem (`cdxgen /usr/local/bundle/gems/rails-7.1.0`) still
|
|
860
|
+
* reads that gem's files. The scanned directory may itself be the `gems`
|
|
861
|
+
* directory, as when an image scan reads each gem home on its own.
|
|
862
|
+
*
|
|
863
|
+
* @param {string} filePath absolute path, either separator
|
|
864
|
+
* @param {function(string): boolean} exists existence check
|
|
865
|
+
* @param {string} [scanRoot] the directory the scan started from
|
|
866
|
+
* @returns {boolean}
|
|
867
|
+
*/
|
|
868
|
+
export function isInsideInstalledGem(filePath, exists, scanRoot) {
|
|
869
|
+
if (typeof filePath !== "string" || !filePath) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
const parts = filePath.replaceAll("\\", "/").split("/");
|
|
873
|
+
let lowest = 1;
|
|
874
|
+
if (typeof scanRoot === "string" && scanRoot) {
|
|
875
|
+
const rootParts = scanRoot
|
|
876
|
+
.replaceAll("\\", "/")
|
|
877
|
+
.replace(/\/+$/, "")
|
|
878
|
+
.split("/");
|
|
879
|
+
const isUnderRoot = rootParts.every((part, index) => parts[index] === part);
|
|
880
|
+
if (isUnderRoot) {
|
|
881
|
+
lowest = Math.max(1, rootParts.length - 1);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// The file itself cannot be the gem directory, so stop two short.
|
|
885
|
+
for (let i = parts.length - 3; i >= lowest; i--) {
|
|
886
|
+
if (parts[i] !== "gems" || !/-\d/.test(parts[i + 1] || "")) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const gemHome = parts.slice(0, i).join("/") || "/";
|
|
890
|
+
if (exists(join(gemHome, "specifications"))) {
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Link the default gems of an unpackaged Ruby to the runtime they ship with.
|
|
899
|
+
* The runtime is known from its `file` components, which carry the build
|
|
900
|
+
* version and the path they were found at; a default gem belongs to the
|
|
901
|
+
* runtime installed under the same prefix. Gems an OS package installed are
|
|
902
|
+
* left to that package, as is any gem whose install has no unpackaged runtime.
|
|
903
|
+
*
|
|
904
|
+
* @param {Object[]} components BOM components
|
|
905
|
+
* @param {string} [rootDir] extracted root the source paths may start with
|
|
906
|
+
* @returns {{ref: string, dependsOn: string[]}[]} edges from each runtime to
|
|
907
|
+
* its default gems
|
|
908
|
+
*/
|
|
909
|
+
export function linkDefaultGemsToRuntimes(components, rootDir) {
|
|
910
|
+
if (!Array.isArray(components)) {
|
|
911
|
+
return [];
|
|
912
|
+
}
|
|
913
|
+
const root = rootDir
|
|
914
|
+
? `${rootDir}`.replaceAll("\\", "/").replace(/\/+$/, "")
|
|
915
|
+
: undefined;
|
|
916
|
+
// Paths read from an extracted image start with the extraction directory.
|
|
917
|
+
const inRoot = (value) => {
|
|
918
|
+
if (typeof value !== "string") {
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
const normalized = value.replaceAll("\\", "/");
|
|
922
|
+
return root && normalized.startsWith(`${root}/`)
|
|
923
|
+
? normalized.slice(root.length)
|
|
924
|
+
: normalized;
|
|
925
|
+
};
|
|
926
|
+
const property = (component, name) => {
|
|
927
|
+
const value = component?.properties?.find(
|
|
928
|
+
(prop) => prop.name === name,
|
|
929
|
+
)?.value;
|
|
930
|
+
return name === "internal:SrcFile" ? inRoot(value) : value;
|
|
931
|
+
};
|
|
932
|
+
const runtimeByPrefix = new Map();
|
|
933
|
+
const refs = new Set(components.map((component) => component?.["bom-ref"]));
|
|
934
|
+
for (const component of components) {
|
|
935
|
+
const version = property(component, "cdx:ruby:buildVersion");
|
|
936
|
+
const srcFile = property(component, "internal:SrcFile");
|
|
937
|
+
if (component?.type !== "file" || !version || !srcFile) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const prefix = rubyInstallPrefix(srcFile.replaceAll("\\", "/"));
|
|
941
|
+
const runtimeRef = rubyRuntimeRef(version);
|
|
942
|
+
if (prefix && runtimeRef && refs.has(runtimeRef)) {
|
|
943
|
+
runtimeByPrefix.set(prefix, runtimeRef);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!runtimeByPrefix.size) {
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
949
|
+
const dependsOn = new Map();
|
|
950
|
+
for (const component of components) {
|
|
951
|
+
if (
|
|
952
|
+
!component?.["bom-ref"] ||
|
|
953
|
+
property(component, "cdx:gem:defaultGem") !== "true" ||
|
|
954
|
+
property(component, "cdx:os:installedBy")
|
|
955
|
+
) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const install = defaultGemInstall(property(component, "internal:SrcFile"));
|
|
959
|
+
const runtimeRef = install && runtimeByPrefix.get(install.prefix);
|
|
960
|
+
if (!runtimeRef) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (!dependsOn.has(runtimeRef)) {
|
|
964
|
+
dependsOn.set(runtimeRef, new Set());
|
|
965
|
+
}
|
|
966
|
+
dependsOn.get(runtimeRef).add(component["bom-ref"]);
|
|
967
|
+
}
|
|
968
|
+
return [...dependsOn.entries()].map(([ref, gems]) => ({
|
|
969
|
+
ref,
|
|
970
|
+
dependsOn: [...gems].sort(),
|
|
971
|
+
}));
|
|
972
|
+
}
|