@cyclonedx/cdxgen 12.2.0 → 12.3.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 +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +30 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -35
- 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.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/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- package/types/bin/licenses.d.ts.map +0 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { assert, describe, it } from "poku";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
analyzeSuspiciousJsFile,
|
|
15
|
+
detectExtensionCapabilities,
|
|
16
|
+
findJSImportsExports,
|
|
17
|
+
} from "./analyzer.js";
|
|
18
|
+
|
|
19
|
+
const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-analyzer-poku-"));
|
|
20
|
+
|
|
21
|
+
process.on("exit", () => {
|
|
22
|
+
rmSync(baseTempDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const createProject = (subDirName, entryContent) => {
|
|
26
|
+
const projectDir = join(baseTempDir, subDirName);
|
|
27
|
+
mkdirSync(projectDir, { recursive: true });
|
|
28
|
+
writeFileSync(join(projectDir, "index.js"), entryContent, {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
});
|
|
31
|
+
return projectDir;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const createProjectFromFixture = (subDirName, fixtureFileName) => {
|
|
35
|
+
const projectDir = join(baseTempDir, subDirName);
|
|
36
|
+
mkdirSync(projectDir, { recursive: true });
|
|
37
|
+
const fixturePath = new URL(
|
|
38
|
+
`../../test/data/${fixtureFileName}`,
|
|
39
|
+
import.meta.url,
|
|
40
|
+
);
|
|
41
|
+
copyFileSync(fixturePath, join(projectDir, fixtureFileName));
|
|
42
|
+
return projectDir;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("findJSImportsExports() wasm and wasi detection", () => {
|
|
46
|
+
it("captures wasm exports from WebAssembly.instantiate() flow", async () => {
|
|
47
|
+
const projectDir = createProject(
|
|
48
|
+
"instantiate-flow",
|
|
49
|
+
`import fs from "node:fs/promises";
|
|
50
|
+
const wasmBuffer = await fs.readFile("./add.wasm");
|
|
51
|
+
const wasmModule = await WebAssembly.instantiate(wasmBuffer);
|
|
52
|
+
const { add } = wasmModule.instance.exports;
|
|
53
|
+
console.log(add(5, 6));
|
|
54
|
+
`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
58
|
+
assert.ok(allImports["add.wasm"], "expected add.wasm to be discovered");
|
|
59
|
+
const occurrences = Array.from(allImports["add.wasm"]);
|
|
60
|
+
assert.ok(
|
|
61
|
+
occurrences.some((occ) => occ.importedModules?.includes("add")),
|
|
62
|
+
"expected add export symbol to be tracked",
|
|
63
|
+
);
|
|
64
|
+
const addOccurrence = occurrences.find((occ) =>
|
|
65
|
+
occ.importedModules?.includes("add"),
|
|
66
|
+
);
|
|
67
|
+
assert.ok(addOccurrence, "expected add symbol occurrence to exist");
|
|
68
|
+
assert.ok(
|
|
69
|
+
addOccurrence.fileName?.includes("index.js"),
|
|
70
|
+
"expected source filename to be tracked",
|
|
71
|
+
);
|
|
72
|
+
assert.strictEqual(addOccurrence.lineNumber, 4);
|
|
73
|
+
assert.strictEqual(typeof addOccurrence.columnNumber, "number");
|
|
74
|
+
assert.ok(addOccurrence.columnNumber >= 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("captures wasm exports from instantiateStreaming(fetch(new URL(...)))", async () => {
|
|
78
|
+
const projectDir = createProject(
|
|
79
|
+
"streaming-flow",
|
|
80
|
+
`const { instance } = await WebAssembly.instantiateStreaming(
|
|
81
|
+
fetch(new URL("./stream.wasm", import.meta.url)),
|
|
82
|
+
);
|
|
83
|
+
const { run } = instance.exports;
|
|
84
|
+
console.log(run());
|
|
85
|
+
`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
89
|
+
assert.ok(
|
|
90
|
+
allImports["stream.wasm"],
|
|
91
|
+
"expected stream.wasm to be discovered",
|
|
92
|
+
);
|
|
93
|
+
const occurrences = Array.from(allImports["stream.wasm"]);
|
|
94
|
+
assert.ok(
|
|
95
|
+
occurrences.some((occ) => occ.importedModules?.includes("run")),
|
|
96
|
+
"expected run export symbol to be tracked",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not treat arbitrary function calls with .wasm literals as wasm imports", async () => {
|
|
101
|
+
const projectDir = createProject(
|
|
102
|
+
"non-wasm-callee",
|
|
103
|
+
`doSomething("./ignored.wasm");
|
|
104
|
+
`,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
108
|
+
assert.ok(
|
|
109
|
+
!allImports["./ignored.wasm"] && !allImports["ignored.wasm"],
|
|
110
|
+
"expected non-wasm callee usage to be ignored",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("captures wasi constructor and lifecycle API usage", async () => {
|
|
115
|
+
const projectDir = createProject(
|
|
116
|
+
"wasi-flow",
|
|
117
|
+
`import { WASI } from "node:wasi";
|
|
118
|
+
const wasi = new WASI({ version: "preview1" });
|
|
119
|
+
wasi.initialize(instance);
|
|
120
|
+
`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
124
|
+
assert.ok(allImports["node:wasi"], "expected node:wasi to be discovered");
|
|
125
|
+
const occurrences = Array.from(allImports["node:wasi"]);
|
|
126
|
+
assert.ok(
|
|
127
|
+
occurrences.some((occ) => occ.importedModules?.includes("WASI")),
|
|
128
|
+
"expected WASI usage to be tracked",
|
|
129
|
+
);
|
|
130
|
+
assert.ok(
|
|
131
|
+
occurrences.some((occ) => occ.importedModules?.includes("initialize")),
|
|
132
|
+
"expected initialize API usage to be tracked",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("captures wasi constructor alias invoked without new", async () => {
|
|
137
|
+
const projectDir = createProject(
|
|
138
|
+
"wasi-call-alias-flow",
|
|
139
|
+
`import { WASI as WasiCtor } from "node:wasi";
|
|
140
|
+
const wasi = WasiCtor({ version: "preview1" });
|
|
141
|
+
wasi.start(instance);
|
|
142
|
+
`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
146
|
+
assert.ok(allImports["node:wasi"], "expected node:wasi to be discovered");
|
|
147
|
+
const occurrences = Array.from(allImports["node:wasi"]);
|
|
148
|
+
assert.ok(
|
|
149
|
+
occurrences.some((occ) => occ.importedModules?.includes("WASI")),
|
|
150
|
+
"expected WASI constructor alias usage to be tracked",
|
|
151
|
+
);
|
|
152
|
+
assert.ok(
|
|
153
|
+
occurrences.some((occ) => occ.importedModules?.includes("start")),
|
|
154
|
+
"expected start API usage to be tracked",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("detects wasm import/export functions from libmagic wrapper fixture", async () => {
|
|
159
|
+
const projectDir = createProjectFromFixture(
|
|
160
|
+
"libmagic-wrapper",
|
|
161
|
+
"libmagic-wrapper.js",
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const { allImports, allExports } = await findJSImportsExports(
|
|
165
|
+
projectDir,
|
|
166
|
+
false,
|
|
167
|
+
);
|
|
168
|
+
assert.ok(allImports.fs, "expected fs require import to be detected");
|
|
169
|
+
assert.ok(
|
|
170
|
+
allImports.crypto,
|
|
171
|
+
"expected crypto require import to be detected",
|
|
172
|
+
);
|
|
173
|
+
assert.ok(
|
|
174
|
+
allImports["libmagic-wrapper.wasm"],
|
|
175
|
+
"expected libmagic-wrapper.wasm to be detected",
|
|
176
|
+
);
|
|
177
|
+
assert.ok(
|
|
178
|
+
allExports["libmagic-wrapper.wasm"],
|
|
179
|
+
"expected libmagic-wrapper.wasm exports to be detected",
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const wasmImportOccurrences = Array.from(
|
|
183
|
+
allImports["libmagic-wrapper.wasm"],
|
|
184
|
+
);
|
|
185
|
+
const wasmExportOccurrences = Array.from(
|
|
186
|
+
allExports["libmagic-wrapper.wasm"],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
assert.ok(
|
|
190
|
+
wasmImportOccurrences.some(
|
|
191
|
+
(occ) =>
|
|
192
|
+
occ.fileName?.includes("libmagic-wrapper.js") &&
|
|
193
|
+
typeof occ.lineNumber === "number" &&
|
|
194
|
+
typeof occ.columnNumber === "number",
|
|
195
|
+
),
|
|
196
|
+
"expected wasm import occurrences to include source location metadata",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const importedModules = new Set(
|
|
200
|
+
wasmImportOccurrences.flatMap((occ) => occ.importedModules || []),
|
|
201
|
+
);
|
|
202
|
+
for (const expectedImportedModule of [
|
|
203
|
+
"free",
|
|
204
|
+
"malloc",
|
|
205
|
+
"magic_wrapper_load",
|
|
206
|
+
"magic_wrapper_detect",
|
|
207
|
+
"_emscripten_stack_restore",
|
|
208
|
+
"_emscripten_stack_alloc",
|
|
209
|
+
"emscripten_stack_get_current",
|
|
210
|
+
"memory",
|
|
211
|
+
"__indirect_function_table",
|
|
212
|
+
]) {
|
|
213
|
+
assert.ok(
|
|
214
|
+
importedModules.has(expectedImportedModule),
|
|
215
|
+
`expected imported wasm symbol ${expectedImportedModule}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const exportedModules = new Set(
|
|
220
|
+
wasmExportOccurrences.flatMap((occ) => occ.exportedModules || []),
|
|
221
|
+
);
|
|
222
|
+
for (const expectedExportedModule of [
|
|
223
|
+
"_free",
|
|
224
|
+
"_malloc",
|
|
225
|
+
"_magic_wrapper_load",
|
|
226
|
+
"_magic_wrapper_detect",
|
|
227
|
+
]) {
|
|
228
|
+
assert.ok(
|
|
229
|
+
exportedModules.has(expectedExportedModule),
|
|
230
|
+
`expected exported wasm symbol ${expectedExportedModule}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("detectExtensionCapabilities()", () => {
|
|
237
|
+
it("should detect extension capability signals from source usage", () => {
|
|
238
|
+
const projectDir = createProject(
|
|
239
|
+
"extension-capabilities",
|
|
240
|
+
`chrome.scripting.executeScript({ target: { tabId: 1 }, files: ["inject.js"] });
|
|
241
|
+
chrome.bluetooth.getDevices(() => {});
|
|
242
|
+
chrome.downloads.download({ url: "https://example.invalid/a.txt" });
|
|
243
|
+
const canvas = document.createElement("canvas");
|
|
244
|
+
canvas.toDataURL();
|
|
245
|
+
fetch("https://example.invalid/api");
|
|
246
|
+
navigator.userAgentData?.getHighEntropyValues(["platformVersion"]);
|
|
247
|
+
`,
|
|
248
|
+
);
|
|
249
|
+
const detected = detectExtensionCapabilities(projectDir);
|
|
250
|
+
assert.ok(detected.capabilities.includes("codeInjection"));
|
|
251
|
+
assert.ok(detected.capabilities.includes("bluetooth"));
|
|
252
|
+
assert.ok(detected.capabilities.includes("deviceAccess"));
|
|
253
|
+
assert.ok(detected.capabilities.includes("fileAccess"));
|
|
254
|
+
assert.ok(detected.capabilities.includes("network"));
|
|
255
|
+
assert.ok(detected.capabilities.includes("fingerprinting"));
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("should detect fingerprinting from canvas member-chain APIs", () => {
|
|
259
|
+
const projectDir = createProject(
|
|
260
|
+
"extension-capabilities-canvas-only",
|
|
261
|
+
`const canvas = document.createElement("canvas");
|
|
262
|
+
const ctx = canvas.getContext("2d");
|
|
263
|
+
ctx.getImageData(0, 0, 1, 1);
|
|
264
|
+
canvas.toDataURL();
|
|
265
|
+
ctx.measureText("a");
|
|
266
|
+
`,
|
|
267
|
+
);
|
|
268
|
+
const detected = detectExtensionCapabilities(projectDir);
|
|
269
|
+
assert.ok(detected.capabilities.includes("fingerprinting"));
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("analyzeSuspiciousJsFile()", () => {
|
|
274
|
+
it("detects encoded child-process loader patterns", () => {
|
|
275
|
+
const projectDir = createProject(
|
|
276
|
+
"suspicious-lifecycle-js",
|
|
277
|
+
[
|
|
278
|
+
"import cp from 'node:child_process';",
|
|
279
|
+
"const payload = Buffer.from('ZXZhbCgnY29uc29sZS5sb2coMSknKQ==', 'base64');",
|
|
280
|
+
"cp.execSync(payload.toString());",
|
|
281
|
+
].join("\n"),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const analysis = analyzeSuspiciousJsFile(join(projectDir, "index.js"));
|
|
285
|
+
assert.match(analysis.obfuscationIndicators.join(","), /buffer-base64/);
|
|
286
|
+
assert.match(analysis.executionIndicators.join(","), /child-process/);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("detects network-capable script files referenced by lifecycle hooks", () => {
|
|
290
|
+
const projectDir = createProject(
|
|
291
|
+
"network-lifecycle-js",
|
|
292
|
+
[
|
|
293
|
+
"import https from 'node:https';",
|
|
294
|
+
"https.request('https://example.invalid/payload');",
|
|
295
|
+
].join("\n"),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const analysis = analyzeSuspiciousJsFile(join(projectDir, "index.js"));
|
|
299
|
+
assert.match(analysis.networkIndicators.join(","), /network-request/);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function escapeMarkdownText(value) {
|
|
2
|
+
return String(value ?? "")
|
|
3
|
+
.replace(/&/g, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
6
|
+
.replace(/([\\`*_{}\[\]()#+!|])/g, "\\$1")
|
|
7
|
+
.replace(/\r?\n/g, "<br>")
|
|
8
|
+
.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function escapeMarkdownCell(value) {
|
|
12
|
+
return escapeMarkdownText(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format annotation properties as a markdown table for CycloneDX annotations.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ name: string, value: string }[]} properties annotation properties
|
|
19
|
+
* @returns {string} markdown table text
|
|
20
|
+
*/
|
|
21
|
+
export function propertiesToMarkdownTable(properties) {
|
|
22
|
+
if (!properties?.length) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
const lines = ["| Property | Value |", "| --- | --- |"];
|
|
26
|
+
for (const property of properties) {
|
|
27
|
+
lines.push(
|
|
28
|
+
`| ${escapeMarkdownCell(property?.name)} | ${escapeMarkdownCell(property?.value)} |`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build production-ready markdown annotation text.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} message leading message text
|
|
38
|
+
* @param {{ name: string, value: string }[]} properties annotation properties
|
|
39
|
+
* @param {string[]} [details] optional detail lines shown before the table
|
|
40
|
+
* @returns {string} annotation text
|
|
41
|
+
*/
|
|
42
|
+
export function buildAnnotationText(message, properties, details = []) {
|
|
43
|
+
const lines = [message, ...details.filter(Boolean)].map(escapeMarkdownText);
|
|
44
|
+
const markdownTable = propertiesToMarkdownTable(properties);
|
|
45
|
+
if (markdownTable) {
|
|
46
|
+
lines.push("", markdownTable);
|
|
47
|
+
}
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildAnnotationText,
|
|
5
|
+
propertiesToMarkdownTable,
|
|
6
|
+
} from "./annotationFormatter.js";
|
|
7
|
+
|
|
8
|
+
describe("annotationFormatter", () => {
|
|
9
|
+
it("escapes markdown tables for untrusted annotation properties", () => {
|
|
10
|
+
const table = propertiesToMarkdownTable([
|
|
11
|
+
{
|
|
12
|
+
name: "<script>alert(1)</script>",
|
|
13
|
+
value: "[click](javascript:alert(1))\nnext|cell & more",
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
assert.strictEqual(
|
|
18
|
+
table,
|
|
19
|
+
[
|
|
20
|
+
"| Property | Value |",
|
|
21
|
+
"| --- | --- |",
|
|
22
|
+
String.raw`| <script>alert\(1\)</script> | \[click\]\(javascript:alert\(1\)\)<br>next\|cell & more |`,
|
|
23
|
+
].join("\n"),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("escapes markdown and HTML-sensitive content in annotation messages", () => {
|
|
28
|
+
const htmlLikeMessage =
|
|
29
|
+
String.fromCharCode(60) +
|
|
30
|
+
"img src=x onerror=alert(1)" +
|
|
31
|
+
String.fromCharCode(62);
|
|
32
|
+
const text = buildAnnotationText(
|
|
33
|
+
htmlLikeMessage,
|
|
34
|
+
[{ name: "cdx:test", value: "safe" }],
|
|
35
|
+
["[open](javascript:alert(1))", "line\nbreak"],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
assert.ok(text.startsWith("<img src=x onerror=alert\\(1\\)>"));
|
|
39
|
+
assert.ok(text.includes("\\[open\\]\\(javascript:alert\\(1\\)\\)"));
|
|
40
|
+
assert.ok(text.includes("line<br>break"));
|
|
41
|
+
assert.ok(text.includes("| cdx:test | safe |"));
|
|
42
|
+
assert.ok(!text.includes(htmlLikeMessage));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const SPDX_CONTEXT_PREFIX = "https://spdx.org/rdf/";
|
|
2
|
+
const CYCLONEDX_FORMAT = "CycloneDX";
|
|
3
|
+
const BOM_FORMAT_CYCLONEDX = "cyclonedx";
|
|
4
|
+
const BOM_FORMAT_SPDX = "spdx";
|
|
5
|
+
const BOM_FORMAT_UNKNOWN = "unknown";
|
|
6
|
+
|
|
7
|
+
export const isSpdxJsonLd = (bomJson) =>
|
|
8
|
+
Boolean(
|
|
9
|
+
bomJson?.["@context"]?.startsWith(SPDX_CONTEXT_PREFIX) &&
|
|
10
|
+
Array.isArray(bomJson?.["@graph"]) &&
|
|
11
|
+
bomJson["@graph"].some((element) => element?.type === "SpdxDocument"),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const isCycloneDxBom = (bomJson) =>
|
|
15
|
+
bomJson?.bomFormat === CYCLONEDX_FORMAT && Boolean(bomJson?.specVersion);
|
|
16
|
+
|
|
17
|
+
export const detectBomFormat = (bomJson) => {
|
|
18
|
+
if (isCycloneDxBom(bomJson)) {
|
|
19
|
+
return BOM_FORMAT_CYCLONEDX;
|
|
20
|
+
}
|
|
21
|
+
if (isSpdxJsonLd(bomJson)) {
|
|
22
|
+
return BOM_FORMAT_SPDX;
|
|
23
|
+
}
|
|
24
|
+
return BOM_FORMAT_UNKNOWN;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getNonCycloneDxErrorMessage = (
|
|
28
|
+
bomJson,
|
|
29
|
+
commandName = "This command",
|
|
30
|
+
) => {
|
|
31
|
+
const detectedFormat = detectBomFormat(bomJson);
|
|
32
|
+
if (detectedFormat === BOM_FORMAT_SPDX) {
|
|
33
|
+
return `${commandName} expects a CycloneDX BOM. SPDX input is not supported for this command.`;
|
|
34
|
+
}
|
|
35
|
+
return `${commandName} expects a CycloneDX JSON BOM.`;
|
|
36
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { assert, describe, it } from "poku";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
detectBomFormat,
|
|
5
|
+
getNonCycloneDxErrorMessage,
|
|
6
|
+
isCycloneDxBom,
|
|
7
|
+
isSpdxJsonLd,
|
|
8
|
+
} from "./bomUtils.js";
|
|
9
|
+
|
|
10
|
+
const sampleSpdx = {
|
|
11
|
+
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
|
|
12
|
+
"@graph": [{ type: "SpdxDocument", spdxId: "urn:demo#SPDXRef-DOCUMENT" }],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("bomUtils", () => {
|
|
16
|
+
it("detects CycloneDX documents", () => {
|
|
17
|
+
assert.strictEqual(
|
|
18
|
+
isCycloneDxBom({
|
|
19
|
+
bomFormat: "CycloneDX",
|
|
20
|
+
specVersion: 1.7,
|
|
21
|
+
}),
|
|
22
|
+
true,
|
|
23
|
+
);
|
|
24
|
+
assert.strictEqual(isCycloneDxBom(sampleSpdx), false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("detects SPDX JSON-LD documents", () => {
|
|
28
|
+
assert.strictEqual(isSpdxJsonLd(sampleSpdx), true);
|
|
29
|
+
assert.strictEqual(isSpdxJsonLd({ bomFormat: "CycloneDX" }), false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("classifies BOM formats for CLI reuse", () => {
|
|
33
|
+
assert.strictEqual(detectBomFormat(sampleSpdx), "spdx");
|
|
34
|
+
assert.strictEqual(
|
|
35
|
+
detectBomFormat({ bomFormat: "CycloneDX", specVersion: "1.6" }),
|
|
36
|
+
"cyclonedx",
|
|
37
|
+
);
|
|
38
|
+
assert.strictEqual(detectBomFormat({ foo: "bar" }), "unknown");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("generates clear CycloneDX-only command errors", () => {
|
|
42
|
+
assert.strictEqual(
|
|
43
|
+
getNonCycloneDxErrorMessage(sampleSpdx, "cdx-sign"),
|
|
44
|
+
"cdx-sign expects a CycloneDX BOM. SPDX input is not supported for this command.",
|
|
45
|
+
);
|
|
46
|
+
assert.strictEqual(
|
|
47
|
+
getNonCycloneDxErrorMessage({ foo: "bar" }, "cdx-sign"),
|
|
48
|
+
"cdx-sign expects a CycloneDX JSON BOM.",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
package/lib/helpers/caxa.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { getNpmMetadata,
|
|
3
|
+
import { getNpmMetadata, shouldFetchPackageMetadata } from "./utils.js";
|
|
4
4
|
|
|
5
5
|
export async function parseCaxaMetadata(mfile) {
|
|
6
6
|
let mdata;
|
|
@@ -48,7 +48,7 @@ export async function parseCaxaMetadata(mfile) {
|
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
if (
|
|
51
|
+
if (shouldFetchPackageMetadata()) {
|
|
52
52
|
mdata.components = await getNpmMetadata(mdata.components);
|
|
53
53
|
}
|
|
54
54
|
return mdata;
|