@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,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parser for Terraform / OpenTofu module and provider source addresses.
|
|
3
|
+
*
|
|
4
|
+
* A module `source` argument can be a local path, a registry shorthand, a git
|
|
5
|
+
* URL (forced with `git::` or detected from a github.com/gitlab.com/bitbucket.org
|
|
6
|
+
* host or an scp-style `git@host:path` string), a mercurial URL, or an archive
|
|
7
|
+
* URL over http/s3/gcs. The grammar is go-getter's: an optional `<getter>::`
|
|
8
|
+
* prefix, a base address, an optional `//subdirectory` suffix that starts after
|
|
9
|
+
* the `://` of a URL when one is present, and go-getter query parameters such
|
|
10
|
+
* as `ref`, `rev`, `checksum` and `archive`.
|
|
11
|
+
*
|
|
12
|
+
* Source strings are untrusted and can carry credentials (URL passwords, the
|
|
13
|
+
* git `sshkey` parameter, S3 keys), so this module never returns the raw input.
|
|
14
|
+
* The `url` field is rebuilt from parsed, sanitized pieces — userinfo, query
|
|
15
|
+
* and fragment removed — and `displaySource` is assembled from those pieces
|
|
16
|
+
* only. `credentialInSource` flags that a secret-shaped parameter was present
|
|
17
|
+
* without repeating it.
|
|
18
|
+
*
|
|
19
|
+
* Layer 1: no filesystem, no network, no purl construction. Callers turn the
|
|
20
|
+
* returned plain data into components and purls.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const TERRAFORM_REGISTRY_HOST = "registry.terraform.io";
|
|
24
|
+
export const OPENTOFU_REGISTRY_HOST = "registry.opentofu.org";
|
|
25
|
+
|
|
26
|
+
/** Getters that may be forced with a `name::` prefix. */
|
|
27
|
+
const FORCED_GETTERS = new Set([
|
|
28
|
+
"git",
|
|
29
|
+
"hg",
|
|
30
|
+
"s3",
|
|
31
|
+
"gcs",
|
|
32
|
+
"http",
|
|
33
|
+
"https",
|
|
34
|
+
"file",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** go-getter query parameters that carry credentials (checked case-blind). */
|
|
38
|
+
const CREDENTIAL_QUERY_KEYS = new Set([
|
|
39
|
+
"sshkey",
|
|
40
|
+
"aws_access_key_id",
|
|
41
|
+
"aws_access_key_secret",
|
|
42
|
+
"aws_access_token",
|
|
43
|
+
"token",
|
|
44
|
+
"access_token",
|
|
45
|
+
"private_token",
|
|
46
|
+
"password",
|
|
47
|
+
"secret",
|
|
48
|
+
"sig",
|
|
49
|
+
"signature",
|
|
50
|
+
"x-amz-signature",
|
|
51
|
+
"x-goog-signature",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Registry namespace and name: alphanumeric edges, `-`/`_` inside, 1–64 chars. */
|
|
55
|
+
const REGISTRY_PART = /^[0-9A-Za-z](?:[0-9A-Za-z_-]{0,62}[0-9A-Za-z])?$/;
|
|
56
|
+
/** Registry target system: lower-case only. */
|
|
57
|
+
const REGISTRY_SYSTEM = /^[0-9a-z]{1,64}$/;
|
|
58
|
+
/** Archive filename suffixes go-getter unpacks without `?archive=`. */
|
|
59
|
+
const ARCHIVE_EXTENSIONS = [
|
|
60
|
+
".zip",
|
|
61
|
+
".tar",
|
|
62
|
+
".tar.gz",
|
|
63
|
+
".tgz",
|
|
64
|
+
".tar.bz2",
|
|
65
|
+
".tbz2",
|
|
66
|
+
".tar.xz",
|
|
67
|
+
".txz",
|
|
68
|
+
];
|
|
69
|
+
/** `?checksum=` algorithms and the hex length each digest must have. */
|
|
70
|
+
const CHECKSUM_ALGORITHMS = {
|
|
71
|
+
md5: { alg: "MD5", length: 32 },
|
|
72
|
+
sha1: { alg: "SHA-1", length: 40 },
|
|
73
|
+
sha256: { alg: "SHA-256", length: 64 },
|
|
74
|
+
sha512: { alg: "SHA-512", length: 128 },
|
|
75
|
+
};
|
|
76
|
+
const HEX_DIGEST = /^[0-9a-f]+$/i;
|
|
77
|
+
/** A bare or `=`-prefixed single semver-ish clause. */
|
|
78
|
+
const EXACT_VERSION =
|
|
79
|
+
/^v?[0-9]+(?:\.[0-9]+){0,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
80
|
+
/** A version-shaped git ref; the suffix must start with `-` or `+`. */
|
|
81
|
+
const VCS_TAG = /^v?[0-9]+(?:\.[0-9]+)*(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
82
|
+
/** Windows drive roots such as `C:\` or `C:/`. */
|
|
83
|
+
const DRIVE_ROOT = /^[A-Za-z]:[\\/]/;
|
|
84
|
+
const GIT_HOSTS = new Set(["github.com", "gitlab.com", "bitbucket.org"]);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse a module `source` argument into a classified descriptor.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} source Module source string as written in the configuration
|
|
90
|
+
* @param {Object} [options]
|
|
91
|
+
* @param {string} [options.defaultRegistryHost] Host for 3-segment registry
|
|
92
|
+
* shorthand (`registry.terraform.io` unless the caller knows better)
|
|
93
|
+
* @returns {null|Object} `null` for non-strings, otherwise a descriptor whose
|
|
94
|
+
* `kind` is `local`, `registry`, `git`, `hg`, `http`, `s3`, `gcs`, `file` or
|
|
95
|
+
* `unknown`
|
|
96
|
+
*/
|
|
97
|
+
export function parseModuleSource(source, { defaultRegistryHost } = {}) {
|
|
98
|
+
const defaultHost = defaultRegistryHost || TERRAFORM_REGISTRY_HOST;
|
|
99
|
+
if (typeof source !== "string") {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const text = source.trim();
|
|
103
|
+
if (!text) {
|
|
104
|
+
return { kind: "unknown", credentialInSource: false };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Local sources stay inside the calling package and are never components.
|
|
108
|
+
if (
|
|
109
|
+
text === "." ||
|
|
110
|
+
text === ".." ||
|
|
111
|
+
text.startsWith("./") ||
|
|
112
|
+
text.startsWith("../") ||
|
|
113
|
+
text.startsWith(".\\") ||
|
|
114
|
+
text.startsWith("..\\")
|
|
115
|
+
) {
|
|
116
|
+
return {
|
|
117
|
+
kind: "local",
|
|
118
|
+
path: text,
|
|
119
|
+
credentialInSource: false,
|
|
120
|
+
displaySource: text,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Absolute paths (and drive roots) are file sources; callers never emit them.
|
|
125
|
+
if (text.startsWith("/") || text.startsWith("\\") || DRIVE_ROOT.test(text)) {
|
|
126
|
+
return { kind: "file", credentialInSource: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Forced getter prefix.
|
|
130
|
+
let getter;
|
|
131
|
+
let rest = text;
|
|
132
|
+
const dd = text.indexOf("::");
|
|
133
|
+
if (dd >= 0) {
|
|
134
|
+
const prefix = text.slice(0, dd).toLowerCase();
|
|
135
|
+
if (!FORCED_GETTERS.has(prefix)) {
|
|
136
|
+
return { kind: "unknown", credentialInSource: false };
|
|
137
|
+
}
|
|
138
|
+
getter = prefix;
|
|
139
|
+
rest = text.slice(dd + 2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Split the `//subdirectory` marker and the query string off the base, the
|
|
143
|
+
// way go-getter's SourceDirSubdir does. The marker is only searched for
|
|
144
|
+
// before the first `?` — a query value may itself contain `//` (a URL, or
|
|
145
|
+
// a base64 token) and must never become a subdirectory, because the
|
|
146
|
+
// subdirectory is published while the query is not. The search also starts
|
|
147
|
+
// after `://`, so a URL's scheme slashes do not count. A query found on the
|
|
148
|
+
// subdirectory belongs to the base URL.
|
|
149
|
+
const firstQuery = rest.indexOf("?");
|
|
150
|
+
const stop = firstQuery >= 0 ? firstQuery : rest.length;
|
|
151
|
+
const schemeAt = rest.slice(0, stop).indexOf("://");
|
|
152
|
+
const searchFrom = schemeAt >= 0 ? schemeAt + 3 : 0;
|
|
153
|
+
const marker = rest.slice(0, stop).indexOf("//", searchFrom);
|
|
154
|
+
let base = rest;
|
|
155
|
+
let rawSubdir;
|
|
156
|
+
if (marker >= 0) {
|
|
157
|
+
const tail = rest.slice(marker + 2);
|
|
158
|
+
const queryAt = tail.indexOf("?");
|
|
159
|
+
if (queryAt >= 0) {
|
|
160
|
+
rawSubdir = tail.slice(0, queryAt);
|
|
161
|
+
base = rest.slice(0, marker) + tail.slice(queryAt);
|
|
162
|
+
} else {
|
|
163
|
+
rawSubdir = tail;
|
|
164
|
+
base = rest.slice(0, marker);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const queryAt = base.indexOf("?");
|
|
168
|
+
const address = queryAt >= 0 ? base.slice(0, queryAt) : base;
|
|
169
|
+
const queryString = queryAt >= 0 ? base.slice(queryAt + 1) : "";
|
|
170
|
+
|
|
171
|
+
// Query parameters: keep the identity-relevant ones, flag the secret-shaped
|
|
172
|
+
// ones without ever copying their values.
|
|
173
|
+
const params = new URLSearchParams(queryString);
|
|
174
|
+
const refParam = params.get("ref");
|
|
175
|
+
const revParam = params.get("rev");
|
|
176
|
+
const checksumParam = params.get("checksum");
|
|
177
|
+
const archiveParam = params.has("archive");
|
|
178
|
+
let credentialInSource = false;
|
|
179
|
+
for (const key of params.keys()) {
|
|
180
|
+
if (CREDENTIAL_QUERY_KEYS.has(key.toLowerCase())) {
|
|
181
|
+
credentialInSource = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Normalize the subdirectory: forward slashes, no outer slashes. A `..`
|
|
187
|
+
// segment escapes the package, so the source is unusable.
|
|
188
|
+
let subdir;
|
|
189
|
+
if (rawSubdir !== undefined) {
|
|
190
|
+
const segments = rawSubdir.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
191
|
+
if (segments.includes("..")) {
|
|
192
|
+
return { kind: "unknown", credentialInSource };
|
|
193
|
+
}
|
|
194
|
+
if (segments.length) {
|
|
195
|
+
subdir = segments.join("/");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const result = {
|
|
200
|
+
kind: "unknown",
|
|
201
|
+
credentialInSource,
|
|
202
|
+
...(subdir ? { subdir } : {}),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (getter) {
|
|
206
|
+
fillForcedGetter(result, getter, address);
|
|
207
|
+
} else {
|
|
208
|
+
fillDetectedSource(result, address, defaultHost);
|
|
209
|
+
}
|
|
210
|
+
if (result.kind === "unknown") {
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
if (result.kind === "hg") {
|
|
214
|
+
result.ref = revParam ?? refParam ?? result.ref;
|
|
215
|
+
} else if (result.kind === "git") {
|
|
216
|
+
result.ref = refParam ?? result.ref;
|
|
217
|
+
}
|
|
218
|
+
if (checksumParam && !result.checksum) {
|
|
219
|
+
const checksum = parseChecksumParam(checksumParam);
|
|
220
|
+
if (checksum) {
|
|
221
|
+
result.checksum = checksum;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (archiveParam && !result.archive) {
|
|
225
|
+
result.archive = true;
|
|
226
|
+
}
|
|
227
|
+
if (!result.archive && result.url) {
|
|
228
|
+
const lower = result.url.toLowerCase();
|
|
229
|
+
if (ARCHIVE_EXTENSIONS.some((ext) => lower.endsWith(ext))) {
|
|
230
|
+
result.archive = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
result.displaySource = buildDisplaySource(result);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Classify an address that had no `<getter>::` prefix.
|
|
239
|
+
*
|
|
240
|
+
* @param {Object} result Descriptor under construction, mutated
|
|
241
|
+
* @param {string} address Base address without subdirectory or query
|
|
242
|
+
* @param {string} defaultHost Registry host for 3-segment shorthand
|
|
243
|
+
*/
|
|
244
|
+
function fillDetectedSource(result, address, defaultHost) {
|
|
245
|
+
const segments = address.split("/");
|
|
246
|
+
const host = (segments[0] || "").toLowerCase();
|
|
247
|
+
if (GIT_HOSTS.has(host) && segments.length >= 3 && segments[1]) {
|
|
248
|
+
// go-getter turns `github.com/o/r/x/y` into the repo plus `//x/y`.
|
|
249
|
+
const repo = segments[2].replace(/\.git$/i, "");
|
|
250
|
+
result.kind = "git";
|
|
251
|
+
result.url = sanitizeUrl(
|
|
252
|
+
`https://${segments[0]}/${segments[1]}/${repo}.git`,
|
|
253
|
+
);
|
|
254
|
+
const extra = segments.slice(3).filter(Boolean).join("/");
|
|
255
|
+
if (extra) {
|
|
256
|
+
result.subdir = result.subdir ? `${result.subdir}/${extra}` : extra;
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (address.startsWith("git@")) {
|
|
261
|
+
const scp = parseScpSource(address);
|
|
262
|
+
if (scp) {
|
|
263
|
+
result.kind = "git";
|
|
264
|
+
result.url = scp.url;
|
|
265
|
+
result.credentialInSource = result.credentialInSource || scp.hasPassword;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (address.startsWith("http://") || address.startsWith("https://")) {
|
|
270
|
+
const parsed = parseWebUrl(address);
|
|
271
|
+
if (parsed) {
|
|
272
|
+
result.kind = parsed.kind;
|
|
273
|
+
result.url = parsed.url;
|
|
274
|
+
result.credentialInSource =
|
|
275
|
+
result.credentialInSource || parsed.hasPassword;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const registry = matchRegistryAddress(address, defaultHost);
|
|
281
|
+
if (registry) {
|
|
282
|
+
result.kind = "registry";
|
|
283
|
+
Object.assign(result, registry);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Interpret the base address of a `<getter>::` source.
|
|
289
|
+
*
|
|
290
|
+
* @param {Object} result Descriptor under construction, mutated
|
|
291
|
+
* @param {string} getter Forced getter name
|
|
292
|
+
* @param {string} address Base address without subdirectory or query
|
|
293
|
+
*/
|
|
294
|
+
function fillForcedGetter(result, getter, address) {
|
|
295
|
+
if (getter === "file") {
|
|
296
|
+
result.kind = "file";
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (getter === "git") {
|
|
300
|
+
if (address.startsWith("file://")) {
|
|
301
|
+
result.kind = "file";
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (isWebOrGitScheme(address)) {
|
|
305
|
+
result.kind = "git";
|
|
306
|
+
result.url = sanitizeUrl(address);
|
|
307
|
+
result.credentialInSource =
|
|
308
|
+
result.credentialInSource || urlHasPassword(address);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const scp = parseScpSource(address);
|
|
312
|
+
if (scp) {
|
|
313
|
+
result.kind = "git";
|
|
314
|
+
result.url = scp.url;
|
|
315
|
+
result.credentialInSource = result.credentialInSource || scp.hasPassword;
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (getter === "hg") {
|
|
320
|
+
if (isWebOrGitScheme(address)) {
|
|
321
|
+
result.kind = "hg";
|
|
322
|
+
result.url = sanitizeUrl(address);
|
|
323
|
+
result.credentialInSource =
|
|
324
|
+
result.credentialInSource || urlHasPassword(address);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (getter === "s3" || getter === "gcs" || getter === "http") {
|
|
329
|
+
if (address.startsWith("http://") || address.startsWith("https://")) {
|
|
330
|
+
result.kind = getter === "s3" || getter === "gcs" ? getter : "http";
|
|
331
|
+
result.url = sanitizeUrl(address);
|
|
332
|
+
result.credentialInSource =
|
|
333
|
+
result.credentialInSource || urlHasPassword(address);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Parse an `http(s)` address, mapping S3 and GCS hosts to their kinds.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} address Base address
|
|
342
|
+
* @returns {null|{kind: string, url: string|undefined, hasPassword: boolean}}
|
|
343
|
+
*/
|
|
344
|
+
function parseWebUrl(address) {
|
|
345
|
+
let parsed;
|
|
346
|
+
try {
|
|
347
|
+
parsed = new URL(address);
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const host = parsed.hostname.toLowerCase();
|
|
352
|
+
let kind = "http";
|
|
353
|
+
if (host.endsWith(".amazonaws.com")) {
|
|
354
|
+
kind = "s3";
|
|
355
|
+
} else if (
|
|
356
|
+
host === "www.googleapis.com" &&
|
|
357
|
+
parsed.pathname.startsWith("/storage/")
|
|
358
|
+
) {
|
|
359
|
+
kind = "gcs";
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
kind,
|
|
363
|
+
url: sanitizeUrl(address),
|
|
364
|
+
hasPassword: urlHasPassword(address),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Whether a URL's userinfo carries a credential: a password, or a username
|
|
370
|
+
* that hides one behind a percent-encoded colon (`user%3Atoken@host`), which
|
|
371
|
+
* git and go-getter decode back into `user:token`. A bare username is not a
|
|
372
|
+
* secret and is dropped silently.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} address Absolute URL
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
function urlHasPassword(address) {
|
|
378
|
+
try {
|
|
379
|
+
const parsed = new URL(address);
|
|
380
|
+
return (
|
|
381
|
+
parsed.password !== "" || parsed.username.toLowerCase().includes("%3a")
|
|
382
|
+
);
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Whether the address is an http, https, ssh or git URL.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} address Base address
|
|
392
|
+
* @returns {boolean}
|
|
393
|
+
*/
|
|
394
|
+
function isWebOrGitScheme(address) {
|
|
395
|
+
return (
|
|
396
|
+
address.startsWith("https://") ||
|
|
397
|
+
address.startsWith("http://") ||
|
|
398
|
+
address.startsWith("ssh://") ||
|
|
399
|
+
address.startsWith("git://")
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Convert an scp-style `user[:password]@host:path` string to an ssh URL.
|
|
405
|
+
*
|
|
406
|
+
* @param {string} address Base address
|
|
407
|
+
* @returns {null|{url: string, hasPassword: boolean}}
|
|
408
|
+
*/
|
|
409
|
+
function parseScpSource(address) {
|
|
410
|
+
const at = address.lastIndexOf("@");
|
|
411
|
+
if (at < 0) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const userInfo = address.slice(0, at);
|
|
415
|
+
const hostPart = address.slice(at + 1);
|
|
416
|
+
const colon = hostPart.indexOf(":");
|
|
417
|
+
if (colon < 0) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
const host = hostPart.slice(0, colon);
|
|
421
|
+
const path = hostPart.slice(colon + 1);
|
|
422
|
+
if (!host || !path || host.includes("/")) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
url: sanitizeUrl(`ssh://${host}/${path}`),
|
|
427
|
+
hasPassword:
|
|
428
|
+
userInfo.includes(":") || userInfo.toLowerCase().includes("%3a"),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Strip userinfo, query and fragment from a URL.
|
|
434
|
+
*
|
|
435
|
+
* The conversion of scp forms to `ssh://` happens before this runs, because a
|
|
436
|
+
* generic URL sanitizer keeps userinfo when it cannot parse an authority.
|
|
437
|
+
*
|
|
438
|
+
* @param {string} address Absolute URL
|
|
439
|
+
* @returns {string|undefined} Sanitized URL
|
|
440
|
+
*/
|
|
441
|
+
function sanitizeUrl(address) {
|
|
442
|
+
try {
|
|
443
|
+
const parsed = new URL(address);
|
|
444
|
+
parsed.username = "";
|
|
445
|
+
parsed.password = "";
|
|
446
|
+
parsed.search = "";
|
|
447
|
+
parsed.hash = "";
|
|
448
|
+
return parsed.toString();
|
|
449
|
+
} catch {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Match a registry module address: `[host/]namespace/name/system`.
|
|
456
|
+
*
|
|
457
|
+
* @param {string} address Base address
|
|
458
|
+
* @param {string} defaultHost Host for the 3-segment form
|
|
459
|
+
* @returns {null|{host: string, namespace: string, name: string, system: string, address: string}}
|
|
460
|
+
*/
|
|
461
|
+
function matchRegistryAddress(address, defaultHost) {
|
|
462
|
+
const segments = address.split("/");
|
|
463
|
+
if (segments.length !== 3 && segments.length !== 4) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
let host = null;
|
|
467
|
+
const hasHost = segments.length === 4;
|
|
468
|
+
if (hasHost) {
|
|
469
|
+
let rawHost = segments[0];
|
|
470
|
+
let port = "";
|
|
471
|
+
const colon = rawHost.indexOf(":");
|
|
472
|
+
if (colon >= 0) {
|
|
473
|
+
port = rawHost.slice(colon + 1);
|
|
474
|
+
if (!/^[0-9]{1,5}$/.test(port)) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
rawHost = rawHost.slice(0, colon);
|
|
478
|
+
}
|
|
479
|
+
host = rawHost.toLowerCase();
|
|
480
|
+
if (!host.includes(".") && host !== "localhost") {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
// The port is part of the registry's identity: two registries on one
|
|
484
|
+
// host are different sources.
|
|
485
|
+
if (port) {
|
|
486
|
+
host = `${host}:${port}`;
|
|
487
|
+
}
|
|
488
|
+
} else if (segments.some((segment) => segment.includes(":"))) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const ns = segments[hasHost ? 1 : 0];
|
|
492
|
+
const name = segments[hasHost ? 2 : 1];
|
|
493
|
+
const system = segments[hasHost ? 3 : 2];
|
|
494
|
+
if (
|
|
495
|
+
!REGISTRY_PART.test(ns) ||
|
|
496
|
+
!REGISTRY_PART.test(name) ||
|
|
497
|
+
!REGISTRY_SYSTEM.test(system)
|
|
498
|
+
) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
const fullHost = (host || defaultHost).toLowerCase();
|
|
502
|
+
const lowerNs = ns.toLowerCase();
|
|
503
|
+
const lowerName = name.toLowerCase();
|
|
504
|
+
const lowerSystem = system.toLowerCase();
|
|
505
|
+
return {
|
|
506
|
+
host: fullHost,
|
|
507
|
+
namespace: lowerNs,
|
|
508
|
+
name: lowerName,
|
|
509
|
+
system: lowerSystem,
|
|
510
|
+
address: `${fullHost}/${lowerNs}/${lowerName}/${lowerSystem}`,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Parse a `?checksum=<alg>:<hex>` parameter.
|
|
516
|
+
*
|
|
517
|
+
* @param {string} value Raw parameter value
|
|
518
|
+
* @returns {null|{alg: string, content: string}} Digest descriptor
|
|
519
|
+
*/
|
|
520
|
+
function parseChecksumParam(value) {
|
|
521
|
+
const colon = value.indexOf(":");
|
|
522
|
+
if (colon < 0) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const spec = CHECKSUM_ALGORITHMS[value.slice(0, colon).toLowerCase()];
|
|
526
|
+
const hex = value.slice(colon + 1);
|
|
527
|
+
if (!spec || hex.length !== spec.length || !HEX_DIGEST.test(hex)) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
return { alg: spec.alg, content: hex.toLowerCase() };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Build the sanitized display form Terraform itself uses in `modules.json`.
|
|
535
|
+
*
|
|
536
|
+
* @param {Object} result Classified descriptor
|
|
537
|
+
* @returns {string|undefined} Display source
|
|
538
|
+
*/
|
|
539
|
+
function buildDisplaySource(result) {
|
|
540
|
+
let base;
|
|
541
|
+
if (result.kind === "registry") {
|
|
542
|
+
base = result.address;
|
|
543
|
+
} else if (["git", "hg", "s3", "gcs"].includes(result.kind) && result.url) {
|
|
544
|
+
base = `${result.kind}::${result.url}`;
|
|
545
|
+
} else if (result.kind === "http" && result.url) {
|
|
546
|
+
base = result.url;
|
|
547
|
+
} else {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
return result.subdir ? `${base}//${result.subdir}` : base;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Parse a `required_providers` source string into a provider address.
|
|
555
|
+
*
|
|
556
|
+
* @param {string} source Provider source such as `hashicorp/aws` or
|
|
557
|
+
* `registry.terraform.io/hashicorp/aws`
|
|
558
|
+
* @param {Object} [options]
|
|
559
|
+
* @param {string} [options.defaultRegistryHost] Host for 2-segment shorthand
|
|
560
|
+
* @returns {null|Object} `{ host, namespace, type, address }` lower-cased,
|
|
561
|
+
* `{ builtin: true }` for `terraform.io/builtin/*`, or `null`
|
|
562
|
+
*/
|
|
563
|
+
export function parseProviderSource(source, { defaultRegistryHost } = {}) {
|
|
564
|
+
const defaultHost = defaultRegistryHost || TERRAFORM_REGISTRY_HOST;
|
|
565
|
+
if (typeof source !== "string") {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const text = source.trim();
|
|
569
|
+
if (!text || text.includes("::")) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
if (text.toLowerCase().startsWith("terraform.io/builtin/")) {
|
|
573
|
+
return { builtin: true };
|
|
574
|
+
}
|
|
575
|
+
const segments = text.split("/");
|
|
576
|
+
let host;
|
|
577
|
+
let namespace;
|
|
578
|
+
let type;
|
|
579
|
+
if (segments.length === 2) {
|
|
580
|
+
host = defaultHost;
|
|
581
|
+
namespace = segments[0];
|
|
582
|
+
type = segments[1];
|
|
583
|
+
} else if (segments.length === 3) {
|
|
584
|
+
host = segments[0].toLowerCase();
|
|
585
|
+
if (!host.includes(".") && host !== "localhost") {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
namespace = segments[1];
|
|
589
|
+
type = segments[2];
|
|
590
|
+
} else {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
if (!REGISTRY_PART.test(namespace) || !REGISTRY_PART.test(type)) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const fullHost = host.toLowerCase();
|
|
597
|
+
const lowerNs = namespace.toLowerCase();
|
|
598
|
+
const lowerType = type.toLowerCase();
|
|
599
|
+
return {
|
|
600
|
+
host: fullHost,
|
|
601
|
+
namespace: lowerNs,
|
|
602
|
+
type: lowerType,
|
|
603
|
+
address: `${fullHost}/${lowerNs}/${lowerType}`,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Classify a module `version` constraint by how tightly it pins.
|
|
609
|
+
*
|
|
610
|
+
* @param {string} constraint Constraint string from the configuration
|
|
611
|
+
* @returns {Object} `{ pinning: "exact", version }`, `{ pinning: "range" }` or
|
|
612
|
+
* `{ pinning: "none" }`
|
|
613
|
+
*/
|
|
614
|
+
export function classifyVersionConstraint(constraint) {
|
|
615
|
+
if (typeof constraint !== "string" || !constraint.trim()) {
|
|
616
|
+
return { pinning: "none" };
|
|
617
|
+
}
|
|
618
|
+
const clauses = constraint.split(",").map((clause) => clause.trim());
|
|
619
|
+
if (clauses.length !== 1) {
|
|
620
|
+
return { pinning: "range" };
|
|
621
|
+
}
|
|
622
|
+
let clause = clauses[0];
|
|
623
|
+
if (clause.startsWith("=")) {
|
|
624
|
+
clause = clause.slice(1).trim();
|
|
625
|
+
}
|
|
626
|
+
if (clause && EXACT_VERSION.test(clause)) {
|
|
627
|
+
return { pinning: "exact", version: clause };
|
|
628
|
+
}
|
|
629
|
+
return { pinning: "range" };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Classify a git/hg ref by how immutably it identifies a commit.
|
|
634
|
+
*
|
|
635
|
+
* @param {string} ref Ref value from `?ref=` or `?rev=`
|
|
636
|
+
* @returns {"sha"|"tag"|"branch"|"none"}
|
|
637
|
+
*/
|
|
638
|
+
export function classifyVcsRef(ref) {
|
|
639
|
+
if (typeof ref !== "string" || !ref) {
|
|
640
|
+
return "none";
|
|
641
|
+
}
|
|
642
|
+
if (ref.length > 255) {
|
|
643
|
+
return "branch";
|
|
644
|
+
}
|
|
645
|
+
if (/^[0-9a-f]{40}$/i.test(ref) || /^[0-9a-f]{64}$/i.test(ref)) {
|
|
646
|
+
return "sha";
|
|
647
|
+
}
|
|
648
|
+
if (VCS_TAG.test(ref)) {
|
|
649
|
+
return "tag";
|
|
650
|
+
}
|
|
651
|
+
return "branch";
|
|
652
|
+
}
|
package/lib/server/server.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
readEnvironmentVariable,
|
|
16
16
|
} from "../core/activity.js";
|
|
17
17
|
import { isWin } from "../core/paths.js";
|
|
18
|
+
import { resetRunState } from "../core/runState.js";
|
|
18
19
|
import { CDXGEN_VERSION } from "../core/state.js";
|
|
19
20
|
import { installConsoleShim, ui } from "../core/ui.js";
|
|
20
21
|
import { fetchPomXmlAsJson } from "../ecosystems/ecosystems.js";
|
|
@@ -244,6 +245,26 @@ const configureServer = (cdxgenServer) => {
|
|
|
244
245
|
|
|
245
246
|
const ALL_INTERFACES = new Set(["0.0.0.0", "::", "::/128", "::/0"]);
|
|
246
247
|
|
|
248
|
+
// Registry answers, recorded misses and local cache lookups last for one scan.
|
|
249
|
+
// They are emptied whenever no scan is running, so a scan neither starts from
|
|
250
|
+
// an earlier scan's answers, which would hide new releases and new downloads,
|
|
251
|
+
// nor has its own emptied by a scan running alongside it.
|
|
252
|
+
let activeScans = 0;
|
|
253
|
+
|
|
254
|
+
function beginScan() {
|
|
255
|
+
if (activeScans === 0) {
|
|
256
|
+
resetRunState();
|
|
257
|
+
}
|
|
258
|
+
activeScans++;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function endScan() {
|
|
262
|
+
activeScans--;
|
|
263
|
+
if (activeScans === 0) {
|
|
264
|
+
resetRunState();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
247
268
|
/**
|
|
248
269
|
* Boot the cdxgen HTTP server.
|
|
249
270
|
*
|
|
@@ -393,6 +414,7 @@ const start = (options) => {
|
|
|
393
414
|
}
|
|
394
415
|
let cloneDir;
|
|
395
416
|
let srcDir;
|
|
417
|
+
beginScan();
|
|
396
418
|
try {
|
|
397
419
|
let sourcePath = filePath;
|
|
398
420
|
let purlResolution;
|
|
@@ -606,6 +628,7 @@ const start = (options) => {
|
|
|
606
628
|
}
|
|
607
629
|
console.log("Error while generating SBOM response", err);
|
|
608
630
|
} finally {
|
|
631
|
+
endScan();
|
|
609
632
|
if (cleanup && cloneDir) {
|
|
610
633
|
cleanupSourceDir(cloneDir);
|
|
611
634
|
}
|
|
@@ -137,7 +137,7 @@ export const ECOSYSTEM_MARKERS = Object.freeze({
|
|
|
137
137
|
ocaml: { names: ["dune-project"], suffixes: [".opam"] },
|
|
138
138
|
lua: { names: ["luarocks.lock"], suffixes: [".rockspec"] },
|
|
139
139
|
spack: { names: ["spack.yaml", "spack.lock"], suffixes: [] },
|
|
140
|
-
terraform: { names: [".terraform.lock.hcl"], suffixes: [] },
|
|
140
|
+
terraform: { names: [".terraform.lock.hcl"], suffixes: [".tf", ".tofu"] },
|
|
141
141
|
});
|
|
142
142
|
|
|
143
143
|
/** Marker file name → ecosystems claiming it, and marker suffix → claiming
|