@cyclonedx/cdxgen 11.4.4 → 11.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -604,15 +604,19 @@ export async function getOSPackages(src, imageConfig) {
604
604
  }
605
605
  comp.group = group;
606
606
  comp.name = name;
607
+ try {
608
+ purlObj = PackageURL.fromString(comp.purl);
609
+ purlObj.qualifiers = purlObj.qualifiers || {};
610
+ } catch (_err) {
611
+ // continue regardless of error
612
+ }
607
613
  if (group === "") {
608
614
  try {
609
- purlObj = PackageURL.fromString(comp.purl);
610
- if (purlObj.namespace && purlObj.namespace !== "") {
615
+ if (purlObj?.namespace && purlObj.namespace !== "") {
611
616
  group = purlObj.namespace;
612
617
  comp.group = group;
613
618
  purlObj.namespace = group;
614
619
  }
615
- purlObj.qualifiers = purlObj.qualifiers || {};
616
620
  if (distro_id?.length) {
617
621
  purlObj.qualifiers["distro"] = distro_id;
618
622
  }
@@ -621,7 +625,7 @@ export async function getOSPackages(src, imageConfig) {
621
625
  }
622
626
  // Bug fix for mageia and oracle linux
623
627
  // Type is being returned as none for ubuntu as well!
624
- if (purlObj.type === "none") {
628
+ if (purlObj?.type === "none") {
625
629
  purlObj["type"] = purl_type;
626
630
  purlObj["namespace"] = "";
627
631
  comp.group = "";
@@ -641,11 +645,11 @@ export async function getOSPackages(src, imageConfig) {
641
645
  ).toString();
642
646
  comp["bom-ref"] = decodeURIComponent(comp.purl);
643
647
  }
644
- if (purlObj.type !== "none") {
648
+ if (purlObj?.type !== "none") {
645
649
  allTypes.add(purlObj.type);
646
650
  }
647
651
  // Prefix distro codename for ubuntu
648
- if (purlObj.qualifiers?.distro) {
652
+ if (purlObj?.qualifiers?.distro) {
649
653
  allTypes.add(purlObj.qualifiers.distro);
650
654
  if (OS_DISTRO_ALIAS[purlObj.qualifiers.distro]) {
651
655
  distro_codename =
@@ -689,8 +693,28 @@ export async function getOSPackages(src, imageConfig) {
689
693
  }
690
694
  if (comp.purl.includes("epoch=")) {
691
695
  try {
692
- purlObj = PackageURL.fromString(comp.purl);
693
- purlObj.qualifiers = purlObj.qualifiers || {};
696
+ const epoch = purlObj.qualifiers?.epoch;
697
+ // trivy seems to be removing the epoch from the version and moving it to a qualifier
698
+ // let's fix this hack to improve confidence.
699
+ if (epoch) {
700
+ purlObj.version = `${epoch}:${purlObj.version}`;
701
+ comp.version = purlObj.version;
702
+ }
703
+ comp.evidence = {
704
+ identity: [
705
+ {
706
+ field: "purl",
707
+ confidence: 1,
708
+ methods: [
709
+ {
710
+ technique: "other",
711
+ confidence: 1,
712
+ value: comp.purl,
713
+ },
714
+ ],
715
+ },
716
+ ],
717
+ };
694
718
  if (distro_id?.length) {
695
719
  purlObj.qualifiers["distro"] = distro_id;
696
720
  }
@@ -756,16 +780,35 @@ export async function getOSPackages(src, imageConfig) {
756
780
  const compProperties = comp.properties;
757
781
  let srcName;
758
782
  let srcVersion;
783
+ let srcRelease;
784
+ let epoch;
759
785
  if (compProperties && Array.isArray(compProperties)) {
760
786
  for (const aprop of compProperties) {
787
+ // Property name: aquasecurity:trivy:SrcName
761
788
  if (aprop.name.endsWith("SrcName")) {
762
789
  srcName = aprop.value;
763
790
  }
791
+ // Property name: aquasecurity:trivy:SrcVersion
764
792
  if (aprop.name.endsWith("SrcVersion")) {
765
793
  srcVersion = aprop.value;
766
794
  }
795
+ // Property name: aquasecurity:trivy:SrcRelease
796
+ if (aprop.name.endsWith("SrcRelease")) {
797
+ srcRelease = aprop.value;
798
+ }
799
+ // Property name: aquasecurity:trivy:SrcEpoch
800
+ if (aprop.name.endsWith("SrcEpoch")) {
801
+ epoch = aprop.value;
802
+ }
767
803
  }
768
804
  }
805
+ // See issue #2067
806
+ if (srcVersion && srcRelease) {
807
+ srcVersion = `${srcVersion}-${srcRelease}`;
808
+ }
809
+ if (epoch) {
810
+ srcVersion = `${epoch}:${srcVersion}`;
811
+ }
769
812
  delete comp.properties;
770
813
  // Bug fix: We can get bom-ref like this: pkg:rpm/sles/libstdc%2B%2B6@14.2.0+git10526-150000.1.6.1?arch=x86_64&distro=sles-15.5
771
814
  if (
@@ -785,18 +828,44 @@ export async function getOSPackages(src, imageConfig) {
785
828
  if (compDeps) {
786
829
  dependenciesList.push(compDeps);
787
830
  }
788
- // If there is a source package defined include it as well
831
+ // HACK: Many vulnerability databases, including vdb, track vulnerabilities based on source package names :(
832
+ // If there is a source package defined we include it as well to make such SCA scanners work.
833
+ // As a compromise, we reduce the confidence to zero so that there is a way to filter these out.
789
834
  if (srcName && srcVersion && srcName !== comp.name) {
790
835
  const newComp = Object.assign({}, comp);
791
836
  newComp.name = srcName;
792
837
  newComp.version = srcVersion;
838
+ newComp.tags = ["source"];
839
+ newComp.evidence = {
840
+ identity: [
841
+ {
842
+ field: "purl",
843
+ confidence: 0,
844
+ methods: [
845
+ {
846
+ technique: "filename",
847
+ confidence: 0,
848
+ value: comp.name,
849
+ },
850
+ ],
851
+ },
852
+ ],
853
+ };
854
+ // Track upstream and source versions as qualifiers
793
855
  if (purlObj) {
856
+ const newCompQualifiers = {
857
+ ...purlObj.qualifiers,
858
+ };
859
+ delete newCompQualifiers.epoch;
860
+ if (epoch) {
861
+ newCompQualifiers.epoch = epoch;
862
+ }
794
863
  newComp.purl = new PackageURL(
795
864
  purlObj.type,
796
865
  purlObj.namespace,
797
866
  srcName,
798
867
  srcVersion,
799
- purlObj.qualifiers,
868
+ newCompQualifiers,
800
869
  purlObj.subpath,
801
870
  ).toString();
802
871
  }
@@ -879,7 +948,8 @@ function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
879
948
 
880
949
  const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
881
950
  try {
882
- const tmpDependsOn = tmpDependencies[origBomRef] || [];
951
+ const tmpDependsOn =
952
+ tmpDependencies[origBomRef] || tmpDependencies[comp["bom-ref"]] || [];
883
953
  const dependsOn = new Set();
884
954
  tmpDependsOn.forEach((d) => {
885
955
  try {
@@ -896,6 +966,14 @@ const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
896
966
  tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
897
967
  }
898
968
  }
969
+ if (tmpPurl.qualifiers) {
970
+ if (
971
+ tmpPurl.qualifiers.epoch &&
972
+ !tmpPurl.version.startsWith(`${tmpPurl.qualifiers.epoch}:`)
973
+ ) {
974
+ tmpPurl.version = `${tmpPurl.qualifiers.epoch}:${tmpPurl.version}`;
975
+ }
976
+ }
899
977
  dependsOn.add(decodeURIComponent(tmpPurl.toString()));
900
978
  } catch (_e) {
901
979
  // ignore
@@ -474,7 +474,6 @@ export const getConnection = async (options, forRegistry) => {
474
474
  if (_platform() === "win32") {
475
475
  console.warn(
476
476
  "Ensure Docker for Desktop is running as an administrator with 'Exposing daemon on TCP without TLS' setting turned on.",
477
- opts,
478
477
  );
479
478
  } else if (_platform() === "darwin" && !isNerdctl) {
480
479
  if (detectRancherDesktop() || detectColima()) {
@@ -488,7 +487,6 @@ export const getConnection = async (options, forRegistry) => {
488
487
  } else {
489
488
  console.warn(
490
489
  "Ensure docker/podman service or Docker for Desktop is running.",
491
- opts,
492
490
  );
493
491
  console.log(
494
492
  "Check if the post-installation steps were performed correctly as per this documentation https://docs.docker.com/engine/install/linux-postinstall/",
@@ -693,7 +691,7 @@ export const getImage = async (fullImageName) => {
693
691
  console.log(
694
692
  "Set the environment variable DOCKER_CMD to use an alternative command such as nerdctl or podman.",
695
693
  );
696
- } else {
694
+ } else if (result.stderr) {
697
695
  console.log(result.stderr);
698
696
  }
699
697
  }
@@ -702,7 +700,9 @@ export const getImage = async (fullImageName) => {
702
700
  encoding: "utf-8",
703
701
  });
704
702
  if (result.status !== 0 || result.error) {
705
- console.log(result.stderr);
703
+ if (result.stderr) {
704
+ console.log(result.stderr);
705
+ }
706
706
  return localData;
707
707
  }
708
708
  try {
@@ -847,6 +847,43 @@ function handleAbsolutePath(entry) {
847
847
  }
848
848
  }
849
849
 
850
+ // These paths are known to cause extract errors
851
+ const EXTRACT_EXCLUDE_PATHS = [
852
+ "etc/machine-id",
853
+ "etc/gshadow",
854
+ "etc/shadow",
855
+ "etc/passwd",
856
+ "etc/ssl/certs",
857
+ "etc/pki/ca-trust",
858
+ "usr/lib/systemd/",
859
+ "usr/lib64/libdevmapper.so",
860
+ "usr/sbin/",
861
+ "cacerts",
862
+ "ssl/certs",
863
+ "logs/",
864
+ "dev/",
865
+ "usr/share/zoneinfo/",
866
+ "usr/share/doc/",
867
+ "usr/share/i18n/",
868
+ "var/lib/ca-certificates",
869
+ "root/.gnupg",
870
+ "root/.dotnet",
871
+ "usr/share/licenses/device-mapper-libs",
872
+ ];
873
+
874
+ // These device types are known to cause extract errors
875
+ const EXTRACT_EXCLUDE_TYPES = new Set([
876
+ "BlockDevice",
877
+ "CharacterDevice",
878
+ "FIFO",
879
+ "MultiVolume",
880
+ "TapeVolume",
881
+ "SymbolicLink",
882
+ "RenamedOrSymlinked",
883
+ "HardLink",
884
+ "Link",
885
+ ]);
886
+
850
887
  export const extractTar = async (fullImageName, dir, options) => {
851
888
  try {
852
889
  await stream.pipeline(
@@ -866,39 +903,11 @@ export const extractTar = async (fullImageName, dir, options) => {
866
903
  },
867
904
  onReadEntry: handleAbsolutePath,
868
905
  filter: (path, entry) => {
869
- // Some files are known to cause issues with extract
906
+ const name = basename(path);
870
907
  return !(
871
- path.includes("etc/machine-id") ||
872
- path.includes("etc/gshadow") ||
873
- path.includes("etc/shadow") ||
874
- path.includes("etc/passwd") ||
875
- path.includes("etc/ssl/certs") ||
876
- path.includes("etc/pki/ca-trust") ||
877
- path.includes("usr/lib/systemd/") ||
878
- path.includes("usr/lib64/libdevmapper.so") ||
879
- path.includes("usr/sbin/") ||
880
- path.includes("cacerts") ||
881
- path.includes("ssl/certs") ||
882
- path.includes("logs/") ||
883
- path.includes("dev/") ||
884
- path.includes("usr/share/zoneinfo/") ||
885
- path.includes("usr/share/doc/") ||
886
- path.includes("usr/share/i18n/") ||
887
- path.includes("var/lib/ca-certificates") ||
888
- path.includes("root/.gnupg") ||
889
- basename(path).startsWith(".") ||
890
- path.includes("usr/share/licenses/device-mapper-libs") ||
891
- [
892
- "BlockDevice",
893
- "CharacterDevice",
894
- "FIFO",
895
- "MultiVolume",
896
- "TapeVolume",
897
- "SymbolicLink",
898
- "RenamedOrSymlinked",
899
- "HardLink",
900
- "Link",
901
- ].includes(entry.type)
908
+ name.startsWith(".") ||
909
+ EXTRACT_EXCLUDE_PATHS.some((p) => path.includes(p)) ||
910
+ EXTRACT_EXCLUDE_TYPES.has(entry.type)
902
911
  );
903
912
  },
904
913
  }),
@@ -937,7 +946,8 @@ export const extractTar = async (fullImageName, dir, options) => {
937
946
  } else if (["TAR_ENTRY_INFO", "TAR_ENTRY_INVALID"].includes(err.code)) {
938
947
  if (
939
948
  err?.header?.path?.includes("{") ||
940
- err?.message?.includes("linkpath required")
949
+ err?.message?.includes("linkpath required") ||
950
+ err?.message?.includes("linkpath forbidden")
941
951
  ) {
942
952
  return false;
943
953
  }
@@ -9,7 +9,14 @@ import compression from "compression";
9
9
  import connect from "connect";
10
10
 
11
11
  import { createBom, submitBom } from "../cli/index.js";
12
- import { getTmpDir, isSecureMode, safeSpawnSync } from "../helpers/utils.js";
12
+ import {
13
+ getTmpDir,
14
+ hasDangerousUnicode,
15
+ isSecureMode,
16
+ isValidDriveRoot,
17
+ isWin,
18
+ safeSpawnSync,
19
+ } from "../helpers/utils.js";
13
20
  import { postProcess } from "../stages/postgen/postgen.js";
14
21
 
15
22
  // Timeout milliseconds. Default 10 mins
@@ -57,19 +64,90 @@ app.use(
57
64
  );
58
65
  app.use(compression());
59
66
 
67
+ /**
68
+ * Checks the given hostname against the allowed list.
69
+ *
70
+ * @param {string} hostname Host name to check
71
+ * @returns {boolean} true if the hostname in its entirety is allowed. false otherwise.
72
+ */
60
73
  export function isAllowedHost(hostname) {
61
74
  if (!process.env.CDXGEN_SERVER_ALLOWED_HOSTS) {
62
75
  return true;
63
76
  }
77
+ // Guard against dangerous Unicode characters
78
+ if (hasDangerousUnicode(hostname)) {
79
+ return false;
80
+ }
64
81
  return (process.env.CDXGEN_SERVER_ALLOWED_HOSTS || "")
65
82
  .split(",")
66
83
  .includes(hostname);
67
84
  }
68
85
 
86
+ /**
87
+ * Checks the given path string to belong to a drive in Windows.
88
+ *
89
+ * @param {string} p Path string to check
90
+ * @returns {boolean} true if the windows path belongs to a drive. false otherwise (device names)
91
+ */
92
+ export function isAllowedWinPath(p) {
93
+ if (typeof p !== "string") {
94
+ return false;
95
+ }
96
+ if (p === "") {
97
+ return true;
98
+ }
99
+ // Guard against dangerous Unicode characters
100
+ if (hasDangerousUnicode(p)) {
101
+ return false;
102
+ }
103
+ try {
104
+ const normalized = path.normalize(p);
105
+ // Check the entire normalized path for dangerous patterns
106
+ if (hasDangerousUnicode(normalized)) {
107
+ return false;
108
+ }
109
+ const { root } = path.parse(normalized);
110
+ // Both Relative paths and invalid windows device names are resulting in an empty root
111
+ // To keep things simple, we don't accept relative paths for Windows server-mode users at all
112
+
113
+ // Invocations with unix-style paths result in "\\" as the root on windows
114
+ // path.parse(path.normalize("/foo/bar"))
115
+ // { root: '\\', dir: '\\foo', base: 'bar', ext: '', name: 'bar' }
116
+ if (root === "\\") {
117
+ return true;
118
+ }
119
+ // Check for device/UNC paths - these should always return false
120
+ if (root.startsWith("\\\\")) {
121
+ return false;
122
+ }
123
+ // Strict validation for drive letter format
124
+ return isValidDriveRoot(root);
125
+ } catch (_err) {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Checks the given path against the allowed list.
132
+ *
133
+ * @param {string} p Path string to check
134
+ * @returns {boolean} true if the path is present in the allowed paths. false otherwise.
135
+ */
69
136
  export function isAllowedPath(p) {
137
+ if (typeof p !== "string") {
138
+ return false;
139
+ }
140
+ // Guard against dangerous Unicode characters
141
+ if (hasDangerousUnicode(p)) {
142
+ return false;
143
+ }
70
144
  if (!process.env.CDXGEN_SERVER_ALLOWED_PATHS) {
71
145
  return true;
72
146
  }
147
+ // Handle CVE-2025-27210 without relying entirely on node blocklists
148
+ if (isWin && !isAllowedWinPath(p)) {
149
+ return false;
150
+ }
73
151
  return (process.env.CDXGEN_SERVER_ALLOWED_PATHS || "")
74
152
  .split(",")
75
153
  .some((ap) => p.startsWith(ap));
@@ -90,7 +168,8 @@ function gitClone(repoUrl, branch = null) {
90
168
  tempDir,
91
169
  ];
92
170
  if (branch) {
93
- gitArgs.splice(2, 0, "--branch", branch);
171
+ const cloneIndex = gitArgs.indexOf("clone");
172
+ gitArgs.splice(cloneIndex + 1, 0, "--branch", branch);
94
173
  }
95
174
  console.log(
96
175
  `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
@@ -174,6 +253,39 @@ export function parseQueryString(q, body = {}, options = {}) {
174
253
  return options;
175
254
  }
176
255
 
256
+ export function getQueryParams(req) {
257
+ try {
258
+ if (!req || !req.url) {
259
+ return {};
260
+ }
261
+
262
+ const protocol = req.protocol || "http";
263
+ const host = req.headers?.host || "localhost";
264
+ const baseUrl = `${protocol}://${host}`;
265
+
266
+ const fullUrl = new URL(req.url, baseUrl);
267
+ const params = {};
268
+
269
+ // Convert multiple values to an array
270
+ for (const [key, value] of fullUrl.searchParams) {
271
+ if (params[key]) {
272
+ if (Array.isArray(params[key])) {
273
+ params[key].push(value);
274
+ } else {
275
+ params[key] = [params[key], value];
276
+ }
277
+ } else {
278
+ params[key] = value;
279
+ }
280
+ }
281
+
282
+ return params;
283
+ } catch (error) {
284
+ console.error("Error parsing URL:", error);
285
+ return {};
286
+ }
287
+ }
288
+
177
289
  const applyProfileOptions = (options) => {
178
290
  switch (options.profile) {
179
291
  case "appsec":
@@ -257,8 +369,7 @@ const start = (options) => {
257
369
  }),
258
370
  );
259
371
  }
260
- const requestUrl = new URL(req.url, `http://${req.headers.host}`);
261
- const q = Object.fromEntries(requestUrl.searchParams.entries());
372
+ const q = getQueryParams(req);
262
373
  let cleanup = false;
263
374
  let reqOptions = {};
264
375
  try {
@@ -277,7 +388,7 @@ const start = (options) => {
277
388
  }),
278
389
  );
279
390
  }
280
- const filePath = q.path || q.url || req.body.path || req.body.url;
391
+ const filePath = q?.path || q?.url || req?.body?.path || req?.body?.url;
281
392
  if (!filePath) {
282
393
  res.writeHead(500, { "Content-Type": "application/json" });
283
394
  return res.end(
@@ -302,7 +413,10 @@ const start = (options) => {
302
413
  srcDir = gitClone(filePath, reqOptions.gitBranch);
303
414
  cleanup = true;
304
415
  } else {
305
- if (!isAllowedPath(path.resolve(srcDir))) {
416
+ if (
417
+ !isAllowedPath(path.resolve(srcDir)) ||
418
+ (isWin && !isAllowedWinPath(srcDir))
419
+ ) {
306
420
  res.writeHead(403, { "Content-Type": "application/json" });
307
421
  return res.end(
308
422
  JSON.stringify({