@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
package/lib/helpers/cbomutils.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { executeOsQuery } from "../managers/binary.js";
|
|
5
5
|
import { detectJsCryptoInventory } from "./analyzer.js";
|
|
6
|
+
import { analyzeDosaiCrypto } from "./dosai.js";
|
|
6
7
|
import {
|
|
7
8
|
createOccurrenceEvidence,
|
|
8
9
|
formatOccurrenceEvidence,
|
|
@@ -239,16 +240,19 @@ function mergeAlgorithmComponentUsage(component, usage, src, options) {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
if (usage.source) {
|
|
243
|
+
const sourceType =
|
|
244
|
+
usage.source === "dosai" ? undefined : `js-ast:${usage.source}`;
|
|
242
245
|
if (
|
|
246
|
+
sourceType &&
|
|
243
247
|
!properties.some(
|
|
244
248
|
(property) =>
|
|
245
249
|
property.name === "cdx:crypto:sourceType" &&
|
|
246
|
-
property.value ===
|
|
250
|
+
property.value === sourceType,
|
|
247
251
|
)
|
|
248
252
|
) {
|
|
249
253
|
properties.push({
|
|
250
254
|
name: "cdx:crypto:sourceType",
|
|
251
|
-
value:
|
|
255
|
+
value: sourceType,
|
|
252
256
|
});
|
|
253
257
|
}
|
|
254
258
|
}
|
|
@@ -271,6 +275,93 @@ function mergeAlgorithmComponentUsage(component, usage, src, options) {
|
|
|
271
275
|
return component;
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
function normalizeDosaiCryptoNames(cryptoObject) {
|
|
279
|
+
const rawName = cryptoObject?.Name || cryptoObject;
|
|
280
|
+
const names = new Set([rawName]);
|
|
281
|
+
const cleanName = String(rawName || "").trim();
|
|
282
|
+
if (!cleanName) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
if (cleanName.includes("/")) {
|
|
286
|
+
for (const part of cleanName
|
|
287
|
+
.split("/")
|
|
288
|
+
.map((candidate) => candidate.trim())
|
|
289
|
+
.filter(Boolean)) {
|
|
290
|
+
names.add(part);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (/^SHA-?256$/i.test(cleanName)) {
|
|
294
|
+
names.add("sha-256");
|
|
295
|
+
} else if (/^SHA-?384$/i.test(cleanName)) {
|
|
296
|
+
names.add("sha-384");
|
|
297
|
+
} else if (/^SHA-?512$/i.test(cleanName)) {
|
|
298
|
+
names.add("sha-512");
|
|
299
|
+
} else if (/^SHA-?1$/i.test(cleanName)) {
|
|
300
|
+
names.add("sha-1");
|
|
301
|
+
}
|
|
302
|
+
const context = [
|
|
303
|
+
cryptoObject?.Symbol,
|
|
304
|
+
cryptoObject?.Code,
|
|
305
|
+
cryptoObject?.Algorithm,
|
|
306
|
+
]
|
|
307
|
+
.filter(Boolean)
|
|
308
|
+
.join(" ");
|
|
309
|
+
if (/^SHA-?2$/i.test(cleanName)) {
|
|
310
|
+
if (/SHA-?256/i.test(context)) {
|
|
311
|
+
names.add("sha-256");
|
|
312
|
+
}
|
|
313
|
+
if (/SHA-?384/i.test(context)) {
|
|
314
|
+
names.add("sha-384");
|
|
315
|
+
}
|
|
316
|
+
if (/SHA-?512/i.test(context)) {
|
|
317
|
+
names.add("sha-512");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return Array.from(names);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function dosaiCryptoUsage(assetOrOperation) {
|
|
324
|
+
const location = assetOrOperation.Location || {};
|
|
325
|
+
return {
|
|
326
|
+
fileName: location.Path || location.FileName,
|
|
327
|
+
lineNumber: location.LineNumber || undefined,
|
|
328
|
+
columnNumber: location.ColumnNumber || undefined,
|
|
329
|
+
primitive: assetOrOperation.Family || assetOrOperation.OperationType,
|
|
330
|
+
source: "dosai",
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function addDosaiProperties(component, dosaiObject, evidenceType) {
|
|
335
|
+
const properties = component.properties || [];
|
|
336
|
+
const addProperty = (name, value) => {
|
|
337
|
+
if (value === undefined || value === null || value === "") {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (
|
|
341
|
+
!properties.some(
|
|
342
|
+
(property) =>
|
|
343
|
+
property.name === name && property.value === String(value),
|
|
344
|
+
)
|
|
345
|
+
) {
|
|
346
|
+
properties.push({ name, value: String(value) });
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
addProperty("cdx:crypto:sourceType", `dosai:${evidenceType}`);
|
|
350
|
+
addProperty("cdx:dosai:crypto:id", dosaiObject.Id);
|
|
351
|
+
addProperty("cdx:dosai:crypto:strength", dosaiObject.Strength);
|
|
352
|
+
addProperty(
|
|
353
|
+
"cdx:dosai:crypto:reachableFromEntryPoint",
|
|
354
|
+
dosaiObject.ReachableFromEntryPoint,
|
|
355
|
+
);
|
|
356
|
+
if (dosaiObject.EntryPointIds?.length) {
|
|
357
|
+
addProperty(
|
|
358
|
+
"cdx:dosai:crypto:entryPointCount",
|
|
359
|
+
dosaiObject.EntryPointIds.length,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
component.properties = properties;
|
|
363
|
+
}
|
|
364
|
+
|
|
274
365
|
export async function collectSourceCryptoComponents(src, options = {}) {
|
|
275
366
|
const inventory = await detectJsCryptoInventory(src, Boolean(options.deep));
|
|
276
367
|
const componentsByRef = new Map();
|
|
@@ -317,6 +408,75 @@ export async function collectSourceCryptoComponents(src, options = {}) {
|
|
|
317
408
|
);
|
|
318
409
|
}
|
|
319
410
|
|
|
411
|
+
export async function collectDosaiCryptoComponents(src, options = {}) {
|
|
412
|
+
const dosaiCrypto = analyzeDosaiCrypto(src, options);
|
|
413
|
+
if (!dosaiCrypto) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
const componentsByRef = new Map();
|
|
417
|
+
const cryptoObjects = [
|
|
418
|
+
...(dosaiCrypto.Assets || []).filter(
|
|
419
|
+
(asset) => asset.AssetType === "algorithm",
|
|
420
|
+
),
|
|
421
|
+
...(dosaiCrypto.Operations || []).map((operation) => ({
|
|
422
|
+
...operation,
|
|
423
|
+
Name: operation.Algorithm,
|
|
424
|
+
Family: operation.OperationType,
|
|
425
|
+
})),
|
|
426
|
+
];
|
|
427
|
+
for (const cryptoObject of cryptoObjects) {
|
|
428
|
+
for (const candidateName of normalizeDosaiCryptoNames(cryptoObject)) {
|
|
429
|
+
const normalizedName = normalizeDetectedCryptoAlgorithmName(
|
|
430
|
+
candidateName,
|
|
431
|
+
"algorithm",
|
|
432
|
+
);
|
|
433
|
+
const algorithmMetadata =
|
|
434
|
+
cbomCryptoOids[normalizedName] || cbomCryptoOids[candidateName];
|
|
435
|
+
if (!algorithmMetadata?.oid) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const bomRef = cryptoAlgorithmBomRef(
|
|
439
|
+
normalizedName,
|
|
440
|
+
algorithmMetadata.oid,
|
|
441
|
+
);
|
|
442
|
+
const component = componentsByRef.get(bomRef) || {
|
|
443
|
+
type: "cryptographic-asset",
|
|
444
|
+
name: normalizedName,
|
|
445
|
+
"bom-ref": bomRef,
|
|
446
|
+
description:
|
|
447
|
+
algorithmMetadata.description ||
|
|
448
|
+
"Cryptographic algorithm detected by dosai source analysis",
|
|
449
|
+
cryptoProperties: {
|
|
450
|
+
assetType: "algorithm",
|
|
451
|
+
oid: algorithmMetadata.oid,
|
|
452
|
+
},
|
|
453
|
+
properties: [],
|
|
454
|
+
};
|
|
455
|
+
mergeAlgorithmComponentUsage(
|
|
456
|
+
component,
|
|
457
|
+
dosaiCryptoUsage(cryptoObject),
|
|
458
|
+
src,
|
|
459
|
+
options,
|
|
460
|
+
);
|
|
461
|
+
addDosaiProperties(
|
|
462
|
+
component,
|
|
463
|
+
cryptoObject,
|
|
464
|
+
cryptoObject.OperationType ? "operation" : "asset",
|
|
465
|
+
);
|
|
466
|
+
componentsByRef.set(bomRef, component);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const components = Array.from(componentsByRef.values());
|
|
470
|
+
components.forEach((component) => {
|
|
471
|
+
normalizeCryptoComponentEvidence(component, options);
|
|
472
|
+
});
|
|
473
|
+
return components.sort((left, right) =>
|
|
474
|
+
`${left.name}:${left["bom-ref"]}`.localeCompare(
|
|
475
|
+
`${right.name}:${right["bom-ref"]}`,
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
320
480
|
/**
|
|
321
481
|
* Find crypto algorithm in the given code snippet
|
|
322
482
|
*
|
|
@@ -2,6 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
+
import esmock from "esmock";
|
|
5
6
|
import { assert, describe, it } from "poku";
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -248,4 +249,103 @@ describe("cbom utils", () => {
|
|
|
248
249
|
rmSync(projectDir, { recursive: true, force: true });
|
|
249
250
|
}
|
|
250
251
|
});
|
|
252
|
+
|
|
253
|
+
it("collectDosaiCryptoComponents() maps dosai algorithms to CBOM components with OIDs", async () => {
|
|
254
|
+
const { collectDosaiCryptoComponents } = await esmock("./cbomutils.js", {
|
|
255
|
+
"./dosai.js": {
|
|
256
|
+
analyzeDosaiCrypto: () => ({
|
|
257
|
+
Assets: [
|
|
258
|
+
{
|
|
259
|
+
Id: "cas1",
|
|
260
|
+
AssetType: "algorithm",
|
|
261
|
+
Name: "SHA-256",
|
|
262
|
+
Family: "hash",
|
|
263
|
+
Strength: "strong",
|
|
264
|
+
Location: {
|
|
265
|
+
Path: "Program.cs",
|
|
266
|
+
FileName: "Program.cs",
|
|
267
|
+
LineNumber: 12,
|
|
268
|
+
ColumnNumber: 9,
|
|
269
|
+
},
|
|
270
|
+
ReachableFromEntryPoint: true,
|
|
271
|
+
EntryPointIds: ["ep1"],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
Id: "cas2",
|
|
275
|
+
AssetType: "algorithm",
|
|
276
|
+
Name: "UnknownCipher",
|
|
277
|
+
Location: {
|
|
278
|
+
Path: "Program.cs",
|
|
279
|
+
FileName: "Program.cs",
|
|
280
|
+
LineNumber: 20,
|
|
281
|
+
ColumnNumber: 9,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
Operations: [
|
|
286
|
+
{
|
|
287
|
+
Id: "cop1",
|
|
288
|
+
OperationType: "hash",
|
|
289
|
+
Algorithm: "SHA-256",
|
|
290
|
+
Location: {
|
|
291
|
+
Path: "Program.cs",
|
|
292
|
+
FileName: "Program.cs",
|
|
293
|
+
LineNumber: 12,
|
|
294
|
+
ColumnNumber: 9,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
Id: "cop2",
|
|
299
|
+
OperationType: "use",
|
|
300
|
+
Algorithm: "SHA-2",
|
|
301
|
+
Symbol: "SHA256.HashData",
|
|
302
|
+
Location: {
|
|
303
|
+
Path: "Program.vb",
|
|
304
|
+
FileName: "Program.vb",
|
|
305
|
+
LineNumber: 42,
|
|
306
|
+
ColumnNumber: 22,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
}),
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const components = await collectDosaiCryptoComponents("/tmp/project", {
|
|
315
|
+
evidence: true,
|
|
316
|
+
specVersion: 1.7,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
assert.strictEqual(components.length, 1);
|
|
320
|
+
assert.strictEqual(components[0].name, "sha-256");
|
|
321
|
+
assert.strictEqual(components[0].type, "cryptographic-asset");
|
|
322
|
+
assert.strictEqual(components[0].cryptoProperties.assetType, "algorithm");
|
|
323
|
+
assert.ok(components[0].cryptoProperties.oid);
|
|
324
|
+
assert.ok(
|
|
325
|
+
components[0].properties.some(
|
|
326
|
+
(property) =>
|
|
327
|
+
property.name === "cdx:crypto:sourceType" &&
|
|
328
|
+
property.value === "dosai:operation",
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
assert.ok(
|
|
332
|
+
!components[0].properties.some(
|
|
333
|
+
(property) =>
|
|
334
|
+
property.name === "cdx:crypto:sourceType" &&
|
|
335
|
+
["dosai", "js-ast:dosai"].includes(property.value),
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
assert.ok(
|
|
339
|
+
components[0].evidence.occurrences.some(
|
|
340
|
+
(occurrence) =>
|
|
341
|
+
occurrence.location === "Program.cs" && occurrence.line === 12,
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
assert.ok(
|
|
345
|
+
components[0].evidence.occurrences.some(
|
|
346
|
+
(occurrence) =>
|
|
347
|
+
occurrence.location === "Program.vb" && occurrence.line === 42,
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
});
|
|
251
351
|
});
|
|
@@ -342,9 +342,21 @@ function normalizeRunnerLabels(runsOn) {
|
|
|
342
342
|
.map((label) => label.trim())
|
|
343
343
|
.filter(Boolean);
|
|
344
344
|
}
|
|
345
|
+
if (typeof runsOn === "object") {
|
|
346
|
+
return normalizeRunnerLabels(runsOn.labels);
|
|
347
|
+
}
|
|
345
348
|
return [];
|
|
346
349
|
}
|
|
347
350
|
|
|
351
|
+
function normalizeRunnerValue(runsOn) {
|
|
352
|
+
const labels = normalizeRunnerLabels(runsOn);
|
|
353
|
+
if (runsOn && typeof runsOn === "object" && !Array.isArray(runsOn)) {
|
|
354
|
+
const group = runsOn.group ? String(runsOn.group).trim() : "";
|
|
355
|
+
return [group, ...labels].filter(Boolean).join(",") || "unknown";
|
|
356
|
+
}
|
|
357
|
+
return labels.join(",") || "unknown";
|
|
358
|
+
}
|
|
359
|
+
|
|
348
360
|
function isSelfHostedRunner(runsOn) {
|
|
349
361
|
return normalizeRunnerLabels(runsOn).some((label) =>
|
|
350
362
|
label.toLowerCase().includes("self-hosted"),
|
|
@@ -1761,7 +1773,7 @@ function buildReusableWorkflowComponent(
|
|
|
1761
1773
|
{ name: "cdx:github:job:name", value: jobName },
|
|
1762
1774
|
{
|
|
1763
1775
|
name: "cdx:github:job:runner",
|
|
1764
|
-
value:
|
|
1776
|
+
value: normalizeRunnerValue(jobRunner),
|
|
1765
1777
|
},
|
|
1766
1778
|
{ name: "cdx:github:reusableWorkflow:uses", value: uses },
|
|
1767
1779
|
{
|
|
@@ -1951,7 +1963,7 @@ export function parseWorkflowFile(f, options) {
|
|
|
1951
1963
|
{ name: "cdx:github:job:name", value: jobName },
|
|
1952
1964
|
{
|
|
1953
1965
|
name: "cdx:github:job:runner",
|
|
1954
|
-
value:
|
|
1966
|
+
value: normalizeRunnerValue(jobRunner),
|
|
1955
1967
|
},
|
|
1956
1968
|
];
|
|
1957
1969
|
if (jobEnvironment) {
|
|
@@ -2042,7 +2054,7 @@ export function parseWorkflowFile(f, options) {
|
|
|
2042
2054
|
{ name: "cdx:github:job:name", value: jobName },
|
|
2043
2055
|
{
|
|
2044
2056
|
name: "cdx:github:job:runner",
|
|
2045
|
-
value:
|
|
2057
|
+
value: normalizeRunnerValue(jobRunner),
|
|
2046
2058
|
},
|
|
2047
2059
|
{ name: "cdx:github:action:uses", value: step.uses },
|
|
2048
2060
|
{
|
|
@@ -180,6 +180,58 @@ describe("githubActionsParser", () => {
|
|
|
180
180
|
}
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
it("normalizes object-form runs-on values", () => {
|
|
184
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-gha-"));
|
|
185
|
+
const workflowFile = path.join(tmpDir, "object-runs-on.yml");
|
|
186
|
+
writeFileSync(
|
|
187
|
+
workflowFile,
|
|
188
|
+
[
|
|
189
|
+
"name: Object runs-on",
|
|
190
|
+
"on: push",
|
|
191
|
+
"jobs:",
|
|
192
|
+
" grouped:",
|
|
193
|
+
" runs-on:",
|
|
194
|
+
" group: ubuntu-runners",
|
|
195
|
+
" labels: ubuntu-20.04-16core",
|
|
196
|
+
" steps:",
|
|
197
|
+
" - uses: actions/checkout@v4",
|
|
198
|
+
" selfHosted:",
|
|
199
|
+
" runs-on:",
|
|
200
|
+
" group: larger-runners",
|
|
201
|
+
" labels: [self-hosted, linux, x64]",
|
|
202
|
+
" steps:",
|
|
203
|
+
' - run: echo "ok"',
|
|
204
|
+
].join("\n"),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const result = parseWorkflowFile(workflowFile, { specVersion: 1.7 });
|
|
209
|
+
const actionComp = result.components.find(
|
|
210
|
+
(component) =>
|
|
211
|
+
getProp(component, "cdx:github:action:uses") ===
|
|
212
|
+
"actions/checkout@v4",
|
|
213
|
+
);
|
|
214
|
+
assert.strictEqual(
|
|
215
|
+
getProp(actionComp, "cdx:github:job:runner"),
|
|
216
|
+
"ubuntu-runners,ubuntu-20.04-16core",
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const selfHostedTask = result.workflows[0].tasks.find(
|
|
220
|
+
(task) => task.name === "selfHosted",
|
|
221
|
+
);
|
|
222
|
+
assert.strictEqual(
|
|
223
|
+
getProp(selfHostedTask, "cdx:github:job:runner"),
|
|
224
|
+
"larger-runners,self-hosted,linux,x64",
|
|
225
|
+
);
|
|
226
|
+
assert.strictEqual(
|
|
227
|
+
getProp(selfHostedTask, "cdx:github:job:isSelfHosted"),
|
|
228
|
+
"true",
|
|
229
|
+
);
|
|
230
|
+
} finally {
|
|
231
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
183
235
|
it("derives unnamed workflow names from the file stem without leaking Windows-style path segments", () => {
|
|
184
236
|
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "cdxgen-gha-"));
|
|
185
237
|
const workflowFile = path.join(tmpDir, "nested\\workflow-file.yml");
|
package/lib/helpers/display.js
CHANGED
|
@@ -42,6 +42,7 @@ const MULTIVALUE_ACTIVITY_TARGET_KEYS = new Set([
|
|
|
42
42
|
"SrcFiles",
|
|
43
43
|
]);
|
|
44
44
|
const PATH_SEPARATOR_REGEX = /[\\/]+/;
|
|
45
|
+
const SUSPICIOUS_SHELL_PATH_LABEL = "⚠ shell-metacharacters";
|
|
45
46
|
const ENV_AUDIT_SEVERITY_RANK = {
|
|
46
47
|
low: 1,
|
|
47
48
|
medium: 2,
|
|
@@ -1220,16 +1221,21 @@ export function printActivitySummary(reportType = undefined) {
|
|
|
1220
1221
|
}
|
|
1221
1222
|
return;
|
|
1222
1223
|
}
|
|
1223
|
-
const formatActivityTarget = (
|
|
1224
|
+
const formatActivityTarget = (activity) => {
|
|
1225
|
+
const target = activity?.target;
|
|
1226
|
+
const suspiciousPrefix =
|
|
1227
|
+
activity?.risk === "shell-metacharacters"
|
|
1228
|
+
? `${SUSPICIOUS_SHELL_PATH_LABEL}\n`
|
|
1229
|
+
: "";
|
|
1224
1230
|
if (typeof target !== "string" || !target.includes(",")) {
|
|
1225
|
-
return target || ""
|
|
1231
|
+
return `${suspiciousPrefix}${target || ""}`;
|
|
1226
1232
|
}
|
|
1227
1233
|
const targetEntries = splitCommaSeparatedActivityEntries(target);
|
|
1228
1234
|
if (isLikelyActivityPathList(targetEntries)) {
|
|
1229
|
-
return sortActivityTargetEntries(targetEntries).join("\n")
|
|
1235
|
+
return `${suspiciousPrefix}${sortActivityTargetEntries(targetEntries).join("\n")}`;
|
|
1230
1236
|
}
|
|
1231
1237
|
if (!(target.includes(":") || target.includes("="))) {
|
|
1232
|
-
return target || ""
|
|
1238
|
+
return `${suspiciousPrefix}${target || ""}`;
|
|
1233
1239
|
}
|
|
1234
1240
|
const targetSegments = target.split(/,\s*(?=[A-Za-z][\w-]*\s*[:=])/);
|
|
1235
1241
|
let didFormat = false;
|
|
@@ -1249,7 +1255,7 @@ export function printActivitySummary(reportType = undefined) {
|
|
|
1249
1255
|
.map((entry) => `- ${entry}`)
|
|
1250
1256
|
.join("\n")}`;
|
|
1251
1257
|
});
|
|
1252
|
-
return didFormat ? renderedSegments.join("\n") : target
|
|
1258
|
+
return `${suspiciousPrefix}${didFormat ? renderedSegments.join("\n") : target}`;
|
|
1253
1259
|
};
|
|
1254
1260
|
const formatActivityType = (type) => {
|
|
1255
1261
|
if (typeof type !== "string" || !type.includes(",")) {
|
|
@@ -1275,7 +1281,7 @@ export function printActivitySummary(reportType = undefined) {
|
|
|
1275
1281
|
formatActivityType(activity.projectType),
|
|
1276
1282
|
activity.packageType || "",
|
|
1277
1283
|
activity.kind || "",
|
|
1278
|
-
formatActivityTarget(activity
|
|
1284
|
+
formatActivityTarget(activity),
|
|
1279
1285
|
activity.reason
|
|
1280
1286
|
? `${formatStatus(activity.status)}\n${activity.reason}`.trim()
|
|
1281
1287
|
: formatStatus(activity.status),
|
|
@@ -481,6 +481,44 @@ it("renders plain comma-separated activity paths one per line sorted by depth",
|
|
|
481
481
|
}
|
|
482
482
|
});
|
|
483
483
|
|
|
484
|
+
it("highlights suspicious shell-metacharacter paths in the activity summary", async () => {
|
|
485
|
+
const tableStub = sinon.stub().returns("activity-table");
|
|
486
|
+
const shellIfs = "$" + "{IFS}";
|
|
487
|
+
try {
|
|
488
|
+
const { printActivitySummary: printActivitySummaryMocked } = await esmock(
|
|
489
|
+
"./display.js",
|
|
490
|
+
{
|
|
491
|
+
"./table.js": {
|
|
492
|
+
createStream: sinon.stub(),
|
|
493
|
+
table: tableStub,
|
|
494
|
+
},
|
|
495
|
+
"./utils.js": {
|
|
496
|
+
getRecordedActivities: sinon.stub().returns([
|
|
497
|
+
{
|
|
498
|
+
identifier: "ACT-0005",
|
|
499
|
+
kind: "inspect",
|
|
500
|
+
reason: "Suspicious path contains shell metacharacters.",
|
|
501
|
+
risk: "shell-metacharacters",
|
|
502
|
+
status: "completed",
|
|
503
|
+
target: `/tmp/repo/evil;cd${shellIfs}..;printf${shellIfs}marker>CDXGEN_GITURL_E2E_MARKER;#/pom.xml`,
|
|
504
|
+
},
|
|
505
|
+
]),
|
|
506
|
+
isDryRun: true,
|
|
507
|
+
isSecureMode: false,
|
|
508
|
+
safeExistsSync: sinon.stub(),
|
|
509
|
+
toCamel: sinon.stub(),
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
printActivitySummaryMocked();
|
|
514
|
+
const [data] = tableStub.firstCall.args;
|
|
515
|
+
assert.ok(data[1][4].startsWith("⚠ shell-metacharacters\n"));
|
|
516
|
+
assert.ok(data[1][4].includes(`evil;cd${shellIfs}..`));
|
|
517
|
+
} finally {
|
|
518
|
+
sinon.restore();
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
484
522
|
it("prints grouped environment audit findings in a secure-mode panel", async () => {
|
|
485
523
|
const tableStub = sinon.stub().returns("env-audit-table");
|
|
486
524
|
try {
|