dependabot-npm_and_yarn 0.382.0 → 0.383.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.
- checksums.yaml +4 -4
- data/helpers/lib/npm/vulnerability-auditor.ts +101 -1
- data/helpers/test/npm/fixtures/vulnerability-auditor/locked-by-parent/package-lock.json +60 -0
- data/helpers/test/npm/fixtures/vulnerability-auditor/locked-by-parent/package.json +15 -0
- data/helpers/test/npm/vulnerability-auditor.test.ts +46 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher.rb +101 -7
- data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +72 -12
- data/lib/dependabot/npm_and_yarn/package/registry_finder.rb +2 -2
- data/lib/dependabot/npm_and_yarn/registry_helper.rb +12 -4
- data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +17 -13
- data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +1 -1
- data/lib/dependabot/npm_and_yarn/update_checker/vulnerability_auditor.rb +135 -50
- data/lib/dependabot/npm_and_yarn/update_checker.rb +6 -8
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36bb333ca707b5155b7c82a95c5a3e934a5b7acf0ae96723bf9e3ef46723679f
|
|
4
|
+
data.tar.gz: '0889ddfed838d230dd4a7e299847816690a4151ac4cd81b6230160a933e44e67'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 162d5f20b24ad893b8ae76c6389ded05c31309f7fb43a4f1ce43186cfe8c0cafea09816e245b1a30e3ecc4ffc5f3792a01e5167f5aff09c9f86ff8baa1fa7d11
|
|
7
|
+
data.tar.gz: 8a97cb90adf36509589b5309bb50036a2d6f86def852456dadec850c8fec0b3b532042898716f1c4920587d42da48b9d62dd08ad57f3d6d7b20821a9c6d11a53
|
|
@@ -49,6 +49,13 @@ interface FixUpdate {
|
|
|
49
49
|
target_version?: string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
interface BlockingDependency {
|
|
53
|
+
name: string;
|
|
54
|
+
version: string;
|
|
55
|
+
requirement: string;
|
|
56
|
+
top_level_ancestor: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
interface AuditResponse {
|
|
53
60
|
dependency_name: string;
|
|
54
61
|
current_version?: string;
|
|
@@ -56,6 +63,7 @@ interface AuditResponse {
|
|
|
56
63
|
fix_available: boolean;
|
|
57
64
|
fix_updates: FixUpdate[];
|
|
58
65
|
top_level_ancestors: string[];
|
|
66
|
+
blocking_dependencies?: BlockingDependency[];
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
interface BulkAdvisoryEntry {
|
|
@@ -154,6 +162,9 @@ export async function findVulnerableDependencies(
|
|
|
154
162
|
response.fix_available = !!fixAvailable;
|
|
155
163
|
|
|
156
164
|
if (!fixAvailable) {
|
|
165
|
+
// Surface the parent package(s) that pull in the vulnerable version so the
|
|
166
|
+
// Ruby side can explain exactly what is blocking the update.
|
|
167
|
+
response.blocking_dependencies = extractBlockingDependencies(vuln, name);
|
|
157
168
|
return response;
|
|
158
169
|
}
|
|
159
170
|
|
|
@@ -161,8 +172,10 @@ export async function findVulnerableDependencies(
|
|
|
161
172
|
|
|
162
173
|
// In order for the vuln dependency in question to be considered fixable,
|
|
163
174
|
// all dependency chains originating from it must be fixable.
|
|
164
|
-
|
|
175
|
+
const unfixableChains = chains.filter((chain) => !chain.fixAvailable);
|
|
176
|
+
if (unfixableChains.length > 0) {
|
|
165
177
|
response.fix_available = false;
|
|
178
|
+
response.blocking_dependencies = extractBlockingDependencies(vuln, name);
|
|
166
179
|
return response;
|
|
167
180
|
}
|
|
168
181
|
|
|
@@ -345,6 +358,93 @@ function buildDependencyChains(
|
|
|
345
358
|
return helper(auditReport.tree, { nodes: [] }, new Set());
|
|
346
359
|
}
|
|
347
360
|
|
|
361
|
+
/* Identify the parent package(s) that require a vulnerable copy of `name`,
|
|
362
|
+
* together with the requirement (spec) they place on it and the top-level
|
|
363
|
+
* dependency each is reachable from. This explains why the vulnerable version
|
|
364
|
+
* can't simply be replaced: a package in the tree depends on it directly.
|
|
365
|
+
*
|
|
366
|
+
* We inspect the actual vulnerable nodes' incoming edges rather than the audit's
|
|
367
|
+
* dependency chains because, when a vulnerability can't be fixed, npm propagates
|
|
368
|
+
* it up the entire chain. As a result `buildDependencyChains` collapses to the
|
|
369
|
+
* top-level package and loses the immediate parent that pins the vulnerable
|
|
370
|
+
* version. Results are de-duplicated by parent + requirement + ancestor.
|
|
371
|
+
*/
|
|
372
|
+
function extractBlockingDependencies(
|
|
373
|
+
vuln: Arborist.Vuln,
|
|
374
|
+
name: string
|
|
375
|
+
): BlockingDependency[] {
|
|
376
|
+
const blockers = new Map<string, BlockingDependency>();
|
|
377
|
+
for (const node of vuln.nodes) {
|
|
378
|
+
// `vuln.nodes` may include packages that are only vulnerable *via* this
|
|
379
|
+
// dependency; we only care about the actual vulnerable copies of `name`.
|
|
380
|
+
if (node.name !== name) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
for (const edge of node.edgesIn) {
|
|
384
|
+
const parent = edge.from;
|
|
385
|
+
if (!parent || parent.isProjectRoot) {
|
|
386
|
+
// The vulnerable dependency is itself a top-level dependency. Arborist
|
|
387
|
+
// uses the project root node (which has no incoming edges) as `edge.from`
|
|
388
|
+
// for direct dependencies, so skip it to avoid reporting the project
|
|
389
|
+
// root itself as a blocking dependency.
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const requirement = edge.spec || "*";
|
|
393
|
+
const ancestors = findTopLevelAncestorNames(edge);
|
|
394
|
+
const topLevelAncestors = ancestors.length > 0 ? ancestors : [parent.name];
|
|
395
|
+
for (const ancestor of topLevelAncestors) {
|
|
396
|
+
const key = `${parent.name}@${parent.version}|${requirement}|${ancestor}`;
|
|
397
|
+
if (!blockers.has(key)) {
|
|
398
|
+
blockers.set(key, {
|
|
399
|
+
name: parent.name,
|
|
400
|
+
version: parent.version,
|
|
401
|
+
requirement,
|
|
402
|
+
top_level_ancestor: ancestor,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return [...blockers.values()].sort(
|
|
409
|
+
(a, b) =>
|
|
410
|
+
a.name.localeCompare(b.name) ||
|
|
411
|
+
a.version.localeCompare(b.version) ||
|
|
412
|
+
a.requirement.localeCompare(b.requirement) ||
|
|
413
|
+
a.top_level_ancestor.localeCompare(b.top_level_ancestor)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* Walk up the dependency graph from the given edge to find the name(s) of the
|
|
418
|
+
* top-level dependencies (direct dependencies of the project root) the edge is
|
|
419
|
+
* reachable from. */
|
|
420
|
+
function findTopLevelAncestorNames(edge: Arborist.Edge): string[] {
|
|
421
|
+
const result = new Set<string>();
|
|
422
|
+
const seen = new Set<Arborist.Edge>();
|
|
423
|
+
const visit = (current: Arborist.Edge): void => {
|
|
424
|
+
if (seen.has(current)) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
seen.add(current);
|
|
428
|
+
const from = current.from;
|
|
429
|
+
if (!from) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
for (const parentEdge of from.edgesIn) {
|
|
433
|
+
if (parentEdge.from?.isProjectRoot) {
|
|
434
|
+
// `parentEdge.from` is the project root, so `parentEdge.to` is a
|
|
435
|
+
// direct (top-level) dependency.
|
|
436
|
+
if (parentEdge.to) {
|
|
437
|
+
result.add(parentEdge.to.name);
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
visit(parentEdge);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
visit(edge);
|
|
445
|
+
return [...result];
|
|
446
|
+
}
|
|
447
|
+
|
|
348
448
|
function groupBy<T>(
|
|
349
449
|
elems: Iterable<T>,
|
|
350
450
|
fn: (elem: T, index: number, elems: Iterable<T>) => string
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "transitive-dependency-locked-by-parent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 2,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "transitive-dependency-locked-by-parent",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@dependabot-fixtures/npm-parent-dependency-5": "1.0.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/@dependabot-fixtures/npm-intermediate-dependency": {
|
|
16
|
+
"version": "0.0.1",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-intermediate-dependency/-/npm-intermediate-dependency-0.0.1.tgz",
|
|
18
|
+
"integrity": "sha512-/N77Dzpfg8BIfFgpJrMk86ueUYTVhmpc4RobuHpIpKSc3GZr4Ltu4au92brnUGk66UkzgrMmtgqRXO8OrOspKQ==",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@dependabot-fixtures/npm-transitive-dependency": "1.0.0"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"node_modules/@dependabot-fixtures/npm-parent-dependency-5": {
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-parent-dependency-5/-/npm-parent-dependency-5-1.0.0.tgz",
|
|
26
|
+
"integrity": "sha512-xWlNw4sxT1wbrnSXZU/5PVd0ta4X+15XT9vNgZfB82q0ybr91SNVVUs+IPNZPbJ/nY70LZuRAg01kOuVPeHFlg==",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@dependabot-fixtures/npm-intermediate-dependency": "0.0.1"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"node_modules/@dependabot-fixtures/npm-transitive-dependency": {
|
|
32
|
+
"version": "1.0.0",
|
|
33
|
+
"resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-transitive-dependency/-/npm-transitive-dependency-1.0.0.tgz",
|
|
34
|
+
"integrity": "sha512-nFbzQH0TRgdzSA2/FH6MPnxZDpD+5Bgz00aD5Edgbc1wY/k8VC9s7lnk22dBTgJLwoY7MgbrnAf9rAvN08hHVg=="
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@dependabot-fixtures/npm-intermediate-dependency": {
|
|
39
|
+
"version": "0.0.1",
|
|
40
|
+
"resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-intermediate-dependency/-/npm-intermediate-dependency-0.0.1.tgz",
|
|
41
|
+
"integrity": "sha512-/N77Dzpfg8BIfFgpJrMk86ueUYTVhmpc4RobuHpIpKSc3GZr4Ltu4au92brnUGk66UkzgrMmtgqRXO8OrOspKQ==",
|
|
42
|
+
"requires": {
|
|
43
|
+
"@dependabot-fixtures/npm-transitive-dependency": "1.0.0"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"@dependabot-fixtures/npm-parent-dependency-5": {
|
|
47
|
+
"version": "1.0.0",
|
|
48
|
+
"resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-parent-dependency-5/-/npm-parent-dependency-5-1.0.0.tgz",
|
|
49
|
+
"integrity": "sha512-xWlNw4sxT1wbrnSXZU/5PVd0ta4X+15XT9vNgZfB82q0ybr91SNVVUs+IPNZPbJ/nY70LZuRAg01kOuVPeHFlg==",
|
|
50
|
+
"requires": {
|
|
51
|
+
"@dependabot-fixtures/npm-intermediate-dependency": "0.0.1"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"@dependabot-fixtures/npm-transitive-dependency": {
|
|
55
|
+
"version": "1.0.0",
|
|
56
|
+
"resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-transitive-dependency/-/npm-transitive-dependency-1.0.0.tgz",
|
|
57
|
+
"integrity": "sha512-nFbzQH0TRgdzSA2/FH6MPnxZDpD+5Bgz00aD5Edgbc1wY/k8VC9s7lnk22dBTgJLwoY7MgbrnAf9rAvN08hHVg=="
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "transitive-dependency-locked-by-parent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@dependabot-fixtures/npm-parent-dependency-5": "1.0.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -144,6 +144,52 @@ describe("findVulnerableDependencies", () => {
|
|
|
144
144
|
expect(actual).toEqual(expected);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
+
it("reports blocking dependencies when no fix is available", async () => {
|
|
148
|
+
helpers.copyDependencies("vulnerability-auditor/locked-by-parent", tempDir);
|
|
149
|
+
|
|
150
|
+
const advisories = [
|
|
151
|
+
{
|
|
152
|
+
dependency_name: "@dependabot-fixtures/npm-transitive-dependency",
|
|
153
|
+
affected_versions: ["< 1.0.1"],
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const actual = await findVulnerableDependencies(tempDir, advisories);
|
|
157
|
+
|
|
158
|
+
expect(actual.dependency_name).toBe(
|
|
159
|
+
"@dependabot-fixtures/npm-transitive-dependency"
|
|
160
|
+
);
|
|
161
|
+
expect(actual.current_version).toBe("1.0.0");
|
|
162
|
+
expect(actual.fix_available).toBe(false);
|
|
163
|
+
expect(actual.fix_updates).toEqual([]);
|
|
164
|
+
expect(actual.blocking_dependencies).toEqual([
|
|
165
|
+
{
|
|
166
|
+
name: "@dependabot-fixtures/npm-intermediate-dependency",
|
|
167
|
+
version: "0.0.1",
|
|
168
|
+
requirement: "1.0.0",
|
|
169
|
+
top_level_ancestor: "@dependabot-fixtures/npm-parent-dependency-5",
|
|
170
|
+
},
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not report the project root as a blocking dependency", async () => {
|
|
175
|
+
helpers.copyDependencies("vulnerability-auditor/simple", tempDir);
|
|
176
|
+
|
|
177
|
+
const advisories = [
|
|
178
|
+
{
|
|
179
|
+
dependency_name: "@dependabot-fixtures/npm-parent-dependency",
|
|
180
|
+
affected_versions: [">= 0"],
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
const actual = await findVulnerableDependencies(tempDir, advisories);
|
|
184
|
+
|
|
185
|
+
expect(actual.dependency_name).toBe(
|
|
186
|
+
"@dependabot-fixtures/npm-parent-dependency"
|
|
187
|
+
);
|
|
188
|
+
expect(actual.fix_available).toBe(false);
|
|
189
|
+
expect(actual.fix_updates).toEqual([]);
|
|
190
|
+
expect(actual.blocking_dependencies).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
147
193
|
it("has undefined target_version when a vulnerable package is removed", async () => {
|
|
148
194
|
helpers.copyDependencies(
|
|
149
195
|
"vulnerability-auditor/fix-removes-package",
|
|
@@ -12,6 +12,7 @@ require "dependabot/npm_and_yarn/helpers"
|
|
|
12
12
|
require "dependabot/npm_and_yarn/package_manager"
|
|
13
13
|
require "dependabot/npm_and_yarn/file_parser"
|
|
14
14
|
require "dependabot/npm_and_yarn/file_parser/lockfile_parser"
|
|
15
|
+
require "dependabot/npm_and_yarn/file_updater/npmrc_builder"
|
|
15
16
|
|
|
16
17
|
module Dependabot
|
|
17
18
|
module NpmAndYarn
|
|
@@ -82,7 +83,7 @@ module Dependabot
|
|
|
82
83
|
def fetch_files
|
|
83
84
|
fetched_files = T.let([], T::Array[DependencyFile])
|
|
84
85
|
fetched_files << package_json
|
|
85
|
-
fetched_files << T.must(npmrc) if npmrc
|
|
86
|
+
fetched_files << T.must(npmrc) if npmrc && !scope_overrides_npmrc?
|
|
86
87
|
fetched_files += npm_files if npm_version
|
|
87
88
|
fetched_files += yarn_files if yarn_version
|
|
88
89
|
fetched_files += pnpm_files if pnpm_version
|
|
@@ -137,15 +138,35 @@ module Dependabot
|
|
|
137
138
|
fetched_lerna_files
|
|
138
139
|
end
|
|
139
140
|
|
|
140
|
-
#
|
|
141
|
-
#
|
|
141
|
+
# Generates or infers an .npmrc file for the project.
|
|
142
|
+
# Priority order:
|
|
143
|
+
# 1. If credentials have `scope` → generate from credentials (authoritative, overrides everything)
|
|
144
|
+
# 2. If no `scope` AND `.npmrc` in repo → return nil (committed file handled upstream)
|
|
145
|
+
# 3. If no `scope` AND no `.npmrc` → try lockfile inference (transitional)
|
|
146
|
+
# 4. If nothing works → return nil
|
|
142
147
|
|
|
143
148
|
# rubocop:disable Metrics/AbcSize
|
|
144
149
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
150
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
151
|
+
# rubocop:disable Metrics/MethodLength
|
|
145
152
|
sig { returns(T.nilable(DependencyFile)) }
|
|
146
153
|
def inferred_npmrc # rubocop:disable Metrics/PerceivedComplexity
|
|
147
154
|
return @inferred_npmrc if defined?(@inferred_npmrc)
|
|
148
|
-
|
|
155
|
+
|
|
156
|
+
if Dependabot::Experiments.enabled?(:enable_npmrc_credential_generation) && credentials_have_scope?
|
|
157
|
+
npmrc_from_credentials = generate_npmrc_from_credentials
|
|
158
|
+
if npmrc_from_credentials
|
|
159
|
+
Dependabot.logger.info("Generated .npmrc from credential scope configuration (overrides committed .npmrc)")
|
|
160
|
+
return @inferred_npmrc ||= T.let(npmrc_from_credentials, T.nilable(DependencyFile))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
unless npmrc.nil? && package_lock
|
|
165
|
+
# If .npmrc exists in the repo, it handles things — no rejection needed.
|
|
166
|
+
# If no .npmrc AND no lockfile, we can't infer, so check for rejection.
|
|
167
|
+
reject_if_private_registry_without_config! if npmrc.nil?
|
|
168
|
+
return @inferred_npmrc ||= T.let(nil, T.nilable(DependencyFile))
|
|
169
|
+
end
|
|
149
170
|
|
|
150
171
|
known_registries = []
|
|
151
172
|
FileParser::JsonLock.new(T.must(package_lock)).parsed.fetch(
|
|
@@ -157,9 +178,6 @@ module Dependabot
|
|
|
157
178
|
begin
|
|
158
179
|
uri = URI.parse(resolved)
|
|
159
180
|
rescue URI::InvalidURIError
|
|
160
|
-
# Ignoring non-URIs since they're not registries.
|
|
161
|
-
# This can happen if resolved is `false`, for instance
|
|
162
|
-
# npm6 bug https://github.com/npm/cli/issues/1138
|
|
163
181
|
next
|
|
164
182
|
end
|
|
165
183
|
|
|
@@ -187,11 +205,87 @@ module Dependabot
|
|
|
187
205
|
)
|
|
188
206
|
end
|
|
189
207
|
|
|
208
|
+
# Lockfile inference failed — fall back to replaces-base credential generation
|
|
209
|
+
if Dependabot::Experiments.enabled?(:enable_npmrc_credential_generation)
|
|
210
|
+
npmrc_from_credentials = generate_npmrc_from_credentials
|
|
211
|
+
if npmrc_from_credentials
|
|
212
|
+
Dependabot.logger.info("Generated .npmrc from credential replaces-base configuration")
|
|
213
|
+
return @inferred_npmrc ||= npmrc_from_credentials
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Phase 3: Reject updates when private registries exist but no config is resolvable
|
|
218
|
+
reject_if_private_registry_without_config!
|
|
219
|
+
|
|
190
220
|
@inferred_npmrc ||= nil
|
|
191
221
|
end
|
|
222
|
+
# rubocop:enable Metrics/MethodLength
|
|
223
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
192
224
|
# rubocop:enable Metrics/AbcSize
|
|
193
225
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
194
226
|
|
|
227
|
+
sig { returns(T.nilable(DependencyFile)) }
|
|
228
|
+
def generate_npmrc_from_credentials
|
|
229
|
+
content = NpmAndYarn::FileUpdater::NpmrcBuilder.npmrc_content_from_credentials(wrapped_credentials)
|
|
230
|
+
return unless content
|
|
231
|
+
|
|
232
|
+
Dependabot::DependencyFile.new(
|
|
233
|
+
name: ".npmrc",
|
|
234
|
+
content: content
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
sig { void }
|
|
239
|
+
def reject_if_private_registry_without_config!
|
|
240
|
+
return unless Dependabot::Experiments.enabled?(:enable_npmrc_credential_generation)
|
|
241
|
+
|
|
242
|
+
private_registry_creds = wrapped_credentials.select do |cred|
|
|
243
|
+
next false unless cred["type"] == "npm_registry"
|
|
244
|
+
|
|
245
|
+
registry = cred["registry"]
|
|
246
|
+
next false if registry.nil?
|
|
247
|
+
|
|
248
|
+
# Normalize: strip scheme to compare against CENTRAL_REGISTRIES (bare hostnames)
|
|
249
|
+
normalized = registry.sub(%r{^https?://}, "")
|
|
250
|
+
!NpmAndYarn::FileUpdater::NpmrcBuilder::CENTRAL_REGISTRIES.include?(normalized)
|
|
251
|
+
end
|
|
252
|
+
return if private_registry_creds.empty?
|
|
253
|
+
|
|
254
|
+
registry = private_registry_creds.first&.fetch("registry", nil) || "unknown"
|
|
255
|
+
raise Dependabot::PrivateRegistryConfigNotFound, registry
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
sig { returns(T::Boolean) }
|
|
259
|
+
def credentials_have_scope?
|
|
260
|
+
wrapped_credentials.any? { |cred| cred["type"] == "npm_registry" && cred.scope }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# file_fetcher_command.rb may pass raw Hashes as credentials at runtime.
|
|
264
|
+
# Wrap them in Credential objects so we can use .scope and .replaces_base? safely.
|
|
265
|
+
# Credential#initialize destructively removes "scope"/"replaces-base" keys, so we .dup first.
|
|
266
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
267
|
+
def wrapped_credentials
|
|
268
|
+
@wrapped_credentials ||= T.let(
|
|
269
|
+
credentials.map { |cred| ensure_credential(cred) },
|
|
270
|
+
T.nilable(T::Array[Dependabot::Credential])
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
sig do
|
|
275
|
+
params(cred: T.any(Dependabot::Credential, T::Hash[String, T.untyped]))
|
|
276
|
+
.returns(Dependabot::Credential)
|
|
277
|
+
end
|
|
278
|
+
def ensure_credential(cred)
|
|
279
|
+
return cred if cred.is_a?(Dependabot::Credential)
|
|
280
|
+
|
|
281
|
+
Dependabot::Credential.new(cred.dup)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
sig { returns(T::Boolean) }
|
|
285
|
+
def scope_overrides_npmrc?
|
|
286
|
+
Dependabot::Experiments.enabled?(:enable_npmrc_credential_generation) && credentials_have_scope?
|
|
287
|
+
end
|
|
288
|
+
|
|
195
289
|
sig { returns(T.nilable(T.any(Integer, String))) }
|
|
196
290
|
def npm_version
|
|
197
291
|
@npm_version ||= T.let(package_manager_helper.setup(NpmPackageManager::NAME), T.nilable(T.any(Integer, String)))
|
|
@@ -12,7 +12,7 @@ module Dependabot
|
|
|
12
12
|
# Build a .npmrc file from the lockfile content, credentials, and any
|
|
13
13
|
# committed .npmrc
|
|
14
14
|
# We should refactor this to use Package::RegistryFinder
|
|
15
|
-
class NpmrcBuilder
|
|
15
|
+
class NpmrcBuilder # rubocop:disable Metrics/ClassLength
|
|
16
16
|
extend T::Sig
|
|
17
17
|
|
|
18
18
|
CENTRAL_REGISTRIES = T.let(
|
|
@@ -38,9 +38,50 @@ module Dependabot
|
|
|
38
38
|
@dependencies = dependencies
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# Generates .npmrc content solely from credential scope/replaces-base properties,
|
|
42
|
+
# without lockfile inference or auth token lines. Used by FileFetcher as a fallback
|
|
43
|
+
# when lockfile inference fails.
|
|
44
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
45
|
+
sig { params(credentials: T::Array[Dependabot::Credential]).returns(T.nilable(String)) }
|
|
46
|
+
def self.npmrc_content_from_credentials(credentials)
|
|
47
|
+
registry_creds = credentials.select { |cred| cred.fetch("type") == "npm_registry" }
|
|
48
|
+
replaces_base_cred = registry_creds.find(&:replaces_base?)
|
|
49
|
+
scoped_credentials = registry_creds.select { |cred| cred.scope && cred["registry"] }
|
|
50
|
+
return if replaces_base_cred.nil? && scoped_credentials.empty?
|
|
51
|
+
|
|
52
|
+
lines = T.let([], T::Array[String])
|
|
53
|
+
|
|
54
|
+
if replaces_base_cred
|
|
55
|
+
registry = replaces_base_cred.fetch("registry")
|
|
56
|
+
registry_url = registry.start_with?("http") ? registry : "https://#{registry}"
|
|
57
|
+
lines << "registry=#{registry_url}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
scoped_credentials.each do |cred|
|
|
61
|
+
registry = cred.fetch("registry")
|
|
62
|
+
registry_url = registry.start_with?("http") ? registry : "https://#{registry}"
|
|
63
|
+
T.must(cred.scope).each do |s|
|
|
64
|
+
lines << "#{Helpers.normalize_npm_scope(s)}:registry=#{registry_url}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
lines.join("\n")
|
|
69
|
+
end
|
|
70
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
71
|
+
|
|
41
72
|
# PROXY WORK
|
|
73
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
42
74
|
sig { returns(String) }
|
|
43
75
|
def npmrc_content
|
|
76
|
+
# When credentials have explicit scope, always generate from credentials
|
|
77
|
+
# (overrides committed .npmrc and lockfile inference)
|
|
78
|
+
if credentials_have_scope?
|
|
79
|
+
Dependabot.logger.info(
|
|
80
|
+
"Generating .npmrc from credential scope configuration (committed .npmrc ignored)"
|
|
81
|
+
)
|
|
82
|
+
return build_npmrc_from_scope_credentials
|
|
83
|
+
end
|
|
84
|
+
|
|
44
85
|
initial_content =
|
|
45
86
|
if npmrc_file then complete_npmrc_from_credentials
|
|
46
87
|
elsif yarnrc_file then build_npmrc_from_yarnrc
|
|
@@ -60,6 +101,7 @@ module Dependabot
|
|
|
60
101
|
|
|
61
102
|
final_content
|
|
62
103
|
end
|
|
104
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
63
105
|
|
|
64
106
|
# PROXY WORK
|
|
65
107
|
# Yarn allows registries to be defined either in an .npmrc or .yarnrc
|
|
@@ -87,6 +129,30 @@ module Dependabot
|
|
|
87
129
|
sig { returns(T::Array[Dependabot::Dependency]) }
|
|
88
130
|
attr_reader :dependencies
|
|
89
131
|
|
|
132
|
+
sig { returns(T::Boolean) }
|
|
133
|
+
def credentials_have_scope?
|
|
134
|
+
registry_credentials.any?(&:scope)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
sig { returns(String) }
|
|
138
|
+
def build_npmrc_from_scope_credentials
|
|
139
|
+
content = T.must(NpmrcBuilder.npmrc_content_from_credentials(credentials))
|
|
140
|
+
|
|
141
|
+
# Append auth lines for all configured registries
|
|
142
|
+
lines = [content]
|
|
143
|
+
registry_credentials.each do |cred|
|
|
144
|
+
token = cred.fetch("token", nil)
|
|
145
|
+
next unless token
|
|
146
|
+
|
|
147
|
+
lines << auth_line(token, cred.fetch("registry"))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
replaces_base_cred = registry_credentials.find(&:replaces_base?)
|
|
151
|
+
lines << "always-auth = true" if replaces_base_cred
|
|
152
|
+
|
|
153
|
+
lines.reject(&:empty?).join("\n")
|
|
154
|
+
end
|
|
155
|
+
|
|
90
156
|
sig { returns(T.nilable(String)) }
|
|
91
157
|
def build_npmrc_content_from_lockfile
|
|
92
158
|
return unless yarn_lock || package_lock || shrinkwrap
|
|
@@ -101,19 +167,13 @@ module Dependabot
|
|
|
101
167
|
|
|
102
168
|
sig { returns(T.nilable(String)) }
|
|
103
169
|
def build_npmrc_content_from_credential_scopes
|
|
104
|
-
|
|
105
|
-
return
|
|
170
|
+
content = NpmrcBuilder.npmrc_content_from_credentials(credentials)
|
|
171
|
+
return unless content
|
|
106
172
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
registry = cred.fetch("registry")
|
|
110
|
-
registry_url = registry.start_with?("http") ? registry : "https://#{registry}"
|
|
111
|
-
T.must(cred.scope).each do |s|
|
|
112
|
-
lines << "#{Helpers.normalize_npm_scope(s)}:registry=#{registry_url}"
|
|
113
|
-
end
|
|
114
|
-
end
|
|
173
|
+
replaces_base_cred = registry_credentials.find(&:replaces_base?)
|
|
174
|
+
return content unless replaces_base_cred
|
|
115
175
|
|
|
116
|
-
|
|
176
|
+
"#{content}\nalways-auth = true"
|
|
117
177
|
end
|
|
118
178
|
|
|
119
179
|
sig { returns(T.nilable(String)) }
|
|
@@ -61,7 +61,7 @@ module Dependabot
|
|
|
61
61
|
def registry
|
|
62
62
|
return @registry if @registry
|
|
63
63
|
|
|
64
|
-
@registry =
|
|
64
|
+
@registry = scoped_credential_registry_for_dependency || configured_registry || locked_registry ||
|
|
65
65
|
first_registry_with_dependency_details
|
|
66
66
|
T.must(@registry)
|
|
67
67
|
end
|
|
@@ -226,7 +226,7 @@ module Dependabot
|
|
|
226
226
|
.select { |cred| cred["type"] == "npm_registry" && cred["registry"] }
|
|
227
227
|
.tap do |arr|
|
|
228
228
|
arr.each do |c|
|
|
229
|
-
c["registry"] = prepare_registry_url(c["registry"])
|
|
229
|
+
c["registry"] = prepare_registry_url(c["registry"])&.delete_suffix("/")
|
|
230
230
|
c["token"] ||= nil
|
|
231
231
|
end
|
|
232
232
|
end
|
|
@@ -27,6 +27,16 @@ module Dependabot
|
|
|
27
27
|
# Default npm registry - no need to set env vars for this
|
|
28
28
|
DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org"
|
|
29
29
|
|
|
30
|
+
# Canonicalise a registry URL: ensure an https:// scheme and strip any
|
|
31
|
+
# trailing slashes. Used wherever a registry URL is written into an env var
|
|
32
|
+
# so there is a single source of truth for URL normalisation.
|
|
33
|
+
sig { params(url: String).returns(String) }
|
|
34
|
+
def self.normalize_registry_url(url)
|
|
35
|
+
normalized = url.start_with?("http://", "https://") ? url.dup : "https://#{url}"
|
|
36
|
+
normalized.delete_suffix!("/") while normalized.end_with?("/")
|
|
37
|
+
normalized
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
sig do
|
|
31
41
|
params(
|
|
32
42
|
registry_config_files: T::Hash[Symbol, T.nilable(Dependabot::DependencyFile)],
|
|
@@ -44,11 +54,9 @@ module Dependabot
|
|
|
44
54
|
|
|
45
55
|
env_variables = {}
|
|
46
56
|
|
|
47
|
-
if registry_info[:registry]
|
|
48
|
-
registry =
|
|
49
|
-
registry = "https://#{T.must(registry)}" unless T.must(registry).start_with?("http://", "https://")
|
|
57
|
+
if (raw_registry = registry_info[:registry])
|
|
58
|
+
registry = RegistryHelper.normalize_registry_url(raw_registry)
|
|
50
59
|
|
|
51
|
-
# Set both in the env_variables hash
|
|
52
60
|
unless registry == DEFAULT_NPM_REGISTRY
|
|
53
61
|
env_variables[COREPACK_NPM_REGISTRY_ENV] = registry # For Corepack
|
|
54
62
|
env_variables[NPM_CONFIG_REGISTRY_ENV] = registry # For npm
|
|
@@ -11,6 +11,7 @@ require "sorbet-runtime"
|
|
|
11
11
|
require "dependabot/npm_and_yarn/requirement"
|
|
12
12
|
require "dependabot/npm_and_yarn/update_checker"
|
|
13
13
|
require "dependabot/npm_and_yarn/version"
|
|
14
|
+
require "dependabot/dependency_requirement"
|
|
14
15
|
require "dependabot/requirements_update_strategy"
|
|
15
16
|
|
|
16
17
|
module Dependabot
|
|
@@ -33,7 +34,7 @@ module Dependabot
|
|
|
33
34
|
|
|
34
35
|
sig do
|
|
35
36
|
params(
|
|
36
|
-
requirements: T::Array[
|
|
37
|
+
requirements: T::Array[Dependabot::DependencyRequirement],
|
|
37
38
|
updated_source: T.nilable(T::Hash[Symbol, T.untyped]),
|
|
38
39
|
update_strategy: Dependabot::RequirementsUpdateStrategy,
|
|
39
40
|
latest_resolvable_version: T.nilable(T.any(String, Gem::Version))
|
|
@@ -41,7 +42,10 @@ module Dependabot
|
|
|
41
42
|
.void
|
|
42
43
|
end
|
|
43
44
|
def initialize(requirements:, updated_source:, update_strategy:, latest_resolvable_version:)
|
|
44
|
-
@requirements =
|
|
45
|
+
@requirements = T.let(
|
|
46
|
+
requirements.map { |req| Dependabot::DependencyRequirement.create(req) },
|
|
47
|
+
T::Array[Dependabot::DependencyRequirement]
|
|
48
|
+
)
|
|
45
49
|
@updated_source = updated_source
|
|
46
50
|
@update_strategy = update_strategy
|
|
47
51
|
|
|
@@ -55,12 +59,12 @@ module Dependabot
|
|
|
55
59
|
)
|
|
56
60
|
end
|
|
57
61
|
|
|
58
|
-
sig { returns(T::Array[
|
|
62
|
+
sig { returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
59
63
|
def updated_requirements
|
|
60
64
|
return requirements if update_strategy.lockfile_only?
|
|
61
65
|
|
|
62
66
|
requirements.map do |req|
|
|
63
|
-
req = req.merge(source: updated_source)
|
|
67
|
+
req = Dependabot::DependencyRequirement.create(req.merge(source: updated_source))
|
|
64
68
|
next req unless latest_resolvable_version
|
|
65
69
|
next initial_req_after_source_change(req) unless req[:requirement]
|
|
66
70
|
next req if req[:requirement].sub(NpmAndYarn::Requirement::JSR_PREFIX, "")
|
|
@@ -78,7 +82,7 @@ module Dependabot
|
|
|
78
82
|
|
|
79
83
|
private
|
|
80
84
|
|
|
81
|
-
sig { returns(T::Array[
|
|
85
|
+
sig { returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
82
86
|
attr_reader :requirements
|
|
83
87
|
|
|
84
88
|
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
@@ -105,15 +109,15 @@ module Dependabot
|
|
|
105
109
|
original_source&.fetch(:type) == "git"
|
|
106
110
|
end
|
|
107
111
|
|
|
108
|
-
sig { params(req:
|
|
112
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
109
113
|
def initial_req_after_source_change(req)
|
|
110
114
|
return req unless updating_from_git_to_npm?
|
|
111
115
|
return req unless req[:requirement].nil?
|
|
112
116
|
|
|
113
|
-
req.merge(requirement: "^#{latest_resolvable_version}")
|
|
117
|
+
Dependabot::DependencyRequirement.create(req.merge(requirement: "^#{latest_resolvable_version}"))
|
|
114
118
|
end
|
|
115
119
|
|
|
116
|
-
sig { params(req:
|
|
120
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
117
121
|
def update_version_requirement(req)
|
|
118
122
|
current_requirement = req[:requirement]
|
|
119
123
|
|
|
@@ -122,14 +126,14 @@ module Dependabot
|
|
|
122
126
|
return req if ruby_req&.satisfied_by?(latest_resolvable_version)
|
|
123
127
|
|
|
124
128
|
updated_req = update_range_requirement(current_requirement)
|
|
125
|
-
return req.merge(requirement: updated_req)
|
|
129
|
+
return Dependabot::DependencyRequirement.create(req.merge(requirement: updated_req))
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
|
|
129
|
-
req.merge(requirement: update_version_string(reqs.first))
|
|
133
|
+
Dependabot::DependencyRequirement.create(req.merge(requirement: update_version_string(reqs.first)))
|
|
130
134
|
end
|
|
131
135
|
|
|
132
|
-
sig { params(req:
|
|
136
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
133
137
|
def update_version_requirement_if_needed(req)
|
|
134
138
|
current_requirement = req[:requirement]
|
|
135
139
|
version = latest_resolvable_version
|
|
@@ -141,7 +145,7 @@ module Dependabot
|
|
|
141
145
|
update_version_requirement(req)
|
|
142
146
|
end
|
|
143
147
|
|
|
144
|
-
sig { params(req:
|
|
148
|
+
sig { params(req: Dependabot::DependencyRequirement).returns(Dependabot::DependencyRequirement) }
|
|
145
149
|
def widen_requirement(req)
|
|
146
150
|
current_requirement = req[:requirement]
|
|
147
151
|
version = latest_resolvable_version
|
|
@@ -161,7 +165,7 @@ module Dependabot
|
|
|
161
165
|
current_requirement
|
|
162
166
|
end
|
|
163
167
|
|
|
164
|
-
req.merge(requirement: updated_requirement)
|
|
168
|
+
Dependabot::DependencyRequirement.create(req.merge(requirement: updated_requirement))
|
|
165
169
|
end
|
|
166
170
|
|
|
167
171
|
sig { params(requirement_string: String).returns(T::Array[NpmAndYarn::Requirement]) }
|
|
@@ -935,7 +935,7 @@ module Dependabot
|
|
|
935
935
|
registry_url = replaces_base_cred&.[]("registry")
|
|
936
936
|
return nil unless registry_url
|
|
937
937
|
|
|
938
|
-
registry_url =
|
|
938
|
+
registry_url = Dependabot::NpmAndYarn::RegistryHelper.normalize_registry_url(registry_url)
|
|
939
939
|
|
|
940
940
|
{ "npm_config_registry" => registry_url }
|
|
941
941
|
end
|
|
@@ -32,7 +32,6 @@ module Dependabot
|
|
|
32
32
|
@credentials = credentials
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# rubocop:disable Metrics/MethodLength
|
|
36
35
|
# Finds any dependencies in the `package-lock.json` or `npm-shrinkwrap.json` that have
|
|
37
36
|
# a subdependency on the given dependency that is locked to a vuln version range.
|
|
38
37
|
#
|
|
@@ -63,69 +62,122 @@ module Dependabot
|
|
|
63
62
|
end
|
|
64
63
|
def audit(dependency:, security_advisories:)
|
|
65
64
|
Dependabot.logger.info("VulnerabilityAuditor: starting audit")
|
|
65
|
+
fix_unavailable = fix_unavailable_response(dependency)
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
run_audit(
|
|
68
|
+
dependency: dependency,
|
|
69
|
+
security_advisories: security_advisories,
|
|
70
|
+
fix_unavailable: fix_unavailable
|
|
71
|
+
)
|
|
72
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
73
|
+
log_helper_subprocess_failure(dependency, e)
|
|
74
|
+
T.must(fix_unavailable)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
80
|
+
attr_reader :dependency_files
|
|
81
|
+
|
|
82
|
+
sig { returns(T::Array[Dependabot::Credential]) }
|
|
83
|
+
attr_reader :credentials
|
|
84
|
+
|
|
85
|
+
sig { params(dependency: Dependabot::Dependency).returns(T::Hash[String, T.untyped]) }
|
|
86
|
+
def fix_unavailable_response(dependency)
|
|
87
|
+
{
|
|
68
88
|
"dependency_name" => dependency.name,
|
|
69
89
|
"fix_available" => false,
|
|
70
90
|
"fix_updates" => [],
|
|
71
91
|
"top_level_ancestors" => []
|
|
72
92
|
}
|
|
93
|
+
end
|
|
73
94
|
|
|
95
|
+
sig do
|
|
96
|
+
params(
|
|
97
|
+
dependency: Dependabot::Dependency,
|
|
98
|
+
security_advisories: T::Array[Dependabot::SecurityAdvisory],
|
|
99
|
+
fix_unavailable: T::Hash[String, T.untyped]
|
|
100
|
+
).returns(T::Hash[String, T.untyped])
|
|
101
|
+
end
|
|
102
|
+
def run_audit(dependency:, security_advisories:, fix_unavailable:)
|
|
74
103
|
SharedHelpers.in_a_temporary_directory do
|
|
75
|
-
dependency_files_builder =
|
|
104
|
+
dependency_files_builder = prepare_dependency_files(dependency)
|
|
105
|
+
return fix_unavailable unless lockfile_present?(dependency_files_builder)
|
|
106
|
+
|
|
107
|
+
audit_result = run_vulnerability_auditor(security_advisories)
|
|
108
|
+
handle_audit_result(
|
|
76
109
|
dependency: dependency,
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
security_advisories: security_advisories,
|
|
111
|
+
audit_result: audit_result,
|
|
112
|
+
fix_unavailable: fix_unavailable
|
|
79
113
|
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# `npm-shrinkwrap.js`, if present, takes precedence over `package-lock.js`.
|
|
83
|
-
# Both files use the same format. See https://bit.ly/3lDIAJV for more.
|
|
84
|
-
lockfile = (dependency_files_builder.shrinkwraps + dependency_files_builder.package_locks).first
|
|
85
|
-
unless lockfile
|
|
86
|
-
Dependabot.logger.info("VulnerabilityAuditor: missing lockfile")
|
|
87
|
-
return fix_unavailable
|
|
88
|
-
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
89
116
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
sig { params(dependency: Dependabot::Dependency).returns(DependencyFilesBuilder) }
|
|
118
|
+
def prepare_dependency_files(dependency)
|
|
119
|
+
dependency_files_builder = DependencyFilesBuilder.new(
|
|
120
|
+
dependency: dependency,
|
|
121
|
+
dependency_files: dependency_files,
|
|
122
|
+
credentials: credentials
|
|
123
|
+
)
|
|
124
|
+
dependency_files_builder.write_temporary_dependency_files
|
|
125
|
+
dependency_files_builder
|
|
126
|
+
end
|
|
96
127
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
T::Hash[String, T.untyped]
|
|
104
|
-
)
|
|
128
|
+
sig { params(dependency_files_builder: DependencyFilesBuilder).returns(T::Boolean) }
|
|
129
|
+
def lockfile_present?(dependency_files_builder)
|
|
130
|
+
# `npm-shrinkwrap.js`, if present, takes precedence over `package-lock.js`.
|
|
131
|
+
# Both files use the same format. See https://bit.ly/3lDIAJV for more.
|
|
132
|
+
lockfile = (dependency_files_builder.shrinkwraps + dependency_files_builder.package_locks).first
|
|
133
|
+
return true if lockfile
|
|
105
134
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
fix_unavailable["explanation"] = explain_fix_unavailable(validation_result, dependency, audit_result)
|
|
110
|
-
return fix_unavailable
|
|
111
|
-
end
|
|
135
|
+
Dependabot.logger.info("VulnerabilityAuditor: missing lockfile")
|
|
136
|
+
false
|
|
137
|
+
end
|
|
112
138
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
end
|
|
116
|
-
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
117
|
-
log_helper_subprocess_failure(dependency, e)
|
|
118
|
-
T.must(fix_unavailable)
|
|
139
|
+
sig do
|
|
140
|
+
params(security_advisories: T::Array[Dependabot::SecurityAdvisory]).returns(T::Hash[String, T.untyped])
|
|
119
141
|
end
|
|
120
|
-
|
|
142
|
+
def run_vulnerability_auditor(security_advisories)
|
|
143
|
+
vuln_versions = security_advisories.map do |advisory|
|
|
144
|
+
{
|
|
145
|
+
dependency_name: advisory.dependency_name,
|
|
146
|
+
affected_versions: advisory.vulnerable_version_strings
|
|
147
|
+
}
|
|
148
|
+
end
|
|
121
149
|
|
|
122
|
-
|
|
150
|
+
T.cast(
|
|
151
|
+
SharedHelpers.run_helper_subprocess(
|
|
152
|
+
command: NativeHelpers.helper_path,
|
|
153
|
+
function: "npm:vulnerabilityAuditor",
|
|
154
|
+
args: [Dir.pwd, vuln_versions]
|
|
155
|
+
),
|
|
156
|
+
T::Hash[String, T.untyped]
|
|
157
|
+
)
|
|
158
|
+
end
|
|
123
159
|
|
|
124
|
-
sig
|
|
125
|
-
|
|
160
|
+
sig do
|
|
161
|
+
params(
|
|
162
|
+
dependency: Dependabot::Dependency,
|
|
163
|
+
security_advisories: T::Array[Dependabot::SecurityAdvisory],
|
|
164
|
+
audit_result: T::Hash[String, T.untyped],
|
|
165
|
+
fix_unavailable: T::Hash[String, T.untyped]
|
|
166
|
+
).returns(T::Hash[String, T.untyped])
|
|
167
|
+
end
|
|
168
|
+
def handle_audit_result(dependency:, security_advisories:, audit_result:, fix_unavailable:)
|
|
169
|
+
validation_result = validate_audit_result(audit_result, security_advisories)
|
|
170
|
+
if validation_result == :viable
|
|
171
|
+
Dependabot.logger.info("VulnerabilityAuditor: audit result viable")
|
|
172
|
+
return audit_result
|
|
173
|
+
end
|
|
126
174
|
|
|
127
|
-
|
|
128
|
-
|
|
175
|
+
Dependabot.logger.info("VulnerabilityAuditor: audit result not viable: #{validation_result}")
|
|
176
|
+
fix_unavailable["explanation"] = explain_fix_unavailable(validation_result, dependency, audit_result)
|
|
177
|
+
blocking_dependencies = audit_result["blocking_dependencies"]
|
|
178
|
+
fix_unavailable["blocking_dependencies"] = blocking_dependencies if blocking_dependencies
|
|
179
|
+
fix_unavailable
|
|
180
|
+
end
|
|
129
181
|
|
|
130
182
|
sig do
|
|
131
183
|
params(
|
|
@@ -137,7 +189,7 @@ module Dependabot
|
|
|
137
189
|
def explain_fix_unavailable(validation_result, dependency, audit_result)
|
|
138
190
|
case validation_result
|
|
139
191
|
when :fix_unavailable
|
|
140
|
-
fix_unavailable_message(dependency)
|
|
192
|
+
fix_unavailable_message(dependency, audit_result)
|
|
141
193
|
when :dependency_still_vulnerable
|
|
142
194
|
dependency_still_vulnerable_message(dependency, audit_result)
|
|
143
195
|
when :downgrades_dependencies
|
|
@@ -209,10 +261,43 @@ module Dependabot
|
|
|
209
261
|
current > target
|
|
210
262
|
end
|
|
211
263
|
|
|
264
|
+
sig do
|
|
265
|
+
params(
|
|
266
|
+
dependency: Dependabot::Dependency,
|
|
267
|
+
audit_result: T::Hash[String, T.untyped]
|
|
268
|
+
).returns(String)
|
|
269
|
+
end
|
|
270
|
+
def fix_unavailable_message(dependency, audit_result)
|
|
271
|
+
blockers = audit_result["blocking_dependencies"]
|
|
272
|
+
return generic_fix_unavailable_message(dependency) if blockers.nil? || blockers.empty?
|
|
273
|
+
|
|
274
|
+
conflicts = blockers.map do |blocker|
|
|
275
|
+
blocker_name = blocker["name"]
|
|
276
|
+
blocker_version = blocker["version"]
|
|
277
|
+
blocker_requirement = blocker["requirement"]
|
|
278
|
+
detail = "#{blocker_name}@#{blocker_version} requires " \
|
|
279
|
+
"#{dependency.name}@#{blocker_requirement}"
|
|
280
|
+
ancestor = blocker["top_level_ancestor"]
|
|
281
|
+
|
|
282
|
+
detail += " (pulled in via #{ancestor})" if ancestor && ancestor != blocker_name
|
|
283
|
+
detail
|
|
284
|
+
end.join("; ")
|
|
285
|
+
|
|
286
|
+
"#{dependency.name} can't be updated to a non-vulnerable version because the npm helper identified parent " \
|
|
287
|
+
"package constraints in the dependency tree that still require a vulnerable version: #{conflicts}. To " \
|
|
288
|
+
"resolve this, update the parent " \
|
|
289
|
+
"package(s) listed above to a release that allows a non-vulnerable #{dependency.name}, or add an " \
|
|
290
|
+
"override/resolution pinning #{dependency.name} to a non-vulnerable version."
|
|
291
|
+
end
|
|
292
|
+
|
|
212
293
|
sig { params(dependency: Dependabot::Dependency).returns(String) }
|
|
213
|
-
def
|
|
214
|
-
"
|
|
215
|
-
"
|
|
294
|
+
def generic_fix_unavailable_message(dependency)
|
|
295
|
+
"#{dependency.name} is a transitive dependency and no update path was found that " \
|
|
296
|
+
"resolves it to a non-vulnerable version. A fixed version of #{dependency.name} is published, but the " \
|
|
297
|
+
"parent package(s) that depend on it in the lockfile require a vulnerable version, and Dependabot could " \
|
|
298
|
+
"not update those parent package(s) to a release that allows the fixed version without introducing a " \
|
|
299
|
+
"conflict. To resolve this, update the parent dependency that requires #{dependency.name}, or add an " \
|
|
300
|
+
"override/resolution pinning #{dependency.name} to a non-vulnerable version."
|
|
216
301
|
end
|
|
217
302
|
|
|
218
303
|
sig do
|
|
@@ -177,14 +177,12 @@ module Dependabot
|
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
@updated_requirements ||=
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
).updated_requirements
|
|
187
|
-
)
|
|
180
|
+
RequirementsUpdater.new(
|
|
181
|
+
requirements: dependency.requirements,
|
|
182
|
+
updated_source: updated_source,
|
|
183
|
+
latest_resolvable_version: resolvable_version,
|
|
184
|
+
update_strategy: T.must(requirements_update_strategy)
|
|
185
|
+
).updated_requirements
|
|
188
186
|
end
|
|
189
187
|
|
|
190
188
|
sig { returns(T::Boolean) }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-npm_and_yarn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.383.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.383.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.383.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -275,6 +275,8 @@ files:
|
|
|
275
275
|
- helpers/test/npm/fixtures/vulnerability-auditor/fix-hoists-package/package.json
|
|
276
276
|
- helpers/test/npm/fixtures/vulnerability-auditor/fix-removes-package/package-lock.json
|
|
277
277
|
- helpers/test/npm/fixtures/vulnerability-auditor/fix-removes-package/package.json
|
|
278
|
+
- helpers/test/npm/fixtures/vulnerability-auditor/locked-by-parent/package-lock.json
|
|
279
|
+
- helpers/test/npm/fixtures/vulnerability-auditor/locked-by-parent/package.json
|
|
278
280
|
- helpers/test/npm/fixtures/vulnerability-auditor/outdated-package-lock/package-lock.json
|
|
279
281
|
- helpers/test/npm/fixtures/vulnerability-auditor/outdated-package-lock/package.json
|
|
280
282
|
- helpers/test/npm/fixtures/vulnerability-auditor/simple/package-lock.json
|
|
@@ -375,7 +377,7 @@ licenses:
|
|
|
375
377
|
- MIT
|
|
376
378
|
metadata:
|
|
377
379
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
378
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
380
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.383.0
|
|
379
381
|
rdoc_options: []
|
|
380
382
|
require_paths:
|
|
381
383
|
- lib
|