@cdxgen/cdxgen 13.4.0 → 13.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -13,11 +13,16 @@ 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";
17
21
  import { recordDegradation } from "../core/buildLedger.js";
18
22
  import { deferFailOnError } from "../core/deferredExit.js";
19
23
  import { shouldFetchLicense } from "../core/env.js";
20
24
  import {
25
+ getTmpDir,
21
26
  safeExistsSync,
22
27
  safeSpawnSync,
23
28
  safeWriteSync,
@@ -25,6 +30,10 @@ import {
25
30
  } from "../core/fs.js";
26
31
  import { thoughtLog } from "../core/logger.js";
27
32
  import { isWin } from "../core/paths.js";
33
+ import {
34
+ isProjectToolingAllowed,
35
+ reportProjectToolingSkipped,
36
+ } from "../core/projectTooling.js";
28
37
  import { registerRunReset } from "../core/runState.js";
29
38
  import { mesonWrapDB } from "../core/state.js";
30
39
  import { isGitHubUrl } from "../core/urls.js";
@@ -44,6 +53,7 @@ import {
44
53
  } from "../inventory/purl.js";
45
54
  import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
46
55
  import { cmakeFetchDependencies } from "../parsers/cmakeLists.js";
56
+ import { isValidIdnEmail } from "../parsers/email.js";
47
57
  import { submodulePurlCoordinates } from "../parsers/gitmodules.js";
48
58
  import { getDartMetadata } from "./ecosystems.js";
49
59
  import { parsePkgJson } from "./parsers-js.js";
@@ -1555,11 +1565,146 @@ export function parseComposerJson(composerJsonFile) {
1555
1565
  return { rootRequires, moduleParent };
1556
1566
  }
1557
1567
 
1568
+ // Composer's platform packages stand for what the runtime provides, and none
1569
+ // is installable, so none has a purl or an edge in the dependency graph. These
1570
+ // are the ones that describe PHP: PHP and its build flavours, HHVM, and the
1571
+ // extensions and system libraries PHP was built with. composer,
1572
+ // composer-plugin-api and composer-runtime-api are platform packages too, but
1573
+ // they describe Composer, so they are left out.
1574
+ // https://github.com/composer/composer/blob/2.10.3/src/Composer/Repository/PlatformRepository.php#L39
1575
+ const COMPOSER_PLATFORM_NAMES = new Set([
1576
+ "php",
1577
+ "php-64bit",
1578
+ "php-ipv6",
1579
+ "php-zts",
1580
+ "php-debug",
1581
+ "hhvm",
1582
+ ]);
1583
+
1584
+ function isComposerPlatformRequirement(name) {
1585
+ return (
1586
+ COMPOSER_PLATFORM_NAMES.has(name) ||
1587
+ name.startsWith("ext-") ||
1588
+ name.startsWith("lib-")
1589
+ );
1590
+ }
1591
+
1592
+ // What the lock records about the platform is a constraint, or the version a
1593
+ // project told Composer to assume. It is a fact about the project's
1594
+ // requirements, never the version of anything that runs, so it is kept as a
1595
+ // property and never becomes a component.
1596
+ const COMPOSER_PLATFORM_SECTIONS = [
1597
+ ["platform", "cdx:composer:platform:"],
1598
+ ["platform-dev", "cdx:composer:platformDev:"],
1599
+ ["platform-overrides", "cdx:composer:platformOverride:"],
1600
+ ];
1601
+ const COMPOSER_REQUIRE_PREFIX = "cdx:composer:require:";
1602
+ const MAX_COMPOSER_PLATFORM_VALUE_LENGTH = 256;
1603
+ // A real lock names a few dozen platform packages per map at most; the cap
1604
+ // keeps a crafted one from turning each map into thousands of properties.
1605
+ const MAX_COMPOSER_PLATFORM_ENTRIES = 128;
1606
+
1607
+ function isComposerPlatformKey(key) {
1608
+ return /^[A-Za-z0-9._-]{1,100}$/.test(key);
1609
+ }
1610
+
1611
+ /**
1612
+ * The value a platform entry carries as a property: a constraint or version
1613
+ * string, or `false`, which `config.platform` uses to disable a package:
1614
+ * https://github.com/composer/composer/blob/2.10.3/src/Composer/Repository/PlatformRepository.php#L125
1615
+ *
1616
+ * @param {unknown} value value from the lock
1617
+ * @returns {string|undefined} property value, or undefined when it is not one
1618
+ */
1619
+ function composerPlatformValue(value) {
1620
+ if (value === false) {
1621
+ return "false";
1622
+ }
1623
+ if (typeof value !== "string") {
1624
+ return undefined;
1625
+ }
1626
+ const trimmed = value.trim();
1627
+ if (
1628
+ !trimmed ||
1629
+ trimmed.length > MAX_COMPOSER_PLATFORM_VALUE_LENGTH ||
1630
+ hasDangerousUnicode(trimmed) ||
1631
+ /[\t\n\r]/.test(trimmed)
1632
+ ) {
1633
+ return undefined;
1634
+ }
1635
+ return trimmed;
1636
+ }
1637
+
1638
+ /**
1639
+ * Turn a map of platform entries into properties named `<prefix><name>`, in
1640
+ * name order, at most MAX_COMPOSER_PLATFORM_ENTRIES of them. Over the cap the
1641
+ * PHP and HHVM entries are kept first, since `ext-*` sorts before `php`. An
1642
+ * array (Composer writes an empty section as `[]`) or any other shape holds no
1643
+ * entries.
1644
+ *
1645
+ * @param {unknown} entries map of requirement name to constraint
1646
+ * @param {string} prefix property name prefix
1647
+ * @param {function(string): boolean} [keep] which names to keep
1648
+ * @returns {{name: string, value: string}[]} properties
1649
+ */
1650
+ function composerPlatformProperties(entries, prefix, keep = () => true) {
1651
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
1652
+ return [];
1653
+ }
1654
+ const kept = [];
1655
+ for (const key of Object.keys(entries).sort()) {
1656
+ const value = composerPlatformValue(entries[key]);
1657
+ if (value !== undefined && isComposerPlatformKey(key) && keep(key)) {
1658
+ kept.push({ key, value });
1659
+ }
1660
+ }
1661
+ let selected = kept;
1662
+ if (kept.length > MAX_COMPOSER_PLATFORM_ENTRIES) {
1663
+ const runtimes = kept.filter(({ key }) => COMPOSER_PLATFORM_NAMES.has(key));
1664
+ const others = kept
1665
+ .filter(({ key }) => !COMPOSER_PLATFORM_NAMES.has(key))
1666
+ .slice(0, MAX_COMPOSER_PLATFORM_ENTRIES - runtimes.length);
1667
+ const chosen = new Set([...runtimes, ...others]);
1668
+ selected = kept.filter((entry) => chosen.has(entry));
1669
+ }
1670
+ return selected.map(({ key, value }) => ({ name: `${prefix}${key}`, value }));
1671
+ }
1672
+
1673
+ /**
1674
+ * A CycloneDX `organizationalContact` from an author record in package
1675
+ * metadata. The schema asserts `idn-email` on the address, so one that is not
1676
+ * a valid address is dropped rather than failing BOM validation; an author with
1677
+ * neither a name nor a valid address yields nothing.
1678
+ *
1679
+ * @param {{name?: string, email?: unknown}} author Author as the manifest has it
1680
+ * @returns {Array<{name?: string, email?: string}>} Zero or one contact
1681
+ */
1682
+ function contactFromAuthor(author) {
1683
+ const contact = {};
1684
+ if (typeof author?.name === "string" && author.name.trim()) {
1685
+ contact.name = author.name;
1686
+ }
1687
+ const email = typeof author?.email === "string" ? author.email.trim() : "";
1688
+ if (isValidIdnEmail(email)) {
1689
+ contact.email = email;
1690
+ }
1691
+ return Object.keys(contact).length ? [contact] : [];
1692
+ }
1693
+
1558
1694
  /**
1559
1695
  * Parse composer lock file
1560
1696
  *
1561
1697
  * @param {string} pkgLockFile composer.lock file
1562
1698
  * @param {array} rootRequires require section from composer.json
1699
+ * @returns {{pkgList: Object[], dependenciesList: Object[], rootList: Object[], platformProperties: {name: string, value: string}[]}|Array}
1700
+ * The locked packages, the dependency edges between them, the packages the
1701
+ * root requires, and the lock's `platform`, `platform-dev` and
1702
+ * `platform-overrides` entries as `cdx:composer:*` properties. The platform
1703
+ * entries are constraints and assumed versions, not the PHP that runs; the
1704
+ * caller puts them on the module component. Every locked package carries its
1705
+ * own `php`, `hhvm`, `ext-*` and `lib-*` requirements as
1706
+ * `cdx:composer:require:<name>` properties. A lock that is not valid JSON
1707
+ * gives an empty array.
1563
1708
  */
