@cyclonedx/cdxgen 12.1.5 → 12.2.1

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 (193) hide show
  1. package/README.md +51 -40
  2. package/bin/cdxgen.js +194 -97
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +449 -429
  14. package/lib/cli/index.poku.js +117 -0
  15. package/lib/evinser/db.js +137 -0
  16. package/lib/{helpers → evinser}/db.poku.js +2 -6
  17. package/lib/evinser/evinser.js +2 -14
  18. package/lib/helpers/analyzer.js +606 -3
  19. package/lib/helpers/analyzer.poku.js +230 -0
  20. package/lib/helpers/bomSigner.js +312 -0
  21. package/lib/helpers/bomSigner.poku.js +156 -0
  22. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  23. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  24. package/lib/helpers/ciParsers/circleCi.js +286 -0
  25. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  26. package/lib/helpers/ciParsers/common.js +24 -0
  27. package/lib/helpers/ciParsers/githubActions.js +636 -0
  28. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  29. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  30. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  31. package/lib/helpers/ciParsers/jenkins.js +181 -0
  32. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  33. package/lib/helpers/depsUtils.js +219 -0
  34. package/lib/helpers/depsUtils.poku.js +207 -0
  35. package/lib/helpers/display.js +426 -5
  36. package/lib/helpers/envcontext.js +18 -3
  37. package/lib/helpers/formulationParsers.js +351 -0
  38. package/lib/helpers/logger.js +14 -0
  39. package/lib/helpers/protobom.js +9 -9
  40. package/lib/helpers/pythonutils.js +9 -0
  41. package/lib/helpers/remote/dependency-track.js +84 -0
  42. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  43. package/lib/helpers/table.js +384 -0
  44. package/lib/helpers/table.poku.js +186 -0
  45. package/lib/helpers/utils.js +865 -416
  46. package/lib/helpers/utils.poku.js +172 -265
  47. package/lib/helpers/versutils.js +202 -0
  48. package/lib/helpers/versutils.poku.js +315 -0
  49. package/lib/helpers/vsixutils.js +1061 -0
  50. package/lib/helpers/vsixutils.poku.js +2247 -0
  51. package/lib/managers/binary.js +19 -19
  52. package/lib/managers/docker.js +108 -1
  53. package/lib/managers/oci.js +10 -0
  54. package/lib/managers/piptree.js +3 -9
  55. package/lib/parsers/npmrc.js +17 -13
  56. package/lib/parsers/npmrc.poku.js +41 -5
  57. package/lib/server/openapi.yaml +34 -1
  58. package/lib/server/server.js +50 -13
  59. package/lib/server/server.poku.js +332 -144
  60. package/lib/stages/postgen/annotator.js +1 -1
  61. package/lib/stages/postgen/auditBom.js +196 -0
  62. package/lib/stages/postgen/auditBom.poku.js +378 -0
  63. package/lib/stages/postgen/postgen.js +54 -1
  64. package/lib/stages/postgen/postgen.poku.js +90 -1
  65. package/lib/stages/postgen/ruleEngine.js +369 -0
  66. package/lib/stages/pregen/envAudit.js +299 -0
  67. package/lib/stages/pregen/envAudit.poku.js +572 -0
  68. package/lib/stages/pregen/pregen.js +12 -8
  69. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  70. package/lib/validator/complianceEngine.js +241 -0
  71. package/lib/validator/complianceEngine.poku.js +168 -0
  72. package/lib/validator/complianceRules.js +1610 -0
  73. package/lib/validator/complianceRules.poku.js +328 -0
  74. package/lib/validator/index.js +222 -0
  75. package/lib/validator/index.poku.js +144 -0
  76. package/lib/validator/reporters/annotations.js +121 -0
  77. package/lib/validator/reporters/console.js +149 -0
  78. package/lib/validator/reporters/index.js +41 -0
  79. package/lib/validator/reporters/json.js +37 -0
  80. package/lib/validator/reporters/sarif.js +184 -0
  81. package/lib/validator/reporters.poku.js +150 -0
  82. package/package.json +8 -9
  83. package/types/bin/sign.d.ts +3 -0
  84. package/types/bin/sign.d.ts.map +1 -0
  85. package/types/bin/validate.d.ts +3 -0
  86. package/types/bin/validate.d.ts.map +1 -0
  87. package/types/helpers/utils.d.ts +0 -1
  88. package/types/lib/cli/index.d.ts +49 -52
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/evinser/db.d.ts +34 -0
  91. package/types/lib/evinser/db.d.ts.map +1 -0
  92. package/types/lib/evinser/evinser.d.ts +63 -16
  93. package/types/lib/evinser/evinser.d.ts.map +1 -1
  94. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  95. package/types/lib/helpers/bomSigner.d.ts +27 -0
  96. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  101. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  102. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  103. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  104. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  105. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  106. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  107. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  108. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  109. package/types/lib/helpers/depsUtils.d.ts +21 -0
  110. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  111. package/types/lib/helpers/display.d.ts +111 -11
  112. package/types/lib/helpers/display.d.ts.map +1 -1
  113. package/types/lib/helpers/envcontext.d.ts +19 -7
  114. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  115. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  116. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  117. package/types/lib/helpers/logger.d.ts +15 -1
  118. package/types/lib/helpers/logger.d.ts.map +1 -1
  119. package/types/lib/helpers/protobom.d.ts +2 -2
  120. package/types/lib/helpers/pythonutils.d.ts +10 -1
  121. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  122. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  123. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  124. package/types/lib/helpers/table.d.ts +6 -0
  125. package/types/lib/helpers/table.d.ts.map +1 -0
  126. package/types/lib/helpers/utils.d.ts +533 -128
  127. package/types/lib/helpers/utils.d.ts.map +1 -1
  128. package/types/lib/helpers/versutils.d.ts +8 -0
  129. package/types/lib/helpers/versutils.d.ts.map +1 -0
  130. package/types/lib/helpers/vsixutils.d.ts +130 -0
  131. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  132. package/types/lib/managers/docker.d.ts +12 -31
  133. package/types/lib/managers/docker.d.ts.map +1 -1
  134. package/types/lib/managers/oci.d.ts +11 -1
  135. package/types/lib/managers/oci.d.ts.map +1 -1
  136. package/types/lib/managers/piptree.d.ts.map +1 -1
  137. package/types/lib/parsers/npmrc.d.ts +4 -1
  138. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  139. package/types/lib/server/server.d.ts +22 -2
  140. package/types/lib/server/server.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  142. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  144. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  145. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  146. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  147. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  148. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  149. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  150. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  151. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  152. package/types/lib/validator/complianceEngine.d.ts +66 -0
  153. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  154. package/types/lib/validator/complianceRules.d.ts +70 -0
  155. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  156. package/types/lib/validator/index.d.ts +70 -0
  157. package/types/lib/validator/index.d.ts.map +1 -0
  158. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  159. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  160. package/types/lib/validator/reporters/console.d.ts +30 -0
  161. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  162. package/types/lib/validator/reporters/index.d.ts +21 -0
  163. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  164. package/types/lib/validator/reporters/json.d.ts +11 -0
  165. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  166. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  167. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  168. package/lib/helpers/db.js +0 -162
  169. package/lib/stages/pregen/env-audit.js +0 -34
  170. package/lib/stages/pregen/env-audit.poku.js +0 -290
  171. package/types/helpers/db.d.ts +0 -35
  172. package/types/helpers/db.d.ts.map +0 -1
  173. package/types/lib/helpers/db.d.ts +0 -35
  174. package/types/lib/helpers/db.d.ts.map +0 -1
  175. package/types/lib/helpers/validator.d.ts.map +0 -1
  176. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  177. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  178. package/types/managers/binary.d.ts +0 -37
  179. package/types/managers/binary.d.ts.map +0 -1
  180. package/types/managers/docker.d.ts +0 -56
  181. package/types/managers/docker.d.ts.map +0 -1
  182. package/types/managers/oci.d.ts +0 -2
  183. package/types/managers/oci.d.ts.map +0 -1
  184. package/types/managers/piptree.d.ts +0 -2
  185. package/types/managers/piptree.d.ts.map +0 -1
  186. package/types/server/server.d.ts +0 -34
  187. package/types/server/server.d.ts.map +0 -1
  188. package/types/stages/postgen/annotator.d.ts +0 -27
  189. package/types/stages/postgen/annotator.d.ts.map +0 -1
  190. package/types/stages/postgen/postgen.d.ts +0 -51
  191. package/types/stages/postgen/postgen.d.ts.map +0 -1
  192. package/types/stages/pregen/pregen.d.ts +0 -59
  193. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -1,4 +1,9 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ import esmock from "esmock";
