@cyclonedx/cdxgen 12.3.2 → 12.4.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 (182) hide show
  1. package/README.md +70 -22
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +238 -116
  4. package/bin/convert.js +28 -13
  5. package/bin/hbom.js +490 -0
  6. package/bin/repl.js +580 -29
  7. package/bin/validate.js +34 -4
  8. package/bin/verify.js +40 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/predictive-audit-allowlist.json +11 -0
  13. package/data/queries-darwin.json +12 -1
  14. package/data/queries-win.json +7 -1
  15. package/data/queries.json +39 -2
  16. package/data/rules/ai-agent-governance.yaml +16 -0
  17. package/data/rules/asar-archives.yaml +150 -0
  18. package/data/rules/chrome-extensions.yaml +8 -0
  19. package/data/rules/ci-permissions.yaml +171 -15
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +76 -5
  22. package/data/rules/hbom-compliance.yaml +325 -0
  23. package/data/rules/hbom-performance.yaml +307 -0
  24. package/data/rules/hbom-security.yaml +248 -0
  25. package/data/rules/host-topology.yaml +165 -0
  26. package/data/rules/mcp-servers.yaml +18 -3
  27. package/data/rules/obom-runtime.yaml +907 -22
  28. package/data/rules/package-integrity.yaml +36 -0
  29. package/data/rules/rootfs-hardening.yaml +179 -0
  30. package/data/rules/vscode-extensions.yaml +9 -0
  31. package/lib/audit/index.js +209 -8
  32. package/lib/audit/index.poku.js +332 -0
  33. package/lib/audit/reporters.js +222 -0
  34. package/lib/audit/targets.js +146 -1
  35. package/lib/audit/targets.poku.js +186 -0
  36. package/lib/cli/asar.poku.js +328 -0
  37. package/lib/cli/index.js +647 -127
  38. package/lib/cli/index.poku.js +1905 -187
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/agentFormulationParser.js +6 -2
  41. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  42. package/lib/helpers/analyzer.js +1444 -38
  43. package/lib/helpers/analyzer.poku.js +409 -0
  44. package/lib/helpers/analyzerScope.js +712 -0
  45. package/lib/helpers/asarutils.js +1556 -0
  46. package/lib/helpers/asarutils.poku.js +443 -0
  47. package/lib/helpers/auditCategories.js +12 -0
  48. package/lib/helpers/auditCategories.poku.js +32 -0
  49. package/lib/helpers/cbomutils.js +271 -1
  50. package/lib/helpers/cbomutils.poku.js +248 -5
  51. package/lib/helpers/chromextutils.js +25 -3
  52. package/lib/helpers/chromextutils.poku.js +68 -0
  53. package/lib/helpers/ciParsers/githubActions.js +79 -0
  54. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  55. package/lib/helpers/communityAiConfigParser.js +15 -5
  56. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  57. package/lib/helpers/depsUtils.js +5 -0
  58. package/lib/helpers/depsUtils.poku.js +55 -0
  59. package/lib/helpers/display.js +336 -23
  60. package/lib/helpers/display.poku.js +179 -43
  61. package/lib/helpers/evidenceUtils.js +58 -0
  62. package/lib/helpers/evidenceUtils.poku.js +54 -0
  63. package/lib/helpers/exportUtils.js +9 -0
  64. package/lib/helpers/gtfobins.js +142 -8
  65. package/lib/helpers/gtfobins.poku.js +24 -1
  66. package/lib/helpers/hbom.js +710 -0
  67. package/lib/helpers/hbom.poku.js +496 -0
  68. package/lib/helpers/hbomAnalysis.js +268 -0
  69. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  70. package/lib/helpers/hbomLoader.js +35 -0
  71. package/lib/helpers/hostTopology.js +803 -0
  72. package/lib/helpers/hostTopology.poku.js +363 -0
  73. package/lib/helpers/inventoryStats.js +69 -0
  74. package/lib/helpers/inventoryStats.poku.js +86 -0
  75. package/lib/helpers/lolbas.js +19 -1
  76. package/lib/helpers/lolbas.poku.js +23 -0
  77. package/lib/helpers/mcpConfigParser.js +21 -5
  78. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  79. package/lib/helpers/osqueryTransform.js +47 -0
  80. package/lib/helpers/osqueryTransform.poku.js +47 -0
  81. package/lib/helpers/plugins.js +349 -0
  82. package/lib/helpers/plugins.poku.js +57 -0
  83. package/lib/helpers/propertySanitizer.js +121 -0
  84. package/lib/helpers/protobom.js +156 -45
  85. package/lib/helpers/protobom.poku.js +140 -5
  86. package/lib/helpers/remote/dependency-track.js +36 -3
  87. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  88. package/lib/helpers/source.js +24 -0
  89. package/lib/helpers/source.poku.js +32 -0
  90. package/lib/helpers/utils.js +2454 -198
  91. package/lib/helpers/utils.poku.js +1798 -74
  92. package/lib/managers/binary.e2e.poku.js +367 -0
  93. package/lib/managers/binary.js +2306 -350
  94. package/lib/managers/binary.poku.js +1700 -1
  95. package/lib/managers/docker.js +441 -95
  96. package/lib/managers/docker.poku.js +1479 -14
  97. package/lib/server/server.js +2 -24
  98. package/lib/server/server.poku.js +36 -1
  99. package/lib/stages/postgen/annotator.js +38 -0
  100. package/lib/stages/postgen/annotator.poku.js +107 -1
  101. package/lib/stages/postgen/auditBom.js +121 -18
  102. package/lib/stages/postgen/auditBom.poku.js +2967 -990
  103. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  104. package/lib/stages/postgen/postgen.js +192 -1
  105. package/lib/stages/postgen/postgen.poku.js +321 -0
  106. package/lib/stages/postgen/ruleEngine.js +116 -0
  107. package/lib/stages/pregen/envAudit.js +14 -3
  108. package/package.json +24 -21
  109. package/types/bin/hbom.d.ts +3 -0
  110. package/types/bin/hbom.d.ts.map +1 -0
  111. package/types/bin/repl.d.ts.map +1 -1
  112. package/types/lib/audit/index.d.ts +44 -0
  113. package/types/lib/audit/index.d.ts.map +1 -1
  114. package/types/lib/audit/reporters.d.ts +16 -0
  115. package/types/lib/audit/reporters.d.ts.map +1 -1
  116. package/types/lib/audit/targets.d.ts.map +1 -1
  117. package/types/lib/cli/index.d.ts +16 -0
  118. package/types/lib/cli/index.d.ts.map +1 -1
  119. package/types/lib/evinser/evinser.d.ts +4 -0
  120. package/types/lib/evinser/evinser.d.ts.map +1 -1
  121. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  122. package/types/lib/helpers/analyzer.d.ts +33 -0
  123. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  124. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  125. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  126. package/types/lib/helpers/asarutils.d.ts +34 -0
  127. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  128. package/types/lib/helpers/auditCategories.d.ts +5 -0
  129. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  130. package/types/lib/helpers/cbomutils.d.ts +3 -2
  131. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  132. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  133. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  134. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  135. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  136. package/types/lib/helpers/display.d.ts +1 -0
  137. package/types/lib/helpers/display.d.ts.map +1 -1
  138. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  139. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  141. package/types/lib/helpers/gtfobins.d.ts +8 -0
  142. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  143. package/types/lib/helpers/hbom.d.ts +49 -0
  144. package/types/lib/helpers/hbom.d.ts.map +1 -0
  145. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  146. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  147. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  148. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  149. package/types/lib/helpers/hostTopology.d.ts +12 -0
  150. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  151. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  152. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  153. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  154. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  155. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  156. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  157. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  158. package/types/lib/helpers/plugins.d.ts +58 -0
  159. package/types/lib/helpers/plugins.d.ts.map +1 -0
  160. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  161. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  162. package/types/lib/helpers/protobom.d.ts +3 -4
  163. package/types/lib/helpers/protobom.d.ts.map +1 -1
  164. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  165. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  166. package/types/lib/helpers/source.d.ts.map +1 -1
  167. package/types/lib/helpers/utils.d.ts +74 -8
  168. package/types/lib/helpers/utils.d.ts.map +1 -1
  169. package/types/lib/managers/binary.d.ts +5 -0
  170. package/types/lib/managers/binary.d.ts.map +1 -1
  171. package/types/lib/managers/docker.d.ts +3 -0
  172. package/types/lib/managers/docker.d.ts.map +1 -1
  173. package/types/lib/server/server.d.ts +2 -0
  174. package/types/lib/server/server.d.ts.map +1 -1
  175. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  176. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  177. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  178. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  179. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  180. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  182. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -20,8 +20,12 @@ import {
20
20
  getAllFiles,
21
21
  getTmpDir,
22
22
  isDryRun,
23
+ readEnvironmentVariable,
23
24
  recordActivity,
25
+ recordDecisionActivity,
26
+ recordSensitiveFileRead,
24
27
  safeExistsSync,
28
+ safeExtractArchive,
25
29
  safeMkdirSync,
26
30
  safeMkdtempSync,
27
31
  safeRmSync,
@@ -32,6 +36,13 @@ import { getDirs, getOnlyDirs } from "./containerutils.js";
32
36
 
33
37
  export const isWin = _platform() === "win32";
34
38
  export const DOCKER_HUB_REGISTRY = "docker.io";
39
+ // Docker commonly stores Hub credentials under index.docker.io or
40
+ // registry-1.docker.io while pulls target docker.io.
41
+ const DOCKER_HUB_REGISTRY_ALIASES = new Set([
42
+ DOCKER_HUB_REGISTRY,
43
+ "index.docker.io",
44
+ "registry-1.docker.io",
45
+ ]);
35
46
 
36
47
  /**
37
48
  * Encode a value as base64url (RFC 4648 §5) with padding.
@@ -42,6 +53,212 @@ export const DOCKER_HUB_REGISTRY = "docker.io";
42
53
  const toBase64Url = (value) =>
43
54
  Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
44
55
 
56
+ const normalizeRegistryPath = (registryPath) => {
57
+ if (!registryPath || registryPath === "/") {
58
+ return "";
59
+ }
60
+ let normalizedPath = registryPath.trim();
61
+ if (!normalizedPath.startsWith("/")) {
62
+ normalizedPath = `/${normalizedPath}`;
63
+ }
64
+ while (normalizedPath.endsWith("/")) {
65
+ normalizedPath = normalizedPath.slice(0, -1);
66
+ }
67
+ const lowerCasePath = normalizedPath.toLowerCase();
68
+ if (lowerCasePath.endsWith("/v1") || lowerCasePath.endsWith("/v2")) {
69
+ normalizedPath = normalizedPath.slice(0, -3);
70
+ }
71
+ return normalizedPath === "/" ? "" : normalizedPath;
72
+ };
73
+
74
+ const buildRegistryAuthority = (hostname, port) => {
75
+ if (!hostname) {
76
+ return undefined;
77
+ }
78
+ hostname = hostname.toLowerCase();
79
+ if (hostname.includes(":") && !hostname.startsWith("[")) {
80
+ hostname = `[${hostname}]`;
81
+ }
82
+ if (port) {
83
+ return `${hostname}:${port}`;
84
+ }
85
+ return hostname;
86
+ };
87
+
88
+ const parseRawRegistryAuthority = (authority) => {
89
+ if (!authority?.trim()) {
90
+ return undefined;
91
+ }
92
+ authority = authority.trim();
93
+ if (authority.startsWith("[")) {
94
+ const closingBracketIndex = authority.indexOf("]");
95
+ if (closingBracketIndex === -1) {
96
+ return undefined;
97
+ }
98
+ const hostname = authority.slice(0, closingBracketIndex + 1);
99
+ const portSuffix = authority.slice(closingBracketIndex + 1);
100
+ if (!portSuffix) {
101
+ return buildRegistryAuthority(hostname);
102
+ }
103
+ if (!/^:\d+$/.test(portSuffix)) {
104
+ return undefined;
105
+ }
106
+ return buildRegistryAuthority(hostname, portSuffix.slice(1));
107
+ }
108
+ const colonIndex = authority.lastIndexOf(":");
109
+ if (colonIndex > -1 && authority.indexOf(":") === colonIndex) {
110
+ const portCandidate = authority.slice(colonIndex + 1);
111
+ if (/^\d+$/.test(portCandidate)) {
112
+ return buildRegistryAuthority(
113
+ authority.slice(0, colonIndex),
114
+ portCandidate,
115
+ );
116
+ }
117
+ }
118
+ return buildRegistryAuthority(authority);
119
+ };
120
+
121
+ const parseRegistryReference = (registry) => {
122
+ if (!registry?.trim()) {
123
+ return undefined;
124
+ }
125
+ registry = registry.trim();
126
+ if (registry.includes("://")) {
127
+ if (!URL.canParse(registry)) {
128
+ return undefined;
129
+ }
130
+ const registryUrl = new URL(registry);
131
+ const authoritySource = registry
132
+ .slice(registry.indexOf("://") + 3)
133
+ .split("/")[0];
134
+ return {
135
+ authority: parseRawRegistryAuthority(authoritySource),
136
+ path: normalizeRegistryPath(registryUrl.pathname),
137
+ };
138
+ }
139
+ const slashIndex = registry.indexOf("/");
140
+ const authority =
141
+ slashIndex === -1 ? registry : registry.slice(0, slashIndex);
142
+ const registryPath = slashIndex === -1 ? "" : registry.slice(slashIndex);
143
+ if (!authority) {
144
+ return undefined;
145
+ }
146
+ try {
147
+ // Raw registry references such as host:port are not absolute URLs, so we
148
+ // add an https scheme only to parse the authority and optional port.
149
+ return {
150
+ authority: parseRawRegistryAuthority(authority),
151
+ path: normalizeRegistryPath(registryPath),
152
+ };
153
+ } catch (_err) {
154
+ return undefined;
155
+ }
156
+ };
157
+
158
+ const looksLikeImageReference = (value) => {
159
+ if (typeof value !== "string" || !value.trim()) {
160
+ return false;
161
+ }
162
+ value = value.trim();
163
+ if (value.includes("://")) {
164
+ return false;
165
+ }
166
+ if (!value.includes("/")) {
167
+ if (value.includes(":")) {
168
+ const tagOrPortSuffix = value.slice(value.lastIndexOf(":") + 1);
169
+ if (!/^\d+$/.test(tagOrPortSuffix)) {
170
+ return true;
171
+ }
172
+ return !parseRegistryReference(value)?.authority;
173
+ }
174
+ return !(
175
+ value.includes(".") ||
176
+ value === "localhost" ||
177
+ value.startsWith("[")
178
+ );
179
+ }
180
+ const firstSegment = value.split("/")[0];
181
+ return !(
182
+ parseRegistryReference(firstSegment)?.authority &&
183
+ (firstSegment.includes(".") ||
184
+ firstSegment.includes(":") ||
185
+ firstSegment === "localhost" ||
186
+ firstSegment.startsWith("["))
187
+ );
188
+ };
189
+
190
+ const resolveRequestedRegistryRef = (forRegistry, requestedRegistryRef) => {
191
+ const fallbackRegistry =
192
+ forRegistry || process.env.DOCKER_SERVER_ADDRESS || DOCKER_HUB_REGISTRY;
193
+ if (
194
+ typeof requestedRegistryRef !== "string" ||
195
+ !requestedRegistryRef.trim()
196
+ ) {
197
+ return fallbackRegistry;
198
+ }
199
+ requestedRegistryRef = requestedRegistryRef.trim();
200
+ if (requestedRegistryRef.includes("://")) {
201
+ return requestedRegistryRef;
202
+ }
203
+ return looksLikeImageReference(requestedRegistryRef)
204
+ ? fallbackRegistry
205
+ : requestedRegistryRef;
206
+ };
207
+
208
+ const extractRequestedRegistryRefFromPath = (path, forRegistry) => {
209
+ if (!path?.includes("?")) {
210
+ return resolveRequestedRegistryRef(forRegistry, forRegistry);
211
+ }
212
+ const queryString = path.slice(path.indexOf("?") + 1);
213
+ const requestedImageRef = new URLSearchParams(queryString).get("fromImage");
214
+ return resolveRequestedRegistryRef(
215
+ forRegistry,
216
+ requestedImageRef || forRegistry,
217
+ );
218
+ };
219
+
220
+ const normalizeRegistryReference = (registry) => {
221
+ const parsedRegistry = parseRegistryReference(registry);
222
+ if (!parsedRegistry?.authority) {
223
+ return undefined;
224
+ }
225
+ return parsedRegistry.path
226
+ ? `${parsedRegistry.authority}${parsedRegistry.path}`
227
+ : parsedRegistry.authority;
228
+ };
229
+
230
+ const registriesMatch = (configuredRegistry, requestedRegistry) => {
231
+ if (!requestedRegistry) {
232
+ return false;
233
+ }
234
+ const normalizedConfiguredRegistry =
235
+ parseRegistryReference(configuredRegistry);
236
+ const normalizedRequestedRegistry = parseRegistryReference(requestedRegistry);
237
+ if (
238
+ !normalizedConfiguredRegistry?.authority ||
239
+ !normalizedRequestedRegistry?.authority
240
+ ) {
241
+ return false;
242
+ }
243
+ const hostMatches =
244
+ normalizedConfiguredRegistry.authority ===
245
+ normalizedRequestedRegistry.authority ||
246
+ (DOCKER_HUB_REGISTRY_ALIASES.has(normalizedConfiguredRegistry.authority) &&
247
+ DOCKER_HUB_REGISTRY_ALIASES.has(normalizedRequestedRegistry.authority));
248
+ if (!hostMatches) {
249
+ return false;
250
+ }
251
+ if (!normalizedConfiguredRegistry.path) {
252
+ return true;
253
+ }
254
+ return (
255
+ normalizedConfiguredRegistry.path === normalizedRequestedRegistry.path ||
256
+ normalizedRequestedRegistry.path.startsWith(
257
+ `${normalizedConfiguredRegistry.path}/`,
258
+ )
259
+ );
260
+ };
261
+
45
262
  // Should we extract the tar image in non-strict mode
46
263
  const NON_STRICT_TAR_EXTRACT = ["true", "1"].includes(
47
264
  process?.env?.NON_STRICT_TAR_EXTRACT,
@@ -185,19 +402,52 @@ const REQUEST_TIMEOUT_SECS = 60000;
185
402
  *
186
403
  * @param {string} [forRegistry] Registry hostname (e.g. "registry-1.docker.io").
187
404
  * Defaults to DOCKER_SERVER_ADDRESS env var or "docker.io".
405
+ * @param {string} [requestedRegistryRef] Requested registry/image reference used
406
+ * to scope config.json auth matching. Unqualified images default to Docker Hub.
188
407
  * @returns {Object} Options object suitable for passing to `got`
189
408
  */
190
- const getDefaultOptions = (forRegistry) => {
409
+ const getDefaultOptions = (forRegistry, requestedRegistryRef = forRegistry) => {
191
410
  let authTokenSet = false;
192
- if (!forRegistry) {
193
- forRegistry = process.env.DOCKER_SERVER_ADDRESS ?? DOCKER_HUB_REGISTRY;
194
- }
195
- if (forRegistry) {
196
- forRegistry = forRegistry.replace("http://", "").replace("https://", "");
197
- if (forRegistry.includes("/")) {
198
- forRegistry = forRegistry.split("/")[0];
411
+ const credentialSourceEvaluations = [];
412
+ let selectedCredentialSource;
413
+ const noteCredentialSource = (source, outcome, detail = undefined) => {
414
+ credentialSourceEvaluations.push({
415
+ detail,
416
+ outcome,
417
+ source,
418
+ });
419
+ if (outcome === "selected") {
420
+ selectedCredentialSource = source;
199
421
  }
422
+ };
423
+ const dockerServerAddress = readEnvironmentVariable("DOCKER_SERVER_ADDRESS");
424
+ const dockerConfig = readEnvironmentVariable("DOCKER_CONFIG");
425
+ const dockerAuthConfig = readEnvironmentVariable("DOCKER_AUTH_CONFIG", {
426
+ sensitive: true,
427
+ });
428
+ const dockerUser = readEnvironmentVariable("DOCKER_USER", {
429
+ sensitive: true,
430
+ });
431
+ const dockerPassword = readEnvironmentVariable("DOCKER_PASSWORD", {
432
+ sensitive: true,
433
+ });
434
+ const dockerEmail = readEnvironmentVariable("DOCKER_EMAIL", {
435
+ sensitive: true,
436
+ });
437
+ if (!forRegistry) {
438
+ forRegistry = dockerServerAddress ?? DOCKER_HUB_REGISTRY;
200
439
  }
440
+ requestedRegistryRef = resolveRequestedRegistryRef(
441
+ forRegistry,
442
+ requestedRegistryRef,
443
+ );
444
+ const normalizedForRegistry =
445
+ parseRegistryReference(forRegistry)?.authority ?? forRegistry;
446
+ const authDecisionTarget =
447
+ requestedRegistryRef ||
448
+ normalizedForRegistry ||
449
+ forRegistry ||
450
+ DOCKER_HUB_REGISTRY;
201
451
  const opts = {
202
452
  enableUnixSockets: true,
203
453
  throwHttpErrors: true,
@@ -214,47 +464,64 @@ const getDefaultOptions = (forRegistry) => {
214
464
  hooks: { beforeError: [] },
215
465
  mutableDefaults: true,
216
466
  };
217
- const DOCKER_CONFIG = process.env.DOCKER_CONFIG || join(homedir(), ".docker");
467
+ const DOCKER_CONFIG = dockerConfig || join(homedir(), ".docker");
468
+ const dockerConfigFile = join(DOCKER_CONFIG, "config.json");
218
469
  // Support for private registry
219
- if (process.env.DOCKER_AUTH_CONFIG) {
470
+ if (dockerAuthConfig) {
220
471
  opts.headers = {
221
- "X-Registry-Auth": process.env.DOCKER_AUTH_CONFIG,
472
+ "X-Registry-Auth": dockerAuthConfig,
222
473
  };
223
474
  authTokenSet = true;
475
+ noteCredentialSource("DOCKER_AUTH_CONFIG", "selected");
476
+ } else {
477
+ noteCredentialSource("DOCKER_AUTH_CONFIG", "skipped", "not set");
224
478
  }
225
479
  if (
226
480
  !authTokenSet &&
227
- process.env.DOCKER_USER &&
228
- process.env.DOCKER_PASSWORD &&
229
- process.env.DOCKER_EMAIL &&
230
- forRegistry
481
+ dockerUser &&
482
+ dockerPassword &&
483
+ dockerEmail &&
484
+ normalizedForRegistry
231
485
  ) {
232
486
  const authPayload = {
233
- username: process.env.DOCKER_USER,
234
- email: process.env.DOCKER_EMAIL,
235
- serveraddress: forRegistry,
487
+ username: dockerUser,
488
+ email: dockerEmail,
489
+ serveraddress: normalizedForRegistry,
236
490
  };
237
- if (process.env.DOCKER_USER === "<token>") {
238
- authPayload.IdentityToken = process.env.DOCKER_PASSWORD;
491
+ if (dockerUser === "<token>") {
492
+ authPayload.IdentityToken = dockerPassword;
239
493
  } else {
240
- authPayload.password = process.env.DOCKER_PASSWORD;
494
+ authPayload.password = dockerPassword;
241
495
  }
242
496
  opts.headers = {
243
497
  "X-Registry-Auth": toBase64Url(JSON.stringify(authPayload)),
244
498
  };
245
- }
246
- if (!authTokenSet && safeExistsSync(join(DOCKER_CONFIG, "config.json"))) {
247
- const configData = readFileSync(
248
- join(DOCKER_CONFIG, "config.json"),
249
- "utf-8",
499
+ authTokenSet = true;
500
+ noteCredentialSource(
501
+ "DOCKER_USER/DOCKER_PASSWORD/DOCKER_EMAIL",
502
+ "selected",
250
503
  );
504
+ } else if (!authTokenSet) {
505
+ noteCredentialSource(
506
+ "DOCKER_USER/DOCKER_PASSWORD/DOCKER_EMAIL",
507
+ "skipped",
508
+ dockerUser || dockerPassword || dockerEmail
509
+ ? "incomplete environment credentials"
510
+ : "not set",
511
+ );
512
+ }
513
+ if (!authTokenSet && safeExistsSync(dockerConfigFile)) {
514
+ const configData = readFileSync(dockerConfigFile, "utf-8");
515
+ recordSensitiveFileRead(dockerConfigFile, {
516
+ label: "Docker credential file",
517
+ });
251
518
  if (configData) {
252
519
  try {
253
520
  const configJson = JSON.parse(configData);
254
521
  if (configJson.auths) {
255
522
  // Check if there are hardcoded tokens
256
523
  for (const serverAddress of Object.keys(configJson.auths)) {
257
- if (forRegistry && !serverAddress.includes(forRegistry)) {
524
+ if (!registriesMatch(serverAddress, requestedRegistryRef)) {
258
525
  continue;
259
526
  }
260
527
  if (configJson.auths[serverAddress].auth) {
@@ -274,6 +541,11 @@ const getDefaultOptions = (forRegistry) => {
274
541
  "X-Registry-Auth": toBase64Url(JSON.stringify(authPayload)),
275
542
  };
276
543
  authTokenSet = true;
544
+ noteCredentialSource(
545
+ "docker-config-auth",
546
+ "selected",
547
+ serverAddress,
548
+ );
277
549
  break;
278
550
  }
279
551
  if (configJson.credsStore) {
@@ -286,6 +558,11 @@ const getDefaultOptions = (forRegistry) => {
286
558
  "X-Registry-Auth": helperAuthToken,
287
559
  };
288
560
  authTokenSet = true;
561
+ noteCredentialSource(
562
+ `docker-credential-helper:${configJson.credsStore}`,
563
+ "selected",
564
+ serverAddress,
565
+ );
289
566
  break;
290
567
  }
291
568
  }
@@ -293,7 +570,7 @@ const getDefaultOptions = (forRegistry) => {
293
570
  } else if (configJson.credHelpers) {
294
571
  // Support for credential helpers
295
572
  for (const serverAddress of Object.keys(configJson.credHelpers)) {
296
- if (forRegistry && !serverAddress.includes(forRegistry)) {
573
+ if (!registriesMatch(serverAddress, requestedRegistryRef)) {
297
574
  continue;
298
575
  }
299
576
  if (configJson.credHelpers[serverAddress]) {
@@ -306,22 +583,40 @@ const getDefaultOptions = (forRegistry) => {
306
583
  "X-Registry-Auth": helperAuthToken,
307
584
  };
308
585
  authTokenSet = true;
586
+ noteCredentialSource(
587
+ `docker-credential-helper:${configJson.credHelpers[serverAddress]}`,
588
+ "selected",
589
+ serverAddress,
590
+ );
309
591
  break;
310
592
  }
311
593
  }
312
594
  }
313
595
  }
596
+ if (!authTokenSet) {
597
+ noteCredentialSource(
598
+ "docker-config",
599
+ "skipped",
600
+ "no matching config.json auth entry",
601
+ );
602
+ }
314
603
  } catch (_err) {
315
604
  // pass
605
+ noteCredentialSource("docker-config", "skipped", "config parse failed");
316
606
  }
317
607
  }
608
+ } else if (!authTokenSet) {
609
+ noteCredentialSource("docker-config", "skipped", "config.json not found");
318
610
  }
319
611
  const userInfo = _userInfo();
320
612
  opts.podmanPrefixUrl = isWin ? "" : "http://unix:/run/podman/podman.sock:";
321
613
  opts.podmanRootlessPrefixUrl = isWin
322
614
  ? ""
323
615
  : `http://unix:/run/user/${userInfo.uid}/podman/podman.sock:`;
324
- if (!process.env.DOCKER_HOST) {
616
+ const dockerHost = readEnvironmentVariable("DOCKER_HOST");
617
+ const dockerCertPath = readEnvironmentVariable("DOCKER_CERT_PATH");
618
+ const dockerTlsVerify = readEnvironmentVariable("DOCKER_TLS_VERIFY");
619
+ if (!dockerHost) {
325
620
  if (isPodman) {
326
621
  opts.prefixUrl = isPodmanRootless
327
622
  ? opts.podmanRootlessPrefixUrl
@@ -345,7 +640,7 @@ const getDefaultOptions = (forRegistry) => {
345
640
  }
346
641
  }
347
642
  } else {
348
- let hostStr = process.env.DOCKER_HOST;
643
+ let hostStr = dockerHost;
349
644
  if (hostStr.startsWith("unix:///")) {
350
645
  hostStr = hostStr.replace("unix:///", "http://unix:/");
351
646
  if (hostStr.includes("docker.sock")) {
@@ -354,29 +649,54 @@ const getDefaultOptions = (forRegistry) => {
354
649
  }
355
650
  }
356
651
  opts.prefixUrl = hostStr;
357
- if (process.env.DOCKER_CERT_PATH) {
652
+ if (dockerCertPath) {
653
+ const dockerCertFile = join(dockerCertPath, "cert.pem");
654
+ const dockerKeyFile = join(dockerCertPath, "key.pem");
655
+ const dockerCertificate = readFileSync(dockerCertFile, "utf8");
656
+ recordSensitiveFileRead(dockerCertFile, {
657
+ label: "Docker client certificate",
658
+ });
659
+ const dockerKey = readFileSync(dockerKeyFile, "utf8");
660
+ recordSensitiveFileRead(dockerKeyFile, {
661
+ label: "Docker client private key",
662
+ });
358
663
  opts.https = {
359
- certificate: readFileSync(
360
- join(process.env.DOCKER_CERT_PATH, "cert.pem"),
361
- "utf8",
362
- ),
363
- key: readFileSync(
364
- join(process.env.DOCKER_CERT_PATH, "key.pem"),
365
- "utf8",
366
- ),
664
+ certificate: dockerCertificate,
665
+ key: dockerKey,
367
666
  };
368
667
  // Disable tls on empty values
369
668
  // From the docker docs: Setting the DOCKER_TLS_VERIFY environment variable to any value other than the empty string is equivalent to setting the --tlsverify flag
370
- if (
371
- process.env.DOCKER_TLS_VERIFY &&
372
- process.env.DOCKER_TLS_VERIFY === ""
373
- ) {
669
+ if (dockerTlsVerify === "") {
374
670
  opts.https.rejectUnauthorized = false;
375
671
  console.log("TLS Verification disabled for", hostStr);
376
672
  }
377
673
  }
378
674
  }
379
675
 
676
+ if (!selectedCredentialSource) {
677
+ noteCredentialSource(
678
+ "anonymous",
679
+ "selected",
680
+ "no credential source resolved",
681
+ );
682
+ }
683
+ const skippedSources = credentialSourceEvaluations
684
+ .filter((entry) => entry.outcome !== "selected")
685
+ .map((entry) =>
686
+ entry.detail ? `${entry.source} (${entry.detail})` : entry.source,
687
+ );
688
+ recordDecisionActivity(`docker-auth:${authDecisionTarget}`, {
689
+ metadata: {
690
+ decisionType: "credential-source-selection",
691
+ evaluatedSources: credentialSourceEvaluations.map(
692
+ ({ detail, outcome, source }) =>
693
+ detail ? `${source}:${outcome}:${detail}` : `${source}:${outcome}`,
694
+ ),
695
+ selectedSource: selectedCredentialSource,
696
+ },
697
+ reason: `Selected Docker auth source '${selectedCredentialSource}' for ${authDecisionTarget}. Skipped: ${skippedSources.length ? skippedSources.join(", ") : "none"}.`,
698
+ });
699
+
380
700
  return opts;
381
701
  };
382
702
 
@@ -395,7 +715,20 @@ const getDefaultOptions = (forRegistry) => {
395
715
  * daemon base URL, or `undefined`
396
716
  */
397
717
  export const getConnection = async (options, forRegistry) => {
718
+ if (isContainerd || isNerdctl) {
719
+ return undefined;
720
+ }
398
721
  if (isDryRun) {
722
+ try {
723
+ getDefaultOptions(forRegistry);
724
+ } catch (error) {
725
+ recordActivity({
726
+ kind: "read",
727
+ reason: `Dry run mode failed while tracing Docker credential inputs: ${error.message}`,
728
+ status: "failed",
729
+ target: error?.path || forRegistry || "container-daemon",
730
+ });
731
+ }
399
732
  recordActivity({
400
733
  kind: "network",
401
734
  reason:
@@ -405,9 +738,6 @@ export const getConnection = async (options, forRegistry) => {
405
738
  });
406
739
  return undefined;
407
740
  }
408
- if (isContainerd || isNerdctl) {
409
- return undefined;
410
- }
411
741
  if (!dockerConn) {
412
742
  const defaultOptions = getDefaultOptions(forRegistry);
413
743
  const podmanRootlessUrl = defaultOptions.podmanRootlessPrefixUrl;
@@ -535,7 +865,10 @@ export const makeRequest = async (path, method, forRegistry) => {
535
865
  }
536
866
  // Use the client's prefixUrl (set correctly by getConnection for
537
867
  // docker/podman). Only pass per-request auth headers and method options.
538
- const defaultOptions = getDefaultOptions(forRegistry);
868
+ const defaultOptions = getDefaultOptions(
869
+ forRegistry,
870
+ extractRequestedRegistryRefFromPath(path, forRegistry),
871
+ );
539
872
  const opts = {
540
873
  responseType: method === "GET" ? "json" : "buffer",
541
874
  resolveBodyOnly: true,
@@ -858,13 +1191,17 @@ export const getImage = async (fullImageName) => {
858
1191
  return localData;
859
1192
  };
860
1193
 
1194
+ /**
1195
+ * @typedef {{ path: string }} TarReadEntryLike
1196
+ */
1197
+
861
1198
  /**
862
1199
  * Warnings such as TAR_ENTRY_INFO are treated as errors in strict mode. While this is mostly desired, we can relax this
863
1200
  * requirement for one particular warning related to absolute paths.
864
1201
  * This callback function checks for absolute paths in the entry read from the archive and strips them using a custom
865
1202
  * method.
866
1203
  *
867
- * @param entry {tar.ReadEntry} ReadEntry object from node-tar
1204
+ * @param {TarReadEntryLike} entry ReadEntry object from node-tar
868
1205
  */
869
1206
  function handleAbsolutePath(entry) {
870
1207
  if (entry.path === "/" || win32.isAbsolute(entry.path)) {
@@ -981,34 +1318,36 @@ const EXTRACT_EXCLUDE_TYPES = new Set([
981
1318
  * empty or a non-fatal error was encountered
982
1319
  */
983
1320
  export const extractTar = async (fullImageName, dir, options) => {
984
- if (isDryRun) {
985
- recordActivity({
986
- kind: "untar",
987
- reason:
988
- "Dry run mode blocks untar and layer extraction operations because they create files on disk.",
989
- status: "blocked",
990
- target: `${fullImageName} -> ${dir}`,
991
- });
992
- return false;
993
- }
994
1321
  try {
995
- await stream.pipeline(
996
- createReadStream(fullImageName),
997
- x({
998
- sync: false,
999
- preserveOwner: false,
1000
- noMtime: true,
1001
- noChmod: true,
1002
- strict: !NON_STRICT_TAR_EXTRACT,
1003
- C: dir,
1004
- portable: true,
1005
- unlink: true,
1006
- onwarn: handleTarWarning,
1007
- onReadEntry: handleAbsolutePath,
1008
- filter: tarFilter,
1009
- }),
1322
+ return await safeExtractArchive(
1323
+ fullImageName,
1324
+ dir,
1325
+ async () =>
1326
+ await stream.pipeline(
1327
+ createReadStream(fullImageName),
1328
+ x({
1329
+ sync: false,
1330
+ preserveOwner: false,
1331
+ noMtime: true,
1332
+ noChmod: true,
1333
+ strict: !NON_STRICT_TAR_EXTRACT,
1334
+ C: dir,
1335
+ portable: true,
1336
+ unlink: true,
1337
+ onwarn: handleTarWarning,
1338
+ onReadEntry: handleAbsolutePath,
1339
+ filter: tarFilter,
1340
+ }),
1341
+ ),
1342
+ "untar",
1343
+ {
1344
+ blockedReason:
1345
+ "Dry run mode blocks untar and layer extraction operations because they create files on disk.",
1346
+ metadata: {
1347
+ archiveFormat: "tar",
1348
+ },
1349
+ },
1010
1350
  );
1011
- return true;
1012
1351
  } catch (err) {
1013
1352
  if (err.code === "EPERM" && err.syscall === "symlink") {
1014
1353
  console.log(
@@ -1382,7 +1721,22 @@ export const extractFromManifest = async (
1382
1721
  * Returns the location of the layers with additional packages related metadata
1383
1722
  */
1384
1723
  export const exportImage = async (fullImageName, options) => {
1724
+ // Safely ignore local directories
1725
+ if (
1726
+ !fullImageName ||
1727
+ fullImageName === "." ||
1728
+ safeExistsSync(resolve(fullImageName))
1729
+ ) {
1730
+ return undefined;
1731
+ }
1385
1732
  if (isDryRun) {
1733
+ const imageDetails = parseImageName(fullImageName);
1734
+ const requestedRegistryRef = imageDetails.registry
1735
+ ? imageDetails.repo
1736
+ ? `${imageDetails.registry}/${imageDetails.repo}`
1737
+ : imageDetails.registry
1738
+ : DOCKER_HUB_REGISTRY;
1739
+ await getConnection({}, requestedRegistryRef);
1386
1740
  recordActivity({
1387
1741
  kind: "container",
1388
1742
  reason:
@@ -1392,14 +1746,6 @@ export const exportImage = async (fullImageName, options) => {
1392
1746
  });
1393
1747
  return undefined;
1394
1748
  }
1395
- // Safely ignore local directories
1396
- if (
1397
- !fullImageName ||
1398
- fullImageName === "." ||
1399
- safeExistsSync(resolve(fullImageName))
1400
- ) {
1401
- return undefined;
1402
- }
1403
1749
  // Try to get the data locally first
1404
1750
  const localData = await getImage(fullImageName);
1405
1751
  if (!localData) {
@@ -1694,8 +2040,9 @@ export const removeImage = async (fullImageName, force = false) => {
1694
2040
  * if the helper is unavailable or returns an error
1695
2041
  */
1696
2042
  export const getCredsFromHelper = (exeSuffix, serverAddress) => {
1697
- if (registry_auth_keys[serverAddress]) {
1698
- return registry_auth_keys[serverAddress];
2043
+ const cacheKey = `${exeSuffix}:${normalizeRegistryReference(serverAddress) ?? serverAddress}`;
2044
+ if (registry_auth_keys[cacheKey]) {
2045
+ return registry_auth_keys[cacheKey];
1699
2046
  }
1700
2047
  let credHelperExe = `docker-credential-${exeSuffix}`;
1701
2048
  if (isWin) {
@@ -1711,22 +2058,21 @@ export const getCredsFromHelper = (exeSuffix, serverAddress) => {
1711
2058
  } else if (result.stdout) {
1712
2059
  const cmdOutput = Buffer.from(result.stdout).toString();
1713
2060
  try {
2061
+ const dockerUser = readEnvironmentVariable("DOCKER_USER", {
2062
+ sensitive: true,
2063
+ });
2064
+ const dockerPassword = readEnvironmentVariable("DOCKER_PASSWORD", {
2065
+ sensitive: true,
2066
+ });
1714
2067
  const authPayload = JSON.parse(cmdOutput);
1715
2068
  const fixedAuthPayload = {
1716
- username:
1717
- authPayload.username ||
1718
- authPayload.Username ||
1719
- process.env.DOCKER_USER,
1720
- password:
1721
- authPayload.password ||
1722
- authPayload.Secret ||
1723
- process.env.DOCKER_PASSWORD,
1724
- email:
1725
- authPayload.email || authPayload.username || process.env.DOCKER_USER,
2069
+ username: authPayload.username || authPayload.Username || dockerUser,
2070
+ password: authPayload.password || authPayload.Secret || dockerPassword,
2071
+ email: authPayload.email || authPayload.username || dockerUser,
1726
2072
  serveraddress: serverAddress,
1727
2073
  };
1728
2074
  const authKey = toBase64Url(JSON.stringify(fixedAuthPayload));
1729
- registry_auth_keys[serverAddress] = authKey;
2075
+ registry_auth_keys[cacheKey] = authKey;
1730
2076
  return authKey;
1731
2077
  } catch (_err) {
1732
2078
  return undefined;