dependabot-nuget 0.271.0 → 0.272.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f17779ebeb91c554c3642c6e83a28f9f2ee7eb6c1677d35b089688bfc6fa637
4
- data.tar.gz: d6e33cbab0d4429218df7e0184c9c18bceab5021fc532de0753a216c8ebc80d7
3
+ metadata.gz: b33f6b0a817def66d738b6d998bedf0248881c225ddf6b97fa0a5405d92b9d23
4
+ data.tar.gz: 9e736306b9d21e95639627ea2fe585d2e47a8979e1096545b4da2ed49e8dbd5f
5
5
  SHA512:
6
- metadata.gz: 8c07ed738181748492e32a54dc8181256956174e23fdf645e121cb4f451bb20840c336a116d48f3c5db78b6de192efc991ff3c4ab19f097e1634f9e70fb001f6
7
- data.tar.gz: 885a7af5bd7bcdbbd34931a735b28f2fb929cfb63bd1bfb09ab048dfd6d25ba3f6d905303f824693834cc1172ca23996675ef079a2e3d84bb1ce941719025b44
6
+ metadata.gz: 4dddbe1b9c9cbf4fb9d7876c143cfbd713bf2f6ca501210ae40a7c37bb2076687ea75996042e9ccc6582a6cd535a24eb3b2caa3143b5e8c3ef8ff500455f7827
7
+ data.tar.gz: f51748be257babb63de285cec8981ca84e580fa48b6d6fec2a89892f485f46ae237af86ab387a584c27c8f207ba1ae16167570bffdc59cf3cd4bb64ac27e9830
@@ -108,6 +108,7 @@ public partial class AnalyzeWorker
108
108
  discovery,
109
109
  dependenciesToUpdate,
110
110
  updatedVersion,
111
+ dependencyInfo,
111
112
  nugetContext,
112
113
  _logger,
113
114
  CancellationToken.None);
@@ -359,6 +360,7 @@ public partial class AnalyzeWorker
359
360
  WorkspaceDiscoveryResult discovery,
360
361
  ImmutableHashSet<string> packageIds,
361
362
  NuGetVersion updatedVersion,
363
+ DependencyInfo dependencyInfo,
362
364
  NuGetContext nugetContext,
363
365
  Logger logger,
364
366
  CancellationToken cancellationToken)
@@ -379,10 +381,23 @@ public partial class AnalyzeWorker
379
381
  .Select(NuGetFramework.Parse)
380
382
  .ToImmutableArray();
381
383
 
382
- // When updating peer dependencies, we only need to consider top-level dependencies.
383
- var projectDependencyNames = projectsWithDependency
384
- .SelectMany(p => p.Dependencies)
385
- .Where(d => !d.IsTransitive)
384
+ // When updating dependencies, we only need to consider top-level dependencies _UNLESS_ it's specifically vulnerable
385
+ var relevantDependencies = projectsWithDependency.SelectMany(p => p.Dependencies)
386
+ .Where(d =>
387
+ {
388
+ if (string.Compare(d.Name, dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) == 0 &&
389
+ dependencyInfo.IsVulnerable)
390
+ {
391
+ // if this dependency is one we're specifically updating _and_ if it's vulnerable, always update it
392
+ return true;
393
+ }
394
+ else
395
+ {
396
+ // otherwise only update if it's a top-level dependency
397
+ return !d.IsTransitive;
398
+ }
399
+ });
400
+ var projectDependencyNames = relevantDependencies
386
401
  .Select(d => d.Name)
387
402
  .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
388
403
 
@@ -199,13 +199,21 @@ public partial class DiscoveryWorker
199
199
  }
200
200
  else
201
201
  {
202
- // .csproj, .fsproj, .vbproj
203
- // keep this project and check for references
204
- expandedProjects.Add(candidateEntryPoint);
205
- IEnumerable<string> referencedProjects = ExpandItemGroupFilesFromProject(candidateEntryPoint, "ProjectReference");
206
- foreach (string referencedProject in referencedProjects)
202
+ switch (extension)
207
203
  {
208
- filesToExpand.Push(referencedProject);
204
+ case ".csproj":
205
+ case ".fsproj":
206
+ case ".vbproj":
207
+ // keep this project and check for references
208
+ expandedProjects.Add(candidateEntryPoint);
209
+ IEnumerable<string> referencedProjects = ExpandItemGroupFilesFromProject(candidateEntryPoint, "ProjectReference");
210
+ foreach (string referencedProject in referencedProjects)
211
+ {
212
+ filesToExpand.Push(referencedProject);
213
+ }
214
+ break;
215
+ default:
216
+ continue;
209
217
  }
210
218
  }
211
219
  }
@@ -169,7 +169,7 @@ internal static partial class MSBuildHelper
169
169
  string.Equals(property.Condition, $"'$({property.Name})' == ''", StringComparison.OrdinalIgnoreCase);
170
170
  if (hasEmptyCondition || conditionIsCheckingForEmptyString)
171
171
  {
172
- properties[property.Name] = new(property.Name, property.Value, buildFile.RelativePath);
172
+ properties[property.Name] = new(property.Name, property.Value, PathHelper.NormalizePathToUnix(buildFile.RelativePath));
173
173
  }
174
174
  }
175
175
  }
@@ -347,6 +347,57 @@ public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase
347
347
  );
348
348
  }
349
349
 
350
+ [Fact]
351
+ public async Task AnalyzeVulnerableTransitiveDependencies()
352
+ {
353
+ await TestAnalyzeAsync(
354
+ packages:
355
+ [
356
+ MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "1.0.0", "net8.0"),
357
+ MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "1.0.1", "net8.0"),
358
+ ],
359
+ discovery: new()
360
+ {
361
+ Path = "/",
362
+ Projects = [
363
+ new()
364
+ {
365
+ FilePath = "project.csproj",
366
+ TargetFrameworks = ["net8.0"],
367
+ Dependencies = [
368
+ new("Some.Transitive.Dependency", "1.0.0", DependencyType.Unknown, TargetFrameworks: ["net8.0"], IsTransitive: true),
369
+ ]
370
+ }
371
+ ]
372
+ },
373
+ dependencyInfo: new()
374
+ {
375
+ Name = "Some.Transitive.Dependency",
376
+ Version = "1.0.0",
377
+ IsVulnerable = true,
378
+ IgnoredVersions = [],
379
+ Vulnerabilities = [
380
+ new()
381
+ {
382
+ DependencyName = "Some.Transitive.Dependency",
383
+ PackageManager = "nuget",
384
+ VulnerableVersions = [Requirement.Parse("<= 1.0.0")],
385
+ SafeVersions = [Requirement.Parse("= 1.0.1")],
386
+ }
387
+ ]
388
+ },
389
+ expectedResult: new()
390
+ {
391
+ UpdatedVersion = "1.0.1",
392
+ CanUpdate = true,
393
+ VersionComesFromMultiDependencyProperty = false,
394
+ UpdatedDependencies = [
395
+ new("Some.Transitive.Dependency", "1.0.1", DependencyType.Unknown, TargetFrameworks: ["net8.0"]),
396
+ ],
397
+ }
398
+ );
399
+ }
400
+
350
401
  [Fact]
351
402
  public async Task IgnoredVersionsCanHandleWildcardSpecification()
352
403
  {
@@ -375,6 +375,89 @@ public partial class DiscoveryWorkerTests : DiscoveryWorkerTestBase
375
375
  );
376
376
  }
377
377
 
378
+ [Fact]
379
+ public async Task NonSupportedProjectExtensionsAreSkipped()
380
+ {
381
+ await TestDiscoveryAsync(
382
+ packages:
383
+ [
384
+ MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0"),
385
+ ],
386
+ workspacePath: "/",
387
+ files: new[]
388
+ {
389
+ ("solution.sln", """
390
+ Microsoft Visual Studio Solution File, Format Version 12.00
391
+ # Visual Studio Version 17
392
+ VisualStudioVersion = 17.10.35027.167
393
+ MinimumVisualStudioVersion = 10.0.40219.1
394
+ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "supported", "src\supported.csproj", "{4A3B8D8A-A585-4593-8AF3-DED05AE3C40F}"
395
+ EndProject
396
+ Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "unsupported", "src\unsupported.vdproj", "{271E533C-8A44-4572-8C18-CD65A79F8658}"
397
+ EndProject
398
+ Global
399
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
400
+ Debug|Any CPU = Debug|Any CPU
401
+ Release|Any CPU = Release|Any CPU
402
+ EndGlobalSection
403
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
404
+ {4A3B8D8A-A585-4593-8AF3-DED05AE3C40F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
405
+ {4A3B8D8A-A585-4593-8AF3-DED05AE3C40F}.Debug|Any CPU.Build.0 = Debug|Any CPU
406
+ {4A3B8D8A-A585-4593-8AF3-DED05AE3C40F}.Release|Any CPU.ActiveCfg = Release|Any CPU
407
+ {4A3B8D8A-A585-4593-8AF3-DED05AE3C40F}.Release|Any CPU.Build.0 = Release|Any CPU
408
+ {271E533C-8A44-4572-8C18-CD65A79F8658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
409
+ {271E533C-8A44-4572-8C18-CD65A79F8658}.Debug|Any CPU.Build.0 = Debug|Any CPU
410
+ {271E533C-8A44-4572-8C18-CD65A79F8658}.Release|Any CPU.ActiveCfg = Release|Any CPU
411
+ {271E533C-8A44-4572-8C18-CD65A79F8658}.Release|Any CPU.Build.0 = Release|Any CPU
412
+ EndGlobalSection
413
+ GlobalSection(SolutionProperties) = preSolution
414
+ HideSolutionNode = FALSE
415
+ EndGlobalSection
416
+ GlobalSection(ExtensibilityGlobals) = postSolution
417
+ SolutionGuid = {EE5BDEF7-1D4D-4773-9659-FC4A3846CD6D}
418
+ EndGlobalSection
419
+ EndGlobal
420
+ """),
421
+ ("src/supported.csproj", """
422
+ <Project Sdk="Microsoft.NET.Sdk">
423
+ <PropertyGroup>
424
+ <TargetFramework>net8.0</TargetFramework>
425
+ </PropertyGroup>
426
+ <ItemGroup>
427
+ <PackageReference Include="Some.Package" Version="1.0.0" />
428
+ </ItemGroup>
429
+ </Project>
430
+ """),
431
+ ("src/unsupported.vdproj", """
432
+ "DeployProject"
433
+ {
434
+ "SomeKey" = "SomeValue"
435
+ }
436
+ """),
437
+ },
438
+ expectedResult: new()
439
+ {
440
+ Path = "",
441
+ Projects = [
442
+ new()
443
+ {
444
+ FilePath = "src/supported.csproj",
445
+ TargetFrameworks = ["net8.0"],
446
+ ReferencedProjectPaths = [],
447
+ ExpectedDependencyCount = 2,
448
+ Dependencies = [
449
+ new("Microsoft.NET.Sdk", null, DependencyType.MSBuildSdk),
450
+ new("Some.Package", "1.0.0", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true)
451
+ ],
452
+ Properties = [
453
+ new("TargetFramework", "net8.0", @"src/supported.csproj"),
454
+ ]
455
+ }
456
+ ]
457
+ }
458
+ );
459
+ }
460
+
378
461
  [Fact]
379
462
  public async Task ResultFileHasCorrectShapeForAuthenticationFailure()
380
463
  {
@@ -50,7 +50,10 @@ module Dependabot
50
50
  # cache discovery results
51
51
  NativeDiscoveryJsonReader.set_discovery_from_dependency_files(dependency_files: dependency_files,
52
52
  discovery: discovery_json_reader)
53
- discovery_json_reader.dependency_set.dependencies
53
+ # we only return top-level dependencies and requirements here
54
+ dependency_set = discovery_json_reader.dependency_set(dependency_files: dependency_files,
55
+ top_level_only: true)
56
+ dependency_set.dependencies
54
57
  end
55
58
 
56
59
  T.must(self.class.file_dependency_cache[key])
@@ -16,35 +16,32 @@ module Dependabot
16
16
  class FileUpdater < Dependabot::FileUpdaters::Base
17
17
  extend T::Sig
18
18
 
19
- sig { override.params(allowlist_enabled: T::Boolean).returns(T::Array[Regexp]) }
20
- def self.updated_files_regex(allowlist_enabled = false)
21
- if allowlist_enabled
22
- [
23
- /^.*\.([a-z]{2})?proj$/,
24
- /^packages\.config$/i,
25
- /^app\.config$/i,
26
- /^web\.config$/i,
27
- /^global\.json$/i,
28
- /^dotnet-tools\.json$/i,
29
- /^Directory\.Build\.props$/i,
30
- /^Directory\.Build\.targets$/i,
31
- /^Packages\.props$/i
32
- ]
33
- else
34
- # Old regex. After 100% rollout of the allowlist, this will be removed.
35
- [
36
- %r{^[^/]*\.([a-z]{2})?proj$},
37
- /^.*\.([a-z]{2})?proj$/,
38
- /^packages\.config$/i,
39
- /^app\.config$/i,
40
- /^web\.config$/i,
41
- /^global\.json$/i,
42
- /^dotnet-tools\.json$/i,
43
- /^Directory\.Build\.props$/i,
44
- /^Directory\.Build\.targets$/i,
45
- /^Packages\.props$/i
46
- ]
47
- end
19
+ DependencyDetails = T.type_alias do
20
+ {
21
+ file: String,
22
+ name: String,
23
+ version: String,
24
+ previous_version: String,
25
+ is_transitive: T::Boolean
26
+ }
27
+ end
28
+
29
+ sig { override.returns(T::Array[Regexp]) }
30
+ def self.updated_files_regex
31
+ [
32
+ /.*\.([a-z]{2})?proj$/, # Matches files with any extension like .csproj, .vbproj, etc., in any directory
33
+ /packages\.config$/i, # Matches packages.config in any directory
34
+ /app\.config$/i, # Matches app.config in any directory
35
+ /web\.config$/i, # Matches web.config in any directory
36
+ /global\.json$/i, # Matches global.json in any directory
37
+ /dotnet-tools\.json$/i, # Matches dotnet-tools.json in any directory
38
+ /Directory\.Build\.props$/i, # Matches Directory.Build.props in any directory
39
+ /Directory\.Build\.targets$/i, # Matches Directory.Build.targets in any directory
40
+ /Directory\.targets$/i, # Matches Directory.targets in any directory or root directory
41
+ /Packages\.props$/i, # Matches Packages.props in any directory
42
+ /.*\.nuspec$/, # Matches any .nuspec files in any directory
43
+ %r{^\.config/dotnet-tools\.json$} # Matches .config/dotnet-tools.json in only root directory
44
+ ]
48
45
  end
49
46
 
50
47
  sig { params(original_content: T.nilable(String), updated_content: String).returns(T::Boolean) }
@@ -65,9 +62,21 @@ module Dependabot
65
62
  def updated_dependency_files
66
63
  base_dir = "/"
67
64
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
68
- dependencies.each do |dependency|
69
- try_update_projects(dependency) || try_update_json(dependency)
65
+ expanded_dependency_details.each do |dep_details|
66
+ file = T.let(dep_details.fetch(:file), String)
67
+ name = T.let(dep_details.fetch(:name), String)
68
+ version = T.let(dep_details.fetch(:version), String)
69
+ previous_version = T.let(dep_details.fetch(:previous_version), String)
70
+ is_transitive = T.let(dep_details.fetch(:is_transitive), T::Boolean)
71
+ NativeHelpers.run_nuget_updater_tool(repo_root: T.must(repo_contents_path),
72
+ proj_path: file,
73
+ dependency_name: name,
74
+ version: version,
75
+ previous_version: previous_version,
76
+ is_transitive: is_transitive,
77
+ credentials: credentials)
70
78
  end
79
+
71
80
  updated_files = dependency_files.filter_map do |f|
72
81
  updated_content = File.read(dependency_file_path(f))
73
82
  next if updated_content == f.content
@@ -87,104 +96,69 @@ module Dependabot
87
96
 
88
97
  private
89
98
 
90
- sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
91
- def try_update_projects(dependency)
92
- update_ran = T.let(false, T::Boolean)
93
- checked_files = Set.new
94
-
95
- # run update for each project file
96
- project_files.each do |project_file|
97
- project_dependencies = project_dependencies(project_file)
98
- proj_path = dependency_file_path(project_file)
99
-
100
- next unless project_dependencies.any? { |dep| dep.name.casecmp?(dependency.name) }
101
-
102
- next unless repo_contents_path
103
-
104
- checked_key = "#{project_file.name}-#{dependency.name}#{dependency.version}"
105
- call_nuget_updater_tool(dependency, proj_path) unless checked_files.include?(checked_key)
106
-
107
- checked_files.add(checked_key)
108
- # We need to check the downstream references even though we're already evaluated the file
109
- downstream_files = referenced_project_paths(project_file)
110
- downstream_files.each do |downstream_file|
111
- checked_files.add("#{downstream_file}-#{dependency.name}#{dependency.version}")
99
+ # rubocop:disable Metrics/AbcSize
100
+ sig { returns(T::Array[DependencyDetails]) }
101
+ def expanded_dependency_details
102
+ discovery_json_reader = NativeDiscoveryJsonReader.get_discovery_from_dependency_files(dependency_files)
103
+ dependency_set = discovery_json_reader.dependency_set(dependency_files: dependency_files, top_level_only: false)
104
+ all_dependencies = dependency_set.dependencies
105
+ dependencies.map do |dep|
106
+ # if vulnerable metadata is set, re-fetch all requirements from discovery
107
+ is_vulnerable = T.let(dep.metadata.fetch(:is_vulnerable, false), T::Boolean)
108
+ relevant_dependencies = all_dependencies.filter { |d| d.name.casecmp?(dep.name) }
109
+ candidate_vulnerable_dependency = T.must(relevant_dependencies.first)
110
+ relevant_dependency = is_vulnerable ? candidate_vulnerable_dependency : dep
111
+ relevant_details = relevant_dependency.requirements.filter_map do |req|
112
+ dependency_details_from_requirement(dep.name, req, is_vulnerable: is_vulnerable)
112
113
  end
113
- update_ran = true
114
- end
115
- update_ran
116
- end
117
-
118
- sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
119
- def try_update_json(dependency)
120
- if dotnet_tools_json_dependencies.any? { |dep| dep.name.casecmp?(dependency.name) } ||
121
- global_json_dependencies.any? { |dep| dep.name.casecmp?(dependency.name) }
122
-
123
- # We just need to feed the updater a project file, grab the first
124
- project_file = T.must(project_files.first)
125
- proj_path = dependency_file_path(project_file)
126
-
127
- return false unless repo_contents_path
128
-
129
- call_nuget_updater_tool(dependency, proj_path)
130
- return true
131
- end
132
114
 
133
- false
134
- end
135
-
136
- sig { params(dependency: Dependency, proj_path: String).void }
137
- def call_nuget_updater_tool(dependency, proj_path)
138
- NativeHelpers.run_nuget_updater_tool(repo_root: T.must(repo_contents_path), proj_path: proj_path,
139
- dependency: dependency, is_transitive: !dependency.top_level?,
140
- credentials: credentials)
141
-
142
- # Tests need to track how many times we call the tooling updater to ensure we don't recurse needlessly
143
- # Ideally we should find a way to not run this code in prod
144
- # (or a better way to track calls made to NativeHelpers)
145
- @update_tooling_calls ||= T.let({}, T.nilable(T::Hash[String, Integer]))
146
- key = "#{proj_path.delete_prefix(T.must(repo_contents_path))}+#{dependency.name}"
147
- @update_tooling_calls[key] =
148
- if @update_tooling_calls[key]
149
- T.must(@update_tooling_calls[key]) + 1
150
- else
151
- 1
115
+ next relevant_details if relevant_details.any?
116
+
117
+ # If we didn't find anything to update, we're in a very specific corner case: we were explicitly asked to
118
+ # (1) update a certain dependency, (2) it wasn't listed as a security update, but (3) it only exists as a
119
+ # transitive dependency. In this case, we need to rebuild the dependency requirements as if this were a
120
+ # security update so that we can perform the appropriate update.
121
+ candidate_vulnerable_dependency.requirements.filter_map do |req|
122
+ rebuilt_req = {
123
+ file: req[:file], # simple copy
124
+ requirement: relevant_dependency.version, # the newly available version
125
+ metadata: {
126
+ is_transitive: T.let(req[:metadata], T::Hash[Symbol, T.untyped])[:is_transitive], # simple copy
127
+ previous_requirement: req[:requirement] # the old requirement's "current" version is now the "previous"
128
+ }
129
+ }
130
+ dependency_details_from_requirement(dep.name, rebuilt_req, is_vulnerable: true)
152
131
  end
153
- end
154
-
155
- # Don't call this from outside tests, we're only checking that we aren't recursing needlessly
156
- sig { returns(T.nilable(T::Hash[String, Integer])) }
157
- def testonly_update_tooling_calls
158
- @update_tooling_calls
159
- end
160
-
161
- sig { returns(T.nilable(NativeWorkspaceDiscovery)) }
162
- def workspace
163
- discovery_json_reader = NativeDiscoveryJsonReader.get_discovery_from_dependency_files(dependency_files)
164
- discovery_json_reader.workspace_discovery
165
- end
166
-
167
- sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[String]) }
168
- def referenced_project_paths(project_file)
169
- workspace&.projects&.find { |p| p.file_path == project_file.name }&.referenced_project_paths || []
170
- end
171
-
172
- sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[NativeDependencyDetails]) }
173
- def project_dependencies(project_file)
174
- workspace&.projects&.find do |p|
175
- full_project_file_path = File.join(project_file.directory, project_file.name)
176
- p.file_path == full_project_file_path
177
- end&.dependencies || []
178
- end
179
-
180
- sig { returns(T::Array[NativeDependencyDetails]) }
181
- def global_json_dependencies
182
- workspace&.global_json&.dependencies || []
183
- end
184
-
185
- sig { returns(T::Array[NativeDependencyDetails]) }
186
- def dotnet_tools_json_dependencies
187
- workspace&.dotnet_tools_json&.dependencies || []
132
+ end.flatten
133
+ end
134
+ # rubocop:enable Metrics/AbcSize
135
+
136
+ sig do
137
+ params(
138
+ name: String,
139
+ requirement: T::Hash[Symbol, T.untyped],
140
+ is_vulnerable: T::Boolean
141
+ ).returns(T.nilable(DependencyDetails))
142
+ end
143
+ def dependency_details_from_requirement(name, requirement, is_vulnerable:)
144
+ metadata = T.let(requirement.fetch(:metadata), T::Hash[Symbol, T.untyped])
145
+ current_file = T.let(requirement.fetch(:file), String)
146
+ return nil unless current_file.match?(/\.(cs|vb|fs)proj$/)
147
+
148
+ is_transitive = T.let(metadata.fetch(:is_transitive), T::Boolean)
149
+ return nil if !is_vulnerable && is_transitive
150
+
151
+ version = T.let(requirement.fetch(:requirement), String)
152
+ previous_version = T.let(metadata[:previous_requirement], String)
153
+ return nil if version == previous_version
154
+
155
+ {
156
+ file: T.let(requirement.fetch(:file), String),
157
+ name: name,
158
+ version: version,
159
+ previous_version: previous_version,
160
+ is_transitive: is_transitive
161
+ }
188
162
  end
