@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
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { readFileSync } from "node:fs";
2
+ import { readdirSync, readFileSync } from "node:fs";
3
3
  import {
4
4
  sep as _sep,
5
5
  basename,
@@ -13,11 +13,17 @@ import {
13
13
  import { build, Purl } from "@cdxgen/cdx-purl";
14
14
  import { parse as _load } from "yaml";
15
15
 
16
- import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
16
+ import {
17
+ DEBUG_MODE,
18
+ hasDangerousUnicode,
19
+ readEnvironmentVariable,
20
+ } from "../core/activity.js";
21
+ import { readRegularTextFile } from "../core/boundedFile.js";
17
22
  import { recordDegradation } from "../core/buildLedger.js";
18
23
  import { deferFailOnError } from "../core/deferredExit.js";
19
24
  import { shouldFetchLicense } from "../core/env.js";
20
25
  import {
26
+ getTmpDir,
21
27
  safeExistsSync,
22
28
  safeSpawnSync,
23
29
  safeWriteSync,
@@ -25,6 +31,10 @@ import {
25
31
  } from "../core/fs.js";
26
32
  import { thoughtLog } from "../core/logger.js";
27
33
  import { isWin } from "../core/paths.js";
34
+ import {
35
+ isProjectToolingAllowed,
36
+ reportProjectToolingSkipped,
37
+ } from "../core/projectTooling.js";
28
38
  import { registerRunReset } from "../core/runState.js";
29
39
  import { mesonWrapDB } from "../core/state.js";
30
40
  import { isGitHubUrl } from "../core/urls.js";
@@ -43,7 +53,13 @@ import {
43
53
  tryBuildPurl,
44
54
  } from "../inventory/purl.js";
45
55
  import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
46
- import { cmakeFetchDependencies } from "../parsers/cmakeLists.js";
56
+ import {
57
+ cmakeFetchDependencies,
58
+ cmakeSetVariables,
59
+ expandCmakeVariables,
60
+ parseCmakeCommands,
61
+ } from "../parsers/cmakeLists.js";
62
+ import { isValidIdnEmail } from "../parsers/email.js";
47
63
  import { submodulePurlCoordinates } from "../parsers/gitmodules.js";
48
64
  import { getDartMetadata } from "./ecosystems.js";
49
65
  import { parsePkgJson } from "./parsers-js.js";
@@ -1555,11 +1571,146 @@ export function parseComposerJson(composerJsonFile) {
1555
1571
  return { rootRequires, moduleParent };
1556
1572
  }
1557
1573
 
1574
+ // Composer's platform packages stand for what the runtime provides, and none
1575
+ // is installable, so none has a purl or an edge in the dependency graph. These
1576
+ // are the ones that describe PHP: PHP and its build flavours, HHVM, and the
1577
+ // extensions and system libraries PHP was built with. composer,
1578
+ // composer-plugin-api and composer-runtime-api are platform packages too, but
1579
+ // they describe Composer, so they are left out.
1580
+ // https://github.com/composer/composer/blob/2.10.3/src/Composer/Repository/PlatformRepository.php#L39
1581
+ const COMPOSER_PLATFORM_NAMES = new Set([
1582
+ "php",
1583
+ "php-64bit",
1584
+ "php-ipv6",
1585
+ "php-zts",
1586
+ "php-debug",
1587
+ "hhvm",
1588
+ ]);
1589
+
1590
+ function isComposerPlatformRequirement(name) {
1591
+ return (
1592
+ COMPOSER_PLATFORM_NAMES.has(name) ||
1593
+ name.startsWith("ext-") ||
1594
+ name.startsWith("lib-")
1595
+ );
1596
+ }
1597
+
1598
+ // What the lock records about the platform is a constraint, or the version a
1599
+ // project told Composer to assume. It is a fact about the project's
1600
+ // requirements, never the version of anything that runs, so it is kept as a
1601
+ // property and never becomes a component.
1602
+ const COMPOSER_PLATFORM_SECTIONS = [
1603
+ ["platform", "cdx:composer:platform:"],
1604
+ ["platform-dev", "cdx:composer:platformDev:"],
1605
+ ["platform-overrides", "cdx:composer:platformOverride:"],
1606
+ ];
1607
+ const COMPOSER_REQUIRE_PREFIX = "cdx:composer:require:";
1608
+ const MAX_COMPOSER_PLATFORM_VALUE_LENGTH = 256;
1609
+ // A real lock names a few dozen platform packages per map at most; the cap
1610
+ // keeps a crafted one from turning each map into thousands of properties.
1611
+ const MAX_COMPOSER_PLATFORM_ENTRIES = 128;
1612
+
1613
+ function isComposerPlatformKey(key) {
1614
+ return /^[A-Za-z0-9._-]{1,100}$/.test(key);
1615
+ }
1616
+
1617
+ /**
1618
+ * The value a platform entry carries as a property: a constraint or version
1619
+ * string, or `false`, which `config.platform` uses to disable a package:
1620
+ * https://github.com/composer/composer/blob/2.10.3/src/Composer/Repository/PlatformRepository.php#L125
1621
+ *
1622
+ * @param {unknown} value value from the lock
1623
+ * @returns {string|undefined} property value, or undefined when it is not one
1624
+ */
1625
+ function composerPlatformValue(value) {
1626
+ if (value === false) {
1627
+ return "false";
1628
+ }
1629
+ if (typeof value !== "string") {
1630
+ return undefined;
1631
+ }
1632
+ const trimmed = value.trim();
1633
+ if (
1634
+ !trimmed ||
1635
+ trimmed.length > MAX_COMPOSER_PLATFORM_VALUE_LENGTH ||
1636
+ hasDangerousUnicode(trimmed) ||
1637
+ /[\t\n\r]/.test(trimmed)
1638
+ ) {
1639
+ return undefined;
1640
+ }
1641
+ return trimmed;
1642
+ }
1643
+
1644
+ /**
1645
+ * Turn a map of platform entries into properties named `<prefix><name>`, in
1646
+ * name order, at most MAX_COMPOSER_PLATFORM_ENTRIES of them. Over the cap the
1647
+ * PHP and HHVM entries are kept first, since `ext-*` sorts before `php`. An
1648
+ * array (Composer writes an empty section as `[]`) or any other shape holds no
1649
+ * entries.
1650
+ *
1651
+ * @param {unknown} entries map of requirement name to constraint
1652
+ * @param {string} prefix property name prefix
1653
+ * @param {function(string): boolean} [keep] which names to keep
1654
+ * @returns {{name: string, value: string}[]} properties
1655
+ */
1656
+ function composerPlatformProperties(entries, prefix, keep = () => true) {
1657
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
1658
+ return [];
1659
+ }
1660
+ const kept = [];
1661
+ for (const key of Object.keys(entries).sort()) {
1662
+ const value = composerPlatformValue(entries[key]);
1663
+ if (value !== undefined && isComposerPlatformKey(key) && keep(key)) {
1664
+ kept.push({ key, value });
1665
+ }
1666
+ }
1667
+ let selected = kept;
1668
+ if (kept.length > MAX_COMPOSER_PLATFORM_ENTRIES) {
1669
+ const runtimes = kept.filter(({ key }) => COMPOSER_PLATFORM_NAMES.has(key));
1670
+ const others = kept
1671
+ .filter(({ key }) => !COMPOSER_PLATFORM_NAMES.has(key))
1672
+ .slice(0, MAX_COMPOSER_PLATFORM_ENTRIES - runtimes.length);
1673
+ const chosen = new Set([...runtimes, ...others]);
1674
+ selected = kept.filter((entry) => chosen.has(entry));
1675
+ }
1676
+ return selected.map(({ key, value }) => ({ name: `${prefix}${key}`, value }));
1677
+ }
1678
+
1679
+ /**
1680
+ * A CycloneDX `organizationalContact` from an author record in package
1681
+ * metadata. The schema asserts `idn-email` on the address, so one that is not
1682
+ * a valid address is dropped rather than failing BOM validation; an author with
1683
+ * neither a name nor a valid address yields nothing.
1684
+ *
1685
+ * @param {{name?: string, email?: unknown}} author Author as the manifest has it
1686
+ * @returns {Array<{name?: string, email?: string}>} Zero or one contact
1687
+ */
1688
+ function contactFromAuthor(author) {
1689
+ const contact = {};
1690
+ if (typeof author?.name === "string" && author.name.trim()) {
1691
+ contact.name = author.name;
1692
+ }
1693
+ const email = typeof author?.email === "string" ? author.email.trim() : "";
1694
+ if (isValidIdnEmail(email)) {
1695
+ contact.email = email;
1696
+ }
1697
+ return Object.keys(contact).length ? [contact] : [];
1698
+ }
1699
+
1558
1700
  /**
1559
1701
  * Parse composer lock file
1560
1702
  *
1561
1703
  * @param {string} pkgLockFile composer.lock file
1562
1704
  * @param {array} rootRequires require section from composer.json
1705
+ * @returns {{pkgList: Object[], dependenciesList: Object[], rootList: Object[], platformProperties: {name: string, value: string}[]}|Array}
1706
+ * The locked packages, the dependency edges between them, the packages the
1707
+ * root requires, and the lock's `platform`, `platform-dev` and
1708
+ * `platform-overrides` entries as `cdx:composer:*` properties. The platform
1709
+ * entries are constraints and assumed versions, not the PHP that runs; the
1710
+ * caller puts them on the module component. Every locked package carries its
1711
+ * own `php`, `hhvm`, `ext-*` and `lib-*` requirements as
1712
+ * `cdx:composer:require:<name>` properties. A lock that is not valid JSON
1713
+ * gives an empty array.
1563
1714
  */
1564
1715
  export function parseComposerLock(pkgLockFile, rootRequires) {
1565
1716
  const pkgList = [];
@@ -1568,16 +1719,12 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1568
1719
  const pkgNamePurlMap = {};
1569
1720
  const rootList = [];
1570
1721
  const rootRequiresMap = {};
1722
+ let platformProperties = [];
1571
1723
  if (rootRequires) {
1572
1724
  for (const rr of Object.keys(rootRequires)) {
1573
1725
  // Skip platform requirements (php, hhvm, ext-*, lib-*) — they are never
1574
1726
  // Composer package names and must not be used to identify root packages.
1575
- if (
1576
- rr === "php" ||
1577
- rr === "hhvm" ||
1578
- rr.startsWith("ext-") ||
1579
- rr.startsWith("lib-")
1580
- ) {
1727
+ if (isComposerPlatformRequirement(rr)) {
1581
1728
  continue;
1582
1729
  }
1583
1730
  rootRequiresMap[rr] = true;
@@ -1592,6 +1739,11 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1592
1739
  return [];
1593
1740
  }
1594
1741
  if (lockData) {
1742
+ for (const [section, prefix] of COMPOSER_PLATFORM_SECTIONS) {
1743
+ platformProperties = platformProperties.concat(
1744
+ composerPlatformProperties(lockData[section], prefix),
1745
+ );
1746
+ }
1595
1747
  const packages = {};
1596
1748
  if (lockData["packages"]) {
1597
1749
  packages["required"] = lockData["packages"];
@@ -1664,9 +1816,10 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1664
1816
  apkg.distribution = { url: distUrl };
1665
1817
  }
1666
1818
  if (pkg?.authors?.length) {
1667
- apkg.authors = pkg.authors.map((a) => {
1668
- return { name: a.name, email: a.email };
1669
- });
1819
+ const authors = pkg.authors.flatMap(contactFromAuthor);
1820
+ if (authors.length) {
1821
+ apkg.authors = authors;
1822
+ }
1670
1823
  }
1671
1824
  if (pkg?.keywords?.length) {
1672
1825
  apkg.tags = pkg.keywords;
@@ -1687,6 +1840,13 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1687
1840
  });
1688
1841
  }
1689
1842
  }
1843
+ apkg.properties.push(
1844
+ ...composerPlatformProperties(
1845
+ pkg.require,
1846
+ COMPOSER_REQUIRE_PREFIX,
1847
+ isComposerPlatformRequirement,
1848
+ ),
1849
+ );
1690
1850
  pkgList.push(apkg);
1691
1851
  dependenciesMap[purl] = new Set();
1692
1852
  pkgNamePurlMap[pkg.name] = purl;
@@ -1726,6 +1886,7 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1726
1886
  pkgList,
1727
1887
  dependenciesList,
1728
1888
  rootList,
1889
+ platformProperties,
1729
1890
  };
1730
1891
  }
1731
1892
 
@@ -2629,6 +2790,130 @@ export function cmakeFetchDependencyComponent(dep, srcFile) {
2629
2790
  return component;
2630
2791
  }
2631
2792
 
2793
+ const MAX_MESON_WRAPS = 256;
2794
+ const MAX_MESON_WRAP_BYTES = 64 * 1024;
2795
+
2796
+ function upstreamVersionOf(text) {
2797
+ if (typeof text !== "string") {
2798
+ return undefined;
2799
+ }
2800
+ const value = text.trim().replace(/^v(?=\d)/, "");
2801
+ return /^\d+(\.\d+)*([-+._~][0-9A-Za-z.]+)?$/.test(value) ? value : undefined;
2802
+ }
2803
+
2804
+ // `brotli-1.1.0`, `brotli-1.1.0.tar.gz`: the version after the last `-` that
2805
+ // starts a number.
2806
+ function versionFromArchiveName(name) {
2807
+ if (typeof name !== "string") {
2808
+ return undefined;
2809
+ }
2810
+ const stem = name.replace(/\.(tar\.gz|tar\.xz|tar\.bz2|tgz|zip|tar)$/i, "");
2811
+ const dash = stem.search(/-\d[^-]*$/);
2812
+ return dash === -1 ? undefined : upstreamVersionOf(stem.slice(dash + 1));
2813
+ }
2814
+
2815
+ /**
2816
+ * Read the wrap files of a meson project's subprojects directory: which
2817
+ * dependency names each wrap provides, and the version it pins.
2818
+ * https://mesonbuild.com/Wrap-dependency-system-manual.html
2819
+ *
2820
+ * A WrapDB wrap records `wrapdb_version = <upstream>-<wrap revision>`; any wrap
2821
+ * names its unpacked `directory` and its archive, which carry the upstream
2822
+ * version; a git wrap pins a `revision` that is often a version tag.
2823
+ *
2824
+ * @param {string} subprojectsDir path of the subprojects directory
2825
+ * @returns {Map<string, {wrap: string, version: string, source: string, revision?: string}>}
2826
+ * by provided dependency name, lower-case
2827
+ */
2828
+ export function readMesonWraps(subprojectsDir) {
2829
+ const provided = new Map();
2830
+ let wrapFiles = [];
2831
+ try {
2832
+ wrapFiles = readdirSync(subprojectsDir)
2833
+ .filter((name) => name.endsWith(".wrap"))
2834
+ .sort()
2835
+ .slice(0, MAX_MESON_WRAPS);
2836
+ } catch {
2837
+ return provided;
2838
+ }
2839
+ for (const wrapFile of wrapFiles) {
2840
+ // A regular file only: a FIFO named *.wrap would block a plain read.
2841
+ const text = readRegularTextFile(
2842
+ join(subprojectsDir, wrapFile),
2843
+ MAX_MESON_WRAP_BYTES,
2844
+ );
2845
+ if (text === undefined) {
2846
+ continue;
2847
+ }
2848
+ const sections = {};
2849
+ let section = "";
2850
+ for (const rawLine of text.split(/\r?\n/)) {
2851
+ const line = rawLine.trim();
2852
+ if (!line || line.startsWith("#") || line.startsWith(";")) {
2853
+ continue;
2854
+ }
2855
+ if (line.startsWith("[") && line.endsWith("]")) {
2856
+ section = line.slice(1, -1).trim();
2857
+ sections[section] = sections[section] || {};
2858
+ continue;
2859
+ }
2860
+ const equals = line.indexOf("=");
2861
+ if (equals <= 0 || !section) {
2862
+ continue;
2863
+ }
2864
+ sections[section][line.slice(0, equals).trim()] = line
2865
+ .slice(equals + 1)
2866
+ .trim();
2867
+ }
2868
+ const body = sections["wrap-file"] || sections["wrap-git"] || {};
2869
+ let version;
2870
+ let source;
2871
+ let revision;
2872
+ if (body.wrapdb_version) {
2873
+ const dash = body.wrapdb_version.lastIndexOf("-");
2874
+ version = upstreamVersionOf(
2875
+ dash > 0 ? body.wrapdb_version.slice(0, dash) : body.wrapdb_version,
2876
+ );
2877
+ revision = dash > 0 ? body.wrapdb_version.slice(dash + 1) : undefined;
2878
+ source = "wrapdb_version";
2879
+ }
2880
+ if (!version && sections["wrap-file"]) {
2881
+ version =
2882
+ versionFromArchiveName(body.directory) ||
2883
+ versionFromArchiveName(body.source_filename);
2884
+ source = version ? "directory" : undefined;
2885
+ }
2886
+ if (!version && sections["wrap-git"]) {
2887
+ version = upstreamVersionOf(body.revision);
2888
+ source = version ? "revision" : undefined;
2889
+ }
2890
+ if (!version) {
2891
+ continue;
2892
+ }
2893
+ const wrap = wrapFile.slice(0, -".wrap".length);
2894
+ const entry = { wrap, version, source, ...(revision ? { revision } : {}) };
2895
+ const names = new Set([wrap.toLowerCase()]);
2896
+ const provide = sections.provide || {};
2897
+ for (const [key, value] of Object.entries(provide)) {
2898
+ if (key === "dependency_names" || key === "program_names") {
2899
+ for (const name of value.split(",")) {
2900
+ if (name.trim()) {
2901
+ names.add(name.trim().toLowerCase());
2902
+ }
2903
+ }
2904
+ } else {
2905
+ names.add(key.toLowerCase());
2906
+ }
2907
+ }
2908
+ for (const name of names) {
2909
+ if (!provided.has(name)) {
2910
+ provided.set(name, entry);
2911
+ }
2912
+ }
2913
+ }
2914
+ return provided;
2915
+ }
2916
+
2632
2917
  /**
2633
2918
  * Parse a CMake-like build file (CMakeLists.txt, meson.build, etc.) and extract the
2634
2919
  * parent component and list of dependency packages.
@@ -2646,6 +2931,11 @@ export function cmakeFetchDependencyComponent(dep, srcFile) {
2646
2931
  */
2647
2932
  export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2648
2933
  const rawData = readFileSync(cmakeListFile, { encoding: "utf-8" });
2934
+ // The wraps a meson project ships pin the versions of the subprojects its
2935
+ // dependency() calls fall back to.
2936
+ const mesonWraps = basename(cmakeListFile).startsWith("meson")
2937
+ ? readMesonWraps(join(dirname(cmakeListFile), "subprojects"))
2938
+ : new Map();
2649
2939
  let cmakeListData = rawData;
2650
2940
  const pkgList = [];
2651
2941
  const pkgAddedMap = {};
@@ -2653,6 +2943,25 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2653
2943
  const versionsMap = {};
2654
2944
  let parentComponent = {};
2655
2945
  const templateValues = {};
2946
+ // The parent component is built the same way whether its name and version
2947
+ // came from the parsed project() command or from the line loop below.
2948
+ const toParentComponent = (parentName, parentVersion) => {
2949
+ const component = {
2950
+ group: options.projectGroup || "",
2951
+ name: parentName,
2952
+ version: parentVersion || options.projectVersion || "",
2953
+ type: "application",
2954
+ };
2955
+ component["purl"] = build({
2956
+ type: pkgType,
2957
+ namespace: component.group || null,
2958
+ name: component.name,
2959
+ version: component.version || null,
2960
+ subpath: null,
2961
+ });
2962
+ component["bom-ref"] = decodeURIComponent(component["purl"]);
2963
+ return component;
2964
+ };
2656
2965
  // A dependency fetched at configure time is the same package a find_package()
2657
2966
  // of the same name asks for: the fetched component, with its source and pin,
2658
2967
  // stands for both.
@@ -2668,6 +2977,38 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2668
2977
  fetchedNames.add(dep.name.toLowerCase());
2669
2978
  pkgList.push(component);
2670
2979
  }
