@cyclonedx/cdxgen 9.10.2 → 9.11.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.
package/bin/evinse.js CHANGED
@@ -63,7 +63,8 @@ const args = yargs(hideBin(process.argv))
63
63
  "python",
64
64
  "android",
65
65
  "c",
66
- "cpp"
66
+ "cpp",
67
+ "php"
67
68
  ]
68
69
  })
69
70
  .option("db-path", {
@@ -160,6 +160,31 @@
160
160
  "pkg:generic/userver",
161
161
  "pkg:generic/Wt/",
162
162
  "pkg:generic/klone",
163
- "pkg:generic/kcgi"
163
+ "pkg:generic/kcgi",
164
+ "pkg:composer/laravel",
165
+ "pkg:composer/symfony",
166
+ "pkg:composer/codeigniter",
167
+ "pkg:composer/cakephp",
168
+ "pkg:composer/zend",
169
+ "pkg:composer/yii",
170
+ "pkg:composer/aura",
171
+ "pkg:composer/phalcon",
172
+ "pkg:composer/fatfree",
173
+ "pkg:composer/qcodo",
174
+ "pkg:composer/fuelphp",
175
+ "pkg:composer/slim",
176
+ "pkg:composer/phpixie",
177
+ "pkg:composer/ice",
178
+ "pkg:composer/modx",
179
+ "pkg:composer/typo",
180
+ "pkg:composer/cappuccino",
181
+ "pkg:composer/pop",
182
+ "pkg:composer/zest",
183
+ "pkg:composer/silverstripe",
184
+ "pkg:composer/dash",
185
+ "pkg:composer/roducks",
186
+ "pkg:composer/queryphp",
187
+ "pkg:composer/silex",
188
+ "pkg:composer/psr"
164
189
  ]
165
190
  }
package/evinser.js CHANGED
@@ -207,7 +207,9 @@ export const createSlice = (
207
207
  if (!filePath) {
208
208
  return;
209
209
  }
210
- console.log(`Create ${sliceType} slice for ${purlOrLanguage} ${filePath}`);
210
+ console.log(
211
+ `Create ${sliceType} slice for ${path.resolve(filePath)}. Please wait ...`
212
+ );
211
213
  const language = purlOrLanguage.startsWith("pkg:")
212
214
  ? purlToLanguage(purlOrLanguage, filePath)
213
215
  : purlOrLanguage;
@@ -271,22 +273,35 @@ export const purlToLanguage = (purl, filePath) => {
271
273
  case "pypi":
272
274
  language = "python";
273
275
  break;
276
+ case "composer":
277
+ language = "php";
278
+ break;
279
+ case "generic":
280
+ language = "c";
274
281
  }
275
282
  return language;
276
283
  };
277
284
 
