@cdxgen/cdxgen 13.2.0 → 13.3.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 (158) hide show
  1. package/README.md +28 -12
  2. package/bin/cdxgen.js +227 -119
  3. package/bin/evinse.js +5 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/remediations.json +24 -0
  9. package/lib/cli/bomAssembly.js +105 -15
  10. package/lib/cli/cliOptions.js +93 -1
  11. package/lib/cli/index.js +43 -4
  12. package/lib/cli/jsBom.js +49 -24
  13. package/lib/cli/jvmBom.js +46 -12
  14. package/lib/cli/managedBom.js +112 -41
  15. package/lib/cli/nativeBom.js +125 -14
  16. package/lib/core/env.js +5 -6
  17. package/lib/core/fs.js +174 -6
  18. package/lib/core/metadataProperties.js +97 -0
  19. package/lib/core/projectConfig.js +31 -4
  20. package/lib/ecosystems/bunutils.js +269 -101
  21. package/lib/ecosystems/caxa.js +434 -2
  22. package/lib/ecosystems/cmakeResolver.js +66 -0
  23. package/lib/ecosystems/cppEvidence.js +64 -31
  24. package/lib/ecosystems/ecosystems.js +19 -8
  25. package/lib/ecosystems/gradleutils.js +42 -7
  26. package/lib/ecosystems/jsEvidence.js +19 -20
  27. package/lib/ecosystems/kosi.js +10 -3
  28. package/lib/ecosystems/npmutils.js +110 -9
  29. package/lib/ecosystems/parsers-dotnet.js +51 -10
  30. package/lib/ecosystems/parsers-go.js +47 -9
  31. package/lib/ecosystems/parsers-js.js +171 -50
  32. package/lib/ecosystems/parsers-misc.js +747 -161
  33. package/lib/ecosystems/parsers-python.js +26 -14
  34. package/lib/ecosystems/parsers-rust.js +24 -18
  35. package/lib/ecosystems/rubyutils.js +1426 -166
  36. package/lib/ecosystems/rusi.js +32 -1
  37. package/lib/evinser/evinser.js +436 -128
  38. package/lib/evinser/swiftsem.js +1914 -302
  39. package/lib/helpers/bomSigner.js +652 -221
  40. package/lib/inventory/atomUtils.js +568 -31
  41. package/lib/inventory/cdxrs.js +103 -12
  42. package/lib/inventory/depsUtils.js +183 -11
  43. package/lib/inventory/display.js +4 -5
  44. package/lib/inventory/dosai.js +550 -13
  45. package/lib/inventory/dynamic.js +39 -1
  46. package/lib/inventory/evidenceUtils.js +14 -6
  47. package/lib/inventory/fetchBatch.js +116 -17
  48. package/lib/inventory/protobom.js +8 -1
  49. package/lib/inventory/purl.js +152 -3
  50. package/lib/inventory/sbomDocument.js +2 -70
  51. package/lib/inventory/source.js +37 -19
  52. package/lib/inventory/traceRunner.js +210 -21
  53. package/lib/managers/binary.js +115 -24
  54. package/lib/managers/docker.js +440 -20
  55. package/lib/managers/oci.js +39 -1
  56. package/lib/parsers/cmakeLists.js +452 -0
  57. package/lib/parsers/largeJson.js +454 -0
  58. package/lib/parsers/npmrc.js +19 -1
  59. package/lib/stages/postgen/postgen.js +68 -15
  60. package/lib/validator/bomValidator.js +6 -16
  61. package/lib/validator/index.js +48 -10
  62. package/lib/validator/reporters/console.js +3 -0
  63. package/lib/validator/reporters/json.js +1 -0
  64. package/lib/validator/reporters/sarif.js +7 -0
  65. package/package.json +20 -20
  66. package/types/lib/cli/bomAssembly.d.ts +2 -1
  67. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  68. package/types/lib/cli/cliOptions.d.ts +33 -1
  69. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  70. package/types/lib/cli/index.d.ts.map +1 -1
  71. package/types/lib/cli/jsBom.d.ts.map +1 -1
  72. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  73. package/types/lib/cli/managedBom.d.ts.map +1 -1
  74. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  75. package/types/lib/core/env.d.ts.map +1 -1
  76. package/types/lib/core/fs.d.ts +55 -0
  77. package/types/lib/core/fs.d.ts.map +1 -1
  78. package/types/lib/core/metadataProperties.d.ts +39 -0
  79. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  80. package/types/lib/core/projectConfig.d.ts +6 -2
  81. package/types/lib/core/projectConfig.d.ts.map +1 -1
  82. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  83. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  84. package/types/lib/ecosystems/caxa.d.ts +14 -0
  85. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  86. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  87. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  88. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  89. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  90. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  91. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  92. package/types/lib/ecosystems/gradleutils.d.ts +20 -4
  93. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  94. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  95. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  96. package/types/lib/ecosystems/kosi.d.ts +13 -0
  97. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  98. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  99. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  102. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  103. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  104. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  105. package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
  106. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  109. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  110. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  111. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  112. package/types/lib/ecosystems/rusi.d.ts +13 -0
  113. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  114. package/types/lib/evinser/evinser.d.ts +59 -9
  115. package/types/lib/evinser/evinser.d.ts.map +1 -1
  116. package/types/lib/evinser/swiftsem.d.ts +280 -15
  117. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  118. package/types/lib/helpers/bomSigner.d.ts +106 -13
  119. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  120. package/types/lib/inventory/atomUtils.d.ts +145 -3
  121. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  122. package/types/lib/inventory/cdxrs.d.ts +8 -3
  123. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts +15 -0
  125. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  126. package/types/lib/inventory/display.d.ts.map +1 -1
  127. package/types/lib/inventory/dosai.d.ts +64 -1
  128. package/types/lib/inventory/dosai.d.ts.map +1 -1
  129. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  130. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  131. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  132. package/types/lib/inventory/plugins.d.ts.map +1 -1
  133. package/types/lib/inventory/protobom.d.ts.map +1 -1
  134. package/types/lib/inventory/purl.d.ts +71 -2
  135. package/types/lib/inventory/purl.d.ts.map +1 -1
  136. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  137. package/types/lib/inventory/source.d.ts.map +1 -1
  138. package/types/lib/inventory/traceRunner.d.ts +47 -1
  139. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  140. package/types/lib/managers/binary.d.ts +30 -1
  141. package/types/lib/managers/binary.d.ts.map +1 -1
  142. package/types/lib/managers/docker.d.ts +29 -0
  143. package/types/lib/managers/docker.d.ts.map +1 -1
  144. package/types/lib/managers/oci.d.ts +14 -0
  145. package/types/lib/managers/oci.d.ts.map +1 -1
  146. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  147. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  148. package/types/lib/parsers/largeJson.d.ts +52 -0
  149. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  150. package/types/lib/parsers/npmrc.d.ts +5 -0
  151. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  152. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  153. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  154. package/types/lib/validator/index.d.ts +10 -1
  155. package/types/lib/validator/index.d.ts.map +1 -1
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  157. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  158. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -40,10 +40,15 @@ import {
40
40
  isRusiRustLanguage,
41
41
  } from "../ecosystems/rusi.js";