2980
+ // A project() call may span several lines, which the line loop below
2981
+ // cannot rejoin, so the parent name and version are read from the parsed
2982
+ // commands instead. The first argument is the project name and the
2983
+ // VERSION keyword is followed by the version.
2984
+ const commands = parseCmakeCommands(rawData);
2985
+ const variables = cmakeSetVariables(commands);
2986
+ const projectCommand = commands.find(
2987
+ (command) => command.name === "project",
2988
+ );
2989
+ if (projectCommand?.args.length) {
2990
+ const args = projectCommand.args.map((arg) =>
2991
+ expandCmakeVariables(arg, variables),
2992
+ );
2993
+ const parentName = args[0].replace(":", "").trim();
2994
+ let parentVersion;
2995
+ const versionIndex = args.findIndex(
2996
+ (arg) => arg.toLowerCase() === "version",
2997
+ );
2998
+ if (versionIndex > -1 && args.length > versionIndex + 1) {
2999
+ parentVersion = args[versionIndex + 1];
3000
+ }
3001
+ // A `${VAR}` that no `set()` in this file defines stays literal. It is
3002
+ // not a version, and percent-encoding it into a purl publishes the
3003
+ // variable name as the version, so the field is left empty instead.
3004
+ // CMakeCache.txt carries the evaluated value when a build tree exists.
3005
+ if (parentVersion?.includes("$")) {
3006
+ parentVersion = undefined;
3007
+ }
3008
+ if (parentName.length && !parentName.includes("$")) {
3009
+ parentComponent = toParentComponent(parentName, parentVersion);
3010
+ }
3011
+ }
2671
3012
  }
