@cdxgen/cdxgen 13.0.1 → 13.2.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 +30 -16
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +141 -7
- package/bin/evinse.js +3 -1
- package/data/component-tags.json +8 -1
- package/data/crypto-oid.json +88 -12
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1167 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +32 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +268 -49
- package/lib/cli/jsBom.js +91 -5
- package/lib/cli/jvmBom.js +1515 -1314
- package/lib/cli/managedBom.js +249 -28
- package/lib/cli/nativeBom.js +986 -10
- package/lib/core/activity.js +120 -2
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +108 -2
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +250 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +498 -17
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +188 -2
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +172 -36
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +944 -19
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +141 -13
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +17 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +508 -22
- package/lib/inventory/envcontext.js +215 -24
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +131 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +594 -0
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +250 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +284 -3
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +26 -26
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +10 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +43 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +53 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +21 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +34 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +12 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +43 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +43 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +35 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader over the BOM's formulation section, the third evidence source.
|
|
3
|
+
*
|
|
4
|
+
* Formulation is two things at once, and the distinction governs every
|
|
5
|
+
* confidence decision downstream. The run-derived half — `type: "platform"`
|
|
6
|
+
* components recording the toolchain the generating run probed, and the git
|
|
7
|
+
* provenance components — is what cdxgen itself did. The config-parsed half —
|
|
8
|
+
* the commands, environment variables and task types under
|
|
9
|
+
* `formulation[].workflows[]` — is what the repo's CI declares, and a
|
|
10
|
+
* declared command was never observed to run.
|
|
11
|
+
*
|
|
12
|
+
* This module is pure: the BOM and the caller's run facts are the only
|
|
13
|
+
* inputs, nothing is read from the filesystem, and environment variable
|
|
14
|
+
* *values* are never touched — names only, because the BOM stores them and
|
|
15
|
+
* copying them into a report would republish them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Property carrying the id of the run that stamped a BOM's formulation, set
|
|
20
|
+
* on every formulation workflow by the introspection pass that reflected
|
|
21
|
+
* over the freshly generated document.
|
|
22
|
+
*
|
|
23
|
+
* @type {string}
|
|
24
|
+
*/
|
|
25
|
+
export const FORMULATION_RUN_ID_PROPERTY = "cdx:introspection:runId";
|
|
26
|
+
|
|
27
|
+
/** The formulation record was produced by the run being reflected. */
|
|
28
|
+
export const FORMULATION_ORIGIN_SAME_RUN = "same-run";
|
|
29
|
+
|
|
30
|
+
/** The formulation record came with a BOM this run did not generate. */
|
|
31
|
+
export const FORMULATION_ORIGIN_FOREIGN = "foreign";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The BOM carries no formulation record at all, which is neither of the
|
|
35
|
+
* above: there is no evidence to weigh, so every consumer must behave as it
|
|
36
|
+
* did before formulation became a source.
|
|
37
|
+
*/
|
|
38
|
+
export const FORMULATION_ORIGIN_ABSENT = "absent";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Platform component names, as the environment probes emit them, mapped to
|
|
42
|
+
* the canonical ecosystem whose row their facts can corroborate. Names not
|
|
43
|
+
* listed here have no fidelity row to join.
|
|
44
|
+
*
|
|
45
|
+
* @type {Readonly<Record<string, string>>}
|
|
46
|
+
*/
|
|
47
|
+
export const TOOL_NAME_ECOSYSTEMS = Object.freeze({
|
|
48
|
+
java: "java",
|
|
49
|
+
dotnet: "csharp",
|
|
50
|
+
python: "python",
|
|
51
|
+
"Node.js": "npm",
|
|
52
|
+
gcc: "c",
|
|
53
|
+
rustc: "rust",
|
|
54
|
+
go: "go",
|
|
55
|
+
ruby: "ruby",
|
|
56
|
+
swift: "swift",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolver executables whose presence in a declared command says a build or
|
|
61
|
+
* dependency resolution was attempted, mapped to the ecosystem they drive.
|
|
62
|
+
* Keys are executable basenames, lower-cased.
|
|
63
|
+
*
|
|
64
|
+
* @type {Readonly<Record<string, string>>}
|
|
65
|
+
*/
|
|
66
|
+
export const RESOLVER_EXECUTABLES = Object.freeze({
|
|
67
|
+
mvn: "java",
|
|
68
|
+
mvnw: "java",
|
|
69
|
+
gradle: "java",
|
|
70
|
+
gradlew: "java",
|
|
71
|
+
java: "java",
|
|
72
|
+
ant: "java",
|
|
73
|
+
sbt: "java",
|
|
74
|
+
npm: "npm",
|
|
75
|
+
pnpm: "npm",
|
|
76
|
+
yarn: "npm",
|
|
77
|
+
bun: "npm",
|
|
78
|
+
deno: "npm",
|
|
79
|
+
npx: "npm",
|
|
80
|
+
node: "npm",
|
|
81
|
+
pip: "python",
|
|
82
|
+
pip3: "python",
|
|
83
|
+
poetry: "python",
|
|
84
|
+
uv: "python",
|
|
85
|
+
pdm: "python",
|
|
86
|
+
pipenv: "python",
|
|
87
|
+
conda: "python",
|
|
88
|
+
python: "python",
|
|
89
|
+
python3: "python",
|
|
90
|
+
cargo: "rust",
|
|
91
|
+
go: "go",
|
|
92
|
+
bundle: "ruby",
|
|
93
|
+
gem: "ruby",
|
|
94
|
+
rake: "ruby",
|
|
95
|
+
ruby: "ruby",
|
|
96
|
+
dotnet: "csharp",
|
|
97
|
+
nuget: "csharp",
|
|
98
|
+
composer: "php",
|
|
99
|
+
dart: "dart",
|
|
100
|
+
flutter: "dart",
|
|
101
|
+
helm: "helm",
|
|
102
|
+
lein: "clojure",
|
|
103
|
+
clj: "clojure",
|
|
104
|
+
clojure: "clojure",
|
|
105
|
+
swift: "swift",
|
|
106
|
+
pod: "cocoa",
|
|
107
|
+
mix: "elixir",
|
|
108
|
+
cmake: "c",
|
|
109
|
+
make: "c",
|
|
110
|
+
meson: "c",
|
|
111
|
+
ninja: "c",
|
|
112
|
+
cabal: "haskell",
|
|
113
|
+
stack: "haskell",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The runtime names the reflection reports, mapped to the family a
|
|
118
|
+
* formulation platform component records for the same runtime.
|
|
119
|
+
*
|
|
120
|
+
* @type {Readonly<Record<string, string>>}
|
|
121
|
+
*/
|
|
122
|
+
const RUNTIME_FAMILIES = Object.freeze({
|
|
123
|
+
"Node.js": "Node.js",
|
|
124
|
+
node: "Node.js",
|
|
125
|
+
Deno: "Deno",
|
|
126
|
+
deno: "Deno",
|
|
127
|
+
Bun: "Bun",
|
|
128
|
+
bun: "Bun",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The final path segment of a POSIX- or Windows-flavoured command token.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} token Command token.
|
|
135
|
+
* @returns {string} The token after its last path separator.
|
|
136
|
+
*/
|
|
137
|
+
function basenameOfToken(token) {
|
|
138
|
+
const slash = token.lastIndexOf("/");
|
|
139
|
+
const backslash = token.lastIndexOf("\\");
|
|
140
|
+
return token.slice(Math.max(slash, backslash) + 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Whether an `executed` value is a CI action reference such as
|
|
145
|
+
* `actions/checkout@<sha>` rather than a shell command. An action reference
|
|
146
|
+
* carries no whitespace, contains a path separator, and names a revision of
|
|
147
|
+
* the repository after the last separator; a relative-path executable such
|
|
148
|
+
* as `./mvnw` carries no revision and stays a command.
|
|
149
|
+
*
|
|
150
|
+
* @param {string|undefined} executed The executed value of a step command.
|
|
151
|
+
* @returns {boolean} True when the value names an action, not a command.
|
|
152
|
+
*/
|
|
153
|
+
export function isActionReference(executed) {
|
|
154
|
+
if (typeof executed !== "string" || !executed.trim()) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const text = executed.trim();
|
|
158
|
+
if (/\s/.test(text) || !text.includes("/")) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const lastSegment = text.slice(text.lastIndexOf("/") + 1);
|
|
162
|
+
return lastSegment.includes("@");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The executable a command line starts with, as a bare name.
|
|
167
|
+
*
|
|
168
|
+
* @param {string|undefined} commandLine Command line.
|
|
169
|
+
* @returns {string} Basename of the first whitespace token, or "" when absent.
|
|
170
|
+
*/
|
|
171
|
+
export function executableOf(commandLine) {
|
|
172
|
+
const first = `${commandLine || ""}`.trim().split(/\s+/)[0] || "";
|
|
173
|
+
return basenameOfToken(first);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The revision a git provenance component records, from its OmniBOR or
|
|
178
|
+
* Software Heritage identifier. Identifiers prefix the raw hash with a
|
|
179
|
+
* scheme and type, so the value after the last colon is the revision itself.
|
|
180
|
+
*
|
|
181
|
+
* @param {Object} component Formulation component of type `file`.
|
|
182
|
+
* @returns {string|undefined} The raw revision, when the component names one.
|
|
183
|
+
*/
|
|
184
|
+
function revisionOf(component) {
|
|
185
|
+
const identifiers = [
|
|
186
|
+
...(Array.isArray(component?.omniborId) ? component.omniborId : []),
|
|
187
|
+
...(Array.isArray(component?.swhid) ? component.swhid : []),
|
|
188
|
+
];
|
|
189
|
+
for (const identifier of identifiers) {
|
|
190
|
+
if (typeof identifier === "string" && identifier.includes(":")) {
|
|
191
|
+
return identifier.slice(identifier.lastIndexOf(":") + 1) || undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Whether a formulation component is one of the git graph nodes the
|
|
199
|
+
* provenance builder adds beside the per-file blobs.
|
|
200
|
+
*
|
|
201
|
+
* @param {Object} component Formulation component.
|
|
202
|
+
* @returns {boolean} True for `git-parent` and `git-tree`.
|
|
203
|
+
*/
|
|
204
|
+
function isGitGraphNode(component) {
|
|
205
|
+
return component?.name === "git-parent" || component?.name === "git-tree";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The run id a formulation workflow carries, when the introspection pass
|
|
210
|
+
* that reflected over it stamped one.
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} workflow Formulation workflow.
|
|
213
|
+
* @returns {string|undefined} The stamped run id, when present.
|
|
214
|
+
*/
|
|
215
|
+
function stampedRunId(workflow) {
|
|
216
|
+
for (const property of Array.isArray(workflow?.properties)
|
|
217
|
+
? workflow.properties
|
|
218
|
+
: []) {
|
|
219
|
+
if (property?.name === FORMULATION_RUN_ID_PROPERTY) {
|
|
220
|
+
return property?.value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Whether a formulation workflow carries this run's id marker.
|
|
228
|
+
*
|
|
229
|
+
* @param {Object} bomJson CycloneDX BOM.
|
|
230
|
+
* @param {string} runId The reflection's own run id.
|
|
231
|
+
* @returns {boolean} True when the formulation names this run.
|
|
232
|
+
*/
|
|
233
|
+
function carriesRunIdMarker(bomJson, runId) {
|
|
234
|
+
if (!runId) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
for (const entry of Array.isArray(bomJson?.formulation)
|
|
238
|
+
? bomJson.formulation
|
|
239
|
+
: []) {
|
|
240
|
+
for (const workflow of Array.isArray(entry?.workflows)
|
|
241
|
+
? entry.workflows
|
|
242
|
+
: []) {
|
|
243
|
+
if (stampedRunId(workflow) === runId) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Read the provenance-aware formulation evidence out of a BOM.
|
|
253
|
+
*
|
|
254
|
+
* `origin` decides how the evidence may be used. `same-run` means the
|
|
255
|
+
* formulation describes this very run — its platform components are the same
|
|
256
|
+
* probes the ledger recorded as `tool.resolved`, so the evidence corroborates
|
|
257
|
+
* and must never add confidence. `foreign` means the BOM arrived without a
|
|
258
|
+
* ledger for its generation, and the formulation is the only surviving trace
|
|
259
|
+
* of what that build attempted. `absent` is neither: the BOM carries no
|
|
260
|
+
* formulation record, so there is nothing to weigh and every consumer keeps
|
|
261
|
+
* the behaviour it had before formulation became a source.
|
|
262
|
+
*
|
|
263
|
+
* Environment variable names are collected; values are never read.
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} bomJson CycloneDX BOM.
|
|
266
|
+
* @param {Object} [context] Run facts the caller already knows.
|
|
267
|
+
* @param {string} [context.runId] This reflection's run id, matched against the formulation's marker.
|
|
268
|
+
* @param {number} [context.ledgerEventCount] Events the ledger holds for this BOM's generation.
|
|
269
|
+
* @returns {{
|
|
270
|
+
* origin: "same-run"|"foreign"|"absent",
|
|
271
|
+
* tools: {name: string|undefined, version: string|undefined, source: "probe"}[],
|
|
272
|
+
* declaredCommands: {executable: string, commandLine: string, workflow: string|undefined, task: string|undefined, step: string|undefined, source: "ci-config"}[],
|
|
273
|
+
* sourceTree: {commit: string|undefined, treeId: string|undefined, fileCount: number}|undefined,
|
|
274
|
+
* environmentKeys: string[]
|
|
275
|
+
* }} The formulation evidence.
|
|
276
|
+
*/
|
|
277
|
+
export function readFormulationEvidence(bomJson, context = {}) {
|
|
278
|
+
const entries = Array.isArray(bomJson?.formulation)
|
|
279
|
+
? bomJson.formulation
|
|
280
|
+
: [];
|
|
281
|
+
const sameRun =
|
|
282
|
+
Number(context.ledgerEventCount) > 0 ||
|
|
283
|
+
carriesRunIdMarker(bomJson, context.runId);
|
|
284
|
+
const tools = [];
|
|
285
|
+
let commit;
|
|
286
|
+
let treeId;
|
|
287
|
+
let fileCount = 0;
|
|
288
|
+
const environmentKeys = [];
|
|
289
|
+
const declaredCommands = [];
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
for (const component of Array.isArray(entry?.components)
|
|
292
|
+
? entry.components
|
|
293
|
+
: []) {
|
|
294
|
+
if (component?.type === "platform") {
|
|
295
|
+
tools.push({
|
|
296
|
+
name: component.name,
|
|
297
|
+
version: component.version,
|
|
298
|
+
source: "probe",
|
|
299
|
+
});
|
|
300
|
+
} else if (component?.type === "file") {
|
|
301
|
+
if (isGitGraphNode(component)) {
|
|
302
|
+
if (component.name === "git-parent") {
|
|
303
|
+
commit = revisionOf(component) || commit;
|
|
304
|
+
} else {
|
|
305
|
+
treeId = revisionOf(component) || treeId;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
fileCount += 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const workflow of Array.isArray(entry?.workflows)
|
|
313
|
+
? entry.workflows
|
|
314
|
+
: []) {
|
|
315
|
+
for (const input of Array.isArray(workflow?.inputs)
|
|
316
|
+
? workflow.inputs
|
|
317
|
+
: []) {
|
|
318
|
+
for (const variable of Array.isArray(input?.environmentVars)
|
|
319
|
+
? input.environmentVars
|
|
320
|
+
: []) {
|
|
321
|
+
if (variable?.name && !environmentKeys.includes(variable.name)) {
|
|
322
|
+
environmentKeys.push(variable.name);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const task of Array.isArray(workflow?.tasks) ? workflow.tasks : []) {
|
|
327
|
+
for (const step of Array.isArray(task?.steps) ? task.steps : []) {
|
|
328
|
+
for (const command of Array.isArray(step?.commands)
|
|
329
|
+
? step.commands
|
|
330
|
+
: []) {
|
|
331
|
+
const executed = command?.executed;
|
|
332
|
+
if (
|
|
333
|
+
typeof executed !== "string" ||
|
|
334
|
+
!executed.trim() ||
|
|
335
|
+
isActionReference(executed)
|
|
336
|
+
) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
declaredCommands.push({
|
|
340
|
+
executable: executableOf(executed),
|
|
341
|
+
commandLine: executed.trim(),
|
|
342
|
+
workflow: workflow?.name || workflow?.uid,
|
|
343
|
+
task: task?.name || task?.uid,
|
|
344
|
+
step: step?.name,
|
|
345
|
+
source: "ci-config",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
origin: !entries.length
|
|
354
|
+
? FORMULATION_ORIGIN_ABSENT
|
|
355
|
+
: sameRun
|
|
356
|
+
? FORMULATION_ORIGIN_SAME_RUN
|
|
357
|
+
: FORMULATION_ORIGIN_FOREIGN,
|
|
358
|
+
tools,
|
|
359
|
+
declaredCommands,
|
|
360
|
+
sourceTree:
|
|
361
|
+
commit || treeId || fileCount > 0
|
|
362
|
+
? { commit, treeId, fileCount }
|
|
363
|
+
: undefined,
|
|
364
|
+
environmentKeys,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The formulation platform entry for the runtime family this reflection runs
|
|
370
|
+
* on, when the record cannot describe the machine in use: either the
|
|
371
|
+
* recorded version of the family differs from the runtime actually running,
|
|
372
|
+
* or the record names other toolchains but no entry of this family at all. A
|
|
373
|
+
* difference proves the formulation's toolchain record describes a machine
|
|
374
|
+
* other than the one re-scanning the BOM; it says nothing about the other
|
|
375
|
+
* toolchains the record names, which no probe of this run observed.
|
|
376
|
+
*
|
|
377
|
+
* @param {Object} evidence Formulation evidence from readFormulationEvidence.
|
|
378
|
+
* @param {Object} runtime The reflection's runtime facts.
|
|
379
|
+
* @param {string} runtime.name Runtime name, such as "Node.js".
|
|
380
|
+
* @param {string} runtime.version Runtime version.
|
|
381
|
+
* @returns {{tool: string, recordedVersion: string|undefined, runtimeVersion: string}|undefined} The mismatching formulation entry, when one exists.
|
|
382
|
+
*/
|
|
383
|
+
export function findRuntimeToolMismatch(evidence, runtime) {
|
|
384
|
+
const family = RUNTIME_FAMILIES[`${runtime?.name}`];
|
|
385
|
+
const tools = Array.isArray(evidence?.tools) ? evidence.tools : [];
|
|
386
|
+
if (!family || !runtime?.version || !tools.length) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
for (const tool of tools) {
|
|
390
|
+
if (tool?.name === family) {
|
|
391
|
+
if (tool.version !== runtime.version) {
|
|
392
|
+
return {
|
|
393
|
+
tool: family,
|
|
394
|
+
recordedVersion: tool.version,
|
|
395
|
+
runtimeVersion: runtime.version,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
tool: family,
|
|
403
|
+
recordedVersion: undefined,
|
|
404
|
+
runtimeVersion: runtime.version,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The declared CI command a formulation records for one ecosystem, if any.
|
|
410
|
+
* Commands carry no ecosystem tag, so the executable answers: the first
|
|
411
|
+
* declared command whose executable drives that ecosystem's resolver wins,
|
|
412
|
+
* in the order the formulation records them.
|
|
413
|
+
*
|
|
414
|
+
* @param {Object} evidence Formulation evidence from readFormulationEvidence.
|
|
415
|
+
* @param {string} ecosystem Canonical ecosystem name.
|
|
416
|
+
* @returns {Object|undefined} The matching declared command entry.
|
|
417
|
+
*/
|
|
418
|
+
export function declaredCommandForEcosystem(evidence, ecosystem) {
|
|
419
|
+
for (const command of Array.isArray(evidence?.declaredCommands)
|
|
420
|
+
? evidence.declaredCommands
|
|
421
|
+
: []) {
|
|
422
|
+
if (RESOLVER_EXECUTABLES[command.executable] === ecosystem) {
|
|
423
|
+
return command;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ecosystem marker detection for the build-introspection reflection step.
|
|
3
|
+
*
|
|
4
|
+
* A marker is a manifest or lockfile whose presence on disk proves an
|
|
5
|
+
* ecosystem was part of the scanned project. Markers drive two verdicts:
|
|
6
|
+
* an ecosystem with markers but zero components in the BOM is `absent`, and
|
|
7
|
+
* an ecosystem that has markers but no cdxgen project type at all is a
|
|
8
|
+
* coverage gap (`unsupported`), which is cdxgen's backlog rather than the
|
|
9
|
+
* user's problem.
|
|
10
|
+
*
|
|
11
|
+
* Detection is deliberately bounded: the scanned directory itself plus one
|
|
12
|
+
* level of immediate subdirectories. postProcess runs after generation, so a
|
|
13
|
+
* second full tree walk would be a measurable cost on large repos; markers
|
|
14
|
+
* deeper than one directory level are simply not seen, which is acceptable
|
|
15
|
+
* because tier assignment never relies on markers alone when the BOM already
|
|
16
|
+
* carries the ecosystem's components.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readdirSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { safeExistsSync } from "../../../core/fs.js";
|
|
23
|
+
|
|
24
|
+
/** Largest number of immediate subdirectories inspected at depth one. */
|
|
25
|
+
const MAX_SUBDIRS_SCANNED = 100;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Marker table: ecosystem → manifest and lockfile names that indicate the
|
|
29
|
+
* ecosystem is part of the project. Entries are either exact file names or
|
|
30
|
+
* `{ suffix }` matchers for extension-driven ecosystems. Ecosystems cdxgen
|
|
31
|
+
* cannot parse (currently only dub, the D package manager) are included so
|
|
32
|
+
* the reflection can report them as coverage gaps instead of silently
|
|
33
|
+
* ignoring them.
|
|
34
|
+
*
|
|
35
|
+
* @type {Readonly<Record<string, {names: string[], suffixes: string[]}>>}
|
|
36
|
+
*/
|
|
37
|
+
export const ECOSYSTEM_MARKERS = Object.freeze({
|
|
38
|
+
java: {
|
|
39
|
+
names: [
|
|
40
|
+
"pom.xml",
|
|
41
|
+
"build.gradle",
|
|
42
|
+
"build.gradle.kts",
|
|
43
|
+
"settings.gradle",
|
|
44
|
+
"settings.gradle.kts",
|
|
45
|
+
"gradlew",
|
|
46
|
+
"build.sbt",
|
|
47
|
+
"mill-version",
|
|
48
|
+
".sdkmanrc",
|
|
49
|
+
],
|
|
50
|
+
suffixes: [],
|
|
51
|
+
},
|
|
52
|
+
npm: {
|
|
53
|
+
names: [
|
|
54
|
+
"package.json",
|
|
55
|
+
"package-lock.json",
|
|
56
|
+
"pnpm-lock.yaml",
|
|
57
|
+
"pnpm-workspace.yaml",
|
|
58
|
+
"yarn.lock",
|
|
59
|
+
"bun.lockb",
|
|
60
|
+
"bun.lock",
|
|
61
|
+
"deno.lock",
|
|
62
|
+
],
|
|
63
|
+
suffixes: [],
|
|
64
|
+
},
|
|
65
|
+
python: {
|
|
66
|
+
names: [
|
|
67
|
+
"requirements.txt",
|
|
68
|
+
"Pipfile",
|
|
69
|
+
"Pipfile.lock",
|
|
70
|
+
"pyproject.toml",
|
|
71
|
+
"poetry.lock",
|
|
72
|
+
"pdm.lock",
|
|
73
|
+
"uv.lock",
|
|
74
|
+
"setup.py",
|
|
75
|
+
"setup.cfg",
|
|
76
|
+
".python-version",
|
|
77
|
+
"environment.yml",
|
|
78
|
+
],
|
|
79
|
+
suffixes: [],
|
|
80
|
+
},
|
|
81
|
+
go: { names: ["go.mod", "go.sum", "go.work"], suffixes: [] },
|
|
82
|
+
rust: {
|
|
83
|
+
names: [
|
|
84
|
+
"Cargo.toml",
|
|
85
|
+
"Cargo.lock",
|
|
86
|
+
"rust-toolchain.toml",
|
|
87
|
+
"rust-toolchain",
|
|
88
|
+
],
|
|
89
|
+
suffixes: [],
|
|
90
|
+
},
|
|
91
|
+
ruby: { names: ["Gemfile", "Gemfile.lock", ".ruby-version"], suffixes: [] },
|
|
92
|
+
csharp: {
|
|
93
|
+
names: ["global.json", "nuget.config"],
|
|
94
|
+
suffixes: [".csproj", ".vbproj", ".fsproj", ".sln"],
|
|
95
|
+
},
|
|
96
|
+
php: { names: ["composer.json", "composer.lock"], suffixes: [] },
|
|
97
|
+
dart: { names: ["pubspec.yaml", "pubspec.lock"], suffixes: [] },
|
|
98
|
+
helm: { names: ["Chart.yaml", "Chart.yml"], suffixes: [] },
|
|
99
|
+
clojure: {
|
|
100
|
+
names: ["project.clj", "deps.edn", "bb.edn", "shadow-cljs.edn"],
|
|
101
|
+
suffixes: [],
|
|
102
|
+
},
|
|
103
|
+
cocoa: { names: ["Podfile", "Podfile.lock"], suffixes: [] },
|
|
104
|
+
swift: { names: ["Package.swift", "Package.resolved"], suffixes: [] },
|
|
105
|
+
elixir: { names: ["mix.exs", "mix.lock"], suffixes: [] },
|
|
106
|
+
c: {
|
|
107
|
+
names: [
|
|
108
|
+
"CMakeLists.txt",
|
|
109
|
+
"conanfile.txt",
|
|
110
|
+
"conanfile.py",
|
|
111
|
+
"xmake-requires.lock",
|
|
112
|
+
],
|
|
113
|
+
suffixes: [],
|
|
114
|
+
},
|
|
115
|
+
haskell: {
|
|
116
|
+
names: ["cabal.project", "stack.yaml", "stack.yaml.lock", "stack.lock"],
|
|
117
|
+
suffixes: [".cabal"],
|
|
118
|
+
},
|
|
119
|
+
elm: { names: ["elm.json", "elm-package.json"], suffixes: [] },
|
|
120
|
+
dub: { names: ["dub.json", "dub.sdl"], suffixes: [] },
|
|
121
|
+
crystal: { names: ["shard.yml", "shard.lock"], suffixes: [] },
|
|
122
|
+
nim: { names: ["nim.cfg", "config.nims"], suffixes: [".nimble"] },
|
|
123
|
+
perl: {
|
|
124
|
+
names: [
|
|
125
|
+
"cpanfile",
|
|
126
|
+
"cpanfile.snapshot",
|
|
127
|
+
"META.json",
|
|
128
|
+
"META.yml",
|
|
129
|
+
"Makefile.PL",
|
|
130
|
+
"Build.PL",
|
|
131
|
+
],
|
|
132
|
+
suffixes: [],
|
|
133
|
+
},
|
|
134
|
+
r: { names: ["DESCRIPTION", "renv.lock"], suffixes: [] },
|
|
135
|
+
julia: { names: ["Project.toml", "Manifest.toml"], suffixes: [] },
|
|
136
|
+
erlang: { names: ["rebar.config", "rebar.lock"], suffixes: [] },
|
|
137
|
+
ocaml: { names: ["dune-project"], suffixes: [".opam"] },
|
|
138
|
+
lua: { names: ["luarocks.lock"], suffixes: [".rockspec"] },
|
|
139
|
+
spack: { names: ["spack.yaml", "spack.lock"], suffixes: [] },
|
|
140
|
+
terraform: { names: [".terraform.lock.hcl"], suffixes: [] },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/** Marker file name → ecosystems claiming it, and marker suffix → claiming
|
|
144
|
+
* ecosystems, both built once from the table. The name index is exported so
|
|
145
|
+
* the reflection can recognize marker paths carried in ledger events without
|
|
146
|
+
* rescanning the filesystem. */
|
|
147
|
+
export const MARKERS_BY_NAME = new Map();
|
|
148
|
+
/** Marker suffix → ecosystems claiming it. */
|
|
149
|
+
const MARKERS_BY_SUFFIX = [];
|
|
150
|
+
for (const [ecosystem, markers] of Object.entries(ECOSYSTEM_MARKERS)) {
|
|
151
|
+
for (const name of markers.names) {
|
|
152
|
+
const ecosystems = MARKERS_BY_NAME.get(name) || [];
|
|
153
|
+
ecosystems.push(ecosystem);
|
|
154
|
+
MARKERS_BY_NAME.set(name, ecosystems);
|
|
155
|
+
}
|
|
156
|
+
for (const suffix of markers.suffixes) {
|
|
157
|
+
MARKERS_BY_SUFFIX.push({ ecosystem, suffix });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* List a directory's entries as plain `{ name, directory }` records, sorted by
|
|
163
|
+
* name so detection results are deterministic. Callers may inject this helper
|
|
164
|
+
* to bound or observe the scan in tests.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} dirPath Directory to list.
|
|
167
|
+
* @returns {{name: string, directory: boolean}[]} Sorted entries.
|
|
168
|
+
*/
|
|
169
|
+
function listDirectory(dirPath) {
|
|
170
|
+
return readdirSync(dirPath, { withFileTypes: true })
|
|
171
|
+
.map((entry) => ({ name: entry.name, directory: entry.isDirectory() }))
|
|
172
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Match one directory's entries against the marker index and record the full
|
|
177
|
+
* paths of every marker found.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} dirPath Directory the entries belong to.
|
|
180
|
+
* @param {{name: string, directory: boolean}[]} entries Directory entries.
|
|
181
|
+
* @param {Map<string, Set<string>>} markersByEcosystem Accumulator keyed by ecosystem.
|
|
182
|
+
* @returns {void}
|
|
183
|
+
*/
|
|
184
|
+
function scanEntries(dirPath, entries, markersByEcosystem) {
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
const nameEcosystems = MARKERS_BY_NAME.get(entry.name);
|
|
187
|
+
if (nameEcosystems) {
|
|
188
|
+
for (const ecosystem of nameEcosystems) {
|
|
189
|
+
const paths = markersByEcosystem.get(ecosystem) || new Set();
|
|
190
|
+
paths.add(join(dirPath, entry.name));
|
|
191
|
+
markersByEcosystem.set(ecosystem, paths);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const { ecosystem, suffix } of MARKERS_BY_SUFFIX) {
|
|
195
|
+
if (entry.name.length > suffix.length && entry.name.endsWith(suffix)) {
|
|
196
|
+
const paths = markersByEcosystem.get(ecosystem) || new Set();
|
|
197
|
+
paths.add(join(dirPath, entry.name));
|
|
198
|
+
markersByEcosystem.set(ecosystem, paths);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Detect ecosystem markers under a project directory without walking the full
|
|
206
|
+
* tree: the directory itself is inspected, then up to
|
|
207
|
+
* {@link MAX_SUBDIRS_SCANNED} immediate subdirectories in sorted order. Paths
|
|
208
|
+
* are built with `node:path`, so `markersOnDisk` carries platform-correct
|
|
209
|
+
* separators.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} projectPath Directory that was scanned, when known.
|
|
212
|
+
* @param {Object} [hooks] Test hooks.
|
|
213
|
+
* @param {(dirPath: string) => {name: string, directory: boolean}[]} [hooks.listDir] Directory lister override.
|
|
214
|
+
* @returns {{markersByEcosystem: Map<string, string[]>, scannedDirectories: number}} Marker paths per ecosystem and the number of directories inspected.
|
|
215
|
+
*/
|
|
216
|
+
export function detectEcosystemMarkers(projectPath, hooks = {}) {
|
|
217
|
+
const markersByEcosystem = new Map();
|
|
218
|
+
if (!projectPath || !safeExistsSync(projectPath)) {
|
|
219
|
+
return { markersByEcosystem, scannedDirectories: 0 };
|
|
220
|
+
}
|
|
221
|
+
const listDir = hooks.listDir || listDirectory;
|
|
222
|
+
let rootEntries;
|
|
223
|
+
try {
|
|
224
|
+
rootEntries = listDir(projectPath);
|
|
225
|
+
} catch {
|
|
226
|
+
return { markersByEcosystem, scannedDirectories: 0 };
|
|
227
|
+
}
|
|
228
|
+
scanEntries(projectPath, rootEntries, markersByEcosystem);
|
|
229
|
+
const subdirs = rootEntries
|
|
230
|
+
.filter((entry) => entry.directory)
|
|
231
|
+
.map((entry) => entry.name)
|
|
232
|
+
.slice(0, MAX_SUBDIRS_SCANNED);
|
|
233
|
+
let scannedDirectories = 1;
|
|
234
|
+
for (const subdir of subdirs) {
|
|
235
|
+
const subdirPath = join(projectPath, subdir);
|
|
236
|
+
try {
|
|
237
|
+
scanEntries(subdirPath, listDir(subdirPath), markersByEcosystem);
|
|
238
|
+
} catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
scannedDirectories += 1;
|
|
242
|
+
}
|
|
243
|
+
const sorted = new Map(
|
|
244
|
+
[...markersByEcosystem.entries()].map(([ecosystem, paths]) => [
|
|
245
|
+
ecosystem,
|
|
246
|
+
[...paths].sort(),
|
|
247
|
+
]),
|
|
248
|
+
);
|
|
249
|
+
return { markersByEcosystem: sorted, scannedDirectories };
|
|
250
|
+
}
|