dependabot-nuget 0.271.0 → 0.272.0

Sign up to get free protection for your applications and to get access to all the features.
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: