@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
@@ -0,0 +1,213 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import { parse as _load } from "yaml";
5
+
6
+ import { disambiguateSteps } from "./common.js";
7
+
8
+ /**
9
+ * Parse a single .gitlab-ci.yml file and return formulation-shaped data.
10
+ *
11
+ * @param {string} f Absolute path to the YAML file
12
+ * @param {Object} _options CLI options
13
+ * @returns {{ workflows: Object[], components: Object[], services: Object[], properties: Object[], dependencies: Object[] }}
14
+ */
15
+ function parseGitlabCiFile(f, _options) {
16
+ const workflows = [];
17
+ const components = [];
18
+ const services = [];
19
+ const dependencies = [];
20
+
21
+ let raw;
22
+ try {
23
+ raw = readFileSync(f, { encoding: "utf-8" });
24
+ } catch (_e) {
25
+ return { workflows, components, services, properties: [], dependencies };
26
+ }
27
+
28
+ let yamlObj;
29
+ try {
30
+ yamlObj = _load(raw);
31
+ } catch (_e) {
32
+ return { workflows, components, services, properties: [], dependencies };
33
+ }
34
+
35
+ if (!yamlObj || typeof yamlObj !== "object") {
36
+ return { workflows, components, services, properties: [], dependencies };
37
+ }
38
+
39
+ // Top-level reserved keys that are not job names
40
+ const RESERVED_KEYS = new Set([
41
+ "image",
42
+ "services",
43
+ "stages",
44
+ "types",
45
+ "before_script",
46
+ "after_script",
47
+ "variables",
48
+ "cache",
49
+ "include",
50
+ "workflow",
51
+ "default",
52
+ "pages",
53
+ ".pre",
54
+ ".post",
55
+ ]);
56
+
57
+ const globalImage = yamlObj.image?.name || yamlObj.image || "";
58
+ const stages = Array.isArray(yamlObj.stages) ? yamlObj.stages : [];
59
+
60
+ // Collect global services as CycloneDX service objects
61
+ const globalServices = Array.isArray(yamlObj.services)
62
+ ? yamlObj.services
63
+ : [];
64
+ for (const svc of globalServices) {
65
+ const svcName = typeof svc === "string" ? svc : svc?.name || "";
66
+ if (svcName) {
67
+ services.push({ name: svcName });
68
+ }
69
+ }
70
+
71
+ const tasks = [];
72
+ const workflowRef = uuidv4();
73
+ const workflowDependsOn = [];
74
+
75
+ for (const key of Object.keys(yamlObj)) {
76
+ if (RESERVED_KEYS.has(key) || key.startsWith(".")) {
77
+ continue;
78
+ }
79
+ const job = yamlObj[key];
80
+ if (!job || typeof job !== "object" || Array.isArray(job)) {
81
+ continue;
82
+ }
83
+
84
+ const jobRef = uuidv4();
85
+ const steps = [];
86
+ const jobProperties = [{ name: "cdx:gitlab:job:name", value: key }];
87
+
88
+ const jobStage = job.stage || "test";
89
+ jobProperties.push({ name: "cdx:gitlab:job:stage", value: jobStage });
90
+
91
+ const jobImage = job.image?.name || job.image || globalImage;
92
+ if (jobImage) {
93
+ jobProperties.push({ name: "cdx:gitlab:job:image", value: jobImage });
94
+ components.push({ type: "container", name: jobImage });
95
+ }
96
+
97
+ const jobEnv = job.environment?.name || job.environment || "";
98
+ if (jobEnv) {
99
+ jobProperties.push({ name: "cdx:gitlab:job:environment", value: jobEnv });
100
+ }
101
+
102
+ // Collect job-level services
103
+ const jobServices = Array.isArray(job.services) ? job.services : [];
104
+ for (const svc of jobServices) {
105
+ const svcName = typeof svc === "string" ? svc : svc?.name || "";
106
+ if (svcName) {
107
+ services.push({ name: svcName });
108
+ }
109
+ }
110
+ if (jobServices.length) {
111
+ jobProperties.push({
112
+ name: "cdx:gitlab:job:services",
113
+ value: jobServices
114
+ .map((s) => (typeof s === "string" ? s : s?.name || ""))
115
+ .join(","),
116
+ });
117
+ }
118
+
119
+ const jobNeeds = Array.isArray(job.needs) ? job.needs : [];
120
+ if (jobNeeds.length) {
121
+ const jobNeedNames = jobNeeds
122
+ .map((need) => (typeof need === "string" ? need : need?.job || ""))
123
+ .filter(Boolean);
124
+ if (jobNeedNames.length) {
125
+ jobProperties.push({
126
+ name: "cdx:gitlab:job:needs",
127
+ value: jobNeedNames.join(","),
128
+ });
129
+ }
130
+ }
131
+
132
+ // before_script
133
+ for (const cmd of Array.isArray(job.before_script)
134
+ ? job.before_script
135
+ : []) {
136
+ steps.push({ name: "before_script", commands: [{ executed: cmd }] });
137
+ }
138
+ // script (main)
139
+ for (const cmd of Array.isArray(job.script) ? job.script : []) {
140
+ steps.push({ name: "script", commands: [{ executed: cmd }] });
141
+ }
142
+ // after_script
143
+ for (const cmd of Array.isArray(job.after_script) ? job.after_script : []) {
144
+ steps.push({ name: "after_script", commands: [{ executed: cmd }] });
145
+ }
146
+
147
+ tasks.push({
148
+ "bom-ref": jobRef,
149
+ uid: jobRef,
150
+ name: key,
151
+ taskTypes: ["build"],
152
+ steps: disambiguateSteps(steps),
153
+ properties: jobProperties,
154
+ });
155
+ workflowDependsOn.push(jobRef);
156
+ }
157
+
158
+ const stagesProperty = stages.length
159
+ ? [{ name: "cdx:gitlab:stages", value: stages.join(",") }]
160
+ : [];
161
+
162
+ const workflow = {
163
+ "bom-ref": workflowRef,
164
+ uid: workflowRef,
165
+ name: "GitLab CI Pipeline",
166
+ taskTypes: ["build"],
167
+ tasks: tasks.length ? tasks : undefined,
168
+ properties: [{ name: "cdx:gitlab:config", value: f }, ...stagesProperty],
169
+ };
170
+
171
+ workflows.push(workflow);
172
+ if (workflowDependsOn.length) {
173
+ dependencies.push({ ref: workflowRef, dependsOn: workflowDependsOn });
174
+ }
175
+
176
+ return { workflows, components, services, properties: [], dependencies };
177
+ }
178
+
179
+ /**
180
+ * GitLab CI formulation parser.
181
+ *
182
+ * Matches `.gitlab-ci.yml` files and converts them into CycloneDX formulation
183
+ * workflow objects. Each GitLab job becomes a task; script lines become steps.
184
+ *
185
+ * Parser contract: `parse(files, options)` returns
186
+ * `{ workflows, components, services, properties, dependencies }`.
187
+ */
188
+ export const gitlabCiParser = {
189
+ id: "gitlab-ci",
190
+ patterns: ["**/.gitlab-ci.yml", ".gitlab-ci.yml"],
191
+
192
+ /**
193
+ * @param {string[]} files Matched CI config file paths
194
+ * @param {Object} options CLI options
195
+ * @returns {{ workflows: Object[], components: Object[], services: Object[], properties: Object[], dependencies: Object[] }}
196
+ */
197
+ parse(files, options) {
198
+ const workflows = [];
199
+ const components = [];
200
+ const services = [];
201
+ const dependencies = [];
202
+
203
+ for (const f of files) {
204
+ const result = parseGitlabCiFile(f, options);
205
+ workflows.push(...result.workflows);
206
+ components.push(...result.components);
207
+ services.push(...result.services);
208
+ dependencies.push(...result.dependencies);
209
+ }
210
+
211
+ return { workflows, components, services, properties: [], dependencies };
212
+ },
213
+ };
@@ -0,0 +1,247 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { assert, describe, it } from "poku";
5
+
6
+ import { gitlabCiParser } from "./gitlabCi.js";
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const repoRoot = path.resolve(__dirname, "../../..");
10
+
11
+ describe("gitlabCiParser", () => {
12
+ it("has correct metadata", () => {
13
+ assert.strictEqual(gitlabCiParser.id, "gitlab-ci");
14
+ assert.ok(Array.isArray(gitlabCiParser.patterns));
15
+ assert.ok(gitlabCiParser.patterns.length > 0);
16
+ assert.strictEqual(typeof gitlabCiParser.parse, "function");
17
+ });
18
+
19
+ it("returns empty arrays for no files", () => {
20
+ const result = gitlabCiParser.parse([], {});
21
+ assert.deepStrictEqual(result.workflows, []);
22
+ assert.deepStrictEqual(result.components, []);
23
+ assert.deepStrictEqual(result.services, []);
24
+ assert.deepStrictEqual(result.properties, []);
25
+ assert.deepStrictEqual(result.dependencies, []);
26
+ });
27
+
28
+ it("parses the GitLab CI fixture", () => {
29
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml");
30
+ const result = gitlabCiParser.parse([f], {});
31
+
32
+ assert.ok(Array.isArray(result.workflows));
33
+ assert.strictEqual(result.workflows.length, 1, "expected one workflow");
34
+
35
+ const wf = result.workflows[0];
36
+ assert.ok(wf["bom-ref"]);
37
+ assert.strictEqual(wf.name, "GitLab CI Pipeline");
38
+ assert.ok(Array.isArray(wf.tasks));
39
+ assert.ok(wf.tasks.length > 0, "expected at least one task (job)");
40
+
41
+ const jobNames = wf.tasks.map((t) => t.name);
42
+ assert.ok(jobNames.includes("build"), "expected build job");
43
+ assert.ok(jobNames.includes("test"), "expected test job");
44
+
45
+ // image used in jobs captured as components
46
+ assert.ok(Array.isArray(result.components));
47
+ const compNames = result.components.map((c) => c.name);
48
+ assert.ok(
49
+ compNames.includes("node:20"),
50
+ "expected node:20 container component",
51
+ );
52
+ });
53
+
54
+ it("extracts services from jobs", () => {
55
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml");
56
+ const result = gitlabCiParser.parse([f], {});
57
+ // The test job has services: [postgres:14, redis:7]
58
+ assert.ok(Array.isArray(result.services));
59
+ assert.ok(
60
+ result.services.length > 0,
61
+ "expected at least one service from jobs",
62
+ );
63
+ const svcNames = result.services.map((s) => s.name);
64
+ assert.ok(
65
+ svcNames.some((n) => n.includes("postgres")),
66
+ "expected postgres service",
67
+ );
68
+ assert.ok(
69
+ svcNames.some((n) => n.includes("redis")),
70
+ "expected redis service",
71
+ );
72
+ });
73
+
74
+ it("produces workflow dependency links", () => {
75
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml");
76
+ const result = gitlabCiParser.parse([f], {});
77
+
78
+ assert.ok(result.dependencies.length > 0);
79
+ const wfDep = result.dependencies.find(
80
+ (d) => d.ref === result.workflows[0]["bom-ref"],
81
+ );
82
+ assert.ok(wfDep);
83
+ assert.ok(wfDep.dependsOn.length > 0);
84
+ });
85
+
86
+ it("gracefully handles missing file", () => {
87
+ const result = gitlabCiParser.parse(["/no/such/file/.gitlab-ci.yml"], {});
88
+ assert.deepStrictEqual(result.workflows, []);
89
+ assert.deepStrictEqual(result.components, []);
90
+ });
91
+
92
+ it("skips anchor/reserved keys", () => {
93
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml");
94
+ const result = gitlabCiParser.parse([f], {});
95
+ const taskNames = result.workflows[0].tasks.map((t) => t.name);
96
+ // Should not include 'image', 'stages', 'variables', 'cache', 'services'
97
+ assert.ok(!taskNames.includes("image"));
98
+ assert.ok(!taskNames.includes("stages"));
99
+ assert.ok(!taskNames.includes("variables"));
100
+ assert.ok(!taskNames.includes("cache"));
101
+ });
102
+
103
+ it("parses gitlab-ci-rules.yml: all jobs extracted (rules-based pipeline)", () => {
104
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml");
105
+ const result = gitlabCiParser.parse([f], {});
106
+
107
+ assert.strictEqual(result.workflows.length, 1);
108
+ const taskNames = result.workflows[0].tasks.map((t) => t.name);
109
+
110
+ // Core jobs must be present
111
+ assert.ok(taskNames.includes("flake8"), "expected flake8 job");
112
+ assert.ok(taskNames.includes("mypy"), "expected mypy job");
113
+ assert.ok(taskNames.includes("build:wheel"), "expected build:wheel job");
114
+ assert.ok(taskNames.includes("build:docker"), "expected build:docker job");
115
+ assert.ok(taskNames.includes("test:unit"), "expected test:unit job");
116
+ assert.ok(
117
+ taskNames.includes("test:integration"),
118
+ "expected test:integration job",
119
+ );
120
+ assert.ok(taskNames.includes("test:matrix"), "expected test:matrix job");
121
+ assert.ok(
122
+ taskNames.includes("deploy:staging"),
123
+ "expected deploy:staging job",
124
+ );
125
+ assert.ok(
126
+ taskNames.includes("deploy:production"),
127
+ "expected deploy:production job",
128
+ );
129
+
130
+ // Hidden jobs (starts with '.') must NOT appear
131
+ assert.ok(
132
+ !taskNames.some((n) => n.startsWith(".")),
133
+ "hidden jobs must not appear",
134
+ );
135
+ });
136
+
137
+ it("parses gitlab-ci-rules.yml: job-level image object extracted", () => {
138
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml");
139
+ const result = gitlabCiParser.parse([f], {});
140
+
141
+ // build:docker uses `image: { name: gcr.io/kaniko-project/executor:debug, entrypoint: [""] }`
142
+ const compNames = result.components.map((c) => c.name);
143
+ assert.ok(
144
+ compNames.some((n) => n.includes("kaniko")),
145
+ "expected kaniko image as component from image object syntax",
146
+ );
147
+ });
148
+
149
+ it("parses gitlab-ci-rules.yml: services with alias captured", () => {
150
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml");
151
+ const result = gitlabCiParser.parse([f], {});
152
+
153
+ const svcNames = result.services.map((s) => s.name);
154
+ assert.ok(
155
+ svcNames.some((n) => n.includes("postgres")),
156
+ "expected postgres:15-alpine service",
157
+ );
158
+ assert.ok(
159
+ svcNames.some((n) => n.includes("redis")),
160
+ "expected redis:7-alpine service",
161
+ );
162
+
163
+ // test:integration job should record services in its properties
164
+ const task = result.workflows[0].tasks.find(
165
+ (t) => t.name === "test:integration",
166
+ );
167
+ assert.ok(task, "test:integration task must exist");
168
+ const svcProp = task.properties.find(
169
+ (p) => p.name === "cdx:gitlab:job:services",
170
+ );
171
+ assert.ok(svcProp, "expected cdx:gitlab:job:services property");
172
+ assert.ok(
173
+ svcProp.value.includes("postgres"),
174
+ "services property must include postgres",
175
+ );
176
+ });
177
+
178
+ it("parses gitlab-ci-rules.yml: DAG needs recorded in job properties", () => {
179
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml");
180
+ const result = gitlabCiParser.parse([f], {});
181
+
182
+ // test:unit needs build:wheel
183
+ const unitTask = result.workflows[0].tasks.find(
184
+ (t) => t.name === "test:unit",
185
+ );
186
+ assert.ok(unitTask, "test:unit task must exist");
187
+ const needsProp = unitTask.properties.find(
188
+ (p) => p.name === "cdx:gitlab:job:needs",
189
+ );
190
+ assert.ok(needsProp, "expected cdx:gitlab:job:needs property on test:unit");
191
+ assert.ok(
192
+ needsProp.value.includes("build:wheel"),
193
+ "needs must reference build:wheel",
194
+ );
195
+ });
196
+
197
+ it("parses gitlab-ci-rules.yml: stages property recorded on workflow", () => {
198
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml");
199
+ const result = gitlabCiParser.parse([f], {});
200
+
201
+ const stagesProp = result.workflows[0].properties.find(
202
+ (p) => p.name === "cdx:gitlab:stages",
203
+ );
204
+ assert.ok(stagesProp, "expected cdx:gitlab:stages property");
205
+ assert.ok(stagesProp.value.includes("lint"), "stages must include lint");
206
+ assert.ok(
207
+ stagesProp.value.includes("deploy"),
208
+ "stages must include deploy",
209
+ );
210
+ });
211
+
212
+ it("parses gitlab-ci-minimal.yml: minimal config — no stages, single job, no image", () => {
213
+ const f = path.join(repoRoot, "test", "data", "gitlab-ci-minimal.yml");
214
+ const result = gitlabCiParser.parse([f], {});
215
+
216
+ assert.strictEqual(result.workflows.length, 1, "expected one workflow");
217
+ const taskNames = result.workflows[0].tasks.map((t) => t.name);
218
+ assert.ok(
219
+ taskNames.includes("build_and_test"),
220
+ "expected build_and_test job",
221
+ );
222
+
223
+ // No global image → no container components
224
+ assert.strictEqual(
225
+ result.components.filter((c) => c.type === "container").length,
226
+ 0,
227
+ "no container components expected for minimal config",
228
+ );
229
+
230
+ // No stages property (stages array is empty)
231
+ const stagesProp = result.workflows[0].properties.find(
232
+ (p) => p.name === "cdx:gitlab:stages",
233
+ );
234
+ assert.ok(!stagesProp, "no stages property expected for minimal config");
235
+ });
236
+
237
+ it("parses multiple files: two separate .gitlab-ci.yml configs produce two workflows", () => {
238
+ const f1 = path.join(repoRoot, "test", "data", "gitlab-ci.yml");
239
+ const f2 = path.join(repoRoot, "test", "data", "gitlab-ci-minimal.yml");
240
+ const result = gitlabCiParser.parse([f1, f2], {});
241
+ assert.strictEqual(
242
+ result.workflows.length,
243
+ 2,
244
+ "expected two workflows for two files",
245
+ );
246
+ });
247
+ });
@@ -0,0 +1,181 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { v4 as uuidv4 } from "uuid";
4
+
5
+ import { disambiguateSteps } from "./common.js";
6
+
7
+ /**
8
+ * Very lightweight declarative Jenkinsfile parser using regex heuristics.
9
+ *
10
+ * Only parses the declarative pipeline syntax (`pipeline { ... }`).
11
+ * Full Groovy/scripted pipelines are not supported.
12
+ *
13
+ * @param {string} f Path to Jenkinsfile
14
+ * @param {Object} _options CLI options
15
+ * @returns {{ workflows: Object[], components: Object[], services: Object[], properties: Object[], dependencies: Object[] }}
16
+ */
17
+ function parseJenkinsfile(f, _options) {
18
+ const workflows = [];
19
+ const components = [];
20
+ const dependencies = [];
21
+
22
+ let raw;
23
+ try {
24
+ raw = readFileSync(f, { encoding: "utf-8" });
25
+ } catch (_e) {
26
+ return {
27
+ workflows,
28
+ components,
29
+ services: [],
30
+ properties: [],
31
+ dependencies,
32
+ };
33
+ }
34
+
35
+ // Quick check: must look like a declarative pipeline
36
+ if (!raw.includes("pipeline") || !raw.includes("stages")) {
37
+ return {
38
+ workflows,
39
+ components,
40
+ services: [],
41
+ properties: [],
42
+ dependencies,
43
+ };
44
+ }
45
+
46
+ const workflowRef = uuidv4();
47
+ const tasks = [];
48
+ const workflowDependsOn = [];
49
+ const workflowProperties = [{ name: "cdx:jenkins:file", value: f }];
50
+
51
+ // Extract agent info
52
+ const agentMatch = raw.match(
53
+ /agent\s*\{[^}]*docker\s*\{[^}]*image\s+['"]([^'"]+)['"]/s,
54
+ );
55
+ if (agentMatch) {
56
+ const agentImage = agentMatch[1];
57
+ components.push({ type: "container", name: agentImage });
58
+ workflowProperties.push({
59
+ name: "cdx:jenkins:agent:image",
60
+ value: agentImage,
61
+ });
62
+ } else {
63
+ const simpleAgentMatch = raw.match(/agent\s+['"]?(\w+)['"]?/);
64
+ if (simpleAgentMatch) {
65
+ workflowProperties.push({
66
+ name: "cdx:jenkins:agent",
67
+ value: simpleAgentMatch[1],
68
+ });
69
+ }
70
+ }
71
+
72
+ // Extract stage blocks using a regex heuristic.
73
+ // NOTE: This only works reliably for declarative pipelines with simple,
74
+ // non-deeply-nested stage blocks. Scripted pipelines or stages with heavily
75
+ // nested closures may produce incomplete or incorrect results.
76
+ const stagePattern =
77
+ /stage\s*\(\s*['"]([^'"]+)['"]\s*\)\s*\{([\s\S]*?)(?=stage\s*\(|post\s*\{|$)/g;
78
+ let stageMatch;
79
+ while ((stageMatch = stagePattern.exec(raw)) !== null) {
80
+ const stageName = stageMatch[1];
81
+ const stageBody = stageMatch[2];
82
+ const taskRef = uuidv4();
83
+ const steps = [];
84
+ const taskProperties = [
85
+ { name: "cdx:jenkins:stage:name", value: stageName },
86
+ ];
87
+
88
+ // Detect parallel stages
89
+ if (stageBody.includes("parallel")) {
90
+ taskProperties.push({
91
+ name: "cdx:jenkins:stage:parallel",
92
+ value: "true",
93
+ });
94
+ }
95
+
96
+ // Detect when conditions
97
+ const whenMatch = stageBody.match(/when\s*\{([^}]+)\}/);
98
+ if (whenMatch) {
99
+ taskProperties.push({
100
+ name: "cdx:jenkins:stage:when",
101
+ value: whenMatch[1].trim(),
102
+ });
103
+ }
104
+
105
+ // Extract sh/echo/script steps
106
+ const shPattern = /(?:sh|bat|powershell)\s+['"]([^'"]+)['"]/g;
107
+ let shMatch;
108
+ while ((shMatch = shPattern.exec(stageBody)) !== null) {
109
+ steps.push({
110
+ name: `sh: ${shMatch[1].substring(0, 60)}`,
111
+ commands: [{ executed: shMatch[1] }],
112
+ });
113
+ }
114
+
115
+ tasks.push({
116
+ "bom-ref": taskRef,
117
+ uid: taskRef,
118
+ name: stageName,
119
+ taskTypes: ["build"],
120
+ steps: disambiguateSteps(steps),
121
+ properties: taskProperties,
122
+ });
123
+ workflowDependsOn.push(taskRef);
124
+ }
125
+
126
+ const workflow = {
127
+ "bom-ref": workflowRef,
128
+ uid: workflowRef,
129
+ name: "Jenkinsfile Pipeline",
130
+ taskTypes: ["build"],
131
+ tasks: tasks.length ? tasks : undefined,
132
+ properties: workflowProperties,
133
+ };
134
+
135
+ workflows.push(workflow);
136
+ if (workflowDependsOn.length) {
137
+ dependencies.push({ ref: workflowRef, dependsOn: workflowDependsOn });
138
+ }
139
+
140
+ return { workflows, components, services: [], properties: [], dependencies };
141
+ }
142
+
143
+ /**
144
+ * Jenkins formulation parser.
145
+ *
146
+ * Matches `Jenkinsfile` and `Jenkinsfile.*` at any directory depth and converts
147
+ * declarative pipeline syntax into CycloneDX formulation workflow objects.
148
+ *
149
+ * Parser contract: `parse(files, options)` returns
150
+ * `{ workflows, components, services, properties, dependencies }`.
151
+ */
152
+ export const jenkinsParser = {
153
+ id: "jenkins",
154
+ patterns: ["**/Jenkinsfile", "**/Jenkinsfile.*"],
155
+
156
+ /**
157
+ * @param {string[]} files Matched Jenkinsfile paths
158
+ * @param {Object} options CLI options
159
+ * @returns {{ workflows: Object[], components: Object[], services: Object[], properties: Object[], dependencies: Object[] }}
160
+ */
161
+ parse(files, options) {
162
+ const workflows = [];
163
+ const components = [];
164
+ const dependencies = [];
165
+
166
+ for (const f of files) {
167
+ const result = parseJenkinsfile(f, options);
168
+ workflows.push(...result.workflows);
169
+ components.push(...result.components);
170
+ dependencies.push(...result.dependencies);
171
+ }
172
+
173
+ return {
174
+ workflows,
175
+ components,
176
+ services: [],
177
+ properties: [],
178
+ dependencies,
179
+ };
180
+ },
181
+ };