@cyclonedx/cdxgen 10.10.6 → 10.11.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/bin/cdxgen.js +9 -4
- package/bin/evinse.js +9 -0
- package/lib/cli/index.js +44 -38
- package/lib/evinser/evinser.js +191 -16
- package/lib/evinser/evinser.test.js +16 -0
- package/lib/evinser/swiftsem.js +593 -3
- package/lib/evinser/swiftsem.test.js +375 -0
- package/lib/helpers/envcontext.js +5 -1
- package/lib/helpers/utils.js +84 -51
- package/lib/helpers/utils.test.js +2 -2
- package/lib/managers/binary.js +39 -5
- package/lib/stages/pregen/pregen.js +33 -6
- package/package.json +12 -11
- package/types/lib/cli/index.d.ts +2 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +16 -3
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +102 -46
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +3 -2
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +29 -20
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +7 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +2 -2
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
package/bin/cdxgen.js
CHANGED
|
@@ -202,9 +202,14 @@ const args = yargs(hideBin(process.argv))
|
|
|
202
202
|
default: "reachables.slices.json",
|
|
203
203
|
hidden: true,
|
|
204
204
|
})
|
|
205
|
+
.option("semantics-slices-file", {
|
|
206
|
+
description: "Path for the semantics slices file.",
|
|
207
|
+
default: "semantics.slices.json",
|
|
208
|
+
hidden: true,
|
|
209
|
+
})
|
|
205
210
|
.option("spec-version", {
|
|
206
|
-
description: "CycloneDX Specification version to use. Defaults to 1.
|
|
207
|
-
default: 1.
|
|
211
|
+
description: "CycloneDX Specification version to use. Defaults to 1.6",
|
|
212
|
+
default: 1.6,
|
|
208
213
|
type: "number",
|
|
209
214
|
})
|
|
210
215
|
.option("filter", {
|
|
@@ -380,7 +385,7 @@ if (options.includeFormulation) {
|
|
|
380
385
|
/**
|
|
381
386
|
* Method to apply advanced options such as profile and lifecycles
|
|
382
387
|
*
|
|
383
|
-
* @param {object} CLI options
|
|
388
|
+
* @param {object} options CLI options
|
|
384
389
|
*/
|
|
385
390
|
const applyAdvancedOptions = (options) => {
|
|
386
391
|
switch (options.profile) {
|
|
@@ -731,7 +736,7 @@ const checkPermissions = (filePath) => {
|
|
|
731
736
|
printTable(bomNSData.bomJson);
|
|
732
737
|
// CBOM related print
|
|
733
738
|
if (options.includeCrypto) {
|
|
734
|
-
console.log("*** Cryptography BOM ***");
|
|
739
|
+
console.log("\n*** Cryptography BOM ***");
|
|
735
740
|
printTable(bomNSData.bomJson, ["cryptographic-asset"]);
|
|
736
741
|
printDependencyTree(bomNSData.bomJson, "provides");
|
|
737
742
|
}
|
package/bin/evinse.js
CHANGED
|
@@ -63,12 +63,15 @@ const args = yargs(hideBin(process.argv))
|
|
|
63
63
|
"js",
|
|
64
64
|
"ts",
|
|
65
65
|
"javascript",
|
|
66
|
+
"nodejs",
|
|
66
67
|
"py",
|
|
67
68
|
"python",
|
|
68
69
|
"android",
|
|
69
70
|
"c",
|
|
70
71
|
"cpp",
|
|
71
72
|
"php",
|
|
73
|
+
"swift",
|
|
74
|
+
"ios",
|
|
72
75
|
],
|
|
73
76
|
})
|
|
74
77
|
.option("db-path", {
|
|
@@ -120,6 +123,11 @@ const args = yargs(hideBin(process.argv))
|
|
|
120
123
|
description: "Use an existing reachables slices file.",
|
|
121
124
|
default: "reachables.slices.json",
|
|
122
125
|
})
|
|
126
|
+
.option("semantics-slices-file", {
|
|
127
|
+
description: "Use an existing semantics slices file.",
|
|
128
|
+
default: "semantics.slices.json",
|
|
129
|
+
hidden: true,
|
|
130
|
+
})
|
|
123
131
|
.option("print", {
|
|
124
132
|
alias: "p",
|
|
125
133
|
type: "boolean",
|
|
@@ -141,6 +149,7 @@ const args = yargs(hideBin(process.argv))
|
|
|
141
149
|
.scriptName("evinse")
|
|
142
150
|
.version()
|
|
143
151
|
.help("h")
|
|
152
|
+
.alias("h", "help")
|
|
144
153
|
.wrap(Math.min(120, yargs().terminalWidth())).argv;
|
|
145
154
|
|
|
146
155
|
const evinseArt = `
|
package/lib/cli/index.js
CHANGED
|
@@ -19,8 +19,12 @@ import process from "node:process";
|
|
|
19
19
|
import { URL } from "node:url";
|
|
20
20
|
import got from "got";
|
|
21
21
|
import { PackageURL } from "packageurl-js";
|
|
22
|
+
import { gte, lte } from "semver";
|
|
22
23
|
import { parse } from "ssri";
|
|
24
|
+
import { table } from "table";
|
|
23
25
|
import { v4 as uuidv4 } from "uuid";
|
|
26
|
+
import { findJSImportsExports } from "../helpers/analyzer.js";
|
|
27
|
+
import { collectOSCryptoLibs } from "../helpers/cbomutils.js";
|
|
24
28
|
import {
|
|
25
29
|
collectEnvInfo,
|
|
26
30
|
getBranch,
|
|
@@ -144,19 +148,6 @@ import {
|
|
|
144
148
|
recomputeScope,
|
|
145
149
|
splitOutputByGradleProjects,
|
|
146
150
|
} from "../helpers/utils.js";
|
|
147
|
-
let url = import.meta.url;
|
|
148
|
-
if (!url.startsWith("file://")) {
|
|
149
|
-
url = new URL(`file://${import.meta.url}`).toString();
|
|
150
|
-
}
|
|
151
|
-
const dirName = dirNameStr;
|
|
152
|
-
|
|
153
|
-
const selfPJson = JSON.parse(
|
|
154
|
-
readFileSync(join(dirName, "package.json"), "utf-8"),
|
|
155
|
-
);
|
|
156
|
-
const _version = selfPJson.version;
|
|
157
|
-
import { gte, lte } from "semver";
|
|
158
|
-
import { findJSImportsExports } from "../helpers/analyzer.js";
|
|
159
|
-
import { collectOSCryptoLibs } from "../helpers/cbomutils.js";
|
|
160
151
|
import {
|
|
161
152
|
executeOsQuery,
|
|
162
153
|
getBinaryBom,
|
|
@@ -171,6 +162,17 @@ import {
|
|
|
171
162
|
parseImageName,
|
|
172
163
|
} from "../managers/docker.js";
|
|
173
164
|
|
|
165
|
+
let url = import.meta.url;
|
|
166
|
+
if (!url.startsWith("file://")) {
|
|
167
|
+
url = new URL(`file://${import.meta.url}`).toString();
|
|
168
|
+
}
|
|
169
|
+
const dirName = dirNameStr;
|
|
170
|
+
|
|
171
|
+
const selfPJson = JSON.parse(
|
|
172
|
+
readFileSync(join(dirName, "package.json"), "utf-8"),
|
|
173
|
+
);
|
|
174
|
+
const _version = selfPJson.version;
|
|
175
|
+
|
|
174
176
|
const isWin = _platform() === "win32";
|
|
175
177
|
|
|
176
178
|
let osQueries = {};
|
|
@@ -195,8 +197,6 @@ const cosDbQueries = JSON.parse(
|
|
|
195
197
|
readFileSync(join(dirName, "data", "cosdb-queries.json"), "utf-8"),
|
|
196
198
|
);
|
|
197
199
|
|
|
198
|
-
import { table } from "table";
|
|
199
|
-
|
|
200
200
|
// Construct gradle cache directory
|
|
201
201
|
let GRADLE_CACHE_DIR =
|
|
202
202
|
process.env.GRADLE_CACHE_DIR ||
|
|
@@ -221,8 +221,9 @@ const HASH_PATTERN =
|
|
|
221
221
|
/**
|
|
222
222
|
* Creates a default parent component based on the directory name.
|
|
223
223
|
*
|
|
224
|
-
* @param {
|
|
225
|
-
* @param {
|
|
224
|
+
* @param {String} path Directory or file name
|
|
225
|
+
* @param {String} type Package type
|
|
226
|
+
* @param {Object} options CLI options
|
|
226
227
|
* @returns component object
|
|
227
228
|
*/
|
|
228
229
|
const createDefaultParentComponent = (
|
|
@@ -734,7 +735,7 @@ function addMetadata(parentComponent = {}, options = {}, context = {}) {
|
|
|
734
735
|
*/
|
|
735
736
|
function addExternalReferences(opkg) {
|
|
736
737
|
let externalReferences = [];
|
|
737
|
-
let pkgList
|
|
738
|
+
let pkgList;
|
|
738
739
|
if (Array.isArray(opkg)) {
|
|
739
740
|
pkgList = opkg;
|
|
740
741
|
} else {
|
|
@@ -843,11 +844,12 @@ function addComponent(
|
|
|
843
844
|
pkg.qualifiers,
|
|
844
845
|
encodeForPurl(pkg.subpath),
|
|
845
846
|
);
|
|
847
|
+
let purlString = purl.toString();
|
|
846
848
|
// There is no purl for cryptographic-asset
|
|
847
849
|
if (ptype === "cryptographic-asset") {
|
|
848
850
|
purl = undefined;
|
|
851
|
+
purlString = undefined;
|
|
849
852
|
}
|
|
850
|
-
const purlString = purl.toString();
|
|
851
853
|
const description = pkg.description || undefined;
|
|
852
854
|
let compScope = pkg.scope;
|
|
853
855
|
if (allImports) {
|
|
@@ -1025,8 +1027,7 @@ function processHashes(pkg, component) {
|
|
|
1025
1027
|
addComponentHash(ahash.alg, ahash.content, component);
|
|
1026
1028
|
}
|
|
1027
1029
|
} else if (pkg._shasum) {
|
|
1028
|
-
|
|
1029
|
-
ahash = { alg: "SHA-1", content: pkg._shasum };
|
|
1030
|
+
const ahash = { alg: "SHA-1", content: pkg._shasum };
|
|
1030
1031
|
component.hashes.push(ahash);
|
|
1031
1032
|
} else if (pkg._integrity) {
|
|
1032
1033
|
const integrity = parse(pkg._integrity) || {};
|
|
@@ -1054,7 +1055,7 @@ function processHashes(pkg, component) {
|
|
|
1054
1055
|
* Adds a hash to component.
|
|
1055
1056
|
*/
|
|
1056
1057
|
function addComponentHash(alg, digest, component) {
|
|
1057
|
-
let hash
|
|
1058
|
+
let hash;
|
|
1058
1059
|
// If it is a valid hash simply use it
|
|
1059
1060
|
if (new RegExp(HASH_PATTERN).test(digest)) {
|
|
1060
1061
|
hash = digest;
|
|
@@ -1135,7 +1136,7 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
|
|
|
1135
1136
|
*/
|
|
1136
1137
|
export async function createJarBom(path, options) {
|
|
1137
1138
|
let pkgList = [];
|
|
1138
|
-
let jarFiles
|
|
1139
|
+
let jarFiles;
|
|
1139
1140
|
let nsMapping = {};
|
|
1140
1141
|
const parentComponent = createDefaultParentComponent(path, "maven", options);
|
|
1141
1142
|
if (options.useGradleCache) {
|
|
@@ -2267,14 +2268,21 @@ export async function createNodejsBom(path, options) {
|
|
|
2267
2268
|
const pkgData = JSON.parse(readFileSync(`${path}/package.json`, "utf8"));
|
|
2268
2269
|
const mgrData = pkgData.packageManager;
|
|
2269
2270
|
let mgr = "";
|
|
2271
|
+
let installArgs = ["install"];
|
|
2270
2272
|
if (mgrData) {
|
|
2271
2273
|
mgr = mgrData.split("@")[0];
|
|
2272
2274
|
}
|
|
2273
2275
|
if (supPkgMgrs.includes(mgr)) {
|
|
2274
2276
|
pkgMgr = mgr;
|
|
2275
2277
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
+
// Support for passing additional args to the install command
|
|
2279
|
+
if (process.env[`${pkgMgr.toUpperCase()}_INSTALL_ARGS`]) {
|
|
2280
|
+
const addArgs =
|
|
2281
|
+
process.env[`${pkgMgr.toUpperCase()}_INSTALL_ARGS`].split(" ");
|
|
2282
|
+
installArgs = installArgs.concat(addArgs);
|
|
2283
|
+
}
|
|
2284
|
+
console.log(`Executing '${pkgMgr} ${installArgs.join(" ")}' in`, path);
|
|
2285
|
+
const result = spawnSync(pkgMgr, installArgs, {
|
|
2278
2286
|
cwd: path,
|
|
2279
2287
|
encoding: "utf-8",
|
|
2280
2288
|
timeout: TIMEOUT_MS,
|
|
@@ -2661,8 +2669,7 @@ export function createPixiBom(path, options) {
|
|
|
2661
2669
|
// Add parentComponent Details
|
|
2662
2670
|
const pixiTomlMode = existsSync(pixiToml);
|
|
2663
2671
|
if (pixiTomlMode) {
|
|
2664
|
-
|
|
2665
|
-
parentComponent = tmpParentComponent;
|
|
2672
|
+
parentComponent = parsePixiTomlFile(pixiToml);
|
|
2666
2673
|
parentComponent.type = "application";
|
|
2667
2674
|
const ppurl = new PackageURL(
|
|
2668
2675
|
"pixi",
|
|
@@ -3181,7 +3188,7 @@ export async function createGoBom(path, options) {
|
|
|
3181
3188
|
const allImports = {};
|
|
3182
3189
|
let parentComponent = createDefaultParentComponent(path, "golang", options);
|
|
3183
3190
|
// Is this a binary file
|
|
3184
|
-
let maybeBinary
|
|
3191
|
+
let maybeBinary;
|
|
3185
3192
|
try {
|
|
3186
3193
|
maybeBinary = statSync(path).isFile();
|
|
3187
3194
|
} catch (err) {
|
|
@@ -3583,7 +3590,7 @@ export async function createRustBom(path, options) {
|
|
|
3583
3590
|
let pkgList = [];
|
|
3584
3591
|
let parentComponent = {};
|
|
3585
3592
|
// Is this a binary file
|
|
3586
|
-
let maybeBinary
|
|
3593
|
+
let maybeBinary;
|
|
3587
3594
|
try {
|
|
3588
3595
|
maybeBinary = statSync(path).isFile();
|
|
3589
3596
|
} catch (err) {
|
|
@@ -4195,7 +4202,7 @@ export function createCloudBuildBom(path, options) {
|
|
|
4195
4202
|
/**
|
|
4196
4203
|
* Function to create obom string for the current OS using osquery
|
|
4197
4204
|
*
|
|
4198
|
-
* @param {string}
|
|
4205
|
+
* @param {string} _path to the project
|
|
4199
4206
|
* @param {Object} options Parse options from the cli
|
|
4200
4207
|
*/
|
|
4201
4208
|
export function createOSBom(_path, options) {
|
|
@@ -4803,9 +4810,9 @@ export function createPHPBom(path, options) {
|
|
|
4803
4810
|
if (DEBUG_MODE) {
|
|
4804
4811
|
console.log("Parsing version", versionResult.stdout);
|
|
4805
4812
|
}
|
|
4806
|
-
|
|
4813
|
+
let tmpV = undefined;
|
|
4807
4814
|
if (versionResult?.stdout) {
|
|
4808
|
-
versionResult.stdout.split(" ");
|
|
4815
|
+
tmpV = versionResult.stdout.split(" ");
|
|
4809
4816
|
}
|
|
4810
4817
|
if (tmpV && tmpV.length > 1) {
|
|
4811
4818
|
composerVersion = tmpV[1];
|
|
@@ -6427,7 +6434,7 @@ export async function createBom(path, options) {
|
|
|
6427
6434
|
options.path = path;
|
|
6428
6435
|
options.parentComponent = {};
|
|
6429
6436
|
// Create parent component based on the inspect config
|
|
6430
|
-
const inspectData = exportData
|
|
6437
|
+
const inspectData = exportData?.inspectData;
|
|
6431
6438
|
if (
|
|
6432
6439
|
inspectData?.RepoDigests &&
|
|
6433
6440
|
inspectData.RepoTags &&
|
|
@@ -6477,13 +6484,12 @@ export async function createBom(path, options) {
|
|
|
6477
6484
|
}
|
|
6478
6485
|
// Pass the entire export data about the image layers
|
|
6479
6486
|
options.exportData = exportData;
|
|
6480
|
-
options.lastWorkingDir = exportData
|
|
6481
|
-
options.allLayersExplodedDir = exportData
|
|
6482
|
-
|
|
6483
|
-
[...new Set(exportData
|
|
6487
|
+
options.lastWorkingDir = exportData?.lastWorkingDir;
|
|
6488
|
+
options.allLayersExplodedDir = exportData?.allLayersExplodedDir;
|
|
6489
|
+
return await createMultiXBom(
|
|
6490
|
+
[...new Set(exportData?.pkgPathList)],
|
|
6484
6491
|
options,
|
|
6485
6492
|
);
|
|
6486
|
-
return bomData;
|
|
6487
6493
|
}
|
|
6488
6494
|
if (path.endsWith(".war")) {
|
|
6489
6495
|
projectType = ["java"];
|
package/lib/evinser/evinser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
3
|
+
import path, { resolve } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { PackageURL } from "packageurl-js";
|
|
6
6
|
import { Op } from "sequelize";
|
|
@@ -8,6 +8,7 @@ import { findCryptoAlgos } from "../helpers/cbomutils.js";
|
|
|
8
8
|
import * as db from "../helpers/db.js";
|
|
9
9
|
import {
|
|
10
10
|
DEBUG_MODE,
|
|
11
|
+
PROJECT_TYPE_ALIASES,
|
|
11
12
|
collectGradleDependencies,
|
|
12
13
|
collectMvnDependencies,
|
|
13
14
|
executeAtom,
|
|
@@ -16,13 +17,15 @@ import {
|
|
|
16
17
|
getMavenCommand,
|
|
17
18
|
getTimestamp,
|
|
18
19
|
} from "../helpers/utils.js";
|
|
20
|
+
import { createSemanticsSlices } from "./swiftsem.js";
|
|
21
|
+
|
|
19
22
|
const DB_NAME = "evinser.db";
|
|
20
23
|
const typePurlsCache = {};
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Function to create the db for the libraries referred in the sbom.
|
|
24
27
|
*
|
|
25
|
-
* @param {Object} Command line options
|
|
28
|
+
* @param {Object} options Command line options
|
|
26
29
|
*/
|
|
27
30
|
export const prepareDB = async (options) => {
|
|
28
31
|
if (!options.dbPath.includes("memory") && !fs.existsSync(options.dbPath)) {
|
|
@@ -210,11 +213,6 @@ export const createSlice = (
|
|
|
210
213
|
if (!filePath) {
|
|
211
214
|
return;
|
|
212
215
|
}
|
|
213
|
-
console.log(
|
|
214
|
-
`Creating ${sliceType} slice for ${path.resolve(
|
|
215
|
-
filePath,
|
|
216
|
-
)}. Please wait ...`,
|
|
217
|
-
);
|
|
218
216
|
const firstLanguage = Array.isArray(purlOrLanguages)
|
|
219
217
|
? purlOrLanguages[0]
|
|
220
218
|
: purlOrLanguages;
|
|
@@ -224,6 +222,13 @@ export const createSlice = (
|
|
|
224
222
|
if (!language) {
|
|
225
223
|
return undefined;
|
|
226
224
|
}
|
|
225
|
+
if (
|
|
226
|
+
PROJECT_TYPE_ALIASES.swift.includes(language) &&
|
|
227
|
+
sliceType !== "semantics"
|
|
228
|
+
) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
227
232
|
let sliceOutputDir = fs.mkdtempSync(
|
|
228
233
|
path.join(tmpdir(), `atom-${sliceType}-`),
|
|
229
234
|
);
|
|
@@ -234,8 +239,21 @@ export const createSlice = (
|
|
|
234
239
|
? path.basename(options.output)
|
|
235
240
|
: path.dirname(options.output);
|
|
236
241
|
}
|
|
237
|
-
const atomFile = path.join(sliceOutputDir, "app.atom");
|
|
238
242
|
const slicesFile = path.join(sliceOutputDir, `${sliceType}.slices.json`);
|
|
243
|
+
if (sliceType === "semantics") {
|
|
244
|
+
const slicesData = createSemanticsSlices(resolve(filePath), options);
|
|
245
|
+
// Write the semantics slices data
|
|
246
|
+
if (slicesData) {
|
|
247
|
+
fs.writeFileSync(slicesFile, JSON.stringify(slicesData, null, null));
|
|
248
|
+
}
|
|
249
|
+
return { tempDir: sliceOutputDir, slicesFile };
|
|
250
|
+
}
|
|
251
|
+
console.log(
|
|
252
|
+
`Creating ${sliceType} slice for ${path.resolve(
|
|
253
|
+
filePath,
|
|
254
|
+
)}. Please wait ...`,
|
|
255
|
+
);
|
|
256
|
+
const atomFile = path.join(sliceOutputDir, "app.atom");
|
|
239
257
|
let args = [sliceType];
|
|
240
258
|
// Support for crypto slices aka CBOM
|
|
241
259
|
if (sliceType === "reachables" && options.includeCrypto) {
|
|
@@ -331,7 +349,7 @@ export const initFromSbom = (components, language) => {
|
|
|
331
349
|
* Function to analyze the project
|
|
332
350
|
*
|
|
333
351
|
* @param {Object} dbObjMap DB and model instances
|
|
334
|
-
* @param {Object} Command line options
|
|
352
|
+
* @param {Object} options Command line options
|
|
335
353
|
*/
|
|
336
354
|
export const analyzeProject = async (dbObjMap, options) => {
|
|
337
355
|
const dirPath = options._[0] || ".";
|
|
@@ -340,9 +358,11 @@ export const analyzeProject = async (dbObjMap, options) => {
|
|
|
340
358
|
let usageSlice = undefined;
|
|
341
359
|
let dataFlowSlice = undefined;
|
|
342
360
|
let reachablesSlice = undefined;
|
|
361
|
+
let semanticsSlice = undefined;
|
|
343
362
|
let usagesSlicesFile = undefined;
|
|
344
363
|
let dataFlowSlicesFile = undefined;
|
|
345
364
|
let reachablesSlicesFile = undefined;
|
|
365
|
+
let semanticsSlicesFile = undefined;
|
|
346
366
|
let dataFlowFrames = {};
|
|
347
367
|
let servicesMap = {};
|
|
348
368
|
let retMap = {};
|
|
@@ -394,6 +414,29 @@ export const analyzeProject = async (dbObjMap, options) => {
|
|
|
394
414
|
usagesSlicesFile = retMap.slicesFile;
|
|
395
415
|
}
|
|
396
416
|
}
|
|
417
|
+
// Support for semantics slicing
|
|
418
|
+
if (PROJECT_TYPE_ALIASES.swift.includes(language) && components.length) {
|
|
419
|
+
// Reuse existing semantics slices
|
|
420
|
+
if (
|
|
421
|
+
options.semanticsSlicesFile &&
|
|
422
|
+
fs.existsSync(options.semanticsSlicesFile)
|
|
423
|
+
) {
|
|
424
|
+
semanticsSlice = JSON.parse(
|
|
425
|
+
fs.readFileSync(options.semanticsSlicesFile, "utf-8"),
|
|
426
|
+
);
|
|
427
|
+
semanticsSlicesFile = options.semanticsSlicesFile;
|
|
428
|
+
} else {
|
|
429
|
+
// Generate our own slices
|
|
430
|
+
retMap = createSlice(language, dirPath, "semantics", options);
|
|
431
|
+
if (retMap?.slicesFile && fs.existsSync(retMap.slicesFile)) {
|
|
432
|
+
semanticsSlice = JSON.parse(
|
|
433
|
+
fs.readFileSync(retMap.slicesFile, "utf-8"),
|
|
434
|
+
);
|
|
435
|
+
semanticsSlicesFile = retMap.slicesFile;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Parse usage slices
|
|
397
440
|
if (usageSlice && Object.keys(usageSlice).length) {
|
|
398
441
|
const retMap = await parseObjectSlices(
|
|
399
442
|
language,
|
|
@@ -407,6 +450,16 @@ export const analyzeProject = async (dbObjMap, options) => {
|
|
|
407
450
|
servicesMap = retMap.servicesMap;
|
|
408
451
|
userDefinedTypesMap = retMap.userDefinedTypesMap;
|
|
409
452
|
}
|
|
453
|
+
// Parse the semantics slices
|
|
454
|
+
if (
|
|
455
|
+
semanticsSlice &&
|
|
456
|
+
Object.keys(semanticsSlice).length &&
|
|
457
|
+
components.length
|
|
458
|
+
) {
|
|
459
|
+
// Identify the purl locations
|
|
460
|
+
const retMap = parseSemanticSlices(components, semanticsSlice);
|
|
461
|
+
purlLocationMap = retMap.purlLocationMap;
|
|
462
|
+
}
|
|
410
463
|
if (options.withDataFlow) {
|
|
411
464
|
if (
|
|
412
465
|
options.dataFlowSlicesFile &&
|
|
@@ -435,14 +488,15 @@ export const analyzeProject = async (dbObjMap, options) => {
|
|
|
435
488
|
);
|
|
436
489
|
}
|
|
437
490
|
return {
|
|
438
|
-
atomFile: retMap
|
|
491
|
+
atomFile: retMap?.atomFile,
|
|
439
492
|
usagesSlicesFile,
|
|
440
493
|
dataFlowSlicesFile,
|
|
441
494
|
reachablesSlicesFile,
|
|
495
|
+
semanticsSlicesFile,
|
|
442
496
|
purlLocationMap,
|
|
443
497
|
servicesMap,
|
|
444
498
|
dataFlowFrames,
|
|
445
|
-
tempDir: retMap
|
|
499
|
+
tempDir: retMap?.tempDir,
|
|
446
500
|
userDefinedTypesMap,
|
|
447
501
|
cryptoComponents,
|
|
448
502
|
cryptoGeneratePurls,
|
|
@@ -501,7 +555,7 @@ export const parseObjectSlices = async (
|
|
|
501
555
|
*
|
|
502
556
|
* @param {string} language Application language
|
|
503
557
|
* @param {Object} userDefinedTypesMap User Defined types in the application
|
|
504
|
-
* @param {Array}
|
|
558
|
+
* @param {Array} slice Usages array for each objectSlice
|
|
505
559
|
* @param {Object} dbObjMap DB Models
|
|
506
560
|
* @param {Object} purlLocationMap Object to track locations where purls are used
|
|
507
561
|
* @param {Object} purlImportsMap Object to track package urls and their import aliases
|
|
@@ -711,6 +765,127 @@ export const parseSliceUsages = async (
|
|
|
711
765
|
}
|
|
712
766
|
};
|
|
713
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Method to parse semantic slice data
|
|
770
|
+
*
|
|
771
|
+
* @param {Array} components Components from the input SBOM
|
|
772
|
+
* @param {Object} semanticsSlice Semantic slice data
|
|
773
|
+
* @returns {Object} Parsed metadata
|
|
774
|
+
*/
|
|
775
|
+
export function parseSemanticSlices(components, semanticsSlice) {
|
|
776
|
+
const metadata = {};
|
|
777
|
+
// We have two attributes in the semantics slice to expand a given module to its constituent symbols
|
|
778
|
+
// - A less precise buildSymbols, which is obtained by parsing the various output-file-map.json files
|
|
779
|
+
// - A granular and precise moduleInfos, which has the exact classes, protocols, enums etc belonging to each module
|
|
780
|
+
// The objective then is to build the purlLocationMap, which is the list of locations where a given purl is used
|
|
781
|
+
// For this, we have two attributes:
|
|
782
|
+
// - A simpler fileStructures attribute that has the mapping between a swift file and the list of referenced types
|
|
783
|
+
// - A granular fileIndexes attribute that contains information about the clang modules and line number information
|
|
784
|
+
|
|
785
|
+
// We first need to map out the component names to their purls
|
|
786
|
+
// This is because the semantics slice use the module names everywhere
|
|
787
|
+
const componentNamePurlMap = {};
|
|
788
|
+
const componentSymbolsMap = {};
|
|
789
|
+
const allObfuscationsMap = {};
|
|
790
|
+
for (const comp of components) {
|
|
791
|
+
componentNamePurlMap[comp.name] = comp.purl;
|
|
792
|
+
if (!componentSymbolsMap[comp.name]) {
|
|
793
|
+
componentSymbolsMap[comp.name] = new Set();
|
|
794
|
+
}
|
|
795
|
+
if (semanticsSlice?.buildSymbols[comp.name]) {
|
|
796
|
+
for (const asym of semanticsSlice.buildSymbols[comp.name]) {
|
|
797
|
+
componentSymbolsMap[comp.name].add(asym);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const moduleInfo = semanticsSlice?.moduleInfos[comp.name] || {};
|
|
801
|
+
if (moduleInfo.classes) {
|
|
802
|
+
for (const asym of moduleInfo.classes) {
|
|
803
|
+
componentSymbolsMap[comp.name].add(asym);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (moduleInfo.protocols) {
|
|
807
|
+
for (const asym of moduleInfo.protocols) {
|
|
808
|
+
componentSymbolsMap[comp.name].add(asym);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (moduleInfo.enums) {
|
|
812
|
+
for (const asym of moduleInfo.enums) {
|
|
813
|
+
componentSymbolsMap[comp.name].add(asym);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// Now collect the method signatures from class and protocol methods
|
|
817
|
+
if (moduleInfo.classMethods) {
|
|
818
|
+
for (const aclassName of Object.keys(moduleInfo.classMethods)) {
|
|
819
|
+
for (const asym of moduleInfo.classMethods[aclassName]) {
|
|
820
|
+
componentSymbolsMap[comp.name].add(asym);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (moduleInfo.protocolMethods) {
|
|
825
|
+
for (const aclassName of Object.keys(moduleInfo.protocolMethods)) {
|
|
826
|
+
for (const asym of moduleInfo.protocolMethods[aclassName]) {
|
|
827
|
+
componentSymbolsMap[comp.name].add(asym);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Build a large obfuscation map
|
|
832
|
+
if (moduleInfo.obfuscationMap) {
|
|
833
|
+
for (const asym of Object.keys(moduleInfo.obfuscationMap)) {
|
|
834
|
+
allObfuscationsMap[asym] = moduleInfo.obfuscationMap[asym];
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const purlLocationsSet = {};
|
|
839
|
+
// We now have a good set of data in componentSymbolsMap and allObfuscationsMap
|
|
840
|
+
// We can iterate these symbols and check if they exist under fileIndexes.symbolLocations
|
|
841
|
+
for (const compName of Object.keys(componentSymbolsMap)) {
|
|
842
|
+
const compSymbols = Array.from(componentSymbolsMap[compName]);
|
|
843
|
+
const searchHits = searchSymbolLocations(
|
|
844
|
+
compSymbols,
|
|
845
|
+
semanticsSlice?.fileIndexes,
|
|
846
|
+
);
|
|
847
|
+
// We have an occurrence hit. Let's populate the purlLocationMap
|
|
848
|
+
if (searchHits?.length) {
|
|
849
|
+
const locations =
|
|
850
|
+
purlLocationsSet[componentNamePurlMap[compName]] || new Set();
|
|
851
|
+
for (const ahit of searchHits) {
|
|
852
|
+
for (const aline of ahit.lineNumbers) {
|
|
853
|
+
locations.add(`${ahit.file}#${aline}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
purlLocationsSet[componentNamePurlMap[compName]] = locations;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const purlLocationMap = {};
|
|
860
|
+
for (const apurl of Object.keys(purlLocationsSet)) {
|
|
861
|
+
purlLocationMap[apurl] = Array.from(purlLocationsSet[apurl]).sort();
|
|
862
|
+
}
|
|
863
|
+
return { purlLocationMap };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function searchSymbolLocations(compSymbols, fileIndexes) {
|
|
867
|
+
const searchHits = [];
|
|
868
|
+
if (!fileIndexes) {
|
|
869
|
+
return undefined;
|
|
870
|
+
}
|
|
871
|
+
for (const aswiftFile of Object.keys(fileIndexes)) {
|
|
872
|
+
if (!fileIndexes[aswiftFile].symbolLocations) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
for (const asym of Object.keys(fileIndexes[aswiftFile].symbolLocations)) {
|
|
876
|
+
const lineNumbers = fileIndexes[aswiftFile].symbolLocations[asym];
|
|
877
|
+
if (compSymbols.includes(asym)) {
|
|
878
|
+
searchHits.push({
|
|
879
|
+
file: aswiftFile,
|
|
880
|
+
symbol: asym,
|
|
881
|
+
lineNumbers,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return searchHits;
|
|
887
|
+
}
|
|
888
|
+
|
|
714
889
|
export const isFilterableType = (
|
|
715
890
|
language,
|
|
716
891
|
userDefinedTypesMap,
|
|
@@ -786,7 +961,7 @@ export const isFilterableType = (
|
|
|
786
961
|
* Method to detect services from annotation objects in the usage slice
|
|
787
962
|
*
|
|
788
963
|
* @param {string} language Application language
|
|
789
|
-
* @param {Array}
|
|
964
|
+
* @param {Array} slice Usages array for each objectSlice
|
|
790
965
|
* @param {Object} servicesMap Existing service map
|
|
791
966
|
*/
|
|
792
967
|
export const detectServicesFromUsages = (language, slice, servicesMap = {}) => {
|
|
@@ -1124,7 +1299,7 @@ export const createEvinseFile = (sliceArtefacts, options) => {
|
|
|
1124
1299
|
console.log(evinseOutFile, "created successfully.");
|
|
1125
1300
|
} else {
|
|
1126
1301
|
console.log(
|
|
1127
|
-
"Unable to identify component evidence for the input SBOM. Only java, javascript, python, and php projects are supported by evinse.",
|
|
1302
|
+
"Unable to identify component evidence for the input SBOM. Only java, javascript, python, swift, and php projects are supported by evinse.",
|
|
1128
1303
|
);
|
|
1129
1304
|
}
|
|
1130
1305
|
if (tempDir?.startsWith(tmpdir())) {
|
|
@@ -1141,7 +1316,7 @@ export const createEvinseFile = (sliceArtefacts, options) => {
|
|
|
1141
1316
|
* @param {Object} userDefinedTypesMap User Defined types in the application
|
|
1142
1317
|
* @param {Object} dataFlowSlice Data flow slice object from atom
|
|
1143
1318
|
* @param {Object} dbObjMap DB models
|
|
1144
|
-
* @param {Object}
|
|
1319
|
+
* @param {Object} _purlLocationMap Object to track locations where purls are used
|
|
1145
1320
|
* @param {Object} purlImportsMap Object to track package urls and their import aliases
|
|
1146
1321
|
*/
|
|
1147
1322
|
export const collectDataFlowFrames = async (
|
|
@@ -1265,7 +1440,7 @@ export const collectDataFlowFrames = async (
|
|
|
1265
1440
|
*
|
|
1266
1441
|
* Implemented based on the logic proposed here - https://github.com/AppThreat/atom/blob/main/specification/docs/slices.md#data-flow-slice
|
|
1267
1442
|
*
|
|
1268
|
-
* @param {string}
|
|
1443
|
+
* @param {string} _language Application language
|
|
1269
1444
|
* @param {Object} reachablesSlice Reachables slice object from atom
|
|
1270
1445
|
*/
|
|
1271
1446
|
export const collectReachableFrames = (_language, reachablesSlice) => {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
constructServiceName,
|
|
5
5
|
detectServicesFromUsages,
|
|
6
6
|
extractEndpoints,
|
|
7
|
+
parseSemanticSlices,
|
|
7
8
|
} from "./evinser.js";
|
|
8
9
|
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
@@ -91,3 +92,18 @@ test("extract endpoints test", () => {
|
|
|
91
92
|
),
|
|
92
93
|
).toEqual(["/{accountName}"]);
|
|
93
94
|
});
|
|
95
|
+
|
|
96
|
+
test("parseSemanticSlices", () => {
|
|
97
|
+
const semanticsSlice = JSON.parse(
|
|
98
|
+
readFileSync("./test/data/swiftsem/semantics.slices.json", {
|
|
99
|
+
encoding: "utf-8",
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
const bomJson = JSON.parse(
|
|
103
|
+
readFileSync("./test/data/swiftsem/bom-hakit.json", {
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
const retMap = parseSemanticSlices(bomJson.components, semanticsSlice);
|
|
108
|
+
expect(retMap).toBeDefined();
|
|
109
|
+
});
|