@cyclonedx/cdxgen 12.1.5 → 12.2.0
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 +47 -39
- package/bin/cdxgen.js +175 -96
- package/bin/evinse.js +4 -4
- package/bin/repl.js +1 -1
- package/bin/sign.js +102 -0
- package/bin/validate.js +233 -0
- package/bin/verify.js +69 -28
- package/data/queries.json +1 -1
- package/data/rules/ci-permissions.yaml +186 -0
- package/data/rules/dependency-sources.yaml +123 -0
- package/data/rules/package-integrity.yaml +135 -0
- package/data/rules/vscode-extensions.yaml +228 -0
- package/lib/cli/index.js +327 -372
- package/lib/evinser/db.js +137 -0
- package/lib/{helpers → evinser}/db.poku.js +2 -6
- package/lib/evinser/evinser.js +2 -14
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/bomSigner.poku.js +156 -0
- package/lib/helpers/ciParsers/azurePipelines.js +295 -0
- package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
- package/lib/helpers/ciParsers/circleCi.js +286 -0
- package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
- package/lib/helpers/ciParsers/common.js +24 -0
- package/lib/helpers/ciParsers/githubActions.js +636 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
- package/lib/helpers/ciParsers/gitlabCi.js +213 -0
- package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
- package/lib/helpers/ciParsers/jenkins.js +181 -0
- package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
- package/lib/helpers/depsUtils.js +203 -0
- package/lib/helpers/depsUtils.poku.js +150 -0
- package/lib/helpers/display.js +423 -4
- package/lib/helpers/envcontext.js +18 -3
- package/lib/helpers/formulationParsers.js +351 -0
- package/lib/helpers/logger.js +14 -0
- package/lib/helpers/protobom.js +9 -9
- package/lib/helpers/pythonutils.js +9 -0
- package/lib/helpers/utils.js +681 -406
- package/lib/helpers/utils.poku.js +55 -255
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/versutils.poku.js +315 -0
- package/lib/helpers/vsixutils.js +1061 -0
- package/lib/helpers/vsixutils.poku.js +2247 -0
- package/lib/managers/binary.js +19 -19
- package/lib/managers/docker.js +108 -1
- package/lib/managers/oci.js +10 -0
- package/lib/managers/piptree.js +3 -9
- package/lib/parsers/npmrc.js +17 -13
- package/lib/parsers/npmrc.poku.js +41 -5
- package/lib/server/openapi.yaml +1 -1
- package/lib/server/server.js +40 -11
- package/lib/server/server.poku.js +123 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +197 -0
- package/lib/stages/postgen/auditBom.poku.js +378 -0
- package/lib/stages/postgen/postgen.js +54 -1
- package/lib/stages/postgen/postgen.poku.js +90 -1
- package/lib/stages/postgen/ruleEngine.js +369 -0
- package/lib/stages/pregen/envAudit.js +299 -0
- package/lib/stages/pregen/envAudit.poku.js +572 -0
- package/lib/stages/pregen/pregen.js +12 -8
- package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceEngine.poku.js +168 -0
- package/lib/validator/complianceRules.js +1610 -0
- package/lib/validator/complianceRules.poku.js +328 -0
- package/lib/validator/index.js +222 -0
- package/lib/validator/index.poku.js +144 -0
- package/lib/validator/reporters/annotations.js +121 -0
- package/lib/validator/reporters/console.js +149 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/lib/validator/reporters.poku.js +150 -0
- package/package.json +8 -8
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/validate.d.ts +3 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/helpers/utils.d.ts +0 -1
- package/types/lib/cli/index.d.ts +49 -52
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/db.d.ts +34 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -16
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +27 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/common.d.ts +11 -0
- package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +21 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +111 -11
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +19 -7
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts +50 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
- package/types/lib/helpers/logger.d.ts +15 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -2
- package/types/lib/helpers/pythonutils.d.ts +10 -1
- package/types/lib/helpers/pythonutils.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +532 -128
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +12 -31
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +11 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/npmrc.d.ts +4 -1
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +21 -2
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +20 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +8 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +8 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +66 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +70 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +70 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +31 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +30 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +21 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +16 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/lib/helpers/db.js +0 -162
- package/lib/stages/pregen/env-audit.js +0 -34
- package/lib/stages/pregen/env-audit.poku.js +0 -290
- package/types/helpers/db.d.ts +0 -35
- package/types/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/db.d.ts +0 -35
- package/types/lib/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/validator.d.ts.map +0 -1
- package/types/lib/stages/pregen/env-audit.d.ts +0 -2
- package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
- package/types/managers/binary.d.ts +0 -37
- package/types/managers/binary.d.ts.map +0 -1
- package/types/managers/docker.d.ts +0 -56
- package/types/managers/docker.d.ts.map +0 -1
- package/types/managers/oci.d.ts +0 -2
- package/types/managers/oci.d.ts.map +0 -1
- package/types/managers/piptree.d.ts +0 -2
- package/types/managers/piptree.d.ts.map +0 -1
- package/types/server/server.d.ts +0 -34
- package/types/server/server.d.ts.map +0 -1
- package/types/stages/postgen/annotator.d.ts +0 -27
- package/types/stages/postgen/annotator.d.ts.map +0 -1
- package/types/stages/postgen/postgen.d.ts +0 -51
- package/types/stages/postgen/postgen.d.ts.map +0 -1
- package/types/stages/pregen/pregen.d.ts +0 -59
- package/types/stages/pregen/pregen.d.ts.map +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
class Model {
|
|
2
|
+
constructor(tableName) {
|
|
3
|
+
this.tableName = tableName;
|
|
4
|
+
this.store = new Map();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async init() {
|
|
8
|
+
this.store.clear();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async findByPk(purl) {
|
|
12
|
+
if (this.store.has(purl)) {
|
|
13
|
+
const record = this.store.get(purl);
|
|
14
|
+
let parsedData;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
parsedData = JSON.parse(record.dataStr);
|
|
18
|
+
} catch (_e) {
|
|
19
|
+
parsedData = record.dataStr;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
purl: record.purl,
|
|
24
|
+
data: parsedData,
|
|
25
|
+
createdAt: record.createdAt,
|
|
26
|
+
updatedAt: record.updatedAt,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async findOrCreate(options) {
|
|
34
|
+
const { where, defaults } = options;
|
|
35
|
+
const existing = await this.findByPk(where.purl);
|
|
36
|
+
|
|
37
|
+
if (existing) {
|
|
38
|
+
return [existing, false];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let dataStr;
|
|
42
|
+
if (typeof defaults.data === "string") {
|
|
43
|
+
dataStr = defaults.data;
|
|
44
|
+
} else {
|
|
45
|
+
dataStr = JSON.stringify(defaults.data);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
const searchStr = dataStr.toLowerCase();
|
|
50
|
+
|
|
51
|
+
const record = {
|
|
52
|
+
purl: defaults.purl,
|
|
53
|
+
dataStr: dataStr,
|
|
54
|
+
searchStr: searchStr,
|
|
55
|
+
createdAt: now,
|
|
56
|
+
updatedAt: now,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.store.set(defaults.purl, record);
|
|
60
|
+
|
|
61
|
+
let parsedData;
|
|
62
|
+
try {
|
|
63
|
+
parsedData = JSON.parse(record.dataStr);
|
|
64
|
+
} catch (_e) {
|
|
65
|
+
parsedData = record.dataStr;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const instance = {
|
|
69
|
+
purl: record.purl,
|
|
70
|
+
data: parsedData,
|
|
71
|
+
createdAt: record.createdAt,
|
|
72
|
+
updatedAt: record.updatedAt,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return [instance, true];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async findAll(options) {
|
|
79
|
+
const results = [];
|
|
80
|
+
let searchTerm = null;
|
|
81
|
+
|
|
82
|
+
if (options?.where?.data?.like) {
|
|
83
|
+
searchTerm = options.where.data.like.replace(/%/g, "").toLowerCase();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const record of this.store.values()) {
|
|
87
|
+
let matches = true;
|
|
88
|
+
|
|
89
|
+
if (searchTerm) {
|
|
90
|
+
if (!record.searchStr.includes(searchTerm)) {
|
|
91
|
+
matches = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (matches) {
|
|
96
|
+
let parsedData;
|
|
97
|
+
try {
|
|
98
|
+
parsedData = JSON.parse(record.dataStr);
|
|
99
|
+
} catch (_e) {
|
|
100
|
+
parsedData = record.dataStr;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
results.push({
|
|
104
|
+
purl: record.purl,
|
|
105
|
+
data: parsedData,
|
|
106
|
+
createdAt: record.createdAt,
|
|
107
|
+
updatedAt: record.updatedAt,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const createOrLoad = async () => {
|
|
117
|
+
const Namespaces = new Model("Namespaces");
|
|
118
|
+
const Usages = new Model("Usages");
|
|
119
|
+
const DataFlows = new Model("DataFlows");
|
|
120
|
+
|
|
121
|
+
await Namespaces.init();
|
|
122
|
+
await Usages.init();
|
|
123
|
+
await DataFlows.init();
|
|
124
|
+
|
|
125
|
+
const sequelize = {
|
|
126
|
+
close: () => {
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
sequelize,
|
|
133
|
+
Namespaces,
|
|
134
|
+
Usages,
|
|
135
|
+
DataFlows,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
@@ -2,12 +2,8 @@ import { assert, describe, test } from "poku";
|
|
|
2
2
|
|
|
3
3
|
import { createOrLoad } from "./db.js";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
6
|
-
const { sequelize, Namespaces } = await createOrLoad(
|
|
7
|
-
"test.db",
|
|
8
|
-
":memory:",
|
|
9
|
-
false,
|
|
10
|
-
);
|
|
5
|
+
describe("In-Memory DB Helper Tests", async () => {
|
|
6
|
+
const { sequelize, Namespaces } = await createOrLoad();
|
|
11
7
|
|
|
12
8
|
await test("Model Initialization", () => {
|
|
13
9
|
assert.ok(sequelize, "Database instance should exist");
|
package/lib/evinser/evinser.js
CHANGED
|
@@ -5,7 +5,6 @@ import process from "node:process";
|
|
|
5
5
|
import { PackageURL } from "packageurl-js";
|
|
6
6
|
|
|
7
7
|
import { findCryptoAlgos } from "../helpers/cbomutils.js";
|
|
8
|
-
import * as db from "../helpers/db.js";
|
|
9
8
|
import {
|
|
10
9
|
collectGradleDependencies,
|
|
11
10
|
collectMvnDependencies,
|
|
@@ -18,13 +17,12 @@ import {
|
|
|
18
17
|
getTmpDir,
|
|
19
18
|
PROJECT_TYPE_ALIASES,
|
|
20
19
|
safeExistsSync,
|
|
21
|
-
safeMkdirSync,
|
|
22
20
|
} from "../helpers/utils.js";
|
|
23
21
|
import { postProcess } from "../stages/postgen/postgen.js";
|
|
22
|
+
import { createOrLoad } from "./db.js";
|
|
24
23
|
import { findPurlLocations } from "./scalasem.js";
|
|
25
24
|
import { createSemanticsSlices } from "./swiftsem.js";
|
|
26
25
|
|
|
27
|
-
const DB_NAME = "evinser.db";
|
|
28
26
|
const typePurlsCache = {};
|
|
29
27
|
|
|
30
28
|
/**
|
|
@@ -33,13 +31,6 @@ const typePurlsCache = {};
|
|
|
33
31
|
* @param {Object} options Command line options
|
|
34
32
|
*/
|
|
35
33
|
export async function prepareDB(options) {
|
|
36
|
-
if (!options.dbPath.includes("memory") && !safeExistsSync(options.dbPath)) {
|
|
37
|
-
try {
|
|
38
|
-
safeMkdirSync(options.dbPath, { recursive: true });
|
|
39
|
-
} catch (_e) {
|
|
40
|
-
// ignore
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
34
|
const dirPath = options._[0] || ".";
|
|
44
35
|
const bomJsonFile = options.input;
|
|
45
36
|
if (!safeExistsSync(bomJsonFile)) {
|
|
@@ -61,10 +52,7 @@ export async function prepareDB(options) {
|
|
|
61
52
|
process.exit(0);
|
|
62
53
|
}
|
|
63
54
|
const components = bomJson.components || [];
|
|
64
|
-
const { sequelize, Namespaces, Usages, DataFlows } = await
|
|
65
|
-
DB_NAME,
|
|
66
|
-
options.dbPath,
|
|
67
|
-
);
|
|
55
|
+
const { sequelize, Namespaces, Usages, DataFlows } = await createOrLoad();
|
|
68
56
|
let hasMavenPkgs = false;
|
|
69
57
|
// We need to slice only non-maven packages
|
|
70
58
|
const purlsToSlice = {};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight, deterministic JSON Canonicalizer (similar to RFC 8785).
|
|
5
|
+
* Required by JSF to ensure the signature payload remains identical across systems.
|
|
6
|
+
*
|
|
7
|
+
* @param {any} value - The JSON object/value to canonicalize
|
|
8
|
+
* @returns {string} - Canonicalized JSON string
|
|
9
|
+
*/
|
|
10
|
+
function canonicalize(value) {
|
|
11
|
+
if (value === null || typeof value !== "object") {
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
16
|
+
}
|
|
17
|
+
const keys = Object.keys(value).sort();
|
|
18
|
+
let str = "{";
|
|
19
|
+
for (let i = 0; i < keys.length; i++) {
|
|
20
|
+
if (i > 0) str += ",";
|
|
21
|
+
str += `${JSON.stringify(keys[i])}:${canonicalize(value[keys[i]])}`;
|
|
22
|
+
}
|
|
23
|
+
str += "}";
|
|
24
|
+
return str;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a JSF-compliant signature block.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} payload - The object to sign (e.g., BOM, component)
|
|
31
|
+
* @param {string|Buffer|crypto.KeyObject} privateKey - The signing key
|
|
32
|
+
* @param {string} alg - JSF algorithm identifier
|
|
33
|
+
* @param {Object} [publicKeyJwk] - Optional JWK representation of the public key
|
|
34
|
+
* @param {string} keyId - Key ID
|
|
35
|
+
*
|
|
36
|
+
* @returns {Object} - JSF signature block
|
|
37
|
+
*/
|
|
38
|
+
function createSignatureBlock(
|
|
39
|
+
payload,
|
|
40
|
+
privateKey,
|
|
41
|
+
alg,
|
|
42
|
+
publicKeyJwk = null,
|
|
43
|
+
keyId = null,
|
|
44
|
+
) {
|
|
45
|
+
// Exclude existing signatures from the canonicalized payload as per JSF
|
|
46
|
+
const { signature, ...dataToSign } = payload;
|
|
47
|
+
const canonicalData = canonicalize(dataToSign);
|
|
48
|
+
|
|
49
|
+
let hashAlg;
|
|
50
|
+
const signOptions = { key: privateKey };
|
|
51
|
+
|
|
52
|
+
// Handle HMAC (Symmetric)
|
|
53
|
+
if (alg.startsWith("HS")) {
|
|
54
|
+
const hash = alg.replace("HS", "sha");
|
|
55
|
+
const value = crypto
|
|
56
|
+
.createHmac(hash, privateKey)
|
|
57
|
+
.update(canonicalData, "utf8")
|
|
58
|
+
.digest("base64url");
|
|
59
|
+
const block = { algorithm: alg, value };
|
|
60
|
+
if (publicKeyJwk) {
|
|
61
|
+
block.publicKey = publicKeyJwk;
|
|
62
|
+
}
|
|
63
|
+
if (keyId) {
|
|
64
|
+
block.keyId = keyId;
|
|
65
|
+
}
|
|
66
|
+
return block;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle Asymmetric Algorithms
|
|
70
|
+
if (alg.startsWith("RS")) {
|
|
71
|
+
hashAlg = alg.replace("RS", "SHA");
|
|
72
|
+
} else if (alg.startsWith("PS")) {
|
|
73
|
+
hashAlg = alg.replace("PS", "SHA");
|
|
74
|
+
signOptions.padding = crypto.constants.RSA_PKCS1_PSS_PADDING;
|
|
75
|
+
signOptions.saltLength = crypto.constants.RSA_PSS_SALTLEN_AUTO;
|
|
76
|
+
} else if (alg.startsWith("ES")) {
|
|
77
|
+
hashAlg = alg.replace("ES", "SHA");
|
|
78
|
+
// Standard JWA format requires IEEE P1363 (R || S) instead of ASN.1 DER
|
|
79
|
+
signOptions.dsaEncoding = "ieee-p1363";
|
|
80
|
+
} else if (alg === "Ed25519" || alg === "Ed448") {
|
|
81
|
+
// Native EdDSA algorithms do not require a separate hash algorithm definition
|
|
82
|
+
hashAlg = null;
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Unsupported JSF algorithm: ${alg}`);
|
|
85
|
+
}
|
|
86
|
+
const sigBuffer = crypto.sign(
|
|
87
|
+
hashAlg,
|
|
88
|
+
Buffer.from(canonicalData, "utf8"),
|
|
89
|
+
signOptions,
|
|
90
|
+
);
|
|
91
|
+
const block = { algorithm: alg, value: sigBuffer.toString("base64url") };
|
|
92
|
+
if (publicKeyJwk) {
|
|
93
|
+
block.publicKey = publicKeyJwk;
|
|
94
|
+
}
|
|
95
|
+
if (keyId) {
|
|
96
|
+
block.keyId = keyId;
|
|
97
|
+
}
|
|
98
|
+
return block;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Appends or replaces a signature on a target object based on the configured mode.
|
|
103
|
+
*/
|
|
104
|
+
function addSignature(target, sigBlock, mode) {
|
|
105
|
+
if (!target.signature) {
|
|
106
|
+
target.signature = sigBlock;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (mode === "chain") {
|
|
110
|
+
if (target.signature.chain) {
|
|
111
|
+
target.signature.chain.push(sigBlock);
|
|
112
|
+
} else if (target.signature.signers) {
|
|
113
|
+
throw new Error("Cannot mix signature chains and multi-signers.");
|
|
114
|
+
} else {
|
|
115
|
+
target.signature = { chain: [target.signature, sigBlock] };
|
|
116
|
+
}
|
|
117
|
+
} else if (mode === "signers") {
|
|
118
|
+
if (target.signature.signers) {
|
|
119
|
+
target.signature.signers.push(sigBlock);
|
|
120
|
+
} else if (target.signature.chain) {
|
|
121
|
+
throw new Error("Cannot mix signature chains and multi-signers.");
|
|
122
|
+
} else {
|
|
123
|
+
target.signature = { signers: [target.signature, sigBlock] };
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
target.signature = sigBlock;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Recursively applies signatures to the BOM and its granular components.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} bomJson - CycloneDX BOM Object
|
|
134
|
+
* @param {Object} options - Signing options { privateKey, algorithm, mode, ... }
|
|
135
|
+
* @returns {Object} - Signed BOM Object
|
|
136
|
+
*/
|
|
137
|
+
export function signBom(bomJson, options = {}) {
|
|
138
|
+
const {
|
|
139
|
+
privateKey,
|
|
140
|
+
algorithm = "RS512",
|
|
141
|
+
publicKeyJwk = null,
|
|
142
|
+
keyId = null,
|
|
143
|
+
mode = "replace", // Supports: 'replace', 'chain', 'signers'
|
|
144
|
+
signComponents = true,
|
|
145
|
+
signServices = true,
|
|
146
|
+
signAnnotations = true,
|
|
147
|
+
} = options;
|
|
148
|
+
|
|
149
|
+
if (!privateKey) {
|
|
150
|
+
throw new Error("privateKey is required for signing");
|
|
151
|
+
}
|
|
152
|
+
if (signComponents && Array.isArray(bomJson.components)) {
|
|
153
|
+
for (const comp of bomJson.components) {
|
|
154
|
+
addSignature(
|
|
155
|
+
comp,
|
|
156
|
+
createSignatureBlock(comp, privateKey, algorithm, publicKeyJwk, keyId),
|
|
157
|
+
mode,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (signServices && Array.isArray(bomJson.services)) {
|
|
162
|
+
for (const svc of bomJson.services) {
|
|
163
|
+
addSignature(
|
|
164
|
+
svc,
|
|
165
|
+
createSignatureBlock(svc, privateKey, algorithm, publicKeyJwk, keyId),
|
|
166
|
+
mode,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (signAnnotations && Array.isArray(bomJson.annotations)) {
|
|
171
|
+
for (const ann of bomJson.annotations) {
|
|
172
|
+
addSignature(
|
|
173
|
+
ann,
|
|
174
|
+
createSignatureBlock(ann, privateKey, algorithm, publicKeyJwk, keyId),
|
|
175
|
+
mode,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
addSignature(
|
|
180
|
+
bomJson,
|
|
181
|
+
createSignatureBlock(bomJson, privateKey, algorithm, publicKeyJwk, keyId),
|
|
182
|
+
mode,
|
|
183
|
+
);
|
|
184
|
+
return bomJson;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validates a single JSF signature block against the payload.
|
|
189
|
+
*
|
|
190
|
+
* @param {Object} payload - The payload to verify
|
|
191
|
+
* @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
|
|
192
|
+
* @param {Object} sigBlock Signature
|
|
193
|
+
*
|
|
194
|
+
* @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
|
|
195
|
+
*/
|
|
196
|
+
function verifySignatureBlock(payload, publicKey, sigBlock) {
|
|
197
|
+
const { signature, ...dataToVerify } = payload;
|
|
198
|
+
const canonicalData = canonicalize(dataToVerify);
|
|
199
|
+
|
|
200
|
+
const { algorithm: alg, value } = sigBlock;
|
|
201
|
+
|
|
202
|
+
if (alg.startsWith("HS")) {
|
|
203
|
+
const hash = alg.replace("HS", "sha");
|
|
204
|
+
const expected = crypto
|
|
205
|
+
.createHmac(hash, publicKey)
|
|
206
|
+
.update(canonicalData, "utf8")
|
|
207
|
+
.digest("base64url");
|
|
208
|
+
const isValid = expected === value;
|
|
209
|
+
return isValid ? sigBlock : false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const verifyOptions = { key: publicKey };
|
|
213
|
+
let hashAlg;
|
|
214
|
+
|
|
215
|
+
if (alg.startsWith("RS")) {
|
|
216
|
+
hashAlg = alg.replace("RS", "SHA");
|
|
217
|
+
} else if (alg.startsWith("PS")) {
|
|
218
|
+
hashAlg = alg.replace("PS", "SHA");
|
|
219
|
+
verifyOptions.padding = crypto.constants.RSA_PKCS1_PSS_PADDING;
|
|
220
|
+
verifyOptions.saltLength = crypto.constants.RSA_PSS_SALTLEN_AUTO;
|
|
221
|
+
} else if (alg.startsWith("ES")) {
|
|
222
|
+
hashAlg = alg.replace("ES", "SHA");
|
|
223
|
+
verifyOptions.dsaEncoding = "ieee-p1363";
|
|
224
|
+
} else if (alg === "Ed25519" || alg === "Ed448") {
|
|
225
|
+
hashAlg = null;
|
|
226
|
+
} else {
|
|
227
|
+
console.warn(`${alg} is unknown.`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const isValid = crypto.verify(
|
|
232
|
+
hashAlg,
|
|
233
|
+
Buffer.from(canonicalData, "utf8"),
|
|
234
|
+
verifyOptions,
|
|
235
|
+
Buffer.from(value, "base64url"),
|
|
236
|
+
);
|
|
237
|
+
return isValid ? sigBlock : false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Verifies the integrity of a specific element node (e.g., BOM root, Component, Service, Annotation).
|
|
242
|
+
* Resolves standard JSF signatures, multisignature (signers), and chains.
|
|
243
|
+
*
|
|
244
|
+
* @param {Object} node - The BOM or granular object to verify
|
|
245
|
+
* @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
|
|
246
|
+
* @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
|
|
247
|
+
*/
|
|
248
|
+
export function verifyNode(node, publicKey) {
|
|
249
|
+
if (!node?.signature) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const sigTarget = node.signature;
|
|
253
|
+
if (sigTarget.signers) {
|
|
254
|
+
for (const sig of sigTarget.signers) {
|
|
255
|
+
const match = verifySignatureBlock(node, publicKey, sig);
|
|
256
|
+
if (match) {
|
|
257
|
+
return match;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (sigTarget.chain) {
|
|
263
|
+
for (const sig of sigTarget.chain) {
|
|
264
|
+
const match = verifySignatureBlock(node, publicKey, sig);
|
|
265
|
+
if (match) {
|
|
266
|
+
return match;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
return verifySignatureBlock(node, publicKey, sigTarget);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Verifies the integrity of a BOM's top-level signature, as well as nested components, services, and annotations.
|
|
276
|
+
* Returns true only if the root signature is valid AND all signed nested elements are valid.
|
|
277
|
+
*
|
|
278
|
+
* @param {Object} bom - CycloneDX BOM Object
|
|
279
|
+
* @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
|
|
280
|
+
* @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
|
|
281
|
+
*/
|
|
282
|
+
export function verifyBom(bom, publicKey) {
|
|
283
|
+
if (!bom?.signature) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const rootMatch = verifyNode(bom, publicKey);
|
|
287
|
+
if (!rootMatch) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(bom.components)) {
|
|
291
|
+
for (const comp of bom.components) {
|
|
292
|
+
if (comp.signature && !verifyNode(comp, publicKey)) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (Array.isArray(bom.services)) {
|
|
298
|
+
for (const svc of bom.services) {
|
|
299
|
+
if (svc.signature && !verifyNode(svc, publicKey)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(bom.annotations)) {
|
|
305
|
+
for (const ann of bom.annotations) {
|
|
306
|
+
if (ann.signature && !verifyNode(ann, publicKey)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return rootMatch;
|
|
312
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
|
|
4
|
+
import { describe, it } from "poku";
|
|
5
|
+
|
|
6
|
+
import { signBom, verifyBom, verifyNode } from "./bomSigner.js";
|
|
7
|
+
|
|
8
|
+
const rsaKeys = crypto.generateKeyPairSync("rsa", {
|
|
9
|
+
modulusLength: 2048,
|
|
10
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
11
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const ecKeys = crypto.generateKeyPairSync("ec", {
|
|
15
|
+
namedCurve: "prime256v1",
|
|
16
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
17
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const generateMockBom = () => ({
|
|
21
|
+
bomFormat: "CycloneDX",
|
|
22
|
+
specVersion: "1.6",
|
|
23
|
+
components: [{ type: "library", name: "cdxgen", version: "1.0.0" }],
|
|
24
|
+
services: [{ name: "acme-service", endpoints: ["https://appthreat.com"] }],
|
|
25
|
+
annotations: [{ subject: "ref-1", annotator: { name: "System" } }],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("bomSigner Tests", async () => {
|
|
29
|
+
it("Test basic RS512 Signature & Verification", () => {
|
|
30
|
+
const bomRsa = generateMockBom();
|
|
31
|
+
const signedRsa = signBom(bomRsa, {
|
|
32
|
+
privateKey: rsaKeys.privateKey,
|
|
33
|
+
algorithm: "RS512",
|
|
34
|
+
});
|
|
35
|
+
assert.ok(signedRsa.signature, "Root BOM should be signed");
|
|
36
|
+
assert.strictEqual(signedRsa.signature.algorithm, "RS512");
|
|
37
|
+
assert.ok(
|
|
38
|
+
signedRsa.components[0].signature,
|
|
39
|
+
"Granular component should be signed",
|
|
40
|
+
);
|
|
41
|
+
assert.ok(
|
|
42
|
+
signedRsa.services[0].signature,
|
|
43
|
+
"Granular service should be signed",
|
|
44
|
+
);
|
|
45
|
+
assert.ok(
|
|
46
|
+
signedRsa.annotations[0].signature,
|
|
47
|
+
"Granular annotation should be signed",
|
|
48
|
+
);
|
|
49
|
+
assert.ok(verifyBom(signedRsa, rsaKeys.publicKey));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("Test ECDSA (ES256) Signature & Verification (JWA IEEE P1363 Format Compliance)", () => {
|
|
53
|
+
const bomEc = generateMockBom();
|
|
54
|
+
const signedEc = signBom(bomEc, {
|
|
55
|
+
privateKey: ecKeys.privateKey,
|
|
56
|
+
algorithm: "ES256",
|
|
57
|
+
});
|
|
58
|
+
assert.strictEqual(signedEc.signature.algorithm, "ES256");
|
|
59
|
+
assert.ok(verifyBom(signedEc, ecKeys.publicKey));
|
|
60
|
+
const signedRsa = signBom(bomEc, {
|
|
61
|
+
privateKey: rsaKeys.privateKey,
|
|
62
|
+
algorithm: "RS512",
|
|
63
|
+
});
|
|
64
|
+
assert.strictEqual(
|
|
65
|
+
verifyBom(signedRsa, ecKeys.publicKey),
|
|
66
|
+
false,
|
|
67
|
+
"Verification must fail with the wrong public key",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("Test Multi-Signature Support (`signers`)", () => {
|
|
72
|
+
const bomMulti = generateMockBom();
|
|
73
|
+
|
|
74
|
+
// 1st Pass: First signer signs the whole BOM including inner elements
|
|
75
|
+
signBom(bomMulti, {
|
|
76
|
+
privateKey: rsaKeys.privateKey,
|
|
77
|
+
algorithm: "RS512",
|
|
78
|
+
mode: "signers",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.ok(
|
|
82
|
+
bomMulti.signature.algorithm,
|
|
83
|
+
"Initial signature block takes root format",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 2nd Pass: Second signer ONLY co-signs the root BOM.
|
|
87
|
+
signBom(bomMulti, {
|
|
88
|
+
privateKey: ecKeys.privateKey,
|
|
89
|
+
algorithm: "ES256",
|
|
90
|
+
mode: "signers",
|
|
91
|
+
signComponents: false,
|
|
92
|
+
signServices: false,
|
|
93
|
+
signAnnotations: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
assert.ok(
|
|
97
|
+
Array.isArray(bomMulti.signature.signers),
|
|
98
|
+
"Signature should be converted to signers array",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
assert.strictEqual(
|
|
102
|
+
bomMulti.signature.signers.length,
|
|
103
|
+
2,
|
|
104
|
+
"Should contain exactly two signers",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// RSA key signed EVERYTHING (root + components), so full deep verifyBom passes
|
|
108
|
+
assert.ok(verifyBom(bomMulti, rsaKeys.publicKey));
|
|
109
|
+
|
|
110
|
+
// EC key ONLY signed the root.
|
|
111
|
+
assert.ok(verifyNode(bomMulti, ecKeys.publicKey));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("Test Signature Chain Support (`chain`)", () => {
|
|
115
|
+
const bomChain = generateMockBom();
|
|
116
|
+
|
|
117
|
+
signBom(bomChain, {
|
|
118
|
+
privateKey: rsaKeys.privateKey,
|
|
119
|
+
algorithm: "RS512",
|
|
120
|
+
mode: "chain",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
signBom(bomChain, {
|
|
124
|
+
privateKey: ecKeys.privateKey,
|
|
125
|
+
algorithm: "ES256",
|
|
126
|
+
mode: "chain",
|
|
127
|
+
signComponents: false,
|
|
128
|
+
signServices: false,
|
|
129
|
+
signAnnotations: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.ok(
|
|
133
|
+
Array.isArray(bomChain.signature.chain),
|
|
134
|
+
"Signature should be converted to chain array",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(bomChain.signature.chain.length, 2);
|
|
138
|
+
|
|
139
|
+
// RSA key signed everything, verifyBom works
|
|
140
|
+
assert.ok(verifyBom(bomChain, rsaKeys.publicKey));
|
|
141
|
+
|
|
142
|
+
// EC key only chained the root, verifyNode strictly checks the root
|
|
143
|
+
assert.ok(verifyNode(bomChain, ecKeys.publicKey));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("Test HMAC Symmetric Signature (HS256)", () => {
|
|
147
|
+
const symmetricKey = crypto.randomBytes(32).toString("hex");
|
|
148
|
+
const bomHmac = generateMockBom();
|
|
149
|
+
const signedHmac = signBom(bomHmac, {
|
|
150
|
+
privateKey: symmetricKey,
|
|
151
|
+
algorithm: "HS256",
|
|
152
|
+
});
|
|
153
|
+
assert.strictEqual(signedHmac.signature.algorithm, "HS256");
|
|
154
|
+
assert.ok(verifyBom(signedHmac, symmetricKey));
|
|
155
|
+
});
|
|
156
|
+
});
|