@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.
Files changed (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -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 device types are known to cause extract errors
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
- "MultiVolume",
1643
- "TapeVolume",
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
+ };