42
42
  import {
43
+ atomCompileCommandsArgs,
43
44
  buildAtomCommandEnv,
44
45
  executeAtom,
45
46
  filterAtomSlicesByExcludePatterns,
47
+ readReachablesSlices,
48
+ readSlicesFile,
49
+ removeReachablesChunkFiles,
46
50
  } from "../inventory/atomUtils.js";
51
+ import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
47
52
  import {
48
53
  collectDosaiCryptoComponents,
49
54
  findCryptoAlgos,
@@ -62,9 +67,12 @@ import {
62
67
  isDosaiDotnetLanguage,
63
68
  normalizeDosaiServiceMap,
64
69
  persistDosaiSemanticsReport,
70
+ readDosaiDataFlowReport,
71
+ readDosaiMethodsReport,
65
72
  } from "../inventory/dosai.js";
66
73
  import { parseOccurrenceEvidenceLocation } from "../inventory/evidenceUtils.js";
67
74
  import { executeOsQuery } from "../managers/binary.js";
75
+ import { MAX_JSON_TEXT_BYTES, readJsonFile } from "../parsers/largeJson.js";
68
76
  import { postProcess } from "../stages/postgen/postgen.js";
69
77
  import { createOrLoad } from "./db.js";
70
78
  import { findPurlLocations } from "./scalasem.js";
@@ -96,10 +104,88 @@ function appendComponentProperties(component, properties = []) {
96
104
  }
97
105
  }
98
106
 
107
+ /**
108
+ * Merges one analyzer run's metadata properties into the BOM's metadata
109
+ * component. Analyzer metadata describes a single run, so when the input BOM
110
+ * already carries facts from an earlier run (cdxgen --profile research, then
111
+ * evinse), everything under the analyzer's own `cdx:<analyzer>:` prefix is
112
+ * replaced. Replacing by exact name would keep facts the new run no longer
113
+ * emits, such as a cdx:rusi:requestedBackend left from a fallback run.
114
+ *
115
+ * @param {Object} component Metadata component to update in place
116
+ * @param {Object[]} metadataProperties Properties from the analyzer run
117
+ */
118
+ export function mergeAnalyzerMetadataProperties(
119
+ component,
120
+ metadataProperties = [],
121
+ ) {
122
+ if (!component || !metadataProperties.length) {
123
+ return;
124
+ }
125
+ if (component.properties?.length) {
126
+ const analyzerPrefixes = [
127
+ ...new Set(
128
+ metadataProperties
129
+ .map((p) => /^cdx:[^:]+:/.exec(p.name)?.[0])
130
+ .filter(Boolean),
131
+ ),
132
+ ];
133
+ component.properties = component.properties.filter(
134
+ (p) => !analyzerPrefixes.some((prefix) => p.name?.startsWith(prefix)),
135
+ );
136
+ }
137
+ appendComponentProperties(component, metadataProperties);
138
+ }
139
+
99
140
  function filterAtomSliceData(sliceData, options = {}) {
100
141
  return filterAtomSlicesByExcludePatterns(sliceData, options.exclude);
101
142
  }
102
143
 
