@cyclonedx/cdxgen 12.3.2 → 12.4.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 +70 -22
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +238 -116
- package/bin/convert.js +28 -13
- package/bin/hbom.js +490 -0
- package/bin/repl.js +580 -29
- package/bin/validate.js +34 -4
- package/bin/verify.js +40 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -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 +171 -15
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +76 -5
- 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 +36 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +209 -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 +647 -127
- package/lib/cli/index.poku.js +1905 -187
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/agentFormulationParser.js +6 -2
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/analyzer.js +1444 -38
- package/lib/helpers/analyzer.poku.js +409 -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/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +336 -23
- package/lib/helpers/display.poku.js +179 -43
- 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/mcpConfigParser.js +21 -5
- package/lib/helpers/mcpConfigParser.poku.js +39 -2
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +349 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/propertySanitizer.js +121 -0
- package/lib/helpers/protobom.js +156 -45
- package/lib/helpers/protobom.poku.js +140 -5
- 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 +2454 -198
- package/lib/helpers/utils.poku.js +1798 -74
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2306 -350
- package/lib/managers/binary.poku.js +1700 -1
- package/lib/managers/docker.js +441 -95
- package/lib/managers/docker.poku.js +1479 -14
- package/lib/server/server.js +2 -24
- package/lib/server/server.poku.js +36 -1
- 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 +2967 -990
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +192 -1
- package/lib/stages/postgen/postgen.poku.js +321 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/package.json +24 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- 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/agentFormulationParser.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/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- 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 +62 -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/mcpConfigParser.d.ts +1 -1
- package/types/lib/helpers/mcpConfigParser.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/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +3 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- 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 +74 -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 +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -0
- 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/data/spdx-model-v3.0.1.jsonld +0 -15999
package/lib/helpers/utils.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
mkdirSync,
|
|
12
12
|
mkdtempSync,
|
|
13
13
|
readFileSync,
|
|
14
|
+
realpathSync,
|
|
14
15
|
rmSync,
|
|
15
16
|
unlinkSync,
|
|
16
17
|
writeFileSync,
|
|
@@ -55,17 +56,25 @@ import { getTreeWithPlugin } from "../managers/piptree.js";
|
|
|
55
56
|
import { IriValidationStrategy, validateIri } from "../parsers/iri.js";
|
|
56
57
|
import Arborist from "../third-party/arborist/lib/index.js";
|
|
57
58
|
import { analyzeSuspiciousJsFile } from "./analyzer.js";
|
|
59
|
+
import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "./auditCategories.js";
|
|
58
60
|
import { parseWorkflowFile } from "./ciParsers/githubActions.js";
|
|
59
61
|
import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
|
|
62
|
+
import {
|
|
63
|
+
createOccurrenceEvidence,
|
|
64
|
+
parseOccurrenceEvidenceLocation,
|
|
65
|
+
} from "./evidenceUtils.js";
|
|
66
|
+
import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
|
|
60
67
|
import { thoughtLog, traceLog } from "./logger.js";
|
|
61
68
|
import { createLolbasProperties } from "./lolbas.js";
|
|
62
69
|
import {
|
|
70
|
+
createOsQueryFallbackBomRef,
|
|
63
71
|
createOsQueryPurl,
|
|
64
72
|
deriveOsQueryDescription,
|
|
65
73
|
deriveOsQueryName,
|
|
66
74
|
deriveOsQueryPublisher,
|
|
67
75
|
deriveOsQueryVersion,
|
|
68
76
|
sanitizeOsQueryIdentity,
|
|
77
|
+
shouldCreateOsQueryPurl,
|
|
69
78
|
} from "./osqueryTransform.js";
|
|
70
79
|
import {
|
|
71
80
|
collectPyLockFileComponents,
|
|
@@ -116,6 +125,596 @@ export const DRY_RUN_ERROR_CODE = "CDXGEN_DRY_RUN";
|
|
|
116
125
|
const activityLedger = [];
|
|
117
126
|
let activityCounter = 0;
|
|
118
127
|
let currentActivityContext = {};
|
|
128
|
+
const dryRunReadTraceState =
|
|
129
|
+
globalThis.__cdxgenDryRunReadTraceState ||
|
|
130
|
+
(globalThis.__cdxgenDryRunReadTraceState = {
|
|
131
|
+
environmentReads: new Map(),
|
|
132
|
+
observations: new Map(),
|
|
133
|
+
recordActivity: undefined,
|
|
134
|
+
sensitiveFileReads: new Map(),
|
|
135
|
+
});
|
|
136
|
+
const SENSITIVE_ENV_VAR_PATTERN =
|
|
137
|
+
/(^|_)(?:token|key|secret|pass(?:word)?|credential(?:s)?|cred|auth|session|cookie|email|user)$/i;
|
|
138
|
+
const DIRECTORY_DISCOVERY_NAMES = new Set([
|
|
139
|
+
".cargo",
|
|
140
|
+
".docker",
|
|
141
|
+
".gem",
|
|
142
|
+
".github",
|
|
143
|
+
".m2",
|
|
144
|
+
".nuget",
|
|
145
|
+
".venv",
|
|
146
|
+
".yarn",
|
|
147
|
+
"blobs",
|
|
148
|
+
"extensions",
|
|
149
|
+
"node_modules",
|
|
150
|
+
"target",
|
|
151
|
+
"vendor",
|
|
152
|
+
]);
|
|
153
|
+
const LOCKFILE_ACTIVITY_HINTS = new Map([
|
|
154
|
+
[
|
|
155
|
+
"bun.lock",
|
|
156
|
+
{ classification: "lockfile", ecosystem: "bun", label: "Bun lockfile" },
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"cargo.lock",
|
|
160
|
+
{ classification: "lockfile", ecosystem: "cargo", label: "Cargo lockfile" },
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"composer.lock",
|
|
164
|
+
{
|
|
165
|
+
classification: "lockfile",
|
|
166
|
+
ecosystem: "composer",
|
|
167
|
+
label: "Composer lockfile",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
"gemfile.lock",
|
|
172
|
+
{
|
|
173
|
+
classification: "lockfile",
|
|
174
|
+
ecosystem: "rubygems",
|
|
175
|
+
label: "Bundler lockfile",
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
"package-lock.json",
|
|
180
|
+
{ classification: "lockfile", ecosystem: "npm", label: "npm lockfile" },
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
"packages.lock.json",
|
|
184
|
+
{ classification: "lockfile", ecosystem: "nuget", label: "NuGet lockfile" },
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
"pdm.lock",
|
|
188
|
+
{ classification: "lockfile", ecosystem: "python", label: "PDM lockfile" },
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
"pnpm-lock.yaml",
|
|
192
|
+
{ classification: "lockfile", ecosystem: "pnpm", label: "pnpm lockfile" },
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
"poetry.lock",
|
|
196
|
+
{
|
|
197
|
+
classification: "lockfile",
|
|
198
|
+
ecosystem: "python",
|
|
199
|
+
label: "Poetry lockfile",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
"podfile.lock",
|
|
204
|
+
{
|
|
205
|
+
classification: "lockfile",
|
|
206
|
+
ecosystem: "cocoapods",
|
|
207
|
+
label: "CocoaPods lockfile",
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
"pylock.toml",
|
|
212
|
+
{
|
|
213
|
+
classification: "lockfile",
|
|
214
|
+
ecosystem: "python",
|
|
215
|
+
label: "PEP 751 lockfile",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
"uv.lock",
|
|
220
|
+
{ classification: "lockfile", ecosystem: "python", label: "uv lockfile" },
|
|
221
|
+
],
|
|
222
|
+
[
|
|
223
|
+
"yarn.lock",
|
|
224
|
+
{ classification: "lockfile", ecosystem: "yarn", label: "Yarn lockfile" },
|
|
225
|
+
],
|
|
226
|
+
]);
|
|
227
|
+
const MANIFEST_ACTIVITY_HINTS = new Map([
|
|
228
|
+
[
|
|
229
|
+
"cargo.toml",
|
|
230
|
+
{ classification: "manifest", ecosystem: "cargo", label: "Cargo manifest" },
|
|
231
|
+
],
|
|
232
|
+
[
|
|
233
|
+
"composer.json",
|
|
234
|
+
{
|
|
235
|
+
classification: "manifest",
|
|
236
|
+
ecosystem: "composer",
|
|
237
|
+
label: "Composer manifest",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
[
|
|
241
|
+
"gemfile",
|
|
242
|
+
{
|
|
243
|
+
classification: "manifest",
|
|
244
|
+
ecosystem: "rubygems",
|
|
245
|
+
label: "Gem manifest",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
"package.json",
|
|
250
|
+
{ classification: "manifest", ecosystem: "npm", label: "package manifest" },
|
|
251
|
+
],
|
|
252
|
+
[
|
|
253
|
+
"pom.xml",
|
|
254
|
+
{ classification: "manifest", ecosystem: "maven", label: "Maven manifest" },
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
"pyproject.toml",
|
|
258
|
+
{
|
|
259
|
+
classification: "manifest",
|
|
260
|
+
ecosystem: "python",
|
|
261
|
+
label: "Python project manifest",
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
[
|
|
265
|
+
"requirements.txt",
|
|
266
|
+
{
|
|
267
|
+
classification: "manifest",
|
|
268
|
+
ecosystem: "python",
|
|
269
|
+
label: "Python requirements manifest",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
"setup.py",
|
|
274
|
+
{
|
|
275
|
+
classification: "manifest",
|
|
276
|
+
ecosystem: "python",
|
|
277
|
+
label: "Python setup manifest",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
]);
|
|
281
|
+
const SENSITIVE_CONFIG_ACTIVITY_HINTS = [
|
|
282
|
+
{
|
|
283
|
+
matcher: (lowerPath, _baseName) =>
|
|
284
|
+
lowerPath.includes("/.cargo/config.toml") ||
|
|
285
|
+
lowerPath.endsWith("/.cargo/credentials") ||
|
|
286
|
+
lowerPath.endsWith("/.cargo/credentials.toml"),
|
|
287
|
+
metadata: {
|
|
288
|
+
classification: "config",
|
|
289
|
+
ecosystem: "cargo",
|
|
290
|
+
label: "Cargo registry configuration",
|
|
291
|
+
sensitive: true,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
matcher: (lowerPath, baseName) =>
|
|
296
|
+
lowerPath.includes("/.docker/config.json") ||
|
|
297
|
+
(baseName === "config.json" && lowerPath.includes("/docker")),
|
|
298
|
+
metadata: {
|
|
299
|
+
classification: "credential",
|
|
300
|
+
ecosystem: "oci",
|
|
301
|
+
label: "Docker credential file",
|
|
302
|
+
sensitive: true,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
matcher: (lowerPath) => lowerPath.endsWith("/.gem/credentials"),
|
|
307
|
+
metadata: {
|
|
308
|
+
classification: "credential",
|
|
309
|
+
ecosystem: "rubygems",
|
|
310
|
+
label: "RubyGems credentials file",
|
|
311
|
+
sensitive: true,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
matcher: (_lowerPath, baseName) =>
|
|
316
|
+
baseName === ".npmrc" || baseName === ".pnpmrc" || baseName === ".yarnrc",
|
|
317
|
+
metadata: {
|
|
318
|
+
classification: "config",
|
|
319
|
+
ecosystem: "npm",
|
|
320
|
+
label: "JavaScript package manager configuration",
|
|
321
|
+
sensitive: true,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
matcher: (_lowerPath, baseName) => baseName === ".yarnrc.yml",
|
|
326
|
+
metadata: {
|
|
327
|
+
classification: "config",
|
|
328
|
+
ecosystem: "yarn",
|
|
329
|
+
label: "Yarn configuration",
|
|
330
|
+
sensitive: true,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
matcher: (_lowerPath, baseName) =>
|
|
335
|
+
baseName === ".pypirc" || baseName === "pip.conf",
|
|
336
|
+
metadata: {
|
|
337
|
+
classification: "config",
|
|
338
|
+
ecosystem: "python",
|
|
339
|
+
label: "Python package publishing configuration",
|
|
340
|
+
sensitive: true,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
matcher: (_lowerPath, baseName) =>
|
|
345
|
+
baseName === "uv.toml" || baseName === "poetry.toml",
|
|
346
|
+
metadata: {
|
|
347
|
+
classification: "config",
|
|
348
|
+
ecosystem: "python",
|
|
349
|
+
label: "Python package manager configuration",
|
|
350
|
+
sensitive: true,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
matcher: (_lowerPath, baseName) => baseName === "nuget.config",
|
|
355
|
+
metadata: {
|
|
356
|
+
classification: "config",
|
|
357
|
+
ecosystem: "nuget",
|
|
358
|
+
label: "NuGet configuration",
|
|
359
|
+
sensitive: true,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
matcher: (_lowerPath, baseName) => baseName === "settings.xml",
|
|
364
|
+
metadata: {
|
|
365
|
+
classification: "config",
|
|
366
|
+
ecosystem: "maven",
|
|
367
|
+
label: "Maven settings.xml",
|
|
368
|
+
sensitive: true,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
const CERTIFICATE_FILE_EXTENSIONS = new Set([".crt", ".cer", ".pem"]);
|
|
373
|
+
const KEY_FILE_EXTENSIONS = new Set([
|
|
374
|
+
".key",
|
|
375
|
+
".jks",
|
|
376
|
+
".keystore",
|
|
377
|
+
".p12",
|
|
378
|
+
".pfx",
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
const buildReadCountSuffix = (count) => (count > 1 ? ` (${count} times)` : "");
|
|
382
|
+
|
|
383
|
+
const buildEnvironmentReadReason = (varName, count, sensitive) =>
|
|
384
|
+
`Read ${sensitive ? "sensitive " : ""}environment variable ${varName}${buildReadCountSuffix(count)}.`;
|
|
385
|
+
|
|
386
|
+
const buildSensitiveFileReadReason = (filePath, count, label) =>
|
|
387
|
+
`Read ${label} ${filePath}${buildReadCountSuffix(count)}.`;
|
|
388
|
+
|
|
389
|
+
function emitActivity(activity) {
|
|
390
|
+
if (typeof dryRunReadTraceState.recordActivity !== "function") {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
return dryRunReadTraceState.recordActivity(activity);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function classifyActivityPath(filePath) {
|
|
397
|
+
if (typeof filePath !== "string" || !filePath.length) {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
401
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
402
|
+
const baseName = basename(lowerPath);
|
|
403
|
+
if (LOCKFILE_ACTIVITY_HINTS.has(baseName)) {
|
|
404
|
+
return LOCKFILE_ACTIVITY_HINTS.get(baseName);
|
|
405
|
+
}
|
|
406
|
+
if (MANIFEST_ACTIVITY_HINTS.has(baseName)) {
|
|
407
|
+
return MANIFEST_ACTIVITY_HINTS.get(baseName);
|
|
408
|
+
}
|
|
409
|
+
for (const { matcher, metadata } of SENSITIVE_CONFIG_ACTIVITY_HINTS) {
|
|
410
|
+
if (matcher(lowerPath, baseName)) {
|
|
411
|
+
return metadata;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
lowerPath.includes("/cache/") ||
|
|
416
|
+
lowerPath.includes("/.cache/") ||
|
|
417
|
+
lowerPath.includes("/caches/")
|
|
418
|
+
) {
|
|
419
|
+
return {
|
|
420
|
+
classification: "cache",
|
|
421
|
+
label: "cache path",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (
|
|
425
|
+
CERTIFICATE_FILE_EXTENSIONS.has(extname(baseName)) ||
|
|
426
|
+
baseName === "cert.pem"
|
|
427
|
+
) {
|
|
428
|
+
return {
|
|
429
|
+
classification: "certificate",
|
|
430
|
+
label: "certificate file",
|
|
431
|
+
sensitive: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (
|
|
435
|
+
KEY_FILE_EXTENSIONS.has(extname(baseName)) ||
|
|
436
|
+
baseName === "key.pem" ||
|
|
437
|
+
baseName.startsWith("id_")
|
|
438
|
+
) {
|
|
439
|
+
return {
|
|
440
|
+
classification: "key",
|
|
441
|
+
label: "private key file",
|
|
442
|
+
sensitive: true,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const trimmedPath = normalizedPath.endsWith("/")
|
|
446
|
+
? normalizedPath.slice(0, -1)
|
|
447
|
+
: normalizedPath;
|
|
448
|
+
const directoryName = basename(trimmedPath.toLowerCase());
|
|
449
|
+
if (DIRECTORY_DISCOVERY_NAMES.has(directoryName)) {
|
|
450
|
+
return {
|
|
451
|
+
classification: "directory",
|
|
452
|
+
label: "directory discovery path",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function classifyDiscoveryPattern(pattern) {
|
|
459
|
+
const patternValue = Array.isArray(pattern)
|
|
460
|
+
? pattern.join(",")
|
|
461
|
+
: String(pattern);
|
|
462
|
+
const lowerPattern = patternValue.toLowerCase();
|
|
463
|
+
if (
|
|
464
|
+
lowerPattern.includes("package-lock.json") ||
|
|
465
|
+
lowerPattern.includes("pnpm-lock.yaml") ||
|
|
466
|
+
lowerPattern.includes("yarn.lock") ||
|
|
467
|
+
lowerPattern.includes("poetry.lock") ||
|
|
468
|
+
lowerPattern.includes("uv.lock") ||
|
|
469
|
+
lowerPattern.includes("cargo.lock") ||
|
|
470
|
+
lowerPattern.includes("gemfile.lock")
|
|
471
|
+
) {
|
|
472
|
+
return {
|
|
473
|
+
discoveryType: "lockfile-discovery",
|
|
474
|
+
label: "lockfile discovery",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (
|
|
478
|
+
lowerPattern.includes("package.json") ||
|
|
479
|
+
lowerPattern.includes("pom.xml") ||
|
|
480
|
+
lowerPattern.includes("pyproject.toml") ||
|
|
481
|
+
lowerPattern.includes("cargo.toml") ||
|
|
482
|
+
lowerPattern.includes("composer.json")
|
|
483
|
+
) {
|
|
484
|
+
return {
|
|
485
|
+
discoveryType: "manifest-discovery",
|
|
486
|
+
label: "manifest discovery",
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
discoveryType: "directory-enumeration",
|
|
491
|
+
label: "directory enumeration",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function recordDeduplicatedRead(traceMap, traceKey, activity, createReason) {
|
|
496
|
+
const existingTrace = traceMap.get(traceKey);
|
|
497
|
+
if (existingTrace) {
|
|
498
|
+
existingTrace.count += 1;
|
|
499
|
+
if (existingTrace.entry) {
|
|
500
|
+
existingTrace.entry.count = existingTrace.count;
|
|
501
|
+
existingTrace.entry.reason = createReason(existingTrace.count);
|
|
502
|
+
}
|
|
503
|
+
return existingTrace.entry;
|
|
504
|
+
}
|
|
505
|
+
const entry = emitActivity({
|
|
506
|
+
...activity,
|
|
507
|
+
reason: createReason(1),
|
|
508
|
+
});
|
|
509
|
+
if (entry) {
|
|
510
|
+
entry.count = 1;
|
|
511
|
+
}
|
|
512
|
+
traceMap.set(traceKey, {
|
|
513
|
+
count: 1,
|
|
514
|
+
entry,
|
|
515
|
+
});
|
|
516
|
+
return entry;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function isSensitiveEnvironmentVariableName(varName) {
|
|
520
|
+
return typeof varName === "string" && SENSITIVE_ENV_VAR_PATTERN.test(varName);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function recordObservedActivity(kind, target, options = {}) {
|
|
524
|
+
if (!(isDryRun || DEBUG_MODE) || !kind || !target) {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
const status = options.status || "completed";
|
|
528
|
+
const traceKey =
|
|
529
|
+
options.traceKey ||
|
|
530
|
+
`${kind}:${status}:${target}:${options.traceDetail || ""}`;
|
|
531
|
+
const metadata = options.metadata || {};
|
|
532
|
+
const reasonBuilder =
|
|
533
|
+
options.reasonBuilder ||
|
|
534
|
+
((count) =>
|
|
535
|
+
options.reason
|
|
536
|
+
? `${options.reason}${buildReadCountSuffix(count)}`
|
|
537
|
+
: `Recorded ${kind} activity for ${target}${buildReadCountSuffix(count)}.`);
|
|
538
|
+
return recordDeduplicatedRead(
|
|
539
|
+
dryRunReadTraceState.observations,
|
|
540
|
+
traceKey,
|
|
541
|
+
{
|
|
542
|
+
kind,
|
|
543
|
+
status,
|
|
544
|
+
target,
|
|
545
|
+
...metadata,
|
|
546
|
+
},
|
|
547
|
+
reasonBuilder,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function recordDecisionActivity(target, options = {}) {
|
|
552
|
+
return recordObservedActivity(options.kind || "decision", target, options);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function recordDiscoveryActivity(target, options = {}) {
|
|
556
|
+
return recordObservedActivity(options.kind || "discover", target, options);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function recordPolicyActivity(target, options = {}) {
|
|
560
|
+
return recordObservedActivity(options.kind || "policy", target, options);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function normalizeRecordedPathForComparison(
|
|
564
|
+
candidatePath,
|
|
565
|
+
basePath = undefined,
|
|
566
|
+
) {
|
|
567
|
+
if (typeof candidatePath !== "string" || !candidatePath.length) {
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
let normalizedPath = candidatePath.replaceAll("\\", "/");
|
|
571
|
+
if (basePath && path.isAbsolute(candidatePath)) {
|
|
572
|
+
const resolvedBasePath = resolve(basePath);
|
|
573
|
+
const normalizedBasePath = resolvedBasePath.replaceAll("\\", "/");
|
|
574
|
+
const isWithinBasePath = (candidate) => {
|
|
575
|
+
const normalizedCandidate = candidate.replaceAll("\\", "/");
|
|
576
|
+
return (
|
|
577
|
+
normalizedCandidate === normalizedBasePath ||
|
|
578
|
+
normalizedCandidate.startsWith(`${normalizedBasePath}/`)
|
|
579
|
+
);
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const resolvedCandidatePath = resolve(candidatePath);
|
|
583
|
+
if (isWithinBasePath(resolvedCandidatePath)) {
|
|
584
|
+
normalizedPath = relative(
|
|
585
|
+
resolvedBasePath,
|
|
586
|
+
resolvedCandidatePath,
|
|
587
|
+
).replaceAll("\\", "/");
|
|
588
|
+
} else {
|
|
589
|
+
const rebasedCandidatePath = resolve(
|
|
590
|
+
resolvedBasePath,
|
|
591
|
+
candidatePath.replace(/^([A-Za-z]:)?[\\/]+/, ""),
|
|
592
|
+
);
|
|
593
|
+
if (isWithinBasePath(rebasedCandidatePath)) {
|
|
594
|
+
normalizedPath = relative(
|
|
595
|
+
resolvedBasePath,
|
|
596
|
+
rebasedCandidatePath,
|
|
597
|
+
).replaceAll("\\", "/");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return normalizedPath;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function recordSymlinkResolution(
|
|
605
|
+
sourcePath,
|
|
606
|
+
resolvedPath,
|
|
607
|
+
options = {},
|
|
608
|
+
) {
|
|
609
|
+
const normalizedSourcePath = normalizeRecordedPathForComparison(
|
|
610
|
+
sourcePath,
|
|
611
|
+
options.basePath,
|
|
612
|
+
);
|
|
613
|
+
const normalizedResolvedPath = normalizeRecordedPathForComparison(
|
|
614
|
+
resolvedPath,
|
|
615
|
+
options.basePath,
|
|
616
|
+
);
|
|
617
|
+
const status = options.status || "completed";
|
|
618
|
+
if (
|
|
619
|
+
!normalizedSourcePath ||
|
|
620
|
+
(status === "completed" &&
|
|
621
|
+
(!normalizedResolvedPath ||
|
|
622
|
+
normalizedSourcePath === normalizedResolvedPath))
|
|
623
|
+
) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
const metadata = {
|
|
627
|
+
capability: "symlink-resolution",
|
|
628
|
+
...(normalizedResolvedPath ? { resolvedPath: normalizedResolvedPath } : {}),
|
|
629
|
+
...(options.errorCode ? { errorCode: options.errorCode } : {}),
|
|
630
|
+
...(options.metadata || {}),
|
|
631
|
+
};
|
|
632
|
+
return recordObservedActivity("symlink-resolution", normalizedSourcePath, {
|
|
633
|
+
metadata,
|
|
634
|
+
reason:
|
|
635
|
+
options.reason ||
|
|
636
|
+
(status === "failed"
|
|
637
|
+
? `Failed to resolve symlink ${normalizedSourcePath}.`
|
|
638
|
+
: `Resolved symlink ${normalizedSourcePath} to ${normalizedResolvedPath}.`),
|
|
639
|
+
status,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function getArchiveSourceByteSize(sourcePath) {
|
|
644
|
+
if (!sourcePath || !safeExistsSync(sourcePath)) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const sourceStats = lstatSync(sourcePath);
|
|
649
|
+
return sourceStats.isFile() ? sourceStats.size : undefined;
|
|
650
|
+
} catch {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function recordEnvironmentRead(varName, options = {}) {
|
|
656
|
+
// Read tracing intentionally mirrors the activity ledger's dry-run/debug behavior.
|
|
657
|
+
if (!(isDryRun || DEBUG_MODE) || !varName) {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
const source = options.source || "process.env";
|
|
661
|
+
const sensitive =
|
|
662
|
+
options.sensitive ?? isSensitiveEnvironmentVariableName(varName);
|
|
663
|
+
const status = options.status || "completed";
|
|
664
|
+
const traceKey = `${source}:${varName}:${status}`;
|
|
665
|
+
const target = `${source}:${varName}`;
|
|
666
|
+
return recordDeduplicatedRead(
|
|
667
|
+
dryRunReadTraceState.environmentReads,
|
|
668
|
+
traceKey,
|
|
669
|
+
{
|
|
670
|
+
kind: "env",
|
|
671
|
+
redacted: sensitive,
|
|
672
|
+
secretCategory: sensitive ? "environment-variable" : undefined,
|
|
673
|
+
sensitive,
|
|
674
|
+
status,
|
|
675
|
+
target,
|
|
676
|
+
},
|
|
677
|
+
(count) =>
|
|
678
|
+
options.reason || buildEnvironmentReadReason(varName, count, sensitive),
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function recordSensitiveFileRead(filePath, options = {}) {
|
|
683
|
+
// Read tracing intentionally mirrors the activity ledger's dry-run/debug behavior.
|
|
684
|
+
if (!(isDryRun || DEBUG_MODE) || !filePath) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
const kind = options.kind || "read";
|
|
688
|
+
const pathMetadata = classifyActivityPath(filePath) || {};
|
|
689
|
+
const label = options.label || pathMetadata.label || "sensitive file";
|
|
690
|
+
const status = options.status || "completed";
|
|
691
|
+
const traceKey = `${kind}:${status}:${filePath}`;
|
|
692
|
+
return recordDeduplicatedRead(
|
|
693
|
+
dryRunReadTraceState.sensitiveFileReads,
|
|
694
|
+
traceKey,
|
|
695
|
+
{
|
|
696
|
+
classification: pathMetadata.classification,
|
|
697
|
+
ecosystem: pathMetadata.ecosystem,
|
|
698
|
+
kind,
|
|
699
|
+
redacted: pathMetadata.sensitive ?? true,
|
|
700
|
+
secretCategory:
|
|
701
|
+
pathMetadata.classification === "key"
|
|
702
|
+
? "private-key"
|
|
703
|
+
: pathMetadata.classification === "certificate"
|
|
704
|
+
? "certificate"
|
|
705
|
+
: "credential-file",
|
|
706
|
+
status,
|
|
707
|
+
target: filePath,
|
|
708
|
+
},
|
|
709
|
+
(count) =>
|
|
710
|
+
options.reason || buildSensitiveFileReadReason(filePath, count, label),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function readEnvironmentVariable(varName, options = {}) {
|
|
715
|
+
recordEnvironmentRead(varName, options);
|
|
716
|
+
return process.env[varName];
|
|
717
|
+
}
|
|
119
718
|
|
|
120
719
|
export function setDryRunMode(enabled) {
|
|
121
720
|
isDryRun = !!enabled;
|
|
@@ -160,10 +759,8 @@ export function recordActivity(activity) {
|
|
|
160
759
|
const identifier = `ACT-${String(++activityCounter).padStart(4, "0")}`;
|
|
161
760
|
const entry = {
|
|
162
761
|
identifier,
|
|
762
|
+
...currentActivityContext,
|
|
163
763
|
timestamp: new Date().toISOString(),
|
|
164
|
-
projectType: currentActivityContext.projectType,
|
|
165
|
-
packageType: currentActivityContext.packageType,
|
|
166
|
-
sourcePath: currentActivityContext.sourcePath,
|
|
167
764
|
...activity,
|
|
168
765
|
};
|
|
169
766
|
activityLedger.push(entry);
|
|
@@ -171,6 +768,8 @@ export function recordActivity(activity) {
|
|
|
171
768
|
return entry;
|
|
172
769
|
}
|
|
173
770
|
|
|
771
|
+
dryRunReadTraceState.recordActivity = recordActivity;
|
|
772
|
+
|
|
174
773
|
export function getRecordedActivities() {
|
|
175
774
|
return [...activityLedger];
|
|
176
775
|
}
|
|
@@ -178,11 +777,21 @@ export function getRecordedActivities() {
|
|
|
178
777
|
export function resetRecordedActivities() {
|
|
179
778
|
activityLedger.length = 0;
|
|
180
779
|
activityCounter = 0;
|
|
780
|
+
dryRunReadTraceState.environmentReads.clear();
|
|
781
|
+
dryRunReadTraceState.observations.clear();
|
|
782
|
+
dryRunReadTraceState.sensitiveFileReads.clear();
|
|
181
783
|
}
|
|
182
784
|
|
|
183
|
-
function recordFilesystemActivity(
|
|
785
|
+
function recordFilesystemActivity(
|
|
786
|
+
kind,
|
|
787
|
+
target,
|
|
788
|
+
status,
|
|
789
|
+
reason = undefined,
|
|
790
|
+
metadata = {},
|
|
791
|
+
) {
|
|
184
792
|
return recordActivity({
|
|
185
793
|
kind,
|
|
794
|
+
...metadata,
|
|
186
795
|
reason,
|
|
187
796
|
status,
|
|
188
797
|
target,
|
|
@@ -217,13 +826,40 @@ function hasWritePermission(filePath) {
|
|
|
217
826
|
* @Boolean True if the path exists. False otherwise
|
|
218
827
|
*/
|
|
219
828
|
export function safeExistsSync(filePath) {
|
|
829
|
+
const pathMetadata = classifyActivityPath(filePath);
|
|
220
830
|
if (!hasReadPermission(filePath)) {
|
|
221
831
|
if (DEBUG_MODE) {
|
|
222
832
|
console.log("cdxgen lacks read permission for a requested path.");
|
|
223
833
|
}
|
|
834
|
+
if (pathMetadata) {
|
|
835
|
+
recordPolicyActivity(filePath, {
|
|
836
|
+
metadata: {
|
|
837
|
+
classification: pathMetadata.classification,
|
|
838
|
+
ecosystem: pathMetadata.ecosystem,
|
|
839
|
+
policyType: "fs.read",
|
|
840
|
+
},
|
|
841
|
+
reason: `Denied inspection of ${pathMetadata.label} ${filePath} due to missing fs.read permission.`,
|
|
842
|
+
status: "blocked",
|
|
843
|
+
});
|
|
844
|
+
}
|
|
224
845
|
return false;
|
|
225
846
|
}
|
|
226
|
-
|
|
847
|
+
const exists = existsSync(filePath);
|
|
848
|
+
if (pathMetadata) {
|
|
849
|
+
const inspectionKind =
|
|
850
|
+
pathMetadata.classification === "directory" ? "discover" : "inspect";
|
|
851
|
+
recordObservedActivity(inspectionKind, filePath, {
|
|
852
|
+
metadata: {
|
|
853
|
+
classification: pathMetadata.classification,
|
|
854
|
+
ecosystem: pathMetadata.ecosystem,
|
|
855
|
+
exists,
|
|
856
|
+
redacted: pathMetadata.sensitive ?? false,
|
|
857
|
+
},
|
|
858
|
+
reasonBuilder: (count) =>
|
|
859
|
+
`${exists ? "Inspected" : "Checked for"} ${pathMetadata.label} ${filePath}${buildReadCountSuffix(count)}.`,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return exists;
|
|
227
863
|
}
|
|
228
864
|
|
|
229
865
|
export function safeWriteSync(filePath, data, options) {
|
|
@@ -248,9 +884,8 @@ export function safeWriteSync(filePath, data, options) {
|
|
|
248
884
|
);
|
|
249
885
|
return undefined;
|
|
250
886
|
}
|
|
251
|
-
|
|
887
|
+
writeFileSync(filePath, data, options);
|
|
252
888
|
recordFilesystemActivity("write", filePath, "completed");
|
|
253
|
-
return result;
|
|
254
889
|
}
|
|
255
890
|
|
|
256
891
|
/**
|
|
@@ -282,24 +917,32 @@ export function safeMkdirSync(filePath, options) {
|
|
|
282
917
|
);
|
|
283
918
|
return undefined;
|
|
284
919
|
}
|
|
285
|
-
|
|
920
|
+
mkdirSync(filePath, options);
|
|
286
921
|
recordFilesystemActivity("mkdir", filePath, "completed");
|
|
287
|
-
return result;
|
|
288
922
|
}
|
|
289
923
|
|
|
290
924
|
export function safeMkdtempSync(prefix, options = undefined) {
|
|
925
|
+
const resourceType =
|
|
926
|
+
typeof prefix === "string" && prefix.toLowerCase().includes("cache")
|
|
927
|
+
? "cache"
|
|
928
|
+
: "temporary-workspace";
|
|
291
929
|
if (isDryRun) {
|
|
292
930
|
const tempPath = `${prefix}${randomUUID().replaceAll("-", "").slice(0, 6)}`;
|
|
293
931
|
recordFilesystemActivity(
|
|
294
932
|
"temp-dir",
|
|
295
933
|
tempPath,
|
|
296
934
|
"blocked",
|
|
297
|
-
|
|
935
|
+
`Dry run mode blocks temporary directory creation for ${resourceType}.`,
|
|
936
|
+
{
|
|
937
|
+
resourceType,
|
|
938
|
+
},
|
|
298
939
|
);
|
|
299
940
|
return tempPath;
|
|
300
941
|
}
|
|
301
942
|
const tempPath = mkdtempSync(prefix, options);
|
|
302
|
-
recordFilesystemActivity("temp-dir", tempPath, "completed"
|
|
943
|
+
recordFilesystemActivity("temp-dir", tempPath, "completed", undefined, {
|
|
944
|
+
resourceType,
|
|
945
|
+
});
|
|
303
946
|
return tempPath;
|
|
304
947
|
}
|
|
305
948
|
|
|
@@ -313,9 +956,8 @@ export function safeRmSync(filePath, options = undefined) {
|
|
|
313
956
|
);
|
|
314
957
|
return undefined;
|
|
315
958
|
}
|
|
316
|
-
|
|
959
|
+
rmSync(filePath, options);
|
|
317
960
|
recordFilesystemActivity("cleanup", filePath, "completed");
|
|
318
|
-
return result;
|
|
319
961
|
}
|
|
320
962
|
|
|
321
963
|
export function safeUnlinkSync(filePath) {
|
|
@@ -328,9 +970,8 @@ export function safeUnlinkSync(filePath) {
|
|
|
328
970
|
);
|
|
329
971
|
return undefined;
|
|
330
972
|
}
|
|
331
|
-
|
|
973
|
+
unlinkSync(filePath);
|
|
332
974
|
recordFilesystemActivity("cleanup", filePath, "completed");
|
|
333
|
-
return result;
|
|
334
975
|
}
|
|
335
976
|
|
|
336
977
|
export function safeCopyFileSync(src, dest, mode = undefined) {
|
|
@@ -356,32 +997,69 @@ export async function safeExtractArchive(
|
|
|
356
997
|
targetPath,
|
|
357
998
|
extractor,
|
|
358
999
|
kind = "unzip",
|
|
1000
|
+
options = undefined,
|
|
359
1001
|
) {
|
|
1002
|
+
const traceArchiveStats = isDryRun || DEBUG_MODE;
|
|
1003
|
+
const sourceBytes = traceArchiveStats
|
|
1004
|
+
? getArchiveSourceByteSize(sourcePath)
|
|
1005
|
+
: undefined;
|
|
360
1006
|
if (isDryRun) {
|
|
361
1007
|
recordActivity({
|
|
1008
|
+
archiveKind: kind,
|
|
1009
|
+
capability: "archive-extraction",
|
|
362
1010
|
kind,
|
|
363
|
-
|
|
1011
|
+
...(options?.metadata || {}),
|
|
1012
|
+
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
1013
|
+
reason:
|
|
1014
|
+
options?.blockedReason ||
|
|
1015
|
+
`Dry run mode blocks ${kind} extraction from ${sourcePath} into ${targetPath}.`,
|
|
364
1016
|
status: "blocked",
|
|
365
1017
|
target: `${sourcePath} -> ${targetPath}`,
|
|
366
1018
|
});
|
|
367
1019
|
return false;
|
|
368
1020
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
await extractor();
|
|
1023
|
+
recordActivity({
|
|
1024
|
+
archiveKind: kind,
|
|
1025
|
+
capability: "archive-extraction",
|
|
1026
|
+
kind,
|
|
1027
|
+
...(options?.metadata || {}),
|
|
1028
|
+
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
1029
|
+
status: "completed",
|
|
1030
|
+
target: `${sourcePath} -> ${targetPath}`,
|
|
1031
|
+
});
|
|
1032
|
+
return true;
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
recordActivity({
|
|
1035
|
+
archiveKind: kind,
|
|
1036
|
+
capability: "archive-extraction",
|
|
1037
|
+
kind,
|
|
1038
|
+
...(options?.metadata || {}),
|
|
1039
|
+
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
1040
|
+
...(error?.code ? { errorCode: error.code } : {}),
|
|
1041
|
+
reason:
|
|
1042
|
+
options?.failureReason ||
|
|
1043
|
+
`Failed ${kind} extraction from ${sourcePath} into ${targetPath}: ${error.message}`,
|
|
1044
|
+
status: "failed",
|
|
1045
|
+
target: `${sourcePath} -> ${targetPath}`,
|
|
1046
|
+
});
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
376
1049
|
}
|
|
377
1050
|
|
|
378
1051
|
export const commandsExecuted = new Set();
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1052
|
+
function isAllowedCommand(
|
|
1053
|
+
command,
|
|
1054
|
+
allowedCommandsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS"),
|
|
1055
|
+
) {
|
|
1056
|
+
if (!allowedCommandsEnv) {
|
|
382
1057
|
return true;
|
|
383
1058
|
}
|
|
384
|
-
return
|
|
1059
|
+
return allowedCommandsEnv
|
|
1060
|
+
.split(",")
|
|
1061
|
+
.map((entry) => entry.trim())
|
|
1062
|
+
.includes(command.trim());
|
|
385
1063
|
}
|
|
386
1064
|
|
|
387
1065
|
const ALLOWED_WRAPPERS = new Set(["gradlew", "mvnw"]);
|
|
@@ -428,6 +1106,71 @@ function isWindowsShellHijackRisk(command, options) {
|
|
|
428
1106
|
return false;
|
|
429
1107
|
}
|
|
430
1108
|
|
|
1109
|
+
const VERSION_PROBE_ARGS = new Set(["--version", "-version", "version"]);
|
|
1110
|
+
|
|
1111
|
+
function detectProbeType(command, args = []) {
|
|
1112
|
+
const normalizedCommand = basename(String(command || "")).toLowerCase();
|
|
1113
|
+
const normalizedArgs = (args || []).map((arg) => String(arg).toLowerCase());
|
|
1114
|
+
if (
|
|
1115
|
+
normalizedArgs.some((arg) => VERSION_PROBE_ARGS.has(arg)) ||
|
|
1116
|
+
(normalizedArgs.length === 1 && normalizedArgs[0] === "-v")
|
|
1117
|
+
) {
|
|
1118
|
+
return "version-check";
|
|
1119
|
+
}
|
|
1120
|
+
if (normalizedCommand === "which" || normalizedArgs.includes("--help")) {
|
|
1121
|
+
return "capability-probe";
|
|
1122
|
+
}
|
|
1123
|
+
if (
|
|
1124
|
+
normalizedCommand.startsWith("python") &&
|
|
1125
|
+
normalizedArgs.includes("-c") &&
|
|
1126
|
+
normalizedArgs.some((arg) => arg.includes("import"))
|
|
1127
|
+
) {
|
|
1128
|
+
return "runtime-probe";
|
|
1129
|
+
}
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function buildCommandActivityDescriptor(command, args, options) {
|
|
1134
|
+
const target = `${command}${args?.length ? ` ${args.join(" ")}` : ""}`;
|
|
1135
|
+
const cdxgenActivity = options?.cdxgenActivity || {};
|
|
1136
|
+
const probeType = cdxgenActivity.probeType || detectProbeType(command, args);
|
|
1137
|
+
const metadata = {
|
|
1138
|
+
...(cdxgenActivity.metadata || {}),
|
|
1139
|
+
};
|
|
1140
|
+
if (probeType) {
|
|
1141
|
+
metadata.capability = metadata.capability || "tool-runtime-probe";
|
|
1142
|
+
metadata.probeType = probeType;
|
|
1143
|
+
}
|
|
1144
|
+
if (cdxgenActivity.gitOperation) {
|
|
1145
|
+
metadata.gitOperation = cdxgenActivity.gitOperation;
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
blockedReason:
|
|
1149
|
+
cdxgenActivity.blockedReason ||
|
|
1150
|
+
(probeType
|
|
1151
|
+
? `Dry run mode blocks ${probeType.replaceAll("-", " ")} command execution.`
|
|
1152
|
+
: "Dry run mode blocks child process execution."),
|
|
1153
|
+
kind: cdxgenActivity.kind || "execute",
|
|
1154
|
+
metadata,
|
|
1155
|
+
target: cdxgenActivity.target || target,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function getOutputByteSize(value, encoding = "utf-8") {
|
|
1160
|
+
if (value === undefined || value === null) {
|
|
1161
|
+
return 0;
|
|
1162
|
+
}
|
|
1163
|
+
if (Buffer.isBuffer(value)) {
|
|
1164
|
+
return value.length;
|
|
1165
|
+
}
|
|
1166
|
+
if (ArrayBuffer.isView(value)) {
|
|
1167
|
+
return value.byteLength;
|
|
1168
|
+
}
|
|
1169
|
+
const safeEncoding =
|
|
1170
|
+
typeof encoding === "string" && encoding !== "buffer" ? encoding : "utf8";
|
|
1171
|
+
return Buffer.byteLength(String(value), safeEncoding);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
431
1174
|
/**
|
|
432
1175
|
* Safe wrapper around spawnSync that enforces permission checks, injects default
|
|
433
1176
|
* options (maxBuffer, encoding, timeout), warns about unsafe Python and pip/uv
|
|
@@ -439,17 +1182,49 @@ function isWindowsShellHijackRisk(command, options) {
|
|
|
439
1182
|
* @returns {Object} spawnSync result object with status, stdout, stderr, and error fields
|
|
440
1183
|
*/
|
|
441
1184
|
export function safeSpawnSync(command, args, options) {
|
|
1185
|
+
const activityDescriptor = buildCommandActivityDescriptor(
|
|
1186
|
+
command,
|
|
1187
|
+
args,
|
|
1188
|
+
options,
|
|
1189
|
+
);
|
|
1190
|
+
const allowedCommandsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_COMMANDS");
|
|
1191
|
+
const commandAllowed = isAllowedCommand(command, allowedCommandsEnv);
|
|
1192
|
+
if (allowedCommandsEnv) {
|
|
1193
|
+
recordPolicyActivity(command, {
|
|
1194
|
+
metadata: {
|
|
1195
|
+
allowed: commandAllowed,
|
|
1196
|
+
allowlist: allowedCommandsEnv,
|
|
1197
|
+
policyType: "command-allowlist",
|
|
1198
|
+
},
|
|
1199
|
+
reason: `${commandAllowed ? "Allowed" : "Blocked"} command ${command} against CDXGEN_ALLOWED_COMMANDS.`,
|
|
1200
|
+
status: commandAllowed ? "completed" : "blocked",
|
|
1201
|
+
traceDetail: "allowlist",
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
if (isSecureMode && process.permission) {
|
|
1205
|
+
const hasChildPermission = process.permission.has("child");
|
|
1206
|
+
recordPolicyActivity(command, {
|
|
1207
|
+
metadata: {
|
|
1208
|
+
allowed: hasChildPermission,
|
|
1209
|
+
policyType: "child-process",
|
|
1210
|
+
},
|
|
1211
|
+
reason: `${hasChildPermission ? "Confirmed" : "Denied"} child-process permission for ${command}.`,
|
|
1212
|
+
status: hasChildPermission ? "completed" : "blocked",
|
|
1213
|
+
traceDetail: "child-permission",
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
442
1216
|
if (isDryRun) {
|
|
443
1217
|
const error = createDryRunError(
|
|
444
1218
|
"execute",
|
|
445
1219
|
command,
|
|
446
|
-
|
|
1220
|
+
activityDescriptor.blockedReason,
|
|
447
1221
|
);
|
|
448
1222
|
recordActivity({
|
|
449
|
-
kind:
|
|
1223
|
+
kind: activityDescriptor.kind,
|
|
1224
|
+
...activityDescriptor.metadata,
|
|
450
1225
|
reason: error.message,
|
|
451
1226
|
status: "blocked",
|
|
452
|
-
target:
|
|
1227
|
+
target: activityDescriptor.target,
|
|
453
1228
|
});
|
|
454
1229
|
return {
|
|
455
1230
|
status: 1,
|
|
@@ -460,16 +1235,17 @@ export function safeSpawnSync(command, args, options) {
|
|
|
460
1235
|
}
|
|
461
1236
|
if (
|
|
462
1237
|
(isSecureMode && process.permission && !process.permission.has("child")) ||
|
|
463
|
-
!
|
|
1238
|
+
!commandAllowed
|
|
464
1239
|
) {
|
|
465
1240
|
if (DEBUG_MODE) {
|
|
466
1241
|
console.log(`cdxgen lacks execute permission for ${command}`);
|
|
467
1242
|
}
|
|
468
1243
|
recordActivity({
|
|
469
|
-
kind:
|
|
1244
|
+
kind: activityDescriptor.kind,
|
|
1245
|
+
...activityDescriptor.metadata,
|
|
470
1246
|
reason: "cdxgen lacks execute permission for this command.",
|
|
471
1247
|
status: "blocked",
|
|
472
|
-
target:
|
|
1248
|
+
target: activityDescriptor.target,
|
|
473
1249
|
});
|
|
474
1250
|
return {
|
|
475
1251
|
status: 1,
|
|
@@ -483,10 +1259,11 @@ export function safeSpawnSync(command, args, options) {
|
|
|
483
1259
|
const blockedReason = `${command} matches local file in cwd (Windows shell hijack risk)`;
|
|
484
1260
|
console.warn(`\x1b[1;31mSecurity Alert: ${blockedReason}\x1b[0m`);
|
|
485
1261
|
recordActivity({
|
|
486
|
-
kind:
|
|
1262
|
+
kind: activityDescriptor.kind,
|
|
1263
|
+
...activityDescriptor.metadata,
|
|
487
1264
|
reason: blockedReason,
|
|
488
1265
|
status: "blocked",
|
|
489
|
-
target:
|
|
1266
|
+
target: activityDescriptor.target,
|
|
490
1267
|
});
|
|
491
1268
|
return {
|
|
492
1269
|
status: 1,
|
|
@@ -505,6 +1282,11 @@ export function safeSpawnSync(command, args, options) {
|
|
|
505
1282
|
}
|
|
506
1283
|
if (!options) {
|
|
507
1284
|
options = {};
|
|
1285
|
+
} else if (options.cdxgenActivity) {
|
|
1286
|
+
options = {
|
|
1287
|
+
...options,
|
|
1288
|
+
};
|
|
1289
|
+
delete options.cdxgenActivity;
|
|
508
1290
|
}
|
|
509
1291
|
// Inject maxBuffer
|
|
510
1292
|
if (!options.maxBuffer) {
|
|
@@ -604,10 +1386,13 @@ export function safeSpawnSync(command, args, options) {
|
|
|
604
1386
|
}
|
|
605
1387
|
const result = spawnSync(command, args, options);
|
|
606
1388
|
recordActivity({
|
|
607
|
-
kind:
|
|
1389
|
+
kind: activityDescriptor.kind,
|
|
1390
|
+
...activityDescriptor.metadata,
|
|
1391
|
+
stderrBytes: getOutputByteSize(result.stderr, options.encoding),
|
|
608
1392
|
reason: result.error?.message,
|
|
609
1393
|
status: result.status === 0 && !result.error ? "completed" : "failed",
|
|
610
|
-
|
|
1394
|
+
stdoutBytes: getOutputByteSize(result.stdout, options.encoding),
|
|
1395
|
+
target: activityDescriptor.target,
|
|
611
1396
|
});
|
|
612
1397
|
return result;
|
|
613
1398
|
}
|
|
@@ -981,9 +1766,10 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
981
1766
|
dart: ["dart", "flutter", "pub"],
|
|
982
1767
|
haskell: ["haskell", "hackage", "cabal"],
|
|
983
1768
|
elixir: ["elixir", "hex", "mix"],
|
|
984
|
-
c: ["c", "cpp", "c++", "conan"],
|
|
1769
|
+
c: ["c", "cpp", "c++", "conan", "collider"],
|
|
985
1770
|
clojure: ["clojure", "edn", "clj", "leiningen"],
|
|
986
1771
|
github: ["github", "actions"],
|
|
1772
|
+
hbom: ["hbom", "hardware"],
|
|
987
1773
|
os: ["os", "osquery", "windows", "linux", "mac", "macos", "darwin"],
|
|
988
1774
|
jenkins: ["jenkins", "hpi"],
|
|
989
1775
|
helm: ["helm", "charts"],
|
|
@@ -1014,17 +1800,19 @@ export const PROJECT_TYPE_ALIASES = {
|
|
|
1014
1800
|
"visionos",
|
|
1015
1801
|
],
|
|
1016
1802
|
binary: ["binary", "blint"],
|
|
1017
|
-
oci: ["docker", "oci", "container", "podman"],
|
|
1803
|
+
oci: ["docker", "oci", "container", "podman", "rootfs", "oci-dir"],
|
|
1018
1804
|
cocoa: ["cocoa", "cocoapods", "objective-c", "swift", "ios"],
|
|
1019
1805
|
scala: ["scala", "scala3", "sbt", "mill"],
|
|
1020
1806
|
nix: ["nix", "nixos", "flake"],
|
|
1021
1807
|
caxa: ["caxa"],
|
|
1808
|
+
asar: ["asar", "electron", "electron-asar"],
|
|
1022
1809
|
"vscode-extension": [
|
|
1023
1810
|
"vscode-extension",
|
|
1024
1811
|
"vsix",
|
|
1025
1812
|
"vscode",
|
|
1026
1813
|
"openvsx",
|
|
1027
1814
|
"vscode-extensions",
|
|
1815
|
+
"ide-extension",
|
|
1028
1816
|
"ide-extensions",
|
|
1029
1817
|
],
|
|
1030
1818
|
"chrome-extension": [
|
|
@@ -1154,6 +1942,90 @@ export function hasAnyProjectType(projectTypes, options, defaultStatus = true) {
|
|
|
1154
1942
|
return shouldInclude;
|
|
1155
1943
|
}
|
|
1156
1944
|
|
|
1945
|
+
/**
|
|
1946
|
+
* Determine whether the predictive dependency audit should run for the current
|
|
1947
|
+
* CLI invocation.
|
|
1948
|
+
*
|
|
1949
|
+
* OBOM-focused runs (`obom` or explicit `-t os` / OS aliases only) should keep
|
|
1950
|
+
* the direct BOM audit findings but skip the predictive dependency audit.
|
|
1951
|
+
*
|
|
1952
|
+
* @param {object} options CLI options
|
|
1953
|
+
* @param {string} [commandPath] Invoked command path or name
|
|
1954
|
+
* @returns {boolean} True when predictive dependency audit should run
|
|
1955
|
+
*/
|
|
1956
|
+
export function shouldRunPredictiveBomAudit(options, commandPath) {
|
|
1957
|
+
const normalizedCommandPath = `${commandPath || ""}`.toLowerCase();
|
|
1958
|
+
if (normalizedCommandPath.includes("obom")) {
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
if (normalizedCommandPath.includes("hbom")) {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
const projectTypes = Array.isArray(options?.projectType)
|
|
1965
|
+
? options.projectType
|
|
1966
|
+
: typeof options?.projectType === "string"
|
|
1967
|
+
? options.projectType.split(",")
|
|
1968
|
+
: [];
|
|
1969
|
+
const normalizedProjectTypes = projectTypes
|
|
1970
|
+
.map((projectType) => `${projectType || ""}`.trim().toLowerCase())
|
|
1971
|
+
.filter(Boolean);
|
|
1972
|
+
if (!normalizedProjectTypes.length) {
|
|
1973
|
+
return true;
|
|
1974
|
+
}
|
|
1975
|
+
const hbomProjectTypes = new Set(["hbom", "hardware"]);
|
|
1976
|
+
if (
|
|
1977
|
+
normalizedProjectTypes.every((projectType) =>
|
|
1978
|
+
hbomProjectTypes.has(projectType),
|
|
1979
|
+
)
|
|
1980
|
+
) {
|
|
1981
|
+
return false;
|
|
1982
|
+
}
|
|
1983
|
+
const osProjectTypes = new Set(["os", ...(PROJECT_TYPE_ALIASES.os || [])]);
|
|
1984
|
+
return !normalizedProjectTypes.every((projectType) =>
|
|
1985
|
+
osProjectTypes.has(projectType),
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Determine the default BOM audit categories for the current CLI invocation.
|
|
1991
|
+
*
|
|
1992
|
+
* OBOM-focused runs should default to the runtime-specific rule pack unless the
|
|
1993
|
+
* user explicitly requests other categories.
|
|
1994
|
+
*
|
|
1995
|
+
* @param {object} options CLI options
|
|
1996
|
+
* @param {string} [commandPath] Invoked command path or name
|
|
1997
|
+
* @returns {string | undefined} Default category string, if any
|
|
1998
|
+
*/
|
|
1999
|
+
export function getDefaultBomAuditCategories(options, commandPath) {
|
|
2000
|
+
const normalizedCommandPath = `${commandPath || ""}`.toLowerCase();
|
|
2001
|
+
const defaultHbomCategories = options?.includeRuntime
|
|
2002
|
+
? `${DEFAULT_HBOM_AUDIT_CATEGORIES},host-topology`
|
|
2003
|
+
: DEFAULT_HBOM_AUDIT_CATEGORIES;
|
|
2004
|
+
if (normalizedCommandPath.includes("hbom")) {
|
|
2005
|
+
return defaultHbomCategories;
|
|
2006
|
+
}
|
|
2007
|
+
const projectTypes = Array.isArray(options?.projectType)
|
|
2008
|
+
? options.projectType
|
|
2009
|
+
: typeof options?.projectType === "string"
|
|
2010
|
+
? options.projectType.split(",")
|
|
2011
|
+
: [];
|
|
2012
|
+
const normalizedProjectTypes = projectTypes
|
|
2013
|
+
.map((projectType) => `${projectType || ""}`.trim().toLowerCase())
|
|
2014
|
+
.filter(Boolean);
|
|
2015
|
+
if (
|
|
2016
|
+
normalizedProjectTypes.length &&
|
|
2017
|
+
normalizedProjectTypes.every((projectType) =>
|
|
2018
|
+
["hbom", "hardware"].includes(projectType),
|
|
2019
|
+
)
|
|
2020
|
+
) {
|
|
2021
|
+
return defaultHbomCategories;
|
|
2022
|
+
}
|
|
2023
|
+
if (!shouldRunPredictiveBomAudit(options, commandPath)) {
|
|
2024
|
+
return "obom-runtime";
|
|
2025
|
+
}
|
|
2026
|
+
return undefined;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1157
2029
|
/**
|
|
1158
2030
|
* Convenient method to check if the given package manager is allowed.
|
|
1159
2031
|
*
|
|
@@ -1195,23 +2067,76 @@ function isCacheDisabled() {
|
|
|
1195
2067
|
const cache = isCacheDisabled() ? undefined : gotHttpCache;
|
|
1196
2068
|
export const remoteHostsAccessed = new Set();
|
|
1197
2069
|
|
|
1198
|
-
function
|
|
1199
|
-
|
|
2070
|
+
export function isAllowedHttpHost(
|
|
2071
|
+
hostname,
|
|
2072
|
+
allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS"),
|
|
2073
|
+
) {
|
|
2074
|
+
if (!allowedHostsEnv) {
|
|
1200
2075
|
return true;
|
|
1201
2076
|
}
|
|
1202
|
-
|
|
2077
|
+
if (!hostname || hasDangerousUnicode(hostname)) {
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
2081
|
+
const allow_hosts = allowedHostsEnv
|
|
2082
|
+
.split(",")
|
|
2083
|
+
.map((host) => host.trim().toLowerCase())
|
|
2084
|
+
.filter(Boolean);
|
|
1203
2085
|
for (const ahost of allow_hosts) {
|
|
1204
|
-
if (
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
if (hostname === ahost) {
|
|
2086
|
+
if (normalizedHostname === ahost) {
|
|
1208
2087
|
return true;
|
|
1209
2088
|
}
|
|
1210
2089
|
// wildcard support
|
|
1211
|
-
if (
|
|
2090
|
+
if (
|
|
2091
|
+
ahost.startsWith("*.") &&
|
|
2092
|
+
normalizedHostname.length > ahost.length - 1 &&
|
|
2093
|
+
normalizedHostname.endsWith(`.${ahost.slice(2)}`)
|
|
2094
|
+
) {
|
|
1212
2095
|
return true;
|
|
1213
2096
|
}
|
|
1214
2097
|
}
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function hostnameMatches(hostname, candidateHost) {
|
|
2102
|
+
return hostname === candidateHost || hostname.endsWith(`.${candidateHost}`);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function inferNetworkIntent(requestUrl) {
|
|
2106
|
+
const hostname = requestUrl?.hostname?.toLowerCase() || "";
|
|
2107
|
+
const pathname = requestUrl?.pathname?.toLowerCase() || "";
|
|
2108
|
+
if (pathname.includes("/api/v1/bom")) {
|
|
2109
|
+
return "sbom-submit";
|
|
2110
|
+
}
|
|
2111
|
+
if (pathname.includes("/manifests/")) {
|
|
2112
|
+
return "oci-manifest-access";
|
|
2113
|
+
}
|
|
2114
|
+
if (pathname.includes("/blobs/")) {
|
|
2115
|
+
return "oci-layer-access";
|
|
2116
|
+
}
|
|
2117
|
+
if (
|
|
2118
|
+
pathname.includes("license") ||
|
|
2119
|
+
hostnameMatches(hostname, "spdx.org") ||
|
|
2120
|
+
hostnameMatches(hostname, "opensource.org")
|
|
2121
|
+
) {
|
|
2122
|
+
return "license-fetch";
|
|
2123
|
+
}
|
|
2124
|
+
if (
|
|
2125
|
+
hostnameMatches(hostname, "registry.npmjs.org") ||
|
|
2126
|
+
hostnameMatches(hostname, "pypi.org") ||
|
|
2127
|
+
hostnameMatches(hostname, "rubygems.org") ||
|
|
2128
|
+
hostnameMatches(hostname, "repo.maven.apache.org") ||
|
|
2129
|
+
hostnameMatches(hostname, "repo1.maven.org") ||
|
|
2130
|
+
hostnameMatches(hostname, "crates.io") ||
|
|
2131
|
+
hostnameMatches(hostname, "pub.dev") ||
|
|
2132
|
+
hostnameMatches(hostname, "nuget.org")
|
|
2133
|
+
) {
|
|
2134
|
+
return "registry-lookup";
|
|
2135
|
+
}
|
|
2136
|
+
if (hostnameMatches(hostname, "github.com") && pathname.endsWith(".git")) {
|
|
2137
|
+
return "git-fetch";
|
|
2138
|
+
}
|
|
2139
|
+
return "metadata-fetch";
|
|
1215
2140
|
}
|
|
1216
2141
|
|
|
1217
2142
|
// Custom user-agent for cdxgen
|
|
@@ -1227,18 +2152,40 @@ export const cdxgenAgent = got.extend({
|
|
|
1227
2152
|
hooks: {
|
|
1228
2153
|
beforeRequest: [
|
|
1229
2154
|
(options) => {
|
|
2155
|
+
const networkIntent =
|
|
2156
|
+
options.context?.activityIntent || inferNetworkIntent(options.url);
|
|
2157
|
+
const allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS");
|
|
2158
|
+
const hostAllowed = isAllowedHttpHost(
|
|
2159
|
+
options.url.hostname,
|
|
2160
|
+
allowedHostsEnv,
|
|
2161
|
+
);
|
|
1230
2162
|
options.context = {
|
|
1231
2163
|
...options.context,
|
|
2164
|
+
activityIntent: networkIntent,
|
|
1232
2165
|
activityTarget: options.url.toString(),
|
|
1233
2166
|
};
|
|
2167
|
+
if (allowedHostsEnv) {
|
|
2168
|
+
recordPolicyActivity(options.url.hostname, {
|
|
2169
|
+
metadata: {
|
|
2170
|
+
allowed: hostAllowed,
|
|
2171
|
+
allowlist: allowedHostsEnv,
|
|
2172
|
+
networkIntent,
|
|
2173
|
+
policyType: "host-allowlist",
|
|
2174
|
+
},
|
|
2175
|
+
reason: `${hostAllowed ? "Allowed" : "Blocked"} host ${options.url.hostname} against CDXGEN_ALLOWED_HOSTS.`,
|
|
2176
|
+
status: hostAllowed ? "completed" : "blocked",
|
|
2177
|
+
traceDetail: "host-allowlist",
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
1234
2180
|
if (isDryRun) {
|
|
1235
2181
|
const error = createDryRunError(
|
|
1236
2182
|
"network",
|
|
1237
2183
|
options.url.toString(),
|
|
1238
|
-
|
|
2184
|
+
`Dry run mode blocks outbound network access (${networkIntent}).`,
|
|
1239
2185
|
);
|
|
1240
2186
|
recordActivity({
|
|
1241
2187
|
kind: "network",
|
|
2188
|
+
networkIntent,
|
|
1242
2189
|
reason: error.message,
|
|
1243
2190
|
status: "blocked",
|
|
1244
2191
|
target: options.url.toString(),
|
|
@@ -1246,12 +2193,13 @@ export const cdxgenAgent = got.extend({
|
|
|
1246
2193
|
options.context.activityBlocked = true;
|
|
1247
2194
|
throw error;
|
|
1248
2195
|
}
|
|
1249
|
-
if (!
|
|
2196
|
+
if (!hostAllowed) {
|
|
1250
2197
|
console.log(
|
|
1251
2198
|
`Access to the remote host '${options.url.hostname}' is not permitted.`,
|
|
1252
2199
|
);
|
|
1253
2200
|
recordActivity({
|
|
1254
2201
|
kind: "network",
|
|
2202
|
+
networkIntent,
|
|
1255
2203
|
reason: "The remote host is not permitted.",
|
|
1256
2204
|
status: "blocked",
|
|
1257
2205
|
target: options.url.toString(),
|
|
@@ -1266,6 +2214,7 @@ export const cdxgenAgent = got.extend({
|
|
|
1266
2214
|
);
|
|
1267
2215
|
recordActivity({
|
|
1268
2216
|
kind: "network",
|
|
2217
|
+
networkIntent,
|
|
1269
2218
|
reason: `The '${options.url.protocol}' protocol is not permitted in secure mode.`,
|
|
1270
2219
|
status: "blocked",
|
|
1271
2220
|
target: options.url.toString(),
|
|
@@ -1292,6 +2241,7 @@ export const cdxgenAgent = got.extend({
|
|
|
1292
2241
|
response.url;
|
|
1293
2242
|
recordActivity({
|
|
1294
2243
|
kind: "network",
|
|
2244
|
+
networkIntent: response.request.options.context?.activityIntent,
|
|
1295
2245
|
status: "completed",
|
|
1296
2246
|
target: activityTarget,
|
|
1297
2247
|
});
|
|
@@ -1305,6 +2255,7 @@ export const cdxgenAgent = got.extend({
|
|
|
1305
2255
|
}
|
|
1306
2256
|
recordActivity({
|
|
1307
2257
|
kind: "network",
|
|
2258
|
+
networkIntent: error.options?.context?.activityIntent,
|
|
1308
2259
|
reason: error.message,
|
|
1309
2260
|
status: "failed",
|
|
1310
2261
|
target:
|
|
@@ -1388,6 +2339,10 @@ export function getAllFilesWithIgnore(
|
|
|
1388
2339
|
includeDot,
|
|
1389
2340
|
ignoreList,
|
|
1390
2341
|
) {
|
|
2342
|
+
const patternValue = Array.isArray(pattern)
|
|
2343
|
+
? pattern.join(",")
|
|
2344
|
+
: String(pattern);
|
|
2345
|
+
const discoveryMetadata = classifyDiscoveryPattern(patternValue);
|
|
1391
2346
|
try {
|
|
1392
2347
|
const files = globSync(pattern, {
|
|
1393
2348
|
cwd: dirPath,
|
|
@@ -1398,6 +2353,16 @@ export function getAllFilesWithIgnore(
|
|
|
1398
2353
|
follow: false,
|
|
1399
2354
|
ignore: ignoreList,
|
|
1400
2355
|
});
|
|
2356
|
+
recordDiscoveryActivity(`${dirPath} :: ${patternValue}`, {
|
|
2357
|
+
metadata: {
|
|
2358
|
+
discoveryType: discoveryMetadata.discoveryType,
|
|
2359
|
+
matchedCount: files.length,
|
|
2360
|
+
pattern: patternValue,
|
|
2361
|
+
},
|
|
2362
|
+
traceDetail: `matched:${files.length}`,
|
|
2363
|
+
reasonBuilder: (count) =>
|
|
2364
|
+
`Scanned ${dirPath} with glob '${patternValue}' for ${discoveryMetadata.label}; matched ${files.length} path(s)${buildReadCountSuffix(count)}.`,
|
|
2365
|
+
});
|
|
1401
2366
|
if (files.length > 1) {
|
|
1402
2367
|
thoughtLog(
|
|
1403
2368
|
`Found ${files.length} files for the pattern '${pattern}' at '${dirPath}'.`,
|
|
@@ -1405,6 +2370,14 @@ export function getAllFilesWithIgnore(
|
|
|
1405
2370
|
}
|
|
1406
2371
|
return files;
|
|
1407
2372
|
} catch (err) {
|
|
2373
|
+
recordDiscoveryActivity(`${dirPath} :: ${patternValue}`, {
|
|
2374
|
+
metadata: {
|
|
2375
|
+
discoveryType: discoveryMetadata.discoveryType,
|
|
2376
|
+
pattern: patternValue,
|
|
2377
|
+
},
|
|
2378
|
+
reason: `File discovery failed for glob '${patternValue}' under ${dirPath}: ${err.message}`,
|
|
2379
|
+
status: "failed",
|
|
2380
|
+
});
|
|
1408
2381
|
if (DEBUG_MODE) {
|
|
1409
2382
|
console.error(err);
|
|
1410
2383
|
}
|
|
@@ -2585,6 +3558,23 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
2585
3558
|
value: "true",
|
|
2586
3559
|
});
|
|
2587
3560
|
}
|
|
3561
|
+
const npmManifestSources = collectNpmManifestSources(node);
|
|
3562
|
+
if (npmManifestSources.length) {
|
|
3563
|
+
addComponentProperty(
|
|
3564
|
+
pkg,
|
|
3565
|
+
"cdx:npm:manifestSourceType",
|
|
3566
|
+
npmManifestSources
|
|
3567
|
+
.map((manifestSource) => manifestSource.type)
|
|
3568
|
+
.join(","),
|
|
3569
|
+
);
|
|
3570
|
+
addComponentProperty(
|
|
3571
|
+
pkg,
|
|
3572
|
+
"cdx:npm:manifestSource",
|
|
3573
|
+
npmManifestSources
|
|
3574
|
+
.map((manifestSource) => manifestSource.value)
|
|
3575
|
+
.join(","),
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
2588
3578
|
// This getter method could fail with errors at times.
|
|
2589
3579
|
// Example Error: Invalid tag name "^>=6.0.0" of package "^>=6.0.0": Tags may not have any characters that encodeURIComponent encodes.
|
|
2590
3580
|
try {
|
|
@@ -6811,8 +7801,17 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
6811
7801
|
value: origName,
|
|
6812
7802
|
});
|
|
6813
7803
|
}
|
|
6814
|
-
|
|
6815
|
-
|
|
7804
|
+
const releaseEntries = body.releases?.[p.version]?.length
|
|
7805
|
+
? body.releases[p.version]
|
|
7806
|
+
: Array.isArray(body.urls)
|
|
7807
|
+
? body.urls
|
|
7808
|
+
: [];
|
|
7809
|
+
mergeExternalReferences(
|
|
7810
|
+
p,
|
|
7811
|
+
collectPypiReleaseExternalReferences(releaseEntries),
|
|
7812
|
+
);
|
|
7813
|
+
if (releaseEntries.length) {
|
|
7814
|
+
const digest = releaseEntries[0].digests;
|
|
6816
7815
|
if (digest["sha256"]) {
|
|
6817
7816
|
p._integrity = `sha256-${digest["sha256"]}`;
|
|
6818
7817
|
} else if (digest["md5"]) {
|
|
@@ -7001,46 +8000,327 @@ export function parseBdistMetadata(mDataFile, rawMetadata = undefined) {
|
|
|
7001
8000
|
break;
|
|
7002
8001
|
}
|
|
7003
8002
|
}
|
|
7004
|
-
if (mDataFile) {
|
|
7005
|
-
pkg.evidence = {
|
|
7006
|
-
identity: {
|
|
7007
|
-
field: "purl",
|
|
7008
|
-
confidence: 0.5,
|
|
7009
|
-
methods: [
|
|
7010
|
-
{
|
|
7011
|
-
technique: "manifest-analysis",
|
|
7012
|
-
confidence: 0.5,
|
|
7013
|
-
value: mDataFile,
|
|
7014
|
-
},
|
|
7015
|
-
],
|
|
7016
|
-
},
|
|
8003
|
+
if (mDataFile) {
|
|
8004
|
+
pkg.evidence = {
|
|
8005
|
+
identity: {
|
|
8006
|
+
field: "purl",
|
|
8007
|
+
confidence: 0.5,
|
|
8008
|
+
methods: [
|
|
8009
|
+
{
|
|
8010
|
+
technique: "manifest-analysis",
|
|
8011
|
+
confidence: 0.5,
|
|
8012
|
+
value: mDataFile,
|
|
8013
|
+
},
|
|
8014
|
+
],
|
|
8015
|
+
},
|
|
8016
|
+
};
|
|
8017
|
+
}
|
|
8018
|
+
pkg.purl = `pkg:pypi/${pkg.name.toLowerCase().replaceAll("_", "-")}@${pkg.version}`;
|
|
8019
|
+
pkg["bom-ref"] = pkg.purl;
|
|
8020
|
+
return [pkg];
|
|
8021
|
+
}
|
|
8022
|
+
|
|
8023
|
+
/**
|
|
8024
|
+
* Method to parse pipfile.lock data
|
|
8025
|
+
*
|
|
8026
|
+
* @param {Object} lockData JSON data from Pipfile.lock
|
|
8027
|
+
*/
|
|
8028
|
+
export async function parsePiplockData(lockData) {
|
|
8029
|
+
const pkgList = [];
|
|
8030
|
+
Object.keys(lockData)
|
|
8031
|
+
.filter((i) => i !== "_meta")
|
|
8032
|
+
.forEach((k) => {
|
|
8033
|
+
const depBlock = lockData[k];
|
|
8034
|
+
Object.keys(depBlock).forEach((p) => {
|
|
8035
|
+
const pkg = depBlock[p];
|
|
8036
|
+
if (Object.hasOwn(pkg, "version")) {
|
|
8037
|
+
const versionStr = pkg.version.replace("==", "");
|
|
8038
|
+
pkgList.push({ name: p, version: versionStr });
|
|
8039
|
+
}
|
|
8040
|
+
});
|
|
8041
|
+
});
|
|
8042
|
+
return await getPyMetadata(pkgList, false);
|
|
8043
|
+
}
|
|
8044
|
+
|
|
8045
|
+
function addComponentProperty(component, name, value) {
|
|
8046
|
+
if (value === undefined || value === null || value === "" || !component) {
|
|
8047
|
+
return;
|
|
8048
|
+
}
|
|
8049
|
+
component.properties = component.properties || [];
|
|
8050
|
+
if (
|
|
8051
|
+
component.properties.some(
|
|
8052
|
+
(property) => property.name === name && property.value === value,
|
|
8053
|
+
)
|
|
8054
|
+
) {
|
|
8055
|
+
return;
|
|
8056
|
+
}
|
|
8057
|
+
component.properties.push({
|
|
8058
|
+
name,
|
|
8059
|
+
value,
|
|
8060
|
+
});
|
|
8061
|
+
}
|
|
8062
|
+
|
|
8063
|
+
const PYTHON_DIRECT_REFERENCE_PATTERN =
|
|
8064
|
+
/^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?\s*@\s*(\S+)$/;
|
|
8065
|
+
|
|
8066
|
+
function isWindowsAbsolutePath(value) {
|
|
8067
|
+
return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("\\\\");
|
|
8068
|
+
}
|
|
8069
|
+
|
|
8070
|
+
function createExternalReferenceKey(reference) {
|
|
8071
|
+
return JSON.stringify([
|
|
8072
|
+
reference.type,
|
|
8073
|
+
reference.url,
|
|
8074
|
+
reference.comment || "",
|
|
8075
|
+
]);
|
|
8076
|
+
}
|
|
8077
|
+
|
|
8078
|
+
function classifyNpmManifestSource(spec) {
|
|
8079
|
+
if (typeof spec !== "string" || !spec.trim()) {
|
|
8080
|
+
return undefined;
|
|
8081
|
+
}
|
|
8082
|
+
const normalizedSpec = spec.trim();
|
|
8083
|
+
const lowerSpec = normalizedSpec.toLowerCase();
|
|
8084
|
+
if (
|
|
8085
|
+
lowerSpec.startsWith("git+") ||
|
|
8086
|
+
lowerSpec.startsWith("git://") ||
|
|
8087
|
+
lowerSpec.startsWith("github:") ||
|
|
8088
|
+
lowerSpec.startsWith("gitlab:") ||
|
|
8089
|
+
lowerSpec.startsWith("bitbucket:") ||
|
|
8090
|
+
lowerSpec.startsWith("gist:")
|
|
8091
|
+
) {
|
|
8092
|
+
return {
|
|
8093
|
+
type: "git",
|
|
8094
|
+
value: normalizedSpec,
|
|
8095
|
+
};
|
|
8096
|
+
}
|
|
8097
|
+
if (lowerSpec.startsWith("http://") || lowerSpec.startsWith("https://")) {
|
|
8098
|
+
return {
|
|
8099
|
+
type: "url",
|
|
8100
|
+
value: normalizedSpec,
|
|
8101
|
+
};
|
|
8102
|
+
}
|
|
8103
|
+
if (
|
|
8104
|
+
lowerSpec.startsWith("file:") ||
|
|
8105
|
+
lowerSpec.startsWith("link:") ||
|
|
8106
|
+
lowerSpec.startsWith("workspace:") ||
|
|
8107
|
+
normalizedSpec.startsWith("./") ||
|
|
8108
|
+
normalizedSpec.startsWith("../") ||
|
|
8109
|
+
normalizedSpec.startsWith("/") ||
|
|
8110
|
+
isWindowsAbsolutePath(normalizedSpec)
|
|
8111
|
+
) {
|
|
8112
|
+
return {
|
|
8113
|
+
type: "path",
|
|
8114
|
+
value: normalizedSpec,
|
|
8115
|
+
};
|
|
8116
|
+
}
|
|
8117
|
+
return undefined;
|
|
8118
|
+
}
|
|
8119
|
+
|
|
8120
|
+
function collectNpmManifestSources(node) {
|
|
8121
|
+
const manifestSources = [];
|
|
8122
|
+
const seen = new Set();
|
|
8123
|
+
if (!node?.edgesIn) {
|
|
8124
|
+
return manifestSources;
|
|
8125
|
+
}
|
|
8126
|
+
for (const edge of node.edgesIn) {
|
|
8127
|
+
const manifestSource = classifyNpmManifestSource(edge?.spec);
|
|
8128
|
+
if (!manifestSource) {
|
|
8129
|
+
continue;
|
|
8130
|
+
}
|
|
8131
|
+
const dedupeKey = `${manifestSource.type}|${manifestSource.value}`;
|
|
8132
|
+
if (seen.has(dedupeKey)) {
|
|
8133
|
+
continue;
|
|
8134
|
+
}
|
|
8135
|
+
seen.add(dedupeKey);
|
|
8136
|
+
manifestSources.push(manifestSource);
|
|
8137
|
+
}
|
|
8138
|
+
return manifestSources;
|
|
8139
|
+
}
|
|
8140
|
+
|
|
8141
|
+
function normalizePythonDependencyKey(value) {
|
|
8142
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
8143
|
+
return undefined;
|
|
8144
|
+
}
|
|
8145
|
+
return value.trim().toLowerCase().replaceAll("_", "-");
|
|
8146
|
+
}
|
|
8147
|
+
|
|
8148
|
+
function extractPythonDependencyKey(value) {
|
|
8149
|
+
const manifestSource = parsePyProjectDependencySourceString(value);
|
|
8150
|
+
if (manifestSource?.name) {
|
|
8151
|
+
return normalizePythonDependencyKey(manifestSource.name);
|
|
8152
|
+
}
|
|
8153
|
+
const packageMatch =
|
|
8154
|
+
typeof value === "string"
|
|
8155
|
+
? value.trim().match(/^([A-Za-z0-9_.-]+)(?:\[[^\]]+])?/)
|
|
8156
|
+
: undefined;
|
|
8157
|
+
return normalizePythonDependencyKey(packageMatch?.[1]);
|
|
8158
|
+
}
|
|
8159
|
+
function classifyPythonManifestSourceValue(value) {
|
|
8160
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
8161
|
+
return undefined;
|
|
8162
|
+
}
|
|
8163
|
+
const normalizedValue = value.trim();
|
|
8164
|
+
const lowerValue = normalizedValue.toLowerCase();
|
|
8165
|
+
if (
|
|
8166
|
+
lowerValue.startsWith("git+") ||
|
|
8167
|
+
lowerValue.startsWith("git://") ||
|
|
8168
|
+
lowerValue.startsWith("git@") ||
|
|
8169
|
+
lowerValue.startsWith("ssh://git@")
|
|
8170
|
+
) {
|
|
8171
|
+
return {
|
|
8172
|
+
type: "git",
|
|
8173
|
+
value: normalizedValue,
|
|
8174
|
+
};
|
|
8175
|
+
}
|
|
8176
|
+
if (
|
|
8177
|
+
lowerValue.startsWith("http://") ||
|
|
8178
|
+
lowerValue.startsWith("https://") ||
|
|
8179
|
+
lowerValue.startsWith("ftp://")
|
|
8180
|
+
) {
|
|
8181
|
+
return {
|
|
8182
|
+
type: "url",
|
|
8183
|
+
value: normalizedValue,
|
|
8184
|
+
};
|
|
8185
|
+
}
|
|
8186
|
+
if (
|
|
8187
|
+
lowerValue.startsWith("file:") ||
|
|
8188
|
+
normalizedValue.startsWith("./") ||
|
|
8189
|
+
normalizedValue.startsWith("../") ||
|
|
8190
|
+
normalizedValue.startsWith("/") ||
|
|
8191
|
+
isWindowsAbsolutePath(normalizedValue)
|
|
8192
|
+
) {
|
|
8193
|
+
return {
|
|
8194
|
+
type: "path",
|
|
8195
|
+
value: normalizedValue,
|
|
8196
|
+
};
|
|
8197
|
+
}
|
|
8198
|
+
return undefined;
|
|
8199
|
+
}
|
|
8200
|
+
|
|
8201
|
+
function applyManifestSourceProperties(
|
|
8202
|
+
component,
|
|
8203
|
+
propertyPrefix,
|
|
8204
|
+
manifestSource,
|
|
8205
|
+
) {
|
|
8206
|
+
if (!manifestSource?.type || !manifestSource?.value) {
|
|
8207
|
+
return;
|
|
8208
|
+
}
|
|
8209
|
+
addComponentProperty(
|
|
8210
|
+
component,
|
|
8211
|
+
`${propertyPrefix}:manifestSourceType`,
|
|
8212
|
+
manifestSource.type,
|
|
8213
|
+
);
|
|
8214
|
+
addComponentProperty(
|
|
8215
|
+
component,
|
|
8216
|
+
`${propertyPrefix}:manifestSource`,
|
|
8217
|
+
manifestSource.value,
|
|
8218
|
+
);
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
function recordPythonDependencySource(
|
|
8222
|
+
dependencySourceMap,
|
|
8223
|
+
dependencyName,
|
|
8224
|
+
sourceType,
|
|
8225
|
+
sourceValue,
|
|
8226
|
+
) {
|
|
8227
|
+
const normalizedKey = normalizePythonDependencyKey(dependencyName);
|
|
8228
|
+
if (!normalizedKey || !sourceType || !sourceValue) {
|
|
8229
|
+
return;
|
|
8230
|
+
}
|
|
8231
|
+
dependencySourceMap[normalizedKey] = {
|
|
8232
|
+
type: sourceType,
|
|
8233
|
+
value: sourceValue,
|
|
8234
|
+
};
|
|
8235
|
+
}
|
|
8236
|
+
|
|
8237
|
+
function parsePyProjectDependencySourceString(value) {
|
|
8238
|
+
if (typeof value !== "string" || !value.includes("@")) {
|
|
8239
|
+
return undefined;
|
|
8240
|
+
}
|
|
8241
|
+
const directReferenceMatch = value
|
|
8242
|
+
.trim()
|
|
8243
|
+
.match(PYTHON_DIRECT_REFERENCE_PATTERN);
|
|
8244
|
+
if (!directReferenceMatch) {
|
|
8245
|
+
return undefined;
|
|
8246
|
+
}
|
|
8247
|
+
const manifestSource = classifyPythonManifestSourceValue(
|
|
8248
|
+
directReferenceMatch[2],
|
|
8249
|
+
);
|
|
8250
|
+
if (!manifestSource) {
|
|
8251
|
+
return undefined;
|
|
8252
|
+
}
|
|
8253
|
+
return {
|
|
8254
|
+
name: directReferenceMatch[1],
|
|
8255
|
+
...manifestSource,
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
|
|
8259
|
+
function collectPythonManifestSource(pkg) {
|
|
8260
|
+
const sourceCandidates = [
|
|
8261
|
+
{ kind: "git", value: pkg?.source?.git },
|
|
8262
|
+
{ kind: "git", value: pkg?.vcs?.git },
|
|
8263
|
+
{ kind: "url", value: pkg?.vcs?.url },
|
|
8264
|
+
{ kind: "url", value: pkg?.source?.url },
|
|
8265
|
+
{ kind: "path", value: pkg?.source?.path },
|
|
8266
|
+
{ kind: "path", value: pkg?.source?.editable },
|
|
8267
|
+
{ kind: "path", value: pkg?.source?.virtual },
|
|
8268
|
+
];
|
|
8269
|
+
for (const candidate of sourceCandidates) {
|
|
8270
|
+
if (typeof candidate.value !== "string" || !candidate.value.trim()) {
|
|
8271
|
+
continue;
|
|
8272
|
+
}
|
|
8273
|
+
const normalizedValue = candidate.value.trim();
|
|
8274
|
+
if (candidate.kind === "git") {
|
|
8275
|
+
return {
|
|
8276
|
+
type: "git",
|
|
8277
|
+
value: normalizedValue.startsWith("git+")
|
|
8278
|
+
? normalizedValue
|
|
8279
|
+
: `git+${normalizedValue}`,
|
|
8280
|
+
};
|
|
8281
|
+
}
|
|
8282
|
+
const manifestSource = classifyPythonManifestSourceValue(normalizedValue);
|
|
8283
|
+
if (manifestSource) {
|
|
8284
|
+
return manifestSource;
|
|
8285
|
+
}
|
|
8286
|
+
return {
|
|
8287
|
+
type: candidate.kind,
|
|
8288
|
+
value: normalizedValue,
|
|
7017
8289
|
};
|
|
7018
8290
|
}
|
|
7019
|
-
|
|
7020
|
-
pkg["bom-ref"] = pkg.purl;
|
|
7021
|
-
return [pkg];
|
|
8291
|
+
return undefined;
|
|
7022
8292
|
}
|
|
7023
8293
|
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
8294
|
+
function parsePythonRequirementManifestSource(value) {
|
|
8295
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
8296
|
+
return undefined;
|
|
8297
|
+
}
|
|
8298
|
+
const normalizedValue = value.trim();
|
|
8299
|
+
const directReferenceMatch = normalizedValue.match(
|
|
8300
|
+
PYTHON_DIRECT_REFERENCE_PATTERN,
|
|
8301
|
+
);
|
|
8302
|
+
if (directReferenceMatch) {
|
|
8303
|
+
const manifestSource = classifyPythonManifestSourceValue(
|
|
8304
|
+
directReferenceMatch[2],
|
|
8305
|
+
);
|
|
8306
|
+
if (manifestSource) {
|
|
8307
|
+
return {
|
|
8308
|
+
name: directReferenceMatch[1],
|
|
8309
|
+
...manifestSource,
|
|
8310
|
+
};
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
const vcsRequirementMatch = normalizedValue.match(
|
|
8314
|
+
/^(git\+\S+?)(?:#.*egg=([A-Za-z0-9_.-]+))?$/,
|
|
8315
|
+
);
|
|
8316
|
+
if (vcsRequirementMatch?.[2]) {
|
|
8317
|
+
return {
|
|
8318
|
+
name: vcsRequirementMatch[2],
|
|
8319
|
+
type: "git",
|
|
8320
|
+
value: vcsRequirementMatch[1],
|
|
8321
|
+
};
|
|
8322
|
+
}
|
|
8323
|
+
return undefined;
|
|
7044
8324
|
}
|
|
7045
8325
|
|
|
7046
8326
|
/**
|
|
@@ -7102,6 +8382,7 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
7102
8382
|
let tomlData;
|
|
7103
8383
|
const directDepsKeys = {};
|
|
7104
8384
|
const groupDepsKeys = {};
|
|
8385
|
+
const dependencySourceMap = {};
|
|
7105
8386
|
try {
|
|
7106
8387
|
tomlData = toml.parse(readFileSync(tomlFile, { encoding: "utf-8" }));
|
|
7107
8388
|
} catch (err) {
|
|
@@ -7173,8 +8454,19 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
7173
8454
|
}
|
|
7174
8455
|
if (tomlData?.project?.dependencies) {
|
|
7175
8456
|
for (const adep of tomlData.project.dependencies) {
|
|
7176
|
-
|
|
7177
|
-
|
|
8457
|
+
const dependencyKey = extractPythonDependencyKey(adep);
|
|
8458
|
+
if (dependencyKey) {
|
|
8459
|
+
directDepsKeys[dependencyKey] = true;
|
|
8460
|
+
}
|
|
8461
|
+
const manifestSource = parsePyProjectDependencySourceString(adep);
|
|
8462
|
+
if (manifestSource) {
|
|
8463
|
+
recordPythonDependencySource(
|
|
8464
|
+
dependencySourceMap,
|
|
8465
|
+
manifestSource.name,
|
|
8466
|
+
manifestSource.type,
|
|
8467
|
+
manifestSource.value,
|
|
8468
|
+
);
|
|
8469
|
+
}
|
|
7178
8470
|
}
|
|
7179
8471
|
}
|
|
7180
8472
|
if (tomlData["dependency-groups"]) {
|
|
@@ -7186,6 +8478,15 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
7186
8478
|
groupDepsKeys[pname] = [];
|
|
7187
8479
|
}
|
|
7188
8480
|
groupDepsKeys[pname].push(agroup);
|
|
8481
|
+
const manifestSource = parsePyProjectDependencySourceString(p);
|
|
8482
|
+
if (manifestSource) {
|
|
8483
|
+
recordPythonDependencySource(
|
|
8484
|
+
dependencySourceMap,
|
|
8485
|
+
manifestSource.name,
|
|
8486
|
+
manifestSource.type,
|
|
8487
|
+
manifestSource.value,
|
|
8488
|
+
);
|
|
8489
|
+
}
|
|
7189
8490
|
} else {
|
|
7190
8491
|
return;
|
|
7191
8492
|
}
|
|
@@ -7206,6 +8507,29 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
7206
8507
|
].includes(adep)
|
|
7207
8508
|
) {
|
|
7208
8509
|
directDepsKeys[adep] = true;
|
|
8510
|
+
const poetryDependency = tomlData.tool.poetry.dependencies[adep];
|
|
8511
|
+
if (poetryDependency?.git) {
|
|
8512
|
+
recordPythonDependencySource(
|
|
8513
|
+
dependencySourceMap,
|
|
8514
|
+
adep,
|
|
8515
|
+
"git",
|
|
8516
|
+
poetryDependency.git,
|
|
8517
|
+
);
|
|
8518
|
+
} else if (poetryDependency?.url) {
|
|
8519
|
+
recordPythonDependencySource(
|
|
8520
|
+
dependencySourceMap,
|
|
8521
|
+
adep,
|
|
8522
|
+
"url",
|
|
8523
|
+
poetryDependency.url,
|
|
8524
|
+
);
|
|
8525
|
+
} else if (poetryDependency?.path) {
|
|
8526
|
+
recordPythonDependencySource(
|
|
8527
|
+
dependencySourceMap,
|
|
8528
|
+
adep,
|
|
8529
|
+
"path",
|
|
8530
|
+
poetryDependency.path,
|
|
8531
|
+
);
|
|
8532
|
+
}
|
|
7209
8533
|
}
|
|
7210
8534
|
} // for
|
|
7211
8535
|
if (tomlData?.tool?.poetry?.group) {
|
|
@@ -7217,10 +8541,63 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
7217
8541
|
groupDepsKeys[adep] = [];
|
|
7218
8542
|
}
|
|
7219
8543
|
groupDepsKeys[adep].push(agroup);
|
|
8544
|
+
const poetryDependency =
|
|
8545
|
+
tomlData.tool.poetry.group[agroup]?.dependencies?.[adep];
|
|
8546
|
+
if (poetryDependency?.git) {
|
|
8547
|
+
recordPythonDependencySource(
|
|
8548
|
+
dependencySourceMap,
|
|
8549
|
+
adep,
|
|
8550
|
+
"git",
|
|
8551
|
+
poetryDependency.git,
|
|
8552
|
+
);
|
|
8553
|
+
} else if (poetryDependency?.url) {
|
|
8554
|
+
recordPythonDependencySource(
|
|
8555
|
+
dependencySourceMap,
|
|
8556
|
+
adep,
|
|
8557
|
+
"url",
|
|
8558
|
+
poetryDependency.url,
|
|
8559
|
+
);
|
|
8560
|
+
} else if (poetryDependency?.path) {
|
|
8561
|
+
recordPythonDependencySource(
|
|
8562
|
+
dependencySourceMap,
|
|
8563
|
+
adep,
|
|
8564
|
+
"path",
|
|
8565
|
+
poetryDependency.path,
|
|
8566
|
+
);
|
|
8567
|
+
}
|
|
7220
8568
|
}
|
|
7221
8569
|
} // for
|
|
7222
8570
|
}
|
|
7223
8571
|
}
|
|
8572
|
+
if (tomlData?.tool?.uv?.sources) {
|
|
8573
|
+
for (const adep of Object.keys(tomlData.tool.uv.sources)) {
|
|
8574
|
+
const uvSource = Array.isArray(tomlData.tool.uv.sources[adep])
|
|
8575
|
+
? tomlData.tool.uv.sources[adep][0]
|
|
8576
|
+
: tomlData.tool.uv.sources[adep];
|
|
8577
|
+
if (uvSource?.git) {
|
|
8578
|
+
recordPythonDependencySource(
|
|
8579
|
+
dependencySourceMap,
|
|
8580
|
+
adep,
|
|
8581
|
+
"git",
|
|
8582
|
+
uvSource.git,
|
|
8583
|
+
);
|
|
8584
|
+
} else if (uvSource?.url) {
|
|
8585
|
+
recordPythonDependencySource(
|
|
8586
|
+
dependencySourceMap,
|
|
8587
|
+
adep,
|
|
8588
|
+
"url",
|
|
8589
|
+
uvSource.url,
|
|
8590
|
+
);
|
|
8591
|
+
} else if (uvSource?.path) {
|
|
8592
|
+
recordPythonDependencySource(
|
|
8593
|
+
dependencySourceMap,
|
|
8594
|
+
adep,
|
|
8595
|
+
"path",
|
|
8596
|
+
uvSource.path,
|
|
8597
|
+
);
|
|
8598
|
+
}
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
7224
8601
|
return {
|
|
7225
8602
|
parentComponent: pkg,
|
|
7226
8603
|
poetryMode,
|
|
@@ -7229,9 +8606,146 @@ export function parsePyProjectTomlFile(tomlFile) {
|
|
|
7229
8606
|
workspacePaths,
|
|
7230
8607
|
directDepsKeys,
|
|
7231
8608
|
groupDepsKeys,
|
|
8609
|
+
dependencySourceMap,
|
|
7232
8610
|
};
|
|
7233
8611
|
}
|
|
7234
8612
|
|
|
8613
|
+
function collectPythonLockDistributionReferences(pkg) {
|
|
8614
|
+
const externalReferences = [];
|
|
8615
|
+
const seen = new Set();
|
|
8616
|
+
|
|
8617
|
+
function addExternalReference(type, url, comment) {
|
|
8618
|
+
if (typeof url !== "string" || !url.trim()) {
|
|
8619
|
+
return;
|
|
8620
|
+
}
|
|
8621
|
+
const normalizedUrl = url.trim();
|
|
8622
|
+
const reference = {
|
|
8623
|
+
type,
|
|
8624
|
+
url: normalizedUrl,
|
|
8625
|
+
comment,
|
|
8626
|
+
};
|
|
8627
|
+
const referenceKey = createExternalReferenceKey(reference);
|
|
8628
|
+
if (seen.has(referenceKey)) {
|
|
8629
|
+
return;
|
|
8630
|
+
}
|
|
8631
|
+
seen.add(referenceKey);
|
|
8632
|
+
externalReferences.push(reference);
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8635
|
+
addExternalReference("distribution", pkg?.archive?.url, "archive");
|
|
8636
|
+
addExternalReference("distribution", pkg?.sdist?.url, "sdist");
|
|
8637
|
+
if (Array.isArray(pkg?.wheels)) {
|
|
8638
|
+
for (const wheel of pkg.wheels) {
|
|
8639
|
+
addExternalReference(
|
|
8640
|
+
"distribution",
|
|
8641
|
+
wheel?.url,
|
|
8642
|
+
wheel?.file || wheel?.name || wheel?.filename || "wheel",
|
|
8643
|
+
);
|
|
8644
|
+
}
|
|
8645
|
+
}
|
|
8646
|
+
const vcsSource = [
|
|
8647
|
+
{ kind: "url", value: pkg?.vcs?.url },
|
|
8648
|
+
{ kind: "git", value: pkg?.vcs?.git },
|
|
8649
|
+
{ kind: "git", value: pkg?.source?.git },
|
|
8650
|
+
].find(
|
|
8651
|
+
(entry) => typeof entry.value === "string" && entry.value.trim().length > 0,
|
|
8652
|
+
);
|
|
8653
|
+
if (vcsSource) {
|
|
8654
|
+
const vcsUrl = vcsSource.value.trim();
|
|
8655
|
+
const normalizedVcsUrl =
|
|
8656
|
+
vcsSource.kind === "git" && !vcsUrl.startsWith("git+")
|
|
8657
|
+
? `git+${vcsUrl}`
|
|
8658
|
+
: vcsUrl;
|
|
8659
|
+
addExternalReference("vcs", normalizedVcsUrl, "vcs");
|
|
8660
|
+
}
|
|
8661
|
+
if (pkg?.source?.url) {
|
|
8662
|
+
const manifestSource = classifyPythonManifestSourceValue(pkg.source.url);
|
|
8663
|
+
addExternalReference(
|
|
8664
|
+
manifestSource?.type === "git" ? "vcs" : "distribution",
|
|
8665
|
+
pkg.source.url,
|
|
8666
|
+
"source",
|
|
8667
|
+
);
|
|
8668
|
+
}
|
|
8669
|
+
return externalReferences;
|
|
8670
|
+
}
|
|
8671
|
+
|
|
8672
|
+
function collectPythonLockMetadataFileEntries(lockTomlObj, pkg) {
|
|
8673
|
+
if (!lockTomlObj?.metadata?.files || !pkg?.name) {
|
|
8674
|
+
return [];
|
|
8675
|
+
}
|
|
8676
|
+
const expectedKeys = new Set([normalizePythonDependencyKey(pkg.name)]);
|
|
8677
|
+
if (pkg.version) {
|
|
8678
|
+
expectedKeys.add(
|
|
8679
|
+
`${normalizePythonDependencyKey(pkg.name)} ${`${pkg.version}`.trim().toLowerCase()}`,
|
|
8680
|
+
);
|
|
8681
|
+
}
|
|
8682
|
+
const matchingEntries = [];
|
|
8683
|
+
for (const [entryKey, entryValues] of Object.entries(
|
|
8684
|
+
lockTomlObj.metadata.files,
|
|
8685
|
+
)) {
|
|
8686
|
+
if (!Array.isArray(entryValues)) {
|
|
8687
|
+
continue;
|
|
8688
|
+
}
|
|
8689
|
+
if (expectedKeys.has(normalizePythonDependencyKey(entryKey))) {
|
|
8690
|
+
matchingEntries.push(...entryValues);
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
return matchingEntries;
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
function mergeExternalReferences(component, references) {
|
|
8697
|
+
if (!references?.length) {
|
|
8698
|
+
return;
|
|
8699
|
+
}
|
|
8700
|
+
const existingReferences = component.externalReferences || [];
|
|
8701
|
+
const seen = new Set(
|
|
8702
|
+
existingReferences.map((reference) =>
|
|
8703
|
+
createExternalReferenceKey(reference),
|
|
8704
|
+
),
|
|
8705
|
+
);
|
|
8706
|
+
for (const reference of references) {
|
|
8707
|
+
const dedupeKey = createExternalReferenceKey(reference);
|
|
8708
|
+
if (seen.has(dedupeKey)) {
|
|
8709
|
+
continue;
|
|
8710
|
+
}
|
|
8711
|
+
seen.add(dedupeKey);
|
|
8712
|
+
existingReferences.push(reference);
|
|
8713
|
+
}
|
|
8714
|
+
if (existingReferences.length) {
|
|
8715
|
+
component.externalReferences = existingReferences;
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
|
|
8719
|
+
function collectPythonLockMetadataDistributionReferences(fileEntries) {
|
|
8720
|
+
const distributionReferences = [];
|
|
8721
|
+
for (const fileEntry of fileEntries || []) {
|
|
8722
|
+
if (typeof fileEntry?.url !== "string" || !fileEntry.url.trim()) {
|
|
8723
|
+
continue;
|
|
8724
|
+
}
|
|
8725
|
+
distributionReferences.push({
|
|
8726
|
+
type: "distribution",
|
|
8727
|
+
url: fileEntry.url.trim(),
|
|
8728
|
+
comment: fileEntry.file,
|
|
8729
|
+
});
|
|
8730
|
+
}
|
|
8731
|
+
return distributionReferences;
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
function collectPypiReleaseExternalReferences(releaseEntries) {
|
|
8735
|
+
const externalReferences = [];
|
|
8736
|
+
for (const releaseEntry of releaseEntries || []) {
|
|
8737
|
+
if (typeof releaseEntry?.url !== "string" || !releaseEntry.url.trim()) {
|
|
8738
|
+
continue;
|
|
8739
|
+
}
|
|
8740
|
+
externalReferences.push({
|
|
8741
|
+
type: "distribution",
|
|
8742
|
+
url: releaseEntry.url.trim(),
|
|
8743
|
+
comment: releaseEntry.filename || releaseEntry.packagetype,
|
|
8744
|
+
});
|
|
8745
|
+
}
|
|
8746
|
+
return externalReferences;
|
|
8747
|
+
}
|
|
8748
|
+
|
|
7235
8749
|
/**
|
|
7236
8750
|
* Method to parse python lock files such as poetry.lock, pdm.lock, uv.lock, and pylock.toml.
|
|
7237
8751
|
*
|
|
@@ -7248,6 +8762,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7248
8762
|
const pkgBomRefMap = {};
|
|
7249
8763
|
let directDepsKeys = {};
|
|
7250
8764
|
let groupDepsKeys = {};
|
|
8765
|
+
let dependencySourceMap = {};
|
|
7251
8766
|
let parentComponent;
|
|
7252
8767
|
let workspacePaths;
|
|
7253
8768
|
let workspaceWarningShown = false;
|
|
@@ -7255,6 +8770,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7255
8770
|
let pyLockProperties = [];
|
|
7256
8771
|
// Keep track of any workspace components to be added to the parent component
|
|
7257
8772
|
const workspaceComponentMap = {};
|
|
8773
|
+
const workspaceDependencySourceMap = {};
|
|
7258
8774
|
const workspacePyProjMap = {};
|
|
7259
8775
|
const workspaceRefPyProjMap = {};
|
|
7260
8776
|
const pkgParentMap = {};
|
|
@@ -7274,6 +8790,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7274
8790
|
const pyProjMap = parsePyProjectTomlFile(pyProjectFile);
|
|
7275
8791
|
directDepsKeys = pyProjMap.directDepsKeys || {};
|
|
7276
8792
|
groupDepsKeys = pyProjMap.groupDepsKeys || {};
|
|
8793
|
+
dependencySourceMap = pyProjMap.dependencySourceMap || {};
|
|
7277
8794
|
parentComponent = pyProjMap.parentComponent;
|
|
7278
8795
|
workspacePaths = pyProjMap.workspacePaths;
|
|
7279
8796
|
if (workspacePaths?.length) {
|
|
@@ -7337,6 +8854,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7337
8854
|
}
|
|
7338
8855
|
}
|
|
7339
8856
|
const wparentComponentRef = wcompMap.parentComponent["bom-ref"];
|
|
8857
|
+
if (wcompMap?.dependencySourceMap) {
|
|
8858
|
+
Object.assign(
|
|
8859
|
+
workspaceDependencySourceMap,
|
|
8860
|
+
wcompMap.dependencySourceMap,
|
|
8861
|
+
);
|
|
8862
|
+
}
|
|
7340
8863
|
// Track the parents of workspace direct dependencies
|
|
7341
8864
|
if (wcompMap?.directDepsKeys) {
|
|
7342
8865
|
for (const wdd of Object.keys(wcompMap?.directDepsKeys)) {
|
|
@@ -7408,6 +8931,11 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7408
8931
|
value: workspacePyProjMap[apkg.name] || pyProjectFile,
|
|
7409
8932
|
});
|
|
7410
8933
|
}
|
|
8934
|
+
const manifestSource =
|
|
8935
|
+
dependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
|
|
8936
|
+
workspaceDependencySourceMap[normalizePythonDependencyKey(apkg.name)] ||
|
|
8937
|
+
collectPythonManifestSource(apkg);
|
|
8938
|
+
applyManifestSourceProperties(pkg, "cdx:pypi", manifestSource);
|
|
7411
8939
|
if (apkg.optional) {
|
|
7412
8940
|
pkg.scope = "optional";
|
|
7413
8941
|
}
|
|
@@ -7449,6 +8977,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7449
8977
|
});
|
|
7450
8978
|
}
|
|
7451
8979
|
}
|
|
8980
|
+
mergeExternalReferences(pkg, collectPythonLockDistributionReferences(apkg));
|
|
7452
8981
|
if (pyLockMode) {
|
|
7453
8982
|
pkg.properties = pkg.properties.concat(
|
|
7454
8983
|
collectPyLockPackageProperties(apkg),
|
|
@@ -7511,9 +9040,17 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7511
9040
|
}
|
|
7512
9041
|
}
|
|
7513
9042
|
}
|
|
7514
|
-
|
|
9043
|
+
const metadataFileEntries = collectPythonLockMetadataFileEntries(
|
|
9044
|
+
lockTomlObj,
|
|
9045
|
+
pkg,
|
|
9046
|
+
);
|
|
9047
|
+
mergeExternalReferences(
|
|
9048
|
+
pkg,
|
|
9049
|
+
collectPythonLockMetadataDistributionReferences(metadataFileEntries),
|
|
9050
|
+
);
|
|
9051
|
+
if (metadataFileEntries.length) {
|
|
7515
9052
|
pkg.components = [];
|
|
7516
|
-
for (const afileObj of
|
|
9053
|
+
for (const afileObj of metadataFileEntries) {
|
|
7517
9054
|
const hashParts = afileObj?.hash?.split(":");
|
|
7518
9055
|
let hashes;
|
|
7519
9056
|
if (hashParts?.length === 2) {
|
|
@@ -7547,8 +9084,9 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
7547
9084
|
pkg.components = (pkg.components || []).concat(pylockFileComponents);
|
|
7548
9085
|
}
|
|
7549
9086
|
}
|
|
9087
|
+
const normalizedPkgName = normalizePythonDependencyKey(pkg.name);
|
|
7550
9088
|
if (
|
|
7551
|
-
directDepsKeys[
|
|
9089
|
+
directDepsKeys[normalizedPkgName] ||
|
|
7552
9090
|
(hasWorkspaces && !Object.keys(workspaceComponentMap).length)
|
|
7553
9091
|
) {
|
|
7554
9092
|
rootList.push(pkg);
|
|
@@ -7743,6 +9281,23 @@ export async function parseReqFile(reqFile, fetchDepsInfo = false) {
|
|
|
7743
9281
|
const LICENSE_ID_COMMENTS_PATTERN =
|
|
7744
9282
|
/^(Apache-2\.0|MIT|ISC|GPL-|LGPL-|BSD-[23]-Clause)/i;
|
|
7745
9283
|
|
|
9284
|
+
function parseLicenseComment(comment) {
|
|
9285
|
+
if (!comment) {
|
|
9286
|
+
return undefined;
|
|
9287
|
+
}
|
|
9288
|
+
const licenses = comment
|
|
9289
|
+
.split("/")
|
|
9290
|
+
.map((value) => {
|
|
9291
|
+
const licenseId = value.trim();
|
|
9292
|
+
if (!licenseId.match(LICENSE_ID_COMMENTS_PATTERN)) {
|
|
9293
|
+
return undefined;
|
|
9294
|
+
}
|
|
9295
|
+
return { license: { id: licenseId } };
|
|
9296
|
+
})
|
|
9297
|
+
.filter((value) => value !== undefined);
|
|
9298
|
+
return licenses.length ? licenses : undefined;
|
|
9299
|
+
}
|
|
9300
|
+
|
|
7746
9301
|
/**
|
|
7747
9302
|
* Method to parse requirements.txt file. Must only be used internally.
|
|
7748
9303
|
*
|
|
@@ -7780,11 +9335,16 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
7780
9335
|
const lines = normalizedData.split("\n");
|
|
7781
9336
|
for (const line of lines) {
|
|
7782
9337
|
let l = line.trim();
|
|
9338
|
+
let editableRequirement = false;
|
|
7783
9339
|
if (l.includes("# Basic requirements")) {
|
|
7784
9340
|
compScope = "required";
|
|
7785
9341
|
} else if (l.includes("added by pip freeze")) {
|
|
7786
9342
|
compScope = undefined;
|
|
7787
9343
|
}
|
|
9344
|
+
if (l.startsWith("-e ") || l.startsWith("--editable ")) {
|
|
9345
|
+
editableRequirement = true;
|
|
9346
|
+
l = l.replace(/^--editable\s+|^-e\s+/, "").trim();
|
|
9347
|
+
}
|
|
7788
9348
|
if (l.startsWith("Skipping line") || l.startsWith("(add")) {
|
|
7789
9349
|
continue;
|
|
7790
9350
|
}
|
|
@@ -7833,10 +9393,49 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
7833
9393
|
markers = parts.slice(1).join(";").trim();
|
|
7834
9394
|
structuredMarkers = parseReqEnvMarkers(markers);
|
|
7835
9395
|
}
|
|
9396
|
+
const requirementManifestSource = parsePythonRequirementManifestSource(l);
|
|
9397
|
+
if (requirementManifestSource?.name) {
|
|
9398
|
+
const apkg = {
|
|
9399
|
+
name: requirementManifestSource.name,
|
|
9400
|
+
version: null,
|
|
9401
|
+
scope: compScope,
|
|
9402
|
+
evidence,
|
|
9403
|
+
};
|
|
9404
|
+
if (hashes.length > 0) {
|
|
9405
|
+
apkg.hashes = hashes;
|
|
9406
|
+
}
|
|
9407
|
+
const licenses = parseLicenseComment(comment);
|
|
9408
|
+
if (licenses) {
|
|
9409
|
+
apkg.licenses = licenses;
|
|
9410
|
+
}
|
|
9411
|
+
applyManifestSourceProperties(
|
|
9412
|
+
apkg,
|
|
9413
|
+
"cdx:pypi",
|
|
9414
|
+
requirementManifestSource,
|
|
9415
|
+
);
|
|
9416
|
+
if (editableRequirement) {
|
|
9417
|
+
addComponentProperty(apkg, "cdx:pypi:editable", "true");
|
|
9418
|
+
}
|
|
9419
|
+
if (markers) {
|
|
9420
|
+
addComponentProperty(apkg, "cdx:pip:markers", markers);
|
|
9421
|
+
if (structuredMarkers?.length > 0) {
|
|
9422
|
+
addComponentProperty(
|
|
9423
|
+
apkg,
|
|
9424
|
+
"cdx:pip:structuredMarkers",
|
|
9425
|
+
JSON.stringify(structuredMarkers),
|
|
9426
|
+
);
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
if (reqFile) {
|
|
9430
|
+
addComponentProperty(apkg, "SrcFile", reqFile);
|
|
9431
|
+
}
|
|
9432
|
+
pkgList.push(apkg);
|
|
9433
|
+
continue;
|
|
9434
|
+
}
|
|
7836
9435
|
|
|
7837
9436
|
// Handle extras (e.g., package[extra1,extra2])
|
|
7838
9437
|
let extras = null;
|
|
7839
|
-
const extrasMatch = l.match(/^([a-zA-Z0-9_
|
|
9438
|
+
const extrasMatch = l.match(/^([a-zA-Z0-9_\-.]+)(\[([^\]]+)])?(.*)$/);
|
|
7840
9439
|
if (extrasMatch) {
|
|
7841
9440
|
const [, packageName, , extrasStr, versionSpecifiers] = extrasMatch;
|
|
7842
9441
|
const name = packageName;
|
|
@@ -7866,20 +9465,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
7866
9465
|
if (hashes.length > 0) {
|
|
7867
9466
|
apkg.hashes = hashes;
|
|
7868
9467
|
}
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
.map((c) => {
|
|
7873
|
-
const licenseObj = {};
|
|
7874
|
-
const cId = c.trim();
|
|
7875
|
-
if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
|
|
7876
|
-
licenseObj.id = cId;
|
|
7877
|
-
} else {
|
|
7878
|
-
return undefined;
|
|
7879
|
-
}
|
|
7880
|
-
return { license: licenseObj };
|
|
7881
|
-
})
|
|
7882
|
-
.filter((l) => l !== undefined);
|
|
9468
|
+
const licenses = parseLicenseComment(comment);
|
|
9469
|
+
if (licenses) {
|
|
9470
|
+
apkg.licenses = licenses;
|
|
7883
9471
|
}
|
|
7884
9472
|
if (extras && extras.length > 0) {
|
|
7885
9473
|
properties.push({
|
|
@@ -7905,12 +9493,18 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
7905
9493
|
});
|
|
7906
9494
|
}
|
|
7907
9495
|
}
|
|
9496
|
+
if (editableRequirement) {
|
|
9497
|
+
properties.push({
|
|
9498
|
+
name: "cdx:pypi:editable",
|
|
9499
|
+
value: "true",
|
|
9500
|
+
});
|
|
9501
|
+
}
|
|
7908
9502
|
if (properties.length) {
|
|
7909
9503
|
apkg.properties = properties;
|
|
7910
9504
|
}
|
|
7911
9505
|
pkgList.push(apkg);
|
|
7912
9506
|
} else {
|
|
7913
|
-
const match = l.match(/^([a-zA-Z0-9_
|
|
9507
|
+
const match = l.match(/^([a-zA-Z0-9_\-.]+)(.*)$/);
|
|
7914
9508
|
if (!match) {
|
|
7915
9509
|
continue;
|
|
7916
9510
|
}
|
|
@@ -7932,20 +9526,9 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
7932
9526
|
scope: compScope,
|
|
7933
9527
|
evidence,
|
|
7934
9528
|
};
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
.map((c) => {
|
|
7939
|
-
const licenseObj = {};
|
|
7940
|
-
const cId = c.trim();
|
|
7941
|
-
if (cId.match(LICENSE_ID_COMMENTS_PATTERN)) {
|
|
7942
|
-
licenseObj.id = cId;
|
|
7943
|
-
} else {
|
|
7944
|
-
return undefined;
|
|
7945
|
-
}
|
|
7946
|
-
return { license: licenseObj };
|
|
7947
|
-
})
|
|
7948
|
-
.filter((l) => l !== undefined);
|
|
9529
|
+
const licenses = parseLicenseComment(comment);
|
|
9530
|
+
if (licenses) {
|
|
9531
|
+
apkg.licenses = licenses;
|
|
7949
9532
|
}
|
|
7950
9533
|
if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
|
|
7951
9534
|
properties.push({
|
|
@@ -7965,6 +9548,12 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
7965
9548
|
});
|
|
7966
9549
|
}
|
|
7967
9550
|
}
|
|
9551
|
+
if (editableRequirement) {
|
|
9552
|
+
properties.push({
|
|
9553
|
+
name: "cdx:pypi:editable",
|
|
9554
|
+
value: "true",
|
|
9555
|
+
});
|
|
9556
|
+
}
|
|
7968
9557
|
if (properties.length) {
|
|
7969
9558
|
apkg.properties = properties;
|
|
7970
9559
|
}
|
|
@@ -12621,48 +14210,230 @@ export function parseConanLockData(conanLockData) {
|
|
|
12621
14210
|
}
|
|
12622
14211
|
}
|
|
12623
14212
|
}
|
|
12624
|
-
return { pkgList, dependencies, parentComponentDependencies };
|
|
14213
|
+
return { pkgList, dependencies, parentComponentDependencies };
|
|
14214
|
+
}
|
|
14215
|
+
|
|
14216
|
+
/**
|
|
14217
|
+
* Parse a Conan conanfile.txt and extract required and optional packages.
|
|
14218
|
+
*
|
|
14219
|
+
* @param {string} conanData Raw text contents of a conanfile.txt
|
|
14220
|
+
* @returns {Object[]} Array of package objects with purl, name, version, and scope
|
|
14221
|
+
*/
|
|
14222
|
+
export function parseConanData(conanData) {
|
|
14223
|
+
const pkgList = [];
|
|
14224
|
+
if (!conanData) {
|
|
14225
|
+
return pkgList;
|
|
14226
|
+
}
|
|
14227
|
+
let scope = "required";
|
|
14228
|
+
conanData.split("\n").forEach((l) => {
|
|
14229
|
+
l = l.replace("\r", "");
|
|
14230
|
+
if (l.includes("[build_requires]")) {
|
|
14231
|
+
scope = "optional";
|
|
14232
|
+
}
|
|
14233
|
+
if (l.includes("[requires]")) {
|
|
14234
|
+
scope = "required";
|
|
14235
|
+
}
|
|
14236
|
+
|
|
14237
|
+
// The line must start with sequence non-whitespace characters, followed by a slash,
|
|
14238
|
+
// followed by at least one more non-whitespace character.
|
|
14239
|
+
// Provides a heuristic for locating Conan package references inside conanfile.txt files.
|
|
14240
|
+
if (l.match(/^[^\s\/]+\/\S+/)) {
|
|
14241
|
+
const [purl, name, version] =
|
|
14242
|
+
mapConanPkgRefToPurlStringAndNameAndVersion(l);
|
|
14243
|
+
if (purl !== null) {
|
|
14244
|
+
pkgList.push({
|
|
14245
|
+
name,
|
|
14246
|
+
version,
|
|
14247
|
+
purl,
|
|
14248
|
+
"bom-ref": decodeURIComponent(purl),
|
|
14249
|
+
scope,
|
|
14250
|
+
});
|
|
14251
|
+
}
|
|
14252
|
+
}
|
|
14253
|
+
});
|
|
14254
|
+
return pkgList;
|
|
14255
|
+
}
|
|
14256
|
+
|
|
14257
|
+
/**
|
|
14258
|
+
* Construct a generic package component object for collider-managed packages.
|
|
14259
|
+
*
|
|
14260
|
+
* @param {string} name Package name
|
|
14261
|
+
* @param {Object} pkgData Locked package entry from collider.lock
|
|
14262
|
+
* @param {string} lockFile Source lock file path
|
|
14263
|
+
* @param {string} dependencyKind Whether the package is direct or transitive
|
|
14264
|
+
* @returns {Object|undefined} Package component
|
|
14265
|
+
*/
|
|
14266
|
+
function buildColliderComponent(name, pkgData, lockFile, dependencyKind) {
|
|
14267
|
+
if (!name) {
|
|
14268
|
+
return undefined;
|
|
14269
|
+
}
|
|
14270
|
+
pkgData = pkgData || {};
|
|
14271
|
+
dependencyKind = dependencyKind || "transitive";
|
|
14272
|
+
const version = pkgData?.version || "";
|
|
14273
|
+
const purl = new PackageURL(
|
|
14274
|
+
"generic",
|
|
14275
|
+
"",
|
|
14276
|
+
name,
|
|
14277
|
+
version || undefined,
|
|
14278
|
+
null,
|
|
14279
|
+
null,
|
|
14280
|
+
).toString();
|
|
14281
|
+
const properties = [
|
|
14282
|
+
{
|
|
14283
|
+
name: "cdx:collider:dependencyKind",
|
|
14284
|
+
value: dependencyKind,
|
|
14285
|
+
},
|
|
14286
|
+
];
|
|
14287
|
+
if (lockFile) {
|
|
14288
|
+
properties.unshift({
|
|
14289
|
+
name: "SrcFile",
|
|
14290
|
+
value: lockFile,
|
|
14291
|
+
});
|
|
14292
|
+
}
|
|
14293
|
+
const wrapHash =
|
|
14294
|
+
typeof pkgData?.wrap_hash === "string" ? pkgData.wrap_hash.trim() : "";
|
|
14295
|
+
const wrapHashMatch = wrapHash.match(/^sha256:([0-9A-Fa-f]{64})$/);
|
|
14296
|
+
if (wrapHash) {
|
|
14297
|
+
properties.push({
|
|
14298
|
+
name: "cdx:collider:wrapHash",
|
|
14299
|
+
value: wrapHash,
|
|
14300
|
+
});
|
|
14301
|
+
}
|
|
14302
|
+
properties.push({
|
|
14303
|
+
name: "cdx:collider:hasWrapHash",
|
|
14304
|
+
value: wrapHashMatch ? "true" : "false",
|
|
14305
|
+
});
|
|
14306
|
+
if (wrapHash && !wrapHashMatch) {
|
|
14307
|
+
properties.push({
|
|
14308
|
+
name: "cdx:collider:wrapHashInvalid",
|
|
14309
|
+
value: "true",
|
|
14310
|
+
});
|
|
14311
|
+
}
|
|
14312
|
+
let originReference;
|
|
14313
|
+
if (typeof pkgData?.origin === "string" && pkgData.origin.trim()) {
|
|
14314
|
+
try {
|
|
14315
|
+
const originUrl = new URL(pkgData.origin.trim());
|
|
14316
|
+
const originHadSensitiveParts = Boolean(
|
|
14317
|
+
originUrl.username ||
|
|
14318
|
+
originUrl.password ||
|
|
14319
|
+
originUrl.search ||
|
|
14320
|
+
originUrl.hash,
|
|
14321
|
+
);
|
|
14322
|
+
originUrl.username = "";
|
|
14323
|
+
originUrl.password = "";
|
|
14324
|
+
originUrl.search = "";
|
|
14325
|
+
originUrl.hash = "";
|
|
14326
|
+
originReference = originUrl.toString();
|
|
14327
|
+
properties.push({
|
|
14328
|
+
name: "cdx:collider:origin",
|
|
14329
|
+
value: originReference,
|
|
14330
|
+
});
|
|
14331
|
+
properties.push({
|
|
14332
|
+
name: "cdx:collider:originScheme",
|
|
14333
|
+
value: originUrl.protocol.replace(":", ""),
|
|
14334
|
+
});
|
|
14335
|
+
if (originUrl.host) {
|
|
14336
|
+
properties.push({
|
|
14337
|
+
name: "cdx:collider:originHost",
|
|
14338
|
+
value: originUrl.host,
|
|
14339
|
+
});
|
|
14340
|
+
}
|
|
14341
|
+
if (originHadSensitiveParts) {
|
|
14342
|
+
properties.push({
|
|
14343
|
+
name: "cdx:collider:originSanitized",
|
|
14344
|
+
value: "true",
|
|
14345
|
+
});
|
|
14346
|
+
}
|
|
14347
|
+
} catch {
|
|
14348
|
+
thoughtLog("Ignoring invalid Collider origin URL");
|
|
14349
|
+
}
|
|
14350
|
+
}
|
|
14351
|
+
const component = {
|
|
14352
|
+
name,
|
|
14353
|
+
version,
|
|
14354
|
+
purl,
|
|
14355
|
+
"bom-ref": decodeURIComponent(purl),
|
|
14356
|
+
properties,
|
|
14357
|
+
};
|
|
14358
|
+
if (dependencyKind === "direct") {
|
|
14359
|
+
component.scope = "required";
|
|
14360
|
+
}
|
|
14361
|
+
if (wrapHashMatch) {
|
|
14362
|
+
component.hashes = [
|
|
14363
|
+
{
|
|
14364
|
+
alg: "SHA-256",
|
|
14365
|
+
content: wrapHashMatch[1].toLowerCase(),
|
|
14366
|
+
},
|
|
14367
|
+
];
|
|
14368
|
+
}
|
|
14369
|
+
if (originReference) {
|
|
14370
|
+
component.externalReferences = [
|
|
14371
|
+
{
|
|
14372
|
+
type: "distribution",
|
|
14373
|
+
url: originReference,
|
|
14374
|
+
},
|
|
14375
|
+
];
|
|
14376
|
+
}
|
|
14377
|
+
return component;
|
|
12625
14378
|
}
|
|
12626
14379
|
|
|
12627
14380
|
/**
|
|
12628
|
-
* Parse
|
|
14381
|
+
* Parse Collider lock file data (collider.lock) and return the package list and
|
|
14382
|
+
* parent component dependencies.
|
|
12629
14383
|
*
|
|
12630
|
-
* @param {string}
|
|
12631
|
-
* @
|
|
14384
|
+
* @param {string} colliderLockData Raw JSON string of the Collider lock file
|
|
14385
|
+
* @param {string} lockFile Source lock file path
|
|
14386
|
+
* @returns {{ pkgList: Object[], dependencies: Object, parentComponentDependencies: string[] }}
|
|
12632
14387
|
*/
|
|
12633
|
-
export function
|
|
14388
|
+
export function parseColliderLockData(colliderLockData, lockFile) {
|
|
12634
14389
|
const pkgList = [];
|
|
12635
|
-
|
|
12636
|
-
|
|
14390
|
+
const dependencies = {};
|
|
14391
|
+
const parentComponentDependencies = [];
|
|
14392
|
+
if (!colliderLockData) {
|
|
14393
|
+
return { pkgList, dependencies, parentComponentDependencies };
|
|
12637
14394
|
}
|
|
12638
|
-
let
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
14395
|
+
let parsedLockFile;
|
|
14396
|
+
try {
|
|
14397
|
+
parsedLockFile = JSON.parse(colliderLockData);
|
|
14398
|
+
} catch {
|
|
14399
|
+
return { pkgList, dependencies, parentComponentDependencies };
|
|
14400
|
+
}
|
|
14401
|
+
const addedBomRefs = new Set();
|
|
14402
|
+
const directDependencies = parsedLockFile.dependencies || {};
|
|
14403
|
+
const packages = parsedLockFile.packages || {};
|
|
14404
|
+
for (const [name, pkgData] of Object.entries(directDependencies)) {
|
|
14405
|
+
const component = buildColliderComponent(name, pkgData, lockFile, "direct");
|
|
14406
|
+
if (!component) {
|
|
14407
|
+
continue;
|
|
12643
14408
|
}
|
|
12644
|
-
if (
|
|
12645
|
-
|
|
14409
|
+
if (!addedBomRefs.has(component["bom-ref"])) {
|
|
14410
|
+
pkgList.push(component);
|
|
14411
|
+
addedBomRefs.add(component["bom-ref"]);
|
|
12646
14412
|
}
|
|
12647
|
-
|
|
12648
|
-
|
|
12649
|
-
// followed by at least one more non-whitespace character.
|
|
12650
|
-
// Provides a heuristic for locating Conan package references inside conanfile.txt files.
|
|
12651
|
-
if (l.match(/^[^\s\/]+\/\S+/)) {
|
|
12652
|
-
const [purl, name, version] =
|
|
12653
|
-
mapConanPkgRefToPurlStringAndNameAndVersion(l);
|
|
12654
|
-
if (purl !== null) {
|
|
12655
|
-
pkgList.push({
|
|
12656
|
-
name,
|
|
12657
|
-
version,
|
|
12658
|
-
purl,
|
|
12659
|
-
"bom-ref": decodeURIComponent(purl),
|
|
12660
|
-
scope,
|
|
12661
|
-
});
|
|
12662
|
-
}
|
|
14413
|
+
if (!parentComponentDependencies.includes(component["bom-ref"])) {
|
|
14414
|
+
parentComponentDependencies.push(component["bom-ref"]);
|
|
12663
14415
|
}
|
|
12664
|
-
|
|
12665
|
-
|
|
14416
|
+
if (!(component["bom-ref"] in dependencies)) {
|
|
14417
|
+
dependencies[component["bom-ref"]] = [];
|
|
14418
|
+
}
|
|
14419
|
+
}
|
|
14420
|
+
for (const [name, pkgData] of Object.entries(packages)) {
|
|
14421
|
+
const component = buildColliderComponent(
|
|
14422
|
+
name,
|
|
14423
|
+
pkgData,
|
|
14424
|
+
lockFile,
|
|
14425
|
+
"transitive",
|
|
14426
|
+
);
|
|
14427
|
+
if (!component || addedBomRefs.has(component["bom-ref"])) {
|
|
14428
|
+
continue;
|
|
14429
|
+
}
|
|
14430
|
+
pkgList.push(component);
|
|
14431
|
+
addedBomRefs.add(component["bom-ref"]);
|
|
14432
|
+
if (!(component["bom-ref"] in dependencies)) {
|
|
14433
|
+
dependencies[component["bom-ref"]] = [];
|
|
14434
|
+
}
|
|
14435
|
+
}
|
|
14436
|
+
return { pkgList, dependencies, parentComponentDependencies };
|
|
12666
14437
|
}
|
|
12667
14438
|
|
|
12668
14439
|
/**
|
|
@@ -12800,7 +14571,7 @@ export function parseFlakeNix(flakeNixFile) {
|
|
|
12800
14571
|
const flakeContent = readFileSync(flakeNixFile, "utf-8");
|
|
12801
14572
|
|
|
12802
14573
|
// Extract inputs from flake.nix using regex
|
|
12803
|
-
const inputsRegex = /inputs\s*=\s*\{[^}]
|
|
14574
|
+
const inputsRegex = /inputs\s*=\s*\{[^}]*}/g;
|
|
12804
14575
|
let match;
|
|
12805
14576
|
while ((match = inputsRegex.exec(flakeContent)) !== null) {
|
|
12806
14577
|
const inputBlock = match[0];
|
|
@@ -12808,7 +14579,7 @@ export function parseFlakeNix(flakeNixFile) {
|
|
|
12808
14579
|
// Match different input patterns including nested inputs
|
|
12809
14580
|
const inputPatterns = [
|
|
12810
14581
|
/([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\.url\s*=\s*"([^"]+)"/g,
|
|
12811
|
-
/([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]
|
|
14582
|
+
/([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)\s*=\s*\{\s*url\s*=\s*"([^"]+)"[^}]*}/gs,
|
|
12812
14583
|
];
|
|
12813
14584
|
|
|
12814
14585
|
const addedPackages = new Set();
|
|
@@ -14750,16 +16521,22 @@ export function convertOSQueryResults(
|
|
|
14750
16521
|
if (name) {
|
|
14751
16522
|
name = sanitizeOsQueryIdentity(name);
|
|
14752
16523
|
group = sanitizeOsQueryIdentity(group);
|
|
14753
|
-
const
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
16524
|
+
const isCryptoAsset = queryObj.componentType === "cryptographic-asset";
|
|
16525
|
+
const purl = shouldCreateOsQueryPurl(queryObj.componentType)
|
|
16526
|
+
? createOsQueryPurl(
|
|
16527
|
+
queryObj.purlType,
|
|
16528
|
+
group,
|
|
16529
|
+
name,
|
|
16530
|
+
version,
|
|
16531
|
+
qualifiers,
|
|
16532
|
+
subpath,
|
|
16533
|
+
)
|
|
16534
|
+
: undefined;
|
|
14761
16535
|
const props = [{ name: "cdx:osquery:category", value: queryCategory }];
|
|
14762
16536
|
props.push(...createLolbasProperties(queryCategory, res));
|
|
16537
|
+
if (platform() === "linux") {
|
|
16538
|
+
props.push(...createGtfoBinsPropertiesFromRow(queryCategory, res));
|
|
16539
|
+
}
|
|
14763
16540
|
let providesList;
|
|
14764
16541
|
if (enhance) {
|
|
14765
16542
|
switch (queryObj.purlType) {
|
|
@@ -14785,25 +16562,39 @@ export function convertOSQueryResults(
|
|
|
14785
16562
|
if (providesList) {
|
|
14786
16563
|
props.push({ name: "PkgProvides", value: providesList.join(", ") });
|
|
14787
16564
|
}
|
|
16565
|
+
const cryptoProperties = isCryptoAsset
|
|
16566
|
+
? createOsQueryCryptoProperties(queryCategory, res, version)
|
|
16567
|
+
: undefined;
|
|
16568
|
+
const hashes = createOsQueryComponentHashes(res);
|
|
14788
16569
|
const apkg = {
|
|
14789
16570
|
name,
|
|
14790
16571
|
group,
|
|
14791
16572
|
version: version || "",
|
|
14792
16573
|
description,
|
|
14793
16574
|
publisher,
|
|
14794
|
-
"bom-ref":
|
|
16575
|
+
"bom-ref": createOsQueryBomRef(
|
|
16576
|
+
queryCategory,
|
|
16577
|
+
queryObj.componentType,
|
|
16578
|
+
res,
|
|
16579
|
+
name,
|
|
16580
|
+
version,
|
|
16581
|
+
purl,
|
|
16582
|
+
),
|
|
14795
16583
|
purl,
|
|
14796
16584
|
scope,
|
|
14797
16585
|
type: queryObj.componentType,
|
|
14798
16586
|
};
|
|
16587
|
+
if (hashes?.length) {
|
|
16588
|
+
apkg.hashes = hashes;
|
|
16589
|
+
}
|
|
16590
|
+
if (cryptoProperties) {
|
|
16591
|
+
apkg.cryptoProperties = cryptoProperties;
|
|
16592
|
+
}
|
|
14799
16593
|
for (const k of Object.keys(res).filter((p) => {
|
|
14800
16594
|
if (["version", "description", "publisher"].includes(p)) {
|
|
14801
16595
|
return false;
|
|
14802
16596
|
}
|
|
14803
|
-
|
|
14804
|
-
return false;
|
|
14805
|
-
}
|
|
14806
|
-
return true;
|
|
16597
|
+
return !(queryObj.purlType !== "chrome-extension" && p === "name");
|
|
14807
16598
|
})) {
|
|
14808
16599
|
if (res[k] && res[k] !== "null") {
|
|
14809
16600
|
props.push({
|
|
@@ -14820,6 +16611,182 @@ export function convertOSQueryResults(
|
|
|
14820
16611
|
return pkgList;
|
|
14821
16612
|
}
|
|
14822
16613
|
|
|
16614
|
+
function createOsQueryComponentHashes(res) {
|
|
16615
|
+
const hashes = [];
|
|
16616
|
+
if (res?.md5) {
|
|
16617
|
+
hashes.push({ alg: "MD5", content: res.md5 });
|
|
16618
|
+
}
|
|
16619
|
+
if (res?.sha1) {
|
|
16620
|
+
hashes.push({ alg: "SHA-1", content: res.sha1 });
|
|
16621
|
+
}
|
|
16622
|
+
if (res?.sha256) {
|
|
16623
|
+
hashes.push({ alg: "SHA-256", content: res.sha256 });
|
|
16624
|
+
}
|
|
16625
|
+
return hashes.length ? hashes : undefined;
|
|
16626
|
+
}
|
|
16627
|
+
|
|
16628
|
+
function createOsQueryBomRef(
|
|
16629
|
+
queryCategory,
|
|
16630
|
+
componentType,
|
|
16631
|
+
res,
|
|
16632
|
+
name,
|
|
16633
|
+
version,
|
|
16634
|
+
purl,
|
|
16635
|
+
) {
|
|
16636
|
+
if (purl) {
|
|
16637
|
+
return decodeURIComponent(purl);
|
|
16638
|
+
}
|
|
16639
|
+
if (componentType === "cryptographic-asset") {
|
|
16640
|
+
return createOsQueryCryptoBomRef(queryCategory, res, name, version);
|
|
16641
|
+
}
|
|
16642
|
+
const identityEntry = [
|
|
16643
|
+
["path", res?.path],
|
|
16644
|
+
["key_file", res?.key_file],
|
|
16645
|
+
["history_file", res?.history_file],
|
|
16646
|
+
["fragment_path", res?.fragment_path],
|
|
16647
|
+
["source_path", res?.source_path],
|
|
16648
|
+
["source", res?.source],
|
|
16649
|
+
["key", res?.key],
|
|
16650
|
+
["label", res?.label],
|
|
16651
|
+
["identifier", res?.identifier],
|
|
16652
|
+
["uuid", res?.uuid],
|
|
16653
|
+
["device_id", res?.device_id],
|
|
16654
|
+
["sid", res?.sid],
|
|
16655
|
+
["logon_id", res?.logon_id],
|
|
16656
|
+
["pid", res?.pid],
|
|
16657
|
+
["uid", res?.uid],
|
|
16658
|
+
].find(
|
|
16659
|
+
([, value]) =>
|
|
16660
|
+
value !== undefined && value !== null && String(value).length,
|
|
16661
|
+
);
|
|
16662
|
+
return createOsQueryFallbackBomRef(
|
|
16663
|
+
queryCategory,
|
|
16664
|
+
componentType,
|
|
16665
|
+
name,
|
|
16666
|
+
version,
|
|
16667
|
+
identityEntry?.[0],
|
|
16668
|
+
identityEntry?.[1],
|
|
16669
|
+
);
|
|
16670
|
+
}
|
|
16671
|
+
|
|
16672
|
+
function createOsQueryCryptoBomRef(queryCategory, res, name, version) {
|
|
16673
|
+
const encodedName = encodeURIComponent(
|
|
16674
|
+
name || queryCategory || "crypto-asset",
|
|
16675
|
+
);
|
|
16676
|
+
switch (queryCategory) {
|
|
16677
|
+
case "trusted_gpg_keys":
|
|
16678
|
+
return `crypto/related-crypto-material/public-key/${encodedName}@sha256:${res?.sha256 || version || "unknown"}`;
|
|
16679
|
+
case "kernel_keys":
|
|
16680
|
+
return `crypto/related-crypto-material/key/${encodedName}@${version || res?.serial_number || "unknown"}`;
|
|
16681
|
+
case "certificates":
|
|
16682
|
+
case "secureboot_certificates":
|
|
16683
|
+
return `crypto/certificate/${encodedName}@${res?.sha256 ? `sha256:${res.sha256}` : version || "unknown"}`;
|
|
16684
|
+
default:
|
|
16685
|
+
return `crypto/related-crypto-material/unknown/${encodedName}@${version || "unknown"}`;
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
16688
|
+
|
|
16689
|
+
function createOsQueryCryptoProperties(queryCategory, res, version) {
|
|
16690
|
+
switch (queryCategory) {
|
|
16691
|
+
case "trusted_gpg_keys":
|
|
16692
|
+
return {
|
|
16693
|
+
assetType: "related-crypto-material",
|
|
16694
|
+
relatedCryptoMaterialProperties: {
|
|
16695
|
+
type: "public-key",
|
|
16696
|
+
id: res?.sha256 || version || res?.path || res?.name || "unknown",
|
|
16697
|
+
state: "active",
|
|
16698
|
+
},
|
|
16699
|
+
};
|
|
16700
|
+
case "kernel_keys":
|
|
16701
|
+
return {
|
|
16702
|
+
assetType: "related-crypto-material",
|
|
16703
|
+
relatedCryptoMaterialProperties: {
|
|
16704
|
+
type: "key",
|
|
16705
|
+
id:
|
|
16706
|
+
res?.serial_number ||
|
|
16707
|
+
version ||
|
|
16708
|
+
res?.description ||
|
|
16709
|
+
res?.name ||
|
|
16710
|
+
"unknown",
|
|
16711
|
+
state: res?.timeout === "expd" ? "deactivated" : "active",
|
|
16712
|
+
},
|
|
16713
|
+
};
|
|
16714
|
+
case "certificates":
|
|
16715
|
+
case "secureboot_certificates": {
|
|
16716
|
+
const certificateFileExtension = extname(res?.path || "")
|
|
16717
|
+
.replace(/^\./, "")
|
|
16718
|
+
.toLowerCase();
|
|
16719
|
+
const certificateFormat =
|
|
16720
|
+
queryCategory === "secureboot_certificates"
|
|
16721
|
+
? "X.509"
|
|
16722
|
+
: deriveCertificateFormat(certificateFileExtension);
|
|
16723
|
+
return {
|
|
16724
|
+
assetType: "certificate",
|
|
16725
|
+
algorithmProperties: {
|
|
16726
|
+
executionEnvironment: "unknown",
|
|
16727
|
+
implementationPlatform: "unknown",
|
|
16728
|
+
},
|
|
16729
|
+
certificateProperties: {
|
|
16730
|
+
serialNumber: res?.serial || undefined,
|
|
16731
|
+
subjectName: res?.subject || res?.common_name || undefined,
|
|
16732
|
+
issuerName: res?.issuer || undefined,
|
|
16733
|
+
notValidBefore: normalizeCertificateDate(res?.not_valid_before),
|
|
16734
|
+
notValidAfter: normalizeCertificateDate(res?.not_valid_after),
|
|
16735
|
+
certificateFormat,
|
|
16736
|
+
certificateFileExtension: certificateFileExtension || undefined,
|
|
16737
|
+
fingerprint: res?.sha1
|
|
16738
|
+
? { alg: "SHA-1", content: res.sha1 }
|
|
16739
|
+
: undefined,
|
|
16740
|
+
},
|
|
16741
|
+
};
|
|
16742
|
+
}
|
|
16743
|
+
default:
|
|
16744
|
+
return {
|
|
16745
|
+
assetType: "related-crypto-material",
|
|
16746
|
+
relatedCryptoMaterialProperties: {
|
|
16747
|
+
type: "unknown",
|
|
16748
|
+
id: version || res?.path || res?.name || "unknown",
|
|
16749
|
+
},
|
|
16750
|
+
};
|
|
16751
|
+
}
|
|
16752
|
+
}
|
|
16753
|
+
|
|
16754
|
+
function deriveCertificateFormat(certificateFileExtension) {
|
|
16755
|
+
switch ((certificateFileExtension || "").toLowerCase()) {
|
|
16756
|
+
case "pem":
|
|
16757
|
+
return "PEM";
|
|
16758
|
+
case "der":
|
|
16759
|
+
return "DER";
|
|
16760
|
+
case "cer":
|
|
16761
|
+
case "crt":
|
|
16762
|
+
return "X.509";
|
|
16763
|
+
default:
|
|
16764
|
+
return undefined;
|
|
16765
|
+
}
|
|
16766
|
+
}
|
|
16767
|
+
|
|
16768
|
+
function normalizeCertificateDate(value) {
|
|
16769
|
+
if (value === undefined || value === null || value === "") {
|
|
16770
|
+
return undefined;
|
|
16771
|
+
}
|
|
16772
|
+
const stringValue = `${value}`.trim();
|
|
16773
|
+
if (!stringValue) {
|
|
16774
|
+
return undefined;
|
|
16775
|
+
}
|
|
16776
|
+
const numericValue = Number(stringValue);
|
|
16777
|
+
if (Number.isFinite(numericValue)) {
|
|
16778
|
+
const millis = stringValue.length > 10 ? numericValue : numericValue * 1000;
|
|
16779
|
+
const parsedDate = new Date(millis);
|
|
16780
|
+
return Number.isNaN(parsedDate.getTime())
|
|
16781
|
+
? undefined
|
|
16782
|
+
: parsedDate.toISOString();
|
|
16783
|
+
}
|
|
16784
|
+
const parsedDate = new Date(stringValue);
|
|
16785
|
+
return Number.isNaN(parsedDate.getTime())
|
|
16786
|
+
? undefined
|
|
16787
|
+
: parsedDate.toISOString();
|
|
16788
|
+
}
|
|
16789
|
+
|
|
14823
16790
|
/**
|
|
14824
16791
|
* Create a PackageURL object from a repository URL string, package type, and version.
|
|
14825
16792
|
*
|
|
@@ -16286,6 +18253,14 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
16286
18253
|
// continue regardless of error
|
|
16287
18254
|
}
|
|
16288
18255
|
gradleCmd = resolve(join(srcPath, findGradleFile));
|
|
18256
|
+
recordDecisionActivity(gradleCmd, {
|
|
18257
|
+
metadata: {
|
|
18258
|
+
decisionType: "path-resolution",
|
|
18259
|
+
selectedSource: "project-wrapper",
|
|
18260
|
+
tool: "gradle",
|
|
18261
|
+
},
|
|
18262
|
+
reason: `Selected project-local Gradle wrapper ${gradleCmd}.`,
|
|
18263
|
+
});
|
|
16289
18264
|
} else if (rootPath && safeExistsSync(join(rootPath, findGradleFile))) {
|
|
16290
18265
|
// Check if the root directory has a wrapper script
|
|
16291
18266
|
try {
|
|
@@ -16294,10 +18269,43 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
16294
18269
|
// continue regardless of error
|
|
16295
18270
|
}
|
|
16296
18271
|
gradleCmd = resolve(join(rootPath, findGradleFile));
|
|
18272
|
+
recordDecisionActivity(gradleCmd, {
|
|
18273
|
+
metadata: {
|
|
18274
|
+
decisionType: "path-resolution",
|
|
18275
|
+
selectedSource: "root-wrapper",
|
|
18276
|
+
tool: "gradle",
|
|
18277
|
+
},
|
|
18278
|
+
reason: `Selected root-level Gradle wrapper ${gradleCmd}.`,
|
|
18279
|
+
});
|
|
16297
18280
|
} else if (process.env.GRADLE_CMD) {
|
|
16298
18281
|
gradleCmd = process.env.GRADLE_CMD;
|
|
18282
|
+
recordDecisionActivity(gradleCmd, {
|
|
18283
|
+
metadata: {
|
|
18284
|
+
decisionType: "path-resolution",
|
|
18285
|
+
selectedSource: "GRADLE_CMD",
|
|
18286
|
+
tool: "gradle",
|
|
18287
|
+
},
|
|
18288
|
+
reason: `Selected Gradle command from GRADLE_CMD (${gradleCmd}).`,
|
|
18289
|
+
});
|
|
16299
18290
|
} else if (process.env.GRADLE_HOME) {
|
|
16300
18291
|
gradleCmd = join(process.env.GRADLE_HOME, "bin", "gradle");
|
|
18292
|
+
recordDecisionActivity(gradleCmd, {
|
|
18293
|
+
metadata: {
|
|
18294
|
+
decisionType: "path-resolution",
|
|
18295
|
+
selectedSource: "GRADLE_HOME",
|
|
18296
|
+
tool: "gradle",
|
|
18297
|
+
},
|
|
18298
|
+
reason: `Selected Gradle command from GRADLE_HOME (${gradleCmd}).`,
|
|
18299
|
+
});
|
|
18300
|
+
} else {
|
|
18301
|
+
recordDecisionActivity(gradleCmd, {
|
|
18302
|
+
metadata: {
|
|
18303
|
+
decisionType: "path-resolution",
|
|
18304
|
+
selectedSource: "PATH",
|
|
18305
|
+
tool: "gradle",
|
|
18306
|
+
},
|
|
18307
|
+
reason: "Falling back to Gradle from PATH.",
|
|
18308
|
+
});
|
|
16301
18309
|
}
|
|
16302
18310
|
return gradleCmd;
|
|
16303
18311
|
}
|
|
@@ -17175,6 +19183,14 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
17175
19183
|
}
|
|
17176
19184
|
mavenWrapperCmd = resolve(join(srcPath, findMavenFile));
|
|
17177
19185
|
isWrapperFound = true;
|
|
19186
|
+
recordDecisionActivity(mavenWrapperCmd, {
|
|
19187
|
+
metadata: {
|
|
19188
|
+
decisionType: "path-resolution",
|
|
19189
|
+
selectedSource: "project-wrapper-candidate",
|
|
19190
|
+
tool: "maven",
|
|
19191
|
+
},
|
|
19192
|
+
reason: `Found Maven wrapper candidate ${mavenWrapperCmd}.`,
|
|
19193
|
+
});
|
|
17178
19194
|
} else if (rootPath && safeExistsSync(join(rootPath, findMavenFile))) {
|
|
17179
19195
|
// Check if the root directory has a wrapper script
|
|
17180
19196
|
try {
|
|
@@ -17184,6 +19200,14 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
17184
19200
|
}
|
|
17185
19201
|
mavenWrapperCmd = resolve(join(rootPath, findMavenFile));
|
|
17186
19202
|
isWrapperFound = true;
|
|
19203
|
+
recordDecisionActivity(mavenWrapperCmd, {
|
|
19204
|
+
metadata: {
|
|
19205
|
+
decisionType: "path-resolution",
|
|
19206
|
+
selectedSource: "root-wrapper-candidate",
|
|
19207
|
+
tool: "maven",
|
|
19208
|
+
},
|
|
19209
|
+
reason: `Found root-level Maven wrapper candidate ${mavenWrapperCmd}.`,
|
|
19210
|
+
});
|
|
17187
19211
|
}
|
|
17188
19212
|
if (isWrapperFound) {
|
|
17189
19213
|
if (DEBUG_MODE) {
|
|
@@ -17192,25 +19216,74 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
17192
19216
|
);
|
|
17193
19217
|
}
|
|
17194
19218
|
const result = safeSpawnSync(mavenWrapperCmd, ["wrapper:wrapper"], {
|
|
19219
|
+
cdxgenActivity: {
|
|
19220
|
+
kind: "probe",
|
|
19221
|
+
metadata: {
|
|
19222
|
+
tool: "maven",
|
|
19223
|
+
},
|
|
19224
|
+
probeType: "wrapper-readiness",
|
|
19225
|
+
},
|
|
17195
19226
|
cwd: rootPath,
|
|
17196
19227
|
shell: isWin,
|
|
17197
19228
|
});
|
|
17198
19229
|
if (!result.error && !result.status) {
|
|
17199
19230
|
isWrapperReady = true;
|
|
17200
19231
|
mavenCmd = mavenWrapperCmd;
|
|
19232
|
+
recordDecisionActivity(mavenCmd, {
|
|
19233
|
+
metadata: {
|
|
19234
|
+
decisionType: "path-resolution",
|
|
19235
|
+
selectedSource: "wrapper",
|
|
19236
|
+
tool: "maven",
|
|
19237
|
+
},
|
|
19238
|
+
reason: `Selected Maven wrapper ${mavenCmd} after readiness probe.`,
|
|
19239
|
+
});
|
|
17201
19240
|
} else {
|
|
17202
19241
|
if (DEBUG_MODE) {
|
|
17203
19242
|
console.log(
|
|
17204
19243
|
"Maven wrapper script test has failed. Will use the installed version of maven.",
|
|
17205
19244
|
);
|
|
17206
19245
|
}
|
|
19246
|
+
recordDecisionActivity(mavenWrapperCmd, {
|
|
19247
|
+
metadata: {
|
|
19248
|
+
decisionType: "fallback",
|
|
19249
|
+
selectedSource: "PATH",
|
|
19250
|
+
skippedSource: "wrapper",
|
|
19251
|
+
tool: "maven",
|
|
19252
|
+
},
|
|
19253
|
+
reason: `Maven wrapper readiness probe failed for ${mavenWrapperCmd}; falling back to installed Maven.`,
|
|
19254
|
+
});
|
|
17207
19255
|
}
|
|
17208
19256
|
}
|
|
17209
19257
|
if (!isWrapperFound || !isWrapperReady) {
|
|
17210
19258
|
if (process.env.MVN_CMD || process.env.MAVEN_CMD) {
|
|
17211
19259
|
mavenCmd = process.env.MVN_CMD || process.env.MAVEN_CMD;
|
|
19260
|
+
recordDecisionActivity(mavenCmd, {
|
|
19261
|
+
metadata: {
|
|
19262
|
+
decisionType: "path-resolution",
|
|
19263
|
+
selectedSource: process.env.MVN_CMD ? "MVN_CMD" : "MAVEN_CMD",
|
|
19264
|
+
tool: "maven",
|
|
19265
|
+
},
|
|
19266
|
+
reason: `Selected Maven command from environment (${mavenCmd}).`,
|
|
19267
|
+
});
|
|
17212
19268
|
} else if (process.env.MAVEN_HOME) {
|
|
17213
19269
|
mavenCmd = join(process.env.MAVEN_HOME, "bin", "mvn");
|
|
19270
|
+
recordDecisionActivity(mavenCmd, {
|
|
19271
|
+
metadata: {
|
|
19272
|
+
decisionType: "path-resolution",
|
|
19273
|
+
selectedSource: "MAVEN_HOME",
|
|
19274
|
+
tool: "maven",
|
|
19275
|
+
},
|
|
19276
|
+
reason: `Selected Maven command from MAVEN_HOME (${mavenCmd}).`,
|
|
19277
|
+
});
|
|
19278
|
+
} else {
|
|
19279
|
+
recordDecisionActivity(mavenCmd, {
|
|
19280
|
+
metadata: {
|
|
19281
|
+
decisionType: "path-resolution",
|
|
19282
|
+
selectedSource: "PATH",
|
|
19283
|
+
tool: "maven",
|
|
19284
|
+
},
|
|
19285
|
+
reason: "Falling back to Maven from PATH.",
|
|
19286
|
+
});
|
|
17214
19287
|
}
|
|
17215
19288
|
}
|
|
17216
19289
|
return mavenCmd;
|
|
@@ -18363,6 +20436,92 @@ export function parsePackageJsonName(name) {
|
|
|
18363
20436
|
return returnObject;
|
|
18364
20437
|
}
|
|
18365
20438
|
|
|
20439
|
+
/**
|
|
20440
|
+
* Collect bom-refs from metadata.tools entries.
|
|
20441
|
+
*
|
|
20442
|
+
* @param {Object[]|Object} tools CycloneDX metadata.tools section
|
|
20443
|
+
* @param {Function} predicate Optional filter function
|
|
20444
|
+
* @returns {string[]} Unique tool bom-refs
|
|
20445
|
+
*/
|
|
20446
|
+
export function extractToolRefs(tools, predicate) {
|
|
20447
|
+
if (!tools) {
|
|
20448
|
+
return [];
|
|
20449
|
+
}
|
|
20450
|
+
const toolRefs = new Set();
|
|
20451
|
+
const toolList = Array.isArray(tools)
|
|
20452
|
+
? tools
|
|
20453
|
+
: [...(tools.components || []), ...(tools.services || [])];
|
|
20454
|
+
for (const tool of toolList) {
|
|
20455
|
+
let toolRef = tool?.["bom-ref"];
|
|
20456
|
+
if (!toolRef && tool?.purl) {
|
|
20457
|
+
toolRef = decodeURIComponent(tool.purl);
|
|
20458
|
+
}
|
|
20459
|
+
if (!toolRef && tool?.name) {
|
|
20460
|
+
try {
|
|
20461
|
+
toolRef = new PackageURL(
|
|
20462
|
+
"generic",
|
|
20463
|
+
tool.group || tool.publisher || tool.manufacturer?.name || undefined,
|
|
20464
|
+
tool.name,
|
|
20465
|
+
tool.version || undefined,
|
|
20466
|
+
null,
|
|
20467
|
+
null,
|
|
20468
|
+
).toString();
|
|
20469
|
+
} catch (_err) {
|
|
20470
|
+
thoughtLog("Unable to derive bom-ref for external tool", {
|
|
20471
|
+
group: tool.group,
|
|
20472
|
+
manufacturer: tool.manufacturer?.name,
|
|
20473
|
+
name: tool.name,
|
|
20474
|
+
publisher: tool.publisher,
|
|
20475
|
+
version: tool.version,
|
|
20476
|
+
});
|
|
20477
|
+
toolRef = undefined;
|
|
20478
|
+
}
|
|
20479
|
+
}
|
|
20480
|
+
if (!toolRef) {
|
|
20481
|
+
continue;
|
|
20482
|
+
}
|
|
20483
|
+
if (!tool["bom-ref"]) {
|
|
20484
|
+
tool["bom-ref"] = toolRef;
|
|
20485
|
+
}
|
|
20486
|
+
if (predicate && !predicate(tool)) {
|
|
20487
|
+
continue;
|
|
20488
|
+
}
|
|
20489
|
+
toolRefs.add(toolRef);
|
|
20490
|
+
}
|
|
20491
|
+
return Array.from(toolRefs);
|
|
20492
|
+
}
|
|
20493
|
+
|
|
20494
|
+
/**
|
|
20495
|
+
* Attach evidence.identity.tools references to the supplied subjects.
|
|
20496
|
+
*
|
|
20497
|
+
* @param {Object|Object[]} subjects Component or service objects
|
|
20498
|
+
* @param {string[]} toolRefs Tool bom-refs
|
|
20499
|
+
* @returns {Object|Object[]} The same mutated subject(s)
|
|
20500
|
+
*/
|
|
20501
|
+
export function attachIdentityTools(subjects, toolRefs) {
|
|
20502
|
+
if (!subjects || !toolRefs?.length) {
|
|
20503
|
+
return subjects;
|
|
20504
|
+
}
|
|
20505
|
+
const uniqueToolRefs = Array.from(new Set(toolRefs.filter(Boolean)));
|
|
20506
|
+
if (!uniqueToolRefs.length) {
|
|
20507
|
+
return subjects;
|
|
20508
|
+
}
|
|
20509
|
+
const subjectList = Array.isArray(subjects) ? subjects : [subjects];
|
|
20510
|
+
for (const subject of subjectList) {
|
|
20511
|
+
const identities = Array.isArray(subject?.evidence?.identity)
|
|
20512
|
+
? subject.evidence.identity
|
|
20513
|
+
: subject?.evidence?.identity
|
|
20514
|
+
? [subject.evidence.identity]
|
|
20515
|
+
: [];
|
|
20516
|
+
for (const identity of identities) {
|
|
20517
|
+
identity.tools = Array.from(
|
|
20518
|
+
new Set([...(identity.tools || []), ...uniqueToolRefs]),
|
|
20519
|
+
);
|
|
20520
|
+
}
|
|
20521
|
+
}
|
|
20522
|
+
return subjects;
|
|
20523
|
+
}
|
|
20524
|
+
|
|
18366
20525
|
/**
|
|
18367
20526
|
* Method to add occurrence evidence for components based on import statements. Currently useful for js
|
|
18368
20527
|
*
|
|
@@ -18459,16 +20618,17 @@ export async function addEvidenceForImports(
|
|
|
18459
20618
|
const evidences = allImports[subevidence];
|
|
18460
20619
|
for (const evidence of evidences) {
|
|
18461
20620
|
if (evidence && Object.keys(evidence).length && evidence.fileName) {
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
20621
|
+
const occurrence = createOccurrenceEvidence(evidence.fileName, {
|
|
20622
|
+
...(evidence.lineNumber ? { line: evidence.lineNumber } : {}),
|
|
20623
|
+
});
|
|
20624
|
+
if (occurrence) {
|
|
20625
|
+
pkg.evidence = pkg.evidence || {};
|
|
20626
|
+
pkg.evidence.occurrences = pkg.evidence.occurrences || [];
|
|
20627
|
+
const occurrenceLocation = `${occurrence.location}${occurrence.line ? `#${occurrence.line}` : ""}`;
|
|
20628
|
+
if (!seenOccurrenceLocations.has(occurrenceLocation)) {
|
|
20629
|
+
pkg.evidence.occurrences.push(occurrence);
|
|
20630
|
+
seenOccurrenceLocations.add(occurrenceLocation);
|
|
20631
|
+
}
|
|
18472
20632
|
}
|
|
18473
20633
|
importedModules.add(evidence.importedAs);
|
|
18474
20634
|
for (const importedSm of evidence.importedModules || []) {
|
|
@@ -19693,14 +21853,16 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
19693
21853
|
purlLocationMap[apkg.purl],
|
|
19694
21854
|
).sort();
|
|
19695
21855
|
// Add the occurrences evidence
|
|
19696
|
-
apkg.evidence
|
|
19697
|
-
|
|
19698
|
-
|
|
21856
|
+
apkg.evidence = apkg.evidence || {};
|
|
21857
|
+
apkg.evidence.occurrences = locationOccurrences.map((l) =>
|
|
21858
|
+
parseOccurrenceEvidenceLocation(l),
|
|
21859
|
+
);
|
|
19699
21860
|
// Set the package scope
|
|
19700
21861
|
apkg.scope = "required";
|
|
19701
21862
|
}
|
|
19702
21863
|
// Add the imported modules to properties
|
|
19703
21864
|
if (purlModulesMap[apkg.purl]) {
|
|
21865
|
+
apkg.properties = apkg.properties || [];
|
|
19704
21866
|
apkg.properties.push({
|
|
19705
21867
|
name: "ImportedModules",
|
|
19706
21868
|
value: Array.from(purlModulesMap[apkg.purl]).sort().join(", "),
|
|
@@ -19708,6 +21870,7 @@ export function addEvidenceForDotnet(pkgList, slicesFile) {
|
|
|
19708
21870
|
}
|
|
19709
21871
|
// Add the called methods to properties
|
|
19710
21872
|
if (purlMethodsMap[apkg.purl]) {
|
|
21873
|
+
apkg.properties = apkg.properties || [];
|
|
19711
21874
|
apkg.properties.push({
|
|
19712
21875
|
name: "CalledMethods",
|
|
19713
21876
|
value: Array.from(purlMethodsMap[apkg.purl]).sort().join(", "),
|
|
@@ -19946,6 +22109,14 @@ export function extractPathEnv(envValues) {
|
|
|
19946
22109
|
expandedBinPaths.push(apath);
|
|
19947
22110
|
}
|
|
19948
22111
|
}
|
|
22112
|
+
recordObservedActivity("path-resolution", "PATH", {
|
|
22113
|
+
metadata: {
|
|
22114
|
+
capability: "path-lookup",
|
|
22115
|
+
pathCount: expandedBinPaths.length,
|
|
22116
|
+
},
|
|
22117
|
+
reasonBuilder: (count) =>
|
|
22118
|
+
`Expanded PATH into ${expandedBinPaths.length} executable search path(s)${buildReadCountSuffix(count)}.`,
|
|
22119
|
+
});
|
|
19949
22120
|
return expandedBinPaths;
|
|
19950
22121
|
}
|
|
19951
22122
|
|
|
@@ -19954,13 +22125,17 @@ export function extractPathEnv(envValues) {
|
|
|
19954
22125
|
*
|
|
19955
22126
|
* @param basePath Base directory
|
|
19956
22127
|
* @param binPaths {Array[String]} Paths containing potential binaries
|
|
22128
|
+
* @param excludePaths {Array[String]} Container-relative paths that should be excluded from the result set
|
|
19957
22129
|
* @return {Array[String]} List of executables
|
|
19958
22130
|
*/
|
|
19959
|
-
export function collectExecutables(basePath, binPaths) {
|
|
22131
|
+
export function collectExecutables(basePath, binPaths, excludePaths = []) {
|
|
19960
22132
|
if (!binPaths) {
|
|
19961
22133
|
return [];
|
|
19962
22134
|
}
|
|
19963
|
-
|
|
22135
|
+
const executablesByResolvedPath = new Map();
|
|
22136
|
+
const excludedPathSet = new Set(
|
|
22137
|
+
(excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
|
|
22138
|
+
);
|
|
19964
22139
|
const ignoreList = [
|
|
19965
22140
|
"**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
|
|
19966
22141
|
"[",
|
|
@@ -19976,12 +22151,64 @@ export function collectExecutables(basePath, binPaths) {
|
|
|
19976
22151
|
follow: true,
|
|
19977
22152
|
ignore: ignoreList,
|
|
19978
22153
|
});
|
|
19979
|
-
|
|
22154
|
+
for (const file of files) {
|
|
22155
|
+
let resolvedFile = file;
|
|
22156
|
+
try {
|
|
22157
|
+
resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
|
|
22158
|
+
if (resolvedFile !== file) {
|
|
22159
|
+
recordSymlinkResolution(join(basePath, file), resolvedFile, {
|
|
22160
|
+
basePath,
|
|
22161
|
+
metadata: {
|
|
22162
|
+
resolutionKind: "executable",
|
|
22163
|
+
},
|
|
22164
|
+
reason: `Resolved executable candidate ${file} to ${resolvedFile}.`,
|
|
22165
|
+
});
|
|
22166
|
+
}
|
|
22167
|
+
} catch (err) {
|
|
22168
|
+
recordSymlinkResolution(join(basePath, file), undefined, {
|
|
22169
|
+
basePath,
|
|
22170
|
+
errorCode: err?.code || err?.name,
|
|
22171
|
+
metadata: {
|
|
22172
|
+
resolutionKind: "executable",
|
|
22173
|
+
},
|
|
22174
|
+
reason: `Failed to resolve executable candidate ${file}.`,
|
|
22175
|
+
status: "failed",
|
|
22176
|
+
});
|
|
22177
|
+
// Broken symlinks or permission errors can prevent realpath resolution.
|
|
22178
|
+
if (DEBUG_MODE) {
|
|
22179
|
+
console.log(`Unable to resolve executable path alias for ${file}`);
|
|
22180
|
+
}
|
|
22181
|
+
}
|
|
22182
|
+
if (
|
|
22183
|
+
excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
|
|
22184
|
+
excludedPathSet.has(
|
|
22185
|
+
resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
|
|
22186
|
+
)
|
|
22187
|
+
) {
|
|
22188
|
+
continue;
|
|
22189
|
+
}
|
|
22190
|
+
const existingFile = executablesByResolvedPath.get(resolvedFile);
|
|
22191
|
+
if (shouldPreferUsrMergedExecutablePath(file, existingFile)) {
|
|
22192
|
+
executablesByResolvedPath.set(resolvedFile, file);
|
|
22193
|
+
}
|
|
22194
|
+
}
|
|
19980
22195
|
} catch (_err) {
|
|
19981
22196
|
// ignore
|
|
19982
22197
|
}
|
|
19983
22198
|
}
|
|
19984
|
-
return Array.from(
|
|
22199
|
+
return Array.from(executablesByResolvedPath.values()).sort();
|
|
22200
|
+
}
|
|
22201
|
+
|
|
22202
|
+
function shouldPreferUsrMergedExecutablePath(file, existingFile) {
|
|
22203
|
+
if (!existingFile) {
|
|
22204
|
+
return true;
|
|
22205
|
+
}
|
|
22206
|
+
const fileUsesUsrPrefix = file.startsWith("usr/");
|
|
22207
|
+
const existingFileUsesUsrPrefix = existingFile.startsWith("usr/");
|
|
22208
|
+
if (fileUsesUsrPrefix !== existingFileUsesUsrPrefix) {
|
|
22209
|
+
return fileUsesUsrPrefix;
|
|
22210
|
+
}
|
|
22211
|
+
return file < existingFile;
|
|
19985
22212
|
}
|
|
19986
22213
|
|
|
19987
22214
|
/**
|
|
@@ -19991,6 +22218,7 @@ export function collectExecutables(basePath, binPaths) {
|
|
|
19991
22218
|
* @param libPaths {Array[String]} Paths containing potential libraries
|
|
19992
22219
|
* @param ldConf {String} Config file used by ldconfig to locate additional paths
|
|
19993
22220
|
* @param ldConfDirPattern {String} Config directory that can contain more .conf files for ldconfig
|
|
22221
|
+
* @param excludePaths {Array[String]} Container-relative paths that should be excluded from the result set
|
|
19994
22222
|
*
|
|
19995
22223
|
* @return {Array[String]} List of executables
|
|
19996
22224
|
*/
|
|
@@ -19999,11 +22227,15 @@ export function collectSharedLibs(
|
|
|
19999
22227
|
libPaths,
|
|
20000
22228
|
ldConf,
|
|
20001
22229
|
ldConfDirPattern,
|
|
22230
|
+
excludePaths = [],
|
|
20002
22231
|
) {
|
|
20003
22232
|
if (!libPaths) {
|
|
20004
22233
|
return [];
|
|
20005
22234
|
}
|
|
20006
|
-
|
|
22235
|
+
const sharedLibs = [];
|
|
22236
|
+
const excludedPathSet = new Set(
|
|
22237
|
+
(excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
|
|
22238
|
+
);
|
|
20007
22239
|
const ignoreList = [
|
|
20008
22240
|
"**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
|
|
20009
22241
|
];
|
|
@@ -20035,7 +22267,39 @@ export function collectSharedLibs(
|
|
|
20035
22267
|
follow: true,
|
|
20036
22268
|
ignore: ignoreList,
|
|
20037
22269
|
});
|
|
20038
|
-
|
|
22270
|
+
for (const file of files) {
|
|
22271
|
+
let resolvedFile = file;
|
|
22272
|
+
try {
|
|
22273
|
+
resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
|
|
22274
|
+
recordSymlinkResolution(join(basePath, file), resolvedFile, {
|
|
22275
|
+
basePath,
|
|
22276
|
+
metadata: {
|
|
22277
|
+
resolutionKind: "shared-library",
|
|
22278
|
+
},
|
|
22279
|
+
reason: `Resolved shared library candidate ${file} to ${resolvedFile}.`,
|
|
22280
|
+
});
|
|
22281
|
+
} catch (err) {
|
|
22282
|
+
recordSymlinkResolution(join(basePath, file), undefined, {
|
|
22283
|
+
basePath,
|
|
22284
|
+
errorCode: err?.code || err?.name,
|
|
22285
|
+
metadata: {
|
|
22286
|
+
resolutionKind: "shared-library",
|
|
22287
|
+
},
|
|
22288
|
+
reason: `Failed to resolve shared library candidate ${file}.`,
|
|
22289
|
+
status: "failed",
|
|
22290
|
+
});
|
|
22291
|
+
// Broken symlinks or permission errors can prevent realpath resolution.
|
|
22292
|
+
}
|
|
22293
|
+
if (
|
|
22294
|
+
excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
|
|
22295
|
+
excludedPathSet.has(
|
|
22296
|
+
resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
|
|
22297
|
+
)
|
|
22298
|
+
) {
|
|
22299
|
+
continue;
|
|
22300
|
+
}
|
|
22301
|
+
sharedLibs.push(file);
|
|
22302
|
+
}
|
|
20039
22303
|
} catch (_err) {
|
|
20040
22304
|
// ignore
|
|
20041
22305
|
}
|
|
@@ -20169,11 +22433,7 @@ export function hasDangerousUnicode(str) {
|
|
|
20169
22433
|
|
|
20170
22434
|
// Check for control characters (except common ones like \n, \r, \t)
|
|
20171
22435
|
const controlChars = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/;
|
|
20172
|
-
|
|
20173
|
-
return true;
|
|
20174
|
-
}
|
|
20175
|
-
|
|
20176
|
-
return false;
|
|
22436
|
+
return controlChars.test(str);
|
|
20177
22437
|
}
|
|
20178
22438
|
// biome-ignore-end lint/suspicious/noControlCharactersInRegex: validation
|
|
20179
22439
|
|
|
@@ -20208,11 +22468,7 @@ export function isValidDriveRoot(root) {
|
|
|
20208
22468
|
}
|
|
20209
22469
|
|
|
20210
22470
|
// Backslash (optional) must be exactly ASCII backslash (0x5C)
|
|
20211
|
-
|
|
20212
|
-
return false;
|
|
20213
|
-
}
|
|
20214
|
-
|
|
20215
|
-
return true;
|
|
22471
|
+
return !(backslash && backslash.charCodeAt(0) !== 0x5c);
|
|
20216
22472
|
}
|
|
20217
22473
|
|
|
20218
22474
|
/**
|