@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
package/lib/inventory/source.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { URL } from "node:url";
|
|
4
|
+
import { fileURLToPath, URL } from "node:url";
|
|
5
5
|
|
|
6
6
|
import { Purl } from "@cdxgen/cdx-purl";
|
|
7
7
|
import { coerce, diff, prerelease } from "semver";
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
prefetchedResponse,
|
|
31
31
|
prefetchJson,
|
|
32
32
|
} from "./fetchBatch.js";
|
|
33
|
+
import { mavenCentralFetchUrl } from "./mavenCentralMirror.js";
|
|
33
34
|
import { canonicalPurlFromLooseString } from "./purl.js";
|
|
34
35
|
|
|
35
36
|
/** Raised when a caller omits a registry helper that the purl type requires. */
|
|
@@ -111,6 +112,9 @@ function inferGitOperation(args = []) {
|
|
|
111
112
|
* @param {string[]} args git arguments
|
|
112
113
|
* @param {Object} options command options
|
|
113
114
|
* @param {string|undefined} options.cwd working directory
|
|
115
|
+
* @param {boolean|undefined} options.noRepository run outside any repository,
|
|
116
|
+
* so the config of a repository in the working directory, such as url.*.insteadOf
|
|
117
|
+
* or core.sshCommand, cannot change where a remote command connects
|
|
114
118
|
* @returns {Object} spawn result
|
|
115
119
|
*/
|
|
116
120
|
export function hardenedGitCommand(args, options = {}) {
|
|
@@ -122,8 +126,14 @@ export function hardenedGitCommand(args, options = {}) {
|
|
|
122
126
|
GIT_CONFIG_VALUE_0: "false",
|
|
123
127
|
GIT_CONFIG_KEY_1: "safe.bareRepository",
|
|
124
128
|
GIT_CONFIG_VALUE_1: "explicit",
|
|
129
|
+
GIT_NO_LAZY_FETCH: "1",
|
|
125
130
|
GIT_TERMINAL_PROMPT: "0",
|
|
126
131
|
};
|
|
132
|
+
if (options.noRepository) {
|
|
133
|
+
// ls-remote works without a repository. Pointing GIT_DIR at a path that
|
|
134
|
+
// does not exist stops git from discovering one in the working directory.
|
|
135
|
+
envConfigs.GIT_DIR = path.join(getTmpDir(), "cdxgen-no-repository.invalid");
|
|
136
|
+
}
|
|
127
137
|
const env = isSecureMode
|
|
128
138
|
? {
|
|
129
139
|
...process.env,
|
|
@@ -275,10 +285,14 @@ export function buildReleaseNotesFromGit(repoPath, options = {}) {
|
|
|
275
285
|
typeof remoteUrl === "string"
|
|
276
286
|
? validateAndRejectGitSource(remoteUrl)
|
|
277
287
|
: null;
|
|
288
|
+
// remote.origin.url comes from the scanned repository. Only a remote that
|
|
289
|
+
// validateAndRejectGitSource recognises and checks may reach ls-remote; git
|
|
290
|
+
// also treats forms such as host:path as remotes, which skip that check.
|
|
278
291
|
const canDiscoverRemoteTags =
|
|
279
292
|
(!currentTag || !previousTag) &&
|
|
280
293
|
typeof remoteUrl === "string" &&
|
|
281
294
|
!remoteUrl.startsWith("-") &&
|
|
295
|
+
maybeRemotePath(remoteUrl) &&
|
|
282
296
|
!remoteUrlValidationError &&
|
|
283
297
|
/github\.com[:/]/i.test(remoteUrl);
|
|
284
298
|
if (canDiscoverRemoteTags) {
|
|
@@ -298,7 +312,7 @@ export function buildReleaseNotesFromGit(repoPath, options = {}) {
|
|
|
298
312
|
"--",
|
|
299
313
|
remoteUrl,
|
|
300
314
|
],
|
|
301
|
-
{},
|
|
315
|
+
{ noRepository: true },
|
|
302
316
|
);
|
|
303
317
|
const remoteTags = parseLsRemoteTags(remoteTagsResult.stdout);
|
|
304
318
|
if (!currentTag && remoteTags.length) {
|
|
@@ -508,6 +522,59 @@ export function isAllowedPath(p) {
|
|
|
508
522
|
});
|
|
509
523
|
}
|
|
510
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Resolve symlinks in a path. When the path does not exist yet, the nearest
|
|
527
|
+
* existing parent is resolved and the remaining segments are appended, so
|
|
528
|
+
* /tmp/new on macOS still becomes /private/tmp/new.
|
|
529
|
+
*
|
|
530
|
+
* @param {string} p Path string
|
|
531
|
+
* @returns {string} real path
|
|
532
|
+
*/
|
|
533
|
+
function realPathOrResolved(p) {
|
|
534
|
+
let current = path.resolve(p);
|
|
535
|
+
const missing = [];
|
|
536
|
+
while (true) {
|
|
537
|
+
try {
|
|
538
|
+
return path.join(fs.realpathSync(current), ...missing);
|
|
539
|
+
} catch (_err) {
|
|
540
|
+
const parent = path.dirname(current);
|
|
541
|
+
if (parent === current) {
|
|
542
|
+
return path.resolve(p);
|
|
543
|
+
}
|
|
544
|
+
missing.unshift(path.basename(current));
|
|
545
|
+
current = parent;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Checks where the given path really points, after following symlinks, against
|
|
552
|
+
* where the allowed paths really point. Both sides are resolved, so an allowed
|
|
553
|
+
* directory that is itself reached through a link (such as /tmp on macOS) still
|
|
554
|
+
* matches, while a link inside an allowed directory that leads outside it does
|
|
555
|
+
* not.
|
|
556
|
+
*
|
|
557
|
+
* @param {string} p Path string to check
|
|
558
|
+
* @returns {boolean} true if the real path is inside a real allowed path
|
|
559
|
+
*/
|
|
560
|
+
export function isAllowedRealPath(p) {
|
|
561
|
+
if (typeof p !== "string" || hasDangerousUnicode(p)) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const allowedPaths = getAllowedPaths();
|
|
565
|
+
if (!allowedPaths.length) {
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
const realP = realPathOrResolved(p);
|
|
569
|
+
return allowedPaths.some((ap) => {
|
|
570
|
+
const relativePath = path.relative(realPathOrResolved(ap), realP);
|
|
571
|
+
return (
|
|
572
|
+
relativePath === "" ||
|
|
573
|
+
(!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
511
578
|
/**
|
|
512
579
|
* Determine if the path could be a package URL.
|
|
513
580
|
*
|
|
@@ -586,6 +653,49 @@ export function validateAndRejectGitSource(filePath) {
|
|
|
586
653
|
};
|
|
587
654
|
}
|
|
588
655
|
|
|
656
|
+
if (hasAmbiguousRemoteCharacters(filePath)) {
|
|
657
|
+
return {
|
|
658
|
+
status: 400,
|
|
659
|
+
error: "Invalid URL Format",
|
|
660
|
+
details:
|
|
661
|
+
"The provided Git URL contains backslashes, whitespace or control characters.",
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// git reads file:// URLs from the local disk, so they get the path
|
|
666
|
+
// allowlist rather than the host allowlist.
|
|
667
|
+
if (gitUrlObj.protocol === "file:") {
|
|
668
|
+
let localPath;
|
|
669
|
+
try {
|
|
670
|
+
localPath = fileURLToPath(gitUrlObj);
|
|
671
|
+
} catch (_err) {
|
|
672
|
+
localPath = undefined;
|
|
673
|
+
}
|
|
674
|
+
if (
|
|
675
|
+
gitUrlObj.hostname ||
|
|
676
|
+
!localPath ||
|
|
677
|
+
!isAllowedPath(localPath) ||
|
|
678
|
+
(isWin && !isAllowedWinPath(localPath))
|
|
679
|
+
) {
|
|
680
|
+
return {
|
|
681
|
+
status: 403,
|
|
682
|
+
error: "Path Not Allowed",
|
|
683
|
+
details: "Path is not allowed as per the allowlist.",
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// git, libcurl and OpenSSH must connect to the host that was checked.
|
|
690
|
+
const gitRemoteHost = getGitRemoteHost(filePath);
|
|
691
|
+
if (!gitRemoteHost || gitUrlObj.hostname.toLowerCase() !== gitRemoteHost) {
|
|
692
|
+
return {
|
|
693
|
+
status: 400,
|
|
694
|
+
error: "Invalid URL Format",
|
|
695
|
+
details: "The host of the provided Git URL is ambiguous.",
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
589
699
|
if (!isAllowedHost(gitUrlObj.hostname)) {
|
|
590
700
|
return {
|
|
591
701
|
status: 403,
|
|
@@ -598,6 +708,136 @@ export function validateAndRejectGitSource(filePath) {
|
|
|
598
708
|
return null;
|
|
599
709
|
}
|
|
600
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Detect characters that git, libcurl or OpenSSH read differently from the
|
|
713
|
+
* WHATWG URL parser. A backslash ends the host for new URL() but is part of
|
|
714
|
+
* the user name for git, and new URL() silently drops tabs and newlines.
|
|
715
|
+
*
|
|
716
|
+
* @param {string} remote Git URL or SCP-like address
|
|
717
|
+
* @returns {boolean} true if the remote contains such characters
|
|
718
|
+
*/
|
|
719
|
+
function hasAmbiguousRemoteCharacters(remote) {
|
|
720
|
+
if (hasDangerousUnicode(remote)) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
for (const ch of remote) {
|
|
724
|
+
const code = ch.codePointAt(0);
|
|
725
|
+
if (ch === "\\" || code <= 0x20 || (code >= 0x7f && code <= 0x9f)) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Return the host a git client connects to for a remote, read the way git,
|
|
734
|
+
* libcurl and OpenSSH read it rather than the way new URL() does. git ends the
|
|
735
|
+
* authority of ssh:// and git:// URLs only at the first slash, percent-decodes
|
|
736
|
+
* it before splitting off the host, and reads a leading "[...]" as the host.
|
|
737
|
+
* OpenSSH takes the host after the last "@". So a "?" or "#" before the first
|
|
738
|
+
* slash, a second "@", brackets in the user name, or a percent sign in an
|
|
739
|
+
* ssh:// or git:// authority make the host ambiguous.
|
|
740
|
+
*
|
|
741
|
+
* @param {string} remote Git URL or SCP-like address
|
|
742
|
+
* @returns {string|undefined} lower-cased host, or undefined when it is ambiguous
|
|
743
|
+
*/
|
|
744
|
+
export function getGitRemoteHost(remote) {
|
|
745
|
+
if (typeof remote !== "string") {
|
|
746
|
+
return undefined;
|
|
747
|
+
}
|
|
748
|
+
let authority;
|
|
749
|
+
const schemeEnd = remote.indexOf("://");
|
|
750
|
+
if (schemeEnd > -1) {
|
|
751
|
+
const scheme = remote.slice(0, schemeEnd).toLowerCase();
|
|
752
|
+
const rest = remote.slice(schemeEnd + 3);
|
|
753
|
+
const slash = rest.indexOf("/");
|
|
754
|
+
authority = slash > -1 ? rest.slice(0, slash) : rest;
|
|
755
|
+
// libcurl reads the http(s) authority as written. git decodes the others.
|
|
756
|
+
if (!["http", "https"].includes(scheme) && authority.includes("%")) {
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
// git reads user@host:path as SCP-like only when the colon comes before
|
|
761
|
+
// any slash. Otherwise it is a local path.
|
|
762
|
+
const colon = remote.indexOf(":");
|
|
763
|
+
const slash = remote.indexOf("/");
|
|
764
|
+
if (colon < 0 || (slash > -1 && slash < colon)) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
authority = remote.slice(0, colon);
|
|
768
|
+
}
|
|
769
|
+
if (authority.includes("?") || authority.includes("#")) {
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
const at = authority.indexOf("@");
|
|
773
|
+
if (at !== authority.lastIndexOf("@")) {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
const userInfo = at > -1 ? authority.slice(0, at) : "";
|
|
777
|
+
if (userInfo.includes("[") || userInfo.includes("]")) {
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
let host = authority.slice(at + 1);
|
|
781
|
+
if (host.startsWith("[")) {
|
|
782
|
+
const close = host.indexOf("]");
|
|
783
|
+
if (close < 0) {
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
host = host.slice(0, close + 1);
|
|
787
|
+
} else {
|
|
788
|
+
const portSep = host.indexOf(":");
|
|
789
|
+
if (portSep > -1) {
|
|
790
|
+
host = host.slice(0, portSep);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!host || host.startsWith("-")) {
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
return host.toLowerCase();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Remove credentials from git output before it is logged. Every URL in the
|
|
801
|
+
* text loses its user name and password, which git copies into its error
|
|
802
|
+
* messages from the remote it was given.
|
|
803
|
+
*
|
|
804
|
+
* @param {string|undefined} text Output of a git command
|
|
805
|
+
* @param {string|undefined} repoUrl Remote the command was given
|
|
806
|
+
* @returns {string|undefined} Output with credentials replaced by ***
|
|
807
|
+
*/
|
|
808
|
+
export function redactGitOutput(text, repoUrl) {
|
|
809
|
+
if (!text || typeof text !== "string") {
|
|
810
|
+
return text;
|
|
811
|
+
}
|
|
812
|
+
let redacted = text;
|
|
813
|
+
if (repoUrl && typeof repoUrl === "string") {
|
|
814
|
+
redacted = redacted.split(repoUrl).join(sanitizeRemoteUrlForLogs(repoUrl));
|
|
815
|
+
}
|
|
816
|
+
let from = 0;
|
|
817
|
+
while (from < redacted.length) {
|
|
818
|
+
const schemeEnd = redacted.indexOf("://", from);
|
|
819
|
+
if (schemeEnd === -1) {
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
const authorityStart = schemeEnd + 3;
|
|
823
|
+
let authorityEnd = authorityStart;
|
|
824
|
+
while (
|
|
825
|
+
authorityEnd < redacted.length &&
|
|
826
|
+
!"/?#'\"<> \t\r\n".includes(redacted[authorityEnd])
|
|
827
|
+
) {
|
|
828
|
+
authorityEnd++;
|
|
829
|
+
}
|
|
830
|
+
const at = redacted.lastIndexOf("@", authorityEnd - 1);
|
|
831
|
+
if (at >= authorityStart) {
|
|
832
|
+
redacted = `${redacted.slice(0, authorityStart)}***:***${redacted.slice(at)}`;
|
|
833
|
+
from = authorityStart + "***:***".length;
|
|
834
|
+
} else {
|
|
835
|
+
from = authorityEnd;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return redacted;
|
|
839
|
+
}
|
|
840
|
+
|
|
601
841
|
/**
|
|
602
842
|
* Clone a git repository into a temporary directory.
|
|
603
843
|
*
|
|
@@ -606,6 +846,11 @@ export function validateAndRejectGitSource(filePath) {
|
|
|
606
846
|
* @returns {string} cloned directory path
|
|
607
847
|
*/
|
|
608
848
|
export function gitClone(repoUrl, branch = null) {
|
|
849
|
+
// Callers validate first, but git must never see a remote that fails it.
|
|
850
|
+
const validationError = validateAndRejectGitSource(repoUrl);
|
|
851
|
+
if (validationError) {
|
|
852
|
+
throw new Error(`${validationError.error}: ${validationError.details}`);
|
|
853
|
+
}
|
|
609
854
|
let baseDirName = path.basename(repoUrl);
|
|
610
855
|
if (!/^[a-zA-Z0-9_-]+$/.test(baseDirName)) {
|
|
611
856
|
baseDirName = "repo-";
|
|
@@ -635,6 +880,8 @@ export function gitClone(repoUrl, branch = null) {
|
|
|
635
880
|
"safe.bareRepository=explicit",
|
|
636
881
|
"-c",
|
|
637
882
|
"core.hooksPath=/dev/null",
|
|
883
|
+
"-c",
|
|
884
|
+
"core.symlinks=false",
|
|
638
885
|
"clone",
|
|
639
886
|
"--template=",
|
|
640
887
|
repoUrl,
|
|
@@ -655,8 +902,11 @@ export function gitClone(repoUrl, branch = null) {
|
|
|
655
902
|
`Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
|
|
656
903
|
);
|
|
657
904
|
const result = hardenedGitCommand(gitArgs);
|
|
658
|
-
if (result.status !== 0) {
|
|
659
|
-
console.error(result.stderr);
|
|
905
|
+
if (result.status !== 0 || result.error) {
|
|
906
|
+
console.error(redactGitOutput(result.stderr, repoUrl));
|
|
907
|
+
// The directory was created by this call, so it is always safe to remove.
|
|
908
|
+
safeRmSync(tempDir, { recursive: true, force: true });
|
|
909
|
+
throw new Error("Git clone failed.");
|
|
660
910
|
}
|
|
661
911
|
|
|
662
912
|
return tempDir;
|
|
@@ -735,7 +985,7 @@ export function findGitRefForPurlVersion(repoUrl, purlResolution) {
|
|
|
735
985
|
"--",
|
|
736
986
|
repoUrl,
|
|
737
987
|
],
|
|
738
|
-
{},
|
|
988
|
+
{ noRepository: true },
|
|
739
989
|
);
|
|
740
990
|
if (result.status !== 0 || !result.stdout) {
|
|
741
991
|
return undefined;
|
|
@@ -1384,6 +1634,7 @@ export async function resolveGitUrlFromPurl(purlString, helpers = {}) {
|
|
|
1384
1634
|
break;
|
|
1385
1635
|
}
|
|
1386
1636
|
const mavenCentralUrl =
|
|
1637
|
+
mavenCentralFetchUrl() ||
|
|
1387
1638
|
readEnvironmentVariable("MAVEN_CENTRAL_URL") ||
|
|
1388
1639
|
"https://repo1.maven.org/maven2/";
|
|
1389
1640
|
registry = mavenCentralUrl.endsWith("/")
|
package/lib/inventory/spdx.js
CHANGED
|
@@ -440,6 +440,150 @@ export function licenseIdFromText(content) {
|
|
|
440
440
|
return undefined;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// The GNU licenses by the title line their text opens with, and their SPDX
|
|
444
|
+
// ids by version.
|
|
445
|
+
const GNU_LICENSE_TITLES = {
|
|
446
|
+
"GNU AFFERO GENERAL PUBLIC LICENSE": { 3: "AGPL-3.0-only" },
|
|
447
|
+
"GNU LESSER GENERAL PUBLIC LICENSE": {
|
|
448
|
+
3: "LGPL-3.0-only",
|
|
449
|
+
2.1: "LGPL-2.1-only",
|
|
450
|
+
2: "LGPL-2.0-only",
|
|
451
|
+
},
|
|
452
|
+
"GNU LIBRARY GENERAL PUBLIC LICENSE": {
|
|
453
|
+
2.1: "LGPL-2.1-only",
|
|
454
|
+
2: "LGPL-2.0-only",
|
|
455
|
+
},
|
|
456
|
+
"GNU GENERAL PUBLIC LICENSE": {
|
|
457
|
+
3: "GPL-3.0-only",
|
|
458
|
+
2: "GPL-2.0-only",
|
|
459
|
+
1: "GPL-1.0-only",
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// Phrases that only the grant of one license carries, so finding one anywhere
|
|
464
|
+
// in a notice means that license is granted, not merely mentioned.
|
|
465
|
+
const LICENSE_GRANT_PHRASES = [
|
|
466
|
+
["PERMISSION IS HEREBY GRANTED, FREE OF CHARGE", "MIT"],
|
|
467
|
+
[
|
|
468
|
+
"PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED",
|
|
469
|
+
"ISC",
|
|
470
|
+
],
|
|
471
|
+
[
|
|
472
|
+
"PERMISSION IS GRANTED TO ANYONE TO USE THIS SOFTWARE FOR ANY PURPOSE",
|
|
473
|
+
"Zlib",
|
|
474
|
+
],
|
|
475
|
+
[
|
|
476
|
+
"THIS IS FREE AND UNENCUMBERED SOFTWARE RELEASED INTO THE PUBLIC DOMAIN",
|
|
477
|
+
"Unlicense",
|
|
478
|
+
],
|
|
479
|
+
["BOOST SOFTWARE LICENSE - VERSION 1.0", "BSL-1.0"],
|
|
480
|
+
["MOZILLA PUBLIC LICENSE VERSION 2.0", "MPL-2.0"],
|
|
481
|
+
["MOZILLA PUBLIC LICENSE, VERSION 2.0", "MPL-2.0"],
|
|
482
|
+
["MOZILLA PUBLIC LICENSE, V. 2.0", "MPL-2.0"],
|
|
483
|
+
[
|
|
484
|
+
"CREATIVE COMMONS ATTRIBUTION 4.0 INTERNATIONAL PUBLIC LICENSE",
|
|
485
|
+
"CC-BY-4.0",
|
|
486
|
+
],
|
|
487
|
+
[
|
|
488
|
+
"CREATIVE COMMONS ATTRIBUTION-SHAREALIKE 4.0 INTERNATIONAL PUBLIC LICENSE",
|
|
489
|
+
"CC-BY-SA-4.0",
|
|
490
|
+
],
|
|
491
|
+
["CC0 1.0 UNIVERSAL", "CC0-1.0"],
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Identify every license a notice grants, in the order the notice grants
|
|
496
|
+
* them. A notice can grant several: one LICENSE file holding an MIT grant and
|
|
497
|
+
* an Apache-2.0 grant (gopkg.in/yaml.v3), or a choice of licenses followed by
|
|
498
|
+
* each full text (github.com/spdx/tools-golang). A license text quotes others
|
|
499
|
+
* (the GNU GPL names the GNU Lesser GPL, the MPL names the GNU licenses), so
|
|
500
|
+
* only phrases that a grant carries count: a GNU license is found by its title
|
|
501
|
+
* standing on a line of its own, the others by a sentence of their grant.
|
|
502
|
+
* A notice none of these phrases match falls back to {@link guessLicenseId}.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} content License file contents
|
|
505
|
+
* @returns {string[]} SPDX license ids or expressions; empty when none is
|
|
506
|
+
* recognized
|
|
507
|
+
*/
|
|
508
|
+
export function guessLicenseIds(content) {
|
|
509
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
const text = content.replace(/\s+/g, " ").toUpperCase();
|
|
513
|
+
const found = [];
|
|
514
|
+
const add = (at, id) => {
|
|
515
|
+
if (at >= 0 && id) {
|
|
516
|
+
found.push({ at, id });
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
// Walk the lines alongside `text`, so a title line is placed in it.
|
|
520
|
+
let offset = 0;
|
|
521
|
+
for (const line of content.split("\n")) {
|
|
522
|
+
const normalized = line.replace(/\s+/g, " ").trim().toUpperCase();
|
|
523
|
+
const at = normalized ? text.indexOf(normalized, offset) : -1;
|
|
524
|
+
if (at < 0) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
offset = at + normalized.length;
|
|
528
|
+
const ids = GNU_LICENSE_TITLES[normalized];
|
|
529
|
+
if (ids) {
|
|
530
|
+
const version = text
|
|
531
|
+
.slice(offset, offset + 60)
|
|
532
|
+
.match(/VERSION (\d(?:\.\d)?)/)?.[1];
|
|
533
|
+
add(at, ids[version]);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
for (const marker of [
|
|
537
|
+
"APACHE LICENSE VERSION 2.0",
|
|
538
|
+
"APACHE LICENSE, VERSION 2.0",
|
|
539
|
+
"APACHE LICENSE 2.0",
|
|
540
|
+
]) {
|
|
541
|
+
add(
|
|
542
|
+
text.indexOf(marker),
|
|
543
|
+
text.includes("LLVM EXCEPTION")
|
|
544
|
+
? "Apache-2.0 WITH LLVM-exception"
|
|
545
|
+
: "Apache-2.0",
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
for (const [phrase, id] of LICENSE_GRANT_PHRASES) {
|
|
549
|
+
add(text.indexOf(phrase), id);
|
|
550
|
+
}
|
|
551
|
+
// A notice can hold several BSD texts (one per copyright holder), each read
|
|
552
|
+
// up to where the next one starts.
|
|
553
|
+
const bsdPhrase = "REDISTRIBUTION AND USE IN SOURCE AND BINARY FORMS";
|
|
554
|
+
for (
|
|
555
|
+
let bsdAt = text.indexOf(bsdPhrase);
|
|
556
|
+
bsdAt >= 0;
|
|
557
|
+
bsdAt = text.indexOf(bsdPhrase, bsdAt + bsdPhrase.length)
|
|
558
|
+
) {
|
|
559
|
+
const nextAt = text.indexOf(bsdPhrase, bsdAt + bsdPhrase.length);
|
|
560
|
+
const clauses = text.slice(
|
|
561
|
+
bsdAt,
|
|
562
|
+
Math.min(bsdAt + 2000, nextAt < 0 ? text.length : nextAt),
|
|
563
|
+
);
|
|
564
|
+
add(
|
|
565
|
+
bsdAt,
|
|
566
|
+
clauses.includes("ALL ADVERTISING MATERIALS MENTIONING")
|
|
567
|
+
? "BSD-4-Clause"
|
|
568
|
+
: clauses.includes("NEITHER THE NAME") ||
|
|
569
|
+
clauses.includes("THE NAME OF THE AUTHOR MAY NOT BE USED")
|
|
570
|
+
? "BSD-3-Clause"
|
|
571
|
+
: "BSD-2-Clause",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const ids = [];
|
|
575
|
+
for (const { id } of found.sort((a, b) => a.at - b.at)) {
|
|
576
|
+
if (!ids.includes(id)) {
|
|
577
|
+
ids.push(id);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (ids.length) {
|
|
581
|
+
return ids;
|
|
582
|
+
}
|
|
583
|
+
const guessed = guessLicenseId(content);
|
|
584
|
+
return guessed ? [guessed] : [];
|
|
585
|
+
}
|
|
586
|
+
|
|
443
587
|
/**
|
|
444
588
|
* Method to guess the spdx license id from license contents
|
|
445
589
|
*
|
|
@@ -195,7 +195,7 @@ export function resolveSaferExecBinary() {
|
|
|
195
195
|
* @param {boolean} [options.denyPersistenceWrites] - Deny writes to auto-execution and persistence locations
|
|
196
196
|
* @param {boolean} [options.privateTmp] - Private tmpfs /tmp and /var/tmp (Linux)
|
|
197
197
|
* @param {string} [options.protectHome] - Home isolation: read-only, tmpfs, or off (Linux)
|
|
198
|
-
* @returns {Promise<{libPaths: string[], httpAccessEntries: Object[], egressEntries?: Object[], dryRunSummary?: Object}>} Collected libraries, HTTP URLs, proxy egress decisions, and the dry-run summary
|
|
198
|
+
* @returns {Promise<{libPaths: string[], httpAccessEntries: Object[], cryptoEntries?: Object[], cryptoComponents?: Object[], cryptoResult?: Object, egressEntries?: Object[], dryRunSummary?: Object}>} Collected libraries, HTTP URLs, crypto observations, proxy egress decisions, and the dry-run summary
|
|
199
199
|
* @throws {Error} When the @cdxgen/safer-exec tracing runtime is not installed
|
|
200
200
|
* (for example after pruning it from a standalone payload), since the traced
|
|
201
201
|
* command could never run and the BOM would silently be empty
|
|
@@ -531,6 +531,9 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
|
531
531
|
httpAccessEntries: [...collectedUrls, ...urls],
|
|
532
532
|
cryptoEntries: [...collectedCrypto, ...cryptoLogs],
|
|
533
533
|
cryptoComponents,
|
|
534
|
+
// The CRYPTO result from the report channel: the cipher entries carry
|
|
535
|
+
// the certificate chains servers presented (safer-exec ≥ 1.2.0).
|
|
536
|
+
cryptoResult: result?.crypto,
|
|
534
537
|
egressEntries: parseEgressEntries(result.auditLog),
|
|
535
538
|
dryRunSummary,
|
|
536
539
|
};
|
|
@@ -541,6 +544,7 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
|
|
|
541
544
|
httpAccessEntries: collectedUrls,
|
|
542
545
|
cryptoEntries: collectedCrypto,
|
|
543
546
|
cryptoComponents,
|
|
547
|
+
cryptoResult: result?.crypto,
|
|
544
548
|
egressEntries: [],
|
|
545
549
|
dryRunSummary,
|
|
546
550
|
};
|
|
@@ -3,6 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { thoughtLog } from "../core/logger.js";
|
|
5
5
|
import { dirNameStr } from "../core/paths.js";
|
|
6
|
+
import {
|
|
7
|
+
CertificateCollector,
|
|
8
|
+
certRecordFromObservedChainEntry,
|
|
9
|
+
toWellFormedText,
|
|
10
|
+
} from "./certificates.js";
|
|
6
11
|
import { applyAlgorithmProperties } from "./cryptoAlgorithmFamily.js";
|
|
7
12
|
|
|
8
13
|
const cryptoOids = JSON.parse(
|
|
@@ -270,6 +275,130 @@ export function normalizeTracedCryptoComponents(cbomComponents) {
|
|
|
270
275
|
return Array.from(byRef.values());
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
// safer-exec walks at most this many certificates of one server's chain
|
|
279
|
+
// (tlsobserve maxChainCerts); a longer array did not come from it, so only the
|
|
280
|
+
// first entries are read.
|
|
281
|
+
const MAX_CHAIN_CERTS = 10;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The bom-ref normalizeTracedCryptoComponents gives the protocol component of
|
|
285
|
+
* a negotiated TLS version: safer-exec's CBOM names it "TLSv1.2" with the
|
|
286
|
+
* version the prefix leaves ("1.2").
|
|
287
|
+
*
|
|
288
|
+
* @param {string} protocol negotiated protocol name from the crypto result
|
|
289
|
+
* @returns {string} protocol component bom-ref
|
|
290
|
+
*/
|
|
291
|
+
function tracedProtocolBomRef(protocol) {
|
|
292
|
+
const name = String(protocol || "");
|
|
293
|
+
const version = name.startsWith("TLSv") ? name.slice("TLSv".length) : name;
|
|
294
|
+
return `crypto/protocol/${encodeURIComponent(name)}@${encodeURIComponent(version || "unknown")}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Builds the shared certificate model for the chains servers presented in TLS
|
|
299
|
+
* ≤ 1.2 handshakes, from the `chain` arrays of safer-exec's crypto result
|
|
300
|
+
* (report channel, safer-exec ≥ 1.2.0; earlier releases and TLS 1.3 entries
|
|
301
|
+
* carry no chain and yield nothing). The records go through the shared
|
|
302
|
+
* certificate collector, so bom-refs, properties and dedup match certificates
|
|
303
|
+
* found on disk. A presented chain is untrusted server input: no issuer edges
|
|
304
|
+
* are inferred from it, and the certificate and public-key components link
|
|
305
|
+
* through the collector as every producer's do. Each TLS protocol component
|
|
306
|
+
* provides the certificates and keys presented under it — the edge safer-
|
|
307
|
+
* exec's own CBOM records — which answers which servers presented a key.
|
|
308
|
+
*
|
|
309
|
+
* @param {Object} cryptoResult safer-exec CRYPTO result (`result.crypto`)
|
|
310
|
+
* @param {Set<string>} protocolRefs bom-refs of the protocol components in the BOM;
|
|
311
|
+
* a provides edge is only emitted for a protocol that is actually present
|
|
312
|
+
* @returns {{components: Object[], dependencies: Object[]}} certificate,
|
|
313
|
+
* public-key and algorithm components, and the protocol provides edges
|
|
314
|
+
*/
|
|
315
|
+
export function observedChainComponents(
|
|
316
|
+
cryptoResult,
|
|
317
|
+
protocolRefs = new Set(),
|
|
318
|
+
) {
|
|
319
|
+
const collector = new CertificateCollector();
|
|
320
|
+
// Protocol name -> {certSha256, spkiSha256} pairs in presentation order,
|
|
321
|
+
// leaf first; sorted to refs at the end so the output never depends on the
|
|
322
|
+
// connection order the engine happened to report.
|
|
323
|
+
const chainEntriesByProtocol = new Map();
|
|
324
|
+
// The collector keeps the first record's fields for a certSha256, so two
|
|
325
|
+
// servers presenting one hash with conflicting text would otherwise make
|
|
326
|
+
// the name (and bom-ref) depend on connection order; sort first.
|
|
327
|
+
const ciphers = (
|
|
328
|
+
Array.isArray(cryptoResult?.ciphers) ? cryptoResult.ciphers : []
|
|
329
|
+
)
|
|
330
|
+
.map((cipher) => [JSON.stringify(cipher) ?? "", cipher])
|
|
331
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
332
|
+
.map(([, cipher]) => cipher);
|
|
333
|
+
for (const cipher of ciphers) {
|
|
334
|
+
const chain = cipher?.chain;
|
|
335
|
+
if (!Array.isArray(chain) || chain.length === 0) {
|
|
336
|
+
// safer-exec ≤ 1.1.2, or a TLS 1.3 handshake (encrypted Certificate
|
|
337
|
+
// message): no chain was observed.
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const host = typeof cipher.host === "string" ? cipher.host : "";
|
|
341
|
+
for (const entry of chain.slice(0, MAX_CHAIN_CERTS)) {
|
|
342
|
+
const record = certRecordFromObservedChainEntry(entry, { host });
|
|
343
|
+
if (record) {
|
|
344
|
+
collector.addRecord(record);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// A protocol name is free text from the report channel; it only reaches a
|
|
348
|
+
// bom-ref through encodeURIComponent, so it must be well-formed.
|
|
349
|
+
const protocol = toWellFormedText(cipher.protocol);
|
|
350
|
+
if (protocol) {
|
|
351
|
+
let entries = chainEntriesByProtocol.get(protocol);
|
|
352
|
+
if (!entries) {
|
|
353
|
+
entries = [];
|
|
354
|
+
chainEntriesByProtocol.set(protocol, entries);
|
|
355
|
+
}
|
|
356
|
+
for (const entry of chain.slice(0, MAX_CHAIN_CERTS)) {
|
|
357
|
+
const certSha256 = String(entry?.certSha256 || "").toLowerCase();
|
|
358
|
+
const spkiSha256 = String(entry?.spkiSha256 || "").toLowerCase();
|
|
359
|
+
if (certSha256.length === 64) {
|
|
360
|
+
entries.push({ certSha256, spkiSha256 });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const { components, dependencies } = collector.build();
|
|
366
|
+
// The collector's components identify their asset by the hash in `version`
|
|
367
|
+
// (certSha256 for certificates, spkiSha256 for public keys).
|
|
368
|
+
const certRefBySha = new Map();
|
|
369
|
+
const keyRefBySpki = new Map();
|
|
370
|
+
for (const component of components) {
|
|
371
|
+
const assetType = component.cryptoProperties?.assetType;
|
|
372
|
+
if (assetType === "certificate") {
|
|
373
|
+
certRefBySha.set(component.version, component["bom-ref"]);
|
|
374
|
+
} else if (assetType === "related-crypto-material") {
|
|
375
|
+
keyRefBySpki.set(component.version, component["bom-ref"]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const outputDependencies = [...dependencies];
|
|
379
|
+
for (const [protocol, entries] of chainEntriesByProtocol) {
|
|
380
|
+
const protocolRef = tracedProtocolBomRef(protocol);
|
|
381
|
+
if (!protocolRefs.has(protocolRef)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const provides = [
|
|
385
|
+
...new Set(
|
|
386
|
+
entries.flatMap(({ certSha256, spkiSha256 }) => [
|
|
387
|
+
certRefBySha.get(certSha256),
|
|
388
|
+
keyRefBySpki.get(spkiSha256),
|
|
389
|
+
]),
|
|
390
|
+
),
|
|
391
|
+
]
|
|
392
|
+
.filter((ref) => ref)
|
|
393
|
+
.sort();
|
|
394
|
+
if (provides.length) {
|
|
395
|
+
outputDependencies.push({ ref: protocolRef, provides });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
outputDependencies.sort((left, right) => left.ref.localeCompare(right.ref));
|
|
399
|
+
return { components, dependencies: outputDependencies };
|
|
400
|
+
}
|
|
401
|
+
|
|
273
402
|
/**
|
|
274
403
|
* Maps the crypto libraries safer-exec reported to the paths they were loaded
|
|
275
404
|
* from.
|