1564
1709
  export function parseComposerLock(pkgLockFile, rootRequires) {
1565
1710
  const pkgList = [];
@@ -1568,16 +1713,12 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1568
1713
  const pkgNamePurlMap = {};
1569
1714
  const rootList = [];
1570
1715
  const rootRequiresMap = {};
1716
+ let platformProperties = [];
1571
1717
  if (rootRequires) {
1572
1718
  for (const rr of Object.keys(rootRequires)) {
1573
1719
  // Skip platform requirements (php, hhvm, ext-*, lib-*) — they are never
1574
1720
  // 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
- ) {
1721
+ if (isComposerPlatformRequirement(rr)) {
1581
1722
  continue;
1582
1723
  }
1583
1724
  rootRequiresMap[rr] = true;
@@ -1592,6 +1733,11 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1592
1733
  return [];
1593
1734
  }
1594
1735
  if (lockData) {
1736
+ for (const [section, prefix] of COMPOSER_PLATFORM_SECTIONS) {
1737
+ platformProperties = platformProperties.concat(
1738
+ composerPlatformProperties(lockData[section], prefix),
1739
+ );
1740
+ }
1595
1741
  const packages = {};
1596
1742
  if (lockData["packages"]) {
1597
1743
  packages["required"] = lockData["packages"];
@@ -1664,9 +1810,10 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1664
1810
  apkg.distribution = { url: distUrl };
1665
1811
  }
1666
1812
  if (pkg?.authors?.length) {
1667
- apkg.authors = pkg.authors.map((a) => {
1668
- return { name: a.name, email: a.email };
1669
- });
1813
+ const authors = pkg.authors.flatMap(contactFromAuthor);
1814
+ if (authors.length) {
1815
+ apkg.authors = authors;
1816
+ }
1670
1817
  }
1671
1818
  if (pkg?.keywords?.length) {
1672
1819
  apkg.tags = pkg.keywords;
@@ -1687,6 +1834,13 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1687
1834
  });
1688
1835
  }
1689
1836
  }
