@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.
Files changed (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -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 device types are known to cause extract errors
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
- "MultiVolume",
1643
- "TapeVolume",
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
+ };
@@ -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
- function scanObjectMembers(cursor) {
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.