@cyclonedx/cdxgen 12.4.1 → 12.4.3
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/bin/evinse.js +15 -0
- package/lib/cli/index.js +60 -9
- package/lib/cli/index.poku.js +161 -0
- package/lib/evinser/evinser.js +118 -3
- 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/display.js +12 -6
- package/lib/helpers/display.poku.js +38 -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/utils.js +198 -1
- package/lib/helpers/utils.poku.js +352 -0
- package/lib/stages/postgen/annotator.js +2 -1
- package/lib/stages/postgen/annotator.poku.js +28 -0
- package/package.json +12 -12
- 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 +1 -3
- 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/display.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/utils.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.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/utils.js
CHANGED
|
@@ -58,6 +58,13 @@ import Arborist from "../third-party/arborist/lib/index.js";
|
|
|
58
58
|
import { analyzeSuspiciousJsFile } from "./analyzer.js";
|
|
59
59
|
import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "./auditCategories.js";
|
|
60
60
|
import { parseWorkflowFile } from "./ciParsers/githubActions.js";
|
|
61
|
+
import {
|
|
62
|
+
addDosaiSetValue,
|
|
63
|
+
buildDosaiPurlAliasMap,
|
|
64
|
+
dosaiSourceLocation,
|
|
65
|
+
dosaiSourceLocationFromNode,
|
|
66
|
+
resolveDosaiComponentPurl,
|
|
67
|
+
} from "./dosaiParsers.js";
|
|
61
68
|
import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
|
|
62
69
|
import {
|
|
63
70
|
createOccurrenceEvidence,
|
|
@@ -1108,6 +1115,50 @@ function isWindowsShellHijackRisk(command, options) {
|
|
|
1108
1115
|
|
|
1109
1116
|
const VERSION_PROBE_ARGS = new Set(["--version", "-version", "version"]);
|
|
1110
1117
|
|
|
1118
|
+
const POSIX_SHELL_METACHARACTERS = /[;&|<>$`\\\n\r]/;
|
|
1119
|
+
const WINDOWS_SHELL_METACHARACTERS = /[&|<>^%\n\r]/;
|
|
1120
|
+
|
|
1121
|
+
function hasShellMetacharacters(value) {
|
|
1122
|
+
if (value === undefined || value === null) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
const stringValue = String(value);
|
|
1126
|
+
return isWin
|
|
1127
|
+
? WINDOWS_SHELL_METACHARACTERS.test(stringValue)
|
|
1128
|
+
: POSIX_SHELL_METACHARACTERS.test(stringValue);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function getUnsafeShellToken(command, args) {
|
|
1132
|
+
if (hasShellMetacharacters(command)) {
|
|
1133
|
+
return command;
|
|
1134
|
+
}
|
|
1135
|
+
const argList = Array.isArray(args)
|
|
1136
|
+
? args
|
|
1137
|
+
: args === undefined || args === null
|
|
1138
|
+
? []
|
|
1139
|
+
: [args];
|
|
1140
|
+
return argList.find((arg) => hasShellMetacharacters(arg));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function recordSuspiciousShellPathActivities(files, metadata = {}) {
|
|
1144
|
+
for (const file of files) {
|
|
1145
|
+
if (!hasShellMetacharacters(file)) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
recordActivity({
|
|
1149
|
+
classification: "suspicious-path",
|
|
1150
|
+
discoveryType: metadata.discoveryType,
|
|
1151
|
+
kind: "inspect",
|
|
1152
|
+
pattern: metadata.pattern,
|
|
1153
|
+
reason:
|
|
1154
|
+
"Suspicious path contains shell metacharacters. cdxgen passes direct process arguments as argv values, but review this path before invoking external build tools on untrusted projects.",
|
|
1155
|
+
risk: "shell-metacharacters",
|
|
1156
|
+
status: "completed",
|
|
1157
|
+
target: file,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1111
1162
|
function detectProbeType(command, args = []) {
|
|
1112
1163
|
const normalizedCommand = basename(String(command || "")).toLowerCase();
|
|
1113
1164
|
const normalizedArgs = (args || []).map((arg) => String(arg).toLowerCase());
|
|
@@ -1286,8 +1337,30 @@ export function safeSpawnSync(command, args, options) {
|
|
|
1286
1337
|
options = {
|
|
1287
1338
|
...options,
|
|
1288
1339
|
};
|
|
1340
|
+
}
|
|
1341
|
+
if (options.cdxgenActivity) {
|
|
1289
1342
|
delete options.cdxgenActivity;
|
|
1290
1343
|
}
|
|
1344
|
+
if (options.shell === true) {
|
|
1345
|
+
const unsafeShellToken = getUnsafeShellToken(command, args);
|
|
1346
|
+
if (unsafeShellToken !== undefined) {
|
|
1347
|
+
const blockedReason = `Blocked shell execution for ${command}: command or argument contains shell metacharacters.`;
|
|
1348
|
+
console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
|
|
1349
|
+
recordActivity({
|
|
1350
|
+
kind: activityDescriptor.kind,
|
|
1351
|
+
...activityDescriptor.metadata,
|
|
1352
|
+
reason: blockedReason,
|
|
1353
|
+
status: "blocked",
|
|
1354
|
+
target: activityDescriptor.target,
|
|
1355
|
+
});
|
|
1356
|
+
return {
|
|
1357
|
+
status: 1,
|
|
1358
|
+
stdout: undefined,
|
|
1359
|
+
stderr: undefined,
|
|
1360
|
+
error: new Error(blockedReason),
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1291
1364
|
// Inject maxBuffer
|
|
1292
1365
|
if (!options.maxBuffer) {
|
|
1293
1366
|
options.maxBuffer = MAX_BUFFER;
|
|
@@ -1753,6 +1826,10 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
1753
1826
|
"dotnet-framework47",
|
|
1754
1827
|
"dotnet-framework48",
|
|
1755
1828
|
"vb",
|
|
1829
|
+
"vbnet",
|
|
1830
|
+
"visualbasic",
|
|
1831
|
+
"f#",
|
|
1832
|
+
"fs",
|
|
1756
1833
|
"fsharp",
|
|
1757
1834
|
"twincat",
|
|
1758
1835
|
"csproj",
|
|
@@ -2363,6 +2440,10 @@ export function getAllFilesWithIgnore(
|
|
|
2363
2440
|
reasonBuilder: (count) =>
|
|
2364
2441
|
`Scanned ${dirPath} with glob '${patternValue}' for ${discoveryMetadata.label}; matched ${files.length} path(s)${buildReadCountSuffix(count)}.`,
|
|
2365
2442
|
});
|
|
2443
|
+
recordSuspiciousShellPathActivities(files, {
|
|
2444
|
+
discoveryType: discoveryMetadata.discoveryType,
|
|
2445
|
+
pattern: patternValue,
|
|
2446
|
+
});
|
|
2366
2447
|
if (files.length > 1) {
|
|
2367
2448
|
thoughtLog(
|
|
2368
2449
|
`Found ${files.length} files for the pattern '${pattern}' at '${dirPath}'.`,
|
|
@@ -21739,6 +21820,42 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
|
|
|
21739
21820
|
};
|
|
21740
21821
|
}
|
|
21741
21822
|
|
|
21823
|
+
function addDotnetIdentityMethod(apkg, value) {
|
|
21824
|
+
if (!value) {
|
|
21825
|
+
return;
|
|
21826
|
+
}
|
|
21827
|
+
apkg.evidence = apkg.evidence || {};
|
|
21828
|
+
const identityList = Array.isArray(apkg.evidence.identity)
|
|
21829
|
+
? apkg.evidence.identity
|
|
21830
|
+
: undefined;
|
|
21831
|
+
let identity = identityList
|
|
21832
|
+
? identityList.find((entry) => entry?.field === "purl") ||
|
|
21833
|
+
identityList.find((entry) => !entry?.field)
|
|
21834
|
+
: apkg.evidence.identity;
|
|
21835
|
+
if (!identity) {
|
|
21836
|
+
identity = { field: "purl", confidence: 1, methods: [] };
|
|
21837
|
+
if (identityList) {
|
|
21838
|
+
identityList.push(identity);
|
|
21839
|
+
}
|
|
21840
|
+
}
|
|
21841
|
+
identity.field ??= "purl";
|
|
21842
|
+
identity.confidence ??= 1;
|
|
21843
|
+
identity.methods ??= [];
|
|
21844
|
+
if (
|
|
21845
|
+
!identity.methods.some(
|
|
21846
|
+
(method) =>
|
|
21847
|
+
method.technique === "source-code-analysis" && method.value === value,
|
|
21848
|
+
)
|
|
21849
|
+
) {
|
|
21850
|
+
identity.methods.push({
|
|
21851
|
+
technique: "source-code-analysis",
|
|
21852
|
+
confidence: 1,
|
|
21853
|
+
value,
|
|
21854
|
+
});
|
|
21855
|
+
}
|
|
21856
|
+
apkg.evidence.identity = identityList || identity;
|
|
21857
|
+
}
|
|
21858
|
+
|
|
21742
21859
|
/**
|
|
21743
21860
|
* Enrich .NET package components with occurrence evidence and imported module/method
|
|
21744
21861
|
* information from a dosai dependency slices file.
|
|
@@ -21762,6 +21879,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
21762
21879
|
const purlLocationMap = {};
|
|
21763
21880
|
const purlModulesMap = {};
|
|
21764
21881
|
const purlMethodsMap = {};
|
|
21882
|
+
const purlAliasMap = buildDosaiPurlAliasMap(pkgList);
|
|
21765
21883
|
for (const apkg of pkgList) {
|
|
21766
21884
|
if (apkg.properties && Array.isArray(apkg.properties)) {
|
|
21767
21885
|
apkg.properties
|
|
@@ -21778,13 +21896,33 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
21778
21896
|
});
|
|
21779
21897
|
}
|
|
21780
21898
|
}
|
|
21781
|
-
|
|
21899
|
+
let slicesData;
|
|
21900
|
+
try {
|
|
21901
|
+
slicesData = JSON.parse(readFileSync(slicesFile, "utf-8"));
|
|
21902
|
+
} catch (_err) {
|
|
21903
|
+
return pkgList;
|
|
21904
|
+
}
|
|
21782
21905
|
if (slicesData && Object.keys(slicesData)) {
|
|
21783
21906
|
thoughtLog(
|
|
21784
21907
|
"Let's thoroughly inspect the dependency slice to identify where and how the components are used.",
|
|
21785
21908
|
);
|
|
21786
21909
|
if (slicesData.Dependencies) {
|
|
21787
21910
|
for (const adep of slicesData.Dependencies) {
|
|
21911
|
+
if (adep.Purl) {
|
|
21912
|
+
const modPurl = resolveDosaiComponentPurl(adep.Purl, purlAliasMap);
|
|
21913
|
+
if (modPurl) {
|
|
21914
|
+
addDosaiSetValue(
|
|
21915
|
+
purlLocationMap,
|
|
21916
|
+
modPurl,
|
|
21917
|
+
dosaiSourceLocation(adep),
|
|
21918
|
+
);
|
|
21919
|
+
addDosaiSetValue(
|
|
21920
|
+
purlModulesMap,
|
|
21921
|
+
modPurl,
|
|
21922
|
+
adep.Name || adep.Namespace,
|
|
21923
|
+
);
|
|
21924
|
+
}
|
|
21925
|
+
}
|
|
21788
21926
|
// Case 1: Dependencies slice has the .dll file
|
|
21789
21927
|
if (adep.Module?.endsWith(".dll") && pkgFilePurlMap[adep.Module]) {
|
|
21790
21928
|
const modPurl = pkgFilePurlMap[adep.Module];
|
|
@@ -21810,6 +21948,64 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
21810
21948
|
}
|
|
21811
21949
|
}
|
|
21812
21950
|
}
|
|
21951
|
+
if (slicesData.PackageReachability) {
|
|
21952
|
+
const graphEdges = Object.fromEntries(
|
|
21953
|
+
(slicesData.CallGraph?.Edges || []).map((edge) => [edge.Id, edge]),
|
|
21954
|
+
);
|
|
21955
|
+
const graphNodes = Object.fromEntries(
|
|
21956
|
+
(slicesData.CallGraph?.Nodes || []).map((node) => [node.Id, node]),
|
|
21957
|
+
);
|
|
21958
|
+
for (const reachability of slicesData.PackageReachability) {
|
|
21959
|
+
const modPurl = resolveDosaiComponentPurl(
|
|
21960
|
+
reachability.Purl,
|
|
21961
|
+
purlAliasMap,
|
|
21962
|
+
);
|
|
21963
|
+
if (!modPurl) {
|
|
21964
|
+
continue;
|
|
21965
|
+
}
|
|
21966
|
+
let hasExplicitSourceLocations = false;
|
|
21967
|
+
for (const sourceLocation of reachability.SourceLocations || []) {
|
|
21968
|
+
const location = dosaiSourceLocation(sourceLocation);
|
|
21969
|
+
addDosaiSetValue(purlLocationMap, modPurl, location);
|
|
21970
|
+
hasExplicitSourceLocations ||= Boolean(location);
|
|
21971
|
+
}
|
|
21972
|
+
for (const edgeId of reachability.EdgeIds || []) {
|
|
21973
|
+
const edge = graphEdges[edgeId];
|
|
21974
|
+
if (!hasExplicitSourceLocations) {
|
|
21975
|
+
addDosaiSetValue(
|
|
21976
|
+
purlLocationMap,
|
|
21977
|
+
modPurl,
|
|
21978
|
+
dosaiSourceLocation(edge),
|
|
21979
|
+
);
|
|
21980
|
+
}
|
|
21981
|
+
addDosaiSetValue(
|
|
21982
|
+
purlMethodsMap,
|
|
21983
|
+
modPurl,
|
|
21984
|
+
edge?.CalledMethodName || edge?.TargetName,
|
|
21985
|
+
);
|
|
21986
|
+
}
|
|
21987
|
+
for (const nodeId of reachability.NodeIds || []) {
|
|
21988
|
+
const node = graphNodes[nodeId];
|
|
21989
|
+
if (!hasExplicitSourceLocations) {
|
|
21990
|
+
addDosaiSetValue(
|
|
21991
|
+
purlLocationMap,
|
|
21992
|
+
modPurl,
|
|
21993
|
+
dosaiSourceLocationFromNode(node),
|
|
21994
|
+
);
|
|
21995
|
+
}
|
|
21996
|
+
addDosaiSetValue(
|
|
21997
|
+
purlModulesMap,
|
|
21998
|
+
modPurl,
|
|
21999
|
+
node?.ClassName || node?.Module,
|
|
22000
|
+
);
|
|
22001
|
+
addDosaiSetValue(
|
|
22002
|
+
purlMethodsMap,
|
|
22003
|
+
modPurl,
|
|
22004
|
+
node?.Name || node?.Identity?.MethodName,
|
|
22005
|
+
);
|
|
22006
|
+
}
|
|
22007
|
+
}
|
|
22008
|
+
}
|
|
21813
22009
|
if (slicesData.MethodCalls) {
|
|
21814
22010
|
for (const amethodCall of slicesData.MethodCalls) {
|
|
21815
22011
|
if (
|
|
@@ -21857,6 +22053,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
21857
22053
|
apkg.evidence.occurrences = locationOccurrences.map((l) =>
|
|
21858
22054
|
parseOccurrenceEvidenceLocation(l),
|
|
21859
22055
|
);
|
|
22056
|
+
addDotnetIdentityMethod(apkg, locationOccurrences[0]);
|
|
21860
22057
|
// Set the package scope
|
|
21861
22058
|
apkg.scope = "required";
|
|
21862
22059
|
}
|