1837
+ apkg.properties.push(
1838
+ ...composerPlatformProperties(
1839
+ pkg.require,
1840
+ COMPOSER_REQUIRE_PREFIX,
1841
+ isComposerPlatformRequirement,
1842
+ ),
1843
+ );
1690
1844
  pkgList.push(apkg);
1691
1845
  dependenciesMap[purl] = new Set();
1692
1846
  pkgNamePurlMap[pkg.name] = purl;
@@ -1726,6 +1880,7 @@ export function parseComposerLock(pkgLockFile, rootRequires) {
1726
1880
  pkgList,
1727
1881
  dependenciesList,
1728
1882
  rootList,
1883
+ platformProperties,
1729
1884
  };
1730
1885
  }
1731
1886
 
@@ -3608,6 +3763,12 @@ async function fullScanCocoaPod(dependency, component, options) {
3608
3763
  value: podspecLocation,
3609
3764
  });
3610
3765
  }
3766
+ // A Ruby podspec is a program that only `pod` can read, and it comes from
3767
+ // the scanned project or a repository its lockfile names. Without
3768
+ // permission to run the project's tooling, only JSON podspecs are read,
3769
+ // directly and without `pod`.
3770
+ const toolingAllowed = isProjectToolingAllowed(options);
3771
+ const isRemote = podspecLocation.startsWith("http");
3611
3772
  let replacements = [];