278
- export const initFromSbom = (components) => {
285
+ export const initFromSbom = (components, language) => {
279
286
  const purlLocationMap = {};
280
287
  const purlImportsMap = {};
281
288
  for (const comp of components) {
282
289
  if (!comp || !comp.evidence) {
283
290
  continue;
284
291
  }
285
- (comp.properties || [])
286
- .filter((v) => v.name === "ImportedModules")
287
- .forEach((v) => {
288
- purlImportsMap[comp.purl] = (v.value || "").split(",");
289
- });
292
+ if (language === "php") {
293
+ (comp.properties || [])
294
+ .filter((v) => v.name === "Namespaces")
295
+ .forEach((v) => {
296
+ purlImportsMap[comp.purl] = (v.value || "").split(", ");
297
+ });
298
+ } else {
299
+ (comp.properties || [])
300
+ .filter((v) => v.name === "ImportedModules")
301
+ .forEach((v) => {
302
+ purlImportsMap[comp.purl] = (v.value || "").split(",");
303
+ });
304
+ }
290
305
  if (comp.evidence.occurrences) {
291
306
  purlLocationMap[comp.purl] = new Set(
292
307
  comp.evidence.occurrences.map((v) => v.location)
@@ -323,7 +338,7 @@ export const analyzeProject = async (dbObjMap, options) => {
323
338
  const components = bomJson.components || [];
324
339
  // Load any existing purl-location information from the sbom.
325
340
  // For eg: cdxgen populates this information for javascript projects
326
- let { purlLocationMap, purlImportsMap } = initFromSbom(components);
341
+ let { purlLocationMap, purlImportsMap } = initFromSbom(components, language);
327
342
  // Do reachables first so that usages slicing can reuse the atom file
328
343
  if (options.withReachables) {
329
344
  if (
@@ -487,6 +502,11 @@ export const parseSliceUsages = async (
487
502
  typesToLookup.add(slice.fullName);
488
503
  addToOverrides(lKeyOverrides, slice.fullName, fileName, slice.lineNumber);
489
504
  }
505
+ // PHP imports from usages
506
+ if (slice.code && slice.code.startsWith("use") && !usages.length) {
507
+ typesToLookup.add(slice.fullName);
508
+ addToOverrides(lKeyOverrides, slice.fullName, fileName, slice.lineNumber);
509
+ }
490
510
  for (const ausage of usages) {
491
511
  const ausageLine =
492
512
  ausage?.targetObj?.lineNumber || ausage?.definedBy?.lineNumber;
@@ -616,12 +636,25 @@ export const parseSliceUsages = async (
616
636
  if (purlImportsMap && Object.keys(purlImportsMap).length) {
617
637
  for (const apurl of Object.keys(purlImportsMap)) {
618
638
  const apurlImports = purlImportsMap[apurl];
619
- if (apurlImports && apurlImports.includes(atype)) {
620
- if (!purlLocationMap[apurl]) {
621
- purlLocationMap[apurl] = new Set();
639
+ if (language === "php") {
640
+ for (const aimp of apurlImports) {
641
+ if (atype.startsWith(aimp)) {
642
+ if (!purlLocationMap[apurl]) {
643
+ purlLocationMap[apurl] = new Set();
644
+ }
645
+ if (lKeyOverrides[atype]) {
646
+ purlLocationMap[apurl].add(...lKeyOverrides[atype]);
647
+ }
648
+ }
622
649
  }
623
- if (lKeyOverrides[atype]) {
624
- purlLocationMap[apurl].add(...lKeyOverrides[atype]);
650
+ } else {
651
+ if (apurlImports && apurlImports.includes(atype)) {
652
+ if (!purlLocationMap[apurl]) {
653
+ purlLocationMap[apurl] = new Set();
654
+ }
655
+ if (lKeyOverrides[atype]) {
656
+ purlLocationMap[apurl].add(...lKeyOverrides[atype]);
657
+ }
625
658
  }
626
659
  }
627
660
  }
@@ -715,6 +748,11 @@ export const isFilterableType = (
715
748
  return true;
716
749
  }
717
750
  }
751
+ if (["php"].includes(language)) {
752
+ if (!typeFullName.includes("\\") && !typeFullName.startsWith("use")) {
753
+ return true;
754
+ }
755
+ }
718
756
  if (userDefinedTypesMap[typeFullName]) {
719
757
  return true;
720
758
  }
@@ -739,12 +777,16 @@ export const detectServicesFromUsages = (language, slice, servicesMap = {}) => {
739
777
  let endpoints = [];
740
778
  let authenticated = undefined;
741
779
  if (targetObj && targetObj?.resolvedMethod) {
742
- endpoints = extractEndpoints(language, targetObj?.resolvedMethod);
780
+ if (language != "php") {
781
+ endpoints = extractEndpoints(language, targetObj?.resolvedMethod);
782
+ }
743
783
  if (targetObj?.resolvedMethod.toLowerCase().includes("auth")) {
744
784
  authenticated = true;
745
785
  }
746
786
  } else if (definedBy && definedBy?.resolvedMethod) {
747
- endpoints = extractEndpoints(language, definedBy?.resolvedMethod);
787
+ if (language != "php") {
788
+ endpoints = extractEndpoints(language, definedBy?.resolvedMethod);
789
+ }
748
790
  if (definedBy?.resolvedMethod.toLowerCase().includes("auth")) {
749
791
  authenticated = true;
750
792
  }
@@ -752,12 +794,17 @@ export const detectServicesFromUsages = (language, slice, servicesMap = {}) => {
752
794
  if (usage.invokedCalls) {
753
795
  for (const acall of usage.invokedCalls) {
754
796
  if (acall.resolvedMethod) {
755
- const tmpEndpoints = extractEndpoints(language, acall.resolvedMethod);
756
- if (acall.resolvedMethod.toLowerCase().includes("auth")) {
757
- authenticated = true;
758
- }
759
- if (tmpEndpoints && tmpEndpoints.length) {
760
- endpoints = (endpoints || []).concat(tmpEndpoints);
797
+ if (language != "php") {
798
+ const tmpEndpoints = extractEndpoints(
799
+ language,
800
+ acall.resolvedMethod
801
+ );
802
+ if (acall.resolvedMethod.toLowerCase().includes("auth")) {
803
+ authenticated = true;
804
+ }
805
+ if (tmpEndpoints && tmpEndpoints.length) {
806
+ endpoints = (endpoints || []).concat(tmpEndpoints);
807
+ }
761
808
  }
762
809
  }
763
810
  }
@@ -791,7 +838,7 @@ export const detectServicesFromUDT = (
791
838
  servicesMap
792
839
  ) => {
793
840
  if (
794
- ["python", "py", "c", "cpp", "c++"].includes(language) &&
841
+ ["python", "py", "c", "cpp", "c++", "php"].includes(language) &&
795
842
  userDefinedTypes &&
796
843
  userDefinedTypes.length
797
844
  ) {
@@ -803,7 +850,15 @@ export const detectServicesFromUDT = (
803
850
  audt.name.toLowerCase().includes("registerhandler") ||
804
851
  audt.name.toLowerCase().includes("endpoint") ||
805
852
  audt.name.toLowerCase().includes("api") ||
806
- audt.name.toLowerCase().includes("add_method")
853
+ audt.name.toLowerCase().includes("add_method") ||
854
+ audt.name.toLowerCase().includes("get") ||
855
+ audt.name.toLowerCase().includes("post") ||
856
+ audt.name.toLowerCase().includes("delete") ||
857
+ audt.name.toLowerCase().includes("put") ||
858
+ audt.name.toLowerCase().includes("head") ||
859
+ audt.name.toLowerCase().includes("options") ||
860
+ audt.name.toLowerCase().includes("addRoute") ||
861
+ audt.name.toLowerCase().includes("connect")
807
862
  ) {
808
863
  const fields = audt.fields || [];
809
864
  if (
@@ -819,14 +874,14 @@ export const detectServicesFromUDT = (
819
874
  audt.fileName.replace(".py", "")
820
875
  )}-service`;
821
876
  }
822
- if (!servicesMap[serviceName]) {
823
- servicesMap[serviceName] = {
824
- endpoints: new Set(),
825
- authenticated: false,
826
- xTrustBoundary: undefined
827
- };
828
- }
829
- if (endpoints) {
877
+ if (endpoints && endpoints.length) {
878
+ if (!servicesMap[serviceName]) {
879
+ servicesMap[serviceName] = {
880
+ endpoints: new Set(),
881
+ authenticated: false,
882
+ xTrustBoundary: undefined
883
+ };
884
+ }
830
885
  for (const endpoint of endpoints) {
831
886
  servicesMap[serviceName].endpoints.add(endpoint);
832
887
  }
@@ -897,7 +952,7 @@ export const extractEndpoints = (language, code) => {
897
952
  default:
898
953
  endpoints = (code.match(/['"](.*?)['"]/gi) || [])
899
954
  .map((v) => v.replace(/["']/g, "").replace("\n", ""))
900
- .filter((v) => v.length > 2);
955
+ .filter((v) => v.length > 2 && v.includes("/"));
901
956
  break;
902
957
  }
903
958
  return endpoints;
@@ -1015,7 +1070,7 @@ export const createEvinseFile = (sliceArtefacts, options) => {
1015
1070
  console.log(evinseOutFile, "created successfully.");
1016
1071
  } else {
1017
1072
  console.log(
1018
- "Unable to identify component evidence for the input SBOM. Only java, javascript and python projects are supported by evinse."
1073
+ "Unable to identify component evidence for the input SBOM. Only java, javascript, python, and php projects are supported by evinse."
1019
1074
  );
1020
1075
  }
1021
1076
  if (tempDir && tempDir.startsWith(tmpdir())) {
@@ -1249,6 +1304,8 @@ export const getClassTypeFromSignature = (language, typeFullName) => {
1249
1304
  .replace(".<body>", "")
1250
1305
  .replace(".__iter__", "")
1251
1306
  .replace(".__init__", "");
1307
+ } else if (["php"].includes(language)) {
1308
+ typeFullName = typeFullName.split("->")[0].split("::")[0];
1252
1309
  }
1253
1310
  if (
1254
1311
  typeFullName.startsWith("<unresolved") ||
package/index.js CHANGED
@@ -4055,6 +4055,11 @@ export const createPHPBom = (path, options) => {
4055
4055
  (options.multiProject ? "**/" : "") + "composer.json",
4056
4056
  options
4057
4057
  );
4058
+ if (!options.exclude) {
4059
+ options.exclude = [];
4060
+ }
4061
+ // Ignore vendor directories for lock files
4062
+ options.exclude.push("**/vendor/**");
4058
4063
  let composerLockFiles = getAllFiles(
4059
4064
  path,
4060
4065
  (options.multiProject ? "**/" : "") + "composer.lock",
@@ -4123,6 +4128,7 @@ export const createPHPBom = (path, options) => {
4123
4128
  if (DEBUG_MODE) {
4124
4129
  console.log(`Parsing ${f}`);
4125
4130
  }
4131
+ let rootRequires = [];
4126
4132
  // Is there a composer.json to find the parent component
4127
4133
  if (
4128
4134
  !Object.keys(parentComponent).length &&
@@ -4131,6 +4137,7 @@ export const createPHPBom = (path, options) => {
4131
4137
  const composerData = JSON.parse(
4132
4138
  readFileSync(join(basePath, "composer.json"), { encoding: "utf-8" })
4133
4139
  );
4140
+ rootRequires = composerData.require;
4134
4141
  const pkgName = composerData.name;
4135
4142
  if (pkgName) {
4136
4143
  parentComponent.group = dirname(pkgName);
@@ -4152,16 +4159,33 @@ export const createPHPBom = (path, options) => {
4152
4159
  );
4153
4160
  }
4154
4161
  }
4155
- const retMap = parseComposerLock(f);
4162
+ const retMap = parseComposerLock(f, rootRequires);
4156
4163
  if (retMap.pkgList && retMap.pkgList.length) {
4157
4164
  pkgList = pkgList.concat(retMap.pkgList);
4158
4165
  }
4159
4166
  if (retMap.dependenciesList) {
4167
+ if (!Object.keys(parentComponent).length) {
4168
+ parentComponent = createDefaultParentComponent(
4169
+ path,
4170
+ "composer",
4171
+ options
4172
+ );
4173
+ }
4174
+ // Complete the dependency tree by making parent component depend on the first level
4175
+ const parentDependsOn = [];
4176
+ for (const p of retMap.rootList) {
4177
+ parentDependsOn.push(p["bom-ref"]);
4178
+ }
4179
+ const pdependencies = {
4180
+ ref: parentComponent["bom-ref"],
4181
+ dependsOn: parentDependsOn
4182
+ };
4160
4183
  dependencies = mergeDependencies(
4161
4184
  dependencies,
4162
4185
  retMap.dependenciesList,
4163
4186
  parentComponent
4164
4187
  );
4188
+ dependencies.splice(0, 0, pdependencies);
4165
4189
  }
4166
4190
  }
4167
4191
  return buildBomNSData(options, pkgList, "composer", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyclonedx/cdxgen",
3
- "version": "9.10.2",
3
+ "version": "9.11.0",
4
4
  "description": "Creates CycloneDX Software Bill of Materials (SBOM) from source or container image",
5
5
  "homepage": "http://github.com/cyclonedx/cdxgen",
6
6
  "author": "Prabhu Subramanian <prabhu@appthreat.com>",
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@babel/parser": "^7.23.6",
59
- "@babel/traverse": "^7.23.6",
59
+ "@babel/traverse": "^7.23.7",
60
60
  "@npmcli/arborist": "7.2.2",
61
61
  "ajv": "^8.12.0",
62
62
  "ajv-formats": "^2.1.1",
@@ -83,7 +83,7 @@
83
83
  "yargs": "^17.7.2"
84
84
  },
85
85
  "optionalDependencies": {
86
- "@appthreat/atom": "1.8.0",
86
+ "@appthreat/atom": "1.8.3",
87
87
  "@appthreat/cdx-proto": "^0.0.4",
88
88
  "@cyclonedx/cdxgen-plugins-bin": "^1.5.4",
89
89
  "@cyclonedx/cdxgen-plugins-bin-windows-amd64": "^1.5.4",
@@ -94,7 +94,7 @@
94
94
  "connect": "^3.7.0",
95
95
  "jsonata": "^2.0.3",
96
96
  "sequelize": "^6.35.2",
97
- "sqlite3": "^5.1.6"
97
+ "sqlite3": "^5.1.7"
98
98
  },
99
99
  "files": [
100
100
  "*.js",
package/utils.js CHANGED
@@ -4059,6 +4059,9 @@ export const parseGemfileLockData = async function (gemLockData) {
4059
4059
  const tmpA = l.split(" ");
4060
4060
  if (tmpA && tmpA.length == 2) {
4061
4061
  const name = tmpA[0];
4062
+ if (name === "remote:") {
4063
+ return;
4064
+ }
4062
4065
  if (!pkgnames[name]) {
4063
4066
  let version = tmpA[1].split(", ")[0];
4064
4067
  version = version.replace(/[(>=<)~ ]/g, "");
@@ -4077,7 +4080,8 @@ export const parseGemfileLockData = async function (gemLockData) {
4077
4080
  l === "PLATFORMS" ||
4078
4081
  l === "DEPENDENCIES" ||
4079
4082
  l === "RUBY VERSION" ||
4080
- l === "BUNDLED WITH"
4083
+ l === "BUNDLED WITH" ||
4084
+ l === "PATH"
4081
4085
  ) {
4082
4086
  specsFound = false;
4083
4087
  }
@@ -5711,12 +5715,20 @@ export const parsePaketLockData = async function (paketLockData, pkgLockFile) {
5711
5715
  * Parse composer lock file
5712
5716
  *
5713
5717
  * @param {string} pkgLockFile composer.lock file
5718
+ * @param {array} rootRequires require section from composer.json
5714
5719
  */
5715
- export const parseComposerLock = function (pkgLockFile) {
5720
+ export const parseComposerLock = function (pkgLockFile, rootRequires) {
5716
5721
  const pkgList = [];
5717
5722
  const dependenciesList = [];
5718
5723
  const dependenciesMap = {};
5719
5724
  const pkgNamePurlMap = {};
5725
+ const rootList = [];
5726
+ const rootRequiresMap = {};
5727
+ if (rootRequires) {
5728
+ for (const rr of Object.keys(rootRequires)) {
5729
+ rootRequiresMap[rr] = true;
5730
+ }
5731
+ }
5720
5732
  if (existsSync(pkgLockFile)) {
5721
5733
  let lockData = {};
5722
5734
  try {
@@ -5741,6 +5753,7 @@ export const parseComposerLock = function (pkgLockFile) {
5741
5753
  if (!pkg || !pkg.name || !pkg.version) {
5742
5754
  continue;
5743
5755
  }
5756
+
5744
5757
  let group = dirname(pkg.name);
5745
5758
  if (group === ".") {
5746
5759
  group = "";
@@ -5789,7 +5802,7 @@ export const parseComposerLock = function (pkgLockFile) {
5789
5802
  for (const aaload of Object.keys(pkg.autoload)) {
5790
5803
  if (aaload.startsWith("psr")) {
5791
5804
  for (const ans of Object.keys(pkg.autoload[aaload])) {
5792
- namespaces.push(ans);
5805
+ namespaces.push(ans.trim());
5793
5806
  }
5794
5807
  }
5795
5808
  }
@@ -5803,6 +5816,10 @@ export const parseComposerLock = function (pkgLockFile) {
5803
5816
  pkgList.push(apkg);
5804
5817
  dependenciesMap[purl] = new Set();
5805
5818
  pkgNamePurlMap[pkg.name] = purl;
5819
+ // Add this package to the root list if needed
5820
+ if (rootRequiresMap[pkg.name]) {
5821
+ rootList.push(apkg);
5822
+ }
5806
5823
  }
5807
5824
  }
5808
5825
  // Pass 2: Construct dependency tree
@@ -5833,7 +5850,8 @@ export const parseComposerLock = function (pkgLockFile) {
5833
5850
  }
5834
5851
  return {
5835
5852
  pkgList,
5836
- dependenciesList
5853
+ dependenciesList,
5854
+ rootList
5837
5855
  };
5838
5856
  };
5839
5857
 
package/utils.test.js CHANGED
@@ -1214,7 +1214,7 @@ test("parse github actions workflow data", async () => {
1214
1214
  dep_list = parseGitHubWorkflowData(
1215
1215
  readFileSync("./.github/workflows/repotests.yml", { encoding: "utf-8" })
1216
1216
  );
1217
- expect(dep_list.length).toEqual(8);
1217
+ expect(dep_list.length).toEqual(7);
1218
1218
  expect(dep_list[0]).toEqual({
1219
1219
  group: "actions",
1220
1220
  name: "checkout",
@@ -1762,8 +1762,8 @@ test("parsePkgLock v3", async () => {
1762
1762
  projectName: "cdxgen"
1763
1763
  });
1764
1764
  deps = parsedList.pkgList;
1765
- expect(deps.length).toEqual(1195);
1766
- expect(parsedList.dependenciesList.length).toEqual(1195);
1765
+ expect(deps.length).toEqual(1199);
1766
+ expect(parsedList.dependenciesList.length).toEqual(1199);
1767
1767
  });
1768
1768
 
1769
1769
  test("parseBowerJson", async () => {