@cyclonedx/cdxgen 12.4.0 → 12.4.2
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 +6 -4
- package/bin/cdxgen.js +32 -11
- package/bin/convert.js +12 -8
- package/bin/evinse.js +15 -0
- package/bin/hbom.js +13 -8
- package/bin/repl.js +14 -10
- package/bin/validate.js +10 -13
- package/bin/verify.js +7 -29
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
- package/lib/audit/index.js +2 -1
- package/lib/cli/index.js +77 -16
- package/lib/cli/index.poku.js +197 -0
- package/lib/evinser/evinser.js +118 -3
- package/lib/helpers/bomUtils.js +155 -1
- package/lib/helpers/bomUtils.poku.js +79 -1
- package/lib/helpers/cbomutils.js +162 -2
- package/lib/helpers/cbomutils.poku.js +100 -0
- package/lib/helpers/ciParsers/githubActions.js +15 -3
- package/lib/helpers/ciParsers/githubActions.poku.js +52 -0
- package/lib/helpers/dosai.js +433 -0
- package/lib/helpers/dosai.poku.js +302 -0
- package/lib/helpers/dosaiParsers.js +103 -0
- package/lib/helpers/plugins.js +17 -16
- package/lib/helpers/protobom.js +53 -0
- package/lib/helpers/protobom.poku.js +44 -1
- package/lib/helpers/protobomLoader.js +43 -0
- package/lib/helpers/protobomLoader.poku.js +31 -0
- package/lib/helpers/utils.js +130 -1
- package/lib/helpers/utils.poku.js +295 -0
- package/lib/server/server.js +2 -1
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +28 -0
- package/lib/stages/postgen/postgen.js +219 -12
- package/lib/stages/postgen/postgen.poku.js +163 -0
- package/lib/validator/bomValidator.js +90 -38
- package/lib/validator/bomValidator.poku.js +90 -0
- package/lib/validator/complianceRules.js +4 -2
- package/lib/validator/index.poku.js +14 -0
- package/package.json +12 -12
- package/types/bin/repl.d.ts +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +15 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/bomUtils.d.ts +8 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +1 -0
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/dosai.d.ts +24 -0
- package/types/lib/helpers/dosai.d.ts.map +1 -0
- package/types/lib/helpers/dosaiParsers.d.ts +8 -0
- package/types/lib/helpers/dosaiParsers.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +14 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -1
- package/types/lib/helpers/hostTopology.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -0
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/protobomLoader.d.ts +17 -0
- package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import esmock from "esmock";
|
|
2
|
+
import { assert, describe, it } from "poku";
|
|
3
|
+
import sinon from "sinon";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildPurlAliasMap,
|
|
7
|
+
collectDosaiDataFlowFrames,
|
|
8
|
+
collectDosaiPurlEvidence,
|
|
9
|
+
collectDosaiServicesFromMethods,
|
|
10
|
+
isDosaiDotnetLanguage,
|
|
11
|
+
normalizeDosaiServiceMap,
|
|
12
|
+
resolveComponentPurl,
|
|
13
|
+
} from "./dosai.js";
|
|
14
|
+
|
|
15
|
+
describe("dosai helpers", () => {
|
|
16
|
+
it("recognizes C#, VB.NET, and F# language aliases", () => {
|
|
17
|
+
for (const language of [
|
|
18
|
+
"csharp",
|
|
19
|
+
"dotnet",
|
|
20
|
+
"vb",
|
|
21
|
+
"vbnet",
|
|
22
|
+
"visualbasic",
|
|
23
|
+
"f#",
|
|
24
|
+
"fs",
|
|
25
|
+
"fsharp",
|
|
26
|
+
]) {
|
|
27
|
+
assert.strictEqual(isDosaiDotnetLanguage(language), true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("matches versionless dosai purls to cdxgen component purls", () => {
|
|
32
|
+
const components = [{ purl: "pkg:nuget/System.Text.Json@10.0.0" }];
|
|
33
|
+
const aliases = buildPurlAliasMap(components);
|
|
34
|
+
|
|
35
|
+
assert.strictEqual(
|
|
36
|
+
resolveComponentPurl("pkg:nuget/System.Text.Json", aliases),
|
|
37
|
+
"pkg:nuget/System.Text.Json@10.0.0",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("collects package occurrence evidence from dosai PackageReachability", () => {
|
|
42
|
+
const methodsSlice = {
|
|
43
|
+
CallGraph: {
|
|
44
|
+
Edges: [
|
|
45
|
+
{
|
|
46
|
+
Id: "e1",
|
|
47
|
+
FileName: "System.Text.Json.dll",
|
|
48
|
+
LineNumber: 12,
|
|
49
|
+
CalledMethodName: "System.Text.Json.JsonSerializer.Deserialize",
|
|
50
|
+
TargetName: "Deserialize",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
Nodes: [
|
|
54
|
+
{
|
|
55
|
+
Id: "n1",
|
|
56
|
+
FileName: "Program.cs",
|
|
57
|
+
LineNumber: 10,
|
|
58
|
+
ClassName: "Program",
|
|
59
|
+
Name: "Main",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
Id: "n2",
|
|
63
|
+
FileName: "System.Text.Json.dll",
|
|
64
|
+
LineNumber: 0,
|
|
65
|
+
ClassName: "JsonSerializer",
|
|
66
|
+
Name: "Deserialize",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
PackageReachability: [
|
|
71
|
+
{
|
|
72
|
+
Purl: "pkg:nuget/System.Text.Json",
|
|
73
|
+
EdgeIds: ["e1"],
|
|
74
|
+
NodeIds: ["n1", "n2"],
|
|
75
|
+
SourceLocations: [
|
|
76
|
+
{
|
|
77
|
+
Path: "Controllers/Parser.cs",
|
|
78
|
+
FileName: "Parser.cs",
|
|
79
|
+
LineNumber: 42,
|
|
80
|
+
ColumnNumber: 13,
|
|
81
|
+
Kind: "CallGraphEdge",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
const retMap = collectDosaiPurlEvidence(methodsSlice, [
|
|
88
|
+
{ purl: "pkg:nuget/System.Text.Json@10.0.0" },
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
assert.deepStrictEqual(
|
|
92
|
+
Array.from(
|
|
93
|
+
retMap.purlLocationMap["pkg:nuget/System.Text.Json@10.0.0"],
|
|
94
|
+
).sort(),
|
|
95
|
+
["Controllers/Parser.cs#42"],
|
|
96
|
+
);
|
|
97
|
+
assert.ok(
|
|
98
|
+
retMap.purlMethodsMap["pkg:nuget/System.Text.Json@10.0.0"].has(
|
|
99
|
+
"System.Text.Json.JsonSerializer.Deserialize",
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("keeps PackageReachability fallback occurrence evidence source-only", () => {
|
|
105
|
+
const retMap = collectDosaiPurlEvidence(
|
|
106
|
+
{
|
|
107
|
+
CallGraph: {
|
|
108
|
+
Edges: [
|
|
109
|
+
{
|
|
110
|
+
Id: "e1",
|
|
111
|
+
FileName: "System.Text.Json.dll",
|
|
112
|
+
LineNumber: 12,
|
|
113
|
+
CalledMethodName: "System.Text.Json.JsonSerializer.Deserialize",
|
|
114
|
+
CallLocation: {
|
|
115
|
+
FileName: "Program.fs",
|
|
116
|
+
LineNumber: 8,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
Id: "e2",
|
|
121
|
+
FileName: "Controllers/EpisodesController.cs",
|
|
122
|
+
LineNumber: 17,
|
|
123
|
+
CalledMethodName: "System.Text.Json.JsonSerializer.Serialize",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
PackageReachability: [
|
|
128
|
+
{
|
|
129
|
+
Purl: "pkg:nuget/System.Text.Json",
|
|
130
|
+
EdgeIds: ["e1", "e2"],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
[{ purl: "pkg:nuget/System.Text.Json@10.0.0" }],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
assert.deepStrictEqual(
|
|
138
|
+
Array.from(retMap.purlLocationMap["pkg:nuget/System.Text.Json@10.0.0"]),
|
|
139
|
+
["Program.fs#8", "Controllers/EpisodesController.cs#17"],
|
|
140
|
+
);
|
|
141
|
+
assert.ok(
|
|
142
|
+
!Array.from(
|
|
143
|
+
retMap.purlLocationMap["pkg:nuget/System.Text.Json@10.0.0"],
|
|
144
|
+
).some((location) => location.includes(".dll")),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("collects package occurrence evidence from dosai Dependencies with purls", () => {
|
|
149
|
+
const retMap = collectDosaiPurlEvidence(
|
|
150
|
+
{
|
|
151
|
+
Dependencies: [
|
|
152
|
+
{
|
|
153
|
+
Path: "Program.vb",
|
|
154
|
+
FileName: "Program.vb",
|
|
155
|
+
Name: "Newtonsoft.Json",
|
|
156
|
+
Purl: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
|
157
|
+
LineNumber: 4,
|
|
158
|
+
ColumnNumber: 9,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
[{ purl: "pkg:nuget/Newtonsoft.Json@13.0.3" }],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
assert.deepStrictEqual(
|
|
166
|
+
Array.from(retMap.purlLocationMap["pkg:nuget/Newtonsoft.Json@13.0.3"]),
|
|
167
|
+
["Program.vb#4"],
|
|
168
|
+
);
|
|
169
|
+
assert.ok(
|
|
170
|
+
retMap.purlModulesMap["pkg:nuget/Newtonsoft.Json@13.0.3"].has(
|
|
171
|
+
"Newtonsoft.Json",
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("builds CycloneDX services from dosai ApiEndpoints without raw policy names", () => {
|
|
177
|
+
const servicesMap = collectDosaiServicesFromMethods({
|
|
178
|
+
ApiEndpoints: [
|
|
179
|
+
{
|
|
180
|
+
Route: "/api/podcasts?sig=secret",
|
|
181
|
+
FileName: "EpisodesController.cs",
|
|
182
|
+
Path: "Controllers/EpisodesController.cs",
|
|
183
|
+
ClassName: "EpisodesController",
|
|
184
|
+
MethodName: "Get",
|
|
185
|
+
HttpMethod: "GET",
|
|
186
|
+
EndpointKind: "Attribute",
|
|
187
|
+
AuthorizationRequired: true,
|
|
188
|
+
AuthorizationPolicies: ["InternalPolicyName"],
|
|
189
|
+
Roles: ["Admin"],
|
|
190
|
+
AllowAnonymous: false,
|
|
191
|
+
LineNumber: 42,
|
|
192
|
+
ColumnNumber: 9,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
const services = normalizeDosaiServiceMap(servicesMap);
|
|
197
|
+
|
|
198
|
+
assert.strictEqual(services.length, 1);
|
|
199
|
+
assert.deepStrictEqual(services[0].endpoints, ["/api/podcasts"]);
|
|
200
|
+
assert.strictEqual(services[0].authenticated, true);
|
|
201
|
+
assert.ok(
|
|
202
|
+
services[0].properties.some(
|
|
203
|
+
(property) =>
|
|
204
|
+
property.name === "cdx:dosai:authorizationPolicyCount" &&
|
|
205
|
+
property.value === "1",
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
assert.ok(!JSON.stringify(services[0]).includes("InternalPolicyName"));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("collects callstack frames from dosai data-flow slices", () => {
|
|
212
|
+
const frames = collectDosaiDataFlowFrames(
|
|
213
|
+
{
|
|
214
|
+
Nodes: [
|
|
215
|
+
{
|
|
216
|
+
Id: "dfn1",
|
|
217
|
+
Path: "Controllers/EpisodesController.cs",
|
|
218
|
+
Namespace: "Podcast.Api",
|
|
219
|
+
ClassName: "EpisodesController",
|
|
220
|
+
MethodName: "Get",
|
|
221
|
+
LineNumber: 12,
|
|
222
|
+
ColumnNumber: 5,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
Id: "dfn2",
|
|
226
|
+
Path: "Services/JsonLoader.cs",
|
|
227
|
+
Namespace: "Podcast.Api",
|
|
228
|
+
ClassName: "JsonLoader",
|
|
229
|
+
MethodName: "Load",
|
|
230
|
+
LineNumber: 20,
|
|
231
|
+
ColumnNumber: 9,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
Slices: [
|
|
235
|
+
{
|
|
236
|
+
NodeIds: ["dfn1", "dfn2"],
|
|
237
|
+
Purls: ["pkg:nuget/System.Text.Json"],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
[{ purl: "pkg:nuget/System.Text.Json@10.0.0" }],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
assert.strictEqual(frames["pkg:nuget/System.Text.Json@10.0.0"].length, 1);
|
|
245
|
+
assert.strictEqual(
|
|
246
|
+
frames["pkg:nuget/System.Text.Json@10.0.0"][0][1].function,
|
|
247
|
+
"Load",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("rejects unsafe dosai command inputs before spawning", async () => {
|
|
252
|
+
const safeSpawnSync = sinon.stub().returns({ status: 0 });
|
|
253
|
+
const { runDosaiCommand } = await esmock("./dosai.js", {
|
|
254
|
+
"./plugins.js": { resolvePluginBinary: sinon.stub().returns("dosai") },
|
|
255
|
+
"./utils.js": {
|
|
256
|
+
DEBUG_MODE: false,
|
|
257
|
+
getTmpDir: sinon.stub().returns("/tmp"),
|
|
258
|
+
safeExistsSync: sinon.stub().returns(true),
|
|
259
|
+
safeMkdtempSync: sinon.stub(),
|
|
260
|
+
safeRmSync: sinon.stub(),
|
|
261
|
+
safeSpawnSync,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
assert.strictEqual(
|
|
266
|
+
runDosaiCommand("methods;rm -rf /", "/tmp/project", "/tmp/out.json"),
|
|
267
|
+
false,
|
|
268
|
+
);
|
|
269
|
+
assert.strictEqual(
|
|
270
|
+
runDosaiCommand("methods", "/tmp/project\n--bad", "/tmp/out.json"),
|
|
271
|
+
false,
|
|
272
|
+
);
|
|
273
|
+
sinon.assert.notCalled(safeSpawnSync);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("spawns dosai with argument arrays and shell disabled", async () => {
|
|
277
|
+
const safeSpawnSync = sinon.stub().returns({ status: 0 });
|
|
278
|
+
const { runDosaiCommand } = await esmock("./dosai.js", {
|
|
279
|
+
"./plugins.js": { resolvePluginBinary: sinon.stub().returns("dosai") },
|
|
280
|
+
"./utils.js": {
|
|
281
|
+
DEBUG_MODE: false,
|
|
282
|
+
getTmpDir: sinon.stub().returns("/tmp"),
|
|
283
|
+
safeExistsSync: sinon.stub().returns(true),
|
|
284
|
+
safeMkdtempSync: sinon.stub(),
|
|
285
|
+
safeRmSync: sinon.stub(),
|
|
286
|
+
safeSpawnSync,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
assert.strictEqual(
|
|
291
|
+
runDosaiCommand("dataflows", "/tmp/project", "/tmp/out.json", {
|
|
292
|
+
dataFlowPatterns: "/tmp/patterns.json",
|
|
293
|
+
patternPacks: "/tmp/packs",
|
|
294
|
+
}),
|
|
295
|
+
true,
|
|
296
|
+
);
|
|
297
|
+
sinon.assert.calledOnce(safeSpawnSync);
|
|
298
|
+
assert.strictEqual(safeSpawnSync.firstCall.args[0], "dosai");
|
|
299
|
+
assert.ok(Array.isArray(safeSpawnSync.firstCall.args[1]));
|
|
300
|
+
assert.strictEqual(safeSpawnSync.firstCall.args[2].shell, false);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { PackageURL } from "packageurl-js";
|
|
2
|
+
|
|
3
|
+
export function normalizeDosaiPurlKey(purl) {
|
|
4
|
+
if (!purl || typeof purl !== "string") {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const purlObj = PackageURL.fromString(purl);
|
|
9
|
+
return [
|
|
10
|
+
purlObj.type?.toLowerCase(),
|
|
11
|
+
purlObj.namespace?.toLowerCase() || "",
|
|
12
|
+
purlObj.name?.toLowerCase(),
|
|
13
|
+
].join("/");
|
|
14
|
+
} catch (_err) {
|
|
15
|
+
return purl.split("?")[0].split("#")[0].split("@")[0].toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addDosaiSetValue(map, key, value) {
|
|
20
|
+
if (!key || !value) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
map[key] ??= new Set();
|
|
24
|
+
map[key].add(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function dosaiLocation(item) {
|
|
28
|
+
const location = item?.Location || item?.CallLocation || item;
|
|
29
|
+
const fileName =
|
|
30
|
+
location?.Path || location?.FileName || item?.Path || item?.FileName;
|
|
31
|
+
if (!fileName || fileName === "<unknown>") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const lineNumber = location?.LineNumber || item?.LineNumber;
|
|
35
|
+
if (lineNumber && lineNumber > 0) {
|
|
36
|
+
return `${fileName}#${lineNumber}`;
|
|
37
|
+
}
|
|
38
|
+
return fileName;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dosaiSourceFileName(item) {
|
|
42
|
+
const location = item?.Location || item?.CallLocation || item;
|
|
43
|
+
return String(
|
|
44
|
+
location?.Path || location?.FileName || item?.Path || item?.FileName || "",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function dosaiSourceLineNumber(item) {
|
|
49
|
+
const location = item?.Location || item?.CallLocation || item;
|
|
50
|
+
return location?.LineNumber || item?.LineNumber;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function dosaiSourceLocationFromNode(node) {
|
|
54
|
+
const location = dosaiLocation(node);
|
|
55
|
+
const fileName = dosaiSourceFileName(node).toLowerCase();
|
|
56
|
+
const lineNumber = dosaiSourceLineNumber(node);
|
|
57
|
+
if (!location || !/\.(cs|vb|fs|fsx)$/i.test(fileName)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (!lineNumber || lineNumber <= 0) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return location;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function dosaiSourceLocation(location) {
|
|
67
|
+
const sourceLocation = dosaiLocation(location);
|
|
68
|
+
const fileName = dosaiSourceFileName(location);
|
|
69
|
+
const lineNumber = dosaiSourceLineNumber(location);
|
|
70
|
+
if (!sourceLocation || !/\.(cs|vb|fs|fsx)$/i.test(fileName)) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (!lineNumber || lineNumber <= 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return sourceLocation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildDosaiPurlAliasMap(components = []) {
|
|
80
|
+
const purlAliasMap = new Map();
|
|
81
|
+
for (const component of components) {
|
|
82
|
+
if (!component?.purl) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
purlAliasMap.set(component.purl, component.purl);
|
|
86
|
+
const key = normalizeDosaiPurlKey(component.purl);
|
|
87
|
+
if (key && !purlAliasMap.has(key)) {
|
|
88
|
+
purlAliasMap.set(key, component.purl);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return purlAliasMap;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveDosaiComponentPurl(purl, purlAliasMap) {
|
|
95
|
+
if (!purl) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
purlAliasMap.get(purl) ||
|
|
100
|
+
purlAliasMap.get(normalizeDosaiPurlKey(purl)) ||
|
|
101
|
+
purl
|
|
102
|
+
);
|
|
103
|
+
}
|
package/lib/helpers/plugins.js
CHANGED
|
@@ -24,6 +24,21 @@ function isMusl() {
|
|
|
24
24
|
return result?.stdout?.includes("musl") || result?.stderr?.includes("musl");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function hasUsablePluginsDir(pluginsDir) {
|
|
28
|
+
return (
|
|
29
|
+
safeExistsSync(pluginsDir) &&
|
|
30
|
+
(safeExistsSync(join(pluginsDir, "plugins-manifest.json")) ||
|
|
31
|
+
[
|
|
32
|
+
"cargo-auditable",
|
|
33
|
+
"dosai",
|
|
34
|
+
"osquery",
|
|
35
|
+
"sourcekitten",
|
|
36
|
+
"trivy",
|
|
37
|
+
"trustinspector",
|
|
38
|
+
].some((pluginName) => safeExistsSync(join(pluginsDir, pluginName))))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
/**
|
|
28
43
|
* Determine the normalized plugin target tuple for the current runtime.
|
|
29
44
|
*
|
|
@@ -81,17 +96,13 @@ export function resolveCdxgenPlugins() {
|
|
|
81
96
|
let pluginsDir = process.env.CDXGEN_PLUGINS_DIR || "";
|
|
82
97
|
let extraNMBinPath;
|
|
83
98
|
|
|
84
|
-
if (
|
|
85
|
-
!pluginsDir &&
|
|
86
|
-
safeExistsSync(join(dirNameStr, "plugins")) &&
|
|
87
|
-
safeExistsSync(join(dirNameStr, "plugins", "trivy"))
|
|
88
|
-
) {
|
|
99
|
+
if (!pluginsDir && hasUsablePluginsDir(join(dirNameStr, "plugins"))) {
|
|
89
100
|
pluginsDir = join(dirNameStr, "plugins");
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
if (
|
|
93
104
|
!pluginsDir &&
|
|
94
|
-
|
|
105
|
+
hasUsablePluginsDir(
|
|
95
106
|
join(
|
|
96
107
|
dirNameStr,
|
|
97
108
|
"node_modules",
|
|
@@ -99,16 +110,6 @@ export function resolveCdxgenPlugins() {
|
|
|
99
110
|
`cdxgen-plugins-bin${target.pluginsBinSuffix}`,
|
|
100
111
|
"plugins",
|
|
101
112
|
),
|
|
102
|
-
) &&
|
|
103
|
-
safeExistsSync(
|
|
104
|
-
join(
|
|
105
|
-
dirNameStr,
|
|
106
|
-
"node_modules",
|
|
107
|
-
"@cdxgen",
|
|
108
|
-
`cdxgen-plugins-bin${target.pluginsBinSuffix}`,
|
|
109
|
-
"plugins",
|
|
110
|
-
"trivy",
|
|
111
|
-
),
|
|
112
113
|
)
|
|
113
114
|
) {
|
|
114
115
|
pluginsDir = join(
|
package/lib/helpers/protobom.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
supportedSpecVersions,
|
|
12
12
|
} from "@appthreat/cdx-proto";
|
|
13
13
|
|
|
14
|
+
import { toCycloneDxSpecVersionString } from "./bomUtils.js";
|
|
14
15
|
import { safeExistsSync, safeWriteSync } from "./utils.js";
|
|
15
16
|
|
|
16
17
|
const JSON_READ_OPTIONS = {
|
|
@@ -29,6 +30,11 @@ const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
|
|
|
29
30
|
|
|
30
31
|
const DEFAULT_SPEC_VERSION =
|
|
31
32
|
supportedSpecVersions[supportedSpecVersions.length - 1];
|
|
33
|
+
const PROTO_SUPPORTED_SPEC_VERSIONS = new Set(
|
|
34
|
+
supportedSpecVersions.map((specVersion) =>
|
|
35
|
+
toCycloneDxSpecVersionString(specVersion),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
32
38
|
|
|
33
39
|
const isProtoMessageBom = (bom) =>
|
|
34
40
|
Boolean(
|
|
@@ -47,6 +53,42 @@ const hasExplicitSpecVersion = (bomJson) =>
|
|
|
47
53
|
(bomJson.specVersion !== undefined || bomJson.spec_version !== undefined),
|
|
48
54
|
);
|
|
49
55
|
|
|
56
|
+
const resolveExplicitSpecVersion = (bomJson) =>
|
|
57
|
+
bomJson?.specVersion ?? bomJson?.spec_version;
|
|
58
|
+
|
|
59
|
+
const hasProvidedSpecVersion = (specVersion) =>
|
|
60
|
+
specVersion !== undefined &&
|
|
61
|
+
specVersion !== null &&
|
|
62
|
+
`${specVersion}`.trim() !== "";
|
|
63
|
+
|
|
64
|
+
export const isProtoSupportedSpecVersion = (specVersion) => {
|
|
65
|
+
if (!hasProvidedSpecVersion(specVersion)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
|
|
69
|
+
return (
|
|
70
|
+
normalizedSpecVersion !== undefined &&
|
|
71
|
+
PROTO_SUPPORTED_SPEC_VERSIONS.has(normalizedSpecVersion)
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const assertProtoSupportedSpecVersion = (
|
|
76
|
+
specVersion,
|
|
77
|
+
operation = "protobuf operations",
|
|
78
|
+
) => {
|
|
79
|
+
if (!hasProvidedSpecVersion(specVersion)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
|
|
83
|
+
if (isProtoSupportedSpecVersion(specVersion)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const displaySpecVersion = normalizedSpecVersion || `${specVersion}`.trim();
|
|
87
|
+
throw new Error(
|
|
88
|
+
`CycloneDX ${displaySpecVersion} is not currently supported for ${operation}. @appthreat/cdx-proto supports ${supportedSpecVersions.join(", ")} only.`,
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
50
92
|
const OBJECT_WRAPPED_LIST_FIELDS = ["declarations", "definitions"];
|
|
51
93
|
|
|
52
94
|
const isPlainObject = (value) =>
|
|
@@ -121,15 +163,25 @@ const resolveBomMessage = (bomJson, specVersion = DEFAULT_SPEC_VERSION) => {
|
|
|
121
163
|
JSON.parse(`${bomJson}`),
|
|
122
164
|
);
|
|
123
165
|
if (hasExplicitSpecVersion(parsedBomJson)) {
|
|
166
|
+
assertProtoSupportedSpecVersion(
|
|
167
|
+
resolveExplicitSpecVersion(parsedBomJson),
|
|
168
|
+
"protobuf serialization",
|
|
169
|
+
);
|
|
124
170
|
return parseBomJson(parsedBomJson, JSON_READ_OPTIONS);
|
|
125
171
|
}
|
|
172
|
+
assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
|
|
126
173
|
return decodeBomJson(specVersion, parsedBomJson, JSON_READ_OPTIONS);
|
|
127
174
|
}
|
|
128
175
|
if (bomJson && typeof bomJson === "object" && !Array.isArray(bomJson)) {
|
|
129
176
|
const normalizedBomJson = normalizeObjectWrappedListsForProto(bomJson);
|
|
130
177
|
if (hasExplicitSpecVersion(normalizedBomJson)) {
|
|
178
|
+
assertProtoSupportedSpecVersion(
|
|
179
|
+
resolveExplicitSpecVersion(normalizedBomJson),
|
|
180
|
+
"protobuf serialization",
|
|
181
|
+
);
|
|
131
182
|
return parseBomJson(normalizedBomJson, JSON_READ_OPTIONS);
|
|
132
183
|
}
|
|
184
|
+
assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
|
|
133
185
|
return decodeBomJson(specVersion, normalizedBomJson, JSON_READ_OPTIONS);
|
|
134
186
|
}
|
|
135
187
|
return createBom(specVersion);
|
|
@@ -175,6 +227,7 @@ export const writeBinary = (
|
|
|
175
227
|
*/
|
|
176
228
|
export const readBinary = (binFile, asJson, specVersion) => {
|
|
177
229
|
asJson = asJson ?? true;
|
|
230
|
+
assertProtoSupportedSpecVersion(specVersion, "protobuf decoding");
|
|
178
231
|
if (!safeExistsSync(binFile)) {
|
|
179
232
|
return undefined;
|
|
180
233
|
}
|
|
@@ -3,7 +3,12 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { assert, it } from "poku";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assertProtoSupportedSpecVersion,
|
|
8
|
+
isProtoBomFile,
|
|
9
|
+
readBinary,
|
|
10
|
+
writeBinary,
|
|
11
|
+
} from "./protobom.js";
|
|
7
12
|
import { getTmpDir } from "./utils.js";
|
|
8
13
|
|
|
9
14
|
const testBom = JSON.parse(
|
|
@@ -137,6 +142,44 @@ it("keeps canonical definitions and declarations as objects during proto round-t
|
|
|
137
142
|
cleanupTempDir(tempDir);
|
|
138
143
|
});
|
|
139
144
|
|
|
145
|
+
it("rejects unsupported CycloneDX 2.0 protobuf operations with a clear error", () => {
|
|
146
|
+
const tempDir = createTempDir();
|
|
147
|
+
const binFile = join(tempDir, "unsupported-2.0.cdx");
|
|
148
|
+
assert.throws(
|
|
149
|
+
() =>
|
|
150
|
+
writeBinary(
|
|
151
|
+
{
|
|
152
|
+
specFormat: "CycloneDX",
|
|
153
|
+
specVersion: "2.0",
|
|
154
|
+
version: 1,
|
|
155
|
+
},
|
|
156
|
+
binFile,
|
|
157
|
+
),
|
|
158
|
+
/CycloneDX 2\.0 is not currently supported for protobuf serialization/,
|
|
159
|
+
);
|
|
160
|
+
assert.throws(
|
|
161
|
+
() => assertProtoSupportedSpecVersion("2.0", "protobuf export"),
|
|
162
|
+
/@appthreat\/cdx-proto supports 1\.5, 1\.6, 1\.7 only/,
|
|
163
|
+
);
|
|
164
|
+
assert.throws(
|
|
165
|
+
() =>
|
|
166
|
+
writeBinary(
|
|
167
|
+
{
|
|
168
|
+
bomFormat: "CycloneDX",
|
|
169
|
+
specVersion: "2.0.1",
|
|
170
|
+
version: 1,
|
|
171
|
+
},
|
|
172
|
+
binFile,
|
|
173
|
+
),
|
|
174
|
+
/CycloneDX 2\.0\.1 is not currently supported for protobuf serialization/,
|
|
175
|
+
);
|
|
176
|
+
assert.throws(
|
|
177
|
+
() => assertProtoSupportedSpecVersion("2.0.1", "protobuf export"),
|
|
178
|
+
/CycloneDX 2\.0\.1 is not currently supported for protobuf export/,
|
|
179
|
+
);
|
|
180
|
+
cleanupTempDir(tempDir);
|
|
181
|
+
});
|
|
182
|
+
|
|
140
183
|
it("round-trips real CBOM fixture data with cryptographic assets intact", () => {
|
|
141
184
|
const tempDir = createTempDir();
|
|
142
185
|
const binFile = join(tempDir, "cbom-fixture.cdx");
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determine whether a path looks like a CycloneDX protobuf BOM file.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} filePath File path
|
|
7
|
+
* @returns {boolean} true when the path uses a protobuf BOM extension
|
|
8
|
+
*/
|
|
9
|
+
export function isProtoBomPath(filePath) {
|
|
10
|
+
const normalizedPath = `${filePath || ""}`.toLowerCase();
|
|
11
|
+
return PROTO_BOM_FILE_EXTENSIONS.some((extension) =>
|
|
12
|
+
normalizedPath.endsWith(extension),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Import protobuf BOM helpers and replace optional-dependency loader failures
|
|
18
|
+
* with actionable command-specific messages.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} [commandName="cdxgen"] CLI command name
|
|
21
|
+
* @param {string} [featureDescription="protobuf support"] Feature being used
|
|
22
|
+
* @returns {Promise<object>} Loaded protobom module namespace
|
|
23
|
+
*/
|
|
24
|
+
export async function importProtobomModule(
|
|
25
|
+
commandName = "cdxgen",
|
|
26
|
+
featureDescription = "protobuf support",
|
|
27
|
+
) {
|
|
28
|
+
try {
|
|
29
|
+
return await import("./protobom.js");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const message = `${error?.message || ""}`;
|
|
32
|
+
if (
|
|
33
|
+
error?.code === "ERR_MODULE_NOT_FOUND" ||
|
|
34
|
+
message.includes("@appthreat/cdx-proto") ||
|
|
35
|
+
message.includes("@bufbuild/protobuf")
|
|
36
|
+
) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${commandName} ${featureDescription} requires the optional '@appthreat/cdx-proto' and '@bufbuild/protobuf' dependencies. Install optional dependencies or use a binary that bundles protobuf support.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import { importProtobomModule, isProtoBomPath } from "./protobomLoader.js";
|
|
4
|
+
|
|
5
|
+
describe("protobomLoader", () => {
|
|
6
|
+
it("detects protobuf BOM file extensions", () => {
|
|
7
|
+
assert.strictEqual(isProtoBomPath("bom.cdx"), true);
|
|
8
|
+
assert.strictEqual(isProtoBomPath("bom.CDX.BIN"), true);
|
|
9
|
+
assert.strictEqual(isProtoBomPath("bom.proto"), true);
|
|
10
|
+
assert.strictEqual(isProtoBomPath("bom.json"), false);
|
|
11
|
+
assert.strictEqual(isProtoBomPath(""), false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("imports the protobuf BOM helper when optional support is installed", async () => {
|
|
15
|
+
let protobomModule;
|
|
16
|
+
try {
|
|
17
|
+
protobomModule = await importProtobomModule(
|
|
18
|
+
"cdx-test",
|
|
19
|
+
"protobuf BOM input",
|
|
20
|
+
);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
assert.match(
|
|
23
|
+
error.message,
|
|
24
|
+
/requires the optional '@appthreat\/cdx-proto' and '@bufbuild\/protobuf' dependencies/u,
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
assert.strictEqual(typeof protobomModule.readBinary, "function");
|
|
29
|
+
assert.strictEqual(typeof protobomModule.writeBinary, "function");
|
|
30
|
+
});
|
|
31
|
+
});
|