@cyclonedx/cdxgen 12.3.3 → 12.4.1
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 +69 -25
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +270 -127
- package/bin/convert.js +34 -15
- package/bin/hbom.js +495 -0
- package/bin/repl.js +592 -37
- package/bin/validate.js +31 -4
- package/bin/verify.js +18 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/queries-darwin.json +12 -1
- package/data/queries-win.json +7 -1
- package/data/queries.json +39 -2
- package/data/rules/ai-agent-governance.yaml +16 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/chrome-extensions.yaml +8 -0
- package/data/rules/ci-permissions.yaml +42 -18
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +11 -0
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +14 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +210 -8
- package/lib/audit/index.poku.js +332 -0
- package/lib/audit/reporters.js +222 -0
- package/lib/audit/targets.js +146 -1
- package/lib/audit/targets.poku.js +186 -0
- package/lib/cli/asar.poku.js +328 -0
- package/lib/cli/index.js +527 -99
- package/lib/cli/index.poku.js +1469 -212
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/analyzer.js +1406 -29
- package/lib/helpers/analyzer.poku.js +342 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/bomUtils.js +155 -1
- package/lib/helpers/bomUtils.poku.js +79 -1
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/display.js +291 -1
- package/lib/helpers/display.poku.js +149 -0
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +350 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/protobom.js +209 -45
- package/lib/helpers/protobom.poku.js +183 -5
- package/lib/helpers/protobomLoader.js +43 -0
- package/lib/helpers/protobomLoader.poku.js +31 -0
- package/lib/helpers/remote/dependency-track.js +36 -3
- package/lib/helpers/remote/dependency-track.poku.js +44 -0
- package/lib/helpers/source.js +24 -0
- package/lib/helpers/source.poku.js +32 -0
- package/lib/helpers/utils.js +1438 -93
- package/lib/helpers/utils.poku.js +846 -4
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2293 -353
- package/lib/managers/binary.poku.js +1699 -1
- package/lib/managers/docker.js +201 -79
- package/lib/managers/docker.poku.js +337 -12
- package/lib/server/server.js +4 -28
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +1366 -31
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +406 -8
- package/lib/stages/postgen/postgen.poku.js +484 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- 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 +23 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts +1 -1
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/helpers/hbomLoader.d.ts +7 -0
- package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
- package/types/lib/helpers/hostTopology.d.ts +12 -0
- package/types/lib/helpers/hostTopology.d.ts.map +1 -0
- package/types/lib/helpers/inventoryStats.d.ts +11 -0
- package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -1
- package/types/lib/helpers/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +5 -4
- 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/remote/dependency-track.d.ts +10 -3
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +45 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -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/auditBom.d.ts +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -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/stages/pregen/envAudit.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/data/spdx-model-v3.0.1.jsonld +0 -15999
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as nodeFs from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import esmock from "esmock";
|
|
14
|
+
import { assert, describe, it } from "poku";
|
|
15
|
+
import sinon from "sinon";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createAsarFixture,
|
|
19
|
+
writeElectronAsarIntegrityPlist,
|
|
20
|
+
} from "../../test/helpers/asar-fixture-builder.js";
|
|
21
|
+
import {
|
|
22
|
+
cleanupAsarTempDir,
|
|
23
|
+
extractAsarToTempDir,
|
|
24
|
+
listAsarEntries,
|
|
25
|
+
parseAsarArchive,
|
|
26
|
+
readAsarArchiveHeaderSync,
|
|
27
|
+
rewriteExtractedArchivePaths,
|
|
28
|
+
} from "./asarutils.js";
|
|
29
|
+
import { safeRmSync } from "./utils.js";
|
|
30
|
+
|
|
31
|
+
const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-asar-poku-"));
|
|
32
|
+
|
|
33
|
+
function align4(value) {
|
|
34
|
+
return value + ((4 - (value % 4)) % 4);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeStringPickle(value) {
|
|
38
|
+
const valueBuffer = Buffer.from(value, "utf8");
|
|
39
|
+
const alignedStringLength = align4(valueBuffer.length);
|
|
40
|
+
const payloadLength = 4 + alignedStringLength;
|
|
41
|
+
const buffer = Buffer.alloc(4 + payloadLength);
|
|
42
|
+
buffer.writeUInt32LE(payloadLength, 0);
|
|
43
|
+
buffer.writeInt32LE(valueBuffer.length, 4);
|
|
44
|
+
valueBuffer.copy(buffer, 8);
|
|
45
|
+
return buffer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeSizePickle(value) {
|
|
49
|
+
const buffer = Buffer.alloc(8);
|
|
50
|
+
buffer.writeUInt32LE(4, 0);
|
|
51
|
+
buffer.writeUInt32LE(value, 4);
|
|
52
|
+
return buffer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function rewriteArchiveHeaderSync(archivePath, transformHeader) {
|
|
56
|
+
const archiveBuffer = readFileSync(archivePath);
|
|
57
|
+
const headerPickleSize = archiveBuffer.readUInt32LE(4);
|
|
58
|
+
const headerBuffer = archiveBuffer.subarray(8, 8 + headerPickleSize);
|
|
59
|
+
const headerStringLength = headerBuffer.readInt32LE(4);
|
|
60
|
+
const headerString = headerBuffer.toString("utf8", 8, 8 + headerStringLength);
|
|
61
|
+
const nextHeader = transformHeader(JSON.parse(headerString));
|
|
62
|
+
const nextHeaderPickle = makeStringPickle(JSON.stringify(nextHeader));
|
|
63
|
+
writeFileSync(
|
|
64
|
+
archivePath,
|
|
65
|
+
Buffer.concat([
|
|
66
|
+
makeSizePickle(nextHeaderPickle.length),
|
|
67
|
+
nextHeaderPickle,
|
|
68
|
+
archiveBuffer.subarray(8 + headerPickleSize),
|
|
69
|
+
]),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
process.on("exit", () => {
|
|
74
|
+
safeRmSync(baseTempDir, { force: true, recursive: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("extractAsarToTempDir()", () => {
|
|
78
|
+
it("returns undefined when dry-run blocks ASAR extraction", async () => {
|
|
79
|
+
const safeExtractArchive = sinon.stub().resolves(false);
|
|
80
|
+
const { extractAsarToTempDir: extractAsarToTempDirMocked } = await esmock(
|
|
81
|
+
"./asarutils.js",
|
|
82
|
+
{
|
|
83
|
+
"./utils.js": {
|
|
84
|
+
DEBUG_MODE: false,
|
|
85
|
+
getTmpDir: sinon.stub().returns("/tmp"),
|
|
86
|
+
isDryRun: false,
|
|
87
|
+
recordActivity: sinon.stub(),
|
|
88
|
+
safeCopyFileSync: sinon.stub(),
|
|
89
|
+
safeExtractArchive,
|
|
90
|
+
safeMkdirSync: sinon.stub(),
|
|
91
|
+
safeMkdtempSync: sinon.stub().returns("/tmp/asar-deps-test"),
|
|
92
|
+
safeRmSync: sinon.stub(),
|
|
93
|
+
safeWriteSync: sinon.stub(),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const extractedDir = await extractAsarToTempDirMocked("/tmp/sample.asar");
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(extractedDir, undefined);
|
|
101
|
+
sinon.assert.calledOnce(safeExtractArchive);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("parseAsarArchive()", () => {
|
|
106
|
+
it("catalogs file inventory, hashes, evidence, and security-sensitive properties", async () => {
|
|
107
|
+
const archivePath = join(baseTempDir, "fixture.asar");
|
|
108
|
+
createAsarFixture(archivePath, {
|
|
109
|
+
corruptIntegrityPaths: ["config/settings.json"],
|
|
110
|
+
executablePaths: ["scripts/postinstall.js"],
|
|
111
|
+
symlinks: {
|
|
112
|
+
"config-link": "config/settings.json",
|
|
113
|
+
},
|
|
114
|
+
unpackedPaths: ["native/addon.node"],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const analysis = await parseAsarArchive(archivePath, {});
|
|
118
|
+
const entryList = listAsarEntries(archivePath);
|
|
119
|
+
|
|
120
|
+
assert.ok(entryList.entries.some((entry) => entry.path === "config-link"));
|
|
121
|
+
assert.strictEqual(analysis.parentComponent.name, "Sample Electron App");
|
|
122
|
+
assert.strictEqual(
|
|
123
|
+
analysis.parentComponent.purl,
|
|
124
|
+
"pkg:npm/sample-electron-app@1.2.3",
|
|
125
|
+
);
|
|
126
|
+
assert.strictEqual(
|
|
127
|
+
analysis.summary.integrityMismatchCount,
|
|
128
|
+
1,
|
|
129
|
+
"expected one mismatched declared integrity hash",
|
|
130
|
+
);
|
|
131
|
+
assert.ok(analysis.summary.capabilities.includes("fileAccess"));
|
|
132
|
+
assert.ok(analysis.summary.capabilities.includes("network"));
|
|
133
|
+
assert.ok(analysis.summary.capabilities.includes("hardware"));
|
|
134
|
+
assert.ok(analysis.summary.capabilities.includes("dynamicFetch"));
|
|
135
|
+
assert.ok(analysis.summary.capabilities.includes("dynamicImport"));
|
|
136
|
+
assert.strictEqual(analysis.summary.hasEval, true);
|
|
137
|
+
const archiveProps = analysis.parentComponent.properties;
|
|
138
|
+
assert.strictEqual(
|
|
139
|
+
archiveProps.find((property) => property.name === "cdx:asar:hasEval")
|
|
140
|
+
?.value,
|
|
141
|
+
"true",
|
|
142
|
+
);
|
|
143
|
+
assert.strictEqual(
|
|
144
|
+
archiveProps.find(
|
|
145
|
+
(property) => property.name === "cdx:asar:hasNativeAddons",
|
|
146
|
+
)?.value,
|
|
147
|
+
"true",
|
|
148
|
+
);
|
|
149
|
+
assert.strictEqual(
|
|
150
|
+
archiveProps.find(
|
|
151
|
+
(property) => property.name === "cdx:asar:hasIntegrityMismatch",
|
|
152
|
+
)?.value,
|
|
153
|
+
"true",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const mainFileComponent = analysis.components.find((component) =>
|
|
157
|
+
component.properties?.some(
|
|
158
|
+
(property) =>
|
|
159
|
+
property.name === "cdx:asar:path" && property.value === "src/main.js",
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
assert.ok(mainFileComponent, "expected src/main.js file component");
|
|
163
|
+
assert.ok(mainFileComponent.hashes?.length, "expected SHA-256 hash");
|
|
164
|
+
assert.strictEqual(
|
|
165
|
+
mainFileComponent.evidence?.occurrences?.[0]?.location,
|
|
166
|
+
`${archivePath}#/src/main.js`,
|
|
167
|
+
);
|
|
168
|
+
assert.strictEqual(
|
|
169
|
+
mainFileComponent.properties.find(
|
|
170
|
+
(property) => property.name === "cdx:asar:js:hasDynamicFetch",
|
|
171
|
+
)?.value,
|
|
172
|
+
"true",
|
|
173
|
+
);
|
|
174
|
+
assert.strictEqual(
|
|
175
|
+
mainFileComponent.properties.find(
|
|
176
|
+
(property) => property.name === "cdx:asar:js:capability:hardware",
|
|
177
|
+
)?.value,
|
|
178
|
+
"true",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const unpackedComponent = analysis.components.find((component) =>
|
|
182
|
+
component.properties?.some(
|
|
183
|
+
(property) =>
|
|
184
|
+
property.name === "cdx:asar:path" &&
|
|
185
|
+
property.value === "native/addon.node",
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
assert.ok(unpackedComponent, "expected native addon component");
|
|
189
|
+
assert.strictEqual(
|
|
190
|
+
unpackedComponent.properties.find(
|
|
191
|
+
(property) => property.name === "cdx:asar:unpacked",
|
|
192
|
+
)?.value,
|
|
193
|
+
"true",
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("extracts ASAR archives and rewrites extracted source paths back to archive paths", async () => {
|
|
198
|
+
const archivePath = join(baseTempDir, "fixture-extract.asar");
|
|
199
|
+
createAsarFixture(archivePath, {
|
|
200
|
+
unpackedPaths: ["native/addon.node"],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const extractedDir = await extractAsarToTempDir(archivePath);
|
|
204
|
+
assert.ok(extractedDir, "expected extraction temp dir");
|
|
205
|
+
assert.ok(existsSync(join(extractedDir, "src", "main.js")));
|
|
206
|
+
assert.ok(existsSync(join(extractedDir, "native", "addon.node")));
|
|
207
|
+
|
|
208
|
+
const component = {
|
|
209
|
+
evidence: {
|
|
210
|
+
identity: {
|
|
211
|
+
methods: [
|
|
212
|
+
{
|
|
213
|
+
confidence: 1,
|
|
214
|
+
technique: "manifest-analysis",
|
|
215
|
+
value: join(extractedDir, "package.json"),
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
occurrences: [
|
|
220
|
+
{
|
|
221
|
+
location: join(extractedDir, "src", "main.js"),
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
properties: [
|
|
226
|
+
{
|
|
227
|
+
name: "SrcFile",
|
|
228
|
+
value: join(
|
|
229
|
+
extractedDir,
|
|
230
|
+
"node_modules",
|
|
231
|
+
"sketchy-addon",
|
|
232
|
+
"package.json",
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
rewriteExtractedArchivePaths(component, extractedDir, archivePath);
|
|
238
|
+
assert.strictEqual(
|
|
239
|
+
component.properties[0].value,
|
|
240
|
+
`${archivePath}#/node_modules/sketchy-addon/package.json`,
|
|
241
|
+
);
|
|
242
|
+
assert.strictEqual(
|
|
243
|
+
component.evidence.identity.methods[0].value,
|
|
244
|
+
`${archivePath}#/package.json`,
|
|
245
|
+
);
|
|
246
|
+
assert.strictEqual(
|
|
247
|
+
component.evidence.occurrences[0].location,
|
|
248
|
+
`${archivePath}#/src/main.js`,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
cleanupAsarTempDir(extractedDir);
|
|
252
|
+
assert.ok(!existsSync(extractedDir), "expected extracted temp dir cleanup");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("verifies Electron ASAR signing metadata and emits a crypto component", async () => {
|
|
256
|
+
const appDir = join(baseTempDir, "Signed.app");
|
|
257
|
+
const archivePath = join(
|
|
258
|
+
appDir,
|
|
259
|
+
"Contents",
|
|
260
|
+
"Resources",
|
|
261
|
+
"app & signed.asar",
|
|
262
|
+
);
|
|
263
|
+
mkdirSync(join(appDir, "Contents", "Resources"), { recursive: true });
|
|
264
|
+
createAsarFixture(archivePath);
|
|
265
|
+
const { headerString } = readAsarArchiveHeaderSync(archivePath);
|
|
266
|
+
const headerHash = createHash("sha256")
|
|
267
|
+
.update(headerString, "utf8")
|
|
268
|
+
.digest("hex");
|
|
269
|
+
writeElectronAsarIntegrityPlist(join(appDir, "Contents", "Info.plist"), {
|
|
270
|
+
"Resources/app & signed.asar": {
|
|
271
|
+
algorithm: "SHA256",
|
|
272
|
+
hash: headerHash,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const analysis = await parseAsarArchive(archivePath, { specVersion: 1.7 });
|
|
277
|
+
const signingComponent = analysis.components.find(
|
|
278
|
+
(component) =>
|
|
279
|
+
component.type === "cryptographic-asset" &&
|
|
280
|
+
component.properties?.some(
|
|
281
|
+
(property) =>
|
|
282
|
+
property.name === "cdx:asar:signingVerified" &&
|
|
283
|
+
property.value === "true",
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
assert.strictEqual(
|
|
287
|
+
analysis.parentComponent.properties.find(
|
|
288
|
+
(property) => property.name === "cdx:asar:hasSigningMetadata",
|
|
289
|
+
)?.value,
|
|
290
|
+
"true",
|
|
291
|
+
);
|
|
292
|
+
assert.strictEqual(
|
|
293
|
+
analysis.parentComponent.properties.find(
|
|
294
|
+
(property) => property.name === "cdx:asar:signingVerified",
|
|
295
|
+
)?.value,
|
|
296
|
+
"true",
|
|
297
|
+
);
|
|
298
|
+
assert.strictEqual(
|
|
299
|
+
analysis.parentComponent.properties.find(
|
|
300
|
+
(property) => property.name === "cdx:asar:signingScope",
|
|
301
|
+
)?.value,
|
|
302
|
+
"header-only",
|
|
303
|
+
);
|
|
304
|
+
assert.ok(signingComponent, "expected ASAR signing crypto component");
|
|
305
|
+
assert.strictEqual(
|
|
306
|
+
signingComponent.properties.find(
|
|
307
|
+
(property) => property.name === "cdx:asar:signingScope",
|
|
308
|
+
)?.value,
|
|
309
|
+
"header-only",
|
|
310
|
+
);
|
|
311
|
+
assert.ok(
|
|
312
|
+
analysis.dependencies.some(
|
|
313
|
+
(dependency) =>
|
|
314
|
+
dependency.ref === analysis.parentComponent["bom-ref"] &&
|
|
315
|
+
dependency.dependsOn.includes(signingComponent["bom-ref"]),
|
|
316
|
+
),
|
|
317
|
+
"expected parent archive to depend on the signing component",
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("rejects ASAR headers with oversized file entries", async () => {
|
|
322
|
+
const archivePath = join(baseTempDir, "fixture-oversized.asar");
|
|
323
|
+
createAsarFixture(archivePath, {
|
|
324
|
+
extraEntries: {
|
|
325
|
+
"huge.bin": {
|
|
326
|
+
content: "x",
|
|
327
|
+
size: 256 * 1024 * 1024 + 1,
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
await assert.rejects(
|
|
333
|
+
() => parseAsarArchive(archivePath, {}),
|
|
334
|
+
/Invalid ASAR file entry/,
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("rejects ASAR entries with offsets beyond the safe read limit", async () => {
|
|
339
|
+
const archivePath = join(baseTempDir, "fixture-offset.asar");
|
|
340
|
+
createAsarFixture(archivePath, {
|
|
341
|
+
extraEntries: {
|
|
342
|
+
"too-far.bin": {
|
|
343
|
+
content: "x",
|
|
344
|
+
offset: Number.MAX_SAFE_INTEGER + 10,
|
|
345
|
+
size: 1,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await assert.rejects(
|
|
351
|
+
() => parseAsarArchive(archivePath, {}),
|
|
352
|
+
/offset exceeds the safe read limit/,
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("rejects ASAR headers with excessive nesting depth", async () => {
|
|
357
|
+
const archivePath = join(baseTempDir, "fixture-deep.asar");
|
|
358
|
+
const deeplyNestedPath = `${Array.from({ length: 260 }, (_, index) => `d${index}`).join("/")}/payload.txt`;
|
|
359
|
+
createAsarFixture(archivePath, {
|
|
360
|
+
extraEntries: {
|
|
361
|
+
[deeplyNestedPath]: {
|
|
362
|
+
content: "payload",
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await assert.rejects(
|
|
368
|
+
() => parseAsarArchive(archivePath, {}),
|
|
369
|
+
/nesting exceeds 256 levels/,
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("rejects ASAR headers with conflicting entry kinds", async () => {
|
|
374
|
+
const archivePath = join(baseTempDir, "fixture-conflicting-kinds.asar");
|
|
375
|
+
createAsarFixture(archivePath);
|
|
376
|
+
rewriteArchiveHeaderSync(archivePath, (header) => {
|
|
377
|
+
header.files["bad-link"] = {
|
|
378
|
+
files: {},
|
|
379
|
+
link: "src/main.js",
|
|
380
|
+
};
|
|
381
|
+
return header;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await assert.rejects(
|
|
385
|
+
() => parseAsarArchive(archivePath, {}),
|
|
386
|
+
/Invalid ASAR symlink entry/,
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("rejects symlinks that escape the extraction root", async () => {
|
|
391
|
+
const archivePath = join(baseTempDir, "fixture-link-escape.asar");
|
|
392
|
+
createAsarFixture(archivePath, {
|
|
393
|
+
symlinks: {
|
|
394
|
+
"escape-link": "../../outside.txt",
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const extractedDir = await extractAsarToTempDir(archivePath);
|
|
399
|
+
assert.strictEqual(extractedDir, undefined);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("rejects circular symlink chains during extraction", async () => {
|
|
403
|
+
const archivePath = join(baseTempDir, "fixture-link-cycle.asar");
|
|
404
|
+
createAsarFixture(archivePath, {
|
|
405
|
+
symlinks: {
|
|
406
|
+
a: "b",
|
|
407
|
+
b: "a",
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const extractedDir = await extractAsarToTempDir(archivePath);
|
|
412
|
+
assert.strictEqual(extractedDir, undefined);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("reuses one packed-entry file descriptor per parse and extraction pass", async () => {
|
|
416
|
+
const archivePath = join(baseTempDir, "fixture-open-reuse.asar");
|
|
417
|
+
createAsarFixture(archivePath, {
|
|
418
|
+
unpackedPaths: ["native/addon.node"],
|
|
419
|
+
});
|
|
420
|
+
const openSync = sinon.spy((...args) => nodeFs.openSync(...args));
|
|
421
|
+
const closeSync = sinon.spy((...args) => nodeFs.closeSync(...args));
|
|
422
|
+
const {
|
|
423
|
+
cleanupAsarTempDir: cleanupAsarTempDirMocked,
|
|
424
|
+
extractAsarToTempDir: extractAsarToTempDirMocked,
|
|
425
|
+
parseAsarArchive: parseAsarArchiveMocked,
|
|
426
|
+
} = await esmock("./asarutils.js", {
|
|
427
|
+
"node:fs": {
|
|
428
|
+
...nodeFs,
|
|
429
|
+
closeSync,
|
|
430
|
+
openSync,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await parseAsarArchiveMocked(archivePath, {});
|
|
435
|
+
const extractedDir = await extractAsarToTempDirMocked(archivePath);
|
|
436
|
+
|
|
437
|
+
assert.ok(extractedDir, "expected extraction temp dir");
|
|
438
|
+
assert.strictEqual(openSync.callCount, 4);
|
|
439
|
+
assert.strictEqual(closeSync.callCount, 4);
|
|
440
|
+
|
|
441
|
+
cleanupAsarTempDirMocked(extractedDir);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
export const HBOM_AUDIT_CATEGORIES = Object.freeze([
|
|
2
|
+
"hbom-security",
|
|
3
|
+
"hbom-performance",
|
|
4
|
+
"hbom-compliance",
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export const HOST_TOPOLOGY_AUDIT_CATEGORIES = Object.freeze(["host-topology"]);
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_HBOM_AUDIT_CATEGORIES = HBOM_AUDIT_CATEGORIES.join(",");
|
|
10
|
+
|
|
1
11
|
export const BOM_AUDIT_CATEGORY_ALIASES = Object.freeze({
|
|
2
12
|
"ai-inventory": ["ai-agent", "mcp-server"],
|
|
13
|
+
hbom: [...HBOM_AUDIT_CATEGORIES],
|
|
14
|
+
host: [...HBOM_AUDIT_CATEGORIES, ...HOST_TOPOLOGY_AUDIT_CATEGORIES],
|
|
3
15
|
});
|
|
4
16
|
|
|
5
17
|
function uniqueNonEmptyCategories(categories) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
expandBomAuditCategories,
|
|
5
|
+
validateBomAuditCategories,
|
|
6
|
+
} from "./auditCategories.js";
|
|
7
|
+
|
|
8
|
+
describe("auditCategories", () => {
|
|
9
|
+
it("keeps host-topology as a direct category", () => {
|
|
10
|
+
assert.deepStrictEqual(expandBomAuditCategories("host-topology"), [
|
|
11
|
+
"host-topology",
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("expands the host alias to the HBOM packs plus host-topology", () => {
|
|
16
|
+
assert.deepStrictEqual(expandBomAuditCategories("host"), [
|
|
17
|
+
"hbom-security",
|
|
18
|
+
"hbom-performance",
|
|
19
|
+
"hbom-compliance",
|
|
20
|
+
"host-topology",
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("accepts host-topology during validation", () => {
|
|
25
|
+
const validation = validateBomAuditCategories("host-topology", [
|
|
26
|
+
{ category: "host-topology" },
|
|
27
|
+
{ category: "hbom-security" },
|
|
28
|
+
]);
|
|
29
|
+
assert.deepStrictEqual(validation.categories, ["host-topology"]);
|
|
30
|
+
assert.deepStrictEqual(validation.expandedCategories, ["host-topology"]);
|
|
31
|
+
});
|
|
32
|
+
});
|
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)) {
|