144
+ /**
145
+ * Build the options for the evinse phase (prepareDB, analyzeProject, and
146
+ * createEvinseFile) from the cdxgen CLI options.
147
+ *
148
+ * This object used to be assembled inline in bin/cdxgen.js, which dropped
149
+ * fields the analyzers read - most notably `exclude`, so every dosai and atom
150
+ * run triggered by `--evidence` ignored the user's `--exclude` patterns. Keep
151
+ * the wiring in one place so the forwarded fields stay testable.
152
+ *
153
+ * @param {Object} options CLI options
154
+ * @param {Object} args Parsed CLI arguments; `args._[0]` is the source directory
155
+ * @param {string} input Path of the BOM file the evinse phase enriches
156
+ * @returns {Object} Options for prepareDB, analyzeProject, and createEvinseFile
157
+ */
158
+ export function buildEvinseOptions(options, args, input) {
159
+ const sourceDir = args._?.[0] || ".";
160
+ return {
161
+ _: args._,
162
+ input,
163
+ output: options.evinseOutput,
164
+ language: options.projectType,
165
+ skipMavenCollector: false,
166
+ force: false,
167
+ withReachables: options.deep,
168
+ usagesSlicesFile: options.usagesSlicesFile,
169
+ dataFlowSlicesFile: options.dataFlowSlicesFile,
170
+ reachablesSlicesFile: options.reachablesSlicesFile,
171
+ semanticsSlicesFile: options.semanticsSlicesFile,
172
+ openapiSpecFile: options.openapiSpecFile,
173
+ componentType: options.componentType,
174
+ includeCrypto:
175
+ options.includeCrypto &&
176
+ isCycloneDxComponentTypeEnabled("cryptographic-asset", options),
177
+ specVersion: options.specVersion,
178
+ profile: options.profile,
179
+ jsonPretty: options.jsonPretty,
180
+ exclude: options.exclude,
181
+ // Absolute path of the deps slice createCsharpBom writes for --deep runs,
182
+ // so analyzeProject can reuse it instead of rerunning dosai methods.
183
+ depsSlicesFile: options.depsSlicesFile
184
+ ? resolve(sourceDir, options.depsSlicesFile)
185
+ : undefined,
186
+ };
187
+ }
188
+
103
189
  /**
104
190
  * Function to create the db for the libraries referred in the sbom.
105
191
  *
@@ -444,6 +530,11 @@ export async function createSlice(
444
530
  `Creating ${sliceType} slice for ${resolve(filePath)}. Please wait ...`,
445
531
  );
446
532
  const atomFile = join(sliceOutputDir, `${language}-app.atom`);
533
+ if (sliceType === "reachables") {
534
+ // atom writes reachables in numbered chunks next to the slices file; one
535
+ // left by an earlier run must not be read back as part of this one.
536
+ removeReachablesChunkFiles(slicesFile);
537
+ }
447
538
  let args = [sliceType];
448
539
  // Support for crypto slices aka CBOM
449
540
  if (sliceType === "reachables" && options.includeCrypto) {
@@ -488,6 +579,7 @@ export async function createSlice(
488
579
  args.push("--slice-depth");
489
580
  args.push(readEnvironmentVariable("ATOM_SLICE_DEPTH"));
490
581
  }
582
+ args.push(...atomCompileCommandsArgs(filePath, language, options));
491
583
  args.push(resolve(filePath));
492
584
  const atomExcludeEnv = buildAtomCommandEnv(options, language);
493
585
  // Execute atom
@@ -598,8 +690,9 @@ export function purlToLanguage(purl, filePath) {
598
690
  * Seed purl-location and import maps from SBOM component evidence.
599
691
  *
600
692
  * Walks the supplied components reading `internal:ImportedModules` (or
601
- * `internal:Namespaces` for php/ruby) and `evidence.occurrences` properties
602
- * to construct lookup maps used during slice analysis.
693
+ * `internal:Namespaces` for php/ruby, plus the `internal:ImportedSymbols` the
694
+ * C/C++ collector records) and `evidence.occurrences` properties to construct
695
+ * lookup maps used during slice analysis.
603
696
  *
604
697
  * @param {Object[]} components CycloneDX components from the input SBOM
605
698
  * @param {string} language Application language used to select the import property name
@@ -624,6 +717,18 @@ export function initFromSbom(components, language) {
624
717
  .forEach((v) => {
625
718
  purlImportsMap[comp.purl] = (v.value || "").split(",");
626
719
  });
720
+ // A C/C++ component records the symbols the project uses from it, which
721
+ // are what C usages are matched against. The list is pipe-separated:
722
+ // C++ symbols may contain commas.
723
+ if (["c", "cpp", "c++"].includes(language)) {
724
+ (comp.properties || [])
725
+ .filter((v) => v.name === "internal:ImportedSymbols")
726
+ .forEach((v) => {
727
+ purlImportsMap[comp.purl] = (
728
+ purlImportsMap[comp.purl] || []
729
+ ).concat((v.value || "").split("|").filter(Boolean));
730
+ });
731
+ }
627
732
  }
628
733
  if (comp.evidence.occurrences) {
629
734
  // Keep the `location#line` shape so a re-run over a cdxgen BOM does not
@@ -654,6 +759,65 @@ function usableSlicesFile(slicesFile) {
654
759
  return fileSizeInBytes > 1024;
655
760
  }
656
761
 
762
+ /**
763
+ * Load a semantics slices file for reuse when it belongs to this project.
764
+ *
765
+ * `--semantics-slices-file` defaults to `semantics.slices.json` in the working
766
+ * directory, which is shared by every project scanned from there and also
767
+ * receives the reports of other analyzers (dosai, rusi, golem). A file is
768
+ * only reused when its shape matches the language. Swift slices must also
769
+ * describe the same project directory and must not predate the input SBOM:
770
+ * `cdxgen --evidence` writes a fresh SBOM before evinse runs, so an older
771
+ * slice was made from an earlier state of the sources and is regenerated.
772
+ * Scala slices cannot be regenerated by evinse and are reused whenever their
773
+ * shape matches.
774
+ *
775
+ * @param {string} language Project language
776
+ * @param {string} slicesFile Candidate semantics slices file
777
+ * @param {string} bomFile Input SBOM
778
+ * @param {string} dirPath Project directory
779
+ * @returns {undefined|Object} Parsed slice when it can be reused
780
+ */
781
+ export function loadReusableSemanticsSlice(
782
+ language,
783
+ slicesFile,
784
+ bomFile,
785
+ dirPath,
786
+ ) {
787
+ if (!usableSlicesFile(slicesFile)) {
788
+ return undefined;
789
+ }
790
+ let parsed;
791
+ try {
792
+ parsed = readJsonFile(slicesFile);
793
+ } catch (_e) {
794
+ return undefined;
795
+ }
796
+ if (!parsed || typeof parsed !== "object") {
797
+ return undefined;
798
+ }
799
+ if (!PROJECT_TYPE_ALIASES.swift.includes(language)) {
800
+ // Scala semantics slices are keyed by .scala source file names
801
+ return Object.keys(parsed).some((key) => key.endsWith(".scala"))
802
+ ? parsed
803
+ : undefined;
804
+ }
805
+ if (!(parsed.fileIndexes || parsed.buildSymbols || parsed.moduleInfos)) {
806
+ return undefined;
807
+ }
808
+ if (parsed.projectPath && resolve(parsed.projectPath) !== resolve(dirPath)) {
809
+ return undefined;
810
+ }
811
+ try {
812
+ if (fs.statSync(slicesFile).mtimeMs < fs.statSync(bomFile).mtimeMs) {
813
+ return undefined;
814
+ }
815
+ } catch (_e) {
816
+ // Without timestamps the shape and project checks decide
817
+ }
818
+ return parsed;
819
+ }
820
+
657
821
  /**
658
822
  * Function to analyze the project
659
823
  *
@@ -826,14 +990,23 @@ export async function analyzeProject(dbObjMap, options) {
826
990
  options.usagesSlicesFile &&
827
991
  usableSlicesFile(options.usagesSlicesFile)
828
992
  ) {
829
- usageSlice = JSON.parse(
830
- fs.readFileSync(options.usagesSlicesFile, "utf-8"),
831
- );
993
+ usageSlice = readDosaiMethodsReport(options.usagesSlicesFile);
832
994
  usagesSlicesFile = options.usagesSlicesFile;
995
+ } else if (usableSlicesFile(options.depsSlicesFile)) {
996
+ // createCsharpBom already ran dosai methods on this tree for --deep runs
997
+ // and wrote the deps slice. For dosai, that output IS the usages slice,
998
+ // so reuse it instead of invoking dosai a second time.
999
+ if (DEBUG_MODE) {
1000
+ console.log(
1001
+ `Reusing the dosai deps slices file "${options.depsSlicesFile}" for the usages analysis.`,
1002
+ );
1003
+ }
1004
+ usageSlice = readDosaiMethodsReport(options.depsSlicesFile);
1005
+ usagesSlicesFile = options.depsSlicesFile;
833
1006
  } else {
834
1007
  retMap = await createSlice(language, dirPath, "usages", options);
835
1008
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
836
- usageSlice = JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8"));
1009
+ usageSlice = readDosaiMethodsReport(retMap.slicesFile);
837
1010
  usagesSlicesFile = retMap.slicesFile;
838
1011
  }
839
1012
  }
@@ -867,16 +1040,12 @@ export async function analyzeProject(dbObjMap, options) {
867
1040
  safeExistsSync(options.dataFlowSlicesFile)
868
1041
  ) {
869
1042
  dataFlowSlicesFile = options.dataFlowSlicesFile;
870
- dataFlowSlice = JSON.parse(
871
- fs.readFileSync(options.dataFlowSlicesFile, "utf-8"),
872
- );
1043
+ dataFlowSlice = readDosaiDataFlowReport(options.dataFlowSlicesFile);
873
1044
  } else {
874
1045
  retMap = await createSlice(language, dirPath, "data-flow", options);
875
1046
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
876
1047
  dataFlowSlicesFile = retMap.slicesFile;
877
- dataFlowSlice = JSON.parse(
878
- fs.readFileSync(retMap.slicesFile, "utf-8"),
879
- );
1048
+ dataFlowSlice = readDosaiDataFlowReport(retMap.slicesFile);
880
1049
  }
881
1050
  }
882
1051
  if (dataFlowSlice && Object.keys(dataFlowSlice).length) {
@@ -923,7 +1092,7 @@ export async function analyzeProject(dbObjMap, options) {
923
1092
  ) {
924
1093
  reachablesSlicesFile = options.reachablesSlicesFile;
925
1094
  reachablesSlice = filterAtomSliceData(
926
- JSON.parse(fs.readFileSync(options.reachablesSlicesFile, "utf-8")),
1095
+ readReachablesSlices(options.reachablesSlicesFile),
927
1096
  options,
928
1097
  );
929
1098
  } else {
@@ -931,7 +1100,7 @@ export async function analyzeProject(dbObjMap, options) {
931
1100
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
932
1101
  reachablesSlicesFile = retMap.slicesFile;
933
1102
  reachablesSlice = filterAtomSliceData(
934
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1103
+ readReachablesSlices(retMap.slicesFile),
935
1104
  options,
936
1105
  );
937
1106
  }
@@ -947,7 +1116,7 @@ export async function analyzeProject(dbObjMap, options) {
947
1116
  // Reuse existing usages slices
948
1117
  if (options.usagesSlicesFile && usableSlicesFile(options.usagesSlicesFile)) {
949
1118
  usageSlice = filterAtomSliceData(
950
- JSON.parse(fs.readFileSync(options.usagesSlicesFile, "utf-8")),
1119
+ readSlicesFile(options.usagesSlicesFile, "usages"),
951
1120
  options,
952
1121
  );
953
1122
  usagesSlicesFile = options.usagesSlicesFile;
@@ -956,7 +1125,7 @@ export async function analyzeProject(dbObjMap, options) {
956
1125
  retMap = await createSlice(language, dirPath, "usages", options);
957
1126
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
958
1127
  usageSlice = filterAtomSliceData(
959
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1128
+ readSlicesFile(retMap.slicesFile, "usages"),
960
1129
  options,
961
1130
  );
962
1131
  usagesSlicesFile = retMap.slicesFile;
@@ -982,21 +1151,20 @@ export async function analyzeProject(dbObjMap, options) {
982
1151
  components.length
983
1152
  ) {
984
1153
  // Reuse existing semantics slices for swift and scala
985
- if (
986
- options.semanticsSlicesFile &&
987
- safeExistsSync(options.semanticsSlicesFile)
988
- ) {
989
- semanticsSlice = JSON.parse(
990
- fs.readFileSync(options.semanticsSlicesFile, "utf-8"),
991
- );
1154
+ const reusableSlice = loadReusableSemanticsSlice(
1155
+ language,
1156
+ options.semanticsSlicesFile,
1157
+ bomFile,
1158
+ dirPath,
1159
+ );
1160
+ if (reusableSlice) {
1161
+ semanticsSlice = reusableSlice;
992
1162
  semanticsSlicesFile = options.semanticsSlicesFile;
993
1163
  } else if (PROJECT_TYPE_ALIASES.swift.includes(language)) {
994
1164
  // Generate our own slices for swift
995
1165
  retMap = await createSlice(language, dirPath, "semantics", options);
996
1166
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
997
- semanticsSlice = JSON.parse(
998
- fs.readFileSync(retMap.slicesFile, "utf-8"),
999
- );
1167
+ semanticsSlice = readSlicesFile(retMap.slicesFile, "semantics");
1000
1168
  semanticsSlicesFile = retMap.slicesFile;
1001
1169
  }
1002
1170
  }
@@ -1033,7 +1201,7 @@ export async function analyzeProject(dbObjMap, options) {
1033
1201
  ) {
1034
1202
  dataFlowSlicesFile = options.dataFlowSlicesFile;
1035
1203
  dataFlowSlice = filterAtomSliceData(
1036
- JSON.parse(fs.readFileSync(options.dataFlowSlicesFile, "utf-8")),
1204
+ readSlicesFile(options.dataFlowSlicesFile, "data-flow"),
1037
1205
  options,
1038
1206
  );
1039
1207
  } else if (!PROJECT_TYPE_ALIASES.scala.includes(language)) {
@@ -1041,7 +1209,7 @@ export async function analyzeProject(dbObjMap, options) {
1041
1209
  if (retMap?.slicesFile && safeExistsSync(retMap.slicesFile)) {
1042
1210
  dataFlowSlicesFile = retMap.slicesFile;
1043
1211
  dataFlowSlice = filterAtomSliceData(
1044
- JSON.parse(fs.readFileSync(retMap.slicesFile, "utf-8")),
1212
+ readSlicesFile(retMap.slicesFile, "data-flow"),
1045
1213
  options,
1046
1214
  );
1047
1215
  }
@@ -1400,6 +1568,14 @@ export async function parseSliceUsages(
1400
1568
  /**
1401
1569
  * Method to parse semantic slice data. Currently supported for swift and scala languages.
1402
1570
  *
1571
+ * Swift slices created by cdxgen record, for every indexed source file, the
1572
+ * lines that reference each dependency module (`moduleReferences`); the module
1573
+ * of a reference is the declaring module recovered from the compiler-resolved
1574
+ * USR. Components are mapped to their package's modules through the
1575
+ * `packages` slice, so every occurrence is backed by a resolved reference.
1576
+ * Slices without `moduleReferences` (created by older cdxgen versions) fall
1577
+ * back to matching symbol names.
1578
+ *
1403
1579
  * @param {String} language Project language.
1404
1580
  * @param {Array} components Components from the input SBOM
1405
1581
  * @param {Object} semanticsSlice Semantic slice data
@@ -1413,86 +1589,47 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
1413
1589
  if (!components) {
1414
1590
  return undefined;
1415
1591
  }
1416
- const componentNamePurlMap = {};
1417
- const componentSymbolsMap = {};
1418
- const allObfuscationsMap = {};
1419
- // We have two attributes in the semantics slice to expand a given module to its constituent symbols
1420
- // - A less precise buildSymbols, which is obtained by parsing the various output-file-map.json files
1421
- // - A granular and precise moduleInfos, which has the exact classes, protocols, enums etc belonging to each module
1422
- // The objective then is to build the purlLocationMap, which is the list of locations where a given purl is used
1423
- // For this, we have two attributes:
1424
- // - A simpler fileStructures attribute that has the mapping between a swift file and the list of referenced types
1425
- // - A granular fileIndexes attribute that contains information about the clang modules and line number information
1426
-
1427
- // We first need to map out the component names to their purls
1428
- // This is because the semantics slice use the module names everywhere
1592
+ const fileIndexes = semanticsSlice?.fileIndexes || {};
1593
+ const hasModuleReferences = Object.values(fileIndexes).some(
1594
+ (afileIndex) => afileIndex?.moduleReferences,
1595
+ );
1596
+ const purlLocationsSet = {};
1429
1597
  for (const comp of components) {
1430
- componentNamePurlMap[comp.name] = comp.purl;
1431
- if (!componentSymbolsMap[comp.name]) {
1432
- componentSymbolsMap[comp.name] = new Set();
1433
- }
1434
- if (semanticsSlice?.buildSymbols[comp.name]) {
1435
- for (const asym of semanticsSlice.buildSymbols[comp.name]) {
1436
- componentSymbolsMap[comp.name].add(asym);
1437
- }
1438
- }
1439
- const moduleInfo = semanticsSlice?.moduleInfos[comp.name] || {};
1440
- if (moduleInfo.classes) {
1441
- for (const asym of moduleInfo.classes) {
1442
- componentSymbolsMap[comp.name].add(asym);
1443
- }
1444
- }
1445
- if (moduleInfo.protocols) {
1446
- for (const asym of moduleInfo.protocols) {
1447
- componentSymbolsMap[comp.name].add(asym);
1448
- }
1449
- }
1450
- if (moduleInfo.enums) {
1451
- for (const asym of moduleInfo.enums) {
1452
- componentSymbolsMap[comp.name].add(asym);
1453
- }
1598
+ // Local packages and the root application have no purl, so there is
1599
+ // nothing the evidence could be attached to downstream
1600
+ if (!comp?.purl) {
1601
+ continue;
1454
1602
  }
1455
- // Now collect the method signatures from class and protocol methods
1456
- if (moduleInfo.classMethods) {
1457
- for (const aclassName of Object.keys(moduleInfo.classMethods)) {
1458
- for (const asym of moduleInfo.classMethods[aclassName]) {
1459
- componentSymbolsMap[comp.name].add(asym);
1460
- }
1461
- }
1603
+ const moduleNames = resolveSwiftComponentModules(comp, semanticsSlice);
1604
+ if (!moduleNames.size) {
1605
+ continue;
1462
1606
  }
1463
- if (moduleInfo.protocolMethods) {
1464
- for (const aclassName of Object.keys(moduleInfo.protocolMethods)) {
1465
- for (const asym of moduleInfo.protocolMethods[aclassName]) {
1466
- componentSymbolsMap[comp.name].add(asym);
1607
+ const locations = purlLocationsSet[comp.purl] || new Set();
1608
+ if (hasModuleReferences) {
1609
+ for (const [aswiftFile, afileIndex] of Object.entries(fileIndexes)) {
1610
+ for (const [amodule, lines] of Object.entries(
1611
+ afileIndex?.moduleReferences || {},
1612
+ )) {
1613
+ if (!moduleNames.has(amodule)) {
1614
+ continue;
1615
+ }
1616
+ for (const aline of lines) {
1617
+ locations.add(`${aswiftFile}#${aline}`);
1618
+ }
1467
1619
  }
1468
1620
  }
1469
- }
1470
- // Build a large obfuscation map
1471
- if (moduleInfo.obfuscationMap) {
1472
- for (const asym of Object.keys(moduleInfo.obfuscationMap)) {
1473
- allObfuscationsMap[asym] = moduleInfo.obfuscationMap[asym];
1474
- }
1475
- }
1476
- }
1477
- const purlLocationsSet = {};
1478
- // We now have a good set of data in componentSymbolsMap and allObfuscationsMap
1479
- // We can iterate these symbols and check if they exist under fileIndexes.symbolLocations
1480
- for (const compName of Object.keys(componentSymbolsMap)) {
1481
- const compSymbols = Array.from(componentSymbolsMap[compName]);
1482
- const searchHits = searchSymbolLocations(
1483
- compSymbols,
1484
- semanticsSlice?.fileIndexes,
1485
- );
1486
- // We have an occurrence hit. Let's populate the purlLocationMap
1487
- if (searchHits?.length) {
1488
- const locations =
1489
- purlLocationsSet[componentNamePurlMap[compName]] || new Set();
1490
- for (const ahit of searchHits) {
1621
+ } else {
1622
+ for (const ahit of searchSymbolLocations(
1623
+ collectLegacySwiftSymbols(moduleNames, semanticsSlice),
1624
+ fileIndexes,
1625
+ )) {
1491
1626
  for (const aline of ahit.lineNumbers) {
1492
1627
  locations.add(`${ahit.file}#${aline}`);
1493
1628
  }
1494
1629
  }
1495
- purlLocationsSet[componentNamePurlMap[compName]] = locations;
1630
+ }
1631
+ if (locations.size) {
1632
+ purlLocationsSet[comp.purl] = locations;
1496
1633
  }
1497
1634
  }
1498
1635
  const purlLocationMap = {};
@@ -1502,22 +1639,184 @@ export function parseSemanticSlices(language, components, semanticsSlice) {
1502
1639
  return { purlLocationMap };
1503
1640
  }
1504
1641
 
1505
- function searchSymbolLocations(compSymbols, fileIndexes) {
1506
- const searchHits = [];
1507
- if (!fileIndexes) {
1642
+ /**
1643
+ * Normalise a repository location or Swift purl into `host/owner/name`.
1644
+ *
1645
+ * @param {String} value Repository URL, scp-style git location, or swift purl
1646
+ * @returns {undefined|String} Lowercase `host/path` without scheme and `.git`
1647
+ */
1648
+ function normalizeSwiftPackageLocation(value) {
1649
+ if (!value || typeof value !== "string") {
1508
1650
  return undefined;
1509
1651
  }
1510
- for (const aswiftFile of Object.keys(fileIndexes)) {
1511
- if (!fileIndexes[aswiftFile].symbolLocations) {
1652
+ let text = value.trim();
1653
+ if (text.startsWith("pkg:swift/")) {
1654
+ text = text.slice("pkg:swift/".length).split("?")[0].split("#")[0];
1655
+ const at = text.lastIndexOf("@");
1656
+ if (at > 0) {
1657
+ text = text.slice(0, at);
1658
+ }
1659
+ try {
1660
+ text = decodeURIComponent(text);
1661
+ } catch (_e) {
1662
+ // Keep the raw value
1663
+ }
1664
+ } else {
1665
+ text = text.split("?")[0].split("#")[0];
1666
+ const scheme = text.indexOf("://");
1667
+ if (scheme >= 0) {
1668
+ text = text.slice(scheme + 3);
1669
+ // Drop userinfo such as git@
1670
+ const slash = text.indexOf("/");
1671
+ const at = text.indexOf("@");
1672
+ if (at >= 0 && (slash < 0 || at < slash)) {
1673
+ text = text.slice(at + 1);
1674
+ }
1675
+ } else if (text.includes("@") && text.includes(":")) {
1676
+ // scp-style git@github.com:owner/name.git
1677
+ text = text.slice(text.indexOf("@") + 1).replace(":", "/");
1678
+ } else {
1679
+ return undefined;
1680
+ }
1681
+ }
1682
+ text = text.replace(/\/+$/, "");
1683
+ if (text.endsWith(".git")) {
1684
+ text = text.slice(0, -4);
1685
+ }
1686
+ if (text.startsWith("www.")) {
1687
+ text = text.slice(4);
1688
+ }
1689
+ return text.toLowerCase() || undefined;
1690
+ }
1691
+
1692
+ /**
1693
+ * Resolve the Swift module names that belong to a SBOM component.
1694
+ *
1695
+ * A component is named after its package repository or identity while the
1696
+ * semantic slices are keyed by module names. With the `packages` slice, a
1697
+ * package cloned from a repository matches the component whose purl names
1698
+ * the same repository, so a fork with the same name never inherits another
1699
+ * package's evidence; registry and local packages, which have no repository
1700
+ * URL, match by identity or name. Older slices only carry the
1701
+ * `packageModules` aliases (identity, package name, repository name), which
1702
+ * are matched against the component name and the `cdx:swift:packageName`
1703
+ * property, so that `swift-argument-parser` resolves to its `ArgumentParser`
1704
+ * module.
1705
+ *
1706
+ * @param {Object} comp SBOM component
1707
+ * @param {Object} semanticsSlice Semantic slice data
1708
+ * @returns {Set<string>} Module names associated with the component
1709
+ */
1710
+ function resolveSwiftComponentModules(comp, semanticsSlice) {
1711
+ const moduleNames = new Set();
1712
+ if (!comp?.name) {
1713
+ return moduleNames;
1714
+ }
1715
+ const aliases = new Set(
1716
+ [comp.name, getPropertyValue(comp, "cdx:swift:packageName")]
1717
+ .filter(Boolean)
1718
+ .map((alias) => String(alias).toLowerCase()),
1719
+ );
1720
+ if (Array.isArray(semanticsSlice?.packages)) {
1721
+ const componentLocation = normalizeSwiftPackageLocation(comp.purl);
1722
+ for (const apkg of semanticsSlice.packages) {
1723
+ const packageLocation = normalizeSwiftPackageLocation(apkg?.location);
1724
+ const matches =
1725
+ packageLocation && componentLocation
1726
+ ? packageLocation === componentLocation
1727
+ : [apkg?.identity, apkg?.name].some(
1728
+ (alias) => alias && aliases.has(String(alias).toLowerCase()),
1729
+ );
1730
+ if (matches) {
1731
+ for (const amodule of apkg.modules || []) {
1732
+ moduleNames.add(amodule);
1733
+ }
1734
+ }
1735
+ }
1736
+ return moduleNames;
1737
+ }
1738
+ const packageModules = semanticsSlice?.packageModules || {};
1739
+ for (const alias of new Set([comp.name, ...aliases])) {
1740
+ if (Array.isArray(packageModules[alias])) {
1741
+ for (const amodule of packageModules[alias]) {
1742
+ moduleNames.add(amodule);
1743
+ }
1744
+ }
1745
+ }
1746
+ // Modules named after the package (Yams, SWXMLHash) still match
1747
+ if (!moduleNames.size) {
1748
+ moduleNames.add(comp.name);
1749
+ }
1750
+ return moduleNames;
1751
+ }
1752
+
1753
+ function getPropertyValue(comp, propertyName) {
1754
+ for (const aproperty of comp?.properties || []) {
1755
+ if (aproperty?.name === propertyName) {
1756
+ return aproperty.value;
1757
+ }
1758
+ }
1759
+ return undefined;
1760
+ }
1761
+
1762
+ /**
1763
+ * Collect the symbol names of a component's modules from slices that predate
1764
+ * `moduleReferences`, using the moduleInfos data and the coarser buildSymbols
1765
+ * derived from the build's output file maps.
1766
+ *
1767
+ * @param {Set<String>} moduleNames Module names of the component
1768
+ * @param {Object} semanticsSlice Semantic slice data
1769
+ * @returns {Object} `{ symbols, freeFunctions }` name sets
1770
+ */
1771
+ function collectLegacySwiftSymbols(moduleNames, semanticsSlice) {
1772
+ const symbols = new Set();
1773
+ const freeFunctions = new Set();
1774
+ for (const moduleName of moduleNames) {
1775
+ for (const asym of semanticsSlice?.buildSymbols?.[moduleName] || []) {
1776
+ symbols.add(asym);
1777
+ }
1778
+ const moduleInfo = semanticsSlice?.moduleInfos?.[moduleName] || {};
1779
+ for (const asym of [
1780
+ ...(moduleInfo.classes || []),
1781
+ ...(moduleInfo.protocols || []),
1782
+ ...(moduleInfo.enums || []),
1783
+ ]) {
1784
+ symbols.add(asym);
1785
+ }
1786
+ for (const methods of [
1787
+ ...Object.values(moduleInfo.classMethods || {}),
1788
+ ...Object.values(moduleInfo.protocolMethods || {}),
1789
+ ]) {
1790
+ for (const asym of methods || []) {
1791
+ symbols.add(asym);
1792
+ }
1793
+ }
1794
+ for (const asym of moduleInfo.functions || []) {
1795
+ freeFunctions.add(asym);
1796
+ }
1797
+ }
1798
+ return { symbols, freeFunctions };
1799
+ }
1800
+
1801
+ function searchSymbolLocations(componentSymbols, fileIndexes) {
1802
+ const searchHits = [];
1803
+ const { symbols, freeFunctions } = componentSymbols;
1804
+ for (const aswiftFile of Object.keys(fileIndexes || {})) {
1805
+ const symbolLocations = fileIndexes[aswiftFile]?.symbolLocations;
1806
+ if (!symbolLocations) {
1512
1807
  continue;
1513
1808
  }
1514
- for (const asym of Object.keys(fileIndexes[aswiftFile].symbolLocations)) {
1515
- const lineNumbers = fileIndexes[aswiftFile].symbolLocations[asym];
1516
- if (compSymbols.includes(asym)) {
1809
+ for (const asym of Object.keys(symbolLocations)) {
1810
+ // The index records free functions by their full selector
1811
+ // (`dump(object:canonical:...)`) while module interfaces give the base
1812
+ // name. Only top-level functions are matched by base name: members such
1813
+ // as `encode(to:)` exist in nearly every module
1814
+ const baseName = asym.includes("(") ? asym.split("(")[0] : undefined;
1815
+ if (symbols.has(asym) || (baseName && freeFunctions.has(baseName))) {
1517
1816
  searchHits.push({
1518
1817
  file: aswiftFile,
1519
1818
  symbol: asym,
1520
- lineNumbers,
1819
+ lineNumbers: symbolLocations[asym],
1521
1820
  });
1522
1821
  }
1523
1822
  }
@@ -1950,7 +2249,10 @@ export async function createEvinseFile(sliceArtefacts, options) {
1950
2249
  bomJson.metadata.component = workspaceComponent;
1951
2250
  }
1952
2251
  }
1953
- appendComponentProperties(bomJson.metadata?.component, metadataProperties);
2252
+ mergeAnalyzerMetadataProperties(
2253
+ bomJson.metadata?.component,
2254
+ metadataProperties,
2255
+ );
1954
2256
  let occEvidencePresent = false;
1955
2257
  let csEvidencePresent = false;
1956
2258
  let servicesPresent = false;
@@ -2009,8 +2311,15 @@ export async function createEvinseFile(sliceArtefacts, options) {
2009
2311
  servicesPresent = true;
2010
2312
  }
2011
2313
  // Add the crypto components to the components list
2314
+ // Multi-type scans may have collected the same crypto assets during BOM
2315
+ // generation, so skip any bom-ref already present to keep the BOM valid.
2012
2316
  if (cryptoComponents?.length) {
2013
- bomJson.components = bomJson.components.concat(cryptoComponents);
2317
+ const existingRefs = new Set(
2318
+ bomJson.components.map((comp) => comp["bom-ref"]).filter(Boolean),
2319
+ );
2320
+ bomJson.components = bomJson.components.concat(
2321
+ cryptoComponents.filter((comp) => !existingRefs.has(comp["bom-ref"])),
2322
+ );
2014
2323
  }
2015
2324
  // Add the AI components (machine-learning-model, dataset) discovered by dosai
2016
2325
  if (aiComponents?.length) {
@@ -2035,28 +2344,27 @@ export async function createEvinseFile(sliceArtefacts, options) {
2035
2344
  bomJson.dependencies = newDependencies;
2036
2345
  }
2037
2346
  if (options.annotate) {
2038
- if (usagesSlicesFile && safeExistsSync(usagesSlicesFile)) {
2039
- bomJson.annotations.push({
2040
- subjects: [bomJson.serialNumber],
2041
- annotator: { component: bomJson.metadata.tools.components[0] },
2042
- timestamp: getTimestamp(),
2043
- text: fs.readFileSync(usagesSlicesFile, "utf8"),
2044
- });
2045
- }
2046
- if (dataFlowSlicesFile && safeExistsSync(dataFlowSlicesFile)) {
2047
- bomJson.annotations.push({
2048
- subjects: [bomJson.serialNumber],
2049
- annotator: { component: bomJson.metadata.tools.components[0] },
2050
- timestamp: getTimestamp(),
2051
- text: fs.readFileSync(dataFlowSlicesFile, "utf8"),
2052
- });
2053
- }
2054
- if (reachablesSlicesFile && safeExistsSync(reachablesSlicesFile)) {
2347
+ for (const slicesFile of [
2348
+ usagesSlicesFile,
2349
+ dataFlowSlicesFile,
2350
+ reachablesSlicesFile,
2351
+ ]) {
2352
+ if (!slicesFile || !safeExistsSync(slicesFile)) {
2353
+ continue;
2354
+ }
2355
+ // The annotation embeds the slice as one string, which a file this
2356
+ // large cannot become.
2357
+ if (fs.statSync(slicesFile).size > MAX_JSON_TEXT_BYTES) {
2358
+ console.warn(
2359
+ `Not annotating the BOM with the slices file "${slicesFile}", which is too large to embed as text.`,
2360
+ );
2361
+ continue;
2362
+ }
2055
2363
  bomJson.annotations.push({
2056
2364
  subjects: [bomJson.serialNumber],
2057
2365
  annotator: { component: bomJson.metadata.tools.components[0] },
2058
2366
  timestamp: getTimestamp(),
2059
- text: fs.readFileSync(reachablesSlicesFile, "utf8"),
2367
+ text: fs.readFileSync(slicesFile, "utf8"),
2060
2368
  });
2061
2369
  }
2062
2370
  }