189
163
 
190
164
  # rubocop:disable Metrics/PerceivedComplexity
@@ -106,22 +106,20 @@ module Dependabot
106
106
  .returns(T.nilable(T::Hash[Symbol, T.untyped]))
107
107
  end
108
108
  def build_requirement(file_name, dependency_details)
109
- return if dependency_details.is_transitive
110
-
111
109
  version = dependency_details.version
112
110
  version = nil if version&.empty?
111
+ metadata = { is_transitive: dependency_details.is_transitive }
113
112
 
114
113
  requirement = {
115
114
  requirement: version,
116
115
  file: file_name,
117
116
  groups: [dependency_details.is_dev_dependency ? "devDependencies" : "dependencies"],
118
- source: nil
117
+ source: nil,
118
+ metadata: metadata
119
119
  }
120
120
 
121
121
  property_name = dependency_details.evaluation&.root_property_name
122
- return requirement unless property_name
123
-
124
- requirement[:metadata] = { property_name: property_name }
122
+ metadata[:property_name] = property_name if property_name
125
123
  requirement
126
124
  end
127
125
  end
@@ -125,16 +125,90 @@ module Dependabot
125
125
  sig { returns(T.nilable(NativeWorkspaceDiscovery)) }
126
126
  attr_reader :workspace_discovery
