@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
package/lib/helpers/bomUtils.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
const SPDX_CONTEXT_PREFIX = "https://spdx.org/rdf/";
|
|
2
2
|
const CYCLONEDX_FORMAT = "CycloneDX";
|
|
3
|
+
const LEGACY_CYCLONEDX_ROOT_KEY = "bomFormat";
|
|
4
|
+
const MODERN_CYCLONEDX_ROOT_KEY = "specFormat";
|
|
3
5
|
const BOM_FORMAT_CYCLONEDX = "cyclonedx";
|
|
4
6
|
const BOM_FORMAT_SPDX = "spdx";
|
|
5
7
|
const BOM_FORMAT_UNKNOWN = "unknown";
|
|
8
|
+
const CYCLONEDX_SPEC_VERSION_PATTERN = /^(\d+)(?:\.(\d+))?$/u;
|
|
9
|
+
const CYCLONEDX_FORMAT_KEYS = new Set([
|
|
10
|
+
LEGACY_CYCLONEDX_ROOT_KEY,
|
|
11
|
+
MODERN_CYCLONEDX_ROOT_KEY,
|
|
12
|
+
"specVersion",
|
|
13
|
+
]);
|
|
6
14
|
|
|
7
15
|
export const isSpdxJsonLd = (bomJson) =>
|
|
8
16
|
Boolean(
|
|
@@ -11,8 +19,154 @@ export const isSpdxJsonLd = (bomJson) =>
|
|
|
11
19
|
bomJson["@graph"].some((element) => element?.type === "SpdxDocument"),
|
|
12
20
|
);
|
|
13
21
|
|
|
22
|
+
const parseCycloneDxSpecVersion = (specVersion) => {
|
|
23
|
+
const match = `${specVersion ?? ""}`
|
|
24
|
+
.trim()
|
|
25
|
+
.match(CYCLONEDX_SPEC_VERSION_PATTERN);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
major: Number.parseInt(match[1], 10),
|
|
31
|
+
minor: Number.parseInt(match[2] || "0", 10),
|
|
32
|
+
minorText: match[2] || "0",
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const normalizeCycloneDxSpecVersion = (specVersion) => {
|
|
37
|
+
const parsed = parseCycloneDxSpecVersion(specVersion);
|
|
38
|
+
if (!parsed) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return Number(`${parsed.major}.${parsed.minor}`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const toCycloneDxSpecVersionString = (specVersion) => {
|
|
45
|
+
const parsed = parseCycloneDxSpecVersion(specVersion);
|
|
46
|
+
if (!parsed) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (typeof specVersion === "string" && parsed.minorText !== "0") {
|
|
50
|
+
return `${parsed.major}.${parsed.minorText}`;
|
|
51
|
+
}
|
|
52
|
+
return `${parsed.major}.${parsed.minor}`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const isCycloneDxSpecVersionAtLeast = (specVersion, minimumVersion) => {
|
|
56
|
+
const parsedSpecVersion = parseCycloneDxSpecVersion(specVersion);
|
|
57
|
+
const parsedMinimumVersion = parseCycloneDxSpecVersion(minimumVersion);
|
|
58
|
+
if (!parsedSpecVersion || !parsedMinimumVersion) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (parsedSpecVersion.major !== parsedMinimumVersion.major) {
|
|
62
|
+
return parsedSpecVersion.major > parsedMinimumVersion.major;
|
|
63
|
+
}
|
|
64
|
+
return parsedSpecVersion.minor >= parsedMinimumVersion.minor;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const isCycloneDx20SpecVersion = (specVersion) =>
|
|
68
|
+
isCycloneDxSpecVersionAtLeast(specVersion, 2);
|
|
69
|
+
|
|
70
|
+
export const getCycloneDxRootFormatKey = (specVersionOrBom) => {
|
|
71
|
+
const specVersion =
|
|
72
|
+
specVersionOrBom && typeof specVersionOrBom === "object"
|
|
73
|
+
? specVersionOrBom.specVersion
|
|
74
|
+
: specVersionOrBom;
|
|
75
|
+
return isCycloneDx20SpecVersion(specVersion)
|
|
76
|
+
? MODERN_CYCLONEDX_ROOT_KEY
|
|
77
|
+
: LEGACY_CYCLONEDX_ROOT_KEY;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const getCycloneDxFormat = (bomJson) =>
|
|
81
|
+
bomJson?.specFormat || bomJson?.bomFormat;
|
|
82
|
+
|
|
83
|
+
export const hasCycloneDxFormat = (bomJson) =>
|
|
84
|
+
getCycloneDxFormat(bomJson) === CYCLONEDX_FORMAT;
|
|
85
|
+
|
|
14
86
|
export const isCycloneDxBom = (bomJson) =>
|
|
15
|
-
bomJson
|
|
87
|
+
hasCycloneDxFormat(bomJson) &&
|
|
88
|
+
normalizeCycloneDxSpecVersion(bomJson?.specVersion) !== undefined;
|
|
89
|
+
|
|
90
|
+
const rewriteCycloneDxRootFields = (
|
|
91
|
+
bomJson,
|
|
92
|
+
rootKey,
|
|
93
|
+
specVersion,
|
|
94
|
+
preserveLegacyBomFormat,
|
|
95
|
+
) => {
|
|
96
|
+
const remainingEntries = Object.entries(bomJson).filter(
|
|
97
|
+
([key]) => !CYCLONEDX_FORMAT_KEYS.has(key),
|
|
98
|
+
);
|
|
99
|
+
for (const key of Object.keys(bomJson)) {
|
|
100
|
+
delete bomJson[key];
|
|
101
|
+
}
|
|
102
|
+
if (rootKey === LEGACY_CYCLONEDX_ROOT_KEY) {
|
|
103
|
+
bomJson.bomFormat = CYCLONEDX_FORMAT;
|
|
104
|
+
if (specVersion !== undefined) {
|
|
105
|
+
bomJson.specVersion = specVersion;
|
|
106
|
+
}
|
|
107
|
+
} else if (preserveLegacyBomFormat) {
|
|
108
|
+
bomJson.bomFormat = CYCLONEDX_FORMAT;
|
|
109
|
+
if (specVersion !== undefined) {
|
|
110
|
+
bomJson.specVersion = specVersion;
|
|
111
|
+
}
|
|
112
|
+
bomJson.specFormat = CYCLONEDX_FORMAT;
|
|
113
|
+
} else {
|
|
114
|
+
bomJson.specFormat = CYCLONEDX_FORMAT;
|
|
115
|
+
if (specVersion !== undefined) {
|
|
116
|
+
bomJson.specVersion = specVersion;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const [key, value] of remainingEntries) {
|
|
120
|
+
bomJson[key] = value;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Mutates a CycloneDX BOM object so the appropriate root format key is present
|
|
126
|
+
* for the requested spec version, while preserving conventional serialized
|
|
127
|
+
* root-key ordering (`bomFormat`/`specFormat` and `specVersion` first). Only the currently
|
|
128
|
+
* supported CycloneDX major.minor version shape is accepted; multi-component
|
|
129
|
+
* future versions such as `2.0.1` intentionally return `undefined` from the
|
|
130
|
+
* normalizer rather than being silently truncated.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} bomJson BOM JSON object to mutate.
|
|
133
|
+
* @param {string|number} specVersion Desired CycloneDX spec version.
|
|
134
|
+
* @param {object} options Root-key compatibility options.
|
|
135
|
+
* @returns {object} The same `bomJson` object, after in-place mutation.
|
|
136
|
+
*/
|
|
137
|
+
export const setCycloneDxFormat = (
|
|
138
|
+
bomJson,
|
|
139
|
+
specVersion,
|
|
140
|
+
{ preserveLegacyBomFormat = false } = {},
|
|
141
|
+
) => {
|
|
142
|
+
if (!bomJson || typeof bomJson !== "object" || Array.isArray(bomJson)) {
|
|
143
|
+
return bomJson;
|
|
144
|
+
}
|
|
145
|
+
const resolvedSpecVersion =
|
|
146
|
+
toCycloneDxSpecVersionString(specVersion ?? bomJson.specVersion) ||
|
|
147
|
+
bomJson.specVersion;
|
|
148
|
+
if (resolvedSpecVersion !== undefined) {
|
|
149
|
+
bomJson.specVersion = resolvedSpecVersion;
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
getCycloneDxRootFormatKey(resolvedSpecVersion) === MODERN_CYCLONEDX_ROOT_KEY
|
|
153
|
+
) {
|
|
154
|
+
rewriteCycloneDxRootFields(
|
|
155
|
+
bomJson,
|
|
156
|
+
MODERN_CYCLONEDX_ROOT_KEY,
|
|
157
|
+
resolvedSpecVersion,
|
|
158
|
+
preserveLegacyBomFormat,
|
|
159
|
+
);
|
|
160
|
+
return bomJson;
|
|
161
|
+
}
|
|
162
|
+
rewriteCycloneDxRootFields(
|
|
163
|
+
bomJson,
|
|
164
|
+
LEGACY_CYCLONEDX_ROOT_KEY,
|
|
165
|
+
resolvedSpecVersion,
|
|
166
|
+
false,
|
|
167
|
+
);
|
|
168
|
+
return bomJson;
|
|
169
|
+
};
|
|
16
170
|
|
|
17
171
|
export const detectBomFormat = (bomJson) => {
|
|
18
172
|
if (isCycloneDxBom(bomJson)) {
|
|
@@ -2,9 +2,16 @@ import { assert, describe, it } from "poku";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
detectBomFormat,
|
|
5
|
+
getCycloneDxFormat,
|
|
6
|
+
getCycloneDxRootFormatKey,
|
|
5
7
|
getNonCycloneDxErrorMessage,
|
|
8
|
+
isCycloneDx20SpecVersion,
|
|
6
9
|
isCycloneDxBom,
|
|
10
|
+
isCycloneDxSpecVersionAtLeast,
|
|
7
11
|
isSpdxJsonLd,
|
|
12
|
+
normalizeCycloneDxSpecVersion,
|
|
13
|
+
setCycloneDxFormat,
|
|
14
|
+
toCycloneDxSpecVersionString,
|
|
8
15
|
} from "./bomUtils.js";
|
|
9
16
|
|
|
10
17
|
const sampleSpdx = {
|
|
@@ -13,7 +20,7 @@ const sampleSpdx = {
|
|
|
13
20
|
};
|
|
14
21
|
|
|
15
22
|
describe("bomUtils", () => {
|
|
16
|
-
it("detects CycloneDX documents", () => {
|
|
23
|
+
it("detects CycloneDX documents across root format styles", () => {
|
|
17
24
|
assert.strictEqual(
|
|
18
25
|
isCycloneDxBom({
|
|
19
26
|
bomFormat: "CycloneDX",
|
|
@@ -21,6 +28,13 @@ describe("bomUtils", () => {
|
|
|
21
28
|
}),
|
|
22
29
|
true,
|
|
23
30
|
);
|
|
31
|
+
assert.strictEqual(
|
|
32
|
+
isCycloneDxBom({
|
|
33
|
+
specFormat: "CycloneDX",
|
|
34
|
+
specVersion: "2.0",
|
|
35
|
+
}),
|
|
36
|
+
true,
|
|
37
|
+
);
|
|
24
38
|
assert.strictEqual(isCycloneDxBom(sampleSpdx), false);
|
|
25
39
|
});
|
|
26
40
|
|
|
@@ -35,6 +49,10 @@ describe("bomUtils", () => {
|
|
|
35
49
|
detectBomFormat({ bomFormat: "CycloneDX", specVersion: "1.6" }),
|
|
36
50
|
"cyclonedx",
|
|
37
51
|
);
|
|
52
|
+
assert.strictEqual(
|
|
53
|
+
detectBomFormat({ specFormat: "CycloneDX", specVersion: "2.0" }),
|
|
54
|
+
"cyclonedx",
|
|
55
|
+
);
|
|
38
56
|
assert.strictEqual(detectBomFormat({ foo: "bar" }), "unknown");
|
|
39
57
|
});
|
|
40
58
|
|
|
@@ -48,4 +66,64 @@ describe("bomUtils", () => {
|
|
|
48
66
|
"cdx-sign expects a CycloneDX JSON BOM.",
|
|
49
67
|
);
|
|
50
68
|
});
|
|
69
|
+
|
|
70
|
+
it("normalizes CycloneDX spec versions and capability checks", () => {
|
|
71
|
+
assert.strictEqual(normalizeCycloneDxSpecVersion("2.0"), 2);
|
|
72
|
+
assert.strictEqual(normalizeCycloneDxSpecVersion(undefined), undefined);
|
|
73
|
+
assert.strictEqual(normalizeCycloneDxSpecVersion("2.0.1"), undefined);
|
|
74
|
+
assert.strictEqual(toCycloneDxSpecVersionString(2), "2.0");
|
|
75
|
+
assert.strictEqual(toCycloneDxSpecVersionString("1.10"), "1.10");
|
|
76
|
+
assert.strictEqual(toCycloneDxSpecVersionString("2.0.1"), undefined);
|
|
77
|
+
assert.strictEqual(isCycloneDx20SpecVersion("2.0"), true);
|
|
78
|
+
assert.strictEqual(isCycloneDx20SpecVersion("1.7"), false);
|
|
79
|
+
assert.strictEqual(isCycloneDxSpecVersionAtLeast("2.0", 1.7), true);
|
|
80
|
+
assert.strictEqual(isCycloneDxSpecVersionAtLeast("1.10", "1.7"), true);
|
|
81
|
+
assert.strictEqual(isCycloneDxSpecVersionAtLeast(undefined, 1.7), false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("selects and writes the correct CycloneDX root format key", () => {
|
|
85
|
+
assert.strictEqual(getCycloneDxRootFormatKey("1.7"), "bomFormat");
|
|
86
|
+
assert.strictEqual(getCycloneDxRootFormatKey("2.0"), "specFormat");
|
|
87
|
+
|
|
88
|
+
const bom17 = setCycloneDxFormat(
|
|
89
|
+
{ name: "demo", specVersion: "1.7" },
|
|
90
|
+
"1.7",
|
|
91
|
+
);
|
|
92
|
+
assert.strictEqual(bom17.bomFormat, "CycloneDX");
|
|
93
|
+
assert.strictEqual(bom17.specFormat, undefined);
|
|
94
|
+
assert.strictEqual(getCycloneDxFormat(bom17), "CycloneDX");
|
|
95
|
+
assert.deepStrictEqual(Object.keys(bom17), [
|
|
96
|
+
"bomFormat",
|
|
97
|
+
"specVersion",
|
|
98
|
+
"name",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const bom20Input = { name: "demo", specVersion: 2 };
|
|
102
|
+
const bom20 = setCycloneDxFormat(bom20Input, 2);
|
|
103
|
+
assert.strictEqual(bom20, bom20Input);
|
|
104
|
+
assert.strictEqual(bom20.specFormat, "CycloneDX");
|
|
105
|
+
assert.strictEqual(bom20.bomFormat, undefined);
|
|
106
|
+
assert.strictEqual(bom20.specVersion, "2.0");
|
|
107
|
+
assert.deepStrictEqual(Object.keys(bom20), [
|
|
108
|
+
"specFormat",
|
|
109
|
+
"specVersion",
|
|
110
|
+
"name",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const internalBom20 = setCycloneDxFormat(
|
|
114
|
+
{ name: "demo", specVersion: 2 },
|
|
115
|
+
2,
|
|
116
|
+
{
|
|
117
|
+
preserveLegacyBomFormat: true,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
assert.strictEqual(internalBom20.specFormat, "CycloneDX");
|
|
121
|
+
assert.strictEqual(internalBom20.bomFormat, "CycloneDX");
|
|
122
|
+
assert.deepStrictEqual(Object.keys(internalBom20), [
|
|
123
|
+
"bomFormat",
|
|
124
|
+
"specVersion",
|
|
125
|
+
"specFormat",
|
|
126
|
+
"name",
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
51
129
|
});
|
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");
|