@cdxgen/cdxgen 13.4.0 → 13.5.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 +7 -7
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +25 -5
- package/bin/evinse.js +5 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +283 -1
- package/data/rules/cbom-security.yaml +235 -4
- package/data/rules/go-binaries.yaml +46 -0
- package/data/rules/golem-go.yaml +80 -0
- package/data/rules/hbom-compliance.yaml +40 -22
- package/data/rules/hbom-performance.yaml +7 -1
- package/data/rules/host-topology.yaml +1 -1
- package/data/rules/obom-runtime.yaml +31 -2
- package/data/rules/php-runtime.yaml +143 -0
- package/lib/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +28 -3
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +73 -4
- package/lib/cli/jvmBom.js +126 -20
- package/lib/cli/managedBom.js +711 -71
- package/lib/cli/nativeBom.js +225 -28
- package/lib/core/env.js +30 -8
- package/lib/core/fs.js +28 -19
- package/lib/core/httpClient.js +74 -45
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -0
- package/lib/ecosystems/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +30 -20
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +229 -44
- package/lib/ecosystems/parsers-python.js +11 -10
- package/lib/ecosystems/rubyutils.js +6 -3
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +2 -1
- package/lib/ecosystems/scalasem.js +3 -1
- package/lib/evinser/evinser.js +126 -18
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +33 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +6 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +500 -58
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +168 -5
- package/lib/inventory/evidenceUtils.js +436 -23
- package/lib/inventory/formulationParsers.js +3 -2
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostTopology.js +21 -0
- package/lib/inventory/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +89 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/phpBuildInfo.js +487 -0
- package/lib/inventory/phpHostProbe.js +641 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/source.js +207 -3
- package/lib/inventory/spdx.js +144 -0
- package/lib/inventory/traceRunner.js +5 -1
- package/lib/inventory/tracedCrypto.js +129 -0
- package/lib/managers/binary.js +529 -183
- package/lib/managers/docker.js +166 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +34 -2
- package/lib/server/server.js +186 -1
- package/lib/stages/postgen/annotator.js +17 -1
- package/lib/stages/postgen/introspection/reflect.js +4 -1
- package/lib/stages/postgen/postgen.js +5 -5
- package/lib/stages/postgen/ruleEngine.js +58 -1
- package/lib/stages/postgen/specVersionCompat.js +30 -0
- package/lib/stages/pregen/pregen.js +117 -5
- package/lib/validator/bomValidator.js +26 -5
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +26 -7
- package/lib/validator/generated/validate-1.7.mjs +31 -12
- package/lib/validator/generated/validate-2.0.mjs +30 -11
- package/package.json +17 -17
- package/types/bin/cdxgen-secure.d.ts +3 -0
- package/types/bin/cdxgen-secure.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +9 -2
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +43 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +20 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +18 -2
- 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.map +1 -1
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/projectTooling.d.ts +41 -0
- package/types/lib/core/projectTooling.d.ts.map +1 -0
- package/types/lib/core/propertySanitizer.d.ts +26 -0
- package/types/lib/core/propertySanitizer.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/golem.d.ts +81 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +10 -8
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +85 -13
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +4 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +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/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +25 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +10 -0
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
- package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
- package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
- package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +14 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.d.ts.map +1 -1
- package/types/lib/inventory/certificates.d.ts +205 -0
- package/types/lib/inventory/certificates.d.ts.map +1 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts +2 -1
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts +18 -0
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +145 -6
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +6 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts +40 -2
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +29 -10
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/goBuildInfo.d.ts +10 -0
- package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
- package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
- package/types/lib/inventory/hostTopology.d.ts.map +1 -1
- package/types/lib/inventory/osPackagePurl.d.ts +99 -0
- package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +6 -2
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/osinfo.d.ts +31 -6
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/osqueryTransform.d.ts +51 -0
- package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/phpHostProbe.d.ts +35 -0
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/rootfsCerts.d.ts +27 -0
- package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +28 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +15 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +4 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +22 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +10 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +12 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/email.d.ts +19 -0
- package/types/lib/parsers/email.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +13 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +8 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +14 -1
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
openSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { basename, join, relative, sep } from "node:path";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
androidNetworkSecurityConfigPins,
|
|
13
|
+
CertificateCollector,
|
|
14
|
+
certRecordFromDer,
|
|
15
|
+
classifyCryptoFile,
|
|
16
|
+
kubernetesTlsSecretRecords,
|
|
17
|
+
linkFallbackIssuers,
|
|
18
|
+
} from "./certificates.js";
|
|
19
|
+
|
|
20
|
+
// Bounds mirror trustinspector's rootfs mode (PLAN section 4.1): 16 MiB per
|
|
21
|
+
// file, 20,000 files read and 500,000 entries visited per tree, depth 12.
|
|
22
|
+
export const ROOTFS_CERT_LIMITS = Object.freeze({
|
|
23
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
24
|
+
maxFiles: 20000,
|
|
25
|
+
maxEntries: 500000,
|
|
26
|
+
maxDepth: 12,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Coverage table: the same locations and sourceTypes as trustinspector's
|
|
30
|
+
// rootfs mode (thirdparty/trustinspector/rootfs.go candidateDirs).
|
|
31
|
+
const CA_STORE_DIRS = [
|
|
32
|
+
"etc/ssl/certs",
|
|
33
|
+
"usr/local/share/ca-certificates",
|
|
34
|
+
"usr/share/ca-certificates",
|
|
35
|
+
"etc/pki/tls/certs",
|
|
36
|
+
"etc/pki/ca-trust/source/anchors",
|
|
37
|
+
"etc/pki/ca-trust/extracted",
|
|
38
|
+
"var/lib/ca-certificates",
|
|
39
|
+
"usr/share/pki/ca-trust-source/anchors",
|
|
40
|
+
];
|
|
41
|
+
const CA_STORE_FILES = ["etc/ssl/cert.pem"];
|
|
42
|
+
const CA_BLOCKLIST_DIRS = [
|
|
43
|
+
"etc/pki/ca-trust/source/blocklist",
|
|
44
|
+
"etc/pki/ca-trust/source/blacklist",
|
|
45
|
+
"usr/share/pki/ca-trust-source/blocklist",
|
|
46
|
+
"usr/share/pki/ca-trust-source/blacklist",
|
|
47
|
+
];
|
|
48
|
+
const SERVER_CONFIG_DIRS = [
|
|
49
|
+
"etc/nginx",
|
|
50
|
+
"etc/apache2",
|
|
51
|
+
"etc/httpd",
|
|
52
|
+
"etc/haproxy",
|
|
53
|
+
"etc/letsencrypt/live",
|
|
54
|
+
"etc/letsencrypt/archive",
|
|
55
|
+
"etc/kubernetes/pki",
|
|
56
|
+
"var/lib/kubelet/pki",
|
|
57
|
+
"etc/docker/certs.d",
|
|
58
|
+
"etc/containerd/certs.d",
|
|
59
|
+
"etc/envoy",
|
|
60
|
+
"etc/traefik",
|
|
61
|
+
];
|
|
62
|
+
const JAVA_DIRS = [
|
|
63
|
+
"usr/lib/jvm",
|
|
64
|
+
"opt",
|
|
65
|
+
"etc/ssl/certs/java",
|
|
66
|
+
"etc/pki/java",
|
|
67
|
+
"usr/share/elasticsearch",
|
|
68
|
+
];
|
|
69
|
+
const LANGUAGE_BUNDLE_DIRS = ["usr/lib", "usr/local/lib", "opt"];
|
|
70
|
+
// Kubernetes secrets and Android configs are cdxgen-side producers: the
|
|
71
|
+
// plugin has no YAML or Android parser.
|
|
72
|
+
const SECRET_YAML_DIRS = [
|
|
73
|
+
"etc/kubernetes",
|
|
74
|
+
"var/run/secrets",
|
|
75
|
+
"run/secrets",
|
|
76
|
+
"var/lib/kubelet",
|
|
77
|
+
"srv",
|
|
78
|
+
"app",
|
|
79
|
+
"opt",
|
|
80
|
+
"home",
|
|
81
|
+
"root",
|
|
82
|
+
];
|
|
83
|
+
const ANDROID_DIRS = ["app", "opt", "srv", "data", "home", "root"];
|
|
84
|
+
|
|
85
|
+
const CERT_SUFFIXES = [
|
|
86
|
+
".pem",
|
|
87
|
+
".crt",
|
|
88
|
+
".cer",
|
|
89
|
+
".der",
|
|
90
|
+
".p7b",
|
|
91
|
+
".p7c",
|
|
92
|
+
".p12",
|
|
93
|
+
".pfx",
|
|
94
|
+
".jks",
|
|
95
|
+
".jceks",
|
|
96
|
+
".truststore",
|
|
97
|
+
".key",
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function isJavaKeystoreName(base) {
|
|
101
|
+
if (base === "cacerts" || base === "jssecacerts") {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return [".jks", ".jceks", ".truststore", ".p12", ".pfx"].some((suffix) =>
|
|
105
|
+
base.endsWith(suffix),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Mirrors looksLikeTrustMaterialPath in trustinspector: private-key
|
|
110
|
+
// directories are never read.
|
|
111
|
+
function looksLikeTrustMaterialPath(rel, sourceType) {
|
|
112
|
+
const lower = rel.toLowerCase();
|
|
113
|
+
if (
|
|
114
|
+
lower.startsWith("etc/ssl/private/") ||
|
|
115
|
+
lower.startsWith("etc/pki/tls/private/")
|
|
116
|
+
) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const base = basename(lower);
|
|
120
|
+
if (sourceType === "language-bundle") {
|
|
121
|
+
return base === "cacert.pem";
|
|
122
|
+
}
|
|
123
|
+
if (sourceType === "java-truststore") {
|
|
124
|
+
return isJavaKeystoreName(base);
|
|
125
|
+
}
|
|
126
|
+
if (isJavaKeystoreName(base)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return (
|
|
130
|
+
CERT_SUFFIXES.some((suffix) => lower.endsWith(suffix)) ||
|
|
131
|
+
lower.includes("/certs/")
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read at most `maxBytes` of a regular file. The cap applies to the read
|
|
137
|
+
* itself, so a file that grows after the size check cannot exceed it. Files
|
|
138
|
+
* that are not regular (FIFOs, devices, sockets) are never opened.
|
|
139
|
+
*/
|
|
140
|
+
function readCapped(absolute, maxBytes) {
|
|
141
|
+
const before = lstatSync(absolute);
|
|
142
|
+
if (!before.isFile() || before.size > maxBytes) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const fd = openSync(absolute, "r");
|
|
146
|
+
try {
|
|
147
|
+
const buffer = Buffer.alloc(Math.min(before.size, maxBytes) + 1);
|
|
148
|
+
let total = 0;
|
|
149
|
+
while (total < buffer.length) {
|
|
150
|
+
const n = readSync(fd, buffer, total, buffer.length - total, null);
|
|
151
|
+
if (n === 0) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
total += n;
|
|
155
|
+
}
|
|
156
|
+
if (total > maxBytes) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return buffer.subarray(0, total);
|
|
160
|
+
} finally {
|
|
161
|
+
closeSync(fd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Pure-JS inventory of the certificate locations trustinspector's rootfs
|
|
167
|
+
* mode covers, plus Kubernetes TLS secrets and Android pins. Used when the
|
|
168
|
+
* plugin is not available. Produces components through the shared
|
|
169
|
+
* CertificateCollector, so bom-refs, names and deduplication match the other
|
|
170
|
+
* producers; locations are root-relative ("/etc/ssl/certs/x.crt").
|
|
171
|
+
*
|
|
172
|
+
* Every access stays inside the root: symlinks (files or directories) are
|
|
173
|
+
* skipped, only regular files are opened, and the walk is bounded by entry,
|
|
174
|
+
* file, size and depth limits.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} rootDir extracted root filesystem
|
|
177
|
+
* @param {object} [limits] overrides for ROOTFS_CERT_LIMITS (tests)
|
|
178
|
+
* @returns {{components: object[], dependencies: object[], warnings: string[]}}
|
|
179
|
+
*/
|
|
180
|
+
export function collectRootfsCertificates(rootDir, limits = {}) {
|
|
181
|
+
const bounds = { ...ROOTFS_CERT_LIMITS, ...limits };
|
|
182
|
+
const warnings = [];
|
|
183
|
+
let realRoot;
|
|
184
|
+
try {
|
|
185
|
+
realRoot = realpathSync(rootDir);
|
|
186
|
+
} catch {
|
|
187
|
+
return { components: [], dependencies: [], warnings };
|
|
188
|
+
}
|
|
189
|
+
const collector = new CertificateCollector();
|
|
190
|
+
const records = [];
|
|
191
|
+
const components = [];
|
|
192
|
+
const seenFiles = new Set();
|
|
193
|
+
let filesRead = 0;
|
|
194
|
+
|
|
195
|
+
// Resolve a root-relative directory to an absolute path that is proven to
|
|
196
|
+
// stay inside the root; undefined when it does not exist, is not a
|
|
197
|
+
// directory, or any symlink on the way leaves the root.
|
|
198
|
+
const resolveInside = (rel) => {
|
|
199
|
+
const absolute = join(realRoot, rel);
|
|
200
|
+
let real;
|
|
201
|
+
try {
|
|
202
|
+
real = realpathSync(absolute);
|
|
203
|
+
} catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const inside = relative(realRoot, real);
|
|
207
|
+
if (inside.startsWith("..") || inside.split(sep).includes("..")) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return real;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const walk = (relBase, visit) => {
|
|
214
|
+
const start = resolveInside(relBase);
|
|
215
|
+
if (!start) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let startStat;
|
|
219
|
+
try {
|
|
220
|
+
startStat = lstatSync(start);
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!startStat.isDirectory()) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
let entries = 0;
|
|
228
|
+
const stack = [{ abs: start, rel: relBase, depth: 0 }];
|
|
229
|
+
while (stack.length) {
|
|
230
|
+
const { abs, rel, depth } = stack.pop();
|
|
231
|
+
let dirents;
|
|
232
|
+
try {
|
|
233
|
+
dirents = readdirSync(abs, { withFileTypes: true });
|
|
234
|
+
} catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
dirents.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
238
|
+
for (const dirent of dirents) {
|
|
239
|
+
entries += 1;
|
|
240
|
+
if (entries > bounds.maxEntries) {
|
|
241
|
+
warnings.push(
|
|
242
|
+
`entry limit (${bounds.maxEntries}) reached in /${relBase}`,
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// Symlinks and special files are skipped outright.
|
|
247
|
+
if (dirent.isSymbolicLink()) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const childRel = `${rel}/${dirent.name}`;
|
|
251
|
+
if (dirent.isDirectory()) {
|
|
252
|
+
if (depth + 1 >= bounds.maxDepth) {
|
|
253
|
+
warnings.push(
|
|
254
|
+
`depth limit (${bounds.maxDepth}) exceeded in /${relBase}`,
|
|
255
|
+
);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
stack.push({
|
|
259
|
+
abs: join(abs, dirent.name),
|
|
260
|
+
rel: childRel,
|
|
261
|
+
depth: depth + 1,
|
|
262
|
+
});
|
|
263
|
+
} else if (dirent.isFile()) {
|
|
264
|
+
if (visit(childRel, join(abs, dirent.name)) === false) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const readRel = (rel, abs) => {
|
|
273
|
+
if (filesRead >= bounds.maxFiles) {
|
|
274
|
+
warnings.push(`file count limit (${bounds.maxFiles}) reached`);
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
filesRead += 1;
|
|
278
|
+
try {
|
|
279
|
+
const bytes = readCapped(abs, bounds.maxFileBytes);
|
|
280
|
+
if (!bytes) {
|
|
281
|
+
warnings.push(
|
|
282
|
+
`file over ${bounds.maxFileBytes} bytes skipped: /${rel}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return bytes;
|
|
286
|
+
} catch {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const consumeMaterial = (rel, abs, sourceType) => {
|
|
292
|
+
if (seenFiles.has(rel) || !looksLikeTrustMaterialPath(rel, sourceType)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const bytes = readRel(rel, abs);
|
|
296
|
+
if (!bytes?.length) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
seenFiles.add(rel);
|
|
300
|
+
const location = `/${rel}`;
|
|
301
|
+
const classification = classifyCryptoFile(bytes);
|
|
302
|
+
if (classification.kind === "certificates") {
|
|
303
|
+
const dot = basename(rel).lastIndexOf(".");
|
|
304
|
+
for (const der of classification.ders) {
|
|
305
|
+
const record = certRecordFromDer(der, { location, sourceType });
|
|
306
|
+
if (record) {
|
|
307
|
+
record.fileExtension =
|
|
308
|
+
dot > 0 ? basename(rel).slice(dot + 1) : undefined;
|
|
309
|
+
records.push(record);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} else if (classification.kind === "private-key") {
|
|
313
|
+
components.push(
|
|
314
|
+
rootfsMaterialComponent("private-key", location, {
|
|
315
|
+
format: classification.format,
|
|
316
|
+
sourceType,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
} else if (classification.kind === "keystore") {
|
|
320
|
+
components.push(
|
|
321
|
+
rootfsMaterialComponent("keystore", location, {
|
|
322
|
+
format: classification.format,
|
|
323
|
+
sourceType,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Most specific location first, so a file under two candidates takes the
|
|
330
|
+
// sourceType of the narrower one (same rule as the plugin).
|
|
331
|
+
const candidates = [
|
|
332
|
+
...CA_STORE_DIRS.map((rel) => ({ rel, sourceType: "ca-store" })),
|
|
333
|
+
...CA_BLOCKLIST_DIRS.map((rel) => ({ rel, sourceType: "ca-blocklist" })),
|
|
334
|
+
...SERVER_CONFIG_DIRS.map((rel) => ({ rel, sourceType: "server-config" })),
|
|
335
|
+
...JAVA_DIRS.map((rel) => ({ rel, sourceType: "java-truststore" })),
|
|
336
|
+
...LANGUAGE_BUNDLE_DIRS.map((rel) => ({
|
|
337
|
+
rel,
|
|
338
|
+
sourceType: "language-bundle",
|
|
339
|
+
})),
|
|
340
|
+
].sort((a, b) => b.rel.split("/").length - a.rel.split("/").length);
|
|
341
|
+
for (const file of CA_STORE_FILES) {
|
|
342
|
+
const abs = resolveInside(file);
|
|
343
|
+
if (abs && lstatSync(abs).isFile()) {
|
|
344
|
+
consumeMaterial(file, abs, "ca-store");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const { rel, sourceType } of candidates) {
|
|
348
|
+
walk(rel, (childRel, abs) => {
|
|
349
|
+
consumeMaterial(childRel, abs, sourceType);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const secretFiles = new Set();
|
|
354
|
+
for (const dir of SECRET_YAML_DIRS) {
|
|
355
|
+
walk(dir, (childRel, abs) => {
|
|
356
|
+
if (/\.ya?ml$/i.test(childRel) && !secretFiles.has(childRel)) {
|
|
357
|
+
secretFiles.add(childRel);
|
|
358
|
+
const bytes = readRel(childRel, abs);
|
|
359
|
+
const text = bytes?.toString("utf-8");
|
|
360
|
+
// Cheap pre-filter, as in the source scan.
|
|
361
|
+
if (text?.includes("kubernetes.io/tls")) {
|
|
362
|
+
records.push(
|
|
363
|
+
...kubernetesTlsSecretRecords(text, { file: `/${childRel}` }),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const androidFiles = new Set();
|
|
370
|
+
for (const dir of ANDROID_DIRS) {
|
|
371
|
+
walk(dir, (childRel, abs) => {
|
|
372
|
+
if (
|
|
373
|
+
/\/res\/xml\/[^/]+\.xml$/i.test(childRel) &&
|
|
374
|
+
!androidFiles.has(childRel)
|
|
375
|
+
) {
|
|
376
|
+
androidFiles.add(childRel);
|
|
377
|
+
const text = readRel(childRel, abs)?.toString("utf-8");
|
|
378
|
+
if (text?.includes("<network-security-config")) {
|
|
379
|
+
for (const pin of androidNetworkSecurityConfigPins(text, {
|
|
380
|
+
file: `/${childRel}`,
|
|
381
|
+
})) {
|
|
382
|
+
collector.addPin(pin);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (records.length) {
|
|
390
|
+
linkFallbackIssuers(records);
|
|
391
|
+
collector.addRecords(records);
|
|
392
|
+
}
|
|
393
|
+
const built = collector.build();
|
|
394
|
+
return {
|
|
395
|
+
components: [...built.components, ...components],
|
|
396
|
+
dependencies: built.dependencies,
|
|
397
|
+
warnings,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Same shape as the keystore and private-key components the other producers
|
|
402
|
+
// emit (managedBom sourceKeystoreComponent / sourcePrivateKeyComponent).
|
|
403
|
+
function rootfsMaterialComponent(kind, location, { format, sourceType }) {
|
|
404
|
+
return {
|
|
405
|
+
name: basename(location),
|
|
406
|
+
type: "cryptographic-asset",
|
|
407
|
+
"bom-ref": `crypto/related-crypto-material/${kind}/${encodeURIComponent(location)}`,
|
|
408
|
+
cryptoProperties: {
|
|
409
|
+
assetType: "related-crypto-material",
|
|
410
|
+
relatedCryptoMaterialProperties: {
|
|
411
|
+
type: kind === "keystore" ? "other" : "private-key",
|
|
412
|
+
...(format ? { format } : {}),
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
evidence: { occurrences: [{ location }] },
|
|
416
|
+
properties: [
|
|
417
|
+
{ name: "internal:SrcFile", value: location },
|
|
418
|
+
...(sourceType
|
|
419
|
+
? [{ name: "cdx:crypto:sourceType", value: sourceType }]
|
|
420
|
+
: []),
|
|
421
|
+
...(kind === "keystore"
|
|
422
|
+
? [{ name: "cdx:crypto:keystoreReadable", value: "false" }]
|
|
423
|
+
: []),
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
package/lib/inventory/source.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { URL } from "node:url";
|
|
4
|
+
import { fileURLToPath, URL } from "node:url";
|
|
5
5
|
|
|
6
6
|
import { Purl } from "@cdxgen/cdx-purl";
|
|
7
7
|
import { coerce, diff, prerelease } from "semver";
|
|
@@ -111,6 +111,9 @@ function inferGitOperation(args = []) {
|
|
|
111
111
|
* @param {string[]} args git arguments
|
|
112
112
|
* @param {Object} options command options
|
|
113
113
|
* @param {string|undefined} options.cwd working directory
|
|
114
|
+
* @param {boolean|undefined} options.noRepository run outside any repository,
|
|
115
|
+
* so the config of a repository in the working directory, such as url.*.insteadOf
|
|
116
|
+
* or core.sshCommand, cannot change where a remote command connects
|
|
114
117
|
* @returns {Object} spawn result
|
|
115
118
|
*/
|
|
116
119
|
export function hardenedGitCommand(args, options = {}) {
|
|
@@ -122,8 +125,14 @@ export function hardenedGitCommand(args, options = {}) {
|
|
|
122
125
|
GIT_CONFIG_VALUE_0: "false",
|
|
123
126
|
GIT_CONFIG_KEY_1: "safe.bareRepository",
|
|
124
127
|
GIT_CONFIG_VALUE_1: "explicit",
|
|
128
|
+
GIT_NO_LAZY_FETCH: "1",
|
|
125
129
|
GIT_TERMINAL_PROMPT: "0",
|
|
126
130
|
};
|
|
131
|
+
if (options.noRepository) {
|
|
132
|
+
// ls-remote works without a repository. Pointing GIT_DIR at a path that
|
|
133
|
+
// does not exist stops git from discovering one in the working directory.
|
|
134
|
+
envConfigs.GIT_DIR = path.join(getTmpDir(), "cdxgen-no-repository.invalid");
|
|
135
|
+
}
|
|
127
136
|
const env = isSecureMode
|
|
128
137
|
? {
|
|
129
138
|
...process.env,
|
|
@@ -275,10 +284,14 @@ export function buildReleaseNotesFromGit(repoPath, options = {}) {
|
|
|
275
284
|
typeof remoteUrl === "string"
|
|
276
285
|
? validateAndRejectGitSource(remoteUrl)
|
|
277
286
|
: null;
|
|
287
|
+
// remote.origin.url comes from the scanned repository. Only a remote that
|
|
288
|
+
// validateAndRejectGitSource recognises and checks may reach ls-remote; git
|
|
289
|
+
// also treats forms such as host:path as remotes, which skip that check.
|
|
278
290
|
const canDiscoverRemoteTags =
|
|
279
291
|
(!currentTag || !previousTag) &&
|
|
280
292
|
typeof remoteUrl === "string" &&
|
|
281
293
|
!remoteUrl.startsWith("-") &&
|
|
294
|
+
maybeRemotePath(remoteUrl) &&
|
|
282
295
|
!remoteUrlValidationError &&
|
|
283
296
|
/github\.com[:/]/i.test(remoteUrl);
|
|
284
297
|
if (canDiscoverRemoteTags) {
|
|
@@ -298,7 +311,7 @@ export function buildReleaseNotesFromGit(repoPath, options = {}) {
|
|
|
298
311
|
"--",
|
|
299
312
|
remoteUrl,
|
|
300
313
|
],
|
|
301
|
-
{},
|
|
314
|
+
{ noRepository: true },
|
|
302
315
|
);
|
|
303
316
|
const remoteTags = parseLsRemoteTags(remoteTagsResult.stdout);
|
|
304
317
|
if (!currentTag && remoteTags.length) {
|
|
@@ -508,6 +521,59 @@ export function isAllowedPath(p) {
|
|
|
508
521
|
});
|
|
509
522
|
}
|
|
510
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Resolve symlinks in a path. When the path does not exist yet, the nearest
|
|
526
|
+
* existing parent is resolved and the remaining segments are appended, so
|
|
527
|
+
* /tmp/new on macOS still becomes /private/tmp/new.
|
|
528
|
+
*
|
|
529
|
+
* @param {string} p Path string
|
|
530
|
+
* @returns {string} real path
|
|
531
|
+
*/
|
|
532
|
+
function realPathOrResolved(p) {
|
|
533
|
+
let current = path.resolve(p);
|
|
534
|
+
const missing = [];
|
|
535
|
+
while (true) {
|
|
536
|
+
try {
|
|
537
|
+
return path.join(fs.realpathSync(current), ...missing);
|
|
538
|
+
} catch (_err) {
|
|
539
|
+
const parent = path.dirname(current);
|
|
540
|
+
if (parent === current) {
|
|
541
|
+
return path.resolve(p);
|
|
542
|
+
}
|
|
543
|
+
missing.unshift(path.basename(current));
|
|
544
|
+
current = parent;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Checks where the given path really points, after following symlinks, against
|
|
551
|
+
* where the allowed paths really point. Both sides are resolved, so an allowed
|
|
552
|
+
* directory that is itself reached through a link (such as /tmp on macOS) still
|
|
553
|
+
* matches, while a link inside an allowed directory that leads outside it does
|
|
554
|
+
* not.
|
|
555
|
+
*
|
|
556
|
+
* @param {string} p Path string to check
|
|
557
|
+
* @returns {boolean} true if the real path is inside a real allowed path
|
|
558
|
+
*/
|
|
559
|
+
export function isAllowedRealPath(p) {
|
|
560
|
+
if (typeof p !== "string" || hasDangerousUnicode(p)) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
const allowedPaths = getAllowedPaths();
|
|
564
|
+
if (!allowedPaths.length) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
const realP = realPathOrResolved(p);
|
|
568
|
+
return allowedPaths.some((ap) => {
|
|
569
|
+
const relativePath = path.relative(realPathOrResolved(ap), realP);
|
|
570
|
+
return (
|
|
571
|
+
relativePath === "" ||
|
|
572
|
+
(!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
511
577
|
/**
|
|
512
578
|
* Determine if the path could be a package URL.
|
|
513
579
|
*
|
|
@@ -586,6 +652,49 @@ export function validateAndRejectGitSource(filePath) {
|
|
|
586
652
|
};
|
|
587
653
|
}
|
|
588
654
|
|
|
655
|
+
if (hasAmbiguousRemoteCharacters(filePath)) {
|
|
656
|
+
return {
|
|
657
|
+
status: 400,
|
|
658
|
+
error: "Invalid URL Format",
|
|
659
|
+
details:
|
|
660
|
+
"The provided Git URL contains backslashes, whitespace or control characters.",
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// git reads file:// URLs from the local disk, so they get the path
|
|
665
|
+
// allowlist rather than the host allowlist.
|
|
666
|
+
if (gitUrlObj.protocol === "file:") {
|
|
667
|
+
let localPath;
|
|
668
|
+
try {
|
|
669
|
+
localPath = fileURLToPath(gitUrlObj);
|
|
670
|
+
} catch (_err) {
|
|
671
|
+
localPath = undefined;
|
|
672
|
+
}
|
|
673
|
+
if (
|
|
674
|
+
gitUrlObj.hostname ||
|
|
675
|
+
!localPath ||
|
|
676
|
+
!isAllowedPath(localPath) ||
|
|
677
|
+
(isWin && !isAllowedWinPath(localPath))
|
|
678
|
+
) {
|
|
679
|
+
return {
|
|
680
|
+
status: 403,
|
|
681
|
+
error: "Path Not Allowed",
|
|
682
|
+
details: "Path is not allowed as per the allowlist.",
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// git, libcurl and OpenSSH must connect to the host that was checked.
|
|
689
|
+
const gitRemoteHost = getGitRemoteHost(filePath);
|
|
690
|
+
if (!gitRemoteHost || gitUrlObj.hostname.toLowerCase() !== gitRemoteHost) {
|
|
691
|
+
return {
|
|
692
|
+
status: 400,
|
|
693
|
+
error: "Invalid URL Format",
|
|
694
|
+
details: "The host of the provided Git URL is ambiguous.",
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
589
698
|
if (!isAllowedHost(gitUrlObj.hostname)) {
|
|
590
699
|
return {
|
|
591
700
|
status: 403,
|
|
@@ -598,6 +707,94 @@ export function validateAndRejectGitSource(filePath) {
|
|
|
598
707
|
return null;
|
|
599
708
|
}
|
|
600
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Detect characters that git, libcurl or OpenSSH read differently from the
|
|
712
|
+
* WHATWG URL parser. A backslash ends the host for new URL() but is part of
|
|
713
|
+
* the user name for git, and new URL() silently drops tabs and newlines.
|
|
714
|
+
*
|
|
715
|
+
* @param {string} remote Git URL or SCP-like address
|
|
716
|
+
* @returns {boolean} true if the remote contains such characters
|
|
717
|
+
*/
|
|
718
|
+
function hasAmbiguousRemoteCharacters(remote) {
|
|
719
|
+
if (hasDangerousUnicode(remote)) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
for (const ch of remote) {
|
|
723
|
+
const code = ch.codePointAt(0);
|
|
724
|
+
if (ch === "\\" || code <= 0x20 || (code >= 0x7f && code <= 0x9f)) {
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Return the host a git client connects to for a remote, read the way git,
|
|
733
|
+
* libcurl and OpenSSH read it rather than the way new URL() does. git ends the
|
|
734
|
+
* authority of ssh:// and git:// URLs only at the first slash, percent-decodes
|
|
735
|
+
* it before splitting off the host, and reads a leading "[...]" as the host.
|
|
736
|
+
* OpenSSH takes the host after the last "@". So a "?" or "#" before the first
|
|
737
|
+
* slash, a second "@", brackets in the user name, or a percent sign in an
|
|
738
|
+
* ssh:// or git:// authority make the host ambiguous.
|
|
739
|
+
*
|
|
740
|
+
* @param {string} remote Git URL or SCP-like address
|
|
741
|
+
* @returns {string|undefined} lower-cased host, or undefined when it is ambiguous
|
|
742
|
+
*/
|
|
743
|
+
export function getGitRemoteHost(remote) {
|
|
744
|
+
if (typeof remote !== "string") {
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
let authority;
|
|
748
|
+
const schemeEnd = remote.indexOf("://");
|
|
749
|
+
if (schemeEnd > -1) {
|
|
750
|
+
const scheme = remote.slice(0, schemeEnd).toLowerCase();
|
|
751
|
+
const rest = remote.slice(schemeEnd + 3);
|
|
752
|
+
const slash = rest.indexOf("/");
|
|
753
|
+
authority = slash > -1 ? rest.slice(0, slash) : rest;
|
|
754
|
+
// libcurl reads the http(s) authority as written. git decodes the others.
|
|
755
|
+
if (!["http", "https"].includes(scheme) && authority.includes("%")) {
|
|
756
|
+
return undefined;
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
// git reads user@host:path as SCP-like only when the colon comes before
|
|
760
|
+
// any slash. Otherwise it is a local path.
|
|
761
|
+
const colon = remote.indexOf(":");
|
|
762
|
+
const slash = remote.indexOf("/");
|
|
763
|
+
if (colon < 0 || (slash > -1 && slash < colon)) {
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
authority = remote.slice(0, colon);
|
|
767
|
+
}
|
|
768
|
+
if (authority.includes("?") || authority.includes("#")) {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
const at = authority.indexOf("@");
|
|
772
|
+
if (at !== authority.lastIndexOf("@")) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
const userInfo = at > -1 ? authority.slice(0, at) : "";
|
|
776
|
+
if (userInfo.includes("[") || userInfo.includes("]")) {
|
|
777
|
+
return undefined;
|
|
778
|
+
}
|
|
779
|
+
let host = authority.slice(at + 1);
|
|
780
|
+
if (host.startsWith("[")) {
|
|
781
|
+
const close = host.indexOf("]");
|
|
782
|
+
if (close < 0) {
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
host = host.slice(0, close + 1);
|
|
786
|
+
} else {
|
|
787
|
+
const portSep = host.indexOf(":");
|
|
788
|
+
if (portSep > -1) {
|
|
789
|
+
host = host.slice(0, portSep);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (!host || host.startsWith("-")) {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
return host.toLowerCase();
|
|
796
|
+
}
|
|
797
|
+
|
|
601
798
|
/**
|
|
602
799
|
* Clone a git repository into a temporary directory.
|
|
603
800
|
*
|
|
@@ -606,6 +803,11 @@ export function validateAndRejectGitSource(filePath) {
|
|
|
606
803
|
* @returns {string} cloned directory path
|
|
607
804
|
*/
|
|
608
805
|
export function gitClone(repoUrl, branch = null) {
|
|
806
|
+
// Callers validate first, but git must never see a remote that fails it.
|
|
807
|
+
const validationError = validateAndRejectGitSource(repoUrl);
|
|
808
|
+
if (validationError) {
|
|
809
|
+
throw new Error(`${validationError.error}: ${validationError.details}`);
|
|
810
|
+
}
|
|
609
811
|
let baseDirName = path.basename(repoUrl);
|
|
610
812
|
if (!/^[a-zA-Z0-9_-]+$/.test(baseDirName)) {
|
|
611
813
|
baseDirName = "repo-";
|
|
@@ -635,6 +837,8 @@ export function gitClone(repoUrl, branch = null) {
|
|
|
635
837
|
"safe.bareRepository=explicit",
|
|
636
838
|
"-c",
|
|
637
839
|
"core.hooksPath=/dev/null",
|
|
840
|
+
"-c",
|
|
841
|
+
"core.symlinks=false",
|
|
638
842
|
"clone",
|
|
639
843
|
"--template=",
|
|
640
844
|
repoUrl,
|
|
@@ -735,7 +939,7 @@ export function findGitRefForPurlVersion(repoUrl, purlResolution) {
|
|
|
735
939
|
"--",
|
|
736
940
|
repoUrl,
|
|
737
941
|
],
|
|
738
|
-
{},
|
|
942
|
+
{ noRepository: true },
|
|
739
943
|
);
|
|
740
944
|
if (result.status !== 0 || !result.stdout) {
|
|
741
945
|
return undefined;
|