3612
3773
  if (
3613
3774
  podspecLocation.endsWith(".podspec") &&
@@ -3624,34 +3785,50 @@ async function fullScanCocoaPod(dependency, component, options) {
3624
3785
  "COCOA_PODSPEC_JSON_REPLACEMENTS",
3625
3786
  ).split(";");
3626
3787
  }
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;
3788
+ let podspecContent;
3789
+ if (isRemote) {
3790
+ let httpResult;
3791
+ for (const candidate of podspecCandidates(podspecLocation)) {
3792
+ if (!toolingAllowed && !candidate.endsWith(".json")) {
3793
+ continue;
3646
3794
  }
3647
- } else {
3648
- podspecContent = readFileSync(podspecLocation, "utf-8");
3795
+ try {
3796
+ httpResult =
3797
+ prefetchedResponse(cocoaPodspecPrefetch, candidate) ||
3798
+ (await gatedGet(candidate, { responseType: "text" }));
3799
+ podspecLocation = candidate;
3800
+ } catch (_err) {
3801
+ continue;
3802
+ }
3803
+ component.properties.find(
3804
+ ({ name }) => name === "cdx:pods:podspecLocation",
3805
+ ).value = podspecLocation;
3806
+ podspecContent = httpResult.body;
3807
+ break;
3649
3808
  }
3650
3809
  // A location none of the candidates served contributes nothing: the
3651
3810
  // component keeps what the lockfile already said about the pod.
3652
3811
  if (podspecContent === undefined) {
3653
3812
  return;
3654
3813
  }
3814
+ } else if (!toolingAllowed && !podspecLocation.endsWith(".json")) {
3815
+ reportProjectToolingSkipped(
3816
+ options,
3817
+ "cocoa.podspec.tooling-disabled",
3818
+ "Skipping 'pod ipc spec' for Ruby podspecs since dependency installation is disabled. Their pods keep the details from Podfile.lock.",
3819
+ {
3820
+ ecosystem: "cocoa",
3821
+ tool: "pod",
3822
+ impact: "licenses",
3823
+ detail:
3824
+ "Dependency installation is disabled, so Ruby podspecs were not evaluated and their pods lack license and author details.",
3825
+ },
3826
+ );
3827
+ return;
3828
+ } else if (replacements.length || !toolingAllowed) {
3829
+ podspecContent = readFileSync(podspecLocation, "utf-8");
3830
+ }
3831
+ if (podspecContent !== undefined) {
3655
3832
  for (const replacement of replacements) {
3656
3833
  const replacementPair = replacement.split("=");
3657
3834
  let match = replacementPair[0].replaceAll("<NEWLINE>", "\n");
@@ -3661,18 +3838,28 @@ async function fullScanCocoaPod(dependency, component, options) {
3661
3838
  const repl = replacementPair[1].replaceAll("<NEWLINE>", "\n");
3662
3839
  podspecContent = podspecContent.replaceAll(match, repl);
3663
3840
  }
3664
- podspecLocation = join(
3665
- dirname(podspecLocation),
3666
- `${randomUUID()}.${podspecLocation.substring(podspecLocation.lastIndexOf(".") + 1)}`,
3841
+ }
3842
+ if (!toolingAllowed) {
3843
+ // Only JSON podspecs reach this point, and they need no `pod`
3844
+ result = { stdout: podspecContent };
3845
+ } else {
3846
+ if (podspecContent !== undefined) {
3847
+ // A downloaded podspec goes to the temporary directory; a local one
3848
+ // with replacements stays beside the original, whose relative paths
3849
+ // its Ruby may read.
3850
+ podspecLocation = join(
3851
+ isRemote ? getTmpDir() : dirname(podspecLocation),
3852
+ `${randomUUID()}.${podspecLocation.substring(podspecLocation.lastIndexOf(".") + 1)}`,
3853
+ );
3854
+ safeWriteSync(podspecLocation, podspecContent);
3855
+ temporaryFiles.add(podspecLocation);
3856
+ }
3857
+ result = executePodCommand(
3858
+ ["ipc", "spec", "--silent", podspecLocation],
3859
+ undefined,
3860
+ options,
3667
3861
  );
3668
- safeWriteSync(podspecLocation, podspecContent);
3669
- temporaryFiles.add(podspecLocation);
3670
3862
  }
3671
- result = executePodCommand(
3672
- ["ipc", "spec", "--silent", podspecLocation],
3673
- undefined,
3674
- options,
3675
- );
3676
3863
  } else {
3677
3864
  let dependencyName = dependency.name;
3678
3865
  if (dependencyName.includes("/")) {
@@ -3737,9 +3924,7 @@ async function fullScanCocoaPod(dependency, component, options) {
3737
3924
  component.authors = [];
3738
3925
  if (podspec.authors.constructor === Object) {
3739
3926
  Object.entries(podspec.authors).forEach(([name, email]) => {
3740
- email.includes("@")
3741
- ? component.authors.push({ name, email })
3742
- : component.authors.push({ name });
3927
+ component.authors.push(...contactFromAuthor({ name, email }));
3743
3928
  });
3744
3929
  } else if (podspec.authors.constructor === Array) {
3745
3930
  podspec.authors.forEach((name) => {
@@ -1,6 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, dirname, join, relative } from "node:path";
3
- import process from "node:process";
4
3
  import { URL } from "node:url";
5
4
 
6
5
  import { build } from "@cdxgen/cdx-purl";
@@ -2205,28 +2204,30 @@ export function parsePixiTomlFile(pixiToml) {
2205
2204
  }
2206
2205
 
2207
2206
  /**
2208
- * Method to run cli command `pixi install`
2209
- *
2207
+ * Method to run cli command `pixi install` in the project directory.
2210
2208
  *
2209
+ * @param {string} projectPath Directory containing pixi.toml
2210
+ * @returns {boolean} true when pixi install succeeded
2211
2211
  */
2212
- export function generatePixiLockFile(_path) {
2213
- const result = safeSpawnSync("pixi", ["install"]);
2212
+ export function generatePixiLockFile(projectPath) {
2213
+ const result = safeSpawnSync("pixi", ["install"], { cwd: projectPath });
2214
2214
 
2215
2215
  if (result.status !== 0) {
2216
- // Handle errors
2216
+ // Handle errors. The caller carries on without a lock file: exiting here
2217
+ // would also stop a long-running cdxgen server.
2217
2218
  if (result.error && result.error.code === "ENOENT") {
2218
2219
  console.error(
2219
2220
  "Error: pixi command not found. Make sure pixi.js is installed globally.",
2220
2221
  );
2221
2222
  } else {
2222
2223
  console.error(
2223
- `Error executing pixi install: ${result.error || result.stderr.toString()}`,
2224
+ `Error executing pixi install: ${result.error || result.stderr?.toString()}`,
2224
2225
  );
2225
2226
  }
2226
- process.exit(1);
2227
- } else {
2228
- console.log("Dependencies installed successfully.");
2227
+ return false;
2229
2228
  }
2229
+ console.log("Dependencies installed successfully.");
2230
+ return true;
2230
2231
  }
2231
2232
 
2232
2233
  /**
@@ -29,6 +29,7 @@ import {
29
29
  recordPolicyDegradationFromError,
30
30
  } from "../inventory/fetchBatch.js";
31
31
  import { genericPurl } from "../inventory/purl.js";
32
+ import { isValidIdnEmail } from "../parsers/email.js";
32
33
 
33
34
  // FIXME. This has to get removed, once we improve the module detection one-liner.
34
35
  // If you're a Rubyist, please help us improve this code.
@@ -1967,9 +1968,11 @@ export async function parseGemspecData(gemspecData, gemspecFile, options = {}) {
1967
1968
  // `spec.email` holds the contact addresses of the gem, not of an author.
1968
1969
  // An address can only be attributed when there is one per author, in the
1969
1970
  // same order, or when the gem has a single author.
1970
- const emails = (lists.email || []).filter(
1971
- (email) => email.includes("@") && !email.includes(" "),
1972
- );
1971
+ // Only a valid `idn-email` may reach `organizationalContact.email`; the
1972
+ // schema asserts the format, so anything else fails BOM validation.
1973
+ const emails = (lists.email || [])
1974
+ .map((email) => `${email}`.trim())
1975
+ .filter(isValidIdnEmail);
1973
1976
  pkg.authors = lists.authors.map((name, i) => {
1974
1977
  let email;
1975
1978
  if (emails.length === lists.authors.length) {
@@ -14,6 +14,7 @@ import {
14
14
  reportPluginRunFailure,
15
15
  resolvePluginSourceDir,
16
16
  } from "../core/pluginRun.js";
17
+ import { isProjectToolingAllowed } from "../core/projectTooling.js";
17
18
  import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
18
19
  import { resolvePluginBinary } from "../inventory/plugins.js";
19
20
 
@@ -821,6 +822,15 @@ export function runRusiAnalysis(src, outputFile, options = {}) {
821
822
  if (!executable || !src || !outputFile) {
822
823
  return false;
823
824
  }
825
+ // rusi runs cargo and rustc on the project, which follow its
826
+ // `.cargo/config.toml` and `rust-toolchain.toml` and can compile its build
827
+ // scripts, so it waits for permission to run the project's tooling.
828
+ if (!isProjectToolingAllowed(options)) {
829
+ console.log(
830
+ "Skipping rusi since dependency installation is disabled. Rust evidence needs cargo to run on the project.",
831
+ );
832
+ return false;
833
+ }
824
834
 
825
835
  // cdxgen may run with its working directory already inside the project
826
836
  // while the source argument is relative to a repository root; a plain
@@ -5,6 +5,7 @@ import { DEBUG_MODE } from "../core/activity.js";
5
5
  import { commandOutputText } from "../core/buildLedger.js";
6
6
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
7
7
  import { isWin } from "../core/paths.js";
8
+ import { isProjectToolingAllowed } from "../core/projectTooling.js";
8
9
  import { noteBuildToolRateLimit } from "../inventory/buildToolRateLimit.js";
9
10
  import { parseScalaArtifact } from "../inventory/scalaCoords.js";
10
11
  import { completeComponent } from "./parsers-jvm.js";
@@ -283,7 +284,7 @@ export function collectScalaCliComponents(dirPath, options = {}) {
283
284
  // The transitive dependencies of the declared ones come from the class
284
285
  // path the tool itself resolves; the compile requires installs to be
285
286
  // allowed.
286
- if (options.installDeps === false) {
287
+ if (!isProjectToolingAllowed(options)) {
287
288
  console.log(
288
289
  "Skipping the scala-cli class path resolution since dependency installation is disabled.",
289
290
  );
@@ -18,6 +18,7 @@ import {
18
18
  import { dirNameStr } from "../core/paths.js";
19
19
  import { resolvePluginSourceDir } from "../core/pluginRun.js";
20
20
  import { formatDuration } from "../core/processTree.js";
21
+ import { isProjectToolingAllowed } from "../core/projectTooling.js";
21
22
  import { parsetoolsFile } from "../inventory/atomUtils.js";
22
23
  import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
23
24
  import { getJarClasses } from "../inventory/deps.js";
@@ -341,7 +342,8 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
341
342
  const runDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
342
343
  const runReportFile = join(runDir, "scalasem.slices.json");
343
344
  const args = [scalasem, analysisDir, runReportFile];
344
- if (options.installDeps === false) {
345
+ // The build runs the project's sbt definitions
346
+ if (!isProjectToolingAllowed(options)) {
345
347
  args.push("--no-build");
346
348
  }
347
349
  if (options.scalasemIncludeTests && !options.requiredOnly) {