127
127
 
128
- sig { returns(Dependabot::FileParsers::Base::DependencySet) }
129
- attr_reader :dependency_set
130
-
131
128
  sig { params(discovery_json: DependencyFile).void }
132
129
  def initialize(discovery_json:)
133
130
  @discovery_json = discovery_json
134
131
  @workspace_discovery = T.let(read_workspace_discovery, T.nilable(Dependabot::Nuget::NativeWorkspaceDiscovery))
135
- @dependency_set = T.let(read_dependency_set, Dependabot::FileParsers::Base::DependencySet)
136
132
  end
137
133
 
134
+ # rubocop:disable Metrics/AbcSize
135
+ # rubocop:disable Metrics/MethodLength
136
+ # rubocop:disable Metrics/PerceivedComplexity
137
+ sig do
138
+ params(
139
+ dependency_files: T::Array[Dependabot::DependencyFile],
140
+ top_level_only: T::Boolean
141
+ ).returns(Dependabot::FileParsers::Base::DependencySet)
142
+ end
143
+ def dependency_set(dependency_files:, top_level_only:)
144
+ # dependencies must be recalculated so that we:
145
+ # 1. only return dependencies that are in the file set we reported earlier
146
+ # see https://github.com/dependabot/dependabot-core/issues/10303
147
+ # 2. the reported version is the minimum across all requirements; this ensures that we get the opportunity
148
+ # to update everything later
149
+ dependency_file_set = T.let(Set.new(dependency_files.map do |df|
150
+ Pathname.new(File.join(df.directory, df.name)).cleanpath.to_path
151
+ end), T::Set[String])
152
+
153
+ rebuilt_dependencies = read_dependency_set.dependencies.filter_map do |dep|
154
+ # only report requirements in files we know about
155
+ matching_requirements = dep.requirements.filter do |req|
156
+ file = T.let(req.fetch(:file), String)
157
+ dependency_file_set.include?(file)
158
+ end
159
+
160
+ # find the minimum version across all requirements
161
+ min_version = matching_requirements.filter_map do |req|
162
+ v = T.let(req.fetch(:requirement), T.nilable(String))
163
+ next unless v
164
+
165
+ Dependabot::Nuget::Version.new(v)
166
+ end.min
167
+ next unless min_version
168
+
169
+ # only return dependency requirements that are top-level
170
+ if top_level_only
171
+ matching_requirements.reject! do |req|
172
+ metadata = T.let(req.fetch(:metadata), T::Hash[Symbol, T.untyped])
173
+ T.let(metadata.fetch(:is_transitive), T::Boolean)
174
+ end
175
+ end
176
+
177
+ # we might need to return a dependency like this
178
+ dep_without_reqs = Dependabot::Dependency.new(
179
+ name: dep.name,
180
+ version: min_version.to_s,
181
+ package_manager: "nuget",
182
+ requirements: []
183
+ )
184
+
185
+ dep_with_reqs = matching_requirements.filter_map do |req|
186
+ version = T.let(req.fetch(:requirement, nil), T.nilable(String))
187
+ next unless version
188
+
189
+ Dependabot::Dependency.new(
190
+ name: dep.name,
191
+ version: min_version.to_s,
192
+ package_manager: "nuget",
193
+ requirements: [req]
194
+ )
195
+ end
196
+
197
+ # if only returning top-level dependencies and we had no non-transitive requirements, return an empty
198
+ # dependency so it can be tracked for security updates
199
+ matching_requirements.empty? && top_level_only ? [dep_without_reqs] : dep_with_reqs
200
+ end.flatten
201
+
202
+ final_dependency_set = Dependabot::FileParsers::Base::DependencySet.new
203
+ rebuilt_dependencies.each do |dep|
204
+ final_dependency_set << dep
205
+ end
206
+ final_dependency_set
207
+ end
208
+ # rubocop:enable Metrics/PerceivedComplexity
209
+ # rubocop:enable Metrics/MethodLength
210
+ # rubocop:enable Metrics/AbcSize
211
+
138
212
  private