1
5
  import { afterEach, assert, beforeEach, describe, it } from "poku";
6
+ import sinon from "sinon";
2
7
 
3
8
  import { isWin } from "../helpers/utils.js";
4
9
  import {
@@ -11,53 +16,71 @@ import {
11
16
  validateAndRejectGitSource,
12
17
  } from "./server.js";
13
18
 
19
+ function nullProtoObj(obj) {
20
+ if (obj === null || typeof obj !== "object") {
21
+ return obj;
22
+ }
23
+ if (Array.isArray(obj)) {
24
+ return obj.map(nullProtoObj);
25
+ }
26
+ if (Object.prototype.toString.call(obj) === "[object Object]") {
27
+ const result = Object.create(null);
28
+ for (const [key, value] of Object.entries(obj)) {
29
+ result[key] = nullProtoObj(value);
30
+ }
31
+ return result;
32
+ }
33
+ return obj;
34
+ }
35
+
36
+ function checkEqual(actual, expected, message) {
37
+ assert.deepStrictEqual(nullProtoObj(actual), nullProtoObj(expected), message);
38
+ }
39
+
14
40
  it("parseValue tests", () => {
15
- assert.deepStrictEqual(parseValue("foo"), "foo");
16
- assert.deepStrictEqual(parseValue("foo\n"), "foo");
17
- assert.deepStrictEqual(parseValue("foo\r\n"), "foo");
18
- assert.deepStrictEqual(parseValue(1), 1);
19
- assert.deepStrictEqual(parseValue("true"), true);
20
- assert.deepStrictEqual(parseValue("false"), false);
21
- assert.deepStrictEqual(parseValue(["foo", "bar", 42]), ["foo", "bar", 42]);
41
+ checkEqual(parseValue("foo"), "foo");
42
+ checkEqual(parseValue("foo\n"), "foo");
43
+ checkEqual(parseValue("foo\r\n"), "foo");
44
+ checkEqual(parseValue(1), 1);
45
+ checkEqual(parseValue("true"), true);
46
+ checkEqual(parseValue("false"), false);
47
+ checkEqual(parseValue(["foo", "bar", 42]), ["foo", "bar", 42]);
22
48
  assert.throws(() => parseValue({ foo: "bar" }), TypeError);
23
49
  assert.throws(() => parseValue([42, "foo", { foo: "bar" }]), TypeError);
24
50
  assert.throws(() => parseValue([42, "foo", new Error()]), TypeError);
25
51
  assert.throws(() => parseValue(["foo", "bar", new String(42)]), TypeError);
26
- assert.deepStrictEqual(parseValue(true), true);
27
- assert.deepStrictEqual(parseValue(false), false);
28
- assert.deepStrictEqual(parseValue(null), null);
29
- assert.deepStrictEqual(parseValue(undefined), undefined);
30
- assert.deepStrictEqual(parseValue([null, undefined, null]), [
52
+ checkEqual(parseValue(true), true);
53
+ checkEqual(parseValue(false), false);
54
+ checkEqual(parseValue(null), null);
55
+ checkEqual(parseValue(undefined), undefined);
56
+ checkEqual(parseValue([null, undefined, null]), [null, undefined, null]);
57
+ checkEqual(parseValue(""), "");
58
+ checkEqual(parseValue(" \n"), " ");
59
+ checkEqual(parseValue("42"), "42");
60
+ checkEqual(parseValue("0"), "0");
61
+ checkEqual(parseValue("-1"), "-1");
62
+ checkEqual(parseValue("True"), "True");
63
+ checkEqual(parseValue("False"), "False");
64
+ checkEqual(parseValue(" TRUE "), " TRUE ");
65
+ checkEqual(parseValue(["true", "false", 0, "0", null, undefined]), [
66
+ true,
67
+ false,
68
+ 0,
69
+ "0",
31
70
  null,
32
71
  undefined,
33
- null,
34
72
  ]);
35
- assert.deepStrictEqual(parseValue(""), "");
36
- assert.deepStrictEqual(parseValue(" \n"), " ");
37
- assert.deepStrictEqual(parseValue("42"), "42");
38
- assert.deepStrictEqual(parseValue("0"), "0");
39
- assert.deepStrictEqual(parseValue("-1"), "-1");
40
- assert.deepStrictEqual(parseValue("True"), "True");
41
- assert.deepStrictEqual(parseValue("False"), "False");
42
- assert.deepStrictEqual(parseValue(" TRUE "), " TRUE ");
43
- assert.deepStrictEqual(
44
- parseValue(["true", "false", 0, "0", null, undefined]),
45
- [true, false, 0, "0", null, undefined],
46
- );
47
73
  assert.throws(() => parseValue([["nested"]]), TypeError);
48
74
  assert.throws(() => parseValue(Symbol("test")), TypeError);
49
75
  assert.throws(() => parseValue(BigInt(42)), TypeError);
50
76
  // biome-ignore-start lint/suspicious/noEmptyBlockStatements: test
51
77
  assert.throws(() => parseValue(() => {}), TypeError);
52
78
  // biome-ignore-end lint/suspicious/noEmptyBlockStatements: test
53
- assert.deepStrictEqual(parseValue(Number.NaN), Number.NaN);
54
- assert.deepStrictEqual(
55
- parseValue(Number.POSITIVE_INFINITY),
56
- Number.POSITIVE_INFINITY,
57
- );
79
+ checkEqual(parseValue(Number.NaN), Number.NaN);
80
+ checkEqual(parseValue(Number.POSITIVE_INFINITY), Number.POSITIVE_INFINITY);
58
81
  const obj = { toString: () => "foo" };
59
82
  assert.throws(() => parseValue(obj), TypeError);
60
- assert.deepStrictEqual(parseValue("hello\r\n"), "hello");
83
+ checkEqual(parseValue("hello\r\n"), "hello");
61
84
  });
62
85
 
63
86
  describe("parseQueryString tests", () => {
@@ -70,22 +93,42 @@ describe("parseQueryString tests", () => {
70
93
  };
71
94
  const options = {};
72
95
  const result = parseQueryString(q, body, options);
73
- assert.deepStrictEqual(result.foo, undefined);
74
- assert.deepStrictEqual(result.excludeType, ["2"]);
75
- assert.deepStrictEqual(result.technique, ["manifest-analysis"]);
96
+ checkEqual(result.foo, undefined);
97
+ checkEqual(result.excludeType, ["2"]);
98
+ checkEqual(result.technique, ["manifest-analysis"]);
76
99
  });
77
100
 
78
101
  it("splits type into projectType and removes type", () => {
79
102
  const options = { type: "a,b,c" };
80
103
  const result = parseQueryString({}, {}, options);
81
- assert.deepStrictEqual(result.projectType, ["a", "b", "c"]);
82
- assert.deepStrictEqual(result.type, undefined);
104
+ checkEqual(result.projectType, ["a", "b", "c"]);
105
+ checkEqual(result.type, undefined);
83
106
  });
84
107
 
85
108
  it("sets installDeps to false for pre-build lifecycle", () => {
86
109
  const options = { lifecycle: "pre-build" };
87
110
  const result = parseQueryString({}, {}, options);
88
- assert.deepStrictEqual(result.installDeps, false);
111
+ checkEqual(result.installDeps, false);
112
+ });
113
+
114
+ it("parses parentProjectName and parentProjectVersion", () => {
115
+ const q = {
116
+ parentProjectName: "parent-app",
117
+ parentProjectVersion: "1.2.3",
118
+ };
119
+ const result = parseQueryString(q, {}, {});
120
+ checkEqual(result.parentProjectName, "parent-app");
121
+ checkEqual(result.parentProjectVersion, "1.2.3");
122
+ });
123
+
124
+ it("parses autoCreate and isLatest boolean options", () => {
125
+ const q = {
126
+ autoCreate: "false",
127
+ isLatest: "true",
128
+ };
129
+ const result = parseQueryString(q, {}, {});
130
+ checkEqual(result.autoCreate, false);
131
+ checkEqual(result.isLatest, true);
89
132
  });
90
133
  });
91
134
 
@@ -102,23 +145,23 @@ describe("isAllowedHost()", () => {
102
145
 
103
146
  it("returns true if CDXGEN_SERVER_ALLOWED_HOSTS is not set", () => {
104
147
  delete process.env.CDXGEN_SERVER_ALLOWED_HOSTS;
105
- assert.deepStrictEqual(isAllowedHost("anything"), true);
148
+ checkEqual(isAllowedHost("anything"), true);
106
149
  });
107
150
 
108
151
  it("returns true for a hostname that is in the list", () => {
109
152
  process.env.CDXGEN_SERVER_ALLOWED_HOSTS = "foo.com,bar.com";
110
- assert.deepStrictEqual(isAllowedHost("foo.com"), true);
111
- assert.deepStrictEqual(isAllowedHost("bar.com"), true);
153
+ checkEqual(isAllowedHost("foo.com"), true);
154
+ checkEqual(isAllowedHost("bar.com"), true);
112
155
  });
113
156
 
114
157
  it("returns false for a hostname not in the list", () => {
115
158
  process.env.CDXGEN_SERVER_ALLOWED_HOSTS = "foo.com,bar.com";
116
- assert.deepStrictEqual(isAllowedHost("baz.com"), false);
159
+ checkEqual(isAllowedHost("baz.com"), false);
117
160
  });
118
161
 
119
162
  it("treats an empty-string env var as unset (returns true)", () => {
120
163
  process.env.CDXGEN_SERVER_ALLOWED_HOSTS = "";
121
- assert.deepStrictEqual(isAllowedHost("whatever"), true);
164
+ checkEqual(isAllowedHost("whatever"), true);
122
165
  });
123
166
  });
124
167
 
@@ -203,15 +246,15 @@ describe("isAllowedPath()", () => {
203
246
  describe("isAllowedWinPath windows tests()", () => {
204
247
  it("returns false for windows device name paths", () => {
205
248
  if (isWin) {
206
- assert.deepStrictEqual(isAllowedWinPath("CON:../foo"), false);
207
- assert.deepStrictEqual(isAllowedWinPath("X:\\foo\\..\\bar"), true);
208
- assert.deepStrictEqual(isAllowedWinPath("C:\\Users"), true);
209
- assert.deepStrictEqual(isAllowedWinPath("C:\\🚀"), true);
210
- assert.deepStrictEqual(isAllowedWinPath("C:"), true);
211
- assert.deepStrictEqual(isAllowedWinPath("c:"), true);
212
- assert.deepStrictEqual(isAllowedWinPath("CON:"), false);
213
- assert.deepStrictEqual(isAllowedWinPath("COM¹:"), false);
214
- assert.deepStrictEqual(isAllowedWinPath("COM¹:../foo"), false);
249
+ checkEqual(isAllowedWinPath("CON:../foo"), false);
250
+ checkEqual(isAllowedWinPath("X:\\foo\\..\\bar"), true);
251
+ checkEqual(isAllowedWinPath("C:\\Users"), true);
252
+ checkEqual(isAllowedWinPath("C:\\🚀"), true);
253
+ checkEqual(isAllowedWinPath("C:"), true);
254
+ checkEqual(isAllowedWinPath("c:"), true);
255
+ checkEqual(isAllowedWinPath("CON:"), false);
256
+ checkEqual(isAllowedWinPath("COM¹:"), false);
257
+ checkEqual(isAllowedWinPath("COM¹:../foo"), false);
215
258
  for (const d of [
216
259
  "PRN:.\\..\\bar",
217
260
  "LpT5:/another/path",
@@ -244,7 +287,7 @@ describe("isAllowedWinPath windows tests()", () => {
244
287
  "🚀:\\",
245
288
  "⚡:\\",
246
289
  ]) {
247
- assert.deepStrictEqual(isAllowedWinPath(d), false);
290
+ checkEqual(isAllowedWinPath(d), false);
248
291
  }
249
292
  }
250
293
  });
@@ -264,7 +307,7 @@ describe("getQueryParams", () => {
264
307
  );
265
308
  const result = getQueryParams(req);
266
309
 
267
- assert.deepStrictEqual(result, {
310
+ checkEqual(result, {
268
311
  url: "https://example.com",
269
312
  multiProject: "true",
270
313
  type: "js",
@@ -277,7 +320,7 @@ describe("getQueryParams", () => {
277
320
  );
278
321
  const result = getQueryParams(req);
279
322
 
280
- assert.deepStrictEqual(result, {
323
+ checkEqual(result, {
281
324
  q: "hello world",
282
325
  filter: "category=tech",
283
326
  });
@@ -288,7 +331,7 @@ describe("getQueryParams", () => {
288
331
  const result = getQueryParams(req);
289
332
 
290
333
  // URLSearchParams.entries() returns the first value when there are duplicates
291
- assert.deepStrictEqual(result, {
334
+ checkEqual(result, {
292
335
  tags: ["javascript", "react", "node"],
293
336
  });
294
337
  });
@@ -297,21 +340,21 @@ describe("getQueryParams", () => {
297
340
  const req = createMockRequest("/sbom");
298
341
  const result = getQueryParams(req);
299
342
 
300
- assert.deepStrictEqual(result, {});
343
+ checkEqual(result, {});
301
344
  });
302
345
 
303
346
  it("should handle query string with only question mark", () => {
304
347
  const req = createMockRequest("/sbom?");
305
348
  const result = getQueryParams(req);
306
349
 
307
- assert.deepStrictEqual(result, {});
350
+ checkEqual(result, {});
308
351
  });
309
352
 
310
353
  it("should handle parameters without values", () => {
311
354
  const req = createMockRequest("/api?flag1&flag2&param=value");
312
355
  const result = getQueryParams(req);
313
356
 
314
- assert.deepStrictEqual(result, {
357
+ checkEqual(result, {
315
358
  flag1: "",
316
359
  flag2: "",
317
360
  param: "value",
@@ -325,7 +368,7 @@ describe("getQueryParams", () => {
325
368
  );
326
369
  const result = getQueryParams(req);
327
370
 
328
- assert.deepStrictEqual(result, {
371
+ checkEqual(result, {
329
372
  param1: "value1",
330
373
  });
331
374
  });
@@ -338,7 +381,7 @@ describe("getQueryParams", () => {
338
381
  );
339
382
  const result = getQueryParams(req);
340
383
 
341
- assert.deepStrictEqual(result, {
384
+ checkEqual(result, {
342
385
  token: "abc123",
343
386
  });
344
387
  });
@@ -349,7 +392,7 @@ describe("getQueryParams", () => {
349
392
  );
350
393
  const result = getQueryParams(req);
351
394
 
352
- assert.deepStrictEqual(result, {
395
+ checkEqual(result, {
353
396
  name: "john",
354
397
  age: "25",
355
398
  active: "true",
@@ -362,7 +405,7 @@ describe("getQueryParams", () => {
362
405
  );
363
406
  const result = getQueryParams(req);
364
407
 
365
- assert.deepStrictEqual(result, {
408
+ checkEqual(result, {
366
409
  q: "hello world!",
367
410
  category: "web development",
368
411
  });
@@ -372,14 +415,14 @@ describe("getQueryParams", () => {
372
415
  const req = createMockRequest(undefined);
373
416
  const result = getQueryParams(req);
374
417
 
375
- assert.deepStrictEqual(result, {});
418
+ checkEqual(result, {});
376
419
  });
377
420
 
378
421
  it("should handle numeric values as strings", () => {
379
422
  const req = createMockRequest("/calculate?x=10&y=20&operation=add");
380
423
  const result = getQueryParams(req);
381
424
 
382
- assert.deepStrictEqual(result, {
425
+ checkEqual(result, {
383
426
  x: "10",
384
427
  y: "20",
385
428
  operation: "add",
@@ -390,7 +433,7 @@ describe("getQueryParams", () => {
390
433
  const req = createMockRequest("/config?debug=true&verbose=false&enabled=1");
391
434
  const result = getQueryParams(req);
392
435
 
393
- assert.deepStrictEqual(result, {
436
+ checkEqual(result, {
394
437
  debug: "true",
395
438
  verbose: "false",
396
439
  enabled: "1",
@@ -402,7 +445,7 @@ describe("getQueryParams", () => {
402
445
  const req = createMockRequest("not-a-valid-url");
403
446
  const result = getQueryParams(req);
404
447
 
405
- assert.deepStrictEqual(result, {});
448
+ checkEqual(result, {});
406
449
  });
407
450
 
408
451
  it("should handle empty host gracefully", () => {
@@ -413,7 +456,7 @@ describe("getQueryParams", () => {
413
456
  };
414
457
  const result = getQueryParams(req);
415
458
 
416
- assert.deepStrictEqual(result, {
459
+ checkEqual(result, {
417
460
  param: "value",
418
461
  });
419
462
  });
@@ -426,7 +469,7 @@ describe("getQueryParams", () => {
426
469
  };
427
470
  const result = getQueryParams(req);
428
471
 
429
- assert.deepStrictEqual(result, {
472
+ checkEqual(result, {
430
473
  param: "value",
431
474
  });
432
475
  });
@@ -450,84 +493,60 @@ describe("validateGitSource() tests", () => {
450
493
  });
451
494
 
452
495
  it("should reject ext:: and fd:: outright", () => {
453
- assert.deepStrictEqual(
496
+ checkEqual(
454
497
  validateAndRejectGitSource("ext::sh -c id").error,
455
498
  "Invalid Protocol",
456
499
  );
457
- assert.deepStrictEqual(
458
- validateAndRejectGitSource("fd::123").error,
459
- "Invalid Protocol",
460
- );
461
- assert.deepStrictEqual(
500
+ checkEqual(validateAndRejectGitSource("fd::123").error, "Invalid Protocol");
501
+ checkEqual(
462
502
  validateAndRejectGitSource("EXT::sh -c id").error,
463
503
  "Invalid Protocol",
464
504
  );
465
505
  });
466
506
 
467
507
  it("should allow standard local paths to bypass validation", () => {
468
- assert.deepStrictEqual(validateAndRejectGitSource("/tmp/local-path"), null);
469
- assert.deepStrictEqual(
470
- validateAndRejectGitSource("C:\\Users\\local"),
471
- null,
472
- );
508
+ checkEqual(validateAndRejectGitSource("/tmp/local-path"), null);
509
+ checkEqual(validateAndRejectGitSource("C:\\Users\\local"), null);
473
510
  });
474
511
 
475
512
  it("should handle ssh git@ format gracefully", () => {
476
- assert.deepStrictEqual(
477
- validateAndRejectGitSource("git@github.com:foo/bar.git"),
478
- null,
479
- );
513
+ checkEqual(validateAndRejectGitSource("git@github.com:foo/bar.git"), null);
480
514
  });
481
515
 
482
516
  it("should reject malformed git URLs", () => {
483
517
  // invalid URL format (can't parse via node's new URL object)
484
- assert.deepStrictEqual(
518
+ checkEqual(
485
519
  validateAndRejectGitSource("http://[:::1]/bad-ipv6").error,
486
520
  "Invalid URL Format",
487
521
  );
488
522
  });
489
523
 
490
524
  it("should enforce GIT_ALLOW_PROTOCOL default schemes", () => {
491
- assert.deepStrictEqual(
492
- validateAndRejectGitSource("https://github.com/repo"),
493
- null,
494
- );
495
- assert.deepStrictEqual(
496
- validateAndRejectGitSource("http://github.com/repo"),
497
- {
498
- status: 400,
499
- error: "Protocol Not Allowed",
500
- details: "The protocol 'http:' is not permitted by GIT_ALLOW_PROTOCOL.",
501
- },
502
- );
503
- assert.deepStrictEqual(
504
- validateAndRejectGitSource("git://github.com/repo"),
505
- null,
506
- );
507
- assert.deepStrictEqual(
508
- validateAndRejectGitSource("ssh://github.com/repo"),
509
- null,
510
- );
511
- assert.deepStrictEqual(
512
- validateAndRejectGitSource("git+ssh://github.com/repo"),
513
- null,
514
- );
525
+ checkEqual(validateAndRejectGitSource("https://github.com/repo"), null);
526
+ checkEqual(validateAndRejectGitSource("http://github.com/repo"), {
527
+ status: 400,
528
+ error: "Protocol Not Allowed",
529
+ details: "The protocol 'http:' is not permitted by GIT_ALLOW_PROTOCOL.",
530
+ });
531
+ checkEqual(validateAndRejectGitSource("git://github.com/repo"), null);
532
+ checkEqual(validateAndRejectGitSource("ssh://github.com/repo"), null);
533
+ checkEqual(validateAndRejectGitSource("git+ssh://github.com/repo"), null);
515
534
 
516
535
  // ftp is not allowed by default
517
536
  const res = validateAndRejectGitSource("ftp://github.com/repo");
518
- assert.deepStrictEqual(res.error, "Protocol Not Allowed");
519
- assert.deepStrictEqual(
537
+ checkEqual(res.error, "Protocol Not Allowed");
538
+ checkEqual(
520
539
  res.details,
521
540
  "The protocol 'ftp:' is not permitted by GIT_ALLOW_PROTOCOL.",
522
541
  );
523
542
  });
524
543
 
525
544
  it("should reject protocol smuggling techniques", () => {
526
- assert.deepStrictEqual(
545
+ checkEqual(
527
546
  validateAndRejectGitSource("git+ext://github.com/repo").error,
528
547
  "Protocol Not Allowed",
529
548
  );
530
- assert.deepStrictEqual(
549
+ checkEqual(
531
550
  validateAndRejectGitSource("http+ext://github.com/repo").error,
532
551
  "Protocol Not Allowed",
533
552
  );
@@ -535,30 +554,24 @@ describe("validateGitSource() tests", () => {
535
554
 
536
555
  it("should respect custom CDXGEN_SERVER_GIT_ALLOW_PROTOCOL configs", () => {
537
556
  process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL = "https:git";
538
- assert.deepStrictEqual(
539
- validateAndRejectGitSource("https://github.com/repo"),
540
- null,
541
- );
542
- assert.deepStrictEqual(
543
- validateAndRejectGitSource("git://github.com/repo"),
544
- null,
545
- );
557
+ checkEqual(validateAndRejectGitSource("https://github.com/repo"), null);
558
+ checkEqual(validateAndRejectGitSource("git://github.com/repo"), null);
546
559
 
547
560
  // http is no longer allowed
548
561
  const res = validateAndRejectGitSource("http://github.com/repo");
549
- assert.deepStrictEqual(res.error, "Protocol Not Allowed");
550
- assert.deepStrictEqual(
562
+ checkEqual(res.error, "Protocol Not Allowed");
563
+ checkEqual(
551
564
  res.details,
552
565
  "The protocol 'http:' is not permitted by GIT_ALLOW_PROTOCOL.",
553
566
  );
554
567
  });
555
568
 
556
569
  it("should reject remote helper syntax (::) inside valid schemes", () => {
557
- assert.deepStrictEqual(
570
+ checkEqual(
558
571
  validateAndRejectGitSource("https://github.com/ext::sh -c id").error,
559
572
  "Invalid URL Syntax",
560
573
  );
561
- assert.deepStrictEqual(
574
+ checkEqual(
562
575
  validateAndRejectGitSource("git://foo::bar/repo").error,
563
576
  "Invalid URL Format",
564
577
  );
@@ -566,44 +579,219 @@ describe("validateGitSource() tests", () => {
566
579
 
567
580
  it("should validate allowed hosts", () => {
568
581
  process.env.CDXGEN_SERVER_ALLOWED_HOSTS = "github.com,gitlab.com";
569
- assert.deepStrictEqual(
570
- validateAndRejectGitSource("https://github.com/repo"),
571
- null,
572
- );
582
+ checkEqual(validateAndRejectGitSource("https://github.com/repo"), null);
573
583
 
574
584
  const res = validateAndRejectGitSource("https://evil.com/repo");
575
- assert.deepStrictEqual(res.error, "Host Not Allowed");
576
- assert.deepStrictEqual(res.status, 403);
585
+ checkEqual(res.error, "Host Not Allowed");
586
+ checkEqual(res.status, 403);
577
587
  });
578
588
  });
579
589
  it("should correctly normalize and validate various git@ (SCP-like) formats", () => {
580
- assert.deepStrictEqual(
590
+ checkEqual(
581
591
  validateAndRejectGitSource("git@gitlab.com:group/project.git"),
582
592
  null,
583
593
  );
584
- assert.deepStrictEqual(
594
+ checkEqual(
585
595
  validateAndRejectGitSource("git@bitbucket.org:workspace/repo:name.git"),
586
596
  null,
587
597
  );
588
- assert.deepStrictEqual(
589
- validateAndRejectGitSource("git@github.com/user/repo.git"),
590
- null,
591
- );
592
- assert.deepStrictEqual(
598
+ checkEqual(validateAndRejectGitSource("git@github.com/user/repo.git"), null);
599
+ checkEqual(
593
600
  validateAndRejectGitSource("ssh://git@github.com/user/repo.git"),
594
601
  null,
595
602
  );
596
603
  process.env.CDXGEN_SERVER_ALLOWED_HOSTS = "github.com,bitbucket.org";
597
- assert.deepStrictEqual(
598
- validateAndRejectGitSource("git@github.com:user/repo.git"),
599
- null,
600
- );
601
- assert.deepStrictEqual(
604
+ checkEqual(validateAndRejectGitSource("git@github.com:user/repo.git"), null);
605
+ checkEqual(
602
606
  validateAndRejectGitSource("git@bitbucket.org:workspace/repo.git"),
603
607
  null,
604
608
  );
605
609
  const deniedRes = validateAndRejectGitSource("git@evil.com:foo/bar.git");
606
- assert.deepStrictEqual(deniedRes.status, 403);
607
- assert.deepStrictEqual(deniedRes.error, "Host Not Allowed");
610
+ checkEqual(deniedRes.status, 403);
611
+ checkEqual(deniedRes.error, "Host Not Allowed");
608
612
  delete process.env.CDXGEN_SERVER_ALLOWED_HOSTS;
609
613
  });
614
+
615
+ describe("gitClone() hardening tests", () => {
616
+ it("passes core.hooksPath=/dev/null and --template= flags to git", async () => {
617
+ const spawnStub = sinon.stub().returns({ status: 0, stderr: "" });
618
+ const mkdtempStub = sinon
619
+ .stub()
620
+ .returns(path.join(os.tmpdir(), "fake-repo"));
621
+
622
+ const { gitClone } = await esmock("./server.js", {
623
+ "../helpers/utils.js": {
624
+ safeSpawnSync: spawnStub,
625
+ isSecureMode: false,
626
+ hasDangerousUnicode: sinon.stub().returns(false),
627
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
628
+ },
629
+ "node:fs": {
630
+ mkdtempSync: mkdtempStub,
631
+ existsSync: sinon.stub().returns(false),
632
+ readdirSync: sinon.stub().returns([]),
633
+ statSync: sinon.stub().returns({ isDirectory: () => true }),
634
+ readFileSync: sinon.stub().returns(""),
635
+ },
636
+ });
637
+
638
+ gitClone("https://example.com/repo.git");
639
+
640
+ sinon.assert.calledOnce(spawnStub);
641
+ const [cmd, args] = spawnStub.firstCall.args;
642
+ assert.strictEqual(cmd, "git");
643
+
644
+ // core.hooksPath=/dev/null must be present as a -c flag
645
+ const hooksPathIdx = args.indexOf("core.hooksPath=/dev/null");
646
+ assert.ok(
647
+ hooksPathIdx > 0 && args[hooksPathIdx - 1] === "-c",
648
+ "expected -c core.hooksPath=/dev/null in git args",
649
+ );
650
+
651
+ // --template= must appear after "clone"
652
+ const cloneIdx = args.indexOf("clone");
653
+ assert.ok(cloneIdx >= 0, "expected 'clone' subcommand in git args");
654
+ assert.ok(
655
+ args.slice(cloneIdx).includes("--template="),
656
+ "expected --template= after clone in git args",
657
+ );
658
+ });
659
+
660
+ it("uses GIT_CONFIG_GLOBAL=/dev/null instead of invalid GIT_CONFIG_NOGLOBAL in secure mode", async () => {
661
+ const spawnStub = sinon.stub().returns({ status: 0, stderr: "" });
662
+ const mkdtempStub = sinon
663
+ .stub()
664
+ .returns(path.join(os.tmpdir(), "fake-repo"));
665
+
666
+ const { gitClone } = await esmock("./server.js", {
667
+ "../helpers/utils.js": {
668
+ safeSpawnSync: spawnStub,
669
+ isSecureMode: true,
670
+ hasDangerousUnicode: sinon.stub().returns(false),
671
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
672
+ },
673
+ "node:fs": {
674
+ mkdtempSync: mkdtempStub,
675
+ existsSync: sinon.stub().returns(false),
676
+ readdirSync: sinon.stub().returns([]),
677
+ statSync: sinon.stub().returns({ isDirectory: () => true }),
678
+ readFileSync: sinon.stub().returns(""),
679
+ },
680
+ });
681
+
682
+ gitClone("https://example.com/repo.git");
683
+
684
+ sinon.assert.calledOnce(spawnStub);
685
+ const opts = spawnStub.firstCall.args[2];
686
+ assert.ok(opts.env, "expected env to be set");
687
+ assert.ok(
688
+ !("GIT_CONFIG_NOGLOBAL" in opts.env),
689
+ "GIT_CONFIG_NOGLOBAL must not be set (it is not a valid Git env var)",
690
+ );
691
+ assert.strictEqual(
692
+ opts.env.GIT_CONFIG_GLOBAL,
693
+ "/dev/null",
694
+ "GIT_CONFIG_GLOBAL must be /dev/null in secure mode",
695
+ );
696
+ assert.strictEqual(
697
+ opts.env.GIT_CONFIG_NOSYSTEM,
698
+ "1",
699
+ "GIT_CONFIG_NOSYSTEM must be '1' in secure mode",
700
+ );
701
+ });
702
+
703
+ it("sets GIT_TERMINAL_PROMPT=0 in both secure and non-secure mode", async () => {
704
+ for (const secureMode of [false, true]) {
705
+ const spawnStub = sinon.stub().returns({ status: 0, stderr: "" });
706
+ const mkdtempStub = sinon
707
+ .stub()
708
+ .returns(path.join(os.tmpdir(), "fake-repo"));
709
+
710
+ const { gitClone } = await esmock("./server.js", {
711
+ "../helpers/utils.js": {
712
+ safeSpawnSync: spawnStub,
713
+ isSecureMode: secureMode,
714
+ hasDangerousUnicode: sinon.stub().returns(false),
715
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
716
+ },
717
+ "node:fs": {
718
+ mkdtempSync: mkdtempStub,
719
+ existsSync: sinon.stub().returns(false),
720
+ readdirSync: sinon.stub().returns([]),
721
+ statSync: sinon.stub().returns({ isDirectory: () => true }),
722
+ readFileSync: sinon.stub().returns(""),
723
+ },
724
+ });
725
+
726
+ gitClone("https://example.com/repo.git");
727
+
728
+ const opts = spawnStub.firstCall.args[2];
729
+ assert.strictEqual(
730
+ opts.env.GIT_TERMINAL_PROMPT,
731
+ "0",
732
+ `GIT_TERMINAL_PROMPT must be '0' when isSecureMode=${secureMode}`,
733
+ );
734
+ }
735
+ });
736
+
737
+ it("inserts --branch before repoUrl when a valid branch is specified", async () => {
738
+ const spawnStub = sinon.stub().returns({ status: 0, stderr: "" });
739
+ const mkdtempStub = sinon
740
+ .stub()
741
+ .returns(path.join(os.tmpdir(), "fake-repo"));
742
+
743
+ const { gitClone } = await esmock("./server.js", {
744
+ "../helpers/utils.js": {
745
+ safeSpawnSync: spawnStub,
746
+ isSecureMode: false,
747
+ hasDangerousUnicode: sinon.stub().returns(false),
748
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
749
+ },
750
+ "node:fs": {
751
+ mkdtempSync: mkdtempStub,
752
+ existsSync: sinon.stub().returns(false),
753
+ readdirSync: sinon.stub().returns([]),
754
+ statSync: sinon.stub().returns({ isDirectory: () => true }),
755
+ readFileSync: sinon.stub().returns(""),
756
+ },
757
+ });
758
+
759
+ gitClone("https://example.com/repo.git", "main");
760
+
761
+ const [, args] = spawnStub.firstCall.args;
762
+ const branchIdx = args.indexOf("--branch");
763
+ assert.ok(branchIdx >= 0, "expected --branch in git args");
764
+ assert.strictEqual(args[branchIdx + 1], "main");
765
+ });
766
+
767
+ it("skips --branch when the branch name starts with a dash", async () => {
768
+ const spawnStub = sinon.stub().returns({ status: 0, stderr: "" });
769
+ const mkdtempStub = sinon
770
+ .stub()
771
+ .returns(path.join(os.tmpdir(), "fake-repo"));
772
+
773
+ const { gitClone } = await esmock("./server.js", {
774
+ "../helpers/utils.js": {
775
+ safeSpawnSync: spawnStub,
776
+ isSecureMode: false,
777
+ hasDangerousUnicode: sinon.stub().returns(false),
778
+ getTmpDir: sinon.stub().returns(os.tmpdir()),
779
+ },
780
+ "node:fs": {
781
+ mkdtempSync: mkdtempStub,
782
+ existsSync: sinon.stub().returns(false),
783
+ readdirSync: sinon.stub().returns([]),
784
+ statSync: sinon.stub().returns({ isDirectory: () => true }),
785
+ readFileSync: sinon.stub().returns(""),
786
+ },
787
+ });
788
+
789
+ gitClone("https://example.com/repo.git", "--malicious");
790
+
791
+ const [, args] = spawnStub.firstCall.args;
792
+ assert.ok(
793
+ !args.includes("--branch"),
794
+ "must not include --branch for dash-prefixed branch names",
795
+ );
796
+ });
797
+ });