@cyclonedx/cdxgen 12.4.2 → 12.4.4
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 -0
- package/bin/audit.js +7 -0
- package/bin/cdxgen.js +48 -2
- package/bin/evinse.js +7 -0
- package/lib/audit/index.js +165 -2
- package/lib/audit/index.poku.js +462 -0
- package/lib/cli/index.js +320 -172
- package/lib/cli/index.poku.js +81 -0
- package/lib/evinser/evinser.js +31 -9
- package/lib/helpers/analyzer.js +890 -0
- package/lib/helpers/analyzer.poku.js +341 -0
- package/lib/helpers/atomUtils.js +445 -0
- package/lib/helpers/atomUtils.poku.js +137 -0
- package/lib/helpers/bomUtils.js +71 -0
- package/lib/helpers/bomUtils.poku.js +45 -0
- package/lib/helpers/depsUtils.js +146 -0
- package/lib/helpers/depsUtils.poku.js +183 -0
- package/lib/helpers/display.js +12 -6
- package/lib/helpers/display.poku.js +38 -0
- package/lib/helpers/utils.js +653 -191
- package/lib/helpers/utils.poku.js +414 -4
- package/lib/managers/binary.js +18 -9
- package/lib/stages/postgen/postgen.js +215 -0
- package/lib/stages/postgen/postgen.poku.js +218 -3
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +8 -8
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/atomUtils.d.ts +18 -0
- package/types/lib/helpers/atomUtils.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts +9 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/dosaiParsers.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +19 -0
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +2 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -5,10 +5,13 @@ import {
|
|
|
5
5
|
getCycloneDxFormat,
|
|
6
6
|
getCycloneDxRootFormatKey,
|
|
7
7
|
getNonCycloneDxErrorMessage,
|
|
8
|
+
getSupportedCycloneDxComponentTypes,
|
|
8
9
|
isCycloneDx20SpecVersion,
|
|
9
10
|
isCycloneDxBom,
|
|
11
|
+
isCycloneDxComponentTypeEnabled,
|
|
10
12
|
isCycloneDxSpecVersionAtLeast,
|
|
11
13
|
isSpdxJsonLd,
|
|
14
|
+
normalizeCycloneDxComponentTypeFilter,
|
|
12
15
|
normalizeCycloneDxSpecVersion,
|
|
13
16
|
setCycloneDxFormat,
|
|
14
17
|
toCycloneDxSpecVersionString,
|
|
@@ -81,6 +84,48 @@ describe("bomUtils", () => {
|
|
|
81
84
|
assert.strictEqual(isCycloneDxSpecVersionAtLeast(undefined, 1.7), false);
|
|
82
85
|
});
|
|
83
86
|
|
|
87
|
+
it("reports supported component types by CycloneDX spec version", () => {
|
|
88
|
+
assert.strictEqual(
|
|
89
|
+
getSupportedCycloneDxComponentTypes(1.5).includes("cryptographic-asset"),
|
|
90
|
+
false,
|
|
91
|
+
);
|
|
92
|
+
assert.strictEqual(
|
|
93
|
+
getSupportedCycloneDxComponentTypes(1.6).includes("cryptographic-asset"),
|
|
94
|
+
true,
|
|
95
|
+
);
|
|
96
|
+
assert.strictEqual(
|
|
97
|
+
getSupportedCycloneDxComponentTypes(1.7).includes("data"),
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("normalizes and applies requested CycloneDX component type filters", () => {
|
|
103
|
+
assert.deepStrictEqual(
|
|
104
|
+
normalizeCycloneDxComponentTypeFilter(["library", "", "library"]),
|
|
105
|
+
["library"],
|
|
106
|
+
);
|
|
107
|
+
assert.strictEqual(
|
|
108
|
+
isCycloneDxComponentTypeEnabled("cryptographic-asset", {
|
|
109
|
+
specVersion: 1.5,
|
|
110
|
+
}),
|
|
111
|
+
false,
|
|
112
|
+
);
|
|
113
|
+
assert.strictEqual(
|
|
114
|
+
isCycloneDxComponentTypeEnabled("cryptographic-asset", {
|
|
115
|
+
componentType: ["library"],
|
|
116
|
+
specVersion: 1.7,
|
|
117
|
+
}),
|
|
118
|
+
false,
|
|
119
|
+
);
|
|
120
|
+
assert.strictEqual(
|
|
121
|
+
isCycloneDxComponentTypeEnabled("library", {
|
|
122
|
+
componentType: ["library"],
|
|
123
|
+
specVersion: 1.5,
|
|
124
|
+
}),
|
|
125
|
+
true,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
84
129
|
it("selects and writes the correct CycloneDX root format key", () => {
|
|
85
130
|
assert.strictEqual(getCycloneDxRootFormatKey("1.7"), "bomFormat");
|
|
86
131
|
assert.strictEqual(getCycloneDxRootFormatKey("2.0"), "specFormat");
|
package/lib/helpers/depsUtils.js
CHANGED
|
@@ -82,6 +82,102 @@ export function mergeDependencies(
|
|
|
82
82
|
return retlist;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
const NPM_NON_RUNTIME_SCOPE_PROPERTIES = new Set([
|
|
86
|
+
"cdx:npm:package:development",
|
|
87
|
+
"cdx:npm:package:optional",
|
|
88
|
+
"cdx:npm:package:peer",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
function hasNonRuntimeNpmScope(component) {
|
|
92
|
+
return (component?.properties || []).some(
|
|
93
|
+
(property) =>
|
|
94
|
+
NPM_NON_RUNTIME_SCOPE_PROPERTIES.has(property.name) &&
|
|
95
|
+
property.value === "true",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeBomRef(ref) {
|
|
100
|
+
const refString = String(ref || "");
|
|
101
|
+
try {
|
|
102
|
+
return decodeURIComponent(refString).toLowerCase();
|
|
103
|
+
} catch {
|
|
104
|
+
return refString.toLowerCase();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Propagates required scope through a dependency graph.
|
|
110
|
+
*
|
|
111
|
+
* If component A has `scope: "required"` and dependency metadata says A depends
|
|
112
|
+
* on B, B is also runtime-relevant. Keep packages optional when lockfile/parser
|
|
113
|
+
* metadata explicitly identifies them as development, optional, or peer-only.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object[]} components CycloneDX component objects
|
|
116
|
+
* @param {Object[]} dependencies CycloneDX dependency entries
|
|
117
|
+
* @returns {Object[]} The same component array with scopes updated in place
|
|
118
|
+
*/
|
|
119
|
+
export function propagateRequiredScopeFromDependencies(
|
|
120
|
+
components = [],
|
|
121
|
+
dependencies = [],
|
|
122
|
+
) {
|
|
123
|
+
if (!components?.length || !dependencies?.length) {
|
|
124
|
+
return components;
|
|
125
|
+
}
|
|
126
|
+
const componentByRef = new Map();
|
|
127
|
+
for (const component of components) {
|
|
128
|
+
for (const ref of [component?.["bom-ref"], component?.purl]) {
|
|
129
|
+
if (ref) {
|
|
130
|
+
componentByRef.set(normalizeBomRef(ref), component);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!componentByRef.size) {
|
|
135
|
+
return components;
|
|
136
|
+
}
|
|
137
|
+
const dependencyMap = new Map();
|
|
138
|
+
for (const dependency of dependencies || []) {
|
|
139
|
+
const ref = normalizeBomRef(dependency?.ref);
|
|
140
|
+
if (!ref) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const dependsOn = (dependency.dependsOn || [])
|
|
144
|
+
.map((depRef) => normalizeBomRef(depRef))
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
dependencyMap.set(
|
|
147
|
+
ref,
|
|
148
|
+
Array.from(new Set([...(dependencyMap.get(ref) || []), ...dependsOn])),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const requiredStack = [];
|
|
152
|
+
const visited = new Set();
|
|
153
|
+
for (const component of components) {
|
|
154
|
+
if (component?.scope === "required") {
|
|
155
|
+
const ref = normalizeBomRef(component["bom-ref"] || component.purl);
|
|
156
|
+
if (ref) {
|
|
157
|
+
requiredStack.push(ref);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
while (requiredStack.length) {
|
|
162
|
+
const requiredRef = requiredStack.pop();
|
|
163
|
+
if (visited.has(requiredRef)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
visited.add(requiredRef);
|
|
167
|
+
for (const childRef of dependencyMap.get(requiredRef) || []) {
|
|
168
|
+
const childComponent = componentByRef.get(childRef);
|
|
169
|
+
if (!childComponent || hasNonRuntimeNpmScope(childComponent)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (childComponent.scope !== "required") {
|
|
173
|
+
childComponent.scope = "required";
|
|
174
|
+
}
|
|
175
|
+
requiredStack.push(childRef);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return components;
|
|
179
|
+
}
|
|
180
|
+
|
|
85
181
|
function serviceIdentityKey(service) {
|
|
86
182
|
if (service?.["bom-ref"]) {
|
|
87
183
|
return service["bom-ref"].toLowerCase();
|
|
@@ -330,3 +426,53 @@ export function trimComponents(components) {
|
|
|
330
426
|
}
|
|
331
427
|
return filteredComponents;
|
|
332
428
|
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Filter out invalid cryptographic-asset components from a component list.
|
|
432
|
+
* Removes algorithm components without a valid cryptoProperties.oid and
|
|
433
|
+
* certificate components without cryptoProperties.algorithmProperties.
|
|
434
|
+
*
|
|
435
|
+
* @param {Object[] | undefined | null} components Array of CycloneDX components
|
|
436
|
+
* @returns {Object[]} Filtered array with invalid crypto components removed
|
|
437
|
+
*/
|
|
438
|
+
export function filterInvalidCryptoComponents(components) {
|
|
439
|
+
if (!components?.length) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
return components.filter((comp) => {
|
|
443
|
+
if (comp.type !== "cryptographic-asset") {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
if (!comp.cryptoProperties) {
|
|
447
|
+
if (DEBUG_MODE) {
|
|
448
|
+
console.log(
|
|
449
|
+
`Removing cryptographic-asset '${comp.name}' without cryptoProperties`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (
|
|
455
|
+
comp.cryptoProperties.assetType === "algorithm" &&
|
|
456
|
+
!comp.cryptoProperties.oid
|
|
457
|
+
) {
|
|
458
|
+
if (DEBUG_MODE) {
|
|
459
|
+
console.log(
|
|
460
|
+
`Removing cryptographic-asset algorithm '${comp.name}' without OID`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
if (
|
|
466
|
+
comp.cryptoProperties.assetType === "certificate" &&
|
|
467
|
+
!comp.cryptoProperties.algorithmProperties
|
|
468
|
+
) {
|
|
469
|
+
if (DEBUG_MODE) {
|
|
470
|
+
console.log(
|
|
471
|
+
`Removing cryptographic-asset certificate '${comp.name}' without algorithmProperties`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { assert, describe, it } from "poku";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
filterInvalidCryptoComponents,
|
|
4
5
|
mergeDependencies,
|
|
5
6
|
mergeServices,
|
|
7
|
+
propagateRequiredScopeFromDependencies,
|
|
6
8
|
trimComponents,
|
|
7
9
|
} from "./depsUtils.js";
|
|
8
10
|
|
|
@@ -153,6 +155,105 @@ describe("mergeDependencies()", () => {
|
|
|
153
155
|
});
|
|
154
156
|
});
|
|
155
157
|
|
|
158
|
+
describe("propagateRequiredScopeFromDependencies()", () => {
|
|
159
|
+
it("marks transitive dependencies of required components as required", () => {
|
|
160
|
+
const components = [
|
|
161
|
+
{
|
|
162
|
+
name: "app-lib",
|
|
163
|
+
"bom-ref": "pkg:npm/app-lib@1.0.0",
|
|
164
|
+
scope: "required",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "runtime-a",
|
|
168
|
+
"bom-ref": "pkg:npm/runtime-a@1.0.0",
|
|
169
|
+
scope: "optional",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "runtime-b",
|
|
173
|
+
"bom-ref": "pkg:npm/runtime-b@1.0.0",
|
|
174
|
+
scope: "optional",
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
const dependencies = [
|
|
178
|
+
{ ref: "pkg:npm/app-lib@1.0.0", dependsOn: ["pkg:npm/runtime-a@1.0.0"] },
|
|
179
|
+
{
|
|
180
|
+
ref: "pkg:npm/runtime-a@1.0.0",
|
|
181
|
+
dependsOn: ["pkg:npm/runtime-b@1.0.0"],
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
propagateRequiredScopeFromDependencies(components, dependencies);
|
|
186
|
+
|
|
187
|
+
assert.strictEqual(components[1].scope, "required");
|
|
188
|
+
assert.strictEqual(components[2].scope, "required");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("preserves known development, optional, and peer-only packages", () => {
|
|
192
|
+
const components = [
|
|
193
|
+
{
|
|
194
|
+
name: "app-lib",
|
|
195
|
+
"bom-ref": "pkg:npm/app-lib@1.0.0",
|
|
196
|
+
scope: "required",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "dev-tool",
|
|
200
|
+
"bom-ref": "pkg:npm/dev-tool@1.0.0",
|
|
201
|
+
scope: "optional",
|
|
202
|
+
properties: [{ name: "cdx:npm:package:development", value: "true" }],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "optional-runtime",
|
|
206
|
+
"bom-ref": "pkg:npm/optional-runtime@1.0.0",
|
|
207
|
+
scope: "optional",
|
|
208
|
+
properties: [{ name: "cdx:npm:package:optional", value: "true" }],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "peer-runtime",
|
|
212
|
+
"bom-ref": "pkg:npm/peer-runtime@1.0.0",
|
|
213
|
+
scope: "optional",
|
|
214
|
+
properties: [{ name: "cdx:npm:package:peer", value: "true" }],
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
const dependencies = [
|
|
218
|
+
{
|
|
219
|
+
ref: "pkg:npm/app-lib@1.0.0",
|
|
220
|
+
dependsOn: [
|
|
221
|
+
"pkg:npm/dev-tool@1.0.0",
|
|
222
|
+
"pkg:npm/optional-runtime@1.0.0",
|
|
223
|
+
"pkg:npm/peer-runtime@1.0.0",
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
propagateRequiredScopeFromDependencies(components, dependencies);
|
|
229
|
+
|
|
230
|
+
assert.strictEqual(components[1].scope, "optional");
|
|
231
|
+
assert.strictEqual(components[2].scope, "optional");
|
|
232
|
+
assert.strictEqual(components[3].scope, "optional");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("handles decoded and encoded bom-ref variants", () => {
|
|
236
|
+
const components = [
|
|
237
|
+
{
|
|
238
|
+
name: "scoped",
|
|
239
|
+
"bom-ref": "pkg:npm/@scope/scoped@1.0.0",
|
|
240
|
+
scope: "required",
|
|
241
|
+
},
|
|
242
|
+
{ name: "child", purl: "pkg:npm/@scope/child@1.0.0", scope: "optional" },
|
|
243
|
+
];
|
|
244
|
+
const dependencies = [
|
|
245
|
+
{
|
|
246
|
+
ref: "pkg:npm/%40scope/scoped@1.0.0",
|
|
247
|
+
dependsOn: ["pkg:npm/%40scope/child@1.0.0"],
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
propagateRequiredScopeFromDependencies(components, dependencies);
|
|
252
|
+
|
|
253
|
+
assert.strictEqual(components[1].scope, "required");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
156
257
|
describe("trimComponents()", () => {
|
|
157
258
|
it("retains hashes from duplicate components", () => {
|
|
158
259
|
const components = [
|
|
@@ -331,3 +432,85 @@ describe("mergeServices()", () => {
|
|
|
331
432
|
assert.deepStrictEqual(result[0].endpoints, ["/mcp", "/health"]);
|
|
332
433
|
});
|
|
333
434
|
});
|
|
435
|
+
|
|
436
|
+
describe("filterInvalidCryptoComponents()", () => {
|
|
437
|
+
it("removes algorithm components without OID", () => {
|
|
438
|
+
const components = [
|
|
439
|
+
{
|
|
440
|
+
name: "sha-256",
|
|
441
|
+
type: "cryptographic-asset",
|
|
442
|
+
cryptoProperties: {
|
|
443
|
+
assetType: "algorithm",
|
|
444
|
+
oid: "2.16.840.1.101.3.4.2.1",
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "unknown-algo",
|
|
449
|
+
type: "cryptographic-asset",
|
|
450
|
+
cryptoProperties: { assetType: "algorithm" },
|
|
451
|
+
},
|
|
452
|
+
{ name: "express", type: "library", purl: "pkg:npm/express@4.18.0" },
|
|
453
|
+
];
|
|
454
|
+
const result = filterInvalidCryptoComponents(components);
|
|
455
|
+
assert.strictEqual(result.length, 2);
|
|
456
|
+
assert.strictEqual(result[0].name, "sha-256");
|
|
457
|
+
assert.strictEqual(result[1].name, "express");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("removes crypto components without cryptoProperties", () => {
|
|
461
|
+
const components = [
|
|
462
|
+
{ name: "bad-cert", type: "cryptographic-asset" },
|
|
463
|
+
{
|
|
464
|
+
name: "good-cert",
|
|
465
|
+
type: "cryptographic-asset",
|
|
466
|
+
cryptoProperties: {
|
|
467
|
+
assetType: "certificate",
|
|
468
|
+
algorithmProperties: {
|
|
469
|
+
executionEnvironment: "unknown",
|
|
470
|
+
implementationPlatform: "unknown",
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
];
|
|
475
|
+
const result = filterInvalidCryptoComponents(components);
|
|
476
|
+
assert.strictEqual(result.length, 1);
|
|
477
|
+
assert.strictEqual(result[0].name, "good-cert");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("removes certificate components without algorithmProperties", () => {
|
|
481
|
+
const components = [
|
|
482
|
+
{
|
|
483
|
+
name: "bad-cert",
|
|
484
|
+
type: "cryptographic-asset",
|
|
485
|
+
cryptoProperties: { assetType: "certificate" },
|
|
486
|
+
},
|
|
487
|
+
];
|
|
488
|
+
const result = filterInvalidCryptoComponents(components);
|
|
489
|
+
assert.strictEqual(result.length, 0);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("preserves related-crypto-material components", () => {
|
|
493
|
+
const components = [
|
|
494
|
+
{
|
|
495
|
+
name: "gpg-key",
|
|
496
|
+
type: "cryptographic-asset",
|
|
497
|
+
cryptoProperties: {
|
|
498
|
+
assetType: "related-crypto-material",
|
|
499
|
+
relatedCryptoMaterialProperties: {
|
|
500
|
+
type: "public-key",
|
|
501
|
+
id: "abc",
|
|
502
|
+
state: "active",
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
];
|
|
507
|
+
const result = filterInvalidCryptoComponents(components);
|
|
508
|
+
assert.strictEqual(result.length, 1);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("returns an empty array for empty/falsy input", () => {
|
|
512
|
+
assert.strictEqual(filterInvalidCryptoComponents([]).length, 0);
|
|
513
|
+
assert.deepStrictEqual(filterInvalidCryptoComponents(undefined), []);
|
|
514
|
+
assert.deepStrictEqual(filterInvalidCryptoComponents(null), []);
|
|
515
|
+
});
|
|
516
|
+
});
|
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 {
|