2672
3013
  cmakeListData = cmakeListData
2673
3014
  .replace(/^ {2}/g, "")
@@ -2728,23 +3069,7 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2728
3069
  parentVersion = undefined;
2729
3070
  }
2730
3071
  if (parentName?.length && !parentName.includes("$")) {
2731
- parentComponent = {
2732
- group: options.projectGroup || "",
2733
- name: parentName,
2734
- version: parentVersion || options.projectVersion || "",
2735
- type: "application",
2736
- };
2737
- const safePath = path ? path.replace(/^\/+/, "") : null;
2738
- parentComponent["purl"] = build({
2739
- type: pkgType,
2740
- namespace: parentComponent.group || null,
2741
- name: parentComponent.name,
2742
- version: parentComponent.version || null,
2743
- subpath: safePath,
2744
- });
2745
- parentComponent["bom-ref"] = decodeURIComponent(
2746
- parentComponent["purl"],
2747
- );
3072
+ parentComponent = toParentComponent(parentName, parentVersion);
2748
3073
  }
2749
3074
  } else if (CMAKE_FIND_COMMANDS.has(command)) {
2750
3075
  let tmpB = commandArgs.split(")")[0].split(" ");
@@ -2886,7 +3211,26 @@ export function parseCmakeLikeFile(cmakeListFile, pkgType, options = {}) {
2886
3211
  // wrap match has since replaced, so the scraped name is the fallback.
2887
3212
  const versionSpecifier =
2888
3213
  versionSpecifiersMap[n] || versionSpecifiersMap[scrapedName];
2889
- const version = versionsMap[n] || versionsMap[scrapedName] || "";
3214
+ let version = versionsMap[n] || versionsMap[scrapedName] || "";
3215
+ const localWrap =
3216
+ mesonWraps.get(scrapedName.toLowerCase()) ||
3217
+ mesonWraps.get(n.toLowerCase());
3218
+ if (localWrap && !version) {
3219
+ version = localWrap.version;
3220
+ props.push({ name: "cdx:meson:wrap", value: localWrap.wrap });
3221
+ props.push({
3222
+ name: "cdx:meson:wrap:versionSource",
3223
+ value: localWrap.source,
3224
+ });
3225
+ if (localWrap.revision) {
3226
+ props.push({
3227
+ name: "cdx:meson:wrap:revision",
3228
+ value: localWrap.revision,
3229
+ });
3230
+ }
3231
+ // A pinned wrap is a stronger identity than a name match alone.
3232
+ confidence = Math.max(confidence, 0.7);
3233
+ }
2890
3234
  if (versionSpecifier) {
2891
3235
  props.push({
2892
3236
  name: "cdx:build:versionSpecifiers",
@@ -3608,6 +3952,12 @@ async function fullScanCocoaPod(dependency, component, options) {
3608
3952
  value: podspecLocation,
3609
3953
  });
3610
3954
  }
3955
+ // A Ruby podspec is a program that only `pod` can read, and it comes from
3956
+ // the scanned project or a repository its lockfile names. Without
3957
+ // permission to run the project's tooling, only JSON podspecs are read,
3958
+ // directly and without `pod`.
3959
+ const toolingAllowed = isProjectToolingAllowed(options);
3960
+ const isRemote = podspecLocation.startsWith("http");
3611
3961
  let replacements = [];
3612
3962
  if (
3613
3963
  podspecLocation.endsWith(".podspec") &&
@@ -3624,34 +3974,50 @@ async function fullScanCocoaPod(dependency, component, options) {
3624
3974
  "COCOA_PODSPEC_JSON_REPLACEMENTS",
3625
3975
  ).split(";");
3626
3976
  }
3627
- if (replacements || podspecLocation.startsWith("http")) {
3628
- let podspecContent;
3629
- if (podspecLocation.startsWith("http")) {
3630
- let httpResult;
3631
- for (const candidate of podspecCandidates(podspecLocation)) {
3632
- try {
3633
- httpResult =
3634
- prefetchedResponse(cocoaPodspecPrefetch, candidate) ||
3635
- (await gatedGet(candidate, { responseType: "text" }));
3636
- podspecLocation = candidate;
3637
- } catch (_err) {
3638
- continue;
3639
- }
3640
- component.properties.find(
3641
- ({ name }) => name === "cdx:pods:podspecLocation",
3642
- ).value = podspecLocation;
3643
- podspecLocation = `${randomUUID()}.${podspecLocation.substring(podspecLocation.lastIndexOf(".") + 1)}`;
3644
- podspecContent = httpResult.body;
3645
- break;
3977
+ let podspecContent;
3978
+ if (isRemote) {
3979
+ let httpResult;
3980
+ for (const candidate of podspecCandidates(podspecLocation)) {
3981
+ if (!toolingAllowed && !candidate.endsWith(".json")) {
3982
+ continue;
3646
3983
  }
3647
- } else {
3648
- podspecContent = readFileSync(podspecLocation, "utf-8");
3984
+ try {
3985
+ httpResult =
3986
+ prefetchedResponse(cocoaPodspecPrefetch, candidate) ||
3987
+ (await gatedGet(candidate, { responseType: "text" }));
3988
+ podspecLocation = candidate;
3989
+ } catch (_err) {
3990
+ continue;
3991
+ }
3992
+ component.properties.find(
3993
+ ({ name }) => name === "cdx:pods:podspecLocation",
3994
+ ).value = podspecLocation;
3995
+ podspecContent = httpResult.body;
3996
+ break;
3649
3997
  }
3650
3998
  // A location none of the candidates served contributes nothing: the
3651
3999
  // component keeps what the lockfile already said about the pod.
3652
4000
  if (podspecContent === undefined) {
3653
4001
  return;
3654
4002
  }
4003
+ } else if (!toolingAllowed && !podspecLocation.endsWith(".json")) {
4004
+ reportProjectToolingSkipped(
4005
+ options,
4006
+ "cocoa.podspec.tooling-disabled",
4007
+ "Skipping 'pod ipc spec' for Ruby podspecs since dependency installation is disabled. Their pods keep the details from Podfile.lock.",
4008
+ {
4009
+ ecosystem: "cocoa",
4010
+ tool: "pod",
4011
+ impact: "licenses",
4012
+ detail:
4013
+ "Dependency installation is disabled, so Ruby podspecs were not evaluated and their pods lack license and author details.",
4014
+ },
4015
+ );
4016
+ return;
4017
+ } else if (replacements.length || !toolingAllowed) {
4018
+ podspecContent = readFileSync(podspecLocation, "utf-8");
4019
+ }
4020
+ if (podspecContent !== undefined) {
3655
4021
  for (const replacement of replacements) {
3656
4022
  const replacementPair = replacement.split("=");
3657
4023
  let match = replacementPair[0].replaceAll("<NEWLINE>", "\n");
@@ -3661,18 +4027,28 @@ async function fullScanCocoaPod(dependency, component, options) {
3661
4027
  const repl = replacementPair[1].replaceAll("<NEWLINE>", "\n");
3662
4028
  podspecContent = podspecContent.replaceAll(match, repl);
3663
4029
  }
3664
- podspecLocation = join(
3665
- dirname(podspecLocation),
3666
- `${randomUUID()}.${podspecLocation.substring(podspecLocation.lastIndexOf(".") + 1)}`,
4030
+ }
4031
+ if (!toolingAllowed) {
4032
+ // Only JSON podspecs reach this point, and they need no `pod`
4033
+ result = { stdout: podspecContent };
4034
+ } else {
4035
+ if (podspecContent !== undefined) {
4036
+ // A downloaded podspec goes to the temporary directory; a local one
4037
+ // with replacements stays beside the original, whose relative paths
4038
+ // its Ruby may read.
4039
+ podspecLocation = join(
4040
+ isRemote ? getTmpDir() : dirname(podspecLocation),
4041
+ `${randomUUID()}.${podspecLocation.substring(podspecLocation.lastIndexOf(".") + 1)}`,
4042
+ );
4043
+ safeWriteSync(podspecLocation, podspecContent);
4044
+ temporaryFiles.add(podspecLocation);
4045
+ }
4046
+ result = executePodCommand(
4047
+ ["ipc", "spec", "--silent", podspecLocation],
4048
+ undefined,
4049
+ options,
3667
4050
  );
3668
- safeWriteSync(podspecLocation, podspecContent);
3669
- temporaryFiles.add(podspecLocation);
3670
4051
  }
3671
- result = executePodCommand(
3672
- ["ipc", "spec", "--silent", podspecLocation],
3673
- undefined,
3674
- options,
3675
- );
3676
4052
  } else {
3677
4053
  let dependencyName = dependency.name;
3678
4054
  if (dependencyName.includes("/")) {
@@ -3737,9 +4113,7 @@ async function fullScanCocoaPod(dependency, component, options) {
3737
4113
  component.authors = [];
3738
4114
  if (podspec.authors.constructor === Object) {
3739
4115
  Object.entries(podspec.authors).forEach(([name, email]) => {
3740
- email.includes("@")
3741
- ? component.authors.push({ name, email })
3742
- : component.authors.push({ name });
4116
+ component.authors.push(...contactFromAuthor({ name, email }));
3743
4117
  });
3744
4118
  } else if (podspec.authors.constructor === Array) {
3745
4119
  podspec.authors.forEach((name) => {