@cyclonedx/cdxgen 9.11.6 → 10.0.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 +6 -5
- package/analyzer.js +1 -0
- package/bin/cdxgen.js +140 -142
- package/bin/repl.js +5 -5
- package/bin/verify.js +1 -1
- package/binary.js +19 -13
- package/cbomutils.js +39 -0
- package/cbomutils.test.js +8 -0
- package/data/README.md +1 -0
- package/data/cbomosdb-queries.json +68 -0
- package/data/cosdb-queries.json +1 -1
- package/display.js +2 -2
- package/docker.js +4 -2
- package/envcontext.js +302 -0
- package/envcontext.test.js +31 -0
- package/evinser.js +9 -8
- package/index.js +229 -486
- package/package.json +7 -8
- package/protobom.test.js +1 -1
- package/server.js +2 -1
- package/utils.js +212 -159
- package/utils.test.js +37 -32
- package/validator.js +5 -4
package/data/cosdb-queries.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"purlType": "deb"
|
|
6
6
|
},
|
|
7
7
|
"portage_packages": {
|
|
8
|
-
"query": "select * from portage_packages where
|
|
8
|
+
"query": "select * from portage_packages where package like '%dev%' OR package like '%header%';",
|
|
9
9
|
"description": "Retrieves all the installed packages on the target Linux system.",
|
|
10
10
|
"purlType": "ebuild"
|
|
11
11
|
},
|
package/display.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { createStream, table } from "table";
|
|
3
3
|
|
|
4
4
|
// https://github.com/yangshun/tree-node-cli/blob/master/src/index.js
|
|
@@ -166,7 +166,7 @@ export const printCallStack = (bomJson) => {
|
|
|
166
166
|
)
|
|
167
167
|
)
|
|
168
168
|
).sort(locationComparator);
|
|
169
|
-
|
|
169
|
+
const frameDisplay = [frames[0]];
|
|
170
170
|
if (frames.length > 1) {
|
|
171
171
|
for (let i = 1; i < frames.length - 1; i++) {
|
|
172
172
|
frameDisplay.push(`${SYMBOLS_ANSI.BRANCH} ${frames[i]}`);
|
package/docker.js
CHANGED
|
@@ -2,6 +2,8 @@ import got from "got";
|
|
|
2
2
|
import { globSync } from "glob";
|
|
3
3
|
import { parse } from "node:url";
|
|
4
4
|
import stream from "node:stream/promises";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
5
7
|
import {
|
|
6
8
|
existsSync,
|
|
7
9
|
readdirSync,
|
|
@@ -498,7 +500,7 @@ export const getImage = async (fullImageName) => {
|
|
|
498
500
|
let localData = undefined;
|
|
499
501
|
let pullData = undefined;
|
|
500
502
|
const { registry, repo, tag, digest } = parseImageName(fullImageName);
|
|
501
|
-
|
|
503
|
+
const repoWithTag =
|
|
502
504
|
registry && registry !== "docker.io"
|
|
503
505
|
? fullImageName
|
|
504
506
|
: `${repo}:${tag !== "" ? tag : ":latest"}`;
|
|
@@ -718,7 +720,7 @@ export const extractTar = async (fullImageName, dir) => {
|
|
|
718
720
|
"Please run cdxgen from a powershell terminal with admin privileges to create symlinks."
|
|
719
721
|
);
|
|
720
722
|
console.log(err);
|
|
721
|
-
} else if (err.code
|
|
723
|
+
} else if (!["TAR_BAD_ARCHIVE", "TAR_ENTRY_INFO"].includes(err.code)) {
|
|
722
724
|
console.log(
|
|
723
725
|
`Error while extracting image ${fullImageName} to ${dir}. Please file this bug to the cdxgen repo. https://github.com/CycloneDX/cdxgen/issues`
|
|
724
726
|
);
|
package/envcontext.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
isWin,
|
|
4
|
+
PYTHON_CMD,
|
|
5
|
+
JAVA_CMD,
|
|
6
|
+
DOTNET_CMD,
|
|
7
|
+
NODE_CMD,
|
|
8
|
+
NPM_CMD,
|
|
9
|
+
GCC_CMD,
|
|
10
|
+
GO_CMD,
|
|
11
|
+
RUSTC_CMD,
|
|
12
|
+
CARGO_CMD
|
|
13
|
+
} from "./utils.js";
|
|
14
|
+
import process from "node:process";
|
|
15
|
+
import { Buffer } from "node:buffer";
|
|
16
|
+
|
|
17
|
+
const GIT_COMMAND = process.env.GIT_CMD || "git";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves a git config item
|
|
21
|
+
* @param {string} configKey Git config key
|
|
22
|
+
* @param {string} dir repo directory
|
|
23
|
+
*
|
|
24
|
+
* @returns Output from git config or undefined
|
|
25
|
+
*/
|
|
26
|
+
export const getGitConfig = (configKey, dir) => {
|
|
27
|
+
return execGitCommand(dir, ["config", "--get", configKey]);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Retrieves the git origin url
|
|
32
|
+
* @param {string} dir repo directory
|
|
33
|
+
*
|
|
34
|
+
* @returns Output from git config or undefined
|
|
35
|
+
*/
|
|
36
|
+
export const getOriginUrl = (dir) => {
|
|
37
|
+
return getGitConfig("remote.origin.url", dir);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Retrieves the git branch name
|
|
42
|
+
* @param {string} configKey Git config key
|
|
43
|
+
* @param {string} dir repo directory
|
|
44
|
+
*
|
|
45
|
+
* @returns Output from git config or undefined
|
|
46
|
+
*/
|
|
47
|
+
export const getBranch = (configKey, dir) => {
|
|
48
|
+
return execGitCommand(dir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Retrieves the files list from git
|
|
53
|
+
* @param {string} dir repo directory
|
|
54
|
+
*
|
|
55
|
+
* @returns Output from git config or undefined
|
|
56
|
+
*/
|
|
57
|
+
export const listFiles = (dir) => {
|
|
58
|
+
const filesList = [];
|
|
59
|
+
const output = execGitCommand(dir, [
|
|
60
|
+
"ls-tree",
|
|
61
|
+
"-l",
|
|
62
|
+
"-r",
|
|
63
|
+
"--full-tree",
|
|
64
|
+
"HEAD"
|
|
65
|
+
]);
|
|
66
|
+
if (output) {
|
|
67
|
+
output.split("\n").forEach((l) => {
|
|
68
|
+
l = l.replace("\r", "");
|
|
69
|
+
if (l === "\n" || l.startsWith("#")) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const tmpA = l.split(" ");
|
|
73
|
+
if (tmpA && tmpA.length >= 5) {
|
|
74
|
+
const lastParts = tmpA[tmpA.length - 1].split("\t");
|
|
75
|
+
filesList.push({
|
|
76
|
+
hash: tmpA[2],
|
|
77
|
+
name: lastParts[lastParts.length - 1]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return filesList;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute a git command
|
|
87
|
+
*
|
|
88
|
+
* @param {string} dir Repo directory
|
|
89
|
+
* @param {Array} args arguments to git command
|
|
90
|
+
*
|
|
91
|
+
* @returns Output from the git command
|
|
92
|
+
*/
|
|
93
|
+
export const execGitCommand = (dir, args) => {
|
|
94
|
+
return getCommandOutput(GIT_COMMAND, dir, args);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Collect Java version and installed modules
|
|
99
|
+
*
|
|
100
|
+
* @param {string} dir Working directory
|
|
101
|
+
* @returns Object containing the java details
|
|
102
|
+
*/
|
|
103
|
+
export const collectJavaInfo = (dir) => {
|
|
104
|
+
const versionDesc = getCommandOutput(JAVA_CMD, dir, ["--version"]);
|
|
105
|
+
const moduleDesc = getCommandOutput(JAVA_CMD, dir, ["--list-modules"]) || "";
|
|
106
|
+
if (versionDesc) {
|
|
107
|
+
return {
|
|
108
|
+
type: "platform",
|
|
109
|
+
name: "java",
|
|
110
|
+
version: versionDesc.split("\n")[0].replace("java ", ""),
|
|
111
|
+
description: versionDesc,
|
|
112
|
+
properties: [
|
|
113
|
+
{
|
|
114
|
+
name: "java:modules",
|
|
115
|
+
value: moduleDesc.replaceAll("\n", ", ")
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collect dotnet version
|
|
125
|
+
*
|
|
126
|
+
* @param {string} dir Working directory
|
|
127
|
+
* @returns Object containing dotnet details
|
|
128
|
+
*/
|
|
129
|
+
export const collectDotnetInfo = (dir) => {
|
|
130
|
+
const versionDesc = getCommandOutput(DOTNET_CMD, dir, ["--version"]);
|
|
131
|
+
const moduleDesc =
|
|
132
|
+
getCommandOutput(DOTNET_CMD, dir, ["--list-runtimes"]) || "";
|
|
133
|
+
if (versionDesc) {
|
|
134
|
+
return {
|
|
135
|
+
type: "platform",
|
|
136
|
+
name: "dotnet",
|
|
137
|
+
version: versionDesc.trim(),
|
|
138
|
+
description: moduleDesc.replaceAll("\n", "\\n")
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Collect python version
|
|
146
|
+
*
|
|
147
|
+
* @param {string} dir Working directory
|
|
148
|
+
* @returns Object containing python details
|
|
149
|
+
*/
|
|
150
|
+
export const collectPythonInfo = (dir) => {
|
|
151
|
+
const versionDesc = getCommandOutput(PYTHON_CMD, dir, ["--version"]);
|
|
152
|
+
const moduleDesc =
|
|
153
|
+
getCommandOutput(PYTHON_CMD, dir, ["-m", "pip", "--version"]) || "";
|
|
154
|
+
if (versionDesc) {
|
|
155
|
+
return {
|
|
156
|
+
type: "platform",
|
|
157
|
+
name: "python",
|
|
158
|
+
version: versionDesc.replace("Python ", ""),
|
|
159
|
+
description: moduleDesc.replaceAll("\n", "\\n")
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Collect node version
|
|
167
|
+
*
|
|
168
|
+
* @param {string} dir Working directory
|
|
169
|
+
* @returns Object containing node details
|
|
170
|
+
*/
|
|
171
|
+
export const collectNodeInfo = (dir) => {
|
|
172
|
+
const versionDesc = getCommandOutput(NODE_CMD, dir, ["--version"]);
|
|
173
|
+
let moduleDesc = getCommandOutput(NPM_CMD, dir, ["--version"]);
|
|
174
|
+
if (moduleDesc) {
|
|
175
|
+
moduleDesc = `npm: ${moduleDesc}`;
|
|
176
|
+
}
|
|
177
|
+
if (versionDesc) {
|
|
178
|
+
return {
|
|
179
|
+
type: "platform",
|
|
180
|
+
name: "node",
|
|
181
|
+
version: versionDesc.trim(),
|
|
182
|
+
description: moduleDesc
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Collect gcc version
|
|
190
|
+
*
|
|
191
|
+
* @param {string} dir Working directory
|
|
192
|
+
* @returns Object containing gcc details
|
|
193
|
+
*/
|
|
194
|
+
export const collectGccInfo = (dir) => {
|
|
195
|
+
const versionDesc = getCommandOutput(GCC_CMD, dir, ["--version"]);
|
|
196
|
+
const moduleDesc = getCommandOutput(GCC_CMD, dir, ["-print-search-dirs"]);
|
|
197
|
+
if (versionDesc) {
|
|
198
|
+
return {
|
|
199
|
+
type: "platform",
|
|
200
|
+
name: "gcc",
|
|
201
|
+
version: versionDesc.split("\n")[0],
|
|
202
|
+
description: moduleDesc.replaceAll("\n", "\\n")
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Collect rust version
|
|
210
|
+
*
|
|
211
|
+
* @param {string} dir Working directory
|
|
212
|
+
* @returns Object containing rust details
|
|
213
|
+
*/
|
|
214
|
+
export const collectRustInfo = (dir) => {
|
|
215
|
+
const versionDesc = getCommandOutput(RUSTC_CMD, dir, ["--version"]);
|
|
216
|
+
const moduleDesc = getCommandOutput(CARGO_CMD, dir, ["--version"]);
|
|
217
|
+
if (versionDesc) {
|
|
218
|
+
return {
|
|
219
|
+
type: "platform",
|
|
220
|
+
name: "rustc",
|
|
221
|
+
version: versionDesc.trim(),
|
|
222
|
+
description: moduleDesc.trim()
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Collect go version
|
|
230
|
+
*
|
|
231
|
+
* @param {string} dir Working directory
|
|
232
|
+
* @returns Object containing go details
|
|
233
|
+
*/
|
|
234
|
+
export const collectGoInfo = (dir) => {
|
|
235
|
+
const versionDesc = getCommandOutput(GO_CMD, dir, ["version"]);
|
|
236
|
+
if (versionDesc) {
|
|
237
|
+
return {
|
|
238
|
+
type: "platform",
|
|
239
|
+
name: "go",
|
|
240
|
+
version: versionDesc.trim()
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const collectEnvInfo = (dir) => {
|
|
247
|
+
const infoComponents = [];
|
|
248
|
+
let cmp = collectJavaInfo(dir);
|
|
249
|
+
if (cmp) {
|
|
250
|
+
infoComponents.push(cmp);
|
|
251
|
+
}
|
|
252
|
+
cmp = collectDotnetInfo(dir);
|
|
253
|
+
if (cmp) {
|
|
254
|
+
infoComponents.push(cmp);
|
|
255
|
+
}
|
|
256
|
+
cmp = collectPythonInfo(dir);
|
|
257
|
+
if (cmp) {
|
|
258
|
+
infoComponents.push(cmp);
|
|
259
|
+
}
|
|
260
|
+
cmp = collectNodeInfo(dir);
|
|
261
|
+
if (cmp) {
|
|
262
|
+
infoComponents.push(cmp);
|
|
263
|
+
}
|
|
264
|
+
cmp = collectGccInfo(dir);
|
|
265
|
+
if (cmp) {
|
|
266
|
+
infoComponents.push(cmp);
|
|
267
|
+
}
|
|
268
|
+
cmp = collectRustInfo(dir);
|
|
269
|
+
if (cmp) {
|
|
270
|
+
infoComponents.push(cmp);
|
|
271
|
+
}
|
|
272
|
+
cmp = collectGoInfo(dir);
|
|
273
|
+
if (cmp) {
|
|
274
|
+
infoComponents.push(cmp);
|
|
275
|
+
}
|
|
276
|
+
return infoComponents;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Execute any command to retrieve the output
|
|
281
|
+
*
|
|
282
|
+
* @param {*} cmd Command to execute
|
|
283
|
+
* @param {*} dir working directory
|
|
284
|
+
* @param {*} args arguments
|
|
285
|
+
* @returns String output from the command or undefined in case of error
|
|
286
|
+
*/
|
|
287
|
+
const getCommandOutput = (cmd, dir, args) => {
|
|
288
|
+
const result = spawnSync(cmd, args, {
|
|
289
|
+
cwd: dir,
|
|
290
|
+
encoding: "utf-8",
|
|
291
|
+
shell: isWin
|
|
292
|
+
});
|
|
293
|
+
if (result.status !== 0 || result.error) {
|
|
294
|
+
return undefined;
|
|
295
|
+
} else {
|
|
296
|
+
const stdout = result.stdout;
|
|
297
|
+
if (stdout) {
|
|
298
|
+
const cmdOutput = Buffer.from(stdout).toString();
|
|
299
|
+
return cmdOutput.trim();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { expect, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getBranch,
|
|
5
|
+
getOriginUrl,
|
|
6
|
+
listFiles,
|
|
7
|
+
collectJavaInfo,
|
|
8
|
+
collectDotnetInfo,
|
|
9
|
+
collectPythonInfo,
|
|
10
|
+
collectNodeInfo,
|
|
11
|
+
collectGccInfo,
|
|
12
|
+
collectRustInfo,
|
|
13
|
+
collectGoInfo
|
|
14
|
+
} from "./envcontext.js";
|
|
15
|
+
|
|
16
|
+
test("git tests", () => {
|
|
17
|
+
expect(getBranch()).toBeDefined();
|
|
18
|
+
expect(getOriginUrl()).toBeDefined();
|
|
19
|
+
const files = listFiles();
|
|
20
|
+
expect(files.length).toBeGreaterThan(10);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("tools tests", () => {
|
|
24
|
+
expect(collectJavaInfo()).toBeDefined();
|
|
25
|
+
expect(collectDotnetInfo()).toBeDefined();
|
|
26
|
+
expect(collectPythonInfo()).toBeDefined();
|
|
27
|
+
expect(collectNodeInfo()).toBeDefined();
|
|
28
|
+
expect(collectGccInfo()).toBeDefined();
|
|
29
|
+
expect(collectRustInfo()).toBeDefined();
|
|
30
|
+
expect(collectGoInfo()).toBeDefined();
|
|
31
|
+
});
|
package/evinser.js
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
getGradleCommand,
|
|
5
5
|
getMavenCommand,
|
|
6
6
|
collectGradleDependencies,
|
|
7
|
-
collectMvnDependencies
|
|
7
|
+
collectMvnDependencies,
|
|
8
|
+
DEBUG_MODE
|
|
8
9
|
} from "./utils.js";
|
|
9
10
|
import { tmpdir } from "node:os";
|
|
10
11
|
import path from "node:path";
|
|
@@ -98,7 +99,7 @@ export const catalogMavenDeps = async (
|
|
|
98
99
|
if (fs.existsSync(path.join(dirPath, "bom.json.map"))) {
|
|
99
100
|
try {
|
|
100
101
|
const mapData = JSON.parse(
|
|
101
|
-
fs.readFileSync(path.join(dirPath, "bom.json.map"))
|
|
102
|
+
fs.readFileSync(path.join(dirPath, "bom.json.map"), "utf-8")
|
|
102
103
|
);
|
|
103
104
|
if (mapData && Object.keys(mapData).length) {
|
|
104
105
|
jarNSMapping = mapData;
|
|
@@ -111,7 +112,7 @@ export const catalogMavenDeps = async (
|
|
|
111
112
|
console.log("About to collect jar dependencies for the path", dirPath);
|
|
112
113
|
const mavenCmd = getMavenCommand(dirPath, dirPath);
|
|
113
114
|
// collect all jars including from the cache if data-flow mode is enabled
|
|
114
|
-
jarNSMapping = collectMvnDependencies(
|
|
115
|
+
jarNSMapping = await collectMvnDependencies(
|
|
115
116
|
mavenCmd,
|
|
116
117
|
dirPath,
|
|
117
118
|
false,
|
|
@@ -145,7 +146,7 @@ export const catalogGradleDeps = async (dirPath, purlsJars, Namespaces) => {
|
|
|
145
146
|
);
|
|
146
147
|
const gradleCmd = getGradleCommand(dirPath, dirPath);
|
|
147
148
|
// collect all jars including from the cache if data-flow mode is enabled
|
|
148
|
-
const jarNSMapping = collectGradleDependencies(
|
|
149
|
+
const jarNSMapping = await collectGradleDependencies(
|
|
149
150
|
gradleCmd,
|
|
150
151
|
dirPath,
|
|
151
152
|
false,
|
|
@@ -1110,7 +1111,7 @@ export const collectDataFlowFrames = async (
|
|
|
1110
1111
|
}
|
|
1111
1112
|
const paths = dataFlowSlice?.paths || [];
|
|
1112
1113
|
for (const apath of paths) {
|
|
1113
|
-
|
|
1114
|
+
const aframe = [];
|
|
1114
1115
|
let referredPurls = new Set();
|
|
1115
1116
|
for (const nid of apath) {
|
|
1116
1117
|
const theNode = nodeCache[nid];
|
|
@@ -1163,7 +1164,7 @@ export const collectDataFlowFrames = async (
|
|
|
1163
1164
|
referredPurls.add(ns.purl);
|
|
1164
1165
|
}
|
|
1165
1166
|
typePurlsCache[typeFullName] = nsHits;
|
|
1166
|
-
} else {
|
|
1167
|
+
} else if (DEBUG_MODE) {
|
|
1167
1168
|
console.log("Unable to identify purl for", typeFullName);
|
|
1168
1169
|
}
|
|
1169
1170
|
}
|
|
@@ -1213,14 +1214,14 @@ export const collectDataFlowFrames = async (
|
|
|
1213
1214
|
* @param {string} language Application language
|
|
1214
1215
|
* @param {object} reachablesSlice Reachables slice object from atom
|
|
1215
1216
|
*/
|
|
1216
|
-
export const collectReachableFrames =
|
|
1217
|
+
export const collectReachableFrames = (language, reachablesSlice) => {
|
|
1217
1218
|
const reachableNodes = reachablesSlice?.reachables || [];
|
|
1218
1219
|
// purl key and an array of frames array
|
|
1219
1220
|
// CycloneDX 1.5 currently accepts only 1 frame as evidence
|
|
1220
1221
|
// so this method is more future-proof
|
|
1221
1222
|
const dfFrames = {};
|
|
1222
1223
|
for (const anode of reachableNodes) {
|
|
1223
|
-
|
|
1224
|
+
const aframe = [];
|
|
1224
1225
|
let referredPurls = new Set(anode.purls || []);
|
|
1225
1226
|
for (const fnode of anode.flows) {
|
|
1226
1227
|
if (!fnode.parentFileName || fnode.parentFileName === "<unknown>") {
|