@cdxgen/cdxgen 13.3.0 → 13.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 +9 -6
- package/bin/cdxgen.js +90 -26
- package/bin/evinse.js +18 -1
- package/bin/repl.js +86 -0
- package/bin/sign.js +45 -15
- package/bin/tracebom.js +5 -1
- package/bin/validate.js +84 -18
- package/bin/verify.js +155 -86
- package/data/helpers/init.gradle +46 -21
- package/data/remediations.json +61 -0
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +38 -6
- package/lib/cli/index.js +113 -54
- package/lib/cli/jsBom.js +50 -29
- package/lib/cli/jvmBom.js +665 -138
- package/lib/cli/managedBom.js +320 -200
- package/lib/cli/nativeBom.js +139 -30
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +17 -9
- package/lib/core/fs.js +3 -2
- package/lib/core/httpClient.js +159 -24
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +5 -7
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/cmakeResolver.js +10 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +337 -22
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1967 -788
- package/lib/ecosystems/golem.js +28 -12
- package/lib/ecosystems/gradleutils.js +314 -30
- package/lib/ecosystems/jsEvidence.js +85 -24
- package/lib/ecosystems/kosi.js +21 -4
- package/lib/ecosystems/npmutils.js +64 -0
- package/lib/ecosystems/parsers-dotnet.js +567 -151
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +324 -61
- package/lib/ecosystems/parsers-js.js +92 -26
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +204 -12
- package/lib/ecosystems/parsers-misc.js +182 -55
- package/lib/ecosystems/parsers-python.js +141 -10
- package/lib/ecosystems/parsers-rust.js +4 -4
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +67 -17
- package/lib/ecosystems/rusi.js +22 -9
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +342 -0
- package/lib/ecosystems/scalasem.js +2618 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +578 -69
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +576 -128
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/atomUtils.js +171 -48
- package/lib/inventory/auditCategories.js +7 -0
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/deps.js +501 -124
- package/lib/inventory/dosai.js +248 -44
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +52 -6
- package/lib/inventory/evidenceUtils.js +154 -109
- package/lib/inventory/fetchBatch.js +574 -38
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +8 -8
- package/lib/inventory/spdx.js +101 -0
- package/lib/inventory/traceRunner.js +88 -7
- package/lib/inventory/tracedCrypto.js +294 -0
- package/lib/managers/docker.js +33 -32
- package/lib/managers/oci.js +24 -2
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +16 -2
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +23 -0
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/postgen.js +15 -1
- package/lib/stages/postgen/specVersionCompat.js +32 -0
- package/lib/validator/index.js +25 -10
- package/lib/validator/reporters/json.js +1 -0
- package/package.json +21 -21
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +24 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +9 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +13 -8
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.d.ts.map +1 -0
- package/types/lib/core/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +12 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +50 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmutils.d.ts +15 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +44 -2
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +6 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +53 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +27 -2
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +21 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +75 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +80 -4
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +82 -10
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
- package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +59 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +57 -8
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +10 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +19 -4
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +9 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +12 -0
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +42 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +3 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +2 -1
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.d.ts.map +1 -0
- package/types/lib/parsers/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +8 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
|
|
2
|
+
import { sanitizeBomUrl } from "../core/propertySanitizer.js";
|
|
3
|
+
import {
|
|
4
|
+
gatedGet,
|
|
5
|
+
prefetchEnabled,
|
|
6
|
+
prefetchedResponse,
|
|
7
|
+
prefetchJson,
|
|
8
|
+
} from "../inventory/fetchBatch.js";
|
|
9
|
+
import { getRepoLicense, prefetchRepoLicenses } from "./ecosystems.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Opt-in remote enrichment for Terraform / OpenTofu components: licenses,
|
|
13
|
+
* repository URLs and registry flags.
|
|
14
|
+
*
|
|
15
|
+
* Callers gate this on `shouldFetchPackageMetadata()` (the same rule as the
|
|
16
|
+
* Elm collector), so an ordinary scan makes no requests at all. When it does
|
|
17
|
+
* run, only the two public registries are contacted — `registry.terraform.io`
|
|
18
|
+
* and `registry.opentofu.org` — never a private registry host, whose name
|
|
19
|
+
* alone can be confidential.
|
|
20
|
+
*
|
|
21
|
+
* The OpenTofu docs API is queried for both public hosts because it fronts
|
|
22
|
+
* the same namespace with per-version SPDX ids and a confidence score; the
|
|
23
|
+
* Terraform registry only contributes repository URLs, verification flags
|
|
24
|
+
* and tiers. GitHub's license API is the last resort and reports the default
|
|
25
|
+
* branch's license, not necessarily the license at the pinned tag.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const PUBLIC_REGISTRY_HOSTS = new Set([
|
|
29
|
+
"registry.terraform.io",
|
|
30
|
+
"registry.opentofu.org",
|
|
31
|
+
]);
|
|
32
|
+
const MIN_LICENSE_CONFIDENCE = 0.8;
|
|
33
|
+
const GITHUB_TREE_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/.+$/;
|
|
34
|
+
const GITHUB_REPO_URL = /^https:\/\/github\.com\/[^/]+\/[^/]+$/;
|
|
35
|
+
|
|
36
|
+
const property = (pkg, name) =>
|
|
37
|
+
(pkg.properties || []).find((prop) => prop.name === name)?.value;
|
|
38
|
+
|
|
39
|
+
const setProperty = (pkg, name, value) => {
|
|
40
|
+
(pkg.properties = pkg.properties || []).push({ name, value });
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Registry address of a component, split into its parts.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} pkg Component
|
|
47
|
+
* @returns {undefined|{ host: string, parts: string[] }} Address descriptor
|
|
48
|
+
*/
|
|
49
|
+
function registryAddress(pkg) {
|
|
50
|
+
const address = property(pkg, "cdx:tf:address");
|
|
51
|
+
if (!address) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const parts = address.split("/");
|
|
55
|
+
if (parts.length < 3 || parts.length > 4) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return { host: parts[0], parts };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Enrich Terraform components in place. Never throws; a failed lookup leaves
|
|
63
|
+
* the component exactly as the offline pass produced it.
|
|
64
|
+
*
|
|
65
|
+
* @param {object[]} pkgList Components to enrich
|
|
66
|
+
* @returns {Promise<object[]>} The same package list
|
|
67
|
+
*/
|
|
68
|
+
export async function getTerraformRegistryMetadata(pkgList) {
|
|
69
|
+
const components = pkgList || [];
|
|
70
|
+
// Only public-registry components with an exact version take part in the
|
|
71
|
+
// registry steps; the GitHub fallback below runs for the whole list.
|
|
72
|
+
const targets = components.filter(
|
|
73
|
+
(pkg) =>
|
|
74
|
+
pkg.version &&
|
|
75
|
+
PUBLIC_REGISTRY_HOSTS.has(registryAddress(pkg)?.host || ""),
|
|
76
|
+
);
|
|
77
|
+
const docsBase = (
|
|
78
|
+
readEnvironmentVariable("CDXGEN_TOFU_DOCS_URL") ||
|
|
79
|
+
"https://api.opentofu.org"
|
|
80
|
+
).replace(/\/+$/u, "");
|
|
81
|
+
const tfBase = (
|
|
82
|
+
readEnvironmentVariable("CDXGEN_TF_REGISTRY_URL") ||
|
|
83
|
+
"https://registry.terraform.io"
|
|
84
|
+
).replace(/\/+$/u, "");
|
|
85
|
+
|
|
86
|
+
const docsUrl = (pkg) => {
|
|
87
|
+
const { parts } = registryAddress(pkg);
|
|
88
|
+
const encoded = parts.map((segment) => encodeURIComponent(segment));
|
|
89
|
+
const version = encodeURIComponent(pkg.version);
|
|
90
|
+
if (property(pkg, "cdx:tf:kind") === "provider") {
|
|
91
|
+
// parts: host, namespace, type
|
|
92
|
+
return `${docsBase}/registry/docs/providers/${encoded[1]}/${encoded[2]}/v${version}/index.json`;
|
|
93
|
+
}
|
|
94
|
+
// parts: host, namespace, name, system
|
|
95
|
+
return `${docsBase}/registry/docs/modules/${encoded[1]}/${encoded[2]}/${encoded[3]}/v${version}/index.json`;
|
|
96
|
+
};
|
|
97
|
+
const registryUrl = (pkg) => {
|
|
98
|
+
const { parts } = registryAddress(pkg);
|
|
99
|
+
const encoded = parts.map((segment) => encodeURIComponent(segment));
|
|
100
|
+
const version = encodeURIComponent(pkg.version);
|
|
101
|
+
if (property(pkg, "cdx:tf:kind") === "provider") {
|
|
102
|
+
return `${tfBase}/v1/providers/${encoded[1]}/${encoded[2]}/${version}`;
|
|
103
|
+
}
|
|
104
|
+
return `${tfBase}/v1/modules/${encoded[1]}/${encoded[2]}/${encoded[3]}/${version}`;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// The OpenTofu docs API fronts both public registries; the Terraform
|
|
108
|
+
// registry API is asked only about its own components, so scanning an
|
|
109
|
+
// OpenTofu project never sends its module list to registry.terraform.io.
|
|
110
|
+
const onTerraformRegistry = (pkg) =>
|
|
111
|
+
registryAddress(pkg)?.host === "registry.terraform.io";
|
|
112
|
+
const urls = [
|
|
113
|
+
...new Set(
|
|
114
|
+
targets.flatMap((pkg) =>
|
|
115
|
+
onTerraformRegistry(pkg)
|
|
116
|
+
? [docsUrl(pkg), registryUrl(pkg)]
|
|
117
|
+
: [docsUrl(pkg)],
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
];
|
|
121
|
+
const prefetched = await prefetchJson(
|
|
122
|
+
prefetchEnabled() ? urls.map((url) => ({ url })) : [],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
for (const pkg of targets) {
|
|
126
|
+
// 1. OpenTofu docs API: per-version SPDX ids with confidence.
|
|
127
|
+
const docs = await fetchJson(docsUrl(pkg), prefetched);
|
|
128
|
+
if (docs) {
|
|
129
|
+
const entries = Array.isArray(docs.licenses)
|
|
130
|
+
? docs.licenses
|
|
131
|
+
: Array.isArray(docs.license)
|
|
132
|
+
? docs.license
|
|
133
|
+
: [];
|
|
134
|
+
const best = entries
|
|
135
|
+
.filter(
|
|
136
|
+
(entry) =>
|
|
137
|
+
entry &&
|
|
138
|
+
typeof entry.spdx === "string" &&
|
|
139
|
+
entry.spdx &&
|
|
140
|
+
typeof entry.confidence === "number" &&
|
|
141
|
+
entry.confidence >= MIN_LICENSE_CONFIDENCE,
|
|
142
|
+
)
|
|
143
|
+
.sort((a, b) => b.confidence - a.confidence)[0];
|
|
144
|
+
if (best && !pkg.license) {
|
|
145
|
+
pkg.license = best.spdx;
|
|
146
|
+
setProperty(pkg, "cdx:tf:licenseSource", "opentofu-registry");
|
|
147
|
+
}
|
|
148
|
+
const link = typeof docs.link === "string" ? docs.link : "";
|
|
149
|
+
if (registryAddress(pkg).host === "registry.opentofu.org") {
|
|
150
|
+
const tree = link.match(GITHUB_TREE_URL);
|
|
151
|
+
if (tree && !pkg.repository) {
|
|
152
|
+
pkg.repository = {
|
|
153
|
+
url: `https://github.com/${tree[1]}/${tree[2]}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 2. Terraform registry: repository, verification, tier, deprecation.
|
|
159
|
+
if (!onTerraformRegistry(pkg)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const meta = await fetchJson(registryUrl(pkg), prefetched);
|
|
163
|
+
if (meta) {
|
|
164
|
+
applyRegistryMetadata(pkg, meta);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 3. GitHub fallback for anything still without a license.
|
|
169
|
+
const githubUrls = [
|
|
170
|
+
...new Set(
|
|
171
|
+
components
|
|
172
|
+
.filter((pkg) => !pkg.license)
|
|
173
|
+
.map((pkg) => githubRepositoryUrl(pkg))
|
|
174
|
+
.filter(Boolean),
|
|
175
|
+
),
|
|
176
|
+
];
|
|
177
|
+
if (githubUrls.length) {
|
|
178
|
+
await prefetchRepoLicenses(githubUrls);
|
|
179
|
+
for (const pkg of components.filter((pkg) => !pkg.license)) {
|
|
180
|
+
const url = githubRepositoryUrl(pkg);
|
|
181
|
+
if (!url) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const license = await getRepoLicense(url, undefined);
|
|
186
|
+
if (license?.id) {
|
|
187
|
+
pkg.license = license.id;
|
|
188
|
+
setProperty(pkg, "cdx:tf:licenseSource", "github");
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (DEBUG_MODE) {
|
|
192
|
+
console.log(
|
|
193
|
+
`Unable to read the GitHub license of ${url}: ${err.message}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return pkgList;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The GitHub repository URL of a component, if it has one.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} pkg Component
|
|
206
|
+
* @returns {string|undefined}
|
|
207
|
+
*/
|
|
208
|
+
function githubRepositoryUrl(pkg) {
|
|
209
|
+
const repository =
|
|
210
|
+
typeof pkg.repository?.url === "string" ? pkg.repository.url : undefined;
|
|
211
|
+
if (repository && GITHUB_REPO_URL.test(repository)) {
|
|
212
|
+
return repository;
|
|
213
|
+
}
|
|
214
|
+
// A git module's sanitized VCS URL, kept in its display source.
|
|
215
|
+
const source = property(pkg, "cdx:tf:module:source") || "";
|
|
216
|
+
if (source.startsWith("git::https://github.com/")) {
|
|
217
|
+
const url = source.slice("git::".length).replace(/\.git$/u, "");
|
|
218
|
+
if (GITHUB_REPO_URL.test(url)) {
|
|
219
|
+
return url;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Apply one Terraform registry document to a component.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} pkg Component
|
|
229
|
+
* @param {object} meta Registry document
|
|
230
|
+
*/
|
|
231
|
+
function applyRegistryMetadata(pkg, meta) {
|
|
232
|
+
if (
|
|
233
|
+
typeof meta.source === "string" &&
|
|
234
|
+
(meta.source.startsWith("http://") || meta.source.startsWith("https://")) &&
|
|
235
|
+
!pkg.repository
|
|
236
|
+
) {
|
|
237
|
+
const sanitized = sanitizeBomUrl(meta.source);
|
|
238
|
+
if (sanitized) {
|
|
239
|
+
pkg.repository = { url: sanitized };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (property(pkg, "cdx:tf:kind") === "module") {
|
|
243
|
+
if (typeof meta.verified === "boolean") {
|
|
244
|
+
setProperty(
|
|
245
|
+
pkg,
|
|
246
|
+
"cdx:tf:registry:verified",
|
|
247
|
+
meta.verified ? "true" : "false",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (meta.deprecation !== null && meta.deprecation !== undefined) {
|
|
251
|
+
setProperty(pkg, "cdx:tf:deprecated", "true");
|
|
252
|
+
}
|
|
253
|
+
if (typeof meta.description === "string" && !pkg.description) {
|
|
254
|
+
pkg.description = meta.description.slice(0, 1024);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
if (
|
|
258
|
+
meta.tier === "official" ||
|
|
259
|
+
meta.tier === "partner" ||
|
|
260
|
+
meta.tier === "community"
|
|
261
|
+
) {
|
|
262
|
+
setProperty(pkg, "cdx:tf:registry:tier", meta.tier);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Read one registry document, preferring the batched prefetch. A document the
|
|
269
|
+
* batch did not answer is fetched through the host's gate, so a rate limited
|
|
270
|
+
* registry is not asked again and a definite miss is remembered for the scan.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} url Document URL
|
|
273
|
+
* @param {Map} prefetched Batched results
|
|
274
|
+
* @returns {Promise<object|undefined>} Parsed document
|
|
275
|
+
*/
|
|
276
|
+
async function fetchJson(url, prefetched) {
|
|
277
|
+
try {
|
|
278
|
+
const res =
|
|
279
|
+
prefetchedResponse(prefetched, url) ||
|
|
280
|
+
(await gatedGet(url, { responseType: "json" }));
|
|
281
|
+
return res?.body;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (DEBUG_MODE) {
|
|
284
|
+
console.log(`Unable to fetch ${url}: ${err.message}`);
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
package/lib/ecosystems/utils.js
CHANGED
|
@@ -211,6 +211,7 @@ import {
|
|
|
211
211
|
parseGoModWhy,
|
|
212
212
|
parseGopkgData,
|
|
213
213
|
parseGosumData,
|
|
214
|
+
parseGosumHashes,
|
|
214
215
|
parseGoVersionData,
|
|
215
216
|
} from "./parsers-go.js";
|
|
216
217
|
import {
|
|
@@ -482,6 +483,7 @@ export {
|
|
|
482
483
|
parseGoModWhy,
|
|
483
484
|
parseGopkgData,
|
|
484
485
|
parseGosumData,
|
|
486
|
+
parseGosumHashes,
|
|
485
487
|
parseGoVersionData,
|
|
486
488
|
parseHelmYamlData,
|
|
487
489
|
parseJarManifest,
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code a project carries inside its own tree under a license of its own: a
|
|
3
|
+
* copied library, an imported test suite. Such a directory holds its own
|
|
4
|
+
* license file, and the license differs from the project's.
|
|
5
|
+
*
|
|
6
|
+
* Layer 3: reads license files under the scan root.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, statSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
11
|
+
|
|
12
|
+
import { getAllFiles } from "../core/fs.js";
|
|
13
|
+
import { isInsideDir } from "../inventory/cmakeBuildDirs.js";
|
|
14
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
15
|
+
import { guessLicenseId } from "../inventory/spdx.js";
|
|
16
|
+
|
|
17
|
+
/** A license file larger than this is not a license text. */
|
|
18
|
+
const MAX_LICENSE_FILE_BYTES = 256 * 1024;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Download caches that hold other projects' packages rather than code the
|
|
22
|
+
* project carries. `.terraform` and `.terragrunt-cache` are filled by
|
|
23
|
+
* `terraform init`/Terragrunt; their modules and providers are inventoried,
|
|
24
|
+
* with their licenses, by the Terraform collector, so reporting them again as
|
|
25
|
+
* vendored directories would only duplicate those components under a weaker
|
|
26
|
+
* identity. Listed explicitly because a multi-type scan may have widened the
|
|
27
|
+
* shared options (`includeNodeModulesDir`) to dot directories.
|
|
28
|
+
*/
|
|
29
|
+
const DOWNLOAD_CACHE_EXCLUDES = ["**/.terraform/**", "**/.terragrunt-cache/**"];
|
|
30
|
+
|
|
31
|
+
/** Extensions a license file is written with. */
|
|
32
|
+
const LICENSE_EXTENSIONS = new Set(["", ".TXT", ".MD", ".RST", ".LIB"]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a file name is a license file: `LICENSE`, `LICENCE` or `COPYING`,
|
|
36
|
+
* bare, with a text extension, or with a suffix naming the license
|
|
37
|
+
* (`LICENSE-MIT`).
|
|
38
|
+
*
|
|
39
|
+
* @param {string} name File name
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isLicenseFileName(name) {
|
|
43
|
+
const upper = `${name}`.toUpperCase();
|
|
44
|
+
for (const stem of ["LICENSE", "LICENCE", "COPYING"]) {
|
|
45
|
+
if (!upper.startsWith(stem)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const rest = upper.slice(stem.length);
|
|
49
|
+
if (LICENSE_EXTENSIONS.has(rest)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// LICENSE-MIT, LICENSE-APACHE-2.0, LICENSE-BSD.txt; not license-checker.js
|
|
53
|
+
if (/^-[A-Z0-9-]+(\.[0-9]+)*(\.(TXT|MD|RST))?$/.test(rest)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function licenseOf(file) {
|
|
61
|
+
try {
|
|
62
|
+
const stat = statSync(file);
|
|
63
|
+
if (!stat.isFile() || stat.size > MAX_LICENSE_FILE_BYTES) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return guessLicenseId(readFileSync(file, "utf-8"));
|
|
67
|
+
} catch (_err) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function licenseChoice(id) {
|
|
73
|
+
return id.includes(" ") ? { expression: id } : { license: { id } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find the directories of a project that carry code under a license other
|
|
78
|
+
* than the project's.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} root Project scan root
|
|
81
|
+
* @param {Object} options CLI options (exclusions)
|
|
82
|
+
* @param {string[]} [excludeDirs] Directories already known to hold
|
|
83
|
+
* dependencies (submodules, fetched sources, build trees), not searched
|
|
84
|
+
* @returns {{components: Object[], dirs: string[]}} A component per vendored
|
|
85
|
+
* directory, and the directories
|
|
86
|
+
*/
|
|
87
|
+
export function findVendoredCode(root, options = {}, excludeDirs = []) {
|
|
88
|
+
const absRoot = resolve(root);
|
|
89
|
+
const files = getAllFiles(absRoot, "**/{licen[cs]e,copying}*", {
|
|
90
|
+
...options,
|
|
91
|
+
exclude: [...(options.exclude || []), ...DOWNLOAD_CACHE_EXCLUDES],
|
|
92
|
+
})
|
|
93
|
+
.filter((f) => isLicenseFileName(basename(f)))
|
|
94
|
+
.map((f) => resolve(f))
|
|
95
|
+
.filter((f) => !excludeDirs.some((d) => isInsideDir(f, d)));
|
|
96
|
+
const relDir = (f) => relative(absRoot, dirname(f)).split(sep).join("/");
|
|
97
|
+
const rootLicenses = new Set(
|
|
98
|
+
files
|
|
99
|
+
.filter((f) => relDir(f) === "")
|
|
100
|
+
.map(licenseOf)
|
|
101
|
+
.filter(Boolean),
|
|
102
|
+
);
|
|
103
|
+
const byDepth = files
|
|
104
|
+
.filter((f) => relDir(f) !== "")
|
|
105
|
+
.sort(
|
|
106
|
+
(a, b) =>
|
|
107
|
+
relDir(a).split("/").length - relDir(b).split("/").length ||
|
|
108
|
+
a.localeCompare(b),
|
|
109
|
+
);
|
|
110
|
+
const components = [];
|
|
111
|
+
const dirs = [];
|
|
112
|
+
for (const file of byDepth) {
|
|
113
|
+
const dir = dirname(file);
|
|
114
|
+
if (dirs.some((d) => isInsideDir(dir, d))) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const id = licenseOf(file);
|
|
118
|
+
if (!id || rootLicenses.has(id)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const path = relDir(file);
|
|
122
|
+
const name = basename(dir);
|
|
123
|
+
const purl = tryBuildPurl({ type: "generic", name, subpath: path });
|
|
124
|
+
const licenseFile = relative(absRoot, file).split(sep).join("/");
|
|
125
|
+
const component = {
|
|
126
|
+
type: "library",
|
|
127
|
+
name,
|
|
128
|
+
version: "",
|
|
129
|
+
licenses: [licenseChoice(id)],
|
|
130
|
+
properties: [
|
|
131
|
+
{ name: "cdx:vendored", value: "true" },
|
|
132
|
+
{ name: "cdx:vendored:path", value: path },
|
|
133
|
+
{ name: "cdx:vendored:licenseFile", value: licenseFile },
|
|
134
|
+
],
|
|
135
|
+
evidence: {
|
|
136
|
+
identity: {
|
|
137
|
+
field: "name",
|
|
138
|
+
confidence: 0.4,
|
|
139
|
+
methods: [
|
|
140
|
+
{ technique: "filename", confidence: 0.4, value: licenseFile },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
licenses: [licenseChoice(id)],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
if (purl) {
|
|
147
|
+
component.purl = purl;
|
|
148
|
+
component["bom-ref"] = decodeURIComponent(purl);
|
|
149
|
+
} else {
|
|
150
|
+
component["bom-ref"] = `vendored:${path}`;
|
|
151
|
+
}
|
|
152
|
+
components.push(component);
|
|
153
|
+
dirs.push(dir);
|
|
154
|
+
}
|
|
155
|
+
return { components, dirs };
|
|
156
|
+
}
|