@cyclonedx/cdxgen 9.10.1 → 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/README.md CHANGED
@@ -22,7 +22,7 @@ Most SBOM tools are like barcode scanners. They can scan a few package manifest
22
22
  | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------- |
23
23
  | Node.js | npm-shrinkwrap.json, package-lock.json, pnpm-lock.yaml, yarn.lock, rush.js, bower.json, .min.js | Yes except .min.js | Yes |
24
24
  | Java | maven (pom.xml [1]), gradle (build.gradle, .kts), scala (sbt), bazel | Yes unless pom.xml is manually parsed due to unavailability of maven or errors | Yes |
25
- | PHP | composer.lock | Yes | |
25
+ | PHP | composer.lock | Yes | Yes |
26
26
  | Python | pyproject.toml, setup.py, requirements.txt [2], Pipfile.lock, poetry.lock, pdm.lock, bdist_wheel, .whl, .egg-info | Yes using the automatic pip install/freeze. When disabled, only with Pipfile.lock and poetry.lock | Yes |
27
27
  | Go | binary, go.mod, go.sum, Gopkg.lock | Yes except binary | Yes |
28
28
  | Ruby | Gemfile.lock, gemspec | Only for Gemfile.lock | |
@@ -347,6 +347,7 @@ cdxgen can retain the dependency tree under the `dependencies` attribute for a s
347
347
  - Python (requirements.txt, setup.py, pyproject.toml, poetry.lock)
348
348
  - .NET (project.assets.json, paket.lock)
349
349
  - Go (go.mod)
350
+ - PHP (composer.lock)
350
351
 
351
352
  ## Environment variables
352
353
 
package/bin/cdxgen.js CHANGED
@@ -220,6 +220,17 @@ const args = yargs(hideBin(process.argv))
220
220
  description: "Additional glob pattern(s) to ignore",
221
221
  hidden: true
222
222
  })
223
+ .option("export-proto", {
224
+ type: "boolean",
225
+ default: false,
226
+ description: "Serialize and export BOM as protobuf binary.",
227
+ hidden: true
228
+ })
229
+ .option("proto-bin-file", {
230
+ description: "Path for the serialized protobuf binary.",
231
+ default: "bom.cdx",
232
+ hidden: true
233
+ })
223
234
  .completion("completion", "Generate bash/zsh completion")
224
235
  .array("filter")
225
236
  .array("only")
@@ -582,7 +593,11 @@ const checkPermissions = (filePath) => {
582
593
  console.log(err);
583
594
  }
584
595
  }
