@cyclonedx/cdxgen 12.1.5 → 12.2.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 +47 -39
- package/bin/cdxgen.js +175 -96
- package/bin/evinse.js +4 -4
- package/bin/repl.js +1 -1
- package/bin/sign.js +102 -0
- package/bin/validate.js +233 -0
- package/bin/verify.js +69 -28
- package/data/queries.json +1 -1
- package/data/rules/ci-permissions.yaml +186 -0
- package/data/rules/dependency-sources.yaml +123 -0
- package/data/rules/package-integrity.yaml +135 -0
- package/data/rules/vscode-extensions.yaml +228 -0
- package/lib/cli/index.js +327 -372
- package/lib/evinser/db.js +137 -0
- package/lib/{helpers → evinser}/db.poku.js +2 -6
- package/lib/evinser/evinser.js +2 -14
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/bomSigner.poku.js +156 -0
- package/lib/helpers/ciParsers/azurePipelines.js +295 -0
- package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
- package/lib/helpers/ciParsers/circleCi.js +286 -0
- package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
- package/lib/helpers/ciParsers/common.js +24 -0
- package/lib/helpers/ciParsers/githubActions.js +636 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
- package/lib/helpers/ciParsers/gitlabCi.js +213 -0
- package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
- package/lib/helpers/ciParsers/jenkins.js +181 -0
- package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
- package/lib/helpers/depsUtils.js +203 -0
- package/lib/helpers/depsUtils.poku.js +150 -0
- package/lib/helpers/display.js +423 -4
- package/lib/helpers/envcontext.js +18 -3
- package/lib/helpers/formulationParsers.js +351 -0
- package/lib/helpers/logger.js +14 -0
- package/lib/helpers/protobom.js +9 -9
- package/lib/helpers/pythonutils.js +9 -0
- package/lib/helpers/utils.js +681 -406
- package/lib/helpers/utils.poku.js +55 -255
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/versutils.poku.js +315 -0
- package/lib/helpers/vsixutils.js +1061 -0
- package/lib/helpers/vsixutils.poku.js +2247 -0
- package/lib/managers/binary.js +19 -19
- package/lib/managers/docker.js +108 -1
- package/lib/managers/oci.js +10 -0
- package/lib/managers/piptree.js +3 -9
- package/lib/parsers/npmrc.js +17 -13
- package/lib/parsers/npmrc.poku.js +41 -5
- package/lib/server/openapi.yaml +1 -1
- package/lib/server/server.js +40 -11
- package/lib/server/server.poku.js +123 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +197 -0
- package/lib/stages/postgen/auditBom.poku.js +378 -0
- package/lib/stages/postgen/postgen.js +54 -1
- package/lib/stages/postgen/postgen.poku.js +90 -1
- package/lib/stages/postgen/ruleEngine.js +369 -0
- package/lib/stages/pregen/envAudit.js +299 -0
- package/lib/stages/pregen/envAudit.poku.js +572 -0
- package/lib/stages/pregen/pregen.js +12 -8
- package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceEngine.poku.js +168 -0
- package/lib/validator/complianceRules.js +1610 -0
- package/lib/validator/complianceRules.poku.js +328 -0
- package/lib/validator/index.js +222 -0
- package/lib/validator/index.poku.js +144 -0
- package/lib/validator/reporters/annotations.js +121 -0
- package/lib/validator/reporters/console.js +149 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/lib/validator/reporters.poku.js +150 -0
- package/package.json +8 -8
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/validate.d.ts +3 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/helpers/utils.d.ts +0 -1
- package/types/lib/cli/index.d.ts +49 -52
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/db.d.ts +34 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -16
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +27 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/common.d.ts +11 -0
- package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +21 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +111 -11
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +19 -7
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts +50 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
- package/types/lib/helpers/logger.d.ts +15 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -2
- package/types/lib/helpers/pythonutils.d.ts +10 -1
- package/types/lib/helpers/pythonutils.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +532 -128
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +12 -31
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +11 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/npmrc.d.ts +4 -1
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +21 -2
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +20 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +8 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +8 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +66 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +70 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +70 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +31 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +30 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +21 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +16 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/lib/helpers/db.js +0 -162
- package/lib/stages/pregen/env-audit.js +0 -34
- package/lib/stages/pregen/env-audit.poku.js +0 -290
- package/types/helpers/db.d.ts +0 -35
- package/types/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/db.d.ts +0 -35
- package/types/lib/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/validator.d.ts.map +0 -1
- package/types/lib/stages/pregen/env-audit.d.ts +0 -2
- package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
- package/types/managers/binary.d.ts +0 -37
- package/types/managers/binary.d.ts.map +0 -1
- package/types/managers/docker.d.ts +0 -56
- package/types/managers/docker.d.ts.map +0 -1
- package/types/managers/oci.d.ts +0 -2
- package/types/managers/oci.d.ts.map +0 -1
- package/types/managers/piptree.d.ts +0 -2
- package/types/managers/piptree.d.ts.map +0 -1
- package/types/server/server.d.ts +0 -34
- package/types/server/server.d.ts.map +0 -1
- package/types/stages/postgen/annotator.d.ts +0 -27
- package/types/stages/postgen/annotator.d.ts.map +0 -1
- package/types/stages/postgen/postgen.d.ts +0 -51
- package/types/stages/postgen/postgen.d.ts.map +0 -1
- package/types/stages/pregen/pregen.d.ts +0 -59
- package/types/stages/pregen/pregen.d.ts.map +0 -1
|
@@ -0,0 +1,286 @@
|
|
|
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 CircleCI config file and return formulation-shaped data.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} f Absolute path to the config file
|
|
12
|
+
* @param {Object} _options CLI options
|
|
13
|
+
* @returns {{ workflows: Object[], components: Object[], services: Object[], properties: Object[], dependencies: Object[] }}
|
|
14
|
+
*/
|
|
15
|
+
function parseCircleCiFile(f, _options) {
|
|
16
|
+
const workflows = [];
|
|
17
|
+
const components = [];
|
|
18
|
+
const dependencies = [];
|
|
19
|
+
|
|
20
|
+
let raw;
|
|
21
|
+
try {
|
|
22
|
+
raw = readFileSync(f, { encoding: "utf-8" });
|
|
23
|
+
} catch (_e) {
|
|
24
|
+
return {
|
|
25
|
+
workflows,
|
|
26
|
+
components,
|
|
27
|
+
services: [],
|
|
28
|
+
properties: [],
|
|
29
|
+
dependencies,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let yamlObj;
|
|
34
|
+
try {
|
|
35
|
+
yamlObj = _load(raw);
|
|
36
|
+
} catch (_e) {
|
|
37
|
+
return {
|
|
38
|
+
workflows,
|
|
39
|
+
components,
|
|
40
|
+
services: [],
|
|
41
|
+
properties: [],
|
|
42
|
+
dependencies,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!yamlObj || typeof yamlObj !== "object") {
|
|
47
|
+
return {
|
|
48
|
+
workflows,
|
|
49
|
+
components,
|
|
50
|
+
services: [],
|
|
51
|
+
properties: [],
|
|
52
|
+
dependencies,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Collect orbs as components
|
|
57
|
+
if (yamlObj.orbs && typeof yamlObj.orbs === "object") {
|
|
58
|
+
for (const [orbAlias, orbRef] of Object.entries(yamlObj.orbs)) {
|
|
59
|
+
if (typeof orbRef === "string") {
|
|
60
|
+
const atIdx = orbRef.lastIndexOf("@");
|
|
61
|
+
const fullName = atIdx >= 0 ? orbRef.substring(0, atIdx) : orbRef;
|
|
62
|
+
const version = atIdx >= 0 ? orbRef.substring(atIdx + 1) : "";
|
|
63
|
+
const slashIdx = fullName.indexOf("/");
|
|
64
|
+
const namespace = slashIdx >= 0 ? fullName.substring(0, slashIdx) : "";
|
|
65
|
+
const name =
|
|
66
|
+
slashIdx >= 0 ? fullName.substring(slashIdx + 1) : fullName;
|
|
67
|
+
components.push({
|
|
68
|
+
"bom-ref": orbRef,
|
|
69
|
+
type: "application",
|
|
70
|
+
group: namespace,
|
|
71
|
+
name,
|
|
72
|
+
version,
|
|
73
|
+
properties: [
|
|
74
|
+
{ name: "SrcFile", value: f },
|
|
75
|
+
{ name: "cdx:circleci:orb:alias", value: orbAlias },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Collect executor images as components
|
|
83
|
+
if (yamlObj.executors && typeof yamlObj.executors === "object") {
|
|
84
|
+
for (const [exName, exDef] of Object.entries(yamlObj.executors)) {
|
|
85
|
+
const image =
|
|
86
|
+
exDef?.docker?.[0]?.image ||
|
|
87
|
+
exDef?.machine?.image ||
|
|
88
|
+
exDef?.macos?.xcode ||
|
|
89
|
+
"";
|
|
90
|
+
if (image) {
|
|
91
|
+
components.push({
|
|
92
|
+
type: "container",
|
|
93
|
+
name: image,
|
|
94
|
+
properties: [
|
|
95
|
+
{ name: "SrcFile", value: f },
|
|
96
|
+
{ name: "cdx:circleci:executor:name", value: exName },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build a workflow/task tree per CircleCI workflow
|
|
104
|
+
const circleCiWorkflows =
|
|
105
|
+
yamlObj.workflows && typeof yamlObj.workflows === "object"
|
|
106
|
+
? Object.entries(yamlObj.workflows).filter(([key]) => key !== "version")
|
|
107
|
+
: [];
|
|
108
|
+
|
|
109
|
+
for (const [wfName, wfDef] of circleCiWorkflows) {
|
|
110
|
+
if (!wfDef || typeof wfDef !== "object") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const workflowRef = uuidv4();
|
|
114
|
+
const tasks = [];
|
|
115
|
+
const workflowDependsOn = [];
|
|
116
|
+
|
|
117
|
+
const wfJobs = Array.isArray(wfDef.jobs) ? wfDef.jobs : [];
|
|
118
|
+
for (const jobEntry of wfJobs) {
|
|
119
|
+
// Each entry is either a string (job name) or { jobName: { requires, ... } }
|
|
120
|
+
let jobName;
|
|
121
|
+
let jobConfig = {};
|
|
122
|
+
if (typeof jobEntry === "string") {
|
|
123
|
+
jobName = jobEntry;
|
|
124
|
+
} else if (typeof jobEntry === "object") {
|
|
125
|
+
jobName = Object.keys(jobEntry)[0];
|
|
126
|
+
jobConfig = jobEntry[jobName] || {};
|
|
127
|
+
}
|
|
128
|
+
if (!jobName) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const taskRef = uuidv4();
|
|
133
|
+
const taskProperties = [
|
|
134
|
+
{ name: "cdx:circleci:job:name", value: jobName },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const requires = Array.isArray(jobConfig.requires)
|
|
138
|
+
? jobConfig.requires
|
|
139
|
+
: [];
|
|
140
|
+
if (requires.length) {
|
|
141
|
+
taskProperties.push({
|
|
142
|
+
name: "cdx:circleci:job:requires",
|
|
143
|
+
value: requires.join(","),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const jobFilters = jobConfig.filters;
|
|
148
|
+
if (jobFilters?.branches) {
|
|
149
|
+
const only = Array.isArray(jobFilters.branches.only)
|
|
150
|
+
? jobFilters.branches.only.join(",")
|
|
151
|
+
: jobFilters.branches.only || "";
|
|
152
|
+
if (only) {
|
|
153
|
+
taskProperties.push({
|
|
154
|
+
name: "cdx:circleci:job:branch:only",
|
|
155
|
+
value: only,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Look up job definition for steps
|
|
161
|
+
const jobDef = yamlObj.jobs?.[jobName] || {};
|
|
162
|
+
const steps = [];
|
|
163
|
+
for (const step of Array.isArray(jobDef.steps) ? jobDef.steps : []) {
|
|
164
|
+
if (typeof step === "string") {
|
|
165
|
+
steps.push({ name: step });
|
|
166
|
+
} else if (typeof step === "object") {
|
|
167
|
+
const stepKey = Object.keys(step)[0];
|
|
168
|
+
const stepVal = step[stepKey];
|
|
169
|
+
const stepName =
|
|
170
|
+
typeof stepVal?.name === "string" ? stepVal.name : stepKey;
|
|
171
|
+
const command =
|
|
172
|
+
typeof stepVal?.command === "string" ? stepVal.command : undefined;
|
|
173
|
+
steps.push({
|
|
174
|
+
name: stepName,
|
|
175
|
+
commands: command ? [{ executed: command }] : undefined,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
tasks.push({
|
|
181
|
+
"bom-ref": taskRef,
|
|
182
|
+
uid: taskRef,
|
|
183
|
+
name: jobName,
|
|
184
|
+
taskTypes: ["build"],
|
|
185
|
+
steps: disambiguateSteps(steps),
|
|
186
|
+
properties: taskProperties,
|
|
187
|
+
});
|
|
188
|
+
workflowDependsOn.push(taskRef);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const workflow = {
|
|
192
|
+
"bom-ref": workflowRef,
|
|
193
|
+
uid: workflowRef,
|
|
194
|
+
name: wfName,
|
|
195
|
+
taskTypes: ["build"],
|
|
196
|
+
tasks: tasks.length ? tasks : undefined,
|
|
197
|
+
properties: [
|
|
198
|
+
{ name: "cdx:circleci:config", value: f },
|
|
199
|
+
{ name: "cdx:circleci:workflow:name", value: wfName },
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
workflows.push(workflow);
|
|
204
|
+
if (workflowDependsOn.length) {
|
|
205
|
+
dependencies.push({ ref: workflowRef, dependsOn: workflowDependsOn });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fallback: if no workflows block, create a single workflow from jobs
|
|
210
|
+
if (
|
|
211
|
+
workflows.length === 0 &&
|
|
212
|
+
yamlObj.jobs &&
|
|
213
|
+
typeof yamlObj.jobs === "object"
|
|
214
|
+
) {
|
|
215
|
+
const workflowRef = uuidv4();
|
|
216
|
+
const tasks = [];
|
|
217
|
+
const workflowDependsOn = [];
|
|
218
|
+
|
|
219
|
+
for (const jobName of Object.keys(yamlObj.jobs)) {
|
|
220
|
+
const taskRef = uuidv4();
|
|
221
|
+
tasks.push({
|
|
222
|
+
"bom-ref": taskRef,
|
|
223
|
+
uid: taskRef,
|
|
224
|
+
name: jobName,
|
|
225
|
+
taskTypes: ["build"],
|
|
226
|
+
properties: [{ name: "cdx:circleci:job:name", value: jobName }],
|
|
227
|
+
});
|
|
228
|
+
workflowDependsOn.push(taskRef);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
workflows.push({
|
|
232
|
+
"bom-ref": workflowRef,
|
|
233
|
+
uid: workflowRef,
|
|
234
|
+
name: "CircleCI Pipeline",
|
|
235
|
+
taskTypes: ["build"],
|
|
236
|
+
tasks: tasks.length ? tasks : undefined,
|
|
237
|
+
properties: [{ name: "cdx:circleci:config", value: f }],
|
|
238
|
+
});
|
|
239
|
+
if (workflowDependsOn.length) {
|
|
240
|
+
dependencies.push({ ref: workflowRef, dependsOn: workflowDependsOn });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { workflows, components, services: [], properties: [], dependencies };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* CircleCI formulation parser.
|
|
249
|
+
*
|
|
250
|
+
* Matches `.circleci/config.yml` and `.circleci/config.yaml` and converts them
|
|
251
|
+
* into CycloneDX formulation workflow objects. Referenced orbs are captured as
|
|
252
|
+
* components.
|
|
253
|
+
*
|
|
254
|
+
* Parser contract: `parse(files, options)` returns
|
|
255
|
+
* `{ workflows, components, services, properties, dependencies }`.
|
|
256
|
+
*/
|
|
257
|
+
export const circleCiParser = {
|
|
258
|
+
id: "circleci",
|
|
259
|
+
patterns: [".circleci/config.{yml,yaml}"],
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {string[]} files Matched config file paths
|
|
263
|
+
* @param {Object} options CLI options
|
|
264
|
+
* @returns {{ workflows: Object[], components: Object[], services: Object[], properties: Object[], dependencies: Object[] }}
|
|
265
|
+
*/
|
|
266
|
+
parse(files, options) {
|
|
267
|
+
const workflows = [];
|
|
268
|
+
const components = [];
|
|
269
|
+
const dependencies = [];
|
|
270
|
+
|
|
271
|
+
for (const f of files) {
|
|
272
|
+
const result = parseCircleCiFile(f, options);
|
|
273
|
+
workflows.push(...result.workflows);
|
|
274
|
+
components.push(...result.components);
|
|
275
|
+
dependencies.push(...result.dependencies);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
workflows,
|
|
280
|
+
components,
|
|
281
|
+
services: [],
|
|
282
|
+
properties: [],
|
|
283
|
+
dependencies,
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { assert, describe, it } from "poku";
|
|
5
|
+
|
|
6
|
+
import { circleCiParser } from "./circleCi.js";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "../../..");
|
|
10
|
+
|
|
11
|
+
describe("circleCiParser", () => {
|
|
12
|
+
it("has correct metadata", () => {
|
|
13
|
+
assert.strictEqual(circleCiParser.id, "circleci");
|
|
14
|
+
assert.ok(Array.isArray(circleCiParser.patterns));
|
|
15
|
+
assert.ok(circleCiParser.patterns.length > 0);
|
|
16
|
+
assert.strictEqual(typeof circleCiParser.parse, "function");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns empty arrays for no files", () => {
|
|
20
|
+
const result = circleCiParser.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 CircleCI fixture", () => {
|
|
29
|
+
const f = path.join(repoRoot, "test", "data", "circleci-config.yml");
|
|
30
|
+
const result = circleCiParser.parse([f], {});
|
|
31
|
+
|
|
32
|
+
assert.ok(Array.isArray(result.workflows));
|
|
33
|
+
assert.ok(result.workflows.length > 0, "expected at least one workflow");
|
|
34
|
+
|
|
35
|
+
// The fixture has one workflow named 'build-test-deploy'
|
|
36
|
+
const wf = result.workflows.find((w) => w.name === "build-test-deploy");
|
|
37
|
+
assert.ok(wf, "expected build-test-deploy workflow");
|
|
38
|
+
assert.ok(wf["bom-ref"]);
|
|
39
|
+
assert.ok(Array.isArray(wf.tasks));
|
|
40
|
+
assert.ok(wf.tasks.length > 0);
|
|
41
|
+
|
|
42
|
+
const taskNames = wf.tasks.map((t) => t.name);
|
|
43
|
+
assert.ok(taskNames.includes("build"), "expected build job");
|
|
44
|
+
assert.ok(taskNames.includes("test"), "expected test job");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("captures orb references as components", () => {
|
|
48
|
+
const f = path.join(repoRoot, "test", "data", "circleci-config.yml");
|
|
49
|
+
const result = circleCiParser.parse([f], {});
|
|
50
|
+
|
|
51
|
+
// The fixture uses circleci/node and circleci/aws-ecr orbs
|
|
52
|
+
assert.ok(result.components.length > 0, "expected orb components");
|
|
53
|
+
const orbNames = result.components.map((c) => c.name);
|
|
54
|
+
assert.ok(orbNames.includes("node"), "expected circleci/node orb");
|
|
55
|
+
assert.ok(orbNames.includes("aws-ecr"), "expected circleci/aws-ecr orb");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("captures executor images as components", () => {
|
|
59
|
+
const f = path.join(repoRoot, "test", "data", "circleci-config.yml");
|
|
60
|
+
const result = circleCiParser.parse([f], {});
|
|
61
|
+
|
|
62
|
+
const containerComps = result.components.filter(
|
|
63
|
+
(c) => c.type === "container",
|
|
64
|
+
);
|
|
65
|
+
assert.ok(
|
|
66
|
+
containerComps.length > 0,
|
|
67
|
+
"expected container executor components",
|
|
68
|
+
);
|
|
69
|
+
assert.ok(
|
|
70
|
+
containerComps.some((c) => c.name?.includes("node")),
|
|
71
|
+
"expected a node executor image component",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("produces workflow dependency links", () => {
|
|
76
|
+
const f = path.join(repoRoot, "test", "data", "circleci-config.yml");
|
|
77
|
+
const result = circleCiParser.parse([f], {});
|
|
78
|
+
|
|
79
|
+
assert.ok(result.dependencies.length > 0);
|
|
80
|
+
const wfDep = result.dependencies.find(
|
|
81
|
+
(d) => d.ref === result.workflows[0]["bom-ref"],
|
|
82
|
+
);
|
|
83
|
+
assert.ok(wfDep);
|
|
84
|
+
assert.ok(wfDep.dependsOn.length > 0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("gracefully handles missing file", () => {
|
|
88
|
+
const result = circleCiParser.parse(["/no/such/.circleci/config.yml"], {});
|
|
89
|
+
assert.deepStrictEqual(result.workflows, []);
|
|
90
|
+
assert.deepStrictEqual(result.components, []);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("parses circleci-machine.yml: machine executor components extracted", () => {
|
|
94
|
+
const f = path.join(repoRoot, "test", "data", "circleci-machine.yml");
|
|
95
|
+
const result = circleCiParser.parse([f], {});
|
|
96
|
+
|
|
97
|
+
// machine executors produce container components
|
|
98
|
+
const machineComps = result.components.filter(
|
|
99
|
+
(c) => c.type === "container" && c.name?.includes("ubuntu"),
|
|
100
|
+
);
|
|
101
|
+
assert.ok(
|
|
102
|
+
machineComps.length > 0,
|
|
103
|
+
"expected ubuntu machine executor components",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("parses circleci-machine.yml: no orbs — orb components absent", () => {
|
|
108
|
+
const f = path.join(repoRoot, "test", "data", "circleci-machine.yml");
|
|
109
|
+
const result = circleCiParser.parse([f], {});
|
|
110
|
+
const orbComps = result.components.filter((c) => c.type === "application");
|
|
111
|
+
assert.strictEqual(orbComps.length, 0, "no orb components expected");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("parses circleci-machine.yml: approval gate job present", () => {
|
|
115
|
+
const f = path.join(repoRoot, "test", "data", "circleci-machine.yml");
|
|
116
|
+
const result = circleCiParser.parse([f], {});
|
|
117
|
+
|
|
118
|
+
const wf = result.workflows.find((w) => w.name === "ci-cd");
|
|
119
|
+
assert.ok(wf, "expected ci-cd workflow");
|
|
120
|
+
const taskNames = wf.tasks.map((t) => t.name);
|
|
121
|
+
assert.ok(
|
|
122
|
+
taskNames.includes("hold-for-approval"),
|
|
123
|
+
"expected hold-for-approval task",
|
|
124
|
+
);
|
|
125
|
+
assert.ok(
|
|
126
|
+
taskNames.includes("deploy-staging"),
|
|
127
|
+
"expected deploy-staging task",
|
|
128
|
+
);
|
|
129
|
+
assert.ok(
|
|
130
|
+
taskNames.includes("deploy-production"),
|
|
131
|
+
"expected deploy-production task",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("parses circleci-machine.yml: requires chain recorded in task properties", () => {
|
|
136
|
+
const f = path.join(repoRoot, "test", "data", "circleci-machine.yml");
|
|
137
|
+
const result = circleCiParser.parse([f], {});
|
|
138
|
+
|
|
139
|
+
const wf = result.workflows[0];
|
|
140
|
+
const approvalTask = wf.tasks.find((t) => t.name === "hold-for-approval");
|
|
141
|
+
assert.ok(approvalTask, "hold-for-approval task must exist");
|
|
142
|
+
const requiresProp = approvalTask.properties.find(
|
|
143
|
+
(p) => p.name === "cdx:circleci:job:requires",
|
|
144
|
+
);
|
|
145
|
+
assert.ok(requiresProp, "expected cdx:circleci:job:requires property");
|
|
146
|
+
assert.ok(
|
|
147
|
+
requiresProp.value.includes("integration-test"),
|
|
148
|
+
"requires must include integration-test",
|
|
149
|
+
);
|
|
150
|
+
assert.ok(
|
|
151
|
+
requiresProp.value.includes("security-scan"),
|
|
152
|
+
"requires must include security-scan",
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("parses circleci-docker-sidecar.yml: multiple workflows extracted", () => {
|
|
157
|
+
const f = path.join(
|
|
158
|
+
repoRoot,
|
|
159
|
+
"test",
|
|
160
|
+
"data",
|
|
161
|
+
"circleci-docker-sidecar.yml",
|
|
162
|
+
);
|
|
163
|
+
const result = circleCiParser.parse([f], {});
|
|
164
|
+
|
|
165
|
+
const wfNames = result.workflows.map((w) => w.name);
|
|
166
|
+
assert.ok(wfNames.includes("test-matrix"), "expected test-matrix workflow");
|
|
167
|
+
assert.ok(
|
|
168
|
+
wfNames.includes("scheduled-tests"),
|
|
169
|
+
"expected scheduled-tests workflow",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("parses circleci-docker-sidecar.yml: sidecar containers as executor components", () => {
|
|
174
|
+
const f = path.join(
|
|
175
|
+
repoRoot,
|
|
176
|
+
"test",
|
|
177
|
+
"data",
|
|
178
|
+
"circleci-docker-sidecar.yml",
|
|
179
|
+
);
|
|
180
|
+
const result = circleCiParser.parse([f], {});
|
|
181
|
+
|
|
182
|
+
// The app-with-db executor has cimg/python as first image
|
|
183
|
+
const pythonComp = result.components.find(
|
|
184
|
+
(c) => c.type === "container" && c.name?.includes("python"),
|
|
185
|
+
);
|
|
186
|
+
assert.ok(pythonComp, "expected Python executor image component");
|
|
187
|
+
|
|
188
|
+
// The app-with-mongo executor has cimg/node:20.0 as primary image
|
|
189
|
+
const nodeComp = result.components.find(
|
|
190
|
+
(c) => c.type === "container" && c.name?.includes("node"),
|
|
191
|
+
);
|
|
192
|
+
assert.ok(nodeComp, "expected Node.js executor image component");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("parses circleci-docker-sidecar.yml: Slack orb captured as component", () => {
|
|
196
|
+
const f = path.join(
|
|
197
|
+
repoRoot,
|
|
198
|
+
"test",
|
|
199
|
+
"data",
|
|
200
|
+
"circleci-docker-sidecar.yml",
|
|
201
|
+
);
|
|
202
|
+
const result = circleCiParser.parse([f], {});
|
|
203
|
+
|
|
204
|
+
const orbComps = result.components.filter((c) => c.type === "application");
|
|
205
|
+
assert.ok(
|
|
206
|
+
orbComps.length > 0,
|
|
207
|
+
"expected at least one circleci orb component",
|
|
208
|
+
);
|
|
209
|
+
assert.ok(
|
|
210
|
+
orbComps.some((c) => c.name === "slack"),
|
|
211
|
+
"expected circleci/slack orb component",
|
|
212
|
+
);
|
|
213
|
+
assert.ok(
|
|
214
|
+
orbComps.some((c) => c.version === "4.12.5"),
|
|
215
|
+
"expected slack orb version 4.12.5",
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("parses multiple CircleCI files: two files produce combined results", () => {
|
|
220
|
+
const f1 = path.join(repoRoot, "test", "data", "circleci-config.yml");
|
|
221
|
+
const f2 = path.join(repoRoot, "test", "data", "circleci-machine.yml");
|
|
222
|
+
const result = circleCiParser.parse([f1, f2], {});
|
|
223
|
+
// f1 has 1 workflow, f2 has 1 workflow → combined 2
|
|
224
|
+
assert.strictEqual(
|
|
225
|
+
result.workflows.length,
|
|
226
|
+
2,
|
|
227
|
+
"expected workflows from both files",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure all step objects in the array are unique (CycloneDX `uniqueItems: true`).
|
|
3
|
+
*
|
|
4
|
+
* Identical steps are disambiguated by appending a ` (N)` counter to the step name.
|
|
5
|
+
* The first occurrence is always left unchanged.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object[]} steps
|
|
8
|
+
* @returns {Object[]|undefined}
|
|
9
|
+
*/
|
|
10
|
+
export function disambiguateSteps(steps) {
|
|
11
|
+
if (!steps?.length) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const seenKeys = new Map();
|
|
15
|
+
return steps.map((step) => {
|
|
16
|
+
const key = JSON.stringify(step);
|
|
17
|
+
const count = seenKeys.get(key) ?? 0;
|
|
18
|
+
seenKeys.set(key, count + 1);
|
|
19
|
+
if (count === 0) {
|
|
20
|
+
return step;
|
|
21
|
+
}
|
|
22
|
+
return { ...step, name: `${step.name} (${count + 1})` };
|
|
23
|
+
});
|
|
24
|
+
}
|