139
213
 
140
214
  sig { returns(DependencyFile) }
@@ -1,6 +1,7 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "dependabot/file_parsers/base/dependency_set"
4
5
  require "dependabot/nuget/native_discovery/native_dependency_details"
5
6
  require "dependabot/nuget/native_discovery/native_property_details"
6
7
  require "sorbet-runtime"
@@ -171,10 +171,18 @@ module Dependabot
171
171
 
172
172
  # rubocop:disable Metrics/MethodLength
173
173
  sig do
174
- params(repo_root: String, proj_path: String, dependency: Dependency,
175
- is_transitive: T::Boolean, result_output_path: String).returns([String, String])
174
+ params(
175
+ repo_root: String,
176
+ proj_path: String,
177
+ dependency_name: String,
178
+ version: String,
179
+ previous_version: String,
180
+ is_transitive: T::Boolean,
181
+ result_output_path: String
182
+ ).returns([String, String])
176
183
  end
177
- def self.get_nuget_updater_tool_command(repo_root:, proj_path:, dependency:, is_transitive:, result_output_path:)
184
+ def self.get_nuget_updater_tool_command(repo_root:, proj_path:, dependency_name:, version:, previous_version:,
185
+ is_transitive:, result_output_path:)
178
186
  exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli")
179
187
  command_parts = [
180
188
  exe_path,
@@ -184,11 +192,11 @@ module Dependabot
184
192
  "--solution-or-project",
185
193
  proj_path,
186
194
  "--dependency",
187
- dependency.name,
195
+ dependency_name,
188
196
  "--new-version",
189
- dependency.version,
197
+ version,
190
198
  "--previous-version",
191
- dependency.previous_version,
199
+ previous_version,
192
200
  is_transitive ? "--transitive" : nil,
193
201
  "--result-output-path",
194
202
  result_output_path,
@@ -229,14 +237,21 @@ module Dependabot
229
237
  params(
230
238
  repo_root: String,
231
239
  proj_path: String,
232
- dependency: Dependency,
240
+ dependency_name: String,
241
+ version: String,
242
+ previous_version: String,
233
243
  is_transitive: T::Boolean,
234
244
  credentials: T::Array[Dependabot::Credential]
235
245
  ).void
236
246
  end
237
- def self.run_nuget_updater_tool(repo_root:, proj_path:, dependency:, is_transitive:, credentials:)
238
- (command, fingerprint) = get_nuget_updater_tool_command(repo_root: repo_root, proj_path: proj_path,
239
- dependency: dependency, is_transitive: is_transitive,
247
+ def self.run_nuget_updater_tool(repo_root:, proj_path:, dependency_name:, version:, previous_version:,
248
+ is_transitive:, credentials:)
249
+ (command, fingerprint) = get_nuget_updater_tool_command(repo_root: repo_root,
250
+ proj_path: proj_path,
251
+ dependency_name: dependency_name,
252
+ version: version,
253
+ previous_version: previous_version,
254
+ is_transitive: is_transitive,
240
255
  result_output_path: update_result_file_path)
241
256
 
242
257
  puts "running NuGet updater:\n" + command
@@ -21,15 +21,18 @@ module Dependabot
21
21
  sig do
22
22
  params(
23
23
  requirements: T::Array[T::Hash[Symbol, T.untyped]],
24
- dependency_details: T.nilable(Dependabot::Nuget::NativeDependencyDetails)
24
+ dependency_details: T.nilable(Dependabot::Nuget::NativeDependencyDetails),
25
+ vulnerable: T::Boolean
25
26
  )
26
27
  .void
27
28
  end
28
- def initialize(requirements:, dependency_details:)
29
+ def initialize(requirements:, dependency_details:, vulnerable:)
29
30
  @requirements = requirements
30
31
  @dependency_details = dependency_details
32
+ @vulnerable = vulnerable
31
33
  end
32
34
 
35
+ # rubocop:disable Metrics/PerceivedComplexity
33
36
  sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
34
37
  def updated_requirements
35
38
  return requirements unless clean_version
@@ -37,13 +40,18 @@ module Dependabot
37
40
  # NOTE: Order is important here. The FileUpdater needs the updated
38
41
  # requirement at index `i` to correspond to the previous requirement
39
42
  # at the same index.
40
- requirements.map do |req|
41
- next req if req.fetch(:requirement).nil?
42
- next req if req.fetch(:requirement).include?(",")
43
+ requirements.filter_map do |req|
44
+ next if !@vulnerable && req[:metadata][:is_transitive]
45
+
46
+ previous_requirement = req.fetch(:requirement)
47
+ req[:metadata][:previous_requirement] = previous_requirement
48
+
49
+ next req if previous_requirement.nil?
50
+ next req if previous_requirement.include?(",")
43
51
 
44
52
  new_req =
45
- if req.fetch(:requirement).include?("*")
46
- update_wildcard_requirement(req.fetch(:requirement))
53
+ if previous_requirement.include?("*")
54
+ update_wildcard_requirement(previous_requirement)
47
55
  else
48
56
  # Since range requirements are excluded by the line above we can
49
57
  # replace anything that looks like a version with the new
@@ -54,7 +62,7 @@ module Dependabot
54
62
  )
55
63
  end
56
64
 
57
- next req if new_req == req.fetch(:requirement)
65
+ next req if new_req == previous_requirement
58
66
 
59
67
  new_source = req[:source]&.dup
60
68
  unless @dependency_details.nil?
@@ -67,6 +75,7 @@ module Dependabot
67
75
  req.merge({ requirement: new_req, source: new_source })
68
76
  end
69
77
  end
78
+ # rubocop:enable Metrics/PerceivedComplexity
70
79
 
71
80
  private
72
81
 
@@ -56,7 +56,8 @@ module Dependabot
56
56
  dep_details = updated_dependency_details.find { |d| d.name.casecmp?(dependency.name) }
57
57
  NativeRequirementsUpdater.new(
58
58
  requirements: dependency.requirements,
59
- dependency_details: dep_details
59
+ dependency_details: dep_details,
60
+ vulnerable: vulnerable?
60
61
  ).updated_requirements
61
62
  end
62
63
 
@@ -112,9 +113,10 @@ module Dependabot
112
113
 
113
114
  sig { void }
114
115
  def write_dependency_info
116
+ dependency_version = T.let(dependency.requirements.first&.fetch(:requirement, nil), T.nilable(String))
115
117
  dependency_info = {
116
118
  Name: dependency.name,
117
- Version: dependency.version.to_s,
119
+ Version: dependency_version || dependency.version.to_s,
118
120
  IsVulnerable: vulnerable?,
119
121
  IgnoredVersions: ignored_versions,
120
122
  Vulnerabilities: security_advisories.map do |vulnerability|
@@ -141,7 +143,7 @@ module Dependabot
141
143
  sig { returns(Dependabot::FileParsers::Base::DependencySet) }
142
144
  def discovered_dependencies
143
145
  discovery_json_reader = NativeDiscoveryJsonReader.get_discovery_from_dependency_files(dependency_files)
144
- discovery_json_reader.dependency_set
146
+ discovery_json_reader.dependency_set(dependency_files: dependency_files, top_level_only: false)
145
147
  end
146
148
 
147
149
  sig { override.returns(T::Boolean) }
@@ -150,6 +152,10 @@ module Dependabot
150
152
  true
151
153
  end
152
154
 
155
+ # rubocop:disable Metrics/AbcSize
156
+ # rubocop:disable Metrics/BlockLength
157
+ # rubocop:disable Metrics/MethodLength
158
+ # rubocop:disable Metrics/PerceivedComplexity
153
159
  sig { override.returns(T::Array[Dependabot::Dependency]) }
154
160
  def updated_dependencies_after_full_unlock
155
161
  dependencies = discovered_dependencies.dependencies
@@ -157,14 +163,16 @@ module Dependabot
157
163
  dep = dependencies.find { |d| d.name.casecmp(dependency_details.name)&.zero? }
158
164
  next unless dep
159
165
 
160
- metadata = {}
166
+ dep_metadata = T.let({}, T::Hash[Symbol, T.untyped])
161
167
  # For peer dependencies, instruct updater to not directly update this dependency
162
- metadata = { information_only: true } unless dependency.name.casecmp(dependency_details.name)&.zero?
168
+ dep_metadata[:information_only] = true unless dependency.name.casecmp(dependency_details.name)&.zero?
169
+ dep_metadata[:is_vulnerable] = vulnerable?
163
170
 
164
171
  # rebuild the new requirements with the updated dependency details
165
172
  updated_reqs = dep.requirements.map do |r|
166
173
  r = r.clone
167
- r[:requirement] = dependency_details.version
174
+ T.let(r[:metadata], T::Hash[Symbol, T.untyped])[:previous_requirement] = r[:requirement] # keep old version
175
+ r[:requirement] = dependency_details.version # set new version
168
176
  r[:source] = {
169
177
  type: "nuget_repo",
170
178
  source_url: dependency_details.info_url
@@ -172,17 +180,44 @@ module Dependabot
172
180
  r
173
181
  end
174
182
 
183
+ reqs = dep.requirements
184
+ unless vulnerable?
185
+ updated_reqs = updated_reqs.filter do |r|
186
+ req_metadata = T.let(r.fetch(:metadata, {}), T::Hash[Symbol, T.untyped])
187
+ !T.let(req_metadata[:is_transitive], T::Boolean)
188
+ end
189
+ reqs = reqs.filter do |r|
190
+ req_metadata = T.let(r.fetch(:metadata, {}), T::Hash[Symbol, T.untyped])
191
+ !T.let(req_metadata[:is_transitive], T::Boolean)
192
+ end
193
+ end
194
+
195
+ # report back the highest version that all of these dependencies can be updated to
196
+ # this will ensure that we get a chance to update all relevant dependencies
197
+ max_updatable_version = updated_reqs.filter_map do |r|
198
+ v = T.let(r.fetch(:requirement, nil), T.nilable(String))
199
+ next unless v
200
+
201
+ Dependabot::Nuget::Version.new(v)
202
+ end.max
203
+ next unless max_updatable_version
204
+
205
+ previous_version = T.let(dep.requirements.first&.fetch(:requirement, nil), T.nilable(String))
175
206
  Dependency.new(
176
207
  name: dep.name,
177
- version: dependency_details.version,
208
+ version: max_updatable_version.to_s,
178
209
  requirements: updated_reqs,
179
- previous_version: dep.version,
180
- previous_requirements: dep.requirements,
210
+ previous_version: previous_version,
211
+ previous_requirements: reqs,
181
212
  package_manager: dep.package_manager,
182
- metadata: metadata
213
+ metadata: dep_metadata
183
214
  )
184
215
  end
185
216
  end
217
+ # rubocop:enable Metrics/PerceivedComplexity
218
+ # rubocop:enable Metrics/MethodLength
219
+ # rubocop:enable Metrics/BlockLength
220
+ # rubocop:enable Metrics/AbcSize
186
221
 
187
222
  sig { returns(T::Array[Dependabot::Nuget::NativeDependencyDetails]) }
188
223
  def updated_dependency_details
@@ -37,10 +37,11 @@ module Dependabot
37
37
  def updated_requirements
38
38
  return requirements unless latest_version
39
39
 
40
- # NOTE: Order is important here. The FileUpdater needs the updated
41
- # requirement at index `i` to correspond to the previous requirement
42
- # at the same index.
43
40
  requirements.map do |req|
41
+ req[:metadata] ||= {}
42
+ req[:metadata][:is_transitive] = false
43
+ req[:metadata][:previous_requirement] = req[:requirement]
44
+
44
45
  next req if req.fetch(:requirement).nil?
45
46
  next req if req.fetch(:requirement).include?(",")
46
47
 
@@ -56,7 +57,6 @@ module Dependabot
56
57
  latest_version.to_s
57
58
  )
58
59
  end
59
-
60
60
  next req if new_req == req.fetch(:requirement)
61
61
 
62
62
  req.merge(requirement: new_req, source: updated_source)
@@ -166,7 +166,8 @@ module Dependabot
166
166
  requirements: updated_requirements,
167
167
  previous_version: dependency.version,
168
168
  previous_requirements: dependency.requirements,
169
- package_manager: dependency.package_manager
169
+ package_manager: dependency.package_manager,
170
+ metadata: { is_vulnerable: vulnerable? }
170
171
  )
171
172
  updated_dependencies = [updated_dependency]
172
173
  updated_dependencies += DependencyFinder.new(
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-nuget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.271.0
4
+ version: 0.272.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-15 00:00:00.000000000 Z
11
+ date: 2024-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.271.0
19
+ version: 0.272.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.271.0
26
+ version: 0.272.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rubyzip
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -463,7 +463,7 @@ licenses:
463
463
  - MIT
464
464
  metadata:
465
465
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
466
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.271.0
466
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.272.0
467
467
  post_install_message:
468
468
  rdoc_options: []
469
469
  require_paths: