@cdxgen/cdxgen 13.4.0 → 13.5.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 +7 -7
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +25 -5
- package/bin/evinse.js +5 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +283 -1
- 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/lib/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +28 -3
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +73 -4
- package/lib/cli/jvmBom.js +126 -20
- package/lib/cli/managedBom.js +711 -71
- package/lib/cli/nativeBom.js +225 -28
- package/lib/core/env.js +30 -8
- package/lib/core/fs.js +28 -19
- package/lib/core/httpClient.js +74 -45
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -0
- package/lib/ecosystems/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +30 -20
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +229 -44
- package/lib/ecosystems/parsers-python.js +11 -10
- package/lib/ecosystems/rubyutils.js +6 -3
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +2 -1
- package/lib/ecosystems/scalasem.js +3 -1
- package/lib/evinser/evinser.js +126 -18
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +33 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +6 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +500 -58
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +168 -5
- package/lib/inventory/evidenceUtils.js +436 -23
- package/lib/inventory/formulationParsers.js +3 -2
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostTopology.js +21 -0
- package/lib/inventory/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +89 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/phpBuildInfo.js +487 -0
- package/lib/inventory/phpHostProbe.js +641 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/source.js +207 -3
- 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 +529 -183
- package/lib/managers/docker.js +166 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +34 -2
- package/lib/server/server.js +186 -1
- 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 +58 -1
- package/lib/stages/postgen/specVersionCompat.js +30 -0
- package/lib/stages/pregen/pregen.js +117 -5
- package/lib/validator/bomValidator.js +26 -5
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +26 -7
- package/lib/validator/generated/validate-1.7.mjs +31 -12
- package/lib/validator/generated/validate-2.0.mjs +30 -11
- package/package.json +17 -17
- 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 +9 -2
- 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/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/env.d.ts +18 -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.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/ecosystems/cppEvidence.d.ts +1 -1
- 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 +10 -8
- package/types/lib/ecosystems/gradleutils.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-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +4 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- 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/evinser/evinser.d.ts +25 -0
- 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 +14 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.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 +145 -6
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- 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 +40 -2
- 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/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/hostTopology.d.ts.map +1 -1
- 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 +6 -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/source.d.ts +28 -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 +12 -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 +13 -0
- 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/pregen.d.ts +14 -1
- 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
|
@@ -18,10 +18,11 @@ import process from "node:process";
|
|
|
18
18
|
import stream from "node:stream/promises";
|
|
19
19
|
import { URL } from "node:url";
|
|
20
20
|
|
|
21
|
-
import { x } from "tar";
|
|
21
|
+
import { t as listTar, x } from "tar";
|
|
22
22
|
|
|
23
23
|
import {
|
|
24
24
|
DEBUG_MODE,
|
|
25
|
+
isAllowedHttpHost,
|
|
25
26
|
isDryRun,
|
|
26
27
|
readEnvironmentVariable,
|
|
27
28
|
recordActivity,
|
|
@@ -1081,6 +1082,36 @@ const isSafeImageReference = (fullImageName) =>
|
|
|
1081
1082
|
fullImageName === fullImageName.trim() &&
|
|
1082
1083
|
isImageReference(fullImageName);
|
|
1083
1084
|
|
|
1085
|
+
/**
|
|
1086
|
+
* Whether an image named in a scanned manifest, such as a docker-compose file
|
|
1087
|
+
* or a Kubernetes spec, may be pulled and inventoried. The name comes from the
|
|
1088
|
+
* scanned project, so a local archive path would read files outside the
|
|
1089
|
+
* allowed paths and a registry outside CDXGEN_ALLOWED_HOSTS would bypass the
|
|
1090
|
+
* host allowlist.
|
|
1091
|
+
*
|
|
1092
|
+
* @param {string} fullImageName Image name from the manifest
|
|
1093
|
+
* @param {Object} [imageObj] Result of parseImageName for the same name
|
|
1094
|
+
* @returns {boolean} true when the image may be followed
|
|
1095
|
+
*/
|
|
1096
|
+
export const isFollowableManifestImage = (fullImageName, imageObj) => {
|
|
1097
|
+
if (!isSafeImageReference(fullImageName)) {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
const allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS");
|
|
1101
|
+
if (!allowedHostsEnv) {
|
|
1102
|
+
return true;
|
|
1103
|
+
}
|
|
1104
|
+
const registry =
|
|
1105
|
+
(imageObj || parseImageName(fullImageName)).registry || "docker.io";
|
|
1106
|
+
let registryHost;
|
|
1107
|
+
try {
|
|
1108
|
+
registryHost = new URL(`https://${registry}`).hostname;
|
|
1109
|
+
} catch (_err) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
return isAllowedHttpHost(registryHost, allowedHostsEnv);
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1084
1115
|
/**
|
|
1085
1116
|
* Parse image name
|
|
1086
1117
|
*
|
|
@@ -1586,19 +1617,20 @@ function handleTarWarning(code, message) {
|
|
|
1586
1617
|
}
|
|
1587
1618
|
}
|
|
1588
1619
|
|
|
1589
|
-
// These paths are known to cause extract errors
|
|
1620
|
+
// These paths are known to cause extract errors. CA-store directories
|
|
1621
|
+
// (etc/ssl/certs, etc/pki/ca-trust, ssl/certs, cacerts,
|
|
1622
|
+
// var/lib/ca-certificates) are deliberately NOT excluded any more: their
|
|
1623
|
+
// original extract failures came from the hash-named symlink farms, and
|
|
1624
|
+
// symlinks/hardlinks are excluded by type below, while the regular
|
|
1625
|
+
// certificate files are exactly what a CBOM must inventory.
|
|
1590
1626
|
const EXTRACT_EXCLUDE_PATHS = [
|
|
1591
1627
|
"etc/machine-id",
|
|
1592
1628
|
"etc/gshadow",
|
|
1593
1629
|
"etc/shadow",
|
|
1594
1630
|
"etc/passwd",
|
|
1595
|
-
"etc/ssl/certs",
|
|
1596
|
-
"etc/pki/ca-trust",
|
|
1597
1631
|
"usr/lib/systemd/",
|
|
1598
1632
|
"usr/lib64/libdevmapper.so",
|
|
1599
1633
|
"usr/sbin/",
|
|
1600
|
-
"cacerts",
|
|
1601
|
-
"ssl/certs",
|
|
1602
1634
|
"logs/",
|
|
1603
1635
|
"dev/",
|
|
1604
1636
|
"proc/",
|
|
@@ -1608,7 +1640,6 @@ const EXTRACT_EXCLUDE_PATHS = [
|
|
|
1608
1640
|
"usr/share/man/",
|
|
1609
1641
|
"usr/share/icons/",
|
|
1610
1642
|
"usr/share/i18n/",
|
|
1611
|
-
"var/lib/ca-certificates",
|
|
1612
1643
|
"root/.gnupg",
|
|
1613
1644
|
"root/.dotnet",
|
|
1614
1645
|
"usr/share/licenses/device-mapper-libs",
|
|
@@ -1634,16 +1665,18 @@ const MEDIA_EXTENSIONS = new Set([
|
|
|
1634
1665
|
".eot",
|
|
1635
1666
|
]);
|
|
1636
1667
|
|
|
1637
|
-
// These
|
|
1668
|
+
// These entry types are known to cause extract errors. The names are the
|
|
1669
|
+
// ones node-tar's header parser assigns (types.js in the tar package):
|
|
1670
|
+
// symlinks and hardlinks ("SymbolicLink", "Link") are how CA-store
|
|
1671
|
+
// hash-named entries appear, which is why the CA paths could be un-excluded
|
|
1672
|
+
// above once these were dropped by type.
|
|
1638
1673
|
const EXTRACT_EXCLUDE_TYPES = new Set([
|
|
1639
1674
|
"BlockDevice",
|
|
1640
1675
|
"CharacterDevice",
|
|
1641
1676
|
"FIFO",
|
|
1642
|
-
"
|
|
1643
|
-
"
|
|
1677
|
+
"ContinuationFile",
|
|
1678
|
+
"TapeVolumeHeader",
|
|
1644
1679
|
"SymbolicLink",
|
|
1645
|
-
"RenamedOrSymlinked",
|
|
1646
|
-
"HardLink",
|
|
1647
1680
|
"Link",
|
|
1648
1681
|
]);
|
|
1649
1682
|
|
|
@@ -1994,6 +2027,126 @@ const resolveContainedArchivePath = (tempDir, archiveRef, kind) => {
|
|
|
1994
2027
|
return join(tempDir, archiveRef);
|
|
1995
2028
|
};
|
|
1996
2029
|
|
|
2030
|
+
const WHITEOUT_PREFIX = ".wh.";
|
|
2031
|
+
const WHITEOUT_OPAQUE = ".wh..wh..opq";
|
|
2032
|
+
|
|
2033
|
+
/**
|
|
2034
|
+
* Split an untrusted tar entry path into safe segments. Returns undefined when
|
|
2035
|
+
* the path contains a parent-directory segment.
|
|
2036
|
+
*
|
|
2037
|
+
* @param {string} entryPath Raw entry path from a layer
|
|
2038
|
+
* @returns {string[]|undefined} Path segments, or undefined when unsafe
|
|
2039
|
+
*/
|
|
2040
|
+
const safeEntrySegments = (entryPath) => {
|
|
2041
|
+
const stripped = stripAbsolutePath(String(entryPath).replaceAll("\\", "/"));
|
|
2042
|
+
const parts = stripped.split("/").filter((p) => p && p !== ".");
|
|
2043
|
+
return parts.includes("..") ? undefined : parts;
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Walk down from rootDir through real directories only. A symlink or a
|
|
2048
|
+
* missing component stops the walk, so removals never leave rootDir.
|
|
2049
|
+
*
|
|
2050
|
+
* @param {string} rootDir Extraction root
|
|
2051
|
+
* @param {string[]} parts Directory segments below the root
|
|
2052
|
+
* @returns {string|undefined} The directory path, or undefined
|
|
2053
|
+
*/
|
|
2054
|
+
const resolveRealDir = (rootDir, parts) => {
|
|
2055
|
+
let current = rootDir;
|
|
2056
|
+
for (const part of parts) {
|
|
2057
|
+
current = join(current, part);
|
|
2058
|
+
let st;
|
|
2059
|
+
try {
|
|
2060
|
+
st = lstatSync(current);
|
|
2061
|
+
} catch (_err) {
|
|
2062
|
+
return undefined;
|
|
2063
|
+
}
|
|
2064
|
+
if (!st.isDirectory()) {
|
|
2065
|
+
return undefined;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return current;
|
|
2069
|
+
};
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* Apply the OCI/Docker whiteouts of one layer to the merged extraction
|
|
2073
|
+
* directory. `.wh.<name>` removes <name>, and `.wh..wh..opq` empties the
|
|
2074
|
+
* directory it sits in, both only for content from earlier layers, so call
|
|
2075
|
+
* this before extracting the layer itself. Matches
|
|
2076
|
+
* ci/reconstruct-staged-rootfs.py.
|
|
2077
|
+
* https://github.com/opencontainers/image-spec/blob/v1.1.0/layer.md#whiteouts
|
|
2078
|
+
*
|
|
2079
|
+
* @param {string} layerPath Layer tar (optionally compressed)
|
|
2080
|
+
* @param {string} rootDir Merged extraction directory
|
|
2081
|
+
*/
|
|
2082
|
+
// Applies one whiteout entry of a layer to the merged root.
|
|
2083
|
+
const applyWhiteout = (entryPath, rootDir) => {
|
|
2084
|
+
const parts = safeEntrySegments(entryPath);
|
|
2085
|
+
if (!parts?.length) {
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
const name = parts[parts.length - 1];
|
|
2089
|
+
const parentParts = parts.slice(0, -1);
|
|
2090
|
+
if (name === WHITEOUT_OPAQUE) {
|
|
2091
|
+
const dir = resolveRealDir(rootDir, parentParts);
|
|
2092
|
+
if (!dir) {
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
for (const child of readdirSync(dir)) {
|
|
2096
|
+
safeRmSync(join(dir, child), { recursive: true, force: true });
|
|
2097
|
+
}
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
// Other .wh..wh.* names are AUFS metadata, not deletions.
|
|
2101
|
+
if (name.startsWith(".wh..wh.")) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const target = name.slice(WHITEOUT_PREFIX.length);
|
|
2105
|
+
if (!target || target === "." || target === "..") {
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
const dir = resolveRealDir(rootDir, parentParts);
|
|
2109
|
+
if (dir) {
|
|
2110
|
+
safeRmSync(join(dir, target), { recursive: true, force: true });
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2114
|
+
const applyLayerWhiteouts = async (layerPath, rootDir) => {
|
|
2115
|
+
const whiteouts = [];
|
|
2116
|
+
try {
|
|
2117
|
+
await listTar({
|
|
2118
|
+
file: layerPath,
|
|
2119
|
+
strict: false,
|
|
2120
|
+
onwarn: () => {
|
|
2121
|
+
// extractTar reports damaged layers
|
|
2122
|
+
},
|
|
2123
|
+
onReadEntry: (entry) => {
|
|
2124
|
+
if (
|
|
2125
|
+
basename(String(entry.path).replaceAll("\\", "/")).startsWith(
|
|
2126
|
+
WHITEOUT_PREFIX,
|
|
2127
|
+
)
|
|
2128
|
+
) {
|
|
2129
|
+
whiteouts.push(entry.path);
|
|
2130
|
+
}
|
|
2131
|
+
entry.resume();
|
|
2132
|
+
},
|
|
2133
|
+
});
|
|
2134
|
+
} catch (_err) {
|
|
2135
|
+
// extractTar reports unreadable layers
|
|
2136
|
+
}
|
|
2137
|
+
for (const entryPath of whiteouts) {
|
|
2138
|
+
// One whiteout that cannot be applied (name too long, EACCES) must not
|
|
2139
|
+
// abort the layer's extraction, which would hide everything it adds.
|
|
2140
|
+
try {
|
|
2141
|
+
applyWhiteout(entryPath, rootDir);
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
if (DEBUG_MODE) {
|
|
2144
|
+
console.log(`Skipping whiteout ${entryPath}: ${err.message}`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
|
|
1997
2150
|
/**
|
|
1998
2151
|
* Parse a Docker/containerd manifest file and extract all image layers into a
|
|
1999
2152
|
* single merged directory. Resolves the last layer's config to determine the
|
|
@@ -2074,6 +2227,7 @@ export const extractFromManifest = async (
|
|
|
2074
2227
|
console.log(`Extracting layer ${layer} to ${allLayersExplodedDir}`);
|
|
2075
2228
|
}
|
|
2076
2229
|
try {
|
|
2230
|
+
await applyLayerWhiteouts(containedLayerPath, allLayersExplodedDir);
|
|
2077
2231
|
await extractTar(containedLayerPath, allLayersExplodedDir, options);
|
|
2078
2232
|
} catch (err) {
|
|
2079
2233
|
if (err.code === "TAR_BAD_ARCHIVE") {
|
|
@@ -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
|
+
};
|
package/lib/parsers/largeJson.js
CHANGED
|
@@ -256,8 +256,9 @@ class ByteCursor {
|
|
|
256
256
|
|
|
257
257
|
// The members of the object at the cursor, as { key, start, end } byte ranges
|
|
258
258
|
// of their values, in file order. A repeated key keeps its first position and
|
|
259
|
-
// its last range, which is what JSON.parse does.
|
|
260
|
-
|
|
259
|
+
// its last range, which is what JSON.parse does. With `stopAt`, the scan ends
|
|
260
|
+
// at the first member of that name, leaving the rest of the object unread.
|
|
261
|
+
function scanObjectMembers(cursor, stopAt = undefined) {
|
|
261
262
|
if (cursor.nextToken() !== OPEN_BRACE) {
|
|
262
263
|
cursor.fail("Expected an object");
|
|
263
264
|
}
|
|
@@ -280,6 +281,9 @@ function scanObjectMembers(cursor) {
|
|
|
280
281
|
const start = cursor.offset;
|
|
281
282
|
cursor.skipValue();
|
|
282
283
|
members.set(key, { start, end: cursor.offset });
|
|
284
|
+
if (key === stopAt) {
|
|
285
|
+
return members;
|
|
286
|
+
}
|
|
283
287
|
const delimiter = cursor.nextToken();
|
|
284
288
|
if (delimiter === CLOSE_BRACE) {
|
|
285
289
|
return members;
|
|
@@ -412,6 +416,34 @@ export function indexJsonObject(filePath, options = {}) {
|
|
|
412
416
|
});
|
|
413
417
|
}
|
|
414
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Decode one member of the top-level object in a JSON file, reading the file
|
|
421
|
+
* only up to the end of that member. A member near the start of a large file
|
|
422
|
+
* costs a few bounded reads, however large the rest of the file is; the rest
|
|
423
|
+
* is not checked for well-formedness.
|
|
424
|
+
*
|
|
425
|
+
* @param {string} filePath JSON file whose top-level value is an object
|
|
426
|
+
* @param {string} name Member to decode
|
|
427
|
+
* @param {Object} [options] Batch and chunk size overrides, for tests
|
|
428
|
+
* @returns {*} The decoded value, or undefined when the object has no such member
|
|
429
|
+
* @throws {SyntaxError} When the object is malformed before the member ends
|
|
430
|
+
*/
|
|
431
|
+
export function readJsonMember(filePath, name, options = {}) {
|
|
432
|
+
const resolved = resolveOptions(options);
|
|
433
|
+
return withFile(filePath, (fd) => {
|
|
434
|
+
const cursor = new ByteCursor(
|
|
435
|
+
fd,
|
|
436
|
+
0,
|
|
437
|
+
statSync(filePath).size,
|
|
438
|
+
resolved.chunkBytes,
|
|
439
|
+
);
|
|
440
|
+
const range = scanObjectMembers(cursor, name).get(name);
|
|
441
|
+
return range
|
|
442
|
+
? decodeRange(fd, range.start, range.end, undefined, resolved)
|
|
443
|
+
: undefined;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
415
447
|
/**
|
|
416
448
|
* Decode the JSON value stored in a byte range of a file, without ever holding
|
|
417
449
|
* more than a bounded run of it in one string.
|