@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/managers/docker.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
basename,
|
|
11
11
|
isAbsolute,
|
|
12
12
|
join,
|
|
13
|
+
posix,
|
|
13
14
|
relative,
|
|
14
15
|
resolve,
|
|
15
16
|
win32,
|
|
@@ -18,10 +19,11 @@ import process from "node:process";
|
|
|
18
19
|
import stream from "node:stream/promises";
|
|
19
20
|
import { URL } from "node:url";
|
|
20
21
|
|
|
21
|
-
import { x } from "tar";
|
|
22
|
+
import { t as listTar, x } from "tar";
|
|
22
23
|
|
|
23
24
|
import {
|
|
24
25
|
DEBUG_MODE,
|
|
26
|
+
isAllowedHttpHost,
|
|
25
27
|
isDryRun,
|
|
26
28
|
readEnvironmentVariable,
|
|
27
29
|
recordActivity,
|
|
@@ -1081,6 +1083,36 @@ const isSafeImageReference = (fullImageName) =>
|
|
|
1081
1083
|
fullImageName === fullImageName.trim() &&
|
|
1082
1084
|
isImageReference(fullImageName);
|
|
1083
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* Whether an image named in a scanned manifest, such as a docker-compose file
|
|
1088
|
+
* or a Kubernetes spec, may be pulled and inventoried. The name comes from the
|
|
1089
|
+
* scanned project, so a local archive path would read files outside the
|
|
1090
|
+
* allowed paths and a registry outside CDXGEN_ALLOWED_HOSTS would bypass the
|
|
1091
|
+
* host allowlist.
|
|
1092
|
+
*
|
|
1093
|
+
* @param {string} fullImageName Image name from the manifest
|
|
1094
|
+
* @param {Object} [imageObj] Result of parseImageName for the same name
|
|
1095
|
+
* @returns {boolean} true when the image may be followed
|
|
1096
|
+
*/
|
|
1097
|
+
export const isFollowableManifestImage = (fullImageName, imageObj) => {
|
|
1098
|
+
if (!isSafeImageReference(fullImageName)) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
const allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS");
|
|
1102
|
+
if (!allowedHostsEnv) {
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
const registry =
|
|
1106
|
+
(imageObj || parseImageName(fullImageName)).registry || "docker.io";
|
|
1107
|
+
let registryHost;
|
|
1108
|
+
try {
|
|
1109
|
+
registryHost = new URL(`https://${registry}`).hostname;
|
|
1110
|
+
} catch (_err) {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
return isAllowedHttpHost(registryHost, allowedHostsEnv);
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1084
1116
|
/**
|
|
1085
1117
|
* Parse image name
|
|
1086
1118
|
*
|
|
@@ -1586,19 +1618,20 @@ function handleTarWarning(code, message) {
|
|
|
1586
1618
|
}
|
|
1587
1619
|
}
|
|
1588
1620
|
|
|
1589
|
-
// These paths are known to cause extract errors
|
|
1621
|
+
// These paths are known to cause extract errors. CA-store directories
|
|
1622
|
+
// (etc/ssl/certs, etc/pki/ca-trust, ssl/certs, cacerts,
|
|
1623
|
+
// var/lib/ca-certificates) are deliberately NOT excluded any more: their
|
|
1624
|
+
// original extract failures came from the hash-named symlink farms, and
|
|
1625
|
+
// symlinks/hardlinks are excluded by type below, while the regular
|
|
1626
|
+
// certificate files are exactly what a CBOM must inventory.
|
|
1590
1627
|
const EXTRACT_EXCLUDE_PATHS = [
|
|
1591
1628
|
"etc/machine-id",
|
|
1592
1629
|
"etc/gshadow",
|
|
1593
1630
|
"etc/shadow",
|
|
1594
1631
|
"etc/passwd",
|
|
1595
|
-
"etc/ssl/certs",
|
|
1596
|
-
"etc/pki/ca-trust",
|
|
1597
1632
|
"usr/lib/systemd/",
|
|
1598
1633
|
"usr/lib64/libdevmapper.so",
|
|
1599
1634
|
"usr/sbin/",
|
|
1600
|
-
"cacerts",
|
|
1601
|
-
"ssl/certs",
|
|
1602
1635
|
"logs/",
|
|
1603
1636
|
"dev/",
|
|
1604
1637
|
"proc/",
|
|
@@ -1608,7 +1641,6 @@ const EXTRACT_EXCLUDE_PATHS = [
|
|
|
1608
1641
|
"usr/share/man/",
|
|
1609
1642
|
"usr/share/icons/",
|
|
1610
1643
|
"usr/share/i18n/",
|
|
1611
|
-
"var/lib/ca-certificates",
|
|
1612
1644
|
"root/.gnupg",
|
|
1613
1645
|
"root/.dotnet",
|
|
1614
1646
|
"usr/share/licenses/device-mapper-libs",
|
|
@@ -1634,16 +1666,18 @@ const MEDIA_EXTENSIONS = new Set([
|
|
|
1634
1666
|
".eot",
|
|
1635
1667
|
]);
|
|
1636
1668
|
|
|
1637
|
-
// These
|
|
1669
|
+
// These entry types are known to cause extract errors. The names are the
|
|
1670
|
+
// ones node-tar's header parser assigns (types.js in the tar package):
|
|
1671
|
+
// symlinks and hardlinks ("SymbolicLink", "Link") are how CA-store
|
|
1672
|
+
// hash-named entries appear, which is why the CA paths could be un-excluded
|
|
1673
|
+
// above once these were dropped by type.
|
|
1638
1674
|
const EXTRACT_EXCLUDE_TYPES = new Set([
|
|
1639
1675
|
"BlockDevice",
|
|
1640
1676
|
"CharacterDevice",
|
|
1641
1677
|
"FIFO",
|
|
1642
|
-
"
|
|
1643
|
-
"
|
|
1678
|
+
"ContinuationFile",
|
|
1679
|
+
"TapeVolumeHeader",
|
|
1644
1680
|
"SymbolicLink",
|
|
1645
|
-
"RenamedOrSymlinked",
|
|
1646
|
-
"HardLink",
|
|
1647
1681
|
"Link",
|
|
1648
1682
|
]);
|
|
1649
1683
|
|
|
@@ -1994,6 +2028,126 @@ const resolveContainedArchivePath = (tempDir, archiveRef, kind) => {
|
|
|
1994
2028
|
return join(tempDir, archiveRef);
|
|
1995
2029
|
};
|
|
1996
2030
|
|
|
2031
|
+
const WHITEOUT_PREFIX = ".wh.";
|
|
2032
|
+
const WHITEOUT_OPAQUE = ".wh..wh..opq";
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Split an untrusted tar entry path into safe segments. Returns undefined when
|
|
2036
|
+
* the path contains a parent-directory segment.
|
|
2037
|
+
*
|
|
2038
|
+
* @param {string} entryPath Raw entry path from a layer
|
|
2039
|
+
* @returns {string[]|undefined} Path segments, or undefined when unsafe
|
|
2040
|
+
*/
|
|
2041
|
+
const safeEntrySegments = (entryPath) => {
|
|
2042
|
+
const stripped = stripAbsolutePath(String(entryPath).replaceAll("\\", "/"));
|
|
2043
|
+
const parts = stripped.split("/").filter((p) => p && p !== ".");
|
|
2044
|
+
return parts.includes("..") ? undefined : parts;
|
|
2045
|
+
};
|
|
2046
|
+
|
|
2047
|
+
/**
|
|
2048
|
+
* Walk down from rootDir through real directories only. A symlink or a
|
|
2049
|
+
* missing component stops the walk, so removals never leave rootDir.
|
|
2050
|
+
*
|
|
2051
|
+
* @param {string} rootDir Extraction root
|
|
2052
|
+
* @param {string[]} parts Directory segments below the root
|
|
2053
|
+
* @returns {string|undefined} The directory path, or undefined
|
|
2054
|
+
*/
|
|
2055
|
+
const resolveRealDir = (rootDir, parts) => {
|
|
2056
|
+
let current = rootDir;
|
|
2057
|
+
for (const part of parts) {
|
|
2058
|
+
current = join(current, part);
|
|
2059
|
+
let st;
|
|
2060
|
+
try {
|
|
2061
|
+
st = lstatSync(current);
|
|
2062
|
+
} catch (_err) {
|
|
2063
|
+
return undefined;
|
|
2064
|
+
}
|
|
2065
|
+
if (!st.isDirectory()) {
|
|
2066
|
+
return undefined;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
return current;
|
|
2070
|
+
};
|
|
2071
|
+
|
|
2072
|
+
/**
|
|
2073
|
+
* Apply the OCI/Docker whiteouts of one layer to the merged extraction
|
|
2074
|
+
* directory. `.wh.<name>` removes <name>, and `.wh..wh..opq` empties the
|
|
2075
|
+
* directory it sits in, both only for content from earlier layers, so call
|
|
2076
|
+
* this before extracting the layer itself. Matches
|
|
2077
|
+
* ci/reconstruct-staged-rootfs.py.
|
|
2078
|
+
* https://github.com/opencontainers/image-spec/blob/v1.1.0/layer.md#whiteouts
|
|
2079
|
+
*
|
|
2080
|
+
* @param {string} layerPath Layer tar (optionally compressed)
|
|
2081
|
+
* @param {string} rootDir Merged extraction directory
|
|
2082
|
+
*/
|
|
2083
|
+
// Applies one whiteout entry of a layer to the merged root.
|
|
2084
|
+
const applyWhiteout = (entryPath, rootDir) => {
|
|
2085
|
+
const parts = safeEntrySegments(entryPath);
|
|
2086
|
+
if (!parts?.length) {
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
const name = parts[parts.length - 1];
|
|
2090
|
+
const parentParts = parts.slice(0, -1);
|
|
2091
|
+
if (name === WHITEOUT_OPAQUE) {
|
|
2092
|
+
const dir = resolveRealDir(rootDir, parentParts);
|
|
2093
|
+
if (!dir) {
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
for (const child of readdirSync(dir)) {
|
|
2097
|
+
safeRmSync(join(dir, child), { recursive: true, force: true });
|
|
2098
|
+
}
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
// Other .wh..wh.* names are AUFS metadata, not deletions.
|
|
2102
|
+
if (name.startsWith(".wh..wh.")) {
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
const target = name.slice(WHITEOUT_PREFIX.length);
|
|
2106
|
+
if (!target || target === "." || target === "..") {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const dir = resolveRealDir(rootDir, parentParts);
|
|
2110
|
+
if (dir) {
|
|
2111
|
+
safeRmSync(join(dir, target), { recursive: true, force: true });
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
const applyLayerWhiteouts = async (layerPath, rootDir) => {
|
|
2116
|
+
const whiteouts = [];
|
|
2117
|
+
try {
|
|
2118
|
+
await listTar({
|
|
2119
|
+
file: layerPath,
|
|
2120
|
+
strict: false,
|
|
2121
|
+
onwarn: () => {
|
|
2122
|
+
// extractTar reports damaged layers
|
|
2123
|
+
},
|
|
2124
|
+
onReadEntry: (entry) => {
|
|
2125
|
+
if (
|
|
2126
|
+
basename(String(entry.path).replaceAll("\\", "/")).startsWith(
|
|
2127
|
+
WHITEOUT_PREFIX,
|
|
2128
|
+
)
|
|
2129
|
+
) {
|
|
2130
|
+
whiteouts.push(entry.path);
|
|
2131
|
+
}
|
|
2132
|
+
entry.resume();
|
|
2133
|
+
},
|
|
2134
|
+
});
|
|
2135
|
+
} catch (_err) {
|
|
2136
|
+
// extractTar reports unreadable layers
|
|
2137
|
+
}
|
|
2138
|
+
for (const entryPath of whiteouts) {
|
|
2139
|
+
// One whiteout that cannot be applied (name too long, EACCES) must not
|
|
2140
|
+
// abort the layer's extraction, which would hide everything it adds.
|
|
2141
|
+
try {
|
|
2142
|
+
applyWhiteout(entryPath, rootDir);
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
if (DEBUG_MODE) {
|
|
2145
|
+
console.log(`Skipping whiteout ${entryPath}: ${err.message}`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
|
|
1997
2151
|
/**
|
|
1998
2152
|
* Parse a Docker/containerd manifest file and extract all image layers into a
|
|
1999
2153
|
* single merged directory. Resolves the last layer's config to determine the
|
|
@@ -2074,6 +2228,7 @@ export const extractFromManifest = async (
|
|
|
2074
2228
|
console.log(`Extracting layer ${layer} to ${allLayersExplodedDir}`);
|
|
2075
2229
|
}
|
|
2076
2230
|
try {
|
|
2231
|
+
await applyLayerWhiteouts(containedLayerPath, allLayersExplodedDir);
|
|
2077
2232
|
await extractTar(containedLayerPath, allLayersExplodedDir, options);
|
|
2078
2233
|
} catch (err) {
|
|
2079
2234
|
if (err.code === "TAR_BAD_ARCHIVE") {
|
|
@@ -2333,6 +2488,77 @@ export const exportImage = async (fullImageName, options) => {
|
|
|
2333
2488
|
return undefined;
|
|
2334
2489
|
};
|
|
2335
2490
|
|
|
2491
|
+
// Directories that hold far more than gems. An image that names one as its gem
|
|
2492
|
+
// directory would turn the gem scan into a scan of the whole tree.
|
|
2493
|
+
const BROAD_IMAGE_DIRS = new Set([
|
|
2494
|
+
"/bin",
|
|
2495
|
+
"/boot",
|
|
2496
|
+
"/dev",
|
|
2497
|
+
"/etc",
|
|
2498
|
+
"/home",
|
|
2499
|
+
"/lib",
|
|
2500
|
+
"/lib64",
|
|
2501
|
+
"/opt",
|
|
2502
|
+
"/proc",
|
|
2503
|
+
"/root",
|
|
2504
|
+
"/run",
|
|
2505
|
+
"/sbin",
|
|
2506
|
+
"/srv",
|
|
2507
|
+
"/sys",
|
|
2508
|
+
"/tmp",
|
|
2509
|
+
"/usr",
|
|
2510
|
+
"/usr/bin",
|
|
2511
|
+
"/usr/lib",
|
|
2512
|
+
"/usr/lib64",
|
|
2513
|
+
"/usr/local",
|
|
2514
|
+
"/usr/local/lib",
|
|
2515
|
+
"/usr/share",
|
|
2516
|
+
"/var",
|
|
2517
|
+
"/var/lib",
|
|
2518
|
+
]);
|
|
2519
|
+
const MAX_IMAGE_GEM_DIRS = 16;
|
|
2520
|
+
|
|
2521
|
+
/**
|
|
2522
|
+
* The gem directories an image's environment names in GEM_HOME, GEM_PATH and
|
|
2523
|
+
* BUNDLE_PATH. Only absolute paths without `..` segments are kept, so an image
|
|
2524
|
+
* cannot steer the scan outside its own root, and each is normalized; the root
|
|
2525
|
+
* and broad system directories such as /usr or /var are refused, so an image
|
|
2526
|
+
* cannot turn the gem scan into a scan of the whole tree.
|
|
2527
|
+
*
|
|
2528
|
+
* @param {string[]|undefined} env image config Env entries, `NAME=value`
|
|
2529
|
+
* @returns {string[]} absolute in-image directories, deduplicated, at most
|
|
2530
|
+
* MAX_IMAGE_GEM_DIRS
|
|
2531
|
+
*/
|
|
2532
|
+
export function imageGemDirs(env) {
|
|
2533
|
+
const dirs = new Set();
|
|
2534
|
+
if (!Array.isArray(env)) {
|
|
2535
|
+
return [];
|
|
2536
|
+
}
|
|
2537
|
+
for (const entry of env) {
|
|
2538
|
+
if (typeof entry !== "string") {
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
const separator = entry.indexOf("=");
|
|
2542
|
+
if (separator <= 0) {
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
const name = entry.slice(0, separator);
|
|
2546
|
+
if (!["GEM_HOME", "GEM_PATH", "BUNDLE_PATH"].includes(name)) {
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
for (const value of entry.slice(separator + 1).split(":")) {
|
|
2550
|
+
if (!value.startsWith("/") || value.split("/").includes("..")) {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
const dir = posix.normalize(value).replace(/\/+$/, "");
|
|
2554
|
+
if (dir && !BROAD_IMAGE_DIRS.has(dir)) {
|
|
2555
|
+
dirs.add(dir);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return Array.from(dirs).sort().slice(0, MAX_IMAGE_GEM_DIRS);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2336
2562
|
/**
|
|
2337
2563
|
* Method to retrieve path list for system-level packages
|
|
2338
2564
|
*/
|
|
@@ -2376,6 +2602,7 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
|
|
|
2376
2602
|
if (allLayersExplodedDir && allLayersExplodedDir !== "") {
|
|
2377
2603
|
knownSysPaths = knownSysPaths.concat([
|
|
2378
2604
|
join(allLayersExplodedDir, "/usr/local/go"),
|
|
2605
|
+
join(allLayersExplodedDir, "/usr/local/bundle"),
|
|
2379
2606
|
join(allLayersExplodedDir, "/usr/local/lib"),
|
|
2380
2607
|
join(allLayersExplodedDir, "/usr/local/lib64"),
|
|
2381
2608
|
join(allLayersExplodedDir, "/opt"),
|
|
@@ -2390,6 +2617,7 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
|
|
|
2390
2617
|
} else if (allLayersExplodedDir === "") {
|
|
2391
2618
|
knownSysPaths = knownSysPaths.concat([
|
|
2392
2619
|
join(allLayersExplodedDir, "/usr/local/go"),
|
|
2620
|
+
join(allLayersExplodedDir, "/usr/local/bundle"),
|
|
2393
2621
|
join(allLayersExplodedDir, "/usr/local/lib"),
|
|
2394
2622
|
join(allLayersExplodedDir, "/usr/local/lib64"),
|
|
2395
2623
|
join(allLayersExplodedDir, "/opt"),
|
|
@@ -2400,6 +2628,11 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
|
|
|
2400
2628
|
join(allLayersExplodedDir, "/var/lib"),
|
|
2401
2629
|
]);
|
|
2402
2630
|
}
|
|
2631
|
+
// The official Ruby images install gems into GEM_HOME=/usr/local/bundle, and
|
|
2632
|
+
// an image can point GEM_HOME, GEM_PATH or BUNDLE_PATH anywhere else.
|
|
2633
|
+
for (const gemDir of imageGemDirs(exportData?.inspectData?.Config?.Env)) {
|
|
2634
|
+
knownSysPaths.push(join(allLayersExplodedDir || "", gemDir));
|
|
2635
|
+
}
|
|
2403
2636
|
if (safeExistsSync(join(allLayersDir, "Files"))) {
|
|
2404
2637
|
knownSysPaths.push(join(allLayersDir, "Files"));
|
|
2405
2638
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { isIPv4, isIPv6 } from "node:net";
|
|
2
|
+
import { domainToASCII, domainToUnicode } from "node:url";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validation of RFC 6531 internationalized email addresses: the JSON Schema
|
|
6
|
+
* `idn-email` format the CycloneDX schemas assert on contact addresses.
|
|
7
|
+
*
|
|
8
|
+
* It follows what `cdxrs validate` (jsonschema-rs) accepts, so the JS and Rust
|
|
9
|
+
* validators agree on the addresses BOMs actually carry, internationalized
|
|
10
|
+
* ones included. Two deliberate differences keep this small:
|
|
11
|
+
* - a `Display Name <address>` string is not an address here, as RFC 6531
|
|
12
|
+
* says; jsonschema-rs tolerates it, but cdxgen never emits one and the
|
|
13
|
+
* source parsers must not mistake one for an email;
|
|
14
|
+
* - the RFC 5892 contextual rules and the RFC 5893 bidi rule are not applied,
|
|
15
|
+
* so a few exotic internationalized domains are accepted that cdxrs
|
|
16
|
+
* rejects. No valid domain is rejected for that.
|
|
17
|
+
*
|
|
18
|
+
* Every check is a bounded pass over the input with no backtracking regular
|
|
19
|
+
* expression.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const LOCAL_PART_MAX_LENGTH = 64;
|
|
23
|
+
const HOSTNAME_MAX_LENGTH = 253;
|
|
24
|
+
const LABEL_MAX_LENGTH = 63;
|
|
25
|
+
const ATEXT_SPECIALS = "!#$%&'*+-/=?^_`{|}~";
|
|
26
|
+
// Appended before IDNA conversion so a numeric last label is not read as an
|
|
27
|
+
// IPv4 address by the WHATWG host parser behind `domainToASCII`.
|
|
28
|
+
const CONVERSION_SUFFIX = ".invalid";
|
|
29
|
+
// What an internationalized label may hold: letters, combining marks and
|
|
30
|
+
// decimal digits (RFC 5892 PVALID, by general category), plus the characters
|
|
31
|
+
// RFC 5892 makes PVALID by exception, such as the Catalan middle dot.
|
|
32
|
+
const IDNA_LABEL_CHAR = /^[\p{L}\p{Mn}\p{Mc}\p{Nd}]$/u;
|
|
33
|
+
const IDNA_EXCEPTIONS = new Set([
|
|
34
|
+
"·",
|
|
35
|
+
"͵",
|
|
36
|
+
"׳",
|
|
37
|
+
"״",
|
|
38
|
+
"۽",
|
|
39
|
+
"۾",
|
|
40
|
+
"་",
|
|
41
|
+
"",
|
|
42
|
+
"",
|
|
43
|
+
"〇",
|
|
44
|
+
"・",
|
|
45
|
+
]);
|
|
46
|
+
const COMBINING_MARK = /^\p{M}$/u;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} text Text to inspect
|
|
50
|
+
* @returns {boolean} Whether the text holds only ASCII
|
|
51
|
+
*/
|
|
52
|
+
const isAscii = (text) => {
|
|
53
|
+
for (let index = 0; index < text.length; index++) {
|
|
54
|
+
if (text.charCodeAt(index) > 0x7f) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {number} code Code point
|
|
63
|
+
* @returns {boolean} Whether the code point is an ASCII letter or digit
|
|
64
|
+
*/
|
|
65
|
+
const isAsciiAlphanumeric = (code) =>
|
|
66
|
+
(code >= 0x30 && code <= 0x39) ||
|
|
67
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
68
|
+
(code >= 0x61 && code <= 0x7a);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* RFC 5322 `atext`, widened by RFC 6531 to every non-ASCII code point.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} char One code point
|
|
74
|
+
* @returns {boolean} Whether it may appear in an unquoted local part atom
|
|
75
|
+
*/
|
|
76
|
+
const isAtext = (char) => {
|
|
77
|
+
const code = char.codePointAt(0);
|
|
78
|
+
return (
|
|
79
|
+
code > 0x7f || isAsciiAlphanumeric(code) || ATEXT_SPECIALS.includes(char)
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The content of a quoted local part: printable ASCII other than `"` and `\`,
|
|
85
|
+
* non-ASCII, spaces and tabs, and backslash-escaped printable characters.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} text Quoted local part without its surrounding quotes
|
|
88
|
+
* @returns {boolean} Whether the quoted content is valid
|
|
89
|
+
*/
|
|
90
|
+
const isQuotedContent = (text) => {
|
|
91
|
+
const chars = [...text];
|
|
92
|
+
for (let index = 0; index < chars.length; index++) {
|
|
93
|
+
const code = chars[index].codePointAt(0);
|
|
94
|
+
if (code === 0x5c) {
|
|
95
|
+
index++;
|
|
96
|
+
const escaped = chars[index]?.codePointAt(0);
|
|
97
|
+
if (escaped === undefined || escaped < 0x21 || escaped === 0x7f) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
} else if (code < 0x20 || code === 0x22 || code === 0x7f) {
|
|
101
|
+
if (code !== 0x09) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A dot-atom (`atext` atoms joined by single dots) or a quoted string, at
|
|
111
|
+
* most 64 characters.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} localPart Text before the last `@`
|
|
114
|
+
* @returns {boolean} Whether it is a valid local part
|
|
115
|
+
*/
|
|
116
|
+
const isValidLocalPart = (localPart) => {
|
|
117
|
+
if (!localPart || [...localPart].length > LOCAL_PART_MAX_LENGTH) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
localPart.length >= 2 &&
|
|
122
|
+
localPart.startsWith('"') &&
|
|
123
|
+
localPart.endsWith('"')
|
|
124
|
+
) {
|
|
125
|
+
// An empty quoted local part (`""@example.com`) is valid per RFC 5321.
|
|
126
|
+
return isQuotedContent(localPart.slice(1, -1));
|
|
127
|
+
}
|
|
128
|
+
return localPart
|
|
129
|
+
.split(".")
|
|
130
|
+
.every((atom) => atom.length > 0 && [...atom].every(isAtext));
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* An RFC 1123 label in ASCII (A-label) form.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} label ASCII label
|
|
137
|
+
* @returns {boolean} Whether the label is valid
|
|
138
|
+
*/
|
|
139
|
+
const isValidAsciiLabel = (label) => {
|
|
140
|
+
if (
|
|
141
|
+
!label ||
|
|
142
|
+
label.length > LABEL_MAX_LENGTH ||
|
|
143
|
+
label.startsWith("-") ||
|
|
144
|
+
label.endsWith("-")
|
|
145
|
+
) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
for (let index = 0; index < label.length; index++) {
|
|
149
|
+
const code = label.charCodeAt(index);
|
|
150
|
+
if (!isAsciiAlphanumeric(code) && code !== 0x2d) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Hyphens in the third and fourth positions are reserved for A-labels.
|
|
155
|
+
return label.slice(2, 4) !== "--" || label.toLowerCase().startsWith("xn--");
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The Unicode form of a label, as `domainToUnicode` returns it.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} label Unicode label
|
|
162
|
+
* @returns {boolean} Whether the label is valid
|
|
163
|
+
*/
|
|
164
|
+
const isValidUnicodeLabel = (label) => {
|
|
165
|
+
// `domainToUnicode` leaves an undecodable A-label as it was.
|
|
166
|
+
if (label.toLowerCase().startsWith("xn--")) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (isAscii(label)) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (
|
|
173
|
+
label.startsWith("-") ||
|
|
174
|
+
label.endsWith("-") ||
|
|
175
|
+
label.slice(2, 4) === "--"
|
|
176
|
+
) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const chars = [...label];
|
|
180
|
+
return (
|
|
181
|
+
!COMBINING_MARK.test(chars[0]) &&
|
|
182
|
+
chars.every(
|
|
183
|
+
(char) =>
|
|
184
|
+
char.charCodeAt(0) <= 0x7f ||
|
|
185
|
+
IDNA_EXCEPTIONS.has(char) ||
|
|
186
|
+
IDNA_LABEL_CHAR.test(char),
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* An internationalized hostname: mapped to its A-label form with UTS #46
|
|
193
|
+
* processing, held to the ASCII hostname rules, and every A-label decoded
|
|
194
|
+
* back and checked for the characters IDNA allows.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} hostname Hostname, possibly holding non-ASCII labels
|
|
197
|
+
* @returns {boolean} Whether it is a valid IDN hostname
|
|
198
|
+
*/
|
|
199
|
+
export const isValidIdnHostname = (hostname) => {
|
|
200
|
+
if (typeof hostname !== "string" || !hostname) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
for (let index = 0; index < hostname.length; index++) {
|
|
204
|
+
// Controls and spaces never belong in a hostname; the WHATWG parser
|
|
205
|
+
// behind `domainToASCII` would silently strip tabs and newlines.
|
|
206
|
+
const code = hostname.charCodeAt(index);
|
|
207
|
+
if (code <= 0x20 || code === 0x7f) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let ascii = hostname;
|
|
212
|
+
if (!isAscii(hostname)) {
|
|
213
|
+
const converted = domainToASCII(`${hostname}${CONVERSION_SUFFIX}`);
|
|
214
|
+
if (!converted.endsWith(CONVERSION_SUFFIX)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
ascii = converted.slice(0, -CONVERSION_SUFFIX.length);
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
!ascii ||
|
|
221
|
+
ascii.length > HOSTNAME_MAX_LENGTH ||
|
|
222
|
+
ascii.endsWith(".") ||
|
|
223
|
+
!ascii.split(".").every(isValidAsciiLabel)
|
|
224
|
+
) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (!/(^|\.)xn--/i.test(ascii)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
const unicode = domainToUnicode(`${ascii}${CONVERSION_SUFFIX}`);
|
|
231
|
+
return (
|
|
232
|
+
unicode.endsWith(CONVERSION_SUFFIX) &&
|
|
233
|
+
unicode
|
|
234
|
+
.slice(0, -CONVERSION_SUFFIX.length)
|
|
235
|
+
.split(".")
|
|
236
|
+
.every(isValidUnicodeLabel)
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {string} domain Text after the last `@`
|
|
242
|
+
* @returns {boolean} Whether it is a valid hostname or address literal
|
|
243
|
+
*/
|
|
244
|
+
const isValidEmailDomain = (domain) => {
|
|
245
|
+
if (domain.startsWith("[") && domain.endsWith("]")) {
|
|
246
|
+
const literal = domain.slice(1, -1);
|
|
247
|
+
// RFC 5321 address-literal tags are case-insensitive.
|
|
248
|
+
if (literal.slice(0, 5).toLowerCase() === "ipv6:") {
|
|
249
|
+
return isIPv6(literal.slice(5));
|
|
250
|
+
}
|
|
251
|
+
return isIPv4(literal);
|
|
252
|
+
}
|
|
253
|
+
return isValidIdnHostname(domain);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Whether a value is a valid RFC 6531 email address (`idn-email`): a dot-atom
|
|
258
|
+
* or quoted local part, where non-ASCII characters are allowed, then `@` and
|
|
259
|
+
* an internationalized hostname or a bracketed IPv4/IPv6 literal.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} value Candidate email address
|
|
262
|
+
* @returns {boolean} true when the value is a valid address
|
|
263
|
+
*/
|
|
264
|
+
export const isValidIdnEmail = (value) => {
|
|
265
|
+
if (typeof value !== "string") {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
// A domain never holds `@`; a quoted local part may.
|
|
269
|
+
const separator = value.lastIndexOf("@");
|
|
270
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return (
|
|
274
|
+
isValidLocalPart(value.slice(0, separator)) &&
|
|
275
|
+
isValidEmailDomain(value.slice(separator + 1))
|
|
276
|
+
);
|
|
277
|
+
};
|