585
-
596
+ // Protobuf serialization
597
+ if (options.exportProto) {
598
+ const protobomModule = await import("../protobom.js");
599
+ protobomModule.writeBinary(bomNSData.bomJson, options.protoBinFile);
600
+ }
586
601
  if (options.print && bomNSData.bomJson && bomNSData.bomJson.components) {
587
602
  printDependencyTree(bomNSData.bomJson);
588
603
  printTable(bomNSData.bomJson);
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
@@ -4048,11 +4048,18 @@ export const createContainerSpecLikeBom = async (path, options) => {
4048
4048
  * @param options Parse options from the cli
4049
4049
  */
4050
4050
  export const createPHPBom = (path, options) => {
4051
+ let dependencies = [];
4052
+ let parentComponent = {};
4051
4053
  const composerJsonFiles = getAllFiles(
4052
4054
  path,
4053
4055
  (options.multiProject ? "**/" : "") + "composer.json",
4054
4056
  options
4055
4057
  );
4058
+ if (!options.exclude) {
4059
+ options.exclude = [];
4060
+ }
4061
+ // Ignore vendor directories for lock files
4062
+ options.exclude.push("**/vendor/**");
4056
4063
  let composerLockFiles = getAllFiles(
4057
4064
  path,
4058
4065
  (options.multiProject ? "**/" : "") + "composer.lock",
@@ -4117,17 +4124,75 @@ export const createPHPBom = (path, options) => {
4117
4124
  );
4118
4125
  if (composerLockFiles.length) {
4119
4126
  for (const f of composerLockFiles) {
4127
+ const basePath = dirname(f);
4120
4128
  if (DEBUG_MODE) {
4121
4129
  console.log(`Parsing ${f}`);
4122
4130
  }
4123
- const dlist = parseComposerLock(f);
4124
- if (dlist && dlist.length) {
4125
- pkgList = pkgList.concat(dlist);
4131
+ let rootRequires = [];
4132
+ // Is there a composer.json to find the parent component
4133
+ if (
4134
+ !Object.keys(parentComponent).length &&
4135
+ existsSync(join(basePath, "composer.json"))
4136
+ ) {
4137
+ const composerData = JSON.parse(
4138
+ readFileSync(join(basePath, "composer.json"), { encoding: "utf-8" })
4139
+ );
4140
+ rootRequires = composerData.require;
4141
+ const pkgName = composerData.name;
4142
+ if (pkgName) {
4143
+ parentComponent.group = dirname(pkgName);
4144
+ if (parentComponent.group === ".") {
4145
+ parentComponent.group = "";
4146
+ }
4147
+ parentComponent.name = basename(pkgName);
4148
+ parentComponent.type = "application";
4149
+ parentComponent.version = composerData.version || "latest";
4150
+ parentComponent["bom-ref"] = decodeURIComponent(
4151
+ new PackageURL(
4152
+ "composer",
4153
+ parentComponent.group,
4154
+ parentComponent.name,
4155
+ parentComponent.version,
4156
+ null,
4157
+ null
4158
+ ).toString()
4159
+ );
4160
+ }
4161
+ }
4162
+ const retMap = parseComposerLock(f, rootRequires);
4163
+ if (retMap.pkgList && retMap.pkgList.length) {
4164
+ pkgList = pkgList.concat(retMap.pkgList);
4165
+ }
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
+ };
4183
+ dependencies = mergeDependencies(
4184
+ dependencies,
4185
+ retMap.dependenciesList,
4186
+ parentComponent
4187
+ );
4188
+ dependencies.splice(0, 0, pdependencies);
4126
4189
  }
4127
4190
  }
4128
4191
  return buildBomNSData(options, pkgList, "composer", {
4129
4192
  src: path,
4130
- filename: composerLockFiles.join(", ")
4193
+ filename: composerLockFiles.join(", "),
4194
+ dependencies,
4195
+ parentComponent
4131
4196
  });
4132
4197
  }
4133
4198
  return {};
@@ -4331,7 +4396,7 @@ export const createCsharpBom = async (
4331
4396
  console.log(`Parsing ${f}`);
4332
4397
  }
4333
4398
  pkgData = readFileSync(f, { encoding: "utf-8" });
4334
- const results = await parsePaketLockData(pkgData);
4399
+ const results = await parsePaketLockData(pkgData, f);
4335
4400
  const dlist = results.pkgList;
4336
4401
  const deps = results.dependenciesList;
4337
4402
  if (dlist && dlist.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyclonedx/cdxgen",
3
- "version": "9.10.1",
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,8 +56,8 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@babel/parser": "^7.23.6",
59
- "@babel/traverse": "^7.23.6",
60
- "@npmcli/arborist": "7.2.0",
59
+ "@babel/traverse": "^7.23.7",
60
+ "@npmcli/arborist": "7.2.2",
61
61
  "ajv": "^8.12.0",
62
62
  "ajv-formats": "^2.1.1",
63
63
  "cheerio": "^1.0.0-rc.12",
@@ -83,7 +83,8 @@
83
83
  "yargs": "^17.7.2"
84
84
  },
85
85
  "optionalDependencies": {
86
- "@appthreat/atom": "1.7.5",
86
+ "@appthreat/atom": "1.8.3",
87
+ "@appthreat/cdx-proto": "^0.0.4",
87
88
  "@cyclonedx/cdxgen-plugins-bin": "^1.5.4",
88
89
  "@cyclonedx/cdxgen-plugins-bin-windows-amd64": "^1.5.4",
89
90
  "@cyclonedx/cdxgen-plugins-bin-arm64": "^1.5.4",
@@ -93,7 +94,7 @@
93
94
  "connect": "^3.7.0",
94
95
  "jsonata": "^2.0.3",
95
96
  "sequelize": "^6.35.2",
96
- "sqlite3": "^5.1.6"
97
+ "sqlite3": "^5.1.7"
97
98
  },
98
99
  "files": [
99
100
  "*.js",
@@ -105,7 +106,7 @@
105
106
  "docsify-cli": "^4.4.4",
106
107
  "eslint": "^8.56.0",
107
108
  "eslint-config-prettier": "^9.1.0",
108
- "eslint-plugin-prettier": "^5.0.1",
109
+ "eslint-plugin-prettier": "^5.1.2",
109
110
  "jest": "^29.7.0",
110
111
  "prettier": "3.1.1"
111
112
  }
package/protobom.js ADDED
@@ -0,0 +1,36 @@
1
+ import { Bom } from "@appthreat/cdx-proto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+
4
+ const stringifyIfNeeded = (bomJson) => {
5
+ if (typeof bomJson === "string" || bomJson instanceof String) {
6
+ return bomJson;
7
+ }
8
+ return JSON.stringify(bomJson);
9
+ };
10
+
11
+ export const writeBinary = (bomJson, binFile) => {
12
+ if (bomJson && binFile) {
13
+ const bomObject = new Bom();
14
+ writeFileSync(
15
+ binFile,
16
+ bomObject
17
+ .fromJsonString(stringifyIfNeeded(bomJson), {
18
+ ignoreUnknownFields: true
19
+ })
20
+ .toBinary({ writeUnknownFields: true })
21
+ );
22
+ }
23
+ };
24
+
25
+ export const readBinary = (binFile, asJson = true) => {
26
+ if (!existsSync(binFile)) {
27
+ return undefined;
28
+ }
29
+ const bomObject = new Bom().fromBinary(readFileSync(binFile), {
30
+ readUnknownFields: true
31
+ });
32
+ if (asJson) {
33
+ return bomObject.toJson({ emitDefaultValues: true });
34
+ }
35
+ return bomObject;
36
+ };
@@ -0,0 +1,32 @@
1
+ import { expect, test } from "@jest/globals";
2
+ import { tmpdir } from "node:os";
3
+ import { existsSync, rmSync, mkdtempSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ import { writeBinary, readBinary } from "./protobom.js";
7
+
8
+ const tempDir = mkdtempSync(join(tmpdir(), "bin-tests-"));
9
+ const testBom = JSON.parse(
10
+ readFileSync("./test/data/bom-java.json", { encoding: "utf-8" })
11
+ );
12
+
13
+ test("proto binary tests", async () => {
14
+ const binFile = join(tempDir, "test.cdx.bin");
15
+ writeBinary({}, binFile);
16
+ expect(existsSync(binFile)).toBeTruthy();
17
+ writeBinary(testBom, binFile);
18
+ expect(existsSync(binFile)).toBeTruthy();
19
+ let bomObject = readBinary(binFile);
20
+ expect(bomObject).toBeDefined();
21
+ expect(bomObject.serialNumber).toEqual(
22
+ "urn:uuid:cc8b5a04-2698-4375-b04c-cedfa4317fee"
23
+ );
24
+ bomObject = readBinary(binFile, false);
25
+ expect(bomObject).toBeDefined();
26
+ expect(bomObject.serialNumber).toEqual(
27
+ "urn:uuid:cc8b5a04-2698-4375-b04c-cedfa4317fee"
28
+ );
29
+ if (tempDir && tempDir.startsWith(tmpdir()) && rmSync) {
30
+ rmSync(tempDir, { recursive: true, force: true });
31
+ }
32
+ });
package/utils.js CHANGED
@@ -666,6 +666,12 @@ export const parsePkgLock = async (pkgLockFile, options = {}) => {
666
666
  purl: purlString,
667
667
  "bom-ref": decodeURIComponent(purlString)
668
668
  };
669
+ if (node.resolved) {
670
+ pkg.properties.push({
671
+ name: "ResolvedUrl",
672
+ value: node.resolved
673
+ });
674
+ }
669
675
  }
670
676
  const packageLicense = node.package.license;
671
677
  if (packageLicense) {
@@ -694,8 +700,9 @@ export const parsePkgLock = async (pkgLockFile, options = {}) => {
694
700
  null
695
701
  ).toString()
696
702
  );
697
-
698
- workspaceDependsOn.push(depWorkspacePurlString);
703
+ if (decodeURIComponent(purlString) !== depWorkspacePurlString) {
704
+ workspaceDependsOn.push(depWorkspacePurlString);
705
+ }
699
706
  }
700
707
  }
701
708
 
@@ -726,7 +733,9 @@ export const parsePkgLock = async (pkgLockFile, options = {}) => {
726
733
  null
727
734
  ).toString()
728
735
  );
729
- childrenDependsOn.push(depChildString);
736
+ if (decodeURIComponent(purlString) !== depChildString) {
737
+ childrenDependsOn.push(depChildString);
738
+ }
730
739
  }
731
740
  }
732
741
 
@@ -735,31 +744,35 @@ export const parsePkgLock = async (pkgLockFile, options = {}) => {
735
744
  for (const edge of node.edgesOut.values()) {
736
745
  let targetVersion;
737
746
  let targetName;
738
-
747
+ let foundMatch = false;
739
748
  // if the edge doesn't have an integrity, it's likely a peer dependency
740
749
  // which isn't installed
741
- let edgeToIntegrity = edge.to ? edge.to.integrity : null;
742
- // let packageName = node.packageName;
743
- // let edgeName = edge.name;
750
+ // Bug #795. At times, npm loses the integrity node completely and such packages are getting missed out
751
+ // To keep things safe, we include these packages.
752
+ let edgeToIntegrity = edge.to ? edge.to.integrity : undefined;
744
753
  if (!edgeToIntegrity) {
745
- continue;
746
- }
747
-
748
- // the edges don't actually contain a version, so we need to search the root node
749
- // children to find the correct version. we check the node children first, then
750
- // we check the root node children
751
- let foundMatch = false;
752
- for (const child of node.children) {
753
- if (child[1].integrity == edgeToIntegrity) {
754
- targetName = child[0].replace(/node_modules\//g, "");
755
- // The package name could be different from the targetName retrieved
756
- // Eg: "string-width-cjs": "npm:string-width@^4.2.0",
757
- if (child[1].packageName && child[1].packageName !== targetName) {
758
- targetName = child[1].packageName;
759
- }
760
- targetVersion = child[1].version;
761
- foundMatch = true;
762
- break;
754
+ // This hack is required to fix the package name
755
+ targetName = node.name.replace(/-cjs$/, "");
756
+ targetVersion = node.version;
757
+ foundMatch = true;
758
+ } else {
759
+ // the edges don't actually contain a version, so we need to search the root node
760
+ // children to find the correct version. we check the node children first, then
761
+ // we check the root node children
762
+ for (const child of node.children) {
763
+ if (edgeToIntegrity) {
764
+ if (child[1].integrity == edgeToIntegrity) {
765
+ targetName = child[0].replace(/node_modules\//g, "");
766
+ // The package name could be different from the targetName retrieved
767
+ // Eg: "string-width-cjs": "npm:string-width@^4.2.0",
768
+ if (child[1].packageName && child[1].packageName !== targetName) {
769
+ targetName = child[1].packageName;
770
+ }
771
+ targetVersion = child[1].version;
772
+ foundMatch = true;
773
+ break;
774
+ }
775
+ }
763
776
  }
764
777
  }
765
778
  if (!foundMatch) {
@@ -792,8 +805,12 @@ export const parsePkgLock = async (pkgLockFile, options = {}) => {
792
805
  null
793
806
  ).toString()
794
807
  );
795
- pkgDependsOn.push(depPurlString);
796
- if (edge.to == null) continue;
808
+ if (decodeURIComponent(purlString) !== depPurlString) {
809
+ pkgDependsOn.push(depPurlString);
810
+ }
811
+ if (edge.to == null) {
812
+ continue;
813
+ }
797
814
  const { pkgList: childPkgList, dependenciesList: childDependenciesList } =
798
815
  parseArboristNode(
799
816
  edge.to,
@@ -4042,6 +4059,9 @@ export const parseGemfileLockData = async function (gemLockData) {
4042
4059
  const tmpA = l.split(" ");
4043
4060
  if (tmpA && tmpA.length == 2) {
4044
4061
  const name = tmpA[0];
4062
+ if (name === "remote:") {
4063
+ return;
4064
+ }
4045
4065
  if (!pkgnames[name]) {
4046
4066
  let version = tmpA[1].split(", ")[0];
4047
4067
  version = version.replace(/[(>=<)~ ]/g, "");
@@ -4060,7 +4080,8 @@ export const parseGemfileLockData = async function (gemLockData) {
4060
4080
  l === "PLATFORMS" ||
4061
4081
  l === "DEPENDENCIES" ||
4062
4082
  l === "RUBY VERSION" ||
4063
- l === "BUNDLED WITH"
4083
+ l === "BUNDLED WITH" ||
4084
+ l === "PATH"
4064
4085
  ) {
4065
4086
  specsFound = false;
4066
4087
  }
@@ -5577,7 +5598,7 @@ export const parseCsPkgLockData = async function (csLockData) {
5577
5598
  return pkgList;
5578
5599
  };
5579
5600
 
5580
- export const parsePaketLockData = async function (paketLockData) {
5601
+ export const parsePaketLockData = async function (paketLockData, pkgLockFile) {
5581
5602
  const pkgList = [];
5582
5603
  const dependenciesList = [];
5583
5604
  const dependenciesMap = {};
@@ -5605,14 +5626,39 @@ export const parsePaketLockData = async function (paketLockData) {
5605
5626
  if (match) {
5606
5627
  const name = match[1];
5607
5628
  const version = match[2];
5608
- const purl = decodeURIComponent(
5609
- new PackageURL("nuget", "", name, version, null, null).toString()
5610
- );
5629
+ const purl = new PackageURL(
5630
+ "nuget",
5631
+ "",
5632
+ name,
5633
+ version,
5634
+ null,
5635
+ null
5636
+ ).toString();
5611
5637
  pkg = {
5612
5638
  group: "",
5613
- name: name,
5614
- version: version,
5615
- purl: purl
5639
+ name,
5640
+ version,
5641
+ purl,
5642
+ "bom-ref": decodeURIComponent(purl),
5643
+ properties: [
5644
+ {
5645
+ name: "SrcFile",
5646
+ value: pkgLockFile
5647
+ }
5648
+ ],
5649
+ evidence: {
5650
+ identity: {
5651
+ field: "purl",
5652
+ confidence: 1,
5653
+ methods: [
5654
+ {
5655
+ technique: "manifest-analysis",
5656
+ confidence: 1,
5657
+ value: pkgLockFile
5658
+ }
5659
+ ]
5660
+ }
5661
+ }
5616
5662
  };
5617
5663
  pkgList.push(pkg);
5618
5664
  dependenciesMap[purl] = new Set();
@@ -5664,13 +5710,25 @@ export const parsePaketLockData = async function (paketLockData) {
5664
5710
  dependenciesList
5665
5711
  };
5666
5712
  };
5713
+
5667
5714
  /**
5668
5715
  * Parse composer lock file
5669
5716
  *
5670
5717
  * @param {string} pkgLockFile composer.lock file
5718
+ * @param {array} rootRequires require section from composer.json
5671
5719
  */
5672
- export const parseComposerLock = function (pkgLockFile) {
5720
+ export const parseComposerLock = function (pkgLockFile, rootRequires) {
5673
5721
  const pkgList = [];
5722
+ const dependenciesList = [];
5723
+ const dependenciesMap = {};
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
+ }
5674
5732
  if (existsSync(pkgLockFile)) {
5675
5733
  let lockData = {};
5676
5734
  try {
@@ -5687,6 +5745,7 @@ export const parseComposerLock = function (pkgLockFile) {
5687
5745
  if (lockData["packages-dev"]) {
5688
5746
  packages["optional"] = lockData["packages-dev"];
5689
5747
  }
5748
+ // Pass 1: Collect all packages
5690
5749
  for (const compScope in packages) {
5691
5750
  for (const i in packages[compScope]) {
5692
5751
  const pkg = packages[compScope][i];
@@ -5694,19 +5753,26 @@ export const parseComposerLock = function (pkgLockFile) {
5694
5753
  if (!pkg || !pkg.name || !pkg.version) {
5695
5754
  continue;
5696
5755
  }
5756
+
5697
5757
  let group = dirname(pkg.name);
5698
5758
  if (group === ".") {
5699
5759
  group = "";
5700
5760
  }
5701
5761
  const name = basename(pkg.name);
5702
- pkgList.push({
5762
+ const purl = new PackageURL(
5763
+ "composer",
5764
+ group,
5765
+ name,
5766
+ pkg.version,
5767
+ null,
5768
+ null
5769
+ ).toString();
5770
+ const apkg = {
5703
5771
  group: group,
5704
5772
  name: name,
5705
- // Remove leading v from version to work around bug
5706
- // https://github.com/OSSIndex/vulns/issues/231
5707
- // @TODO: remove workaround when DependencyTrack v4.4 is released,
5708
- // which has it's own workaround. Or when the 231 bug is fixed.
5709
- version: pkg.version.replace(/^v/, ""),
5773
+ purl,
5774
+ "bom-ref": decodeURIComponent(purl),
5775
+ version: pkg.version,
5710
5776
  repository: pkg.source,
5711
5777
  license: pkg.license,
5712
5778
  description: pkg.description,
@@ -5730,12 +5796,63 @@ export const parseComposerLock = function (pkgLockFile) {
5730
5796
  ]
5731
5797
  }
5732
5798
  }
5733
- });
5799
+ };
5800
+ if (pkg.autoload && Object.keys(pkg.autoload).length) {
5801
+ const namespaces = [];
5802
+ for (const aaload of Object.keys(pkg.autoload)) {
5803
+ if (aaload.startsWith("psr")) {
5804
+ for (const ans of Object.keys(pkg.autoload[aaload])) {
5805
+ namespaces.push(ans.trim());
5806
+ }
5807
+ }
5808
+ }
5809
+ if (namespaces.length) {
5810
+ apkg.properties.push({
5811
+ name: "Namespaces",
5812
+ value: namespaces.join(", ")
5813
+ });
5814
+ }
5815
+ }
5816
+ pkgList.push(apkg);
5817
+ dependenciesMap[purl] = new Set();
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
+ }
5823
+ }
5824
+ }
5825
+ // Pass 2: Construct dependency tree
5826
+ for (const compScope in packages) {
5827
+ for (const i in packages[compScope]) {
5828
+ const pkg = packages[compScope][i];
5829
+ if (!pkg || !pkg.name || !pkg.version) {
5830
+ continue;
5831
+ }
5832
+ if (!pkg.require || !Object.keys(pkg.require).length) {
5833
+ continue;
5834
+ }
5835
+ const purl = pkgNamePurlMap[pkg.name];
5836
+ for (const adepName of Object.keys(pkg.require)) {
5837
+ if (pkgNamePurlMap[adepName]) {
5838
+ dependenciesMap[purl].add(pkgNamePurlMap[adepName]);
5839
+ }
5840
+ }
5734
5841
  }
5735
5842
  }
5736
5843
  }
5737
5844
  }
5738
- return pkgList;
5845
+ for (const ref in dependenciesMap) {
5846
+ dependenciesList.push({
5847
+ ref: ref,
5848
+ dependsOn: Array.from(dependenciesMap[ref])
5849
+ });
5850
+ }
5851
+ return {
5852
+ pkgList,
5853
+ dependenciesList,
5854
+ rootList
5855
+ };
5739
5856
  };
5740
5857
 
5741
5858
  export const parseSbtTree = (sbtTreeFile) => {
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",
@@ -1351,14 +1351,30 @@ test("parse paket.lock", async () => {
1351
1351
  dependenciesList: []
1352
1352
  });
1353
1353
  const dep_list = await parsePaketLockData(
1354
- readFileSync("./test/data/paket.lock", { encoding: "utf-8" })
1354
+ readFileSync("./test/data/paket.lock", { encoding: "utf-8" }),
1355
+ "./test/data/paket.lock"
1355
1356
  );
1356
1357
  expect(dep_list.pkgList.length).toEqual(13);
1357
1358
  expect(dep_list.pkgList[0]).toEqual({
1358
1359
  group: "",
1359
1360
  name: "0x53A.ReferenceAssemblies.Paket",
1360
1361
  version: "0.2",
1361
- purl: "pkg:nuget/0x53A.ReferenceAssemblies.Paket@0.2"
1362
+ purl: "pkg:nuget/0x53A.ReferenceAssemblies.Paket@0.2",
1363
+ "bom-ref": "pkg:nuget/0x53A.ReferenceAssemblies.Paket@0.2",
1364
+ properties: [{ name: "SrcFile", value: "./test/data/paket.lock" }],
1365
+ evidence: {
1366
+ identity: {
1367
+ field: "purl",
1368
+ confidence: 1,
1369
+ methods: [
1370
+ {
1371
+ technique: "manifest-analysis",
1372
+ confidence: 1,
1373
+ value: "./test/data/paket.lock"
1374
+ }
1375
+ ]
1376
+ }
1377
+ }
1362
1378
  });
1363
1379
  expect(dep_list.dependenciesList.length).toEqual(13);
1364
1380
  expect(dep_list.dependenciesList[2]).toEqual({
@@ -1694,7 +1710,7 @@ test("parsePkgLock v2 workspace", async () => {
1694
1710
  );
1695
1711
  let pkgs = parsedList.pkgList;
1696
1712
  let deps = parsedList.dependenciesList;
1697
- expect(pkgs.length).toEqual(1032);
1713
+ expect(pkgs.length).toEqual(1034);
1698
1714
  expect(pkgs[0].license).toEqual("MIT");
1699
1715
  let hasAppWorkspacePkg = pkgs.some(
1700
1716
  (obj) => obj["bom-ref"] === "pkg:npm/app@0.0.0"
@@ -1746,8 +1762,8 @@ test("parsePkgLock v3", async () => {
1746
1762
  projectName: "cdxgen"
1747
1763
  });
1748
1764
  deps = parsedList.pkgList;
1749
- expect(deps.length).toEqual(1205);
1750
- expect(parsedList.dependenciesList.length).toEqual(1205);
1765
+ expect(deps.length).toEqual(1199);
1766
+ expect(parsedList.dependenciesList.length).toEqual(1199);
1751
1767
  });
1752
1768
 
1753
1769
  test("parseBowerJson", async () => {
@@ -2315,13 +2331,16 @@ test("parseYarnLock", async () => {
2315
2331
  });
2316
2332
 
2317
2333
  test("parseComposerLock", () => {
2318
- let deps = parseComposerLock("./test/data/composer.lock");
2319
- expect(deps.length).toEqual(1);
2320
- expect(deps[0]).toEqual({
2334
+ let retMap = parseComposerLock("./test/data/composer.lock");
2335
+ expect(retMap.pkgList.length).toEqual(1);
2336
+ expect(retMap.dependenciesList.length).toEqual(1);
2337
+ expect(retMap.pkgList[0]).toEqual({
2321
2338
  group: "quickbooks",
2322
2339
  name: "v3-php-sdk",
2323
2340
  scope: "required",
2324
- version: "4.0.6.1",
2341
+ version: "v4.0.6.1",
2342
+ purl: "pkg:composer/quickbooks/v3-php-sdk@v4.0.6.1",
2343
+ "bom-ref": "pkg:composer/quickbooks/v3-php-sdk@v4.0.6.1",
2325
2344
  repository: {
2326
2345
  type: "git",
2327
2346
  url: "https://github.com/intuit/QuickBooks-V3-PHP-SDK.git",
@@ -2333,6 +2352,10 @@ test("parseComposerLock", () => {
2333
2352
  {
2334
2353
  name: "SrcFile",
2335
2354
  value: "./test/data/composer.lock"
2355
+ },
2356
+ {
2357
+ name: "Namespaces",
2358
+ value: "QuickBooksOnline\\API\\"
2336
2359
  }
2337
2360
  ],
2338
2361
  evidence: {
@@ -2350,13 +2373,16 @@ test("parseComposerLock", () => {
2350
2373
  }
2351
2374
  });
2352
2375
 
2353
- deps = parseComposerLock("./test/data/composer-2.lock");
2354
- expect(deps.length).toEqual(73);
2355
- expect(deps[0]).toEqual({
2376
+ retMap = parseComposerLock("./test/data/composer-2.lock");
2377
+ expect(retMap.pkgList.length).toEqual(73);
2378
+ expect(retMap.dependenciesList.length).toEqual(73);
2379
+ expect(retMap.pkgList[0]).toEqual({
2356
2380
  group: "amphp",
2357
2381
  name: "amp",
2358
2382
  scope: "required",
2359
- version: "2.4.4",
2383
+ version: "v2.4.4",
2384
+ purl: "pkg:composer/amphp/amp@v2.4.4",
2385
+ "bom-ref": "pkg:composer/amphp/amp@v2.4.4",
2360
2386
  repository: {
2361
2387
  type: "git",
2362
2388
  url: "https://github.com/amphp/amp.git",
@@ -2368,6 +2394,10 @@ test("parseComposerLock", () => {
2368
2394
  {
2369
2395
  name: "SrcFile",
2370
2396
  value: "./test/data/composer-2.lock"
2397
+ },
2398
+ {
2399
+ name: "Namespaces",
2400
+ value: "Amp\\"
2371
2401
  }
2372
2402
  ],
2373
2403
  evidence: {
@@ -2385,12 +2415,15 @@ test("parseComposerLock", () => {
2385
2415
  }
2386
2416
  });
2387
2417
 
2388
- deps = parseComposerLock("./test/data/composer-3.lock");
2389
- expect(deps.length).toEqual(62);
2390
- expect(deps[0]).toEqual({
2418
+ retMap = parseComposerLock("./test/data/composer-3.lock");
2419
+ expect(retMap.pkgList.length).toEqual(62);
2420
+ expect(retMap.dependenciesList.length).toEqual(62);
2421
+ expect(retMap.pkgList[0]).toEqual({
2391
2422
  group: "amphp",
2392
2423
  name: "amp",
2393
- version: "2.6.2",
2424
+ version: "v2.6.2",
2425
+ purl: "pkg:composer/amphp/amp@v2.6.2",
2426
+ "bom-ref": "pkg:composer/amphp/amp@v2.6.2",
2394
2427
  repository: {
2395
2428
  type: "git",
2396
2429
  url: "https://github.com/amphp/amp.git",
@@ -2399,7 +2432,13 @@ test("parseComposerLock", () => {
2399
2432
  license: ["MIT"],
2400
2433
  description: "A non-blocking concurrency framework for PHP applications.",
2401
2434
  scope: "required",
2402
- properties: [{ name: "SrcFile", value: "./test/data/composer-3.lock" }],
2435
+ properties: [
2436
+ { name: "SrcFile", value: "./test/data/composer-3.lock" },
2437
+ {
2438
+ name: "Namespaces",
2439
+ value: "Amp\\"
2440
+ }
2441
+ ],
2403
2442
  evidence: {
2404
2443
  identity: {
2405
2444
  field: "purl",
@@ -2414,6 +2453,42 @@ test("parseComposerLock", () => {
2414
2453
  }
2415
2454
  }
2416
2455
  });
2456
+ retMap = parseComposerLock("./test/data/composer-4.lock");
2457
+ expect(retMap.pkgList.length).toEqual(50);
2458
+ expect(retMap.dependenciesList.length).toEqual(50);
2459
+ expect(retMap.pkgList[0]).toEqual({
2460
+ group: "apache",
2461
+ name: "log4php",
2462
+ purl: "pkg:composer/apache/log4php@2.3.0",
2463
+ "bom-ref": "pkg:composer/apache/log4php@2.3.0",
2464
+ version: "2.3.0",
2465
+ repository: {
2466
+ type: "git",
2467
+ url: "https://git-wip-us.apache.org/repos/asf/logging-log4php.git",
2468
+ reference: "8c6df2481cd68d0d211d38f700406c5f0a9de0c2"
2469
+ },
2470
+ license: ["Apache-2.0"],
2471
+ description: "A versatile logging framework for PHP",
2472
+ scope: "required",
2473
+ properties: [{ name: "SrcFile", value: "./test/data/composer-4.lock" }],
2474
+ evidence: {
2475
+ identity: {
2476
+ field: "purl",
2477
+ confidence: 1,
2478
+ methods: [
2479
+ {
2480
+ confidence: 1,
2481
+ technique: "manifest-analysis",
2482
+ value: "./test/data/composer-4.lock"
2483
+ }
2484
+ ]
2485
+ }
2486
+ }
2487
+ });
2488
+ expect(retMap.dependenciesList[1]).toEqual({
2489
+ ref: "pkg:composer/doctrine/annotations@v1.2.1",
2490
+ dependsOn: ["pkg:composer/doctrine/lexer@v1.0"]
2491
+ });
2417
2492
  });
2418
2493
 
2419
2494
  test("parseGemfileLockData", async () => {